#!/usr/bin/env node // File-only Zen writer. No network, model execution, Git delivery or credentials. import {existsSync,mkdirSync,readFileSync,writeFileSync,renameSync,readdirSync,unlinkSync,realpathSync,statSync,lstatSync} from 'node:fs'; import {resolve,join,dirname,relative,isAbsolute} from 'node:path'; import {randomUUID,createHash} from 'node:crypto'; import {execFileSync} from 'node:child_process'; const types=['LOG','RDT','FDK','TSK','ART','SRT','PIC','VID','AUD','FIL','DOC','SES','MEM','MIL','IDE']; const visibility=['private','local','never-copy','public']; const digest=value=>createHash('sha256').update(value).digest('hex'); const canonical=value=>JSON.stringify(value&&typeof value==='object'&&!Array.isArray(value)?Object.fromEntries(Object.keys(value).sort().map(key=>[key,JSON.parse(canonical(value[key]))])):Array.isArray(value)?value.map(item=>JSON.parse(canonical(item))):value); const json=path=>JSON.parse(readFileSync(path,'utf8')); const now=()=>new Date().toISOString(); function fail(message){throw new Error(message);} function atomic(path,text){mkdirSync(dirname(path),{recursive:true});const tmp=`${path}.${randomUUID()}.tmp`;writeFileSync(tmp,text,{encoding:'utf8',mode:0o600,flush:true});renameSync(tmp,path);} function repository(path){const real=realpathSync(resolve(path));if(!statSync(real).isDirectory())fail('Repository must be an existing directory.');try{const output=execFileSync('git',['worktree','list','--porcelain'],{cwd:real,encoding:'utf8',windowsHide:true,stdio:['ignore','pipe','ignore']});const first=/^worktree (.+)$/m.exec(output)?.[1];return first?realpathSync(first):real;}catch{return real;}} function parse(text){const match=/^---\r?\n([\s\S]*?)\r?\n---\r?\n([\s\S]*)$/.exec(text);if(!match)fail('Invalid record; preserve the file for repair.');let meta;try{meta=JSON.parse(match[1]);}catch{fail('Standalone records use JSON-form YAML frontmatter. Use companion repair for another format.');}return validate({...meta,body:match[2].replace(/^\r?\n/,'').replace(/\r?\n$/,'')});} function serialize(record){const {body,...meta}=record;return `---\n${JSON.stringify(meta,null,2)}\n---\n\n${body}\n`;} function metadata(record){const {id,ref,type,title,status,revision,tags,links,updatedAt}=record;return{id,ref,type,title,status,revision,tags,links,updatedAt};} function safeArtifactPath(file){return typeof file==='string'&&file.length>0&&!isAbsolute(file)&&!file.split(/[\\/]/).some(part=>part==='..'||part.startsWith('.')||['node_modules','learning'].includes(part))&&!/\.(?:pem|key|sqlite)$/i.test(file);} function containedFile(base,path){if(!existsSync(path)||lstatSync(path).isSymbolicLink()||!statSync(path).isFile())return false;const part=relative(resolve(base),realpathSync(path));return !part.startsWith('..')&&!isAbsolute(part);} function sourceRepositoryRoot(path,expected){const source=realpathSync(resolve(path));let top=source;try{top=realpathSync(execFileSync('git',['rev-parse','--show-toplevel'],{cwd:source,encoding:'utf8',windowsHide:true,stdio:['ignore','pipe','ignore']}).trim());}catch{if(source.toLowerCase()!==expected.toLowerCase())fail('Use the canonical repository root for this source manifest.');}if(top.toLowerCase()!==source.toLowerCase()||repository(source).toLowerCase()!==expected.toLowerCase())fail('Source root must be a repository/worktree root mapped to this workspace.');return source;} function validate(record){ const uuid=/^[a-f\d]{8}-[a-f\d]{4}-[a-f\d]{4}-[a-f\d]{4}-[a-f\d]{12}$/i,ref=/^(?:[A-Z]{2,3}-)?(?:LOG|RDT|FDK|TSK|ART|SRT|PIC|VID|AUD|FIL|DOC|SES|MEM|MIL|IDE)-\d{4,}(?:-SRT-\d{2,})?$/; if(record.schemaVersion!==1||!uuid.test(record.id)||!uuid.test(record.workspaceId)||!ref.test(record.ref)||!types.includes(record.type)||!visibility.includes(record.visibility)||typeof record.title!=='string'||!record.title.trim()||record.title.length>240||!Number.isInteger(record.revision)||record.revision<1||!Number.isFinite(Date.parse(record.createdAt))||!Number.isFinite(Date.parse(record.updatedAt)))fail('Invalid record identity, type, title, visibility or revision.'); if(!Array.isArray(record.tags)||record.tags.some(x=>typeof x!=='string')||!Array.isArray(record.links)||record.links.some(x=>typeof x.id!=='string'||typeof x.relation!=='string')||!record.data||typeof record.data!=='object'||Array.isArray(record.data)||typeof record.body!=='string')fail('Invalid tags, links, data or body.'); if(record.type==='TSK'&&!['backlog','review'].includes(record.status))fail('Standalone tasks may be Backlog or Review. Working requires dispatch; Done is owner-controlled.'); if(['MEM','FDK'].includes(record.type)&&record.status!=='proposed')fail('Standalone learning stays proposed; use the owner/companion approval path to apply it.'); return record; } function execute(root,command,input){ const managed=()=>{if(existsSync(join(root,'zen.sqlite')))fail('Managed workspace: use Zen Companion/MCP. Standalone never bypasses its SQLite journal or permissions.');}; managed();mkdirSync(root,{recursive:true});const lock=join(root,'writer.lock'),lockId=`${process.pid}:${randomUUID()}`; if(existsSync(lock)){const pid=Number(readFileSync(lock,'utf8').split(':')[0]);if(!Number.isInteger(pid)||pid<=0)fail('Unknown writer lock; inspect it rather than deleting it.');let alive=true;try{process.kill(pid,0);}catch(error){if(error.code==='ESRCH')alive=false;}if(alive)fail('Another writer owns this directory.');unlinkSync(lock);} writeFileSync(lock,lockId,{flag:'wx',mode:0o600}); try{ managed();const statePath=join(root,'standalone','state.json');let state=existsSync(statePath)?json(statePath):{counters:{},index:{}}; const saveState=()=>atomic(statePath,JSON.stringify(state,null,2)); const registry=join(root,'workspace-registry.json');let workspaces=existsSync(registry)?json(registry):[]; const workspace=id=>{const w=id?workspaces.find(w=>w.id===id):workspaces.length===1?workspaces[0]:null;if(!w)fail('Choose an initialized workspace UUID.');return w;}; const recordsRoot=w=>w.mode==='project'?join(w.repository,'.zen','records'):join(root,'workspaces',w.id,'records'); const pathFor=r=>join(recordsRoot(workspace(r.workspaceId)),r.type.toLowerCase(),`${r.ref}.md`); const historyRoot=r=>join(dirname(recordsRoot(workspace(r.workspaceId))),'history',r.id); const all=()=>{const records=[];const walk=dir=>{if(!existsSync(dir))return;for(const e of readdirSync(dir,{withFileTypes:true})){const path=join(dir,e.name);if(e.isDirectory())walk(path);else if(e.name.endsWith('.md'))records.push(parse(readFileSync(path,'utf8')));}};for(const w of workspaces)walk(recordsRoot(w));return records;}; const find=id=>{const matches=all().filter(r=>r.id===id||r.ref===id);if(matches.length!==1)fail('Record missing or reference ambiguous.');return matches[0];}; const readable=id=>{const r=find(id);if(r.visibility==='never-copy')fail('Record is never-copy and unavailable to the agent.');return r;}; const snapshot=r=>{const path=join(historyRoot(r),`${r.revision}.md`),text=serialize(r);if(existsSync(path)&&readFileSync(path,'utf8')!==text)fail('Immutable history conflict; preserve both sources for repair.');if(!existsSync(path))atomic(path,text);}; const apply=journal=>{ const record=journal.record,path=pathFor(record),text=serialize(record),disk=existsSync(path)?readFileSync(path,'utf8'):null; if(disk!==text&&(disk===null?null:digest(disk))!==journal.oldHash)fail('External file conflict: intended journal and current Markdown are preserved. Resolve before retrying.'); if(disk!==null&&disk!==text)snapshot(parse(disk));snapshot(record);if(disk!==text)atomic(path,text); state.index[record.id]={hash:digest(text),path:relative(root,path)};saveState();journal.status='applied';atomic(journal.path,JSON.stringify({...journal,path:undefined},null,2));return record; }; const commands=join(root,'standalone','commands');mkdirSync(commands,{recursive:true}); for(const name of readdirSync(commands)){if(!name.endsWith('.json'))continue;const path=join(commands,name),journal=json(path);if(journal.status==='prepared')apply({...journal,path});} if(command==='init'){ const repo=repository(input.repository??process.cwd()),mode=input.mode??'project',code=String(input.code??'').toUpperCase(); if(!['project','global'].includes(mode)||!/^([A-Z]{2,3})$/.test(code)||!input.name?.trim())fail('Init requires name, 2–3-letter code and project/global mode.'); if(mode==='project'&&resolve(root)!==resolve(repo,'.zen'))fail('Project mode must use the canonical repository .zen directory; use global mode for a separate data root.'); const existing=workspaces.find(w=>w.repository.toLowerCase()===repo.toLowerCase());if(existing)return{workspace:existing,writer:'standalone',synced:false}; if(workspaces.some(w=>w.code===code)||(mode==='project'&&workspaces.some(w=>w.mode==='project')))fail('Duplicate code or multiple unprefixed projects; choose unique global workspace codes.'); const w={id:randomUUID(),name:input.name,code,repository:repo,mode,mainBranch:input.mainBranch??'main',captureSessions:false,createdAt:now()};workspaces.push(w);atomic(registry,JSON.stringify(workspaces,null,2));mkdirSync(recordsRoot(w),{recursive:true});saveState(); if(mode==='project'){const ignore=join(repo,'.gitignore'),text=existsSync(ignore)?readFileSync(ignore,'utf8'):'';if(!/^\/?\.zen\/?\s*$/m.test(text))atomic(ignore,`${text}\n# Private Zen records\n/.zen/\n`);} return{workspace:w,writer:'standalone',synced:false}; } if(!workspaces.length)fail('Initialize this file-only workspace first.'); if(command==='state'||command==='list')return{writer:'standalone',synced:false,workspaces,records:all().filter(r=>r.visibility!=='never-copy'&&(!input.workspaceId||r.workspaceId===input.workspaceId)&&(!input.type||r.type===input.type)).map(metadata)}; if(command==='recover')return{recovered:true,writer:'standalone',synced:false}; if(command==='get'||command==='history'){ let record=readable(input.id); if(command==='history'){ const dir=historyRoot(record),items=existsSync(dir)?readdirSync(dir).filter(x=>x.endsWith('.md')).map(x=>parse(readFileSync(join(dir,x),'utf8'))).filter(x=>x.visibility!=='never-copy'):[]; if(!input.revision)return{history:items.sort((a,b)=>a.revision-b.revision).map(metadata),writer:'standalone',synced:false}; record=items.find(x=>x.revision===input.revision);if(!record)fail('Requested history revision unavailable.'); } const offset=Math.max(0,Number(input.offset??0)),limit=Math.max(1,Math.min(20000,Number(input.limit??12000))); return{...record,body:record.body.slice(offset,offset+limit),bodyLength:record.body.length,nextOffset:offset+limitr.visibility!=='never-copy'&&(!input.workspaceId||r.workspaceId===input.workspaceId)&&(!input.type||r.type===input.type)&&JSON.stringify(r).toLowerCase().includes(query)).slice(0,20).map(r=>({...metadata(r),body:r.body.slice(0,1500),data:r.data})),writer:'standalone',synced:false};} if(command==='verify'){ const current=readable(input.id);if(current.type!=='SRT')fail('Verify requires an SRT reference.');const collection=readable(current.links.find(l=>l.relation==='collection')?.id); const selected=collection.data.selectedVariantId===current.id,selectedRevision=selected?collection.data.selectedVariantRevision:current.revision; const record=selectedRevision&&selectedRevision!==current.revision?parse(readFileSync(join(historyRoot(current),`${selectedRevision}.md`),'utf8')):current; const snapshot=record.data.snapshot;if(!snapshot||!Array.isArray(snapshot.files)||typeof snapshot.directory!=='string')fail('Variant has no valid source snapshot; pin an explicit file manifest first.'); const w=workspace(record.workspaceId),base=join(dirname(recordsRoot(w)),'artifacts',record.id),relativeSnapshot=relative(base,resolve(snapshot.directory));if(relativeSnapshot.startsWith('..')||isAbsolute(relativeSnapshot))fail('Snapshot directory is outside this variant.');sourceRepositoryRoot(snapshot.sourceRoot,w.repository); const snapshotDrift=[],sourceDrift=[]; for(const file of snapshot.files){if(!safeArtifactPath(file.path)||!/^[a-f\d]{64}$/.test(file.sha256))fail('Unsafe snapshot manifest.');const pinned=join(snapshot.directory,file.path),source=resolve(snapshot.sourceRoot,file.path);if(!containedFile(base,pinned)||digest(readFileSync(pinned))!==file.sha256)snapshotDrift.push(file.path);if(!containedFile(snapshot.sourceRoot,source)||digest(readFileSync(source))!==file.sha256)sourceDrift.push(file.path);} return{ref:record.ref,selected,selectedRevision,recordChanged:current.revision!==selectedRevision,snapshotDirectory:snapshot.directory,snapshotDrift,sourceDrift,ok:!snapshotDrift.length&&!sourceDrift.length&¤t.revision===selectedRevision,writer:'standalone',synced:false}; } if(!['create','update','transition','select','snapshot'].includes(command))fail('Commands: init, state, list, get, search, history, create, update, transition, select, snapshot, verify, recover.'); const key=input.idempotencyKey;if(typeof key!=='string'||!key.trim()||key.length>160)fail('A stable idempotencyKey is required for mutations.'); const requestHash=digest(canonical({command,input})),journalPath=join(commands,`${digest(key)}.json`); if(existsSync(journalPath)){const journal=json(journalPath);if(journal.requestHash!==requestHash)fail('Idempotency key belongs to different input.');const current=find(journal.record.id);return current.visibility==='never-copy'?{...metadata(journal.record),writer:'standalone',synced:false}:{...journal.record,writer:'standalone',synced:false};} let record,oldHash=null; if(command==='create'){ const w=workspace(input.workspaceId),type=input.type;if(!types.includes(type))fail('Unknown record type.'); const parent=input.parentId?readable(input.parentId):null;if(type==='SRT'&&(!parent||parent.type!=='ART'||parent.workspaceId!==w.id))fail('SRT requires an ART parent in this workspace.');if(type!=='SRT'&&parent)fail('parentId is reserved for artifact variants.'); const scope=`${w.id}:${parent?.id??type}`,prefix=parent?`${parent.ref}-SRT-`:`${w.mode==='global'?w.code+'-':''}${type}-`; const observed=all().filter(r=>r.workspaceId===w.id&&r.ref.startsWith(prefix)).map(r=>Number(r.ref.slice(prefix.length))).filter(Number.isFinite); const counter=Math.max(state.counters[scope]??0,...observed,0)+1;state.counters[scope]=counter;saveState(); const at=now(),data=input.data??{}; if(['FDK','MEM'].includes(type)&&input.status&&input.status!=='proposed')fail('Learning starts proposed in standalone mode.'); record={schemaVersion:1,id:randomUUID(),ref:prefix+String(counter).padStart(parent?2:4,'0'),workspaceId:w.id,type,title:input.title,body:input.body??'',status:['FDK','MEM'].includes(type)?'proposed':input.status??(type==='TSK'?'backlog':'active'),tags:input.tags??[],links:[...(input.links??[]),...(parent?[{id:parent.id,relation:'collection'}]:[])],visibility:input.visibility??'local',revision:1,createdAt:at,updatedAt:at,provenance:{actor:'agent',source:'zen-standalone'},data:type==='FDK'?{...data,exactPrompt:input.body??''}:type==='TSK'?{kind:'feature',priority:'Normal',baseRef:'HEAD',acceptanceCriteria:[],checkCommands:[],timezone:'Europe/London',...data}:data}; }else{ const previous=readable(input.id),path=pathFor(previous),disk=readFileSync(path,'utf8');oldHash=digest(disk); if(previous.revision!==input.expectedRevision)fail('Revision conflict: read and compare the current record before editing.'); if(state.index[previous.id]?.hash!==oldHash)fail('External file edit detected; original file and request are preserved. Resolve explicitly.'); let patch=input.patch??{}; if(command==='transition'){if(previous.type!=='TSK')fail('Expected TSK.');patch={status:input.status};} if(command==='select'){const variant=readable(input.variantId);if(previous.type!=='ART'||variant.type!=='SRT'||variant.workspaceId!==previous.workspaceId||!variant.links.some(l=>l.id===previous.id&&l.relation==='collection'))fail('Variant must belong to the specified collection.');if((variant.data.kind==='runnable'||['sourcePath','source','previewPath','entryPath'].some(key=>typeof variant.data[key]==='string'))&&!variant.data.snapshot?.files?.length)fail('Pin the runnable source with snapshot before selecting it.');patch={data:{...previous.data,selectedVariantId:variant.id,selectedVariantRevision:variant.revision,selectedRef:variant.ref}};} if(command==='snapshot'){ if(previous.type!=='SRT')fail('Snapshots belong to an SRT variant.');const w=workspace(previous.workspaceId),sourceRoot=sourceRepositoryRoot(input.sourceRoot??w.repository,w.repository); if(!Array.isArray(input.files)||!input.files.length||input.files.length>100)fail('Supply an explicit manifest of 1–100 relative source files.'); const dir=join(dirname(recordsRoot(w)),'artifacts',previous.id,digest(key).slice(0,24));let total=0;const files=[]; for(const file of input.files){ if(!safeArtifactPath(file))fail('Manifest contains a protected or unsafe source path.'); const source=resolve(sourceRoot,file),canonicalSource=realpathSync(source),part=relative(sourceRoot,canonicalSource);if(part.startsWith('..')||isAbsolute(part)||lstatSync(source).isSymbolicLink()||!statSync(source).isFile())fail('Source must be a regular file inside the declared root.'); const size=statSync(source).size;if(total+size>32*1024*1024||size>8*1024*1024)fail('Snapshot exceeds bounded artifact size.');const bytes=readFileSync(source);total+=bytes.length; const destination=join(dir,file);if(existsSync(destination)&&digest(readFileSync(destination))!==digest(bytes))fail('Snapshot identity already contains different source; preserve it and use a new intent.');if(!existsSync(destination))atomic(destination,bytes); files.push({path:file.replaceAll('\\','/'),sha256:digest(bytes),size:bytes.length}); } patch={data:{...previous.data,snapshot:{directory:dir,sourceRoot,files,createdAt:now()}}}; } if(Object.keys(patch).some(k=>!['title','body','status','tags','links','visibility','data'].includes(k)))fail('Record identity cannot be patched.'); if(previous.type==='FDK'&&patch.body!==undefined&&patch.body!==previous.body)fail('Feedback body is immutable. Create a follow-up FDK.'); record={...previous,...patch,revision:previous.revision+1,updatedAt:now()}; } validate(record);const journal={requestHash,record,oldHash,status:'prepared'};atomic(journalPath,JSON.stringify(journal,null,2));apply({...journal,path:journalPath}); return record.visibility==='never-copy'?{...metadata(record),writer:'standalone',synced:false}:{...record,writer:'standalone',synced:false}; }finally{if(existsSync(lock)&&readFileSync(lock,'utf8')===lockId)unlinkSync(lock);} } try{ const args=process.argv.slice(2);const option=name=>{const i=args.indexOf(name);if(i<0)return null;const value=args[i+1];if(!value)fail(`Missing ${name} value.`);args.splice(i,2);return value;}; const rootOption=option('--root'),inputPath=option('--input'),command=args[0]??'help'; if(command==='help'){console.log('Zen file-only records: --root --input . Mutations require idempotencyKey; never bypass a companion journal.');} else{const raw=inputPath?readFileSync(inputPath==='-'?0:inputPath,'utf8'):'{}';if(Buffer.byteLength(raw)>2*1024*1024)fail('Input exceeds 2 MB; use linked attachments for large evidence.');const input=JSON.parse(raw);const root=resolve(rootOption??join(repository(process.cwd()),'.zen'));console.log(JSON.stringify(execute(root,command,input)));} }catch(error){console.error(JSON.stringify({error:error.message}));process.exitCode=1;}