@misterhuydo/cairn-mcp 1.20.0 → 1.21.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -175,14 +175,26 @@ the **`roadmap/1`** slot format so any cockpit or tool can read it without writi
175
175
  parser per provider. One file carries both halves — two files drift, and the drift is
176
176
  invisible:
177
177
 
178
- - **A markdown half for humans:** checkboxes, per-item status, a one-line summary per
179
- phase, and a `← current` marker on the cursor phase.
180
- - **One `roadmap-json` block for consumers**, at the end of the file. It carries stable
178
+ - **A markdown half for readers:** checkboxes, per-item status, a progress bar, one
179
+ plain sentence per phase, and a `← current` marker on the cursor phase. Written for
180
+ somebody who has never heard of cairn and is reading the file on GitHub: they should
181
+ be able to say what the project is working on and what is next. That means no
182
+ authoring syntax survives into the prose — `[[wikilinks]]` are unwrapped or dropped
183
+ (they are filenames a reader cannot open) and nothing points at `.cairn/`, which is
184
+ gitignored and therefore not there for them. `.cairn/roadmap.md` remains the
185
+ authoring source and stays as detailed as you like; this half is the summary.
186
+ - **One `roadmap-json` block for consumers**, collapsed inside a `<details>` at the end
187
+ of the file so a reader never scrolls through it, and never removed — a cockpit's
188
+ per-phase view is rendered from it, and without it that view degrades silently to
189
+ flat markdown. Collapsing costs nothing: the fence keeps blank lines around it, so it
190
+ stays a valid CommonMark code block and both line-anchored and AST consumers still
191
+ find it. It carries stable
181
192
  phase `id`s, `number`, `title`, `status`, `goal`, `blocked_by`, `slices`, the full
182
- per-phase `detail` (the body you wrote in `.cairn/roadmap.md`), and `links` — the
183
- `[[wikilinks]]` in that body resolved to the memory files behind each phase, so the
184
- reasoning is one tap away instead of "go and find it". A link that cannot be resolved
185
- is dropped rather than emitted dead.
193
+ per-phase `detail` (the body you wrote in `.cairn/roadmap.md`, verbatim and in full —
194
+ thinning the prose never thins this), and `links` — the `[[wikilinks]]` in that body
195
+ resolved to the memory files behind each phase, so the reasoning is one tap away
196
+ instead of "go and find it". A link that cannot be resolved is dropped rather than
197
+ emitted dead.
186
198
 
187
199
  The first line is the marker that says who generated the file and when
188
200
  (`<!-- slot:roadmap format=roadmap/1 provider=cairn generated=… -->`). cairn declares
package/dist/cairn-cli.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import y from"fs";import G from"os";import S from"path";import{execSync as Fn}from"child_process";import Un from"fast-glob";var Oe={".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 te(e,t,s={}){let{noComments:n=!0,noEmptyLines:o=!0,aggressive:r=!1}=s,i=e;return n&&(i=ft(i,t)),o&&(i=mt(i)),i=ht(i),r&&(i=yt(i)),i.trim()}function ft(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,(s,n)=>n||!s.startsWith("//")?s:"");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 mt(e){return e.split(`
2
+ import y from"fs";import G from"os";import S from"path";import{execSync as Ln}from"child_process";import Hn from"fast-glob";var Oe={".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 te(e,t,s={}){let{noComments:n=!0,noEmptyLines:o=!0,aggressive:r=!1}=s,i=e;return n&&(i=ft(i,t)),o&&(i=mt(i)),i=ht(i),r&&(i=yt(i)),i.trim()}function ft(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,(s,n)=>n||!s.startsWith("//")?s:"");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 mt(e){return e.split(`
3
3
  `).filter(t=>t.trim().length>0).join(`
4
4
  `)}function ht(e){return e.split(`
5
5
  `).map(t=>t.trimEnd()).join(`
@@ -23,7 +23,7 @@ import y from"fs";import G from"os";import S from"path";import{execSync as Fn}fr
23
23
  `);for(let n=0;n<s.length;n++){let o=s[n].trim();/^(CREATE|ALTER|DROP|INSERT|UPDATE|DELETE|SELECT|WITH|GRANT|REVOKE|BEGIN|COMMIT)\b/i.test(o)&&t.push(`${n+1}: ${o.slice(0,120)}${o.length>120?"...":""}`)}return t.join(`
24
24
  `)}import E from"fs";import V from"path";function ee(e){if(!e||typeof e!="string")return[];let t=e.split(/\r?\n/),s=!1,n=null,o=[],r=new Set;for(let i of t){if(/^##\s+Phases\s*$/i.test(i)){s=!0;continue}if(/^##\s+/.test(i)&&!/^###/.test(i)){n&&(o.push(n),n=null),s=!1;continue}if(!s)continue;let l=/^###\s+Phase\s+(\d+)\s*:\s*(.+?)\s*$/.exec(i);if(l){n&&o.push(n);let f=parseInt(l[1],10),a=l[2];if(r.has(f)){process.stderr.write(`[cairn] memo phase parser: duplicate Phase ${f} ignored
25
25
  `),n=null;continue}r.add(f),n={phase_number:f,text:a,bodyLines:[]};continue}n&&n.bodyLines.push(i)}return n&&o.push(n),o.map(i=>({phase_number:i.phase_number,text:i.text,body:i.bodyLines.join(`
26
- `).trim()||null}))}function Ie(e,t,s){let n=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),o=new Map(n.map(h=>[h.phase_number,h])),r=new Set,i=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, ?, ?, ?, ?)"),f=e.prepare("UPDATE main.roadmap_phases SET status = 'stale' WHERE id = ?"),a=e.prepare("UPDATE main.roadmap_phases SET phase_number = NULL WHERE id = ?"),c=[],p=[],m=[];for(let h=0;h<s.length;h++){let u=s[h];r.add(u.phase_number);let d=o.get(u.phase_number);if(d&&d.status==="done"&&d.text!==u.text){a.run(d.id);let g=l.run(t,u.phase_number,u.text,u.body,"open",h);p.push({id:Number(g.lastInsertRowid),phase_number:u.phase_number,text:u.text,replaced_shipped:d.id})}else if(d)i.run(u.text,u.body,"stale","open",d.id),c.push({id:d.id,phase_number:u.phase_number,text:u.text,reactivated:d.status==="stale"});else{let g=l.run(t,u.phase_number,u.text,u.body,"open",h);p.push({id:Number(g.lastInsertRowid),phase_number:u.phase_number,text:u.text})}}for(let[h,u]of o)!r.has(h)&&u.status!=="stale"&&u.status!=="done"&&(f.run(u.id),m.push({id:u.id,phase_number:h}));return{updated:c.length,inserted:p.length,staled:m.length,updated_phases:c,inserted_phases:p,staled_phases:m}}var j="roadmap",Wt=`# Roadmap
26
+ `).trim()||null}))}function Ie(e,t,s){let n=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),o=new Map(n.map(h=>[h.phase_number,h])),r=new Set,i=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, ?, ?, ?, ?)"),f=e.prepare("UPDATE main.roadmap_phases SET status = 'stale' WHERE id = ?"),a=e.prepare("UPDATE main.roadmap_phases SET phase_number = NULL WHERE id = ?"),c=[],p=[],m=[];for(let h=0;h<s.length;h++){let u=s[h];r.add(u.phase_number);let d=o.get(u.phase_number);if(d&&d.status==="done"&&d.text!==u.text){a.run(d.id);let g=l.run(t,u.phase_number,u.text,u.body,"open",h);p.push({id:Number(g.lastInsertRowid),phase_number:u.phase_number,text:u.text,replaced_shipped:d.id})}else if(d)i.run(u.text,u.body,"stale","open",d.id),c.push({id:d.id,phase_number:u.phase_number,text:u.text,reactivated:d.status==="stale"});else{let g=l.run(t,u.phase_number,u.text,u.body,"open",h);p.push({id:Number(g.lastInsertRowid),phase_number:u.phase_number,text:u.text})}}for(let[h,u]of o)!r.has(h)&&u.status!=="stale"&&u.status!=="done"&&(f.run(u.id),m.push({id:u.id,phase_number:h}));return{updated:c.length,inserted:p.length,staled:m.length,updated_phases:c,inserted_phases:p,staled_phases:m}}var O="roadmap",Wt=`# Roadmap
27
27
 
28
28
  The forward-looking plan: only upcoming and in-progress phases live here. When a
29
29
  phase is marked done (cairn_roadmap set_status <id> done) it is removed from this
@@ -38,7 +38,7 @@ What this phase delivers.
38
38
  `),"utf8")}function Yt(){let e=Y();if(E.existsSync(e))return null;let t=[],s=Pe();s&&t.push(s),t.push(V.join(q(),"memory"));let n=null,o=null;for(let i of t){let l=V.join(i,ge);n==null&&E.existsSync(l)&&(n=Jt(E.readFileSync(l,"utf8")).trim(),o=l)}if(n==null)return null;let r=/^#\s/m.test(n)?"":`# Roadmap
39
39
 
40
40
  `;E.writeFileSync(e,r+n+`
41
- `,"utf8");for(let i of t){let l=V.join(i,ge);try{E.existsSync(l)&&E.unlinkSync(l)}catch{}try{Bt(i,ge)}catch{}}return{migrated:!0,from:o,to:e}}function X(e){Yt();let t=Y();if(!E.existsSync(t))return null;let s=E.readFileSync(t,"utf8");if(!/^##\s+Phases\s*$/im.test(s))return null;let n=Ht(ee(s));return Ie(e,j,n)}function Se(){let e=Ut();return E.existsSync(e)?E.readFileSync(e,"utf8").split(/\r?\n/).filter(t=>/^-\s+\d{4}-\d{2}-\d{2}\s+—\s+Phase\s/.test(t)).length:0}import Z from"fs";import xe from"path";import{execSync as zt}from"child_process";var We=120;function be(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>We&&(t=t.slice(0,We-1).trimEnd()+"\u2026"),t}function W(e){return String(e||"").toLowerCase().replace(/[^a-z0-9]+/g," ").trim()}function Gt(e){if(!e||typeof e!="string")return[];let t=e.split(/\r?\n/),s=a=>a.map(c=>({text:be(c.titleRaw),body:c.bodyLines.join(`
41
+ `,"utf8");for(let i of t){let l=V.join(i,ge);try{E.existsSync(l)&&E.unlinkSync(l)}catch{}try{Bt(i,ge)}catch{}}return{migrated:!0,from:o,to:e}}function X(e){Yt();let t=Y();if(!E.existsSync(t))return null;let s=E.readFileSync(t,"utf8");if(!/^##\s+Phases\s*$/im.test(s))return null;let n=Ht(ee(s));return Ie(e,O,n)}function Se(){let e=Ut();return E.existsSync(e)?E.readFileSync(e,"utf8").split(/\r?\n/).filter(t=>/^-\s+\d{4}-\d{2}-\d{2}\s+—\s+Phase\s/.test(t)).length:0}import Z from"fs";import xe from"path";import{execSync as zt}from"child_process";var We=120;function be(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>We&&(t=t.slice(0,We-1).trimEnd()+"\u2026"),t}function W(e){return String(e||"").toLowerCase().replace(/[^a-z0-9]+/g," ").trim()}function Gt(e){if(!e||typeof e!="string")return[];let t=e.split(/\r?\n/),s=a=>a.map(c=>({text:be(c.titleRaw),body:c.bodyLines.join(`
42
42
  `).trim()||null})).filter(c=>c.text),n=[],o=null;for(let a of t){let c=/^(#{1,6})\s+(.+?)\s*$/.exec(a);if(c&&c[1].length>=2&&c[1].length<=4){o&&n.push(o),o={titleRaw:c[2],bodyLines:[]};continue}o&&o.bodyLines.push(a)}o&&n.push(o);let r=[];o=null;for(let a of t){let c=/^(\d+)[.)]\s+(.+?)\s*$/.exec(a);if(c){o&&r.push(o),o={titleRaw:c[2],bodyLines:[]};continue}o&&o.bodyLines.push(a)}o&&r.push(o);let i=[s(n),s(r)].filter(a=>a.length>=2);if(i.length)return i.sort((a,c)=>c.length-a.length),i[0];let l=t.find(a=>a.trim());return[{text:be(l||"Plan")||"Plan",body:e.trim()||null}]}function qt(e){let t=[];return e.forEach((s,n)=>{t.push(`### Phase ${n+1}: ${s.text}`),s.body&&s.body.trim()&&t.push(s.body.trim()),t.push("")}),t.join(`
43
43
  `).replace(/\n{3,}/g,`
44
44
 
@@ -52,22 +52,22 @@ ${o}`,"utf8");return}let r=s.length;for(let a=n+1;a<s.length;a++)if(/^##\s+/.tes
52
52
  `).replace(/^\n+/,""),f=`
53
53
 
54
54
  ${o}${l.trim()?`
55
- `+l:""}`;Z.writeFileSync(Y(),i+f,"utf8")}function He(e){let t=Gt(e);if(t.length===0)return{added:[],skipped:[],total:0};let n=_e()?[]:ee(re()).map(f=>({text:f.text,body:f.body})),o=new Set(n.map(f=>W(f.text))),r=[...n],i=[],l=[];for(let f of t){let a=W(f.text);if(o.has(a)){l.push(f.text);continue}o.add(a),r.push(f),i.push(f.text)}return i.length>0&&Ue(r),{added:i,skipped:l,total:r.length}}function Kt(e){return e==="completed"?"done":e==="in_progress"?"in_progress":"open"}function Vt(e){try{return e.prepare("SELECT phase_id FROM main.roadmap_cursor WHERE singleton = 1").get()?.phase_id??null}catch{return null}}function Je(e,t){if(!Array.isArray(t)||t.length===0)return{changed:!1};let s=t.map(m=>({text:be(m.content||m.activeForm||""),status:Kt(m.status)})).filter(m=>m.text);if(s.length===0)return{changed:!1};let n=e.prepare("SELECT id, status FROM main.roadmap_phases WHERE decision_name = ? AND parent_phase_id IS NULL").all(j).filter(m=>m.status!=="stale");if(n.length===0){let h=_e()?[]:ee(re()).map(x=>({text:x.text,body:x.body})),u=new Set(h.map(x=>W(x.text))),d=[...h],g=0;for(let x of s){let R=W(x.text);u.has(R)||(u.add(R),d.push({text:x.text,body:null}),g++)}if(g===0)return{changed:!1};Ue(d);let _=X(e);return{changed:!0,seeded:g,diff:_}}let o=Vt(e);o==null&&(o=n.find(m=>m.status==="in_progress")?.id??n.find(m=>m.status==="open")?.id??n[0].id);let r=e.prepare("SELECT id, text, status FROM main.roadmap_phases WHERE parent_phase_id = ?").all(o),i=new Map(r.map(m=>[W(m.text),m])),l=e.prepare("INSERT INTO main.roadmap_phases (decision_name, phase_number, parent_phase_id, text, status, position) VALUES (NULL, NULL, ?, ?, ?, ?)"),f=e.prepare("UPDATE main.roadmap_phases SET status = ? WHERE id = ?"),a=e.prepare("SELECT COALESCE(MAX(position), -1) + 1 AS n FROM main.roadmap_phases WHERE parent_phase_id = ?").get(o).n,c=0,p=0;for(let m of s){let h=i.get(W(m.text));if(h){if(h.status==="done"&&m.status!=="done")continue;h.status!==m.status&&(f.run(m.status,h.id),p++)}else l.run(o,m.text,m.status,a++),c++}return{changed:c>0||p>0,parentId:o,inserted:c,updated:p}}var Xt=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 Zt(e){return[...new Set(W(e).split(" ").filter(t=>t.length>=4&&!Xt.has(t)))]}function Qt(e,t){if(!e||!t?.length)return null;let s=String(e),n=/\bphase\s*#?\s*(\d+)\b/i.exec(s);if(n){let i=parseInt(n[1],10),l=t.find(f=>f.phase_number===i);if(l)return{...l,confidence:"high"}}let o=new Set(W(s).split(" ").filter(Boolean)),r=null;for(let i of t){let l=Zt(i.text);if(l.length<2)continue;let f=l.filter(c=>o.has(c)),a=f.length/l.length;f.length>=2&&a>=.6&&(!r||a>r.ratio)&&(r={...i,confidence:"medium",ratio:a})}return r?(delete r.ratio,r):null}function we(e,t){return zt(`git ${t}`,{cwd:e,encoding:"utf8",timeout:5e3}).trim()}function Be(e,t){let s=xe.join(t,".cairn"),n=xe.join(s,".last-commit"),o=xe.join(s,"roadmap-pending.json"),r;try{r=we(t,"rev-parse HEAD")}catch{return{candidates:[],newCommits:0}}let i=null;try{i=Z.readFileSync(n,"utf8").trim()||null}catch{}let l=[];if(i&&i!==r)try{l=we(t,`rev-list --no-merges --reverse ${i}..HEAD`).split(`
56
- `).filter(Boolean)}catch{l=[]}try{Z.writeFileSync(n,r,"utf8")}catch{}let f=[];try{let a=JSON.parse(Z.readFileSync(o,"utf8"));Array.isArray(a)&&(f=a)}catch{}if(l.length){let a=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(j);for(let c of l){let p="";try{p=we(t,`show -s --format=%s%n%b ${c}`)}catch{}let m=Qt(p,a);m&&!f.some(h=>h.phase_id===m.id)&&f.push({phase_id:m.id,phase_number:m.phase_number,text:m.text,commit:c.slice(0,7),confidence:m.confidence})}}if(f.length){let a=new Set(e.prepare("SELECT id FROM main.roadmap_phases WHERE status IN ('open','in_progress')").all().map(c=>c.id));f=f.filter(c=>a.has(c.phase_id))}try{Z.writeFileSync(o,JSON.stringify(f),"utf8")}catch{}return{candidates:f,newCommits:l.length}}import A from"fs";import O from"path";var ie="cairn",z="roadmap/1",en="<!-- slot:roadmap";function tn(e){return`${en} format=${z} provider=${ie} generated=${e} -->`}var nn=`<!-- ${ie}:roadmap -->`,sn=/^<!--\s*slot:roadmap\b([^>]*?)-->/,on=/^<!--\s*([A-Za-z0-9_.-]+)\s*:\s*roadmap\s*-->/,rn=/([A-Za-z_][A-Za-z0-9_-]*)=("[^"]*"|\S+)/g;function an(e){if(typeof e!="string")return null;let t=e.indexOf(`
55
+ `+l:""}`;Z.writeFileSync(Y(),i+f,"utf8")}function He(e){let t=Gt(e);if(t.length===0)return{added:[],skipped:[],total:0};let n=_e()?[]:ee(re()).map(f=>({text:f.text,body:f.body})),o=new Set(n.map(f=>W(f.text))),r=[...n],i=[],l=[];for(let f of t){let a=W(f.text);if(o.has(a)){l.push(f.text);continue}o.add(a),r.push(f),i.push(f.text)}return i.length>0&&Ue(r),{added:i,skipped:l,total:r.length}}function Kt(e){return e==="completed"?"done":e==="in_progress"?"in_progress":"open"}function Vt(e){try{return e.prepare("SELECT phase_id FROM main.roadmap_cursor WHERE singleton = 1").get()?.phase_id??null}catch{return null}}function Je(e,t){if(!Array.isArray(t)||t.length===0)return{changed:!1};let s=t.map(m=>({text:be(m.content||m.activeForm||""),status:Kt(m.status)})).filter(m=>m.text);if(s.length===0)return{changed:!1};let n=e.prepare("SELECT id, status FROM main.roadmap_phases WHERE decision_name = ? AND parent_phase_id IS NULL").all(O).filter(m=>m.status!=="stale");if(n.length===0){let h=_e()?[]:ee(re()).map(x=>({text:x.text,body:x.body})),u=new Set(h.map(x=>W(x.text))),d=[...h],g=0;for(let x of s){let R=W(x.text);u.has(R)||(u.add(R),d.push({text:x.text,body:null}),g++)}if(g===0)return{changed:!1};Ue(d);let _=X(e);return{changed:!0,seeded:g,diff:_}}let o=Vt(e);o==null&&(o=n.find(m=>m.status==="in_progress")?.id??n.find(m=>m.status==="open")?.id??n[0].id);let r=e.prepare("SELECT id, text, status FROM main.roadmap_phases WHERE parent_phase_id = ?").all(o),i=new Map(r.map(m=>[W(m.text),m])),l=e.prepare("INSERT INTO main.roadmap_phases (decision_name, phase_number, parent_phase_id, text, status, position) VALUES (NULL, NULL, ?, ?, ?, ?)"),f=e.prepare("UPDATE main.roadmap_phases SET status = ? WHERE id = ?"),a=e.prepare("SELECT COALESCE(MAX(position), -1) + 1 AS n FROM main.roadmap_phases WHERE parent_phase_id = ?").get(o).n,c=0,p=0;for(let m of s){let h=i.get(W(m.text));if(h){if(h.status==="done"&&m.status!=="done")continue;h.status!==m.status&&(f.run(m.status,h.id),p++)}else l.run(o,m.text,m.status,a++),c++}return{changed:c>0||p>0,parentId:o,inserted:c,updated:p}}var Xt=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 Zt(e){return[...new Set(W(e).split(" ").filter(t=>t.length>=4&&!Xt.has(t)))]}function Qt(e,t){if(!e||!t?.length)return null;let s=String(e),n=/\bphase\s*#?\s*(\d+)\b/i.exec(s);if(n){let i=parseInt(n[1],10),l=t.find(f=>f.phase_number===i);if(l)return{...l,confidence:"high"}}let o=new Set(W(s).split(" ").filter(Boolean)),r=null;for(let i of t){let l=Zt(i.text);if(l.length<2)continue;let f=l.filter(c=>o.has(c)),a=f.length/l.length;f.length>=2&&a>=.6&&(!r||a>r.ratio)&&(r={...i,confidence:"medium",ratio:a})}return r?(delete r.ratio,r):null}function we(e,t){return zt(`git ${t}`,{cwd:e,encoding:"utf8",timeout:5e3}).trim()}function Be(e,t){let s=xe.join(t,".cairn"),n=xe.join(s,".last-commit"),o=xe.join(s,"roadmap-pending.json"),r;try{r=we(t,"rev-parse HEAD")}catch{return{candidates:[],newCommits:0}}let i=null;try{i=Z.readFileSync(n,"utf8").trim()||null}catch{}let l=[];if(i&&i!==r)try{l=we(t,`rev-list --no-merges --reverse ${i}..HEAD`).split(`
56
+ `).filter(Boolean)}catch{l=[]}try{Z.writeFileSync(n,r,"utf8")}catch{}let f=[];try{let a=JSON.parse(Z.readFileSync(o,"utf8"));Array.isArray(a)&&(f=a)}catch{}if(l.length){let a=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(O);for(let c of l){let p="";try{p=we(t,`show -s --format=%s%n%b ${c}`)}catch{}let m=Qt(p,a);m&&!f.some(h=>h.phase_id===m.id)&&f.push({phase_id:m.id,phase_number:m.phase_number,text:m.text,commit:c.slice(0,7),confidence:m.confidence})}}if(f.length){let a=new Set(e.prepare("SELECT id FROM main.roadmap_phases WHERE status IN ('open','in_progress')").all().map(c=>c.id));f=f.filter(c=>a.has(c.phase_id))}try{Z.writeFileSync(o,JSON.stringify(f),"utf8")}catch{}return{candidates:f,newCommits:l.length}}import A from"fs";import j from"path";var ie="cairn",z="roadmap/1",en="<!-- slot:roadmap";function tn(e){return`${en} format=${z} provider=${ie} generated=${e} -->`}var nn=`<!-- ${ie}:roadmap -->`,sn=/^<!--\s*slot:roadmap\b([^>]*?)-->/,on=/^<!--\s*([A-Za-z0-9_.-]+)\s*:\s*roadmap\s*-->/,rn=/([A-Za-z_][A-Za-z0-9_-]*)=("[^"]*"|\S+)/g;function an(e){if(typeof e!="string")return null;let t=e.indexOf(`
57
57
  `);return(t===-1?e:e.slice(0,t)).trim()}function cn(e){let t=an(e);if(!t)return null;let s=sn.exec(t);if(s){let o={};for(let r of s[1].matchAll(rn))o[r[1]]=r[2].replace(/^"|"$/g,"");return o.provider?{provider:o.provider.toLowerCase(),format:o.format||null,generated:o.generated||null}:null}let n=on.exec(t);return n?{provider:n[1].toLowerCase(),format:null,generated:null}:null}var ln=/^```roadmap-json[ \t]*\r?\n([\s\S]*?)\r?\n```[ \t]*$/m;function Ge(e){if(typeof e!="string")return null;let t=ln.exec(e);if(!t)return null;try{let s=JSON.parse(t[1]);return s&&typeof s=="object"&&!Array.isArray(s)?s:null}catch{return null}}var un=new Set(["format","provider","generated","title","cursor","phases","migrated_from","migrated"]),pn=new Set(["id","number","title","status","goal","blocked_by","slices","detail","links"]);function qe(e,t){let s={};if(!e||typeof e!="object")return s;for(let n of Object.keys(e).sort())t.has(n)||(s[n]=e[n]);return s}function Ke(e,t=new Map){if(!Array.isArray(e))return t;for(let s of e)s&&typeof s=="object"&&typeof s.id=="string"&&(t.set(s.id,s),Ke(s.slices,t));return t}function dn(e){if(typeof e!="string")return null;let t=Ge(e)?.migrated_from;if(typeof t=="string"&&t)return t;let s=e.indexOf(`
58
- `);if(s===-1)return null;let n=e.indexOf("-->",s),o=n===-1?e.slice(0,512):e.slice(s,n),r=/^\s*migrated-from=(.+?)\s*$/m.exec(o);return r?r[1]:null}var fn=/\[\[([^\]|#\n]+?)(?:[|#][^\]\n]*)?\]\]/g,mn=["decision","knowledge","experience","preference"];function hn(e){let t=O.relative(J(),e).split(O.sep).join("/");return!t||t.startsWith("..")||O.isAbsolute(t)?null:t}function yn(e){let t=O.join(J(),".cairn","memory");return[O.join(t,`${e}.md`),...mn.map(s=>O.join(t,`${s}_${e}.md`))]}function gn(e){if(!e)return[];let t=[],s=new Set;for(let n of String(e).matchAll(fn)){let o=n[1].trim();if(!(!o||s.has(o))){s.add(o);for(let r of yn(o)){if(!A.existsSync(r))continue;let i=hn(r);i&&t.push({kind:"memory",title:o,path:i});break}}}return t}var _n={open:"open",in_progress:"in progress",done:"done",blocked:"blocked",stale:"stale"},Sn=new Set(["open","in_progress","done","blocked","stale"]);function xn(e){return e==="done"?"x":e==="in_progress"?"~":e==="blocked"?"!":" "}function Ve(){return O.join(J(),"ROADMAP.md")}function Xe(){return new Date().toISOString().replace(/\.\d{3}Z$/,"Z")}function wn(e){try{return e.prepare("SELECT phase_id FROM main.roadmap_cursor WHERE singleton = 1").get()?.phase_id??null}catch{return null}}function bn(e){let t=new Map,s;try{s=e.prepare("SELECT blocker_id, blocked_id FROM main.roadmap_phase_deps").all()}catch{return t}for(let n of s)t.has(n.blocked_id)||t.set(n.blocked_id,new Set),t.get(n.blocked_id).add(n.blocker_id);return t}function kn(e,t){let s=e.decision_name??"",n=t.decision_name??"";return s!==n?s<n?-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 En(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(d=>d.status!=="stale").length===0)return null;let s=wn(e),n=bn(e),o=new Map(t.map(d=>[d.id,d.status])),r=new Map(t.map(d=>[d.id,d.phase_number!=null?`Phase ${d.phase_number}`:d.text])),i=new Map(t.map(d=>[d.id,d])),l=new Map;for(let d of t){let g=d.parent_phase_id??"root";l.has(g)||l.set(g,[]),l.get(g).push(d)}for(let d of l.values())d.sort(kn);let f=new Map;for(let d of l.get("root")||[])d.decision_name!=null&&(f.has(d.decision_name)||f.set(d.decision_name,[]),f.get(d.decision_name).push(d));let a=[...f.keys()].sort((d,g)=>d===j?-1:g===j?1:d<g?-1:d>g?1:0),c=new Map,p=!1;for(let d of a){let g=f.get(d).filter(x=>x.status!=="stale"),_=d===j?g.filter(x=>x.status!=="done"):g;_.length!==0&&(p=!0,c.set(d,{visible:_,doneCount:g.filter(x=>x.status==="done").length,total:g.length}))}return p?{cursor:s,decisionNames:a,visibleRoots:c,children:d=>(l.get(d)||[]).filter(g=>g.status!=="stale"),unfinishedBlockers:d=>{let g=n.get(d);return g?[...g].filter(_=>o.get(_)!=="done").map(_=>r.get(_)||`#${_}`):[]},blockerIds:d=>{let g=n.get(d);return g?[...g].filter(_=>i.has(_)).sort((_,x)=>_-x).map(_=>`phase-${_}`):[]}}:null}var ke=120;function Ze(e){if(!e)return null;let t=[];for(let i of e.split(`
59
- `)){let l=i.trim();if(!l){if(t.length)break;continue}t.push(l)}if(!t.length)return null;let s=t.join(" ").replace(/\s+/g," "),n=null,o=/[.?!](?=\s|$)/g,r;for(;(r=o.exec(s))!==null&&(n=s.slice(0,r.index+1),!(n.length>=40)););if(n||(n=s),n.length>ke){let i=n.lastIndexOf(" ",ke-1);n=n.slice(0,i>60?i:ke-1).trimEnd()+"\u2026"}return n}function Ye(e){let t=e.doneCount,s=e.total;if(!s)return"_No phases yet._";let n=Math.round(t/s*10);return`**${t} of ${s} done** \`${"\u2588".repeat(n)}${"\u2591".repeat(10-n)}\``}function $n(e,t){let{cursor:s,decisionNames:n,visibleRoots:o,children:r,unfinishedBlockers:i}=e,l=[tn(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."];t.migratedFrom&&(l.push(" The ROADMAP.md that was here before cairn adopted this file is kept at:"),l.push(` migrated-from=${t.migratedFrom}`)),l.push("-->","","# Roadmap","","Legend: `[x]` done \xB7 `[~]` in progress \xB7 `[ ]` open \xB7 `[!]` blocked.");let f=(c,p)=>{let m=" ".repeat(p),h=c.phase_number!=null?`Phase ${c.phase_number}: ${c.text}`:c.text,u=_n[c.status]||c.status,d=i(c.id);d.length&&(u+=` \xB7 waiting on ${d.join(", ")}`),s===c.id&&(u+=" \xB7 \u2190 current"),l.push(`${m}- [${xn(c.status)}] **${h}** \u2014 ${u}`);let g=Ze(c.body);g&&l.push(`${m} ${g}`);for(let _ of r(c.id))f(_,p+1)};for(let c of n){let p=o.get(c);if(p){l.push(""),c===j?l.push("## Active plan","",Ye(p)):l.push(`## ${c}`,"",Ye(p)),l.push("");for(let m of p.visible)f(m,0)}}let a=Se();return a>0&&l.push("","---","",`**Shipped:** ${a} phase${a===1?"":"s"} \u2014 see \`.cairn/roadmap_completed.md\`.`),l}function Qe(e,t,s,n){let o=s.get(`phase-${e.id}`),r=Ze(e.body),i=gn(e.body),l=t.blockerIds(e.id),f={id:`phase-${e.id}`};if(e.phase_number!=null&&(f.number=e.phase_number),f.title=e.text,f.status=Sn.has(e.status)?e.status:"open",r&&(f.goal=r),l.length&&(f.blocked_by=l),!n){let a=[],c=p=>{for(let m of t.children(p))a.push(Qe(m,t,s,!0)),c(m.id)};c(e.id),a.length&&(f.slices=a)}return e.body&&e.body.trim()&&(f.detail=e.body.trim()),i.length&&(f.links=i),{...f,...qe(o,pn)}}function Rn(e,t){let s=Ke(t.previous?.phases),n=[];for(let i of e.decisionNames){let l=e.visibleRoots.get(i);if(l)for(let f of l.visible)n.push(Qe(f,e,s,!1))}let o={format:z,provider:ie,generated:t.generated,title:`${O.basename(J())} roadmap`,cursor:e.cursor!=null?`phase-${e.cursor}`:null};t.migratedFrom&&(o.migrated_from=t.migratedFrom,t.migrated&&(o.migrated=t.migrated)),o.phases=n;let r={...o,...qe(t.previous,un)};return["","<details>","<summary>Machine-readable plan (roadmap/1)</summary>","","```roadmap-json",JSON.stringify(r,null,2),"```","","</details>"]}function ze(e,t={}){let s={generated:t.generated||Xe(),migratedFrom:t.migratedFrom||null,migrated:t.migrated||null,previous:t.previous&&typeof t.previous=="object"?t.previous:null},n=En(e);return n==null?null:[...$n(n,s),...Rn(n,s)].join(`
58
+ `);if(s===-1)return null;let n=e.indexOf("-->",s),o=n===-1?e.slice(0,512):e.slice(s,n),r=/^\s*migrated-from=(.+?)\s*$/m.exec(o);return r?r[1]:null}var fn=/\[\[([^\]|#\n]+?)(?:[|#][^\]\n]*)?\]\]/g,mn=["decision","knowledge","experience","preference"];function hn(e){let t=j.relative(J(),e).split(j.sep).join("/");return!t||t.startsWith("..")||j.isAbsolute(t)?null:t}function yn(e){let t=j.join(J(),".cairn","memory");return[j.join(t,`${e}.md`),...mn.map(s=>j.join(t,`${s}_${e}.md`))]}function gn(e){if(!e)return[];let t=[],s=new Set;for(let n of String(e).matchAll(fn)){let o=n[1].trim();if(!(!o||s.has(o))){s.add(o);for(let r of yn(o)){if(!A.existsSync(r))continue;let i=hn(r);i&&t.push({kind:"memory",title:o,path:i});break}}}return t}var _n={open:"open",in_progress:"in progress",done:"done",blocked:"blocked",stale:"stale"},Sn=new Set(["open","in_progress","done","blocked","stale"]);function xn(e){return e==="done"?"x":e==="in_progress"?"~":e==="blocked"?"!":" "}function Ve(){return j.join(J(),"ROADMAP.md")}function Xe(){return new Date().toISOString().replace(/\.\d{3}Z$/,"Z")}function wn(e){try{return e.prepare("SELECT phase_id FROM main.roadmap_cursor WHERE singleton = 1").get()?.phase_id??null}catch{return null}}function bn(e){let t=new Map,s;try{s=e.prepare("SELECT blocker_id, blocked_id FROM main.roadmap_phase_deps").all()}catch{return t}for(let n of s)t.has(n.blocked_id)||t.set(n.blocked_id,new Set),t.get(n.blocked_id).add(n.blocker_id);return t}function kn(e,t){let s=e.decision_name??"",n=t.decision_name??"";return s!==n?s<n?-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 En(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(d=>d.status!=="stale").length===0)return null;let s=wn(e),n=bn(e),o=new Map(t.map(d=>[d.id,d.status])),r=new Map(t.map(d=>[d.id,d.phase_number!=null?`Phase ${d.phase_number}`:d.text])),i=new Map(t.map(d=>[d.id,d])),l=new Map;for(let d of t){let g=d.parent_phase_id??"root";l.has(g)||l.set(g,[]),l.get(g).push(d)}for(let d of l.values())d.sort(kn);let f=new Map;for(let d of l.get("root")||[])d.decision_name!=null&&(d.decision_name===O&&d.phase_number==null||(f.has(d.decision_name)||f.set(d.decision_name,[]),f.get(d.decision_name).push(d)));let a=[...f.keys()].sort((d,g)=>d===O?-1:g===O?1:d<g?-1:d>g?1:0),c=new Map,p=!1;for(let d of a){let g=f.get(d).filter(x=>x.status!=="stale"),_=d===O?g.filter(x=>x.status!=="done"):g;_.length!==0&&(p=!0,c.set(d,{visible:_,doneCount:g.filter(x=>x.status==="done").length,total:g.length}))}return p?{cursor:s,decisionNames:a,visibleRoots:c,children:d=>(l.get(d)||[]).filter(g=>g.status!=="stale"),unfinishedBlockers:d=>{let g=n.get(d);return g?[...g].filter(_=>o.get(_)!=="done").map(_=>r.get(_)||`#${_}`):[]},blockerIds:d=>{let g=n.get(d);return g?[...g].filter(_=>i.has(_)).sort((_,x)=>_-x).map(_=>`phase-${_}`):[]}}:null}var ke=120;function $n(e){return e.replace(/\s*\((?:see\s+)?\[\[[^\]]+\]\](?:\s*(?:,|and)\s*\[\[[^\]]+\]\])*\s*\)/gi,"").replace(/\[\[([^\]]+)\]\]/g,(t,s)=>s.replace(/_/g," ").trim()).replace(/\s{2,}/g," ").replace(/\s+([.,;:!?])/g,"$1").trim()}function Ze(e){if(!e)return null;let t=[];for(let i of e.split(`
59
+ `)){let l=i.trim();if(!l){if(t.length)break;continue}t.push(l)}if(!t.length)return null;let s=$n(t.join(" ").replace(/\s+/g," ")),n=null,o=/[.?!](?=\s|$)/g,r;for(;(r=o.exec(s))!==null&&(n=s.slice(0,r.index+1),!(n.length>=40)););if(n||(n=s),n.length>ke){let i=n.lastIndexOf(" ",ke-1);n=n.slice(0,i>60?i:ke-1).trimEnd()+"\u2026"}return n}function Ye(e){let t=e.doneCount,s=e.total;if(!s)return"_No phases yet._";let n=Math.round(t/s*10);return`**${t} of ${s} done** \`${"\u2588".repeat(n)}${"\u2591".repeat(10-n)}\``}function Rn(e,t){let{cursor:s,decisionNames:n,visibleRoots:o,children:r,unfinishedBlockers:i}=e,l=[tn(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."];t.migratedFrom&&(l.push(" The ROADMAP.md that was here before cairn adopted this file is kept at:"),l.push(` migrated-from=${t.migratedFrom}`)),l.push("-->","","# Roadmap","","Legend: `[x]` done \xB7 `[~]` in progress \xB7 `[ ]` open \xB7 `[!]` blocked.");let f=(c,p)=>{let m=" ".repeat(p),h=c.phase_number!=null?`Phase ${c.phase_number}: ${c.text}`:c.text,u=_n[c.status]||c.status,d=i(c.id);d.length&&(u+=` \xB7 waiting on ${d.join(", ")}`),s===c.id&&(u+=" \xB7 \u2190 current"),l.push(`${m}- [${xn(c.status)}] **${h}** \u2014 ${u}`);let g=Ze(c.body);g&&l.push(`${m} ${g}`);for(let _ of r(c.id))f(_,p+1)};for(let c of n){let p=o.get(c);if(p){l.push(""),c===O?l.push("## Active plan","",Ye(p)):l.push(`## ${c}`,"",Ye(p)),l.push("");for(let m of p.visible)f(m,0)}}let a=Se();return a>0&&l.push("","---","",`**Shipped so far:** ${a} phase${a===1?"":"s"}.`),l}function Qe(e,t,s,n){let o=s.get(`phase-${e.id}`),r=Ze(e.body),i=gn(e.body),l=t.blockerIds(e.id),f={id:`phase-${e.id}`};if(e.phase_number!=null&&(f.number=e.phase_number),f.title=e.text,f.status=Sn.has(e.status)?e.status:"open",r&&(f.goal=r),l.length&&(f.blocked_by=l),!n){let a=[],c=p=>{for(let m of t.children(p))a.push(Qe(m,t,s,!0)),c(m.id)};c(e.id),a.length&&(f.slices=a)}return e.body&&e.body.trim()&&(f.detail=e.body.trim()),i.length&&(f.links=i),{...f,...qe(o,pn)}}function vn(e,t){let s=Ke(t.previous?.phases),n=[];for(let i of e.decisionNames){let l=e.visibleRoots.get(i);if(l)for(let f of l.visible)n.push(Qe(f,e,s,!1))}let o={format:z,provider:ie,generated:t.generated,title:`${j.basename(J())} roadmap`,cursor:e.cursor!=null?`phase-${e.cursor}`:null};t.migratedFrom&&(o.migrated_from=t.migratedFrom,t.migrated&&(o.migrated=t.migrated)),o.phases=n;let r={...o,...qe(t.previous,un)};return["","<details>","<summary>Machine-readable plan (roadmap/1)</summary>","","```roadmap-json",JSON.stringify(r,null,2),"```","","</details>"]}function ze(e,t={}){let s={generated:t.generated||Xe(),migratedFrom:t.migratedFrom||null,migrated:t.migrated||null,previous:t.previous&&typeof t.previous=="object"?t.previous:null},n=En(e);return n==null?null:[...Rn(n,s),...vn(n,s)].join(`
60
60
  `)+`
61
- `}function vn(e){let t=O.join(e,"ROADMAP.bak.md");if(!A.existsSync(t))return t;for(let s=2;s<=1e3;s++){let n=O.join(e,`ROADMAP.bak.${s}.md`);if(!A.existsSync(n))return n}return null}function On(e,t){let s=O.join(O.dirname(e),`.${O.basename(e)}.${process.pid}.tmp`);try{A.writeFileSync(s,t,"utf8"),A.renameSync(s,e)}catch(n){try{A.unlinkSync(s)}catch{}throw n}}function U(e,t,s={}){return{written:!1,reason:e,warning:`[cairn] ROADMAP.md was not updated \u2014 ${e}`,path:t,...s}}function et(e){let t=Ve(),s=null;if(A.existsSync(t))try{s=A.readFileSync(t,"utf8")}catch(u){return U(`the existing file could not be read (${u.message})`,t)}let n=cn(s),o=s!=null&&(n?.provider===ie||s.startsWith(nn));if(s!=null&&n&&!o)return{written:!1,reason:`ROADMAP.md is generated by another provider (${n.provider}) \u2014 refusing to overwrite`,conflict_provider:n.provider,warning:`[cairn] ROADMAP.md is published by "${n.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?Ge(s):null,i=o?dn(s):null,l=o&&typeof r?.migrated=="string"?r.migrated:null,f=typeof r?.generated=="string"?r.generated:null,a=ze(e,{generated:f||"1970-01-01T00:00:00Z",migratedFrom:i,migrated:l,previous:r});if(a==null){if(o)try{return A.unlinkSync(t),{written:!1,removed:!0,path:t}}catch(u){return U(`the stale projection could not be removed (${u.message})`,t)}return{written:!1,reason:"empty roadmap \u2014 no file written",path:t}}if(o&&s===a)return{written:!1,unchanged:!0,path:t,bytes:Buffer.byteLength(a)};let c=Xe(),p=null,m=null;if(s!=null&&!o){let u=vn(O.dirname(t));if(u==null)return U("no free ROADMAP.bak name was available to preserve the existing file",t);try{A.renameSync(t,u)}catch(d){return U(`the existing file could not be preserved (${d.message})`,t)}p=O.basename(u),m=u,i=p,l=c}a=ze(e,{generated:c,migratedFrom:i,migrated:l,previous:r});try{On(t,a)}catch(u){if(m)try{return A.renameSync(m,t),U(`write failed (${u.message}); the original ROADMAP.md was restored`,t)}catch{return U(`write failed (${u.message}); the original ROADMAP.md is at ${p} and must be restored by hand`,t,{backed_up_to:p})}return U(`write failed (${u.message})`,t)}let h={written:!0,path:t,bytes:Buffer.byteLength(a),format:z,generated:c};return p&&(h.backed_up_to=p,h.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.`),h}function jn(e){try{return et(e)}catch(t){process.stderr.write(`[cairn] ROADMAP.md publish skipped: ${t.message}
62
- `);let s=null;try{s=Ve()}catch{}return U(`publish failed (${t.message})`,s)}}function ae(e){let t=jn(e);return t&&t.warning?t.warning:null}var Pn=new Set(["open","in_progress"]);function Tn(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 Nn(e,t){let s=e.prepare("SELECT parent_phase_id, status FROM main.roadmap_phases WHERE id = ?"),n=e.prepare("UPDATE main.roadmap_phases SET status = 'in_progress' WHERE id = ? AND status = 'open'"),o=s.get(t);for(;o&&o.parent_phase_id!=null;)n.run(o.parent_phase_id),o=s.get(o.parent_phase_id)}function tt(e,t){Tn(e,t),e.prepare("UPDATE main.roadmap_phases SET status = 'in_progress' WHERE id = ? AND status = 'open'").run(t),Nn(e,t)}function Mn(e){let t=new Map,s;try{s=e.prepare("SELECT blocker_id, blocked_id FROM main.roadmap_phase_deps").all()}catch{return t}for(let n of s)t.has(n.blocked_id)||t.set(n.blocked_id,new Set),t.get(n.blocked_id).add(n.blocker_id);return t}function An(e,t,s){let n=t.get(e);if(!n||n.size===0)return[];let o=[];for(let r of n)s.get(r)!=="done"&&o.push(r);return o}function nt(e,t){let s=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(s.length===0)return{id:null,reason:"No phases \u2014 add a ## Phases section to .cairn/roadmap.md."};let n=new Map(s.map(p=>[p.id,p.status])),o=Mn(e),r=new Map;for(let p of s){let m=p.parent_phase_id??`root:${p.decision_name}`;r.has(m)||r.set(m,[]),r.get(m).push(p)}for(let p of r.values())p.sort((m,h)=>m.phase_number!=null&&h.phase_number!=null?m.phase_number-h.phase_number:m.position-h.position||m.id-h.id);let i=[],l=new Set;for(let p of s){if(p.parent_phase_id!=null||l.has(p.decision_name))continue;l.add(p.decision_name);let m=[],h=r.get(`root:${p.decision_name}`)||[];for(let u=h.length-1;u>=0;u--)m.push(h[u]);for(;m.length;){let u=m.pop();i.push(u);let d=r.get(u.id)||[];for(let g=d.length-1;g>=0;g--)m.push(d[g])}}let f=p=>Pn.has(p.status),a=[],c=0;if(t!=null){let p=i.findIndex(m=>m.id===t);c=p===-1?0:p+1}for(let p=c;p<i.length;p++){let m=i[p];if(!f(m))continue;let h=An(m.id,o,n);if(h.length>0){a.push({id:m.id,blockers:h});continue}let u=t!=null?s.find(_=>_.id===t):null,d=u&&u.decision_name!==m.decision_name,g;return t==null?g=`First actionable phase: ${m.decision_name??"(sub-task)"} / Phase ${m.phase_number??"\xB7"}.`:d?g=`Previous decision "${u.decision_name}" is complete \u2014 advanced to "${m.decision_name}".`:g=`Next open phase in "${m.decision_name??"(sub-task)"}" after id ${t}.`,a.length>0&&(g+=` Skipped ${a.length} dep-blocked phase(s): ${a.map(_=>`id ${_.id} (blocked by ${_.blockers.join(",")})`).join("; ")}.`),{id:m.id,reason:g,skipped_blocked:a}}return a.length>0?{id:null,reason:`All remaining open phases are blocked by unfinished dependencies: ${a.map(p=>`id ${p.id} (blocked by ${p.blockers.join(",")})`).join("; ")}.`,skipped_blocked:a}:{id:null,reason:"No actionable phases remaining."}}var Ln=new Set(["java","typescript","javascript","vue","python","sql"]);function Ee(e,t){let s={hookEventName:"PreToolUse",permissionDecision:"deny",permissionDecisionReason:e};t&&(s.additionalContext=t),process.stdout.write(JSON.stringify({decision:"block",reason:e,hookSpecificOutput:s})),process.exit(0)}var it={name:"cairn",contract:1,title:"cairn",slots:["roadmap"],provides:{roadmap:{path:"ROADMAP.md",format:z}},spec:"Project memory and roadmap. Publishes the plan as ROADMAP.md in the roadmap/1 slot format."};function Dn(){let e=S.join(G.homedir(),".claude.json");if(!y.existsSync(e))return!1;let t;try{t=JSON.parse(y.readFileSync(e,"utf8"))}catch{return!1}let s=t?.mcpServers?.cairn;if(!s||typeof s!="object")return!1;let n=s.terminalConnect,o=n?.provides?.roadmap?.format;if(n&&o===z)return!1;s.terminalConnect=it;try{return y.writeFileSync(e,JSON.stringify(t,null,2),"utf8"),!0}catch{return!1}}function at(e){let t=S.join(G.homedir(),".claude","projects",ue(e));return y.existsSync(t)?S.join(t,"memory"):null}function ct(e){try{return y.readdirSync(e).filter(t=>t.endsWith(".md"))}catch{return[]}}function Cn(e){let t=at(e);if(!t||!y.existsSync(t))return 0;let s=S.join(e,".cairn","memory");y.mkdirSync(s,{recursive:!0});let n=0;for(let o of ct(t)){let r=S.join(t,o),i;try{i=y.statSync(r)}catch{continue}if(!i.isFile())continue;let l=S.join(s,o),f=!y.existsSync(l);if(!f)try{f=i.mtimeMs>y.statSync(l).mtimeMs}catch{f=!0}if(f){y.copyFileSync(r,l);try{y.utimesSync(l,i.atime,i.mtime)}catch{}n++}}return n}function In(e){let t=at(e);if(!t||(y.mkdirSync(t,{recursive:!0}),ct(t).some(o=>o!=="MEMORY.md")))return 0;let s=S.join(e,".cairn","memory");if(!y.existsSync(s))return 0;let n=0;for(let o of y.readdirSync(s)){let r=S.join(s,o),i;try{i=y.statSync(r)}catch{continue}if(i.isFile()){y.copyFileSync(r,S.join(t,o));try{y.utimesSync(S.join(t,o),i.atime,i.mtime)}catch{}n++}}return n}function st(e,t){let s=new Date(t.checkpoint_at).toLocaleString(),r=[` message: ${/^Auto-checkpoint\b/.test(t.message||"")&&t.last_manual_message||t.message}`];r.push(` would_lose: ${fe(se(e,t))}`),r.push(` checkpoint: ${s}`),t.active_files?.length>0&&r.push(` active_files: ${t.active_files.join(", ")}`),t.notes?.length>0&&r.push(` notes: ${t.notes.join(" | ")}`);let i=a=>{try{return Fn(a,{cwd:e,encoding:"utf8",timeout:5e3,stdio:["ignore","pipe","ignore"]}).trim()}catch{return null}};try{let a=new Date(t.checkpoint_at).toISOString(),c=i(`git log --oneline --after="${a}"`);c!==null&&r.push(` commits_since: ${c?c.split(`
61
+ `}function On(e){let t=j.join(e,"ROADMAP.bak.md");if(!A.existsSync(t))return t;for(let s=2;s<=1e3;s++){let n=j.join(e,`ROADMAP.bak.${s}.md`);if(!A.existsSync(n))return n}return null}function jn(e,t){let s=j.join(j.dirname(e),`.${j.basename(e)}.${process.pid}.tmp`);try{A.writeFileSync(s,t,"utf8"),A.renameSync(s,e)}catch(n){try{A.unlinkSync(s)}catch{}throw n}}function U(e,t,s={}){return{written:!1,reason:e,warning:`[cairn] ROADMAP.md was not updated \u2014 ${e}`,path:t,...s}}function et(e){let t=Ve(),s=null;if(A.existsSync(t))try{s=A.readFileSync(t,"utf8")}catch(u){return U(`the existing file could not be read (${u.message})`,t)}let n=cn(s),o=s!=null&&(n?.provider===ie||s.startsWith(nn));if(s!=null&&n&&!o)return{written:!1,reason:`ROADMAP.md is generated by another provider (${n.provider}) \u2014 refusing to overwrite`,conflict_provider:n.provider,warning:`[cairn] ROADMAP.md is published by "${n.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?Ge(s):null,i=o?dn(s):null,l=o&&typeof r?.migrated=="string"?r.migrated:null,f=typeof r?.generated=="string"?r.generated:null,a=ze(e,{generated:f||"1970-01-01T00:00:00Z",migratedFrom:i,migrated:l,previous:r});if(a==null){if(o)try{return A.unlinkSync(t),{written:!1,removed:!0,path:t}}catch(u){return U(`the stale projection could not be removed (${u.message})`,t)}return{written:!1,reason:"empty roadmap \u2014 no file written",path:t}}if(o&&s===a)return{written:!1,unchanged:!0,path:t,bytes:Buffer.byteLength(a)};let c=Xe(),p=null,m=null;if(s!=null&&!o){let u=On(j.dirname(t));if(u==null)return U("no free ROADMAP.bak name was available to preserve the existing file",t);try{A.renameSync(t,u)}catch(d){return U(`the existing file could not be preserved (${d.message})`,t)}p=j.basename(u),m=u,i=p,l=c}a=ze(e,{generated:c,migratedFrom:i,migrated:l,previous:r});try{jn(t,a)}catch(u){if(m)try{return A.renameSync(m,t),U(`write failed (${u.message}); the original ROADMAP.md was restored`,t)}catch{return U(`write failed (${u.message}); the original ROADMAP.md is at ${p} and must be restored by hand`,t,{backed_up_to:p})}return U(`write failed (${u.message})`,t)}let h={written:!0,path:t,bytes:Buffer.byteLength(a),format:z,generated:c};return p&&(h.backed_up_to=p,h.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.`),h}function Pn(e){try{return et(e)}catch(t){process.stderr.write(`[cairn] ROADMAP.md publish skipped: ${t.message}
62
+ `);let s=null;try{s=Ve()}catch{}return U(`publish failed (${t.message})`,s)}}function ae(e){let t=Pn(e);return t&&t.warning?t.warning:null}var Tn=new Set(["open","in_progress"]);function Nn(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 Mn(e,t){let s=e.prepare("SELECT parent_phase_id, status FROM main.roadmap_phases WHERE id = ?"),n=e.prepare("UPDATE main.roadmap_phases SET status = 'in_progress' WHERE id = ? AND status = 'open'"),o=s.get(t);for(;o&&o.parent_phase_id!=null;)n.run(o.parent_phase_id),o=s.get(o.parent_phase_id)}function tt(e,t){Nn(e,t),e.prepare("UPDATE main.roadmap_phases SET status = 'in_progress' WHERE id = ? AND status = 'open'").run(t),Mn(e,t)}function An(e){let t=new Map,s;try{s=e.prepare("SELECT blocker_id, blocked_id FROM main.roadmap_phase_deps").all()}catch{return t}for(let n of s)t.has(n.blocked_id)||t.set(n.blocked_id,new Set),t.get(n.blocked_id).add(n.blocker_id);return t}function Fn(e,t,s){let n=t.get(e);if(!n||n.size===0)return[];let o=[];for(let r of n)s.get(r)!=="done"&&o.push(r);return o}function nt(e,t){let s=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(s.length===0)return{id:null,reason:"No phases \u2014 add a ## Phases section to .cairn/roadmap.md."};let n=new Map(s.map(p=>[p.id,p.status])),o=An(e),r=new Map;for(let p of s){let m=p.parent_phase_id??`root:${p.decision_name}`;r.has(m)||r.set(m,[]),r.get(m).push(p)}for(let p of r.values())p.sort((m,h)=>m.phase_number!=null&&h.phase_number!=null?m.phase_number-h.phase_number:m.position-h.position||m.id-h.id);let i=[],l=new Set;for(let p of s){if(p.parent_phase_id!=null||l.has(p.decision_name))continue;l.add(p.decision_name);let m=[],h=r.get(`root:${p.decision_name}`)||[];for(let u=h.length-1;u>=0;u--)m.push(h[u]);for(;m.length;){let u=m.pop();i.push(u);let d=r.get(u.id)||[];for(let g=d.length-1;g>=0;g--)m.push(d[g])}}let f=p=>Tn.has(p.status),a=[],c=0;if(t!=null){let p=i.findIndex(m=>m.id===t);c=p===-1?0:p+1}for(let p=c;p<i.length;p++){let m=i[p];if(!f(m))continue;let h=Fn(m.id,o,n);if(h.length>0){a.push({id:m.id,blockers:h});continue}let u=t!=null?s.find(_=>_.id===t):null,d=u&&u.decision_name!==m.decision_name,g;return t==null?g=`First actionable phase: ${m.decision_name??"(sub-task)"} / Phase ${m.phase_number??"\xB7"}.`:d?g=`Previous decision "${u.decision_name}" is complete \u2014 advanced to "${m.decision_name}".`:g=`Next open phase in "${m.decision_name??"(sub-task)"}" after id ${t}.`,a.length>0&&(g+=` Skipped ${a.length} dep-blocked phase(s): ${a.map(_=>`id ${_.id} (blocked by ${_.blockers.join(",")})`).join("; ")}.`),{id:m.id,reason:g,skipped_blocked:a}}return a.length>0?{id:null,reason:`All remaining open phases are blocked by unfinished dependencies: ${a.map(p=>`id ${p.id} (blocked by ${p.blockers.join(",")})`).join("; ")}.`,skipped_blocked:a}:{id:null,reason:"No actionable phases remaining."}}var Dn=new Set(["java","typescript","javascript","vue","python","sql"]);function Ee(e,t){let s={hookEventName:"PreToolUse",permissionDecision:"deny",permissionDecisionReason:e};t&&(s.additionalContext=t),process.stdout.write(JSON.stringify({decision:"block",reason:e,hookSpecificOutput:s})),process.exit(0)}var it={name:"cairn",contract:1,title:"cairn",slots:["roadmap"],provides:{roadmap:{path:"ROADMAP.md",format:z}},spec:"Project memory and roadmap. Publishes the plan as ROADMAP.md in the roadmap/1 slot format."};function Cn(){let e=S.join(G.homedir(),".claude.json");if(!y.existsSync(e))return!1;let t;try{t=JSON.parse(y.readFileSync(e,"utf8"))}catch{return!1}let s=t?.mcpServers?.cairn;if(!s||typeof s!="object")return!1;let n=s.terminalConnect,o=n?.provides?.roadmap?.format;if(n&&o===z)return!1;s.terminalConnect=it;try{return y.writeFileSync(e,JSON.stringify(t,null,2),"utf8"),!0}catch{return!1}}function at(e){let t=S.join(G.homedir(),".claude","projects",ue(e));return y.existsSync(t)?S.join(t,"memory"):null}function ct(e){try{return y.readdirSync(e).filter(t=>t.endsWith(".md"))}catch{return[]}}function In(e){let t=at(e);if(!t||!y.existsSync(t))return 0;let s=S.join(e,".cairn","memory");y.mkdirSync(s,{recursive:!0});let n=0;for(let o of ct(t)){let r=S.join(t,o),i;try{i=y.statSync(r)}catch{continue}if(!i.isFile())continue;let l=S.join(s,o),f=!y.existsSync(l);if(!f)try{f=i.mtimeMs>y.statSync(l).mtimeMs}catch{f=!0}if(f){y.copyFileSync(r,l);try{y.utimesSync(l,i.atime,i.mtime)}catch{}n++}}return n}function Wn(e){let t=at(e);if(!t||(y.mkdirSync(t,{recursive:!0}),ct(t).some(o=>o!=="MEMORY.md")))return 0;let s=S.join(e,".cairn","memory");if(!y.existsSync(s))return 0;let n=0;for(let o of y.readdirSync(s)){let r=S.join(s,o),i;try{i=y.statSync(r)}catch{continue}if(i.isFile()){y.copyFileSync(r,S.join(t,o));try{y.utimesSync(S.join(t,o),i.atime,i.mtime)}catch{}n++}}return n}function st(e,t){let s=new Date(t.checkpoint_at).toLocaleString(),r=[` message: ${/^Auto-checkpoint\b/.test(t.message||"")&&t.last_manual_message||t.message}`];r.push(` would_lose: ${fe(se(e,t))}`),r.push(` checkpoint: ${s}`),t.active_files?.length>0&&r.push(` active_files: ${t.active_files.join(", ")}`),t.notes?.length>0&&r.push(` notes: ${t.notes.join(" | ")}`);let i=a=>{try{return Ln(a,{cwd:e,encoding:"utf8",timeout:5e3,stdio:["ignore","pipe","ignore"]}).trim()}catch{return null}};try{let a=new Date(t.checkpoint_at).toISOString(),c=i(`git log --oneline --after="${a}"`);c!==null&&r.push(` commits_since: ${c?c.split(`
63
63
  `).join("; "):"(none)"}`)}catch{}let l=i("git diff --name-only HEAD");l&&r.push(` uncommitted: ${l.split(`
64
64
  `).join(", ")}`);let f=i("git status --short");return f&&r.push(` git_status:
65
65
  ${f.split(`
66
66
  `).map(a=>" "+a).join(`
67
67
  `)}`),r.push(" \u2192 Call cairn_resume for full index status and open todos."),r}async function ot(e){let t={active:!1},s=S.join(e,"index.db");if(!y.existsSync(s))return t;let n;try{({DatabaseSync:n}=await import("node:sqlite"))}catch{return t}let o;try{o=new n(s,{readonly:!0})}catch{return t}let r=l=>l.toLowerCase().replace(/[^a-z0-9]+/g,"_").replace(/^_|_$/g,"");function i(l,f){if(!y.existsSync(l))return null;let a=y.readFileSync(l,"utf8"),c=!1,p=!1,m=[];for(let h of a.split(/\r?\n/)){if(/^##\s+Phases\s*$/i.test(h)){c=!0;continue}if(c&&/^##\s+/.test(h)&&!/^###/.test(h))break;if(!c)continue;let u=/^###\s+Phase\s+(\d+)\s*:\s*(.+?)\s*$/.exec(h);if(u){if(p)break;parseInt(u[1],10)===f&&(m.push(`### Phase ${u[1]}: ${u[2]}`),p=!0);continue}p&&m.push(h)}return m.join(`
68
- `).trim()||null}try{let f=o.prepare("SELECT phase_id FROM roadmap_cursor WHERE singleton = 1").get()?.phase_id??null;if(f==null)return o.close(),t;let a=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(),c={open:"[ ]",in_progress:"[~]",done:"[x]",blocked:"[!]",stale:"[s]"},p=new Map;for(let k of a){let w=k.parent_phase_id??"root";p.has(w)||p.set(w,[]),p.get(w).push(k)}for(let k of p.values())k.sort((w,b)=>w.decision_name&&b.decision_name&&w.decision_name!==b.decision_name?w.decision_name.localeCompare(b.decision_name):w.phase_number!=null&&b.phase_number!=null?w.phase_number-b.phase_number:w.position-b.position||w.id-b.id);let m=p.get("root")||[],h=new Map;for(let k of m)k.decision_name!=null&&(h.has(k.decision_name)||h.set(k.decision_name,[]),h.get(k.decision_name).push(k));let u=[],d=(k,w)=>{for(let b of p.get(k)||[]){if(b.status==="stale")continue;let ce=f===b.id?" \u2192":"",le=b.phase_number!=null?`Phase ${b.phase_number}: ${b.text}`:b.text;u.push(`${" ".repeat(w)}${c[b.status]||"[?]"}${ce} ${le}`),d(b.id,w+1)}};for(let[k,w]of h){let b=w.filter(v=>v.status!=="stale");if(b.length===0)continue;let ce=b.every(v=>v.status==="done"),le=b.some(v=>v.status==="in_progress"),lt=ce?"done":le?"in_progress":"open",ut=b.filter(v=>v.status==="done").length,pt=k===j?"Roadmap":`Decision: ${k}`;u.push(`${c[lt]} ${pt} (${ut}/${b.length} done)`);for(let v of w){if(v.status==="stale"||k===j&&v.status==="done")continue;let dt=f===v.id?" \u2192":"";u.push(` ${c[v.status]||"[?]"}${dt} Phase ${v.phase_number}: ${v.text}`),d(v.id,2)}}let g=k=>a.find(w=>w.id===k),_=g(f),x=_?.decision_name??null,R=_?.phase_number??null,T=_;for(;T&&T.parent_phase_id!=null&&(T=g(T.parent_phase_id),!!T);)x=T.decision_name??x,R=T.phase_number??R;o.close();let C=_?_.phase_number!=null?`${_.decision_name} / Phase ${_.phase_number}: ${_.text}`:`${x} / Phase ${R} \u2192 ${_.text}`:"(unknown)",H=S.join(e,".last-roadmap-cursor"),$e=null;try{$e=parseInt(y.readFileSync(H,"utf8").trim(),10)}catch{}let Re=$e!==f;try{y.writeFileSync(H,String(f),"utf8")}catch{}let ve=null;if(Re&&x!=null&&R!=null){let k=x===j?S.join(e,"roadmap.md"):S.join(e,"memory",`decision_${r(x)}.md`),w=i(k,R);w&&(ve=w.split(`
68
+ `).trim()||null}try{let f=o.prepare("SELECT phase_id FROM roadmap_cursor WHERE singleton = 1").get()?.phase_id??null;if(f==null)return o.close(),t;let a=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(),c={open:"[ ]",in_progress:"[~]",done:"[x]",blocked:"[!]",stale:"[s]"},p=new Map;for(let k of a){let w=k.parent_phase_id??"root";p.has(w)||p.set(w,[]),p.get(w).push(k)}for(let k of p.values())k.sort((w,b)=>w.decision_name&&b.decision_name&&w.decision_name!==b.decision_name?w.decision_name.localeCompare(b.decision_name):w.phase_number!=null&&b.phase_number!=null?w.phase_number-b.phase_number:w.position-b.position||w.id-b.id);let m=p.get("root")||[],h=new Map;for(let k of m)k.decision_name!=null&&(h.has(k.decision_name)||h.set(k.decision_name,[]),h.get(k.decision_name).push(k));let u=[],d=(k,w)=>{for(let b of p.get(k)||[]){if(b.status==="stale")continue;let ce=f===b.id?" \u2192":"",le=b.phase_number!=null?`Phase ${b.phase_number}: ${b.text}`:b.text;u.push(`${" ".repeat(w)}${c[b.status]||"[?]"}${ce} ${le}`),d(b.id,w+1)}};for(let[k,w]of h){let b=w.filter(v=>v.status!=="stale");if(b.length===0)continue;let ce=b.every(v=>v.status==="done"),le=b.some(v=>v.status==="in_progress"),lt=ce?"done":le?"in_progress":"open",ut=b.filter(v=>v.status==="done").length,pt=k===O?"Roadmap":`Decision: ${k}`;u.push(`${c[lt]} ${pt} (${ut}/${b.length} done)`);for(let v of w){if(v.status==="stale"||k===O&&v.status==="done")continue;let dt=f===v.id?" \u2192":"";u.push(` ${c[v.status]||"[?]"}${dt} Phase ${v.phase_number}: ${v.text}`),d(v.id,2)}}let g=k=>a.find(w=>w.id===k),_=g(f),x=_?.decision_name??null,R=_?.phase_number??null,T=_;for(;T&&T.parent_phase_id!=null&&(T=g(T.parent_phase_id),!!T);)x=T.decision_name??x,R=T.phase_number??R;o.close();let C=_?_.phase_number!=null?`${_.decision_name} / Phase ${_.phase_number}: ${_.text}`:`${x} / Phase ${R} \u2192 ${_.text}`:"(unknown)",H=S.join(e,".last-roadmap-cursor"),$e=null;try{$e=parseInt(y.readFileSync(H,"utf8").trim(),10)}catch{}let Re=$e!==f;try{y.writeFileSync(H,String(f),"utf8")}catch{}let ve=null;if(Re&&x!=null&&R!=null){let k=x===O?S.join(e,"roadmap.md"):S.join(e,"memory",`decision_${r(x)}.md`),w=i(k,R);w&&(ve=w.split(`
69
69
  `))}return{active:!0,currentLabel:C,treeLines:u,advanced:Re,memoLines:ve}}catch{try{o?.close()}catch{}return t}}process.on("uncaughtException",e=>{process.stderr.write(`cairn: ${e.message}
70
- `),process.exit(0)});var P=process.argv[2];if(P==="--version"||P==="-v"){let e=JSON.parse(y.readFileSync(new URL("../package.json",import.meta.url),"utf8"));console.log(e.version),process.exit(0)}else if(P==="minify"){let e="";process.stdin.setEncoding("utf8"),process.stdin.on("data",t=>{e+=t}),process.stdin.on("end",()=>{let t,s;try{s=JSON.parse(e)?.tool_input||{},t=s.file_path}catch{process.exit(0)}t||process.exit(0),(s.offset!=null||s.limit!=null)&&process.exit(0),t=S.resolve(t);let n=he(t);n||process.exit(0);let o=S.join(n,"views"),r=S.relative(o,t);!r.startsWith("..")&&!S.isAbsolute(r)&&process.exit(0);let i=B(n),f=oe(n,i)>0,a=i[t];a?.state==="edit-ready"&&(f&&M(n,i),process.exit(0)),a?.state==="compressed"&&(a.state="edit-ready",M(n,i),process.exit(0));let c=S.extname(t).toLowerCase(),p=Oe[c];(!p||!Ln.has(p))&&(f&&M(n,i),process.exit(0));let m;try{m=y.readFileSync(t,"utf8")}catch{f&&M(n,i),process.exit(0)}let h=m.split(`
70
+ `),process.exit(0)});var P=process.argv[2];if(P==="--version"||P==="-v"){let e=JSON.parse(y.readFileSync(new URL("../package.json",import.meta.url),"utf8"));console.log(e.version),process.exit(0)}else if(P==="minify"){let e="";process.stdin.setEncoding("utf8"),process.stdin.on("data",t=>{e+=t}),process.stdin.on("end",()=>{let t,s;try{s=JSON.parse(e)?.tool_input||{},t=s.file_path}catch{process.exit(0)}t||process.exit(0),(s.offset!=null||s.limit!=null)&&process.exit(0),t=S.resolve(t);let n=he(t);n||process.exit(0);let o=S.join(n,"views"),r=S.relative(o,t);!r.startsWith("..")&&!S.isAbsolute(r)&&process.exit(0);let i=B(n),f=oe(n,i)>0,a=i[t];a?.state==="edit-ready"&&(f&&M(n,i),process.exit(0)),a?.state==="compressed"&&(a.state="edit-ready",M(n,i),process.exit(0));let c=S.extname(t).toLowerCase(),p=Oe[c];(!p||!Dn.has(p))&&(f&&M(n,i),process.exit(0));let m;try{m=y.readFileSync(t,"utf8")}catch{f&&M(n,i),process.exit(0)}let h=m.split(`
71
71
  `).length,u=p==="vue"?je(m):te(m,p),d=u.split(`
72
72
  `).length,g=h>0?Math.round((1-d/h)*100):0,_=De(n,t);y.mkdirSync(S.dirname(_),{recursive:!0}),y.writeFileSync(_,u,"utf8");let x=a?.readCount??0;i[t]={tempPath:_,state:"compressed",minifiedAt:y.statSync(t).mtimeMs,readCount:x+1},M(n,i);let R=d>Le,T=x>0;if(T||R){let C=Ce(u,p),H=T?`[cairn: ${h} \u2192 ${d} lines | seen \xD7${x+1} this session]`:`[cairn: ${h} \u2192 outline (${d} minified lines)]`;process.stderr.write(`${H}
73
73
  ${C}
@@ -80,7 +80,7 @@ Cached view: Read("${_}")
80
80
 
81
81
  ${c}`):Ee(`[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(P==="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 s=t?.tool_name||"",n=t?.tool_input||{},o=process.cwd(),r=S.join(o,".cairn");y.existsSync(r)||process.exit(0);let i=S.join(r,"index.db");y.existsSync(i)||process.exit(0);let l;try{({DatabaseSync:l}=await import("node:sqlite"))}catch{process.exit(0)}let f=/exit.?plan.?mode/i.test(s),a=/todo.?write/i.test(s);!f&&!a&&process.exit(0);let c=null,p;try{if(f){let h=n.plan;(typeof h!="string"||!h.trim())&&process.exit(0);let u=He(h);if(p=new l(i),X(p),(p.prepare("SELECT phase_id FROM main.roadmap_cursor WHERE singleton = 1").get()?.phase_id??null)==null){let g=nt(p,null);g.id!=null&&tt(p,g.id)}u.added.length>0&&(c=`[cairn] roadmap: captured ${u.added.length} phase(s) from plan${u.skipped.length?` (${u.skipped.length} already present)`:""}.`)}else if(a){p=new l(i);let h=Je(p,n.todos);h.seeded?c=`[cairn] roadmap: seeded ${h.seeded} phase(s) from todos.`:h.inserted>0&&(c=`[cairn] roadmap: linked ${h.inserted} new sub-task(s) to the current phase.`)}}catch(h){process.stderr.write(`[cairn] roadmap-capture skipped: ${h.message}
82
82
  `);try{p?.close()}catch{}process.exit(0)}let m=p?ae(p):null;try{p?.close()}catch{}m&&(c=c?`${c} ${m}`:m),c&&process.stdout.write(JSON.stringify({systemMessage:c})),process.exit(0)})}else if(P==="validate-map"){let e=S.join(process.cwd(),".cairn");y.existsSync(e)||process.exit(0);let t=B(e),s=oe(e,t);for(let n of Object.values(t))n.readCount=0;M(e,t),s>0&&process.stdout.write(`[cairn] Cleaned minify map: ${s} stale entr${s===1?"y":"ies"} removed
83
- `),process.exit(0)}else if(P==="checkpoint"&&process.argv[3]==="--auto"){let e=`Auto-checkpoint at ${new Date().toISOString()}`,t=[],s=[];try{let n=S.join(process.cwd(),".cairn"),o=S.join(n,"session.json");y.existsSync(o)&&(t=JSON.parse(y.readFileSync(o,"utf8")).notes||[]);let r=B(n);s=Object.entries(r).filter(([,i])=>(i.readCount??0)>0).map(([i])=>i)}catch{}me(null,{message:e,active_files:s,notes:t,auto:!0}),process.exit(0)}else if(P==="session-tick"){let e=process.cwd(),t=S.join(e,".cairn");y.existsSync(t)||process.exit(0);let s={};if(!process.stdin.isTTY)try{s=JSON.parse(y.readFileSync(0,"utf8"))||{}}catch{}let n=[];try{let a=`Auto-checkpoint at ${new Date().toISOString()}`,c=[],p=S.join(t,"session.json");if(y.existsSync(p))try{c=JSON.parse(y.readFileSync(p,"utf8")).notes||[]}catch{}let m=B(t),h=Object.entries(m).filter(([,u])=>(u.readCount??0)>0).map(([u])=>u);me(null,{message:a,active_files:h,notes:c,auto:!0}),n.push("\u2713 checkpoint saved")}catch{}try{let a=Cn(e);a>0&&n.push(`memory backed up (${a})`)}catch{}let o=[],r=null;try{let a=S.join(t,"index.db");if(y.existsSync(a)){let{DatabaseSync:c}=await import("node:sqlite"),p=new c(a);try{X(p),r=ae(p);try{o=Be(p,e).candidates}catch{}}finally{p.close()}}}catch{}let i="";try{let a=await ot(t);if(a.active){n.push(`roadmap ${a.advanced?"updated":"on track"}: ${a.currentLabel}`);let c=["[cairn] Production roadmap (\u2192 = cursor / current phase):",...a.treeLines.map(p=>" "+p)];a.memoLines&&c.push("","\u2500\u2500 Current phase detail (from roadmap.md) \u2500\u2500",...a.memoLines,"\u2500\u2500 (re-read after each phase to confirm direction) \u2500\u2500"),c.push("","Keep this current via cairn_roadmap on MATERIAL change (phase done / blocked / new) \u2014 not every turn."),i=c.join(`
83
+ `),process.exit(0)}else if(P==="checkpoint"&&process.argv[3]==="--auto"){let e=`Auto-checkpoint at ${new Date().toISOString()}`,t=[],s=[];try{let n=S.join(process.cwd(),".cairn"),o=S.join(n,"session.json");y.existsSync(o)&&(t=JSON.parse(y.readFileSync(o,"utf8")).notes||[]);let r=B(n);s=Object.entries(r).filter(([,i])=>(i.readCount??0)>0).map(([i])=>i)}catch{}me(null,{message:e,active_files:s,notes:t,auto:!0}),process.exit(0)}else if(P==="session-tick"){let e=process.cwd(),t=S.join(e,".cairn");y.existsSync(t)||process.exit(0);let s={};if(!process.stdin.isTTY)try{s=JSON.parse(y.readFileSync(0,"utf8"))||{}}catch{}let n=[];try{let a=`Auto-checkpoint at ${new Date().toISOString()}`,c=[],p=S.join(t,"session.json");if(y.existsSync(p))try{c=JSON.parse(y.readFileSync(p,"utf8")).notes||[]}catch{}let m=B(t),h=Object.entries(m).filter(([,u])=>(u.readCount??0)>0).map(([u])=>u);me(null,{message:a,active_files:h,notes:c,auto:!0}),n.push("\u2713 checkpoint saved")}catch{}try{let a=In(e);a>0&&n.push(`memory backed up (${a})`)}catch{}let o=[],r=null;try{let a=S.join(t,"index.db");if(y.existsSync(a)){let{DatabaseSync:c}=await import("node:sqlite"),p=new c(a);try{X(p),r=ae(p);try{o=Be(p,e).candidates}catch{}}finally{p.close()}}}catch{}let i="";try{let a=await ot(t);if(a.active){n.push(`roadmap ${a.advanced?"updated":"on track"}: ${a.currentLabel}`);let c=["[cairn] Production roadmap (\u2192 = cursor / current phase):",...a.treeLines.map(p=>" "+p)];a.memoLines&&c.push("","\u2500\u2500 Current phase detail (from roadmap.md) \u2500\u2500",...a.memoLines,"\u2500\u2500 (re-read after each phase to confirm direction) \u2500\u2500"),c.push("","Keep this current via cairn_roadmap on MATERIAL change (phase done / blocked / new) \u2014 not every turn."),i=c.join(`
84
84
  `)}}catch{}if(r&&(n.push("ROADMAP.md not updated \u2014 see warning"),i=i?`${i}
85
85
 
86
86
  ${r}`:r),o.length>0){n.push(`${o.length} phase(s) may be done \u2014 confirm with user`);let a=["","[cairn] Possible COMPLETED phase(s) detected from recent commits \u2014","CONFIRM WITH THE USER before marking done; do NOT mark done unilaterally:"];for(let c of o)a.push(` \u2022 Phase ${c.phase_number}: ${c.text} (commit ${c.commit}, ${c.confidence}) \u2192 if confirmed: cairn_roadmap set_status ${c.phase_id} done`);i=i?i+`
@@ -90,9 +90,9 @@ ${r}`:r),o.length>0){n.push(`${o.length} phase(s) may be done \u2014 confirm wit
90
90
  ${c.line}
91
91
  ${c.guidance}`;i=i?i+`
92
92
  `+p:p.trimStart()}}catch{}n.length===0&&!l&&process.exit(0);let f={systemMessage:n.length>0?`[cairn] ${n.join(" \xB7 ")}${l?`
93
- ${l.line}`:""}`:l.line};i&&(f.additionalContext=i),process.stdout.write(JSON.stringify(f)),process.exit(0)}else if(P==="memory-restore"){let e=process.cwd();y.existsSync(S.join(e,".cairn"))||process.exit(0);try{let t=In(e);t>0&&process.stdout.write(`[cairn] Restored ${t} memory file(s) from .cairn/memory backup (Claude store was empty)
93
+ ${l.line}`:""}`:l.line};i&&(f.additionalContext=i),process.stdout.write(JSON.stringify(f)),process.exit(0)}else if(P==="memory-restore"){let e=process.cwd();y.existsSync(S.join(e,".cairn"))||process.exit(0);try{let t=Wn(e);t>0&&process.stdout.write(`[cairn] Restored ${t} memory file(s) from .cairn/memory backup (Claude store was empty)
94
94
  `)}catch{}process.exit(0)}else if(P==="resume-hint"){let e=S.join(process.cwd(),".cairn"),t=S.join(e,"session.json"),s=S.join(e,"index.db"),n=S.join(e,".hint-lock"),o=1800*1e3,r=process.cwd()===G.homedir();if(!r&&y.existsSync(e)&&(y.existsSync(s)||y.existsSync(t))){let d=S.join(e,".cairn-project");if(!y.existsSync(d))try{y.writeFileSync(d,"","utf8"),process.stdout.write(`[cairn] Repaired: wrote .cairn-project sentinel for hook compatibility.
95
- `)}catch{}}if(!r&&y.existsSync(e))try{Dn()&&process.stdout.write(`[cairn] Repaired: declared the roadmap slot (${z}) in ~/.claude.json.
95
+ `)}catch{}}if(!r&&y.existsSync(e))try{Cn()&&process.stdout.write(`[cairn] Repaired: declared the roadmap slot (${z}) in ~/.claude.json.
96
96
  `)}catch{}if(!r&&y.existsSync(s))try{let{DatabaseSync:d}=await import("node:sqlite"),g=new d(s,{readonly:!0}),_=null;try{_=ae(g)}finally{g.close()}_&&process.stdout.write(`${_}
97
97
  `)}catch{}{let d=[S.join(process.cwd(),".claude","settings.json"),S.join(G.homedir(),".claude","settings.json")];for(let g of d)try{if(!y.existsSync(g))continue;let _=y.readFileSync(g,"utf8"),x=JSON.parse(_),R=x?.hooks?.PreToolUse;if(!Array.isArray(R))continue;let T=!1;for(let C of R)if(!(C.matcher!=="Edit"||!Array.isArray(C.hooks)))for(let H of C.hooks)H.command==="cairn set-mode edit"&&(H.command="cairn edit-guard",T=!0);T&&(y.writeFileSync(g,JSON.stringify(x,null,2),"utf8"),process.stdout.write(`[cairn] Repaired: migrated cairn set-mode \u2192 cairn edit-guard in ${g}
98
98
  `))}catch{}}let i="";if(!process.stdin.isTTY)try{i=(JSON.parse(y.readFileSync(0,"utf8"))?.prompt||"").toLowerCase()}catch{}let f=/\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(i);y.existsSync(t)||(y.existsSync(s)||(y.existsSync(n)&&Date.now()-y.statSync(n).mtimeMs<o&&process.exit(0),y.mkdirSync(e,{recursive:!0}),y.writeFileSync(n,new Date().toISOString(),"utf8"),process.stdout.write(`[cairn] No index found. Run cairn_maintain to index this project.
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import{Server as ii}from"@modelcontextprotocol/sdk/server/index.js";import{StdioServerTransport as ri}from"@modelcontextprotocol/sdk/server/stdio.js";import{CallToolRequestSchema as oi,ListToolsRequestSchema as ai}from"@modelcontextprotocol/sdk/types.js";import{DatabaseSync as Ae}from"node:sqlite";import ge from"fs";import A from"path";import j from"fs";import kn from"os";import W from"path";var ve=null;function N(){return ve||process.cwd()}function Le(t){let e=ve;return ve=t,e}function D(){let t=W.join(N(),".cairn");j.mkdirSync(W.join(t,"bundles"),{recursive:!0});let e=W.join(t,".cairn-project");return j.existsSync(e)||j.writeFileSync(e,"","utf8"),t}function vn(t){return t.replace(/[:/\\]/g,"-").replace(/^-+/,"")}function $e(){let t=W.join(kn.homedir(),".claude","projects",vn(N()));return j.existsSync(t)?W.join(t,"memory"):null}function nt(t){let e=W.join(t,"MEMORY.md");j.existsSync(e)||j.writeFileSync(e,`# Memory Index
1
+ import{Server as ri}from"@modelcontextprotocol/sdk/server/index.js";import{StdioServerTransport as oi}from"@modelcontextprotocol/sdk/server/stdio.js";import{CallToolRequestSchema as ai,ListToolsRequestSchema as ci}from"@modelcontextprotocol/sdk/types.js";import{DatabaseSync as Ae}from"node:sqlite";import ge from"fs";import A from"path";import j from"fs";import kn from"os";import W from"path";var ve=null;function N(){return ve||process.cwd()}function Le(t){let e=ve;return ve=t,e}function D(){let t=W.join(N(),".cairn");j.mkdirSync(W.join(t,"bundles"),{recursive:!0});let e=W.join(t,".cairn-project");return j.existsSync(e)||j.writeFileSync(e,"","utf8"),t}function vn(t){return t.replace(/[:/\\]/g,"-").replace(/^-+/,"")}function $e(){let t=W.join(kn.homedir(),".claude","projects",vn(N()));return j.existsSync(t)?W.join(t,"memory"):null}function nt(t){let e=W.join(t,"MEMORY.md");j.existsSync(e)||j.writeFileSync(e,`# Memory Index
2
2
  `,"utf8")}function ee(){let t=W.join(D(),"memory");j.mkdirSync(t,{recursive:!0}),nt(t);let e=$e();if(!e)return t;j.mkdirSync(e,{recursive:!0}),nt(e);let n=s=>j.readdirSync(s).some(i=>i.endsWith(".md")&&i!=="MEMORY.md");if(!n(e)&&n(t))for(let s of j.readdirSync(t)){let i=W.join(t,s);try{if(!j.statSync(i).isFile())continue;let r=j.statSync(i);j.copyFileSync(i,W.join(e,s));try{j.utimesSync(W.join(e,s),r.atime,r.mtime)}catch{}}catch{}}return e}var Ln=`
3
3
  CREATE TABLE IF NOT EXISTS files (
4
4
  id INTEGER PRIMARY KEY,
@@ -156,9 +156,9 @@ import{Server as ii}from"@modelcontextprotocol/sdk/server/index.js";import{Stdio
156
156
 
157
157
  `)}async function Mt(t={}){let{bundleName:e="default",noComments:n=!0,noEmptyLines:s=!0,noStyle:i=!1,aggressive:r=!1,onlyChanged:o=!1,filterLang:a=null,filterPaths:c=null,maxSizeKB:p=800}=t,l=se.join(D(),"bundles"),u=process.cwd(),m=se.join(l,`${e}.txt`),f=se.join(l,`${e}.mtime.json`),g=o&&ne.existsSync(f)?JSON.parse(ne.readFileSync(f,"utf8")):{},d={},h=[],y={processed:0,skipped_unchanged:0,skipped_size:0,original_bytes:0,compressed_bytes:0,by_language:{}},_=a?Array.isArray(a)?a:[a]:null,x=await q(u);for(let E of x){let R=se.extname(E).toLowerCase(),O=K[R];if(!O||_&&!_.includes(O))continue;if(c){let fe=se.relative(u,E).replace(/\\/g,"/");if(!c.some(he=>fe.startsWith(he)))continue}let L=ne.statSync(E).mtimeMs;if(d[E]=L,o&&g[E]===L){y.skipped_unchanged++;continue}if(Buffer.byteLength(h.join(`
158
158
  `),"utf8")/1024>p){y.skipped_size++;continue}let w;try{w=ne.readFileSync(E,"utf8")}catch{continue}y.original_bytes+=Buffer.byteLength(w,"utf8");let T=O==="vue"?Se(w,{noStyle:i,noComments:n,noEmptyLines:s,aggressive:r}):te(w,O,{noComments:n,noEmptyLines:s,aggressive:r});if(!T.trim())continue;let ke=se.relative(u,E).replace(/\\/g,"/");h.push(`### File: ${ke}`),h.push(T.trim()),h.push(""),y.compressed_bytes+=Buffer.byteLength(T,"utf8"),y.processed++,y.by_language[O]=(y.by_language[O]||0)+1}let b=h.join(`
159
- `);ne.writeFileSync(m,b,"utf8"),o&&ne.writeFileSync(f,JSON.stringify(d),"utf8");let C=y.original_bytes>0?Math.round((1-y.compressed_bytes/y.original_bytes)*100):0;return{bundle_path:m,processed:y.processed,skipped_unchanged:y.skipped_unchanged,skipped_size_cap:y.skipped_size,original_kb:Math.round(y.original_bytes/1024),compressed_kb:Math.round(y.compressed_bytes/1024),reduction_pct:C,by_language:y.by_language}}async function Ft(t,e){let{bundle_name:n="default",filter_paths:s=null,filter_language:i=null,no_comments:r=!0,no_empty_lines:o=!0,no_style:a=!1,aggressive:c=!1,only_changed:p=!1,max_size_kb:l=800}=e,u=await Mt({bundleName:n,noComments:r,noEmptyLines:o,noStyle:a,aggressive:c,onlyChanged:p,filterLang:i,filterPaths:s,maxSizeKB:l});return u.next_step=`Read the bundle: Read("${u.bundle_path}")`,{content:[{type:"text",text:JSON.stringify(u,null,2)}]}}import B from"fs";import ie from"path";import{execSync as zn}from"child_process";var k="would_be_lost",jt="What would a fresh session NOT know? Name what lives only in this conversation and nowhere else: work you finished but never verified, a thread you are in the middle of, a decision made for a reason that is not in the commit message or the code, a thing you tried that did not work. Write it for someone who never saw this session. Omit only when there is genuinely nothing to carry \u2014 this is the one part of a checkpoint that cannot be recovered from git or the index.",Nr=2*1024*1024;var kr=[256*1024,1024*1024];var Vn=220;function Kn(t,e=Vn){let n=String(t).replace(/\s+/g," ").trim();return n.length<=e?n:n.slice(0,e-1)+"\u2026"}function Qn(t,e){try{let n=zn(`git log --oneline --after="${e}"`,{cwd:t,encoding:"utf8",timeout:5e3,stdio:["ignore","pipe","ignore"]}).trim();return n?n.split(`
159
+ `);ne.writeFileSync(m,b,"utf8"),o&&ne.writeFileSync(f,JSON.stringify(d),"utf8");let C=y.original_bytes>0?Math.round((1-y.compressed_bytes/y.original_bytes)*100):0;return{bundle_path:m,processed:y.processed,skipped_unchanged:y.skipped_unchanged,skipped_size_cap:y.skipped_size,original_kb:Math.round(y.original_bytes/1024),compressed_kb:Math.round(y.compressed_bytes/1024),reduction_pct:C,by_language:y.by_language}}async function Ft(t,e){let{bundle_name:n="default",filter_paths:s=null,filter_language:i=null,no_comments:r=!0,no_empty_lines:o=!0,no_style:a=!1,aggressive:c=!1,only_changed:p=!1,max_size_kb:l=800}=e,u=await Mt({bundleName:n,noComments:r,noEmptyLines:o,noStyle:a,aggressive:c,onlyChanged:p,filterLang:i,filterPaths:s,maxSizeKB:l});return u.next_step=`Read the bundle: Read("${u.bundle_path}")`,{content:[{type:"text",text:JSON.stringify(u,null,2)}]}}import B from"fs";import ie from"path";import{execSync as zn}from"child_process";var k="would_be_lost",jt="What would a fresh session NOT know? Name what lives only in this conversation and nowhere else: work you finished but never verified, a thread you are in the middle of, a decision made for a reason that is not in the commit message or the code, a thing you tried that did not work. Write it for someone who never saw this session. Omit only when there is genuinely nothing to carry \u2014 this is the one part of a checkpoint that cannot be recovered from git or the index.",kr=2*1024*1024;var vr=[256*1024,1024*1024];var Vn=220;function Kn(t,e=Vn){let n=String(t).replace(/\s+/g," ").trim();return n.length<=e?n:n.slice(0,e-1)+"\u2026"}function Qn(t,e){try{let n=zn(`git log --oneline --after="${e}"`,{cwd:t,encoding:"utf8",timeout:5e3,stdio:["ignore","pipe","ignore"]}).trim();return n?n.split(`
160
160
  `).length:0}catch{return null}}function Dt(t,e){let n=typeof e?.[k]=="string"?e[k].trim():"";if(!n)return{recorded:!1,text:"",at:null,commits:null,stale:!1};let s=e[`${k}_at`]||e.checkpoint_at||null,i=s?Qn(t,s):null;return{recorded:!0,text:n,at:s,commits:i,stale:(i??0)>0}}function Pt(t){if(!t.recorded)return"No handoff recorded \u2014 nothing was written down about what the last session would lose.";let e=t.stale?` (written before the ${t.commits} commit(s) since \u2014 may be stale)`:"";return`Carried forward: ${Kn(t.text)}${e}`}var Zn=1e3,es=100*1024;function ts(t,e,n,s){if(t&&typeof t=="object"&&t.ref)return t;let i=typeof t=="string"?t:String(t),r=/^\[\d{4}-\d{2}-\d{2}\]/.test(i)?i:`[${n}] ${i}`;if(r.length<=Zn)return r;let o=ie.join(e,"notes");B.mkdirSync(o,{recursive:!0});let c=`note-${new Date().toISOString().replace(/[:.]/g,"-")}-${s}.md`,p=ie.join(o,c);return B.writeFileSync(p,r,"utf8"),{description:r.split(`
161
- `)[0].slice(0,120),ref:ie.join(".cairn","notes",c)}}function Ut(t,{message:e,active_files:n=[],notes:s=[],auto:i=!1,...r}){if(typeof s=="string")try{s=JSON.parse(s)}catch{s=s?[s]:[]}Array.isArray(s)||(s=[]);let o=D(),a=ie.join(o,"session.json"),c=new Date().toISOString().slice(0,10),p=new Date().toISOString(),l={};try{l=JSON.parse(B.readFileSync(a,"utf8"))||{}}catch{}let u=typeof r[k]=="string"&&r[k].trim()?r[k].trim():null,m=u??(typeof l[k]=="string"?l[k]:null),f=u?p:l[`${k}_at`]??null,g={};try{let y=t.prepare("SELECT path FROM main.files").all();for(let{path:_}of y)try{g[_]=B.statSync(_).mtimeMs}catch{}}catch{}for(let y of n)try{g[y]=B.statSync(y).mtimeMs}catch{}let d=s.map((y,_)=>ts(y,o,c,_));if(B.existsSync(a))try{if(B.statSync(a).size>es){let _=1;for(;B.existsSync(ie.join(o,`session.${_}.json`));)_++;B.renameSync(a,ie.join(o,`session.${_}.json`))}}catch{}let h={message:e,checkpoint_at:p,last_manual_checkpoint_at:i?l.last_manual_checkpoint_at??null:p,last_manual_message:i?l.last_manual_message??null:e,active_files:n,notes:d,[k]:m,[`${k}_at`]:f,mtime_snapshot:g};return B.writeFileSync(a,JSON.stringify(h,null,2),"utf8"),{content:[{type:"text",text:JSON.stringify({saved:!0,checkpoint_at:h.checkpoint_at,active_files_tracked:n.length,notes_saved:d.length,[k]:m?u?"recorded":"carried forward from the previous checkpoint":`not recorded \u2014 nothing will tell the next session what this one would lose (pass ${k})`},null,2)}]}}import z from"fs";import Oe from"path";import{exec as Xs}from"child_process";import{promisify as Ys}from"util";import Q from"fs";import Ht from"path";function Wt(t){try{return JSON.parse(Q.readFileSync(Ht.join(t,"minify-map.json"),"utf8"))}catch{return{}}}function De(t,e){Q.mkdirSync(t,{recursive:!0}),Q.writeFileSync(Ht.join(t,"minify-map.json"),JSON.stringify(e,null,2),"utf8")}function qt(t,e){let n=0;for(let[s,i]of Object.entries(e)){let r=!1;if(!Q.existsSync(s))r=!0;else if(!Q.existsSync(i.tempPath))r=!0;else try{Q.statSync(s).mtimeMs>i.minifiedAt&&(r=!0)}catch{r=!0}if(r){if(i.tempPath)try{Q.unlinkSync(i.tempPath)}catch{}delete e[s],n++}}return n}import P from"fs";import re from"path";var Jt=["preference","experience"],Bt=["preference","experience","decision","knowledge"];function Xt(t){if(!t||typeof t!="string")return[];let e=t.split(/\r?\n/),n=!1,s=null,i=[],r=new Set;for(let o of e){if(/^##\s+Phases\s*$/i.test(o)){n=!0;continue}if(/^##\s+/.test(o)&&!/^###/.test(o)){s&&(i.push(s),s=null),n=!1;continue}if(!n)continue;let a=/^###\s+Phase\s+(\d+)\s*:\s*(.+?)\s*$/.exec(o);if(a){s&&i.push(s);let c=parseInt(a[1],10),p=a[2];if(r.has(c)){process.stderr.write(`[cairn] memo phase parser: duplicate Phase ${c} ignored
161
+ `)[0].slice(0,120),ref:ie.join(".cairn","notes",c)}}function Ut(t,{message:e,active_files:n=[],notes:s=[],auto:i=!1,...r}){if(typeof s=="string")try{s=JSON.parse(s)}catch{s=s?[s]:[]}Array.isArray(s)||(s=[]);let o=D(),a=ie.join(o,"session.json"),c=new Date().toISOString().slice(0,10),p=new Date().toISOString(),l={};try{l=JSON.parse(B.readFileSync(a,"utf8"))||{}}catch{}let u=typeof r[k]=="string"&&r[k].trim()?r[k].trim():null,m=u??(typeof l[k]=="string"?l[k]:null),f=u?p:l[`${k}_at`]??null,g={};try{let y=t.prepare("SELECT path FROM main.files").all();for(let{path:_}of y)try{g[_]=B.statSync(_).mtimeMs}catch{}}catch{}for(let y of n)try{g[y]=B.statSync(y).mtimeMs}catch{}let d=s.map((y,_)=>ts(y,o,c,_));if(B.existsSync(a))try{if(B.statSync(a).size>es){let _=1;for(;B.existsSync(ie.join(o,`session.${_}.json`));)_++;B.renameSync(a,ie.join(o,`session.${_}.json`))}}catch{}let h={message:e,checkpoint_at:p,last_manual_checkpoint_at:i?l.last_manual_checkpoint_at??null:p,last_manual_message:i?l.last_manual_message??null:e,active_files:n,notes:d,[k]:m,[`${k}_at`]:f,mtime_snapshot:g};return B.writeFileSync(a,JSON.stringify(h,null,2),"utf8"),{content:[{type:"text",text:JSON.stringify({saved:!0,checkpoint_at:h.checkpoint_at,active_files_tracked:n.length,notes_saved:d.length,[k]:m?u?"recorded":"carried forward from the previous checkpoint":`not recorded \u2014 nothing will tell the next session what this one would lose (pass ${k})`},null,2)}]}}import z from"fs";import Oe from"path";import{exec as Ys}from"child_process";import{promisify as zs}from"util";import Q from"fs";import Ht from"path";function Wt(t){try{return JSON.parse(Q.readFileSync(Ht.join(t,"minify-map.json"),"utf8"))}catch{return{}}}function De(t,e){Q.mkdirSync(t,{recursive:!0}),Q.writeFileSync(Ht.join(t,"minify-map.json"),JSON.stringify(e,null,2),"utf8")}function qt(t,e){let n=0;for(let[s,i]of Object.entries(e)){let r=!1;if(!Q.existsSync(s))r=!0;else if(!Q.existsSync(i.tempPath))r=!0;else try{Q.statSync(s).mtimeMs>i.minifiedAt&&(r=!0)}catch{r=!0}if(r){if(i.tempPath)try{Q.unlinkSync(i.tempPath)}catch{}delete e[s],n++}}return n}import P from"fs";import re from"path";var Jt=["preference","experience"],Bt=["preference","experience","decision","knowledge"];function Xt(t){if(!t||typeof t!="string")return[];let e=t.split(/\r?\n/),n=!1,s=null,i=[],r=new Set;for(let o of e){if(/^##\s+Phases\s*$/i.test(o)){n=!0;continue}if(/^##\s+/.test(o)&&!/^###/.test(o)){s&&(i.push(s),s=null),n=!1;continue}if(!n)continue;let a=/^###\s+Phase\s+(\d+)\s*:\s*(.+?)\s*$/.exec(o);if(a){s&&i.push(s);let c=parseInt(a[1],10),p=a[2];if(r.has(c)){process.stderr.write(`[cairn] memo phase parser: duplicate Phase ${c} ignored
162
162
  `),s=null;continue}r.add(c),s={phase_number:c,text:p,bodyLines:[]};continue}s&&s.bodyLines.push(o)}return s&&i.push(s),i.map(o=>({phase_number:o.phase_number,text:o.text,body:o.bodyLines.join(`
163
163
  `).trim()||null}))}function Yt(t,e,n){let s=t.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(e),i=new Map(s.map(f=>[f.phase_number,f])),r=new Set,o=t.prepare("UPDATE main.roadmap_phases SET text = ?, body = ?, status = CASE WHEN status = ? THEN ? ELSE status END WHERE id = ?"),a=t.prepare("INSERT INTO main.roadmap_phases (decision_name, phase_number, parent_phase_id, text, body, status, position) VALUES (?, ?, NULL, ?, ?, ?, ?)"),c=t.prepare("UPDATE main.roadmap_phases SET status = 'stale' WHERE id = ?"),p=t.prepare("UPDATE main.roadmap_phases SET phase_number = NULL WHERE id = ?"),l=[],u=[],m=[];for(let f=0;f<n.length;f++){let g=n[f];r.add(g.phase_number);let d=i.get(g.phase_number);if(d&&d.status==="done"&&d.text!==g.text){p.run(d.id);let h=a.run(e,g.phase_number,g.text,g.body,"open",f);u.push({id:Number(h.lastInsertRowid),phase_number:g.phase_number,text:g.text,replaced_shipped:d.id})}else if(d)o.run(g.text,g.body,"stale","open",d.id),l.push({id:d.id,phase_number:g.phase_number,text:g.text,reactivated:d.status==="stale"});else{let h=a.run(e,g.phase_number,g.text,g.body,"open",f);u.push({id:Number(h.lastInsertRowid),phase_number:g.phase_number,text:g.text})}}for(let[f,g]of i)!r.has(f)&&g.status!=="stale"&&g.status!=="done"&&(c.run(g.id),m.push({id:g.id,phase_number:f}));return{updated:l.length,inserted:u.length,staled:m.length,updated_phases:l,inserted_phases:u,staled_phases:m}}function de(t){return t.toLowerCase().replace(/[^a-z0-9]+/g,"_").replace(/^_|_$/g,"")}function ns(t,e){return`${t}_${de(e)}.md`}function ss(t){let e=t.match(/^---\n([\s\S]*?)\n---/);if(!e)return{};let n={};for(let s of e[1].split(`
164
164
  `)){let i=s.indexOf(":");i!==-1&&(n[s.slice(0,i).trim()]=s.slice(i+1).trim())}return n}function zt(t){let e=re.join(t,"MEMORY.md");return P.existsSync(e)?P.readFileSync(e,"utf8"):""}function Gt(t,e){let n=["# Memory Index",""];for(let s of e)n.push(`- [${s.name}](${s.file}) \u2014 ${s.description} \`[${s.type}]\``);P.writeFileSync(re.join(t,"MEMORY.md"),n.join(`
@@ -183,22 +183,22 @@ Shipped roadmap phases, newest last.
183
183
  `),i+=a+`
184
184
  `,v.writeFileSync(s,i,"utf8"),{logged:a}}function be(){let t=en();return v.existsSync(t)?v.readFileSync(t,"utf8").split(/\r?\n/).filter(e=>/^-\s+\d{4}-\d{2}-\d{2}\s+—\s+Phase\s/.test(e)).length:0}import H from"fs";import I from"path";var Te="cairn",We="roadmap/1",ps="<!-- slot:roadmap";function ds(t){return`${ps} format=${We} provider=${Te} generated=${t} -->`}var us=`<!-- ${Te}:roadmap -->`,ms=/^<!--\s*slot:roadmap\b([^>]*?)-->/,fs=/^<!--\s*([A-Za-z0-9_.-]+)\s*:\s*roadmap\s*-->/,hs=/([A-Za-z_][A-Za-z0-9_-]*)=("[^"]*"|\S+)/g;function gs(t){if(typeof t!="string")return null;let e=t.indexOf(`
185
185
  `);return(e===-1?t:t.slice(0,e)).trim()}function ys(t){let e=gs(t);if(!e)return null;let n=ms.exec(e);if(n){let i={};for(let r of n[1].matchAll(hs))i[r[1]]=r[2].replace(/^"|"$/g,"");return i.provider?{provider:i.provider.toLowerCase(),format:i.format||null,generated:i.generated||null}:null}let s=fs.exec(e);return s?{provider:s[1].toLowerCase(),format:null,generated:null}:null}var _s=/^```roadmap-json[ \t]*\r?\n([\s\S]*?)\r?\n```[ \t]*$/m;function an(t){if(typeof t!="string")return null;let e=_s.exec(t);if(!e)return null;try{let n=JSON.parse(e[1]);return n&&typeof n=="object"&&!Array.isArray(n)?n:null}catch{return null}}var Es=new Set(["format","provider","generated","title","cursor","phases","migrated_from","migrated"]),Ss=new Set(["id","number","title","status","goal","blocked_by","slices","detail","links"]);function cn(t,e){let n={};if(!t||typeof t!="object")return n;for(let s of Object.keys(t).sort())e.has(s)||(n[s]=t[s]);return n}function ln(t,e=new Map){if(!Array.isArray(t))return e;for(let n of t)n&&typeof n=="object"&&typeof n.id=="string"&&(e.set(n.id,n),ln(n.slices,e));return e}function xs(t){if(typeof t!="string")return null;let e=an(t)?.migrated_from;if(typeof e=="string"&&e)return e;let n=t.indexOf(`
186
- `);if(n===-1)return null;let s=t.indexOf("-->",n),i=s===-1?t.slice(0,512):t.slice(n,s),r=/^\s*migrated-from=(.+?)\s*$/m.exec(i);return r?r[1]:null}var bs=/\[\[([^\]|#\n]+?)(?:[|#][^\]\n]*)?\]\]/g,Ts=["decision","knowledge","experience","preference"];function ws(t){let e=I.relative(N(),t).split(I.sep).join("/");return!e||e.startsWith("..")||I.isAbsolute(e)?null:e}function Rs(t){let e=I.join(N(),".cairn","memory");return[I.join(e,`${t}.md`),...Ts.map(n=>I.join(e,`${n}_${t}.md`))]}function Os(t){if(!t)return[];let e=[],n=new Set;for(let s of String(t).matchAll(bs)){let i=s[1].trim();if(!(!i||n.has(i))){n.add(i);for(let r of Rs(i)){if(!H.existsSync(r))continue;let o=ws(r);o&&e.push({kind:"memory",title:i,path:o});break}}}return e}var Ns={open:"open",in_progress:"in progress",done:"done",blocked:"blocked",stale:"stale"},ks=new Set(["open","in_progress","done","blocked","stale"]);function vs(t){return t==="done"?"x":t==="in_progress"?"~":t==="blocked"?"!":" "}function pn(){return I.join(N(),"ROADMAP.md")}function dn(){return new Date().toISOString().replace(/\.\d{3}Z$/,"Z")}function Ls(t){try{return t.prepare("SELECT phase_id FROM main.roadmap_cursor WHERE singleton = 1").get()?.phase_id??null}catch{return null}}function $s(t){let e=new Map,n;try{n=t.prepare("SELECT blocker_id, blocked_id FROM main.roadmap_phase_deps").all()}catch{return e}for(let s of n)e.has(s.blocked_id)||e.set(s.blocked_id,new Set),e.get(s.blocked_id).add(s.blocker_id);return e}function As(t,e){let n=t.decision_name??"",s=e.decision_name??"";return n!==s?n<s?-1:1:t.phase_number!=null&&e.phase_number!=null?t.phase_number-e.phase_number:t.position-e.position||t.id-e.id}function Is(t){let e;try{e=t.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(e.filter(d=>d.status!=="stale").length===0)return null;let n=Ls(t),s=$s(t),i=new Map(e.map(d=>[d.id,d.status])),r=new Map(e.map(d=>[d.id,d.phase_number!=null?`Phase ${d.phase_number}`:d.text])),o=new Map(e.map(d=>[d.id,d])),a=new Map;for(let d of e){let h=d.parent_phase_id??"root";a.has(h)||a.set(h,[]),a.get(h).push(d)}for(let d of a.values())d.sort(As);let c=new Map;for(let d of a.get("root")||[])d.decision_name!=null&&(c.has(d.decision_name)||c.set(d.decision_name,[]),c.get(d.decision_name).push(d));let p=[...c.keys()].sort((d,h)=>d===U?-1:h===U?1:d<h?-1:d>h?1:0),l=new Map,u=!1;for(let d of p){let h=c.get(d).filter(_=>_.status!=="stale"),y=d===U?h.filter(_=>_.status!=="done"):h;y.length!==0&&(u=!0,l.set(d,{visible:y,doneCount:h.filter(_=>_.status==="done").length,total:h.length}))}return u?{cursor:n,decisionNames:p,visibleRoots:l,children:d=>(a.get(d)||[]).filter(h=>h.status!=="stale"),unfinishedBlockers:d=>{let h=s.get(d);return h?[...h].filter(y=>i.get(y)!=="done").map(y=>r.get(y)||`#${y}`):[]},blockerIds:d=>{let h=s.get(d);return h?[...h].filter(y=>o.has(y)).sort((y,_)=>y-_).map(y=>`phase-${y}`):[]}}:null}var He=120;function un(t){if(!t)return null;let e=[];for(let o of t.split(`
187
- `)){let a=o.trim();if(!a){if(e.length)break;continue}e.push(a)}if(!e.length)return null;let n=e.join(" ").replace(/\s+/g," "),s=null,i=/[.?!](?=\s|$)/g,r;for(;(r=i.exec(n))!==null&&(s=n.slice(0,r.index+1),!(s.length>=40)););if(s||(s=n),s.length>He){let o=s.lastIndexOf(" ",He-1);s=s.slice(0,o>60?o:He-1).trimEnd()+"\u2026"}return s}function rn(t){let e=t.doneCount,n=t.total;if(!n)return"_No phases yet._";let s=Math.round(e/n*10);return`**${e} of ${n} done** \`${"\u2588".repeat(s)}${"\u2591".repeat(10-s)}\``}function Cs(t,e){let{cursor:n,decisionNames:s,visibleRoots:i,children:r,unfinishedBlockers:o}=t,a=[ds(e.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."];e.migratedFrom&&(a.push(" The ROADMAP.md that was here before cairn adopted this file is kept at:"),a.push(` migrated-from=${e.migratedFrom}`)),a.push("-->","","# Roadmap","","Legend: `[x]` done \xB7 `[~]` in progress \xB7 `[ ]` open \xB7 `[!]` blocked.");let c=(l,u)=>{let m=" ".repeat(u),f=l.phase_number!=null?`Phase ${l.phase_number}: ${l.text}`:l.text,g=Ns[l.status]||l.status,d=o(l.id);d.length&&(g+=` \xB7 waiting on ${d.join(", ")}`),n===l.id&&(g+=" \xB7 \u2190 current"),a.push(`${m}- [${vs(l.status)}] **${f}** \u2014 ${g}`);let h=un(l.body);h&&a.push(`${m} ${h}`);for(let y of r(l.id))c(y,u+1)};for(let l of s){let u=i.get(l);if(u){a.push(""),l===U?a.push("## Active plan","",rn(u)):a.push(`## ${l}`,"",rn(u)),a.push("");for(let m of u.visible)c(m,0)}}let p=be();return p>0&&a.push("","---","",`**Shipped:** ${p} phase${p===1?"":"s"} \u2014 see \`.cairn/roadmap_completed.md\`.`),a}function mn(t,e,n,s){let i=n.get(`phase-${t.id}`),r=un(t.body),o=Os(t.body),a=e.blockerIds(t.id),c={id:`phase-${t.id}`};if(t.phase_number!=null&&(c.number=t.phase_number),c.title=t.text,c.status=ks.has(t.status)?t.status:"open",r&&(c.goal=r),a.length&&(c.blocked_by=a),!s){let p=[],l=u=>{for(let m of e.children(u))p.push(mn(m,e,n,!0)),l(m.id)};l(t.id),p.length&&(c.slices=p)}return t.body&&t.body.trim()&&(c.detail=t.body.trim()),o.length&&(c.links=o),{...c,...cn(i,Ss)}}function Ms(t,e){let n=ln(e.previous?.phases),s=[];for(let o of t.decisionNames){let a=t.visibleRoots.get(o);if(a)for(let c of a.visible)s.push(mn(c,t,n,!1))}let i={format:We,provider:Te,generated:e.generated,title:`${I.basename(N())} roadmap`,cursor:t.cursor!=null?`phase-${t.cursor}`:null};e.migratedFrom&&(i.migrated_from=e.migratedFrom,e.migrated&&(i.migrated=e.migrated)),i.phases=s;let r={...i,...cn(e.previous,Es)};return["","<details>","<summary>Machine-readable plan (roadmap/1)</summary>","","```roadmap-json",JSON.stringify(r,null,2),"```","","</details>"]}function on(t,e={}){let n={generated:e.generated||dn(),migratedFrom:e.migratedFrom||null,migrated:e.migrated||null,previous:e.previous&&typeof e.previous=="object"?e.previous:null},s=Is(t);return s==null?null:[...Cs(s,n),...Ms(s,n)].join(`
186
+ `);if(n===-1)return null;let s=t.indexOf("-->",n),i=s===-1?t.slice(0,512):t.slice(n,s),r=/^\s*migrated-from=(.+?)\s*$/m.exec(i);return r?r[1]:null}var bs=/\[\[([^\]|#\n]+?)(?:[|#][^\]\n]*)?\]\]/g,Ts=["decision","knowledge","experience","preference"];function ws(t){let e=I.relative(N(),t).split(I.sep).join("/");return!e||e.startsWith("..")||I.isAbsolute(e)?null:e}function Rs(t){let e=I.join(N(),".cairn","memory");return[I.join(e,`${t}.md`),...Ts.map(n=>I.join(e,`${n}_${t}.md`))]}function Os(t){if(!t)return[];let e=[],n=new Set;for(let s of String(t).matchAll(bs)){let i=s[1].trim();if(!(!i||n.has(i))){n.add(i);for(let r of Rs(i)){if(!H.existsSync(r))continue;let o=ws(r);o&&e.push({kind:"memory",title:i,path:o});break}}}return e}var Ns={open:"open",in_progress:"in progress",done:"done",blocked:"blocked",stale:"stale"},ks=new Set(["open","in_progress","done","blocked","stale"]);function vs(t){return t==="done"?"x":t==="in_progress"?"~":t==="blocked"?"!":" "}function pn(){return I.join(N(),"ROADMAP.md")}function dn(){return new Date().toISOString().replace(/\.\d{3}Z$/,"Z")}function Ls(t){try{return t.prepare("SELECT phase_id FROM main.roadmap_cursor WHERE singleton = 1").get()?.phase_id??null}catch{return null}}function $s(t){let e=new Map,n;try{n=t.prepare("SELECT blocker_id, blocked_id FROM main.roadmap_phase_deps").all()}catch{return e}for(let s of n)e.has(s.blocked_id)||e.set(s.blocked_id,new Set),e.get(s.blocked_id).add(s.blocker_id);return e}function As(t,e){let n=t.decision_name??"",s=e.decision_name??"";return n!==s?n<s?-1:1:t.phase_number!=null&&e.phase_number!=null?t.phase_number-e.phase_number:t.position-e.position||t.id-e.id}function Is(t){let e;try{e=t.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(e.filter(d=>d.status!=="stale").length===0)return null;let n=Ls(t),s=$s(t),i=new Map(e.map(d=>[d.id,d.status])),r=new Map(e.map(d=>[d.id,d.phase_number!=null?`Phase ${d.phase_number}`:d.text])),o=new Map(e.map(d=>[d.id,d])),a=new Map;for(let d of e){let h=d.parent_phase_id??"root";a.has(h)||a.set(h,[]),a.get(h).push(d)}for(let d of a.values())d.sort(As);let c=new Map;for(let d of a.get("root")||[])d.decision_name!=null&&(d.decision_name===U&&d.phase_number==null||(c.has(d.decision_name)||c.set(d.decision_name,[]),c.get(d.decision_name).push(d)));let p=[...c.keys()].sort((d,h)=>d===U?-1:h===U?1:d<h?-1:d>h?1:0),l=new Map,u=!1;for(let d of p){let h=c.get(d).filter(_=>_.status!=="stale"),y=d===U?h.filter(_=>_.status!=="done"):h;y.length!==0&&(u=!0,l.set(d,{visible:y,doneCount:h.filter(_=>_.status==="done").length,total:h.length}))}return u?{cursor:n,decisionNames:p,visibleRoots:l,children:d=>(a.get(d)||[]).filter(h=>h.status!=="stale"),unfinishedBlockers:d=>{let h=s.get(d);return h?[...h].filter(y=>i.get(y)!=="done").map(y=>r.get(y)||`#${y}`):[]},blockerIds:d=>{let h=s.get(d);return h?[...h].filter(y=>o.has(y)).sort((y,_)=>y-_).map(y=>`phase-${y}`):[]}}:null}var He=120;function Cs(t){return t.replace(/\s*\((?:see\s+)?\[\[[^\]]+\]\](?:\s*(?:,|and)\s*\[\[[^\]]+\]\])*\s*\)/gi,"").replace(/\[\[([^\]]+)\]\]/g,(e,n)=>n.replace(/_/g," ").trim()).replace(/\s{2,}/g," ").replace(/\s+([.,;:!?])/g,"$1").trim()}function un(t){if(!t)return null;let e=[];for(let o of t.split(`
187
+ `)){let a=o.trim();if(!a){if(e.length)break;continue}e.push(a)}if(!e.length)return null;let n=Cs(e.join(" ").replace(/\s+/g," ")),s=null,i=/[.?!](?=\s|$)/g,r;for(;(r=i.exec(n))!==null&&(s=n.slice(0,r.index+1),!(s.length>=40)););if(s||(s=n),s.length>He){let o=s.lastIndexOf(" ",He-1);s=s.slice(0,o>60?o:He-1).trimEnd()+"\u2026"}return s}function rn(t){let e=t.doneCount,n=t.total;if(!n)return"_No phases yet._";let s=Math.round(e/n*10);return`**${e} of ${n} done** \`${"\u2588".repeat(s)}${"\u2591".repeat(10-s)}\``}function Ms(t,e){let{cursor:n,decisionNames:s,visibleRoots:i,children:r,unfinishedBlockers:o}=t,a=[ds(e.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."];e.migratedFrom&&(a.push(" The ROADMAP.md that was here before cairn adopted this file is kept at:"),a.push(` migrated-from=${e.migratedFrom}`)),a.push("-->","","# Roadmap","","Legend: `[x]` done \xB7 `[~]` in progress \xB7 `[ ]` open \xB7 `[!]` blocked.");let c=(l,u)=>{let m=" ".repeat(u),f=l.phase_number!=null?`Phase ${l.phase_number}: ${l.text}`:l.text,g=Ns[l.status]||l.status,d=o(l.id);d.length&&(g+=` \xB7 waiting on ${d.join(", ")}`),n===l.id&&(g+=" \xB7 \u2190 current"),a.push(`${m}- [${vs(l.status)}] **${f}** \u2014 ${g}`);let h=un(l.body);h&&a.push(`${m} ${h}`);for(let y of r(l.id))c(y,u+1)};for(let l of s){let u=i.get(l);if(u){a.push(""),l===U?a.push("## Active plan","",rn(u)):a.push(`## ${l}`,"",rn(u)),a.push("");for(let m of u.visible)c(m,0)}}let p=be();return p>0&&a.push("","---","",`**Shipped so far:** ${p} phase${p===1?"":"s"}.`),a}function mn(t,e,n,s){let i=n.get(`phase-${t.id}`),r=un(t.body),o=Os(t.body),a=e.blockerIds(t.id),c={id:`phase-${t.id}`};if(t.phase_number!=null&&(c.number=t.phase_number),c.title=t.text,c.status=ks.has(t.status)?t.status:"open",r&&(c.goal=r),a.length&&(c.blocked_by=a),!s){let p=[],l=u=>{for(let m of e.children(u))p.push(mn(m,e,n,!0)),l(m.id)};l(t.id),p.length&&(c.slices=p)}return t.body&&t.body.trim()&&(c.detail=t.body.trim()),o.length&&(c.links=o),{...c,...cn(i,Ss)}}function Fs(t,e){let n=ln(e.previous?.phases),s=[];for(let o of t.decisionNames){let a=t.visibleRoots.get(o);if(a)for(let c of a.visible)s.push(mn(c,t,n,!1))}let i={format:We,provider:Te,generated:e.generated,title:`${I.basename(N())} roadmap`,cursor:t.cursor!=null?`phase-${t.cursor}`:null};e.migratedFrom&&(i.migrated_from=e.migratedFrom,e.migrated&&(i.migrated=e.migrated)),i.phases=s;let r={...i,...cn(e.previous,Es)};return["","<details>","<summary>Machine-readable plan (roadmap/1)</summary>","","```roadmap-json",JSON.stringify(r,null,2),"```","","</details>"]}function on(t,e={}){let n={generated:e.generated||dn(),migratedFrom:e.migratedFrom||null,migrated:e.migrated||null,previous:e.previous&&typeof e.previous=="object"?e.previous:null},s=Is(t);return s==null?null:[...Ms(s,n),...Fs(s,n)].join(`
188
188
  `)+`
189
- `}function Fs(t){let e=I.join(t,"ROADMAP.bak.md");if(!H.existsSync(e))return e;for(let n=2;n<=1e3;n++){let s=I.join(t,`ROADMAP.bak.${n}.md`);if(!H.existsSync(s))return s}return null}function js(t,e){let n=I.join(I.dirname(t),`.${I.basename(t)}.${process.pid}.tmp`);try{H.writeFileSync(n,e,"utf8"),H.renameSync(n,t)}catch(s){try{H.unlinkSync(n)}catch{}throw s}}function Y(t,e,n={}){return{written:!1,reason:t,warning:`[cairn] ROADMAP.md was not updated \u2014 ${t}`,path:e,...n}}function qe(t){let e=pn(),n=null;if(H.existsSync(e))try{n=H.readFileSync(e,"utf8")}catch(g){return Y(`the existing file could not be read (${g.message})`,e)}let s=ys(n),i=n!=null&&(s?.provider===Te||n.startsWith(us));if(n!=null&&s&&!i)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:e};let r=i?an(n):null,o=i?xs(n):null,a=i&&typeof r?.migrated=="string"?r.migrated:null,c=typeof r?.generated=="string"?r.generated:null,p=on(t,{generated:c||"1970-01-01T00:00:00Z",migratedFrom:o,migrated:a,previous:r});if(p==null){if(i)try{return H.unlinkSync(e),{written:!1,removed:!0,path:e}}catch(g){return Y(`the stale projection could not be removed (${g.message})`,e)}return{written:!1,reason:"empty roadmap \u2014 no file written",path:e}}if(i&&n===p)return{written:!1,unchanged:!0,path:e,bytes:Buffer.byteLength(p)};let l=dn(),u=null,m=null;if(n!=null&&!i){let g=Fs(I.dirname(e));if(g==null)return Y("no free ROADMAP.bak name was available to preserve the existing file",e);try{H.renameSync(e,g)}catch(d){return Y(`the existing file could not be preserved (${d.message})`,e)}u=I.basename(g),m=g,o=u,a=l}p=on(t,{generated:l,migratedFrom:o,migrated:a,previous:r});try{js(e,p)}catch(g){if(m)try{return H.renameSync(m,e),Y(`write failed (${g.message}); the original ROADMAP.md was restored`,e)}catch{return Y(`write failed (${g.message}); the original ROADMAP.md is at ${u} and must be restored by hand`,e,{backed_up_to:u})}return Y(`write failed (${g.message})`,e)}let f={written:!0,path:e,bytes:Buffer.byteLength(p),format:We,generated:l};return u&&(f.backed_up_to=u,f.warning=`[cairn] The existing ROADMAP.md was not cairn-generated. It has been preserved as ${u}, and ROADMAP.md is now generated from cairn's roadmap.`),f}function Ds(t){try{return qe(t)}catch(e){process.stderr.write(`[cairn] ROADMAP.md publish skipped: ${e.message}
190
- `);let n=null;try{n=pn()}catch{}return Y(`publish failed (${e.message})`,n)}}function fn(t){let e=Ds(t);return e&&e.warning?e.warning:null}var Be={open:"[ ]",in_progress:"[~]",done:"[x]",blocked:"[!]",stale:"[s]"},hn=new Set(["open","in_progress","done","blocked","stale"]),Ps=new Set(["open","in_progress"]);function Re(t){return t.prepare("SELECT phase_id FROM main.roadmap_cursor WHERE singleton = 1").get()?.phase_id??null}function Us(t,e){t.prepare("INSERT INTO main.roadmap_cursor (singleton, phase_id) VALUES (1, ?) ON CONFLICT(singleton) DO UPDATE SET phase_id = excluded.phase_id").run(e)}function gn(t){t.prepare("DELETE FROM main.roadmap_cursor WHERE singleton = 1").run()}function Hs(t,e){let n=t.prepare("SELECT parent_phase_id, status FROM main.roadmap_phases WHERE id = ?"),s=t.prepare("UPDATE main.roadmap_phases SET status = 'in_progress' WHERE id = ? AND status = 'open'"),i=n.get(e);for(;i&&i.parent_phase_id!=null;)s.run(i.parent_phase_id),i=n.get(i.parent_phase_id)}function Ge(t,e){Us(t,e),t.prepare("UPDATE main.roadmap_phases SET status = 'in_progress' WHERE id = ? AND status = 'open'").run(e),Hs(t,e)}function En(t){let e=t.filter(n=>n.status!=="stale");return e.length===0?"stale":e.every(n=>n.status==="done")?"done":e.some(n=>n.status==="in_progress")?"in_progress":e.some(n=>n.status==="blocked")&&!e.some(n=>n.status==="in_progress")?"blocked":"open"}function Je(t){let e=new Map,n;try{n=t.prepare("SELECT blocker_id, blocked_id FROM main.roadmap_phase_deps").all()}catch{return e}for(let s of n)e.has(s.blocked_id)||e.set(s.blocked_id,new Set),e.get(s.blocked_id).add(s.blocker_id);return e}function Xe(t,e,n){let s=e.get(t);if(!s||s.size===0)return[];let i=[];for(let r of s)n.get(r)!=="done"&&i.push(r);return i}function yn(t,e){let n=t.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(u=>[u.id,u.status])),i=Je(t),r=new Map;for(let u of n){let m=u.parent_phase_id??`root:${u.decision_name}`;r.has(m)||r.set(m,[]),r.get(m).push(u)}for(let u of r.values())u.sort((m,f)=>m.phase_number!=null&&f.phase_number!=null?m.phase_number-f.phase_number:m.position-f.position||m.id-f.id);let o=[],a=new Set;for(let u of n){if(u.parent_phase_id!=null||a.has(u.decision_name))continue;a.add(u.decision_name);let m=[],f=r.get(`root:${u.decision_name}`)||[];for(let g=f.length-1;g>=0;g--)m.push(f[g]);for(;m.length;){let g=m.pop();o.push(g);let d=r.get(g.id)||[];for(let h=d.length-1;h>=0;h--)m.push(d[h])}}let c=u=>Ps.has(u.status),p=[],l=0;if(e!=null){let u=o.findIndex(m=>m.id===e);l=u===-1?0:u+1}for(let u=l;u<o.length;u++){let m=o[u];if(!c(m))continue;let f=Xe(m.id,i,s);if(f.length>0){p.push({id:m.id,blockers:f});continue}let g=e!=null?n.find(y=>y.id===e):null,d=g&&g.decision_name!==m.decision_name,h;return e==null?h=`First actionable phase: ${m.decision_name??"(sub-task)"} / Phase ${m.phase_number??"\xB7"}.`:d?h=`Previous decision "${g.decision_name}" is complete \u2014 advanced to "${m.decision_name}".`:h=`Next open phase in "${m.decision_name??"(sub-task)"}" after id ${e}.`,p.length>0&&(h+=` Skipped ${p.length} dep-blocked phase(s): ${p.map(y=>`id ${y.id} (blocked by ${y.blockers.join(",")})`).join("; ")}.`),{id:m.id,reason:h,skipped_blocked:p}}return p.length>0?{id:null,reason:`All remaining open phases are blocked by unfinished dependencies: ${p.map(u=>`id ${u.id} (blocked by ${u.blockers.join(",")})`).join("; ")}.`,skipped_blocked:p}:{id:null,reason:"No actionable phases remaining."}}function Ws(t){let e=t.filter(s=>s.status!=="stale");return{done:e.filter(s=>s.status==="done").length,total:e.length}}function qs(t,{compact:e=!1,showStale:n=!1,decisionFilter:s=null}={}){let i=t.prepare("SELECT id, decision_name, phase_number, parent_phase_id, text, status, position FROM main.roadmap_phases ORDER BY decision_name, phase_number, position, id").all();if(i.length===0)return"(no roadmap \u2014 add a `## Phases` section to .cairn/roadmap.md)";let r=Re(t),o=Je(t),a=new Map(i.map(d=>[d.id,d.status])),c=new Map;for(let d of i){let h=d.parent_phase_id??"root";c.has(h)||c.set(h,[]),c.get(h).push(d)}for(let d of c.values())d.sort((h,y)=>h.decision_name&&y.decision_name&&h.decision_name!==y.decision_name?h.decision_name.localeCompare(y.decision_name):h.phase_number!=null&&y.phase_number!=null?h.phase_number-y.phase_number:h.position-y.position||h.id-y.id);let p=c.get("root")||[],l=new Map;for(let d of p)d.decision_name!=null&&(d.decision_name===U&&d.phase_number==null||s!=null&&d.decision_name!==s||(l.has(d.decision_name)||l.set(d.decision_name,[]),l.get(d.decision_name).push(d)));if(s!=null&&l.size===0)return`(no decision named "${s}" \u2014 try cairn_roadmap action=tree without filter to list all)`;let u=0,m=[],f=d=>{let h=Xe(d.id,o,a);return h.length>0?` blocked-by:${h.join(",")}`:""},g=(d,h)=>{let y=c.get(d)||[];for(let _ of y){if(!n&&_.status==="stale"){u++;continue}let x=Be[_.status]||"[?]",b=r===_.id?" \u2192":"",C=_.phase_number!=null?`Phase ${_.phase_number}: ${_.text}`:_.text,E=e?"":` (id:${_.id})`;m.push(`${" ".repeat(h)}${x}${b} ${C}${E}${f(_)}`),g(_.id,h+1)}};for(let[d,h]of l){if(!n&&h.every(E=>E.status==="stale")){u+=h.length;continue}let y=En(h),{done:_,total:x}=Ws(h),b=x>0?` (${_}/${x} done)`:"",C=d===U?"Roadmap":`Decision: ${d}`;m.push(`${Be[y]} ${C}${b}`);for(let E of h){if(!n&&E.status==="stale"){u++;continue}if(d===U&&E.status==="done")continue;let R=Be[E.status]||"[?]",O=r===E.id?" \u2192":"",L=`Phase ${E.phase_number}: ${E.text}`,M=e?"":` (id:${E.id})`;m.push(` ${R}${O} ${L}${M}${f(E)}`),g(E.id,2)}}return u>0&&m.push(`(${u} stale phase${u===1?"":"s"} hidden \u2014 pass show_stale: true to view)`),m.join(`
191
- `)}function _n(t){let e=t.prepare("SELECT decision_name, status FROM main.roadmap_phases WHERE parent_phase_id IS NULL").all(),n=new Map;for(let o of e)n.has(o.decision_name)||n.set(o.decision_name,[]),n.get(o.decision_name).push(o);let s=0;for(let o of n.values()){let a=En(o);(a==="in_progress"||a==="open")&&s++}let i=Re(t),r=null;if(i!=null){let o=t.prepare("SELECT id, decision_name, phase_number, parent_phase_id, text FROM main.roadmap_phases WHERE id = ?").get(i);if(o){let a=o.decision_name,c=o.phase_number,p=o;for(;p.parent_phase_id!=null&&(p=t.prepare("SELECT id, decision_name, phase_number, parent_phase_id FROM main.roadmap_phases WHERE id = ?").get(p.parent_phase_id),!!p);)a=p.decision_name??a,c=p.phase_number??c;r=o.phase_number!=null?`${o.decision_name} / Phase ${o.phase_number} (${o.text})`:`${a} / Phase ${c} \u2192 sub: ${o.text}`}}return{decisions_total:n.size,decisions_in_flight:s,cursor_phase_id:i,current:r}}function Bs(t,e,n){if(e===n)throw new Error("A phase cannot block itself");if(t.prepare("SELECT id FROM main.roadmap_phases WHERE id IN (?, ?)").all(e,n).length!==2)throw new Error(`One or both phases not found (from=${e}, to=${n})`);let i=t.prepare("SELECT blocker_id, blocked_id FROM main.roadmap_phase_deps").all(),r=new Map;for(let c of i)r.has(c.blocked_id)||r.set(c.blocked_id,[]),r.get(c.blocked_id).push(c.blocker_id);let o=[e],a=new Set;for(;o.length;){let c=o.pop();if(c===n)throw new Error(`Cycle: phase ${n} already (transitively) blocks phase ${e}`);if(!a.has(c)){a.add(c);for(let p of r.get(c)||[])o.push(p)}}return t.prepare("INSERT OR IGNORE INTO main.roadmap_phase_deps (blocker_id, blocked_id) VALUES (?, ?)").run(e,n),{added:{blocker_id:e,blocked_id:n}}}function Gs(t,e,n){if(t.prepare("DELETE FROM main.roadmap_phase_deps WHERE blocker_id = ? AND blocked_id = ?").run(e,n).changes===0)throw new Error(`No dep edge ${e} \u2192 ${n}`);return{removed:{blocker_id:e,blocked_id:n}}}function Js(t,e){if(e!=null){let s=t.prepare("SELECT blocker_id FROM main.roadmap_phase_deps WHERE blocked_id = ?").all(e).map(r=>r.blocker_id),i=t.prepare("SELECT blocked_id FROM main.roadmap_phase_deps WHERE blocker_id = ?").all(e).map(r=>r.blocked_id);return{phase_id:e,blocked_by:s,blocks:i}}return{edges:t.prepare("SELECT blocker_id, blocked_id FROM main.roadmap_phase_deps ORDER BY blocker_id, blocked_id").all()}}function we(t){try{return tn(t)}catch(e){return process.stderr.write(`[cairn] roadmap.md sync skipped: ${e.message}
192
- `),null}}function ue(t,e={}){let n=e.action??"tree",{id:s,status:i,parent:r,text:o}=e;switch(n){case"tree":{we(t);let a=e.show_stale===!0||e.hide_stale===!1,c=qs(t,{compact:e.compact===!0,showStale:a,decisionFilter:e.decision??null}),p=_n(t),l=[c,"",p.current?`Current: ${p.current}`:"(no cursor set \u2014 call cairn_roadmap focus <id>)"];p.decisions_in_flight>1&&l.push(`In flight: ${p.decisions_in_flight} decisions`);let u=be();return u>0&&l.push(`Shipped: ${u} (see .cairn/roadmap_completed.md)`),{content:[{type:"text",text:l.join(`
189
+ `}function js(t){let e=I.join(t,"ROADMAP.bak.md");if(!H.existsSync(e))return e;for(let n=2;n<=1e3;n++){let s=I.join(t,`ROADMAP.bak.${n}.md`);if(!H.existsSync(s))return s}return null}function Ds(t,e){let n=I.join(I.dirname(t),`.${I.basename(t)}.${process.pid}.tmp`);try{H.writeFileSync(n,e,"utf8"),H.renameSync(n,t)}catch(s){try{H.unlinkSync(n)}catch{}throw s}}function Y(t,e,n={}){return{written:!1,reason:t,warning:`[cairn] ROADMAP.md was not updated \u2014 ${t}`,path:e,...n}}function qe(t){let e=pn(),n=null;if(H.existsSync(e))try{n=H.readFileSync(e,"utf8")}catch(g){return Y(`the existing file could not be read (${g.message})`,e)}let s=ys(n),i=n!=null&&(s?.provider===Te||n.startsWith(us));if(n!=null&&s&&!i)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:e};let r=i?an(n):null,o=i?xs(n):null,a=i&&typeof r?.migrated=="string"?r.migrated:null,c=typeof r?.generated=="string"?r.generated:null,p=on(t,{generated:c||"1970-01-01T00:00:00Z",migratedFrom:o,migrated:a,previous:r});if(p==null){if(i)try{return H.unlinkSync(e),{written:!1,removed:!0,path:e}}catch(g){return Y(`the stale projection could not be removed (${g.message})`,e)}return{written:!1,reason:"empty roadmap \u2014 no file written",path:e}}if(i&&n===p)return{written:!1,unchanged:!0,path:e,bytes:Buffer.byteLength(p)};let l=dn(),u=null,m=null;if(n!=null&&!i){let g=js(I.dirname(e));if(g==null)return Y("no free ROADMAP.bak name was available to preserve the existing file",e);try{H.renameSync(e,g)}catch(d){return Y(`the existing file could not be preserved (${d.message})`,e)}u=I.basename(g),m=g,o=u,a=l}p=on(t,{generated:l,migratedFrom:o,migrated:a,previous:r});try{Ds(e,p)}catch(g){if(m)try{return H.renameSync(m,e),Y(`write failed (${g.message}); the original ROADMAP.md was restored`,e)}catch{return Y(`write failed (${g.message}); the original ROADMAP.md is at ${u} and must be restored by hand`,e,{backed_up_to:u})}return Y(`write failed (${g.message})`,e)}let f={written:!0,path:e,bytes:Buffer.byteLength(p),format:We,generated:l};return u&&(f.backed_up_to=u,f.warning=`[cairn] The existing ROADMAP.md was not cairn-generated. It has been preserved as ${u}, and ROADMAP.md is now generated from cairn's roadmap.`),f}function Ps(t){try{return qe(t)}catch(e){process.stderr.write(`[cairn] ROADMAP.md publish skipped: ${e.message}
190
+ `);let n=null;try{n=pn()}catch{}return Y(`publish failed (${e.message})`,n)}}function fn(t){let e=Ps(t);return e&&e.warning?e.warning:null}var Be={open:"[ ]",in_progress:"[~]",done:"[x]",blocked:"[!]",stale:"[s]"},hn=new Set(["open","in_progress","done","blocked","stale"]),Us=new Set(["open","in_progress"]);function Re(t){return t.prepare("SELECT phase_id FROM main.roadmap_cursor WHERE singleton = 1").get()?.phase_id??null}function Hs(t,e){t.prepare("INSERT INTO main.roadmap_cursor (singleton, phase_id) VALUES (1, ?) ON CONFLICT(singleton) DO UPDATE SET phase_id = excluded.phase_id").run(e)}function gn(t){t.prepare("DELETE FROM main.roadmap_cursor WHERE singleton = 1").run()}function Ws(t,e){let n=t.prepare("SELECT parent_phase_id, status FROM main.roadmap_phases WHERE id = ?"),s=t.prepare("UPDATE main.roadmap_phases SET status = 'in_progress' WHERE id = ? AND status = 'open'"),i=n.get(e);for(;i&&i.parent_phase_id!=null;)s.run(i.parent_phase_id),i=n.get(i.parent_phase_id)}function Ge(t,e){Hs(t,e),t.prepare("UPDATE main.roadmap_phases SET status = 'in_progress' WHERE id = ? AND status = 'open'").run(e),Ws(t,e)}function En(t){let e=t.filter(n=>n.status!=="stale");return e.length===0?"stale":e.every(n=>n.status==="done")?"done":e.some(n=>n.status==="in_progress")?"in_progress":e.some(n=>n.status==="blocked")&&!e.some(n=>n.status==="in_progress")?"blocked":"open"}function Je(t){let e=new Map,n;try{n=t.prepare("SELECT blocker_id, blocked_id FROM main.roadmap_phase_deps").all()}catch{return e}for(let s of n)e.has(s.blocked_id)||e.set(s.blocked_id,new Set),e.get(s.blocked_id).add(s.blocker_id);return e}function Xe(t,e,n){let s=e.get(t);if(!s||s.size===0)return[];let i=[];for(let r of s)n.get(r)!=="done"&&i.push(r);return i}function yn(t,e){let n=t.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(u=>[u.id,u.status])),i=Je(t),r=new Map;for(let u of n){let m=u.parent_phase_id??`root:${u.decision_name}`;r.has(m)||r.set(m,[]),r.get(m).push(u)}for(let u of r.values())u.sort((m,f)=>m.phase_number!=null&&f.phase_number!=null?m.phase_number-f.phase_number:m.position-f.position||m.id-f.id);let o=[],a=new Set;for(let u of n){if(u.parent_phase_id!=null||a.has(u.decision_name))continue;a.add(u.decision_name);let m=[],f=r.get(`root:${u.decision_name}`)||[];for(let g=f.length-1;g>=0;g--)m.push(f[g]);for(;m.length;){let g=m.pop();o.push(g);let d=r.get(g.id)||[];for(let h=d.length-1;h>=0;h--)m.push(d[h])}}let c=u=>Us.has(u.status),p=[],l=0;if(e!=null){let u=o.findIndex(m=>m.id===e);l=u===-1?0:u+1}for(let u=l;u<o.length;u++){let m=o[u];if(!c(m))continue;let f=Xe(m.id,i,s);if(f.length>0){p.push({id:m.id,blockers:f});continue}let g=e!=null?n.find(y=>y.id===e):null,d=g&&g.decision_name!==m.decision_name,h;return e==null?h=`First actionable phase: ${m.decision_name??"(sub-task)"} / Phase ${m.phase_number??"\xB7"}.`:d?h=`Previous decision "${g.decision_name}" is complete \u2014 advanced to "${m.decision_name}".`:h=`Next open phase in "${m.decision_name??"(sub-task)"}" after id ${e}.`,p.length>0&&(h+=` Skipped ${p.length} dep-blocked phase(s): ${p.map(y=>`id ${y.id} (blocked by ${y.blockers.join(",")})`).join("; ")}.`),{id:m.id,reason:h,skipped_blocked:p}}return p.length>0?{id:null,reason:`All remaining open phases are blocked by unfinished dependencies: ${p.map(u=>`id ${u.id} (blocked by ${u.blockers.join(",")})`).join("; ")}.`,skipped_blocked:p}:{id:null,reason:"No actionable phases remaining."}}function qs(t){let e=t.filter(s=>s.status!=="stale");return{done:e.filter(s=>s.status==="done").length,total:e.length}}function Bs(t,{compact:e=!1,showStale:n=!1,decisionFilter:s=null}={}){let i=t.prepare("SELECT id, decision_name, phase_number, parent_phase_id, text, status, position FROM main.roadmap_phases ORDER BY decision_name, phase_number, position, id").all();if(i.length===0)return"(no roadmap \u2014 add a `## Phases` section to .cairn/roadmap.md)";let r=Re(t),o=Je(t),a=new Map(i.map(d=>[d.id,d.status])),c=new Map;for(let d of i){let h=d.parent_phase_id??"root";c.has(h)||c.set(h,[]),c.get(h).push(d)}for(let d of c.values())d.sort((h,y)=>h.decision_name&&y.decision_name&&h.decision_name!==y.decision_name?h.decision_name.localeCompare(y.decision_name):h.phase_number!=null&&y.phase_number!=null?h.phase_number-y.phase_number:h.position-y.position||h.id-y.id);let p=c.get("root")||[],l=new Map;for(let d of p)d.decision_name!=null&&(d.decision_name===U&&d.phase_number==null||s!=null&&d.decision_name!==s||(l.has(d.decision_name)||l.set(d.decision_name,[]),l.get(d.decision_name).push(d)));if(s!=null&&l.size===0)return`(no decision named "${s}" \u2014 try cairn_roadmap action=tree without filter to list all)`;let u=0,m=[],f=d=>{let h=Xe(d.id,o,a);return h.length>0?` blocked-by:${h.join(",")}`:""},g=(d,h)=>{let y=c.get(d)||[];for(let _ of y){if(!n&&_.status==="stale"){u++;continue}let x=Be[_.status]||"[?]",b=r===_.id?" \u2192":"",C=_.phase_number!=null?`Phase ${_.phase_number}: ${_.text}`:_.text,E=e?"":` (id:${_.id})`;m.push(`${" ".repeat(h)}${x}${b} ${C}${E}${f(_)}`),g(_.id,h+1)}};for(let[d,h]of l){if(!n&&h.every(E=>E.status==="stale")){u+=h.length;continue}let y=En(h),{done:_,total:x}=qs(h),b=x>0?` (${_}/${x} done)`:"",C=d===U?"Roadmap":`Decision: ${d}`;m.push(`${Be[y]} ${C}${b}`);for(let E of h){if(!n&&E.status==="stale"){u++;continue}if(d===U&&E.status==="done")continue;let R=Be[E.status]||"[?]",O=r===E.id?" \u2192":"",L=`Phase ${E.phase_number}: ${E.text}`,M=e?"":` (id:${E.id})`;m.push(` ${R}${O} ${L}${M}${f(E)}`),g(E.id,2)}}return u>0&&m.push(`(${u} stale phase${u===1?"":"s"} hidden \u2014 pass show_stale: true to view)`),m.join(`
191
+ `)}function _n(t){let e=t.prepare("SELECT decision_name, status FROM main.roadmap_phases WHERE parent_phase_id IS NULL").all(),n=new Map;for(let o of e)n.has(o.decision_name)||n.set(o.decision_name,[]),n.get(o.decision_name).push(o);let s=0;for(let o of n.values()){let a=En(o);(a==="in_progress"||a==="open")&&s++}let i=Re(t),r=null;if(i!=null){let o=t.prepare("SELECT id, decision_name, phase_number, parent_phase_id, text FROM main.roadmap_phases WHERE id = ?").get(i);if(o){let a=o.decision_name,c=o.phase_number,p=o;for(;p.parent_phase_id!=null&&(p=t.prepare("SELECT id, decision_name, phase_number, parent_phase_id FROM main.roadmap_phases WHERE id = ?").get(p.parent_phase_id),!!p);)a=p.decision_name??a,c=p.phase_number??c;r=o.phase_number!=null?`${o.decision_name} / Phase ${o.phase_number} (${o.text})`:`${a} / Phase ${c} \u2192 sub: ${o.text}`}}return{decisions_total:n.size,decisions_in_flight:s,cursor_phase_id:i,current:r}}function Gs(t,e,n){if(e===n)throw new Error("A phase cannot block itself");if(t.prepare("SELECT id FROM main.roadmap_phases WHERE id IN (?, ?)").all(e,n).length!==2)throw new Error(`One or both phases not found (from=${e}, to=${n})`);let i=t.prepare("SELECT blocker_id, blocked_id FROM main.roadmap_phase_deps").all(),r=new Map;for(let c of i)r.has(c.blocked_id)||r.set(c.blocked_id,[]),r.get(c.blocked_id).push(c.blocker_id);let o=[e],a=new Set;for(;o.length;){let c=o.pop();if(c===n)throw new Error(`Cycle: phase ${n} already (transitively) blocks phase ${e}`);if(!a.has(c)){a.add(c);for(let p of r.get(c)||[])o.push(p)}}return t.prepare("INSERT OR IGNORE INTO main.roadmap_phase_deps (blocker_id, blocked_id) VALUES (?, ?)").run(e,n),{added:{blocker_id:e,blocked_id:n}}}function Js(t,e,n){if(t.prepare("DELETE FROM main.roadmap_phase_deps WHERE blocker_id = ? AND blocked_id = ?").run(e,n).changes===0)throw new Error(`No dep edge ${e} \u2192 ${n}`);return{removed:{blocker_id:e,blocked_id:n}}}function Xs(t,e){if(e!=null){let s=t.prepare("SELECT blocker_id FROM main.roadmap_phase_deps WHERE blocked_id = ?").all(e).map(r=>r.blocker_id),i=t.prepare("SELECT blocked_id FROM main.roadmap_phase_deps WHERE blocker_id = ?").all(e).map(r=>r.blocked_id);return{phase_id:e,blocked_by:s,blocks:i}}return{edges:t.prepare("SELECT blocker_id, blocked_id FROM main.roadmap_phase_deps ORDER BY blocker_id, blocked_id").all()}}function we(t){try{return tn(t)}catch(e){return process.stderr.write(`[cairn] roadmap.md sync skipped: ${e.message}
192
+ `),null}}function ue(t,e={}){let n=e.action??"tree",{id:s,status:i,parent:r,text:o}=e;switch(n){case"tree":{we(t);let a=e.show_stale===!0||e.hide_stale===!1,c=Bs(t,{compact:e.compact===!0,showStale:a,decisionFilter:e.decision??null}),p=_n(t),l=[c,"",p.current?`Current: ${p.current}`:"(no cursor set \u2014 call cairn_roadmap focus <id>)"];p.decisions_in_flight>1&&l.push(`In flight: ${p.decisions_in_flight} decisions`);let u=be();return u>0&&l.push(`Shipped: ${u} (see .cairn/roadmap_completed.md)`),{content:[{type:"text",text:l.join(`
193
193
  `)}]}}case"status":return we(t),{content:[{type:"text",text:JSON.stringify(_n(t),null,2)}]};case"sync":{let a=we(t);return{content:[{type:"text",text:JSON.stringify(a==null?{synced:!1,reason:"No .cairn/roadmap.md with a `## Phases` section."}:{synced:!0,...a},null,2)}]}}case"publish":return we(t),{content:[{type:"text",text:JSON.stringify(qe(t),null,2)}]};case"focus":{if(!s)throw new Error("id is required for focus");let a=t.prepare("SELECT * FROM main.roadmap_phases WHERE id = ?").get(s);if(!a)throw new Error(`No phase with id ${s}`);if(a.status==="stale")throw new Error(`Phase ${s} is stale \u2014 restore by re-adding it to .cairn/roadmap.md`);let c=Je(t),p=new Map(t.prepare("SELECT id, status FROM main.roadmap_phases").all().map(f=>[f.id,f.status])),l=Xe(s,c,p);Ge(t,s);let u=t.prepare("SELECT * FROM main.roadmap_phases WHERE id = ?").get(s),m=l.length>0?`Focused phase ${s} despite unfinished blockers: ${l.join(", ")}.`:`Focused phase ${s}.`;return{content:[{type:"text",text:JSON.stringify({focused:u,reason:m,blocked_by:l},null,2)}]}}case"next":{let a=Re(t),c=yn(t,a);if(c.id==null)return gn(t),{content:[{type:"text",text:JSON.stringify({cursor:null,reason:c.reason,skipped_blocked:c.skipped_blocked??[]},null,2)}]};Ge(t,c.id);let p=t.prepare("SELECT * FROM main.roadmap_phases WHERE id = ?").get(c.id);return{content:[{type:"text",text:JSON.stringify({cursor:c.id,phase:p,reason:c.reason,skipped_blocked:c.skipped_blocked??[]},null,2)}]}}case"set_status":{if(!s)throw new Error("id is required for set_status");if(!i)throw new Error("status is required for set_status");if(!hn.has(i))throw new Error(`Invalid status "${i}". Use: ${[...hn].join(", ")}`);let a=t.prepare("SELECT * FROM main.roadmap_phases WHERE id = ?").get(s);if(!a)throw new Error(`No phase with id ${s}`);let c=i==="done"?"datetime('now')":"NULL";t.prepare(`UPDATE main.roadmap_phases SET status = ?, resolved_at = ${c} WHERE id = ?`).run(i,s);let p=null,l=null;if(i==="done"&&Re(t)===s){let g=yn(t,s);g.id!=null?(Ge(t,g.id),p=g.id,l=g.reason):(gn(t),l=g.reason)}let u=null,m=a.decision_name===U&&a.phase_number!=null;if(i==="done"&&m)try{let g=sn(a.phase_number,a.text),d=nn(a.phase_number).removed;u={logged:g.logged,pruned_from_roadmap_md:d}}catch(g){process.stderr.write(`[cairn] ship bookkeeping failed for phase ${s}: ${g.message}
194
- `)}let f=t.prepare("SELECT * FROM main.roadmap_phases WHERE id = ?").get(s);return{content:[{type:"text",text:JSON.stringify({updated:f,cursor_advanced_to:p,reason:l,completed:u},null,2)}]}}case"add":{if(!o)throw new Error("text is required for add");if(r==null)throw new Error("parent (phase_id) is required \u2014 ad-hoc sub-tasks must hang under an existing phase. For freestanding work use cairn_todos add.");let a=t.prepare("SELECT id, decision_name FROM main.roadmap_phases WHERE id = ?").get(r);if(!a)throw new Error(`Parent phase ${r} not found`);let c=t.prepare("SELECT COALESCE(MAX(position), -1) + 1 AS n FROM main.roadmap_phases WHERE parent_phase_id = ?").get(r).n,p=t.prepare("INSERT INTO main.roadmap_phases (decision_name, phase_number, parent_phase_id, text, status, position) VALUES (NULL, NULL, ?, ?, ?, ?)").run(r,o,"open",c),l=t.prepare("SELECT * FROM main.roadmap_phases WHERE id = ?").get(p.lastInsertRowid);return{content:[{type:"text",text:JSON.stringify({added:l,inherits_decision_from:a.decision_name},null,2)}]}}case"deps_add":{let a=e.from,c=e.to;if(a==null||c==null)throw new Error("deps_add requires from (blocker phase id) and to (blocked phase id)");return{content:[{type:"text",text:JSON.stringify(Bs(t,a,c),null,2)}]}}case"deps_remove":{let a=e.from,c=e.to;if(a==null||c==null)throw new Error("deps_remove requires from and to");return{content:[{type:"text",text:JSON.stringify(Gs(t,a,c),null,2)}]}}case"deps_list":return{content:[{type:"text",text:JSON.stringify(Js(t,e.id??null),null,2)}]};default:throw new Error(`Unknown action: "${n}". Use tree, status, sync, publish, focus, next, set_status, add, deps_add, deps_remove, or deps_list.`)}}var Ye=Ys(Xs);async function Sn(t){let e=D(),n=Wt(e);qt(e,n)>0&&De(e,n);let i=Oe.join(e,"session.json");if(!z.existsSync(i))return{content:[{type:"text",text:JSON.stringify({error:"No checkpoint found. Run cairn_maintain to index the project, then cairn_checkpoint to save session state."},null,2)}]};let r=JSON.parse(z.readFileSync(i,"utf8"));if(typeof r.notes=="string")try{r.notes=JSON.parse(r.notes)}catch{r.notes=[]}Array.isArray(r.notes)||(r.notes=[]);let o=r.notes.filter(S=>typeof S=="string"&&S.includes("git-snapshot:"));if(o.length>0){let S=Oe.join(e,"session.bak.json");z.existsSync(S)||z.copyFileSync(i,S),r.notes=r.notes.filter(F=>!(typeof F=="string"&&F.includes("git-snapshot:"))),z.writeFileSync(i,JSON.stringify(r,null,2),"utf8")}let a=null,c=r.mtime_snapshot||{};Object.keys(c).length>0&&(a=Qt(c,N()),a&&(Zt(t,e,r,n,a.oldRoot,a.newRoot),De(e,n)));let p=r.mtime_snapshot||{},l=[];for(let[S,F]of Object.entries(p))try{z.statSync(S).mtimeMs!==F&&l.push({file:S,change:"modified"})}catch{l.push({file:S,change:"deleted"})}try{let S=await q(N());for(let F of S)F in p||l.push({file:F,change:"new"})}catch{}let u=l.filter(S=>S.change==="deleted").map(S=>S.file);if(u.length>0){let S=t.prepare("DELETE FROM main.files WHERE path = ?");for(let F of u)S.run(F)}let m=l.filter(S=>S.change!=="deleted").map(S=>S.file),f={files_reindexed:0,files_unchanged:0};m.length>0&&(await Ee(t,m),f.files_reindexed=m.length),f.files_unchanged=Object.keys(p).length-l.filter(S=>S.change!=="new").length;let g=ee(),d=Vt(g),h=G(g),y=h.length>0?h.map(S=>`[${S.type}] ${S.name} \u2014 ${S.description}`).join(`
194
+ `)}let f=t.prepare("SELECT * FROM main.roadmap_phases WHERE id = ?").get(s);return{content:[{type:"text",text:JSON.stringify({updated:f,cursor_advanced_to:p,reason:l,completed:u},null,2)}]}}case"add":{if(!o)throw new Error("text is required for add");if(r==null)throw new Error("parent (phase_id) is required \u2014 ad-hoc sub-tasks must hang under an existing phase. For freestanding work use cairn_todos add.");let a=t.prepare("SELECT id, decision_name FROM main.roadmap_phases WHERE id = ?").get(r);if(!a)throw new Error(`Parent phase ${r} not found`);let c=t.prepare("SELECT COALESCE(MAX(position), -1) + 1 AS n FROM main.roadmap_phases WHERE parent_phase_id = ?").get(r).n,p=t.prepare("INSERT INTO main.roadmap_phases (decision_name, phase_number, parent_phase_id, text, status, position) VALUES (NULL, NULL, ?, ?, ?, ?)").run(r,o,"open",c),l=t.prepare("SELECT * FROM main.roadmap_phases WHERE id = ?").get(p.lastInsertRowid);return{content:[{type:"text",text:JSON.stringify({added:l,inherits_decision_from:a.decision_name},null,2)}]}}case"deps_add":{let a=e.from,c=e.to;if(a==null||c==null)throw new Error("deps_add requires from (blocker phase id) and to (blocked phase id)");return{content:[{type:"text",text:JSON.stringify(Gs(t,a,c),null,2)}]}}case"deps_remove":{let a=e.from,c=e.to;if(a==null||c==null)throw new Error("deps_remove requires from and to");return{content:[{type:"text",text:JSON.stringify(Js(t,a,c),null,2)}]}}case"deps_list":return{content:[{type:"text",text:JSON.stringify(Xs(t,e.id??null),null,2)}]};default:throw new Error(`Unknown action: "${n}". Use tree, status, sync, publish, focus, next, set_status, add, deps_add, deps_remove, or deps_list.`)}}var Ye=zs(Ys);async function Sn(t){let e=D(),n=Wt(e);qt(e,n)>0&&De(e,n);let i=Oe.join(e,"session.json");if(!z.existsSync(i))return{content:[{type:"text",text:JSON.stringify({error:"No checkpoint found. Run cairn_maintain to index the project, then cairn_checkpoint to save session state."},null,2)}]};let r=JSON.parse(z.readFileSync(i,"utf8"));if(typeof r.notes=="string")try{r.notes=JSON.parse(r.notes)}catch{r.notes=[]}Array.isArray(r.notes)||(r.notes=[]);let o=r.notes.filter(S=>typeof S=="string"&&S.includes("git-snapshot:"));if(o.length>0){let S=Oe.join(e,"session.bak.json");z.existsSync(S)||z.copyFileSync(i,S),r.notes=r.notes.filter(F=>!(typeof F=="string"&&F.includes("git-snapshot:"))),z.writeFileSync(i,JSON.stringify(r,null,2),"utf8")}let a=null,c=r.mtime_snapshot||{};Object.keys(c).length>0&&(a=Qt(c,N()),a&&(Zt(t,e,r,n,a.oldRoot,a.newRoot),De(e,n)));let p=r.mtime_snapshot||{},l=[];for(let[S,F]of Object.entries(p))try{z.statSync(S).mtimeMs!==F&&l.push({file:S,change:"modified"})}catch{l.push({file:S,change:"deleted"})}try{let S=await q(N());for(let F of S)F in p||l.push({file:F,change:"new"})}catch{}let u=l.filter(S=>S.change==="deleted").map(S=>S.file);if(u.length>0){let S=t.prepare("DELETE FROM main.files WHERE path = ?");for(let F of u)S.run(F)}let m=l.filter(S=>S.change!=="deleted").map(S=>S.file),f={files_reindexed:0,files_unchanged:0};m.length>0&&(await Ee(t,m),f.files_reindexed=m.length),f.files_unchanged=Object.keys(p).length-l.filter(S=>S.change!=="new").length;let g=ee(),d=Vt(g),h=G(g),y=h.length>0?h.map(S=>`[${S.type}] ${S.name} \u2014 ${S.description}`).join(`
195
195
  `):null,_=ye(t),x=0;try{x=t.prepare("SELECT COUNT(*) AS n FROM main.todos WHERE status IN ('open', 'in_progress')").get().n}catch{}let b=null;try{let S=z.existsSync(ae()),F=JSON.parse(ue(t,{action:"status"}).content[0].text);if(S||F.decisions_total>0){let J=ue(t,{action:"tree",compact:!0}).content[0].text;b={file:Oe.relative(N(),ae()).replace(/\\/g,"/"),current:F.current,decisions_in_flight:F.decisions_in_flight,tree:J}}}catch{}let C=b?b.current?`Roadmap (${b.file}) \u2014 current: ${b.current}`:`Roadmap (${b.file}) \u2014 no cursor set (call cairn_roadmap next)`:"",E=Dt(N(),r),O=/^Auto-checkpoint\b/.test(r.message||"")&&r.last_manual_message||r.message,L=[`Last session: ${O}`,Pt(E),a?`Project relocated from ${a.oldRoot} \u2192 repaired automatically.`:"",C,l.length>0?`${l.length} file(s) changed since checkpoint`:"No files changed since checkpoint",x>0?`${x} open todo(s)`:"",_.length>0?`${_.length} sub-index(es)`:""].filter(Boolean).join(" \xB7 "),M=null;try{z.readFileSync(Oe.join(N(),".gitignore"),"utf8").split(`
196
196
  `).map(J=>J.trim()).some(J=>J===".cairn"||J===".cairn/"||J==="/.cairn"||J==="/.cairn/")||(M="Add .cairn/ to your .gitignore to avoid committing the index and session cache.")}catch{}let w={last_checkpoint:r.checkpoint_at,message:O,[k]:E.recorded?E.text:null,[`${k}_note`]:E.recorded?E.stale?`Written ${E.at} \u2014 ${E.commits} commit(s) have landed since, so treat it as possibly stale and verify before relying on it.`:`Written ${E.at}, current as of the last commit.`:"Nothing was recorded about what the last session would lose. This is not an all-clear: cairn cannot see what was never written down.",last_manual_checkpoint:r.last_manual_checkpoint_at??null,index_status:m.length>0||u.length>0?"incremental":"up_to_date",files_reindexed:f.files_reindexed,files_deleted:u.length,files_unchanged:Math.max(0,f.files_unchanged),sub_indexes_active:_.length,open_todos:x,changed_since_checkpoint:l.slice(0,20),active_files:r.active_files||[],notes:r.notes||[],resume_summary:L};M&&(w.gitignore_hint=M),b&&(w.roadmap=b),o.length>0&&(w.migrated=`Removed ${o.length} legacy git-snapshot note(s). Original session backed up to session.bak.json.`);let T=N(),ke=new Date(r.checkpoint_at).toISOString(),[fe,he,tt]=await Promise.allSettled([Ye(`git log --oneline --after="${ke}"`,{cwd:T,timeout:5e3}),Ye("git diff --name-only HEAD",{cwd:T,timeout:5e3}),Ye("git status --short",{cwd:T,timeout:5e3})]);if(fe.status==="fulfilled"){let S=fe.value.stdout.trim();S&&(w.commits_since_checkpoint=S.split(`
197
197
  `))}if(he.status==="fulfilled"){let S=he.value.stdout.trim();S&&(w.uncommitted_changes=S.split(`
198
- `))}if(tt.status==="fulfilled"){let S=tt.value.stdout.trim();S&&(w.git_status=S)}return a&&(w.relocated=a),d.length>0&&(w.preferences=d.map(S=>({name:S.name,description:S.description,file:S.file}))),y&&(w.memory_index=y),{content:[{type:"text",text:JSON.stringify(w,null,2)}]}}import zs from"fs";import Vs from"path";var Ks=new Set(["java","typescript","javascript","vue","python","sql"]);function xn(t,{file_path:e}){if(!e)throw new Error("file_path is required");let n=Vs.extname(e).toLowerCase(),s=K[n];if(!s||!Ks.has(s))return{content:[{type:"text",text:JSON.stringify({skipped:!0,reason:`language '${s??n}' is not minified`})}]};let i=zs.readFileSync(e,"utf8"),r=i.split(`
198
+ `))}if(tt.status==="fulfilled"){let S=tt.value.stdout.trim();S&&(w.git_status=S)}return a&&(w.relocated=a),d.length>0&&(w.preferences=d.map(S=>({name:S.name,description:S.description,file:S.file}))),y&&(w.memory_index=y),{content:[{type:"text",text:JSON.stringify(w,null,2)}]}}import Vs from"fs";import Ks from"path";var Qs=new Set(["java","typescript","javascript","vue","python","sql"]);function xn(t,{file_path:e}){if(!e)throw new Error("file_path is required");let n=Ks.extname(e).toLowerCase(),s=K[n];if(!s||!Qs.has(s))return{content:[{type:"text",text:JSON.stringify({skipped:!0,reason:`language '${s??n}' is not minified`})}]};let i=Vs.readFileSync(e,"utf8"),r=i.split(`
199
199
  `).length,o=s==="vue"?Se(i):te(i,s),a=o.split(`
200
200
  `).length,c=r>0?Math.round((1-a/r)*100):0;return{content:[{type:"text",text:`[cairn: ${r} \u2192 ${a} lines (-${c}%)]
201
- ${o}`}]}}import Ne from"path";var Qs=[{group:"read",verbs:["get","fetch","find","load","retrieve","read","query","lookup","select"]},{group:"write",verbs:["create","add","insert","save","persist","store","put","post"]},{group:"update",verbs:["update","modify","edit","change","patch","set","replace","upsert"]},{group:"delete",verbs:["delete","remove","destroy","clear","purge","drop","erase","reset"]}],Zs=[["open","close"],["connect","disconnect"],["start","stop"],["init","destroy"],["init","cleanup"],["create","delete"],["enable","disable"],["lock","unlock"],["encode","decode"],["encrypt","decrypt"],["serialize","deserialize"],["subscribe","unsubscribe"],["register","unregister"],["login","logout"],["mount","unmount"],["acquire","release"],["push","pop"],["enqueue","dequeue"]],ei=5e3;function ti(t){let e=t.prepare(`SELECT f.repo,
201
+ ${o}`}]}}import Ne from"path";var Zs=[{group:"read",verbs:["get","fetch","find","load","retrieve","read","query","lookup","select"]},{group:"write",verbs:["create","add","insert","save","persist","store","put","post"]},{group:"update",verbs:["update","modify","edit","change","patch","set","replace","upsert"]},{group:"delete",verbs:["delete","remove","destroy","clear","purge","drop","erase","reset"]}],ei=[["open","close"],["connect","disconnect"],["start","stop"],["init","destroy"],["init","cleanup"],["create","delete"],["enable","disable"],["lock","unlock"],["encode","decode"],["encrypt","decrypt"],["serialize","deserialize"],["subscribe","unsubscribe"],["register","unregister"],["login","logout"],["mount","unmount"],["acquire","release"],["push","pop"],["enqueue","dequeue"]],ti=5e3;function ni(t){let e=t.prepare(`SELECT f.repo,
202
202
  COUNT(DISTINCT f.id) AS file_count,
203
203
  COUNT(DISTINCT s.id) AS symbol_count,
204
204
  GROUP_CONCAT(DISTINCT f.language) AS languages
@@ -206,9 +206,9 @@ ${o}`}]}}import Ne from"path";var Qs=[{group:"read",verbs:["get","fetch","find",
206
206
  LEFT JOIN symbols s ON s.file_id = f.id
207
207
  GROUP BY f.repo
208
208
  ORDER BY f.repo`).all(),n=e.map(i=>` ${i.repo.padEnd(40)} ${String(i.file_count).padStart(5)} files ${String(i.symbol_count).padStart(6)} symbols [${i.languages}]`),s=e.reduce((i,r)=>({files:i.files+r.file_count,symbols:i.symbols+r.symbol_count}),{files:0,symbols:0});return{content:[{type:"text",text:JSON.stringify({mode:"federation_summary",note:"Index is too large for a full outline. Use repo parameter to drill into a specific service.",total_repos:e.length,total_files:s.files,total_symbols:s.symbols,repos:e,usage:'Call cairn_outline with { repo: "<repo-name>" } to get the full outline for one service.',outline:n.join(`
209
- `)},null,2)}]}}function bn(t,{paths:e,repo:n,checks:s}={}){let i=process.cwd();if(!e?.length&&!n&&t.prepare("SELECT COUNT(*) AS n FROM symbols").get().n>ei)return ti(t);let r;if(n)r=t.prepare("SELECT id, path, language FROM files WHERE repo = ? ORDER BY path").all(n);else if(e&&e.length>0){let d=e.map(()=>"f.path LIKE ?").join(" OR ");r=t.prepare(`SELECT id, path, language FROM files f WHERE (${d}) ORDER BY path`).all(...e.map(h=>`%${h}%`))}else r=t.prepare("SELECT id, path, language FROM files ORDER BY path").all();let o=new Map,a=new Set;for(let d of r){let h=Ne.relative(i,d.path).replace(/\\/g,"/");a.has(h)||(a.add(h),o.set(d.id,{id:d.id,path:d.path,relPath:h,language:d.language,symbols:[]}))}let c=t.prepare("SELECT file_id, fqn, name, kind, exported, line FROM symbols ORDER BY file_id, kind, name").all();for(let d of c)o.has(d.file_id)&&o.get(d.file_id).symbols.push(d);let p=[];for(let d of o.values()){if(d.symbols.length===0)continue;p.push(`
210
- ${d.relPath} [${d.language}]`);let h=new Map,y=[];for(let _ of d.symbols)if(_.kind==="class"||_.kind==="interface"||_.kind==="enum")h.has(_.name)||h.set(_.name,{meta:_,members:[]});else if(_.fqn&&_.fqn.includes(".")){let x=_.fqn.split("."),b=x[x.length-2];h.has(b)?h.get(b).members.push(_):y.push(_)}else y.push(_);for(let[_,{meta:x,members:b}]of h){let C=x.exported?"+":"-",E=x.line?`:${x.line}`:"";p.push(` ${C} ${x.kind} ${_}${E}`);for(let R of b){let O=R.exported?"+":"-",L=R.line?`:${R.line}`:"";p.push(` ${O} ${R.name}${L} (${R.kind})`)}}for(let _ of y){if(_.kind==="class")continue;let x=_.exported?"+":"-",b=_.line?`:${_.line}`:"";p.push(` ${x} ${_.name}${b} (${_.kind})`)}}let l=[],u=new Set;for(let d of o.values())/[/\\](test|spec|__tests__)[/\\]|\.test\.|\.spec\./.test(d.path)&&u.add(Ne.basename(d.path).replace(/\.(test|spec)\.[a-z]+$/,"").toLowerCase());for(let d of o.values()){let h=/[/\\](test|spec|__tests__)[/\\]|\.test\.|\.spec\./.test(d.path),y=d.symbols.filter(E=>E.kind==="method"||E.kind==="function"),_=y.map(E=>E.name),x=_.map(E=>E.toLowerCase());y.length>20&&l.push({file:d.relPath,issue:"god_class",detail:`${y.length} methods \u2014 likely violates single responsibility`,confidence:y.length>35?"HIGH":"MEDIUM",jump_to:d.symbols.find(E=>E.kind==="class")?.name});let b=new Set;for(let[E,R]of Zs){let O=`${E}/${R}`;if(b.has(O))continue;let L=x.some(T=>T===E||T.startsWith(E+"_")||T.startsWith(E[0].toUpperCase()+E.slice(1))),M=x.some(T=>T===R||T.startsWith(R+"_")||T.startsWith(R[0].toUpperCase()+R.slice(1))),w=_.find(T=>T.toLowerCase()===E||T.toLowerCase().startsWith(E));L&&!M&&w&&(b.add(O),l.push({file:d.relPath,issue:"symmetry_gap",detail:`has \`${w}\` but no \`${R}*\` \u2014 possible incomplete lifecycle`,confidence:"MEDIUM",jump_to:w}))}for(let{group:E,verbs:R}of Qs){let O=new Set,L=new Map;for(let M of _){let w=M.toLowerCase();for(let T of R)if(w===T||w.startsWith(T)&&(w.length===T.length||/[A-Z_]/.test(M[T.length]))){O.add(T),L.has(T)||L.set(T,M);break}}if(O.size>=3){let M=[...O].map(w=>`\`${L.get(w)}\``).join(", ");l.push({file:d.relPath,issue:"naming_inconsistency",detail:`Mixed "${E}" verbs: ${M} \u2014 pick one convention`,confidence:"LOW",jump_to:L.get([...O][0])})}}if(!h&&d.symbols.some(E=>E.kind==="class")){let E=Ne.basename(d.path).replace(/\.[^.]+$/,"").toLowerCase();u.has(E)||l.push({file:d.relPath,issue:"missing_tests",detail:`No test file found for ${Ne.basename(d.path)}`,confidence:"LOW",jump_to:d.symbols.find(R=>R.kind==="class")?.name})}let C=d.symbols.filter(E=>E.kind==="class");for(let E of C){let R=E.fqn?E.fqn+".":E.name+".";!d.symbols.some(L=>L.fqn&&L.fqn.startsWith(R))&&!h&&l.push({file:d.relPath,issue:"empty_class",detail:`\`${E.name}\` has no indexed members \u2014 may be a stub or anemic model`,confidence:"LOW",jump_to:E.name})}}let m=new Map;for(let d of o.values())for(let h of d.symbols.filter(y=>y.kind==="class"))m.has(h.name)||m.set(h.name,new Set),m.get(h.name).add(d.relPath);for(let[d,h]of m){let y=[...h];y.length>1&&l.push({file:y.join(", "),issue:"duplicate_class_name",detail:`\`${d}\` defined in ${y.length} files \u2014 risk of confusion or shadowing`,confidence:"MEDIUM",jump_to:d})}let f={HIGH:0,MEDIUM:1,LOW:2};l.sort((d,h)=>(f[d.confidence]??3)-(f[h.confidence]??3));let g=ni(l);return{content:[{type:"text",text:JSON.stringify({file_count:o.size,symbol_count:c.length,suspect_count:l.length,summary:g,suspects:l,outline:p.join(`
211
- `).trim()},null,2)}]}}function ni(t){let e={};for(let o of t)e[o.issue]=(e[o.issue]||0)+1;let n=t.filter(o=>o.confidence==="HIGH").length,s=t.filter(o=>o.confidence==="MEDIUM").length,i=[];n&&i.push(`${n} HIGH`),s&&i.push(`${s} MEDIUM`);let r=t.length-n-s;return r&&i.push(`${r} LOW`),i.length?`${t.length} suspects (${i.join(", ")}): ${Object.entries(e).map(([o,a])=>`${a} ${o}`).join(", ")}`:"No structural suspects detected"}async function Tn(t,e={}){let{text:n,id:s,status:i,source:r}=e,o=e.action??"list",a=e.kind;switch(o){case"list":{let c="SELECT * FROM main.todos WHERE 1=1",p=[];i&&(c+=" AND status = ?",p.push(i)),r&&(c+=" AND source = ?",p.push(r)),a&&(c+=" AND kind = ?",p.push(a.toUpperCase())),c+=" ORDER BY CASE status WHEN 'open' THEN 0 ELSE 1 END, source, file, line";let l=t.prepare(c).all(...p),u=l.filter(f=>f.status==="open").length,m=l.filter(f=>f.status==="done").length;return{content:[{type:"text",text:JSON.stringify({total:l.length,open:u,done:m,todos:l},null,2)}]}}case"add":{if(!n)throw new Error("text is required for add action");let c=(a||"TODO").toUpperCase(),p=t.prepare("INSERT INTO main.todos (source, kind, text) VALUES (?, ?, ?)").run("manual",c,n),l=t.prepare("SELECT * FROM main.todos WHERE id = ?").get(p.lastInsertRowid);return{content:[{type:"text",text:JSON.stringify({added:l},null,2)}]}}case"resolve":{if(!s)throw new Error("id is required for resolve action");if(t.prepare("UPDATE main.todos SET status = ?, resolved_at = datetime('now') WHERE id = ?").run("done",s).changes===0)throw new Error(`No todo found with id ${s}`);let p=t.prepare("SELECT * FROM main.todos WHERE id = ?").get(s);return{content:[{type:"text",text:JSON.stringify({resolved:p},null,2)}]}}case"scan":{let c=await q(process.cwd()),p=await _e(t,c),l=t.prepare("SELECT COUNT(*) AS n FROM main.todos WHERE status = 'open'").get().n;return{content:[{type:"text",text:JSON.stringify({scanned_files:c.length,todos_found:p,open_todos:l},null,2)}]}}default:throw new Error(`Unknown action: "${o}". Use list, add, resolve, or scan.`)}}import V from"fs";import X from"path";var wn=["package.json","pom.xml","Cargo.toml","go.mod","pyproject.toml","build.gradle","build.gradle.kts","composer.json","Gemfile","CMakeLists.txt"];function ze(t){let e;try{e=V.statSync(t).isDirectory()?t:X.dirname(t)}catch{return null}for(;;){if(V.existsSync(X.join(e,".cairn",".cairn-project"))||V.existsSync(X.join(e,".git")))return e;for(let s of wn)if(V.existsSync(X.join(e,s)))return e;let n=X.dirname(e);if(n===e)return null;e=n}}function Rn(t,{project_path:e}={}){let n=process.cwd(),s=N();if(!e)return Le(null),{content:[{type:"text",text:JSON.stringify({switched:!0,project_root:n,previous_root:s,action:"restored_to_session_root"},null,2)}]};let i=ze(e);if(!i)return{content:[{type:"text",text:JSON.stringify({switched:!1,reason:"Could not detect a project root from the given path. No .git, package.json, or other project markers found.",path_given:e},null,2)}]};let r=V.existsSync(X.join(i,".cairn","index.db")),o=[];V.existsSync(X.join(i,".cairn",".cairn-project"))&&o.push("cairn project"),V.existsSync(X.join(i,".git"))&&o.push("git repo");for(let a of wn)if(V.existsSync(X.join(i,a))){o.push(a);break}return Le(i),{content:[{type:"text",text:JSON.stringify({switched:!0,project_root:i,previous_root:s,detected_by:o,has_cairn_index:r,tip:r?"Call cairn_resume to restore this project's session, or cairn_search to query its index.":"No cairn index found. Call cairn_maintain to index this project first."},null,2)}]}}import me from"fs";import Z from"path";function On(t,{source_path:e,types:n,overwrite:s=!1}){if(!e)throw new Error("source_path is required");let i,r=e.replace(/\\/g,"/");if(r.endsWith("/.cairn/memory")||r.endsWith("\\.cairn\\memory"))i=e;else if(r.endsWith("/.cairn")||r.endsWith("\\.cairn"))i=Z.join(e,"memory");else{let f=ze(e);if(!f)return{content:[{type:"text",text:JSON.stringify({migrated:!1,reason:"Could not detect a project root from source_path.",source_path:e},null,2)}]};i=Z.join(f,".cairn","memory")}if(!me.existsSync(i))return{content:[{type:"text",text:JSON.stringify({migrated:!1,reason:"Source project has no .cairn/memory directory. Run cairn_maintain or cairn_resume there first.",source_memory_dir:i},null,2)}]};let o=n&&n.length>0?n:Jt,a=G(i).filter(f=>o.includes(f.type));if(a.length===0)return{content:[{type:"text",text:JSON.stringify({migrated:!1,reason:`No memories of types [${o.join(", ")}] found in source project.`,source_memory_dir:i},null,2)}]};let c=ee(),p=Z.dirname(Z.dirname(i)),l=G(c),u=new Set(l.map(f=>f.file)),m={copied:[],skipped:[],errors:[]};for(let f of a){let g=Z.join(i,f.file);if(!me.existsSync(g)){m.errors.push({name:f.name,reason:"source file missing"});continue}let d=Z.join(c,f.file),h=u.has(f.file);if(h&&!s){m.skipped.push({name:f.name,reason:"already exists (use overwrite: true to replace)"});continue}let y=me.readFileSync(g,"utf8");y.includes(`
209
+ `)},null,2)}]}}function bn(t,{paths:e,repo:n,checks:s}={}){let i=process.cwd();if(!e?.length&&!n&&t.prepare("SELECT COUNT(*) AS n FROM symbols").get().n>ti)return ni(t);let r;if(n)r=t.prepare("SELECT id, path, language FROM files WHERE repo = ? ORDER BY path").all(n);else if(e&&e.length>0){let d=e.map(()=>"f.path LIKE ?").join(" OR ");r=t.prepare(`SELECT id, path, language FROM files f WHERE (${d}) ORDER BY path`).all(...e.map(h=>`%${h}%`))}else r=t.prepare("SELECT id, path, language FROM files ORDER BY path").all();let o=new Map,a=new Set;for(let d of r){let h=Ne.relative(i,d.path).replace(/\\/g,"/");a.has(h)||(a.add(h),o.set(d.id,{id:d.id,path:d.path,relPath:h,language:d.language,symbols:[]}))}let c=t.prepare("SELECT file_id, fqn, name, kind, exported, line FROM symbols ORDER BY file_id, kind, name").all();for(let d of c)o.has(d.file_id)&&o.get(d.file_id).symbols.push(d);let p=[];for(let d of o.values()){if(d.symbols.length===0)continue;p.push(`
210
+ ${d.relPath} [${d.language}]`);let h=new Map,y=[];for(let _ of d.symbols)if(_.kind==="class"||_.kind==="interface"||_.kind==="enum")h.has(_.name)||h.set(_.name,{meta:_,members:[]});else if(_.fqn&&_.fqn.includes(".")){let x=_.fqn.split("."),b=x[x.length-2];h.has(b)?h.get(b).members.push(_):y.push(_)}else y.push(_);for(let[_,{meta:x,members:b}]of h){let C=x.exported?"+":"-",E=x.line?`:${x.line}`:"";p.push(` ${C} ${x.kind} ${_}${E}`);for(let R of b){let O=R.exported?"+":"-",L=R.line?`:${R.line}`:"";p.push(` ${O} ${R.name}${L} (${R.kind})`)}}for(let _ of y){if(_.kind==="class")continue;let x=_.exported?"+":"-",b=_.line?`:${_.line}`:"";p.push(` ${x} ${_.name}${b} (${_.kind})`)}}let l=[],u=new Set;for(let d of o.values())/[/\\](test|spec|__tests__)[/\\]|\.test\.|\.spec\./.test(d.path)&&u.add(Ne.basename(d.path).replace(/\.(test|spec)\.[a-z]+$/,"").toLowerCase());for(let d of o.values()){let h=/[/\\](test|spec|__tests__)[/\\]|\.test\.|\.spec\./.test(d.path),y=d.symbols.filter(E=>E.kind==="method"||E.kind==="function"),_=y.map(E=>E.name),x=_.map(E=>E.toLowerCase());y.length>20&&l.push({file:d.relPath,issue:"god_class",detail:`${y.length} methods \u2014 likely violates single responsibility`,confidence:y.length>35?"HIGH":"MEDIUM",jump_to:d.symbols.find(E=>E.kind==="class")?.name});let b=new Set;for(let[E,R]of ei){let O=`${E}/${R}`;if(b.has(O))continue;let L=x.some(T=>T===E||T.startsWith(E+"_")||T.startsWith(E[0].toUpperCase()+E.slice(1))),M=x.some(T=>T===R||T.startsWith(R+"_")||T.startsWith(R[0].toUpperCase()+R.slice(1))),w=_.find(T=>T.toLowerCase()===E||T.toLowerCase().startsWith(E));L&&!M&&w&&(b.add(O),l.push({file:d.relPath,issue:"symmetry_gap",detail:`has \`${w}\` but no \`${R}*\` \u2014 possible incomplete lifecycle`,confidence:"MEDIUM",jump_to:w}))}for(let{group:E,verbs:R}of Zs){let O=new Set,L=new Map;for(let M of _){let w=M.toLowerCase();for(let T of R)if(w===T||w.startsWith(T)&&(w.length===T.length||/[A-Z_]/.test(M[T.length]))){O.add(T),L.has(T)||L.set(T,M);break}}if(O.size>=3){let M=[...O].map(w=>`\`${L.get(w)}\``).join(", ");l.push({file:d.relPath,issue:"naming_inconsistency",detail:`Mixed "${E}" verbs: ${M} \u2014 pick one convention`,confidence:"LOW",jump_to:L.get([...O][0])})}}if(!h&&d.symbols.some(E=>E.kind==="class")){let E=Ne.basename(d.path).replace(/\.[^.]+$/,"").toLowerCase();u.has(E)||l.push({file:d.relPath,issue:"missing_tests",detail:`No test file found for ${Ne.basename(d.path)}`,confidence:"LOW",jump_to:d.symbols.find(R=>R.kind==="class")?.name})}let C=d.symbols.filter(E=>E.kind==="class");for(let E of C){let R=E.fqn?E.fqn+".":E.name+".";!d.symbols.some(L=>L.fqn&&L.fqn.startsWith(R))&&!h&&l.push({file:d.relPath,issue:"empty_class",detail:`\`${E.name}\` has no indexed members \u2014 may be a stub or anemic model`,confidence:"LOW",jump_to:E.name})}}let m=new Map;for(let d of o.values())for(let h of d.symbols.filter(y=>y.kind==="class"))m.has(h.name)||m.set(h.name,new Set),m.get(h.name).add(d.relPath);for(let[d,h]of m){let y=[...h];y.length>1&&l.push({file:y.join(", "),issue:"duplicate_class_name",detail:`\`${d}\` defined in ${y.length} files \u2014 risk of confusion or shadowing`,confidence:"MEDIUM",jump_to:d})}let f={HIGH:0,MEDIUM:1,LOW:2};l.sort((d,h)=>(f[d.confidence]??3)-(f[h.confidence]??3));let g=si(l);return{content:[{type:"text",text:JSON.stringify({file_count:o.size,symbol_count:c.length,suspect_count:l.length,summary:g,suspects:l,outline:p.join(`
211
+ `).trim()},null,2)}]}}function si(t){let e={};for(let o of t)e[o.issue]=(e[o.issue]||0)+1;let n=t.filter(o=>o.confidence==="HIGH").length,s=t.filter(o=>o.confidence==="MEDIUM").length,i=[];n&&i.push(`${n} HIGH`),s&&i.push(`${s} MEDIUM`);let r=t.length-n-s;return r&&i.push(`${r} LOW`),i.length?`${t.length} suspects (${i.join(", ")}): ${Object.entries(e).map(([o,a])=>`${a} ${o}`).join(", ")}`:"No structural suspects detected"}async function Tn(t,e={}){let{text:n,id:s,status:i,source:r}=e,o=e.action??"list",a=e.kind;switch(o){case"list":{let c="SELECT * FROM main.todos WHERE 1=1",p=[];i&&(c+=" AND status = ?",p.push(i)),r&&(c+=" AND source = ?",p.push(r)),a&&(c+=" AND kind = ?",p.push(a.toUpperCase())),c+=" ORDER BY CASE status WHEN 'open' THEN 0 ELSE 1 END, source, file, line";let l=t.prepare(c).all(...p),u=l.filter(f=>f.status==="open").length,m=l.filter(f=>f.status==="done").length;return{content:[{type:"text",text:JSON.stringify({total:l.length,open:u,done:m,todos:l},null,2)}]}}case"add":{if(!n)throw new Error("text is required for add action");let c=(a||"TODO").toUpperCase(),p=t.prepare("INSERT INTO main.todos (source, kind, text) VALUES (?, ?, ?)").run("manual",c,n),l=t.prepare("SELECT * FROM main.todos WHERE id = ?").get(p.lastInsertRowid);return{content:[{type:"text",text:JSON.stringify({added:l},null,2)}]}}case"resolve":{if(!s)throw new Error("id is required for resolve action");if(t.prepare("UPDATE main.todos SET status = ?, resolved_at = datetime('now') WHERE id = ?").run("done",s).changes===0)throw new Error(`No todo found with id ${s}`);let p=t.prepare("SELECT * FROM main.todos WHERE id = ?").get(s);return{content:[{type:"text",text:JSON.stringify({resolved:p},null,2)}]}}case"scan":{let c=await q(process.cwd()),p=await _e(t,c),l=t.prepare("SELECT COUNT(*) AS n FROM main.todos WHERE status = 'open'").get().n;return{content:[{type:"text",text:JSON.stringify({scanned_files:c.length,todos_found:p,open_todos:l},null,2)}]}}default:throw new Error(`Unknown action: "${o}". Use list, add, resolve, or scan.`)}}import V from"fs";import X from"path";var wn=["package.json","pom.xml","Cargo.toml","go.mod","pyproject.toml","build.gradle","build.gradle.kts","composer.json","Gemfile","CMakeLists.txt"];function ze(t){let e;try{e=V.statSync(t).isDirectory()?t:X.dirname(t)}catch{return null}for(;;){if(V.existsSync(X.join(e,".cairn",".cairn-project"))||V.existsSync(X.join(e,".git")))return e;for(let s of wn)if(V.existsSync(X.join(e,s)))return e;let n=X.dirname(e);if(n===e)return null;e=n}}function Rn(t,{project_path:e}={}){let n=process.cwd(),s=N();if(!e)return Le(null),{content:[{type:"text",text:JSON.stringify({switched:!0,project_root:n,previous_root:s,action:"restored_to_session_root"},null,2)}]};let i=ze(e);if(!i)return{content:[{type:"text",text:JSON.stringify({switched:!1,reason:"Could not detect a project root from the given path. No .git, package.json, or other project markers found.",path_given:e},null,2)}]};let r=V.existsSync(X.join(i,".cairn","index.db")),o=[];V.existsSync(X.join(i,".cairn",".cairn-project"))&&o.push("cairn project"),V.existsSync(X.join(i,".git"))&&o.push("git repo");for(let a of wn)if(V.existsSync(X.join(i,a))){o.push(a);break}return Le(i),{content:[{type:"text",text:JSON.stringify({switched:!0,project_root:i,previous_root:s,detected_by:o,has_cairn_index:r,tip:r?"Call cairn_resume to restore this project's session, or cairn_search to query its index.":"No cairn index found. Call cairn_maintain to index this project first."},null,2)}]}}import me from"fs";import Z from"path";function On(t,{source_path:e,types:n,overwrite:s=!1}){if(!e)throw new Error("source_path is required");let i,r=e.replace(/\\/g,"/");if(r.endsWith("/.cairn/memory")||r.endsWith("\\.cairn\\memory"))i=e;else if(r.endsWith("/.cairn")||r.endsWith("\\.cairn"))i=Z.join(e,"memory");else{let f=ze(e);if(!f)return{content:[{type:"text",text:JSON.stringify({migrated:!1,reason:"Could not detect a project root from source_path.",source_path:e},null,2)}]};i=Z.join(f,".cairn","memory")}if(!me.existsSync(i))return{content:[{type:"text",text:JSON.stringify({migrated:!1,reason:"Source project has no .cairn/memory directory. Run cairn_maintain or cairn_resume there first.",source_memory_dir:i},null,2)}]};let o=n&&n.length>0?n:Jt,a=G(i).filter(f=>o.includes(f.type));if(a.length===0)return{content:[{type:"text",text:JSON.stringify({migrated:!1,reason:`No memories of types [${o.join(", ")}] found in source project.`,source_memory_dir:i},null,2)}]};let c=ee(),p=Z.dirname(Z.dirname(i)),l=G(c),u=new Set(l.map(f=>f.file)),m={copied:[],skipped:[],errors:[]};for(let f of a){let g=Z.join(i,f.file);if(!me.existsSync(g)){m.errors.push({name:f.name,reason:"source file missing"});continue}let d=Z.join(c,f.file),h=u.has(f.file);if(h&&!s){m.skipped.push({name:f.name,reason:"already exists (use overwrite: true to replace)"});continue}let y=me.readFileSync(g,"utf8");y.includes(`
212
212
  origin:`)||(y=y.replace(`---
213
213
 
214
214
  `,`---
@@ -218,7 +218,7 @@ origin: ${p}
218
218
  origin: ${p}
219
219
  `)));try{me.writeFileSync(d,y,"utf8"),h||(l.push({...f}),u.add(f.file)),m.copied.push({name:f.name,type:f.type,origin:p})}catch(_){m.errors.push({name:f.name,reason:_.message})}}if(m.copied.length>0){let f=["# Memory Index",""];for(let g of l)f.push(`- [${g.name}](${g.file}) \u2014 ${g.description} \`[${g.type}]\``);me.writeFileSync(Z.join(c,"MEMORY.md"),f.join(`
220
220
  `)+`
221
- `,"utf8")}return{content:[{type:"text",text:JSON.stringify({migrated:m.copied.length>0,source:p,types_migrated:o,copied:m.copied.length,skipped:m.skipped.length,errors:m.errors.length,details:m},null,2)}]}}import si from"fs";var Ke=Date.now(),Ve=!1;function Qe(){Ke=Date.now()}async function Ze(t){if(Ve)return;Ve=!0;let e=Ke;Ke=Date.now();try{let n=await q(N()),s=[];for(let i of n)try{si.statSync(i).mtimeMs>e&&s.push(i)}catch{}s.length>0&&await Ee(t,s)}finally{Ve=!1}}function Nn(t,e=3e4){let n=setInterval(()=>Ze(t),e);n.unref&&n.unref()}var $=it();Nn($);var et=new ii({name:"cairn",version:"1.0.0"},{capabilities:{tools:{}}});et.setRequestHandler(ai,async()=>({tools:[{name:"cairn_maintain",description:"Index the current project into Cairn's polyglot knowledge graph. Supports Java, TypeScript, JavaScript, Vue, Python, SQL, config, and Markdown. Indexes from the current working directory. Run at session start or after major changes.",inputSchema:{type:"object",properties:{languages:{type:"array",items:{type:"string"},description:'Optional: limit to specific languages e.g. ["java","typescript"]'}}}},{name:"cairn_search",description:"Search the codebase by concept across all languages. Returns ranked symbols with scores.",inputSchema:{type:"object",properties:{query:{type:"string"},limit:{type:"number",default:10},language:{type:"string",description:"Optional: filter by language"},kind:{type:"string",description:"Optional: class/function/component/table/..."}},required:["query"]}},{name:"cairn_describe",description:"Describe what a directory/module does: symbols, responsibilities, upstream/downstream deps.",inputSchema:{type:"object",properties:{path:{type:"string",description:"Directory or file path to describe"}},required:["path"]}},{name:"cairn_code_graph",description:"Analyze module dependencies and instability metrics across all languages. Use before refactoring.",inputSchema:{type:"object",properties:{module:{type:"string",description:"Optional: scope to a specific module/package"},mode:{type:"string",enum:["instability","health","cycles"]}},required:["mode"]}},{name:"cairn_security",description:"Scan for security vulnerabilities across all languages (XSS, XXE, SQLi, weak crypto, hardcoded secrets, etc.)",inputSchema:{type:"object",properties:{paths:{type:"array",items:{type:"string"},description:"Optional: scope to specific paths"},severity:{type:"string",enum:["HIGH","MEDIUM","LOW","ALL"],default:"HIGH"},language:{type:"string",description:"Optional: scope to a specific language"}}}},{name:"cairn_bundle",description:`Produce a minified single-file snapshot of source code for Claude to read.
221
+ `,"utf8")}return{content:[{type:"text",text:JSON.stringify({migrated:m.copied.length>0,source:p,types_migrated:o,copied:m.copied.length,skipped:m.skipped.length,errors:m.errors.length,details:m},null,2)}]}}import ii from"fs";var Ke=Date.now(),Ve=!1;function Qe(){Ke=Date.now()}async function Ze(t){if(Ve)return;Ve=!0;let e=Ke;Ke=Date.now();try{let n=await q(N()),s=[];for(let i of n)try{ii.statSync(i).mtimeMs>e&&s.push(i)}catch{}s.length>0&&await Ee(t,s)}finally{Ve=!1}}function Nn(t,e=3e4){let n=setInterval(()=>Ze(t),e);n.unref&&n.unref()}var $=it();Nn($);var et=new ri({name:"cairn",version:"1.0.0"},{capabilities:{tools:{}}});et.setRequestHandler(ci,async()=>({tools:[{name:"cairn_maintain",description:"Index the current project into Cairn's polyglot knowledge graph. Supports Java, TypeScript, JavaScript, Vue, Python, SQL, config, and Markdown. Indexes from the current working directory. Run at session start or after major changes.",inputSchema:{type:"object",properties:{languages:{type:"array",items:{type:"string"},description:'Optional: limit to specific languages e.g. ["java","typescript"]'}}}},{name:"cairn_search",description:"Search the codebase by concept across all languages. Returns ranked symbols with scores.",inputSchema:{type:"object",properties:{query:{type:"string"},limit:{type:"number",default:10},language:{type:"string",description:"Optional: filter by language"},kind:{type:"string",description:"Optional: class/function/component/table/..."}},required:["query"]}},{name:"cairn_describe",description:"Describe what a directory/module does: symbols, responsibilities, upstream/downstream deps.",inputSchema:{type:"object",properties:{path:{type:"string",description:"Directory or file path to describe"}},required:["path"]}},{name:"cairn_code_graph",description:"Analyze module dependencies and instability metrics across all languages. Use before refactoring.",inputSchema:{type:"object",properties:{module:{type:"string",description:"Optional: scope to a specific module/package"},mode:{type:"string",enum:["instability","health","cycles"]}},required:["mode"]}},{name:"cairn_security",description:"Scan for security vulnerabilities across all languages (XSS, XXE, SQLi, weak crypto, hardcoded secrets, etc.)",inputSchema:{type:"object",properties:{paths:{type:"array",items:{type:"string"},description:"Optional: scope to specific paths"},severity:{type:"string",enum:["HIGH","MEDIUM","LOW","ALL"],default:"HIGH"},language:{type:"string",description:"Optional: scope to a specific language"}}}},{name:"cairn_bundle",description:`Produce a minified single-file snapshot of source code for Claude to read.
222
222
  Strips comments, empty lines, and optionally styles. Returns the bundle file path and compression stats.
223
223
  Use AFTER cairn_search to get a readable version of the files Claude needs to work with.
224
224
  Typical workflow: cairn_search \u2192 find files \u2192 cairn_bundle those paths \u2192 Claude reads bundle.`,inputSchema:{type:"object",properties:{bundle_name:{type:"string",default:"default",description:"Name for the bundle file (saved to .cairn/bundles/<name>.txt)"},filter_paths:{type:"array",items:{type:"string"},description:"Optional: only include files under these relative paths"},filter_language:{type:"string",description:"Optional: only bundle this language (java/typescript/vue/python/...)"},no_comments:{type:"boolean",default:!0,description:"Strip code comments"},no_empty_lines:{type:"boolean",default:!0,description:"Remove empty lines"},no_style:{type:"boolean",default:!1,description:"Skip <style> blocks in Vue files"},aggressive:{type:"boolean",default:!1,description:"Aggressive whitespace/punctuation minification"},only_changed:{type:"boolean",default:!1,description:"Incremental: only re-bundle files changed since last run"},max_size_kb:{type:"number",default:800,description:"Safety cap in KB to avoid context window overflow"}}}},{name:"cairn_checkpoint",description:"Save current session state to .cairn/session.json so it can be restored next session with cairn_resume. At a stopping point (before /clear or /compact, or when the user is leaving) answer would_be_lost \u2014 it is the only part of a checkpoint that cannot be recovered from git or the index.",inputSchema:{type:"object",properties:{message:{type:"string",description:"What you were working on \u2014 will be shown on resume"},active_files:{type:"array",items:{type:"string"},description:"Optional: absolute paths to files actively being worked on"},notes:{type:"array",items:{type:"string"},description:"Optional: things Claude should remember next session"},[k]:{type:"string",description:jt}},required:["message"]}},{name:"cairn_resume",description:"Restore the last saved session state. Detects which files changed since the checkpoint and incrementally re-indexes only those files. Also loads preference memories and surfaces the memory index. Call at the start of a session instead of cairn_maintain when resuming work. If the project was moved to a different drive or directory, paths are repaired automatically.",inputSchema:{type:"object",properties:{}}},{name:"cairn_outline",description:`Generate a structural outline of the project (or a scoped subset) and run heuristic analysis to detect likely issues.
@@ -237,4 +237,4 @@ Detects:
237
237
  - empty_class: class with no indexed members (stub or anemic model)
238
238
  - duplicate_class_name: same class name defined in multiple files
239
239
 
240
- Use to: get a project-wide bird's-eye view, triage code quality, find where to look first.`,inputSchema:{type:"object",properties:{repo:{type:"string",description:'Optional: scope to a specific repo/service by name (e.g. "elprint-component-service")'},paths:{type:"array",items:{type:"string"},description:"Optional: scope to specific file paths or directories (substring match)"},checks:{type:"array",items:{type:"string"},description:'Optional: limit to specific checks e.g. ["god_class","symmetry_gap"]'}}}},{name:"cairn_minify",description:"Minify a single source file and return compressed content. Strips comments and empty lines. Useful when passive hooks are not installed. Supports java, typescript, javascript, vue, python, sql.",inputSchema:{type:"object",properties:{file_path:{type:"string",description:"Absolute path to the file to minify"}},required:["file_path"]}},{name:"cairn_memo",description:'Persist and retrieve typed memories in .cairn/memory/. Use this \u2014 not the auto-memory system \u2014 for all project-level memories. Triggered by: "save cairn memory", "cairn remember", "build cairn memories", "save experience to cairn". Four types: preference (user working style \u2014 always loaded on resume), experience (transferable lessons learned \u2014 migrated between projects), decision (project-specific architectural choices), knowledge (project-specific codebase facts). Memories survive context loss and accumulate across sessions.',inputSchema:{type:"object",properties:{action:{type:"string",enum:["write","read","list","delete"],description:"write: save/update a memory | read: load a memory by name | list: show all memories with index | delete: remove a memory"},name:{type:"string",description:"Memory identifier (required for write/read/delete)"},type:{type:"string",enum:["preference","experience","decision","knowledge"],description:"Memory type (required for write)"},content:{type:"string",description:"Memory body \u2014 what should be remembered (required for write)"},description:{type:"string",description:"One-line summary shown in the memory index"},origin:{type:"string",description:"Optional: source project path, for traceability of migrated memories"}},required:["action"]}},{name:"cairn_switch",description:"Switch the active Cairn project context to a different directory. Detects the project root automatically from any path within the project (via .git, package.json, .cairn, etc.). Call cairn_resume or cairn_maintain after switching. Call with no arguments to return to the original session's project.",inputSchema:{type:"object",properties:{project_path:{type:"string",description:"Any path inside the target project (file or directory). Omit to restore the original session root."}}}},{name:"cairn_employ_memory",description:"Migrate memories from another project's .cairn/memory/ into the current project. By default copies only transferable types (preference + experience) \u2014 skips project-specific decision and knowledge. Imported memories are tagged with their origin project path for traceability.",inputSchema:{type:"object",properties:{source_path:{type:"string",description:"Path to the source project (any file/directory inside it, or its .cairn directory)"},types:{type:"array",items:{type:"string",enum:["preference","experience","decision","knowledge"]},description:'Optional: memory types to migrate (default: ["preference", "experience"])'},overwrite:{type:"boolean",default:!1,description:"Overwrite existing memories with the same filename (default: false)"}},required:["source_path"]}},{name:"cairn_todos",description:"Long-tail backlog: scanned TODO/FIXME/HACK/XXX comments + manual notes. Stats are included in cairn_maintain and cairn_resume reports. For the active multi-phase plan (authored in .cairn/roadmap.md, with a cursor + deps) use cairn_roadmap instead.",inputSchema:{type:"object",properties:{action:{type:"string",enum:["list","add","resolve","scan"],description:"list: show todos | add: create a manual todo | resolve: mark done by id | scan: re-scan codebase"},text:{type:"string",description:"Text for the new todo (add action)"},kind:{type:"string",enum:["TODO","FIXME","HACK","XXX","NOTE"],description:"Kind for new todo (default: TODO)"},id:{type:"number",description:"Todo ID to resolve (resolve action)"},status:{type:"string",enum:["open","done"],description:"Filter by status (list action)"},source:{type:"string",enum:["scan","manual"],description:"Filter by source (list action)"}},required:["action"]}},{name:"cairn_roadmap",description:"Active project plan: phases with status + cursor + dep graph. Phases are AUTHORED in the first-class document .cairn/roadmap.md (NOT in memory) via a `## Phases` section (### Phase 1: title, ### Phase 2: ...). Read actions (tree/status/next/sync) re-seed from that file, so editing roadmap.md IS editing the plan (existing phases preserved, removed marked stale, added appended). Marking a phase done prunes it from roadmap.md and appends it to .cairn/roadmap_completed.md (append-only log); the done row stays in the DB but is hidden from the tree, so the plan always shows what is next. Sub-tasks under a phase are ad-hoc via add --parent. Inter-phase deps via deps_add \u2014 `next` will skip dep-blocked phases and explain in the reason. The Stop hook re-prints this tree after every assistant turn while a cursor is set.",inputSchema:{type:"object",properties:{action:{type:"string",enum:["tree","status","sync","publish","focus","next","set_status","add","deps_add","deps_remove","deps_list"],description:"tree: render the roadmap (M/N done rollup; stale + shipped-done hidden by default \u2014 pass show_stale: true for stale) | status: one-line summary | sync: re-seed from .cairn/roadmap.md (also runs automatically on tree/status/next) | publish: (re)generate the human-readable ROADMAP.md at the repo root from the live plan (also runs automatically after every action; write-if-changed, never a placeholder) | focus <id>: move cursor, mark in_progress, propagate up (warns if dep-blocked but does not refuse) | next: DFS-advance cursor to next actionable phase (skips stale + dep-blocked, returns reason) | set_status <id> <status>: open|in_progress|done|blocked|stale (done auto-advances cursor; a done roadmap phase is pruned from roadmap.md + logged to roadmap_completed.md) | add <parent> <text>: ad-hoc sub-task under an existing phase | deps_add <from> <to>: declare phase <from> blocks phase <to> (cycle-checked) | deps_remove <from> <to> | deps_list [<id>]: list edges (or just edges touching <id>)"},id:{type:"number",description:"Phase ID (focus, set_status, deps_list)"},status:{type:"string",enum:["open","in_progress","done","blocked","stale"],description:"New status (set_status)"},parent:{type:"number",description:"Parent phase ID (add \u2014 required, ad-hoc sub-tasks must hang under an existing phase)"},text:{type:"string",description:"Sub-task text (add action)"},compact:{type:"boolean",description:"Compact tree render \u2014 strips id tags (tree action)"},show_stale:{type:"boolean",description:"Include stale phases in tree (default: hidden)"},hide_stale:{type:"boolean",description:"Deprecated \u2014 stale is hidden by default. Pass hide_stale: false to show, or use show_stale: true."},decision:{type:"string",description:"Filter tree to a single decision (tree action)"},from:{type:"number",description:"Blocker phase ID (deps_add, deps_remove)"},to:{type:"number",description:"Blocked phase ID (deps_add, deps_remove)"}},required:["action"]}}]}));var ci=new Set(["cairn_search","cairn_bundle","cairn_describe","cairn_outline","cairn_code_graph","cairn_security"]);et.setRequestHandler(oi,async t=>{let{name:e,arguments:n}=t.params;switch(ci.has(e)&&await Ze($),e){case"cairn_maintain":{let s=await Lt($,n);return Qe(),s}case"cairn_search":return $t($,n);case"cairn_describe":return At($,n);case"cairn_code_graph":return It($,n);case"cairn_security":return Ct($,n);case"cairn_bundle":return await Ft($,n);case"cairn_checkpoint":return Ut($,{...n,auto:!1});case"cairn_resume":{let s=await Sn($);return Qe(),s}case"cairn_outline":return bn($,n);case"cairn_minify":return xn($,n);case"cairn_todos":return await Tn($,n);case"cairn_roadmap":{let s=ue($,n),i=fn($);return i&&Array.isArray(s?.content)&&s.content.push({type:"text",text:i}),s}case"cairn_memo":return Kt($,n);case"cairn_switch":return Rn($,n);case"cairn_employ_memory":return On($,n);default:throw new Error(`Unknown tool: ${e}`)}});var li=new ri;await et.connect(li);
240
+ Use to: get a project-wide bird's-eye view, triage code quality, find where to look first.`,inputSchema:{type:"object",properties:{repo:{type:"string",description:'Optional: scope to a specific repo/service by name (e.g. "elprint-component-service")'},paths:{type:"array",items:{type:"string"},description:"Optional: scope to specific file paths or directories (substring match)"},checks:{type:"array",items:{type:"string"},description:'Optional: limit to specific checks e.g. ["god_class","symmetry_gap"]'}}}},{name:"cairn_minify",description:"Minify a single source file and return compressed content. Strips comments and empty lines. Useful when passive hooks are not installed. Supports java, typescript, javascript, vue, python, sql.",inputSchema:{type:"object",properties:{file_path:{type:"string",description:"Absolute path to the file to minify"}},required:["file_path"]}},{name:"cairn_memo",description:'Persist and retrieve typed memories in .cairn/memory/. Use this \u2014 not the auto-memory system \u2014 for all project-level memories. Triggered by: "save cairn memory", "cairn remember", "build cairn memories", "save experience to cairn". Four types: preference (user working style \u2014 always loaded on resume), experience (transferable lessons learned \u2014 migrated between projects), decision (project-specific architectural choices), knowledge (project-specific codebase facts). Memories survive context loss and accumulate across sessions.',inputSchema:{type:"object",properties:{action:{type:"string",enum:["write","read","list","delete"],description:"write: save/update a memory | read: load a memory by name | list: show all memories with index | delete: remove a memory"},name:{type:"string",description:"Memory identifier (required for write/read/delete)"},type:{type:"string",enum:["preference","experience","decision","knowledge"],description:"Memory type (required for write)"},content:{type:"string",description:"Memory body \u2014 what should be remembered (required for write)"},description:{type:"string",description:"One-line summary shown in the memory index"},origin:{type:"string",description:"Optional: source project path, for traceability of migrated memories"}},required:["action"]}},{name:"cairn_switch",description:"Switch the active Cairn project context to a different directory. Detects the project root automatically from any path within the project (via .git, package.json, .cairn, etc.). Call cairn_resume or cairn_maintain after switching. Call with no arguments to return to the original session's project.",inputSchema:{type:"object",properties:{project_path:{type:"string",description:"Any path inside the target project (file or directory). Omit to restore the original session root."}}}},{name:"cairn_employ_memory",description:"Migrate memories from another project's .cairn/memory/ into the current project. By default copies only transferable types (preference + experience) \u2014 skips project-specific decision and knowledge. Imported memories are tagged with their origin project path for traceability.",inputSchema:{type:"object",properties:{source_path:{type:"string",description:"Path to the source project (any file/directory inside it, or its .cairn directory)"},types:{type:"array",items:{type:"string",enum:["preference","experience","decision","knowledge"]},description:'Optional: memory types to migrate (default: ["preference", "experience"])'},overwrite:{type:"boolean",default:!1,description:"Overwrite existing memories with the same filename (default: false)"}},required:["source_path"]}},{name:"cairn_todos",description:"Long-tail backlog: scanned TODO/FIXME/HACK/XXX comments + manual notes. Stats are included in cairn_maintain and cairn_resume reports. For the active multi-phase plan (authored in .cairn/roadmap.md, with a cursor + deps) use cairn_roadmap instead.",inputSchema:{type:"object",properties:{action:{type:"string",enum:["list","add","resolve","scan"],description:"list: show todos | add: create a manual todo | resolve: mark done by id | scan: re-scan codebase"},text:{type:"string",description:"Text for the new todo (add action)"},kind:{type:"string",enum:["TODO","FIXME","HACK","XXX","NOTE"],description:"Kind for new todo (default: TODO)"},id:{type:"number",description:"Todo ID to resolve (resolve action)"},status:{type:"string",enum:["open","done"],description:"Filter by status (list action)"},source:{type:"string",enum:["scan","manual"],description:"Filter by source (list action)"}},required:["action"]}},{name:"cairn_roadmap",description:"Active project plan: phases with status + cursor + dep graph. Phases are AUTHORED in the first-class document .cairn/roadmap.md (NOT in memory) via a `## Phases` section (### Phase 1: title, ### Phase 2: ...). Read actions (tree/status/next/sync) re-seed from that file, so editing roadmap.md IS editing the plan (existing phases preserved, removed marked stale, added appended). Marking a phase done prunes it from roadmap.md and appends it to .cairn/roadmap_completed.md (append-only log); the done row stays in the DB but is hidden from the tree, so the plan always shows what is next. Sub-tasks under a phase are ad-hoc via add --parent. Inter-phase deps via deps_add \u2014 `next` will skip dep-blocked phases and explain in the reason. The Stop hook re-prints this tree after every assistant turn while a cursor is set.",inputSchema:{type:"object",properties:{action:{type:"string",enum:["tree","status","sync","publish","focus","next","set_status","add","deps_add","deps_remove","deps_list"],description:"tree: render the roadmap (M/N done rollup; stale + shipped-done hidden by default \u2014 pass show_stale: true for stale) | status: one-line summary | sync: re-seed from .cairn/roadmap.md (also runs automatically on tree/status/next) | publish: (re)generate the human-readable ROADMAP.md at the repo root from the live plan (also runs automatically after every action; write-if-changed, never a placeholder) | focus <id>: move cursor, mark in_progress, propagate up (warns if dep-blocked but does not refuse) | next: DFS-advance cursor to next actionable phase (skips stale + dep-blocked, returns reason) | set_status <id> <status>: open|in_progress|done|blocked|stale (done auto-advances cursor; a done roadmap phase is pruned from roadmap.md + logged to roadmap_completed.md) | add <parent> <text>: ad-hoc sub-task under an existing phase | deps_add <from> <to>: declare phase <from> blocks phase <to> (cycle-checked) | deps_remove <from> <to> | deps_list [<id>]: list edges (or just edges touching <id>)"},id:{type:"number",description:"Phase ID (focus, set_status, deps_list)"},status:{type:"string",enum:["open","in_progress","done","blocked","stale"],description:"New status (set_status)"},parent:{type:"number",description:"Parent phase ID (add \u2014 required, ad-hoc sub-tasks must hang under an existing phase)"},text:{type:"string",description:"Sub-task text (add action)"},compact:{type:"boolean",description:"Compact tree render \u2014 strips id tags (tree action)"},show_stale:{type:"boolean",description:"Include stale phases in tree (default: hidden)"},hide_stale:{type:"boolean",description:"Deprecated \u2014 stale is hidden by default. Pass hide_stale: false to show, or use show_stale: true."},decision:{type:"string",description:"Filter tree to a single decision (tree action)"},from:{type:"number",description:"Blocker phase ID (deps_add, deps_remove)"},to:{type:"number",description:"Blocked phase ID (deps_add, deps_remove)"}},required:["action"]}}]}));var li=new Set(["cairn_search","cairn_bundle","cairn_describe","cairn_outline","cairn_code_graph","cairn_security"]);et.setRequestHandler(ai,async t=>{let{name:e,arguments:n}=t.params;switch(li.has(e)&&await Ze($),e){case"cairn_maintain":{let s=await Lt($,n);return Qe(),s}case"cairn_search":return $t($,n);case"cairn_describe":return At($,n);case"cairn_code_graph":return It($,n);case"cairn_security":return Ct($,n);case"cairn_bundle":return await Ft($,n);case"cairn_checkpoint":return Ut($,{...n,auto:!1});case"cairn_resume":{let s=await Sn($);return Qe(),s}case"cairn_outline":return bn($,n);case"cairn_minify":return xn($,n);case"cairn_todos":return await Tn($,n);case"cairn_roadmap":{let s=ue($,n),i=fn($);return i&&Array.isArray(s?.content)&&s.content.push({type:"text",text:i}),s}case"cairn_memo":return Kt($,n);case"cairn_switch":return Rn($,n);case"cairn_employ_memory":return On($,n);default:throw new Error(`Unknown tool: ${e}`)}});var pi=new oi;await et.connect(pi);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@misterhuydo/cairn-mcp",
3
- "version": "1.20.0",
3
+ "version": "1.21.0",
4
4
  "description": "MCP server that gives Claude Code persistent memory across sessions. Index your codebase once, search symbols, bundle source, scan for vulnerabilities, and checkpoint/resume work — across Java, TypeScript, Vue, Python, SQL and more.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",