@misterhuydo/cairn-mcp 1.16.0 → 1.18.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
@@ -169,14 +169,41 @@ cairn_roadmap { action: "deps_add", from: 2, to: 4 } → phase 2 block
169
169
  cairn_roadmap { action: "publish" } → (re)write the human-readable ROADMAP.md at the repo root
170
170
  ```
171
171
 
172
- The live plan is also projected into a legible **`ROADMAP.md` at the repo root** — a
173
- human-readable view with checkboxes, per-item status, and a `← current` marker on the
174
- cursor phase. It refreshes automatically on session activity (the end-of-turn hook and
175
- the first prompt of a session) as well as on every `cairn_roadmap` call, so a workspace
176
- that has a roadmap but hasn't touched the roadmap tool this session still shows a
177
- populated file. It is deterministic (no timestamps, stable ordering, so an unchanged
178
- plan re-writes byte-identically and leaves clean git diffs), never written as an empty
179
- placeholder, and never clobbers a hand-authored `ROADMAP.md`.
172
+ The live plan is also projected into a **`ROADMAP.md` at the repo root**, published in
173
+ the **`roadmap/1`** slot format so any cockpit or tool can read it without writing a
174
+ parser per provider. One file carries both halves — two files drift, and the drift is
175
+ invisible:
176
+
177
+ - **A markdown half for humans:** checkboxes, per-item status, a one-line summary per
178
+ phase, and a `← current` marker on the cursor phase.
179
+ - **One `roadmap-json` block for consumers**, at the end of the file. It carries stable
180
+ phase `id`s, `number`, `title`, `status`, `goal`, `blocked_by`, `slices`, the full
181
+ per-phase `detail` (the body you wrote in `.cairn/roadmap.md`), and `links` — the
182
+ `[[wikilinks]]` in that body resolved to the memory files behind each phase, so the
183
+ reasoning is one tap away instead of "go and find it". A link that cannot be resolved
184
+ is dropped rather than emitted dead.
185
+
186
+ The first line is the marker that says who generated the file and when
187
+ (`<!-- slot:roadmap format=roadmap/1 provider=cairn generated=… -->`). cairn declares
188
+ the slot in its MCP entry in `~/.claude.json`, so a cockpit does not have to guess.
189
+
190
+ It refreshes automatically on session activity (the end-of-turn hook and the first
191
+ prompt of a session) as well as on every `cairn_roadmap` call, so a workspace that has a
192
+ roadmap but hasn't touched the roadmap tool this session still shows a populated file.
193
+ Phase `id`s are stable across regenerations (they key off the database row, so renaming
194
+ a phase never changes its id), unknown JSON fields survive a rewrite, an unchanged plan
195
+ re-writes byte-identically (the `generated` stamp is only refreshed when something
196
+ actually changed, so diffs stay clean), nothing is ever written as an empty placeholder,
197
+ and writes are atomic (temp file + rename) so a reader never catches a half-written plan.
198
+
199
+ An existing `ROADMAP.md` is never destroyed. If the file is there but carries no
200
+ provider marker (hand-written, or from a tool that doesn't stamp its output), cairn
201
+ **adopts** it: the original is renamed to `ROADMAP.bak.md` (or `ROADMAP.bak.2.md`, `.3`,
202
+ … — an existing backup is never overwritten), the data block records `migrated_from`
203
+ and `migrated`, and the result reports `backed_up_to`. If the file is stamped by a
204
+ *different* provider, that's a conflict over who owns the path: cairn leaves it
205
+ completely alone. Any publish that declines returns a `warning` that the caller surfaces
206
+ to you, so a stale plan can't rot in place unnoticed.
180
207
 
181
208
  Editing `roadmap.md` *is* editing the plan — read actions re-seed from it, and the
182
209
  end-of-turn hook folds in any hand-edits. Marking a phase **done** prunes it from
package/dist/cairn-cli.js CHANGED
@@ -1,26 +1,26 @@
1
1
  #!/usr/bin/env node
2
- import h from"fs";import B from"os";import y from"path";import{execSync as me}from"child_process";import Ft from"fast-glob";var ge={".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 V(e,t,s={}){let{noComments:n=!0,noEmptyLines:o=!0,aggressive:a=!1}=s,i=e;return n&&(i=Ue(i,t)),o&&(i=He(i)),i=Je(i),a&&(i=Be(i)),i.trim()}function Ue(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 He(e){return e.split(`
2
+ import h from"fs";import J from"os";import g from"path";import{execSync as ge}from"child_process";import fn from"fast-glob";var we={".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 Q(e,t,n={}){let{noComments:s=!0,noEmptyLines:r=!0,aggressive:i=!1}=n,c=e;return s&&(c=Ze(c,t)),r&&(c=Qe(c)),c=et(c),i&&(c=tt(c)),c.trim()}function Ze(e,t){switch(t){case"java":case"typescript":case"javascript":case"vue":return e.replace(/\/\*[\s\S]*?\*\//g,"").replace(/(["'`])(?:(?!\1)[^\\]|\\.)*\1|\/(?![/*])(?:[^/\\\n]|\\.)*\/[gimsuy]*|\/\/[^\n]*/g,(n,s)=>s||!n.startsWith("//")?n:"");case"python":return e.replace(/#.*$/gm,"").replace(/'''[\s\S]*?'''/g,"").replace(/"""[\s\S]*?"""/g,"");case"sql":return e.replace(/--.*$/gm,"").replace(/\/\*[\s\S]*?\*\//g,"");case"xml":case"html":return e.replace(/<!--[\s\S]*?-->/g,"");case"config":return e.replace(/#.*$/gm,"");default:return e}}function Qe(e){return e.split(`
3
3
  `).filter(t=>t.trim().length>0).join(`
4
- `)}function Je(e){return e.split(`
4
+ `)}function et(e){return e.split(`
5
5
  `).map(t=>t.trimEnd()).join(`
6
- `)}function Be(e){return e.replace(/\s+/g," ").replace(/\s*([=+\-*/%<>!&|^~?:;,{}()[\]])\s*/g,"$1").replace(/;\s*}/g,"}").replace(/\(\s+/g,"(").replace(/\s+\)/g,")").trim()}function _e(e,t={}){let{noStyle:s=!1,noComments:n=!0,noEmptyLines:o=!0,aggressive:a=!1}=t,i=[],p=e.match(/<template>([\s\S]*?)<\/template>/i);if(p){let c=p[1];n&&(c=c.replace(/<!--[\s\S]*?-->/g,"")),o&&(c=c.split(`
7
- `).filter(m=>m.trim()).join(`
8
- `)),c=c.replace(/\s+/g," ").trim(),c&&i.push(`<template>${c}</template>`)}let f=e.match(/<script([^>]*)>([\s\S]*?)<\/script>/i);if(f){let c=f[1],m=V(f[2],"javascript",{noComments:n,noEmptyLines:o,aggressive:a}),u=/setup/i.test(c)?"<script setup>":"<script>";m&&i.push(`${u}${m}</script>`)}if(!s){let c=e.match(/<style([^>]*)>([\s\S]*?)<\/style>/i);if(c){let m=c[2].replace(/\/\*[\s\S]*?\*\//g,"").replace(/\s+/g," ").trim();m&&i.push(`<style${c[1]}>${m}</style>`)}}return i.join(`
6
+ `)}function tt(e){return e.replace(/\s+/g," ").replace(/\s*([=+\-*/%<>!&|^~?:;,{}()[\]])\s*/g,"$1").replace(/;\s*}/g,"}").replace(/\(\s+/g,"(").replace(/\s+\)/g,")").trim()}function Ee(e,t={}){let{noStyle:n=!1,noComments:s=!0,noEmptyLines:r=!0,aggressive:i=!1}=t,c=[],l=e.match(/<template>([\s\S]*?)<\/template>/i);if(l){let a=l[1];s&&(a=a.replace(/<!--[\s\S]*?-->/g,"")),r&&(a=a.split(`
7
+ `).filter(d=>d.trim()).join(`
8
+ `)),a=a.replace(/\s+/g," ").trim(),a&&c.push(`<template>${a}</template>`)}let p=e.match(/<script([^>]*)>([\s\S]*?)<\/script>/i);if(p){let a=p[1],d=Q(p[2],"javascript",{noComments:s,noEmptyLines:r,aggressive:i}),m=/setup/i.test(a)?"<script setup>":"<script>";d&&c.push(`${m}${d}</script>`)}if(!n){let a=e.match(/<style([^>]*)>([\s\S]*?)<\/style>/i);if(a){let d=a[2].replace(/\/\*[\s\S]*?\*\//g,"").replace(/\s+/g," ").trim();d&&c.push(`<style${a[1]}>${d}</style>`)}}return c.join(`
9
9
 
10
- `)}import L from"fs";import W from"path";import z from"fs";import Ye from"os";import Y from"path";var Ge=null;function K(){return Ge||process.cwd()}function I(){let e=Y.join(K(),".cairn");z.mkdirSync(Y.join(e,"bundles"),{recursive:!0});let t=Y.join(e,".cairn-project");return z.existsSync(t)||z.writeFileSync(t,"","utf8"),e}function se(e){return e.replace(/[:/\\]/g,"-").replace(/^-+/,"")}function Se(){let e=Y.join(Ye.homedir(),".claude","projects",se(K()));return z.existsSync(e)?Y.join(e,"memory"):null}var qe=1e3,Ve=100*1024;function ze(e,t,s,n){if(e&&typeof e=="object"&&e.ref)return e;let o=typeof e=="string"?e:String(e),a=/^\[\d{4}-\d{2}-\d{2}\]/.test(o)?o:`[${s}] ${o}`;if(a.length<=qe)return a;let i=W.join(t,"notes");L.mkdirSync(i,{recursive:!0});let f=`note-${new Date().toISOString().replace(/[:.]/g,"-")}-${n}.md`,c=W.join(i,f);return L.writeFileSync(c,a,"utf8"),{description:a.split(`
11
- `)[0].slice(0,120),ref:W.join(".cairn","notes",f)}}function oe(e,{message:t,active_files:s=[],notes:n=[]}){if(typeof n=="string")try{n=JSON.parse(n)}catch{n=n?[n]:[]}Array.isArray(n)||(n=[]);let o=I(),a=W.join(o,"session.json"),i=new Date().toISOString().slice(0,10),p={};try{let m=e.prepare("SELECT path FROM main.files").all();for(let{path:u}of m)try{p[u]=L.statSync(u).mtimeMs}catch{}}catch{}for(let m of s)try{p[m]=L.statSync(m).mtimeMs}catch{}let f=n.map((m,u)=>ze(m,o,i,u));if(L.existsSync(a))try{if(L.statSync(a).size>Ve){let u=1;for(;L.existsSync(W.join(o,`session.${u}.json`));)u++;L.renameSync(a,W.join(o,`session.${u}.json`))}}catch{}let c={message:t,checkpoint_at:new Date().toISOString(),active_files:s,notes:f,mtime_snapshot:p};return L.writeFileSync(a,JSON.stringify(c,null,2),"utf8"),{content:[{type:"text",text:JSON.stringify({saved:!0,checkpoint_at:c.checkpoint_at,active_files_tracked:s.length,notes_saved:f.length},null,2)}]}}import F from"fs";import O from"path";import Ke from"crypto";var xe=80;function re(e){let t=O.dirname(O.resolve(e)),s=O.parse(t).root;for(;;){let n=O.join(t,".cairn");try{if(F.statSync(n).isDirectory()&&F.existsSync(O.join(n,".cairn-project"))&&n.split(O.sep).filter(a=>a===".cairn").length===1)return n}catch{}if(t===s)return null;t=O.dirname(t)}}function we(e,t){let s=Ke.createHash("sha256").update(O.resolve(t)).digest("hex").slice(0,6);return O.join(e,"views",`${s}_${O.basename(t)}`)}function D(e){try{return JSON.parse(F.readFileSync(O.join(e,"minify-map.json"),"utf8"))}catch{return{}}}function T(e,t){F.mkdirSync(e,{recursive:!0}),F.writeFileSync(O.join(e,"minify-map.json"),JSON.stringify(t,null,2),"utf8")}function X(e,t){let s=0;for(let[n,o]of Object.entries(t)){let a=!1;if(!F.existsSync(n))a=!0;else if(!F.existsSync(o.tempPath))a=!0;else try{F.statSync(n).mtimeMs>o.minifiedAt&&(a=!0)}catch{a=!0}if(a){if(o.tempPath)try{F.unlinkSync(o.tempPath)}catch{}delete t[n],s++}}return s}function Ee(e,t){switch(t){case"javascript":case"typescript":return ie(e);case"java":return ie(e,{annotations:!0});case"python":return et(e);case"vue":return tt(e);case"sql":return nt(e);default:return e.split(`
10
+ `)}import L from"fs";import Y from"path";import ee from"fs";import nt from"os";import V from"path";var st=null;function I(){return st||process.cwd()}function B(){let e=V.join(I(),".cairn");ee.mkdirSync(V.join(e,"bundles"),{recursive:!0});let t=V.join(e,".cairn-project");return ee.existsSync(t)||ee.writeFileSync(t,"","utf8"),e}function ae(e){return e.replace(/[:/\\]/g,"-").replace(/^-+/,"")}function be(){let e=V.join(nt.homedir(),".claude","projects",ae(I()));return ee.existsSync(e)?V.join(e,"memory"):null}var rt=1e3,ot=100*1024;function it(e,t,n,s){if(e&&typeof e=="object"&&e.ref)return e;let r=typeof e=="string"?e:String(e),i=/^\[\d{4}-\d{2}-\d{2}\]/.test(r)?r:`[${n}] ${r}`;if(i.length<=rt)return i;let c=Y.join(t,"notes");L.mkdirSync(c,{recursive:!0});let p=`note-${new Date().toISOString().replace(/[:.]/g,"-")}-${s}.md`,a=Y.join(c,p);return L.writeFileSync(a,i,"utf8"),{description:i.split(`
11
+ `)[0].slice(0,120),ref:Y.join(".cairn","notes",p)}}function ce(e,{message:t,active_files:n=[],notes:s=[]}){if(typeof s=="string")try{s=JSON.parse(s)}catch{s=s?[s]:[]}Array.isArray(s)||(s=[]);let r=B(),i=Y.join(r,"session.json"),c=new Date().toISOString().slice(0,10),l={};try{let d=e.prepare("SELECT path FROM main.files").all();for(let{path:m}of d)try{l[m]=L.statSync(m).mtimeMs}catch{}}catch{}for(let d of n)try{l[d]=L.statSync(d).mtimeMs}catch{}let p=s.map((d,m)=>it(d,r,c,m));if(L.existsSync(i))try{if(L.statSync(i).size>ot){let m=1;for(;L.existsSync(Y.join(r,`session.${m}.json`));)m++;L.renameSync(i,Y.join(r,`session.${m}.json`))}}catch{}let a={message:t,checkpoint_at:new Date().toISOString(),active_files:n,notes:p,mtime_snapshot:l};return L.writeFileSync(i,JSON.stringify(a,null,2),"utf8"),{content:[{type:"text",text:JSON.stringify({saved:!0,checkpoint_at:a.checkpoint_at,active_files_tracked:n.length,notes_saved:p.length},null,2)}]}}import N from"fs";import j from"path";import at from"crypto";var ke=80;function le(e){let t=j.dirname(j.resolve(e)),n=j.parse(t).root;for(;;){let s=j.join(t,".cairn");try{if(N.statSync(s).isDirectory()&&N.existsSync(j.join(s,".cairn-project"))&&s.split(j.sep).filter(i=>i===".cairn").length===1)return s}catch{}if(t===n)return null;t=j.dirname(t)}}function $e(e,t){let n=at.createHash("sha256").update(j.resolve(t)).digest("hex").slice(0,6);return j.join(e,"views",`${n}_${j.basename(t)}`)}function W(e){try{return JSON.parse(N.readFileSync(j.join(e,"minify-map.json"),"utf8"))}catch{return{}}}function M(e,t){N.mkdirSync(e,{recursive:!0}),N.writeFileSync(j.join(e,"minify-map.json"),JSON.stringify(t,null,2),"utf8")}function te(e,t){let n=0;for(let[s,r]of Object.entries(t)){let i=!1;if(!N.existsSync(s))i=!0;else if(!N.existsSync(r.tempPath))i=!0;else try{N.statSync(s).mtimeMs>r.minifiedAt&&(i=!0)}catch{i=!0}if(i){if(r.tempPath)try{N.unlinkSync(r.tempPath)}catch{}delete t[s],n++}}return n}function Re(e,t){switch(t){case"javascript":case"typescript":return pe(e);case"java":return pe(e,{annotations:!0});case"python":return dt(e);case"vue":return ut(e);case"sql":return mt(e);default:return e.split(`
12
12
  `).slice(0,30).join(`
13
13
  `)+`
14
- ...`}}function ie(e,{annotations:t=!1}={}){let s=e.split(`
15
- `),n=[],o=0;for(let a=0;a<s.length;a++){let i=s[a].trim();if(!i)continue;let{opens:p,closes:f}=Ze(i),c=p-f;if(o===0){let m=Xe(i);m!==null?n.push(`${a+1}: ${m}`):i==="}"&&n.push("}")}else if(o===1){let m=Qe(i,t);m!==null&&n.push(` ${a+1}: ${m}`)}o=Math.max(0,o+c)}return n.join(`
16
- `)}function Xe(e){return/^import\s/.test(e)||/^@\w+/.test(e)||/^package\s/.test(e)?e:/^export\s/.test(e)?/^export\s+(default\s+)?(async\s+)?function[\s*]|^export\s+(default\s+)?(abstract\s+)?class\s/.test(e)?M(e):e:/^(async\s+)?function[\s*]/.test(e)||/^(abstract\s+)?class\s/.test(e)||/^(public\s+|private\s+|protected\s+)?(interface|enum|type|record)\s/.test(e)||/^(const|let|var)\s+\w+/.test(e)||/^(public|private|protected|abstract|final)\s+(class|interface|enum)\s/.test(e)?M(e):e==="}"?"}":null}function Qe(e,t){return t&&/^@\w+/.test(e)?e:e==="}"?"}":/^(public|private|protected|static|final|abstract|async|override|readonly|synchronized|native)\s/.test(e)||/^constructor\s*\(/.test(e)||/^(get|set)\s+\w+\s*\(/.test(e)||/^\w[\w$]*\s*[<(]/.test(e)&&!e.startsWith("return")&&!e.startsWith("throw")&&!e.startsWith("if")&&!e.startsWith("for")&&!e.startsWith("while")&&!e.startsWith("switch")&&!e.startsWith("const")&&!e.startsWith("let")&&!e.startsWith("var")?M(e):null}function M(e){let t=0,s=!1,n="";for(let o=0;o<e.length;o++){let a=e[o];if(s)a===n&&e[o-1]!=="\\"&&(s=!1);else if(a==='"'||a==="'"||a==="`")s=!0,n=a;else if(a==="{"){if(t===0)return e.slice(0,o).trimEnd();t++}else a==="}"&&t--}return e}function Ze(e){let t=0,s=0,n=!1,o="";for(let a=0;a<e.length;a++){let i=e[a];n?i===o&&e[a-1]!=="\\"&&(n=!1):i==='"'||i==="'"||i==="`"?(n=!0,o=i):i==="{"?t++:i==="}"&&s++}return{opens:t,closes:s}}function et(e){let t=[],s=e.split(`
17
- `);for(let n=0;n<s.length;n++){let o=s[n];if(!o.trim())continue;let a=o.trimStart();a.startsWith("import ")||a.startsWith("from ")?t.push(o):(a.startsWith("class ")||a.startsWith("def ")||a.startsWith("async def "))&&t.push(`${n+1}: ${o.trim().replace(/:(\s*)$/,":")}`)}return t.join(`
18
- `)}function tt(e){let t=[];for(let n of e.matchAll(/<(template|script|style)(\s[^>]*)?\s*>/g))t.push(n[0]);let s=e.match(/<script(?:\s[^>]*)?\s*>([\s\S]*?)<\/script>/);if(s){let n=ie(s[1]);n.trim()&&(t.push("// script:"),t.push(n))}return t.join(`
19
- `)}function nt(e){let t=[],s=e.split(`
20
- `);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(`
21
- `)}import k from"fs";import U from"path";function G(e){if(!e||typeof e!="string")return[];let t=e.split(/\r?\n/),s=!1,n=null,o=[],a=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 p=/^###\s+Phase\s+(\d+)\s*:\s*(.+?)\s*$/.exec(i);if(p){n&&o.push(n);let f=parseInt(p[1],10),c=p[2];if(a.has(f)){process.stderr.write(`[cairn] memo phase parser: duplicate Phase ${f} ignored
22
- `),n=null;continue}a.add(f),n={phase_number:f,text:c,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(`
23
- `).trim()||null}))}function be(e,t,s){let n=e.prepare("SELECT id, phase_number, status FROM main.roadmap_phases WHERE decision_name = ? ORDER BY phase_number").all(t),o=new Map(n.map(r=>[r.phase_number,r])),a=new Set,i=e.prepare("UPDATE main.roadmap_phases SET text = ?, body = ?, status = CASE WHEN status = ? THEN ? ELSE status END WHERE id = ?"),p=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 = ?"),c=[],m=[],u=[];for(let r=0;r<s.length;r++){let l=s[r];a.add(l.phase_number);let d=o.get(l.phase_number);if(d)i.run(l.text,l.body,"stale","open",d.id),c.push({id:d.id,phase_number:l.phase_number,text:l.text,reactivated:d.status==="stale"});else{let g=p.run(t,l.phase_number,l.text,l.body,"open",r);m.push({id:Number(g.lastInsertRowid),phase_number:l.phase_number,text:l.text})}}for(let[r,l]of o)!a.has(r)&&l.status!=="stale"&&l.status!=="done"&&(f.run(l.id),u.push({id:l.id,phase_number:r}));return{updated:c.length,inserted:m.length,staled:u.length,updated_phases:c,inserted_phases:m,staled_phases:u}}var P="roadmap",st=`# Roadmap
14
+ ...`}}function pe(e,{annotations:t=!1}={}){let n=e.split(`
15
+ `),s=[],r=0;for(let i=0;i<n.length;i++){let c=n[i].trim();if(!c)continue;let{opens:l,closes:p}=pt(c),a=l-p;if(r===0){let d=ct(c);d!==null?s.push(`${i+1}: ${d}`):c==="}"&&s.push("}")}else if(r===1){let d=lt(c,t);d!==null&&s.push(` ${i+1}: ${d}`)}r=Math.max(0,r+a)}return s.join(`
16
+ `)}function ct(e){return/^import\s/.test(e)||/^@\w+/.test(e)||/^package\s/.test(e)?e:/^export\s/.test(e)?/^export\s+(default\s+)?(async\s+)?function[\s*]|^export\s+(default\s+)?(abstract\s+)?class\s/.test(e)?F(e):e:/^(async\s+)?function[\s*]/.test(e)||/^(abstract\s+)?class\s/.test(e)||/^(public\s+|private\s+|protected\s+)?(interface|enum|type|record)\s/.test(e)||/^(const|let|var)\s+\w+/.test(e)||/^(public|private|protected|abstract|final)\s+(class|interface|enum)\s/.test(e)?F(e):e==="}"?"}":null}function lt(e,t){return t&&/^@\w+/.test(e)?e:e==="}"?"}":/^(public|private|protected|static|final|abstract|async|override|readonly|synchronized|native)\s/.test(e)||/^constructor\s*\(/.test(e)||/^(get|set)\s+\w+\s*\(/.test(e)||/^\w[\w$]*\s*[<(]/.test(e)&&!e.startsWith("return")&&!e.startsWith("throw")&&!e.startsWith("if")&&!e.startsWith("for")&&!e.startsWith("while")&&!e.startsWith("switch")&&!e.startsWith("const")&&!e.startsWith("let")&&!e.startsWith("var")?F(e):null}function F(e){let t=0,n=!1,s="";for(let r=0;r<e.length;r++){let i=e[r];if(n)i===s&&e[r-1]!=="\\"&&(n=!1);else if(i==='"'||i==="'"||i==="`")n=!0,s=i;else if(i==="{"){if(t===0)return e.slice(0,r).trimEnd();t++}else i==="}"&&t--}return e}function pt(e){let t=0,n=0,s=!1,r="";for(let i=0;i<e.length;i++){let c=e[i];s?c===r&&e[i-1]!=="\\"&&(s=!1):c==='"'||c==="'"||c==="`"?(s=!0,r=c):c==="{"?t++:c==="}"&&n++}return{opens:t,closes:n}}function dt(e){let t=[],n=e.split(`
17
+ `);for(let s=0;s<n.length;s++){let r=n[s];if(!r.trim())continue;let i=r.trimStart();i.startsWith("import ")||i.startsWith("from ")?t.push(r):(i.startsWith("class ")||i.startsWith("def ")||i.startsWith("async def "))&&t.push(`${s+1}: ${r.trim().replace(/:(\s*)$/,":")}`)}return t.join(`
18
+ `)}function ut(e){let t=[];for(let s of e.matchAll(/<(template|script|style)(\s[^>]*)?\s*>/g))t.push(s[0]);let n=e.match(/<script(?:\s[^>]*)?\s*>([\s\S]*?)<\/script>/);if(n){let s=pe(n[1]);s.trim()&&(t.push("// script:"),t.push(s))}return t.join(`
19
+ `)}function mt(e){let t=[],n=e.split(`
20
+ `);for(let s=0;s<n.length;s++){let r=n[s].trim();/^(CREATE|ALTER|DROP|INSERT|UPDATE|DELETE|SELECT|WITH|GRANT|REVOKE|BEGIN|COMMIT)\b/i.test(r)&&t.push(`${s+1}: ${r.slice(0,120)}${r.length>120?"...":""}`)}return t.join(`
21
+ `)}import k from"fs";import G from"path";function K(e){if(!e||typeof e!="string")return[];let t=e.split(/\r?\n/),n=!1,s=null,r=[],i=new Set;for(let c of t){if(/^##\s+Phases\s*$/i.test(c)){n=!0;continue}if(/^##\s+/.test(c)&&!/^###/.test(c)){s&&(r.push(s),s=null),n=!1;continue}if(!n)continue;let l=/^###\s+Phase\s+(\d+)\s*:\s*(.+?)\s*$/.exec(c);if(l){s&&r.push(s);let p=parseInt(l[1],10),a=l[2];if(i.has(p)){process.stderr.write(`[cairn] memo phase parser: duplicate Phase ${p} ignored
22
+ `),s=null;continue}i.add(p),s={phase_number:p,text:a,bodyLines:[]};continue}s&&s.bodyLines.push(c)}return s&&r.push(s),r.map(c=>({phase_number:c.phase_number,text:c.text,body:c.bodyLines.join(`
23
+ `).trim()||null}))}function ve(e,t,n){let s=e.prepare("SELECT id, phase_number, status FROM main.roadmap_phases WHERE decision_name = ? ORDER BY phase_number").all(t),r=new Map(s.map(o=>[o.phase_number,o])),i=new Set,c=e.prepare("UPDATE main.roadmap_phases SET text = ?, body = ?, status = CASE WHEN status = ? THEN ? ELSE status END WHERE id = ?"),l=e.prepare("INSERT INTO main.roadmap_phases (decision_name, phase_number, parent_phase_id, text, body, status, position) VALUES (?, ?, NULL, ?, ?, ?, ?)"),p=e.prepare("UPDATE main.roadmap_phases SET status = 'stale' WHERE id = ?"),a=[],d=[],m=[];for(let o=0;o<n.length;o++){let u=n[o];i.add(u.phase_number);let f=r.get(u.phase_number);if(f)c.run(u.text,u.body,"stale","open",f.id),a.push({id:f.id,phase_number:u.phase_number,text:u.text,reactivated:f.status==="stale"});else{let y=l.run(t,u.phase_number,u.text,u.body,"open",o);d.push({id:Number(y.lastInsertRowid),phase_number:u.phase_number,text:u.text})}}for(let[o,u]of r)!i.has(o)&&u.status!=="stale"&&u.status!=="done"&&(p.run(u.id),m.push({id:u.id,phase_number:o}));return{updated:a.length,inserted:d.length,staled:m.length,updated_phases:a,inserted_phases:d,staled_phases:m}}var v="roadmap",ft=`# Roadmap
24
24
 
25
25
  The forward-looking plan: only upcoming and in-progress phases live here. When a
26
26
  phase is marked done (cairn_roadmap set_status <id> done) it is removed from this
@@ -31,67 +31,74 @@ the plan \u2014 Cairn syncs it into the live tracker on the next cairn_roadmap r
31
31
 
32
32
  ### Phase 1: First phase title
33
33
  What this phase delivers.
34
- `;function C(){return U.join(I(),"roadmap.md")}function ot(){return U.join(I(),"roadmap_completed.md")}function Q(){let e=C();return k.existsSync(e)?k.readFileSync(e,"utf8"):""}function ce(){let e=C();return k.existsSync(e)?!1:(k.writeFileSync(e,st,"utf8"),!0)}function rt(e){let t=[];for(let s of e){let n=(s.text||"").trim();n&&(!Number.isInteger(s.phase_number)||s.phase_number<1||t.push({phase_number:s.phase_number,text:n,body:s.body??null}))}return t}var ae="decision_production_roadmap.md";function it(e){let t=e.match(/^---\n[\s\S]*?\n---\n?/);return t?e.slice(t[0].length):e}function at(e,t){let s=U.join(e,"MEMORY.md");if(!k.existsSync(s))return;let n=k.readFileSync(s,"utf8").split(/\r?\n/).filter(o=>!o.includes(`](${t})`));k.writeFileSync(s,n.join(`
35
- `),"utf8")}function ct(){let e=C();if(k.existsSync(e))return null;let t=[],s=Se();s&&t.push(s),t.push(U.join(I(),"memory"));let n=null,o=null;for(let i of t){let p=U.join(i,ae);n==null&&k.existsSync(p)&&(n=it(k.readFileSync(p,"utf8")).trim(),o=p)}if(n==null)return null;let a=/^#\s/m.test(n)?"":`# Roadmap
34
+ `;function U(){return G.join(B(),"roadmap.md")}function ht(){return G.join(B(),"roadmap_completed.md")}function ne(){let e=U();return k.existsSync(e)?k.readFileSync(e,"utf8"):""}function ue(){let e=U();return k.existsSync(e)?!1:(k.writeFileSync(e,ft,"utf8"),!0)}function yt(e){let t=[];for(let n of e){let s=(n.text||"").trim();s&&(!Number.isInteger(n.phase_number)||n.phase_number<1||t.push({phase_number:n.phase_number,text:s,body:n.body??null}))}return t}var de="decision_production_roadmap.md";function gt(e){let t=e.match(/^---\n[\s\S]*?\n---\n?/);return t?e.slice(t[0].length):e}function _t(e,t){let n=G.join(e,"MEMORY.md");if(!k.existsSync(n))return;let s=k.readFileSync(n,"utf8").split(/\r?\n/).filter(r=>!r.includes(`](${t})`));k.writeFileSync(n,s.join(`
35
+ `),"utf8")}function St(){let e=U();if(k.existsSync(e))return null;let t=[],n=be();n&&t.push(n),t.push(G.join(B(),"memory"));let s=null,r=null;for(let c of t){let l=G.join(c,de);s==null&&k.existsSync(l)&&(s=gt(k.readFileSync(l,"utf8")).trim(),r=l)}if(s==null)return null;let i=/^#\s/m.test(s)?"":`# Roadmap
36
36
 
37
- `;k.writeFileSync(e,a+n+`
38
- `,"utf8");for(let i of t){let p=U.join(i,ae);try{k.existsSync(p)&&k.unlinkSync(p)}catch{}try{at(i,ae)}catch{}}return{migrated:!0,from:o,to:e}}function H(e){ct();let t=C();if(!k.existsSync(t))return null;let s=k.readFileSync(t,"utf8");if(!/^##\s+Phases\s*$/im.test(s))return null;let n=rt(G(s));return be(e,P,n)}function le(){let e=ot();return k.existsSync(e)?k.readFileSync(e,"utf8").split(/\r?\n/).filter(t=>/^-\s+\d{4}-\d{2}-\d{2}\s+—\s+Phase\s/.test(t)).length:0}import J from"fs";import pe from"path";import{execSync as lt}from"child_process";var ke=120;function ue(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>ke&&(t=t.slice(0,ke-1).trimEnd()+"\u2026"),t}function N(e){return String(e||"").toLowerCase().replace(/[^a-z0-9]+/g," ").trim()}function pt(e){if(!e||typeof e!="string")return[];let t=e.split(/\r?\n/),s=c=>c.map(m=>({text:ue(m.titleRaw),body:m.bodyLines.join(`
39
- `).trim()||null})).filter(m=>m.text),n=[],o=null;for(let c of t){let m=/^(#{1,6})\s+(.+?)\s*$/.exec(c);if(m&&m[1].length>=2&&m[1].length<=4){o&&n.push(o),o={titleRaw:m[2],bodyLines:[]};continue}o&&o.bodyLines.push(c)}o&&n.push(o);let a=[];o=null;for(let c of t){let m=/^(\d+)[.)]\s+(.+?)\s*$/.exec(c);if(m){o&&a.push(o),o={titleRaw:m[2],bodyLines:[]};continue}o&&o.bodyLines.push(c)}o&&a.push(o);let i=[s(n),s(a)].filter(c=>c.length>=2);if(i.length)return i.sort((c,m)=>m.length-c.length),i[0];let p=t.find(c=>c.trim());return[{text:ue(p||"Plan")||"Plan",body:e.trim()||null}]}function dt(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(`
37
+ `;k.writeFileSync(e,i+s+`
38
+ `,"utf8");for(let c of t){let l=G.join(c,de);try{k.existsSync(l)&&k.unlinkSync(l)}catch{}try{_t(c,de)}catch{}}return{migrated:!0,from:r,to:e}}function q(e){St();let t=U();if(!k.existsSync(t))return null;let n=k.readFileSync(t,"utf8");if(!/^##\s+Phases\s*$/im.test(n))return null;let s=yt(K(n));return ve(e,v,s)}function me(){let e=ht();return k.existsSync(e)?k.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 fe from"path";import{execSync as xt}from"child_process";var Oe=120;function ye(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>Oe&&(t=t.slice(0,Oe-1).trimEnd()+"\u2026"),t}function D(e){return String(e||"").toLowerCase().replace(/[^a-z0-9]+/g," ").trim()}function wt(e){if(!e||typeof e!="string")return[];let t=e.split(/\r?\n/),n=a=>a.map(d=>({text:ye(d.titleRaw),body:d.bodyLines.join(`
39
+ `).trim()||null})).filter(d=>d.text),s=[],r=null;for(let a of t){let d=/^(#{1,6})\s+(.+?)\s*$/.exec(a);if(d&&d[1].length>=2&&d[1].length<=4){r&&s.push(r),r={titleRaw:d[2],bodyLines:[]};continue}r&&r.bodyLines.push(a)}r&&s.push(r);let i=[];r=null;for(let a of t){let d=/^(\d+)[.)]\s+(.+?)\s*$/.exec(a);if(d){r&&i.push(r),r={titleRaw:d[2],bodyLines:[]};continue}r&&r.bodyLines.push(a)}r&&i.push(r);let c=[n(s),n(i)].filter(a=>a.length>=2);if(c.length)return c.sort((a,d)=>d.length-a.length),c[0];let l=t.find(a=>a.trim());return[{text:ye(l||"Plan")||"Plan",body:e.trim()||null}]}function Et(e){let t=[];return e.forEach((n,s)=>{t.push(`### Phase ${s+1}: ${n.text}`),n.body&&n.body.trim()&&t.push(n.body.trim()),t.push("")}),t.join(`
40
40
  `).replace(/\n{3,}/g,`
41
41
 
42
42
  `).trimEnd()+`
43
- `}function $e(e){let t=Q(),s=t.split(/\r?\n/),n=s.findIndex(c=>/^##\s+Phases\s*$/i.test(c)),o=dt(e);if(n===-1){let c=t.replace(/\s*$/,"");J.writeFileSync(C(),`${c}
43
+ `}function Pe(e){let t=ne(),n=t.split(/\r?\n/),s=n.findIndex(a=>/^##\s+Phases\s*$/i.test(a)),r=Et(e);if(s===-1){let a=t.replace(/\s*$/,"");z.writeFileSync(U(),`${a}
44
44
 
45
45
  ## Phases
46
46
 
47
- ${o}`,"utf8");return}let a=s.length;for(let c=n+1;c<s.length;c++)if(/^##\s+/.test(s[c])&&!/^###/.test(s[c])){a=c;break}let i=s.slice(0,n+1).join(`
48
- `),p=s.slice(a).join(`
49
- `).replace(/^\n+/,""),f=`
47
+ ${r}`,"utf8");return}let i=n.length;for(let a=s+1;a<n.length;a++)if(/^##\s+/.test(n[a])&&!/^###/.test(n[a])){i=a;break}let c=n.slice(0,s+1).join(`
48
+ `),l=n.slice(i).join(`
49
+ `).replace(/^\n+/,""),p=`
50
50
 
51
- ${o}${p.trim()?`
52
- `+p:""}`;J.writeFileSync(C(),i+f,"utf8")}function Re(e){let t=pt(e);if(t.length===0)return{added:[],skipped:[],total:0};let n=ce()?[]:G(Q()).map(f=>({text:f.text,body:f.body})),o=new Set(n.map(f=>N(f.text))),a=[...n],i=[],p=[];for(let f of t){let c=N(f.text);if(o.has(c)){p.push(f.text);continue}o.add(c),a.push(f),i.push(f.text)}return i.length>0&&$e(a),{added:i,skipped:p,total:a.length}}function ut(e){return e==="completed"?"done":e==="in_progress"?"in_progress":"open"}function mt(e){try{return e.prepare("SELECT phase_id FROM main.roadmap_cursor WHERE singleton = 1").get()?.phase_id??null}catch{return null}}function ve(e,t){if(!Array.isArray(t)||t.length===0)return{changed:!1};let s=t.map(r=>({text:ue(r.content||r.activeForm||""),status:ut(r.status)})).filter(r=>r.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(P).filter(r=>r.status!=="stale");if(n.length===0){let l=ce()?[]:G(Q()).map(x=>({text:x.text,body:x.body})),d=new Set(l.map(x=>N(x.text))),g=[...l],S=0;for(let x of s){let $=N(x.text);d.has($)||(d.add($),g.push({text:x.text,body:null}),S++)}if(S===0)return{changed:!1};$e(g);let _=H(e);return{changed:!0,seeded:S,diff:_}}let o=mt(e);o==null&&(o=n.find(r=>r.status==="in_progress")?.id??n.find(r=>r.status==="open")?.id??n[0].id);let a=e.prepare("SELECT id, text, status FROM main.roadmap_phases WHERE parent_phase_id = ?").all(o),i=new Map(a.map(r=>[N(r.text),r])),p=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 = ?"),c=e.prepare("SELECT COALESCE(MAX(position), -1) + 1 AS n FROM main.roadmap_phases WHERE parent_phase_id = ?").get(o).n,m=0,u=0;for(let r of s){let l=i.get(N(r.text));if(l){if(l.status==="done"&&r.status!=="done")continue;l.status!==r.status&&(f.run(r.status,l.id),u++)}else p.run(o,r.text,r.status,c++),m++}return{changed:m>0||u>0,parentId:o,inserted:m,updated:u}}var ft=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 ht(e){return[...new Set(N(e).split(" ").filter(t=>t.length>=4&&!ft.has(t)))]}function yt(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),p=t.find(f=>f.phase_number===i);if(p)return{...p,confidence:"high"}}let o=new Set(N(s).split(" ").filter(Boolean)),a=null;for(let i of t){let p=ht(i.text);if(p.length<2)continue;let f=p.filter(m=>o.has(m)),c=f.length/p.length;f.length>=2&&c>=.6&&(!a||c>a.ratio)&&(a={...i,confidence:"medium",ratio:c})}return a?(delete a.ratio,a):null}function de(e,t){return lt(`git ${t}`,{cwd:e,encoding:"utf8",timeout:5e3}).trim()}function Pe(e,t){let s=pe.join(t,".cairn"),n=pe.join(s,".last-commit"),o=pe.join(s,"roadmap-pending.json"),a;try{a=de(t,"rev-parse HEAD")}catch{return{candidates:[],newCommits:0}}let i=null;try{i=J.readFileSync(n,"utf8").trim()||null}catch{}let p=[];if(i&&i!==a)try{p=de(t,`rev-list --no-merges --reverse ${i}..HEAD`).split(`
53
- `).filter(Boolean)}catch{p=[]}try{J.writeFileSync(n,a,"utf8")}catch{}let f=[];try{let c=JSON.parse(J.readFileSync(o,"utf8"));Array.isArray(c)&&(f=c)}catch{}if(p.length){let c=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(P);for(let m of p){let u="";try{u=de(t,`show -s --format=%s%n%b ${m}`)}catch{}let r=yt(u,c);r&&!f.some(l=>l.phase_id===r.id)&&f.push({phase_id:r.id,phase_number:r.phase_number,text:r.text,commit:m.slice(0,7),confidence:r.confidence})}}if(f.length){let c=new Set(e.prepare("SELECT id FROM main.roadmap_phases WHERE status IN ('open','in_progress')").all().map(m=>m.id));f=f.filter(m=>c.has(m.phase_id))}try{J.writeFileSync(o,JSON.stringify(f),"utf8")}catch{}return{candidates:f,newCommits:p.length}}import Z from"fs";import gt from"path";var je="<!-- cairn:roadmap -->",_t={open:"open",in_progress:"in progress",done:"done",blocked:"blocked",stale:"stale"};function St(e){return e==="done"?"x":e==="in_progress"?"~":e==="blocked"?"!":" "}function xt(){return gt.join(K(),"ROADMAP.md")}function wt(e){try{return e.prepare("SELECT phase_id FROM main.roadmap_cursor WHERE singleton = 1").get()?.phase_id??null}catch{return null}}function Et(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 bt(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 kt(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=wt(e),n=Et(e),o=new Map(t.map(d=>[d.id,d.status])),a=new Map(t.map(d=>[d.id,d.phase_number!=null?`Phase ${d.phase_number}`:d.text])),i=d=>{let g=n.get(d);return g?[...g].filter(S=>o.get(S)!=="done").map(S=>a.get(S)||`#${S}`):[]},p=new Map;for(let d of t){let g=d.parent_phase_id??"root";p.has(g)||p.set(g,[]),p.get(g).push(d)}for(let d of p.values())d.sort(bt);let f=new Map;for(let d of p.get("root")||[])d.decision_name!=null&&(f.has(d.decision_name)||f.set(d.decision_name,[]),f.get(d.decision_name).push(d));let c=[...f.keys()].sort((d,g)=>d===P?-1:g===P?1:d<g?-1:d>g?1:0),m=[je,"<!--"," 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.","-->","","# Roadmap","","Legend: `[x]` done \xB7 `[~]` in progress \xB7 `[ ]` open \xB7 `[!]` blocked."],u=(d,g)=>{let S=" ".repeat(g),_=d.phase_number!=null?`Phase ${d.phase_number}: ${d.text}`:d.text,x=_t[d.status]||d.status,$=i(d.id);if($.length&&(x+=` \xB7 waiting on ${$.join(", ")}`),s===d.id&&(x+=" \xB7 \u2190 current"),m.push(`${S}- [${St(d.status)}] **${_}** \u2014 ${x}`),d.body){let R=d.body.split(`
54
- `).map(A=>A.trim()).find(Boolean);R&&m.push(`${S} ${R}`)}for(let R of p.get(d.id)||[])R.status!=="stale"&&u(R,g+1)},r=!1;for(let d of c){let g=f.get(d).filter(_=>_.status!=="stale"),S=d===P?g.filter(_=>_.status!=="done"):g;if(S.length!==0){if(r=!0,m.push(""),d===P)m.push("## Active plan");else{let _=g.filter(x=>x.status==="done").length;m.push(`## ${d} \u2014 ${_}/${g.length} done`)}m.push("");for(let _ of S)u(_,0)}}if(!r)return null;let l=le();return l>0&&m.push("","---","",`**Shipped:** ${l} phase${l===1?"":"s"} \u2014 see \`.cairn/roadmap_completed.md\`.`),m.join(`
51
+ ${r}${l.trim()?`
52
+ `+l:""}`;z.writeFileSync(U(),c+p,"utf8")}function je(e){let t=wt(e);if(t.length===0)return{added:[],skipped:[],total:0};let s=ue()?[]:K(ne()).map(p=>({text:p.text,body:p.body})),r=new Set(s.map(p=>D(p.text))),i=[...s],c=[],l=[];for(let p of t){let a=D(p.text);if(r.has(a)){l.push(p.text);continue}r.add(a),i.push(p),c.push(p.text)}return c.length>0&&Pe(i),{added:c,skipped:l,total:i.length}}function bt(e){return e==="completed"?"done":e==="in_progress"?"in_progress":"open"}function kt(e){try{return e.prepare("SELECT phase_id FROM main.roadmap_cursor WHERE singleton = 1").get()?.phase_id??null}catch{return null}}function Me(e,t){if(!Array.isArray(t)||t.length===0)return{changed:!1};let n=t.map(o=>({text:ye(o.content||o.activeForm||""),status:bt(o.status)})).filter(o=>o.text);if(n.length===0)return{changed:!1};let s=e.prepare("SELECT id, status FROM main.roadmap_phases WHERE decision_name = ? AND parent_phase_id IS NULL").all(v).filter(o=>o.status!=="stale");if(s.length===0){let u=ue()?[]:K(ne()).map(x=>({text:x.text,body:x.body})),f=new Set(u.map(x=>D(x.text))),y=[...u],_=0;for(let x of n){let O=D(x.text);f.has(O)||(f.add(O),y.push({text:x.text,body:null}),_++)}if(_===0)return{changed:!1};Pe(y);let S=q(e);return{changed:!0,seeded:_,diff:S}}let r=kt(e);r==null&&(r=s.find(o=>o.status==="in_progress")?.id??s.find(o=>o.status==="open")?.id??s[0].id);let i=e.prepare("SELECT id, text, status FROM main.roadmap_phases WHERE parent_phase_id = ?").all(r),c=new Map(i.map(o=>[D(o.text),o])),l=e.prepare("INSERT INTO main.roadmap_phases (decision_name, phase_number, parent_phase_id, text, status, position) VALUES (NULL, NULL, ?, ?, ?, ?)"),p=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(r).n,d=0,m=0;for(let o of n){let u=c.get(D(o.text));if(u){if(u.status==="done"&&o.status!=="done")continue;u.status!==o.status&&(p.run(o.status,u.id),m++)}else l.run(r,o.text,o.status,a++),d++}return{changed:d>0||m>0,parentId:r,inserted:d,updated:m}}var $t=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 Rt(e){return[...new Set(D(e).split(" ").filter(t=>t.length>=4&&!$t.has(t)))]}function vt(e,t){if(!e||!t?.length)return null;let n=String(e),s=/\bphase\s*#?\s*(\d+)\b/i.exec(n);if(s){let c=parseInt(s[1],10),l=t.find(p=>p.phase_number===c);if(l)return{...l,confidence:"high"}}let r=new Set(D(n).split(" ").filter(Boolean)),i=null;for(let c of t){let l=Rt(c.text);if(l.length<2)continue;let p=l.filter(d=>r.has(d)),a=p.length/l.length;p.length>=2&&a>=.6&&(!i||a>i.ratio)&&(i={...c,confidence:"medium",ratio:a})}return i?(delete i.ratio,i):null}function he(e,t){return xt(`git ${t}`,{cwd:e,encoding:"utf8",timeout:5e3}).trim()}function Te(e,t){let n=fe.join(t,".cairn"),s=fe.join(n,".last-commit"),r=fe.join(n,"roadmap-pending.json"),i;try{i=he(t,"rev-parse HEAD")}catch{return{candidates:[],newCommits:0}}let c=null;try{c=z.readFileSync(s,"utf8").trim()||null}catch{}let l=[];if(c&&c!==i)try{l=he(t,`rev-list --no-merges --reverse ${c}..HEAD`).split(`
53
+ `).filter(Boolean)}catch{l=[]}try{z.writeFileSync(s,i,"utf8")}catch{}let p=[];try{let a=JSON.parse(z.readFileSync(r,"utf8"));Array.isArray(a)&&(p=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(v);for(let d of l){let m="";try{m=he(t,`show -s --format=%s%n%b ${d}`)}catch{}let o=vt(m,a);o&&!p.some(u=>u.phase_id===o.id)&&p.push({phase_id:o.id,phase_number:o.phase_number,text:o.text,commit:d.slice(0,7),confidence:o.confidence})}}if(p.length){let a=new Set(e.prepare("SELECT id FROM main.roadmap_phases WHERE status IN ('open','in_progress')").all().map(d=>d.id));p=p.filter(d=>a.has(d.phase_id))}try{z.writeFileSync(r,JSON.stringify(p),"utf8")}catch{}return{candidates:p,newCommits:l.length}}import T from"fs";import R from"path";var se="cairn",H="roadmap/1",Ot="<!-- slot:roadmap";function Pt(e){return`${Ot} format=${H} provider=${se} generated=${e} -->`}var jt=`<!-- ${se}:roadmap -->`,Mt=/^<!--\s*slot:roadmap\b([^>]*?)-->/,Tt=/^<!--\s*([A-Za-z0-9_.-]+)\s*:\s*roadmap\s*-->/,At=/([A-Za-z_][A-Za-z0-9_-]*)=("[^"]*"|\S+)/g;function Ft(e){if(typeof e!="string")return null;let t=e.indexOf(`
54
+ `);return(t===-1?e:e.slice(0,t)).trim()}function Lt(e){let t=Ft(e);if(!t)return null;let n=Mt.exec(t);if(n){let r={};for(let i of n[1].matchAll(At))r[i[1]]=i[2].replace(/^"|"$/g,"");return r.provider?{provider:r.provider.toLowerCase(),format:r.format||null,generated:r.generated||null}:null}let s=Tt.exec(t);return s?{provider:s[1].toLowerCase(),format:null,generated:null}:null}var Nt=/^```roadmap-json[ \t]*\r?\n([\s\S]*?)\r?\n```[ \t]*$/m;function Fe(e){if(typeof e!="string")return null;let t=Nt.exec(e);if(!t)return null;try{let n=JSON.parse(t[1]);return n&&typeof n=="object"&&!Array.isArray(n)?n:null}catch{return null}}var Dt=new Set(["format","provider","generated","title","cursor","phases","migrated_from","migrated"]),Ct=new Set(["id","number","title","status","goal","blocked_by","slices","detail","links"]);function Le(e,t){let n={};if(!e||typeof e!="object")return n;for(let s of Object.keys(e).sort())t.has(s)||(n[s]=e[s]);return n}function Ne(e,t=new Map){if(!Array.isArray(e))return t;for(let n of e)n&&typeof n=="object"&&typeof n.id=="string"&&(t.set(n.id,n),Ne(n.slices,t));return t}function It(e){if(typeof e!="string")return null;let t=Fe(e)?.migrated_from;if(typeof t=="string"&&t)return t;let n=e.indexOf(`
55
+ `);if(n===-1)return null;let s=e.indexOf("-->",n),r=s===-1?e.slice(0,512):e.slice(n,s),i=/^\s*migrated-from=(.+?)\s*$/m.exec(r);return i?i[1]:null}var Wt=/\[\[([^\]|#\n]+?)(?:[|#][^\]\n]*)?\]\]/g,Ut=["decision","knowledge","experience","preference"];function Ht(e){let t=R.relative(I(),e).split(R.sep).join("/");return!t||t.startsWith("..")||R.isAbsolute(t)?null:t}function Jt(e){let t=R.join(I(),".cairn","memory");return[R.join(t,`${e}.md`),...Ut.map(n=>R.join(t,`${n}_${e}.md`))]}function Bt(e){if(!e)return[];let t=[],n=new Set;for(let s of String(e).matchAll(Wt)){let r=s[1].trim();if(!(!r||n.has(r))){n.add(r);for(let i of Jt(r)){if(!T.existsSync(i))continue;let c=Ht(i);c&&t.push({kind:"memory",title:r,path:c});break}}}return t}var Yt={open:"open",in_progress:"in progress",done:"done",blocked:"blocked",stale:"stale"},Gt=new Set(["open","in_progress","done","blocked","stale"]);function qt(e){return e==="done"?"x":e==="in_progress"?"~":e==="blocked"?"!":" "}function De(){return R.join(I(),"ROADMAP.md")}function Ce(){return new Date().toISOString().replace(/\.\d{3}Z$/,"Z")}function zt(e){try{return e.prepare("SELECT phase_id FROM main.roadmap_cursor WHERE singleton = 1").get()?.phase_id??null}catch{return null}}function Vt(e){let t=new Map,n;try{n=e.prepare("SELECT blocker_id, blocked_id FROM main.roadmap_phase_deps").all()}catch{return t}for(let s of n)t.has(s.blocked_id)||t.set(s.blocked_id,new Set),t.get(s.blocked_id).add(s.blocker_id);return t}function Kt(e,t){let n=e.decision_name??"",s=t.decision_name??"";return n!==s?n<s?-1:1:e.phase_number!=null&&t.phase_number!=null?e.phase_number-t.phase_number:e.position-t.position||e.id-t.id}function Xt(e){let t;try{t=e.prepare("SELECT id, decision_name, phase_number, parent_phase_id, text, body, status, position FROM main.roadmap_phases ORDER BY decision_name, phase_number, position, id").all()}catch{return null}if(t.filter(y=>y.status!=="stale").length===0)return null;let n=zt(e),s=Vt(e),r=new Map(t.map(y=>[y.id,y.status])),i=new Map(t.map(y=>[y.id,y.phase_number!=null?`Phase ${y.phase_number}`:y.text])),c=new Map(t.map(y=>[y.id,y])),l=new Map;for(let y of t){let _=y.parent_phase_id??"root";l.has(_)||l.set(_,[]),l.get(_).push(y)}for(let y of l.values())y.sort(Kt);let p=new Map;for(let y of l.get("root")||[])y.decision_name!=null&&(p.has(y.decision_name)||p.set(y.decision_name,[]),p.get(y.decision_name).push(y));let a=[...p.keys()].sort((y,_)=>y===v?-1:_===v?1:y<_?-1:y>_?1:0),d=new Map,m=!1;for(let y of a){let _=p.get(y).filter(x=>x.status!=="stale"),S=y===v?_.filter(x=>x.status!=="done"):_;S.length!==0&&(m=!0,d.set(y,{visible:S,doneCount:_.filter(x=>x.status==="done").length,total:_.length}))}return m?{cursor:n,decisionNames:a,visibleRoots:d,children:y=>(l.get(y)||[]).filter(_=>_.status!=="stale"),unfinishedBlockers:y=>{let _=s.get(y);return _?[..._].filter(S=>r.get(S)!=="done").map(S=>i.get(S)||`#${S}`):[]},blockerIds:y=>{let _=s.get(y);return _?[..._].filter(S=>c.has(S)).sort((S,x)=>S-x).map(S=>`phase-${S}`):[]}}:null}function Zt(e,t){let{cursor:n,decisionNames:s,visibleRoots:r,children:i,unfinishedBlockers:c}=e,l=[Pt(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 p=(d,m)=>{let o=" ".repeat(m),u=d.phase_number!=null?`Phase ${d.phase_number}: ${d.text}`:d.text,f=Yt[d.status]||d.status,y=c(d.id);if(y.length&&(f+=` \xB7 waiting on ${y.join(", ")}`),n===d.id&&(f+=" \xB7 \u2190 current"),l.push(`${o}- [${qt(d.status)}] **${u}** \u2014 ${f}`),d.body){let _=d.body.split(`
56
+ `).map(S=>S.trim()).find(Boolean);_&&l.push(`${o} ${_}`)}for(let _ of i(d.id))p(_,m+1)};for(let d of s){let m=r.get(d);if(m){l.push(""),d===v?l.push("## Active plan"):l.push(`## ${d} \u2014 ${m.doneCount}/${m.total} done`),l.push("");for(let o of m.visible)p(o,0)}}let a=me();return a>0&&l.push("","---","",`**Shipped:** ${a} phase${a===1?"":"s"} \u2014 see \`.cairn/roadmap_completed.md\`.`),l}function Ie(e,t,n,s){let r=n.get(`phase-${e.id}`),i=e.body?e.body.split(`
57
+ `).map(a=>a.trim()).find(Boolean)??null:null,c=Bt(e.body),l=t.blockerIds(e.id),p={id:`phase-${e.id}`};if(e.phase_number!=null&&(p.number=e.phase_number),p.title=e.text,p.status=Gt.has(e.status)?e.status:"open",i&&(p.goal=i),l.length&&(p.blocked_by=l),!s){let a=[],d=m=>{for(let o of t.children(m))a.push(Ie(o,t,n,!0)),d(o.id)};d(e.id),a.length&&(p.slices=a)}return e.body&&e.body.trim()&&(p.detail=e.body.trim()),c.length&&(p.links=c),{...p,...Le(r,Ct)}}function Qt(e,t){let n=Ne(t.previous?.phases),s=[];for(let c of e.decisionNames){let l=e.visibleRoots.get(c);if(l)for(let p of l.visible)s.push(Ie(p,e,n,!1))}let r={format:H,provider:se,generated:t.generated,title:`${R.basename(I())} roadmap`,cursor:e.cursor!=null?`phase-${e.cursor}`:null};t.migratedFrom&&(r.migrated_from=t.migratedFrom,t.migrated&&(r.migrated=t.migrated)),r.phases=s;let i={...r,...Le(t.previous,Dt)};return["","```roadmap-json",JSON.stringify(i,null,2),"```"]}function Ae(e,t={}){let n={generated:t.generated||Ce(),migratedFrom:t.migratedFrom||null,migrated:t.migrated||null,previous:t.previous&&typeof t.previous=="object"?t.previous:null},s=Xt(e);return s==null?null:[...Zt(s,n),...Qt(s,n)].join(`
55
58
  `)+`
56
- `}function Oe(e){let t=xt(),s=kt(e),n=null;if(Z.existsSync(t))try{n=Z.readFileSync(t,"utf8")}catch{n=null}if(n!=null&&!n.startsWith(je))return{written:!1,reason:"ROADMAP.md exists and is not cairn-generated \u2014 refusing to overwrite",path:t};if(s==null){if(n!=null)try{return Z.unlinkSync(t),{written:!1,removed:!0,path:t}}catch(o){return{written:!1,reason:`remove failed: ${o.message}`,path:t}}return{written:!1,reason:"empty roadmap \u2014 no file written",path:t}}return n===s?{written:!1,unchanged:!0,path:t,bytes:Buffer.byteLength(s)}:(Z.writeFileSync(t,s,"utf8"),{written:!0,path:t,bytes:Buffer.byteLength(s)})}function ee(e){try{return Oe(e)}catch(t){return process.stderr.write(`[cairn] ROADMAP.md publish skipped: ${t.message}
57
- `),null}}var $t=new Set(["open","in_progress"]);function Rt(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 vt(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 Te(e,t){Rt(e,t),e.prepare("UPDATE main.roadmap_phases SET status = 'in_progress' WHERE id = ? AND status = 'open'").run(t),vt(e,t)}function Pt(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 jt(e,t,s){let n=t.get(e);if(!n||n.size===0)return[];let o=[];for(let a of n)s.get(a)!=="done"&&o.push(a);return o}function Me(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(u=>[u.id,u.status])),o=Pt(e),a=new Map;for(let u of s){let r=u.parent_phase_id??`root:${u.decision_name}`;a.has(r)||a.set(r,[]),a.get(r).push(u)}for(let u of a.values())u.sort((r,l)=>r.phase_number!=null&&l.phase_number!=null?r.phase_number-l.phase_number:r.position-l.position||r.id-l.id);let i=[],p=new Set;for(let u of s){if(u.parent_phase_id!=null||p.has(u.decision_name))continue;p.add(u.decision_name);let r=[],l=a.get(`root:${u.decision_name}`)||[];for(let d=l.length-1;d>=0;d--)r.push(l[d]);for(;r.length;){let d=r.pop();i.push(d);let g=a.get(d.id)||[];for(let S=g.length-1;S>=0;S--)r.push(g[S])}}let f=u=>$t.has(u.status),c=[],m=0;if(t!=null){let u=i.findIndex(r=>r.id===t);m=u===-1?0:u+1}for(let u=m;u<i.length;u++){let r=i[u];if(!f(r))continue;let l=jt(r.id,o,n);if(l.length>0){c.push({id:r.id,blockers:l});continue}let d=t!=null?s.find(_=>_.id===t):null,g=d&&d.decision_name!==r.decision_name,S;return t==null?S=`First actionable phase: ${r.decision_name??"(sub-task)"} / Phase ${r.phase_number??"\xB7"}.`:g?S=`Previous decision "${d.decision_name}" is complete \u2014 advanced to "${r.decision_name}".`:S=`Next open phase in "${r.decision_name??"(sub-task)"}" after id ${t}.`,c.length>0&&(S+=` Skipped ${c.length} dep-blocked phase(s): ${c.map(_=>`id ${_.id} (blocked by ${_.blockers.join(",")})`).join("; ")}.`),{id:r.id,reason:S,skipped_blocked:c}}return c.length>0?{id:null,reason:`All remaining open phases are blocked by unfinished dependencies: ${c.map(u=>`id ${u.id} (blocked by ${u.blockers.join(",")})`).join("; ")}.`,skipped_blocked:c}:{id:null,reason:"No actionable phases remaining."}}var Ot=new Set(["java","typescript","javascript","vue","python","sql"]);function Ne(e){let t=y.join(B.homedir(),".claude","projects",se(e));return h.existsSync(t)?y.join(t,"memory"):null}function De(e){try{return h.readdirSync(e).filter(t=>t.endsWith(".md"))}catch{return[]}}function Tt(e){let t=Ne(e);if(!t||!h.existsSync(t))return 0;let s=y.join(e,".cairn","memory");h.mkdirSync(s,{recursive:!0});let n=0;for(let o of De(t)){let a=y.join(t,o),i;try{i=h.statSync(a)}catch{continue}if(!i.isFile())continue;let p=y.join(s,o),f=!h.existsSync(p);if(!f)try{f=i.mtimeMs>h.statSync(p).mtimeMs}catch{f=!0}if(f){h.copyFileSync(a,p);try{h.utimesSync(p,i.atime,i.mtime)}catch{}n++}}return n}function Mt(e){let t=Ne(e);if(!t||(h.mkdirSync(t,{recursive:!0}),De(t).some(o=>o!=="MEMORY.md")))return 0;let s=y.join(e,".cairn","memory");if(!h.existsSync(s))return 0;let n=0;for(let o of h.readdirSync(s)){let a=y.join(s,o),i;try{i=h.statSync(a)}catch{continue}if(i.isFile()){h.copyFileSync(a,y.join(t,o));try{h.utimesSync(y.join(t,o),i.atime,i.mtime)}catch{}n++}}return n}async function Le(e){let t={active:!1},s=y.join(e,"index.db");if(!h.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 a=p=>p.toLowerCase().replace(/[^a-z0-9]+/g,"_").replace(/^_|_$/g,"");function i(p,f){if(!h.existsSync(p))return null;let c=h.readFileSync(p,"utf8"),m=!1,u=!1,r=[];for(let l of c.split(/\r?\n/)){if(/^##\s+Phases\s*$/i.test(l)){m=!0;continue}if(m&&/^##\s+/.test(l)&&!/^###/.test(l))break;if(!m)continue;let d=/^###\s+Phase\s+(\d+)\s*:\s*(.+?)\s*$/.exec(l);if(d){if(u)break;parseInt(d[1],10)===f&&(r.push(`### Phase ${d[1]}: ${d[2]}`),u=!0);continue}u&&r.push(l)}return r.join(`
58
- `).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 c=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(),m={open:"[ ]",in_progress:"[~]",done:"[x]",blocked:"[!]",stale:"[s]"},u=new Map;for(let b of c){let w=b.parent_phase_id??"root";u.has(w)||u.set(w,[]),u.get(w).push(b)}for(let b of u.values())b.sort((w,E)=>w.decision_name&&E.decision_name&&w.decision_name!==E.decision_name?w.decision_name.localeCompare(E.decision_name):w.phase_number!=null&&E.phase_number!=null?w.phase_number-E.phase_number:w.position-E.position||w.id-E.id);let r=u.get("root")||[],l=new Map;for(let b of r)b.decision_name!=null&&(l.has(b.decision_name)||l.set(b.decision_name,[]),l.get(b.decision_name).push(b));let d=[],g=(b,w)=>{for(let E of u.get(b)||[]){if(E.status==="stale")continue;let te=f===E.id?" \u2192":"",ne=E.phase_number!=null?`Phase ${E.phase_number}: ${E.text}`:E.text;d.push(`${" ".repeat(w)}${m[E.status]||"[?]"}${te} ${ne}`),g(E.id,w+1)}};for(let[b,w]of l){let E=w.filter(v=>v.status!=="stale");if(E.length===0)continue;let te=E.every(v=>v.status==="done"),ne=E.some(v=>v.status==="in_progress"),Ce=te?"done":ne?"in_progress":"open",Ae=E.filter(v=>v.status==="done").length,Ie=b===P?"Roadmap":`Decision: ${b}`;d.push(`${m[Ce]} ${Ie} (${Ae}/${E.length} done)`);for(let v of w){if(v.status==="stale"||b===P&&v.status==="done")continue;let We=f===v.id?" \u2192":"";d.push(` ${m[v.status]||"[?]"}${We} Phase ${v.phase_number}: ${v.text}`),g(v.id,2)}}let S=b=>c.find(w=>w.id===b),_=S(f),x=_?.decision_name??null,$=_?.phase_number??null,R=_;for(;R&&R.parent_phase_id!=null&&(R=S(R.parent_phase_id),!!R);)x=R.decision_name??x,$=R.phase_number??$;o.close();let A=_?_.phase_number!=null?`${_.decision_name} / Phase ${_.phase_number}: ${_.text}`:`${x} / Phase ${$} \u2192 ${_.text}`:"(unknown)",q=y.join(e,".last-roadmap-cursor"),fe=null;try{fe=parseInt(h.readFileSync(q,"utf8").trim(),10)}catch{}let he=fe!==f;try{h.writeFileSync(q,String(f),"utf8")}catch{}let ye=null;if(he&&x!=null&&$!=null){let b=x===P?y.join(e,"roadmap.md"):y.join(e,"memory",`decision_${a(x)}.md`),w=i(b,$);w&&(ye=w.split(`
59
- `))}return{active:!0,currentLabel:A,treeLines:d,advanced:he,memoLines:ye}}catch{try{o?.close()}catch{}return t}}process.on("uncaughtException",e=>{process.stderr.write(`cairn: ${e.message}
60
- `),process.exit(0)});var j=process.argv[2];if(j==="--version"||j==="-v"){let e=JSON.parse(h.readFileSync(new URL("../package.json",import.meta.url),"utf8"));console.log(e.version),process.exit(0)}else if(j==="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=y.resolve(t);let n=re(t);n||process.exit(0);let o=y.join(n,"views"),a=y.relative(o,t);!a.startsWith("..")&&!y.isAbsolute(a)&&process.exit(0);let i=D(n),f=X(n,i)>0,c=i[t];c?.state==="edit-ready"&&(f&&T(n,i),process.exit(0)),c?.state==="compressed"&&(c.state="edit-ready",T(n,i),process.exit(0));let m=y.extname(t).toLowerCase(),u=ge[m];(!u||!Ot.has(u))&&(f&&T(n,i),process.exit(0));let r;try{r=h.readFileSync(t,"utf8")}catch{f&&T(n,i),process.exit(0)}let l=r.split(`
61
- `).length,d=u==="vue"?_e(r):V(r,u),g=d.split(`
62
- `).length,S=l>0?Math.round((1-g/l)*100):0,_=we(n,t);h.mkdirSync(y.dirname(_),{recursive:!0}),h.writeFileSync(_,d,"utf8");let x=c?.readCount??0;i[t]={tempPath:_,state:"compressed",minifiedAt:h.statSync(t).mtimeMs,readCount:x+1},T(n,i);let $=g>xe,R=x>0;if(R||$){let A=Ee(d,u),q=R?`[cairn: ${l} \u2192 ${g} lines | seen \xD7${x+1} this session]`:`[cairn: ${l} \u2192 outline (${g} minified lines)]`;process.stderr.write(`${q}
63
- ${A}
64
- Full minified view: Read("${_}")
65
- `)}else process.stderr.write(`[cairn: ${l} \u2192 ${g} lines (-${S}%)]
66
- ${d}
67
- Cached view: Read("${_}")
68
- `);process.exit(2)})}else if(j==="edit-guard"){let e="";process.stdin.setEncoding("utf8"),process.stdin.on("data",t=>{e+=t}),process.stdin.on("end",()=>{let t="(unknown)",s="Edit";try{let u=JSON.parse(e);t=u?.tool_input?.file_path||"(unknown)",s=u?.tool_name||"Edit"}catch{}let n=y.resolve(t),o=re(n);o||process.exit(0);let a=y.join(o,"views"),i=y.relative(a,n);if(!i.startsWith("..")&&!y.isAbsolute(i)){let u=D(o),r=Object.entries(u).find(([,d])=>d.tempPath===n),l=r?`Edit the source file instead: Edit("${r[0]}")`:"Edit the original source file, not the .cairn/views/ cache.";process.stderr.write(`[cairn] Cannot edit a cached view file \u2014 it is read-only and will be overwritten on next read.
69
- ${l}
70
- `),process.exit(2)}let p=D(o),f=X(o,p),c=p[n];c||(f>0&&T(o,p),process.exit(0)),s==="Write"&&(delete p[n],T(o,p),process.exit(0)),c.state==="edit-ready"&&(delete p[n],T(o,p),process.exit(0)),c.state="edit-ready",T(o,p);let m=null;try{let u=h.readFileSync(n,"utf8");u.split(`
71
- `).length<=300&&(m=u)}catch{}m!==null?process.stderr.write(`[cairn] File was compressed in context. Full source content:
59
+ `}function en(e){let t=R.join(e,"ROADMAP.bak.md");if(!T.existsSync(t))return t;for(let n=2;n<=1e3;n++){let s=R.join(e,`ROADMAP.bak.${n}.md`);if(!T.existsSync(s))return s}return null}function tn(e,t){let n=R.join(R.dirname(e),`.${R.basename(e)}.${process.pid}.tmp`);try{T.writeFileSync(n,t,"utf8"),T.renameSync(n,e)}catch(s){try{T.unlinkSync(n)}catch{}throw s}}function C(e,t,n={}){return{written:!1,reason:e,warning:`[cairn] ROADMAP.md was not updated \u2014 ${e}`,path:t,...n}}function We(e){let t=De(),n=null;if(T.existsSync(t))try{n=T.readFileSync(t,"utf8")}catch(f){return C(`the existing file could not be read (${f.message})`,t)}let s=Lt(n),r=n!=null&&(s?.provider===se||n.startsWith(jt));if(n!=null&&s&&!r)return{written:!1,reason:`ROADMAP.md is generated by another provider (${s.provider}) \u2014 refusing to overwrite`,conflict_provider:s.provider,warning:`[cairn] ROADMAP.md is published by "${s.provider}", so cairn left it untouched and the file may not reflect cairn's plan. Two roadmap providers are claiming the same file \u2014 decide which one owns it.`,path:t};let i=r?Fe(n):null,c=r?It(n):null,l=r&&typeof i?.migrated=="string"?i.migrated:null,p=typeof i?.generated=="string"?i.generated:null,a=Ae(e,{generated:p||"1970-01-01T00:00:00Z",migratedFrom:c,migrated:l,previous:i});if(a==null){if(r)try{return T.unlinkSync(t),{written:!1,removed:!0,path:t}}catch(f){return C(`the stale projection could not be removed (${f.message})`,t)}return{written:!1,reason:"empty roadmap \u2014 no file written",path:t}}if(r&&n===a)return{written:!1,unchanged:!0,path:t,bytes:Buffer.byteLength(a)};let d=Ce(),m=null,o=null;if(n!=null&&!r){let f=en(R.dirname(t));if(f==null)return C("no free ROADMAP.bak name was available to preserve the existing file",t);try{T.renameSync(t,f)}catch(y){return C(`the existing file could not be preserved (${y.message})`,t)}m=R.basename(f),o=f,c=m,l=d}a=Ae(e,{generated:d,migratedFrom:c,migrated:l,previous:i});try{tn(t,a)}catch(f){if(o)try{return T.renameSync(o,t),C(`write failed (${f.message}); the original ROADMAP.md was restored`,t)}catch{return C(`write failed (${f.message}); the original ROADMAP.md is at ${m} and must be restored by hand`,t,{backed_up_to:m})}return C(`write failed (${f.message})`,t)}let u={written:!0,path:t,bytes:Buffer.byteLength(a),format:H,generated:d};return m&&(u.backed_up_to=m,u.warning=`[cairn] The existing ROADMAP.md was not cairn-generated. It has been preserved as ${m}, and ROADMAP.md is now generated from cairn's roadmap.`),u}function nn(e){try{return We(e)}catch(t){process.stderr.write(`[cairn] ROADMAP.md publish skipped: ${t.message}
60
+ `);let n=null;try{n=De()}catch{}return C(`publish failed (${t.message})`,n)}}function re(e){let t=nn(e);return t&&t.warning?t.warning:null}var sn=new Set(["open","in_progress"]);function rn(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 on(e,t){let n=e.prepare("SELECT parent_phase_id, status FROM main.roadmap_phases WHERE id = ?"),s=e.prepare("UPDATE main.roadmap_phases SET status = 'in_progress' WHERE id = ? AND status = 'open'"),r=n.get(t);for(;r&&r.parent_phase_id!=null;)s.run(r.parent_phase_id),r=n.get(r.parent_phase_id)}function Ue(e,t){rn(e,t),e.prepare("UPDATE main.roadmap_phases SET status = 'in_progress' WHERE id = ? AND status = 'open'").run(t),on(e,t)}function an(e){let t=new Map,n;try{n=e.prepare("SELECT blocker_id, blocked_id FROM main.roadmap_phase_deps").all()}catch{return t}for(let s of n)t.has(s.blocked_id)||t.set(s.blocked_id,new Set),t.get(s.blocked_id).add(s.blocker_id);return t}function cn(e,t,n){let s=t.get(e);if(!s||s.size===0)return[];let r=[];for(let i of s)n.get(i)!=="done"&&r.push(i);return r}function He(e,t){let n=e.prepare("SELECT id, decision_name, phase_number, parent_phase_id, status, position, text FROM main.roadmap_phases ORDER BY decision_name, phase_number, position, id").all();if(n.length===0)return{id:null,reason:"No phases \u2014 add a ## Phases section to .cairn/roadmap.md."};let s=new Map(n.map(m=>[m.id,m.status])),r=an(e),i=new Map;for(let m of n){let o=m.parent_phase_id??`root:${m.decision_name}`;i.has(o)||i.set(o,[]),i.get(o).push(m)}for(let m of i.values())m.sort((o,u)=>o.phase_number!=null&&u.phase_number!=null?o.phase_number-u.phase_number:o.position-u.position||o.id-u.id);let c=[],l=new Set;for(let m of n){if(m.parent_phase_id!=null||l.has(m.decision_name))continue;l.add(m.decision_name);let o=[],u=i.get(`root:${m.decision_name}`)||[];for(let f=u.length-1;f>=0;f--)o.push(u[f]);for(;o.length;){let f=o.pop();c.push(f);let y=i.get(f.id)||[];for(let _=y.length-1;_>=0;_--)o.push(y[_])}}let p=m=>sn.has(m.status),a=[],d=0;if(t!=null){let m=c.findIndex(o=>o.id===t);d=m===-1?0:m+1}for(let m=d;m<c.length;m++){let o=c[m];if(!p(o))continue;let u=cn(o.id,r,s);if(u.length>0){a.push({id:o.id,blockers:u});continue}let f=t!=null?n.find(S=>S.id===t):null,y=f&&f.decision_name!==o.decision_name,_;return t==null?_=`First actionable phase: ${o.decision_name??"(sub-task)"} / Phase ${o.phase_number??"\xB7"}.`:y?_=`Previous decision "${f.decision_name}" is complete \u2014 advanced to "${o.decision_name}".`:_=`Next open phase in "${o.decision_name??"(sub-task)"}" after id ${t}.`,a.length>0&&(_+=` Skipped ${a.length} dep-blocked phase(s): ${a.map(S=>`id ${S.id} (blocked by ${S.blockers.join(",")})`).join("; ")}.`),{id:o.id,reason:_,skipped_blocked:a}}return a.length>0?{id:null,reason:`All remaining open phases are blocked by unfinished dependencies: ${a.map(m=>`id ${m.id} (blocked by ${m.blockers.join(",")})`).join("; ")}.`,skipped_blocked:a}:{id:null,reason:"No actionable phases remaining."}}var ln=new Set(["java","typescript","javascript","vue","python","sql"]),Ye={name:"cairn",contract:1,title:"cairn",slots:["roadmap"],provides:{roadmap:{path:"ROADMAP.md",format:H}},spec:"Project memory and roadmap. Publishes the plan as ROADMAP.md in the roadmap/1 slot format."};function pn(){let e=g.join(J.homedir(),".claude.json");if(!h.existsSync(e))return!1;let t;try{t=JSON.parse(h.readFileSync(e,"utf8"))}catch{return!1}let n=t?.mcpServers?.cairn;if(!n||typeof n!="object")return!1;let s=n.terminalConnect,r=s?.provides?.roadmap?.format;if(s&&r===H)return!1;n.terminalConnect=Ye;try{return h.writeFileSync(e,JSON.stringify(t,null,2),"utf8"),!0}catch{return!1}}function Ge(e){let t=g.join(J.homedir(),".claude","projects",ae(e));return h.existsSync(t)?g.join(t,"memory"):null}function qe(e){try{return h.readdirSync(e).filter(t=>t.endsWith(".md"))}catch{return[]}}function dn(e){let t=Ge(e);if(!t||!h.existsSync(t))return 0;let n=g.join(e,".cairn","memory");h.mkdirSync(n,{recursive:!0});let s=0;for(let r of qe(t)){let i=g.join(t,r),c;try{c=h.statSync(i)}catch{continue}if(!c.isFile())continue;let l=g.join(n,r),p=!h.existsSync(l);if(!p)try{p=c.mtimeMs>h.statSync(l).mtimeMs}catch{p=!0}if(p){h.copyFileSync(i,l);try{h.utimesSync(l,c.atime,c.mtime)}catch{}s++}}return s}function un(e){let t=Ge(e);if(!t||(h.mkdirSync(t,{recursive:!0}),qe(t).some(r=>r!=="MEMORY.md")))return 0;let n=g.join(e,".cairn","memory");if(!h.existsSync(n))return 0;let s=0;for(let r of h.readdirSync(n)){let i=g.join(n,r),c;try{c=h.statSync(i)}catch{continue}if(c.isFile()){h.copyFileSync(i,g.join(t,r));try{h.utimesSync(g.join(t,r),c.atime,c.mtime)}catch{}s++}}return s}async function Je(e){let t={active:!1},n=g.join(e,"index.db");if(!h.existsSync(n))return t;let s;try{({DatabaseSync:s}=await import("node:sqlite"))}catch{return t}let r;try{r=new s(n,{readonly:!0})}catch{return t}let i=l=>l.toLowerCase().replace(/[^a-z0-9]+/g,"_").replace(/^_|_$/g,"");function c(l,p){if(!h.existsSync(l))return null;let a=h.readFileSync(l,"utf8"),d=!1,m=!1,o=[];for(let u of a.split(/\r?\n/)){if(/^##\s+Phases\s*$/i.test(u)){d=!0;continue}if(d&&/^##\s+/.test(u)&&!/^###/.test(u))break;if(!d)continue;let f=/^###\s+Phase\s+(\d+)\s*:\s*(.+?)\s*$/.exec(u);if(f){if(m)break;parseInt(f[1],10)===p&&(o.push(`### Phase ${f[1]}: ${f[2]}`),m=!0);continue}m&&o.push(u)}return o.join(`
61
+ `).trim()||null}try{let p=r.prepare("SELECT phase_id FROM roadmap_cursor WHERE singleton = 1").get()?.phase_id??null;if(p==null)return r.close(),t;let a=r.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(),d={open:"[ ]",in_progress:"[~]",done:"[x]",blocked:"[!]",stale:"[s]"},m=new Map;for(let b of a){let w=b.parent_phase_id??"root";m.has(w)||m.set(w,[]),m.get(w).push(b)}for(let b of m.values())b.sort((w,E)=>w.decision_name&&E.decision_name&&w.decision_name!==E.decision_name?w.decision_name.localeCompare(E.decision_name):w.phase_number!=null&&E.phase_number!=null?w.phase_number-E.phase_number:w.position-E.position||w.id-E.id);let o=m.get("root")||[],u=new Map;for(let b of o)b.decision_name!=null&&(u.has(b.decision_name)||u.set(b.decision_name,[]),u.get(b.decision_name).push(b));let f=[],y=(b,w)=>{for(let E of m.get(b)||[]){if(E.status==="stale")continue;let oe=p===E.id?" \u2192":"",ie=E.phase_number!=null?`Phase ${E.phase_number}: ${E.text}`:E.text;f.push(`${" ".repeat(w)}${d[E.status]||"[?]"}${oe} ${ie}`),y(E.id,w+1)}};for(let[b,w]of u){let E=w.filter($=>$.status!=="stale");if(E.length===0)continue;let oe=E.every($=>$.status==="done"),ie=E.some($=>$.status==="in_progress"),ze=oe?"done":ie?"in_progress":"open",Ve=E.filter($=>$.status==="done").length,Ke=b===v?"Roadmap":`Decision: ${b}`;f.push(`${d[ze]} ${Ke} (${Ve}/${E.length} done)`);for(let $ of w){if($.status==="stale"||b===v&&$.status==="done")continue;let Xe=p===$.id?" \u2192":"";f.push(` ${d[$.status]||"[?]"}${Xe} Phase ${$.phase_number}: ${$.text}`),y($.id,2)}}let _=b=>a.find(w=>w.id===b),S=_(p),x=S?.decision_name??null,O=S?.phase_number??null,A=S;for(;A&&A.parent_phase_id!=null&&(A=_(A.parent_phase_id),!!A);)x=A.decision_name??x,O=A.phase_number??O;r.close();let X=S?S.phase_number!=null?`${S.decision_name} / Phase ${S.phase_number}: ${S.text}`:`${x} / Phase ${O} \u2192 ${S.text}`:"(unknown)",Z=g.join(e,".last-roadmap-cursor"),_e=null;try{_e=parseInt(h.readFileSync(Z,"utf8").trim(),10)}catch{}let Se=_e!==p;try{h.writeFileSync(Z,String(p),"utf8")}catch{}let xe=null;if(Se&&x!=null&&O!=null){let b=x===v?g.join(e,"roadmap.md"):g.join(e,"memory",`decision_${i(x)}.md`),w=c(b,O);w&&(xe=w.split(`
62
+ `))}return{active:!0,currentLabel:X,treeLines:f,advanced:Se,memoLines:xe}}catch{try{r?.close()}catch{}return t}}process.on("uncaughtException",e=>{process.stderr.write(`cairn: ${e.message}
63
+ `),process.exit(0)});var P=process.argv[2];if(P==="--version"||P==="-v"){let e=JSON.parse(h.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,n;try{n=JSON.parse(e)?.tool_input||{},t=n.file_path}catch{process.exit(0)}t||process.exit(0),(n.offset!=null||n.limit!=null)&&process.exit(0),t=g.resolve(t);let s=le(t);s||process.exit(0);let r=g.join(s,"views"),i=g.relative(r,t);!i.startsWith("..")&&!g.isAbsolute(i)&&process.exit(0);let c=W(s),p=te(s,c)>0,a=c[t];a?.state==="edit-ready"&&(p&&M(s,c),process.exit(0)),a?.state==="compressed"&&(a.state="edit-ready",M(s,c),process.exit(0));let d=g.extname(t).toLowerCase(),m=we[d];(!m||!ln.has(m))&&(p&&M(s,c),process.exit(0));let o;try{o=h.readFileSync(t,"utf8")}catch{p&&M(s,c),process.exit(0)}let u=o.split(`
64
+ `).length,f=m==="vue"?Ee(o):Q(o,m),y=f.split(`
65
+ `).length,_=u>0?Math.round((1-y/u)*100):0,S=$e(s,t);h.mkdirSync(g.dirname(S),{recursive:!0}),h.writeFileSync(S,f,"utf8");let x=a?.readCount??0;c[t]={tempPath:S,state:"compressed",minifiedAt:h.statSync(t).mtimeMs,readCount:x+1},M(s,c);let O=y>ke,A=x>0;if(A||O){let X=Re(f,m),Z=A?`[cairn: ${u} \u2192 ${y} lines | seen \xD7${x+1} this session]`:`[cairn: ${u} \u2192 outline (${y} minified lines)]`;process.stderr.write(`${Z}
66
+ ${X}
67
+ Full minified view: Read("${S}")
68
+ `)}else process.stderr.write(`[cairn: ${u} \u2192 ${y} lines (-${_}%)]
69
+ ${f}
70
+ Cached view: Read("${S}")
71
+ `);process.exit(2)})}else if(P==="edit-guard"){let e="";process.stdin.setEncoding("utf8"),process.stdin.on("data",t=>{e+=t}),process.stdin.on("end",()=>{let t="(unknown)",n="Edit";try{let m=JSON.parse(e);t=m?.tool_input?.file_path||"(unknown)",n=m?.tool_name||"Edit"}catch{}let s=g.resolve(t),r=le(s);r||process.exit(0);let i=g.join(r,"views"),c=g.relative(i,s);if(!c.startsWith("..")&&!g.isAbsolute(c)){let m=W(r),o=Object.entries(m).find(([,f])=>f.tempPath===s),u=o?`Edit the source file instead: Edit("${o[0]}")`:"Edit the original source file, not the .cairn/views/ cache.";process.stderr.write(`[cairn] Cannot edit a cached view file \u2014 it is read-only and will be overwritten on next read.
72
+ ${u}
73
+ `),process.exit(2)}let l=W(r),p=te(r,l),a=l[s];a||(p>0&&M(r,l),process.exit(0)),n==="Write"&&(delete l[s],M(r,l),process.exit(0)),a.state==="edit-ready"&&(delete l[s],M(r,l),process.exit(0)),a.state="edit-ready",M(r,l);let d=null;try{let m=h.readFileSync(s,"utf8");m.split(`
74
+ `).length<=300&&(d=m)}catch{}d!==null?process.stderr.write(`[cairn] File was compressed in context. Full source content:
72
75
 
73
- ${m}
76
+ ${d}
74
77
 
75
78
  Re-apply your Edit using the exact content above (state is now edit-ready).
76
- `):process.stderr.write(`[cairn] File compressed. Steps: (1) Read("${c.tempPath}") \u2014 loads full content into context. (2) Edit("${t}") \u2014 edit the original source, not the view.
77
- `),process.exit(2)})}else if(j==="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(),a=y.join(o,".cairn");h.existsSync(a)||process.exit(0);let i=y.join(a,"index.db");h.existsSync(i)||process.exit(0);let p;try{({DatabaseSync:p}=await import("node:sqlite"))}catch{process.exit(0)}let f=/exit.?plan.?mode/i.test(s),c=/todo.?write/i.test(s);!f&&!c&&process.exit(0);let m=null,u;try{if(f){let r=n.plan;(typeof r!="string"||!r.trim())&&process.exit(0);let l=Re(r);if(u=new p(i),H(u),(u.prepare("SELECT phase_id FROM main.roadmap_cursor WHERE singleton = 1").get()?.phase_id??null)==null){let g=Me(u,null);g.id!=null&&Te(u,g.id)}l.added.length>0&&(m=`[cairn] roadmap: captured ${l.added.length} phase(s) from plan${l.skipped.length?` (${l.skipped.length} already present)`:""}.`)}else if(c){u=new p(i);let r=ve(u,n.todos);r.seeded?m=`[cairn] roadmap: seeded ${r.seeded} phase(s) from todos.`:r.inserted>0&&(m=`[cairn] roadmap: linked ${r.inserted} new sub-task(s) to the current phase.`)}}catch(r){process.stderr.write(`[cairn] roadmap-capture skipped: ${r.message}
78
- `);try{u?.close()}catch{}process.exit(0)}u&&ee(u);try{u?.close()}catch{}m&&process.stdout.write(JSON.stringify({systemMessage:m})),process.exit(0)})}else if(j==="validate-map"){let e=y.join(process.cwd(),".cairn");h.existsSync(e)||process.exit(0);let t=D(e),s=X(e,t);for(let n of Object.values(t))n.readCount=0;T(e,t),s>0&&process.stdout.write(`[cairn] Cleaned minify map: ${s} stale entr${s===1?"y":"ies"} removed
79
- `),process.exit(0)}else if(j==="checkpoint"&&process.argv[3]==="--auto"){let e=`Auto-checkpoint at ${new Date().toISOString()}`,t=[],s=[];try{let n=y.join(process.cwd(),".cairn"),o=y.join(n,"session.json");h.existsSync(o)&&(t=JSON.parse(h.readFileSync(o,"utf8")).notes||[]);let a=D(n);s=Object.entries(a).filter(([,i])=>(i.readCount??0)>0).map(([i])=>i)}catch{}oe(null,{message:e,active_files:s,notes:t}),process.exit(0)}else if(j==="session-tick"){let e=process.cwd(),t=y.join(e,".cairn");h.existsSync(t)||process.exit(0);let s=[];try{let i=`Auto-checkpoint at ${new Date().toISOString()}`,p=[],f=y.join(t,"session.json");if(h.existsSync(f))try{p=JSON.parse(h.readFileSync(f,"utf8")).notes||[]}catch{}let c=D(t),m=Object.entries(c).filter(([,u])=>(u.readCount??0)>0).map(([u])=>u);oe(null,{message:i,active_files:m,notes:p}),s.push("\u2713 checkpoint saved")}catch{}try{let i=Tt(e);i>0&&s.push(`memory backed up (${i})`)}catch{}let n=[];try{let i=y.join(t,"index.db");if(h.existsSync(i)){let{DatabaseSync:p}=await import("node:sqlite"),f=new p(i);try{H(f),ee(f);try{n=Pe(f,e).candidates}catch{}}finally{f.close()}}}catch{}let o="";try{let i=await Le(t);if(i.active){s.push(`roadmap ${i.advanced?"updated":"on track"}: ${i.currentLabel}`);let p=["[cairn] Production roadmap (\u2192 = cursor / current phase):",...i.treeLines.map(f=>" "+f)];i.memoLines&&p.push("","\u2500\u2500 Current phase detail (from roadmap.md) \u2500\u2500",...i.memoLines,"\u2500\u2500 (re-read after each phase to confirm direction) \u2500\u2500"),p.push("","Keep this current via cairn_roadmap on MATERIAL change (phase done / blocked / new) \u2014 not every turn."),o=p.join(`
80
- `)}}catch{}if(n.length>0){s.push(`${n.length} phase(s) may be done \u2014 confirm with user`);let i=["","[cairn] Possible COMPLETED phase(s) detected from recent commits \u2014","CONFIRM WITH THE USER before marking done; do NOT mark done unilaterally:"];for(let p of n)i.push(` \u2022 Phase ${p.phase_number}: ${p.text} (commit ${p.commit}, ${p.confidence}) \u2192 if confirmed: cairn_roadmap set_status ${p.phase_id} done`);o=o?o+`
81
- `+i.join(`
82
- `):i.join(`
83
- `).trimStart()}s.length===0&&process.exit(0);let a={systemMessage:`[cairn] ${s.join(" \xB7 ")}`};o&&(a.additionalContext=o),process.stdout.write(JSON.stringify(a)),process.exit(0)}else if(j==="memory-restore"){let e=process.cwd();h.existsSync(y.join(e,".cairn"))||process.exit(0);try{let t=Mt(e);t>0&&process.stdout.write(`[cairn] Restored ${t} memory file(s) from .cairn/memory backup (Claude store was empty)
84
- `)}catch{}process.exit(0)}else if(j==="resume-hint"){let e=y.join(process.cwd(),".cairn"),t=y.join(e,"session.json"),s=y.join(e,"index.db"),n=y.join(e,".hint-lock"),o=1800*1e3,a=process.cwd()===B.homedir();if(!a&&h.existsSync(e)&&(h.existsSync(s)||h.existsSync(t))){let r=y.join(e,".cairn-project");if(!h.existsSync(r))try{h.writeFileSync(r,"","utf8"),process.stdout.write(`[cairn] Repaired: wrote .cairn-project sentinel for hook compatibility.
85
- `)}catch{}}if(!a&&h.existsSync(s))try{let{DatabaseSync:r}=await import("node:sqlite"),l=new r(s,{readonly:!0});try{ee(l)}finally{l.close()}}catch{}{let r=[y.join(process.cwd(),".claude","settings.json"),y.join(B.homedir(),".claude","settings.json")];for(let l of r)try{if(!h.existsSync(l))continue;let d=h.readFileSync(l,"utf8"),g=JSON.parse(d),S=g?.hooks?.PreToolUse;if(!Array.isArray(S))continue;let _=!1;for(let x of S)if(!(x.matcher!=="Edit"||!Array.isArray(x.hooks)))for(let $ of x.hooks)$.command==="cairn set-mode edit"&&($.command="cairn edit-guard",_=!0);_&&(h.writeFileSync(l,JSON.stringify(g,null,2),"utf8"),process.stdout.write(`[cairn] Repaired: migrated cairn set-mode \u2192 cairn edit-guard in ${l}
86
- `))}catch{}}let i="";try{i=(JSON.parse(h.readFileSync("/dev/stdin","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);h.existsSync(t)||(h.existsSync(s)||(h.existsSync(n)&&Date.now()-h.statSync(n).mtimeMs<o&&process.exit(0),h.mkdirSync(e,{recursive:!0}),h.writeFileSync(n,new Date().toISOString(),"utf8"),process.stdout.write(`[cairn] No index found. Run cairn_maintain to index this project.
87
- `)),process.exit(0)),h.existsSync(n)&&Date.now()-h.statSync(n).mtimeMs<o&&process.exit(0);let c=JSON.parse(h.readFileSync(t,"utf8")),m=new Date(c.checkpoint_at).toLocaleString(),u=[`[cairn] Prior session: "${c.message}" (${m}).`,"[cairn] SESSION: ALWAYS call cairn_resume FIRST when asked to resume, continue, or recall prior work \u2014 do NOT substitute git log or file reads. cairn_resume includes commits_since_checkpoint so you can cross-reference checkpoint notes against what was shipped. At end of session call cairn_checkpoint with message + active_files + notes.","[cairn] TOOLS: NEVER use Bash to run cat, grep, head, tail, find, or ls for file inspection \u2014 always use the Read, Grep, or Glob tools instead. Prefer cairn_search / cairn_bundle / cairn_describe over manual file browsing. Use cairn_outline for structural overview, cairn_todos for backlog, cairn_security before PRs.","[cairn] MEMORY: IMPORTANT \u2014 call cairn_memo IMMEDIATELY and SILENTLY (do NOT ask the user, do NOT list what you are about to save, do NOT confirm afterward) when you observe \u2014 preference: user corrects/confirms your behaviour; experience: non-trivial problem solved; decision: architectural choice made; knowledge: non-obvious codebase fact discovered. cairn_employ_memory is explicit only \u2014 never call it unless asked."];if(f){let r=["[cairn] RESUME \u2014 checkpoint loaded inline:"];r.push(` message: ${c.message}`),r.push(` checkpoint: ${m}`),c.active_files?.length>0&&r.push(` active_files: ${c.active_files.join(", ")}`),c.notes?.length>0&&r.push(` notes: ${c.notes.join(" | ")}`);try{let l=new Date(c.checkpoint_at).toISOString(),d=me(`git log --oneline --after="${l}"`,{cwd:process.cwd(),encoding:"utf8",timeout:5e3}).trim();r.push(` commits_since: ${d?d.split(`
88
- `).join("; "):"(none)"}`)}catch{}try{let l=me("git diff --name-only HEAD",{cwd:process.cwd(),encoding:"utf8",timeout:5e3}).trim();l&&r.push(` uncommitted: ${l.split(`
89
- `).join(", ")}`)}catch{}try{let l=me("git status --short",{cwd:process.cwd(),encoding:"utf8",timeout:5e3}).trim();l&&r.push(` git_status:
90
- ${l.split(`
91
- `).map(d=>" "+d).join(`
92
- `)}`)}catch{}r.push(" \u2192 Call cairn_resume for full index status and open todos."),u.push(r.join(`
93
- `))}process.stdout.write(u.join(`
79
+ `):process.stderr.write(`[cairn] File compressed. Steps: (1) Read("${a.tempPath}") \u2014 loads full content into context. (2) Edit("${t}") \u2014 edit the original source, not the view.
80
+ `),process.exit(2)})}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 n=t?.tool_name||"",s=t?.tool_input||{},r=process.cwd(),i=g.join(r,".cairn");h.existsSync(i)||process.exit(0);let c=g.join(i,"index.db");h.existsSync(c)||process.exit(0);let l;try{({DatabaseSync:l}=await import("node:sqlite"))}catch{process.exit(0)}let p=/exit.?plan.?mode/i.test(n),a=/todo.?write/i.test(n);!p&&!a&&process.exit(0);let d=null,m;try{if(p){let u=s.plan;(typeof u!="string"||!u.trim())&&process.exit(0);let f=je(u);if(m=new l(c),q(m),(m.prepare("SELECT phase_id FROM main.roadmap_cursor WHERE singleton = 1").get()?.phase_id??null)==null){let _=He(m,null);_.id!=null&&Ue(m,_.id)}f.added.length>0&&(d=`[cairn] roadmap: captured ${f.added.length} phase(s) from plan${f.skipped.length?` (${f.skipped.length} already present)`:""}.`)}else if(a){m=new l(c);let u=Me(m,s.todos);u.seeded?d=`[cairn] roadmap: seeded ${u.seeded} phase(s) from todos.`:u.inserted>0&&(d=`[cairn] roadmap: linked ${u.inserted} new sub-task(s) to the current phase.`)}}catch(u){process.stderr.write(`[cairn] roadmap-capture skipped: ${u.message}
81
+ `);try{m?.close()}catch{}process.exit(0)}let o=m?re(m):null;try{m?.close()}catch{}o&&(d=d?`${d} ${o}`:o),d&&process.stdout.write(JSON.stringify({systemMessage:d})),process.exit(0)})}else if(P==="validate-map"){let e=g.join(process.cwd(),".cairn");h.existsSync(e)||process.exit(0);let t=W(e),n=te(e,t);for(let s of Object.values(t))s.readCount=0;M(e,t),n>0&&process.stdout.write(`[cairn] Cleaned minify map: ${n} stale entr${n===1?"y":"ies"} removed
82
+ `),process.exit(0)}else if(P==="checkpoint"&&process.argv[3]==="--auto"){let e=`Auto-checkpoint at ${new Date().toISOString()}`,t=[],n=[];try{let s=g.join(process.cwd(),".cairn"),r=g.join(s,"session.json");h.existsSync(r)&&(t=JSON.parse(h.readFileSync(r,"utf8")).notes||[]);let i=W(s);n=Object.entries(i).filter(([,c])=>(c.readCount??0)>0).map(([c])=>c)}catch{}ce(null,{message:e,active_files:n,notes:t}),process.exit(0)}else if(P==="session-tick"){let e=process.cwd(),t=g.join(e,".cairn");h.existsSync(t)||process.exit(0);let n=[];try{let l=`Auto-checkpoint at ${new Date().toISOString()}`,p=[],a=g.join(t,"session.json");if(h.existsSync(a))try{p=JSON.parse(h.readFileSync(a,"utf8")).notes||[]}catch{}let d=W(t),m=Object.entries(d).filter(([,o])=>(o.readCount??0)>0).map(([o])=>o);ce(null,{message:l,active_files:m,notes:p}),n.push("\u2713 checkpoint saved")}catch{}try{let l=dn(e);l>0&&n.push(`memory backed up (${l})`)}catch{}let s=[],r=null;try{let l=g.join(t,"index.db");if(h.existsSync(l)){let{DatabaseSync:p}=await import("node:sqlite"),a=new p(l);try{q(a),r=re(a);try{s=Te(a,e).candidates}catch{}}finally{a.close()}}}catch{}let i="";try{let l=await Je(t);if(l.active){n.push(`roadmap ${l.advanced?"updated":"on track"}: ${l.currentLabel}`);let p=["[cairn] Production roadmap (\u2192 = cursor / current phase):",...l.treeLines.map(a=>" "+a)];l.memoLines&&p.push("","\u2500\u2500 Current phase detail (from roadmap.md) \u2500\u2500",...l.memoLines,"\u2500\u2500 (re-read after each phase to confirm direction) \u2500\u2500"),p.push("","Keep this current via cairn_roadmap on MATERIAL change (phase done / blocked / new) \u2014 not every turn."),i=p.join(`
83
+ `)}}catch{}if(r&&(n.push("ROADMAP.md not updated \u2014 see warning"),i=i?`${i}
84
+
85
+ ${r}`:r),s.length>0){n.push(`${s.length} phase(s) may be done \u2014 confirm with user`);let l=["","[cairn] Possible COMPLETED phase(s) detected from recent commits \u2014","CONFIRM WITH THE USER before marking done; do NOT mark done unilaterally:"];for(let p of s)l.push(` \u2022 Phase ${p.phase_number}: ${p.text} (commit ${p.commit}, ${p.confidence}) \u2192 if confirmed: cairn_roadmap set_status ${p.phase_id} done`);i=i?i+`
86
+ `+l.join(`
87
+ `):l.join(`
88
+ `).trimStart()}n.length===0&&process.exit(0);let c={systemMessage:`[cairn] ${n.join(" \xB7 ")}`};i&&(c.additionalContext=i),process.stdout.write(JSON.stringify(c)),process.exit(0)}else if(P==="memory-restore"){let e=process.cwd();h.existsSync(g.join(e,".cairn"))||process.exit(0);try{let t=un(e);t>0&&process.stdout.write(`[cairn] Restored ${t} memory file(s) from .cairn/memory backup (Claude store was empty)
89
+ `)}catch{}process.exit(0)}else if(P==="resume-hint"){let e=g.join(process.cwd(),".cairn"),t=g.join(e,"session.json"),n=g.join(e,"index.db"),s=g.join(e,".hint-lock"),r=1800*1e3,i=process.cwd()===J.homedir();if(!i&&h.existsSync(e)&&(h.existsSync(n)||h.existsSync(t))){let o=g.join(e,".cairn-project");if(!h.existsSync(o))try{h.writeFileSync(o,"","utf8"),process.stdout.write(`[cairn] Repaired: wrote .cairn-project sentinel for hook compatibility.
90
+ `)}catch{}}if(!i&&h.existsSync(e))try{pn()&&process.stdout.write(`[cairn] Repaired: declared the roadmap slot (${H}) in ~/.claude.json.
91
+ `)}catch{}if(!i&&h.existsSync(n))try{let{DatabaseSync:o}=await import("node:sqlite"),u=new o(n,{readonly:!0}),f=null;try{f=re(u)}finally{u.close()}f&&process.stdout.write(`${f}
92
+ `)}catch{}{let o=[g.join(process.cwd(),".claude","settings.json"),g.join(J.homedir(),".claude","settings.json")];for(let u of o)try{if(!h.existsSync(u))continue;let f=h.readFileSync(u,"utf8"),y=JSON.parse(f),_=y?.hooks?.PreToolUse;if(!Array.isArray(_))continue;let S=!1;for(let x of _)if(!(x.matcher!=="Edit"||!Array.isArray(x.hooks)))for(let O of x.hooks)O.command==="cairn set-mode edit"&&(O.command="cairn edit-guard",S=!0);S&&(h.writeFileSync(u,JSON.stringify(y,null,2),"utf8"),process.stdout.write(`[cairn] Repaired: migrated cairn set-mode \u2192 cairn edit-guard in ${u}
93
+ `))}catch{}}let c="";try{c=(JSON.parse(h.readFileSync("/dev/stdin","utf8"))?.prompt||"").toLowerCase()}catch{}let p=/\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(c);h.existsSync(t)||(h.existsSync(n)||(h.existsSync(s)&&Date.now()-h.statSync(s).mtimeMs<r&&process.exit(0),h.mkdirSync(e,{recursive:!0}),h.writeFileSync(s,new Date().toISOString(),"utf8"),process.stdout.write(`[cairn] No index found. Run cairn_maintain to index this project.
94
+ `)),process.exit(0)),h.existsSync(s)&&Date.now()-h.statSync(s).mtimeMs<r&&process.exit(0);let a=JSON.parse(h.readFileSync(t,"utf8")),d=new Date(a.checkpoint_at).toLocaleString(),m=[`[cairn] Prior session: "${a.message}" (${d}).`,"[cairn] SESSION: ALWAYS call cairn_resume FIRST when asked to resume, continue, or recall prior work \u2014 do NOT substitute git log or file reads. cairn_resume includes commits_since_checkpoint so you can cross-reference checkpoint notes against what was shipped. At end of session call cairn_checkpoint with message + active_files + notes.","[cairn] TOOLS: NEVER use Bash to run cat, grep, head, tail, find, or ls for file inspection \u2014 always use the Read, Grep, or Glob tools instead. Prefer cairn_search / cairn_bundle / cairn_describe over manual file browsing. Use cairn_outline for structural overview, cairn_todos for backlog, cairn_security before PRs.","[cairn] MEMORY: IMPORTANT \u2014 call cairn_memo IMMEDIATELY and SILENTLY (do NOT ask the user, do NOT list what you are about to save, do NOT confirm afterward) when you observe \u2014 preference: user corrects/confirms your behaviour; experience: non-trivial problem solved; decision: architectural choice made; knowledge: non-obvious codebase fact discovered. cairn_employ_memory is explicit only \u2014 never call it unless asked."];if(p){let o=["[cairn] RESUME \u2014 checkpoint loaded inline:"];o.push(` message: ${a.message}`),o.push(` checkpoint: ${d}`),a.active_files?.length>0&&o.push(` active_files: ${a.active_files.join(", ")}`),a.notes?.length>0&&o.push(` notes: ${a.notes.join(" | ")}`);try{let u=new Date(a.checkpoint_at).toISOString(),f=ge(`git log --oneline --after="${u}"`,{cwd:process.cwd(),encoding:"utf8",timeout:5e3}).trim();o.push(` commits_since: ${f?f.split(`
95
+ `).join("; "):"(none)"}`)}catch{}try{let u=ge("git diff --name-only HEAD",{cwd:process.cwd(),encoding:"utf8",timeout:5e3}).trim();u&&o.push(` uncommitted: ${u.split(`
96
+ `).join(", ")}`)}catch{}try{let u=ge("git status --short",{cwd:process.cwd(),encoding:"utf8",timeout:5e3}).trim();u&&o.push(` git_status:
97
+ ${u.split(`
98
+ `).map(f=>" "+f).join(`
99
+ `)}`)}catch{}o.push(" \u2192 Call cairn_resume for full index status and open todos."),m.push(o.join(`
100
+ `))}process.stdout.write(m.join(`
94
101
  `)+`
95
- `),h.writeFileSync(n,new Date().toISOString(),"utf8"),process.exit(0)}else if(j==="roadmap-hint"){let e=y.join(process.cwd(),".cairn"),t=await Le(e);t.active||process.exit(0);let s=["[cairn] ROADMAP \u2014 cursor is active.",...t.treeLines.map(n=>"[cairn] "+n),`[cairn] Current: ${t.currentLabel}`];if(t.memoLines){s.push("[cairn] \u2500\u2500\u2500\u2500\u2500 Decision memo (current phase) \u2500\u2500\u2500\u2500\u2500");for(let n of t.memoLines)s.push("[cairn] "+n);s.push("[cairn] \u2500\u2500\u2500\u2500\u2500 (re-read after each phase to confirm direction) \u2500\u2500\u2500\u2500\u2500")}s.push("[cairn] Use cairn_roadmap set_status <id> done (auto-advances), focus <id>, or next to move cursor."),process.stdout.write(s.join(`
102
+ `),h.writeFileSync(s,new Date().toISOString(),"utf8"),process.exit(0)}else if(P==="roadmap-hint"){let e=g.join(process.cwd(),".cairn"),t=await Je(e);t.active||process.exit(0);let n=["[cairn] ROADMAP \u2014 cursor is active.",...t.treeLines.map(s=>"[cairn] "+s),`[cairn] Current: ${t.currentLabel}`];if(t.memoLines){n.push("[cairn] \u2500\u2500\u2500\u2500\u2500 Decision memo (current phase) \u2500\u2500\u2500\u2500\u2500");for(let s of t.memoLines)n.push("[cairn] "+s);n.push("[cairn] \u2500\u2500\u2500\u2500\u2500 (re-read after each phase to confirm direction) \u2500\u2500\u2500\u2500\u2500")}n.push("[cairn] Use cairn_roadmap set_status <id> done (auto-advances), focus <id>, or next to move cursor."),process.stdout.write(n.join(`
96
103
  `)+`
97
- `),process.exit(0)}else if(j==="install"){let e=process.platform,t=y.join(B.homedir(),".claude.json"),s={};if(h.existsSync(t))try{s=JSON.parse(h.readFileSync(t,"utf8"))}catch{}s.mcpServers=s.mcpServers||{};let n=!!s.mcpServers.cairn;s.mcpServers.cairn={command:"cairn-mcp"},h.writeFileSync(t,JSON.stringify(s,null,2),"utf8");let o=y.join(B.homedir(),".claude");Fe(o,!0),console.log("Cairn installed successfully!"),console.log(""),console.log(` Platform : ${e}`),console.log(` MCP : cairn-mcp registered in ${t}${n?" (updated)":""}`),console.log(` Hooks : installed in ${y.join(o,"settings.json")}`),console.log(""),console.log("Active hooks:"),console.log(" PreToolUse[Read] -> cairn minify (compress source files for reading)"),console.log(" PreToolUse[Edit] -> cairn edit-guard (block Edit until re-read with full content)"),console.log(" PreToolUse[Write] -> cairn edit-guard (clear minify state before full file overwrite)"),console.log(" PostToolUse[ExitPlanMode] -> cairn roadmap-capture (approved plan -> roadmap phases)"),console.log(" PostToolUse[TodoWrite] -> cairn roadmap-capture (todos -> roadmap sub-tasks)"),console.log(" Stop -> cairn session-tick (checkpoint + memory backup + roadmap, surfaced)"),console.log(" UserPromptSubmit -> cairn resume-hint (remind Claude of prior session)"),console.log(" UserPromptSubmit -> cairn memory-restore (restore .cairn/memory -> Claude store if empty)"),console.log(""),console.log("Restart Claude Code to activate the MCP server, then open any project and start working."),console.log("Upgrade any time with: npm install -g @misterhuydo/cairn-mcp"),process.exit(0)}else if(j==="install-hooks"){let e=process.argv.includes("--global"),t=e?y.join(B.homedir(),".claude"):y.join(process.cwd(),".claude");Fe(t,e),console.log(`Cairn hooks installed in ${e?"global (~/.claude/settings.json)":"project (.claude/settings.json)"}`),console.log(""),console.log("Active hooks:"),console.log(" PreToolUse[Read] -> cairn minify (compress source files for reading)"),console.log(" PreToolUse[Edit] -> cairn edit-guard (block Edit until re-read with full content)"),console.log(" PreToolUse[Write] -> cairn edit-guard (clear minify state before full file overwrite)"),console.log(" PostToolUse[ExitPlanMode] -> cairn roadmap-capture (approved plan -> roadmap phases)"),console.log(" PostToolUse[TodoWrite] -> cairn roadmap-capture (todos -> roadmap sub-tasks)"),console.log(" Stop -> cairn session-tick (checkpoint + memory backup + roadmap, surfaced)"),console.log(" UserPromptSubmit -> cairn resume-hint (remind Claude of prior session)"),console.log(" UserPromptSubmit -> cairn memory-restore (restore .cairn/memory -> Claude store if empty)"),process.exit(0)}else console.error("Usage: cairn <--version | install | install-hooks [--global] | minify | edit-guard | validate-map | checkpoint --auto | session-tick | resume-hint | roadmap-hint | roadmap-capture | memory-restore>"),process.exit(1);function Fe(e,t){let s=y.join(e,"settings.json"),n={};if(h.existsSync(s))try{n=JSON.parse(h.readFileSync(s,"utf8"))}catch{}n.hooks=n.hooks||{};let o=n.hooks.PreToolUse||[];o.some(r=>r.matcher==="Read"&&r.hooks?.some(l=>l.command==="cairn minify"))||o.push({matcher:"Read",hooks:[{type:"command",command:"cairn minify"}]});let a=o.filter(r=>!(r.matcher==="Edit"&&r.hooks?.some(l=>l.command==="cairn set-mode edit")));a.some(r=>r.matcher==="Edit"&&r.hooks?.some(l=>l.command==="cairn edit-guard"))||a.push({matcher:"Edit",hooks:[{type:"command",command:"cairn edit-guard"}]}),a.some(r=>r.matcher==="Write"&&r.hooks?.some(l=>l.command==="cairn edit-guard"))||a.push({matcher:"Write",hooks:[{type:"command",command:"cairn edit-guard"}]}),n.hooks.PreToolUse=a;let i=(r,l)=>(r||[]).map(d=>({...d,hooks:(d.hooks||[]).filter(g=>!l(g.command))})).filter(d=>(d.hooks||[]).length>0),p=r=>typeof r=="string"&&/sync-memory-(forward|restore)\.(mjs|py)/.test(r),f=i(n.hooks.Stop,r=>r==="cairn checkpoint --auto"||r==="cairn roadmap-hint");f.some(r=>r.hooks?.some(l=>l.command==="cairn session-tick"))||f.push({hooks:[{type:"command",command:"cairn session-tick"}]}),n.hooks.Stop=f;let c=i(n.hooks.UserPromptSubmit,p);c.some(r=>r.hooks?.some(l=>l.command==="cairn resume-hint"))||c.push({hooks:[{type:"command",command:"cairn resume-hint"}]}),c.some(r=>r.hooks?.some(l=>l.command==="cairn memory-restore"))||c.push({hooks:[{type:"command",command:"cairn memory-restore"}]}),n.hooks.UserPromptSubmit=c;let m=i(n.hooks.PostToolUse,p),u=r=>{m.some(l=>l.matcher===r&&l.hooks?.some(d=>d.command==="cairn roadmap-capture"))||m.push({matcher:r,hooks:[{type:"command",command:"cairn roadmap-capture"}]})};if(u("ExitPlanMode"),u("TodoWrite"),n.hooks.PostToolUse=m,t){let r=y.join(e,"scripts");for(let l of["sync-memory-forward.mjs","sync-memory-restore.mjs","sync-memory-forward.py","sync-memory-restore.py"]){let d=y.join(r,l);if(h.existsSync(d))try{h.unlinkSync(d)}catch{}}}if(!t){let r=y.join(process.cwd(),".cairn");h.mkdirSync(r,{recursive:!0});let l=y.join(r,".cairn-project");h.existsSync(l)||h.writeFileSync(l,"","utf8")}h.mkdirSync(e,{recursive:!0}),h.writeFileSync(s,JSON.stringify(n,null,2),"utf8")}
104
+ `),process.exit(0)}else if(P==="install"){let e=process.platform,t=g.join(J.homedir(),".claude.json"),n={};if(h.existsSync(t))try{n=JSON.parse(h.readFileSync(t,"utf8"))}catch{}n.mcpServers=n.mcpServers||{};let s=!!n.mcpServers.cairn;n.mcpServers.cairn={...s&&typeof n.mcpServers.cairn=="object"?n.mcpServers.cairn:{},command:"cairn-mcp",terminalConnect:Ye},h.writeFileSync(t,JSON.stringify(n,null,2),"utf8");let r=g.join(J.homedir(),".claude");Be(r,!0),console.log("Cairn installed successfully!"),console.log(""),console.log(` Platform : ${e}`),console.log(` MCP : cairn-mcp registered in ${t}${s?" (updated)":""}`),console.log(` Hooks : installed in ${g.join(r,"settings.json")}`),console.log(""),console.log("Active hooks:"),console.log(" PreToolUse[Read] -> cairn minify (compress source files for reading)"),console.log(" PreToolUse[Edit] -> cairn edit-guard (block Edit until re-read with full content)"),console.log(" PreToolUse[Write] -> cairn edit-guard (clear minify state before full file overwrite)"),console.log(" PostToolUse[ExitPlanMode] -> cairn roadmap-capture (approved plan -> roadmap phases)"),console.log(" PostToolUse[TodoWrite] -> cairn roadmap-capture (todos -> roadmap sub-tasks)"),console.log(" Stop -> cairn session-tick (checkpoint + memory backup + roadmap, surfaced)"),console.log(" UserPromptSubmit -> cairn resume-hint (remind Claude of prior session)"),console.log(" UserPromptSubmit -> cairn memory-restore (restore .cairn/memory -> Claude store if empty)"),console.log(""),console.log("Restart Claude Code to activate the MCP server, then open any project and start working."),console.log("Upgrade any time with: npm install -g @misterhuydo/cairn-mcp"),process.exit(0)}else if(P==="install-hooks"){let e=process.argv.includes("--global"),t=e?g.join(J.homedir(),".claude"):g.join(process.cwd(),".claude");Be(t,e),console.log(`Cairn hooks installed in ${e?"global (~/.claude/settings.json)":"project (.claude/settings.json)"}`),console.log(""),console.log("Active hooks:"),console.log(" PreToolUse[Read] -> cairn minify (compress source files for reading)"),console.log(" PreToolUse[Edit] -> cairn edit-guard (block Edit until re-read with full content)"),console.log(" PreToolUse[Write] -> cairn edit-guard (clear minify state before full file overwrite)"),console.log(" PostToolUse[ExitPlanMode] -> cairn roadmap-capture (approved plan -> roadmap phases)"),console.log(" PostToolUse[TodoWrite] -> cairn roadmap-capture (todos -> roadmap sub-tasks)"),console.log(" Stop -> cairn session-tick (checkpoint + memory backup + roadmap, surfaced)"),console.log(" UserPromptSubmit -> cairn resume-hint (remind Claude of prior session)"),console.log(" UserPromptSubmit -> cairn memory-restore (restore .cairn/memory -> Claude store if empty)"),process.exit(0)}else console.error("Usage: cairn <--version | install | install-hooks [--global] | minify | edit-guard | validate-map | checkpoint --auto | session-tick | resume-hint | roadmap-hint | roadmap-capture | memory-restore>"),process.exit(1);function Be(e,t){let n=g.join(e,"settings.json"),s={};if(h.existsSync(n))try{s=JSON.parse(h.readFileSync(n,"utf8"))}catch{}s.hooks=s.hooks||{};let r=s.hooks.PreToolUse||[];r.some(o=>o.matcher==="Read"&&o.hooks?.some(u=>u.command==="cairn minify"))||r.push({matcher:"Read",hooks:[{type:"command",command:"cairn minify"}]});let i=r.filter(o=>!(o.matcher==="Edit"&&o.hooks?.some(u=>u.command==="cairn set-mode edit")));i.some(o=>o.matcher==="Edit"&&o.hooks?.some(u=>u.command==="cairn edit-guard"))||i.push({matcher:"Edit",hooks:[{type:"command",command:"cairn edit-guard"}]}),i.some(o=>o.matcher==="Write"&&o.hooks?.some(u=>u.command==="cairn edit-guard"))||i.push({matcher:"Write",hooks:[{type:"command",command:"cairn edit-guard"}]}),s.hooks.PreToolUse=i;let c=(o,u)=>(o||[]).map(f=>({...f,hooks:(f.hooks||[]).filter(y=>!u(y.command))})).filter(f=>(f.hooks||[]).length>0),l=o=>typeof o=="string"&&/sync-memory-(forward|restore)\.(mjs|py)/.test(o),p=c(s.hooks.Stop,o=>o==="cairn checkpoint --auto"||o==="cairn roadmap-hint");p.some(o=>o.hooks?.some(u=>u.command==="cairn session-tick"))||p.push({hooks:[{type:"command",command:"cairn session-tick"}]}),s.hooks.Stop=p;let a=c(s.hooks.UserPromptSubmit,l);a.some(o=>o.hooks?.some(u=>u.command==="cairn resume-hint"))||a.push({hooks:[{type:"command",command:"cairn resume-hint"}]}),a.some(o=>o.hooks?.some(u=>u.command==="cairn memory-restore"))||a.push({hooks:[{type:"command",command:"cairn memory-restore"}]}),s.hooks.UserPromptSubmit=a;let d=c(s.hooks.PostToolUse,l),m=o=>{d.some(u=>u.matcher===o&&u.hooks?.some(f=>f.command==="cairn roadmap-capture"))||d.push({matcher:o,hooks:[{type:"command",command:"cairn roadmap-capture"}]})};if(m("ExitPlanMode"),m("TodoWrite"),s.hooks.PostToolUse=d,t){let o=g.join(e,"scripts");for(let u of["sync-memory-forward.mjs","sync-memory-restore.mjs","sync-memory-forward.py","sync-memory-restore.py"]){let f=g.join(o,u);if(h.existsSync(f))try{h.unlinkSync(f)}catch{}}}if(!t){let o=g.join(process.cwd(),".cairn");h.mkdirSync(o,{recursive:!0});let u=g.join(o,".cairn-project");h.existsSync(u)||h.writeFileSync(u,"","utf8")}h.mkdirSync(e,{recursive:!0}),h.writeFileSync(n,JSON.stringify(s,null,2),"utf8")}