@misterhuydo/cairn-mcp 1.36.0 → 1.36.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -497,6 +497,29 @@ cairn cannot type either command.
497
497
  Once the handoff is written, neither option drops anything that was written down, so the
498
498
  choice collapses to that single question about what comes next.
499
499
 
500
+ **v1.36.1 is the release that made the sentence above true.** It was not. The gate would
501
+ demand a thorough `would_be_lost`, the agent would write three and a half thousand
502
+ characters of reasoning that existed nowhere else, correctly tell you a reset was safe —
503
+ and the SessionStart re-injection would hand the fresh session 220 characters of it, cut
504
+ off mid-word. Everything after the first point was gone. The agent could not remember the
505
+ conversation you had just had with it, which read as the checkpoint failing when the
506
+ checkpoint had worked perfectly.
507
+
508
+ Storage was never the problem; delivery was. `inlineResumePayload()` rendered the restore
509
+ payload through the same 220-character clip as the every-prompt budget line. That cap is
510
+ right for the budget line — it rides *every* turn and has to stay one line — and ruinous
511
+ for the restore payload, which fires **once** and is the only copy of the handoff that
512
+ session will ever see. One cap could not serve both, so `handoffSummaryLine()` now takes
513
+ one, and the restore path passes none. It costs about 900 tokens a session.
514
+
515
+ The failure is worth naming because of its shape: the agent was right about what got
516
+ *saved* and wrong about what got *delivered*, and no amount of care inside the writing
517
+ session could have caught the difference. The same bug had a second instance — notes long
518
+ enough to be filed to `.cairn/notes/` are held as objects, and the restore stringified
519
+ them to `[object Object]`, so the most substantial notes were the ones that arrived as
520
+ noise. When a feature demands expensive judgement from the agent, test the whole return
521
+ path, not just the write.
522
+
500
523
  It needs no rate limit: it self-extinguishes. The moment the handoff is written the
501
524
  demand returns null, and it speaks again only if later commits make that answer stale.
502
525
  Crossing is remembered in `.cairn/.handoff-gate` and forgotten when usage drops back
package/dist/cairn-cli.js CHANGED
@@ -1,39 +1,41 @@
1
1
  #!/usr/bin/env node
2
- import h from"fs";import se from"os";import w from"path";import{execSync as ds}from"child_process";import gs from"fast-glob";var Ie={".java":"java",".ts":"typescript",".tsx":"typescript",".js":"javascript",".jsx":"javascript",".mjs":"javascript",".vue":"vue",".py":"python",".sql":"sql",".yml":"config",".yaml":"config",".properties":"config",".env":"config",".xml":"xml",".html":"html",".md":"markdown"};function fe(e,t,n={}){let{noComments:s=!0,noEmptyLines:o=!0,aggressive:r=!1}=n,a=e;return s&&(a=jt(a,t)),o&&(a=Nt(a)),a=Pt(a),r&&(a=Mt(a)),a.trim()}function jt(e,t){switch(t){case"java":case"typescript":case"javascript":case"vue":return e.replace(/\/\*[\s\S]*?\*\//g,"").replace(/(["'`])(?:(?!\1)[^\\]|\\.)*\1|\/(?![/*])(?:[^/\\\n]|\\.)*\/[gimsuy]*|\/\/[^\n]*/g,(n,s)=>s||!n.startsWith("//")?n:"");case"python":return e.replace(/#.*$/gm,"").replace(/'''[\s\S]*?'''/g,"").replace(/"""[\s\S]*?"""/g,"");case"sql":return e.replace(/--.*$/gm,"").replace(/\/\*[\s\S]*?\*\//g,"");case"xml":case"html":return e.replace(/<!--[\s\S]*?-->/g,"");case"config":return e.replace(/#.*$/gm,"");default:return e}}function Nt(e){return e.split(`
2
+ import h from"fs";import se from"os";import w from"path";import{execSync as ps}from"child_process";import Ss from"fast-glob";var Ie={".java":"java",".ts":"typescript",".tsx":"typescript",".js":"javascript",".jsx":"javascript",".mjs":"javascript",".vue":"vue",".py":"python",".sql":"sql",".yml":"config",".yaml":"config",".properties":"config",".env":"config",".xml":"xml",".html":"html",".md":"markdown"};function fe(e,t,n={}){let{noComments:s=!0,noEmptyLines:o=!0,aggressive:r=!1}=n,a=e;return s&&(a=Nt(a,t)),o&&(a=Pt(a)),a=Mt(a),r&&(a=At(a)),a.trim()}function Nt(e,t){switch(t){case"java":case"typescript":case"javascript":case"vue":return e.replace(/\/\*[\s\S]*?\*\//g,"").replace(/(["'`])(?:(?!\1)[^\\]|\\.)*\1|\/(?![/*])(?:[^/\\\n]|\\.)*\/[gimsuy]*|\/\/[^\n]*/g,(n,s)=>s||!n.startsWith("//")?n:"");case"python":return e.replace(/#.*$/gm,"").replace(/'''[\s\S]*?'''/g,"").replace(/"""[\s\S]*?"""/g,"");case"sql":return e.replace(/--.*$/gm,"").replace(/\/\*[\s\S]*?\*\//g,"");case"xml":case"html":return e.replace(/<!--[\s\S]*?-->/g,"");case"config":return e.replace(/#.*$/gm,"");default:return e}}function Pt(e){return e.split(`
3
3
  `).filter(t=>t.trim().length>0).join(`
4
- `)}function Pt(e){return e.split(`
4
+ `)}function Mt(e){return e.split(`
5
5
  `).map(t=>t.trimEnd()).join(`
6
- `)}function Mt(e){return e.replace(/\s+/g," ").replace(/\s*([=+\-*/%<>!&|^~?:;,{}()[\]])\s*/g,"$1").replace(/;\s*}/g,"}").replace(/\(\s+/g,"(").replace(/\s+\)/g,")").trim()}function We(e,t={}){let{noStyle:n=!1,noComments:s=!0,noEmptyLines:o=!0,aggressive:r=!1}=t,a=[],c=e.match(/<template>([\s\S]*?)<\/template>/i);if(c){let i=c[1];s&&(i=i.replace(/<!--[\s\S]*?-->/g,"")),o&&(i=i.split(`
6
+ `)}function At(e){return e.replace(/\s+/g," ").replace(/\s*([=+\-*/%<>!&|^~?:;,{}()[\]])\s*/g,"$1").replace(/;\s*}/g,"}").replace(/\(\s+/g,"(").replace(/\s+\)/g,")").trim()}function We(e,t={}){let{noStyle:n=!1,noComments:s=!0,noEmptyLines:o=!0,aggressive:r=!1}=t,a=[],c=e.match(/<template>([\s\S]*?)<\/template>/i);if(c){let i=c[1];s&&(i=i.replace(/<!--[\s\S]*?-->/g,"")),o&&(i=i.split(`
7
7
  `).filter(u=>u.trim()).join(`
8
8
  `)),i=i.replace(/\s+/g," ").trim(),i&&a.push(`<template>${i}</template>`)}let l=e.match(/<script([^>]*)>([\s\S]*?)<\/script>/i);if(l){let i=l[1],u=fe(l[2],"javascript",{noComments:s,noEmptyLines:o,aggressive:r}),d=/setup/i.test(i)?"<script setup>":"<script>";u&&a.push(`${d}${u}</script>`)}if(!n){let i=e.match(/<style([^>]*)>([\s\S]*?)<\/style>/i);if(i){let u=i[2].replace(/\/\*[\s\S]*?\*\//g,"").replace(/\s+/g," ").trim();u&&a.push(`<style${i[1]}>${u}</style>`)}}return a.join(`
9
9
 
10
- `)}import J from"fs";import oe from"path";import L from"fs";import At from"os";import U from"path";var we=null;function X(){return we||process.cwd()}function He(e){let t=we;return we=e,t}function Z(){let e=U.join(X(),".cairn");L.mkdirSync(U.join(e,"bundles"),{recursive:!0});let t=U.join(e,".cairn-project");return L.existsSync(t)||L.writeFileSync(t,"","utf8"),e}function xe(e){return e.replace(/[:/\\]/g,"-").replace(/^-+/,"")}function ke(){let e=U.join(At.homedir(),".claude","projects",xe(X()));return L.existsSync(e)?U.join(e,"memory"):null}function Ue(e){let t=U.join(e,"MEMORY.md");L.existsSync(t)||L.writeFileSync(t,`# Memory Index
11
- `,"utf8")}function be(){let e=U.join(Z(),"memory");L.mkdirSync(e,{recursive:!0}),Ue(e);let t=ke();if(!t)return e;L.mkdirSync(t,{recursive:!0}),Ue(t);let n=s=>L.readdirSync(s).some(o=>o.endsWith(".md")&&o!=="MEMORY.md");if(!n(t)&&n(e))for(let s of L.readdirSync(e)){let o=U.join(e,s);try{if(!L.statSync(o).isFile())continue;let r=L.statSync(o);L.copyFileSync(o,U.join(t,s));try{L.utimesSync(U.join(t,s),r.atime,r.mtime)}catch{}}catch{}}return t}import H from"fs";import Ee from"path";import{execSync as Ft}from"child_process";var N="would_be_lost",D="context_window",z="context_window_model";var Ts=2*1024*1024,Lt=[256*1024,1024*1024],Dt=".handoff-hint",Ct=220;function $e(e,t=Ct){let n=String(e).replace(/\s+/g," ").trim();return n.length<=t?n:n.slice(0,t-1)+"\u2026"}function It(e,t){try{let n=Ft(`git log --oneline --after="${t}"`,{cwd:e,encoding:"utf8",timeout:5e3,stdio:["ignore","pipe","ignore"]}).trim();return n?n.split(`
12
- `).length:0}catch{return null}}function ue(e,t){let n=typeof t?.[N]=="string"?t[N].trim():"";if(!n)return{recorded:!1,text:"",at:null,commits:null,stale:!1};let s=t[`${N}_at`]||t.checkpoint_at||null,o=s?It(e,s):null;return{recorded:!0,text:n,at:s,commits:o,stale:(o??0)>0}}function ve(e){if(!e.recorded)return"No handoff recorded \u2014 nothing was written down about what the last session would lose.";let t=e.stale?` (written before the ${e.commits} commit(s) since \u2014 may be stale)`:"";return`Carried forward: ${$e(e.text)}${t}`}function Wt(){let e=Number(process.env.CAIRN_CONTEXT_HINT_TOKENS);return Number.isFinite(e)&&e>0?e:null}var Ut=1024,Ht=1e8;function me(e){if(e==null)return null;let t;if(typeof e=="number")t=e;else if(typeof e=="string"){let n=e.trim().replace(/[_,\s]/g,"").match(/^(\d+(?:\.\d+)?)([kKmM])?$/);if(!n)return null;let s=n[2]?{k:1e3,m:1e6}[n[2].toLowerCase()]:1;t=Number(n[1])*s}else return null;return Number.isFinite(t)?(t=Math.round(t),t>=Ut&&t<=Ht?t:null):null}function Jt(e){let t=me(e?.[D]);if(t===null)return null;let n=typeof e?.[z]=="string"?e[z].trim():"";return{tokens:t,model:n,at:e[`${D}_at`]||null}}function Bt(e,t){let n=H.openSync(e,"r");try{let s=H.fstatSync(n).size,o=Math.min(s,t),r=Buffer.alloc(o);return H.readSync(n,r,0,o,s-o),{text:r.toString("utf8"),truncated:o<s}}finally{H.closeSync(n)}}function Yt(e,t){let n=e.split(`
13
- `),s=t?1:0;for(let o=n.length-1;o>=s;o--){let r=n[o].trim();if(!r.startsWith("{"))continue;let a;try{a=JSON.parse(r)}catch{continue}if(a?.isSidechain===!0)continue;let c=a?.message?.usage;if(!c)continue;let l=(c.input_tokens||0)+(c.cache_read_input_tokens||0)+(c.cache_creation_input_tokens||0)+(c.output_tokens||0);if(l>0)return l}return null}function Gt(e){if(!e)return{tokens:0,source:"none",bytes:0};let t;try{t=H.statSync(e).size}catch{return{tokens:0,source:"none",bytes:0}}for(let n of Lt){let s;try{s=Bt(e,n)}catch{break}let o=Yt(s.text,s.truncated);if(o!=null)return{tokens:o,source:"transcript",bytes:t};if(!s.truncated)break}return{tokens:0,source:"bytes",bytes:t}}function Je(e){return e.recorded&&!e.stale?"clear":"compact"}function zt(e){return e.recorded?e.stale?"your saved notes predate the "+e.commits+" commit(s) since, so they no longer describe the work in flight":"your saved notes are current as of the last commit, so a reset costs only what is not already in them":"nothing is written down about what this session would lose, so a hard reset would drop it with no way to get it back"}var Kt=["Cairn cannot measure your context window. If the line above shows none, or shows one that is",`not yours, read yours from your system prompt (e.g. "Opus 5 (1M context)") and pass ${D}`,"to cairn_resume or cairn_checkpoint once. NEVER infer it from how large the token count looks.","Nearly always the work fits: say nothing, carry on, and never re-raise a decline. Only when it",`truly does not, call cairn_checkpoint FIRST (answer ${N} \u2014 it is all of this session`,'that survives), then AskUserQuestion offering "Checkpoint + /clear", "Checkpoint + /compact" or',`"Keep going" \u2014 they must type it, you cannot. Never write ${N} for them, and never`,"issue an all-clear."].join(`
14
- `);function Be({cwd:e,session:t,transcriptPath:n,bytes:s,usage:o}){let r=o??Gt(n);r.source==="none"&&typeof s=="number"&&(r={tokens:0,source:"bytes",bytes:s});let a=s??r.bytes;if(r.source==="none")return null;let c=Jt(t),l;if(r.source!=="transcript")l=`transcript is ${(a/1024/1024).toFixed(1)} MB, token count unreadable (size signal only, not a token count)`;else if(c){let b=Math.round(r.tokens/c.tokens*100),x=c.model?`, declared for ${c.model}`:", declared",k=b>100?" \u2014 over 100% is impossible, so that declared window is wrong: re-declare it":"";l=`${r.tokens.toLocaleString()} of ${c.tokens.toLocaleString()} tokens (${b}%${x})${k}`}else l=`${r.tokens.toLocaleString()} tokens used so far; window NOT declared, so there is no percentage \u2014 pass ${D} to fix that`;let i=ue(e,t),u=Je(i),d;i.recorded?i.stale?d=`Handoff is STALE, written before the ${i.commits} commit(s) since: `+$e(i.text):d=`Handoff is current: ${$e(i.text)}`:d="Nothing is recorded about what a reset would lose.";let p=Wt(),f=p!==null&&r.source==="transcript"&&r.tokens>=p?` This session declared CAIRN_CONTEXT_HINT_TOKENS=${p.toLocaleString()} and is past it.`:"",m="\u2192 If you reset: /"+u+" \u2014 "+zt(i)+".";return{line:`[cairn] Context: ${l}. ${d}${f}
15
- `+m,advice:m,protocol:Kt,state:i,recommend:u,usage:r,bytes:a,window:c}}function Ye(e){try{H.unlinkSync(Ee.join(e,Dt))}catch{}try{H.unlinkSync(Ee.join(e,Ge))}catch{}}var Re=.9,Ge=".handoff-gate";function ze(e,t){let n=Ee.join(e,Ge),s=null;try{s=H.readFileSync(n,"utf8").trim()||null}catch{}if(!t){if(s)try{H.unlinkSync(n)}catch{}return null}if(s)return s;let o=new Date().toISOString();try{H.writeFileSync(n,o,"utf8")}catch{}return o}function Ke({usage:e,window:t,state:n,crossedAt:s}){if(!t?.tokens||e?.source!=="transcript"||e.tokens<Re*t.tokens||n.recorded&&!n.stale&&n.at&&s&&n.at>=s)return null;let r;n.recorded?n.stale?r="the handoff predates the "+n.commits+" commit(s) since":r="the handoff was written before the window reached this point, so it describes a session that has since moved on":r="nothing has been written down about what this session would lose";let a=Math.round(e.tokens/t.tokens*100),c=t.tokens-e.tokens,i="For you: /compact keeps this conversation as a summary, so nothing already read has to be read again. /clear starts a fresh session and cairn re-injects your saved notes on the first message. "+(Je(n)==="clear"?"Your saved notes are current, so neither choice drops anything already written down.":"Either way, let the agent write the notes first \u2014 they do not yet cover the work above.");return"\u26A0 [cairn] "+a+"% of the declared window ("+e.tokens.toLocaleString()+" of "+t.tokens.toLocaleString()+" tokens, "+c.toLocaleString()+" left) and "+r+`. Call cairn_checkpoint with would_be_lost: the every-turn auto-checkpoint carries that field forward but never writes it, so nothing else will. Then tell the user WHICH reset this calls for \u2014 /compact if the work ahead continues this conversation, /clear if it starts something unrelated. You can judge that and cairn cannot: you have the request, the roadmap cursor and where this was going. This is not a verdict on whether your next task fits \u2014 that judgement is still yours.
16
- `+i}var qt=1e3,Vt=100*1024;function Xt(e,t,n,s){if(e&&typeof e=="object"&&e.ref)return e;let o=typeof e=="string"?e:String(e),r=/^\[\d{4}-\d{2}-\d{2}\]/.test(o)?o:`[${n}] ${o}`;if(r.length<=qt)return r;let a=oe.join(t,"notes");J.mkdirSync(a,{recursive:!0});let l=`note-${new Date().toISOString().replace(/[:.]/g,"-")}-${s}.md`,i=oe.join(a,l);return J.writeFileSync(i,r,"utf8"),{description:r.split(`
17
- `)[0].slice(0,120),ref:oe.join(".cairn","notes",l)}}function Oe(e,{message:t,active_files:n=[],notes:s=[],auto:o=!1,...r}){if(typeof s=="string")try{s=JSON.parse(s)}catch{s=s?[s]:[]}Array.isArray(s)||(s=[]);let a=Z(),c=oe.join(a,"session.json"),l=new Date().toISOString().slice(0,10),i=new Date().toISOString(),u={};try{u=JSON.parse(J.readFileSync(c,"utf8"))||{}}catch{}let d=typeof r[N]=="string"&&r[N].trim()?r[N].trim():null,p=d??(typeof u[N]=="string"?u[N]:null),f=d?i:u[`${N}_at`]??null,m=me(r[D]),S=m??me(u[D]),b=m?i:u[`${D}_at`]??null,x=m&&typeof r[z]=="string"&&r[z].trim()?r[z].trim():u[z]??null,k={};try{let y=e.prepare("SELECT path FROM main.files").all();for(let{path:g}of y)try{k[g]=J.statSync(g).mtimeMs}catch{}}catch{}for(let y of n)try{k[y]=J.statSync(y).mtimeMs}catch{}let $=s.map((y,g)=>Xt(y,a,l,g));if(J.existsSync(c))try{if(J.statSync(c).size>Vt){let g=1;for(;J.existsSync(oe.join(a,`session.${g}.json`));)g++;J.renameSync(c,oe.join(a,`session.${g}.json`))}}catch{}let v={message:t,checkpoint_at:i,last_manual_checkpoint_at:o?u.last_manual_checkpoint_at??null:i,last_manual_message:o?u.last_manual_message??null:t,active_files:n,notes:$,[N]:p,[`${N}_at`]:f,[D]:S,[`${D}_at`]:b,[z]:x,mtime_snapshot:k};return J.writeFileSync(c,JSON.stringify(v,null,2),"utf8"),{content:[{type:"text",text:JSON.stringify({saved:!0,checkpoint_at:v.checkpoint_at,active_files_tracked:n.length,notes_saved:$.length,[N]:p?d?"recorded":"carried forward from the previous checkpoint":`not recorded \u2014 nothing will tell the next session what this one would lose (pass ${N})`,[D]:S?`${S.toLocaleString()} (${m?"declared now":"carried forward"})`:`not declared \u2014 cairn can report a token count but no percentage (pass ${D})`},null,2)}]}}import G from"fs";import C from"path";import Zt from"crypto";var qe=80;function Te(e){let t=C.dirname(C.resolve(e)),n=C.parse(t).root;for(;;){let s=C.join(t,".cairn");try{if(G.statSync(s).isDirectory()&&G.existsSync(C.join(s,".cairn-project"))&&s.split(C.sep).filter(r=>r===".cairn").length===1)return s}catch{}if(t===n)return null;t=C.dirname(t)}}function Ve(e,t){let n=Zt.createHash("sha256").update(C.resolve(t)).digest("hex").slice(0,6);return C.join(e,"views",`${n}_${C.basename(t)}`)}function Q(e){try{return JSON.parse(G.readFileSync(C.join(e,"minify-map.json"),"utf8"))}catch{return{}}}function W(e,t){G.mkdirSync(e,{recursive:!0}),G.writeFileSync(C.join(e,"minify-map.json"),JSON.stringify(t,null,2),"utf8")}function he(e,t){let n=0;for(let[s,o]of Object.entries(t)){let r=!1;if(!G.existsSync(s))r=!0;else if(!G.existsSync(o.tempPath))r=!0;else try{G.statSync(s).mtimeMs>o.minifiedAt&&(r=!0)}catch{r=!0}if(r){if(o.tempPath)try{G.unlinkSync(o.tempPath)}catch{}delete t[s],n++}}return n}function Xe(e,t){switch(t){case"javascript":case"typescript":return je(e);case"java":return je(e,{annotations:!0});case"python":return nn(e);case"vue":return sn(e);case"sql":return on(e);default:return e.split(`
10
+ `)}import J from"fs";import oe from"path";import L from"fs";import Ft from"os";import U from"path";var we=null;function X(){return we||process.cwd()}function He(e){let t=we;return we=e,t}function Z(){let e=U.join(X(),".cairn");L.mkdirSync(U.join(e,"bundles"),{recursive:!0});let t=U.join(e,".cairn-project");return L.existsSync(t)||L.writeFileSync(t,"","utf8"),e}function xe(e){return e.replace(/[:/\\]/g,"-").replace(/^-+/,"")}function ke(){let e=U.join(Ft.homedir(),".claude","projects",xe(X()));return L.existsSync(e)?U.join(e,"memory"):null}function Ue(e){let t=U.join(e,"MEMORY.md");L.existsSync(t)||L.writeFileSync(t,`# Memory Index
11
+ `,"utf8")}function be(){let e=U.join(Z(),"memory");L.mkdirSync(e,{recursive:!0}),Ue(e);let t=ke();if(!t)return e;L.mkdirSync(t,{recursive:!0}),Ue(t);let n=s=>L.readdirSync(s).some(o=>o.endsWith(".md")&&o!=="MEMORY.md");if(!n(t)&&n(e))for(let s of L.readdirSync(e)){let o=U.join(e,s);try{if(!L.statSync(o).isFile())continue;let r=L.statSync(o);L.copyFileSync(o,U.join(t,s));try{L.utimesSync(U.join(t,s),r.atime,r.mtime)}catch{}}catch{}}return t}import H from"fs";import Ee from"path";import{execSync as Lt}from"child_process";var N="would_be_lost",D="context_window",z="context_window_model";var Ns=2*1024*1024,Dt=[256*1024,1024*1024],Ct=".handoff-hint",Je=220;function $e(e,t=Je){let n=String(e).replace(/\s+/g," ").trim();return n.length<=t?n:n.slice(0,t-1)+"\u2026"}function It(e,t){try{let n=Lt(`git log --oneline --after="${t}"`,{cwd:e,encoding:"utf8",timeout:5e3,stdio:["ignore","pipe","ignore"]}).trim();return n?n.split(`
12
+ `).length:0}catch{return null}}function ue(e,t){let n=typeof t?.[N]=="string"?t[N].trim():"";if(!n)return{recorded:!1,text:"",at:null,commits:null,stale:!1};let s=t[`${N}_at`]||t.checkpoint_at||null,o=s?It(e,s):null;return{recorded:!0,text:n,at:s,commits:o,stale:(o??0)>0}}var Wt=" ".repeat(16);function ve(e,t=Je){if(!e.recorded)return"No handoff recorded \u2014 nothing was written down about what the last session would lose.";let n=e.stale?" (written before the "+e.commits+" commit(s) since \u2014 may be stale)":"";return"Carried forward: "+(t===null?String(e.text).trim().split(`
13
+ `).join(`
14
+ `+Wt):$e(e.text,t))+n}function Ut(){let e=Number(process.env.CAIRN_CONTEXT_HINT_TOKENS);return Number.isFinite(e)&&e>0?e:null}var Ht=1024,Jt=1e8;function me(e){if(e==null)return null;let t;if(typeof e=="number")t=e;else if(typeof e=="string"){let n=e.trim().replace(/[_,\s]/g,"").match(/^(\d+(?:\.\d+)?)([kKmM])?$/);if(!n)return null;let s=n[2]?{k:1e3,m:1e6}[n[2].toLowerCase()]:1;t=Number(n[1])*s}else return null;return Number.isFinite(t)?(t=Math.round(t),t>=Ht&&t<=Jt?t:null):null}function Bt(e){let t=me(e?.[D]);if(t===null)return null;let n=typeof e?.[z]=="string"?e[z].trim():"";return{tokens:t,model:n,at:e[`${D}_at`]||null}}function Yt(e,t){let n=H.openSync(e,"r");try{let s=H.fstatSync(n).size,o=Math.min(s,t),r=Buffer.alloc(o);return H.readSync(n,r,0,o,s-o),{text:r.toString("utf8"),truncated:o<s}}finally{H.closeSync(n)}}function Gt(e,t){let n=e.split(`
15
+ `),s=t?1:0;for(let o=n.length-1;o>=s;o--){let r=n[o].trim();if(!r.startsWith("{"))continue;let a;try{a=JSON.parse(r)}catch{continue}if(a?.isSidechain===!0)continue;let c=a?.message?.usage;if(!c)continue;let l=(c.input_tokens||0)+(c.cache_read_input_tokens||0)+(c.cache_creation_input_tokens||0)+(c.output_tokens||0);if(l>0)return l}return null}function zt(e){if(!e)return{tokens:0,source:"none",bytes:0};let t;try{t=H.statSync(e).size}catch{return{tokens:0,source:"none",bytes:0}}for(let n of Dt){let s;try{s=Yt(e,n)}catch{break}let o=Gt(s.text,s.truncated);if(o!=null)return{tokens:o,source:"transcript",bytes:t};if(!s.truncated)break}return{tokens:0,source:"bytes",bytes:t}}function Be(e){return e.recorded&&!e.stale?"clear":"compact"}function Kt(e){return e.recorded?e.stale?"your saved notes predate the "+e.commits+" commit(s) since, so they no longer describe the work in flight":"your saved notes are current as of the last commit, so a reset costs only what is not already in them":"nothing is written down about what this session would lose, so a hard reset would drop it with no way to get it back"}var qt=["Cairn cannot measure your context window. If the line above shows none, or shows one that is",`not yours, read yours from your system prompt (e.g. "Opus 5 (1M context)") and pass ${D}`,"to cairn_resume or cairn_checkpoint once. NEVER infer it from how large the token count looks.","Nearly always the work fits: say nothing, carry on, and never re-raise a decline. Only when it",`truly does not, call cairn_checkpoint FIRST (answer ${N} \u2014 it is all of this session`,'that survives), then AskUserQuestion offering "Checkpoint + /clear", "Checkpoint + /compact" or',`"Keep going" \u2014 they must type it, you cannot. Never write ${N} for them, and never`,"issue an all-clear."].join(`
16
+ `);function Ye({cwd:e,session:t,transcriptPath:n,bytes:s,usage:o}){let r=o??zt(n);r.source==="none"&&typeof s=="number"&&(r={tokens:0,source:"bytes",bytes:s});let a=s??r.bytes;if(r.source==="none")return null;let c=Bt(t),l;if(r.source!=="transcript")l=`transcript is ${(a/1024/1024).toFixed(1)} MB, token count unreadable (size signal only, not a token count)`;else if(c){let b=Math.round(r.tokens/c.tokens*100),x=c.model?`, declared for ${c.model}`:", declared",k=b>100?" \u2014 over 100% is impossible, so that declared window is wrong: re-declare it":"";l=`${r.tokens.toLocaleString()} of ${c.tokens.toLocaleString()} tokens (${b}%${x})${k}`}else l=`${r.tokens.toLocaleString()} tokens used so far; window NOT declared, so there is no percentage \u2014 pass ${D} to fix that`;let i=ue(e,t),u=Be(i),d;i.recorded?i.stale?d=`Handoff is STALE, written before the ${i.commits} commit(s) since: `+$e(i.text):d=`Handoff is current: ${$e(i.text)}`:d="Nothing is recorded about what a reset would lose.";let p=Ut(),f=p!==null&&r.source==="transcript"&&r.tokens>=p?` This session declared CAIRN_CONTEXT_HINT_TOKENS=${p.toLocaleString()} and is past it.`:"",m="\u2192 If you reset: /"+u+" \u2014 "+Kt(i)+".";return{line:`[cairn] Context: ${l}. ${d}${f}
17
+ `+m,advice:m,protocol:qt,state:i,recommend:u,usage:r,bytes:a,window:c}}function Ge(e){try{H.unlinkSync(Ee.join(e,Ct))}catch{}try{H.unlinkSync(Ee.join(e,ze))}catch{}}var Re=.9,ze=".handoff-gate";function Ke(e,t){let n=Ee.join(e,ze),s=null;try{s=H.readFileSync(n,"utf8").trim()||null}catch{}if(!t){if(s)try{H.unlinkSync(n)}catch{}return null}if(s)return s;let o=new Date().toISOString();try{H.writeFileSync(n,o,"utf8")}catch{}return o}function qe({usage:e,window:t,state:n,crossedAt:s}){if(!t?.tokens||e?.source!=="transcript"||e.tokens<Re*t.tokens||n.recorded&&!n.stale&&n.at&&s&&n.at>=s)return null;let r;n.recorded?n.stale?r="the handoff predates the "+n.commits+" commit(s) since":r="the handoff was written before the window reached this point, so it describes a session that has since moved on":r="nothing has been written down about what this session would lose";let a=Math.round(e.tokens/t.tokens*100),c=t.tokens-e.tokens,i="For you: /compact keeps this conversation as a summary, so nothing already read has to be read again. /clear starts a fresh session and cairn re-injects your saved notes on the first message. "+(Be(n)==="clear"?"Your saved notes are current, so neither choice drops anything already written down.":"Either way, let the agent write the notes first \u2014 they do not yet cover the work above.");return"\u26A0 [cairn] "+a+"% of the declared window ("+e.tokens.toLocaleString()+" of "+t.tokens.toLocaleString()+" tokens, "+c.toLocaleString()+" left) and "+r+`. Call cairn_checkpoint with would_be_lost: the every-turn auto-checkpoint carries that field forward but never writes it, so nothing else will. Then tell the user WHICH reset this calls for \u2014 /compact if the work ahead continues this conversation, /clear if it starts something unrelated. You can judge that and cairn cannot: you have the request, the roadmap cursor and where this was going. This is not a verdict on whether your next task fits \u2014 that judgement is still yours.
18
+ `+i}var Vt=1e3,Xt=100*1024;function Zt(e,t,n,s){if(e&&typeof e=="object"&&e.ref)return e;let o=typeof e=="string"?e:String(e),r=/^\[\d{4}-\d{2}-\d{2}\]/.test(o)?o:`[${n}] ${o}`;if(r.length<=Vt)return r;let a=oe.join(t,"notes");J.mkdirSync(a,{recursive:!0});let l=`note-${new Date().toISOString().replace(/[:.]/g,"-")}-${s}.md`,i=oe.join(a,l);return J.writeFileSync(i,r,"utf8"),{description:r.split(`
19
+ `)[0].slice(0,120),ref:oe.join(".cairn","notes",l)}}function Oe(e,{message:t,active_files:n=[],notes:s=[],auto:o=!1,...r}){if(typeof s=="string")try{s=JSON.parse(s)}catch{s=s?[s]:[]}Array.isArray(s)||(s=[]);let a=Z(),c=oe.join(a,"session.json"),l=new Date().toISOString().slice(0,10),i=new Date().toISOString(),u={};try{u=JSON.parse(J.readFileSync(c,"utf8"))||{}}catch{}let d=typeof r[N]=="string"&&r[N].trim()?r[N].trim():null,p=d??(typeof u[N]=="string"?u[N]:null),f=d?i:u[`${N}_at`]??null,m=me(r[D]),S=m??me(u[D]),b=m?i:u[`${D}_at`]??null,x=m&&typeof r[z]=="string"&&r[z].trim()?r[z].trim():u[z]??null,k={};try{let y=e.prepare("SELECT path FROM main.files").all();for(let{path:g}of y)try{k[g]=J.statSync(g).mtimeMs}catch{}}catch{}for(let y of n)try{k[y]=J.statSync(y).mtimeMs}catch{}let $=s.map((y,g)=>Zt(y,a,l,g));if(J.existsSync(c))try{if(J.statSync(c).size>Xt){let g=1;for(;J.existsSync(oe.join(a,`session.${g}.json`));)g++;J.renameSync(c,oe.join(a,`session.${g}.json`))}}catch{}let v={message:t,checkpoint_at:i,last_manual_checkpoint_at:o?u.last_manual_checkpoint_at??null:i,last_manual_message:o?u.last_manual_message??null:t,active_files:n,notes:$,[N]:p,[`${N}_at`]:f,[D]:S,[`${D}_at`]:b,[z]:x,mtime_snapshot:k};return J.writeFileSync(c,JSON.stringify(v,null,2),"utf8"),{content:[{type:"text",text:JSON.stringify({saved:!0,checkpoint_at:v.checkpoint_at,active_files_tracked:n.length,notes_saved:$.length,[N]:p?d?"recorded":"carried forward from the previous checkpoint":`not recorded \u2014 nothing will tell the next session what this one would lose (pass ${N})`,[D]:S?`${S.toLocaleString()} (${m?"declared now":"carried forward"})`:`not declared \u2014 cairn can report a token count but no percentage (pass ${D})`},null,2)}]}}import G from"fs";import C from"path";import Qt from"crypto";var Ve=80;function Te(e){let t=C.dirname(C.resolve(e)),n=C.parse(t).root;for(;;){let s=C.join(t,".cairn");try{if(G.statSync(s).isDirectory()&&G.existsSync(C.join(s,".cairn-project"))&&s.split(C.sep).filter(r=>r===".cairn").length===1)return s}catch{}if(t===n)return null;t=C.dirname(t)}}function Xe(e,t){let n=Qt.createHash("sha256").update(C.resolve(t)).digest("hex").slice(0,6);return C.join(e,"views",`${n}_${C.basename(t)}`)}function Q(e){try{return JSON.parse(G.readFileSync(C.join(e,"minify-map.json"),"utf8"))}catch{return{}}}function W(e,t){G.mkdirSync(e,{recursive:!0}),G.writeFileSync(C.join(e,"minify-map.json"),JSON.stringify(t,null,2),"utf8")}function he(e,t){let n=0;for(let[s,o]of Object.entries(t)){let r=!1;if(!G.existsSync(s))r=!0;else if(!G.existsSync(o.tempPath))r=!0;else try{G.statSync(s).mtimeMs>o.minifiedAt&&(r=!0)}catch{r=!0}if(r){if(o.tempPath)try{G.unlinkSync(o.tempPath)}catch{}delete t[s],n++}}return n}function Ze(e,t){switch(t){case"javascript":case"typescript":return je(e);case"java":return je(e,{annotations:!0});case"python":return sn(e);case"vue":return on(e);case"sql":return rn(e);default:return e.split(`
18
20
  `).slice(0,30).join(`
19
21
  `)+`
20
22
  ...`}}function je(e,{annotations:t=!1}={}){let n=e.split(`
21
- `),s=[],o=0;for(let r=0;r<n.length;r++){let a=n[r].trim();if(!a)continue;let{opens:c,closes:l}=tn(a),i=c-l;if(o===0){let u=Qt(a);u!==null?s.push(`${r+1}: ${u}`):a==="}"&&s.push("}")}else if(o===1){let u=en(a,t);u!==null&&s.push(` ${r+1}: ${u}`)}o=Math.max(0,o+i)}return s.join(`
22
- `)}function Qt(e){return/^import\s/.test(e)||/^@\w+/.test(e)||/^package\s/.test(e)?e:/^export\s/.test(e)?/^export\s+(default\s+)?(async\s+)?function[\s*]|^export\s+(default\s+)?(abstract\s+)?class\s/.test(e)?B(e):e:/^(async\s+)?function[\s*]/.test(e)||/^(abstract\s+)?class\s/.test(e)||/^(public\s+|private\s+|protected\s+)?(interface|enum|type|record)\s/.test(e)||/^(const|let|var)\s+\w+/.test(e)||/^(public|private|protected|abstract|final)\s+(class|interface|enum)\s/.test(e)?B(e):e==="}"?"}":null}function en(e,t){return t&&/^@\w+/.test(e)?e:e==="}"?"}":/^(public|private|protected|static|final|abstract|async|override|readonly|synchronized|native)\s/.test(e)||/^constructor\s*\(/.test(e)||/^(get|set)\s+\w+\s*\(/.test(e)||/^\w[\w$]*\s*[<(]/.test(e)&&!e.startsWith("return")&&!e.startsWith("throw")&&!e.startsWith("if")&&!e.startsWith("for")&&!e.startsWith("while")&&!e.startsWith("switch")&&!e.startsWith("const")&&!e.startsWith("let")&&!e.startsWith("var")?B(e):null}function B(e){let t=0,n=!1,s="";for(let o=0;o<e.length;o++){let r=e[o];if(n)r===s&&e[o-1]!=="\\"&&(n=!1);else if(r==='"'||r==="'"||r==="`")n=!0,s=r;else if(r==="{"){if(t===0)return e.slice(0,o).trimEnd();t++}else r==="}"&&t--}return e}function tn(e){let t=0,n=0,s=!1,o="";for(let r=0;r<e.length;r++){let a=e[r];s?a===o&&e[r-1]!=="\\"&&(s=!1):a==='"'||a==="'"||a==="`"?(s=!0,o=a):a==="{"?t++:a==="}"&&n++}return{opens:t,closes:n}}function nn(e){let t=[],n=e.split(`
23
+ `),s=[],o=0;for(let r=0;r<n.length;r++){let a=n[r].trim();if(!a)continue;let{opens:c,closes:l}=nn(a),i=c-l;if(o===0){let u=en(a);u!==null?s.push(`${r+1}: ${u}`):a==="}"&&s.push("}")}else if(o===1){let u=tn(a,t);u!==null&&s.push(` ${r+1}: ${u}`)}o=Math.max(0,o+i)}return s.join(`
24
+ `)}function en(e){return/^import\s/.test(e)||/^@\w+/.test(e)||/^package\s/.test(e)?e:/^export\s/.test(e)?/^export\s+(default\s+)?(async\s+)?function[\s*]|^export\s+(default\s+)?(abstract\s+)?class\s/.test(e)?B(e):e:/^(async\s+)?function[\s*]/.test(e)||/^(abstract\s+)?class\s/.test(e)||/^(public\s+|private\s+|protected\s+)?(interface|enum|type|record)\s/.test(e)||/^(const|let|var)\s+\w+/.test(e)||/^(public|private|protected|abstract|final)\s+(class|interface|enum)\s/.test(e)?B(e):e==="}"?"}":null}function tn(e,t){return t&&/^@\w+/.test(e)?e:e==="}"?"}":/^(public|private|protected|static|final|abstract|async|override|readonly|synchronized|native)\s/.test(e)||/^constructor\s*\(/.test(e)||/^(get|set)\s+\w+\s*\(/.test(e)||/^\w[\w$]*\s*[<(]/.test(e)&&!e.startsWith("return")&&!e.startsWith("throw")&&!e.startsWith("if")&&!e.startsWith("for")&&!e.startsWith("while")&&!e.startsWith("switch")&&!e.startsWith("const")&&!e.startsWith("let")&&!e.startsWith("var")?B(e):null}function B(e){let t=0,n=!1,s="";for(let o=0;o<e.length;o++){let r=e[o];if(n)r===s&&e[o-1]!=="\\"&&(n=!1);else if(r==='"'||r==="'"||r==="`")n=!0,s=r;else if(r==="{"){if(t===0)return e.slice(0,o).trimEnd();t++}else r==="}"&&t--}return e}function nn(e){let t=0,n=0,s=!1,o="";for(let r=0;r<e.length;r++){let a=e[r];s?a===o&&e[r-1]!=="\\"&&(s=!1):a==='"'||a==="'"||a==="`"?(s=!0,o=a):a==="{"?t++:a==="}"&&n++}return{opens:t,closes:n}}function sn(e){let t=[],n=e.split(`
23
25
  `);for(let s=0;s<n.length;s++){let o=n[s];if(!o.trim())continue;let r=o.trimStart();r.startsWith("import ")||r.startsWith("from ")?t.push(o):(r.startsWith("class ")||r.startsWith("def ")||r.startsWith("async def "))&&t.push(`${s+1}: ${o.trim().replace(/:(\s*)$/,":")}`)}return t.join(`
24
- `)}function sn(e){let t=[];for(let s of e.matchAll(/<(template|script|style)(\s[^>]*)?\s*>/g))t.push(s[0]);let n=e.match(/<script(?:\s[^>]*)?\s*>([\s\S]*?)<\/script>/);if(n){let s=je(n[1]);s.trim()&&(t.push("// script:"),t.push(s))}return t.join(`
25
- `)}function on(e){let t=[],n=e.split(`
26
+ `)}function on(e){let t=[];for(let s of e.matchAll(/<(template|script|style)(\s[^>]*)?\s*>/g))t.push(s[0]);let n=e.match(/<script(?:\s[^>]*)?\s*>([\s\S]*?)<\/script>/);if(n){let s=je(n[1]);s.trim()&&(t.push("// script:"),t.push(s))}return t.join(`
27
+ `)}function rn(e){let t=[],n=e.split(`
26
28
  `);for(let s=0;s<n.length;s++){let o=n[s].trim();/^(CREATE|ALTER|DROP|INSERT|UPDATE|DELETE|SELECT|WITH|GRANT|REVOKE|BEGIN|COMMIT)\b/i.test(o)&&t.push(`${s+1}: ${o.slice(0,120)}${o.length>120?"...":""}`)}return t.join(`
27
- `)}import I from"fs";import K from"path";var rn=["preference","experience","decision","knowledge"];function de(e){if(!e||typeof e!="string")return[];let t=e.split(/\r?\n/),n=!1,s=null,o=[],r=new Set;for(let a of t){if(/^##\s+Phases\s*$/i.test(a)){n=!0;continue}if(/^##\s+/.test(a)&&!/^###/.test(a)){s&&(o.push(s),s=null),n=!1;continue}if(!n)continue;let c=/^###\s+Phase\s+(\d+)\s*:\s*(.+?)\s*$/.exec(a);if(c){s&&o.push(s);let l=parseInt(c[1],10),i=c[2];if(r.has(l)){process.stderr.write(`[cairn] memo phase parser: duplicate Phase ${l} ignored
29
+ `)}import I from"fs";import K from"path";var an=["preference","experience","decision","knowledge"];function de(e){if(!e||typeof e!="string")return[];let t=e.split(/\r?\n/),n=!1,s=null,o=[],r=new Set;for(let a of t){if(/^##\s+Phases\s*$/i.test(a)){n=!0;continue}if(/^##\s+/.test(a)&&!/^###/.test(a)){s&&(o.push(s),s=null),n=!1;continue}if(!n)continue;let c=/^###\s+Phase\s+(\d+)\s*:\s*(.+?)\s*$/.exec(a);if(c){s&&o.push(s);let l=parseInt(c[1],10),i=c[2];if(r.has(l)){process.stderr.write(`[cairn] memo phase parser: duplicate Phase ${l} ignored
28
30
  `),s=null;continue}r.add(l),s={phase_number:l,text:i,bodyLines:[]};continue}s&&s.bodyLines.push(a)}return s&&o.push(s),o.map(a=>({phase_number:a.phase_number,text:a.text,body:a.bodyLines.join(`
29
- `).trim()||null}))}var Ne=(e,t)=>e+" :: "+t;function Qe(e,t,n,s={}){let o=e.prepare("SELECT id, phase_number, status, text FROM main.roadmap_phases WHERE decision_name = ? AND phase_number IS NOT NULL ORDER BY phase_number").all(t),r=new Map(o.map(g=>[g.phase_number,g])),a=new Set,c=e.prepare("UPDATE main.roadmap_phases SET text = ?, body = ?, status = CASE WHEN status = ? THEN ? ELSE status END WHERE id = ?"),l=e.prepare("INSERT INTO main.roadmap_phases (decision_name, phase_number, parent_phase_id, text, body, status, position) VALUES (?, ?, NULL, ?, ?, ?, ?)"),i=e.prepare("UPDATE main.roadmap_phases SET status = 'stale' WHERE id = ?"),u=e.prepare("UPDATE main.roadmap_phases SET phase_number = NULL WHERE id = ?"),d=e.prepare("UPDATE main.roadmap_phases SET text = ?, body = ?, status = 'open', resolved_at = NULL WHERE id = ?"),p=s.completed??null,f=p===null?null:new Set(p.map(g=>Ne(g.number,g.title))),m=new Map;for(let g of p??[])g.id!=null&&m.set(Ne(g.number,g.title),g.id);let S=n.some(g=>{let _=r.get(g.phase_number);return _&&_.status==="done"&&_.text!==g.text}),b=[],x=[],k=[],$=[],v=[];for(let g=0;g<n.length;g++){let _=n[g];a.add(_.phase_number);let E=r.get(_.phase_number),ce=!!E&&E.status==="done",O=Ne(_.phase_number,_.text),R=f!==null&&f.has(O),T=R&&m.has(O)&&m.get(O)!==E.id,le=R&&(S||T);if(ce&&E.text===_.text&&f!==null&&!R)d.run(_.text,_.body,E.id),b.push({id:E.id,phase_number:_.phase_number,text:_.text});else if(ce&&(E.text!==_.text||le)){u.run(E.id);let V=l.run(t,_.phase_number,_.text,_.body,"open",g);$.push({id:Number(V.lastInsertRowid),phase_number:_.phase_number,text:_.text,replaced_shipped:E.id})}else if(E)c.run(_.text,_.body,"stale","open",E.id),k.push({id:E.id,phase_number:_.phase_number,text:_.text,reactivated:E.status==="stale"});else{let V=l.run(t,_.phase_number,_.text,_.body,"open",g);$.push({id:Number(V.lastInsertRowid),phase_number:_.phase_number,text:_.text})}}for(let[g,_]of r)!a.has(g)&&_.status!=="stale"&&_.status!=="done"&&(i.run(_.id),v.push({id:_.id,phase_number:g}));if(b.length>0||$.some(g=>g.replaced_shipped!=null)){let g=n.reduce((_,E)=>Math.max(_,E.phase_number),0);for(let[_,E]of r)E.status==="done"&&_>g&&(u.run(E.id),x.push({id:E.id,phase_number:_,text:E.text}))}return{updated:k.length,inserted:$.length,staled:v.length,repaired:b.length,retired:x.length,updated_phases:k,inserted_phases:$,staled_phases:v,repaired_phases:b,retired_phases:x}}function an(e){let t=e.match(/^---\n([\s\S]*?)\n---/);if(!t)return{};let n={};for(let s of t[1].split(`
30
- `)){let o=s.indexOf(":");o!==-1&&(n[s.slice(0,o).trim()]=s.slice(o+1).trim())}return n}function cn(e){let t=K.join(e,"MEMORY.md");return I.existsSync(t)?I.readFileSync(t,"utf8"):""}var ln=/^- \[(.+?)\]\((.+?)\)\s+—\s+(.*?)(?:\s+`\[(.+?)\]`)?\s*$/;function et(e){let t=ln.exec(e);return t?{name:t[1],file:t[2],description:t[3],type:t[4]||null}:null}function Ze(e){let t="- ["+e.name+"]("+e.file+") \u2014 "+e.description;return e.type?t+" `["+e.type+"]`":t}function un(e,t){let n=K.join(e,"MEMORY.md"),s=new Map(t.map(c=>[c.file,c])),o=new Set,r=I.existsSync(n)?I.readFileSync(n,"utf8").split(`
31
- `):["# Memory Index",""],a=[];for(let c of r){let l=et(c);if(!l){a.push(c);continue}let i=s.get(l.file);!i||o.has(i.file)||(o.add(i.file),a.push(Ze(i)))}for(;a.length>0&&a[a.length-1].trim()==="";)a.pop();for(let c of t)o.has(c.file)||(o.add(c.file),a.push(Ze(c)));I.writeFileSync(n,a.join(`
31
+ `).trim()||null}))}var Ne=(e,t)=>e+" :: "+t;function et(e,t,n,s={}){let o=e.prepare("SELECT id, phase_number, status, text FROM main.roadmap_phases WHERE decision_name = ? AND phase_number IS NOT NULL ORDER BY phase_number").all(t),r=new Map(o.map(g=>[g.phase_number,g])),a=new Set,c=e.prepare("UPDATE main.roadmap_phases SET text = ?, body = ?, status = CASE WHEN status = ? THEN ? ELSE status END WHERE id = ?"),l=e.prepare("INSERT INTO main.roadmap_phases (decision_name, phase_number, parent_phase_id, text, body, status, position) VALUES (?, ?, NULL, ?, ?, ?, ?)"),i=e.prepare("UPDATE main.roadmap_phases SET status = 'stale' WHERE id = ?"),u=e.prepare("UPDATE main.roadmap_phases SET phase_number = NULL WHERE id = ?"),d=e.prepare("UPDATE main.roadmap_phases SET text = ?, body = ?, status = 'open', resolved_at = NULL WHERE id = ?"),p=s.completed??null,f=p===null?null:new Set(p.map(g=>Ne(g.number,g.title))),m=new Map;for(let g of p??[])g.id!=null&&m.set(Ne(g.number,g.title),g.id);let S=n.some(g=>{let _=r.get(g.phase_number);return _&&_.status==="done"&&_.text!==g.text}),b=[],x=[],k=[],$=[],v=[];for(let g=0;g<n.length;g++){let _=n[g];a.add(_.phase_number);let E=r.get(_.phase_number),ce=!!E&&E.status==="done",O=Ne(_.phase_number,_.text),R=f!==null&&f.has(O),T=R&&m.has(O)&&m.get(O)!==E.id,le=R&&(S||T);if(ce&&E.text===_.text&&f!==null&&!R)d.run(_.text,_.body,E.id),b.push({id:E.id,phase_number:_.phase_number,text:_.text});else if(ce&&(E.text!==_.text||le)){u.run(E.id);let V=l.run(t,_.phase_number,_.text,_.body,"open",g);$.push({id:Number(V.lastInsertRowid),phase_number:_.phase_number,text:_.text,replaced_shipped:E.id})}else if(E)c.run(_.text,_.body,"stale","open",E.id),k.push({id:E.id,phase_number:_.phase_number,text:_.text,reactivated:E.status==="stale"});else{let V=l.run(t,_.phase_number,_.text,_.body,"open",g);$.push({id:Number(V.lastInsertRowid),phase_number:_.phase_number,text:_.text})}}for(let[g,_]of r)!a.has(g)&&_.status!=="stale"&&_.status!=="done"&&(i.run(_.id),v.push({id:_.id,phase_number:g}));if(b.length>0||$.some(g=>g.replaced_shipped!=null)){let g=n.reduce((_,E)=>Math.max(_,E.phase_number),0);for(let[_,E]of r)E.status==="done"&&_>g&&(u.run(E.id),x.push({id:E.id,phase_number:_,text:E.text}))}return{updated:k.length,inserted:$.length,staled:v.length,repaired:b.length,retired:x.length,updated_phases:k,inserted_phases:$,staled_phases:v,repaired_phases:b,retired_phases:x}}function cn(e){let t=e.match(/^---\n([\s\S]*?)\n---/);if(!t)return{};let n={};for(let s of t[1].split(`
32
+ `)){let o=s.indexOf(":");o!==-1&&(n[s.slice(0,o).trim()]=s.slice(o+1).trim())}return n}function ln(e){let t=K.join(e,"MEMORY.md");return I.existsSync(t)?I.readFileSync(t,"utf8"):""}var un=/^- \[(.+?)\]\((.+?)\)\s+—\s+(.*?)(?:\s+`\[(.+?)\]`)?\s*$/;function tt(e){let t=un.exec(e);return t?{name:t[1],file:t[2],description:t[3],type:t[4]||null}:null}function Qe(e){let t="- ["+e.name+"]("+e.file+") \u2014 "+e.description;return e.type?t+" `["+e.type+"]`":t}function dn(e,t){let n=K.join(e,"MEMORY.md"),s=new Map(t.map(c=>[c.file,c])),o=new Set,r=I.existsSync(n)?I.readFileSync(n,"utf8").split(`
33
+ `):["# Memory Index",""],a=[];for(let c of r){let l=tt(c);if(!l){a.push(c);continue}let i=s.get(l.file);!i||o.has(i.file)||(o.add(i.file),a.push(Qe(i)))}for(;a.length>0&&a[a.length-1].trim()==="";)a.pop();for(let c of t)o.has(c.file)||(o.add(c.file),a.push(Qe(c)));I.writeFileSync(n,a.join(`
32
34
  `).replace(/\n{3,}/g,`
33
35
 
34
36
  `)+`
35
- `,"utf8")}function dn(e){let t=[];for(let n of cn(e).split(`
36
- `)){let s=et(n);s&&t.push(s)}return t}function pn(e){let t=K.join(e,"MEMORY.bak");if(!I.existsSync(t))return t;for(let n=2;n<=1e3;n++){let s=K.join(e,"MEMORY.bak."+n);if(!I.existsSync(s))return s}return null}function tt(e,{dryRun:t=!1}={}){if(!I.existsSync(e))return{repaired:!1,reason:"no memory directory",dir:e};let n=dn(e),s=new Set(n.map(i=>i.file)),o=I.readdirSync(e).filter(i=>i.endsWith(".md")&&i!=="MEMORY.md"&&!/^MEMORY\.bak(\.\d+)?\.md$/.test(i)).sort(),r=[],a=[];for(let i of o){if(s.has(i))continue;let u;try{u=an(I.readFileSync(K.join(e,i),"utf8"))}catch(d){a.push({file:i,reason:d.message});continue}r.push({name:u.name||i.replace(/\.md$/,""),file:i,description:u.description||"(no description in frontmatter)",type:rn.includes(u.type)?u.type:null})}let c=n.filter(i=>!I.existsSync(K.join(e,i.file))).map(i=>({name:i.name,file:i.file})),l=null;if(r.length>0&&!t){let i=pn(e);if(i)try{I.copyFileSync(K.join(e,"MEMORY.md"),i),l=K.basename(i)}catch{}un(e,[...n,...r])}return{repaired:r.length>0&&!t,dry_run:t,dir:e,files_on_disk:o.length,already_indexed:n.length,restored:r.length,restored_entries:r.map(i=>({name:i.name,file:i.file,type:i.type})),dangling:c,unreadable:a,backup:l}}import j from"fs";import re from"path";var M="roadmap",fn=`# Roadmap
37
+ `,"utf8")}function pn(e){let t=[];for(let n of ln(e).split(`
38
+ `)){let s=tt(n);s&&t.push(s)}return t}function fn(e){let t=K.join(e,"MEMORY.bak");if(!I.existsSync(t))return t;for(let n=2;n<=1e3;n++){let s=K.join(e,"MEMORY.bak."+n);if(!I.existsSync(s))return s}return null}function nt(e,{dryRun:t=!1}={}){if(!I.existsSync(e))return{repaired:!1,reason:"no memory directory",dir:e};let n=pn(e),s=new Set(n.map(i=>i.file)),o=I.readdirSync(e).filter(i=>i.endsWith(".md")&&i!=="MEMORY.md"&&!/^MEMORY\.bak(\.\d+)?\.md$/.test(i)).sort(),r=[],a=[];for(let i of o){if(s.has(i))continue;let u;try{u=cn(I.readFileSync(K.join(e,i),"utf8"))}catch(d){a.push({file:i,reason:d.message});continue}r.push({name:u.name||i.replace(/\.md$/,""),file:i,description:u.description||"(no description in frontmatter)",type:an.includes(u.type)?u.type:null})}let c=n.filter(i=>!I.existsSync(K.join(e,i.file))).map(i=>({name:i.name,file:i.file})),l=null;if(r.length>0&&!t){let i=fn(e);if(i)try{I.copyFileSync(K.join(e,"MEMORY.md"),i),l=K.basename(i)}catch{}dn(e,[...n,...r])}return{repaired:r.length>0&&!t,dry_run:t,dir:e,files_on_disk:o.length,already_indexed:n.length,restored:r.length,restored_entries:r.map(i=>({name:i.name,file:i.file,type:i.type})),dangling:c,unreadable:a,backup:l}}import j from"fs";import re from"path";var M="roadmap",mn=`# Roadmap
37
39
 
38
40
  The forward-looking plan: only upcoming and in-progress phases live here. When a
39
41
  phase is marked done (cairn_roadmap set_status <id> done) it is removed from this
@@ -44,16 +46,16 @@ the plan \u2014 Cairn syncs it into the live tracker on the next cairn_roadmap r
44
46
 
45
47
  ### Phase 1: First phase title
46
48
  What this phase delivers.
47
- `;function ee(){return re.join(Z(),"roadmap.md")}function mn(){return re.join(Z(),"roadmap_completed.md")}function ye(){let e=ee();return j.existsSync(e)?j.readFileSync(e,"utf8"):""}function Me(){let e=ee();return j.existsSync(e)?!1:(j.writeFileSync(e,fn,"utf8"),!0)}function hn(e){let t=[];for(let n of e){let s=(n.text||"").trim();s&&(!Number.isInteger(n.phase_number)||n.phase_number<1||t.push({phase_number:n.phase_number,text:s,body:n.body??null}))}return t}var Pe="decision_production_roadmap.md";function yn(e){let t=e.match(/^---\n[\s\S]*?\n---\n?/);return t?e.slice(t[0].length):e}function gn(e,t){let n=re.join(e,"MEMORY.md");if(!j.existsSync(n))return;let s=j.readFileSync(n,"utf8").split(/\r?\n/).filter(o=>!o.includes(`](${t})`));j.writeFileSync(n,s.join(`
48
- `),"utf8")}function _n(){let e=ee();if(j.existsSync(e))return null;let t=[],n=ke();n&&t.push(n),t.push(re.join(Z(),"memory"));let s=null,o=null;for(let a of t){let c=re.join(a,Pe);s==null&&j.existsSync(c)&&(s=yn(j.readFileSync(c,"utf8")).trim(),o=c)}if(s==null)return null;let r=/^#\s/m.test(s)?"":`# Roadmap
49
+ `;function ee(){return re.join(Z(),"roadmap.md")}function hn(){return re.join(Z(),"roadmap_completed.md")}function ye(){let e=ee();return j.existsSync(e)?j.readFileSync(e,"utf8"):""}function Me(){let e=ee();return j.existsSync(e)?!1:(j.writeFileSync(e,mn,"utf8"),!0)}function yn(e){let t=[];for(let n of e){let s=(n.text||"").trim();s&&(!Number.isInteger(n.phase_number)||n.phase_number<1||t.push({phase_number:n.phase_number,text:s,body:n.body??null}))}return t}var Pe="decision_production_roadmap.md";function gn(e){let t=e.match(/^---\n[\s\S]*?\n---\n?/);return t?e.slice(t[0].length):e}function _n(e,t){let n=re.join(e,"MEMORY.md");if(!j.existsSync(n))return;let s=j.readFileSync(n,"utf8").split(/\r?\n/).filter(o=>!o.includes(`](${t})`));j.writeFileSync(n,s.join(`
50
+ `),"utf8")}function Sn(){let e=ee();if(j.existsSync(e))return null;let t=[],n=ke();n&&t.push(n),t.push(re.join(Z(),"memory"));let s=null,o=null;for(let a of t){let c=re.join(a,Pe);s==null&&j.existsSync(c)&&(s=gn(j.readFileSync(c,"utf8")).trim(),o=c)}if(s==null)return null;let r=/^#\s/m.test(s)?"":`# Roadmap
49
51
 
50
52
  `;j.writeFileSync(e,r+s+`
51
- `,"utf8");for(let a of t){let c=re.join(a,Pe);try{j.existsSync(c)&&j.unlinkSync(c)}catch{}try{gn(a,Pe)}catch{}}return{migrated:!0,from:o,to:e}}function ie(e){_n();let t=ee();if(!j.existsSync(t))return null;let n=j.readFileSync(t,"utf8");if(!/^##\s+Phases\s*$/im.test(n))return null;let s=hn(de(n));return Qe(e,M,s,{completed:ge()})}var Sn=/^-\s+(\d{4}-\d{2}-\d{2})\s+—\s+Phase\s+(\d+):\s*(.+?)(?:\s+\(([^)]*)\))?\s*$/,nt=/^phase-(\d+)$/,wn=/^[0-9a-f]{7,40}$/;function ge(){let e=mn();if(!j.existsSync(e))return null;let t=[];for(let n of j.readFileSync(e,"utf8").split(/\r?\n/)){let s=Sn.exec(n);if(!s)continue;let o=s[3],r=null,a=null;if(s[4]!=null){let c=s[4].split(/\s*,\s*/).filter(Boolean);if(c.length>0&&c.every(i=>nt.test(i)||wn.test(i)))for(let i of c){let u=nt.exec(i);u?r=parseInt(u[1],10):a=i}else o=`${o} (${s[4]})`}t.push({date:s[1],number:parseInt(s[2],10),title:o,id:r,commit:a})}return t}import ae from"fs";import Ae from"path";import{execSync as xn}from"child_process";var st=120;function Le(e){if(!e)return"";let t=String(e).replace(/`([^`]*)`/g,"$1").replace(/\*\*([^*]+)\*\*/g,"$1").replace(/\*([^*]+)\*/g,"$1").replace(/\[([^\]]+)\]\([^)]*\)/g,"$1").replace(/^[-*+\s]+/,"").replace(/^#{1,6}\s*/,"").replace(/[*~]/g,"").replace(/\s+/g," ").trim().replace(/[::.\s]+$/,"");return t.length>st&&(t=t.slice(0,st-1).trimEnd()+"\u2026"),t}function q(e){return String(e||"").toLowerCase().replace(/[^a-z0-9]+/g," ").trim()}function kn(e){if(!e||typeof e!="string")return[];let t=e.split(/\r?\n/),n=i=>i.map(u=>({text:Le(u.titleRaw),body:u.bodyLines.join(`
52
- `).trim()||null})).filter(u=>u.text),s=[],o=null;for(let i of t){let u=/^(#{1,6})\s+(.+?)\s*$/.exec(i);if(u&&u[1].length>=2&&u[1].length<=4){o&&s.push(o),o={titleRaw:u[2],bodyLines:[]};continue}o&&o.bodyLines.push(i)}o&&s.push(o);let r=[];o=null;for(let i of t){let u=/^(\d+)[.)]\s+(.+?)\s*$/.exec(i);if(u){o&&r.push(o),o={titleRaw:u[2],bodyLines:[]};continue}o&&o.bodyLines.push(i)}o&&r.push(o);let a=[n(s),n(r)].filter(i=>i.length>=2);if(a.length)return a.sort((i,u)=>u.length-i.length),a[0];let c=t.find(i=>i.trim());return[{text:Le(c||"Plan")||"Plan",body:e.trim()||null}]}function bn(e){let t=[];return e.forEach((n,s)=>{t.push(`### Phase ${s+1}: ${n.text}`),n.body&&n.body.trim()&&t.push(n.body.trim()),t.push("")}),t.join(`
53
+ `,"utf8");for(let a of t){let c=re.join(a,Pe);try{j.existsSync(c)&&j.unlinkSync(c)}catch{}try{_n(a,Pe)}catch{}}return{migrated:!0,from:o,to:e}}function ie(e){Sn();let t=ee();if(!j.existsSync(t))return null;let n=j.readFileSync(t,"utf8");if(!/^##\s+Phases\s*$/im.test(n))return null;let s=yn(de(n));return et(e,M,s,{completed:ge()})}var wn=/^-\s+(\d{4}-\d{2}-\d{2})\s+—\s+Phase\s+(\d+):\s*(.+?)(?:\s+\(([^)]*)\))?\s*$/,st=/^phase-(\d+)$/,xn=/^[0-9a-f]{7,40}$/;function ge(){let e=hn();if(!j.existsSync(e))return null;let t=[];for(let n of j.readFileSync(e,"utf8").split(/\r?\n/)){let s=wn.exec(n);if(!s)continue;let o=s[3],r=null,a=null;if(s[4]!=null){let c=s[4].split(/\s*,\s*/).filter(Boolean);if(c.length>0&&c.every(i=>st.test(i)||xn.test(i)))for(let i of c){let u=st.exec(i);u?r=parseInt(u[1],10):a=i}else o=`${o} (${s[4]})`}t.push({date:s[1],number:parseInt(s[2],10),title:o,id:r,commit:a})}return t}import ae from"fs";import Ae from"path";import{execSync as kn}from"child_process";var ot=120;function Le(e){if(!e)return"";let t=String(e).replace(/`([^`]*)`/g,"$1").replace(/\*\*([^*]+)\*\*/g,"$1").replace(/\*([^*]+)\*/g,"$1").replace(/\[([^\]]+)\]\([^)]*\)/g,"$1").replace(/^[-*+\s]+/,"").replace(/^#{1,6}\s*/,"").replace(/[*~]/g,"").replace(/\s+/g," ").trim().replace(/[::.\s]+$/,"");return t.length>ot&&(t=t.slice(0,ot-1).trimEnd()+"\u2026"),t}function q(e){return String(e||"").toLowerCase().replace(/[^a-z0-9]+/g," ").trim()}function bn(e){if(!e||typeof e!="string")return[];let t=e.split(/\r?\n/),n=i=>i.map(u=>({text:Le(u.titleRaw),body:u.bodyLines.join(`
54
+ `).trim()||null})).filter(u=>u.text),s=[],o=null;for(let i of t){let u=/^(#{1,6})\s+(.+?)\s*$/.exec(i);if(u&&u[1].length>=2&&u[1].length<=4){o&&s.push(o),o={titleRaw:u[2],bodyLines:[]};continue}o&&o.bodyLines.push(i)}o&&s.push(o);let r=[];o=null;for(let i of t){let u=/^(\d+)[.)]\s+(.+?)\s*$/.exec(i);if(u){o&&r.push(o),o={titleRaw:u[2],bodyLines:[]};continue}o&&o.bodyLines.push(i)}o&&r.push(o);let a=[n(s),n(r)].filter(i=>i.length>=2);if(a.length)return a.sort((i,u)=>u.length-i.length),a[0];let c=t.find(i=>i.trim());return[{text:Le(c||"Plan")||"Plan",body:e.trim()||null}]}function En(e){let t=[];return e.forEach((n,s)=>{t.push(`### Phase ${s+1}: ${n.text}`),n.body&&n.body.trim()&&t.push(n.body.trim()),t.push("")}),t.join(`
53
55
  `).replace(/\n{3,}/g,`
54
56
 
55
57
  `).trimEnd()+`
56
- `}function ot(e){let t=ye(),n=t.split(/\r?\n/),s=n.findIndex(i=>/^##\s+Phases\s*$/i.test(i)),o=bn(e);if(s===-1){let i=t.replace(/\s*$/,"");ae.writeFileSync(ee(),`${i}
58
+ `}function rt(e){let t=ye(),n=t.split(/\r?\n/),s=n.findIndex(i=>/^##\s+Phases\s*$/i.test(i)),o=En(e);if(s===-1){let i=t.replace(/\s*$/,"");ae.writeFileSync(ee(),`${i}
57
59
 
58
60
  ## Phases
59
61
 
@@ -62,56 +64,56 @@ ${o}`,"utf8");return}let r=n.length;for(let i=s+1;i<n.length;i++)if(/^##\s+/.tes
62
64
  `).replace(/^\n+/,""),l=`
63
65
 
64
66
  ${o}${c.trim()?`
65
- `+c:""}`;ae.writeFileSync(ee(),a+l,"utf8")}function rt(e){let t=kn(e);if(t.length===0)return{added:[],skipped:[],total:0};let s=Me()?[]:de(ye()).map(l=>({text:l.text,body:l.body})),o=new Set(s.map(l=>q(l.text))),r=[...s],a=[],c=[];for(let l of t){let i=q(l.text);if(o.has(i)){c.push(l.text);continue}o.add(i),r.push(l),a.push(l.text)}return a.length>0&&ot(r),{added:a,skipped:c,total:r.length}}function En(e){return e==="completed"?"done":e==="in_progress"?"in_progress":"open"}function $n(e){try{return e.prepare("SELECT phase_id FROM main.roadmap_cursor WHERE singleton = 1").get()?.phase_id??null}catch{return null}}function it(e,t){if(!Array.isArray(t)||t.length===0)return{changed:!1};let n=t.map(p=>({text:Le(p.content||p.activeForm||""),status:En(p.status)})).filter(p=>p.text);if(n.length===0)return{changed:!1};let s=e.prepare("SELECT id, status FROM main.roadmap_phases WHERE decision_name = ? AND parent_phase_id IS NULL").all(M).filter(p=>p.status!=="stale");if(s.length===0){let f=Me()?[]:de(ye()).map(k=>({text:k.text,body:k.body})),m=new Set(f.map(k=>q(k.text))),S=[...f],b=0;for(let k of n){let $=q(k.text);m.has($)||(m.add($),S.push({text:k.text,body:null}),b++)}if(b===0)return{changed:!1};ot(S);let x=ie(e);return{changed:!0,seeded:b,diff:x}}let o=$n(e);o==null&&(o=s.find(p=>p.status==="in_progress")?.id??s.find(p=>p.status==="open")?.id??s[0].id);let r=e.prepare("SELECT id, text, status FROM main.roadmap_phases WHERE parent_phase_id = ?").all(o),a=new Map(r.map(p=>[q(p.text),p])),c=e.prepare("INSERT INTO main.roadmap_phases (decision_name, phase_number, parent_phase_id, text, status, position) VALUES (NULL, NULL, ?, ?, ?, ?)"),l=e.prepare("UPDATE main.roadmap_phases SET status = ? WHERE id = ?"),i=e.prepare("SELECT COALESCE(MAX(position), -1) + 1 AS n FROM main.roadmap_phases WHERE parent_phase_id = ?").get(o).n,u=0,d=0;for(let p of n){let f=a.get(q(p.text));if(f){if(f.status==="done"&&p.status!=="done")continue;f.status!==p.status&&(l.run(p.status,f.id),d++)}else c.run(o,p.text,p.status,i++),u++}return{changed:u>0||d>0,parentId:o,inserted:u,updated:d}}var vn=new Set(["the","and","for","with","from","into","that","this","then","your","add","fix","update","make","phase","support","implement","feature","refactor","wire","wires","should","will","also","use","using","via"]);function Rn(e){return[...new Set(q(e).split(" ").filter(t=>t.length>=4&&!vn.has(t)))]}function On(e,t){if(!e||!t?.length)return null;let n=String(e),s=/\bphase\s*#?\s*(\d+)\b/i.exec(n);if(s){let a=parseInt(s[1],10),c=t.find(l=>l.phase_number===a);if(c)return{...c,confidence:"high"}}let o=new Set(q(n).split(" ").filter(Boolean)),r=null;for(let a of t){let c=Rn(a.text);if(c.length<2)continue;let l=c.filter(u=>o.has(u)),i=l.length/c.length;l.length>=2&&i>=.6&&(!r||i>r.ratio)&&(r={...a,confidence:"medium",ratio:i})}return r?(delete r.ratio,r):null}function Fe(e,t){return xn(`git ${t}`,{cwd:e,encoding:"utf8",timeout:5e3,stdio:["ignore","pipe","ignore"]}).trim()}function at(e,t){let n=Ae.join(t,".cairn"),s=Ae.join(n,".last-commit"),o=Ae.join(n,"roadmap-pending.json"),r;try{r=Fe(t,"rev-parse HEAD")}catch{return{candidates:[],newCommits:0}}let a=null;try{a=ae.readFileSync(s,"utf8").trim()||null}catch{}let c=[];if(a&&a!==r)try{c=Fe(t,`rev-list --no-merges --reverse ${a}..HEAD`).split(`
66
- `).filter(Boolean)}catch{c=[]}try{ae.writeFileSync(s,r,"utf8")}catch{}let l=[];try{let i=JSON.parse(ae.readFileSync(o,"utf8"));Array.isArray(i)&&(l=i)}catch{}if(c.length){let i=e.prepare("SELECT id, phase_number, text FROM main.roadmap_phases WHERE decision_name = ? AND parent_phase_id IS NULL AND status IN ('open','in_progress') AND phase_number IS NOT NULL").all(M);for(let u of c){let d="";try{d=Fe(t,`show -s --format=%s%n%b ${u}`)}catch{}let p=On(d,i);p&&!l.some(f=>f.phase_id===p.id)&&l.push({phase_id:p.id,phase_number:p.phase_number,text:p.text,commit:u.slice(0,7),confidence:p.confidence})}}if(l.length){let i=new Set(e.prepare("SELECT id FROM main.roadmap_phases WHERE status IN ('open','in_progress')").all().map(u=>u.id));l=l.filter(u=>i.has(u.phase_id))}try{ae.writeFileSync(o,JSON.stringify(l),"utf8")}catch{}return{candidates:l,newCommits:c.length}}import Y from"fs";import A from"path";var _e="cairn",ne="roadmap/1",Tn="<!-- slot:roadmap";function jn(e){return`${Tn} format=${ne} provider=${_e} generated=${e} -->`}var Nn=`<!-- ${_e}:roadmap -->`,Pn=/^<!--\s*slot:roadmap\b([^>]*?)-->/,Mn=/^<!--\s*([A-Za-z0-9_.-]+)\s*:\s*roadmap\s*-->/,An=/([A-Za-z_][A-Za-z0-9_-]*)=("[^"]*"|\S+)/g;function Fn(e){if(typeof e!="string")return null;let t=e.indexOf(`
67
- `);return(t===-1?e:e.slice(0,t)).trim()}function Ln(e){let t=Fn(e);if(!t)return null;let n=Pn.exec(t);if(n){let o={};for(let r of n[1].matchAll(An))o[r[1]]=r[2].replace(/^"|"$/g,"");return o.provider?{provider:o.provider.toLowerCase(),format:o.format||null,generated:o.generated||null}:null}let s=Mn.exec(t);return s?{provider:s[1].toLowerCase(),format:null,generated:null}:null}var Dn=/^```roadmap-json[ \t]*\r?\n([\s\S]*?)\r?\n```[ \t]*$/m;function ut(e){if(typeof e!="string")return null;let t=Dn.exec(e);if(!t)return null;try{let n=JSON.parse(t[1]);return n&&typeof n=="object"&&!Array.isArray(n)?n:null}catch{return null}}var Cn=new Set(["format","provider","generated","title","cursor","cursor_phase","cursor_path","phases","shipped_phases","migrated_from","migrated"]),In=new Set(["id","number","title","status","shipped","commit","goal","blocked_by","slices","detail","links"]);function dt(e,t){let n={};if(!e||typeof e!="object")return n;for(let s of Object.keys(e).sort())t.has(s)||(n[s]=e[s]);return n}function pt(e,t=new Map){if(!Array.isArray(e))return t;for(let n of e)n&&typeof n=="object"&&typeof n.id=="string"&&(t.set(n.id,n),pt(n.slices,t));return t}function Wn(e){if(typeof e!="string")return null;let t=ut(e)?.migrated_from;if(typeof t=="string"&&t)return t;let n=e.indexOf(`
68
- `);if(n===-1)return null;let s=e.indexOf("-->",n),o=s===-1?e.slice(0,512):e.slice(n,s),r=/^\s*migrated-from=(.+?)\s*$/m.exec(o);return r?r[1]:null}var Un=/\[\[([^\]|#\n]+?)(?:[|#][^\]\n]*)?\]\]/g,Hn=["decision","knowledge","experience","preference"];function Jn(e){let t=A.relative(X(),e).split(A.sep).join("/");return!t||t.startsWith("..")||A.isAbsolute(t)?null:t}function Bn(e){let t=A.join(X(),".cairn","memory");return[A.join(t,`${e}.md`),...Hn.map(n=>A.join(t,`${n}_${e}.md`))]}function Yn(e){if(!e)return[];let t=[],n=new Set;for(let s of String(e).matchAll(Un)){let o=s[1].trim();if(!(!o||n.has(o))){n.add(o);for(let r of Bn(o)){if(!Y.existsSync(r))continue;let a=Jn(r);a&&t.push({kind:"memory",title:o,path:a});break}}}return t}var Gn={open:"open",in_progress:"in progress",done:"done",blocked:"blocked",stale:"stale"},zn=new Set(["open","in_progress","done","blocked","stale"]);function Kn(e){return e==="done"?"x":e==="in_progress"?"~":e==="blocked"?"!":" "}function ft(){return A.join(X(),"ROADMAP.md")}function mt(){return new Date().toISOString().replace(/\.\d{3}Z$/,"Z")}function qn(e){try{return e.prepare("SELECT phase_id FROM main.roadmap_cursor WHERE singleton = 1").get()?.phase_id??null}catch{return null}}function Vn(e){let t=new Map,n;try{n=e.prepare("SELECT blocker_id, blocked_id FROM main.roadmap_phase_deps").all()}catch{return t}for(let s of n)t.has(s.blocked_id)||t.set(s.blocked_id,new Set),t.get(s.blocked_id).add(s.blocker_id);return t}function Xn(e,t){let n=e.decision_name??"",s=t.decision_name??"";return n!==s?n<s?-1:1:e.phase_number!=null&&t.phase_number!=null?e.phase_number-t.phase_number:e.position-t.position||e.id-t.id}function Zn(e){let t;try{t=e.prepare("SELECT id, decision_name, phase_number, parent_phase_id, text, body, status, position FROM main.roadmap_phases ORDER BY decision_name, phase_number, position, id").all()}catch{return null}if(t.filter(y=>y.status!=="stale").length===0)return null;let n=qn(e),s=Vn(e),o=new Map(t.map(y=>[y.id,y.status])),r=new Map(t.map(y=>[y.id,y.phase_number!=null?`Phase ${y.phase_number}`:y.text])),a=new Map(t.map(y=>[y.id,y])),c=new Map;for(let y of t){let g=y.parent_phase_id??"root";c.has(g)||c.set(g,[]),c.get(g).push(y)}for(let y of c.values())y.sort(Xn);let l=new Map,i=[];for(let y of c.get("root")||[])if(y.decision_name!=null){if(y.decision_name===M&&y.phase_number==null){y.status==="done"&&i.push(y);continue}l.has(y.decision_name)||l.set(y.decision_name,[]),l.get(y.decision_name).push(y)}let u=[...l.keys()].sort((y,g)=>y===M?-1:g===M?1:y<g?-1:y>g?1:0),d=new Map,p=!1;for(let y of u){let g=l.get(y).filter(E=>E.status!=="stale"),_=y===M?g.filter(E=>E.status!=="done"):g;_.length!==0&&(p=!0,d.set(y,{visible:_,doneCount:g.filter(E=>E.status==="done").length,total:g.length}))}if(!p)return null;let f=y=>{let g=s.get(y);return g?[...g].filter(_=>o.get(_)!=="done").map(_=>r.get(_)||`#${_}`):[]},m=y=>{let g=s.get(y);return g?[...g].filter(_=>a.has(_)).sort((_,E)=>_-E).map(_=>`phase-${_}`):[]},S=y=>(c.get(y)||[]).filter(g=>g.status!=="stale"),b=y=>c.get(y)||[],x=new Map(u.map(y=>[y,l.get(y)])),k=new Map;for(let y of ge()||[])y.id!=null&&k.set(y.id,{shipped:y.date,commit:y.commit||null});return{cursor:n,decisionNames:u,visibleRoots:d,children:S,unfinishedBlockers:f,blockerIds:m,ancestry:y=>{let g=[],_=new Set,E=y;for(;E!=null&&a.has(E)&&!_.has(E);)_.add(E),g.unshift(E),E=a.get(E).parent_phase_id;return g},allChildren:b,jsonRoots:x,archivedRoots:i,shipped:y=>k.get(y)||null}}var De=120;function Qn(e){return e.replace(/\s*\((?:see\s+)?\[\[[^\]]+\]\](?:\s*(?:,|and)\s*\[\[[^\]]+\]\])*\s*\)/gi,"").replace(/\[\[([^\]]+)\]\]/g,(t,n)=>n.replace(/_/g," ").trim()).replace(/\s{2,}/g," ").replace(/\s+([.,;:!?])/g,"$1").trim()}function ht(e){if(!e)return null;let t=[];for(let a of e.split(`
69
- `)){let c=a.trim();if(!c){if(t.length)break;continue}t.push(c)}if(!t.length)return null;let n=Qn(t.join(" ").replace(/\s+/g," ")),s=null,o=/[.?!](?=\s|$)/g,r;for(;(r=o.exec(n))!==null&&(s=n.slice(0,r.index+1),!(s.length>=40)););if(s||(s=n),s.length>De){let a=s.lastIndexOf(" ",De-1);s=s.slice(0,a>60?a:De-1).trimEnd()+"\u2026"}return s}var es=8;function ct(){let e=ge();if(!e||e.length===0)return[];let t=e.slice(-es).reverse(),n=["","---","","## Recently shipped",""];for(let o of t)n.push(`- ${o.date} \u2014 ${o.title}${o.commit?` \`${o.commit}\``:""}`);let s=e.length-t.length;return n.push("",s>0?`_${e.length} phases shipped in total; ${s} earlier one${s===1?"":"s"} not listed._`:`_${e.length} phase${e.length===1?"":"s"} shipped in total._`),n}function lt(e){let t=e.doneCount,n=e.total;if(!n)return"_No phases yet._";let s=Math.round(t/n*10);return`**${t} of ${n} done** \`${"\u2588".repeat(s)}${"\u2591".repeat(10-s)}\``}function ts(e,t){let n=[jn(t.generated),"<!--"," Generated by cairn \u2014 do NOT edit by hand."," The plan lives in .cairn/roadmap.md; status and cursor come from cairn_roadmap."," This file is regenerated whenever the roadmap changes; manual edits are overwritten."," Per-phase detail and links are in the roadmap-json block at the end of this file."];if(t.migratedFrom&&(n.push(" The ROADMAP.md that was here before cairn adopted this file is kept at:"),n.push(` migrated-from=${t.migratedFrom}`)),n.push("-->","","# Roadmap",""),e==null)return n.push("_No active plan right now._"),n.push(...ct()),n;let{cursor:s,decisionNames:o,visibleRoots:r,children:a,unfinishedBlockers:c}=e;n.push("Legend: `[x]` done \xB7 `[~]` in progress \xB7 `[ ]` open \xB7 `[!]` blocked.");let l=(i,u)=>{let d=" ".repeat(u),p=i.phase_number!=null?`Phase ${i.phase_number}: ${i.text}`:i.text,f=Gn[i.status]||i.status,m=c(i.id);m.length&&(f+=` \xB7 waiting on ${m.join(", ")}`),s===i.id&&(f+=" \xB7 \u2190 current"),n.push(`${d}- [${Kn(i.status)}] **${p}** \u2014 ${f}`);let S=ht(i.body);S&&n.push(`${d} ${S}`);for(let b of a(i.id))l(b,u+1)};for(let i of o){let u=r.get(i);if(u){n.push(""),i===M?n.push("## Active plan","",lt(u)):n.push(`## ${i}`,"",lt(u)),n.push("");for(let d of u.visible)l(d,0)}}return n.push(...ct()),n}function yt(e,t,n,s){let o=n.get(`phase-${e.id}`),r=ht(e.body),a=Yn(e.body),c=t.blockerIds(e.id),l={id:`phase-${e.id}`};e.phase_number!=null&&(l.number=e.phase_number),l.title=e.text,l.status=zn.has(e.status)?e.status:"open";let i=e.status==="done"?t.shipped(e.id):null;if(i&&(l.shipped=i.shipped,i.commit&&(l.commit=i.commit)),r&&(l.goal=r),c.length&&(l.blocked_by=c),!s){let u=[],d=p=>{for(let f of t.allChildren(p))u.push(yt(f,t,n,!0)),d(f.id)};d(e.id),u.length&&(l.slices=u)}return e.body&&e.body.trim()&&(l.detail=e.body.trim()),a.length&&(l.links=a),{...l,...dt(o,In)}}function ns(e,t){let n=pt(t.previous?.phases),s=[];for(let u of e?.decisionNames??[]){let d=e.jsonRoots.get(u);if(d)for(let p of d)s.push(yt(p,e,n,!1))}let o=(e?.archivedRoots??[]).map(u=>{let d=e.shipped(u.id),p={id:`phase-${u.id}`,title:u.text,status:"done"};return d&&(p.shipped=d.shipped,d.commit&&(p.commit=d.commit)),p}),r=new Set(s.map(u=>u.id)),a=e?.cursor!=null?e.ancestry(e.cursor):[],c=a.length?`phase-${a[0]}`:null,l={format:ne,provider:_e,generated:t.generated,title:`${A.basename(X())} roadmap`,cursor:e?.cursor!=null?`phase-${e.cursor}`:null,cursor_phase:c!==null&&r.has(c)?c:null,cursor_path:a.length?a.map(u=>`phase-${u}`):null};t.migratedFrom&&(l.migrated_from=t.migratedFrom,t.migrated&&(l.migrated=t.migrated)),l.phases=s,l.shipped_phases=o;let i={...l,...dt(t.previous,Cn)};return["","<details>","<summary>Machine-readable plan (roadmap/1)</summary>","","```roadmap-json",JSON.stringify(i,null,2),"```","","</details>"]}function Ce(e,t={}){let n={generated:t.generated||mt(),migratedFrom:t.migratedFrom||null,migrated:t.migrated||null,previous:t.previous&&typeof t.previous=="object"?t.previous:null},s=Zn(e);return s==null&&!t.allowEmpty?null:[...ts(s,n),...ns(s,n)].join(`
67
+ `+c:""}`;ae.writeFileSync(ee(),a+l,"utf8")}function it(e){let t=bn(e);if(t.length===0)return{added:[],skipped:[],total:0};let s=Me()?[]:de(ye()).map(l=>({text:l.text,body:l.body})),o=new Set(s.map(l=>q(l.text))),r=[...s],a=[],c=[];for(let l of t){let i=q(l.text);if(o.has(i)){c.push(l.text);continue}o.add(i),r.push(l),a.push(l.text)}return a.length>0&&rt(r),{added:a,skipped:c,total:r.length}}function $n(e){return e==="completed"?"done":e==="in_progress"?"in_progress":"open"}function vn(e){try{return e.prepare("SELECT phase_id FROM main.roadmap_cursor WHERE singleton = 1").get()?.phase_id??null}catch{return null}}function at(e,t){if(!Array.isArray(t)||t.length===0)return{changed:!1};let n=t.map(p=>({text:Le(p.content||p.activeForm||""),status:$n(p.status)})).filter(p=>p.text);if(n.length===0)return{changed:!1};let s=e.prepare("SELECT id, status FROM main.roadmap_phases WHERE decision_name = ? AND parent_phase_id IS NULL").all(M).filter(p=>p.status!=="stale");if(s.length===0){let f=Me()?[]:de(ye()).map(k=>({text:k.text,body:k.body})),m=new Set(f.map(k=>q(k.text))),S=[...f],b=0;for(let k of n){let $=q(k.text);m.has($)||(m.add($),S.push({text:k.text,body:null}),b++)}if(b===0)return{changed:!1};rt(S);let x=ie(e);return{changed:!0,seeded:b,diff:x}}let o=vn(e);o==null&&(o=s.find(p=>p.status==="in_progress")?.id??s.find(p=>p.status==="open")?.id??s[0].id);let r=e.prepare("SELECT id, text, status FROM main.roadmap_phases WHERE parent_phase_id = ?").all(o),a=new Map(r.map(p=>[q(p.text),p])),c=e.prepare("INSERT INTO main.roadmap_phases (decision_name, phase_number, parent_phase_id, text, status, position) VALUES (NULL, NULL, ?, ?, ?, ?)"),l=e.prepare("UPDATE main.roadmap_phases SET status = ? WHERE id = ?"),i=e.prepare("SELECT COALESCE(MAX(position), -1) + 1 AS n FROM main.roadmap_phases WHERE parent_phase_id = ?").get(o).n,u=0,d=0;for(let p of n){let f=a.get(q(p.text));if(f){if(f.status==="done"&&p.status!=="done")continue;f.status!==p.status&&(l.run(p.status,f.id),d++)}else c.run(o,p.text,p.status,i++),u++}return{changed:u>0||d>0,parentId:o,inserted:u,updated:d}}var Rn=new Set(["the","and","for","with","from","into","that","this","then","your","add","fix","update","make","phase","support","implement","feature","refactor","wire","wires","should","will","also","use","using","via"]);function On(e){return[...new Set(q(e).split(" ").filter(t=>t.length>=4&&!Rn.has(t)))]}function Tn(e,t){if(!e||!t?.length)return null;let n=String(e),s=/\bphase\s*#?\s*(\d+)\b/i.exec(n);if(s){let a=parseInt(s[1],10),c=t.find(l=>l.phase_number===a);if(c)return{...c,confidence:"high"}}let o=new Set(q(n).split(" ").filter(Boolean)),r=null;for(let a of t){let c=On(a.text);if(c.length<2)continue;let l=c.filter(u=>o.has(u)),i=l.length/c.length;l.length>=2&&i>=.6&&(!r||i>r.ratio)&&(r={...a,confidence:"medium",ratio:i})}return r?(delete r.ratio,r):null}function Fe(e,t){return kn(`git ${t}`,{cwd:e,encoding:"utf8",timeout:5e3,stdio:["ignore","pipe","ignore"]}).trim()}function ct(e,t){let n=Ae.join(t,".cairn"),s=Ae.join(n,".last-commit"),o=Ae.join(n,"roadmap-pending.json"),r;try{r=Fe(t,"rev-parse HEAD")}catch{return{candidates:[],newCommits:0}}let a=null;try{a=ae.readFileSync(s,"utf8").trim()||null}catch{}let c=[];if(a&&a!==r)try{c=Fe(t,`rev-list --no-merges --reverse ${a}..HEAD`).split(`
68
+ `).filter(Boolean)}catch{c=[]}try{ae.writeFileSync(s,r,"utf8")}catch{}let l=[];try{let i=JSON.parse(ae.readFileSync(o,"utf8"));Array.isArray(i)&&(l=i)}catch{}if(c.length){let i=e.prepare("SELECT id, phase_number, text FROM main.roadmap_phases WHERE decision_name = ? AND parent_phase_id IS NULL AND status IN ('open','in_progress') AND phase_number IS NOT NULL").all(M);for(let u of c){let d="";try{d=Fe(t,`show -s --format=%s%n%b ${u}`)}catch{}let p=Tn(d,i);p&&!l.some(f=>f.phase_id===p.id)&&l.push({phase_id:p.id,phase_number:p.phase_number,text:p.text,commit:u.slice(0,7),confidence:p.confidence})}}if(l.length){let i=new Set(e.prepare("SELECT id FROM main.roadmap_phases WHERE status IN ('open','in_progress')").all().map(u=>u.id));l=l.filter(u=>i.has(u.phase_id))}try{ae.writeFileSync(o,JSON.stringify(l),"utf8")}catch{}return{candidates:l,newCommits:c.length}}import Y from"fs";import A from"path";var _e="cairn",ne="roadmap/1",jn="<!-- slot:roadmap";function Nn(e){return`${jn} format=${ne} provider=${_e} generated=${e} -->`}var Pn=`<!-- ${_e}:roadmap -->`,Mn=/^<!--\s*slot:roadmap\b([^>]*?)-->/,An=/^<!--\s*([A-Za-z0-9_.-]+)\s*:\s*roadmap\s*-->/,Fn=/([A-Za-z_][A-Za-z0-9_-]*)=("[^"]*"|\S+)/g;function Ln(e){if(typeof e!="string")return null;let t=e.indexOf(`
69
+ `);return(t===-1?e:e.slice(0,t)).trim()}function Dn(e){let t=Ln(e);if(!t)return null;let n=Mn.exec(t);if(n){let o={};for(let r of n[1].matchAll(Fn))o[r[1]]=r[2].replace(/^"|"$/g,"");return o.provider?{provider:o.provider.toLowerCase(),format:o.format||null,generated:o.generated||null}:null}let s=An.exec(t);return s?{provider:s[1].toLowerCase(),format:null,generated:null}:null}var Cn=/^```roadmap-json[ \t]*\r?\n([\s\S]*?)\r?\n```[ \t]*$/m;function dt(e){if(typeof e!="string")return null;let t=Cn.exec(e);if(!t)return null;try{let n=JSON.parse(t[1]);return n&&typeof n=="object"&&!Array.isArray(n)?n:null}catch{return null}}var In=new Set(["format","provider","generated","title","cursor","cursor_phase","cursor_path","phases","shipped_phases","migrated_from","migrated"]),Wn=new Set(["id","number","title","status","shipped","commit","goal","blocked_by","slices","detail","links"]);function pt(e,t){let n={};if(!e||typeof e!="object")return n;for(let s of Object.keys(e).sort())t.has(s)||(n[s]=e[s]);return n}function ft(e,t=new Map){if(!Array.isArray(e))return t;for(let n of e)n&&typeof n=="object"&&typeof n.id=="string"&&(t.set(n.id,n),ft(n.slices,t));return t}function Un(e){if(typeof e!="string")return null;let t=dt(e)?.migrated_from;if(typeof t=="string"&&t)return t;let n=e.indexOf(`
70
+ `);if(n===-1)return null;let s=e.indexOf("-->",n),o=s===-1?e.slice(0,512):e.slice(n,s),r=/^\s*migrated-from=(.+?)\s*$/m.exec(o);return r?r[1]:null}var Hn=/\[\[([^\]|#\n]+?)(?:[|#][^\]\n]*)?\]\]/g,Jn=["decision","knowledge","experience","preference"];function Bn(e){let t=A.relative(X(),e).split(A.sep).join("/");return!t||t.startsWith("..")||A.isAbsolute(t)?null:t}function Yn(e){let t=A.join(X(),".cairn","memory");return[A.join(t,`${e}.md`),...Jn.map(n=>A.join(t,`${n}_${e}.md`))]}function Gn(e){if(!e)return[];let t=[],n=new Set;for(let s of String(e).matchAll(Hn)){let o=s[1].trim();if(!(!o||n.has(o))){n.add(o);for(let r of Yn(o)){if(!Y.existsSync(r))continue;let a=Bn(r);a&&t.push({kind:"memory",title:o,path:a});break}}}return t}var zn={open:"open",in_progress:"in progress",done:"done",blocked:"blocked",stale:"stale"},Kn=new Set(["open","in_progress","done","blocked","stale"]);function qn(e){return e==="done"?"x":e==="in_progress"?"~":e==="blocked"?"!":" "}function mt(){return A.join(X(),"ROADMAP.md")}function ht(){return new Date().toISOString().replace(/\.\d{3}Z$/,"Z")}function Vn(e){try{return e.prepare("SELECT phase_id FROM main.roadmap_cursor WHERE singleton = 1").get()?.phase_id??null}catch{return null}}function Xn(e){let t=new Map,n;try{n=e.prepare("SELECT blocker_id, blocked_id FROM main.roadmap_phase_deps").all()}catch{return t}for(let s of n)t.has(s.blocked_id)||t.set(s.blocked_id,new Set),t.get(s.blocked_id).add(s.blocker_id);return t}function Zn(e,t){let n=e.decision_name??"",s=t.decision_name??"";return n!==s?n<s?-1:1:e.phase_number!=null&&t.phase_number!=null?e.phase_number-t.phase_number:e.position-t.position||e.id-t.id}function Qn(e){let t;try{t=e.prepare("SELECT id, decision_name, phase_number, parent_phase_id, text, body, status, position FROM main.roadmap_phases ORDER BY decision_name, phase_number, position, id").all()}catch{return null}if(t.filter(y=>y.status!=="stale").length===0)return null;let n=Vn(e),s=Xn(e),o=new Map(t.map(y=>[y.id,y.status])),r=new Map(t.map(y=>[y.id,y.phase_number!=null?`Phase ${y.phase_number}`:y.text])),a=new Map(t.map(y=>[y.id,y])),c=new Map;for(let y of t){let g=y.parent_phase_id??"root";c.has(g)||c.set(g,[]),c.get(g).push(y)}for(let y of c.values())y.sort(Zn);let l=new Map,i=[];for(let y of c.get("root")||[])if(y.decision_name!=null){if(y.decision_name===M&&y.phase_number==null){y.status==="done"&&i.push(y);continue}l.has(y.decision_name)||l.set(y.decision_name,[]),l.get(y.decision_name).push(y)}let u=[...l.keys()].sort((y,g)=>y===M?-1:g===M?1:y<g?-1:y>g?1:0),d=new Map,p=!1;for(let y of u){let g=l.get(y).filter(E=>E.status!=="stale"),_=y===M?g.filter(E=>E.status!=="done"):g;_.length!==0&&(p=!0,d.set(y,{visible:_,doneCount:g.filter(E=>E.status==="done").length,total:g.length}))}if(!p)return null;let f=y=>{let g=s.get(y);return g?[...g].filter(_=>o.get(_)!=="done").map(_=>r.get(_)||`#${_}`):[]},m=y=>{let g=s.get(y);return g?[...g].filter(_=>a.has(_)).sort((_,E)=>_-E).map(_=>`phase-${_}`):[]},S=y=>(c.get(y)||[]).filter(g=>g.status!=="stale"),b=y=>c.get(y)||[],x=new Map(u.map(y=>[y,l.get(y)])),k=new Map;for(let y of ge()||[])y.id!=null&&k.set(y.id,{shipped:y.date,commit:y.commit||null});return{cursor:n,decisionNames:u,visibleRoots:d,children:S,unfinishedBlockers:f,blockerIds:m,ancestry:y=>{let g=[],_=new Set,E=y;for(;E!=null&&a.has(E)&&!_.has(E);)_.add(E),g.unshift(E),E=a.get(E).parent_phase_id;return g},allChildren:b,jsonRoots:x,archivedRoots:i,shipped:y=>k.get(y)||null}}var De=120;function es(e){return e.replace(/\s*\((?:see\s+)?\[\[[^\]]+\]\](?:\s*(?:,|and)\s*\[\[[^\]]+\]\])*\s*\)/gi,"").replace(/\[\[([^\]]+)\]\]/g,(t,n)=>n.replace(/_/g," ").trim()).replace(/\s{2,}/g," ").replace(/\s+([.,;:!?])/g,"$1").trim()}function yt(e){if(!e)return null;let t=[];for(let a of e.split(`
71
+ `)){let c=a.trim();if(!c){if(t.length)break;continue}t.push(c)}if(!t.length)return null;let n=es(t.join(" ").replace(/\s+/g," ")),s=null,o=/[.?!](?=\s|$)/g,r;for(;(r=o.exec(n))!==null&&(s=n.slice(0,r.index+1),!(s.length>=40)););if(s||(s=n),s.length>De){let a=s.lastIndexOf(" ",De-1);s=s.slice(0,a>60?a:De-1).trimEnd()+"\u2026"}return s}var ts=8;function lt(){let e=ge();if(!e||e.length===0)return[];let t=e.slice(-ts).reverse(),n=["","---","","## Recently shipped",""];for(let o of t)n.push(`- ${o.date} \u2014 ${o.title}${o.commit?` \`${o.commit}\``:""}`);let s=e.length-t.length;return n.push("",s>0?`_${e.length} phases shipped in total; ${s} earlier one${s===1?"":"s"} not listed._`:`_${e.length} phase${e.length===1?"":"s"} shipped in total._`),n}function ut(e){let t=e.doneCount,n=e.total;if(!n)return"_No phases yet._";let s=Math.round(t/n*10);return`**${t} of ${n} done** \`${"\u2588".repeat(s)}${"\u2591".repeat(10-s)}\``}function ns(e,t){let n=[Nn(t.generated),"<!--"," Generated by cairn \u2014 do NOT edit by hand."," The plan lives in .cairn/roadmap.md; status and cursor come from cairn_roadmap."," This file is regenerated whenever the roadmap changes; manual edits are overwritten."," Per-phase detail and links are in the roadmap-json block at the end of this file."];if(t.migratedFrom&&(n.push(" The ROADMAP.md that was here before cairn adopted this file is kept at:"),n.push(` migrated-from=${t.migratedFrom}`)),n.push("-->","","# Roadmap",""),e==null)return n.push("_No active plan right now._"),n.push(...lt()),n;let{cursor:s,decisionNames:o,visibleRoots:r,children:a,unfinishedBlockers:c}=e;n.push("Legend: `[x]` done \xB7 `[~]` in progress \xB7 `[ ]` open \xB7 `[!]` blocked.");let l=(i,u)=>{let d=" ".repeat(u),p=i.phase_number!=null?`Phase ${i.phase_number}: ${i.text}`:i.text,f=zn[i.status]||i.status,m=c(i.id);m.length&&(f+=` \xB7 waiting on ${m.join(", ")}`),s===i.id&&(f+=" \xB7 \u2190 current"),n.push(`${d}- [${qn(i.status)}] **${p}** \u2014 ${f}`);let S=yt(i.body);S&&n.push(`${d} ${S}`);for(let b of a(i.id))l(b,u+1)};for(let i of o){let u=r.get(i);if(u){n.push(""),i===M?n.push("## Active plan","",ut(u)):n.push(`## ${i}`,"",ut(u)),n.push("");for(let d of u.visible)l(d,0)}}return n.push(...lt()),n}function gt(e,t,n,s){let o=n.get(`phase-${e.id}`),r=yt(e.body),a=Gn(e.body),c=t.blockerIds(e.id),l={id:`phase-${e.id}`};e.phase_number!=null&&(l.number=e.phase_number),l.title=e.text,l.status=Kn.has(e.status)?e.status:"open";let i=e.status==="done"?t.shipped(e.id):null;if(i&&(l.shipped=i.shipped,i.commit&&(l.commit=i.commit)),r&&(l.goal=r),c.length&&(l.blocked_by=c),!s){let u=[],d=p=>{for(let f of t.allChildren(p))u.push(gt(f,t,n,!0)),d(f.id)};d(e.id),u.length&&(l.slices=u)}return e.body&&e.body.trim()&&(l.detail=e.body.trim()),a.length&&(l.links=a),{...l,...pt(o,Wn)}}function ss(e,t){let n=ft(t.previous?.phases),s=[];for(let u of e?.decisionNames??[]){let d=e.jsonRoots.get(u);if(d)for(let p of d)s.push(gt(p,e,n,!1))}let o=(e?.archivedRoots??[]).map(u=>{let d=e.shipped(u.id),p={id:`phase-${u.id}`,title:u.text,status:"done"};return d&&(p.shipped=d.shipped,d.commit&&(p.commit=d.commit)),p}),r=new Set(s.map(u=>u.id)),a=e?.cursor!=null?e.ancestry(e.cursor):[],c=a.length?`phase-${a[0]}`:null,l={format:ne,provider:_e,generated:t.generated,title:`${A.basename(X())} roadmap`,cursor:e?.cursor!=null?`phase-${e.cursor}`:null,cursor_phase:c!==null&&r.has(c)?c:null,cursor_path:a.length?a.map(u=>`phase-${u}`):null};t.migratedFrom&&(l.migrated_from=t.migratedFrom,t.migrated&&(l.migrated=t.migrated)),l.phases=s,l.shipped_phases=o;let i={...l,...pt(t.previous,In)};return["","<details>","<summary>Machine-readable plan (roadmap/1)</summary>","","```roadmap-json",JSON.stringify(i,null,2),"```","","</details>"]}function Ce(e,t={}){let n={generated:t.generated||ht(),migratedFrom:t.migratedFrom||null,migrated:t.migrated||null,previous:t.previous&&typeof t.previous=="object"?t.previous:null},s=Qn(e);return s==null&&!t.allowEmpty?null:[...ns(s,n),...ss(s,n)].join(`
70
72
  `)+`
71
- `}function ss(e){let t=A.join(e,"ROADMAP.bak.md");if(!Y.existsSync(t))return t;for(let n=2;n<=1e3;n++){let s=A.join(e,`ROADMAP.bak.${n}.md`);if(!Y.existsSync(s))return s}return null}function os(e,t){let n=A.join(A.dirname(e),`.${A.basename(e)}.${process.pid}.tmp`);try{Y.writeFileSync(n,t,"utf8"),Y.renameSync(n,e)}catch(s){try{Y.unlinkSync(n)}catch{}throw s}}function te(e,t,n={}){return{written:!1,reason:e,warning:`[cairn] ROADMAP.md was not updated \u2014 ${e}`,path:t,...n}}function gt(e){let t=ft(),n=null;if(Y.existsSync(t))try{n=Y.readFileSync(t,"utf8")}catch(S){return te(`the existing file could not be read (${S.message})`,t)}let s=Ln(n),o=n!=null&&(s?.provider===_e||n.startsWith(Nn));if(n!=null&&s&&!o)return{written:!1,reason:`ROADMAP.md is generated by another provider (${s.provider}) \u2014 refusing to overwrite`,conflict_provider:s.provider,warning:`[cairn] ROADMAP.md is published by "${s.provider}", so cairn left it untouched and the file may not reflect cairn's plan. Two roadmap providers are claiming the same file \u2014 decide which one owns it.`,path:t};let r=o?ut(n):null,a=o?Wn(n):null,c=o&&typeof r?.migrated=="string"?r.migrated:null,l=typeof r?.generated=="string"?r.generated:null,i=!1,u=Ce(e,{generated:l||"1970-01-01T00:00:00Z",migratedFrom:a,migrated:c,previous:r});if(u==null){if(!o)return{written:!1,reason:"empty roadmap \u2014 no file written",path:t};i=!0,u=Ce(e,{generated:l||"1970-01-01T00:00:00Z",migratedFrom:a,migrated:c,previous:r,allowEmpty:!0})}if(o&&n===u)return{written:!1,unchanged:!0,path:t,bytes:Buffer.byteLength(u)};let d=mt(),p=null,f=null;if(n!=null&&!o){let S=ss(A.dirname(t));if(S==null)return te("no free ROADMAP.bak name was available to preserve the existing file",t);try{Y.renameSync(t,S)}catch(b){return te(`the existing file could not be preserved (${b.message})`,t)}p=A.basename(S),f=S,a=p,c=d}u=Ce(e,{generated:d,migratedFrom:a,migrated:c,previous:r,allowEmpty:i});try{os(t,u)}catch(S){if(f)try{return Y.renameSync(f,t),te(`write failed (${S.message}); the original ROADMAP.md was restored`,t)}catch{return te(`write failed (${S.message}); the original ROADMAP.md is at ${p} and must be restored by hand`,t,{backed_up_to:p})}return te(`write failed (${S.message})`,t)}let m={written:!0,path:t,bytes:Buffer.byteLength(u),format:ne,generated:d};return p&&(m.backed_up_to=p,m.warning=`[cairn] The existing ROADMAP.md was not cairn-generated. It has been preserved as ${p}, and ROADMAP.md is now generated from cairn's roadmap.`),m}function rs(e){try{return gt(e)}catch(t){process.stderr.write(`[cairn] ROADMAP.md publish skipped: ${t.message}
72
- `);let n=null;try{n=ft()}catch{}return te(`publish failed (${t.message})`,n)}}function Se(e){let t=rs(e);return t&&t.warning?t.warning:null}var is=new Set(["open","in_progress"]);function as(e,t){e.prepare("INSERT INTO main.roadmap_cursor (singleton, phase_id) VALUES (1, ?) ON CONFLICT(singleton) DO UPDATE SET phase_id = excluded.phase_id").run(t)}function cs(e,t){let n=e.prepare("SELECT parent_phase_id, status FROM main.roadmap_phases WHERE id = ?"),s=e.prepare("UPDATE main.roadmap_phases SET status = 'in_progress' WHERE id = ? AND status = 'open'"),o=n.get(t);for(;o&&o.parent_phase_id!=null;)s.run(o.parent_phase_id),o=n.get(o.parent_phase_id)}function _t(e,t){as(e,t),e.prepare("UPDATE main.roadmap_phases SET status = 'in_progress' WHERE id = ? AND status = 'open'").run(t),cs(e,t)}function ls(e){let t=new Map,n;try{n=e.prepare("SELECT blocker_id, blocked_id FROM main.roadmap_phase_deps").all()}catch{return t}for(let s of n)t.has(s.blocked_id)||t.set(s.blocked_id,new Set),t.get(s.blocked_id).add(s.blocker_id);return t}function us(e,t,n){let s=t.get(e);if(!s||s.size===0)return[];let o=[];for(let r of s)n.get(r)!=="done"&&o.push(r);return o}function St(e,t){let n=e.prepare("SELECT id, decision_name, phase_number, parent_phase_id, status, position, text FROM main.roadmap_phases ORDER BY decision_name, phase_number, position, id").all();if(n.length===0)return{id:null,reason:"No phases \u2014 add a ## Phases section to .cairn/roadmap.md."};let s=new Map(n.map(d=>[d.id,d.status])),o=ls(e),r=new Map;for(let d of n){let p=d.parent_phase_id??`root:${d.decision_name}`;r.has(p)||r.set(p,[]),r.get(p).push(d)}for(let d of r.values())d.sort((p,f)=>p.phase_number!=null&&f.phase_number!=null?p.phase_number-f.phase_number:p.position-f.position||p.id-f.id);let a=[],c=new Set;for(let d of n){if(d.parent_phase_id!=null||c.has(d.decision_name))continue;c.add(d.decision_name);let p=[],f=r.get(`root:${d.decision_name}`)||[];for(let m=f.length-1;m>=0;m--)p.push(f[m]);for(;p.length;){let m=p.pop();a.push(m);let S=r.get(m.id)||[];for(let b=S.length-1;b>=0;b--)p.push(S[b])}}let l=d=>is.has(d.status),i=[],u=0;if(t!=null){let d=a.findIndex(p=>p.id===t);u=d===-1?0:d+1}for(let d=u;d<a.length;d++){let p=a[d];if(!l(p))continue;let f=us(p.id,o,s);if(f.length>0){i.push({id:p.id,blockers:f});continue}let m=t!=null?n.find(x=>x.id===t):null,S=m&&m.decision_name!==p.decision_name,b;return t==null?b=`First actionable phase: ${p.decision_name??"(sub-task)"} / Phase ${p.phase_number??"\xB7"}.`:S?b=`Previous decision "${m.decision_name}" is complete \u2014 advanced to "${p.decision_name}".`:b=`Next open phase in "${p.decision_name??"(sub-task)"}" after id ${t}.`,i.length>0&&(b+=` Skipped ${i.length} dep-blocked phase(s): ${i.map(x=>`id ${x.id} (blocked by ${x.blockers.join(",")})`).join("; ")}.`),{id:p.id,reason:b,skipped_blocked:i}}return i.length>0?{id:null,reason:`All remaining open phases are blocked by unfinished dependencies: ${i.map(d=>`id ${d.id} (blocked by ${d.blockers.join(",")})`).join("; ")}.`,skipped_blocked:i}:{id:null,reason:"No actionable phases remaining."}}var ps=new Set(["java","typescript","javascript","vue","python","sql"]);function pe(e,t){let n={hookEventName:"PreToolUse",permissionDecision:"deny",permissionDecisionReason:e};t&&(n.additionalContext=t),process.stdout.write(JSON.stringify({decision:"block",reason:e,hookSpecificOutput:n})),process.exit(0)}var bt={name:"cairn",contract:1,title:"cairn",slots:["roadmap"],provides:{roadmap:{path:"ROADMAP.md",format:ne}},spec:"Project memory and roadmap. Publishes the plan as ROADMAP.md in the roadmap/1 slot format."};function fs(){let e=w.join(se.homedir(),".claude.json");if(!h.existsSync(e))return!1;let t;try{t=JSON.parse(h.readFileSync(e,"utf8"))}catch{return!1}let n=t?.mcpServers?.cairn;if(!n||typeof n!="object")return!1;let s=n.terminalConnect,o=s?.provides?.roadmap?.format;if(s&&o===ne)return!1;n.terminalConnect=bt;try{return h.writeFileSync(e,JSON.stringify(t,null,2),"utf8"),!0}catch{return!1}}function Et(e){let t=w.join(se.homedir(),".claude","projects",xe(e));return h.existsSync(t)?w.join(t,"memory"):null}function $t(e){try{return h.readdirSync(e).filter(t=>t.endsWith(".md"))}catch{return[]}}function ms(e){let t=Et(e);if(!t||!h.existsSync(t))return 0;let n=w.join(e,".cairn","memory");h.mkdirSync(n,{recursive:!0});let s=0;for(let o of $t(t)){let r=w.join(t,o),a;try{a=h.statSync(r)}catch{continue}if(!a.isFile())continue;let c=w.join(n,o),l=!h.existsSync(c);if(!l)try{l=a.mtimeMs>h.statSync(c).mtimeMs}catch{l=!0}if(l){h.copyFileSync(r,c);try{h.utimesSync(c,a.atime,a.mtime)}catch{}s++}}return s}function hs(e){let t=Et(e);if(!t||(h.mkdirSync(t,{recursive:!0}),$t(t).some(o=>o!=="MEMORY.md")))return 0;let n=w.join(e,".cairn","memory");if(!h.existsSync(n))return 0;let s=0;for(let o of h.readdirSync(n)){let r=w.join(n,o),a;try{a=h.statSync(r)}catch{continue}if(a.isFile()){h.copyFileSync(r,w.join(t,o));try{h.utimesSync(w.join(t,o),a.atime,a.mtime)}catch{}s++}}return s}function wt(e,t){let n=new Date(t.checkpoint_at).toLocaleString(),r=[` message: ${/^Auto-checkpoint\b/.test(t.message||"")&&t.last_manual_message||t.message}`];r.push(` would_lose: ${ve(ue(e,t))}`),r.push(` checkpoint: ${n}`),t.active_files?.length>0&&r.push(` active_files: ${t.active_files.join(", ")}`),t.notes?.length>0&&r.push(` notes: ${t.notes.join(" | ")}`);let a=i=>{try{return ds(i,{cwd:e,encoding:"utf8",timeout:5e3,stdio:["ignore","pipe","ignore"]}).trim()}catch{return null}};try{let i=new Date(t.checkpoint_at).toISOString(),u=a(`git log --oneline --after="${i}"`);u!==null&&r.push(` commits_since: ${u?u.split(`
73
+ `}function os(e){let t=A.join(e,"ROADMAP.bak.md");if(!Y.existsSync(t))return t;for(let n=2;n<=1e3;n++){let s=A.join(e,`ROADMAP.bak.${n}.md`);if(!Y.existsSync(s))return s}return null}function rs(e,t){let n=A.join(A.dirname(e),`.${A.basename(e)}.${process.pid}.tmp`);try{Y.writeFileSync(n,t,"utf8"),Y.renameSync(n,e)}catch(s){try{Y.unlinkSync(n)}catch{}throw s}}function te(e,t,n={}){return{written:!1,reason:e,warning:`[cairn] ROADMAP.md was not updated \u2014 ${e}`,path:t,...n}}function _t(e){let t=mt(),n=null;if(Y.existsSync(t))try{n=Y.readFileSync(t,"utf8")}catch(S){return te(`the existing file could not be read (${S.message})`,t)}let s=Dn(n),o=n!=null&&(s?.provider===_e||n.startsWith(Pn));if(n!=null&&s&&!o)return{written:!1,reason:`ROADMAP.md is generated by another provider (${s.provider}) \u2014 refusing to overwrite`,conflict_provider:s.provider,warning:`[cairn] ROADMAP.md is published by "${s.provider}", so cairn left it untouched and the file may not reflect cairn's plan. Two roadmap providers are claiming the same file \u2014 decide which one owns it.`,path:t};let r=o?dt(n):null,a=o?Un(n):null,c=o&&typeof r?.migrated=="string"?r.migrated:null,l=typeof r?.generated=="string"?r.generated:null,i=!1,u=Ce(e,{generated:l||"1970-01-01T00:00:00Z",migratedFrom:a,migrated:c,previous:r});if(u==null){if(!o)return{written:!1,reason:"empty roadmap \u2014 no file written",path:t};i=!0,u=Ce(e,{generated:l||"1970-01-01T00:00:00Z",migratedFrom:a,migrated:c,previous:r,allowEmpty:!0})}if(o&&n===u)return{written:!1,unchanged:!0,path:t,bytes:Buffer.byteLength(u)};let d=ht(),p=null,f=null;if(n!=null&&!o){let S=os(A.dirname(t));if(S==null)return te("no free ROADMAP.bak name was available to preserve the existing file",t);try{Y.renameSync(t,S)}catch(b){return te(`the existing file could not be preserved (${b.message})`,t)}p=A.basename(S),f=S,a=p,c=d}u=Ce(e,{generated:d,migratedFrom:a,migrated:c,previous:r,allowEmpty:i});try{rs(t,u)}catch(S){if(f)try{return Y.renameSync(f,t),te(`write failed (${S.message}); the original ROADMAP.md was restored`,t)}catch{return te(`write failed (${S.message}); the original ROADMAP.md is at ${p} and must be restored by hand`,t,{backed_up_to:p})}return te(`write failed (${S.message})`,t)}let m={written:!0,path:t,bytes:Buffer.byteLength(u),format:ne,generated:d};return p&&(m.backed_up_to=p,m.warning=`[cairn] The existing ROADMAP.md was not cairn-generated. It has been preserved as ${p}, and ROADMAP.md is now generated from cairn's roadmap.`),m}function is(e){try{return _t(e)}catch(t){process.stderr.write(`[cairn] ROADMAP.md publish skipped: ${t.message}
74
+ `);let n=null;try{n=mt()}catch{}return te(`publish failed (${t.message})`,n)}}function Se(e){let t=is(e);return t&&t.warning?t.warning:null}var as=new Set(["open","in_progress"]);function cs(e,t){e.prepare("INSERT INTO main.roadmap_cursor (singleton, phase_id) VALUES (1, ?) ON CONFLICT(singleton) DO UPDATE SET phase_id = excluded.phase_id").run(t)}function ls(e,t){let n=e.prepare("SELECT parent_phase_id, status FROM main.roadmap_phases WHERE id = ?"),s=e.prepare("UPDATE main.roadmap_phases SET status = 'in_progress' WHERE id = ? AND status = 'open'"),o=n.get(t);for(;o&&o.parent_phase_id!=null;)s.run(o.parent_phase_id),o=n.get(o.parent_phase_id)}function St(e,t){cs(e,t),e.prepare("UPDATE main.roadmap_phases SET status = 'in_progress' WHERE id = ? AND status = 'open'").run(t),ls(e,t)}function us(e){let t=new Map,n;try{n=e.prepare("SELECT blocker_id, blocked_id FROM main.roadmap_phase_deps").all()}catch{return t}for(let s of n)t.has(s.blocked_id)||t.set(s.blocked_id,new Set),t.get(s.blocked_id).add(s.blocker_id);return t}function ds(e,t,n){let s=t.get(e);if(!s||s.size===0)return[];let o=[];for(let r of s)n.get(r)!=="done"&&o.push(r);return o}function wt(e,t){let n=e.prepare("SELECT id, decision_name, phase_number, parent_phase_id, status, position, text FROM main.roadmap_phases ORDER BY decision_name, phase_number, position, id").all();if(n.length===0)return{id:null,reason:"No phases \u2014 add a ## Phases section to .cairn/roadmap.md."};let s=new Map(n.map(d=>[d.id,d.status])),o=us(e),r=new Map;for(let d of n){let p=d.parent_phase_id??`root:${d.decision_name}`;r.has(p)||r.set(p,[]),r.get(p).push(d)}for(let d of r.values())d.sort((p,f)=>p.phase_number!=null&&f.phase_number!=null?p.phase_number-f.phase_number:p.position-f.position||p.id-f.id);let a=[],c=new Set;for(let d of n){if(d.parent_phase_id!=null||c.has(d.decision_name))continue;c.add(d.decision_name);let p=[],f=r.get(`root:${d.decision_name}`)||[];for(let m=f.length-1;m>=0;m--)p.push(f[m]);for(;p.length;){let m=p.pop();a.push(m);let S=r.get(m.id)||[];for(let b=S.length-1;b>=0;b--)p.push(S[b])}}let l=d=>as.has(d.status),i=[],u=0;if(t!=null){let d=a.findIndex(p=>p.id===t);u=d===-1?0:d+1}for(let d=u;d<a.length;d++){let p=a[d];if(!l(p))continue;let f=ds(p.id,o,s);if(f.length>0){i.push({id:p.id,blockers:f});continue}let m=t!=null?n.find(x=>x.id===t):null,S=m&&m.decision_name!==p.decision_name,b;return t==null?b=`First actionable phase: ${p.decision_name??"(sub-task)"} / Phase ${p.phase_number??"\xB7"}.`:S?b=`Previous decision "${m.decision_name}" is complete \u2014 advanced to "${p.decision_name}".`:b=`Next open phase in "${p.decision_name??"(sub-task)"}" after id ${t}.`,i.length>0&&(b+=` Skipped ${i.length} dep-blocked phase(s): ${i.map(x=>`id ${x.id} (blocked by ${x.blockers.join(",")})`).join("; ")}.`),{id:p.id,reason:b,skipped_blocked:i}}return i.length>0?{id:null,reason:`All remaining open phases are blocked by unfinished dependencies: ${i.map(d=>`id ${d.id} (blocked by ${d.blockers.join(",")})`).join("; ")}.`,skipped_blocked:i}:{id:null,reason:"No actionable phases remaining."}}var fs=new Set(["java","typescript","javascript","vue","python","sql"]);function pe(e,t){let n={hookEventName:"PreToolUse",permissionDecision:"deny",permissionDecisionReason:e};t&&(n.additionalContext=t),process.stdout.write(JSON.stringify({decision:"block",reason:e,hookSpecificOutput:n})),process.exit(0)}var Et={name:"cairn",contract:1,title:"cairn",slots:["roadmap"],provides:{roadmap:{path:"ROADMAP.md",format:ne}},spec:"Project memory and roadmap. Publishes the plan as ROADMAP.md in the roadmap/1 slot format."};function ms(){let e=w.join(se.homedir(),".claude.json");if(!h.existsSync(e))return!1;let t;try{t=JSON.parse(h.readFileSync(e,"utf8"))}catch{return!1}let n=t?.mcpServers?.cairn;if(!n||typeof n!="object")return!1;let s=n.terminalConnect,o=s?.provides?.roadmap?.format;if(s&&o===ne)return!1;n.terminalConnect=Et;try{return h.writeFileSync(e,JSON.stringify(t,null,2),"utf8"),!0}catch{return!1}}function $t(e){let t=w.join(se.homedir(),".claude","projects",xe(e));return h.existsSync(t)?w.join(t,"memory"):null}function vt(e){try{return h.readdirSync(e).filter(t=>t.endsWith(".md"))}catch{return[]}}function hs(e){let t=$t(e);if(!t||!h.existsSync(t))return 0;let n=w.join(e,".cairn","memory");h.mkdirSync(n,{recursive:!0});let s=0;for(let o of vt(t)){let r=w.join(t,o),a;try{a=h.statSync(r)}catch{continue}if(!a.isFile())continue;let c=w.join(n,o),l=!h.existsSync(c);if(!l)try{l=a.mtimeMs>h.statSync(c).mtimeMs}catch{l=!0}if(l){h.copyFileSync(r,c);try{h.utimesSync(c,a.atime,a.mtime)}catch{}s++}}return s}function ys(e){let t=$t(e);if(!t||(h.mkdirSync(t,{recursive:!0}),vt(t).some(o=>o!=="MEMORY.md")))return 0;let n=w.join(e,".cairn","memory");if(!h.existsSync(n))return 0;let s=0;for(let o of h.readdirSync(n)){let r=w.join(n,o),a;try{a=h.statSync(r)}catch{continue}if(a.isFile()){h.copyFileSync(r,w.join(t,o));try{h.utimesSync(w.join(t,o),a.atime,a.mtime)}catch{}s++}}return s}function gs(e){return e&&typeof e=="object"&&e.ref?(e.description||"filed note")+" [see "+e.ref+"]":String(e)}function xt(e,t){let n=new Date(t.checkpoint_at).toLocaleString(),r=[` message: ${/^Auto-checkpoint\b/.test(t.message||"")&&t.last_manual_message||t.message}`];r.push(` would_lose: ${ve(ue(e,t),null)}`),r.push(` checkpoint: ${n}`),t.active_files?.length>0&&r.push(` active_files: ${t.active_files.join(", ")}`),t.notes?.length>0&&r.push(` notes: ${t.notes.map(gs).join(" | ")}`);let a=i=>{try{return ps(i,{cwd:e,encoding:"utf8",timeout:5e3,stdio:["ignore","pipe","ignore"]}).trim()}catch{return null}};try{let i=new Date(t.checkpoint_at).toISOString(),u=a(`git log --oneline --after="${i}"`);u!==null&&r.push(` commits_since: ${u?u.split(`
73
75
  `).join("; "):"(none)"}`)}catch{}let c=a("git diff --name-only HEAD");c&&r.push(` uncommitted: ${c.split(`
74
76
  `).join(", ")}`);let l=a("git status --short");return l&&r.push(` git_status:
75
77
  ${l.split(`
76
78
  `).map(i=>" "+i).join(`
77
- `)}`),r.push(" \u2192 Call cairn_resume for full index status and open todos."),r}async function xt(e){let t={active:!1},n=w.join(e,"index.db");if(!h.existsSync(n))return t;let s;try{({DatabaseSync:s}=await import("node:sqlite"))}catch{return t}let o;try{o=new s(n,{readonly:!0})}catch{return t}let r=c=>c.toLowerCase().replace(/[^a-z0-9]+/g,"_").replace(/^_|_$/g,"");function a(c,l){if(!h.existsSync(c))return null;let i=h.readFileSync(c,"utf8"),u=!1,d=!1,p=[];for(let f of i.split(/\r?\n/)){if(/^##\s+Phases\s*$/i.test(f)){u=!0;continue}if(u&&/^##\s+/.test(f)&&!/^###/.test(f))break;if(!u)continue;let m=/^###\s+Phase\s+(\d+)\s*:\s*(.+?)\s*$/.exec(f);if(m){if(d)break;parseInt(m[1],10)===l&&(p.push(`### Phase ${m[1]}: ${m[2]}`),d=!0);continue}d&&p.push(f)}return p.join(`
78
- `).trim()||null}try{let l=o.prepare("SELECT phase_id FROM roadmap_cursor WHERE singleton = 1").get()?.phase_id??null;if(l==null)return o.close(),t;let i=o.prepare("SELECT id, decision_name, phase_number, parent_phase_id, text, status, position FROM roadmap_phases ORDER BY decision_name, phase_number, position, id").all(),u={open:"[ ]",in_progress:"[~]",done:"[x]",blocked:"[!]",stale:"[s]"},d=new Map;for(let O of i){let R=O.parent_phase_id??"root";d.has(R)||d.set(R,[]),d.get(R).push(O)}for(let O of d.values())O.sort((R,T)=>R.decision_name&&T.decision_name&&R.decision_name!==T.decision_name?R.decision_name.localeCompare(T.decision_name):R.phase_number!=null&&T.phase_number!=null?R.phase_number-T.phase_number:R.position-T.position||R.id-T.id);let p=d.get("root")||[],f=new Map;for(let O of p)O.decision_name!=null&&(f.has(O.decision_name)||f.set(O.decision_name,[]),f.get(O.decision_name).push(O));let m=[],S=(O,R)=>{for(let T of d.get(O)||[]){if(T.status==="stale")continue;let le=l===T.id?" \u2192":"",V=T.phase_number!=null?`Phase ${T.phase_number}: ${T.text}`:T.text;m.push(`${" ".repeat(R)}${u[T.status]||"[?]"}${le} ${V}`),S(T.id,R+1)}};for(let[O,R]of f){let T=R.filter(P=>P.status!=="stale");if(T.length===0)continue;let le=T.every(P=>P.status==="done"),V=T.some(P=>P.status==="in_progress"),vt=le?"done":V?"in_progress":"open",Rt=T.filter(P=>P.status==="done").length,Ot=O===M?"Roadmap":`Decision: ${O}`;m.push(`${u[vt]} ${Ot} (${Rt}/${T.length} done)`);for(let P of R){if(P.status==="stale"||O===M&&P.status==="done")continue;let Tt=l===P.id?" \u2192":"";m.push(` ${u[P.status]||"[?]"}${Tt} Phase ${P.phase_number}: ${P.text}`),S(P.id,2)}}let b=O=>i.find(R=>R.id===O),x=b(l),k=x?.decision_name??null,$=x?.phase_number??null,v=x;for(;v&&v.parent_phase_id!=null&&(v=b(v.parent_phase_id),!!v);)k=v.decision_name??k,$=v.phase_number??$;o.close();let y=x?x.phase_number!=null?`${x.decision_name} / Phase ${x.phase_number}: ${x.text}`:`${k} / Phase ${$} \u2192 ${x.text}`:"(unknown)",g=w.join(e,".last-roadmap-cursor"),_=null;try{_=parseInt(h.readFileSync(g,"utf8").trim(),10)}catch{}let E=_!==l;try{h.writeFileSync(g,String(l),"utf8")}catch{}let ce=null;if(E&&k!=null&&$!=null){let O=k===M?w.join(e,"roadmap.md"):w.join(e,"memory",`decision_${r(k)}.md`),R=a(O,$);R&&(ce=R.split(`
79
+ `)}`),r.push(" \u2192 Call cairn_resume for full index status and open todos."),r}async function kt(e){let t={active:!1},n=w.join(e,"index.db");if(!h.existsSync(n))return t;let s;try{({DatabaseSync:s}=await import("node:sqlite"))}catch{return t}let o;try{o=new s(n,{readonly:!0})}catch{return t}let r=c=>c.toLowerCase().replace(/[^a-z0-9]+/g,"_").replace(/^_|_$/g,"");function a(c,l){if(!h.existsSync(c))return null;let i=h.readFileSync(c,"utf8"),u=!1,d=!1,p=[];for(let f of i.split(/\r?\n/)){if(/^##\s+Phases\s*$/i.test(f)){u=!0;continue}if(u&&/^##\s+/.test(f)&&!/^###/.test(f))break;if(!u)continue;let m=/^###\s+Phase\s+(\d+)\s*:\s*(.+?)\s*$/.exec(f);if(m){if(d)break;parseInt(m[1],10)===l&&(p.push(`### Phase ${m[1]}: ${m[2]}`),d=!0);continue}d&&p.push(f)}return p.join(`
80
+ `).trim()||null}try{let l=o.prepare("SELECT phase_id FROM roadmap_cursor WHERE singleton = 1").get()?.phase_id??null;if(l==null)return o.close(),t;let i=o.prepare("SELECT id, decision_name, phase_number, parent_phase_id, text, status, position FROM roadmap_phases ORDER BY decision_name, phase_number, position, id").all(),u={open:"[ ]",in_progress:"[~]",done:"[x]",blocked:"[!]",stale:"[s]"},d=new Map;for(let O of i){let R=O.parent_phase_id??"root";d.has(R)||d.set(R,[]),d.get(R).push(O)}for(let O of d.values())O.sort((R,T)=>R.decision_name&&T.decision_name&&R.decision_name!==T.decision_name?R.decision_name.localeCompare(T.decision_name):R.phase_number!=null&&T.phase_number!=null?R.phase_number-T.phase_number:R.position-T.position||R.id-T.id);let p=d.get("root")||[],f=new Map;for(let O of p)O.decision_name!=null&&(f.has(O.decision_name)||f.set(O.decision_name,[]),f.get(O.decision_name).push(O));let m=[],S=(O,R)=>{for(let T of d.get(O)||[]){if(T.status==="stale")continue;let le=l===T.id?" \u2192":"",V=T.phase_number!=null?`Phase ${T.phase_number}: ${T.text}`:T.text;m.push(`${" ".repeat(R)}${u[T.status]||"[?]"}${le} ${V}`),S(T.id,R+1)}};for(let[O,R]of f){let T=R.filter(P=>P.status!=="stale");if(T.length===0)continue;let le=T.every(P=>P.status==="done"),V=T.some(P=>P.status==="in_progress"),Rt=le?"done":V?"in_progress":"open",Ot=T.filter(P=>P.status==="done").length,Tt=O===M?"Roadmap":`Decision: ${O}`;m.push(`${u[Rt]} ${Tt} (${Ot}/${T.length} done)`);for(let P of R){if(P.status==="stale"||O===M&&P.status==="done")continue;let jt=l===P.id?" \u2192":"";m.push(` ${u[P.status]||"[?]"}${jt} Phase ${P.phase_number}: ${P.text}`),S(P.id,2)}}let b=O=>i.find(R=>R.id===O),x=b(l),k=x?.decision_name??null,$=x?.phase_number??null,v=x;for(;v&&v.parent_phase_id!=null&&(v=b(v.parent_phase_id),!!v);)k=v.decision_name??k,$=v.phase_number??$;o.close();let y=x?x.phase_number!=null?`${x.decision_name} / Phase ${x.phase_number}: ${x.text}`:`${k} / Phase ${$} \u2192 ${x.text}`:"(unknown)",g=w.join(e,".last-roadmap-cursor"),_=null;try{_=parseInt(h.readFileSync(g,"utf8").trim(),10)}catch{}let E=_!==l;try{h.writeFileSync(g,String(l),"utf8")}catch{}let ce=null;if(E&&k!=null&&$!=null){let O=k===M?w.join(e,"roadmap.md"):w.join(e,"memory",`decision_${r(k)}.md`),R=a(O,$);R&&(ce=R.split(`
79
81
  `))}return{active:!0,currentLabel:y,treeLines:m,advanced:E,memoLines:ce}}catch{try{o?.close()}catch{}return t}}process.on("uncaughtException",e=>{process.stderr.write(`cairn: ${e.message}
80
- `),process.exit(0)});var F=process.argv[2];if(F==="--version"||F==="-v"){let e=JSON.parse(h.readFileSync(new URL("../package.json",import.meta.url),"utf8"));console.log(e.version),process.exit(0)}else if(F==="minify"){let e="";process.stdin.setEncoding("utf8"),process.stdin.on("data",t=>{e+=t}),process.stdin.on("end",()=>{let t,n;try{n=JSON.parse(e)?.tool_input||{},t=n.file_path}catch{process.exit(0)}t||process.exit(0),(n.offset!=null||n.limit!=null)&&process.exit(0),t=w.resolve(t);let s=Te(t);s||process.exit(0);let o=w.join(s,"views"),r=w.relative(o,t);!r.startsWith("..")&&!w.isAbsolute(r)&&process.exit(0);let a=Q(s),l=he(s,a)>0,i=a[t];i?.state==="edit-ready"&&(l&&W(s,a),process.exit(0)),i?.state==="compressed"&&(i.state="edit-ready",W(s,a),process.exit(0));let u=w.extname(t).toLowerCase(),d=Ie[u];(!d||!ps.has(d))&&(l&&W(s,a),process.exit(0));let p;try{p=h.readFileSync(t,"utf8")}catch{l&&W(s,a),process.exit(0)}let f=p.split(`
82
+ `),process.exit(0)});var F=process.argv[2];if(F==="--version"||F==="-v"){let e=JSON.parse(h.readFileSync(new URL("../package.json",import.meta.url),"utf8"));console.log(e.version),process.exit(0)}else if(F==="minify"){let e="";process.stdin.setEncoding("utf8"),process.stdin.on("data",t=>{e+=t}),process.stdin.on("end",()=>{let t,n;try{n=JSON.parse(e)?.tool_input||{},t=n.file_path}catch{process.exit(0)}t||process.exit(0),(n.offset!=null||n.limit!=null)&&process.exit(0),t=w.resolve(t);let s=Te(t);s||process.exit(0);let o=w.join(s,"views"),r=w.relative(o,t);!r.startsWith("..")&&!w.isAbsolute(r)&&process.exit(0);let a=Q(s),l=he(s,a)>0,i=a[t];i?.state==="edit-ready"&&(l&&W(s,a),process.exit(0)),i?.state==="compressed"&&(i.state="edit-ready",W(s,a),process.exit(0));let u=w.extname(t).toLowerCase(),d=Ie[u];(!d||!fs.has(d))&&(l&&W(s,a),process.exit(0));let p;try{p=h.readFileSync(t,"utf8")}catch{l&&W(s,a),process.exit(0)}let f=p.split(`
81
83
  `).length,m=d==="vue"?We(p):fe(p,d),S=m.split(`
82
- `).length,b=f>0?Math.round((1-S/f)*100):0,x=Ve(s,t);h.mkdirSync(w.dirname(x),{recursive:!0}),h.writeFileSync(x,m,"utf8");let k=i?.readCount??0;a[t]={tempPath:x,state:"compressed",minifiedAt:h.statSync(t).mtimeMs,readCount:k+1},W(s,a);let $=S>qe,v=k>0;if(v||$){let y=Xe(m,d),g=v?`[cairn: ${f} \u2192 ${S} lines | seen \xD7${k+1} this session]`:`[cairn: ${f} \u2192 outline (${S} minified lines)]`;pe(`[cairn] Expected step, not a failure: ${w.basename(t)} was served as an outline (${f} lines \u2192 ${S} minified). The outline is in context; Read("${x}") for the full minified view.`,`${g}
84
+ `).length,b=f>0?Math.round((1-S/f)*100):0,x=Xe(s,t);h.mkdirSync(w.dirname(x),{recursive:!0}),h.writeFileSync(x,m,"utf8");let k=i?.readCount??0;a[t]={tempPath:x,state:"compressed",minifiedAt:h.statSync(t).mtimeMs,readCount:k+1},W(s,a);let $=S>Ve,v=k>0;if(v||$){let y=Ze(m,d),g=v?`[cairn: ${f} \u2192 ${S} lines | seen \xD7${k+1} this session]`:`[cairn: ${f} \u2192 outline (${S} minified lines)]`;pe(`[cairn] Expected step, not a failure: ${w.basename(t)} was served as an outline (${f} lines \u2192 ${S} minified). The outline is in context; Read("${x}") for the full minified view.`,`${g}
83
85
  ${y}
84
86
  Full minified view: Read("${x}")`)}else pe(`[cairn] Expected step, not a failure: ${w.basename(t)} was served minified (${f} \u2192 ${S} lines, -${b}%). The content is in context; Read("${x}") for the cached view.`,`[cairn: ${f} \u2192 ${S} lines (-${b}%)]
85
87
  ${m}
86
88
  Cached view: Read("${x}")`)})}else if(F==="edit-guard"){let e="";process.stdin.setEncoding("utf8"),process.stdin.on("data",t=>{e+=t}),process.stdin.on("end",()=>{let t="(unknown)",n="Edit";try{let d=JSON.parse(e);t=d?.tool_input?.file_path||"(unknown)",n=d?.tool_name||"Edit"}catch{}let s=w.resolve(t),o=Te(s);o||process.exit(0);let r=w.join(o,"views"),a=w.relative(r,s);if(!a.startsWith("..")&&!w.isAbsolute(a)){let d=Q(o),p=Object.entries(d).find(([,m])=>m.tempPath===s),f=p?`Edit the source file instead: Edit("${p[0]}")`:"Edit the original source file, not the .cairn/views/ cache.";pe(`[cairn] That path is a cached view, not the source \u2014 it is read-only and is overwritten on the next read. ${f}`)}let c=Q(o),l=he(o,c),i=c[s];i||(l>0&&W(o,c),process.exit(0)),n==="Write"&&(delete c[s],W(o,c),process.exit(0)),i.state==="edit-ready"&&(delete c[s],W(o,c),process.exit(0)),i.state="edit-ready",W(o,c);let u=null;try{let d=h.readFileSync(s,"utf8");d.split(`
87
89
  `).length<=300&&(u=d)}catch{}u!==null?pe(`[cairn] Expected step, not a failure: this file was shown compressed, so the edit was built from partial text. The full source has been provided in context \u2014 re-apply the same edit, or Read("${t}") first if you do not see it.`,`[cairn] Full source of ${t} (the compressed view you saw is not editable text \u2014 use the exact content below, then re-apply your Edit; the guard is now cleared):
88
90
 
89
- ${u}`):pe(`[cairn] Expected step, not a failure: this file was shown compressed. Read("${t}") to load the real source (it comes through in full now), then re-apply the same edit.`)})}else if(F==="roadmap-capture"){let e="";process.stdin.setEncoding("utf8"),process.stdin.on("data",t=>{e+=t}),process.stdin.on("end",async()=>{let t;try{t=JSON.parse(e)}catch{process.exit(0)}let n=t?.tool_name||"",s=t?.tool_input||{},o=process.cwd(),r=w.join(o,".cairn");h.existsSync(r)||process.exit(0);let a=w.join(r,"index.db");h.existsSync(a)||process.exit(0);let c;try{({DatabaseSync:c}=await import("node:sqlite"))}catch{process.exit(0)}let l=/exit.?plan.?mode/i.test(n),i=/todo.?write/i.test(n);!l&&!i&&process.exit(0);let u=null,d;try{if(l){let f=s.plan;(typeof f!="string"||!f.trim())&&process.exit(0);let m=rt(f);if(d=new c(a),ie(d),(d.prepare("SELECT phase_id FROM main.roadmap_cursor WHERE singleton = 1").get()?.phase_id??null)==null){let b=St(d,null);b.id!=null&&_t(d,b.id)}m.added.length>0&&(u=`[cairn] roadmap: captured ${m.added.length} phase(s) from plan${m.skipped.length?` (${m.skipped.length} already present)`:""}.`)}else if(i){d=new c(a);let f=it(d,s.todos);f.seeded?u=`[cairn] roadmap: seeded ${f.seeded} phase(s) from todos.`:f.inserted>0&&(u=`[cairn] roadmap: linked ${f.inserted} new sub-task(s) to the current phase.`)}}catch(f){process.stderr.write(`[cairn] roadmap-capture skipped: ${f.message}
91
+ ${u}`):pe(`[cairn] Expected step, not a failure: this file was shown compressed. Read("${t}") to load the real source (it comes through in full now), then re-apply the same edit.`)})}else if(F==="roadmap-capture"){let e="";process.stdin.setEncoding("utf8"),process.stdin.on("data",t=>{e+=t}),process.stdin.on("end",async()=>{let t;try{t=JSON.parse(e)}catch{process.exit(0)}let n=t?.tool_name||"",s=t?.tool_input||{},o=process.cwd(),r=w.join(o,".cairn");h.existsSync(r)||process.exit(0);let a=w.join(r,"index.db");h.existsSync(a)||process.exit(0);let c;try{({DatabaseSync:c}=await import("node:sqlite"))}catch{process.exit(0)}let l=/exit.?plan.?mode/i.test(n),i=/todo.?write/i.test(n);!l&&!i&&process.exit(0);let u=null,d;try{if(l){let f=s.plan;(typeof f!="string"||!f.trim())&&process.exit(0);let m=it(f);if(d=new c(a),ie(d),(d.prepare("SELECT phase_id FROM main.roadmap_cursor WHERE singleton = 1").get()?.phase_id??null)==null){let b=wt(d,null);b.id!=null&&St(d,b.id)}m.added.length>0&&(u=`[cairn] roadmap: captured ${m.added.length} phase(s) from plan${m.skipped.length?` (${m.skipped.length} already present)`:""}.`)}else if(i){d=new c(a);let f=at(d,s.todos);f.seeded?u=`[cairn] roadmap: seeded ${f.seeded} phase(s) from todos.`:f.inserted>0&&(u=`[cairn] roadmap: linked ${f.inserted} new sub-task(s) to the current phase.`)}}catch(f){process.stderr.write(`[cairn] roadmap-capture skipped: ${f.message}
90
92
  `);try{d?.close()}catch{}process.exit(0)}let p=d?Se(d):null;try{d?.close()}catch{}p&&(u=u?`${u} ${p}`:p),u&&process.stdout.write(JSON.stringify({systemMessage:u})),process.exit(0)})}else if(F==="validate-map"){let e=w.join(process.cwd(),".cairn");h.existsSync(e)||process.exit(0);let t=Q(e),n=he(e,t);for(let s of Object.values(t))s.readCount=0;W(e,t),n>0&&process.stdout.write(`[cairn] Cleaned minify map: ${n} stale entr${n===1?"y":"ies"} removed
91
- `),process.exit(0)}else if(F==="checkpoint"&&process.argv[3]==="--auto"){let e=`Auto-checkpoint at ${new Date().toISOString()}`,t=[],n=[];try{let s=w.join(process.cwd(),".cairn"),o=w.join(s,"session.json");h.existsSync(o)&&(t=JSON.parse(h.readFileSync(o,"utf8")).notes||[]);let r=Q(s);n=Object.entries(r).filter(([,a])=>(a.readCount??0)>0).map(([a])=>a)}catch{}Oe(null,{message:e,active_files:n,notes:t,auto:!0}),process.exit(0)}else if(F==="session-tick"){let e=process.cwd(),t=w.join(e,".cairn");if(h.existsSync(t)||process.exit(0),!process.stdin.isTTY)try{h.readFileSync(0,"utf8")}catch{}let n=[];try{let c=`Auto-checkpoint at ${new Date().toISOString()}`,l=[],i=w.join(t,"session.json");if(h.existsSync(i))try{l=JSON.parse(h.readFileSync(i,"utf8")).notes||[]}catch{}let u=Q(t),d=Object.entries(u).filter(([,p])=>(p.readCount??0)>0).map(([p])=>p);Oe(null,{message:c,active_files:d,notes:l,auto:!0}),n.push("\u2713 checkpoint saved")}catch{}try{let c=ms(e);c>0&&n.push(`memory backed up (${c})`)}catch{}let s=[],o=null;try{let c=w.join(t,"index.db");if(h.existsSync(c)){let{DatabaseSync:l}=await import("node:sqlite"),i=new l(c);try{ie(i),o=Se(i);try{s=at(i,e).candidates}catch{}}finally{i.close()}}}catch{}let r="";try{let c=await xt(t);if(c.active){n.push(`roadmap ${c.advanced?"updated":"on track"}: ${c.currentLabel}`);let l=["[cairn] Production roadmap (\u2192 = cursor / current phase):",...c.treeLines.map(i=>" "+i)];c.memoLines&&l.push("","\u2500\u2500 Current phase detail (from roadmap.md) \u2500\u2500",...c.memoLines,"\u2500\u2500 (re-read after each phase to confirm direction) \u2500\u2500"),l.push("","Keep this current via cairn_roadmap on MATERIAL change (phase done / blocked / new) \u2014 not every turn."),r=l.join(`
93
+ `),process.exit(0)}else if(F==="checkpoint"&&process.argv[3]==="--auto"){let e=`Auto-checkpoint at ${new Date().toISOString()}`,t=[],n=[];try{let s=w.join(process.cwd(),".cairn"),o=w.join(s,"session.json");h.existsSync(o)&&(t=JSON.parse(h.readFileSync(o,"utf8")).notes||[]);let r=Q(s);n=Object.entries(r).filter(([,a])=>(a.readCount??0)>0).map(([a])=>a)}catch{}Oe(null,{message:e,active_files:n,notes:t,auto:!0}),process.exit(0)}else if(F==="session-tick"){let e=process.cwd(),t=w.join(e,".cairn");if(h.existsSync(t)||process.exit(0),!process.stdin.isTTY)try{h.readFileSync(0,"utf8")}catch{}let n=[];try{let c=`Auto-checkpoint at ${new Date().toISOString()}`,l=[],i=w.join(t,"session.json");if(h.existsSync(i))try{l=JSON.parse(h.readFileSync(i,"utf8")).notes||[]}catch{}let u=Q(t),d=Object.entries(u).filter(([,p])=>(p.readCount??0)>0).map(([p])=>p);Oe(null,{message:c,active_files:d,notes:l,auto:!0}),n.push("\u2713 checkpoint saved")}catch{}try{let c=hs(e);c>0&&n.push(`memory backed up (${c})`)}catch{}let s=[],o=null;try{let c=w.join(t,"index.db");if(h.existsSync(c)){let{DatabaseSync:l}=await import("node:sqlite"),i=new l(c);try{ie(i),o=Se(i);try{s=ct(i,e).candidates}catch{}}finally{i.close()}}}catch{}let r="";try{let c=await kt(t);if(c.active){n.push(`roadmap ${c.advanced?"updated":"on track"}: ${c.currentLabel}`);let l=["[cairn] Production roadmap (\u2192 = cursor / current phase):",...c.treeLines.map(i=>" "+i)];c.memoLines&&l.push("","\u2500\u2500 Current phase detail (from roadmap.md) \u2500\u2500",...c.memoLines,"\u2500\u2500 (re-read after each phase to confirm direction) \u2500\u2500"),l.push("","Keep this current via cairn_roadmap on MATERIAL change (phase done / blocked / new) \u2014 not every turn."),r=l.join(`
92
94
  `)}}catch{}if(o&&(n.push("ROADMAP.md not updated \u2014 see warning"),r=r?`${r}
93
95
 
94
96
  ${o}`:o),s.length>0){n.push(`${s.length} phase(s) may be done \u2014 confirm with user`);let c=["","[cairn] Possible COMPLETED phase(s) detected from recent commits \u2014","CONFIRM WITH THE USER before marking done; do NOT mark done unilaterally:"];for(let l of s)c.push(` \u2022 Phase ${l.phase_number}: ${l.text} (commit ${l.commit}, ${l.confidence}) \u2192 if confirmed: cairn_roadmap set_status ${l.phase_id} done`);r=r?r+`
95
97
  `+c.join(`
96
98
  `):c.join(`
97
- `).trimStart()}n.length===0&&process.exit(0);let a={systemMessage:`[cairn] ${n.join(" \xB7 ")}`};r&&(a.additionalContext=r),process.stdout.write(JSON.stringify(a)),process.exit(0)}else if(F==="repair-memory"){let e=process.argv.includes("--dry-run"),t=process.cwd();h.existsSync(w.join(t,".cairn"))||(console.error("[cairn] Not a cairn project (no .cairn/ here). Run this from the project root."),process.exit(1)),He(t);let n=tt(be(),{dryRun:e});if(n.reason&&(console.error(`[cairn] ${n.reason}: ${n.dir}`),process.exit(1)),console.log(`Memory index: ${n.dir}`),console.log(` files on disk : ${n.files_on_disk}`),console.log(` already listed: ${n.already_indexed}`),console.log(` missing : ${n.restored}`),n.restored>0){for(let s of n.restored_entries.slice(0,20))console.log(` + ${s.name}${s.type?" ["+s.type+"]":""} (${s.file})`);n.restored_entries.length>20&&console.log(` \u2026 and ${n.restored_entries.length-20} more`)}if(n.dangling.length>0){console.log(` dangling : ${n.dangling.length} pointer(s) whose file is gone (left alone)`);for(let s of n.dangling.slice(0,10))console.log(` ? ${s.name} (${s.file})`)}n.unreadable.length>0&&console.log(` unreadable : ${n.unreadable.length} file(s) skipped`),e?console.log(n.restored>0?`
99
+ `).trimStart()}n.length===0&&process.exit(0);let a={systemMessage:`[cairn] ${n.join(" \xB7 ")}`};r&&(a.additionalContext=r),process.stdout.write(JSON.stringify(a)),process.exit(0)}else if(F==="repair-memory"){let e=process.argv.includes("--dry-run"),t=process.cwd();h.existsSync(w.join(t,".cairn"))||(console.error("[cairn] Not a cairn project (no .cairn/ here). Run this from the project root."),process.exit(1)),He(t);let n=nt(be(),{dryRun:e});if(n.reason&&(console.error(`[cairn] ${n.reason}: ${n.dir}`),process.exit(1)),console.log(`Memory index: ${n.dir}`),console.log(` files on disk : ${n.files_on_disk}`),console.log(` already listed: ${n.already_indexed}`),console.log(` missing : ${n.restored}`),n.restored>0){for(let s of n.restored_entries.slice(0,20))console.log(` + ${s.name}${s.type?" ["+s.type+"]":""} (${s.file})`);n.restored_entries.length>20&&console.log(` \u2026 and ${n.restored_entries.length-20} more`)}if(n.dangling.length>0){console.log(` dangling : ${n.dangling.length} pointer(s) whose file is gone (left alone)`);for(let s of n.dangling.slice(0,10))console.log(` ? ${s.name} (${s.file})`)}n.unreadable.length>0&&console.log(` unreadable : ${n.unreadable.length} file(s) skipped`),e?console.log(n.restored>0?`
98
100
  Dry run \u2014 nothing written. Re-run without --dry-run to restore them.`:`
99
101
  Dry run \u2014 nothing to restore.`):n.repaired?console.log(`
100
102
  Restored ${n.restored} pointer(s). Previous index kept as ${n.backup}.`):console.log(`
101
- Index already lists every memory file. Nothing to do.`),process.exit(0)}else if(F==="memory-restore"){let e=process.cwd();h.existsSync(w.join(e,".cairn"))||process.exit(0);try{let t=hs(e);t>0&&process.stdout.write(`[cairn] Restored ${t} memory file(s) from .cairn/memory backup (Claude store was empty)
103
+ Index already lists every memory file. Nothing to do.`),process.exit(0)}else if(F==="memory-restore"){let e=process.cwd();h.existsSync(w.join(e,".cairn"))||process.exit(0);try{let t=ys(e);t>0&&process.stdout.write(`[cairn] Restored ${t} memory file(s) from .cairn/memory backup (Claude store was empty)
102
104
  `)}catch{}process.exit(0)}else if(F==="resume-hint"){let e=w.join(process.cwd(),".cairn"),t=w.join(e,"session.json"),n=w.join(e,"index.db"),s=w.join(e,".hint-lock"),o=1800*1e3,r=process.cwd()===se.homedir();if(!r&&h.existsSync(e)&&(h.existsSync(n)||h.existsSync(t))){let x=w.join(e,".cairn-project");if(!h.existsSync(x))try{h.writeFileSync(x,"","utf8"),process.stdout.write(`[cairn] Repaired: wrote .cairn-project sentinel for hook compatibility.
103
- `)}catch{}}if(!r&&h.existsSync(e))try{fs()&&process.stdout.write(`[cairn] Repaired: declared the roadmap slot (${ne}) in ~/.claude.json.
105
+ `)}catch{}}if(!r&&h.existsSync(e))try{ms()&&process.stdout.write(`[cairn] Repaired: declared the roadmap slot (${ne}) in ~/.claude.json.
104
106
  `)}catch{}if(!r&&h.existsSync(n))try{let{DatabaseSync:x}=await import("node:sqlite"),k=new x(n,{readonly:!0}),$=null;try{$=Se(k)}finally{k.close()}$&&process.stdout.write(`${$}
105
107
  `)}catch{}{let x=[w.join(process.cwd(),".claude","settings.json"),w.join(se.homedir(),".claude","settings.json")];for(let k of x)try{if(!h.existsSync(k))continue;let $=h.readFileSync(k,"utf8"),v=JSON.parse($),y=v?.hooks?.PreToolUse;if(!Array.isArray(y))continue;let g=!1;for(let _ of y)if(!(_.matcher!=="Edit"||!Array.isArray(_.hooks)))for(let E of _.hooks)E.command==="cairn set-mode edit"&&(E.command="cairn edit-guard",g=!0);g&&(h.writeFileSync(k,JSON.stringify(v,null,2),"utf8"),process.stdout.write(`[cairn] Repaired: migrated cairn set-mode \u2192 cairn edit-guard in ${k}
106
108
  `))}catch{}}let a="",c="";if(!process.stdin.isTTY)try{let x=JSON.parse(h.readFileSync(0,"utf8"));a=(x?.prompt||"").toLowerCase(),typeof x?.transcript_path=="string"&&(c=x.transcript_path)}catch{}let i=/\b(resume|continue|pick up|where (were|did) (we|i)|last session|carry on|what('s| is) next|what (were|was) (we|i)|start|begin)\b/.test(a);h.existsSync(t)||(h.existsSync(n)||(h.existsSync(s)&&Date.now()-h.statSync(s).mtimeMs<o&&process.exit(0),h.mkdirSync(e,{recursive:!0}),h.writeFileSync(s,new Date().toISOString(),"utf8"),process.stdout.write(`[cairn] No index found. Run cairn_maintain to index this project.
107
- `)),process.exit(0));let u=null;try{let x=JSON.parse(h.readFileSync(t,"utf8")),k=Be({cwd:process.cwd(),session:x,transcriptPath:c});if(k){let $=!!(k.window?.tokens&&k.usage.source==="transcript"&&k.usage.tokens>=Re*k.window.tokens),v=Ke({usage:k.usage,window:k.window,state:k.state,crossedAt:ze(e,$)});u=(v?v+`
109
+ `)),process.exit(0));let u=null;try{let x=JSON.parse(h.readFileSync(t,"utf8")),k=Ye({cwd:process.cwd(),session:x,transcriptPath:c});if(k){let $=!!(k.window?.tokens&&k.usage.source==="transcript"&&k.usage.tokens>=Re*k.window.tokens),v=qe({usage:k.usage,window:k.window,state:k.state,crossedAt:Ke(e,$)});u=(v?v+`
108
110
  `:"")+k.line+`
109
111
  `+k.protocol}}catch{}h.existsSync(s)&&Date.now()-h.statSync(s).mtimeMs<o&&(u&&process.stdout.write(u+`
110
- `),process.exit(0));let d=JSON.parse(h.readFileSync(t,"utf8")),p=new Date(d.checkpoint_at).toLocaleString(),f=/^Auto-checkpoint\b/.test(d.message||""),m=f&&d.last_manual_message||d.message,S=f&&d.last_manual_checkpoint_at?new Date(d.last_manual_checkpoint_at).toLocaleString():p,b=[`[cairn] Prior session: "${m}" (${S}).`,"[cairn] SESSION: ALWAYS call cairn_resume FIRST when asked to resume, continue, or recall prior work \u2014 do NOT substitute git log or file reads. cairn_resume includes commits_since_checkpoint so you can cross-reference checkpoint notes against what was shipped. At end of session, and before any /clear or /compact, call cairn_checkpoint with message + active_files + notes + would_be_lost (what a fresh session would NOT know).","[cairn] TOOLS: NEVER use Bash to run cat, grep, head, tail, find, or ls for file inspection \u2014 always use the Read, Grep, or Glob tools instead. Prefer cairn_search / cairn_bundle / cairn_describe over manual file browsing. Use cairn_outline for structural overview, cairn_todos for backlog, cairn_security before PRs.","[cairn] MEMORY: IMPORTANT \u2014 call cairn_memo IMMEDIATELY and SILENTLY (do NOT ask the user, do NOT list what you are about to save, do NOT confirm afterward) when you observe \u2014 preference: user corrects/confirms your behaviour; experience: non-trivial problem solved; decision: architectural choice made; knowledge: non-obvious codebase fact discovered. cairn_employ_memory is explicit only \u2014 never call it unless asked.",`[cairn] CONTEXT: you cannot see your own token usage, but cairn can and does \u2014 it measures this session's real usage from the transcript on EVERY prompt and reports it in the "[cairn] Context:" line below. What cairn CANNOT see is your context window, so it divides only by a window an agent declared: read yours from your system prompt (e.g. "Opus 5 (1M context)") and pass context_window to cairn_resume or cairn_checkpoint once per project. Until you do, the line carries a raw count and no percentage \u2014 and a raw count is exactly what gets read against a wrongly assumed 200k. There is no threshold and no automatic warning: whether to stop is your judgement, made against the size of the work you are about to start. If the user asks about context, give them the measured count, the declared window if there is one, and your own read on whether the next task fits \u2014 do NOT say cairn has no such feature.`];if(!i){let x=ue(process.cwd(),d);x.recorded&&b.push(`[cairn] ${ve(x)}`)}i&&b.push(["[cairn] RESUME \u2014 checkpoint loaded inline:",...wt(process.cwd(),d)].join(`
112
+ `),process.exit(0));let d=JSON.parse(h.readFileSync(t,"utf8")),p=new Date(d.checkpoint_at).toLocaleString(),f=/^Auto-checkpoint\b/.test(d.message||""),m=f&&d.last_manual_message||d.message,S=f&&d.last_manual_checkpoint_at?new Date(d.last_manual_checkpoint_at).toLocaleString():p,b=[`[cairn] Prior session: "${m}" (${S}).`,"[cairn] SESSION: ALWAYS call cairn_resume FIRST when asked to resume, continue, or recall prior work \u2014 do NOT substitute git log or file reads. cairn_resume includes commits_since_checkpoint so you can cross-reference checkpoint notes against what was shipped. At end of session, and before any /clear or /compact, call cairn_checkpoint with message + active_files + notes + would_be_lost (what a fresh session would NOT know).","[cairn] TOOLS: NEVER use Bash to run cat, grep, head, tail, find, or ls for file inspection \u2014 always use the Read, Grep, or Glob tools instead. Prefer cairn_search / cairn_bundle / cairn_describe over manual file browsing. Use cairn_outline for structural overview, cairn_todos for backlog, cairn_security before PRs.","[cairn] MEMORY: IMPORTANT \u2014 call cairn_memo IMMEDIATELY and SILENTLY (do NOT ask the user, do NOT list what you are about to save, do NOT confirm afterward) when you observe \u2014 preference: user corrects/confirms your behaviour; experience: non-trivial problem solved; decision: architectural choice made; knowledge: non-obvious codebase fact discovered. cairn_employ_memory is explicit only \u2014 never call it unless asked.",`[cairn] CONTEXT: you cannot see your own token usage, but cairn can and does \u2014 it measures this session's real usage from the transcript on EVERY prompt and reports it in the "[cairn] Context:" line below. What cairn CANNOT see is your context window, so it divides only by a window an agent declared: read yours from your system prompt (e.g. "Opus 5 (1M context)") and pass context_window to cairn_resume or cairn_checkpoint once per project. Until you do, the line carries a raw count and no percentage \u2014 and a raw count is exactly what gets read against a wrongly assumed 200k. There is no threshold and no automatic warning: whether to stop is your judgement, made against the size of the work you are about to start. If the user asks about context, give them the measured count, the declared window if there is one, and your own read on whether the next task fits \u2014 do NOT say cairn has no such feature.`];if(!i){let x=ue(process.cwd(),d);x.recorded&&b.push(`[cairn] ${ve(x)}`)}i&&b.push(["[cairn] RESUME \u2014 checkpoint loaded inline:",...xt(process.cwd(),d)].join(`
111
113
  `)),u&&b.push(u),process.stdout.write(b.join(`
112
114
  `)+`
113
- `),h.writeFileSync(s,new Date().toISOString(),"utf8"),process.exit(0)}else if(F==="session-start"){let e={};if(!process.stdin.isTTY)try{e=JSON.parse(h.readFileSync(0,"utf8"))||{}}catch{}let t=typeof e.cwd=="string"&&e.cwd?e.cwd:process.cwd(),n=w.join(t,".cairn"),s=w.join(n,"session.json");h.existsSync(s)||process.exit(0);let o=typeof e.source=="string"?e.source:"",r;try{r=JSON.parse(h.readFileSync(s,"utf8"))}catch{process.exit(0)}Ye(n);let a=/^Auto-checkpoint\b/.test(r.message||""),c=a&&r.last_manual_message||r.message||"(no message recorded)",l=o==="compact"?"a /compact":o==="clear"?"a /clear":o==="fork"?"a fork":o==="resume"?"a resume":"a restart",i=[`[cairn] Session restored automatically after ${l} \u2014 the user did not have to ask:`,...wt(t,r),"","This IS the prior session state. Do not re-derive it from git log or file reads, do not ask","the user what they were doing, and do not ask whether to continue \u2014 it is above, and the",'client that opened this session already said "resume" on their behalf. If their first',"message continues this work, carry on from would_lose without making them ask twice.","","If would_lose says nothing was recorded, treat that as a gap rather than an all-clear: cairn","cannot see what the previous session never wrote down."].join(`
115
+ `),h.writeFileSync(s,new Date().toISOString(),"utf8"),process.exit(0)}else if(F==="session-start"){let e={};if(!process.stdin.isTTY)try{e=JSON.parse(h.readFileSync(0,"utf8"))||{}}catch{}let t=typeof e.cwd=="string"&&e.cwd?e.cwd:process.cwd(),n=w.join(t,".cairn"),s=w.join(n,"session.json");h.existsSync(s)||process.exit(0);let o=typeof e.source=="string"?e.source:"",r;try{r=JSON.parse(h.readFileSync(s,"utf8"))}catch{process.exit(0)}Ge(n);let a=/^Auto-checkpoint\b/.test(r.message||""),c=a&&r.last_manual_message||r.message||"(no message recorded)",l=o==="compact"?"a /compact":o==="clear"?"a /clear":o==="fork"?"a fork":o==="resume"?"a resume":"a restart",i=[`[cairn] Session restored automatically after ${l} \u2014 the user did not have to ask:`,...xt(t,r),"","This IS the prior session state. Do not re-derive it from git log or file reads, do not ask","the user what they were doing, and do not ask whether to continue \u2014 it is above, and the",'client that opened this session already said "resume" on their behalf. If their first',"message continues this work, carry on from would_lose without making them ask twice.","","If would_lose says nothing was recorded, treat that as a gap rather than an all-clear: cairn","cannot see what the previous session never wrote down."].join(`
114
116
  `),u=(k,$)=>{let v=String(k).replace(/\s+/g," ").trim();return v.length<=$?v:v.slice(0,$-1)+"\u2026"},d=ue(t,r),p=a&&r.last_manual_checkpoint_at||r.checkpoint_at,f=p?new Date(p).toLocaleString():"time unknown",m=[`[cairn] Checkpoint restored after ${l} \u2014 saved ${f}`];m.push(` Working on: ${u(c,140)}`),m.push(d.recorded?` Would be lost: ${u(d.text,200)}`+(d.stale?` (written before the ${d.commits} commit(s) since \u2014 may be stale)`:""):" Would be lost: NOT RECORDED \u2014 nothing says what the last session would lose.");let S=[];r.active_files?.length&&S.push(`${r.active_files.length} active file(s)`),r.notes?.length&&S.push(`${r.notes.length} note(s)`),S.push("full detail"),m.push(` Type "resume" for: ${S.join(", ")}.`);let b=m.join(`
115
- `),x=["[cairn] Session restored from checkpoint.","Lead your next reply with ONE short sentence naming what the previous session left","unfinished, then carry straight on with it. Do NOT ask whether to continue, do NOT ask","what they were working on, and do NOT offer a menu \u2014 the answer is in the context you","were given, so use it rather than re-deriving it. If the user has asked for something,","answer that instead, immediately after the sentence: this must never delay, replace or","override their request."].join(" ");process.stdout.write(JSON.stringify({systemMessage:b,hookSpecificOutput:{hookEventName:"SessionStart",additionalContext:i,initialUserMessage:x}})),process.exit(0)}else if(F==="roadmap-hint"){let e=w.join(process.cwd(),".cairn"),t=await xt(e);t.active||process.exit(0);let n=["[cairn] ROADMAP \u2014 cursor is active.",...t.treeLines.map(s=>"[cairn] "+s),`[cairn] Current: ${t.currentLabel}`];if(t.memoLines){n.push("[cairn] \u2500\u2500\u2500\u2500\u2500 Decision memo (current phase) \u2500\u2500\u2500\u2500\u2500");for(let s of t.memoLines)n.push("[cairn] "+s);n.push("[cairn] \u2500\u2500\u2500\u2500\u2500 (re-read after each phase to confirm direction) \u2500\u2500\u2500\u2500\u2500")}n.push("[cairn] Use cairn_roadmap set_status <id> done (auto-advances), focus <id>, or next to move cursor."),process.stdout.write(n.join(`
117
+ `),x=["[cairn] Session restored from checkpoint.","Lead your next reply with ONE short sentence naming what the previous session left","unfinished, then carry straight on with it. Do NOT ask whether to continue, do NOT ask","what they were working on, and do NOT offer a menu \u2014 the answer is in the context you","were given, so use it rather than re-deriving it. If the user has asked for something,","answer that instead, immediately after the sentence: this must never delay, replace or","override their request."].join(" ");process.stdout.write(JSON.stringify({systemMessage:b,hookSpecificOutput:{hookEventName:"SessionStart",additionalContext:i,initialUserMessage:x}})),process.exit(0)}else if(F==="roadmap-hint"){let e=w.join(process.cwd(),".cairn"),t=await kt(e);t.active||process.exit(0);let n=["[cairn] ROADMAP \u2014 cursor is active.",...t.treeLines.map(s=>"[cairn] "+s),`[cairn] Current: ${t.currentLabel}`];if(t.memoLines){n.push("[cairn] \u2500\u2500\u2500\u2500\u2500 Decision memo (current phase) \u2500\u2500\u2500\u2500\u2500");for(let s of t.memoLines)n.push("[cairn] "+s);n.push("[cairn] \u2500\u2500\u2500\u2500\u2500 (re-read after each phase to confirm direction) \u2500\u2500\u2500\u2500\u2500")}n.push("[cairn] Use cairn_roadmap set_status <id> done (auto-advances), focus <id>, or next to move cursor."),process.stdout.write(n.join(`
116
118
  `)+`
117
- `),process.exit(0)}else if(F==="install"){let e=process.platform,t=w.join(se.homedir(),".claude.json"),n={};if(h.existsSync(t))try{n=JSON.parse(h.readFileSync(t,"utf8"))}catch{}n.mcpServers=n.mcpServers||{};let s=!!n.mcpServers.cairn;n.mcpServers.cairn={...s&&typeof n.mcpServers.cairn=="object"?n.mcpServers.cairn:{},command:"cairn-mcp",terminalConnect:bt},h.writeFileSync(t,JSON.stringify(n,null,2),"utf8");let o=w.join(se.homedir(),".claude");kt(o,!0),console.log("Cairn installed successfully!"),console.log(""),console.log(` Platform : ${e}`),console.log(` MCP : cairn-mcp registered in ${t}${s?" (updated)":""}`),console.log(` Hooks : installed in ${w.join(o,"settings.json")}`),console.log(""),console.log("Active hooks:"),console.log(" PreToolUse[Read] -> cairn minify (compress source files for reading)"),console.log(" PreToolUse[Edit] -> cairn edit-guard (block Edit until re-read with full content)"),console.log(" PreToolUse[Write] -> cairn edit-guard (clear minify state before full file overwrite)"),console.log(" PostToolUse[ExitPlanMode] -> cairn roadmap-capture (approved plan -> roadmap phases)"),console.log(" PostToolUse[TodoWrite] -> cairn roadmap-capture (todos -> roadmap sub-tasks)"),console.log(" Stop -> cairn session-tick (checkpoint + memory backup + roadmap, surfaced)"),console.log(" UserPromptSubmit -> cairn resume-hint (remind Claude of prior session)"),console.log(" UserPromptSubmit -> cairn memory-restore (restore .cairn/memory -> Claude store if empty)"),console.log(" SessionStart -> cairn session-start (re-inject checkpoint on restart / clear / compact / fork)"),console.log(""),console.log("Restart Claude Code to activate the MCP server, then open any project and start working."),console.log("Upgrade any time with: npm install -g @misterhuydo/cairn-mcp"),process.exit(0)}else if(F==="install-hooks"){let e=process.argv.includes("--global"),t=e?w.join(se.homedir(),".claude"):w.join(process.cwd(),".claude");kt(t,e),console.log(`Cairn hooks installed in ${e?"global (~/.claude/settings.json)":"project (.claude/settings.json)"}`),console.log(""),console.log("Active hooks:"),console.log(" PreToolUse[Read] -> cairn minify (compress source files for reading)"),console.log(" PreToolUse[Edit] -> cairn edit-guard (block Edit until re-read with full content)"),console.log(" PreToolUse[Write] -> cairn edit-guard (clear minify state before full file overwrite)"),console.log(" PostToolUse[ExitPlanMode] -> cairn roadmap-capture (approved plan -> roadmap phases)"),console.log(" PostToolUse[TodoWrite] -> cairn roadmap-capture (todos -> roadmap sub-tasks)"),console.log(" Stop -> cairn session-tick (checkpoint + memory backup + roadmap, surfaced)"),console.log(" UserPromptSubmit -> cairn resume-hint (remind Claude of prior session)"),console.log(" UserPromptSubmit -> cairn memory-restore (restore .cairn/memory -> Claude store if empty)"),console.log(" SessionStart -> cairn session-start (re-inject checkpoint on restart / clear / compact / fork)"),process.exit(0)}else console.error("Usage: cairn <--version | install | install-hooks [--global] | minify | edit-guard | validate-map | checkpoint --auto | session-tick | session-start | resume-hint | roadmap-hint | roadmap-capture | memory-restore | repair-memory [--dry-run]>"),process.exit(1);function kt(e,t){let n=w.join(e,"settings.json"),s={};if(h.existsSync(n))try{s=JSON.parse(h.readFileSync(n,"utf8"))}catch{}s.hooks=s.hooks||{};let o=s.hooks.PreToolUse||[];o.some(f=>f.matcher==="Read"&&f.hooks?.some(m=>m.command==="cairn minify"))||o.push({matcher:"Read",hooks:[{type:"command",command:"cairn minify"}]});let r=o.filter(f=>!(f.matcher==="Edit"&&f.hooks?.some(m=>m.command==="cairn set-mode edit")));r.some(f=>f.matcher==="Edit"&&f.hooks?.some(m=>m.command==="cairn edit-guard"))||r.push({matcher:"Edit",hooks:[{type:"command",command:"cairn edit-guard"}]}),r.some(f=>f.matcher==="Write"&&f.hooks?.some(m=>m.command==="cairn edit-guard"))||r.push({matcher:"Write",hooks:[{type:"command",command:"cairn edit-guard"}]}),s.hooks.PreToolUse=r;let a=(f,m)=>(f||[]).map(S=>({...S,hooks:(S.hooks||[]).filter(b=>!m(b.command))})).filter(S=>(S.hooks||[]).length>0),c=f=>typeof f=="string"&&/sync-memory-(forward|restore)\.(mjs|py)/.test(f),l=a(s.hooks.Stop,f=>f==="cairn checkpoint --auto"||f==="cairn roadmap-hint");l.some(f=>f.hooks?.some(m=>m.command==="cairn session-tick"))||l.push({hooks:[{type:"command",command:"cairn session-tick"}]}),s.hooks.Stop=l;let i=a(s.hooks.UserPromptSubmit,c);i.some(f=>f.hooks?.some(m=>m.command==="cairn resume-hint"))||i.push({hooks:[{type:"command",command:"cairn resume-hint"}]}),i.some(f=>f.hooks?.some(m=>m.command==="cairn memory-restore"))||i.push({hooks:[{type:"command",command:"cairn memory-restore"}]}),s.hooks.UserPromptSubmit=i;let u=a(s.hooks.SessionStart,f=>f==="cairn session-start");u.push({hooks:[{type:"command",command:"cairn session-start"}]}),s.hooks.SessionStart=u;let d=a(s.hooks.PostToolUse,c),p=f=>{d.some(m=>m.matcher===f&&m.hooks?.some(S=>S.command==="cairn roadmap-capture"))||d.push({matcher:f,hooks:[{type:"command",command:"cairn roadmap-capture"}]})};if(p("ExitPlanMode"),p("TodoWrite"),s.hooks.PostToolUse=d,t){let f=w.join(e,"scripts");for(let m of["sync-memory-forward.mjs","sync-memory-restore.mjs","sync-memory-forward.py","sync-memory-restore.py"]){let S=w.join(f,m);if(h.existsSync(S))try{h.unlinkSync(S)}catch{}}}if(!t){let f=w.join(process.cwd(),".cairn");h.mkdirSync(f,{recursive:!0});let m=w.join(f,".cairn-project");h.existsSync(m)||h.writeFileSync(m,"","utf8")}h.mkdirSync(e,{recursive:!0}),h.writeFileSync(n,JSON.stringify(s,null,2),"utf8")}
119
+ `),process.exit(0)}else if(F==="install"){let e=process.platform,t=w.join(se.homedir(),".claude.json"),n={};if(h.existsSync(t))try{n=JSON.parse(h.readFileSync(t,"utf8"))}catch{}n.mcpServers=n.mcpServers||{};let s=!!n.mcpServers.cairn;n.mcpServers.cairn={...s&&typeof n.mcpServers.cairn=="object"?n.mcpServers.cairn:{},command:"cairn-mcp",terminalConnect:Et},h.writeFileSync(t,JSON.stringify(n,null,2),"utf8");let o=w.join(se.homedir(),".claude");bt(o,!0),console.log("Cairn installed successfully!"),console.log(""),console.log(` Platform : ${e}`),console.log(` MCP : cairn-mcp registered in ${t}${s?" (updated)":""}`),console.log(` Hooks : installed in ${w.join(o,"settings.json")}`),console.log(""),console.log("Active hooks:"),console.log(" PreToolUse[Read] -> cairn minify (compress source files for reading)"),console.log(" PreToolUse[Edit] -> cairn edit-guard (block Edit until re-read with full content)"),console.log(" PreToolUse[Write] -> cairn edit-guard (clear minify state before full file overwrite)"),console.log(" PostToolUse[ExitPlanMode] -> cairn roadmap-capture (approved plan -> roadmap phases)"),console.log(" PostToolUse[TodoWrite] -> cairn roadmap-capture (todos -> roadmap sub-tasks)"),console.log(" Stop -> cairn session-tick (checkpoint + memory backup + roadmap, surfaced)"),console.log(" UserPromptSubmit -> cairn resume-hint (remind Claude of prior session)"),console.log(" UserPromptSubmit -> cairn memory-restore (restore .cairn/memory -> Claude store if empty)"),console.log(" SessionStart -> cairn session-start (re-inject checkpoint on restart / clear / compact / fork)"),console.log(""),console.log("Restart Claude Code to activate the MCP server, then open any project and start working."),console.log("Upgrade any time with: npm install -g @misterhuydo/cairn-mcp"),process.exit(0)}else if(F==="install-hooks"){let e=process.argv.includes("--global"),t=e?w.join(se.homedir(),".claude"):w.join(process.cwd(),".claude");bt(t,e),console.log(`Cairn hooks installed in ${e?"global (~/.claude/settings.json)":"project (.claude/settings.json)"}`),console.log(""),console.log("Active hooks:"),console.log(" PreToolUse[Read] -> cairn minify (compress source files for reading)"),console.log(" PreToolUse[Edit] -> cairn edit-guard (block Edit until re-read with full content)"),console.log(" PreToolUse[Write] -> cairn edit-guard (clear minify state before full file overwrite)"),console.log(" PostToolUse[ExitPlanMode] -> cairn roadmap-capture (approved plan -> roadmap phases)"),console.log(" PostToolUse[TodoWrite] -> cairn roadmap-capture (todos -> roadmap sub-tasks)"),console.log(" Stop -> cairn session-tick (checkpoint + memory backup + roadmap, surfaced)"),console.log(" UserPromptSubmit -> cairn resume-hint (remind Claude of prior session)"),console.log(" UserPromptSubmit -> cairn memory-restore (restore .cairn/memory -> Claude store if empty)"),console.log(" SessionStart -> cairn session-start (re-inject checkpoint on restart / clear / compact / fork)"),process.exit(0)}else console.error("Usage: cairn <--version | install | install-hooks [--global] | minify | edit-guard | validate-map | checkpoint --auto | session-tick | session-start | resume-hint | roadmap-hint | roadmap-capture | memory-restore | repair-memory [--dry-run]>"),process.exit(1);function bt(e,t){let n=w.join(e,"settings.json"),s={};if(h.existsSync(n))try{s=JSON.parse(h.readFileSync(n,"utf8"))}catch{}s.hooks=s.hooks||{};let o=s.hooks.PreToolUse||[];o.some(f=>f.matcher==="Read"&&f.hooks?.some(m=>m.command==="cairn minify"))||o.push({matcher:"Read",hooks:[{type:"command",command:"cairn minify"}]});let r=o.filter(f=>!(f.matcher==="Edit"&&f.hooks?.some(m=>m.command==="cairn set-mode edit")));r.some(f=>f.matcher==="Edit"&&f.hooks?.some(m=>m.command==="cairn edit-guard"))||r.push({matcher:"Edit",hooks:[{type:"command",command:"cairn edit-guard"}]}),r.some(f=>f.matcher==="Write"&&f.hooks?.some(m=>m.command==="cairn edit-guard"))||r.push({matcher:"Write",hooks:[{type:"command",command:"cairn edit-guard"}]}),s.hooks.PreToolUse=r;let a=(f,m)=>(f||[]).map(S=>({...S,hooks:(S.hooks||[]).filter(b=>!m(b.command))})).filter(S=>(S.hooks||[]).length>0),c=f=>typeof f=="string"&&/sync-memory-(forward|restore)\.(mjs|py)/.test(f),l=a(s.hooks.Stop,f=>f==="cairn checkpoint --auto"||f==="cairn roadmap-hint");l.some(f=>f.hooks?.some(m=>m.command==="cairn session-tick"))||l.push({hooks:[{type:"command",command:"cairn session-tick"}]}),s.hooks.Stop=l;let i=a(s.hooks.UserPromptSubmit,c);i.some(f=>f.hooks?.some(m=>m.command==="cairn resume-hint"))||i.push({hooks:[{type:"command",command:"cairn resume-hint"}]}),i.some(f=>f.hooks?.some(m=>m.command==="cairn memory-restore"))||i.push({hooks:[{type:"command",command:"cairn memory-restore"}]}),s.hooks.UserPromptSubmit=i;let u=a(s.hooks.SessionStart,f=>f==="cairn session-start");u.push({hooks:[{type:"command",command:"cairn session-start"}]}),s.hooks.SessionStart=u;let d=a(s.hooks.PostToolUse,c),p=f=>{d.some(m=>m.matcher===f&&m.hooks?.some(S=>S.command==="cairn roadmap-capture"))||d.push({matcher:f,hooks:[{type:"command",command:"cairn roadmap-capture"}]})};if(p("ExitPlanMode"),p("TodoWrite"),s.hooks.PostToolUse=d,t){let f=w.join(e,"scripts");for(let m of["sync-memory-forward.mjs","sync-memory-restore.mjs","sync-memory-forward.py","sync-memory-restore.py"]){let S=w.join(f,m);if(h.existsSync(S))try{h.unlinkSync(S)}catch{}}}if(!t){let f=w.join(process.cwd(),".cairn");h.mkdirSync(f,{recursive:!0});let m=w.join(f,".cairn-project");h.existsSync(m)||h.writeFileSync(m,"","utf8")}h.mkdirSync(e,{recursive:!0}),h.writeFileSync(n,JSON.stringify(s,null,2),"utf8")}