thinkpool-pair 0.7.366 → 0.7.368
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/LICENSE +2 -2
- package/README.md +10 -10
- package/account.mjs +1 -1
- package/bridge.mjs +1 -1
- package/byok-detect.mjs +1 -1
- package/claude-session.mjs +1 -1
- package/code-event-contract.mjs +1 -1
- package/codex-app-server.mjs +1 -1
- package/codex-session.mjs +1 -1
- package/cross-terminal.mjs +1 -1
- package/design-edit.mjs +1 -1
- package/flow-assembly.mjs +1 -1
- package/flow-conductor.mjs +1 -1
- package/flow-models.mjs +1 -1
- package/flow-review.mjs +1 -1
- package/hermes-acp-bootstrap.py +16 -16
- package/hermes-delegation-guard.mjs +1 -1
- package/hermes-isolation.mjs +1 -1
- package/hermes-policy.mjs +1 -1
- package/hermes-probe.mjs +1 -1
- package/hermes-session.mjs +1 -1
- package/hermes-setup.mjs +1 -1
- package/host-memory.mjs +1 -1
- package/keep-awake.mjs +1 -1
- package/launcher.mjs +1 -1
- package/package.json +2 -2
- package/privacy-report.mjs +1 -1
- package/publish-manifest.json +38 -38
- package/scheduled-runs.mjs +1 -1
- package/service.mjs +1 -1
- package/side-lane.mjs +1 -1
- package/terminal-name.mjs +1 -1
- package/thinkpool-capabilities.json +11 -31
- package/thinkpool-prompt-contracts.mjs +1 -1
- package/thinkpool-room-prompt.mjs +1 -1
- package/worker-completion.mjs +1 -1
package/design-edit.mjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import e from"node:crypto";import t from"node:fs";import r from"node:os";import n from"node:path";import{DESIGN_SOURCE_MAP_RELATIVE as o,DESIGN_SOURCE_MAP_VERSION as i,MAX_DESIGN_SOURCE_ENTRIES as s,MAX_DESIGN_SOURCE_MAP_BYTES as a}from"./design-source-contract.mjs";const c=2097152,u=4e3,l=new Set(["dist","build",".next",".git","coverage","node_modules"]),d=(e,t)=>String(e??"").replace(/\0/g,"").trim().slice(0,t),p=(e,t)=>{const r=n.relative(e,t);return""===r||!r.startsWith("..")&&!n.isAbsolute(r)};export async function syncRealtimeAuth(e,t,r=null){const n=t||r;if(!n||"function"!=typeof e?.realtime?.setAuth)return!1;try{return await e.realtime.setAuth(n),!0}catch{return!1}}export function sourceRevision(t){return e.createHash("sha256").update(String(t)).digest("hex").slice(0,24)}export function resolveDesignSourceMap(e,r,u){if(!e||!r||!u)return null;let h,m,f,g,v,y;try{h=t.realpathSync(e),f=t.realpathSync(r),m=t.realpathSync(n.join(f,o)),g=t.realpathSync(u);const i=t.statSync(h);if(!i.isFile()||i.size<=0||i.size>a)return null;if(h!==m||!p(g,f)||!p(f,h))return null;v=t.readFileSync(h,"utf8"),y=JSON.parse(v)}catch{return null}if(y?.version!==i||!y.entries||"object"!=typeof y.entries||Array.isArray(y.entries))return null;const b=Object.entries(y.entries);if(b.length>s)return null;const w=new Map,x=new Map;for(const[e,r]of b){if(!/^[A-Za-z0-9_-]{8,80}$/.test(e)||!r||"object"!=typeof r)return null;const o=d(r.file,700);if(!o||n.isAbsolute(o)||o.split(/[\\/]/).some(e=>".."===e||l.has(e)))return null;let i=w.get(o);if(!i){try{i=t.realpathSync(n.resolve(g,o));const e=t.statSync(i);if(!e.isFile()||e.size>c||!p(g,i))return null}catch{return null}w.set(o,i)}const s=Number(r.start),a=Number(r.end),u=Number(r.line),h=Number(r.column),m=d(r.tag,40).toLowerCase(),f=d(r.fileHash,64).toLowerCase();if(![s,a,u,h].every(Number.isSafeInteger)||s<0||a<=s||u<1||h<0)return null;if(!/^[a-z][a-z0-9:-]*$/.test(m)||!/^[a-f0-9]{24}$/.test(f))return null;x.set(e,{sourceId:e,file:o.split(n.sep).join("/"),sourcePath:i,start:s,end:a,line:u,column:h,tag:m,fileHash:f})}return{path:h,revision:sourceRevision(v),buildId:d(y.buildId,80),entries:x}}export function resolveDesignSource(r,o){if(!r||!o)return null;let i,s;try{i=t.realpathSync(r),s=t.realpathSync(o);const e=t.statSync(i);if(!e.isFile()||e.size>c)return null}catch{return null}const a=n.relative(s,i);if(!a||a.startsWith("..")||n.isAbsolute(a)||".html"!==n.extname(i).toLowerCase())return null;if(a.split(n.sep).some(e=>["dist","build",".next",".git","coverage","node_modules"].includes(e)))return null;const u=t.readFileSync(i,"utf8"),l=sourceRevision(u);return{previewId:e.createHash("sha256").update(`${i}\0${l}`).digest("base64url").slice(0,32),revision:l,sourcePath:i,workspaceRoot:s,source:u}}export function resolvePreviewDesignSource(r,o,i){if(!(r&&"preview"===r.sourceKind&&r.snapshot&&o&&i))return null;let s,a,u;try{s=t.realpathSync(r.snapshot),a=t.realpathSync(o),u=t.realpathSync(i);const e=t.statSync(s);if(!e.isFile()||e.size>c)return null}catch{return null}const l=n.relative(u,s);if(!l||l.startsWith("..")||n.isAbsolute(l)||".html"!==n.extname(s).toLowerCase())return null;const h=d(r.route||"/",500),m=d(r.previewRoot||"dist",500),f=d(r.captureKey,1e3);if(!h.startsWith("/")||!f||f!==JSON.stringify([m,h]))return null;const g=t.readFileSync(s,"utf8");let v=null;try{if(v=t.realpathSync(n.resolve(a,m)),!p(a,v))return null}catch{return null}const y=r.sourceMap?resolveDesignSourceMap(r.sourceMap,v,a):null,b=sourceRevision(y?`${g}\0${y.revision}`:g);return{sourceKind:"preview",previewId:e.createHash("sha256").update(`${a}\0${f}\0${b}`).digest("base64url").slice(0,32),revision:b,sourcePath:s,workspaceRoot:a,source:g,outbox:u,route:h,previewRoot:m,captureKey:f,sourceMap:y,sourceMapPath:y?.path||null,sourceMapDeclared:!!r.sourceMap}}export function resolveManifestDesignSource(e,t,{box:r}={}){return e&&"object"==typeof e?"preview"===e.sourceKind?resolvePreviewDesignSource(e,t,r):resolveDesignSource(e.source||e.html,t):null}export function resolveManifestDisplaySource(e,r,{box:o}={}){if(!r)return null;if("preview"===r.sourceKind||!e?.snapshot)return r.source;let i,s;try{i=t.realpathSync(e.snapshot),s=t.realpathSync(o);const r=t.statSync(i);if(!r.isFile()||r.size>c)return null}catch{return null}const a=n.relative(s,i);return!a||a.startsWith("..")||n.isAbsolute(a)||".html"!==n.extname(i).toLowerCase()?null:t.readFileSync(i,"utf8")}export function refreshDesignSource(e){return e?"preview"!==e.sourceKind?resolveDesignSource(e.sourcePath,e.workspaceRoot):resolvePreviewDesignSource({sourceKind:"preview",snapshot:e.sourcePath,route:e.route,previewRoot:e.previewRoot,captureKey:e.captureKey,sourceMap:e.sourceMapPath},e.workspaceRoot,e.outbox):null}export function restoreDesignSources(e,r,o,i=100){if(!e||!r||!o)return[];let s;try{s=t.readdirSync(e,{withFileTypes:!0}).filter(e=>e.isFile()&&e.name.endsWith(".json")&&!e.name.endsWith(".receipt.json")).map(r=>{const o=n.join(e,r.name);return{file:o,mtimeMs:t.statSync(o).mtimeMs}}).sort((e,t)=>t.mtimeMs-e.mtimeMs).slice(0,Math.max(0,i))}catch{return[]}const a=[];for(const{file:i}of s)try{const s=JSON.parse(t.readFileSync(i,"utf8")),c=resolveManifestDesignSource(s,r,{box:e});if(!c)continue;Object.assign(c,{term:o,slug:String(s.slug||""),title:String(s.title||s.slug||""),desktop:s.desktop||null,mobile:s.mobile||null});const u=n.join(e,".design-revisions",`${c.revision}.html`);t.existsSync(u)&&(c.backupPath=u),a.push(c)}catch{}return a}export function resolveRequestSourceIdentity(e,r){if(!r?.sourceId)return{ok:!0,identity:null};const n=e?.sourceMap?.entries?.get(r.sourceId);if(!n)return{ok:!1,error:e?.sourceMapDeclared?"stale-source-identity":"source-identity-unavailable"};if(r.tag&&n.tag!==r.tag)return{ok:!1,error:"source-identity-mismatch"};let o;try{o=t.readFileSync(n.sourcePath,"utf8")}catch{return{ok:!1,error:"stale-source-identity"}}return sourceRevision(o)!==n.fileHash?{ok:!1,error:"stale-source-identity"}:{ok:!0,identity:{sourceId:n.sourceId,file:n.file,start:n.start,end:n.end,line:n.line,column:n.column,tag:n.tag,fileHash:n.fileHash}}}export function validateDesignRequest(e,t,r=""){if(!e||"object"!=typeof e)return{ok:!1,error:"invalid-request"};const n=d(e.previewId,64),o=t.get(n);if(!o)return{ok:!1,error:"preview-unavailable"};if(d(e.revision,64)!==o.revision)return{ok:!1,error:"stale-preview"};const i=function(e){if(!e||"object"!=typeof e)return null;const t=e.rect&&"object"==typeof e.rect?{x:Number(e.rect.x)||0,y:Number(e.rect.y)||0,width:Math.max(0,Number(e.rect.width)||0),height:Math.max(0,Number(e.rect.height)||0)}:null;return{selector:d(e.selector,600),xpath:d(e.xpath,600),tag:d(e.tag,40).toLowerCase(),role:d(e.role,80),name:d(e.name,240),text:d(e.text,u),canEditText:!0===e.canEditText,canReplaceImage:!0===e.canReplaceImage,nodeKey:d(e.nodeKey,80),parentNodeKey:d(e.parentNodeKey,80),sourceId:d(e.sourceId,80),parentSourceId:d(e.parentSourceId,80),rect:t}}(e.target);if(!i?.selector&&!i?.xpath)return{ok:!1,error:"select-an-element"};const s=["text","prompt","move","image"].includes(e.mode)?e.mode:null;if(!s)return{ok:!1,error:"invalid-mode"};const a=d(e.intent,"text"===s?u:2e3);if(["text","prompt"].includes(s)&&!a)return{ok:!1,error:"text"===s?"enter-text":"describe-change"};if("text"===s&&!i.canEditText)return{ok:!1,error:"text-edit-not-safe"};const c="move"===s?function(e){if(!e||"object"!=typeof e)return null;const t=Number(e.fromIndex),r=Number(e.toIndex);return![t,r].every(Number.isSafeInteger)||t<0||r<0||t>499||r>499?null:{fromIndex:t,toIndex:r}}(e.movement):null;if("move"===s&&(!c||c.fromIndex===c.toIndex))return{ok:!1,error:"invalid-move"};const l="image"===s?function(e,t){if(!e||"object"!=typeof e)return null;const r={path:d(e.path,700),name:d(e.name,240),mediaType:d(e.mediaType,120).toLowerCase(),size:Number(e.size)},n=t?`code/${String(t).toUpperCase()}/design-drafts/`:"code/";return r.path.startsWith(n)&&!r.path.split("/").includes("..")&&r.mediaType.startsWith("image/")?!Number.isSafeInteger(r.size)||r.size<=0||r.size>10485760?null:r:null}(e.asset,r):null;if(!("image"!==s||i.canReplaceImage&&l))return{ok:!1,error:"invalid-image"};const p=resolveRequestSourceIdentity(o,i);return p.ok?{ok:!0,record:o,request:{previewId:n,revision:o.revision,mode:s,intent:a,target:i,movement:c,asset:l,sourceIdentity:p.identity,cid:d(e.cid,80)}}:p}export function validateDesignBatchRequest(e,t,r=""){if(!e||"object"!=typeof e||!Array.isArray(e.edits))return validateDesignRequest(e,t,r);if(e.edits.length<1||e.edits.length>8)return{ok:!1,error:"invalid-batch"};const n=d(e.previewId,64),o=d(e.revision,64),i=d(e.cid,80),s=[],a=new Set;let c=null;for(let u=0;u<e.edits.length;u++){const l=validateDesignRequest({...e.edits[u],previewId:n,revision:o,cid:i},t,r);if(!l.ok)return{...l,editIndex:u};const d=l.request.target.sourceId||l.request.target.nodeKey||l.request.target.selector||l.request.target.xpath;if(a.has(d))return{ok:!1,error:"duplicate-target",editIndex:u};a.add(d),c=l.record,s.push(l.request)}return{ok:!0,record:c,request:{previewId:n,revision:c.revision,mode:"batch",edits:s,cid:i}}}export async function deleteDesignAssetDraft({webBase:e,token:t,room:r,path:n,fetcher:o=fetch}){if(!(e&&t&&r&&n))return!1;const i=await o(`${String(e).replace(/\/$/,"")}/api/code-design-draft`,{method:"DELETE",headers:{"Content-Type":"application/json",Authorization:`Bearer ${t}`},body:JSON.stringify({code:r,path:n})}).catch(()=>null);return!!i?.ok}export async function materializeDesignAsset({webBase:e,token:o,room:i,asset:s,requestId:a,cleanup:c=!0,fetcher:u=fetch}){if(!(e&&o&&i&&s?.path&&a))return null;const l={"Content-Type":"application/json",Authorization:`Bearer ${o}`},p=await u(`${String(e).replace(/\/$/,"")}/api/code-attachment-url`,{method:"POST",headers:l,body:JSON.stringify({code:i,path:s.path})});if(!p.ok)return null;const{url:h}=await p.json();if(!h)return null;const m=await u(h);if(!m.ok)return null;const f=Buffer.from(await m.arrayBuffer());if(!f.length||f.length>10485760)return null;const g=n.extname(s.name).replace(/[^.a-zA-Z0-9]/g,"").slice(0,9)||".img",v=n.join(r.tmpdir(),"thinkpool-design-assets"),y=n.join(v,`${d(a,80)}${g}`);return t.mkdirSync(v,{recursive:!0}),t.writeFileSync(y,f),c&&await deleteDesignAssetDraft({webBase:e,token:o,room:i,path:s.path,fetcher:u}),y}function h(e){return"text"===e.mode?`Replace only this element's visible text with:\n${e.intent}`:"move"===e.mode?`Move only this selected element within its current direct parent, from sibling position ${e.movement.fromIndex+1} to ${e.movement.toIndex+1}. Preserve the parent, responsive behavior, semantics, and every other sibling.`:"image"===e.mode?`Replace only this selected image's content with the uploaded image at this bridge-local temporary path:\n${e.asset.localPath}\nCopy it into an appropriate durable project asset location. Preserve the existing image frame, dimensions, border radius, object-fit behavior, responsive rules, and surrounding layout.`:`Apply this visual change to only the selected element unless the instruction explicitly requires its immediate context:\n${e.intent}`}function m(e){const t=e.sourceIdentity?`\n\nBridge-verified application source:\n${JSON.stringify(e.sourceIdentity,null,2)}`:"\n\nApplication source identity: unavailable — selector/XPath fallback only.";return`Selected element:\n${JSON.stringify(e.target,null,2)}${t}`}export function designTranscript(e,{restore:t=!1}={}){if(t)return"Thinkpool Design · restore the previous verified revision";const r="batch"===e?.mode?e.edits:[e],n=Array.isArray(r)?r.filter(Boolean).slice(0,8):[],o="batch"===e?.mode?`Thinkpool Design · ${n.length} pooled edit${1===n.length?"":"s"}`:"Thinkpool Design · 1 edit",i=n.map((e,t)=>{const r=d(e.target?.name||e.target?.tag||"element",240);return`${t+1}. ${n=e.mode,{text:"Text",prompt:"Prompt",move:"Move",image:"Image"}[n]||"Edit"} · ${r}\n${function(e){return"text"===e.mode?`Replacement text:\n${d(e.intent,u)}`:"prompt"===e.mode?`Instruction:\n${d(e.intent,2e3)}`:"move"===e.mode?`Move from position ${e.movement.fromIndex+1} to position ${e.movement.toIndex+1}.`:"image"===e.mode?`Replacement image: ${d(e.asset?.name||"uploaded image",240)}`:""}(e)}`;var n});return[o,...i].join("\n\n")}export function designPrompt({record:e,request:t,by:r,restore:n=!1,priorRecord:o=null}){const i="batch"===t.mode?t.edits:null,s=n?`Restore only the immediately previous Design Mode change by comparing the current artifact with this bridge-created verified backup: ${o?.backupPath||"(backup unavailable)"}. Do not overwrite unrelated changes.`:i?i.map((e,t)=>`Edit ${t+1} of ${i.length} · ${e.mode}\n${m(e)}\n\n${h(e)}`).join("\n\n---\n\n"):`${m(t)}\n\n${h(t)}`;return"preview"===e.sourceKind?`A room member used ThinkPool Design Mode on a rendered application preview. This is an authorized source edit in your current workspace.\n\nRendered snapshot (read-only evidence): ${e.sourcePath}\nApplication workspace: ${e.workspaceRoot}\nPreview root: ${e.previewRoot}\nPreview route: ${e.route}\nExpected snapshot revision: ${e.revision}\nRequested by: ${d(r||"A room member",120)}\nOperation: ${i?`${i.length} queued edits in one batch`:t.mode}\n\n${s}\n\nRules:\n- Re-read the rendered snapshot and confirm the selected element still matches before editing.\n- Do not edit the snapshot. For an edit with bridge-verified application source, start at that exact relative file and range. For a selector/XPath fallback, locate the represented element in application source and do not call the match exact.\n- Before editing a bridge-verified target, confirm the current source still hashes to the supplied fileHash. If it does not, stop and request a fresh capture/reselection.\n- If fallback selector, text, or surrounding structure does not identify one application source location unambiguously, stop without editing and explain that the person must reselect it.\n${i?"- Apply the queued edits in order in one source pass, then build and capture once after the full batch.":"- Apply only the requested change, then build and capture once."}\n- Preserve the product page faithfully: keep its real copy, structure, fonts, assets, spacing, responsive behavior, and unrelated source unchanged.\n- Run the project build, then use preview_start with root ${e.previewRoot} and preview_capture with path ${e.route}, both viewports, and title ${JSON.stringify(e.title||e.slug||"Design preview")}. The bridge correlates that capture to this request.\n- Do not claim the edit is verified until the room receives the new desktop and mobile capture.\n- If the build or capture fails, report the failure instead of substituting a screenshot-only result.`:`A room member used ThinkPool Design Mode on the artifact below. This is an authorized source edit in your current workspace.\n\nArtifact source: ${e.sourcePath}\nWorkspace: ${e.workspaceRoot}\nExpected source revision: ${e.revision}\nRequested by: ${d(r||"A room member",120)}\nOperation: ${n?"restore the immediately previous verified Design Mode revision":i?`${i.length} queued edits in one batch`:t.mode}\n\n${s}\n\nRules:\n- Re-read the exact artifact source and confirm its revision/content still matches every selected element before editing.\n${i?"- Apply the queued edits in order in one source pass, then render once after the full batch.":"- Apply only the requested change, then render the result once."}\n- Edit the canonical source above; do not search for or edit a different plausible file.\n- Preserve the represented product page faithfully: keep its real copy, structure, fonts, assets, spacing, and responsive behavior except for the selected edits. Never replace it with generic mockup content.\n- Preserve unrelated content and styling.\n- Use the existing mockup render workflow to produce both desktop and mobile captures after the edit. Run it with TP_DESIGN_REQUEST_ID=${t.cid} and TP_DESIGN_PARENT_REVISION=${e.revision} in the command environment so the proof correlates to this request.\n- Do not claim this is live until both captures succeed and the room receives the manifest.\n- If any target is stale or ambiguous, stop without editing and explain that the person must reselect it.`}
|
|
1
|
+
import e from"node:crypto";import t from"node:fs";import r from"node:os";import n from"node:path";import{DESIGN_SOURCE_MAP_RELATIVE as o,DESIGN_SOURCE_MAP_VERSION as i,MAX_DESIGN_SOURCE_ENTRIES as s,MAX_DESIGN_SOURCE_MAP_BYTES as a}from"./design-source-contract.mjs";const c=2097152,u=4e3,l=new Set(["dist","build",".next",".git","coverage","node_modules"]),d=(e,t)=>String(e??"").replace(/\0/g,"").trim().slice(0,t),p=(e,t)=>{const r=n.relative(e,t);return""===r||!r.startsWith("..")&&!n.isAbsolute(r)};export async function syncRealtimeAuth(e,t,r=null){const n=t||r;if(!n||"function"!=typeof e?.realtime?.setAuth)return!1;try{return await e.realtime.setAuth(n),!0}catch{return!1}}export function sourceRevision(t){return e.createHash("sha256").update(String(t)).digest("hex").slice(0,24)}export function resolveDesignSourceMap(e,r,u){if(!e||!r||!u)return null;let h,m,f,g,v,y;try{h=t.realpathSync(e),f=t.realpathSync(r),m=t.realpathSync(n.join(f,o)),g=t.realpathSync(u);const i=t.statSync(h);if(!i.isFile()||i.size<=0||i.size>a)return null;if(h!==m||!p(g,f)||!p(f,h))return null;v=t.readFileSync(h,"utf8"),y=JSON.parse(v)}catch{return null}if(y?.version!==i||!y.entries||"object"!=typeof y.entries||Array.isArray(y.entries))return null;const b=Object.entries(y.entries);if(b.length>s)return null;const w=new Map,x=new Map;for(const[e,r]of b){if(!/^[A-Za-z0-9_-]{8,80}$/.test(e)||!r||"object"!=typeof r)return null;const o=d(r.file,700);if(!o||n.isAbsolute(o)||o.split(/[\\/]/).some(e=>".."===e||l.has(e)))return null;let i=w.get(o);if(!i){try{i=t.realpathSync(n.resolve(g,o));const e=t.statSync(i);if(!e.isFile()||e.size>c||!p(g,i))return null}catch{return null}w.set(o,i)}const s=Number(r.start),a=Number(r.end),u=Number(r.line),h=Number(r.column),m=d(r.tag,40).toLowerCase(),f=d(r.fileHash,64).toLowerCase();if(![s,a,u,h].every(Number.isSafeInteger)||s<0||a<=s||u<1||h<0)return null;if(!/^[a-z][a-z0-9:-]*$/.test(m)||!/^[a-f0-9]{24}$/.test(f))return null;x.set(e,{sourceId:e,file:o.split(n.sep).join("/"),sourcePath:i,start:s,end:a,line:u,column:h,tag:m,fileHash:f})}return{path:h,revision:sourceRevision(v),buildId:d(y.buildId,80),entries:x}}export function resolveDesignSource(r,o){if(!r||!o)return null;let i,s;try{i=t.realpathSync(r),s=t.realpathSync(o);const e=t.statSync(i);if(!e.isFile()||e.size>c)return null}catch{return null}const a=n.relative(s,i);if(!a||a.startsWith("..")||n.isAbsolute(a)||".html"!==n.extname(i).toLowerCase())return null;if(a.split(n.sep).some(e=>["dist","build",".next",".git","coverage","node_modules"].includes(e)))return null;const u=t.readFileSync(i,"utf8"),l=sourceRevision(u);return{previewId:e.createHash("sha256").update(`${i}\0${l}`).digest("base64url").slice(0,32),revision:l,sourcePath:i,workspaceRoot:s,source:u}}export function resolvePreviewDesignSource(r,o,i){if(!(r&&"preview"===r.sourceKind&&r.snapshot&&o&&i))return null;let s,a,u;try{s=t.realpathSync(r.snapshot),a=t.realpathSync(o),u=t.realpathSync(i);const e=t.statSync(s);if(!e.isFile()||e.size>c)return null}catch{return null}const l=n.relative(u,s);if(!l||l.startsWith("..")||n.isAbsolute(l)||".html"!==n.extname(s).toLowerCase())return null;const h=d(r.route||"/",500),m=d(r.previewRoot||"dist",500),f=d(r.captureKey,1e3);if(!h.startsWith("/")||!f||f!==JSON.stringify([m,h]))return null;const g=t.readFileSync(s,"utf8");let v=null;try{if(v=t.realpathSync(n.resolve(a,m)),!p(a,v))return null}catch{return null}const y=r.sourceMap?resolveDesignSourceMap(r.sourceMap,v,a):null,b=sourceRevision(y?`${g}\0${y.revision}`:g);return{sourceKind:"preview",previewId:e.createHash("sha256").update(`${a}\0${f}\0${b}`).digest("base64url").slice(0,32),revision:b,sourcePath:s,workspaceRoot:a,source:g,outbox:u,route:h,previewRoot:m,captureKey:f,sourceMap:y,sourceMapPath:y?.path||null,sourceMapDeclared:!!r.sourceMap}}export function resolveManifestDesignSource(e,t,{box:r}={}){return e&&"object"==typeof e?"preview"===e.sourceKind?resolvePreviewDesignSource(e,t,r):resolveDesignSource(e.source||e.html,t):null}export function resolveManifestDisplaySource(e,r,{box:o}={}){if(!r)return null;if("preview"===r.sourceKind||!e?.snapshot)return r.source;let i,s;try{i=t.realpathSync(e.snapshot),s=t.realpathSync(o);const r=t.statSync(i);if(!r.isFile()||r.size>c)return null}catch{return null}const a=n.relative(s,i);return!a||a.startsWith("..")||n.isAbsolute(a)||".html"!==n.extname(i).toLowerCase()?null:t.readFileSync(i,"utf8")}export function refreshDesignSource(e){return e?"preview"!==e.sourceKind?resolveDesignSource(e.sourcePath,e.workspaceRoot):resolvePreviewDesignSource({sourceKind:"preview",snapshot:e.sourcePath,route:e.route,previewRoot:e.previewRoot,captureKey:e.captureKey,sourceMap:e.sourceMapPath},e.workspaceRoot,e.outbox):null}export function restoreDesignSources(e,r,o,i=100){if(!e||!r||!o)return[];let s;try{s=t.readdirSync(e,{withFileTypes:!0}).filter(e=>e.isFile()&&e.name.endsWith(".json")&&!e.name.endsWith(".receipt.json")).map(r=>{const o=n.join(e,r.name);return{file:o,mtimeMs:t.statSync(o).mtimeMs}}).sort((e,t)=>t.mtimeMs-e.mtimeMs).slice(0,Math.max(0,i))}catch{return[]}const a=[];for(const{file:i}of s)try{const s=JSON.parse(t.readFileSync(i,"utf8")),c=resolveManifestDesignSource(s,r,{box:e});if(!c)continue;Object.assign(c,{term:o,slug:String(s.slug||""),title:String(s.title||s.slug||""),desktop:s.desktop||null,mobile:s.mobile||null});const u=n.join(e,".design-revisions",`${c.revision}.html`);t.existsSync(u)&&(c.backupPath=u),a.push(c)}catch{}return a}export function resolveRequestSourceIdentity(e,r){if(!r?.sourceId)return{ok:!0,identity:null};const n=e?.sourceMap?.entries?.get(r.sourceId);if(!n)return{ok:!1,error:e?.sourceMapDeclared?"stale-source-identity":"source-identity-unavailable"};if(r.tag&&n.tag!==r.tag)return{ok:!1,error:"source-identity-mismatch"};let o;try{o=t.readFileSync(n.sourcePath,"utf8")}catch{return{ok:!1,error:"stale-source-identity"}}return sourceRevision(o)!==n.fileHash?{ok:!1,error:"stale-source-identity"}:{ok:!0,identity:{sourceId:n.sourceId,file:n.file,start:n.start,end:n.end,line:n.line,column:n.column,tag:n.tag,fileHash:n.fileHash}}}export function validateDesignRequest(e,t,r=""){if(!e||"object"!=typeof e)return{ok:!1,error:"invalid-request"};const n=d(e.previewId,64),o=t.get(n);if(!o)return{ok:!1,error:"preview-unavailable"};if(d(e.revision,64)!==o.revision)return{ok:!1,error:"stale-preview"};const i=function(e){if(!e||"object"!=typeof e)return null;const t=e.rect&&"object"==typeof e.rect?{x:Number(e.rect.x)||0,y:Number(e.rect.y)||0,width:Math.max(0,Number(e.rect.width)||0),height:Math.max(0,Number(e.rect.height)||0)}:null;return{selector:d(e.selector,600),xpath:d(e.xpath,600),tag:d(e.tag,40).toLowerCase(),role:d(e.role,80),name:d(e.name,240),text:d(e.text,u),canEditText:!0===e.canEditText,canReplaceImage:!0===e.canReplaceImage,nodeKey:d(e.nodeKey,80),parentNodeKey:d(e.parentNodeKey,80),sourceId:d(e.sourceId,80),parentSourceId:d(e.parentSourceId,80),rect:t}}(e.target);if(!i?.selector&&!i?.xpath)return{ok:!1,error:"select-an-element"};const s=["text","prompt","move","image"].includes(e.mode)?e.mode:null;if(!s)return{ok:!1,error:"invalid-mode"};const a=d(e.intent,"text"===s?u:2e3);if(["text","prompt"].includes(s)&&!a)return{ok:!1,error:"text"===s?"enter-text":"describe-change"};if("text"===s&&!i.canEditText)return{ok:!1,error:"text-edit-not-safe"};const c="move"===s?function(e){if(!e||"object"!=typeof e)return null;const t=Number(e.fromIndex),r=Number(e.toIndex);return![t,r].every(Number.isSafeInteger)||t<0||r<0||t>499||r>499?null:{fromIndex:t,toIndex:r}}(e.movement):null;if("move"===s&&(!c||c.fromIndex===c.toIndex))return{ok:!1,error:"invalid-move"};const l="image"===s?function(e,t){if(!e||"object"!=typeof e)return null;const r={path:d(e.path,700),name:d(e.name,240),mediaType:d(e.mediaType,120).toLowerCase(),size:Number(e.size)},n=t?`code/${String(t).toUpperCase()}/design-drafts/`:"code/";return r.path.startsWith(n)&&!r.path.split("/").includes("..")&&r.mediaType.startsWith("image/")?!Number.isSafeInteger(r.size)||r.size<=0||r.size>10485760?null:r:null}(e.asset,r):null;if(!("image"!==s||i.canReplaceImage&&l))return{ok:!1,error:"invalid-image"};const p=resolveRequestSourceIdentity(o,i);return p.ok?{ok:!0,record:o,request:{previewId:n,revision:o.revision,mode:s,intent:a,target:i,movement:c,asset:l,sourceIdentity:p.identity,cid:d(e.cid,80)}}:p}export function validateDesignBatchRequest(e,t,r=""){if(!e||"object"!=typeof e||!Array.isArray(e.edits))return validateDesignRequest(e,t,r);if(e.edits.length<1||e.edits.length>8)return{ok:!1,error:"invalid-batch"};const n=d(e.previewId,64),o=d(e.revision,64),i=d(e.cid,80),s=[],a=new Set;let c=null;for(let u=0;u<e.edits.length;u++){const l=validateDesignRequest({...e.edits[u],previewId:n,revision:o,cid:i},t,r);if(!l.ok)return{...l,editIndex:u};const d=l.request.target.sourceId||l.request.target.nodeKey||l.request.target.selector||l.request.target.xpath;if(a.has(d))return{ok:!1,error:"duplicate-target",editIndex:u};a.add(d),c=l.record,s.push(l.request)}return{ok:!0,record:c,request:{previewId:n,revision:c.revision,mode:"batch",edits:s,cid:i}}}export async function deleteDesignAssetDraft({webBase:e,token:t,room:r,path:n,fetcher:o=fetch}){if(!(e&&t&&r&&n))return!1;const i=await o(`${String(e).replace(/\/$/,"")}/api/code-design-draft`,{method:"DELETE",headers:{"Content-Type":"application/json",Authorization:`Bearer ${t}`},body:JSON.stringify({code:r,path:n})}).catch(()=>null);return!!i?.ok}export async function materializeDesignAsset({webBase:e,token:o,room:i,asset:s,requestId:a,cleanup:c=!0,fetcher:u=fetch}){if(!(e&&o&&i&&s?.path&&a))return null;const l={"Content-Type":"application/json",Authorization:`Bearer ${o}`},p=await u(`${String(e).replace(/\/$/,"")}/api/code-attachment-url`,{method:"POST",headers:l,body:JSON.stringify({code:i,path:s.path})});if(!p.ok)return null;const{url:h}=await p.json();if(!h)return null;const m=await u(h);if(!m.ok)return null;const f=Buffer.from(await m.arrayBuffer());if(!f.length||f.length>10485760)return null;const g=n.extname(s.name).replace(/[^.a-zA-Z0-9]/g,"").slice(0,9)||".img",v=n.join(r.tmpdir(),"thinkpool-design-assets"),y=n.join(v,`${d(a,80)}${g}`);return t.mkdirSync(v,{recursive:!0}),t.writeFileSync(y,f),c&&await deleteDesignAssetDraft({webBase:e,token:o,room:i,path:s.path,fetcher:u}),y}function h(e){return"text"===e.mode?`Replace only this element's visible text with:\n${e.intent}`:"move"===e.mode?`Move only this selected element within its current direct parent, from sibling position ${e.movement.fromIndex+1} to ${e.movement.toIndex+1}. Preserve the parent, responsive behavior, semantics, and every other sibling.`:"image"===e.mode?`Replace only this selected image's content with the uploaded image at this bridge-local temporary path:\n${e.asset.localPath}\nCopy it into an appropriate durable project asset location. Preserve the existing image frame, dimensions, border radius, object-fit behavior, responsive rules, and surrounding layout.`:`Apply this visual change to only the selected element unless the instruction explicitly requires its immediate context:\n${e.intent}`}function m(e){const t=e.sourceIdentity?`\n\nBridge-verified application source:\n${JSON.stringify(e.sourceIdentity,null,2)}`:"\n\nApplication source identity: unavailable — selector/XPath fallback only.";return`Selected element:\n${JSON.stringify(e.target,null,2)}${t}`}export function designTranscript(e,{restore:t=!1}={}){if(t)return"thinkpool Design · restore the previous verified revision";const r="batch"===e?.mode?e.edits:[e],n=Array.isArray(r)?r.filter(Boolean).slice(0,8):[],o="batch"===e?.mode?`thinkpool Design · ${n.length} pooled edit${1===n.length?"":"s"}`:"thinkpool Design · 1 edit",i=n.map((e,t)=>{const r=d(e.target?.name||e.target?.tag||"element",240);return`${t+1}. ${n=e.mode,{text:"Text",prompt:"Prompt",move:"Move",image:"Image"}[n]||"Edit"} · ${r}\n${function(e){return"text"===e.mode?`Replacement text:\n${d(e.intent,u)}`:"prompt"===e.mode?`Instruction:\n${d(e.intent,2e3)}`:"move"===e.mode?`Move from position ${e.movement.fromIndex+1} to position ${e.movement.toIndex+1}.`:"image"===e.mode?`Replacement image: ${d(e.asset?.name||"uploaded image",240)}`:""}(e)}`;var n});return[o,...i].join("\n\n")}export function designPrompt({record:e,request:t,by:r,restore:n=!1,priorRecord:o=null}){const i="batch"===t.mode?t.edits:null,s=n?`Restore only the immediately previous Design Mode change by comparing the current artifact with this bridge-created verified backup: ${o?.backupPath||"(backup unavailable)"}. Do not overwrite unrelated changes.`:i?i.map((e,t)=>`Edit ${t+1} of ${i.length} · ${e.mode}\n${m(e)}\n\n${h(e)}`).join("\n\n---\n\n"):`${m(t)}\n\n${h(t)}`;return"preview"===e.sourceKind?`A room member used thinkpool Design Mode on a rendered application preview. This is an authorized source edit in your current workspace.\n\nRendered snapshot (read-only evidence): ${e.sourcePath}\nApplication workspace: ${e.workspaceRoot}\nPreview root: ${e.previewRoot}\nPreview route: ${e.route}\nExpected snapshot revision: ${e.revision}\nRequested by: ${d(r||"A room member",120)}\nOperation: ${i?`${i.length} queued edits in one batch`:t.mode}\n\n${s}\n\nRules:\n- Re-read the rendered snapshot and confirm the selected element still matches before editing.\n- Do not edit the snapshot. For an edit with bridge-verified application source, start at that exact relative file and range. For a selector/XPath fallback, locate the represented element in application source and do not call the match exact.\n- Before editing a bridge-verified target, confirm the current source still hashes to the supplied fileHash. If it does not, stop and request a fresh capture/reselection.\n- If fallback selector, text, or surrounding structure does not identify one application source location unambiguously, stop without editing and explain that the person must reselect it.\n${i?"- Apply the queued edits in order in one source pass, then build and capture once after the full batch.":"- Apply only the requested change, then build and capture once."}\n- Preserve the product page faithfully: keep its real copy, structure, fonts, assets, spacing, responsive behavior, and unrelated source unchanged.\n- Run the project build, then use preview_start with root ${e.previewRoot} and preview_capture with path ${e.route}, both viewports, and title ${JSON.stringify(e.title||e.slug||"Design preview")}. The bridge correlates that capture to this request.\n- Do not claim the edit is verified until the room receives the new desktop and mobile capture.\n- If the build or capture fails, report the failure instead of substituting a screenshot-only result.`:`A room member used thinkpool Design Mode on the artifact below. This is an authorized source edit in your current workspace.\n\nArtifact source: ${e.sourcePath}\nWorkspace: ${e.workspaceRoot}\nExpected source revision: ${e.revision}\nRequested by: ${d(r||"A room member",120)}\nOperation: ${n?"restore the immediately previous verified Design Mode revision":i?`${i.length} queued edits in one batch`:t.mode}\n\n${s}\n\nRules:\n- Re-read the exact artifact source and confirm its revision/content still matches every selected element before editing.\n${i?"- Apply the queued edits in order in one source pass, then render once after the full batch.":"- Apply only the requested change, then render the result once."}\n- Edit the canonical source above; do not search for or edit a different plausible file.\n- Preserve the represented product page faithfully: keep its real copy, structure, fonts, assets, spacing, and responsive behavior except for the selected edits. Never replace it with generic mockup content.\n- Preserve unrelated content and styling.\n- Use the existing mockup render workflow to produce both desktop and mobile captures after the edit. Run it with TP_DESIGN_REQUEST_ID=${t.cid} and TP_DESIGN_PARENT_REVISION=${e.revision} in the command environment so the proof correlates to this request.\n- Do not claim this is live until both captures succeed and the room receives the manifest.\n- If any target is stale or ambiguous, stop without editing and explain that the person must reselect it.`}
|
package/flow-assembly.mjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{readFileSync as t}from"node:fs";import e from"node:fs";import r from"node:path";import{execFileSync as i}from"node:child_process";export function inlineSingleHtml({htmlPath:e,root:i,readFile:n=t}){const o=i||r.dirname(e);let s=String(n(e,"utf8"));const c=t=>t&&!/^[a-z]+:\/\//i.test(t)&&!t.startsWith("//"),p=t=>{if(!t||r.isAbsolute(t))return null;const e=r.resolve(o,t.replace(/^\.\//,"")),i=r.relative(o,e);return""===i||i.startsWith("..")||r.isAbsolute(i)?""===i?e:null:e},u=[];if(s=s.replace(/<link\b[^>]*\brel=["']stylesheet["'][^>]*>/gi,t=>{const e=t.match(/\bhref=["']([^"']+)["']/i);if(!e||!c(e[1]))return t;const r=p(e[1]);return r?`<style>\n${String(n(r,"utf8"))}\n</style>`:t}),s=s.replace(/<script\b([^>]*)\bsrc=["']([^"']+)["']([^>]*)>\s*<\/script>/gi,(t,e,r,i)=>{if(!c(r))return t;const o=p(r);if(!o)return t;const s=/\btype=["']module["']/i.test(`${e} ${i}`),d=String(n(o,"utf8")),l=/(^|[\s;{(])(import|export)\s/.test(d);return s&&l?(u.push(`/* inlined: ${r} */\n${d}`),""):`<script>\n${d}\n<\/script>`}),u.length){const t=`<script type="module">\n${u.join("\n")}\n<\/script>`;/<\/body>/i.test(s)?s=s.replace(/<\/body>/i,`${t}\n</body>`):s+=`\n${t}\n`}return s}export function mergeWorktrees({dirs:t,outDir:i,fs:n=e}){const o=new Set([".git","node_modules",".claude"]),s=new Set,c=[],p=[];n.mkdirSync(i,{recursive:!0});const u=(t,e="")=>{const d=r.join(t,e);for(const l of n.readdirSync(d,{withFileTypes:!0})){if(o.has(l.name))continue;const d=e?r.join(e,l.name):l.name;if(l.isDirectory())u(t,d);else if(l.isFile()){const e=r.join(i,d);s.has(d)?p.push(d):(s.add(d),c.push(d)),n.mkdirSync(r.dirname(e),{recursive:!0}),n.copyFileSync(r.join(t,d),e)}}};for(const e of t)u(e);return{files:c,conflicts:p}}export function zipDir({dir:t}){try{return i("zip",["-q","-r","-","."],{cwd:t,maxBuffer:268435456})}catch(t){if(t&&"ENOENT"===t.code){const t=new Error("zipDir: system `zip` not available");throw t.code="NO_ZIP",t}throw t}}export function initRepo({dir:t,git:e=n}){e(["init","-q","-b","main"],t),e(["add","-A"],t);try{return e(["commit","-q","-m","Assembled by
|
|
1
|
+
import{readFileSync as t}from"node:fs";import e from"node:fs";import r from"node:path";import{execFileSync as i}from"node:child_process";export function inlineSingleHtml({htmlPath:e,root:i,readFile:n=t}){const o=i||r.dirname(e);let s=String(n(e,"utf8"));const c=t=>t&&!/^[a-z]+:\/\//i.test(t)&&!t.startsWith("//"),p=t=>{if(!t||r.isAbsolute(t))return null;const e=r.resolve(o,t.replace(/^\.\//,"")),i=r.relative(o,e);return""===i||i.startsWith("..")||r.isAbsolute(i)?""===i?e:null:e},u=[];if(s=s.replace(/<link\b[^>]*\brel=["']stylesheet["'][^>]*>/gi,t=>{const e=t.match(/\bhref=["']([^"']+)["']/i);if(!e||!c(e[1]))return t;const r=p(e[1]);return r?`<style>\n${String(n(r,"utf8"))}\n</style>`:t}),s=s.replace(/<script\b([^>]*)\bsrc=["']([^"']+)["']([^>]*)>\s*<\/script>/gi,(t,e,r,i)=>{if(!c(r))return t;const o=p(r);if(!o)return t;const s=/\btype=["']module["']/i.test(`${e} ${i}`),d=String(n(o,"utf8")),l=/(^|[\s;{(])(import|export)\s/.test(d);return s&&l?(u.push(`/* inlined: ${r} */\n${d}`),""):`<script>\n${d}\n<\/script>`}),u.length){const t=`<script type="module">\n${u.join("\n")}\n<\/script>`;/<\/body>/i.test(s)?s=s.replace(/<\/body>/i,`${t}\n</body>`):s+=`\n${t}\n`}return s}export function mergeWorktrees({dirs:t,outDir:i,fs:n=e}){const o=new Set([".git","node_modules",".claude"]),s=new Set,c=[],p=[];n.mkdirSync(i,{recursive:!0});const u=(t,e="")=>{const d=r.join(t,e);for(const l of n.readdirSync(d,{withFileTypes:!0})){if(o.has(l.name))continue;const d=e?r.join(e,l.name):l.name;if(l.isDirectory())u(t,d);else if(l.isFile()){const e=r.join(i,d);s.has(d)?p.push(d):(s.add(d),c.push(d)),n.mkdirSync(r.dirname(e),{recursive:!0}),n.copyFileSync(r.join(t,d),e)}}};for(const e of t)u(e);return{files:c,conflicts:p}}export function zipDir({dir:t}){try{return i("zip",["-q","-r","-","."],{cwd:t,maxBuffer:268435456})}catch(t){if(t&&"ENOENT"===t.code){const t=new Error("zipDir: system `zip` not available");throw t.code="NO_ZIP",t}throw t}}export function initRepo({dir:t,git:e=n}){e(["init","-q","-b","main"],t),e(["add","-A"],t);try{return e(["commit","-q","-m","Assembled by thinkpool Flow"],t),{committed:!0}}catch{return{committed:!1}}}export async function pushToGithub({dir:t,repoName:e,gh:r=o}){try{return r(["repo","create",e,"--private","--source=.","--push"],t),{pushed:!0}}catch(t){return{pushed:!1,reason:t&&t.message?t.message:String(t)}}}function n(t,e){return i("git",t,{cwd:e,stdio:["ignore","pipe","pipe"],encoding:"utf8"})}function o(t,e){return i("gh",t,{cwd:e,stdio:["ignore","pipe","pipe"],encoding:"utf8"})}
|
package/flow-conductor.mjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{FLOW_MODE as e,FLOW_STATUS as t,normalizePlanOutput as o}from"./flow-task-graph.mjs";import{loadDigest as n}from"./flow-context-store.mjs";import{manifest as s,loadBody as i}from"./flow-skill-registry.mjs";export{o as normalizePlanOutput};export const FLOW_CONDUCTOR_PROMPT=["THINKPOOL FLOW CONDUCTOR — HARD RULES (read first, no exceptions): You are a DECOMPOSITION-ONLY conductor. Your ONLY output action is using the Write tool ONCE to write your task-graph JSON to the file named exactly FLOW_PLAN.json. That single Write IS how you submit your plan — the room ingests it directly. Do NOT use ExitPlanMode and do NOT call submit_flow_plan (both hang here). Do NOT write any other file and do NOT write a plan-document markdown. You MUST NOT write or edit files, MUST NOT run bash/git, MUST NOT call AskUserQuestion, and MUST NOT build the project yourself — those are blocked and will just stall you. You do NOT build; you DECOMPOSE so the lanes build. A fully-specified build is STILL decomposed, never built here. When a detail is ambiguous or the user is unsure, choose the least-invasive reversible default, record that assumption in the summary, and continue. Never ask merely because the user is unsure; ask only when a choice would materially expand scope or authority. User steering always wins. Think, then call submit_flow_plan. That is the whole job.","DO NOT SPAWN SUBAGENTS. You MUST NOT use the Task/Agent tool or launch Explore / sub-agents — every subagent spawn pops a permission card and stalls the whole flow (this is the #1 way the conductor hangs asking for permission). You almost never need to look at code to decompose: decompose from the build request itself. If you genuinely must inspect something first, read it YOURSELF with Read/Grep/Glob (those are allowed without a prompt) — never delegate it to a spawned agent. Decompose, then Write FLOW_PLAN.json.","THINKPOOL FLOW — you are the CONDUCTOR of an ensemble build. Your job RIGHT NOW (Step 1) is to decompose the user's build request into a TASK-GRAPH: a set of RUNNABLE slices with explicit dependencies, then submit that plan for human approval. Do NOT start building and do NOT spawn lanes yet — that happens only after the user approves the plan (Step 2+).","DECOMPOSE INTO RUNNABLE SLICES. Each slice must be something ONE lane can build, RUN in its own browser-side WebContainer sandbox, observe, and self-correct INDEPENDENTLY. A slice that cannot run on its own is not a slice — split it or merge it until it is. The runtime (a lane running what it built) is the moat; slices that can't run break the whole model.","PARTITION BY FILE OWNERSHIP. Each slice MUST own a disjoint set of files/components — state the exact paths in `scope`. Two lanes editing the same file collide (a branch is NOT isolation when two lanes share a tree). If two slices want the same file, one depends on the other, or you split the file. State ownership precisely.","EXPLICIT ACYCLIC DEPENDENCIES. Every slice lists the keys of slices it depends on. Dependencies must form a DAG. A slice needing another's output (an API contract, a shared type, a layout component) depends on it. Minimize coupling — prefer independent slices that build in parallel.",'ACCEPTANCE CRITERIA per slice: how does the lane know it is DONE AND RUNNING? A lane is done when its slice runs in its WebContainer and meets the criteria — not when it has merely written code. State the observable, runnable proof (e.g. "GET /api/todos returns 200 + []", "submitting the form adds a row to the list that persists across reload").',"GATE-FIRST CONTRACT per builder/fix/scaffold slice: include an explicit bounded `nonGoals` list (what this slice will not change) and a `baseline` gate describing the real pre-edit condition that must FAIL or be ABSENT. The builder must observe that gate before editing and later supply a bounded evidence receipt; do not invent a RED sentence. A review task must depend on exactly one builder/fix task and inherits that target contract — it verifies the same acceptance, non-goals, and baseline evidence instead of inventing a second baseline.","RIGHT-SIZE THE SLICE COUNT. Bias to FEW slices that each carry real weight, not many tiny ones. For a small app: 2-3 slices. Concurrent lanes are bounded (a low dispatch ceiling), so a flat fan-out of well-chosen slices beats a sprawling one. Three focused slices outperform seven scattered ones.",'SUBMIT BY WRITING FLOW_PLAN.json — when the decomposition is ready, use the Write tool with file_path "FLOW_PLAN.json" and content set to your plan as ONE JSON object. The room validates the plan is a real DAG (acyclic, all deps resolve), persists it to the task-graph store, and shows a plan-approval card. A malformed plan is rejected with the reason as the tool result — fix it and Write FLOW_PLAN.json again. Do not also narrate the plan in prose. The exact shape:','{ "summary": "<one line: what we are building; assumptions: …>", "tasks": [ { "key": "<short-unique-kebab-key>", "title": "<imperative title>", "scope": "<files/components this lane owns>", "acceptance": "<runnable proof it is done>", "nonGoals": ["<bounded exclusion>"], "baseline": { "gate": "<what currently fails/is absent before edits>", "evidence": "<optional observed command/output receipt>" }, "deps": ["<other-task-key>", ...], "sliceType": "feature|scaffold|review|fix" } ] }',"`key` values are stable short kebab-case identifiers. `deps` references other tasks' keys. The content of FLOW_PLAN.json must be ONLY the JSON object (no surrounding prose, no markdown fences).","PLAN LIVES IN THE STORE, NOT THE CHAT. Your JSON plan is the artifact — it gets persisted to the task-graph (flow_sessions / flow_tasks) and rendered as an approval card. Do not ALSO narrate a long plan in prose. You may add 1-2 sentences framing the decomposition choice (why these slices, what was coupled), nothing more.","MODE-AWARE. The user picked a mode — steer (watch every lane live), guide (DEFAULT: approve this plan, then review at phase boundaries), or autopilot (full-auto with a budget cap + adaptive escalation that pulls them in only if a lane stalls). Produce the full plan now regardless; the mode changes how much the human intervenes later, not how you decompose."].join(" ");export const FLOW_CODEX_CONDUCTOR_PROMPT=["THINKPOOL FLOW CONDUCTOR — you are decomposition-only. Do not build, edit files, run commands, or spawn agents. Read only when needed to make the decomposition concrete.","Turn the request into a small acyclic graph of independently runnable slices with disjoint file ownership. Each builder/fix/scaffold task needs key, title, scope, observable acceptance, bounded nonGoals, and a baseline {gate,evidence?}; the gate states what currently fails or is absent before edits. Every review task depends on exactly ONE builder task and inherits/verifies that builder contract. Review is intelligence-sensitive and must never be assigned a cheap/mechanical tier.","When details are ambiguous or the user says they are unsure, choose a safe reversible default, record it in summary.assumptions text, and continue. Do not ask merely because of uncertainty; ask only when a choice materially expands scope or authority. Honor later user steering.","Submit the finished graph by calling the ThinkPool submit_flow_plan MCP tool with one JSON object encoded in its plan argument. Do not write FLOW_PLAN.json and do not use ExitPlanMode. The MCP submission is your only completion path; after it succeeds, stop and wait for the room."].join(" ");export function buildConductorEnv({flowSessionId:t,mode:o,budgetCapAutopilot:n=null}){return{TP_FLOW_SESSION:t,TP_FLOW_MODE:o,...o===e.autopilot&&null!=n?{TP_FLOW_BUDGET_CAP:String(n)}:{}}}export const PLAN_READY_STATUS=t.awaiting_approval;export const FLOW_LANE_PROMPT=["THINKPOOL FLOW — you are ONE LANE in an ensemble build. The conductor already decomposed the project into RUNNABLE slices; you own exactly ONE. Your task (scope + acceptance + deps) arrives as your first message.","OWN YOUR FILES ONLY. The `scope` names the files/components this lane OWNS — they are DISJOINT from every other lane. Edit ONLY those. Touching another lane's files is a collision that breaks the merge — never do it. If you genuinely need a file outside your scope, STOP and say so instead of editing it.","GATE BEFORE EDITS. First reproduce the assigned baseline gate in the real pre-edit state. Record only what you actually observed (a command/behavior + bounded receipt); never fabricate a RED sentence. Preserve the assigned non-goals. If a bounded detail is ambiguous, take the least-invasive reversible default and state it in your receipt; ask only if the choice would materially expand scope or authority.","DONE MEANS RUNS. You are not done when you've written code — you're done when your slice RUNS and meets the `acceptance` criteria (the observable, runnable proof). Verify it yourself before declaring done. State the proof (the command, the output) when you finish.",'COMMIT WHEN DONE, THEN SIGNAL. You are in your own git worktree on your own branch. When your slice meets acceptance — it RUNS and you have verified the proof — commit it atomically (one focused commit) with `git` via the Bash tool; that commit is the revert target if review later rejects it. THEN, as your VERY LAST action, signal completion by using the Write tool to write a file named exactly FLOW_DONE in your worktree root with content `{"baselineEvidence":"<real bounded pre-edit command/behavior receipt>"}`. That single Write IS the done signal — it records your commit + tells the room the slice is done (unblocking dependents). Do NOT use mark_flow_done (it hangs here). Do not push. Do NOT write FLOW_DONE before your slice actually runs, meets acceptance, records real baseline evidence, AND is committed.',"STAY IN YOUR LANE. Do NOT spawn further lanes (one hop only — the fork-bomb breaker). Do NOT edit other worktrees. If you're blocked on a dependency that isn't ready, say so + stop — another lane is building it.","BE RIGOROUS, NOT VIBES. Trace the data path, root-cause before fixing, verify before claiming done. A lane that ships a guess costs the whole ensemble."].join(" ");export const FLOW_CODEX_LANE_PROMPT=["THINKPOOL FLOW — you are one builder lane in an ensemble. The conductor gives you one runnable slice with disjoint scope. Edit only that scope and never touch another lane or worktree.","Before editing, reproduce the assigned baseline gate in the real pre-edit state. Preserve the explicit non-goals. Record only an actual bounded command/behavior receipt; never manufacture a RED claim. For a bounded ambiguity, take the least-invasive reversible default and state it in that receipt; ask only if a choice would materially expand scope or authority.","Done means the slice runs and its acceptance proof has been observed. Diagnose before changing code, verify with the real command or behavior, and make one focused git commit in your assigned worktree. Do not push.","After the commit and verification succeed, call the ThinkPool mark_flow_done MCP tool exactly once with the real `baselineEvidence` receipt. Do not write FLOW_DONE. The MCP call is the completion signal that records your commit and unblocks dependent slices.","Do not spawn more lanes. If the scope is unsafe or a dependency is missing, report the blocker instead of editing outside your ownership."].join(" ");export function buildLaneEnv({flowSessionId:e,taskKey:t,laneId:o}){return{TP_FLOW_SESSION:e,TP_FLOW_TASK_KEY:t,TP_FLOW_LANE_ID:o}}export function assembleCrossWaveContext(e,{baseDir:t,deps:o=null,fs:s}={}){if(!e||!t)return{text:"",digest:[]};let i;try{i=n(e,{baseDir:t,...s?{fs:s}:{}})}catch{return{text:"",digest:[]}}const a=Array.isArray(o)&&o.length?new Set(o):null,r=a?i.filter(e=>a.has(e.taskKey)):i;return r.length?{text:`CLOSED SLICES (bounded digest — summary + artifact pointers, not full output):\n${r.map(e=>{const t=(e.pointers||[]).map(e=>`${e.name}@${String(e.sha).slice(0,8)}`).join(", ");return` - ${e.summary}${t?` [artifacts: ${t}]`:""}`}).join("\n")}`,digest:i}:{text:"",digest:i}}export function flowSkillDirs(e=process.env){return{publicDir:e.TP_FLOW_SKILLS_DIR||void 0,customDir:e.TP_FLOW_SKILLS_CUSTOM_DIR||void 0}}export const MANIFEST_MAX_CHARS=4096;export const MANIFEST_FIELD_MAX=240;function a(e){const t="string"==typeof e?e:"";return t.length<=240?t:t.slice(0,228)+"…[truncated]"}export function renderSkillManifestBlock(e){if(!Array.isArray(e)||0===e.length)return"";const t=["AVAILABLE SKILLS (menu only — names + descriptions, NOT the instructions). You boot","knowing this menu; you do NOT carry any skill's full instructions. When a task matches","a skill (or you invoke it slash-style), the room loads that ONE skill's full body for","you on demand — never all of them, never up front. The menu:"].join("\n"),o=[];let n=t.length,s=0;for(const t of e){const e=t.whenToUse?` — use when: ${a(t.whenToUse)}`:"",i=` • ${t.name}: ${a(t.description)}${e}`;n+1+i.length>4096?s++:(o.push(i),n+=1+i.length)}return s>0&&o.push(` …[${s} more skill(s) omitted — manifest ceiling reached]`),[t,...o].join("\n")}export function buildLanePrompt({base:e=FLOW_LANE_PROMPT,publicDir:t,customDir:o,fs:n}={}){const i=void 0===t&&void 0===o?flowSkillDirs():{publicDir:t,customDir:o},a=renderSkillManifestBlock(s({...i,...n?{fs:n}:{}}));return a?`${e}\n\n${a}`:e}export function activateLaneSkill(e,{publicDir:t,customDir:o,fs:n}={}){const s=void 0===t&&void 0===o?flowSkillDirs():{publicDir:t,customDir:o};return i(e,{...s,...n?{fs:n}:{}})}
|
|
1
|
+
import{FLOW_MODE as e,FLOW_STATUS as t,normalizePlanOutput as o}from"./flow-task-graph.mjs";import{loadDigest as n}from"./flow-context-store.mjs";import{manifest as i,loadBody as s}from"./flow-skill-registry.mjs";export{o as normalizePlanOutput};export const FLOW_CONDUCTOR_PROMPT=["thinkpool FLOW CONDUCTOR — HARD RULES (read first, no exceptions): You are a DECOMPOSITION-ONLY conductor. Your ONLY output action is using the Write tool ONCE to write your task-graph JSON to the file named exactly FLOW_PLAN.json. That single Write IS how you submit your plan — the room ingests it directly. Do NOT use ExitPlanMode and do NOT call submit_flow_plan (both hang here). Do NOT write any other file and do NOT write a plan-document markdown. You MUST NOT write or edit files, MUST NOT run bash/git, MUST NOT call AskUserQuestion, and MUST NOT build the project yourself — those are blocked and will just stall you. You do NOT build; you DECOMPOSE so the lanes build. A fully-specified build is STILL decomposed, never built here. When a detail is ambiguous or the user is unsure, choose the least-invasive reversible default, record that assumption in the summary, and continue. Never ask merely because the user is unsure; ask only when a choice would materially expand scope or authority. User steering always wins. Think, then call submit_flow_plan. That is the whole job.","DO NOT SPAWN SUBAGENTS. You MUST NOT use the Task/Agent tool or launch Explore / sub-agents — every subagent spawn pops a permission card and stalls the whole flow (this is the #1 way the conductor hangs asking for permission). You almost never need to look at code to decompose: decompose from the build request itself. If you genuinely must inspect something first, read it YOURSELF with Read/Grep/Glob (those are allowed without a prompt) — never delegate it to a spawned agent. Decompose, then Write FLOW_PLAN.json.","thinkpool FLOW — you are the CONDUCTOR of an ensemble build. Your job RIGHT NOW (Step 1) is to decompose the user's build request into a TASK-GRAPH: a set of RUNNABLE slices with explicit dependencies, then submit that plan for human approval. Do NOT start building and do NOT spawn lanes yet — that happens only after the user approves the plan (Step 2+).","DECOMPOSE INTO RUNNABLE SLICES. Each slice must be something ONE lane can build, RUN in its own browser-side WebContainer sandbox, observe, and self-correct INDEPENDENTLY. A slice that cannot run on its own is not a slice — split it or merge it until it is. The runtime (a lane running what it built) is the moat; slices that can't run break the whole model.","PARTITION BY FILE OWNERSHIP. Each slice MUST own a disjoint set of files/components — state the exact paths in `scope`. Two lanes editing the same file collide (a branch is NOT isolation when two lanes share a tree). If two slices want the same file, one depends on the other, or you split the file. State ownership precisely.","EXPLICIT ACYCLIC DEPENDENCIES. Every slice lists the keys of slices it depends on. Dependencies must form a DAG. A slice needing another's output (an API contract, a shared type, a layout component) depends on it. Minimize coupling — prefer independent slices that build in parallel.",'ACCEPTANCE CRITERIA per slice: how does the lane know it is DONE AND RUNNING? A lane is done when its slice runs in its WebContainer and meets the criteria — not when it has merely written code. State the observable, runnable proof (e.g. "GET /api/todos returns 200 + []", "submitting the form adds a row to the list that persists across reload").',"GATE-FIRST CONTRACT per builder/fix/scaffold slice: include an explicit bounded `nonGoals` list (what this slice will not change) and a `baseline` gate describing the real pre-edit condition that must FAIL or be ABSENT. The builder must observe that gate before editing and later supply a bounded evidence receipt; do not invent a RED sentence. A review task must depend on exactly one builder/fix task and inherits that target contract — it verifies the same acceptance, non-goals, and baseline evidence instead of inventing a second baseline.","RIGHT-SIZE THE SLICE COUNT. Bias to FEW slices that each carry real weight, not many tiny ones. For a small app: 2-3 slices. Concurrent lanes are bounded (a low dispatch ceiling), so a flat fan-out of well-chosen slices beats a sprawling one. Three focused slices outperform seven scattered ones.",'SUBMIT BY WRITING FLOW_PLAN.json — when the decomposition is ready, use the Write tool with file_path "FLOW_PLAN.json" and content set to your plan as ONE JSON object. The room validates the plan is a real DAG (acyclic, all deps resolve), persists it to the task-graph store, and shows a plan-approval card. A malformed plan is rejected with the reason as the tool result — fix it and Write FLOW_PLAN.json again. Do not also narrate the plan in prose. The exact shape:','{ "summary": "<one line: what we are building; assumptions: …>", "tasks": [ { "key": "<short-unique-kebab-key>", "title": "<imperative title>", "scope": "<files/components this lane owns>", "acceptance": "<runnable proof it is done>", "nonGoals": ["<bounded exclusion>"], "baseline": { "gate": "<what currently fails/is absent before edits>", "evidence": "<optional observed command/output receipt>" }, "deps": ["<other-task-key>", ...], "sliceType": "feature|scaffold|review|fix" } ] }',"`key` values are stable short kebab-case identifiers. `deps` references other tasks' keys. The content of FLOW_PLAN.json must be ONLY the JSON object (no surrounding prose, no markdown fences).","PLAN LIVES IN THE STORE, NOT THE CHAT. Your JSON plan is the artifact — it gets persisted to the task-graph (flow_sessions / flow_tasks) and rendered as an approval card. Do not ALSO narrate a long plan in prose. You may add 1-2 sentences framing the decomposition choice (why these slices, what was coupled), nothing more.","MODE-AWARE. The user picked a mode — steer (watch every lane live), guide (DEFAULT: approve this plan, then review at phase boundaries), or autopilot (full-auto with a budget cap + adaptive escalation that pulls them in only if a lane stalls). Produce the full plan now regardless; the mode changes how much the human intervenes later, not how you decompose."].join(" ");export const FLOW_CODEX_CONDUCTOR_PROMPT=["thinkpool FLOW CONDUCTOR — you are decomposition-only. Do not build, edit files, run commands, or spawn agents. Read only when needed to make the decomposition concrete.","Turn the request into a small acyclic graph of independently runnable slices with disjoint file ownership. Each builder/fix/scaffold task needs key, title, scope, observable acceptance, bounded nonGoals, and a baseline {gate,evidence?}; the gate states what currently fails or is absent before edits. Every review task depends on exactly ONE builder task and inherits/verifies that builder contract. Review is intelligence-sensitive and must never be assigned a cheap/mechanical tier.","When details are ambiguous or the user says they are unsure, choose a safe reversible default, record it in summary.assumptions text, and continue. Do not ask merely because of uncertainty; ask only when a choice materially expands scope or authority. Honor later user steering.","Submit the finished graph by calling the thinkpool submit_flow_plan MCP tool with one JSON object encoded in its plan argument. Do not write FLOW_PLAN.json and do not use ExitPlanMode. The MCP submission is your only completion path; after it succeeds, stop and wait for the room."].join(" ");export function buildConductorEnv({flowSessionId:t,mode:o,budgetCapAutopilot:n=null}){return{TP_FLOW_SESSION:t,TP_FLOW_MODE:o,...o===e.autopilot&&null!=n?{TP_FLOW_BUDGET_CAP:String(n)}:{}}}export const PLAN_READY_STATUS=t.awaiting_approval;export const FLOW_LANE_PROMPT=["thinkpool FLOW — you are ONE LANE in an ensemble build. The conductor already decomposed the project into RUNNABLE slices; you own exactly ONE. Your task (scope + acceptance + deps) arrives as your first message.","OWN YOUR FILES ONLY. The `scope` names the files/components this lane OWNS — they are DISJOINT from every other lane. Edit ONLY those. Touching another lane's files is a collision that breaks the merge — never do it. If you genuinely need a file outside your scope, STOP and say so instead of editing it.","GATE BEFORE EDITS. First reproduce the assigned baseline gate in the real pre-edit state. Record only what you actually observed (a command/behavior + bounded receipt); never fabricate a RED sentence. Preserve the assigned non-goals. If a bounded detail is ambiguous, take the least-invasive reversible default and state it in your receipt; ask only if the choice would materially expand scope or authority.","DONE MEANS RUNS. You are not done when you've written code — you're done when your slice RUNS and meets the `acceptance` criteria (the observable, runnable proof). Verify it yourself before declaring done. State the proof (the command, the output) when you finish.",'COMMIT WHEN DONE, THEN SIGNAL. You are in your own git worktree on your own branch. When your slice meets acceptance — it RUNS and you have verified the proof — commit it atomically (one focused commit) with `git` via the Bash tool; that commit is the revert target if review later rejects it. THEN, as your VERY LAST action, signal completion by using the Write tool to write a file named exactly FLOW_DONE in your worktree root with content `{"baselineEvidence":"<real bounded pre-edit command/behavior receipt>"}`. That single Write IS the done signal — it records your commit + tells the room the slice is done (unblocking dependents). Do NOT use mark_flow_done (it hangs here). Do not push. Do NOT write FLOW_DONE before your slice actually runs, meets acceptance, records real baseline evidence, AND is committed.',"STAY IN YOUR LANE. Do NOT spawn further lanes (one hop only — the fork-bomb breaker). Do NOT edit other worktrees. If you're blocked on a dependency that isn't ready, say so + stop — another lane is building it.","BE RIGOROUS, NOT VIBES. Trace the data path, root-cause before fixing, verify before claiming done. A lane that ships a guess costs the whole ensemble."].join(" ");export const FLOW_CODEX_LANE_PROMPT=["thinkpool FLOW — you are one builder lane in an ensemble. The conductor gives you one runnable slice with disjoint scope. Edit only that scope and never touch another lane or worktree.","Before editing, reproduce the assigned baseline gate in the real pre-edit state. Preserve the explicit non-goals. Record only an actual bounded command/behavior receipt; never manufacture a RED claim. For a bounded ambiguity, take the least-invasive reversible default and state it in that receipt; ask only if a choice would materially expand scope or authority.","Done means the slice runs and its acceptance proof has been observed. Diagnose before changing code, verify with the real command or behavior, and make one focused git commit in your assigned worktree. Do not push.","After the commit and verification succeed, call the thinkpool mark_flow_done MCP tool exactly once with the real `baselineEvidence` receipt. Do not write FLOW_DONE. The MCP call is the completion signal that records your commit and unblocks dependent slices.","Do not spawn more lanes. If the scope is unsafe or a dependency is missing, report the blocker instead of editing outside your ownership."].join(" ");export function buildLaneEnv({flowSessionId:e,taskKey:t,laneId:o}){return{TP_FLOW_SESSION:e,TP_FLOW_TASK_KEY:t,TP_FLOW_LANE_ID:o}}export function assembleCrossWaveContext(e,{baseDir:t,deps:o=null,fs:i}={}){if(!e||!t)return{text:"",digest:[]};let s;try{s=n(e,{baseDir:t,...i?{fs:i}:{}})}catch{return{text:"",digest:[]}}const a=Array.isArray(o)&&o.length?new Set(o):null,r=a?s.filter(e=>a.has(e.taskKey)):s;return r.length?{text:`CLOSED SLICES (bounded digest — summary + artifact pointers, not full output):\n${r.map(e=>{const t=(e.pointers||[]).map(e=>`${e.name}@${String(e.sha).slice(0,8)}`).join(", ");return` - ${e.summary}${t?` [artifacts: ${t}]`:""}`}).join("\n")}`,digest:s}:{text:"",digest:s}}export function flowSkillDirs(e=process.env){return{publicDir:e.TP_FLOW_SKILLS_DIR||void 0,customDir:e.TP_FLOW_SKILLS_CUSTOM_DIR||void 0}}export const MANIFEST_MAX_CHARS=4096;export const MANIFEST_FIELD_MAX=240;function a(e){const t="string"==typeof e?e:"";return t.length<=240?t:t.slice(0,228)+"…[truncated]"}export function renderSkillManifestBlock(e){if(!Array.isArray(e)||0===e.length)return"";const t=["AVAILABLE SKILLS (menu only — names + descriptions, NOT the instructions). You boot","knowing this menu; you do NOT carry any skill's full instructions. When a task matches","a skill (or you invoke it slash-style), the room loads that ONE skill's full body for","you on demand — never all of them, never up front. The menu:"].join("\n"),o=[];let n=t.length,i=0;for(const t of e){const e=t.whenToUse?` — use when: ${a(t.whenToUse)}`:"",s=` • ${t.name}: ${a(t.description)}${e}`;n+1+s.length>4096?i++:(o.push(s),n+=1+s.length)}return i>0&&o.push(` …[${i} more skill(s) omitted — manifest ceiling reached]`),[t,...o].join("\n")}export function buildLanePrompt({base:e=FLOW_LANE_PROMPT,publicDir:t,customDir:o,fs:n}={}){const s=void 0===t&&void 0===o?flowSkillDirs():{publicDir:t,customDir:o},a=renderSkillManifestBlock(i({...s,...n?{fs:n}:{}}));return a?`${e}\n\n${a}`:e}export function activateLaneSkill(e,{publicDir:t,customDir:o,fs:n}={}){const i=void 0===t&&void 0===o?flowSkillDirs():{publicDir:t,customDir:o};return s(e,{...i,...n?{fs:n}:{}})}
|
package/flow-models.mjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
const e={scaffold:"sonnet",feature:"opus",fix:"opus",review:"opus"},o=["gpt-5.6-luna","gpt-5.4-mini","gpt-5.3-codex-spark"],t=["gpt-5.6-terra","gpt-5.4"],n=["nous:anthropic/claude-sonnet-4.6","nous:anthropic/claude-sonnet-4.5","nous:anthropic/claude-sonnet-4"],r=Object.freeze({fable:"nous:anthropic/claude-fable-5"});export function normalizeFlowRuntime(e,o=null){return"claude"===e||"codex"===e||"hermes"===e?e:"claude"===o||"codex"===o||"hermes"===o?o:null}export function modelCatalogValues(e=[]){return new Set((Array.isArray(e)?e:[]).map(e=>"string"==typeof e?e:e?.value||e?.slug).filter(Boolean))}const a=e=>/^nous:/i.test(e)&&(e=>/^[A-Za-z0-9._-]+:[A-Za-z0-9._-]+\/[A-Za-z0-9._:/-]+$/.test(e))(e);export function resolveHermesOpenModel(e,o=[],{strictCatalog:t=!1}={}){const n=String(e||"").trim();if(!n)return{ok:!0,model:void 0,validatedBy:"default"};const i=[...modelCatalogValues(o)];if(i.includes(n)&&a(n))return{ok:!0,model:n,validatedBy:"catalog"};const l=(e=>{const o=r[e.toLowerCase()];if(o)return o;if(/^glm[-_.]/i.test(e))return`nous:z-ai/${e.toLowerCase()}`;const t=e.match(/^([^:/]+):([^/]+)$/);return t?`nous:${"zai"===t[1].toLowerCase()?"z-ai":t[1].toLowerCase()}/${t[2]}`:e})(n),s=e=>String(e).toLowerCase().replace(/[^a-z0-9]+/g,""),u=s(l).replace(/^nous/,""),c=i.filter(e=>s(e).replace(/^nous/,"")===u);return 1===c.length&&a(c[0])?{ok:!0,model:c[0],validatedBy:"catalog"}:t&&i.length?{ok:!1,error:`Could not open a Hermes terminal on ${JSON.stringify(n)} — that model is not in this session's ACP catalog.`}:a(l)?{ok:!0,model:l,validatedBy:"child-runtime"}:{ok:!1,error:`Could not resolve Hermes model ${JSON.stringify(n)}.
|
|
1
|
+
const e={scaffold:"sonnet",feature:"opus",fix:"opus",review:"opus"},o=["gpt-5.6-luna","gpt-5.4-mini","gpt-5.3-codex-spark"],t=["gpt-5.6-terra","gpt-5.4"],n=["nous:anthropic/claude-sonnet-4.6","nous:anthropic/claude-sonnet-4.5","nous:anthropic/claude-sonnet-4"],r=Object.freeze({fable:"nous:anthropic/claude-fable-5"});export function normalizeFlowRuntime(e,o=null){return"claude"===e||"codex"===e||"hermes"===e?e:"claude"===o||"codex"===o||"hermes"===o?o:null}export function modelCatalogValues(e=[]){return new Set((Array.isArray(e)?e:[]).map(e=>"string"==typeof e?e:e?.value||e?.slug).filter(Boolean))}const a=e=>/^nous:/i.test(e)&&(e=>/^[A-Za-z0-9._-]+:[A-Za-z0-9._-]+\/[A-Za-z0-9._:/-]+$/.test(e))(e);export function resolveHermesOpenModel(e,o=[],{strictCatalog:t=!1}={}){const n=String(e||"").trim();if(!n)return{ok:!0,model:void 0,validatedBy:"default"};const i=[...modelCatalogValues(o)];if(i.includes(n)&&a(n))return{ok:!0,model:n,validatedBy:"catalog"};const l=(e=>{const o=r[e.toLowerCase()];if(o)return o;if(/^glm[-_.]/i.test(e))return`nous:z-ai/${e.toLowerCase()}`;const t=e.match(/^([^:/]+):([^/]+)$/);return t?`nous:${"zai"===t[1].toLowerCase()?"z-ai":t[1].toLowerCase()}/${t[2]}`:e})(n),s=e=>String(e).toLowerCase().replace(/[^a-z0-9]+/g,""),u=s(l).replace(/^nous/,""),c=i.filter(e=>s(e).replace(/^nous/,"")===u);return 1===c.length&&a(c[0])?{ok:!0,model:c[0],validatedBy:"catalog"}:t&&i.length?{ok:!1,error:`Could not open a Hermes terminal on ${JSON.stringify(n)} — that model is not in this session's ACP catalog.`}:a(l)?{ok:!0,model:l,validatedBy:"child-runtime"}:{ok:!1,error:`Could not resolve Hermes model ${JSON.stringify(n)}. thinkpool Hermes only routes through Nous Portal; use a full ACP model ID such as "nous:z-ai/glm-5.2".`}}function i(e,t){const n=modelCatalogValues(t);for(const o of e)if(n.has(o))return o;if(e===o)for(const e of n)if(/spark/i.test(e))return e}function l(e,o){const t=modelCatalogValues(e);for(const e of o)for(const o of t)if(e.test(o))return o}export function flowLaneModelFor({sliceType:r,runtime:a="claude",catalog:s=[],env:u=process.env}={}){if("codex"===normalizeFlowRuntime(a,"claude")){const e=u.TP_FLOW_CODEX_LANE_MODEL;if("inherit"===e)return;return e?modelCatalogValues(s).has(e)?e:void 0:i("scaffold"===r?o:t,s)}if("hermes"===normalizeFlowRuntime(a,"claude")){const e=u.TP_FLOW_HERMES_LANE_MODEL,o=modelCatalogValues(s);if("inherit"===e)return;return e?o.has(e)?e:void 0:"review"===r?i(n,s)||l(s,[/terra/i,/sonnet/i,/balanced/i]):l(s,"scaffold"===r?[/(?:luna|mini)/i]:[/terra/i,/(?:luna|mini)/i])}const c=u.TP_FLOW_CLAUDE_LANE_MODEL||u.TP_FLOW_LANE_MODEL;if("inherit"!==c)return c||e[r]||"opus"}export function flowConductorModelFor({runtime:e="claude",originModel:o,catalog:t=[],env:n=process.env}={}){if("codex"===normalizeFlowRuntime(e,"claude")){const e=n.TP_FLOW_CODEX_CONDUCTOR_MODEL;if("inherit"===e)return;const r=modelCatalogValues(t);return e?r.has(e)?e:void 0:o&&r.has(o)?o:void 0}if("hermes"===normalizeFlowRuntime(e,"claude")){const e=n.TP_FLOW_HERMES_CONDUCTOR_MODEL,r=modelCatalogValues(t);if("inherit"===e)return;return e?r.has(e)?e:void 0:o&&r.has(o)?o:void 0}const r=n.TP_FLOW_CLAUDE_CONDUCTOR_MODEL||n.TP_FLOW_CONDUCTOR_MODEL;if("inherit"!==r)return r||void 0}export function spawnedLaneModelFor({explicitModel:e,sliceType:o,runtime:t="claude",catalog:n=[],env:r=process.env}={}){return e||flowLaneModelFor({sliceType:o,runtime:t,catalog:n,env:r})}export function resolveCodexModel(e,o=[]){if(e)return modelCatalogValues(o).has(e)?e:void 0}export function assertRuntimeModelCompatible({runtime:e="claude",provider:o=null,model:t}={}){if("claude"===e&&(!o||"anthropic"===o)&&/^gpt-/i.test(String(t||"")))throw new Error(`Codex model ${JSON.stringify(t)} cannot run on the built-in Claude provider`);return!0}
|
package/flow-review.mjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{execFileSync as e}from"node:child_process";import{TASK_STATUS as t}from"./flow-task-graph.mjs";import{removeFlowWorktree as o}from"./flow-worktree.mjs";import{reviewReflectionDecision as r,REVIEW_DEFAULTS as n}from"./flow-review-reflect.mjs";export const FLOW_REVIEWER_PROMPT=["
|
|
1
|
+
import{execFileSync as e}from"node:child_process";import{TASK_STATUS as t}from"./flow-task-graph.mjs";import{removeFlowWorktree as o}from"./flow-worktree.mjs";import{reviewReflectionDecision as r,REVIEW_DEFAULTS as n}from"./flow-review-reflect.mjs";export const FLOW_REVIEWER_PROMPT=["thinkpool FLOW — you are an ADVERSARIAL REVIEWER in an ensemble build. You did NOT build this slice. Another lane did, and claims it is done. Your job is to TRY TO BREAK IT — independently verify whether its acceptance criteria are ACTUALLY met. The slice you are reviewing (its scope, acceptance criteria, inherited non-goals, baseline gate/evidence, and worktree/branch) arrives as your first message.",'RUN IT, DON\'T TRUST IT. Check out the lane\'s worktree/branch and RUN the slice yourself. "It compiles" is not "it works". Reproduce the acceptance proof with your own hands — run the command, hit the endpoint, submit the form, reload the page. If you cannot independently reproduce the acceptance criteria, that is a REJECT.',"VERIFY THE INHERITED CONTRACT. Check the builder's baseline receipt against the real pre-edit claim where feasible; it must be an observed command/behavior, not a fabricated sentence. Check the implementation did not cross its inherited non-goals. Do not invent another baseline. HUNT FOR FAILURE: edge cases, empty/malformed input, the second click, the reload, concurrent use, and the error path. A green happy-path is the floor, not the bar.",'DEFAULT TO REJECT. If you cannot independently verify EVERY acceptance criterion — reject. Ambiguity, "probably fine", "looks right", an untestable claim, a flaky run — all REJECT. The cost of a false pass (a broken slice merged into the assembly) is far higher than a re-run.','BE SPECIFIC. Your reasons must name exactly WHAT failed and HOW you found it — the command you ran, the output you got, the acceptance criterion it violated. "Doesn\'t work" is useless. "GET /api/todos returned 500 with `column todos.user_id does not exist`; acceptance required 200 + []" is a usable verdict.',"YOU GET BOUNDED HUNT ROUNDS, NOT ONE GLANCE. You may be asked to dig again — up to a hard ceiling of rounds. A green happy-path on round 1 is NOT a pass; use the next round to hunt harder (edge cases, the reload, the second click, concurrent use, the error path). You only stop early two ways: you reproduce a concrete FAILURE (emit pass:false — that ends it, the slice reverts), OR you have genuinely EXHAUSTED your checks and found nothing (emit pass:true AND exhausted:true — that ends it, the slice passes). If you still have angles left to try, emit pass:true and leave exhausted false/absent — you will be asked to dig one more round until the ceiling, at which point the inconclusive result is handed to the two humans to decide (never an unbounded self-loop).",'EMIT YOUR VERDICT BY WRITING FLOW_REVIEW.json. Do NOT call mark_flow_done or ExitPlanMode (they hang here). As your LAST action, use the Write tool with file_path "FLOW_REVIEW.json" and content = a single JSON object { "pass": <boolean>, "reasons": ["<specific finding>", ...], "taskKey": "<the slice you reviewed>", "exhausted": <boolean, optional — true only when you have nothing left to check> }. `pass: false` reverts the reviewed slice so it rebuilds. The content of FLOW_REVIEW.json must be ONLY that JSON object (no prose, no markdown fences). Do not edit the slice — you review, you do not fix.'].join(" ");export const FLOW_CODEX_REVIEWER_PROMPT=["thinkpool FLOW REVIEW — you are a non-mutating adversarial reviewer. Your native sandbox lets you write only inside your OWN disposable reviewer worktree. The reviewed builder worktree is outside that writable root: never edit, commit, revert, install into, or repair it.","For any install/build/test that produces files, copy the reviewed source into a scratch directory under your own current worktree first, then run the write-producing command against that scratch copy. Read-only inspection may target the reviewed worktree directly.","Independently reproduce every acceptance criterion, verify the inherited baseline evidence and non-goals without inventing a second baseline, and hunt edge cases, reloads, repeated actions, concurrency, malformed input, and error paths. A green happy path is only the floor. If a claim cannot be reproduced, reject it with the exact command/output.",'Submit each review round by calling the thinkpool submit_flow_review MCP tool with a JSON verdict: {"pass":boolean,"reasons":["specific evidence"],"taskKey":"allowed reviewed task","exhausted":boolean}. Do not write FLOW_REVIEW.json and do not call mark_flow_done. A pass without exhausted=true may trigger another bounded hunt round; a failure triggers the room revert path.'].join(" ");export function parseReviewVerdict(e){let t=e;if("string"==typeof e){let o=e.trim();const r=o.match(/```(?:json)?\s*([\s\S]*?)```/i);r&&(o=r[1].trim()),t=JSON.parse(o)}if(!t||"object"!=typeof t)throw new Error("review verdict is not an object");if("boolean"!=typeof t.pass)throw new Error("review verdict missing boolean `pass`");const o=Array.isArray(t.reasons)?t.reasons.map(String):[],r=!0===t.exhausted;return{pass:t.pass,reasons:o,exhausted:r}}export function reviewVerdictToReflection(e){const t=e||{};return{foundFailure:!1===t.pass,exhausted:!0===t.pass&&!0===t.exhausted}}export async function runBoundedReview({maxRounds:e,budgetCap:t=null,getSpent:o=()=>0,runRound:a}){if("function"!=typeof a)throw new Error("runBoundedReview needs a runRound(round) function");const i=Number.isFinite(e)&&e>0?e:n.maxRounds,s=null!=t&&Number.isFinite(t);for(let e=1;;e++){const n=o();if(s&&n>=t)return{...r({round:e,maxRounds:i,spentTokens:n,budgetCap:t,foundFailure:!1,exhausted:!1}),rounds:e-1,verdict:null};const c=await a(e),d=r({round:e,maxRounds:i,spentTokens:o(),budgetCap:t,...reviewVerdictToReflection(c)});if(d.stop)return{...d,rounds:e,verdict:c}}}export function reviewDecision({verdicts:e}){const t=Array.isArray(e)?e:[];if(0===t.length)return{accept:!1,mustRevert:!0,reasons:["no review verdict produced"]};const o=t.every(e=>!0===e.pass);return{accept:o,mustRevert:!o,reasons:t.filter(e=>!e.pass).flatMap(e=>Array.isArray(e.reasons)?e.reasons:[])}}export function revertLane({flowId:e,taskKey:t,root:r,git:n=a,removeWorktree:i=o}){const{branch:s}=function({flowId:e,taskKey:t,root:o}){return{branch:`flow/${String(e).replace(/-/g,"").slice(0,8)}/${String(t).toLowerCase().replace(/[^a-z0-9-]/g,"-").slice(0,40).replace(/^-+|-+$/g,"")||"task"}`}}({flowId:e,taskKey:t,root:r});try{i({flowId:e,taskKey:t,root:r,git:n})}catch{}return{reverted:!0,branch:s}}export function nextTaskStatusAfterReview({accept:e}){return e?t.done:t.reverted}function a(t,o){return e("git",t,{cwd:o,stdio:["ignore","pipe","pipe"],encoding:"utf8"})}
|
package/hermes-acp-bootstrap.py
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
|
-
"""Process-local
|
|
2
|
+
"""Process-local thinkpool Hermes ACP tool policy.
|
|
3
3
|
|
|
4
4
|
Never import this through the user profile. The bridge starts this file with
|
|
5
5
|
Hermes' installed venv interpreter and passes a validated role policy in env.
|
|
@@ -26,7 +26,7 @@ ESSENTIAL_CODING_TOOLS = frozenset({
|
|
|
26
26
|
})
|
|
27
27
|
|
|
28
28
|
def die(message):
|
|
29
|
-
print("
|
|
29
|
+
print("thinkpool Hermes ACP policy error: " + message, file=sys.stderr)
|
|
30
30
|
raise SystemExit(78)
|
|
31
31
|
|
|
32
32
|
def policy():
|
|
@@ -82,7 +82,7 @@ def assert_exact_inventory(agent):
|
|
|
82
82
|
"""Reject an ACP lifecycle that lost the bridge-owned MCP surface.
|
|
83
83
|
|
|
84
84
|
Hermes treats registration errors as non-fatal. That is acceptable for a
|
|
85
|
-
standalone CLI, but never for a
|
|
85
|
+
standalone CLI, but never for a thinkpool role: the bridge must not let a
|
|
86
86
|
reconstructed agent accept a prompt with a partial policy.
|
|
87
87
|
"""
|
|
88
88
|
tools = list(getattr(agent, "tools", []) or [])
|
|
@@ -97,7 +97,7 @@ def assert_exact_inventory(agent):
|
|
|
97
97
|
if missing_builtin: details.append("missing builtins " + ", ".join(sorted(missing_builtin)))
|
|
98
98
|
if missing_mcp: details.append("missing MCP " + ", ".join(sorted(missing_mcp)))
|
|
99
99
|
if extras: details.append("forbidden extras " + ", ".join(sorted(extras)))
|
|
100
|
-
raise RuntimeError("
|
|
100
|
+
raise RuntimeError("thinkpool exact inventory is incomplete: " + "; ".join(details))
|
|
101
101
|
agent.tools = tools
|
|
102
102
|
agent.valid_tool_names = names
|
|
103
103
|
|
|
@@ -105,12 +105,12 @@ def assert_exact_inventory(agent):
|
|
|
105
105
|
# this exact policy; no mutable shell alias or profile config participates.
|
|
106
106
|
import toolsets
|
|
107
107
|
toolsets.TOOLSETS["hermes-acp"] = {
|
|
108
|
-
"description": "
|
|
108
|
+
"description": "thinkpool process-local ACP policy",
|
|
109
109
|
"tools": list(BUILTIN), "includes": []
|
|
110
110
|
}
|
|
111
111
|
|
|
112
112
|
# entry.main() normally discovers profile-configured MCP servers before it
|
|
113
|
-
# creates the ACP server.
|
|
113
|
+
# creates the ACP server. thinkpool ACP never inherits those profile servers;
|
|
114
114
|
# the only permitted registration is the bridge's per-session `thinkpool` one.
|
|
115
115
|
import tools.mcp_tool
|
|
116
116
|
tools.mcp_tool.discover_mcp_tools = lambda *args, **kwargs: []
|
|
@@ -136,34 +136,34 @@ _expand = acp_adapter.session._expand_acp_enabled_toolsets
|
|
|
136
136
|
def constrained_expand(toolsets_arg=None, mcp_server_names=None):
|
|
137
137
|
requested = list(toolsets_arg or ["hermes-acp"])
|
|
138
138
|
if any(name not in {"hermes-acp", "mcp-thinkpool"} for name in requested):
|
|
139
|
-
raise RuntimeError("
|
|
139
|
+
raise RuntimeError("thinkpool ACP only permits hermes-acp and mcp-thinkpool toolsets")
|
|
140
140
|
names = list(mcp_server_names or [])
|
|
141
141
|
if any(name != "thinkpool" for name in names):
|
|
142
|
-
raise RuntimeError("
|
|
142
|
+
raise RuntimeError("thinkpool ACP only permits dynamic MCP server thinkpool")
|
|
143
143
|
# Hermes 0.18.2 calls this from /tools with an already-expanded
|
|
144
144
|
# ["hermes-acp", "mcp-thinkpool"] list and no mcp_server_names. Preserve
|
|
145
|
-
# that exact legal expansion; otherwise /tools silently omits
|
|
145
|
+
# that exact legal expansion; otherwise /tools silently omits thinkpool.
|
|
146
146
|
return ["hermes-acp"] + (["mcp-thinkpool"] if names or "mcp-thinkpool" in requested else [])
|
|
147
147
|
acp_adapter.session._expand_acp_enabled_toolsets = constrained_expand
|
|
148
148
|
|
|
149
149
|
import acp_adapter.server
|
|
150
150
|
|
|
151
|
-
#
|
|
151
|
+
# thinkpool Hermes is a Nous Portal runtime, never a generic provider shell.
|
|
152
152
|
# The profile probe owns the first boundary; this process-local guard is the
|
|
153
153
|
# final defense against profile drift or a future ACP reconstruction bypass.
|
|
154
154
|
_build_model_state = acp_adapter.server.HermesACPAgent._build_model_state
|
|
155
155
|
def nous_only_model_state(self, state):
|
|
156
156
|
provider = str(getattr(getattr(state, "agent", None), "provider", "") or "").strip().lower()
|
|
157
157
|
if provider != "nous":
|
|
158
|
-
raise RuntimeError("
|
|
158
|
+
raise RuntimeError("thinkpool Hermes requires the Nous Portal provider")
|
|
159
159
|
result = _build_model_state(self, state)
|
|
160
160
|
for item in list(getattr(result, "available_models", None) or []):
|
|
161
161
|
if not str(getattr(item, "model_id", "") or "").lower().startswith("nous:"):
|
|
162
|
-
raise RuntimeError("
|
|
162
|
+
raise RuntimeError("thinkpool Hermes received a non-Nous ACP model")
|
|
163
163
|
return result
|
|
164
164
|
acp_adapter.server.HermesACPAgent._build_model_state = nous_only_model_state
|
|
165
165
|
|
|
166
|
-
#
|
|
166
|
+
# thinkpool-only commands live in this process patch rather than in Hermes'
|
|
167
167
|
# profile. They are intentionally narrow: no identity, shared configuration,
|
|
168
168
|
# account tokens, or lifecycle/admin controls enter the room surface.
|
|
169
169
|
_TP_REASONING_CONFIG_ID = "thinkpool_reasoning_effort"
|
|
@@ -416,7 +416,7 @@ acp_adapter.server.make_tool_progress_cb = completion_aware_tool_progress_factor
|
|
|
416
416
|
_register = acp_adapter.server.HermesACPAgent._register_session_mcp_servers
|
|
417
417
|
async def constrained_register(self, state, mcp_servers):
|
|
418
418
|
if any(getattr(server, "name", None) != "thinkpool" for server in (mcp_servers or [])):
|
|
419
|
-
raise RuntimeError("
|
|
419
|
+
raise RuntimeError("thinkpool ACP only permits dynamic MCP server thinkpool")
|
|
420
420
|
if mcp_servers:
|
|
421
421
|
# SessionState is process-local. Keep only the validated descriptors so
|
|
422
422
|
# a subsequent set_model reconstruction can re-register the same MCP.
|
|
@@ -434,7 +434,7 @@ _set_model = acp_adapter.server.HermesACPAgent.set_session_model
|
|
|
434
434
|
async def constrained_set_model(self, model_id, session_id, **kwargs):
|
|
435
435
|
rendered_model_id = str(model_id or "").strip().lower()
|
|
436
436
|
if ":" in rendered_model_id and not rendered_model_id.startswith("nous:"):
|
|
437
|
-
raise RuntimeError("
|
|
437
|
+
raise RuntimeError("thinkpool Hermes only switches models through Nous Portal")
|
|
438
438
|
state = self.session_manager.get_session(session_id)
|
|
439
439
|
if state is None:
|
|
440
440
|
return await _set_model(self, model_id, session_id, **kwargs)
|
|
@@ -445,7 +445,7 @@ async def constrained_set_model(self, model_id, session_id, **kwargs):
|
|
|
445
445
|
raise RuntimeError("Hermes did not acknowledge model switch")
|
|
446
446
|
servers = getattr(state, "_thinkpool_mcp_servers", ())
|
|
447
447
|
if MCP_TOOLS and not servers:
|
|
448
|
-
raise RuntimeError("
|
|
448
|
+
raise RuntimeError("thinkpool MCP registration is unavailable after model switch")
|
|
449
449
|
await constrained_register(self, state, list(servers))
|
|
450
450
|
_apply_reasoning(state)
|
|
451
451
|
assert_exact_inventory(state.agent)
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
let e="";process.stdin.setEncoding("utf8"),process.stdin.on("data",s=>{e+=s}),process.stdin.on("end",()=>{let s=null;try{s=JSON.parse(e)}catch{}const
|
|
2
|
+
let e="";process.stdin.setEncoding("utf8"),process.stdin.on("data",s=>{e+=s}),process.stdin.on("end",()=>{let s=null;try{s=JSON.parse(e)}catch{}const t=!s||"delegate_task"===s.tool_name;process.stdout.write(JSON.stringify(t?{action:"block",message:"thinkpool blocks hidden Hermes delegation. Open visible thinkpool lanes instead."}:{})+"\n")});
|
package/hermes-isolation.mjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import e from"node:fs";import r from"node:os";import o from"node:path";import{randomUUID as t}from"node:crypto";import{createSdkMcpServer as n,tool as s}from"@anthropic-ai/claude-agent-sdk";import{startHermesSession as i}from"./hermes-session.mjs";import{withMcpSessionFactory as a}from"./codex-mcp-http.mjs";const m=(e,r=25e3)=>new Promise((o,t)=>{const n=Date.now(),s=setInterval(()=>{e()?(clearInterval(s),o()):Date.now()-n>r&&(clearInterval(s),t(new Error("Hermes MCP isolation probe timed out")))},50)});export function assertHermesIsolation(e,r){if(!Array.isArray(e)||2!==e.length||!Array.isArray(r)||2!==r.length)throw new TypeError("Hermes MCP isolation requires exactly two session outputs and markers");for(let o=0;o<2;o++){if(!e[o].includes(`mcp__thinkpool__${r[o]}`))throw new Error(`Hermes lane ${o+1} did not advertise its own MCP marker`);if(e[o].includes(`mcp__thinkpool__${r[1-o]}`))throw new Error(`Hermes lane ${o+1} advertised the other lane's MCP marker`)}return!0}export async function verifyHermesMcpIsolation({rounds:l=2,sessionFactory:p=i}={}){const c=[];for(let i=0;i<l;i++){const l=e.mkdtempSync(o.join(r.tmpdir(),"tp-hermes-mcp-isolation-")),d=t().replaceAll("-","").slice(0,10),h=[`lane_alpha_${d}`,`lane_beta_${d}`],f=[[],[]],u=[];try{for(let r=0;r<2;r++){const t=o.join(l,0===r?"alpha":"beta");e.mkdirSync(t);const i=a(()=>n({name:"thinkpool",version:"1.0.0",tools:[s(h[r],"Synthetic
|
|
1
|
+
import e from"node:fs";import r from"node:os";import o from"node:path";import{randomUUID as t}from"node:crypto";import{createSdkMcpServer as n,tool as s}from"@anthropic-ai/claude-agent-sdk";import{startHermesSession as i}from"./hermes-session.mjs";import{withMcpSessionFactory as a}from"./codex-mcp-http.mjs";const m=(e,r=25e3)=>new Promise((o,t)=>{const n=Date.now(),s=setInterval(()=>{e()?(clearInterval(s),o()):Date.now()-n>r&&(clearInterval(s),t(new Error("Hermes MCP isolation probe timed out")))},50)});export function assertHermesIsolation(e,r){if(!Array.isArray(e)||2!==e.length||!Array.isArray(r)||2!==r.length)throw new TypeError("Hermes MCP isolation requires exactly two session outputs and markers");for(let o=0;o<2;o++){if(!e[o].includes(`mcp__thinkpool__${r[o]}`))throw new Error(`Hermes lane ${o+1} did not advertise its own MCP marker`);if(e[o].includes(`mcp__thinkpool__${r[1-o]}`))throw new Error(`Hermes lane ${o+1} advertised the other lane's MCP marker`)}return!0}export async function verifyHermesMcpIsolation({rounds:l=2,sessionFactory:p=i}={}){const c=[];for(let i=0;i<l;i++){const l=e.mkdtempSync(o.join(r.tmpdir(),"tp-hermes-mcp-isolation-")),d=t().replaceAll("-","").slice(0,10),h=[`lane_alpha_${d}`,`lane_beta_${d}`],f=[[],[]],u=[];try{for(let r=0;r<2;r++){const t=o.join(l,0===r?"alpha":"beta");e.mkdirSync(t);const i=a(()=>n({name:"thinkpool",version:"1.0.0",tools:[s(h[r],"Synthetic thinkpool lane-isolation marker.",{},async()=>({content:[{type:"text",text:h[r]}]}))]}));u.push(p({cwd:t,lazy:!0,mcpServers:{thinkpool:i},onEvent:e=>f[r].push(e)}))}u.forEach(e=>e.sendTurn("/tools")),await m(()=>f.every(e=>e.some(e=>"result"===e.kind)||e.some(e=>"error"===e.kind)));const r=f.flatMap(e=>e.filter(e=>"error"===e.kind));if(r.length)throw new Error(`Hermes MCP isolation probe failed: ${r.map(e=>e.message).join("; ")}`);assertHermesIsolation(f.map(e=>e.filter(e=>"assistant_stream"===e.kind).map(e=>e.text).join("")),h),c.push({round:i+1,markers:h,sessionIds:u.map(e=>e.sessionId)})}finally{u.forEach(e=>e.end()),await new Promise(e=>setTimeout(e,250)),e.rmSync(l,{recursive:!0,force:!0})}}return c}
|
package/hermes-policy.mjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{PEER_MCP_TOOLS as e,PEER_READ_MCP_TOOLS as r}from"./cross-terminal.mjs";export const HERMES_POLICY_VERSION=1;export const HERMES_QUESTION_MCP_TOOL="request_user_input";export const MCP_FLIGHT_RECORDER_TOOL="read_mcp_flight_recorder";export const CODING_TOOLS=Object.freeze(["web_search","web_extract","terminal","process","read_file","write_file","patch","search_files","vision_analyze","skills_list","skill_view","skill_manage","browser_navigate","browser_snapshot","browser_click","browser_type","browser_scroll","browser_back","browser_press","browser_get_images","browser_vision","browser_console","browser_cdp","browser_dialog","todo","memory","execute_code"]);const o=Object.freeze(["read_file","search_files"]);export const ESSENTIAL_CODING_TOOLS=Object.freeze(["terminal","process","read_file","write_file","patch","search_files"]);const s=Object.freeze({ordinary:[...e,"request_user_input",MCP_FLIGHT_RECORDER_TOOL,"spawn_terminal","close_terminal"],plan:[],conductor:["submit_flow_plan"],builder:["mark_flow_done"],reviewer:["submit_flow_review","run_review_check","read_review_file"],"manual-review":["run_review_check","read_review_file"]});export function hermesRoleFor({flowRole:e,sliceType:r}={}){return"conductor"===e?"conductor":"reviewer"===e?"reviewer":"review"===r?"manual-review":"builder"===e?"builder":"ordinary"}export function hermesPolicyForRole(r,{mcpTools:i}={}){if(!Object.hasOwn(s,r))throw new Error(`Unknown Hermes role policy: ${r}`);const t=Array.isArray(i)?i.map(String).filter(Boolean):s[r],n=s[r],l="ordinary"===r?[...e,"request_user_input",MCP_FLIGHT_RECORDER_TOOL]:n;for(const e of l)if(!t.includes(e))throw new Error(`Hermes ${r} policy is missing required
|
|
1
|
+
import{PEER_MCP_TOOLS as e,PEER_READ_MCP_TOOLS as r}from"./cross-terminal.mjs";export const HERMES_POLICY_VERSION=1;export const HERMES_QUESTION_MCP_TOOL="request_user_input";export const MCP_FLIGHT_RECORDER_TOOL="read_mcp_flight_recorder";export const CODING_TOOLS=Object.freeze(["web_search","web_extract","terminal","process","read_file","write_file","patch","search_files","vision_analyze","skills_list","skill_view","skill_manage","browser_navigate","browser_snapshot","browser_click","browser_type","browser_scroll","browser_back","browser_press","browser_get_images","browser_vision","browser_console","browser_cdp","browser_dialog","todo","memory","execute_code"]);const o=Object.freeze(["read_file","search_files"]);export const ESSENTIAL_CODING_TOOLS=Object.freeze(["terminal","process","read_file","write_file","patch","search_files"]);const s=Object.freeze({ordinary:[...e,"request_user_input",MCP_FLIGHT_RECORDER_TOOL,"spawn_terminal","close_terminal"],plan:[],conductor:["submit_flow_plan"],builder:["mark_flow_done"],reviewer:["submit_flow_review","run_review_check","read_review_file"],"manual-review":["run_review_check","read_review_file"]});export function hermesRoleFor({flowRole:e,sliceType:r}={}){return"conductor"===e?"conductor":"reviewer"===e?"reviewer":"review"===r?"manual-review":"builder"===e?"builder":"ordinary"}export function hermesPolicyForRole(r,{mcpTools:i}={}){if(!Object.hasOwn(s,r))throw new Error(`Unknown Hermes role policy: ${r}`);const t=Array.isArray(i)?i.map(String).filter(Boolean):s[r],n=s[r],l="ordinary"===r?[...e,"request_user_input",MCP_FLIGHT_RECORDER_TOOL]:n;for(const e of l)if(!t.includes(e))throw new Error(`Hermes ${r} policy is missing required thinkpool tool ${e}`);const _="plan"===r||"conductor"===r||"reviewer"===r||"manual-review"===r,c=_?o:CODING_TOOLS,a=_?o:ESSENTIAL_CODING_TOOLS;return Object.freeze({version:1,role:r,builtinTools:c,requiredBuiltinTools:a,mcpServer:"thinkpool",mcpTools:[...new Set(t)].sort()})}export function hermesRequiredMcpTools(r,{canSpawnWorkers:o=!1}={}){return"ordinary"===r?[...e,"request_user_input",MCP_FLIGHT_RECORDER_TOOL,...o?["spawn_terminal","close_terminal"]:[]]:[...s[r]]}export const HERMES_PLAN_SAFE_MCP_TOOLS=r;export function hermesExactInventory(e,r={}){const o=hermesPolicyForRole(e,r);return Object.freeze({builtinTools:[...o.builtinTools].sort(),requiredBuiltinTools:[...o.requiredBuiltinTools].sort(),mcpTools:[...o.mcpTools].sort(),allBuiltinTools:[...CODING_TOOLS,"delegate_task","session_search"].sort()})}export function hermesPolicyEnv(e,r={}){return JSON.stringify(hermesPolicyForRole(e,r))}
|
package/hermes-probe.mjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{execFileSync as e}from"node:child_process";import o from"node:fs";import t from"node:path";const i=e=>String(e||"").replace(/[\r\n]+/g," ").trim(),n=/Install directory:\s*(.+?)(?:\r?\n|$)/i,r=/Config:\s*(.+?)(?:\r?\n|$)/i,s=/Model:\s*\{[^\r\n]*['"]provider['"]\s*:\s*['"]nous['"][^\r\n]*\}/i;export function resolveHermesAcpRuntime({command:l="thinkpool",execFile:a=e,exists:p=o.existsSync,bootstrap:c=new URL("./hermes-acp-bootstrap.py",import.meta.url)}={}){try{const e=a(l,["--version"],{encoding:"utf8",timeout:1e4,stdio:["ignore","pipe","pipe"]}),o=i(String(e).match(n)?.[1]);if(!o||!t.isAbsolute(o))throw new Error("Hermes did not report an absolute install directory");const d=t.join(o,"venv","bin","python"),m=c instanceof URL?c.pathname:String(c);if(!p(d)||!p(m))throw new Error("Hermes venv Python or
|
|
1
|
+
import{execFileSync as e}from"node:child_process";import o from"node:fs";import t from"node:path";const i=e=>String(e||"").replace(/[\r\n]+/g," ").trim(),n=/Install directory:\s*(.+?)(?:\r?\n|$)/i,r=/Config:\s*(.+?)(?:\r?\n|$)/i,s=/Model:\s*\{[^\r\n]*['"]provider['"]\s*:\s*['"]nous['"][^\r\n]*\}/i;export function resolveHermesAcpRuntime({command:l="thinkpool",execFile:a=e,exists:p=o.existsSync,bootstrap:c=new URL("./hermes-acp-bootstrap.py",import.meta.url)}={}){try{const e=a(l,["--version"],{encoding:"utf8",timeout:1e4,stdio:["ignore","pipe","pipe"]}),o=i(String(e).match(n)?.[1]);if(!o||!t.isAbsolute(o))throw new Error("Hermes did not report an absolute install directory");const d=t.join(o,"venv","bin","python"),m=c instanceof URL?c.pathname:String(c);if(!p(d)||!p(m))throw new Error("Hermes venv Python or thinkpool ACP bootstrap is missing");const u=a(l,["config","show"],{encoding:"utf8",timeout:1e4,stdio:["ignore","pipe","pipe"]});if(!s.test(String(u)))throw new Error("thinkpool Hermes profile is not pinned to Nous Portal");const h=i(String(u).match(r)?.[1]),g=h?t.dirname(h):"";if(!g||!t.isAbsolute(g)||"thinkpool"!==t.basename(g))throw new Error("Hermes did not report the isolated thinkpool profile");return{python:d,bootstrap:m,profile:g,install:o}}catch(e){return{error:i(e?.stderr||e?.message||e)||"could not resolve Hermes ACP runtime"}}}export function probeHermesRuntime({command:t="thinkpool",prefixArgs:n=[],execFile:r=e,env:l=process.env,strictBootstrap:a=!1,exists:p=o.existsSync}={}){try{const e=r(t,[...n,"--version"],{encoding:"utf8",env:l,timeout:1e4,stdio:["ignore","pipe","pipe"]}),o=i(e).match(/Hermes Agent v([^\s]+)/i)?.[1]||null,c=r(t,[...n,"config","show"],{encoding:"utf8",env:l,timeout:1e4,stdio:["ignore","pipe","pipe"]});if(!s.test(String(c)))return{available:!1,version:o,reason:"thinkpool Hermes profile is not pinned to Nous Portal"};r(t,[...n,"acp","--check"],{encoding:"utf8",env:l,timeout:15e3,stdio:["ignore","pipe","pipe"]});const d=r(t,[...n,"hooks","list"],{encoding:"utf8",env:l,timeout:1e4,stdio:["ignore","pipe","pipe"]}),m=r(t,[...n,"hooks","doctor"],{encoding:"utf8",env:l,timeout:1e4,stdio:["ignore","pipe","pipe"]}),u=r(t,[...n,"hooks","test","pre_tool_call","--for-tool","delegate_task"],{encoding:"utf8",env:l,timeout:1e4,stdio:["ignore","pipe","pipe"]}),h=/All shell hooks look healthy/i.test(m)&&!/(?:issue\(s\) found|modified since approval|⚠)/i.test(m),g=/(?:action|decision)["']?\s*[:=]\s*["']block/i.test(u)&&/thinkpool blocks hidden Hermes delegation/i.test(u)&&/pre_tool_call[\s\S]*delegate_task[\s\S]*(?:✓ allowed|allowed)/i.test(d);if(!h||!g)return{available:!1,version:o,reason:"delegate_task guard is not healthy, unchanged, allowlisted, and structurally blocking in the dedicated Hermes profile"};const f=a?resolveHermesAcpRuntime({command:t,execFile:r,exists:p}):null;return f?.error?{available:!1,version:o,reason:`Hermes ACP bootstrap unavailable: ${f.error}`}:{available:!0,version:o,acpProtocol:1,delegationBlocked:!0,...f||{}}}catch(e){return{available:!1,version:null,reason:i(e?.stderr||e?.message||e)}}}
|
package/hermes-session.mjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import e from"node:fs";import t from"node:path";import{randomUUID as o}from"node:crypto";import{AcpClient as n}from"./acp-client.mjs";import{HermesEventMapper as s,hermesToolFor as r}from"./hermes-event-mapper.mjs";import{probeHermesRuntime as i}from"./hermes-probe.mjs";import{HERMES_PLAN_SAFE_MCP_TOOLS as l,HERMES_QUESTION_MCP_TOOL as a,hermesExactInventory as c,hermesPolicyEnv as d,hermesRoleFor as m}from"./hermes-policy.mjs";import{startCodexMcpHttp as u}from"./codex-mcp-http.mjs";import{autoAllow as p,classifyRisk as f}from"./claude-session.mjs";import{crossPostNeedsCard as h}from"./cross-terminal.mjs";import{buildThinkPoolTurnGuidance as g,createRoomContextSelector as w,usesFullThinkPoolReminder as _}from"./thinkpool-room-prompt.mjs";import{createCumulativeEventRelay as y}from"./cumulative-event-relay.mjs";export const HERMES_COMMAND="thinkpool";export const HERMES_ACP_PROTOCOL_VERSION=1;export const HERMES_SUPPORTED_MODES=new Set(["default","acceptEdits","plan","bypassPermissions"]);export const HERMES_EFFORT_LEVELS=new Set(["none","low","medium","high","xhigh","max"]);export const HERMES_EFFORT_CONFIG_ID="thinkpool_reasoning_effort";const E=new Set([...l,"read_review_file"]),I=/(?:TOKEN|SECRET|PASSWORD|PRIVATE_KEY|API_KEY|ACCESS_KEY|CREDENTIAL)/i,S=new Set(["agent_message_chunk","agent_thought_chunk","tool_call","tool_call_update","plan"]);export const hermesChildEnv=(e={})=>Object.fromEntries(Object.entries(e).filter(([e])=>!I.test(e)));const b=o=>{const n=t.extname(o).toLowerCase();return{type:"image",mimeType:".png"===n?"image/png":".webp"===n?"image/webp":".gif"===n?"image/gif":"image/jpeg",data:e.readFileSync(o).toString("base64")}},k=e=>(e?.availableModels||[]).map(e=>({value:e.modelId,displayName:e.name||e.modelId,description:e.description||""})),x=e=>new n(e);export function buildHermesPromptText({text:e,firstTurn:t=!1,terminalRolePrompt:o,rolePrompt:n,roomContext:s,promptIndex:r=0,forceFullReminder:i=!1}={}){const l="function"==typeof s?s():s,a=!!(o||n||l),c=_({promptIndex:r,forceFull:i}),d=[t||c?o:"",a?g({text:e,promptIndex:r,forceFull:i}):"",t||c?n:"",l].filter(Boolean).join("\n\n"),m=String(e??"");return d?`${d}\n\n${m}`:m}const v=(e,t=[])=>{if("deny"===e||!e)return{outcome:"cancelled"};const o=("always"===e?["allow_always","allow_session","allow_once"]:["allow_once","allow_session","allow_always"]).map(e=>t.find(t=>t.kind===e||t.optionId===e)).find(Boolean)||t.find(e=>String(e.kind||"").startsWith("allow"));return o?{outcome:"selected",optionId:o.optionId}:{outcome:"cancelled"}};export function startHermesSession({cwd:e,model:n,resume:g,env:I=process.env,mode:R="default",onEvent:C,requestPermission:H,roomContext:T,terminalRolePrompt:P,rolePrompt:$,mcpServers:M,requiredMcpTools:F=[],prepareCwd:O=null,command:q="thinkpool",args:A=["acp"],clientFactory:j=x,mcpHttpFactory:D=u,lazy:L=!1,hermesRole:N=null,crossPostGate:V=null,didSpawnTarget:B=null,crossRoomPostGate:U=null,effort:z="high",admitStart:G=null,onTurnStart:K=null}={}){let Y=e;const W=n||null;let J=W,Q=HERMES_SUPPORTED_MODES.has(R)?R:"default",X=g||null,Z=null,ee=null,te=null,oe=null,ne=!1,se=!1,re=!1,ie=!1,le=!0,ae=0,ce=!0;const de=w(T);let me="",ue=Promise.resolve(),pe=0;const fe=new Set;let he=()=>{},ge=!1,we=null,_e=!1,ye=!1,Ee=!1,Ie=null===z?null:HERMES_EFFORT_LEVELS.has(z)?z:"high";const Se=[];let be=!1,ke=!1;const xe=N||m({}),ve=()=>"plan"===Q?"plan":xe,Re=()=>"plan"===Q?F.filter(e=>E.has(e)):F,Ce=e=>"acceptEdits"===e?"accept_edits":"bypassPermissions"===e?"dont_ask":"default",He=y(e=>{try{C?.(e)}catch{}});async function Te(e,t,o){if("session/request_permission"!==e)throw Object.assign(new Error(`Unsupported ACP client method: ${e}`),{code:-32601});const n=r(t.toolCall||ee?.tools.get(t.toolCallId)||{}),s=(e=>{const t=String(e||"").toLowerCase();return[...l,"post_to_terminal","post_to_session",a].find(e=>t===e||t===`mcp__thinkpool__${e}`||t===`mcp_thinkpool_${e}`)||null})(n.name),i={id:`hermes-perm:${o}`,toolName:s?`mcp__thinkpool__${s}`:n.name,input:n.input,risk:s?.startsWith("post_to_")?"high":f(n.name,n.input)};if(l.includes(s))return{outcome:v("allow",t.options)};if(s===a)return{outcome:v("allow",t.options)};if("post_to_terminal"===s){if(!(V?V():{ok:!0}).ok)return{outcome:{outcome:"cancelled"}};const e=!!B&&!!B(n.input?.terminal);if(!h({mode:Q,spawnedByMe:e}))return{outcome:v("allow",t.options)}}if("post_to_session"===s){if(!(U?U():{ok:!0}).ok)return{outcome:{outcome:"cancelled"}};let e="deny";try{e=await(H?.(i))}catch{}return{outcome:v(e,t.options)}}if("plan"===Q)return{outcome:{outcome:"cancelled"}};if(p({toolName:i.toolName,input:i.input,mode:Q}))return{outcome:v("allow",t.options)};let c="deny";try{c=await(H?.(i))}catch{}return{outcome:v(c,t.options)}}function Pe(e,t){if("session/update"!==e)return;const o=t?.update||t;we&&"agent_message_chunk"===o?.sessionUpdate&&"text"===o?.content?.type?we.push(String(o.content.text||"")):ge&&S.has(o?.sessionUpdate)||ee?.push(t)}async function $e(){const e=[...new Set((Array.isArray(Re())?Re():[]).map(e=>String(e||"").trim()).filter(Boolean))];if(!e.length&&"plan"!==ve())return{inventory:"",missing:[],forbidden:[]};const t=c(ve(),{mcpTools:e}),n=t.builtinTools,s=t.requiredBuiltinTools,r=t.mcpTools,i=[];we=i;try{await Z.request("session/prompt",{sessionId:X,prompt:[{type:"text",text:"/tools"}],messageId:o()},3e4)}finally{we=null}const l=i.join(""),a=e=>new RegExp(`(?:^|[^a-z0-9_])${e}(?:$|[^a-z0-9_])`,"i").test(l),d=[...l.matchAll(/\bmcp(?:__|_)[a-z0-9_]+/gi)].map(e=>e[0]),m=new Set(r.flatMap(e=>[`mcp__thinkpool__${e}`,`mcp_thinkpool_${e}`])),u=[...new Set(d.filter(e=>!m.has(e)))];return{inventory:l,missing:[...s.filter(e=>!a(e)),...r.filter(e=>!a(`mcp__thinkpool__${e}`)&&!a(`mcp_thinkpool_${e}`))],forbidden:[...t.allBuiltinTools.filter(e=>!n.includes(e)&&a(e)),...u]}}async function Me(){const e=await $e();if(!e.missing.length&&!e.forbidden.length)return e;const t=[e.missing.length?`missing ${e.missing.join(", ")}`:"",e.forbidden.length?`forbidden ${e.forbidden.join(", ")}`:""].filter(Boolean).join("; ");throw new Error(`Hermes ThinkPool MCP readiness failed; ${t}`)}async function Fe(){if(ne)throw new Error("Hermes session is closed");if(ie)throw new Error("Hermes ACP process crashed; this lane must be restarted before sending another turn");if(Z?.alive&&ee)return;if(oe)return oe;const e="function"==typeof G?G():{ok:!0};if(!1===e?.ok)throw new Error(e.reason||"Host memory is critically low. This Hermes runtime was not started.");return oe=(async()=>{if(!re&&O)try{Y=O()||Y}catch{}const e=M?.thinkpool;e&&!te&&(te=await D({sdkServer:e}));let o=hermesChildEnv(I),n=q,r=A;if("thinkpool"===q&&j===x){const e=i({command:q,env:o,strictBootstrap:!0});if(!e.available)throw new Error(`Hermes profile safety check failed: ${e.reason||"runtime unavailable"}`);n=e.python,r=[e.bootstrap],o={...o,HERMES_HOME:e.profile,THINKPOOL_HERMES_ACP_POLICY:d(ve(),{mcpTools:Re()})}}let l=!1;he=()=>{l=!0};const a={protocolVersion:1,clientCapabilities:{fs:{readTextFile:!1,writeTextFile:!1},terminal:!1},clientInfo:{name:"thinkpool-pair",title:"ThinkPool Code",version:"1"}};let c;for(let e=0;e<2;e++){l=!1,he=()=>{l=!0},Z=j({command:n,args:r,cwd:Y,env:o,onNotification:Pe,onRequest:Te,onStderr:e=>{me=(me+e).slice(-2e3)},onClose:e=>{ne||ie||l||(ie=!0,se=!1,He({kind:"error",message:`Hermes ACP closed unexpectedly: ${e?.message||e}`,recoverable:!0}))}}),Z.start();try{c=await Z.request("initialize",a,15e3);break}catch(t){if(ye||0!==e||!/^initialize timed out\b/i.test(String(t?.message||t)))throw t;he();const o=Z;Z=null,o?.end(),me=""}}if(1!==c?.protocolVersion)throw new Error(`Unsupported Hermes ACP protocol ${c?.protocolVersion??"unknown"}; expected 1`);const m={cwd:t.resolve(Y),mcpServers:te?[(u=te.url,{type:"http",name:"thinkpool",url:u,headers:[]})]:[]};var u;let p;if(ee=new s({onEvent:He,sessionId:X,model:J}),X){if(!c?.agentCapabilities?.sessionCapabilities?.resume)throw new Error(`Hermes session ${X} cannot be resumed by this ACP release; no fresh session was substituted`);try{ge=!0,p=await Z.request("session/resume",{...m,sessionId:X},3e4)}catch(e){throw new Error(`Hermes session ${X} is not available; no fresh session was substituted (${e?.message||e})`)}finally{ge=!1}}else p=await Z.request("session/new",m,3e4);if(X||(X=p?.sessionId||null),!X)throw new Error("Hermes ACP did not return a session id");let f=await $e(),h=f.missing;if(h.length&&c?.agentCapabilities?.sessionCapabilities?.resume){try{ge=!0,p=await Z.request("session/resume",{...m,sessionId:X},3e4)}finally{ge=!1}f=await $e(),h=f.missing}if(h.length||f.forbidden.length){const e=[h.length?`missing ${h.join(", ")}`:"",f.forbidden.length?`forbidden ${f.forbidden.join(", ")}`:""].filter(Boolean).join("; ");throw new Error(`Hermes ThinkPool MCP readiness failed; ${e}`)}J=W||p?.models?.currentModelId||null||J,W&&(await Z.request("session/set_model",{sessionId:X,modelId:J}),await Me()),Ie&&await Z.request("session/set_config_option",{sessionId:X,configId:HERMES_EFFORT_CONFIG_ID,value:Ie});const g=p?.models?{...p.models,currentModelId:J}:J?{currentModelId:J,availableModels:[]}:p?.models;Ee?(ee.sessionId=X,ee.model=J,Ee=!1):ee.startSession({sessionId:X,models:g,modes:p?.modes,commands:[]}),re=!0;const w=Ce(Q);p?.modes?.availableModes?.some(e=>e.id===w)&&p.modes.currentModeId!==w&&await Z.request("session/set_mode",{sessionId:X,modeId:w}),He({kind:"capabilities",runtime:"hermes",protocol:"acp",protocolVersion:c?.protocolVersion,capabilities:c?.agentCapabilities||{},models:k(p?.models),flow:"thinkpool"===q&&j===x})})().catch(async e=>{ie=!0,Z?.end();try{await(te?.close())}catch{}throw te=null,ye||He({kind:"error",message:`Hermes ACP startup failed: ${e?.message||e}${me?`: ${me.trim().slice(-240)}`:""}`,recoverable:!0}),e}).finally(()=>{oe=null,ye=!1}),oe}function Oe(){ee?ee.finishTurn({stopReason:"cancelled"}):He({kind:"result",subtype:"aborted",sessionId:X,model:J,costUsd:null,usage:null,numTurns:1,durationMs:void 0,denials:0,resultText:null})}function qe(e,t={},{promptIndex:o=0,forceFull:n=!1}={}){const s=String(e);if(/^\s*\//.test(s))return[{type:"text",text:s}];const r=_({promptIndex:o,forceFull:n}),i=[{type:"text",text:buildHermesPromptText({text:s,firstTurn:le,terminalRolePrompt:P,rolePrompt:$,roomContext:()=>de({force:r}),promptIndex:o,forceFullReminder:n})}];for(const e of Array.isArray(t.images)?t.images:[])try{i.push(b(e))}catch{}return i}async function Ae(e,t={},{steering:n=!1,turnId:s=pe,promptIndex:r=0,forceFull:i=!1}={}){try{await Fe()}catch(e){if(fe.has(s))return{stopReason:"cancelled"};throw e}if(fe.has(s))return{stopReason:"cancelled"};const l=n?`/steer ${String(e)}`:String(e);let a;try{a=await Z.request("session/prompt",{sessionId:X,prompt:qe(l,t,{promptIndex:r,forceFull:i}),messageId:o()},0)}catch(e){if(fe.has(s))return ne||await async function(){const e=Z;he(),Z=null,ee=null,e?.end(),await Fe()}(),{stopReason:"cancelled"};throw e}if(!n){if(fe.has(s))return a;se=!1,le=!1;const t=/^\s*\/compact\b/i.test(String(e||""))?ee.compactOutcome():null;t&&He({kind:"compaction",trigger:"manual",...t});const o=String(e||"").match(/^\s*\/reasoning\s+(low|medium|high|xhigh|max|none|reset)\s*$/i);if(o){const e=o[1].toLowerCase();("reset"===e?/Session reasoning reset to model default/i.test(ee.text):new RegExp(`Session reasoning set to ${e}`,"i").test(ee.text))&&(Ie="reset"===e?null:e,He({kind:"effort",level:Ie}))}ke=Se.length>0&&!ne&&!fe.has(s),ee.finishTurn({stopReason:a?.stopReason,usage:a?.usage}),ke&&async function(){if(!(be||ne||se)){be=!0;try{for(;Se.length&&!ne&&!se;){ke=!1;const e=Se.shift(),t=++pe;se=!0;try{K?.(e.options||{})}catch{}try{await Ae(e.text,e.options,{steering:!1,turnId:t,promptIndex:e.promptIndex,forceFull:e.forceFull})}catch(e){se=!1,He({kind:"error",message:`Hermes queued turn failed: ${e?.message||e}`,recoverable:!0})}}}finally{be=!1,ke=!1}}}()}return a}return L||queueMicrotask(()=>{Fe().catch(()=>{})}),{get sessionId(){return X},get turnActive(){return se},get queuedDepth(){return Se.length},get canSteer(){return!!Z?.alive},get started(){return re},get models(){return[]},get effort(){return Ie},sendTurn(e,t={}){if(ne)return!1;if(!Z?.alive&&!oe){const e="function"==typeof G?G():{ok:!0};if(!1===e?.ok)return He({kind:"error",message:e.reason||"Host memory is critically low. This Hermes runtime was not started.",recoverable:!0}),!1}const o=ae++,n=ce;ce=/^\s*\/(?:compact|reset|clear)\b/i.test(String(e||"")),ie&&!ne&&(he(),Z?.end(),Z=null,ee=null,ie=!1,me="");const s=String(e||"").match(/^\s*\/queue\s+([\s\S]+)$/i);if(ke)return Se.push({text:s?s[1]:String(e||""),options:t,promptIndex:o,forceFull:n}),He({kind:"queue-add",queued:!0,depth:Se.length}),!0;if(se)return s?(Se.push({text:s[1],options:t,promptIndex:o,forceFull:n}),He({kind:"queue-add",queued:!0,depth:Se.length}),!0):(Ae(e,t,{steering:!0,turnId:pe,promptIndex:o,forceFull:n}).catch(e=>He({kind:"error",message:`Hermes steering failed: ${e?.message||e}`,recoverable:!0})),!0);const r=++pe;return se=!0,ue=ue.then(()=>Ae(e,t,{steering:!1,turnId:r,promptIndex:o,forceFull:n})).catch(e=>{se=!1,He({kind:"error",message:`Hermes turn failed: ${e?.message||e}`,recoverable:!0})}),!0},async abort(){if(!se)return;Se.length=0,ke=!1;const e=pe;return fe.add(e),fe.size>32&&fe.delete(fe.values().next().value),se=!1,le=!1,oe?(ye=!0,he(),Z?.end(),Oe(),void await ue.catch(()=>{})):X&&Z?.alive?(Oe(),await Z.notify("session/cancel",{sessionId:X}).catch(e=>{ie=!0,Z?.end(),He({kind:"error",message:`Hermes cancellation delivery failed; ACP process stopped: ${e?.message||e}`,recoverable:!0})}),void await ue.catch(()=>{})):(Oe(),void await ue.catch(()=>{}))},end(){ne=!0,se=!1,He.cancel(),Se.length=0,ke=!1,X&&Z?.alive?Z.request("session/close",{sessionId:X},1500).catch(()=>{}).finally(()=>Z?.end()):Z?.end(),te?.close().catch(()=>{})},setModel(e){if(!e||se||_e)return!1;const t=String(e);return _e=!0,Fe().then(()=>Z.request("session/set_model",{sessionId:X,modelId:t})).then(async()=>{await Me(),ne||(J=t,ee&&(ee.model=J),He({kind:"model-switch",status:"success",model:J,requested:t}))}).catch(e=>{ne||He({kind:"model-switch",status:"failed",model:J,requested:t,message:`Hermes model switch failed: ${e?.message||e}`})}).finally(()=>{_e=!1}),!0},setMode(e){if(se||!HERMES_SUPPORTED_MODES.has(e))return!1;const t=Q;if("plan"===t!=("plan"===e)){if(Q=e,Ee=re,re){he();const e=Z;Z=null,ee=null,re=!1,ie=!1,me="",e?.end()}return Fe().then(()=>He({kind:"mode",mode:Q})).catch(e=>{Q=t,Ee=!1,He({kind:"error",message:`Hermes mode switch failed: ${e?.message||e}`,recoverable:!0})}),!0}return Fe().then(()=>Z.request("session/set_mode",{sessionId:X,modeId:Ce(e)})).then(()=>{Q=e,He({kind:"mode",mode:Q})}).catch(e=>He({kind:"error",message:`Hermes mode switch failed: ${e?.message||e}`,recoverable:!0})),!0},setEffort(e){if(se||!HERMES_EFFORT_LEVELS.has(e))return!1;const t=String(e);return Fe().then(()=>Z.request("session/set_config_option",{sessionId:X,configId:HERMES_EFFORT_CONFIG_ID,value:t})).then(()=>{Ie=t,He({kind:"effort",level:Ie})}).catch(e=>He({kind:"error",message:`Hermes reasoning change failed: ${e?.message||e}`,recoverable:!0})),!0},clearContext(){return!se&&this.sendTurn("/reset")},warm(){Fe().catch(()=>{})}}}
|
|
1
|
+
import e from"node:fs";import t from"node:path";import{randomUUID as o}from"node:crypto";import{AcpClient as n}from"./acp-client.mjs";import{HermesEventMapper as s,hermesToolFor as r}from"./hermes-event-mapper.mjs";import{probeHermesRuntime as i}from"./hermes-probe.mjs";import{HERMES_PLAN_SAFE_MCP_TOOLS as l,HERMES_QUESTION_MCP_TOOL as a,hermesExactInventory as c,hermesPolicyEnv as d,hermesRoleFor as m}from"./hermes-policy.mjs";import{startCodexMcpHttp as u}from"./codex-mcp-http.mjs";import{autoAllow as p,classifyRisk as f}from"./claude-session.mjs";import{crossPostNeedsCard as h}from"./cross-terminal.mjs";import{buildThinkPoolTurnGuidance as g,createRoomContextSelector as w,usesFullThinkPoolReminder as _}from"./thinkpool-room-prompt.mjs";import{createCumulativeEventRelay as y}from"./cumulative-event-relay.mjs";export const HERMES_COMMAND="thinkpool";export const HERMES_ACP_PROTOCOL_VERSION=1;export const HERMES_SUPPORTED_MODES=new Set(["default","acceptEdits","plan","bypassPermissions"]);export const HERMES_EFFORT_LEVELS=new Set(["none","low","medium","high","xhigh","max"]);export const HERMES_EFFORT_CONFIG_ID="thinkpool_reasoning_effort";const E=new Set([...l,"read_review_file"]),I=/(?:TOKEN|SECRET|PASSWORD|PRIVATE_KEY|API_KEY|ACCESS_KEY|CREDENTIAL)/i,S=new Set(["agent_message_chunk","agent_thought_chunk","tool_call","tool_call_update","plan"]);export const hermesChildEnv=(e={})=>Object.fromEntries(Object.entries(e).filter(([e])=>!I.test(e)));const b=o=>{const n=t.extname(o).toLowerCase();return{type:"image",mimeType:".png"===n?"image/png":".webp"===n?"image/webp":".gif"===n?"image/gif":"image/jpeg",data:e.readFileSync(o).toString("base64")}},k=e=>(e?.availableModels||[]).map(e=>({value:e.modelId,displayName:e.name||e.modelId,description:e.description||""})),x=e=>new n(e);export function buildHermesPromptText({text:e,firstTurn:t=!1,terminalRolePrompt:o,rolePrompt:n,roomContext:s,promptIndex:r=0,forceFullReminder:i=!1}={}){const l="function"==typeof s?s():s,a=!!(o||n||l),c=_({promptIndex:r,forceFull:i}),d=[t||c?o:"",a?g({text:e,promptIndex:r,forceFull:i}):"",t||c?n:"",l].filter(Boolean).join("\n\n"),m=String(e??"");return d?`${d}\n\n${m}`:m}const v=(e,t=[])=>{if("deny"===e||!e)return{outcome:"cancelled"};const o=("always"===e?["allow_always","allow_session","allow_once"]:["allow_once","allow_session","allow_always"]).map(e=>t.find(t=>t.kind===e||t.optionId===e)).find(Boolean)||t.find(e=>String(e.kind||"").startsWith("allow"));return o?{outcome:"selected",optionId:o.optionId}:{outcome:"cancelled"}};export function startHermesSession({cwd:e,model:n,resume:g,env:I=process.env,mode:R="default",onEvent:C,requestPermission:H,roomContext:$,terminalRolePrompt:T,rolePrompt:P,mcpServers:M,requiredMcpTools:F=[],prepareCwd:O=null,command:q="thinkpool",args:A=["acp"],clientFactory:j=x,mcpHttpFactory:D=u,lazy:L=!1,hermesRole:N=null,crossPostGate:V=null,didSpawnTarget:B=null,crossRoomPostGate:U=null,effort:z="high",admitStart:G=null,onTurnStart:K=null}={}){let Y=e;const W=n||null;let J=W,Q=HERMES_SUPPORTED_MODES.has(R)?R:"default",X=g||null,Z=null,ee=null,te=null,oe=null,ne=!1,se=!1,re=!1,ie=!1,le=!0,ae=0,ce=!0;const de=w($);let me="",ue=Promise.resolve(),pe=0;const fe=new Set;let he=()=>{},ge=!1,we=null,_e=!1,ye=!1,Ee=!1,Ie=null===z?null:HERMES_EFFORT_LEVELS.has(z)?z:"high";const Se=[];let be=!1,ke=!1;const xe=N||m({}),ve=()=>"plan"===Q?"plan":xe,Re=()=>"plan"===Q?F.filter(e=>E.has(e)):F,Ce=e=>"acceptEdits"===e?"accept_edits":"bypassPermissions"===e?"dont_ask":"default",He=y(e=>{try{C?.(e)}catch{}});async function $e(e,t,o){if("session/request_permission"!==e)throw Object.assign(new Error(`Unsupported ACP client method: ${e}`),{code:-32601});const n=r(t.toolCall||ee?.tools.get(t.toolCallId)||{}),s=(e=>{const t=String(e||"").toLowerCase();return[...l,"post_to_terminal","post_to_session",a].find(e=>t===e||t===`mcp__thinkpool__${e}`||t===`mcp_thinkpool_${e}`)||null})(n.name),i={id:`hermes-perm:${o}`,toolName:s?`mcp__thinkpool__${s}`:n.name,input:n.input,risk:s?.startsWith("post_to_")?"high":f(n.name,n.input)};if(l.includes(s))return{outcome:v("allow",t.options)};if(s===a)return{outcome:v("allow",t.options)};if("post_to_terminal"===s){if(!(V?V():{ok:!0}).ok)return{outcome:{outcome:"cancelled"}};const e=!!B&&!!B(n.input?.terminal);if(!h({mode:Q,spawnedByMe:e}))return{outcome:v("allow",t.options)}}if("post_to_session"===s){if(!(U?U():{ok:!0}).ok)return{outcome:{outcome:"cancelled"}};let e="deny";try{e=await(H?.(i))}catch{}return{outcome:v(e,t.options)}}if("plan"===Q)return{outcome:{outcome:"cancelled"}};if(p({toolName:i.toolName,input:i.input,mode:Q}))return{outcome:v("allow",t.options)};let c="deny";try{c=await(H?.(i))}catch{}return{outcome:v(c,t.options)}}function Te(e,t){if("session/update"!==e)return;const o=t?.update||t;we&&"agent_message_chunk"===o?.sessionUpdate&&"text"===o?.content?.type?we.push(String(o.content.text||"")):ge&&S.has(o?.sessionUpdate)||ee?.push(t)}async function Pe(){const e=[...new Set((Array.isArray(Re())?Re():[]).map(e=>String(e||"").trim()).filter(Boolean))];if(!e.length&&"plan"!==ve())return{inventory:"",missing:[],forbidden:[]};const t=c(ve(),{mcpTools:e}),n=t.builtinTools,s=t.requiredBuiltinTools,r=t.mcpTools,i=[];we=i;try{await Z.request("session/prompt",{sessionId:X,prompt:[{type:"text",text:"/tools"}],messageId:o()},3e4)}finally{we=null}const l=i.join(""),a=e=>new RegExp(`(?:^|[^a-z0-9_])${e}(?:$|[^a-z0-9_])`,"i").test(l),d=[...l.matchAll(/\bmcp(?:__|_)[a-z0-9_]+/gi)].map(e=>e[0]),m=new Set(r.flatMap(e=>[`mcp__thinkpool__${e}`,`mcp_thinkpool_${e}`])),u=[...new Set(d.filter(e=>!m.has(e)))];return{inventory:l,missing:[...s.filter(e=>!a(e)),...r.filter(e=>!a(`mcp__thinkpool__${e}`)&&!a(`mcp_thinkpool_${e}`))],forbidden:[...t.allBuiltinTools.filter(e=>!n.includes(e)&&a(e)),...u]}}async function Me(){const e=await Pe();if(!e.missing.length&&!e.forbidden.length)return e;const t=[e.missing.length?`missing ${e.missing.join(", ")}`:"",e.forbidden.length?`forbidden ${e.forbidden.join(", ")}`:""].filter(Boolean).join("; ");throw new Error(`Hermes thinkpool MCP readiness failed; ${t}`)}async function Fe(){if(ne)throw new Error("Hermes session is closed");if(ie)throw new Error("Hermes ACP process crashed; this lane must be restarted before sending another turn");if(Z?.alive&&ee)return;if(oe)return oe;const e="function"==typeof G?G():{ok:!0};if(!1===e?.ok)throw new Error(e.reason||"Host memory is critically low. This Hermes runtime was not started.");return oe=(async()=>{if(!re&&O)try{Y=O()||Y}catch{}const e=M?.thinkpool;e&&!te&&(te=await D({sdkServer:e}));let o=hermesChildEnv(I),n=q,r=A;if("thinkpool"===q&&j===x){const e=i({command:q,env:o,strictBootstrap:!0});if(!e.available)throw new Error(`Hermes profile safety check failed: ${e.reason||"runtime unavailable"}`);n=e.python,r=[e.bootstrap],o={...o,HERMES_HOME:e.profile,THINKPOOL_HERMES_ACP_POLICY:d(ve(),{mcpTools:Re()})}}let l=!1;he=()=>{l=!0};const a={protocolVersion:1,clientCapabilities:{fs:{readTextFile:!1,writeTextFile:!1},terminal:!1},clientInfo:{name:"thinkpool-pair",title:"thinkpool Code",version:"1"}};let c;for(let e=0;e<2;e++){l=!1,he=()=>{l=!0},Z=j({command:n,args:r,cwd:Y,env:o,onNotification:Te,onRequest:$e,onStderr:e=>{me=(me+e).slice(-2e3)},onClose:e=>{ne||ie||l||(ie=!0,se=!1,He({kind:"error",message:`Hermes ACP closed unexpectedly: ${e?.message||e}`,recoverable:!0}))}}),Z.start();try{c=await Z.request("initialize",a,15e3);break}catch(t){if(ye||0!==e||!/^initialize timed out\b/i.test(String(t?.message||t)))throw t;he();const o=Z;Z=null,o?.end(),me=""}}if(1!==c?.protocolVersion)throw new Error(`Unsupported Hermes ACP protocol ${c?.protocolVersion??"unknown"}; expected 1`);const m={cwd:t.resolve(Y),mcpServers:te?[(u=te.url,{type:"http",name:"thinkpool",url:u,headers:[]})]:[]};var u;let p;if(ee=new s({onEvent:He,sessionId:X,model:J}),X){if(!c?.agentCapabilities?.sessionCapabilities?.resume)throw new Error(`Hermes session ${X} cannot be resumed by this ACP release; no fresh session was substituted`);try{ge=!0,p=await Z.request("session/resume",{...m,sessionId:X},3e4)}catch(e){throw new Error(`Hermes session ${X} is not available; no fresh session was substituted (${e?.message||e})`)}finally{ge=!1}}else p=await Z.request("session/new",m,3e4);if(X||(X=p?.sessionId||null),!X)throw new Error("Hermes ACP did not return a session id");let f=await Pe(),h=f.missing;if(h.length&&c?.agentCapabilities?.sessionCapabilities?.resume){try{ge=!0,p=await Z.request("session/resume",{...m,sessionId:X},3e4)}finally{ge=!1}f=await Pe(),h=f.missing}if(h.length||f.forbidden.length){const e=[h.length?`missing ${h.join(", ")}`:"",f.forbidden.length?`forbidden ${f.forbidden.join(", ")}`:""].filter(Boolean).join("; ");throw new Error(`Hermes thinkpool MCP readiness failed; ${e}`)}J=W||p?.models?.currentModelId||null||J,W&&(await Z.request("session/set_model",{sessionId:X,modelId:J}),await Me()),Ie&&await Z.request("session/set_config_option",{sessionId:X,configId:HERMES_EFFORT_CONFIG_ID,value:Ie});const g=p?.models?{...p.models,currentModelId:J}:J?{currentModelId:J,availableModels:[]}:p?.models;Ee?(ee.sessionId=X,ee.model=J,Ee=!1):ee.startSession({sessionId:X,models:g,modes:p?.modes,commands:[]}),re=!0;const w=Ce(Q);p?.modes?.availableModes?.some(e=>e.id===w)&&p.modes.currentModeId!==w&&await Z.request("session/set_mode",{sessionId:X,modeId:w}),He({kind:"capabilities",runtime:"hermes",protocol:"acp",protocolVersion:c?.protocolVersion,capabilities:c?.agentCapabilities||{},models:k(p?.models),flow:"thinkpool"===q&&j===x})})().catch(async e=>{ie=!0,Z?.end();try{await(te?.close())}catch{}throw te=null,ye||He({kind:"error",message:`Hermes ACP startup failed: ${e?.message||e}${me?`: ${me.trim().slice(-240)}`:""}`,recoverable:!0}),e}).finally(()=>{oe=null,ye=!1}),oe}function Oe(){ee?ee.finishTurn({stopReason:"cancelled"}):He({kind:"result",subtype:"aborted",sessionId:X,model:J,costUsd:null,usage:null,numTurns:1,durationMs:void 0,denials:0,resultText:null})}function qe(e,t={},{promptIndex:o=0,forceFull:n=!1}={}){const s=String(e);if(/^\s*\//.test(s))return[{type:"text",text:s}];const r=_({promptIndex:o,forceFull:n}),i=[{type:"text",text:buildHermesPromptText({text:s,firstTurn:le,terminalRolePrompt:T,rolePrompt:P,roomContext:()=>de({force:r}),promptIndex:o,forceFullReminder:n})}];for(const e of Array.isArray(t.images)?t.images:[])try{i.push(b(e))}catch{}return i}async function Ae(e,t={},{steering:n=!1,turnId:s=pe,promptIndex:r=0,forceFull:i=!1}={}){try{await Fe()}catch(e){if(fe.has(s))return{stopReason:"cancelled"};throw e}if(fe.has(s))return{stopReason:"cancelled"};const l=n?`/steer ${String(e)}`:String(e);let a;try{a=await Z.request("session/prompt",{sessionId:X,prompt:qe(l,t,{promptIndex:r,forceFull:i}),messageId:o()},0)}catch(e){if(fe.has(s))return ne||await async function(){const e=Z;he(),Z=null,ee=null,e?.end(),await Fe()}(),{stopReason:"cancelled"};throw e}if(!n){if(fe.has(s))return a;se=!1,le=!1;const t=/^\s*\/compact\b/i.test(String(e||""))?ee.compactOutcome():null;t&&He({kind:"compaction",trigger:"manual",...t});const o=String(e||"").match(/^\s*\/reasoning\s+(low|medium|high|xhigh|max|none|reset)\s*$/i);if(o){const e=o[1].toLowerCase();("reset"===e?/Session reasoning reset to model default/i.test(ee.text):new RegExp(`Session reasoning set to ${e}`,"i").test(ee.text))&&(Ie="reset"===e?null:e,He({kind:"effort",level:Ie}))}ke=Se.length>0&&!ne&&!fe.has(s),ee.finishTurn({stopReason:a?.stopReason,usage:a?.usage}),ke&&async function(){if(!(be||ne||se)){be=!0;try{for(;Se.length&&!ne&&!se;){ke=!1;const e=Se.shift(),t=++pe;se=!0;try{K?.(e.options||{})}catch{}try{await Ae(e.text,e.options,{steering:!1,turnId:t,promptIndex:e.promptIndex,forceFull:e.forceFull})}catch(e){se=!1,He({kind:"error",message:`Hermes queued turn failed: ${e?.message||e}`,recoverable:!0})}}}finally{be=!1,ke=!1}}}()}return a}return L||queueMicrotask(()=>{Fe().catch(()=>{})}),{get sessionId(){return X},get turnActive(){return se},get queuedDepth(){return Se.length},get canSteer(){return!!Z?.alive},get started(){return re},get models(){return[]},get effort(){return Ie},sendTurn(e,t={}){if(ne)return!1;if(!Z?.alive&&!oe){const e="function"==typeof G?G():{ok:!0};if(!1===e?.ok)return He({kind:"error",message:e.reason||"Host memory is critically low. This Hermes runtime was not started.",recoverable:!0}),!1}const o=ae++,n=ce;ce=/^\s*\/(?:compact|reset|clear)\b/i.test(String(e||"")),ie&&!ne&&(he(),Z?.end(),Z=null,ee=null,ie=!1,me="");const s=String(e||"").match(/^\s*\/queue\s+([\s\S]+)$/i);if(ke)return Se.push({text:s?s[1]:String(e||""),options:t,promptIndex:o,forceFull:n}),He({kind:"queue-add",queued:!0,depth:Se.length}),!0;if(se)return s?(Se.push({text:s[1],options:t,promptIndex:o,forceFull:n}),He({kind:"queue-add",queued:!0,depth:Se.length}),!0):(Ae(e,t,{steering:!0,turnId:pe,promptIndex:o,forceFull:n}).catch(e=>He({kind:"error",message:`Hermes steering failed: ${e?.message||e}`,recoverable:!0})),!0);const r=++pe;return se=!0,ue=ue.then(()=>Ae(e,t,{steering:!1,turnId:r,promptIndex:o,forceFull:n})).catch(e=>{se=!1,He({kind:"error",message:`Hermes turn failed: ${e?.message||e}`,recoverable:!0})}),!0},async abort(){if(!se)return;Se.length=0,ke=!1;const e=pe;return fe.add(e),fe.size>32&&fe.delete(fe.values().next().value),se=!1,le=!1,oe?(ye=!0,he(),Z?.end(),Oe(),void await ue.catch(()=>{})):X&&Z?.alive?(Oe(),await Z.notify("session/cancel",{sessionId:X}).catch(e=>{ie=!0,Z?.end(),He({kind:"error",message:`Hermes cancellation delivery failed; ACP process stopped: ${e?.message||e}`,recoverable:!0})}),void await ue.catch(()=>{})):(Oe(),void await ue.catch(()=>{}))},end(){ne=!0,se=!1,He.cancel(),Se.length=0,ke=!1,X&&Z?.alive?Z.request("session/close",{sessionId:X},1500).catch(()=>{}).finally(()=>Z?.end()):Z?.end(),te?.close().catch(()=>{})},setModel(e){if(!e||se||_e)return!1;const t=String(e);return _e=!0,Fe().then(()=>Z.request("session/set_model",{sessionId:X,modelId:t})).then(async()=>{await Me(),ne||(J=t,ee&&(ee.model=J),He({kind:"model-switch",status:"success",model:J,requested:t}))}).catch(e=>{ne||He({kind:"model-switch",status:"failed",model:J,requested:t,message:`Hermes model switch failed: ${e?.message||e}`})}).finally(()=>{_e=!1}),!0},setMode(e){if(se||!HERMES_SUPPORTED_MODES.has(e))return!1;const t=Q;if("plan"===t!=("plan"===e)){if(Q=e,Ee=re,re){he();const e=Z;Z=null,ee=null,re=!1,ie=!1,me="",e?.end()}return Fe().then(()=>He({kind:"mode",mode:Q})).catch(e=>{Q=t,Ee=!1,He({kind:"error",message:`Hermes mode switch failed: ${e?.message||e}`,recoverable:!0})}),!0}return Fe().then(()=>Z.request("session/set_mode",{sessionId:X,modeId:Ce(e)})).then(()=>{Q=e,He({kind:"mode",mode:Q})}).catch(e=>He({kind:"error",message:`Hermes mode switch failed: ${e?.message||e}`,recoverable:!0})),!0},setEffort(e){if(se||!HERMES_EFFORT_LEVELS.has(e))return!1;const t=String(e);return Fe().then(()=>Z.request("session/set_config_option",{sessionId:X,configId:HERMES_EFFORT_CONFIG_ID,value:t})).then(()=>{Ie=t,He({kind:"effort",level:Ie})}).catch(e=>He({kind:"error",message:`Hermes reasoning change failed: ${e?.message||e}`,recoverable:!0})),!0},clearContext(){return!se&&this.sendTurn("/reset")},warm(){Fe().catch(()=>{})}}}
|
package/hermes-setup.mjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import e from"node:fs";import o from"node:os";import t from"node:path";import{execFileSync as r}from"node:child_process";import i from"yaml";import{probeHermesRuntime as n}from"./hermes-probe.mjs";export const HERMES_PROFILE="thinkpool";export const HERMES_PROVIDER="nous";export const HERMES_DEFAULT_MODEL="openai/gpt-5.6-luna";export const HERMES_GUARD_FILENAME="hermes-delegation-guard.mjs";const s=e=>String(e||"").replace(/[\r\n]+/g," ").trim(),l=e=>`node ${JSON.stringify(e)}`,a=e=>null!=e&&"object"==typeof e&&!Array.isArray(e);export function hermesGuardPath({homeDir:e=o.homedir(),env:r=process.env,platform:i=process.platform}={}){const n="win32"===i?r.LOCALAPPDATA||t.join(e,"AppData","Local"):r.XDG_DATA_HOME||t.join(e,".local","share");return t.join(n,"thinkpool-pair",HERMES_GUARD_FILENAME)}function m(o,r,{fsImpl:i=e,mode:n=384}={}){i.mkdirSync(t.dirname(o),{recursive:!0});const s=`${o}.tmp-${process.pid}-${Date.now()}`;try{i.writeFileSync(s,r,{mode:n}),i.chmodSync?.(s,n),i.renameSync(s,o)}finally{try{i.rmSync(s,{force:!0})}catch{}}}function c(e,o){return o.existsSync(e)?{content:o.readFileSync(e),mode:511&o.statSync(e).mode,atimeMs:o.statSync(e).atimeMs,mtimeMs:o.statSync(e).mtimeMs}:null}function p(e,o,t){o?(m(e,o.content,{fsImpl:t,mode:o.mode}),t.utimesSync?.(e,o.atimeMs/1e3,o.mtimeMs/1e3)):t.rmSync(e,{force:!0})}function h(e,o){const t=o.replace(/[.*+?^${}()|[\]\\]/g,"\\$&");return new RegExp(`Alias:\\s+${t}\\s+.*hermes\\s+-p\\s+${t}`,"i").test(String(e||""))}export function setupHermesRuntime({mode:d,profile:f="thinkpool",command:u="hermes",env:g=process.env,homeDir:E=o.homedir(),platform:w=process.platform,execFile:k=r,fsImpl:_=e,guardSource:y=e.readFileSync(new URL("./hermes-delegation-guard.mjs",import.meta.url),"utf8")}={}){if("thinkpool"!==f)throw new Error("
|
|
1
|
+
import e from"node:fs";import o from"node:os";import t from"node:path";import{execFileSync as r}from"node:child_process";import i from"yaml";import{probeHermesRuntime as n}from"./hermes-probe.mjs";export const HERMES_PROFILE="thinkpool";export const HERMES_PROVIDER="nous";export const HERMES_DEFAULT_MODEL="openai/gpt-5.6-luna";export const HERMES_GUARD_FILENAME="hermes-delegation-guard.mjs";const s=e=>String(e||"").replace(/[\r\n]+/g," ").trim(),l=e=>`node ${JSON.stringify(e)}`,a=e=>null!=e&&"object"==typeof e&&!Array.isArray(e);export function hermesGuardPath({homeDir:e=o.homedir(),env:r=process.env,platform:i=process.platform}={}){const n="win32"===i?r.LOCALAPPDATA||t.join(e,"AppData","Local"):r.XDG_DATA_HOME||t.join(e,".local","share");return t.join(n,"thinkpool-pair",HERMES_GUARD_FILENAME)}function m(o,r,{fsImpl:i=e,mode:n=384}={}){i.mkdirSync(t.dirname(o),{recursive:!0});const s=`${o}.tmp-${process.pid}-${Date.now()}`;try{i.writeFileSync(s,r,{mode:n}),i.chmodSync?.(s,n),i.renameSync(s,o)}finally{try{i.rmSync(s,{force:!0})}catch{}}}function c(e,o){return o.existsSync(e)?{content:o.readFileSync(e),mode:511&o.statSync(e).mode,atimeMs:o.statSync(e).atimeMs,mtimeMs:o.statSync(e).mtimeMs}:null}function p(e,o,t){o?(m(e,o.content,{fsImpl:t,mode:o.mode}),t.utimesSync?.(e,o.atimeMs/1e3,o.mtimeMs/1e3)):t.rmSync(e,{force:!0})}function h(e,o){const t=o.replace(/[.*+?^${}()|[\]\\]/g,"\\$&");return new RegExp(`Alias:\\s+${t}\\s+.*hermes\\s+-p\\s+${t}`,"i").test(String(e||""))}export function setupHermesRuntime({mode:d,profile:f="thinkpool",command:u="hermes",env:g=process.env,homeDir:E=o.homedir(),platform:w=process.platform,execFile:k=r,fsImpl:_=e,guardSource:y=e.readFileSync(new URL("./hermes-delegation-guard.mjs",import.meta.url),"utf8")}={}){if("thinkpool"!==f)throw new Error("thinkpool only manages the isolated Hermes profile thinkpool");if(null!=d&&"clone"!==d&&"clean"!==d)throw new Error("Hermes setup mode must be clone or clean");const S=(e,o={})=>k(u,e,{encoding:"utf8",env:o.env||g,timeout:o.timeout||2e4,stdio:["ignore","pipe","pipe"]}),v=["-p",f];let A="",H=!1;try{A=S(["profile","show",f])}catch{}if(!/Profile:\s*thinkpool/i.test(A)){if(!d)throw new Error("No isolated Hermes thinkpool profile exists; choose --clone or --clean");const e=["profile","create",f,"--description","Isolated thinkpool Code runtime."];"clone"===d?e.push("--clone"):e.push("--no-skills"),S(e,{timeout:6e4}),H=!0,A=S(["profile","show",f])}if(!h(A,f)&&(S(["profile","alias",f,"--name",f]),A=S(["profile","show",f]),!h(A,f)))throw new Error("Hermes did not create the required thinkpool profile alias");const M=s(S([...v,"config","path"]));if(!M)throw new Error("Hermes did not report the thinkpool profile config path");const R=M.startsWith("~")?t.join(E,M.slice(1).replace(/^[/\\]/,"")):t.resolve(M),D=String(A).match(/^Path:\s*(.+)$/im);if(!D)throw new Error("Hermes did not report the isolated thinkpool profile path");const x=D[1].trim().startsWith("~")?t.join(E,D[1].trim().slice(1).replace(/^[/\\]/,"")):t.resolve(D[1].trim()),$=t.relative(x,R);if($.startsWith("..")||t.isAbsolute($))throw new Error("Hermes reported a config path outside the isolated thinkpool profile");let b={};if(_.existsSync(R)){const e=i.parse(_.readFileSync(R,"utf8"));if(null!=e&&!a(e))throw new Error("Hermes thinkpool config is not a YAML mapping; refusing to overwrite it");b=e||{}}if(null!=b.hooks&&!a(b.hooks))throw new Error("Hermes thinkpool hooks config is not a mapping; refusing to overwrite it");if(null!=b.model&&!a(b.model))throw new Error("Hermes thinkpool model config is not a mapping; refusing to overwrite it");const L=b.model||={};L.provider="nous",L.default=HERMES_DEFAULT_MODEL,delete L.base_url,delete L.api_mode;const F=b.hooks||={};if(null!=F.pre_tool_call&&!Array.isArray(F.pre_tool_call))throw new Error("Hermes pre_tool_call hooks are not a list; refusing to overwrite them");const P=F.pre_tool_call||[];F.pre_tool_call=[...P.filter(e=>!(e=>a(e)&&"^delegate_task$"===e.matcher&&new RegExp(`${HERMES_GUARD_FILENAME.replaceAll(".","\\.")}$`).test(String(e.command||"").replace(/["']/g,"")))(e)),{matcher:"^delegate_task$",command:l(hermesGuardPath({homeDir:E,env:g,platform:w})),timeout:5}];const j=hermesGuardPath({homeDir:E,env:g,platform:w}),I=t.join(x,"shell-hooks-allowlist.json"),O=c(j,_),G=c(R,_),U=c(I,_);try{m(j,y,{fsImpl:_,mode:448}),m(R,i.stringify(b,{lineWidth:0}),{fsImpl:_,mode:384});const e={...g,HERMES_ACCEPT_HOOKS:"1"};S([...v,"hooks","revoke",l(j)],{env:e});const o=S([...v,"hooks","test","pre_tool_call","--for-tool","delegate_task"],{env:e});if(!/(?:action|decision)["']?\s*[:=]\s*["']block/i.test(o)||!/thinkpool blocks hidden Hermes delegation/i.test(o))throw new Error("Hermes ran the thinkpool delegation guard but did not report an explicit block");S([...v,"hooks","doctor"],{env:e}),S([...v,"acp","--check"],{env:e});const t=n({command:u,prefixArgs:v,execFile:k,env:e});if(!t.available)throw new Error(`Hermes setup did not pass the final runtime probe: ${t.reason||"unavailable"}`);return{...t,profile:f,created:H,mode:H?d:"existing",guardPath:j,configPath:R}}catch(e){try{p(R,G,_),p(j,O,_),p(I,U,_)}catch(o){e.message=`${e.message}; rollback also failed: ${s(o.message)}`}throw e}}
|
package/host-memory.mjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import e from"node:os";import r from"node:fs";import{execFileSync as t}from"node:child_process";const o=1048576;function n(e,r){const t=Number(e);return Number.isFinite(t)?t:r}export function parseMacMemoryPressure(e){const r=String(e||"").match(/System-wide memory free percentage:\s*([0-9]+(?:\.[0-9]+)?)%/i);return r?Number(r[1]):null}export function parseLinuxMemAvailable(e){const r=String(e||""),t=r.match(/^MemAvailable:\s*(\d+)\s*kB\s*$/im),o=r.match(/^MemTotal:\s*(\d+)\s*kB\s*$/im);return t&&o?{availableBytes:1024*Number(t[1]),totalBytes:1024*Number(o[1]),source:"proc-meminfo"}:null}export function memoryDecision(e,{blocked:r=!1,env:t=process.env}={}){if("1"===t.TP_MEMORY_GUARD_OFF)return{ok:!0,blocked:!1,source:"disabled"};if(!e)return{ok:!0,blocked:!1,source:"unavailable"};const l=Math.max(1,n(t.TP_MEMORY_BLOCK_PERCENT,8)),a=Math.max(l+1,n(t.TP_MEMORY_RELEASE_PERCENT,12)),s=Math.max(64*o,n(t.TP_MEMORY_BLOCK_MIB,512)*o),i=Math.max(s+o,n(t.TP_MEMORY_RELEASE_MIB,1024)*o),c=Number.isFinite(e.availableBytes)?e.availableBytes:null,u=Number.isFinite(e.totalBytes)&&e.totalBytes>0?e.totalBytes:null,m=Number.isFinite(e.availablePercent)?e.availablePercent:null!=c&&u?c/u*100:null,y="macos-memory-pressure"===e.source,b=r?!(y?null!=m&&m>=a:null!=m&&null!=c&&(m>=a||c>=i)):y?null!=m&&m<=l:null!=m&&null!=c&&m<=l&&c<=s;return{ok:!b,blocked:b,source:e.source||"unknown",availablePercent:m,availableBytes:c,blockPercent:l,releasePercent:a}}export function createHostMemoryGuard({platform:o=process.platform,env:n=process.env,execFile:l=t,readFile:a=r.readFileSync,freeMemory:s=e.freemem,totalMemory:i=e.totalmem,now:c=Date.now,cacheMs:u=5e3}={}){let m=null,y=0,b=!1;return function(e="start new work"){const r=c();if(!m||r-y>=u){const e=memoryDecision((()=>{if("darwin"===o)try{const e=parseMacMemoryPressure(l("/usr/bin/memory_pressure",["-Q"],{encoding:"utf8",timeout:1500,stdio:["ignore","pipe","ignore"]}));if(null!=e)return{availablePercent:e,source:"macos-memory-pressure"}}catch{}else if("linux"===o)try{const e=parseLinuxMemAvailable(a("/proc/meminfo","utf8"));if(e)return e}catch{}try{return{availableBytes:s(),totalBytes:i(),source:"os-freemem"}}catch{return null}})(),{blocked:b,env:n});b=e.blocked,m=e,y=r}if(m.ok)return m;const t=null==m.availablePercent?"critically low":`${Math.round(m.availablePercent)}% available`;return{...m,reason:`Host memory is critically low (${t}).
|
|
1
|
+
import e from"node:os";import r from"node:fs";import{execFileSync as t}from"node:child_process";const o=1048576;function n(e,r){const t=Number(e);return Number.isFinite(t)?t:r}export function parseMacMemoryPressure(e){const r=String(e||"").match(/System-wide memory free percentage:\s*([0-9]+(?:\.[0-9]+)?)%/i);return r?Number(r[1]):null}export function parseLinuxMemAvailable(e){const r=String(e||""),t=r.match(/^MemAvailable:\s*(\d+)\s*kB\s*$/im),o=r.match(/^MemTotal:\s*(\d+)\s*kB\s*$/im);return t&&o?{availableBytes:1024*Number(t[1]),totalBytes:1024*Number(o[1]),source:"proc-meminfo"}:null}export function memoryDecision(e,{blocked:r=!1,env:t=process.env}={}){if("1"===t.TP_MEMORY_GUARD_OFF)return{ok:!0,blocked:!1,source:"disabled"};if(!e)return{ok:!0,blocked:!1,source:"unavailable"};const l=Math.max(1,n(t.TP_MEMORY_BLOCK_PERCENT,8)),a=Math.max(l+1,n(t.TP_MEMORY_RELEASE_PERCENT,12)),s=Math.max(64*o,n(t.TP_MEMORY_BLOCK_MIB,512)*o),i=Math.max(s+o,n(t.TP_MEMORY_RELEASE_MIB,1024)*o),c=Number.isFinite(e.availableBytes)?e.availableBytes:null,u=Number.isFinite(e.totalBytes)&&e.totalBytes>0?e.totalBytes:null,m=Number.isFinite(e.availablePercent)?e.availablePercent:null!=c&&u?c/u*100:null,y="macos-memory-pressure"===e.source,b=r?!(y?null!=m&&m>=a:null!=m&&null!=c&&(m>=a||c>=i)):y?null!=m&&m<=l:null!=m&&null!=c&&m<=l&&c<=s;return{ok:!b,blocked:b,source:e.source||"unknown",availablePercent:m,availableBytes:c,blockPercent:l,releasePercent:a}}export function createHostMemoryGuard({platform:o=process.platform,env:n=process.env,execFile:l=t,readFile:a=r.readFileSync,freeMemory:s=e.freemem,totalMemory:i=e.totalmem,now:c=Date.now,cacheMs:u=5e3}={}){let m=null,y=0,b=!1;return function(e="start new work"){const r=c();if(!m||r-y>=u){const e=memoryDecision((()=>{if("darwin"===o)try{const e=parseMacMemoryPressure(l("/usr/bin/memory_pressure",["-Q"],{encoding:"utf8",timeout:1500,stdio:["ignore","pipe","ignore"]}));if(null!=e)return{availablePercent:e,source:"macos-memory-pressure"}}catch{}else if("linux"===o)try{const e=parseLinuxMemAvailable(a("/proc/meminfo","utf8"));if(e)return e}catch{}try{return{availableBytes:s(),totalBytes:i(),source:"os-freemem"}}catch{return null}})(),{blocked:b,env:n});b=e.blocked,m=e,y=r}if(m.ok)return m;const t=null==m.availablePercent?"critically low":`${Math.round(m.availablePercent)}% available`;return{...m,reason:`Host memory is critically low (${t}). thinkpool did not ${e}. Existing lanes stay connected; close a lane or free memory, then retry.`}}}export const hostMemoryAdmission=createHostMemoryGuard();
|
package/keep-awake.mjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import e from"node:os";import r from"node:fs";import t from"node:path";import{spawn as n}from"node:child_process";import{fileURLToPath as o}from"node:url";function i(r=e.homedir()){return t.join(r,".thinkpool-pair","settings.json")}export function loadKeepAwakePreference({home:t=e.homedir(),fsImpl:n=r}={}){try{return!0===JSON.parse(n.readFileSync(i(t),"utf8")).keepAwake}catch{return!1}}export function saveKeepAwakePreference(n,{home:o=e.homedir(),fsImpl:a=r}={}){const s=i(o),l=t.dirname(s);let c={};try{c=JSON.parse(a.readFileSync(s,"utf8"))||{}}catch{}const p={...c,keepAwake:!0===n},u=`${s}.tmp.${process.pid}.${Date.now()}`;try{return a.mkdirSync(l,{recursive:!0}),a.writeFileSync(u,JSON.stringify(p,null,2)+"\n",{mode:384}),a.renameSync(u,s),!0}catch{try{a.rmSync(u,{force:!0})}catch{}return!1}}export function explicitKeepAwakeChoice(e=[]){const r=e.indexOf("--"),t=r>=0?e.slice(0,r):e;return!t.includes("--no-keep-awake")&&(!!t.includes("--keep-awake")||null)}export function keepAwakeEnabled(e=[],{env:r=process.env,loadPreference:t=loadKeepAwakePreference}={}){const n=explicitKeepAwakeChoice(e);return null!==n?n:"1"===r.THINKPOOL_PAIR_KEEP_AWAKE||"0"!==r.THINKPOOL_PAIR_KEEP_AWAKE&&!0===t()}const a=e=>['$ErrorActionPreference = "Stop"',"Add-Type -TypeDefinition 'using System; using System.Runtime.InteropServices; public static class ThinkpoolPower { [DllImport(\"kernel32.dll\")] public static extern uint SetThreadExecutionState(uint flags); }'","[ThinkpoolPower]::SetThreadExecutionState([uint32]0x80000001) | Out-Null",`try { while (Get-Process -Id ${e} -ErrorAction SilentlyContinue) { Start-Sleep -Seconds 5 } }`,"finally { [ThinkpoolPower]::SetThreadExecutionState([uint32]0x80000000) | Out-Null }"].join("; ");export function keepAwakeCommand(e,r,{node:t=process.execPath,modulePath:n=o(import.meta.url)}={}){if(!Number.isInteger(r)||r<=0)throw new Error("keep-awake requires a live bridge pid");return"darwin"===e?{command:"/usr/bin/caffeinate",args:["-i","-w",String(r)],adapter:"caffeinate"}:"linux"===e?{command:"systemd-inhibit",args:["--what=sleep","--mode=block","--who=
|
|
1
|
+
import e from"node:os";import r from"node:fs";import t from"node:path";import{spawn as n}from"node:child_process";import{fileURLToPath as o}from"node:url";function i(r=e.homedir()){return t.join(r,".thinkpool-pair","settings.json")}export function loadKeepAwakePreference({home:t=e.homedir(),fsImpl:n=r}={}){try{return!0===JSON.parse(n.readFileSync(i(t),"utf8")).keepAwake}catch{return!1}}export function saveKeepAwakePreference(n,{home:o=e.homedir(),fsImpl:a=r}={}){const s=i(o),l=t.dirname(s);let c={};try{c=JSON.parse(a.readFileSync(s,"utf8"))||{}}catch{}const p={...c,keepAwake:!0===n},u=`${s}.tmp.${process.pid}.${Date.now()}`;try{return a.mkdirSync(l,{recursive:!0}),a.writeFileSync(u,JSON.stringify(p,null,2)+"\n",{mode:384}),a.renameSync(u,s),!0}catch{try{a.rmSync(u,{force:!0})}catch{}return!1}}export function explicitKeepAwakeChoice(e=[]){const r=e.indexOf("--"),t=r>=0?e.slice(0,r):e;return!t.includes("--no-keep-awake")&&(!!t.includes("--keep-awake")||null)}export function keepAwakeEnabled(e=[],{env:r=process.env,loadPreference:t=loadKeepAwakePreference}={}){const n=explicitKeepAwakeChoice(e);return null!==n?n:"1"===r.THINKPOOL_PAIR_KEEP_AWAKE||"0"!==r.THINKPOOL_PAIR_KEEP_AWAKE&&!0===t()}const a=e=>['$ErrorActionPreference = "Stop"',"Add-Type -TypeDefinition 'using System; using System.Runtime.InteropServices; public static class ThinkpoolPower { [DllImport(\"kernel32.dll\")] public static extern uint SetThreadExecutionState(uint flags); }'","[ThinkpoolPower]::SetThreadExecutionState([uint32]0x80000001) | Out-Null",`try { while (Get-Process -Id ${e} -ErrorAction SilentlyContinue) { Start-Sleep -Seconds 5 } }`,"finally { [ThinkpoolPower]::SetThreadExecutionState([uint32]0x80000000) | Out-Null }"].join("; ");export function keepAwakeCommand(e,r,{node:t=process.execPath,modulePath:n=o(import.meta.url)}={}){if(!Number.isInteger(r)||r<=0)throw new Error("keep-awake requires a live bridge pid");return"darwin"===e?{command:"/usr/bin/caffeinate",args:["-i","-w",String(r)],adapter:"caffeinate"}:"linux"===e?{command:"systemd-inhibit",args:["--what=sleep","--mode=block","--who=thinkpool","--why=Keep the thinkpool bridge reachable","--",t,n,"--wait-for",String(r)],adapter:"systemd-inhibit"}:"win32"===e?{command:"powershell.exe",args:["-NoLogo","-NoProfile","-NonInteractive","-WindowStyle","Hidden","-Command",a(r)],adapter:"Windows power request"}:null}export function startKeepAwake({enabled:e,platform:r=process.platform,parentPid:t=process.pid,spawnImpl:o=n,stderr:i=process.stderr,accountChild:a="1"===process.env.THINKPOOL_PAIR_ACCOUNT_CHILD}={}){if(!e||a)return null;let s,l;try{s=keepAwakeCommand(r,t)}catch(e){return i.write(` ⚠ keep-awake unavailable: ${e?.message||e}\n`),null}if(!s)return i.write(` ⚠ keep-awake is not supported on ${r}; bridge startup will continue normally.\n`),null;const c=Date.now();try{return l=o(s.command,s.args,{stdio:"ignore",windowsHide:!0}),l.once?.("spawn",()=>{i.write(` ◆ keep-awake ON via ${s.adapter}: system sleep is inhibited while this bridge runs.\n Your screen can still dim, lock, and turn off normally.\n`)}),l.once?.("error",e=>{i.write(` ⚠ keep-awake could not start (${s.adapter}: ${e?.message||e}); bridge startup will continue normally.\n`)}),l.once?.("exit",e=>{e&&Date.now()-c<3e4&&i.write(` ⚠ keep-awake helper exited early (${s.adapter}, code ${e}); the computer may sleep normally.\n`)}),l.unref?.(),l}catch(e){return i.write(` ⚠ keep-awake could not start (${s.adapter}: ${e?.message||e}); bridge startup will continue normally.\n`),null}}if(process.argv[1]&&t.resolve(process.argv[1])===o(import.meta.url)&&"--wait-for"===process.argv[2]){const s=Number(process.argv[3]);Number.isInteger(s)&&s>0&&await async function(e){for(;;){try{process.kill(e,0)}catch{return}await new Promise(e=>setTimeout(e,5e3))}}(s)}
|