thinkpool-pair 0.7.394 → 0.7.396
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/catastrophic-command-guard.mjs +1 -0
- package/claude-session.mjs +1 -1
- package/codex-app-server.mjs +1 -1
- package/codex-catastrophic-command-hook.mjs +2 -0
- package/codex-event-mapper.mjs +1 -1
- package/codex-session.mjs +1 -1
- package/edit-diff.mjs +1 -1
- package/hermes-acp-bootstrap.py +21 -0
- package/hermes-delegation-guard.mjs +1 -1
- package/hermes-probe.mjs +1 -1
- package/hermes-session.mjs +1 -1
- package/hermes-setup.mjs +1 -1
- package/package.json +4 -1
- package/publish-manifest.json +18 -15
- package/runtime-skill-parity.mjs +1 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import e from"node:path";import{fileURLToPath as t}from"node:url";export const CATASTROPHIC_BLOCK_MESSAGE="Blocked by thinkpool’s non-bypassable catastrophic-command guard.";export const CODEX_CATASTROPHIC_HOOK_SCRIPT=t(new URL("./codex-catastrophic-command-hook.mjs",import.meta.url));const o=/^(?:main|master|production|prod|trunk|stable)$/i,n=/(?:^|_)(?:API_KEY|ACCESS_KEY|SECRET|TOKEN|PASSWORD|PRIVATE_KEY|CREDENTIALS?)(?:$|_)/i,r=/(?:^|\/)(?:\.ssh\/id_(?:rsa|dsa|ecdsa|ed25519)|\.aws\/credentials|\.config\/gcloud\/application_default_credentials\.json|\.netrc|\.npmrc|auth\.json)$/i,s=/^\/dev\/(?:disk\d+|rdisk\d+|sd[a-z]\d*|nvme\d+n\d+(?:p\d+)?|mmcblk\d+(?:p\d+)?)$/i,i=/^\/(?:Users|home|etc|var|usr|System|Library|Applications|Volumes)(?:\/\*+)?\/?$/,c=(e,t,o)=>({blocked:!0,ruleId:e,reason:`${CATASTROPHIC_BLOCK_MESSAGE} ${t}`,command:o}),a=e=>({blocked:!1,ruleId:null,reason:"",command:e});function l(t,a,l){if(/^\s*:\s*\(\s*\)\s*\{\s*:\s*\|\s*:\s*&\s*\}\s*;?\s*:\s*$/.test(t))return c("fork-bomb","Refusing to start a fork bomb.",a.command);const d=function(e){let t=0;for(;t<e.length&&/^[A-Za-z_][A-Za-z0-9_]*=/.test(e[t]);)t++;if("command"!==e[t]&&"builtin"!==e[t]&&"exec"!==e[t]||t++,"sudo"===e[t])for(t++;t<e.length&&e[t].startsWith("-");)t+=["-u","-g","-h","-p","-C","-T","-R"].includes(e[t])?2:1;return e.slice(t)}(function(e){const t=[];let o="",n="",r=!1,s=!1;const i=()=>{s&&t.push(o),o="",s=!1};for(let t=0;t<e.length;t++){const c=e[t];r?(o+=c,r=!1,s=!0):"\\"!==c||"'"===n?n?(c===n?n="":o+=c,s=!0):"'"!==c&&'"'!==c?/\s/.test(c)?i():(o+=c,s=!0):(n=c,s=!0):(r=!0,s=!0)}return i(),t}(t));if(!d.length)return null;const u=e.basename(d[0]).toLowerCase();if(l<3&&["bash","sh","zsh","fish","dash","ksh"].includes(u)){const e=d.findIndex(e=>"-c"===e||"--command"===e);if(e>=0&&d[e+1])return m(d[e+1],a,l+1)}if(l<3&&"eval"===u&&d[1])return m(d.slice(1).join(" "),a,l+1);if("rm"===u||"rmdir"===u){const t=d.slice(1),o="rm"===u&&t.some(e=>"--recursive"===e||/^-[^-]*[rR]/.test(e)),n=t.filter(e=>"-"===e||!e.startsWith("-")).find(t=>function(t,{cwd:o,home:n,recursive:r}){const s=String(t||"").replace(/\/$/,"")||"/";if(["/","/*","/**","~","~/*","$HOME","$HOME/*","${HOME}","${HOME}/*"].includes(s))return!0;if(r&&[".","./*","./**","*","**","..","../*","../**"].includes(s))return!0;if(r&&i.test(s))return!0;if(e.isAbsolute(s)){const t=e.resolve(s.replace(/\/\*+$/,""));if(t===e.resolve(o||".")||n&&t===e.resolve(n))return!0}return!1}(t,{...a,recursive:o}));if(n)return c("broad-filesystem-delete",`Refusing broad deletion target ${JSON.stringify(n)}.`,a.command)}if(/^mkfs(?:\.|$)/.test(u)||"wipefs"===u)return c("raw-disk-format","Refusing to format or wipe a raw device.",a.command);if("diskutil"===u&&d.some(e=>/^erase(?:disk|volume)$/i.test(e)))return c("raw-disk-format","Refusing to erase a disk or volume.",a.command);if("dd"===u){const e=d.find(e=>e.startsWith("of="))?.slice(3);if(e&&s.test(e))return c("raw-disk-write",`Refusing raw-disk output ${e}.`,a.command)}if(d.some(e=>/^>{1,2}/.test(e)&&s.test(e.replace(/^>{1,2}/,""))))return c("raw-disk-write","Refusing shell redirection to a raw device.",a.command);if(["shutdown","reboot","halt","poweroff"].includes(u))return c("machine-power-control",`Refusing machine power command ${u}.`,a.command);if("launchctl"===u&&"reboot"===d[1]?.toLowerCase())return c("machine-power-control","Refusing machine reboot through launchctl.",a.command);if("kill"===u&&d.includes("-9")&&d.includes("-1"))return c("all-process-kill","Refusing to kill every accessible process.",a.command);if("gh"===u&&"repo"===d[1]&&"delete"===d[2])return c("remote-repository-delete","Refusing remote repository deletion.",a.command);const f=function(e,t){const n=e.indexOf("push");if("git"!==e[0]||n<0)return null;const r=e.slice(n+1),s=r.some(e=>"-f"===e||"--force"===e||/^--force=/.test(e)),i=r.includes("--delete")||r.includes("-d"),a=r.flatMap(e=>e.split(":")).map(e=>e.replace(/^refs\/heads\//,"").replace(/^\+/,"")).find(e=>o.test(e));return i&&a?c("remote-protected-branch-delete",`Refusing to delete protected remote branch ${a}.`,t):s&&a?c("remote-protected-force-push",`Refusing to force-rewrite protected remote branch ${a}.`,t):null}(d,a.command);if(f)return f;if(new Set(["cat","head","tail","less","more","sed","awk","grep","rg","base64","openssl","cp","scp","curl"]).has(u)&&d.slice(1).some(e=>r.test(e.replace(/^@/,""))))return c("credential-file-extraction","Refusing shell extraction of a credential file.",a.command);if(["env","printenv","echo","printf"].includes(u)){const e=d.slice(1).find(e=>n.test(e.replace(/^\$\{?|\}?$/g,"")));if(e)return c("secret-environment-extraction",`Refusing shell extraction of secret environment variable ${e}.`,a.command)}return null}function m(e,t,o=0){if(/:\s*\(\s*\)\s*\{[^}]*:\s*\|\s*:\s*&[^}]*\}\s*;\s*:/.test(String(e)))return c("fork-bomb","Refusing to start a fork bomb.",t.command);for(const n of String(e).matchAll(/\$\(([^()]*)\)/g)){const e=m(n[1],t,o+1);if(e?.blocked)return e}for(const n of function(e){const t=[];let o="",n=!1,r="";for(let s=0;s<e.length;s++){const i=e[s];if(n){r+=i,n=!1;continue}if("\\"===i&&"'"!==o){r+=i,n=!0;continue}if(o){r+=i,i===o&&(o="");continue}if("'"===i||'"'===i){o=i,r+=i;continue}const c=e.slice(s,s+2);"\n"!==i&&";"!==i&&"|"!==i&&"&&"!==c&&"||"!==c?r+=i:(r.trim()&&t.push(r.trim()),r="","&&"!==c&&"||"!==c||s++)}return r.trim()&&t.push(r.trim()),t}(String(e))){const e=l(n,t,o);if(e?.blocked)return e}return null}export function commandFromToolInput(e,t){return["Bash","terminal","shell","exec_command"].includes(String(e||""))?"string"==typeof t?t:String(t?.command??t?.cmd??t?.input?.command??""):""}export function inspectCatastrophicCommand({command:e,cwd:t=process.cwd(),home:o=process.env.HOME||""}={}){const n=String(e||"");return n.trim()&&m(n,{command:n,cwd:String(t||process.cwd()),home:String(o||"")})||a(n)}export function inspectCatastrophicTool({toolName:e,input:t,cwd:o,home:n}={}){const r=commandFromToolInput(e,t);return r?inspectCatastrophicCommand({command:r,cwd:o,home:n}):a("")}export function catastrophicPreToolHookDecision({toolName:e,input:t,cwd:o,home:n}={}){const r=inspectCatastrophicTool({toolName:e,input:t,cwd:o,home:n});return r.blocked?{continue:!0,hookSpecificOutput:{hookEventName:"PreToolUse",permissionDecision:"deny",permissionDecisionReason:`${r.reason} Rule: ${r.ruleId}.`}}:null}const d=e=>`'${String(e).replaceAll("'","'\\''")}'`;export function codexCatastrophicHookCliArgs({scriptPath:e=CODEX_CATASTROPHIC_HOOK_SCRIPT,nodePath:t=process.execPath}={}){const o=`${d(t)} ${d(e)}`;return["--dangerously-bypass-hook-trust","-c","features.hooks=true","-c",`hooks.PreToolUse=[{matcher="^Bash$",hooks=[{type="command",command=${JSON.stringify(o)},timeout=5,statusMessage="Checking catastrophic command"}]}]`]}
|
package/claude-session.mjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{randomUUID as e}from"node:crypto";import{createRequire as t}from"node:module";import{readFileSync as o}from"node:fs";import{dirname as n,join as s}from"node:path";import{query as r}from"@anthropic-ai/claude-agent-sdk";import{sanitizeSession as i}from"./transcript-sanitize.mjs";import{reviewGatePreToolDecision as a}from"./flow-review-gate.mjs";import{crossPostNeedsCard as l}from"./cross-terminal.mjs";import{correctContext as c}from"./context-windows.mjs";import{normalizeClaudeCommandCatalog as d}from"./claude-command-catalog.mjs";import{evidenceForToolResult as u}from"./evidence-citations.mjs";import{THINKPOOL_CASCADE_RULE as p,THINKPOOL_REMOTE_DELIVERY_RULES as m,THINKPOOL_RUNTIME_AUTHORITY_RULE as h,THINKPOOL_RUNTIME_TURN_REMINDER as f,buildThinkPoolTurnGuidance as y,createRoomContextSelector as k,normalizeThinkPoolTurnCause as w,usesFullThinkPoolReminder as g}from"./thinkpool-room-prompt.mjs";import{stallDecision as b,stallEvent as v,isCompactTurn as _}from"./turn-stall.mjs";import{createSameTargetResilienceController as T,formatResilienceTrace as S}from"./provider-resilience.mjs";const E=t(import.meta.url);export async function claudeOneShot({prompt:e,model:t="claude-haiku-4-5",cwd:o,env:n,timeoutMs:s=8e3}={}){const i=new AbortController,a=setTimeout(()=>{try{i.abort()}catch{}},s);try{const s=r({prompt:e,options:{model:t,...o?{cwd:o}:{},env:n,maxTurns:1,permissionMode:"bypassPermissions",settingSources:[],strictMcpConfig:!0,mcpServers:{},abortController:i}});let a="";for await(const e of s){if("assistant"===e.type)for(const t of e.message?.content||[])"text"===t.type&&(a+=t.text);if("result"===e.type)break}return a.trim()}finally{clearTimeout(a)}}const N=(()=>{try{let e=n(E.resolve("@anthropic-ai/claude-agent-sdk"));for(let t=0;t<8;t++){try{const t=JSON.parse(o(s(e,"package.json"),"utf8"));if("@anthropic-ai/claude-agent-sdk"===t.name)return t.version}catch{}const t=n(e);if(t===e)break;e=t}}catch{}return"unknown"})(),O=/\brm\s+\S|\brmdir\s+\S|\bgit\s+(push\s+(-f|--force)|reset\s+--hard|clean\s+-[a-z]*f)|\bdrop\s+(table|database)\b|\b(mkfs|dd)\b|\bsudo\b|>\s*\/dev\/|\bchmod\s+-R|\bchown\s+-R|\bkillall\b|\btruncate\b/i,R=new Set(["Read","Grep","Glob","NotebookRead","TodoRead","LS"]),A=new Set(["WebFetch","WebSearch"]),D=new Set(["Edit","Write","MultiEdit","NotebookEdit","TodoWrite"]);export function classifyRisk(e,t){if("Bash"===e){const e=t&&(t.command??t.cmd)||"";return O.test(e)?"high":"medium"}return R.has(e)?"low":A.has(e)?"network":(D.has(e),"medium")}const x=/(^|\/)\.claude\/SESSIONS\/|(^|\/)CLAUDE\.md$/;export function isSafeDocWrite(e,t){if(!D.has(e))return!1;const o=t&&(t.file_path||t.notebook_path)||"";return x.test(o)}export function autoAllow({toolName:e,input:t,mode:o="default",alwaysAllow:n=new Set}){const s=classifyRisk(e,t);return"low"===s||"bypassPermissions"===o||"acceptEdits"===o&&D.has(e)&&"high"!==s||isSafeDocWrite(e,t)||n.has(`${e}:${s}`)}export function askUserQuestionHookOutput(e){const t=e&&"object"==typeof e?e.decision:e,o="string"==typeof t&&t.startsWith("answer:")?t.slice(7):"",n=e&&"object"==typeof e?e.responder:null,s="string"==typeof n?.name&&n.name.trim()?n.name.trim():"string"==typeof n?.id&&n.id?`room member ${n.id}`:"The user";return{continue:!0,hookSpecificOutput:{hookEventName:"PreToolUse",permissionDecision:"deny",permissionDecisionReason:o?`${s} answered in the thinkpool room — ${o}. Treat this as their selection and continue; do not call AskUserQuestion again for the same question.`:"The user dismissed the question in the thinkpool room without selecting. Ask in plain prose, or proceed with a sensible default."}}}export function askUserQuestionRegression(e,t){return Array.isArray(e)?e.includes("AskUserQuestion")?null:`[SDK-REGRESSION] agent SDK v${t||"unknown"} init tools[] is missing 'AskUserQuestion' — the multiple-choice question card is disabled. A caret SDK bump likely changed the canUseTool gate; pin a known-good SDK in bridge/package.json + republish.`:null}function P(){const e=[];let t=null,o=!1;return{stream:async function*(){for(;!o;)e.length?yield e.shift():await new Promise(e=>{t=e})}(),push:n=>!o&&(e.push({type:"user",message:{role:"user",content:n}}),t&&(t(),t=null),!0),end(){o=!0,t&&(t(),t=null)}}}const I=(e=[])=>e.map(e=>"text"===e.type?{type:"text",text:e.text}:"thinking"===e.type?{type:"thinking",text:e.thinking||"",reasoningKind:"thought"}:"tool_use"===e.type?{type:"tool_use",id:e.id,name:e.name,input:e.input}:{type:e.type}).filter(Boolean),C=new Set(["default","acceptEdits","plan","bypassPermissions"]),M=["You are Claude in a thinkpool Code room, driven live from a phone or browser — NOT a local terminal. Keep using the room's features.",f,"TERMINAL HIERARCHY: obey your authoritative TERMINAL ROLE. A person-authored request to open, launch, start, spawn, or create a new, separate, independent, top-level, or main terminal uses open_main_terminal — NEVER spawn_terminal — even for ordinary work or a handoff. If the wording includes new or separate terminal, main-terminal routing wins. Use spawn_terminal only for agent-decided bounded worker slices or when the person explicitly asks for a worker, sub-terminal, Ensemble lane, or delegated slice. If open_main_terminal is unavailable, say so; never substitute an Ensemble child. Leaf, worker, Side, and managed Flow lanes work directly. Never use built-in invisible Task/Agent subagents or hijack a busy sibling.","WORKTREES: parallel lanes share one repo — before code edits run `git worktree list`; if linked worktrees exist, take your OWN worktree + branch, never the shared checkout or a branch another lane is on.","BUILD WORKFLOW (default, no magic word): right-size within your TERMINAL ROLE — a trivial ask or delegated slice you just do; a conductor-capable role with a genuinely decomposable build FIRST writes a short plan in chat, THEN fans worker slices into visible spawn_terminal lanes and verifies them. Worker/leaf/Side/managed Flow roles do not fan out. A person-requested new or separate terminal uses open_main_terminal. Never plan-mode/ExitPlanMode; plans live in chat and lanes in the existing list."].join(" ");export function startClaudeSession({cwd:t,model:o,effort:n="high",resume:s,env:f,mode:E="default",onEvent:O,requestPermission:R,mcpServers:A,crossPostGate:D,crossRoomPostGate:x,didSpawnTarget:U=null,terminalRolePrompt:L,rolePrompt:W,blockSubagents:F=!1,onSubmitPlan:$=null,onLaneDone:j=null,onReviewVerdict:q=null,reviewGate:G=null,lazy:B=!1,roomContext:H=null,suggest:K=!0,prepareCwd:Q=null,admitStart:Y=null,resilienceObserver:z=null,resilience:J=null}){const V=k(H);let X=0,Z=!0;const ee=(e,{promptIndex:t=0,forceFull:o=!1,turnCause:n=null}={})=>{const s=g({promptIndex:t,forceFull:o,turnCause:n}),r=V({force:s}),i=String(L||"").trim();return`<system-reminder>\n${s&&i?`${i}\n\n`:""}${s?M:y({text:e,promptIndex:t,forceFull:o,turnCause:n})}${r?`\n\n${r}`:""}\n</system-reminder>`},te=new AbortController;let oe=P(),ne=s||null,se=s||null,re=null,ie=null,ae=null,le=null,ce=!1,de=!1,ue=null,pe=!1,me=!1,he=!1,fe=null,ye=C.has(E)?E:"default";const ke=new Set,we=new Map,ge=new Set(["low","medium","high","xhigh","max"]);let be=ge.has(n)?n:"high",ve=0,_e=0,Te=o||null,Se=null,Ee=null,Ne=Promise.resolve(),Oe=!1,Re=!1;const Ae=e=>new Promise(t=>setTimeout(t,e)),De=Math.max(3e4,parseInt(process.env.TP_STALL_MS,10)||9e4);let xe=!1,Pe=Date.now(),Ie=!1;const Ce=/connection closed|connection reset|econnreset|etimedout|socket hang up|fetch failed|network error|socket destroyed/i;let Me=0;const Ue=(e,t={})=>{try{z?.({reason:e,restartCount:Me,...t})}catch{}},Le=!(!J?.policy?.enabled||!J?.providerId||"anthropic"===J.providerId);let We=null,Fe=null,$e=!1,je=0,qe=!1,Ge=null;const Be=Math.max(3*De,parseInt(process.env.TP_FORCE_STOP_MS,10)||3e5);let He=!1,Ke=!1;const Qe=Math.max(Be,parseInt(process.env.TP_COMPACT_FORCE_STOP_MS,10)||12e5);let Ye=!1,ze=0;const Je=R;R=async e=>{ze++;try{return await(Je?.(e))}finally{ze=Math.max(0,ze-1),Pe=Date.now()}};let Ve=!1,Xe=null,Ze=null,et=!1,tt="",ot=null;const nt=Math.max(1200,parseInt(process.env.TP_SUGGEST_FALLBACK_MS,10)||2500),st=e=>{try{O?.(e)}catch{}},rt=e=>{Pe=Date.now(),e&&"stalled"!==e.kind&&(Ie=!1),!e||"compaction"!==e.kind&&"result"!==e.kind&&"error"!==e.kind||(Ye=!1),st(e)},it=e=>{try{J?.onRecord?.(e)}catch{}"retrying"!==e?.outcome&&"cap_blocked"!==e?.outcome&&"circuit_open"!==e?.outcome||rt({kind:"note",text:S(e)})},at=()=>{if(null!=Fe){try{(J?.clearTimer||clearTimeout)(Fe)}catch{}Fe=null}},lt=()=>{if(at(),!We)return;const e=Number(J?.policy?.timeoutMs);if(!Number.isFinite(e)||e<=0)return;const t=J?.setTimer||setTimeout;Fe=t(()=>{if(Fe=null,!ce&&We&&xe){We.failure({message:"timeout"}),xe=!1;try{Se?.abort()}catch{}wt()}},e)},ct=async()=>{if(!We)return!0;let e=null;try{e=await We.preflight()}catch{return!1}return!0===e?.admitted},dt=setInterval(()=>{const e=Date.now()-Pe,t=b({turnActive:xe,awaitingUser:ze,quietMs:e,stallMs:De,forceStopMs:Be,stalledSent:Ie,stallRetried:Ke,compacting:Ye,compactForceStopMs:Qe});if("none"===t)return;const o=v(t,e);if(o&&st(o),"status"!==t){if("retry"===t){if(He=!0,We){Ue("resilience_stall",{quietMs:e}),We.failure({message:"timeout"}),xe=!1;try{Se?.abort()}catch{}return void wt()}return Ue("stall_replay",{quietMs:e}),void kt(e)}He=!0,xe=!1}else Ie=!0},5e3);dt.unref?.();const ut=e=>{setTimeout(()=>{Promise.resolve(fe?.setPermissionMode?.(e)).catch(t=>console.error(`[claude-session] setPermissionMode(${e}) failed:`,t?.message||t))},0)},pt={abortController:te,permissionMode:ye,hooks:{PreToolUse:[{hooks:[async t=>{const o=t.tool_name,n=t.tool_input,s=t.effort?.level;s&&s!==be&&(be=s,rt({kind:"effort",level:be}));const r="Write"===o||"Edit"===o||"MultiEdit"===o||"NotebookEdit"===o;if(F&&r&&/(?:^|[/\\])FLOW_PLAN\.json$/i.test(n?.file_path||n?.notebook_path||"")){const e=n?.content??n?.new_string??"";let t={ok:!1,message:"Plan submission is not wired for this session."};try{t=$&&await $(e)||t}catch(e){t={ok:!1,message:`Plan submission failed: ${e?.message||e}`}}return{continue:!0,hookSpecificOutput:{hookEventName:"PreToolUse",permissionDecision:"deny",permissionDecisionReason:t.message}}}if(F&&("Task"===o||"Agent"===o||"Bash"===o||r))return{continue:!0,hookSpecificOutput:{hookEventName:"PreToolUse",permissionDecision:"deny",permissionDecisionReason:"Bash"===o?"Flow conductors do NOT run Bash — it pops a permission card and stalls the flow. Use the Grep tool (not `grep`/`rg`), the Glob tool (not `find`/`ls`), and Read (not `cat`/`head`) — those run with no prompt. Then DECOMPOSE and submit by Writing your task-graph JSON to the file FLOW_PLAN.json.":r?'Flow conductors do NOT write project or plan-document files. Do NOT use ExitPlanMode and do NOT call submit_flow_plan (both hang here). To SUBMIT your decomposition, use the Write tool with file_path "FLOW_PLAN.json" and content = your task-graph JSON ({"summary":"…","tasks":[…]}). That single Write IS how you submit — the room ingests it directly. Do not write any other file.':"Flow conductors do NOT spawn subagents — it stalls the flow on a permission card. Inspect the codebase yourself with Read/Grep/Glob (no prompt). Then DECOMPOSE and submit by Writing your task-graph JSON to the file FLOW_PLAN.json. Do not call Task/Agent again."}};if(q&&("Write"===o||"Edit"===o)&&/(?:^|[/\\])FLOW_REVIEW(\.[a-zA-Z0-9]+)?$/.test(n?.file_path||"")){let e={ok:!1,message:"verdict signal failed"};try{e=await q(n?.content??n?.new_string??"")||e}catch(t){e={ok:!1,message:`verdict signal failed: ${t?.message||t}`}}return{continue:!0,hookSpecificOutput:{hookEventName:"PreToolUse",permissionDecision:"deny",permissionDecisionReason:e.message}}}if(j&&("Write"===o||"Edit"===o)&&/(?:^|[/\\])FLOW_DONE(\.[a-zA-Z0-9]+)?$/.test(n?.file_path||"")){let e={ok:!1,message:"done signal failed"};try{e=await j(n?.content??n?.new_string??"")||e}catch(t){e={ok:!1,message:`done signal failed: ${t?.message||t}`}}return{continue:!0,hookSpecificOutput:{hookEventName:"PreToolUse",permissionDecision:"deny",permissionDecisionReason:e.message}}}const i=a({reviewGate:G,toolName:o,toolInput:n});if(i)return i;if("mcp__thinkpool__read_terminal"===o)return{continue:!0,hookSpecificOutput:{hookEventName:"PreToolUse",permissionDecision:"allow",permissionDecisionReason:"Auto-approved (read-only thinkpool cross-terminal read)."}};if(/^mcp__thinkpool__preview_(start|capture|inspect|stop)$/.test(o))return{continue:!0,hookSpecificOutput:{hookEventName:"PreToolUse",permissionDecision:"allow",permissionDecisionReason:"Auto-approved (contained bridge-owned viewport preview)."}};if("mcp__thinkpool__submit_flow_plan"===o)return{continue:!0,hookSpecificOutput:{hookEventName:"PreToolUse",permissionDecision:"allow",permissionDecisionReason:"Auto-approved (Flow plan submission — broadcasts the task-graph for human approval)."}};if("mcp__thinkpool__list_sessions"===o||"mcp__thinkpool__read_session"===o)return{continue:!0,hookSpecificOutput:{hookEventName:"PreToolUse",permissionDecision:"allow",permissionDecisionReason:"Auto-approved (read-only thinkpool cross-session read — your own rooms)."}};if("mcp__thinkpool__close_terminal"===o)return{continue:!0,hookSpecificOutput:{hookEventName:"PreToolUse",permissionDecision:"allow",permissionDecisionReason:"Auto-approved (thinkpool cross-terminal close — only self-spawned lanes)."}};if("mcp__thinkpool__spawn_terminal"===o)return{continue:!0,hookSpecificOutput:{hookEventName:"PreToolUse",permissionDecision:"allow",permissionDecisionReason:"Auto-approved (autonomous thinkpool worker; bounded by room caps and isolated in its own worktree)."}};if("mcp__thinkpool__open_main_terminal"===o){const t=n?.mode||ye;if("bypassPermissions"!==t||"bypassPermissions"===ye)return{continue:!0,hookSpecificOutput:{hookEventName:"PreToolUse",permissionDecision:"allow",permissionDecisionReason:`Auto-approved (open a fresh thinkpool terminal in ${t} mode — inherited, no escalation; bounded by room caps).`}};let s="deny";try{s=await(R?.({id:e(),toolName:o,input:{...n,mode:t},risk:"high"}))??"deny"}catch{s="deny"}const r="allow"===s;return{continue:!0,hookSpecificOutput:{hookEventName:"PreToolUse",permissionDecision:r?"allow":"deny",permissionDecisionReason:r?"Bypass terminal creation approved in the thinkpool room — the new terminal runs autonomously.":"Bypass terminal creation denied in the room — do not retry as bypass; open it without mode:bypassPermissions or ask what to do."}}}if("mcp__thinkpool__post_to_terminal"===o){const t=D?D():{ok:!0};if(!t.ok)return{continue:!0,hookSpecificOutput:{hookEventName:"PreToolUse",permissionDecision:"deny",permissionDecisionReason:t.reason||"Cross-terminal post not allowed right now."}};const s=!!U&&!!U(n?.terminal);if(!l({mode:ye,spawnedByMe:s}))return{continue:!0,hookSpecificOutput:{hookEventName:"PreToolUse",permissionDecision:"allow",permissionDecisionReason:"bypassPermissions"===ye?"Auto-approved (cross-terminal post in bypassPermissions mode — bypass means bypass).":"Auto-approved (cross-terminal post into a lane you spawned — parent→child orchestration, no per-post tap)."}};let r="deny";try{r=await(R?.({id:e(),toolName:o,input:n,risk:"high"}))??"deny"}catch{r="deny"}const i="allow"===r;return{continue:!0,hookSpecificOutput:{hookEventName:"PreToolUse",permissionDecision:i?"allow":"deny",permissionDecisionReason:i?"Cross-terminal post approved in the thinkpool room.":"Cross-terminal post denied in the room — do not retry; ask what to do instead."}}}if("mcp__thinkpool__post_to_session"===o){const t=x?x():{ok:!0};if(!t.ok)return{continue:!0,hookSpecificOutput:{hookEventName:"PreToolUse",permissionDecision:"deny",permissionDecisionReason:t.reason||"Cross-room post not allowed right now."}};let s="deny";try{s=await(R?.({id:e(),toolName:o,input:n,risk:"high"}))??"deny"}catch{s="deny"}const r="allow"===s;return{continue:!0,hookSpecificOutput:{hookEventName:"PreToolUse",permissionDecision:r?"allow":"deny",permissionDecisionReason:r?"Cross-room post approved in the sending room — the target room will be asked to accept it.":"Cross-room post denied in the room — do not retry; ask what to do instead."}}}if("ExitPlanMode"===o){if("plan"!==ye)return ut(ye),rt({kind:"mode",mode:ye}),{continue:!0,hookSpecificOutput:{hookEventName:"PreToolUse",permissionDecision:"allow",permissionDecisionReason:`The thinkpool room is in ${ye} mode, not Plan — the user did not ask for a plan. Do NOT call ExitPlanMode; proceed and make the changes directly. Only present a plan if the user switches the room to Plan mode or explicitly asks.`}};let t="keep";try{t=await(R?.({id:e(),toolName:o,input:n,risk:"plan",plan:n?.plan||""}))??"keep"}catch{t="keep"}if("run"===t||"accept"===t){const e="accept"===t?"acceptEdits":"default";return ye=e,ut(e),rt({kind:"mode",mode:e}),{continue:!0,hookSpecificOutput:{hookEventName:"PreToolUse",permissionDecision:"allow",permissionDecisionReason:`Plan approved in the thinkpool room — proceed (${e} mode).`}}}return{continue:!0,hookSpecificOutput:{hookEventName:"PreToolUse",permissionDecision:"deny",permissionDecisionReason:'The user chose "keep planning" in the thinkpool room. Do not exit plan mode — keep refining the plan, then call ExitPlanMode again when ready.'}}}if("AskUserQuestion"===o){let t="";try{t=await(R?.({id:e(),toolName:o,input:n,risk:"ask",questions:n?.questions||[]}))??""}catch{t=""}return askUserQuestionHookOutput(t)}const c=classifyRisk(o,n),d=`${o}:${c}`,u=isSafeDocWrite(o,n),p=autoAllow({toolName:o,input:n,mode:ye,alwaysAllow:ke});let m="allow";if(!p){try{m=await(R?.({id:e(),toolName:o,input:n,risk:c}))??"deny"}catch{m="deny"}"always"===m&&(ke.add(d),m="allow")}const h="deny"===m;return h&&We&&We.failure({permissionDenied:!0}),{continue:!0,hookSpecificOutput:{hookEventName:"PreToolUse",permissionDecision:h?"deny":"allow",permissionDecisionReason:h?"Denied by the user in the thinkpool room. Do not retry this tool — ask what to do instead.":p?u?"Auto-approved (mandated doc write — .claude/SESSIONS/ or CLAUDE.md).":`Auto-approved (${ye}).`:"Approved in the thinkpool room."}}}]}]},canUseTool:async(e,t)=>({behavior:"allow",updatedInput:t}),settingSources:["user","project","local"],appendSystemPrompt:[...L?[L]:[],...W?[W]:[],"ENVIRONMENT (authoritative — overrides any user-global CLAUDE.md or memory that claims otherwise): You are Claude running inside a thinkpool Code room, driven live by a user (and possibly a partner) from a phone or browser, via the thinkpool-pair bridge.",h,'Bias strongly toward DOING the work, not stalling in plan-mode ceremony. For a small "add / fix / change X" request, just make the change directly (for a bigger build, right-size it — see DEFAULT BUILD WORKFLOW below).',"Do NOT enter plan mode, do NOT call ExitPlanMode, and do NOT auto-invoke a brainstorming/planning skill UNLESS the user has switched the room into Plan mode or explicitly asks you to plan, design, or brainstorm first.","Any host-global instruction that says you must always brainstorm or plan before creative work does NOT apply here — this room is the exception.",p,'CRUCIAL RECONCILIATION for that workflow: it is NOT plan mode. Never call ExitPlanMode and never make the room wait behind a "plan ready — approve to start" card — your plan lives in the CHAT as a message, and your lanes live in the room\'s EXISTING terminal/lane list. Reuse only those two surfaces; there is no new Flow panel or mode to switch into, and you must not ask for one. Keep the plan and the lanes VISIBLE — that shared visibility is the whole point (it is the pair differentiator, and it catches bugs a single silent lane would hide); never collapse a decomposable build into one hidden lane just to look tidy.',...m,'CROSS-TERMINAL AWARENESS: ROOM NOW is the default roster and already satisfies the room check when it has enough detail. Do not repeat it with a no-argument read_terminal call unless it is missing or truncated. Use a targeted read_terminal call only when the current task depends on a specific lane’s detailed activity; never poll. Identify a terminal by its NAME or stable ref/id, never by an on-screen number like "Terminal 2" — positional labels renumber when a terminal is closed. The tool is read-only and its optional roster lookup is budgeted separately from bounded targeted transcript reads.','CROSS-TERMINAL HAND-OFF: you also have post_to_terminal(terminal, text) to send a message or task to ANOTHER AGENT terminal in this room (not a plain shell). Use it sparingly and only when the people clearly want the lanes to coordinate — e.g. "tell the backend terminal the API is ready", or to hand a sibling agent a concrete task. Every post requires a person in the room to approve a card before it is delivered, and an agent that was itself reached via a cross-post cannot post onward — so do not rely on it for chit-chat or loops. Prefer read_terminal to understand a sibling before you ever post to it.',"TERMINAL CREATION CONTRACT: main terminals and workers use different tools. A person-authored request to open, launch, start, spawn, or create a new, separate, independent, top-level, or main terminal uses open_main_terminal(name?, task?, model?) — even for ordinary work or a handoff and even without the words Cascade or conductor. If the wording includes new or separate terminal, main-terminal routing wins. Never use spawn_terminal for that request, and if open_main_terminal is unavailable say so instead of substituting. Use spawn_terminal(name?, task?, model?, sliceType?) only for agent-decided bounded WORKER SUB-TERMINALS or when the person explicitly asks for a worker, sub-terminal, Ensemble lane, or delegated slice. Workers never receive the creation tools and never conduct. Do not dump work into busy siblings. Spawned workers are always autonomous in bypassPermissions and never ask the room for an approval card; the bridge still enforces room caps, hop limits, the kill-switch, and isolated linked worktrees. Wait for ROOM NOW or a completion signal instead of polling; after a worker finishes, collect it with one targeted read_terminal call and leave it visible until a person dismisses it with the Ensemble X. Never call close_terminal merely because work finished. Main terminals are independent, keep their requested permission mode, and are not owned/closed through Ensemble.","CROSS-SESSION AWARENESS: the Ensemble reaches across your SESSIONS, not just the terminals in this room. list_sessions() lists your OTHER thinkpool Code rooms — both your own rooms running on this machine AND your partner's rooms in the same pair, reachable over the per-pair bus (a room on the partner's machine shows its host). read_session(session, terminal?) reads recent activity inside one (omit `terminal` to list that room's terminals, or pass a ref/name to read that lane). Both are READ-ONLY — they never change another session, and they reach ONLY your own rooms and rooms you share with your partner, never a stranger's. Reach for them when work spans rooms — \"what's the other project up to\", \"pick up where the other session left off\", or to check a long-running task elsewhere before you act here.","CROSS-SESSION HAND-OFF: post_to_session(session, text, terminal?) sends a task or message to an agent in ANOTHER of your rooms — your own, or your partner's over the pair bus. Use it sparingly and only when the people clearly want the rooms to coordinate — e.g. hand the API room's agent a concrete follow-up once the frontend is ready. It is dual-consent: a person in YOUR room approves sending, and a person in the TARGET room approves receiving, before anything is delivered — so never rely on it for chit-chat or loops, and an agent that was itself reached via a cross-room post cannot post onward to a third room. It spends real model tokens in the other room (maybe on the other person's machine), so prefer read_session to understand a room before you ever post into it, and only post one concrete hand-off at a time. Outbound list/read/post tools need the thinkpool account bridge; a standalone owner room can still receive a paired hand-off directly and will always raise its own approval card before delivery.","SUBAGENT POLICY: in this room, a main terminal delegates worker slices through visible spawn_terminal Ensemble lanes. A person-requested new or separate terminal is created with open_main_terminal, never Ensemble. Worker, leaf, Side, and managed Flow lanes do their assigned work directly. Do NOT reach for built-in Task/Agent subagents: an in-process subagent is invisible to the room, cannot be peered at or steered, and its work is lost to the Ensemble.",'RESEARCH LANE: you have a `research` tool that runs a REAL multi-source web search + adversarial verification and returns each claim marked HELD or REJECTED with citations. Reach for it when the people would genuinely benefit from looking something external up or settling a question of current fact — pricing, "is X still maintained / deprecated", "is that benchmark real", a debate over facts you are not sure of. Do NOT run it unprompted or for things you already know: first OFFER in plain language ("want me to spawn a research lane on that and check it?"), and only call `research(question)` once they agree — it spends real budget (plan-gated Free 5 / Plus 100 runs a month) and takes ~a minute. When it returns, present the held/rejected findings clearly and invite both people to weigh the sources, flagging any held claim that rests on a source they might not trust — that shared scrutiny is the point.',"WORKTREES: parallel lanes share one machine and usually one repo. Run `git worktree list` before your first code edit; if linked worktrees exist, the shared main checkout is contended (and may be guard-blocked) — do your work in your OWN worktree on your OWN branch (`git worktree add <dir> -b <branch>`), and never edit a checkout or ride a branch another lane is using.",'WRITE PLANS INTO THE CHAT: whenever you form or revise a plan — because the room is in Plan mode, or because someone asked you to plan, design, or think it through first — write the actual plan out as a normal message in the room as you develop it: the approach, the concrete steps, the files you will touch, the open questions. The room does NOT surface plan files at all, and the plan-approval card does not reliably carry the plan text, so a plan that lives only in a plan file or only inside ExitPlanMode is INVISIBLE to the people you are working with — they just see "plan ready" with no content. The chat is the canonical place your plan lives; put it there so the room can read and react to it before you proceed.'].join(" "),includePartialMessages:!0,promptSuggestions:!0};"1"===process.env.TP_MCP_STRICT&&(pt.strictMcpConfig=!0,pt.mcpServers={}),A&&Object.keys(A).length&&(pt.mcpServers={...pt.mcpServers||{},...A}),t&&(pt.cwd=t),o&&(pt.model=o),pt.effort=be,pt.env={...f||process.env,CLAUDE_CODE_ENABLE_PROMPT_SUGGESTION:"1"};const mt=async()=>{if(!K||"worker_completion"===ie)return;const e=(tt||"").trim().slice(-1500);if(e&&!ce)try{let o=await claudeOneShot({prompt:`You are predicting the user's NEXT chat message in a live coding session, to prefill their composer. Given the assistant's latest reply below, output the single most likely next user message — short and natural (often just "proceed", "go with option A", "yes do that", or a brief follow-up). One line, <=12 words, imperative, no preamble, no quotes, no markdown.\n\nAssistant's latest reply:\n"""\n${e}\n"""\n\nNext user message:`,model:"claude-haiku-4-5",cwd:t,env:pt.env,timeoutMs:8e3});o=o.trim().split("\n")[0].replace(/^["'`]+|["'`]+$/g,"").trim().slice(0,140),!o||et||ce||/^(sure|of course|certainly|let me know|i can help|happy to)\b/i.test(o)||(rt({kind:"suggestion",text:o,source:"haiku"}),process.stderr.write(` ◆ [suggestion] haiku fallback: ${JSON.stringify(o)}\n`))}catch{}},ht=()=>{const e="function"==typeof Y?Y():{ok:!0};return!1!==e?.ok||(rt({kind:"error",message:e.reason||"Host memory is critically low. This agent runtime was not started.",recoverable:!0}),!1)},ft=async({admitted:e=!1}={})=>{if(Re)return!1;if(!de&&!e&&!ht())return!1;if(null==ue&&(ue=Date.now()),de=!0,Ee)try{Ee()}catch{}Ne=new Promise(e=>{Ee=e});const n=new AbortController;Se=n;const a=()=>{try{n.abort()}catch{}};te.signal.addEventListener("abort",a,{once:!0});try{if(ne){const e=i(t||process.cwd(),ne);e.blocks&&process.stderr.write(`\n ◆ healed ${e.blocks} malformed tool block(s) in the transcript before resume.\n`)}fe=r({prompt:oe.stream,options:{...pt,abortController:n,...ne?{resume:ne}:{}}});for await(const e of fe){if(ce)break;switch(e.type){case"system":if("thinking_tokens"===e.subtype){ve+_e===0&&rt({kind:"thinking_tokens",tokens:e.estimated_tokens,delta:e.estimated_tokens_delta});break}if("compact_boundary"===e.subtype){Z=!0,rt({kind:"compaction",trigger:e.compact_metadata?.trigger||"auto",preTokens:e.compact_metadata?.pre_tokens??null}),(async()=>{try{const e=await(fe?.getContextUsage?.());e&&(rt({kind:"usage",ctx:c({used:e.totalTokens,max:e.maxTokens,pct:Math.round(e.percentage),model:e.model})}),e.model&&(Te=e.model))}catch{}})();break}if(e.session_id&&(ne=e.session_id),pe||(pe=!0,process.stderr.write(`\n ◆ session ready in ${Date.now()-ue}ms — MCP ${"1"===process.env.TP_MCP_STRICT?"OFF":"on"}${s?", resume":", fresh"}.\n`)),Te=pt.model||e.model||o||Te,rt({kind:"system",sessionId:ne,model:pt.model||e.model||o||null,commands:d({slashCommands:e.slash_commands,skills:e.skills})}),!he&&Array.isArray(e.tools)){he=!0;const t=askUserQuestionRegression(e.tools,N);t&&(process.stderr.write(`\n ⚠⚠ ${t}\n`),rt({kind:"error",message:t}))}me||(me=!0,Promise.resolve(fe?.supportedModels?.()).then(e=>{const t=(e||[]).map(e=>({value:e?.value,displayName:e?.displayName,description:e?.description})).filter(e=>e.value);t.length&&rt({kind:"models",models:t})}).catch(()=>{}));break;case"assistant":at(),We?.visibleOutput();for(const t of e.message?.content||[])"tool_use"===t?.type&&t.id&&we.set(t.id,{at:Date.now(),name:t.name,input:t.input});{const t=(e.message?.content||[]).filter(e=>"text"===e?.type).map(e=>e.text).join("\n").trim();t&&(tt=t)}rt({kind:"assistant",blocks:I(e.message?.content),parentToolUseId:e.parent_tool_use_id||null});break;case"user":for(const t of e.message?.content||[])if("tool_result"===t?.type){at(),We?.visibleOutput({tool:!0});const o=we.get(t.tool_use_id);null!=o&&we.delete(t.tool_use_id);const n=u(o?.name,t.content);rt({kind:"tool_result",toolUseId:t.tool_use_id,content:t.content,...n?{evidence:n}:{},isError:!!t.is_error,durationMs:null!=o?.at?Date.now()-o.at:void 0,parentToolUseId:e.parent_tool_use_id||null})}break;case"stream_event":{const t=e.event;"message_start"===t?.type?(ve+=_e,_e=t.message?.usage?.output_tokens||0,rt({kind:"thinking_tokens",tokens:ve+_e})):"message_delta"===t?.type&&t.usage&&(_e=t.usage.output_tokens??_e,rt({kind:"thinking_tokens",tokens:ve+_e}));break}case"result":if(at(),e.session_id&&(ne=e.session_id),e.is_error&&Array.isArray(e.errors)&&e.errors.some(e=>/no conversation found/i.test(String(e)))&&(ne||se)&&Me<3){Me++;const e=xe;xe=!1,ne=null,se=null,rt({kind:"note",text:"previous session not found — continuing on a fresh one"});const t=oe;oe=P();try{t.end()}catch{}const o=e?re:null;Ge=setTimeout(()=>{Ge=null,ce||(ft(),null!=o&&(xe=!0,Pe=Date.now(),Ie=!1,oe.push([{type:"text",text:o},{type:"text",text:ae||ee(o,{forceFull:!0})}])))},300);break}if(Ve&&("aborted"===e.subtype||"error_during_execution"===e.subtype)){je===Xe&&(xe=!1);break}if(e.is_error&&We){if($e)break;We.failure({message:Array.isArray(e.errors)?e.errors.join(" "):e.result,status:e.status??e.statusCode,code:e.code}),xe=!1,wt();break}if(ve=0,_e=0,xe=!1,Oe&&(Oe=!1,setTimeout(()=>{ce||yt()},0)),"success"===e.subtype&&(Me=0,ne&&(se=ne)),"success"===e.subtype&&(We?.success(),We=null),He=!1,Ke=!1,e.modelUsage&&"object"==typeof e.modelUsage){let t=null,o=-1;for(const[n,s]of Object.entries(e.modelUsage)){const e=s?.outputTokens??0;e>o&&(o=e,t=n)}t&&(Te=t)}{const t="string"==typeof e.result?e.result.replace(/\s+/g," ").trim().slice(0,200):"";rt({kind:"result",subtype:e.subtype,sessionId:ne,model:Te,costUsd:e.total_cost_usd,usage:e.usage,numTurns:e.num_turns,durationMs:e.duration_ms??null,denials:Array.isArray(e.permission_denials)?e.permission_denials.length:0,resultText:t||null})}ot&&clearTimeout(ot),"aborted"!==e.subtype&&"worker_completion"!==ie&&(ot=setTimeout(()=>{ot=null,et||ce||mt()},nt)),(async()=>{let t=null;try{const e=await(fe?.getContextUsage?.());e&&(t=c({used:e.totalTokens,max:e.maxTokens,pct:Math.round(e.percentage),model:e.model}),e.model&&(Te=e.model))}catch{}const o=e.usage||{};rt({kind:"usage",costUsd:e.total_cost_usd??null,tokens:(o.input_tokens||0)+(o.output_tokens||0),ctx:t})})();break;case"prompt_suggestion":ot&&(clearTimeout(ot),ot=null),e.suggestion&&(et=!0,rt({kind:"suggestion",text:e.suggestion}))}}}catch(e){if(at(),ce)return;if(n.signal.aborted)return;const t=e?.message||String(e);if(We&&xe)return We.failure({message:t,status:e?.status??e?.statusCode,code:e?.code}),xe=!1,void wt();if(xe=!1,rt({kind:"error",message:t}),Ce.test(t)&&Me<3){Me++,Ue("stream_reconnect",{failureClass:"network"}),rt({kind:"note",text:`connection dropped — reconnecting (${Me}/3)`});const e=oe;oe=P();try{e.end()}catch{}return void(Ge=setTimeout(()=>{Ge=null,ce||ft()},Math.min(1e3*Me,4e3)))}if(/no conversation found/i.test(t)&&ne&&Me<3){Me++,ne=null,se=null;const e=oe;oe=P();try{e.end()}catch{}return void(Ge=setTimeout(()=>{Ge=null,ce||ft()},300))}ce=!0}finally{te.signal.removeEventListener("abort",a),Se===n&&(Se=null);try{await(fe?.return?.())}catch{}Ee&&(Ee(),Ee=null)}},yt=async()=>{if(ce||Re||!de)return;const e=Ne,t=oe;if(oe=P(),Se)try{Se.abort()}catch{}try{t.end()}catch{}try{await e}catch{}await Ae(300),ne=se,ce||Re||ft()},kt=async e=>{if(ce)return;Ke=!0;const t=xe&&!_(re)?re:null;xe=!1;const o=Ne,n=oe;if(oe=P(),Se)try{Se.abort()}catch{}try{n.end()}catch{}try{await o}catch{}await Ae(300),ne=se,ce||(ft(),null!=t&&(xe=!0,Pe=Date.now(),Ie=!1,oe.push(/^\s*\//.test(t)?[{type:"text",text:t}]:[{type:"text",text:t},{type:"text",text:ae||ee(t,{forceFull:!0})}]),st({kind:"note",text:"retrying the stalled turn on a fresh connection"})))},wt=()=>{if(ce||qe||$e)return;const e=We,t=je,o=le;$e=!0;const n=Ne,s=oe;oe=P();try{s.end()}catch{}Ge=setTimeout(async()=>{Ge=null;try{await n}catch{}if(!ce&&$e&&We===e&&je===t)return await ct()?void(!ce&&$e&&We===e&&je===t&&($e=!1,xe=!0,Pe=Date.now(),Ie=!1,ft(),o&&oe.push(o),lt())):($e=!1,void gt())},0)},gt=()=>{at(),xe=!1,We=null,de=!1,rt({kind:"error",message:"The configured provider could not complete this turn. Nothing else was sent.",recoverable:!0})};return B||Le||ft(),{sendTurn(e,n={}){if(ce||Re||qe||$e)return!1;if(!de&&!ht())return!1;if(!de&&Q)try{const e=Q();e&&(t=e,pt.cwd=e)}catch{}xe=!0,et=!1,re=String(e),ie=w(n?.turnCause),ot&&(clearTimeout(ot),ot=null),Pe=Date.now(),Ie=!1,Ke=!1,He=!1;const s=String(e),r=X++,i=Z;if(Z=/^\s*\/(?:compact|clear|reset)\b/i.test(s),ae=/^\s*\//.test(s)?null:ee(s,{promptIndex:r,forceFull:i,turnCause:ie}),Ye=_(s),le=/^\s*\//.test(s)?[{type:"text",text:s}]:[{type:"text",text:s},{type:"text",text:ae}],je++,We=(()=>{if(!Le)return null;try{const e=T({runtime:"claude",providers:J.providers||[],providerId:J.providerId,model:J.model||pt.model||o,requestedModel:J.requestedModel||J.model||pt.model||o,policy:J.policy,bridgeHostId:J.bridgeHostId,circuit:J.circuit,capGate:J.capGate,traceId:J.traceId,turnRev:"function"==typeof J.turnRev?J.turnRev():J.turnRev,onRecord:it});return e.start(),e}catch{return null}})(),We){const e=We,t=je,o=le;ct().then(n=>{!ce&&xe&&We===e&&je===t&&(n?(de||ft({admitted:!0}),oe.push(o),lt()):gt())}).catch(()=>{!ce&&xe&&We===e&&je===t&>()})}else de||ft({admitted:!0}),oe.push(le)},warm(){Le||de||ce||Re||ft()},get started(){return de},async setMode(e){C.has(e)&&!ce&&(ye=e,ut(e),rt({kind:"mode",mode:ye}))},async setModel(e){pt.model=e,Te=e,rt({kind:"note",text:`model → ${e}`}),de&&!ce&&(xe?Oe=!0:await yt())},setEffort:async e=>!(!ge.has(e)||ce||(be=e,pt.effort=e,rt({kind:"effort",level:e}),de&&(xe?(Oe=!0,0):(await yt(),0)))),async listModels(){try{const e=(await(fe?.supportedModels?.())||[]).map(e=>({value:e?.value,displayName:e?.displayName,description:e?.description})).filter(e=>e.value);e.length&&rt({kind:"models",models:e}),rt({kind:"note",text:e.length?`models: ${e.map(e=>e.value).join(", ")}`:"no model list available"})}catch{rt({kind:"note",text:"usage: /model <name>"})}},async park(){if(ce||Re||xe||qe||$e||Ye)return!1;if(!de)return!0;Re=!0;const e=Ne,t=oe;oe=P(),de=!1,ot&&(clearTimeout(ot),ot=null);try{if(Se)try{Se.abort()}catch{}try{t.end()}catch{}try{await e}catch{}return fe=null,se&&(ne=se),!ce}finally{Re=!1}},async abort(){if(!qe){qe=!0;try{const e=$e,t=e?Ne:null;(xe||e)&&(je++,e||($e=!1),at(),We?.failure({aborted:!0}),e&&(clearTimeout(Ge),Ge=null,de=!1),Ve=!0,Xe=je,clearTimeout(Ze),Ze=setTimeout(()=>{Ve=!1,Xe=null},6e3));try{await(fe?.interrupt?.())}catch{}if(t)try{await t}catch{}e&&($e=!1),(xe||e)&&(xe=!1,rt({kind:"result",subtype:"aborted",sessionId:ne}))}finally{qe=!1}}},end(){ce=!0,at(),clearInterval(dt),clearTimeout(Ze),clearTimeout(Ge),ot&&clearTimeout(ot),oe.end();try{te.abort()}catch{}},get sessionId(){return ne},get mode(){return ye},get turnActive(){return xe}}}
|
|
1
|
+
import{randomUUID as e}from"node:crypto";import{createRequire as t}from"node:module";import{readFileSync as o}from"node:fs";import{dirname as n,join as s}from"node:path";import{query as r}from"@anthropic-ai/claude-agent-sdk";import{sanitizeSession as i}from"./transcript-sanitize.mjs";import{reviewGatePreToolDecision as a}from"./flow-review-gate.mjs";import{crossPostNeedsCard as l}from"./cross-terminal.mjs";import{correctContext as c}from"./context-windows.mjs";import{normalizeClaudeCommandCatalog as d}from"./claude-command-catalog.mjs";import{evidenceForToolResult as u}from"./evidence-citations.mjs";import{THINKPOOL_CASCADE_RULE as p,THINKPOOL_REMOTE_DELIVERY_RULES as m,THINKPOOL_RUNTIME_AUTHORITY_RULE as h,THINKPOOL_RUNTIME_TURN_REMINDER as f,buildThinkPoolTurnGuidance as y,createRoomContextSelector as k,normalizeThinkPoolTurnCause as w,usesFullThinkPoolReminder as g}from"./thinkpool-room-prompt.mjs";import{stallDecision as b,stallEvent as v,isCompactTurn as _}from"./turn-stall.mjs";import{createSameTargetResilienceController as T,formatResilienceTrace as S}from"./provider-resilience.mjs";import{catastrophicPreToolHookDecision as E}from"./catastrophic-command-guard.mjs";const N=t(import.meta.url);export async function claudeOneShot({prompt:e,model:t="claude-haiku-4-5",cwd:o,env:n,timeoutMs:s=8e3}={}){const i=new AbortController,a=setTimeout(()=>{try{i.abort()}catch{}},s);try{const s=r({prompt:e,options:{model:t,...o?{cwd:o}:{},env:n,maxTurns:1,permissionMode:"bypassPermissions",settingSources:[],strictMcpConfig:!0,mcpServers:{},abortController:i}});let a="";for await(const e of s){if("assistant"===e.type)for(const t of e.message?.content||[])"text"===t.type&&(a+=t.text);if("result"===e.type)break}return a.trim()}finally{clearTimeout(a)}}const O=(()=>{try{let e=n(N.resolve("@anthropic-ai/claude-agent-sdk"));for(let t=0;t<8;t++){try{const t=JSON.parse(o(s(e,"package.json"),"utf8"));if("@anthropic-ai/claude-agent-sdk"===t.name)return t.version}catch{}const t=n(e);if(t===e)break;e=t}}catch{}return"unknown"})(),R=/\brm\s+\S|\brmdir\s+\S|\bgit\s+(push\s+(-f|--force)|reset\s+--hard|clean\s+-[a-z]*f)|\bdrop\s+(table|database)\b|\b(mkfs|dd)\b|\bsudo\b|>\s*\/dev\/|\bchmod\s+-R|\bchown\s+-R|\bkillall\b|\btruncate\b/i,A=new Set(["Read","Grep","Glob","NotebookRead","TodoRead","LS"]),D=new Set(["WebFetch","WebSearch"]),x=new Set(["Edit","Write","MultiEdit","NotebookEdit","TodoWrite"]);export function classifyRisk(e,t){if("Bash"===e){const e=t&&(t.command??t.cmd)||"";return R.test(e)?"high":"medium"}return A.has(e)?"low":D.has(e)?"network":(x.has(e),"medium")}const P=/(^|\/)\.claude\/SESSIONS\/|(^|\/)CLAUDE\.md$/;export function isSafeDocWrite(e,t){if(!x.has(e))return!1;const o=t&&(t.file_path||t.notebook_path)||"";return P.test(o)}export function autoAllow({toolName:e,input:t,mode:o="default",alwaysAllow:n=new Set}){const s=classifyRisk(e,t);return"low"===s||"bypassPermissions"===o||"acceptEdits"===o&&x.has(e)&&"high"!==s||isSafeDocWrite(e,t)||n.has(`${e}:${s}`)}export function askUserQuestionHookOutput(e){const t=e&&"object"==typeof e?e.decision:e,o="string"==typeof t&&t.startsWith("answer:")?t.slice(7):"",n=e&&"object"==typeof e?e.responder:null,s="string"==typeof n?.name&&n.name.trim()?n.name.trim():"string"==typeof n?.id&&n.id?`room member ${n.id}`:"The user";return{continue:!0,hookSpecificOutput:{hookEventName:"PreToolUse",permissionDecision:"deny",permissionDecisionReason:o?`${s} answered in the thinkpool room — ${o}. Treat this as their selection and continue; do not call AskUserQuestion again for the same question.`:"The user dismissed the question in the thinkpool room without selecting. Ask in plain prose, or proceed with a sensible default."}}}export function askUserQuestionRegression(e,t){return Array.isArray(e)?e.includes("AskUserQuestion")?null:`[SDK-REGRESSION] agent SDK v${t||"unknown"} init tools[] is missing 'AskUserQuestion' — the multiple-choice question card is disabled. A caret SDK bump likely changed the canUseTool gate; pin a known-good SDK in bridge/package.json + republish.`:null}function I(){const e=[];let t=null,o=!1;return{stream:async function*(){for(;!o;)e.length?yield e.shift():await new Promise(e=>{t=e})}(),push:n=>!o&&(e.push({type:"user",message:{role:"user",content:n}}),t&&(t(),t=null),!0),end(){o=!0,t&&(t(),t=null)}}}const C=(e=[])=>e.map(e=>"text"===e.type?{type:"text",text:e.text}:"thinking"===e.type?{type:"thinking",text:e.thinking||"",reasoningKind:"thought"}:"tool_use"===e.type?{type:"tool_use",id:e.id,name:e.name,input:e.input}:{type:e.type}).filter(Boolean),M=new Set(["default","acceptEdits","plan","bypassPermissions"]),U=["You are Claude in a thinkpool Code room, driven live from a phone or browser — NOT a local terminal. Keep using the room's features.",f,"TERMINAL HIERARCHY: obey your authoritative TERMINAL ROLE. A person-authored request to open, launch, start, spawn, or create a new, separate, independent, top-level, or main terminal uses open_main_terminal — NEVER spawn_terminal — even for ordinary work or a handoff. If the wording includes new or separate terminal, main-terminal routing wins. Use spawn_terminal only for agent-decided bounded worker slices or when the person explicitly asks for a worker, sub-terminal, Ensemble lane, or delegated slice. If open_main_terminal is unavailable, say so; never substitute an Ensemble child. Leaf, worker, Side, and managed Flow lanes work directly. Never use built-in invisible Task/Agent subagents or hijack a busy sibling.","WORKTREES: parallel lanes share one repo — before code edits run `git worktree list`; if linked worktrees exist, take your OWN worktree + branch, never the shared checkout or a branch another lane is on.","BUILD WORKFLOW (default, no magic word): right-size within your TERMINAL ROLE — a trivial ask or delegated slice you just do; a conductor-capable role with a genuinely decomposable build FIRST writes a short plan in chat, THEN fans worker slices into visible spawn_terminal lanes and verifies them. Worker/leaf/Side/managed Flow roles do not fan out. A person-requested new or separate terminal uses open_main_terminal. Never plan-mode/ExitPlanMode; plans live in chat and lanes in the existing list."].join(" ");export function startClaudeSession({cwd:t,model:o,effort:n="high",resume:s,env:f,mode:N="default",onEvent:R,requestPermission:A,mcpServers:D,crossPostGate:x,crossRoomPostGate:P,didSpawnTarget:L=null,terminalRolePrompt:W,rolePrompt:F,blockSubagents:j=!1,onSubmitPlan:$=null,onLaneDone:q=null,onReviewVerdict:G=null,reviewGate:B=null,lazy:H=!1,roomContext:K=null,suggest:Q=!0,prepareCwd:Y=null,admitStart:z=null,resilienceObserver:J=null,resilience:V=null}){const X=k(K);let Z=0,ee=!0;const te=(e,{promptIndex:t=0,forceFull:o=!1,turnCause:n=null}={})=>{const s=g({promptIndex:t,forceFull:o,turnCause:n}),r=X({force:s}),i=String(W||"").trim();return`<system-reminder>\n${s&&i?`${i}\n\n`:""}${s?U:y({text:e,promptIndex:t,forceFull:o,turnCause:n})}${r?`\n\n${r}`:""}\n</system-reminder>`},oe=new AbortController;let ne=I(),se=s||null,re=s||null,ie=null,ae=null,le=null,ce=null,de=!1,ue=!1,pe=null,me=!1,he=!1,fe=!1,ye=null,ke=M.has(N)?N:"default";const we=new Set,ge=new Map,be=new Set(["low","medium","high","xhigh","max"]);let ve=be.has(n)?n:"high",_e=0,Te=0,Se=o||null,Ee=null,Ne=null,Oe=Promise.resolve(),Re=!1,Ae=!1;const De=e=>new Promise(t=>setTimeout(t,e)),xe=Math.max(3e4,parseInt(process.env.TP_STALL_MS,10)||9e4);let Pe=!1,Ie=Date.now(),Ce=!1;const Me=/connection closed|connection reset|econnreset|etimedout|socket hang up|fetch failed|network error|socket destroyed/i;let Ue=0;const Le=(e,t={})=>{try{J?.({reason:e,restartCount:Ue,...t})}catch{}},We=!(!V?.policy?.enabled||!V?.providerId||"anthropic"===V.providerId);let Fe=null,je=null,$e=!1,qe=0,Ge=!1,Be=null;const He=Math.max(3*xe,parseInt(process.env.TP_FORCE_STOP_MS,10)||3e5);let Ke=!1,Qe=!1;const Ye=Math.max(He,parseInt(process.env.TP_COMPACT_FORCE_STOP_MS,10)||12e5);let ze=!1,Je=0;const Ve=A;A=async e=>{Je++;try{return await(Ve?.(e))}finally{Je=Math.max(0,Je-1),Ie=Date.now()}};let Xe=!1,Ze=null,et=null,tt=!1,ot="",nt=null;const st=Math.max(1200,parseInt(process.env.TP_SUGGEST_FALLBACK_MS,10)||2500),rt=e=>{try{R?.(e)}catch{}},it=e=>{Ie=Date.now(),e&&"stalled"!==e.kind&&(Ce=!1),!e||"compaction"!==e.kind&&"result"!==e.kind&&"error"!==e.kind||(ze=!1),rt(e)},at=e=>{try{V?.onRecord?.(e)}catch{}"retrying"!==e?.outcome&&"cap_blocked"!==e?.outcome&&"circuit_open"!==e?.outcome||it({kind:"note",text:S(e)})},lt=()=>{if(null!=je){try{(V?.clearTimer||clearTimeout)(je)}catch{}je=null}},ct=()=>{if(lt(),!Fe)return;const e=Number(V?.policy?.timeoutMs);if(!Number.isFinite(e)||e<=0)return;const t=V?.setTimer||setTimeout;je=t(()=>{if(je=null,!de&&Fe&&Pe){Fe.failure({message:"timeout"}),Pe=!1;try{Ee?.abort()}catch{}gt()}},e)},dt=async()=>{if(!Fe)return!0;let e=null;try{e=await Fe.preflight()}catch{return!1}return!0===e?.admitted},ut=setInterval(()=>{const e=Date.now()-Ie,t=b({turnActive:Pe,awaitingUser:Je,quietMs:e,stallMs:xe,forceStopMs:He,stalledSent:Ce,stallRetried:Qe,compacting:ze,compactForceStopMs:Ye});if("none"===t)return;const o=v(t,e);if(o&&rt(o),"status"!==t){if("retry"===t){if(Ke=!0,Fe){Le("resilience_stall",{quietMs:e}),Fe.failure({message:"timeout"}),Pe=!1;try{Ee?.abort()}catch{}return void gt()}return Le("stall_replay",{quietMs:e}),void wt(e)}Ke=!0,Pe=!1}else Ce=!0},5e3);ut.unref?.();const pt=e=>{setTimeout(()=>{Promise.resolve(ye?.setPermissionMode?.(e)).catch(t=>console.error(`[claude-session] setPermissionMode(${e}) failed:`,t?.message||t))},0)},mt={abortController:oe,permissionMode:ke,hooks:{PreToolUse:[{hooks:[async o=>{const n=o.tool_name,s=o.tool_input,r=E({toolName:n,input:s,cwd:t});if(r)return r;const i=o.effort?.level;i&&i!==ve&&(ve=i,it({kind:"effort",level:ve}));const c="Write"===n||"Edit"===n||"MultiEdit"===n||"NotebookEdit"===n;if(j&&c&&/(?:^|[/\\])FLOW_PLAN\.json$/i.test(s?.file_path||s?.notebook_path||"")){const e=s?.content??s?.new_string??"";let t={ok:!1,message:"Plan submission is not wired for this session."};try{t=$&&await $(e)||t}catch(e){t={ok:!1,message:`Plan submission failed: ${e?.message||e}`}}return{continue:!0,hookSpecificOutput:{hookEventName:"PreToolUse",permissionDecision:"deny",permissionDecisionReason:t.message}}}if(j&&("Task"===n||"Agent"===n||"Bash"===n||c))return{continue:!0,hookSpecificOutput:{hookEventName:"PreToolUse",permissionDecision:"deny",permissionDecisionReason:"Bash"===n?"Flow conductors do NOT run Bash — it pops a permission card and stalls the flow. Use the Grep tool (not `grep`/`rg`), the Glob tool (not `find`/`ls`), and Read (not `cat`/`head`) — those run with no prompt. Then DECOMPOSE and submit by Writing your task-graph JSON to the file FLOW_PLAN.json.":c?'Flow conductors do NOT write project or plan-document files. Do NOT use ExitPlanMode and do NOT call submit_flow_plan (both hang here). To SUBMIT your decomposition, use the Write tool with file_path "FLOW_PLAN.json" and content = your task-graph JSON ({"summary":"…","tasks":[…]}). That single Write IS how you submit — the room ingests it directly. Do not write any other file.':"Flow conductors do NOT spawn subagents — it stalls the flow on a permission card. Inspect the codebase yourself with Read/Grep/Glob (no prompt). Then DECOMPOSE and submit by Writing your task-graph JSON to the file FLOW_PLAN.json. Do not call Task/Agent again."}};if(G&&("Write"===n||"Edit"===n)&&/(?:^|[/\\])FLOW_REVIEW(\.[a-zA-Z0-9]+)?$/.test(s?.file_path||"")){let e={ok:!1,message:"verdict signal failed"};try{e=await G(s?.content??s?.new_string??"")||e}catch(t){e={ok:!1,message:`verdict signal failed: ${t?.message||t}`}}return{continue:!0,hookSpecificOutput:{hookEventName:"PreToolUse",permissionDecision:"deny",permissionDecisionReason:e.message}}}if(q&&("Write"===n||"Edit"===n)&&/(?:^|[/\\])FLOW_DONE(\.[a-zA-Z0-9]+)?$/.test(s?.file_path||"")){let e={ok:!1,message:"done signal failed"};try{e=await q(s?.content??s?.new_string??"")||e}catch(t){e={ok:!1,message:`done signal failed: ${t?.message||t}`}}return{continue:!0,hookSpecificOutput:{hookEventName:"PreToolUse",permissionDecision:"deny",permissionDecisionReason:e.message}}}const d=a({reviewGate:B,toolName:n,toolInput:s});if(d)return d;if("mcp__thinkpool__read_terminal"===n)return{continue:!0,hookSpecificOutput:{hookEventName:"PreToolUse",permissionDecision:"allow",permissionDecisionReason:"Auto-approved (read-only thinkpool cross-terminal read)."}};if(/^mcp__thinkpool__preview_(start|capture|inspect|stop)$/.test(n))return{continue:!0,hookSpecificOutput:{hookEventName:"PreToolUse",permissionDecision:"allow",permissionDecisionReason:"Auto-approved (contained bridge-owned viewport preview)."}};if("mcp__thinkpool__submit_flow_plan"===n)return{continue:!0,hookSpecificOutput:{hookEventName:"PreToolUse",permissionDecision:"allow",permissionDecisionReason:"Auto-approved (Flow plan submission — broadcasts the task-graph for human approval)."}};if("mcp__thinkpool__list_sessions"===n||"mcp__thinkpool__read_session"===n)return{continue:!0,hookSpecificOutput:{hookEventName:"PreToolUse",permissionDecision:"allow",permissionDecisionReason:"Auto-approved (read-only thinkpool cross-session read — your own rooms)."}};if("mcp__thinkpool__close_terminal"===n)return{continue:!0,hookSpecificOutput:{hookEventName:"PreToolUse",permissionDecision:"allow",permissionDecisionReason:"Auto-approved (thinkpool cross-terminal close — only self-spawned lanes)."}};if("mcp__thinkpool__spawn_terminal"===n)return{continue:!0,hookSpecificOutput:{hookEventName:"PreToolUse",permissionDecision:"allow",permissionDecisionReason:"Auto-approved (autonomous thinkpool worker; bounded by room caps and isolated in its own worktree)."}};if("mcp__thinkpool__open_main_terminal"===n){const t=s?.mode||ke;if("bypassPermissions"!==t||"bypassPermissions"===ke)return{continue:!0,hookSpecificOutput:{hookEventName:"PreToolUse",permissionDecision:"allow",permissionDecisionReason:`Auto-approved (open a fresh thinkpool terminal in ${t} mode — inherited, no escalation; bounded by room caps).`}};let o="deny";try{o=await(A?.({id:e(),toolName:n,input:{...s,mode:t},risk:"high"}))??"deny"}catch{o="deny"}const r="allow"===o;return{continue:!0,hookSpecificOutput:{hookEventName:"PreToolUse",permissionDecision:r?"allow":"deny",permissionDecisionReason:r?"Bypass terminal creation approved in the thinkpool room — the new terminal runs autonomously.":"Bypass terminal creation denied in the room — do not retry as bypass; open it without mode:bypassPermissions or ask what to do."}}}if("mcp__thinkpool__post_to_terminal"===n){const t=x?x():{ok:!0};if(!t.ok)return{continue:!0,hookSpecificOutput:{hookEventName:"PreToolUse",permissionDecision:"deny",permissionDecisionReason:t.reason||"Cross-terminal post not allowed right now."}};const o=!!L&&!!L(s?.terminal);if(!l({mode:ke,spawnedByMe:o}))return{continue:!0,hookSpecificOutput:{hookEventName:"PreToolUse",permissionDecision:"allow",permissionDecisionReason:"bypassPermissions"===ke?"Auto-approved (cross-terminal post in bypassPermissions mode — bypass means bypass).":"Auto-approved (cross-terminal post into a lane you spawned — parent→child orchestration, no per-post tap)."}};let r="deny";try{r=await(A?.({id:e(),toolName:n,input:s,risk:"high"}))??"deny"}catch{r="deny"}const i="allow"===r;return{continue:!0,hookSpecificOutput:{hookEventName:"PreToolUse",permissionDecision:i?"allow":"deny",permissionDecisionReason:i?"Cross-terminal post approved in the thinkpool room.":"Cross-terminal post denied in the room — do not retry; ask what to do instead."}}}if("mcp__thinkpool__post_to_session"===n){const t=P?P():{ok:!0};if(!t.ok)return{continue:!0,hookSpecificOutput:{hookEventName:"PreToolUse",permissionDecision:"deny",permissionDecisionReason:t.reason||"Cross-room post not allowed right now."}};let o="deny";try{o=await(A?.({id:e(),toolName:n,input:s,risk:"high"}))??"deny"}catch{o="deny"}const r="allow"===o;return{continue:!0,hookSpecificOutput:{hookEventName:"PreToolUse",permissionDecision:r?"allow":"deny",permissionDecisionReason:r?"Cross-room post approved in the sending room — the target room will be asked to accept it.":"Cross-room post denied in the room — do not retry; ask what to do instead."}}}if("ExitPlanMode"===n){if("plan"!==ke)return pt(ke),it({kind:"mode",mode:ke}),{continue:!0,hookSpecificOutput:{hookEventName:"PreToolUse",permissionDecision:"allow",permissionDecisionReason:`The thinkpool room is in ${ke} mode, not Plan — the user did not ask for a plan. Do NOT call ExitPlanMode; proceed and make the changes directly. Only present a plan if the user switches the room to Plan mode or explicitly asks.`}};let t="keep";try{t=await(A?.({id:e(),toolName:n,input:s,risk:"plan",plan:s?.plan||""}))??"keep"}catch{t="keep"}if("run"===t||"accept"===t){const e="accept"===t?"acceptEdits":"default";return ke=e,pt(e),it({kind:"mode",mode:e}),{continue:!0,hookSpecificOutput:{hookEventName:"PreToolUse",permissionDecision:"allow",permissionDecisionReason:`Plan approved in the thinkpool room — proceed (${e} mode).`}}}return{continue:!0,hookSpecificOutput:{hookEventName:"PreToolUse",permissionDecision:"deny",permissionDecisionReason:'The user chose "keep planning" in the thinkpool room. Do not exit plan mode — keep refining the plan, then call ExitPlanMode again when ready.'}}}if("AskUserQuestion"===n){let t="";try{t=await(A?.({id:e(),toolName:n,input:s,risk:"ask",questions:s?.questions||[]}))??""}catch{t=""}return askUserQuestionHookOutput(t)}const u=classifyRisk(n,s),p=`${n}:${u}`,m=isSafeDocWrite(n,s),h=autoAllow({toolName:n,input:s,mode:ke,alwaysAllow:we});let f="allow";if(!h){try{f=await(A?.({id:e(),toolName:n,input:s,risk:u}))??"deny"}catch{f="deny"}"always"===f&&(we.add(p),f="allow")}const y="deny"===f;return y&&Fe&&Fe.failure({permissionDenied:!0}),{continue:!0,hookSpecificOutput:{hookEventName:"PreToolUse",permissionDecision:y?"deny":"allow",permissionDecisionReason:y?"Denied by the user in the thinkpool room. Do not retry this tool — ask what to do instead.":h?m?"Auto-approved (mandated doc write — .claude/SESSIONS/ or CLAUDE.md).":`Auto-approved (${ke}).`:"Approved in the thinkpool room."}}}]}]},canUseTool:async(e,t)=>({behavior:"allow",updatedInput:t}),settingSources:["user","project","local"],appendSystemPrompt:[...W?[W]:[],...F?[F]:[],"ENVIRONMENT (authoritative — overrides any user-global CLAUDE.md or memory that claims otherwise): You are Claude running inside a thinkpool Code room, driven live by a user (and possibly a partner) from a phone or browser, via the thinkpool-pair bridge.",h,'Bias strongly toward DOING the work, not stalling in plan-mode ceremony. For a small "add / fix / change X" request, just make the change directly (for a bigger build, right-size it — see DEFAULT BUILD WORKFLOW below).',"Do NOT enter plan mode, do NOT call ExitPlanMode, and do NOT auto-invoke a brainstorming/planning skill UNLESS the user has switched the room into Plan mode or explicitly asks you to plan, design, or brainstorm first.","Any host-global instruction that says you must always brainstorm or plan before creative work does NOT apply here — this room is the exception.",p,'CRUCIAL RECONCILIATION for that workflow: it is NOT plan mode. Never call ExitPlanMode and never make the room wait behind a "plan ready — approve to start" card — your plan lives in the CHAT as a message, and your lanes live in the room\'s EXISTING terminal/lane list. Reuse only those two surfaces; there is no new Flow panel or mode to switch into, and you must not ask for one. Keep the plan and the lanes VISIBLE — that shared visibility is the whole point (it is the pair differentiator, and it catches bugs a single silent lane would hide); never collapse a decomposable build into one hidden lane just to look tidy.',...m,'CROSS-TERMINAL AWARENESS: ROOM NOW is the default roster and already satisfies the room check when it has enough detail. Do not repeat it with a no-argument read_terminal call unless it is missing or truncated. Use a targeted read_terminal call only when the current task depends on a specific lane’s detailed activity; never poll. Identify a terminal by its NAME or stable ref/id, never by an on-screen number like "Terminal 2" — positional labels renumber when a terminal is closed. The tool is read-only and its optional roster lookup is budgeted separately from bounded targeted transcript reads.','CROSS-TERMINAL HAND-OFF: you also have post_to_terminal(terminal, text) to send a message or task to ANOTHER AGENT terminal in this room (not a plain shell). Use it sparingly and only when the people clearly want the lanes to coordinate — e.g. "tell the backend terminal the API is ready", or to hand a sibling agent a concrete task. Every post requires a person in the room to approve a card before it is delivered, and an agent that was itself reached via a cross-post cannot post onward — so do not rely on it for chit-chat or loops. Prefer read_terminal to understand a sibling before you ever post to it.',"TERMINAL CREATION CONTRACT: main terminals and workers use different tools. A person-authored request to open, launch, start, spawn, or create a new, separate, independent, top-level, or main terminal uses open_main_terminal(name?, task?, model?) — even for ordinary work or a handoff and even without the words Cascade or conductor. If the wording includes new or separate terminal, main-terminal routing wins. Never use spawn_terminal for that request, and if open_main_terminal is unavailable say so instead of substituting. Use spawn_terminal(name?, task?, model?, sliceType?) only for agent-decided bounded WORKER SUB-TERMINALS or when the person explicitly asks for a worker, sub-terminal, Ensemble lane, or delegated slice. Workers never receive the creation tools and never conduct. Do not dump work into busy siblings. Spawned workers are always autonomous in bypassPermissions and never ask the room for an approval card; the bridge still enforces room caps, hop limits, the kill-switch, and isolated linked worktrees. Wait for ROOM NOW or a completion signal instead of polling; after a worker finishes, collect it with one targeted read_terminal call and leave it visible until a person dismisses it with the Ensemble X. Never call close_terminal merely because work finished. Main terminals are independent, keep their requested permission mode, and are not owned/closed through Ensemble.","CROSS-SESSION AWARENESS: the Ensemble reaches across your SESSIONS, not just the terminals in this room. list_sessions() lists your OTHER thinkpool Code rooms — both your own rooms running on this machine AND your partner's rooms in the same pair, reachable over the per-pair bus (a room on the partner's machine shows its host). read_session(session, terminal?) reads recent activity inside one (omit `terminal` to list that room's terminals, or pass a ref/name to read that lane). Both are READ-ONLY — they never change another session, and they reach ONLY your own rooms and rooms you share with your partner, never a stranger's. Reach for them when work spans rooms — \"what's the other project up to\", \"pick up where the other session left off\", or to check a long-running task elsewhere before you act here.","CROSS-SESSION HAND-OFF: post_to_session(session, text, terminal?) sends a task or message to an agent in ANOTHER of your rooms — your own, or your partner's over the pair bus. Use it sparingly and only when the people clearly want the rooms to coordinate — e.g. hand the API room's agent a concrete follow-up once the frontend is ready. It is dual-consent: a person in YOUR room approves sending, and a person in the TARGET room approves receiving, before anything is delivered — so never rely on it for chit-chat or loops, and an agent that was itself reached via a cross-room post cannot post onward to a third room. It spends real model tokens in the other room (maybe on the other person's machine), so prefer read_session to understand a room before you ever post into it, and only post one concrete hand-off at a time. Outbound list/read/post tools need the thinkpool account bridge; a standalone owner room can still receive a paired hand-off directly and will always raise its own approval card before delivery.","SUBAGENT POLICY: in this room, a main terminal delegates worker slices through visible spawn_terminal Ensemble lanes. A person-requested new or separate terminal is created with open_main_terminal, never Ensemble. Worker, leaf, Side, and managed Flow lanes do their assigned work directly. Do NOT reach for built-in Task/Agent subagents: an in-process subagent is invisible to the room, cannot be peered at or steered, and its work is lost to the Ensemble.",'RESEARCH LANE: you have a `research` tool that runs a REAL multi-source web search + adversarial verification and returns each claim marked HELD or REJECTED with citations. Reach for it when the people would genuinely benefit from looking something external up or settling a question of current fact — pricing, "is X still maintained / deprecated", "is that benchmark real", a debate over facts you are not sure of. Do NOT run it unprompted or for things you already know: first OFFER in plain language ("want me to spawn a research lane on that and check it?"), and only call `research(question)` once they agree — it spends real budget (plan-gated Free 5 / Plus 100 runs a month) and takes ~a minute. When it returns, present the held/rejected findings clearly and invite both people to weigh the sources, flagging any held claim that rests on a source they might not trust — that shared scrutiny is the point.',"WORKTREES: parallel lanes share one machine and usually one repo. Run `git worktree list` before your first code edit; if linked worktrees exist, the shared main checkout is contended (and may be guard-blocked) — do your work in your OWN worktree on your OWN branch (`git worktree add <dir> -b <branch>`), and never edit a checkout or ride a branch another lane is using.",'WRITE PLANS INTO THE CHAT: whenever you form or revise a plan — because the room is in Plan mode, or because someone asked you to plan, design, or think it through first — write the actual plan out as a normal message in the room as you develop it: the approach, the concrete steps, the files you will touch, the open questions. The room does NOT surface plan files at all, and the plan-approval card does not reliably carry the plan text, so a plan that lives only in a plan file or only inside ExitPlanMode is INVISIBLE to the people you are working with — they just see "plan ready" with no content. The chat is the canonical place your plan lives; put it there so the room can read and react to it before you proceed.'].join(" "),includePartialMessages:!0,promptSuggestions:!0};"1"===process.env.TP_MCP_STRICT&&(mt.strictMcpConfig=!0,mt.mcpServers={}),D&&Object.keys(D).length&&(mt.mcpServers={...mt.mcpServers||{},...D}),t&&(mt.cwd=t),o&&(mt.model=o),mt.effort=ve,mt.env={...f||process.env,CLAUDE_CODE_ENABLE_PROMPT_SUGGESTION:"1"};const ht=async()=>{if(!Q||"worker_completion"===ae)return;const e=(ot||"").trim().slice(-1500);if(e&&!de)try{let o=await claudeOneShot({prompt:`You are predicting the user's NEXT chat message in a live coding session, to prefill their composer. Given the assistant's latest reply below, output the single most likely next user message — short and natural (often just "proceed", "go with option A", "yes do that", or a brief follow-up). One line, <=12 words, imperative, no preamble, no quotes, no markdown.\n\nAssistant's latest reply:\n"""\n${e}\n"""\n\nNext user message:`,model:"claude-haiku-4-5",cwd:t,env:mt.env,timeoutMs:8e3});o=o.trim().split("\n")[0].replace(/^["'`]+|["'`]+$/g,"").trim().slice(0,140),!o||tt||de||/^(sure|of course|certainly|let me know|i can help|happy to)\b/i.test(o)||(it({kind:"suggestion",text:o,source:"haiku"}),process.stderr.write(` ◆ [suggestion] haiku fallback: ${JSON.stringify(o)}\n`))}catch{}},ft=()=>{const e="function"==typeof z?z():{ok:!0};return!1!==e?.ok||(it({kind:"error",message:e.reason||"Host memory is critically low. This agent runtime was not started.",recoverable:!0}),!1)},yt=async({admitted:e=!1}={})=>{if(Ae)return!1;if(!ue&&!e&&!ft())return!1;if(null==pe&&(pe=Date.now()),ue=!0,Ne)try{Ne()}catch{}Oe=new Promise(e=>{Ne=e});const n=new AbortController;Ee=n;const a=()=>{try{n.abort()}catch{}};oe.signal.addEventListener("abort",a,{once:!0});try{if(se){const e=i(t||process.cwd(),se);e.blocks&&process.stderr.write(`\n ◆ healed ${e.blocks} malformed tool block(s) in the transcript before resume.\n`)}ye=r({prompt:ne.stream,options:{...mt,abortController:n,...se?{resume:se}:{}}});for await(const e of ye){if(de)break;switch(e.type){case"system":if("thinking_tokens"===e.subtype){_e+Te===0&&it({kind:"thinking_tokens",tokens:e.estimated_tokens,delta:e.estimated_tokens_delta});break}if("compact_boundary"===e.subtype){ee=!0,it({kind:"compaction",trigger:e.compact_metadata?.trigger||"auto",preTokens:e.compact_metadata?.pre_tokens??null}),(async()=>{try{const e=await(ye?.getContextUsage?.());e&&(it({kind:"usage",ctx:c({used:e.totalTokens,max:e.maxTokens,pct:Math.round(e.percentage),model:e.model})}),e.model&&(Se=e.model))}catch{}})();break}if(e.session_id&&(se=e.session_id),me||(me=!0,process.stderr.write(`\n ◆ session ready in ${Date.now()-pe}ms — MCP ${"1"===process.env.TP_MCP_STRICT?"OFF":"on"}${s?", resume":", fresh"}.\n`)),Se=mt.model||e.model||o||Se,it({kind:"system",sessionId:se,model:mt.model||e.model||o||null,commands:d({slashCommands:e.slash_commands,skills:e.skills})}),!fe&&Array.isArray(e.tools)){fe=!0;const t=askUserQuestionRegression(e.tools,O);t&&(process.stderr.write(`\n ⚠⚠ ${t}\n`),it({kind:"error",message:t}))}he||(he=!0,Promise.resolve(ye?.supportedModels?.()).then(e=>{const t=(e||[]).map(e=>({value:e?.value,displayName:e?.displayName,description:e?.description})).filter(e=>e.value);t.length&&it({kind:"models",models:t})}).catch(()=>{}));break;case"assistant":lt(),Fe?.visibleOutput();for(const t of e.message?.content||[])"tool_use"===t?.type&&t.id&&ge.set(t.id,{at:Date.now(),name:t.name,input:t.input});{const t=(e.message?.content||[]).filter(e=>"text"===e?.type).map(e=>e.text).join("\n").trim();t&&(ot=t)}it({kind:"assistant",blocks:C(e.message?.content),parentToolUseId:e.parent_tool_use_id||null});break;case"user":for(const t of e.message?.content||[])if("tool_result"===t?.type){lt(),Fe?.visibleOutput({tool:!0});const o=ge.get(t.tool_use_id);null!=o&&ge.delete(t.tool_use_id);const n=u(o?.name,t.content);it({kind:"tool_result",toolUseId:t.tool_use_id,content:t.content,...n?{evidence:n}:{},isError:!!t.is_error,durationMs:null!=o?.at?Date.now()-o.at:void 0,parentToolUseId:e.parent_tool_use_id||null})}break;case"stream_event":{const t=e.event;"message_start"===t?.type?(_e+=Te,Te=t.message?.usage?.output_tokens||0,it({kind:"thinking_tokens",tokens:_e+Te})):"message_delta"===t?.type&&t.usage&&(Te=t.usage.output_tokens??Te,it({kind:"thinking_tokens",tokens:_e+Te}));break}case"result":if(lt(),e.session_id&&(se=e.session_id),e.is_error&&Array.isArray(e.errors)&&e.errors.some(e=>/no conversation found/i.test(String(e)))&&(se||re)&&Ue<3){Ue++;const e=Pe;Pe=!1,se=null,re=null,it({kind:"note",text:"previous session not found — continuing on a fresh one"});const t=ne;ne=I();try{t.end()}catch{}const o=e?ie:null;Be=setTimeout(()=>{Be=null,de||(yt(),null!=o&&(Pe=!0,Ie=Date.now(),Ce=!1,ne.push([{type:"text",text:o},{type:"text",text:le||te(o,{forceFull:!0})}])))},300);break}if(Xe&&("aborted"===e.subtype||"error_during_execution"===e.subtype)){qe===Ze&&(Pe=!1);break}if(e.is_error&&Fe){if($e)break;Fe.failure({message:Array.isArray(e.errors)?e.errors.join(" "):e.result,status:e.status??e.statusCode,code:e.code}),Pe=!1,gt();break}if(_e=0,Te=0,Pe=!1,Re&&(Re=!1,setTimeout(()=>{de||kt()},0)),"success"===e.subtype&&(Ue=0,se&&(re=se)),"success"===e.subtype&&(Fe?.success(),Fe=null),Ke=!1,Qe=!1,e.modelUsage&&"object"==typeof e.modelUsage){let t=null,o=-1;for(const[n,s]of Object.entries(e.modelUsage)){const e=s?.outputTokens??0;e>o&&(o=e,t=n)}t&&(Se=t)}{const t="string"==typeof e.result?e.result.replace(/\s+/g," ").trim().slice(0,200):"";it({kind:"result",subtype:e.subtype,sessionId:se,model:Se,costUsd:e.total_cost_usd,usage:e.usage,numTurns:e.num_turns,durationMs:e.duration_ms??null,denials:Array.isArray(e.permission_denials)?e.permission_denials.length:0,resultText:t||null})}nt&&clearTimeout(nt),"aborted"!==e.subtype&&"worker_completion"!==ae&&(nt=setTimeout(()=>{nt=null,tt||de||ht()},st)),(async()=>{let t=null;try{const e=await(ye?.getContextUsage?.());e&&(t=c({used:e.totalTokens,max:e.maxTokens,pct:Math.round(e.percentage),model:e.model}),e.model&&(Se=e.model))}catch{}const o=e.usage||{};it({kind:"usage",costUsd:e.total_cost_usd??null,tokens:(o.input_tokens||0)+(o.output_tokens||0),ctx:t})})();break;case"prompt_suggestion":nt&&(clearTimeout(nt),nt=null),e.suggestion&&(tt=!0,it({kind:"suggestion",text:e.suggestion}))}}}catch(e){if(lt(),de)return;if(n.signal.aborted)return;const t=e?.message||String(e);if(Fe&&Pe)return Fe.failure({message:t,status:e?.status??e?.statusCode,code:e?.code}),Pe=!1,void gt();if(Pe=!1,it({kind:"error",message:t}),Me.test(t)&&Ue<3){Ue++,Le("stream_reconnect",{failureClass:"network"}),it({kind:"note",text:`connection dropped — reconnecting (${Ue}/3)`});const e=ne;ne=I();try{e.end()}catch{}return void(Be=setTimeout(()=>{Be=null,de||yt()},Math.min(1e3*Ue,4e3)))}if(/no conversation found/i.test(t)&&se&&Ue<3){Ue++,se=null,re=null;const e=ne;ne=I();try{e.end()}catch{}return void(Be=setTimeout(()=>{Be=null,de||yt()},300))}de=!0}finally{oe.signal.removeEventListener("abort",a),Ee===n&&(Ee=null);try{await(ye?.return?.())}catch{}Ne&&(Ne(),Ne=null)}},kt=async()=>{if(de||Ae||!ue)return;const e=Oe,t=ne;if(ne=I(),Ee)try{Ee.abort()}catch{}try{t.end()}catch{}try{await e}catch{}await De(300),se=re,de||Ae||yt()},wt=async e=>{if(de)return;Qe=!0;const t=Pe&&!_(ie)?ie:null;Pe=!1;const o=Oe,n=ne;if(ne=I(),Ee)try{Ee.abort()}catch{}try{n.end()}catch{}try{await o}catch{}await De(300),se=re,de||(yt(),null!=t&&(Pe=!0,Ie=Date.now(),Ce=!1,ne.push(/^\s*\//.test(t)?[{type:"text",text:t}]:[{type:"text",text:t},{type:"text",text:le||te(t,{forceFull:!0})}]),rt({kind:"note",text:"retrying the stalled turn on a fresh connection"})))},gt=()=>{if(de||Ge||$e)return;const e=Fe,t=qe,o=ce;$e=!0;const n=Oe,s=ne;ne=I();try{s.end()}catch{}Be=setTimeout(async()=>{Be=null;try{await n}catch{}if(!de&&$e&&Fe===e&&qe===t)return await dt()?void(!de&&$e&&Fe===e&&qe===t&&($e=!1,Pe=!0,Ie=Date.now(),Ce=!1,yt(),o&&ne.push(o),ct())):($e=!1,void bt())},0)},bt=()=>{lt(),Pe=!1,Fe=null,ue=!1,it({kind:"error",message:"The configured provider could not complete this turn. Nothing else was sent.",recoverable:!0})};return H||We||yt(),{sendTurn(e,n={}){if(de||Ae||Ge||$e)return!1;if(!ue&&!ft())return!1;if(!ue&&Y)try{const e=Y();e&&(t=e,mt.cwd=e)}catch{}Pe=!0,tt=!1,ie=String(e),ae=w(n?.turnCause),nt&&(clearTimeout(nt),nt=null),Ie=Date.now(),Ce=!1,Qe=!1,Ke=!1;const s=String(e),r=Z++,i=ee;if(ee=/^\s*\/(?:compact|clear|reset)\b/i.test(s),le=/^\s*\//.test(s)?null:te(s,{promptIndex:r,forceFull:i,turnCause:ae}),ze=_(s),ce=/^\s*\//.test(s)?[{type:"text",text:s}]:[{type:"text",text:s},{type:"text",text:le}],qe++,Fe=(()=>{if(!We)return null;try{const e=T({runtime:"claude",providers:V.providers||[],providerId:V.providerId,model:V.model||mt.model||o,requestedModel:V.requestedModel||V.model||mt.model||o,policy:V.policy,bridgeHostId:V.bridgeHostId,circuit:V.circuit,capGate:V.capGate,traceId:V.traceId,turnRev:"function"==typeof V.turnRev?V.turnRev():V.turnRev,onRecord:at});return e.start(),e}catch{return null}})(),Fe){const e=Fe,t=qe,o=ce;dt().then(n=>{!de&&Pe&&Fe===e&&qe===t&&(n?(ue||yt({admitted:!0}),ne.push(o),ct()):bt())}).catch(()=>{!de&&Pe&&Fe===e&&qe===t&&bt()})}else ue||yt({admitted:!0}),ne.push(ce)},warm(){We||ue||de||Ae||yt()},get started(){return ue},async setMode(e){M.has(e)&&!de&&(ke=e,pt(e),it({kind:"mode",mode:ke}))},async setModel(e){mt.model=e,Se=e,it({kind:"note",text:`model → ${e}`}),ue&&!de&&(Pe?Re=!0:await kt())},setEffort:async e=>!(!be.has(e)||de||(ve=e,mt.effort=e,it({kind:"effort",level:e}),ue&&(Pe?(Re=!0,0):(await kt(),0)))),async listModels(){try{const e=(await(ye?.supportedModels?.())||[]).map(e=>({value:e?.value,displayName:e?.displayName,description:e?.description})).filter(e=>e.value);e.length&&it({kind:"models",models:e}),it({kind:"note",text:e.length?`models: ${e.map(e=>e.value).join(", ")}`:"no model list available"})}catch{it({kind:"note",text:"usage: /model <name>"})}},async park(){if(de||Ae||Pe||Ge||$e||ze)return!1;if(!ue)return!0;Ae=!0;const e=Oe,t=ne;ne=I(),ue=!1,nt&&(clearTimeout(nt),nt=null);try{if(Ee)try{Ee.abort()}catch{}try{t.end()}catch{}try{await e}catch{}return ye=null,re&&(se=re),!de}finally{Ae=!1}},async abort(){if(!Ge){Ge=!0;try{const e=$e,t=e?Oe:null;(Pe||e)&&(qe++,e||($e=!1),lt(),Fe?.failure({aborted:!0}),e&&(clearTimeout(Be),Be=null,ue=!1),Xe=!0,Ze=qe,clearTimeout(et),et=setTimeout(()=>{Xe=!1,Ze=null},6e3));try{await(ye?.interrupt?.())}catch{}if(t)try{await t}catch{}e&&($e=!1),(Pe||e)&&(Pe=!1,it({kind:"result",subtype:"aborted",sessionId:se}))}finally{Ge=!1}}},end(){de=!0,lt(),clearInterval(ut),clearTimeout(et),clearTimeout(Be),nt&&clearTimeout(nt),ne.end();try{oe.abort()}catch{}},get sessionId(){return se},get mode(){return ke},get turnActive(){return Pe}}}
|
package/codex-app-server.mjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{execFileSync as e,spawn as t}from"node:child_process";import r from"node:readline";export const CODEX_APP_SERVER_ENV="TP_CODEX_APP_SERVER";export const MIN_CODEX_APP_SERVER_VERSION=Object.freeze([0,144,1]);const
|
|
1
|
+
import{execFileSync as e,spawn as t}from"node:child_process";import r from"node:readline";import{codexCatastrophicHookCliArgs as i}from"./catastrophic-command-guard.mjs";export const CODEX_APP_SERVER_ENV="TP_CODEX_APP_SERVER";export const MIN_CODEX_APP_SERVER_VERSION=Object.freeze([0,144,1]);const s=(e,t="unknown")=>{const r=e instanceof Error?e:new Error(String(e||"Codex App Server transport failed"));return r.delivery=t,r};export function codexAppServerGate({env:t=process.env,execFile:r=e}={}){const i=String(t?.[CODEX_APP_SERVER_ENV]||"").trim().toLowerCase();if(["0","false","off"].includes(i))return{enabled:!1,reason:`${CODEX_APP_SERVER_ENV} disables App Server`,version:null};if(i&&!["1","true","on"].includes(i))return{enabled:!1,reason:`${CODEX_APP_SERVER_ENV} has an invalid value`,version:null};try{const e=r("codex",["--version"],{encoding:"utf8",env:t}),i=(e=>{const t=String(e||"").match(/(?:codex-cli\s+)?(\d+)\.(\d+)\.(\d+)/i);return t?t.slice(1,4).map(Number):null})(e);if(!i)return{enabled:!1,reason:`unrecognized Codex version: ${String(e).trim()}`,version:null};const s=i.join(".");return((e,t)=>{for(let r=0;r<3;r++){if(e[r]>t[r])return!0;if(e[r]<t[r])return!1}return!0})(i,MIN_CODEX_APP_SERVER_VERSION)?{enabled:!0,reason:null,version:s}:{enabled:!1,reason:`Codex ${s} is older than ${MIN_CODEX_APP_SERVER_VERSION.join(".")}`,version:s}}catch(e){return{enabled:!1,reason:`could not read Codex version: ${e?.message||e}`,version:null}}}export function buildCodexAppServerArgs({providerConfig:e,mcpUrl:t,writableDirs:r=[]}={}){const s=[...i(),"app-server","--stdio"];return e&&s.push("-c",e),t&&(s.push("-c",`mcp_servers.thinkpool.url=${JSON.stringify(t)}`),s.push("-c","mcp_servers.thinkpool.required=true")),r.length&&s.push("-c",`sandbox_workspace_write.writable_roots=${JSON.stringify(r)}`),s}const o=(e,t=-32e3)=>({code:t,message:String(e||"App Server request failed")}),n=(e,t=[])=>[{type:"text",text:String(e??"")},...t.filter(Boolean).map(e=>({type:"localImage",path:String(e)}))];export class CodexAppServerClient{constructor({cwd:e,env:r,args:i=["app-server","--stdio"],spawnImpl:s=t,onNotification:o,onServerRequest:n,onClose:a,initializeTimeoutMs:d=3e3,threadTimeoutMs:c=5e3,turnStartTimeoutMs:l=5e3,controlTimeoutMs:h=5e3}={}){this.cwd=e,this.env=r,this.args=i,this.spawnImpl=s,this.onNotification=o,this.onServerRequest=n,this.onClose=a,this.initializeTimeoutMs=d,this.threadTimeoutMs=c,this.turnStartTimeoutMs=l,this.controlTimeoutMs=h,this.child=null,this.nextId=1,this.pending=new Map,this.turnWaiters=new Map,this.completedTurns=new Map,this.mcpStartup=new Map,this.mcpWaiters=new Map,this.closedError=null,this.stderrTail=""}get alive(){return!!this.child&&!this.closedError}async start(){if(this.child)return;const e=this.spawnImpl("codex",this.args,{cwd:this.cwd,env:this.env,stdio:["pipe","pipe","pipe"]});this.child=e,r.createInterface({input:e.stdout,crlfDelay:1/0}).on("line",e=>this._onLine(e)),e.stderr?.on?.("data",e=>{this.stderrTail+=String(e),this.stderrTail.length>1200&&(this.stderrTail=this.stderrTail.slice(-1200))}),e.stdin?.on?.("error",e=>this._close(e)),e.on("error",e=>this._close(e)),e.on("close",e=>this._close(new Error(`codex app-server exited ${e??"unknown"}${this.stderrTail?`: ${this.stderrTail.trim().slice(-300)}`:""}`))),await this.request("initialize",{clientInfo:{name:"thinkpool-pair",title:"thinkpool Code",version:"1"},capabilities:{experimentalApi:!0}},this.initializeTimeoutMs),this.notify("initialized")}_write(e){if(!this.child?.stdin||this.closedError)throw this.closedError||new Error("codex app-server is not running");this.child.stdin.write(`${JSON.stringify(e)}\n`)}request(e,t={},r=0){const i=this.nextId++;return new Promise((o,n)=>{let a=null;r>0&&(a=setTimeout(()=>{this.pending.delete(i),n(s(`${e} timed out after ${r}ms`,"unknown"))},r)),this.pending.set(i,{resolve:o,reject:n,timer:a,method:e});try{this._write({id:i,method:e,params:t})}catch(e){this.pending.delete(i),a&&clearTimeout(a),n(s(e,"not_sent"))}})}notify(e,t){this._write(void 0===t?{method:e}:{method:e,params:t})}respond(e,t){this._write({id:e,result:t})}respondError(e,t){this._write({id:e,error:o(t?.message||t)})}_onLine(e){if(!String(e).trim())return;let t;try{t=JSON.parse(e)}catch{return}if(null!=t.id&&("result"in t||"error"in t)&&!t.method){const e=this.pending.get(t.id);if(!e)return;if(this.pending.delete(t.id),e.timer&&clearTimeout(e.timer),t.error){const r=s(t.error.message||`${e.method} failed`,"rejected");r.code=t.error.code,r.data=t.error.data,e.reject(r)}else e.resolve(t.result);return}if(null!=t.id&&t.method)Promise.resolve().then(()=>{if(!this.onServerRequest)throw new Error(`Unsupported Codex App Server request: ${t.method}`);return this.onServerRequest(t.method,t.params||{},t.id)}).then(e=>{try{this.respond(t.id,e??{})}catch{}}).catch(e=>{try{this.respondError(t.id,e)}catch{}});else if(t.method){if("turn/completed"===t.method){const e=t.params?.turn,r=e?.id;if(r){const e=this.turnWaiters.get(r);if(e)this.turnWaiters.delete(r),e.resolve(t.params);else for(this.completedTurns.set(r,t.params);this.completedTurns.size>32;)this.completedTurns.delete(this.completedTurns.keys().next().value)}}if("mcpServer/startupStatus/updated"===t.method){const e=t.params||{},r=String(e.threadId||""),i=String(e.name||"");if(r&&i){const t=`${r}\0${i}`;this.mcpStartup.set(t,e);const s=this.mcpWaiters.get(t);s&&"ready"===e.status?(this.mcpWaiters.delete(t),s.timer&&clearTimeout(s.timer),s.resolve(e)):s&&("failed"===e.status||e.error||e.failureReason)&&(this.mcpWaiters.delete(t),s.timer&&clearTimeout(s.timer),s.reject(new Error(`Codex MCP server ${i} failed to start: ${e.error||e.failureReason||e.status}`)))}}try{this.onNotification?.(t.method,t.params||{})}catch{}}}_close(e){if(!this.closedError){this.closedError=s(e instanceof Error?e:String(e||"codex app-server closed"),"unknown"),this.child=null;for(const{reject:e,timer:t}of this.pending.values())t&&clearTimeout(t),e(this.closedError);this.pending.clear();for(const{reject:e}of this.turnWaiters.values())e(this.closedError);this.turnWaiters.clear(),this.completedTurns.clear();for(const{reject:e,timer:t}of this.mcpWaiters.values())t&&clearTimeout(t),e(this.closedError);this.mcpWaiters.clear(),this.mcpStartup.clear();try{this.onClose?.(this.closedError)}catch{}}}async startThread({threadId:e,cwd:t,model:r,sandbox:i,approvalPolicy:s,config:o}={}){const n={cwd:t,model:r,sandbox:i,approvalPolicy:s,...o?{config:o}:{}},a=e?await this.request("thread/resume",{...n,threadId:e},this.threadTimeoutMs):await this.request("thread/start",n,this.threadTimeoutMs),d=a?.thread?.id||a?.threadId||e;if(!d)throw new Error("Codex App Server did not return a thread id");return d}waitForMcpServer({threadId:e,name:t,timeoutMs:r=this.threadTimeoutMs}={}){const i=String(e||""),s=String(t||"");if(!i||!s)return Promise.reject(new Error("Codex MCP readiness needs a thread id and server name"));const o=`${i}\0${s}`,n=this.mcpStartup.get(o);if("ready"===n?.status)return Promise.resolve(n);if(n&&("failed"===n.status||n.error||n.failureReason))return Promise.reject(new Error(`Codex MCP server ${s} failed to start: ${n.error||n.failureReason||n.status}`));if(this.mcpWaiters.has(o))return this.mcpWaiters.get(o).promise;let a,d,c=null;const l=new Promise((e,t)=>{a=e,d=t,r>0&&(c=setTimeout(()=>{this.mcpWaiters.delete(o),t(new Error(`Codex MCP server ${s} did not become ready within ${r}ms`))},r))});return this.mcpWaiters.set(o,{promise:l,resolve:a,reject:d,timer:c}),l}async startTurn({threadId:e,input:t,images:r,model:i,effort:s,approvalPolicy:o,collaborationMode:a}={}){const d=await this.request("turn/start",{threadId:e,input:n(t,r),...i?{model:i}:{},...s?{effort:s}:{},...o?{approvalPolicy:o}:{},...a?{collaborationMode:a}:{}},this.turnStartTimeoutMs),c=d?.turn?.id||d?.turnId;if(!c)throw new Error("Codex App Server did not return a turn id");return c}waitForTurn(e){if(this.completedTurns.has(e)){const t=this.completedTurns.get(e);return this.completedTurns.delete(e),Promise.resolve(t)}return this.turnWaiters.has(e)?Promise.reject(new Error(`Already waiting for Codex turn ${e}`)):new Promise((t,r)=>this.turnWaiters.set(e,{resolve:t,reject:r}))}async steer({threadId:e,turnId:t,input:r,images:i}={}){return this.request("turn/steer",{threadId:e,expectedTurnId:t,input:n(r,i)},this.controlTimeoutMs)}interrupt({threadId:e,turnId:t}={}){return this.request("turn/interrupt",{threadId:e,turnId:t},this.controlTimeoutMs)}compact({threadId:e}={}){return this.request("thread/compact/start",{threadId:e},this.controlTimeoutMs)}accountUsage(){return this.request("account/usage/read",null,this.controlTimeoutMs)}accountRateLimits(){return this.request("account/rateLimits/read",null,this.controlTimeoutMs)}startReview({threadId:e,target:t}={}){return this.request("review/start",{threadId:e,target:t||{type:"uncommittedChanges"},delivery:"inline"},this.turnStartTimeoutMs)}end(){const e=this.child;if(this._close(new Error("codex app-server ended")),e)try{e.kill("SIGTERM")}catch{}}}export function createCodexAppServer(e){return new CodexAppServerClient(e)}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import{inspectCatastrophicTool as o}from"./catastrophic-command-guard.mjs";let e="";process.stdin.setEncoding("utf8"),process.stdin.on("data",o=>{e+=o}),process.stdin.on("end",()=>{try{const s=JSON.parse(e),t=o({toolName:s?.tool_name,input:s?.tool_input,cwd:s?.cwd});if(!t.blocked)return;process.stdout.write(`${JSON.stringify({hookSpecificOutput:{hookEventName:"PreToolUse",permissionDecision:"deny",permissionDecisionReason:`${t.reason} Rule: ${t.ruleId}.`}})}\n`)}catch(o){process.stderr.write(`thinkpool catastrophic-command guard failed closed: ${o?.message||o}\n`),process.exitCode=2}});
|
package/codex-event-mapper.mjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import t from"node:fs";import{CODEX_COMMAND_CATALOG as e}from"./codex-commands.mjs";import{normalizeEditKind as s,summarizeTextDiff as i}from"./edit-diff.mjs";import{evidenceForToolResult as
|
|
1
|
+
import t from"node:fs";import{CODEX_COMMAND_CATALOG as e}from"./codex-commands.mjs";import{normalizeEditKind as s,summarizeTextDiff as i,summarizeUnifiedDiff as n}from"./edit-diff.mjs";import{evidenceForToolResult as o}from"./evidence-citations.mjs";const r=t=>String(t||"unknown").replace(/[^a-zA-Z0-9_-]/g,"_"),a=t=>`mcp__${r(t?.server)}__${r(t?.tool)}`,d=t=>t?.query||t?.action?.query||t?.action?.url||"",l=t=>{if("string"==typeof t)return t;if(null==t)return"";try{return JSON.stringify(t)}catch{return String(t)}},u=(t,e)=>"string"==typeof t&&t.trim()?t:t&&"object"==typeof t&&"string"==typeof t.message&&t.message.trim()?t.message:l(t)||e;export const isCodexReconnectProgress=t=>/^\s*Reconnecting(?:\.\.\.|…)\s+\d+\s*\/\s*\d+(?:\s+\([^\n]*\))?\s*$/i.test(u(t,""));export class CodexEventMapper{constructor({onEvent:e,model:s=null,sessionId:i=null,fsImpl:n=t,usageSnapshotForSession:o=()=>null}){this._onEvent=e,this._model=s||null,this._sessionId=i||null,this._starts=new Map,this._fileBefore=new Map,this._fs=n,this._systemSent=!1,this._usageSnapshotForSession=o,this._usageBaseline=null}_emit(t){try{this._onEvent?.(t)}catch{}}setModel(t){this._model=t||null}setUsageBaseline(t){this._usageBaseline=t||null}push(t){if(t&&"object"==typeof t)switch(t.type){case"thread.started":return this._sessionId=t.thread_id||this._sessionId||null,void(this._systemSent||(this._systemSent=!0,this._emit({kind:"system",sessionId:this._sessionId,model:this._model,commands:e})));case"turn.started":default:return;case"item.started":{const e=t.item;if(!e)return;if(this._starts.set(e.id,Date.now()),"command_execution"===e.type)this._emit({kind:"assistant",blocks:[{type:"tool_use",id:e.id,name:"Bash",input:{command:e.command||""}}],parentToolUseId:null});else if("file_change"===e.type){const t=(e.changes||[]).map(t=>{const e=s(t.kind);let i="";if("add"!==e&&t.path)try{i=this._fs.readFileSync(t.path,"utf8")}catch{}return{path:t.path||"",kind:e,content:i,diff:"string"==typeof t.diff?t.diff:""}});this._fileBefore.set(e.id,t)}else"web_search"===e.type?this._emit({kind:"assistant",blocks:[{type:"tool_use",id:e.id,name:"WebSearch",input:{query:d(e)}}],parentToolUseId:null}):"mcp_tool_call"===e.type&&this._emit({kind:"assistant",blocks:[{type:"tool_use",id:e.id,name:a(e),input:e.arguments||{}}],parentToolUseId:null});return}case"item.completed":{const e=t.item;if(!e)return;const r=this._starts.get(e.id),u=null!=r?Date.now()-r:void 0;if(this._starts.delete(e.id),"agent_message"===e.type)this._emit({kind:"assistant",blocks:[{type:"text",text:e.text||""}],parentToolUseId:null});else if("reasoning"===e.type)this._emit({kind:"assistant",blocks:[{type:"thinking",text:e.text||"",reasoningKind:"summary"}],parentToolUseId:null});else if("command_execution"===e.type){const t=null!=e.exit_code&&0!==e.exit_code;this._emit({kind:"tool_result",toolUseId:e.id,content:[{type:"text",text:e.aggregated_output||""}],isError:t,durationMs:u,parentToolUseId:null})}else if("file_change"===e.type){const t=this._fileBefore.get(e.id)||[];this._fileBefore.delete(e.id);const o=(Array.isArray(e.changes)&&e.changes.length?e.changes:t).map((e,i)=>{const n=t.find(t=>t.path&&t.path===e.path)||t[i];return{path:e.path||n?.path||"",kind:s(e.kind??n?.kind),content:n?.content||"",diff:"string"==typeof e.diff?e.diff:n?.diff||""}}).map(t=>{let e="";if("delete"!==t.kind&&t.path)try{e=this._fs.readFileSync(t.path,"utf8")}catch{}const s=n(t.diff,{context:2,maxLines:60})||i(t.content,e,{context:2,maxLines:60});return{file_path:t.path,path:t.path,kind:t.kind,added:s.added,removed:s.removed,diff_preview:s.preview,old_string:t.content,new_string:e}}),r=o[0]||{file_path:"",path:"",old_string:"",new_string:"",kind:"update"},a=o.length>1?"MultiEdit":"add"===r.kind?"Write":"Edit",d="MultiEdit"===a?{edits:o}:"Write"===a?{file_path:r.file_path,path:r.path,content:r.new_string}:r;this._emit({kind:"assistant",blocks:[{type:"tool_use",id:e.id,name:a,input:d}],parentToolUseId:null}),this._emit({kind:"tool_result",toolUseId:e.id,content:[{type:"text",text:`${"Write"===a?"wrote":"edited"} ${r.path}`}],isError:!1,durationMs:u,parentToolUseId:null})}else if("web_search"===e.type){const t=d(e);this._emit({kind:"tool_result",toolUseId:e.id,toolInput:{query:t},content:[{type:"text",text:t||"search completed"}],isError:!1,durationMs:u,parentToolUseId:null})}else if("mcp_tool_call"===e.type){const t=[{type:"text",text:e.error?.message||l(e.result)}],s=o(a(e),t);this._emit({kind:"tool_result",toolUseId:e.id,content:t,...s?{evidence:s}:{},isError:"failed"===e.status||!!e.error,durationMs:u,parentToolUseId:null})}return}case"turn.completed":{const e=t.usage||{};let s=null;try{s=this._usageSnapshotForSession(this._sessionId)}catch{}const i=Number(s?.context?.used),n=Number(s?.context?.max);Number.isFinite(i)&&i>=0&&Number.isFinite(n)&&n>0&&this._emit({kind:"usage",ctx:{used:i,max:n,pct:Math.min(100,Math.max(0,Math.round(i/n*100))),over:i>n,model:this._model}});const o=s?.total||e,r=this._usageBaseline?.total||{},a=Math.max(0,(Number(o.input_tokens)||0)-(Number(r.input_tokens)||0)),d=Math.min(a,Math.max(0,(Number(o.cached_input_tokens)||0)-(Number(r.cached_input_tokens)||0))),l=Math.max(0,(Number(o.output_tokens)||0)-(Number(r.output_tokens)||0));return this._usageBaseline=s||null,void this._emit({kind:"result",subtype:"success",sessionId:this._sessionId,model:this._model,costUsd:null,usage:{input_tokens:a-d,cache_read_input_tokens:d,cache_creation_input_tokens:0,output_tokens:l},numTurns:1,durationMs:void 0,denials:0,resultText:null})}case"turn.failed":return this._emit({kind:"error",message:u(t.message??t.error,"codex turn failed"),recoverable:!0}),void this._emit({kind:"result",subtype:"error_during_execution",sessionId:this._sessionId,model:this._model,costUsd:null,usage:null,numTurns:1,durationMs:void 0,denials:0,resultText:null});case"error":{const e=u(t.message??t.error,"codex error");if(/^\s*Reconnecting(?:\.\.\.|…)\s+\d+\s*\/\s*\d+(?:\s+\([^\n]*\))?\s*$/i.test(u(e,"")))return;return void this._emit({kind:"error",message:e,recoverable:!0})}}}}
|
package/codex-session.mjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{execFileSync as e,spawn as t}from"node:child_process";import r from"node:fs";import n from"node:os";import o from"node:path";import s from"node:readline";import{buildCodexAppServerArgs as a,codexAppServerGate as i,createCodexAppServer as l}from"./codex-app-server.mjs";import{CodexEventMapper as u}from"./codex-event-mapper.mjs";import{startCodexMcpHttp as d}from"./codex-mcp-http.mjs";import{CODEX_THINKPOOL_FIRST_TURN_PREAMBLE as c,buildThinkPoolTurnGuidance as p,createRoomContextSelector as m,usesFullThinkPoolReminder as f}from"./thinkpool-room-prompt.mjs";import{questionAnswerResponse as h}from"./question-response.mjs";import{codexReviewTarget as x,isCodexCompactCommand as g}from"./codex-commands.mjs";import{createCumulativeEventRelay as y}from"./cumulative-event-relay.mjs";import{stallDecision as v,stallEvent as b}from"./turn-stall.mjs";import{codexNativeReasoningEffort as w,codexReasoningEfforts as k,normalizeCodexReasoningEffort as C,normalizeReasoningEffortList as I}from"./reasoning-effort.mjs";const M=new Set(["read-only","workspace-write","danger-full-access"]);export const CODEX_MODE_CONFIG={default:{sandbox:"workspace-write",approvalPolicy:"untrusted"},acceptEdits:{sandbox:"workspace-write",approvalPolicy:"on-request"},plan:{sandbox:"read-only",approvalPolicy:"never"},review:{sandbox:"read-only",approvalPolicy:"never"},bypassPermissions:{sandbox:"danger-full-access",approvalPolicy:"never"}};export function codexConfigForMode(e){return CODEX_MODE_CONFIG[e]||CODEX_MODE_CONFIG.default}export function codexDefaultCollaborationMode(e,t){if(!e)return null;const r=w(t);return{mode:"default",settings:{model:String(e),reasoning_effort:r,developer_instructions:null}}}export function defaultModeForRuntime(e){return"codex"===e?"bypassPermissions":"default"}export async function codexOneShot({prompt:e,model:s,timeoutMs:a=22e3,outputSchema:i,cwd:l,tmpDir:u=n.tmpdir(),fsImpl:d=r,spawnImpl:c=t}={}){const p=String(e||"").trim();if(!p)throw new Error("codex one-shot requires a prompt");const m=!l,f=l||d.mkdtempSync(o.join(u,"thinkpool-codex-one-shot-"));let h=null;try{i&&(h=o.join(f,"output-schema.json"),d.writeFileSync(h,`${JSON.stringify(i)}\n`,{mode:384}));const e=["exec","--json","--ephemeral","--ignore-rules","--sandbox","read-only","--skip-git-repo-check","--color","never","-c",'approval_policy="never"',"-c",'model_reasoning_effort="low"',...s?["-m",String(s)]:[],...h?["--output-schema",h]:[],p];return await new Promise((t,r)=>{let n,o,s="",i="",l=!1;const u=(e,n)=>{l||(l=!0,o&&clearTimeout(o),e?r(e):t(n))};try{n=c("codex",e,{cwd:f,env:process.env,stdio:["ignore","pipe","pipe"]})}catch(e){return void u(e)}n.stdout.on("data",e=>{s+=e.toString(),s.length>1e6&&(s=s.slice(-1e6))}),n.stderr.on("data",e=>{i+=e.toString(),i.length>2e3&&(i=i.slice(-2e3))}),n.on("error",e=>u(e)),n.on("close",e=>{if(l)return;const t=s.split(/\r?\n/).filter(Boolean).flatMap(e=>{try{const t=JSON.parse(e);return"item.completed"===t?.type&&"agent_message"===t?.item?.type&&"string"==typeof t.item.text?[t.item.text.trim()]:[]}catch{return[]}}).filter(Boolean).at(-1);0===e&&t?u(null,t):u(new Error(`Codex cleanup failed${null==e?"":` (${e})`}${i.trim()?`: ${i.trim().slice(-240)}`:""}`))}),o=setTimeout(()=>{try{n.kill("SIGTERM")}catch{}u(new Error("Codex cleanup timed out"))},Math.max(1e3,Number(a)||22e3))})}finally{if(m)try{d.rmSync(f,{recursive:!0,force:!0})}catch{}}}export function codexWritableDirsForSandbox(e,t=[]){return"read-only"===normalizeCodexSandbox(e)?[]:t}export function codexExtraWritableDirsForMode(e,t,r=[]){return"review"===e?[]:codexWritableDirsForSandbox(t,r)}export function readCodexDefaultModel({home:e=process.env.CODEX_HOME||o.join(n.homedir(),".codex"),fsImpl:t=r}={}){try{const r=t.readFileSync(o.join(e,"config.toml"),"utf8").split(/^\s*\[/m,1)[0].match(/^\s*model\s*=\s*["']([^"']+)["']\s*(?:#.*)?$/m);return r?.[1]?.trim()||null}catch{return null}}export function readCodexModels({home:e=process.env.CODEX_HOME||o.join(n.homedir(),".codex"),fsImpl:t=r}={}){try{const r=JSON.parse(t.readFileSync(o.join(e,"models_cache.json"),"utf8"));return(Array.isArray(r?.models)?r.models:[]).filter(e=>e?.slug&&"hide"!==e.visibility).sort((e,t)=>(Number(e.priority)||999)-(Number(t.priority)||999)).map(e=>({value:e.slug,displayName:e.display_name||e.slug,description:e.description||"",reasoningEfforts:I(e.supported_reasoning_levels)}))}catch{return[]}}const S=new Map;function _(e,{home:t,fsImpl:r}){if(!e)return null;const n=`${t}\0${e}`,s=S.get(n);if(s)return s;const a=o.join(t,"sessions");try{const t=`${e}.jsonl`,s=r.readdirSync(a,{recursive:!0,withFileTypes:!0}).find(e=>e.isFile()&&e.name.endsWith(t));if(!s)return null;const i=o.join(s.parentPath||s.path||a,s.name);return S.set(n,i),i}catch{return null}}export function codexThreadCanResume(e,{home:t=process.env.CODEX_HOME||o.join(n.homedir(),".codex"),fsImpl:s=r}={}){if(!e)return!1;const a=_(e,{home:t,fsImpl:s});if(!a)return!0;try{return Number(s.statSync(a).size)>0}catch{return!0}}export function readCodexThreadUsage(e,{home:t=process.env.CODEX_HOME||o.join(n.homedir(),".codex"),fsImpl:s=r,tailBytes:a=4194304}={}){const i=_(e,{home:t,fsImpl:s});if(!i)return null;let l;try{const e=Number(s.statSync(i).size)||0,t=Math.min(e,Math.max(65536,a)),r=Buffer.alloc(t);l=s.openSync(i,"r"),s.readSync(l,r,0,t,e-t);const n=r.toString("utf8").split("\n");for(let e=n.length-1;e>=0;e--){let t;try{t=JSON.parse(n[e])}catch{continue}if("token_count"!==t?.payload?.type||!t.payload.info)continue;const r=t.payload.info,o=r.last_token_usage||{},s=r.total_token_usage||{},a=Number(o.total_tokens),i=Number(r.model_context_window);return!Number.isFinite(a)||a<0||!Number.isFinite(i)||i<=0?null:{context:{used:Math.floor(a),max:Math.floor(i)},total:{input_tokens:Math.max(0,Number(s.input_tokens)||0),cached_input_tokens:Math.max(0,Number(s.cached_input_tokens)||0),output_tokens:Math.max(0,Number(s.output_tokens)||0)},rateLimits:t.payload.rate_limits||null}}}catch{return null}finally{if(null!=l)try{s.closeSync(l)}catch{}}return null}export function codexUsageSnapshotFromAppServer(e){const t=Number(e?.last?.totalTokens),r=Number(e?.modelContextWindow);if(!Number.isFinite(t)||t<0||!Number.isFinite(r)||r<=0)return null;const n=e?.total||{};return{context:{used:Math.floor(t),max:Math.floor(r)},total:{input_tokens:Math.max(0,Number(n.inputTokens)||0),cached_input_tokens:Math.max(0,Number(n.cachedInputTokens)||0),output_tokens:Math.max(0,Number(n.outputTokens)||0)},rateLimits:e?.rateLimits||null}}export function normalizeCodexSandbox(e){return M.has(e)?e:"workspace-write"}export function codexPublishAccess({cwd:t,env:r=process.env,tmpdir:s=n.tmpdir(),execFile:a=e}={}){const i=[];try{const e=String(a("git",["rev-parse","--git-common-dir"],{cwd:t,encoding:"utf8"})||"").trim();if(e){const r=o.resolve(t,e),n=o.resolve(t);r===n||r.startsWith(n+o.sep)||i.push(r)}}catch{}return{writableDirs:i,env:{...r,NPM_CONFIG_CACHE:o.join(s,"thinkpool-npm-cache")}}}export const CODEX_ROOM_CASCADE_REMINDER=["thinkpool ROOM ROLE ENFORCEMENT: obey the authoritative TERMINAL ROLE above. A person-authored request for a new, separate, independent, top-level, or main terminal uses open_main_terminal — NEVER spawn_terminal — even for ordinary work or a handoff. If the wording includes new or separate terminal, main-terminal routing wins. Use spawn_terminal only for agent-decided bounded worker slices or when the person explicitly asks for a worker, sub-terminal, Ensemble lane, or delegated slice. If open_main_terminal is unavailable, say so; never substitute an Ensemble child. Leaf, worker, Side, and managed Flow lanes work directly and must not delegate. Never use hidden in-process subagents."].join(" ");export function buildCodexPrompt({text:e,terminalRolePrompt:t,rolePrompt:r,roomContext:n,firstTurn:o=!1,promptIndex:s=0,forceFullReminder:a=!1,turnCause:i=null}){const l="function"==typeof n?n():n,u=f({promptIndex:s,forceFull:a,turnCause:i}),d=[o?c:"",u?t:"",p({text:e,promptIndex:s,forceFull:a,turnCause:i}),u?r||CODEX_ROOM_CASCADE_REMINDER:"",l].map(e=>String(e||"").trim()).filter(Boolean);return d.length?`<thinkpool-context>\n${d.join("\n\n")}\n</thinkpool-context>\n\n${String(e??"")}`:String(e??"")}export function buildCodexExecArgs({sessionId:e,model:t,effort:r,sandbox:n,approvalPolicy:s="untrusted",providerConfig:a,mcpUrl:i,writableDirs:l=[],images:u=[],prompt:d}){const c=normalizeCodexSandbox(n),p=e?["exec","resume","--json","--skip-git-repo-check",..."danger-full-access"===c?["--dangerously-bypass-approvals-and-sandbox"]:["-c",`sandbox_mode="${c}"`]]:["exec","--json","--sandbox",c,"--skip-git-repo-check"];t&&p.push("-m",t),p.push("-c",`approval_policy="${s}"`);const m=w(r);if(m&&p.push("-c",`model_reasoning_effort="${m}"`),a&&p.push("-c",a),!e)for(const e of l)p.push("--add-dir",e);i&&(p.push("-c",`mcp_servers.thinkpool.url=${JSON.stringify(i)}`),p.push("-c","mcp_servers.thinkpool.required=true"));let f=!1;for(const e of u)o.isAbsolute(String(e||""))&&(p.push("--image",String(e)),f=!0);return f&&p.push("--"),e&&p.push(e),p.push(String(d??"")),p}export function codexExecFailedBeforeTurn({stderr:e="",eventTypes:t=[]}={}){if((Array.isArray(t)?t.map(e=>String(e||"")):[]).some(e=>e&&"thread.started"!==e))return!1;const r=String(e||"");return/timed out handshaking with MCP server/i.test(r)||/thread\/resume failed/i.test(r)&&/failed to initialize session/i.test(r)}const T=/\brm\s+\S|\brmdir\s+\S|\bgit\s+(push\s+(-f|--force)|reset\s+--hard|clean\s+-[a-z]*f)|\bdrop\s+(table|database)\b|\b(mkfs|dd)\b|\bsudo\b|>\s*\/dev\/|\bchmod\s+-R|\bchown\s+-R|\bkillall\b|\btruncate\b/i;export function codexApprovalCard(e,t={},r=null){const n=String(t.approvalId||t.itemId||`${t.turnId||"turn"}:${e}`);if("item/tool/requestUserInput"===e){const e=Array.isArray(t.questions)?t.questions:[];return{id:n,toolName:"AskUserQuestion",input:{questions:e},risk:"ask",questions:e,asker:"Codex",answerFormat:"codex",...Number.isFinite(t.autoResolutionMs)&&t.autoResolutionMs>0?{autoResolutionMs:t.autoResolutionMs}:{}}}if("item/commandExecution/requestApproval"===e){const e=t.command||r?.command||"";return{id:n,toolName:"Bash",input:{command:e,cwd:t.cwd||r?.cwd||"",...t.reason?{reason:t.reason}:{}},risk:T.test(e)?"high":"medium"}}if("item/fileChange/requestApproval"===e){const e=Array.isArray(r?.changes)?r.changes:[],o=e[0]||{};return{id:n,toolName:e.length>1?"MultiEdit":"add"===o.kind?"Write":"Edit",input:e.length>1?{edits:e.map(e=>({file_path:e.path||"",path:e.path||"",kind:e.kind||"update"})),...t.reason?{reason:t.reason}:{}}:{file_path:o.path||t.grantRoot||"",path:o.path||t.grantRoot||"",kind:o.kind||"update",...t.reason?{reason:t.reason}:{}},risk:"medium"}}return null}export function codexApprovalResponse(e){return{decision:"always"===e?"acceptForSession":"allow"===e?"accept":"decline"}}export function codexUserInputResponse(e,t=[]){return h(e,t)}function E(e){if(!e||"object"!=typeof e)return e;const t={...e};switch(e.type){case"agentMessage":return{...t,type:"agent_message"};case"commandExecution":return{...t,type:"command_execution",aggregated_output:e.aggregatedOutput||"",exit_code:e.exitCode};case"fileChange":return{...t,type:"file_change"};case"webSearch":return{...t,type:"web_search"};case"mcpToolCall":return{...t,type:"mcp_tool_call",arguments:e.arguments||{},result:e.result,error:e.error};case"reasoning":return{...t,type:"reasoning",text:[...e.summary||[],...e.content||[]].join("\n")};default:return t}}export function startCodexSession({cwd:e,model:c,models:p=[],effort:h="high",resume:I,env:M,sandbox:S,mode:_="default",onEvent:T,providerConfig:F,terminalRolePrompt:N,rolePrompt:O,roomContext:R,mcpServers:P,prepareCwd:A=null,requestPermission:D,appServerGate:U=i,appServerFactory:$=l,mcpHttpFactory:j=d,spawnImpl:q=t,admitStart:L=null,stallOptions:X={}}){let G=CODEX_MODE_CONFIG[_]?_:"default",B=codexConfigForMode(G);S=normalizeCodexSandbox(S||B.sandbox);const W=codexPublishAccess({cwd:e,env:{...process.env,...M||{}}}),z=W.env,H=I||null,J=!H||codexThreadCanResume(H,{home:z.CODEX_HOME||o.join(n.homedir(),".codex")});let K=J?H:null,V=K?1:0,Q=0,Y=!0;const Z=m(R);let ee=null,te=!1,re=!1,ne=!1;const oe="function"==typeof X.now?X.now:Date.now,se="function"==typeof X.setInterval?X.setInterval:setInterval,ae="function"==typeof X.clearInterval?X.clearInterval:clearInterval,ie=Number.isFinite(X.stallMs)?Math.max(1,Number(X.stallMs)):Math.max(3e4,parseInt(z.TP_STALL_MS,10)||9e4),le=Number.isFinite(X.forceStopMs)?Math.max(ie,Number(X.forceStopMs)):Math.max(3*ie,parseInt(z.TP_FORCE_STOP_MS,10)||3e5),ue=Number.isFinite(X.intervalMs)?Math.max(1,Number(X.intervalMs)):5e3;let de=oe(),ce=!1,pe=!1,me=!1,fe=!1,he=!0,xe=0,ge=null,ye=c||null,ve=C(h,{models:p,model:ye}),be=K?readCodexThreadUsage(K):null,we=null;const ke=U({env:z});let Ce=!ke.enabled,Ie=null,Me=!1;const Se=new WeakSet;let _e=null,Te=null,Ee=!1;const Fe=new Set,Ne=new Map,Oe=new Map;let Re=Promise.resolve(),Pe=Promise.resolve();const Ae=[],De=y(e=>{try{T?.(e)}catch{}}),Ue=()=>{de=oe(),ce=!1},$e=e=>{Ue(),De(e)};$e.cancel=()=>De.cancel();const je=new u({onEvent:$e,model:c||null,usageSnapshotForSession:e=>{const t=readCodexThreadUsage(e);return t&&(be=t),t||be}}),qe=e=>{"turn.completed"!==e?.type&&"turn.failed"!==e?.type||(ne=!1),je.push(e)},Le=e=>{ne=!1,$e(e)},Xe=({retry:e=!1}={})=>{ne=!0,de=oe(),ce=!1,me=!1,fe=!1,he=!0,e||(pe=!1)},Ge=e=>$e({kind:"note",text:e}),Be=(e=_e)=>{const t=String(e||"");if(t)for(Fe.add(t);Fe.size>64;)Fe.delete(Fe.values().next().value)},We=(e={})=>{const t=String(e.turnId||"");return!(!t||re||te||Fe.has(t)||t!==String(_e||""))};async function ze(){const e=P?.thinkpool;return e?(we||(we=j({sdkServer:e})),we):null}function He(e,t={}){if("thread/tokenUsage/updated"===e){if(t.threadId&&K&&String(t.threadId)!==String(K))return;const e=codexUsageSnapshotFromAppServer(t.tokenUsage);if(!e)return;if(be=e,Ee){const{used:t,max:r}=e.context;$e({kind:"usage",ctx:{used:t,max:r,pct:Math.min(100,Math.max(0,Math.round(t/r*100))),over:t>r,model:ye}})}return}if("turn/started"===e&&t.turn?.id){const e=String(t.turn.id);if(re||te)return void Be(e);if(Te){const t=Te;return Te=null,void t.resolve(e)}return _e||(_e=e),void Ue()}if("turn/plan/updated"!==e&&"item/agentMessage/delta"!==e&&"item/started"!==e&&"item/completed"!==e&&"error"!==e||We(t)){if(Ue(),"turn/plan/updated"===e){const e=(Array.isArray(t.plan)?t.plan:[]).map(({step:e,status:t})=>({content:e||"",activeForm:e||"",status:"inProgress"===t?"in_progress":"completed"===t?"completed":"pending"})).filter(e=>e.content);return void $e({kind:"assistant",blocks:[{type:"tool_use",id:`codex-plan:${t.turnId||"active"}`,name:"TodoWrite",input:{todos:e}}],parentToolUseId:null})}if("item/agentMessage/delta"===e){if(!t.delta)return;const e=t.itemId||`turn:${t.turnId||_e||"active"}`,r=Oe.get(e)||{cid:`codex-stream:${K||"thread"}:${e}`,text:""};return r.text+=String(t.delta),Oe.set(e,r),void $e({kind:"assistant_stream",cid:r.cid,text:r.text,itemId:e,streaming:!0})}if("item/started"===e||"item/completed"===e){const r=t.item;if(!r)return;if("item/started"===e&&Ne.set(r.id,r),"item/completed"===e&&(Ne.set(r.id,r),"agentMessage"===r.type&&Oe.has(r.id))){const e=Oe.get(r.id);return Oe.delete(r.id),void $e({kind:"assistant",blocks:[{type:"text",text:r.text||e.text}],parentToolUseId:null,replacesCid:e.cid})}return void qe({type:"item/started"===e?"item.started":"item.completed",item:E(r)})}if("error"===e){const e=t.error?.message||t.message||"codex app-server error";qe({type:"error",message:e})}}}async function Je(e,t={}){if(!We(t))return"item/tool/requestUserInput"===e?{answers:{}}:{decision:"decline"};const r=codexApprovalCard(e,t,Ne.get(t.itemId));if(!r)throw new Error(`Unsupported Codex App Server request: ${e}`);let n="deny";xe++;try{n=await(D?.(r))}catch{}finally{xe=Math.max(0,xe-1),Ue()}return"item/tool/requestUserInput"===e?codexUserInputResponse(n,r.questions):codexApprovalResponse(n)}async function Ke(){if(Ce)return!1;if(!1===Ie?.alive)return Me=!1,Ce=!0,!1;if(Ie&&Me)return!0;let t;try{t=await ze()}catch(e){Ge(`thinkpool peer tools unavailable: ${e?.message||e}`)}try{if(!Ie){const r=$({cwd:e,env:z,args:a({providerConfig:F,mcpUrl:t?.url,writableDirs:codexExtraWritableDirsForMode(G,S,W.writableDirs)}),onNotification:He,onServerRequest:Je,onClose:e=>{re||Ce||Se.has(r)||(Me=!1,Ce=!0,Ge(`Codex App Server closed; later turns will use codex exec: ${e?.message||e}`))}});Ie=r,await Ie.start(),Ue()}return K=await Ie.startThread({threadId:V>0?K:null,cwd:e,model:ye,sandbox:S,approvalPolicy:B.approvalPolicy,config:{"features.default_mode_request_user_input":!0}}),Ue(),t?.url&&(await Ie.waitForMcpServer({threadId:K,name:"thinkpool"}),Ue()),Me=!0,qe({type:"thread.started",thread_id:K}),!0}catch{Ce=!0;try{Ie?.end()}catch{}return Ie=null,Me=!1,!1}}function Ve({force:e=!1}={}){const t=Ie,r=_e;if(t&&e){Be(r),Ce=!0,Me=!1;try{t.end()}catch{}Ie===t&&(Ie=null)}else if(t&&r)Be(r),Promise.resolve(t.interrupt({threadId:K,turnId:r})).catch(()=>{Ce=!0,Me=!1;try{t.end()}catch{}Ie===t&&(Ie=null)});else if(t){Ce=!0,Me=!1;try{t.end()}catch{}Ie===t&&(Ie=null)}if(ee){const e=ee;try{e.kill("SIGTERM")}catch{}setTimeout(()=>{if(ee===e)try{e.kill("SIGKILL")}catch{}},1500)}}async function Qe(e,t={}){if(re||te||fe)return!0;if(!await Ke())return!1;if(re||te||fe)return!0;je.setUsageBaseline(K?readCodexThreadUsage(K):null),ne=!0;try{if(he=!1,_e=await Ie.startTurn({threadId:K,input:e,images:Array.isArray(t.images)?t.images:[],model:ye,effort:w(ve),approvalPolicy:B.approvalPolicy,collaborationMode:codexDefaultCollaborationMode(ye,ve)}),Ue(),te||re){Be(_e);try{await Ie.interrupt({threadId:K,turnId:_e})}catch{}return!0}const r=await Ie.waitForTurn(_e);Be(r?.turn?.id||_e);const n=r?.turn?.status;te?Le({kind:"result",subtype:"aborted",sessionId:K,model:ye,costUsd:null,usage:null,numTurns:1,durationMs:r?.turn?.durationMs,denials:0,resultText:null}):"interrupted"===n&&(me||fe)||("interrupted"===n?Le({kind:"result",subtype:"aborted",sessionId:K,model:ye,costUsd:null,usage:null,numTurns:1,durationMs:r?.turn?.durationMs,denials:0,resultText:null}):"failed"===n?(me=!1,qe({type:"turn.failed",message:r?.turn?.error?.message||"codex turn failed"})):(me=!1,qe({type:"turn.completed"})))}catch(e){Ce=!0,Me=!1,Be(_e);try{Ie?.end()}catch{}Ie=null,re||me||fe||qe({type:"turn.failed",message:`codex app-server turn failed: ${e?.message||e}`})}finally{for(const e of Oe.values())e.text&&$e({kind:"assistant",blocks:[{type:"text",text:e.text}],parentToolUseId:null,replacesCid:e.cid});_e=null,ne=!1,Ne.clear(),Oe.clear()}return!0}async function Ye(e){if(re||te||fe)return!0;if(!await Ke())return!1;if(re||te||fe)return!0;je.setUsageBaseline(K?readCodexThreadUsage(K):null),ne=!0;try{he=!1;const t=await Ie.startReview({threadId:K,target:x(e)});if(_e=t?.turn?.id||t?.turnId,!_e)throw new Error("Codex review/start returned no turn id");if(Ue(),te||re){Be(_e);try{await Ie.interrupt({threadId:K,turnId:_e})}catch{}return!0}const r=await Ie.waitForTurn(_e);Be(r?.turn?.id||_e);const n=r?.turn?.status;te?Le({kind:"result",subtype:"aborted",sessionId:K,model:ye,costUsd:null,usage:null,numTurns:1,durationMs:r?.turn?.durationMs,denials:0,resultText:null}):"interrupted"===n&&(me||fe)||("interrupted"===n?Le({kind:"result",subtype:"aborted",sessionId:K,model:ye,costUsd:null,usage:null,numTurns:1,durationMs:r?.turn?.durationMs,denials:0,resultText:null}):"failed"===n?(me=!1,qe({type:"turn.failed",message:r?.turn?.error?.message||"codex review failed"})):(me=!1,qe({type:"turn.completed"})))}catch(e){Ce=!0,Me=!1,Be(_e);try{Ie?.end()}catch{}Ie=null,re||me||fe||qe({type:"turn.failed",message:`codex review failed: ${e?.message||e}`})}finally{for(const e of Oe.values())e.text&&$e({kind:"assistant",blocks:[{type:"text",text:e.text}],parentToolUseId:null,replacesCid:e.cid});_e=null,ne=!1,Ne.clear(),Oe.clear()}return!0}async function Ze(t,n={}){if(re||te||fe)return;let o;try{o=await ze()}catch(e){Ge(`thinkpool peer tools unavailable: ${e?.message||e}`)}try{await(o?.handoff?.())}catch(e){Ge(`thinkpool peer handoff failed: ${e?.message||e}`)}const a=buildCodexExecArgs({sessionId:0===V?null:K,model:ye,effort:ve,sandbox:S,approvalPolicy:B.approvalPolicy,providerConfig:F,mcpUrl:o?.url,writableDirs:codexExtraWritableDirsForMode(G,S,W.writableDirs),images:Array.isArray(n.images)?n.images:[],prompt:t});return je.setUsageBaseline(K?readCodexThreadUsage(K):null),new Promise(t=>{let n;try{n=r.openSync("/dev/null","r")}catch{}const o={cwd:e,env:z,stdio:["ignore","pipe","pipe"]};if(null!=n&&(o.stdio=[n,"pipe","pipe"]),ne=!0,he=!1,ee=q("codex",a,o),null!=n)try{r.closeSync(n)}catch{}const i=[];s.createInterface({input:ee.stdout,crlfDelay:1/0}).on("line",e=>{if(!e.trim())return;let t;try{t=JSON.parse(e)}catch{return}i.push(t.type),"thread.started"===t.type&&t.thread_id&&(K=K||t.thread_id),"turn.completed"!==t.type&&"turn.failed"!==t.type||(u=!0,me=!1),qe(t)});let l="",u=!1,d=!1;ee.stderr.on("data",e=>{l+=e.toString(),l.length>800&&(l=l.slice(-800))}),ee.on("error",e=>{d||(d=!0,ee=null,me||fe||Le({kind:"error",message:`codex failed to start: ${e.message}`,recoverable:!0}),t({preTurnInitFailure:!1}))}),ee.on("close",e=>{if(d)return;d=!0,ee=null,ne=!1;const r=`codex exec exited ${e}${l?": "+l.trim().slice(-300):""}`;0===e||null==e||u||!codexExecFailedBeforeTurn({stderr:l,eventTypes:i})||te||fe?((!me&&!fe||u||te)&&(te&&!u?Le({kind:"result",subtype:"aborted",sessionId:K,model:ye,costUsd:null,usage:null,numTurns:1,durationMs:void 0,denials:0,resultText:null}):te||u||0===e||null==e||Le({kind:"error",message:r,recoverable:!0})),t({preTurnInitFailure:!1})):t({preTurnInitFailure:!0,message:r})})})}function et(){if(re)return;const e=Ae.shift();e&&(Pe=Pe.then(async()=>{Xe();const t=e.options?.turnCause||null,r=f({promptIndex:e.promptIndex,forceFull:e.forceFullReminder,turnCause:t}),n=buildCodexPrompt({text:e.text,terminalRolePrompt:N,rolePrompt:O,roomContext:()=>Z({force:r}),firstTurn:0===V,promptIndex:e.promptIndex,forceFullReminder:e.forceFullReminder,turnCause:t}),o=/^\s*\/review(?:\s|$)/i.test(String(e.text||""));let s=!1,a=!1;for(;!re&&!te&&!fe;){if(!(o?await Ye(e.text):await Qe(n,e.options)))if(o)me=!1,Le({kind:"error",message:"Codex review is unavailable without the tested App Server runtime.",recoverable:!0});else{me&&(me=!1,s=!0,Xe({retry:!0}),De({kind:"note",text:"retrying the stalled turn on a fresh connection"}));const t=await Ze(n,e.options);if(t?.preTurnInitFailure){if(!(a||re||te||fe)){a=!0,Xe({retry:!0}),De({kind:"note",text:"Codex initialization failed before the turn started; retrying once with a fresh thinkpool connection."});continue}Le({kind:"error",message:t.message,recoverable:!0})}}if(!me||s||fe||re||te)break;me=!1,s=!0,Xe({retry:!0}),De({kind:"note",text:"retrying the stalled turn on a fresh connection"})}V++,et()}))}return H&&!J&&queueMicrotask(()=>Ge("The stopped Codex turn had no resumable context; continuing in a fresh thread.")),["0","false","off"].includes(String(z.TP_CODEX_APP_SERVER||"").trim().toLowerCase())||ke.enabled||queueMicrotask(()=>Ge(`Codex App Server disabled: ${ke.reason}`)),ge=se(()=>{const e=oe()-de,t=v({turnActive:ne,awaitingUser:xe,quietMs:e,stallMs:ie,forceStopMs:le,stalledSent:ce,stallRetried:pe});if("none"===t)return;if("retry"===t&&!he)return fe=!0,me=!1,De({kind:"error",recoverable:!0,message:"Codex stopped responding after the turn began. The transport was stopped, but the prompt was not replayed automatically."}),Le({kind:"result",subtype:"aborted",sessionId:K,model:ye,costUsd:null,usage:null,numTurns:1,durationMs:void 0,denials:0,resultText:null}),void Ve({force:!0});const r=b(t,e);if(r&&De(r),"status"!==t){if("retry"===t)return pe=!0,me=!0,de=oe(),ce=!1,void Ve();fe=!0,me=!1,Ae.length=0,Le({kind:"result",subtype:"aborted",sessionId:K,model:ye,costUsd:null,usage:null,numTurns:1,durationMs:void 0,denials:0,resultText:null}),Ve({force:!0})}else ce=!0},ue),ge.unref?.(),{get sessionId(){return K},get usageSnapshot(){return be},get turnActive(){return ne||Ee},get canSteer(){return!(!Ie||!1===Ie.alive||!Me||Ce)},get started(){return ne||Ee||!!ee||!(!Ie||!1===Ie.alive)},sendTurn(t,r={}){if(re)return!1;if(!ne&&0===Ae.length&&!Me&&!ee){const e="function"==typeof L?L():{ok:!0};if(!1===e?.ok)return $e({kind:"error",message:e.reason||"Host memory is critically low. This Codex runtime was not started.",recoverable:!0}),!1}const n=Q++,o=Y;if(Y=g(t)||/^\s*\/(?:reset|clear)\b/i.test(String(t||"")),!ne&&0===V&&A)try{e=A()||e}catch{}if(ne&&Ie&&_e){Ue();const e=r?.turnCause||null,s=f({promptIndex:n,forceFull:o,turnCause:e}),a=buildCodexPrompt({text:t,terminalRolePrompt:N,rolePrompt:O,roomContext:()=>Z({force:s}),promptIndex:n,forceFullReminder:o,turnCause:e}),i=_e;return Re=Re.then(()=>Ie.steer({threadId:K,turnId:i,input:a,images:Array.isArray(r.images)?r.images:[]})).catch(e=>{"rejected"===e?.delivery||"not_sent"===e?.delivery?(Ae.push({text:t,options:r,promptIndex:n,forceFullReminder:o}),ne||1!==Ae.length||et()):$e({kind:"error",message:"Codex steering delivery is uncertain; the message was not replayed automatically.",recoverable:!0})}),!0}return Ae.push({text:t,options:r,promptIndex:n,forceFullReminder:o}),ne||Ee||(te=!1,Xe()),1!==Ae.length||Ee||et(),!0},async park(){if(re||ne||Ee||Ae.length)return!1;const e=Pe;try{await e}catch{}try{await Re}catch{}if(re||ne||Ee||Ae.length||ee||_e)return!1;const t=Ie;if(t){Se.add(t),Ie=null,Me=!1,Ne.clear(),Oe.clear();try{t.end()}catch{return!1}}return!0},abort(){const e=Pe,t=ne&&!_e&&!ee;te=!0,me=!1,fe=!1,Ae.length=0,Be(_e);let r=Promise.resolve();if(Ie&&_e&&(r=Ie.interrupt({threadId:K,turnId:_e}).catch(()=>{})),ee){try{ee.kill("SIGTERM")}catch{}setTimeout(()=>{try{ee?.kill("SIGKILL")}catch{}},1500)}else t&&Le({kind:"result",subtype:"aborted",sessionId:K,model:ye,costUsd:null,usage:null,numTurns:0,durationMs:void 0,denials:0,resultText:null});return r.then(()=>e).catch(()=>e).then(()=>{})},end(){if(re=!0,ne=!1,ge&&ae(ge),$e.cancel(),Ae.length=0,Be(_e),ee)try{ee.kill()}catch{}try{Ie?.end()}catch{}we?.then(e=>e.close()).catch(()=>{})},setModel(e){if(!e||ne)return!1;ye=String(e),je.setModel(ye);const t=C(ve,{models:p,model:ye});return t!==ve&&(ve=t,$e({kind:"effort",level:ve})),!0},setMode:e=>!(ne||!CODEX_MODE_CONFIG[e]||(G=e,B=codexConfigForMode(G),S=B.sandbox,K=null,V=0,be=null,Me=!1,0)),setEffort:e=>!(ne||!k({models:p,model:ye}).includes(e)||(ve=e,$e({kind:"effort",level:ve}),0)),clearContext:()=>!ne&&(K=null,V=0,be=null,Me=!1,!0),async compactContext(){if(ne||Ee)return!1;te=!1,Ee=!0;let e=null,t=!1;try{if(!await Ke()||ne)return!1;const r=new Promise((t,r)=>{e=setTimeout(()=>{Te=null,r(new Error("Codex did not start the compaction turn"))},5e3),Te={resolve:t,reject:r}});await Ie.compact({threadId:K}),t=!0;const n=await r;e&&clearTimeout(e);const o=await Ie.waitForTurn(n);Be(o?.turn?.id||n);const s=o?.turn?.status;if("completed"===s)return!0;throw new Error(o?.turn?.error?.message||`compaction ${s||"failed"}`)}catch(e){return t||!["rejected","not_sent"].includes(e?.delivery)?(Ge(`Native Codex compaction status is uncertain; existing context was preserved: ${e?.message||e}`),null):(Ge(`Native Codex compaction unavailable; using bounded recap fallback: ${e?.message||e}`),!1)}finally{e&&clearTimeout(e),Te=null,Ee=!1,Ae.length&&et()}},async accountUsage(){if(!await Ke())return null;const[e,t]=await Promise.all([Ie.accountUsage().catch(()=>null),Ie.accountRateLimits().catch(()=>null)]);return{usage:e,limits:t}}}}
|
|
1
|
+
import{execFileSync as e,spawn as t}from"node:child_process";import r from"node:fs";import n from"node:os";import o from"node:path";import s from"node:readline";import{buildCodexAppServerArgs as a,codexAppServerGate as i,createCodexAppServer as l}from"./codex-app-server.mjs";import{CodexEventMapper as u}from"./codex-event-mapper.mjs";import{startCodexMcpHttp as d}from"./codex-mcp-http.mjs";import{CODEX_THINKPOOL_FIRST_TURN_PREAMBLE as c,buildThinkPoolTurnGuidance as p,createRoomContextSelector as m,usesFullThinkPoolReminder as f}from"./thinkpool-room-prompt.mjs";import{questionAnswerResponse as h}from"./question-response.mjs";import{codexReviewTarget as x,isCodexCompactCommand as g}from"./codex-commands.mjs";import{createCumulativeEventRelay as y}from"./cumulative-event-relay.mjs";import{stallDecision as v,stallEvent as b}from"./turn-stall.mjs";import{codexCatastrophicHookCliArgs as w}from"./catastrophic-command-guard.mjs";import{codexNativeReasoningEffort as k,codexReasoningEfforts as C,normalizeCodexReasoningEffort as I,normalizeReasoningEffortList as M}from"./reasoning-effort.mjs";const S=new Set(["read-only","workspace-write","danger-full-access"]);export const CODEX_MODE_CONFIG={default:{sandbox:"workspace-write",approvalPolicy:"untrusted"},acceptEdits:{sandbox:"workspace-write",approvalPolicy:"on-request"},plan:{sandbox:"read-only",approvalPolicy:"never"},review:{sandbox:"read-only",approvalPolicy:"never"},bypassPermissions:{sandbox:"danger-full-access",approvalPolicy:"never"}};export function codexConfigForMode(e){return CODEX_MODE_CONFIG[e]||CODEX_MODE_CONFIG.default}export function codexDefaultCollaborationMode(e,t){if(!e)return null;const r=k(t);return{mode:"default",settings:{model:String(e),reasoning_effort:r,developer_instructions:null}}}export function defaultModeForRuntime(e){return"codex"===e?"bypassPermissions":"default"}export async function codexOneShot({prompt:e,model:s,timeoutMs:a=22e3,outputSchema:i,cwd:l,tmpDir:u=n.tmpdir(),fsImpl:d=r,spawnImpl:c=t}={}){const p=String(e||"").trim();if(!p)throw new Error("codex one-shot requires a prompt");const m=!l,f=l||d.mkdtempSync(o.join(u,"thinkpool-codex-one-shot-"));let h=null;try{i&&(h=o.join(f,"output-schema.json"),d.writeFileSync(h,`${JSON.stringify(i)}\n`,{mode:384}));const e=["exec","--json","--ephemeral","--ignore-rules","--sandbox","read-only","--skip-git-repo-check","--color","never","-c",'approval_policy="never"',"-c",'model_reasoning_effort="low"',...s?["-m",String(s)]:[],...h?["--output-schema",h]:[],p];return await new Promise((t,r)=>{let n,o,s="",i="",l=!1;const u=(e,n)=>{l||(l=!0,o&&clearTimeout(o),e?r(e):t(n))};try{n=c("codex",e,{cwd:f,env:process.env,stdio:["ignore","pipe","pipe"]})}catch(e){return void u(e)}n.stdout.on("data",e=>{s+=e.toString(),s.length>1e6&&(s=s.slice(-1e6))}),n.stderr.on("data",e=>{i+=e.toString(),i.length>2e3&&(i=i.slice(-2e3))}),n.on("error",e=>u(e)),n.on("close",e=>{if(l)return;const t=s.split(/\r?\n/).filter(Boolean).flatMap(e=>{try{const t=JSON.parse(e);return"item.completed"===t?.type&&"agent_message"===t?.item?.type&&"string"==typeof t.item.text?[t.item.text.trim()]:[]}catch{return[]}}).filter(Boolean).at(-1);0===e&&t?u(null,t):u(new Error(`Codex cleanup failed${null==e?"":` (${e})`}${i.trim()?`: ${i.trim().slice(-240)}`:""}`))}),o=setTimeout(()=>{try{n.kill("SIGTERM")}catch{}u(new Error("Codex cleanup timed out"))},Math.max(1e3,Number(a)||22e3))})}finally{if(m)try{d.rmSync(f,{recursive:!0,force:!0})}catch{}}}export function codexWritableDirsForSandbox(e,t=[]){return"read-only"===normalizeCodexSandbox(e)?[]:t}export function codexExtraWritableDirsForMode(e,t,r=[]){return"review"===e?[]:codexWritableDirsForSandbox(t,r)}export function readCodexDefaultModel({home:e=process.env.CODEX_HOME||o.join(n.homedir(),".codex"),fsImpl:t=r}={}){try{const r=t.readFileSync(o.join(e,"config.toml"),"utf8").split(/^\s*\[/m,1)[0].match(/^\s*model\s*=\s*["']([^"']+)["']\s*(?:#.*)?$/m);return r?.[1]?.trim()||null}catch{return null}}export function readCodexModels({home:e=process.env.CODEX_HOME||o.join(n.homedir(),".codex"),fsImpl:t=r}={}){try{const r=JSON.parse(t.readFileSync(o.join(e,"models_cache.json"),"utf8"));return(Array.isArray(r?.models)?r.models:[]).filter(e=>e?.slug&&"hide"!==e.visibility).sort((e,t)=>(Number(e.priority)||999)-(Number(t.priority)||999)).map(e=>({value:e.slug,displayName:e.display_name||e.slug,description:e.description||"",reasoningEfforts:M(e.supported_reasoning_levels)}))}catch{return[]}}const _=new Map;function T(e,{home:t,fsImpl:r}){if(!e)return null;const n=`${t}\0${e}`,s=_.get(n);if(s)return s;const a=o.join(t,"sessions");try{const t=`${e}.jsonl`,s=r.readdirSync(a,{recursive:!0,withFileTypes:!0}).find(e=>e.isFile()&&e.name.endsWith(t));if(!s)return null;const i=o.join(s.parentPath||s.path||a,s.name);return _.set(n,i),i}catch{return null}}export function codexThreadCanResume(e,{home:t=process.env.CODEX_HOME||o.join(n.homedir(),".codex"),fsImpl:s=r}={}){if(!e)return!1;const a=T(e,{home:t,fsImpl:s});if(!a)return!0;try{return Number(s.statSync(a).size)>0}catch{return!0}}export function readCodexThreadUsage(e,{home:t=process.env.CODEX_HOME||o.join(n.homedir(),".codex"),fsImpl:s=r,tailBytes:a=4194304}={}){const i=T(e,{home:t,fsImpl:s});if(!i)return null;let l;try{const e=Number(s.statSync(i).size)||0,t=Math.min(e,Math.max(65536,a)),r=Buffer.alloc(t);l=s.openSync(i,"r"),s.readSync(l,r,0,t,e-t);const n=r.toString("utf8").split("\n");for(let e=n.length-1;e>=0;e--){let t;try{t=JSON.parse(n[e])}catch{continue}if("token_count"!==t?.payload?.type||!t.payload.info)continue;const r=t.payload.info,o=r.last_token_usage||{},s=r.total_token_usage||{},a=Number(o.total_tokens),i=Number(r.model_context_window);return!Number.isFinite(a)||a<0||!Number.isFinite(i)||i<=0?null:{context:{used:Math.floor(a),max:Math.floor(i)},total:{input_tokens:Math.max(0,Number(s.input_tokens)||0),cached_input_tokens:Math.max(0,Number(s.cached_input_tokens)||0),output_tokens:Math.max(0,Number(s.output_tokens)||0)},rateLimits:t.payload.rate_limits||null}}}catch{return null}finally{if(null!=l)try{s.closeSync(l)}catch{}}return null}export function codexUsageSnapshotFromAppServer(e){const t=Number(e?.last?.totalTokens),r=Number(e?.modelContextWindow);if(!Number.isFinite(t)||t<0||!Number.isFinite(r)||r<=0)return null;const n=e?.total||{};return{context:{used:Math.floor(t),max:Math.floor(r)},total:{input_tokens:Math.max(0,Number(n.inputTokens)||0),cached_input_tokens:Math.max(0,Number(n.cachedInputTokens)||0),output_tokens:Math.max(0,Number(n.outputTokens)||0)},rateLimits:e?.rateLimits||null}}export function normalizeCodexSandbox(e){return S.has(e)?e:"workspace-write"}export function codexPublishAccess({cwd:t,env:r=process.env,tmpdir:s=n.tmpdir(),execFile:a=e}={}){const i=[];try{const e=String(a("git",["rev-parse","--git-common-dir"],{cwd:t,encoding:"utf8"})||"").trim();if(e){const r=o.resolve(t,e),n=o.resolve(t);r===n||r.startsWith(n+o.sep)||i.push(r)}}catch{}return{writableDirs:i,env:{...r,NPM_CONFIG_CACHE:o.join(s,"thinkpool-npm-cache")}}}export const CODEX_ROOM_CASCADE_REMINDER=["thinkpool ROOM ROLE ENFORCEMENT: obey the authoritative TERMINAL ROLE above. A person-authored request for a new, separate, independent, top-level, or main terminal uses open_main_terminal — NEVER spawn_terminal — even for ordinary work or a handoff. If the wording includes new or separate terminal, main-terminal routing wins. Use spawn_terminal only for agent-decided bounded worker slices or when the person explicitly asks for a worker, sub-terminal, Ensemble lane, or delegated slice. If open_main_terminal is unavailable, say so; never substitute an Ensemble child. Leaf, worker, Side, and managed Flow lanes work directly and must not delegate. Never use hidden in-process subagents."].join(" ");export function buildCodexPrompt({text:e,terminalRolePrompt:t,rolePrompt:r,roomContext:n,firstTurn:o=!1,promptIndex:s=0,forceFullReminder:a=!1,turnCause:i=null}){const l="function"==typeof n?n():n,u=f({promptIndex:s,forceFull:a,turnCause:i}),d=[o?c:"",u?t:"",p({text:e,promptIndex:s,forceFull:a,turnCause:i}),u?r||CODEX_ROOM_CASCADE_REMINDER:"",l].map(e=>String(e||"").trim()).filter(Boolean);return d.length?`<thinkpool-context>\n${d.join("\n\n")}\n</thinkpool-context>\n\n${String(e??"")}`:String(e??"")}export function buildCodexExecArgs({sessionId:e,model:t,effort:r,sandbox:n,approvalPolicy:s="untrusted",providerConfig:a,mcpUrl:i,writableDirs:l=[],images:u=[],prompt:d}){const c=normalizeCodexSandbox(n),p=[...w(),...e?["exec","resume","--json","--skip-git-repo-check",..."danger-full-access"===c?["--dangerously-bypass-approvals-and-sandbox"]:["-c",`sandbox_mode="${c}"`]]:["exec","--json","--sandbox",c,"--skip-git-repo-check"]];t&&p.push("-m",t),p.push("-c",`approval_policy="${s}"`);const m=k(r);if(m&&p.push("-c",`model_reasoning_effort="${m}"`),a&&p.push("-c",a),!e)for(const e of l)p.push("--add-dir",e);i&&(p.push("-c",`mcp_servers.thinkpool.url=${JSON.stringify(i)}`),p.push("-c","mcp_servers.thinkpool.required=true"));let f=!1;for(const e of u)o.isAbsolute(String(e||""))&&(p.push("--image",String(e)),f=!0);return f&&p.push("--"),e&&p.push(e),p.push(String(d??"")),p}export function codexExecFailedBeforeTurn({stderr:e="",eventTypes:t=[]}={}){if((Array.isArray(t)?t.map(e=>String(e||"")):[]).some(e=>e&&"thread.started"!==e))return!1;const r=String(e||"");return/timed out handshaking with MCP server/i.test(r)||/thread\/resume failed/i.test(r)&&/failed to initialize session/i.test(r)}const E=/\brm\s+\S|\brmdir\s+\S|\bgit\s+(push\s+(-f|--force)|reset\s+--hard|clean\s+-[a-z]*f)|\bdrop\s+(table|database)\b|\b(mkfs|dd)\b|\bsudo\b|>\s*\/dev\/|\bchmod\s+-R|\bchown\s+-R|\bkillall\b|\btruncate\b/i;export function codexApprovalCard(e,t={},r=null){const n=String(t.approvalId||t.itemId||`${t.turnId||"turn"}:${e}`);if("item/tool/requestUserInput"===e){const e=Array.isArray(t.questions)?t.questions:[];return{id:n,toolName:"AskUserQuestion",input:{questions:e},risk:"ask",questions:e,asker:"Codex",answerFormat:"codex",...Number.isFinite(t.autoResolutionMs)&&t.autoResolutionMs>0?{autoResolutionMs:t.autoResolutionMs}:{}}}if("item/commandExecution/requestApproval"===e){const e=t.command||r?.command||"";return{id:n,toolName:"Bash",input:{command:e,cwd:t.cwd||r?.cwd||"",...t.reason?{reason:t.reason}:{}},risk:E.test(e)?"high":"medium"}}if("item/fileChange/requestApproval"===e){const e=Array.isArray(r?.changes)?r.changes:[],o=e[0]||{};return{id:n,toolName:e.length>1?"MultiEdit":"add"===o.kind?"Write":"Edit",input:e.length>1?{edits:e.map(e=>({file_path:e.path||"",path:e.path||"",kind:e.kind||"update"})),...t.reason?{reason:t.reason}:{}}:{file_path:o.path||t.grantRoot||"",path:o.path||t.grantRoot||"",kind:o.kind||"update",...t.reason?{reason:t.reason}:{}},risk:"medium"}}return null}export function codexApprovalResponse(e){return{decision:"always"===e?"acceptForSession":"allow"===e?"accept":"decline"}}export function codexUserInputResponse(e,t=[]){return h(e,t)}function F(e){if(!e||"object"!=typeof e)return e;const t={...e};switch(e.type){case"agentMessage":return{...t,type:"agent_message"};case"commandExecution":return{...t,type:"command_execution",aggregated_output:e.aggregatedOutput||"",exit_code:e.exitCode};case"fileChange":return{...t,type:"file_change"};case"webSearch":return{...t,type:"web_search"};case"mcpToolCall":return{...t,type:"mcp_tool_call",arguments:e.arguments||{},result:e.result,error:e.error};case"reasoning":return{...t,type:"reasoning",text:[...e.summary||[],...e.content||[]].join("\n")};default:return t}}export function startCodexSession({cwd:e,model:c,models:p=[],effort:h="high",resume:w,env:M,sandbox:S,mode:_="default",onEvent:T,providerConfig:E,terminalRolePrompt:N,rolePrompt:O,roomContext:R,mcpServers:P,prepareCwd:A=null,requestPermission:D,appServerGate:U=i,appServerFactory:$=l,mcpHttpFactory:j=d,spawnImpl:q=t,admitStart:L=null,stallOptions:X={}}){let G=CODEX_MODE_CONFIG[_]?_:"default",B=codexConfigForMode(G);S=normalizeCodexSandbox(S||B.sandbox);const W=codexPublishAccess({cwd:e,env:{...process.env,...M||{}}}),z=W.env,H=w||null,J=!H||codexThreadCanResume(H,{home:z.CODEX_HOME||o.join(n.homedir(),".codex")});let K=J?H:null,V=K?1:0,Q=0,Y=!0;const Z=m(R);let ee=null,te=!1,re=!1,ne=!1;const oe="function"==typeof X.now?X.now:Date.now,se="function"==typeof X.setInterval?X.setInterval:setInterval,ae="function"==typeof X.clearInterval?X.clearInterval:clearInterval,ie=Number.isFinite(X.stallMs)?Math.max(1,Number(X.stallMs)):Math.max(3e4,parseInt(z.TP_STALL_MS,10)||9e4),le=Number.isFinite(X.forceStopMs)?Math.max(ie,Number(X.forceStopMs)):Math.max(3*ie,parseInt(z.TP_FORCE_STOP_MS,10)||3e5),ue=Number.isFinite(X.intervalMs)?Math.max(1,Number(X.intervalMs)):5e3;let de=oe(),ce=!1,pe=!1,me=!1,fe=!1,he=!0,xe=0,ge=null,ye=c||null,ve=I(h,{models:p,model:ye}),be=K?readCodexThreadUsage(K):null,we=null;const ke=U({env:z});let Ce=!ke.enabled,Ie=null,Me=!1;const Se=new WeakSet;let _e=null,Te=null,Ee=!1;const Fe=new Set,Ne=new Map,Oe=new Map;let Re=Promise.resolve(),Pe=Promise.resolve();const Ae=[],De=y(e=>{try{T?.(e)}catch{}}),Ue=()=>{de=oe(),ce=!1},$e=e=>{Ue(),De(e)};$e.cancel=()=>De.cancel();const je=new u({onEvent:$e,model:c||null,usageSnapshotForSession:e=>{const t=readCodexThreadUsage(e);return t&&(be=t),t||be}}),qe=()=>{const e=[...Oe.values()];Oe.clear();for(const t of e)t.text&&$e({kind:"assistant",blocks:[{type:"text",text:t.text}],parentToolUseId:null,replacesCid:t.cid})},Le=e=>{"turn.completed"!==e?.type&&"turn.failed"!==e?.type||(qe(),ne=!1),je.push(e)},Xe=e=>{qe(),ne=!1,$e(e)},Ge=({retry:e=!1}={})=>{ne=!0,de=oe(),ce=!1,me=!1,fe=!1,he=!0,e||(pe=!1)},Be=e=>$e({kind:"note",text:e}),We=(e=_e)=>{const t=String(e||"");if(t)for(Fe.add(t);Fe.size>64;)Fe.delete(Fe.values().next().value)},ze=(e={})=>{const t=String(e.turnId||"");return!(!t||re||te||Fe.has(t)||t!==String(_e||""))};async function He(){const e=P?.thinkpool;return e?(we||(we=j({sdkServer:e})),we):null}function Je(e,t={}){if("thread/tokenUsage/updated"===e){if(t.threadId&&K&&String(t.threadId)!==String(K))return;const e=codexUsageSnapshotFromAppServer(t.tokenUsage);if(!e)return;if(be=e,Ee){const{used:t,max:r}=e.context;$e({kind:"usage",ctx:{used:t,max:r,pct:Math.min(100,Math.max(0,Math.round(t/r*100))),over:t>r,model:ye}})}return}if("turn/started"===e&&t.turn?.id){const e=String(t.turn.id);if(re||te)return void We(e);if(Te){const t=Te;return Te=null,void t.resolve(e)}return _e||(_e=e),void Ue()}if("turn/plan/updated"!==e&&"item/agentMessage/delta"!==e&&"item/started"!==e&&"item/completed"!==e&&"error"!==e||ze(t)){if(Ue(),"turn/plan/updated"===e){const e=(Array.isArray(t.plan)?t.plan:[]).map(({step:e,status:t})=>({content:e||"",activeForm:e||"",status:"inProgress"===t?"in_progress":"completed"===t?"completed":"pending"})).filter(e=>e.content);return void $e({kind:"assistant",blocks:[{type:"tool_use",id:`codex-plan:${t.turnId||"active"}`,name:"TodoWrite",input:{todos:e}}],parentToolUseId:null})}if("item/agentMessage/delta"===e){if(!t.delta)return;const e=t.itemId||`turn:${t.turnId||_e||"active"}`,r=Oe.get(e)||{cid:`codex-stream:${K||"thread"}:${e}`,text:""};return r.text+=String(t.delta),Oe.set(e,r),void $e({kind:"assistant_stream",cid:r.cid,text:r.text,itemId:e,streaming:!0})}if("item/started"===e||"item/completed"===e){const r=t.item;if(!r)return;if("item/started"===e&&Ne.set(r.id,r),"item/completed"===e&&(Ne.set(r.id,r),"agentMessage"===r.type&&Oe.has(r.id))){const e=Oe.get(r.id);return Oe.delete(r.id),void $e({kind:"assistant",blocks:[{type:"text",text:r.text||e.text}],parentToolUseId:null,replacesCid:e.cid})}return void Le({type:"item/started"===e?"item.started":"item.completed",item:F(r)})}if("error"===e){const e=t.error?.message||t.message||"codex app-server error";Le({type:"error",message:e})}}}async function Ke(e,t={}){if(!ze(t))return"item/tool/requestUserInput"===e?{answers:{}}:{decision:"decline"};const r=codexApprovalCard(e,t,Ne.get(t.itemId));if(!r)throw new Error(`Unsupported Codex App Server request: ${e}`);let n="deny";xe++;try{n=await(D?.(r))}catch{}finally{xe=Math.max(0,xe-1),Ue()}return"item/tool/requestUserInput"===e?codexUserInputResponse(n,r.questions):codexApprovalResponse(n)}async function Ve(){if(Ce)return!1;if(!1===Ie?.alive)return Me=!1,Ce=!0,!1;if(Ie&&Me)return!0;let t;try{t=await He()}catch(e){Be(`thinkpool peer tools unavailable: ${e?.message||e}`)}try{if(!Ie){const r=$({cwd:e,env:z,args:a({providerConfig:E,mcpUrl:t?.url,writableDirs:codexExtraWritableDirsForMode(G,S,W.writableDirs)}),onNotification:Je,onServerRequest:Ke,onClose:e=>{re||Ce||Se.has(r)||(Me=!1,Ce=!0,Be(`Codex App Server closed; later turns will use codex exec: ${e?.message||e}`))}});Ie=r,await Ie.start(),Ue()}return K=await Ie.startThread({threadId:V>0?K:null,cwd:e,model:ye,sandbox:S,approvalPolicy:B.approvalPolicy,config:{"features.default_mode_request_user_input":!0}}),Ue(),t?.url&&(await Ie.waitForMcpServer({threadId:K,name:"thinkpool"}),Ue()),Me=!0,Le({type:"thread.started",thread_id:K}),!0}catch{Ce=!0;try{Ie?.end()}catch{}return Ie=null,Me=!1,!1}}function Qe({force:e=!1}={}){const t=Ie,r=_e;if(t&&e){We(r),Ce=!0,Me=!1;try{t.end()}catch{}Ie===t&&(Ie=null)}else if(t&&r)We(r),Promise.resolve(t.interrupt({threadId:K,turnId:r})).catch(()=>{Ce=!0,Me=!1;try{t.end()}catch{}Ie===t&&(Ie=null)});else if(t){Ce=!0,Me=!1;try{t.end()}catch{}Ie===t&&(Ie=null)}if(ee){const e=ee;try{e.kill("SIGTERM")}catch{}setTimeout(()=>{if(ee===e)try{e.kill("SIGKILL")}catch{}},1500)}}async function Ye(e,t={}){if(re||te||fe)return!0;if(!await Ve())return!1;if(re||te||fe)return!0;je.setUsageBaseline(K?readCodexThreadUsage(K):null),ne=!0;try{if(he=!1,_e=await Ie.startTurn({threadId:K,input:e,images:Array.isArray(t.images)?t.images:[],model:ye,effort:k(ve),approvalPolicy:B.approvalPolicy,collaborationMode:codexDefaultCollaborationMode(ye,ve)}),Ue(),te||re){We(_e);try{await Ie.interrupt({threadId:K,turnId:_e})}catch{}return!0}const r=await Ie.waitForTurn(_e);We(r?.turn?.id||_e);const n=r?.turn?.status;te?Xe({kind:"result",subtype:"aborted",sessionId:K,model:ye,costUsd:null,usage:null,numTurns:1,durationMs:r?.turn?.durationMs,denials:0,resultText:null}):"interrupted"===n&&(me||fe)||("interrupted"===n?Xe({kind:"result",subtype:"aborted",sessionId:K,model:ye,costUsd:null,usage:null,numTurns:1,durationMs:r?.turn?.durationMs,denials:0,resultText:null}):"failed"===n?(me=!1,Le({type:"turn.failed",message:r?.turn?.error?.message||"codex turn failed"})):(me=!1,Le({type:"turn.completed"})))}catch(e){Ce=!0,Me=!1,We(_e);try{Ie?.end()}catch{}Ie=null,re||me||fe||Le({type:"turn.failed",message:`codex app-server turn failed: ${e?.message||e}`})}finally{qe(),_e=null,ne=!1,Ne.clear(),Oe.clear()}return!0}async function Ze(e){if(re||te||fe)return!0;if(!await Ve())return!1;if(re||te||fe)return!0;je.setUsageBaseline(K?readCodexThreadUsage(K):null),ne=!0;try{he=!1;const t=await Ie.startReview({threadId:K,target:x(e)});if(_e=t?.turn?.id||t?.turnId,!_e)throw new Error("Codex review/start returned no turn id");if(Ue(),te||re){We(_e);try{await Ie.interrupt({threadId:K,turnId:_e})}catch{}return!0}const r=await Ie.waitForTurn(_e);We(r?.turn?.id||_e);const n=r?.turn?.status;te?Xe({kind:"result",subtype:"aborted",sessionId:K,model:ye,costUsd:null,usage:null,numTurns:1,durationMs:r?.turn?.durationMs,denials:0,resultText:null}):"interrupted"===n&&(me||fe)||("interrupted"===n?Xe({kind:"result",subtype:"aborted",sessionId:K,model:ye,costUsd:null,usage:null,numTurns:1,durationMs:r?.turn?.durationMs,denials:0,resultText:null}):"failed"===n?(me=!1,Le({type:"turn.failed",message:r?.turn?.error?.message||"codex review failed"})):(me=!1,Le({type:"turn.completed"})))}catch(e){Ce=!0,Me=!1,We(_e);try{Ie?.end()}catch{}Ie=null,re||me||fe||Le({type:"turn.failed",message:`codex review failed: ${e?.message||e}`})}finally{qe(),_e=null,ne=!1,Ne.clear(),Oe.clear()}return!0}async function et(t,n={}){if(re||te||fe)return;let o;try{o=await He()}catch(e){Be(`thinkpool peer tools unavailable: ${e?.message||e}`)}try{await(o?.handoff?.())}catch(e){Be(`thinkpool peer handoff failed: ${e?.message||e}`)}const a=buildCodexExecArgs({sessionId:0===V?null:K,model:ye,effort:ve,sandbox:S,approvalPolicy:B.approvalPolicy,providerConfig:E,mcpUrl:o?.url,writableDirs:codexExtraWritableDirsForMode(G,S,W.writableDirs),images:Array.isArray(n.images)?n.images:[],prompt:t});return je.setUsageBaseline(K?readCodexThreadUsage(K):null),new Promise(t=>{let n;try{n=r.openSync("/dev/null","r")}catch{}const o={cwd:e,env:z,stdio:["ignore","pipe","pipe"]};if(null!=n&&(o.stdio=[n,"pipe","pipe"]),ne=!0,he=!1,ee=q("codex",a,o),null!=n)try{r.closeSync(n)}catch{}const i=[];s.createInterface({input:ee.stdout,crlfDelay:1/0}).on("line",e=>{if(!e.trim())return;let t;try{t=JSON.parse(e)}catch{return}i.push(t.type),"thread.started"===t.type&&t.thread_id&&(K=K||t.thread_id),"turn.completed"!==t.type&&"turn.failed"!==t.type||(u=!0,me=!1),Le(t)});let l="",u=!1,d=!1;ee.stderr.on("data",e=>{l+=e.toString(),l.length>800&&(l=l.slice(-800))}),ee.on("error",e=>{d||(d=!0,ee=null,me||fe||Xe({kind:"error",message:`codex failed to start: ${e.message}`,recoverable:!0}),t({preTurnInitFailure:!1}))}),ee.on("close",e=>{if(d)return;d=!0,ee=null,ne=!1;const r=`codex exec exited ${e}${l?": "+l.trim().slice(-300):""}`;0===e||null==e||u||!codexExecFailedBeforeTurn({stderr:l,eventTypes:i})||te||fe?((!me&&!fe||u||te)&&(te&&!u?Xe({kind:"result",subtype:"aborted",sessionId:K,model:ye,costUsd:null,usage:null,numTurns:1,durationMs:void 0,denials:0,resultText:null}):te||u||0===e||null==e||Xe({kind:"error",message:r,recoverable:!0})),t({preTurnInitFailure:!1})):t({preTurnInitFailure:!0,message:r})})})}function tt(){if(re)return;const e=Ae.shift();e&&(Pe=Pe.then(async()=>{Ge();const t=e.options?.turnCause||null,r=f({promptIndex:e.promptIndex,forceFull:e.forceFullReminder,turnCause:t}),n=buildCodexPrompt({text:e.text,terminalRolePrompt:N,rolePrompt:O,roomContext:()=>Z({force:r}),firstTurn:0===V,promptIndex:e.promptIndex,forceFullReminder:e.forceFullReminder,turnCause:t}),o=/^\s*\/review(?:\s|$)/i.test(String(e.text||""));let s=!1,a=!1;for(;!re&&!te&&!fe;){if(!(o?await Ze(e.text):await Ye(n,e.options)))if(o)me=!1,Xe({kind:"error",message:"Codex review is unavailable without the tested App Server runtime.",recoverable:!0});else{me&&(me=!1,s=!0,Ge({retry:!0}),De({kind:"note",text:"retrying the stalled turn on a fresh connection"}));const t=await et(n,e.options);if(t?.preTurnInitFailure){if(!(a||re||te||fe)){a=!0,Ge({retry:!0}),De({kind:"note",text:"Codex initialization failed before the turn started; retrying once with a fresh thinkpool connection."});continue}Xe({kind:"error",message:t.message,recoverable:!0})}}if(!me||s||fe||re||te)break;me=!1,s=!0,Ge({retry:!0}),De({kind:"note",text:"retrying the stalled turn on a fresh connection"})}V++,tt()}))}return H&&!J&&queueMicrotask(()=>Be("The stopped Codex turn had no resumable context; continuing in a fresh thread.")),["0","false","off"].includes(String(z.TP_CODEX_APP_SERVER||"").trim().toLowerCase())||ke.enabled||queueMicrotask(()=>Be(`Codex App Server disabled: ${ke.reason}`)),ge=se(()=>{const e=oe()-de,t=v({turnActive:ne,awaitingUser:xe,quietMs:e,stallMs:ie,forceStopMs:le,stalledSent:ce,stallRetried:pe});if("none"===t)return;if("retry"===t&&!he)return fe=!0,me=!1,De({kind:"error",recoverable:!0,message:"Codex stopped responding after the turn began. The transport was stopped, but the prompt was not replayed automatically."}),Xe({kind:"result",subtype:"aborted",sessionId:K,model:ye,costUsd:null,usage:null,numTurns:1,durationMs:void 0,denials:0,resultText:null}),void Qe({force:!0});const r=b(t,e);if(r&&De(r),"status"!==t){if("retry"===t)return pe=!0,me=!0,de=oe(),ce=!1,void Qe();fe=!0,me=!1,Ae.length=0,Xe({kind:"result",subtype:"aborted",sessionId:K,model:ye,costUsd:null,usage:null,numTurns:1,durationMs:void 0,denials:0,resultText:null}),Qe({force:!0})}else ce=!0},ue),ge.unref?.(),{get sessionId(){return K},get usageSnapshot(){return be},get turnActive(){return ne||Ee},get canSteer(){return!(!Ie||!1===Ie.alive||!Me||Ce)},get started(){return ne||Ee||!!ee||!(!Ie||!1===Ie.alive)},sendTurn(t,r={}){if(re)return!1;if(!ne&&0===Ae.length&&!Me&&!ee){const e="function"==typeof L?L():{ok:!0};if(!1===e?.ok)return $e({kind:"error",message:e.reason||"Host memory is critically low. This Codex runtime was not started.",recoverable:!0}),!1}const n=Q++,o=Y;if(Y=g(t)||/^\s*\/(?:reset|clear)\b/i.test(String(t||"")),!ne&&0===V&&A)try{e=A()||e}catch{}if(ne&&Ie&&_e){Ue();const e=r?.turnCause||null,s=f({promptIndex:n,forceFull:o,turnCause:e}),a=buildCodexPrompt({text:t,terminalRolePrompt:N,rolePrompt:O,roomContext:()=>Z({force:s}),promptIndex:n,forceFullReminder:o,turnCause:e}),i=_e;return Re=Re.then(()=>Ie.steer({threadId:K,turnId:i,input:a,images:Array.isArray(r.images)?r.images:[]})).catch(e=>{"rejected"===e?.delivery||"not_sent"===e?.delivery?(Ae.push({text:t,options:r,promptIndex:n,forceFullReminder:o}),ne||1!==Ae.length||tt()):$e({kind:"error",message:"Codex steering delivery is uncertain; the message was not replayed automatically.",recoverable:!0})}),!0}return Ae.push({text:t,options:r,promptIndex:n,forceFullReminder:o}),ne||Ee||(te=!1,Ge()),1!==Ae.length||Ee||tt(),!0},async park(){if(re||ne||Ee||Ae.length)return!1;const e=Pe;try{await e}catch{}try{await Re}catch{}if(re||ne||Ee||Ae.length||ee||_e)return!1;const t=Ie;if(t){Se.add(t),Ie=null,Me=!1,Ne.clear(),Oe.clear();try{t.end()}catch{return!1}}return!0},abort(){const e=Pe,t=ne&&!_e&&!ee;te=!0,me=!1,fe=!1,Ae.length=0,We(_e);let r=Promise.resolve();if(Ie&&_e&&(r=Ie.interrupt({threadId:K,turnId:_e}).catch(()=>{})),ee){try{ee.kill("SIGTERM")}catch{}setTimeout(()=>{try{ee?.kill("SIGKILL")}catch{}},1500)}else t&&Xe({kind:"result",subtype:"aborted",sessionId:K,model:ye,costUsd:null,usage:null,numTurns:0,durationMs:void 0,denials:0,resultText:null});return r.then(()=>e).catch(()=>e).then(()=>{})},end(){if(re=!0,ne=!1,ge&&ae(ge),$e.cancel(),Ae.length=0,We(_e),ee)try{ee.kill()}catch{}try{Ie?.end()}catch{}we?.then(e=>e.close()).catch(()=>{})},setModel(e){if(!e||ne)return!1;ye=String(e),je.setModel(ye);const t=I(ve,{models:p,model:ye});return t!==ve&&(ve=t,$e({kind:"effort",level:ve})),!0},setMode:e=>!(ne||!CODEX_MODE_CONFIG[e]||(G=e,B=codexConfigForMode(G),S=B.sandbox,K=null,V=0,be=null,Me=!1,0)),setEffort:e=>!(ne||!C({models:p,model:ye}).includes(e)||(ve=e,$e({kind:"effort",level:ve}),0)),clearContext:()=>!ne&&(K=null,V=0,be=null,Me=!1,!0),async compactContext(){if(ne||Ee)return!1;te=!1,Ee=!0;let e=null,t=!1;try{if(!await Ve()||ne)return!1;const r=new Promise((t,r)=>{e=setTimeout(()=>{Te=null,r(new Error("Codex did not start the compaction turn"))},5e3),Te={resolve:t,reject:r}});await Ie.compact({threadId:K}),t=!0;const n=await r;e&&clearTimeout(e);const o=await Ie.waitForTurn(n);We(o?.turn?.id||n);const s=o?.turn?.status;if("completed"===s)return!0;throw new Error(o?.turn?.error?.message||`compaction ${s||"failed"}`)}catch(e){return t||!["rejected","not_sent"].includes(e?.delivery)?(Be(`Native Codex compaction status is uncertain; existing context was preserved: ${e?.message||e}`),null):(Be(`Native Codex compaction unavailable; using bounded recap fallback: ${e?.message||e}`),!1)}finally{e&&clearTimeout(e),Te=null,Ee=!1,Ae.length&&tt()}},async accountUsage(){if(!await Ve())return null;const[e,t]=await Promise.all([Ie.accountUsage().catch(()=>null),Ie.accountRateLimits().catch(()=>null)]);return{usage:e,limits:t}}}}
|
package/edit-diff.mjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const normalizeEditKind=t=>{const e="object"==typeof t?t?.type:t;return"add"===e||"delete"===e||"rename"===e?e:"update"};const t=t=>{if(!t)return[];const e=String(t).replace(/\r\n?/g,"\n").split("\n");return""===e.at(-1)&&e.pop(),e}
|
|
1
|
+
export const normalizeEditKind=t=>{const e="object"==typeof t?t?.type:t;return"add"===e||"delete"===e||"rename"===e?e:"update"};const t=t=>{if(!t)return[];const e=String(t).replace(/\r\n?/g,"\n").split("\n");return""===e.at(-1)&&e.pop(),e},e=(t,e="")=>"add"===t?`+${e}`:"remove"===t?`-${e}`:"context"===t?` ${e}`:`…${e}`;function n(t,e=2,n=80){const r=[];if(t.forEach((t,e)=>{"context"!==t.type&&r.push(e)}),!r.length)return[];const o=[];for(const n of r){const r=Math.max(0,n-e),s=Math.min(t.length,n+e+1),i=o.at(-1);i&&r<=i.end?i.end=Math.max(i.end,s):o.push({start:r,end:s})}const s=[];if(o.forEach((e,n)=>{n>0&&s.push({type:"omit",text:e.start-o[n-1].end+" unchanged lines"}),s.push(...t.slice(e.start,e.end))}),s.length<=n)return s;const i=Math.floor((n-1)/2),a=n-i-1;return[...s.slice(0,i),{type:"omit",text:s.length-i-a+" preview lines hidden"},...s.slice(-a)]}export function summarizeTextDiff(r,o,s={}){const i=function(t,e,n=600){if(!t.length)return e.map(t=>({type:"add",text:t}));if(!e.length)return t.map(t=>({type:"remove",text:t}));const r=Math.min(t.length+e.length,n),o=new Map([[1,0]]),s=[];for(let n=0;n<=r;n++){s.push(new Map(o));for(let r=-n;r<=n;r+=2){const i=o.get(r+1)??-1,a=o.get(r-1)??-1;let p=r===-n||r!==n&&a<i?Math.max(0,i):Math.max(0,a+1),c=p-r;for(;p<t.length&&c<e.length&&t[p]===e[c];)p++,c++;if(o.set(r,p),p<t.length||c<e.length)continue;const l=[];let h=t.length,d=e.length;for(let r=n;r>=0;r--){const n=s[r],o=h-d,i=n.get(o+1)??-1,a=n.get(o-1)??-1,p=o===-r||o!==r&&a<i?o+1:o-1,c=Math.max(0,n.get(p)??0),u=c-p;for(;h>c&&d>u;)l.push({type:"context",text:t[h-1]}),h--,d--;if(0===r)break;h===c?(l.push({type:"add",text:e[d-1]}),d--):(l.push({type:"remove",text:t[h-1]}),h--)}return l.reverse()}}return((t,e)=>{let n=0;for(;n<t.length&&n<e.length&&t[n]===e[n];)n++;let r=t.length,o=e.length;for(;r>n&&o>n&&t[r-1]===e[o-1];)r--,o--;return[...t.slice(0,n).map(t=>({type:"context",text:t})),...t.slice(n,r).map(t=>({type:"remove",text:t})),...e.slice(n,o).map(t=>({type:"add",text:t})),...t.slice(r).map(t=>({type:"context",text:t}))]})(t,e)}(t(r),t(o),s.maxDistance),a=i.reduce((t,e)=>t+("add"===e.type?1:0),0),p=i.reduce((t,e)=>t+("remove"===e.type?1:0),0),c=n(i,s.context??2,s.maxLines??80);return{added:a,removed:p,lines:c,preview:c.map(t=>e(t.type,t.text)).join("\n")}}export function summarizeUnifiedDiff(t,r={}){const o=String(t||"");if(!o.trim())return null;const s=[];for(const t of o.replace(/\r\n?/g,"\n").split("\n"))!t||t.startsWith("diff --git ")||t.startsWith("index ")||t.startsWith("--- ")||t.startsWith("+++ ")||t.startsWith("@@")||"\"===t||("+"===t[0]?s.push({type:"add",text:t.slice(1)}):"-"===t[0]?s.push({type:"remove",text:t.slice(1)}):" "===t[0]&&s.push({type:"context",text:t.slice(1)}));const i=s.reduce((t,e)=>t+("add"===e.type?1:0),0),a=s.reduce((t,e)=>t+("remove"===e.type?1:0),0),p=n(s,r.context??2,r.maxLines??80);return{added:i,removed:a,lines:p,preview:p.map(t=>e(t.type,t.text)).join("\n")}}
|
package/hermes-acp-bootstrap.py
CHANGED
|
@@ -8,9 +8,11 @@ import json
|
|
|
8
8
|
import os
|
|
9
9
|
import re
|
|
10
10
|
import sys
|
|
11
|
+
from pathlib import Path
|
|
11
12
|
from urllib.parse import urlsplit, urlunsplit
|
|
12
13
|
|
|
13
14
|
POLICY_ENV = "THINKPOOL_HERMES_ACP_POLICY"
|
|
15
|
+
PROJECT_SKILLS_ENV = "THINKPOOL_PROJECT_SKILLS_DIR"
|
|
14
16
|
|
|
15
17
|
CODING_TOOLS = frozenset({
|
|
16
18
|
"web_search", "web_extract", "terminal", "process", "read_file", "write_file",
|
|
@@ -70,6 +72,25 @@ def policy():
|
|
|
70
72
|
ROLE, BUILTIN, REQUIRED_BUILTIN, MCP_TOOLS = policy()
|
|
71
73
|
ALLOWED = frozenset(BUILTIN) | frozenset("mcp__thinkpool__" + x for x in MCP_TOOLS) | frozenset("mcp_thinkpool_" + x for x in MCP_TOOLS)
|
|
72
74
|
|
|
75
|
+
# Project skills stay canonical in the checked-out repository. Hermes supports
|
|
76
|
+
# read-only external skill directories, so inject this lane's exact project
|
|
77
|
+
# directory into the process-local loader instead of copying it into a profile.
|
|
78
|
+
import agent.skill_utils
|
|
79
|
+
_native_external_skill_dirs = agent.skill_utils.get_external_skills_dirs
|
|
80
|
+
def thinkpool_external_skill_dirs():
|
|
81
|
+
result = list(_native_external_skill_dirs())
|
|
82
|
+
raw = os.environ.get(PROJECT_SKILLS_ENV, "").strip()
|
|
83
|
+
if not raw:
|
|
84
|
+
return result
|
|
85
|
+
candidate = Path(raw).expanduser().resolve()
|
|
86
|
+
if not candidate.is_absolute() or not candidate.is_dir():
|
|
87
|
+
raise RuntimeError("thinkpool project skills directory is missing or invalid")
|
|
88
|
+
if candidate not in result:
|
|
89
|
+
result.append(candidate)
|
|
90
|
+
return result
|
|
91
|
+
agent.skill_utils.get_external_skills_dirs = thinkpool_external_skill_dirs
|
|
92
|
+
agent.skill_utils.get_all_skills_dirs = lambda: [agent.skill_utils.get_skills_dir(), *thinkpool_external_skill_dirs()]
|
|
93
|
+
|
|
73
94
|
def name_of(schema):
|
|
74
95
|
if not isinstance(schema, dict): return ""
|
|
75
96
|
fn = schema.get("function")
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
let
|
|
2
|
+
import{inspectCatastrophicTool as e}from"./catastrophic-command-guard.mjs";let t="";process.stdin.setEncoding("utf8"),process.stdin.on("data",e=>{t+=e}),process.stdin.on("end",()=>{let o=null;try{o=JSON.parse(t)}catch{}if(o)if("delegate_task"!==o.tool_name){if("terminal"===o.tool_name){const t=e({toolName:"terminal",input:o.extra?.tool_input||o.tool_input||o.input||o.arguments,cwd:o.extra?.cwd||o.cwd});if(!t.command)return void process.stdout.write(`${JSON.stringify({action:"block",message:"thinkpool catastrophic-command guard received no terminal command and failed closed."})}\n`);if(t.blocked)return void process.stdout.write(`${JSON.stringify({action:"block",message:`${t.reason} Rule: ${t.ruleId}.`})}\n`)}process.stdout.write("{}\n")}else process.stdout.write(`${JSON.stringify({action:"block",message:"thinkpool blocks hidden Hermes delegation. Open visible thinkpool lanes instead."})}\n`);else process.stdout.write(`${JSON.stringify({action:"block",message:"thinkpool safety hook received malformed input and failed closed."})}\n`)});
|
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
|
|
1
|
+
import{execFileSync as e}from"node:child_process";import o from"node:fs";import t from"node:os";import i from"node:path";const n=e=>String(e||"").replace(/[\r\n]+/g," ").trim(),r=/Install directory:\s*(.+?)(?:\r?\n|$)/i,s=/Config:\s*(.+?)(?:\r?\n|$)/i,l=/Model:\s*\{[^\r\n]*['"]provider['"]\s*:\s*['"]nous['"][^\r\n]*\}/i;export function resolveHermesAcpRuntime({command:t="thinkpool",execFile:a=e,exists:p=o.existsSync,bootstrap:c=new URL("./hermes-acp-bootstrap.py",import.meta.url)}={}){try{const e=a(t,["--version"],{encoding:"utf8",timeout:1e4,stdio:["ignore","pipe","pipe"]}),o=n(String(e).match(r)?.[1]);if(!o||!i.isAbsolute(o))throw new Error("Hermes did not report an absolute install directory");const d=i.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(t,["config","show"],{encoding:"utf8",timeout:1e4,stdio:["ignore","pipe","pipe"]});if(!l.test(String(u)))throw new Error("thinkpool Hermes profile is not pinned to Nous Portal");const h=n(String(u).match(s)?.[1]),f=h?i.dirname(h):"";if(!f||!i.isAbsolute(f)||"thinkpool"!==i.basename(f))throw new Error("Hermes did not report the isolated thinkpool profile");return{python:d,bootstrap:m,profile:f,install:o}}catch(e){return{error:n(e?.stderr||e?.message||e)||"could not resolve Hermes ACP runtime"}}}export function probeHermesRuntime({command:r="thinkpool",prefixArgs:s=[],execFile:a=e,env:p=process.env,strictBootstrap:c=!1,exists:d=o.existsSync,fsImpl:m=o,tmpDir:u=t.tmpdir()}={}){try{const e=a(r,[...s,"--version"],{encoding:"utf8",env:p,timeout:1e4,stdio:["ignore","pipe","pipe"]}),o=n(e).match(/Hermes Agent v([^\s]+)/i)?.[1]||null,t=a(r,[...s,"config","show"],{encoding:"utf8",env:p,timeout:1e4,stdio:["ignore","pipe","pipe"]});if(!l.test(String(t)))return{available:!1,version:o,reason:"thinkpool Hermes profile is not pinned to Nous Portal"};a(r,[...s,"acp","--check"],{encoding:"utf8",env:p,timeout:15e3,stdio:["ignore","pipe","pipe"]});const h=a(r,[...s,"hooks","list"],{encoding:"utf8",env:p,timeout:1e4,stdio:["ignore","pipe","pipe"]}),f=a(r,[...s,"hooks","doctor"],{encoding:"utf8",env:p,timeout:1e4,stdio:["ignore","pipe","pipe"]}),g=a(r,[...s,"hooks","test","pre_tool_call","--for-tool","delegate_task"],{encoding:"utf8",env:p,timeout:1e4,stdio:["ignore","pipe","pipe"]}),v=m.mkdtempSync(i.join(u,"thinkpool-hermes-safety-")),k=i.join(v,"payload.json");let y="";try{m.writeFileSync(k,`${JSON.stringify({tool_name:"terminal",tool_input:{command:"rm -rf /"},cwd:v})}\n`,{mode:384}),y=a(r,[...s,"hooks","test","pre_tool_call","--for-tool","terminal","--payload-file",k],{encoding:"utf8",env:p,timeout:1e4,stdio:["ignore","pipe","pipe"]})}finally{m.rmSync(v,{recursive:!0,force:!0})}const b=/All shell hooks look healthy/i.test(f)&&!/(?:issue\(s\) found|modified since approval|⚠)/i.test(f),w=/(?:action|decision)["']?\s*[:=]\s*["']block/i.test(g)&&/thinkpool blocks hidden Hermes delegation/i.test(g)&&/pre_tool_call[\s\S]*delegate_task\|terminal[\s\S]*(?:✓ allowed|allowed)/i.test(h),S=/(?:action|decision)["']?\s*[:=]\s*["']block/i.test(y)&&/catastrophic-command guard/i.test(y);if(!b||!w||!S)return{available:!1,version:o,reason:"Hermes safety guard is not healthy, unchanged, allowlisted, and structurally blocking delegation plus catastrophic terminal commands in the dedicated profile"};const H=c?resolveHermesAcpRuntime({command:r,execFile:a,exists:d}):null;return H?.error?{available:!1,version:o,reason:`Hermes ACP bootstrap unavailable: ${H.error}`}:{available:!0,version:o,acpProtocol:1,delegationBlocked:!0,catastropheBlocked:!0,...H||{}}}catch(e){return{available:!1,version:null,reason:n(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";import{HERMES_REASONING_EFFORT_IDS as E}from"./reasoning-effort.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(E);export const HERMES_EFFORT_CONFIG_ID="thinkpool_reasoning_effort";const I=new Set([...l,"read_review_file"]),S=/(?:TOKEN|SECRET|PASSWORD|PRIVATE_KEY|API_KEY|ACCESS_KEY|CREDENTIAL)/i,b=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])=>!S.test(e)));const k=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")}},x=e=>(e?.availableModels||[]).map(e=>({value:e.modelId,displayName:e.name||e.modelId,description:e.description||""})),v=e=>new n(e);export function buildHermesPromptText({text:e,firstTurn:t=!1,terminalRolePrompt:o,rolePrompt:n,roomContext:s,promptIndex:r=0,forceFullReminder:i=!1,turnCause:l=null}={}){const a="function"==typeof s?s():s,c=!!(o||n||a),d=_({promptIndex:r,forceFull:i,turnCause:l}),m=[t||d?o:"",c?g({text:e,promptIndex:r,forceFull:i,turnCause:l}):"",t||d?n:"",a].filter(Boolean).join("\n\n"),u=String(e??"");return m?`${m}\n\n${u}`:u}const C=(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:E=process.env,mode:S="default",onEvent:R,requestPermission:H,roomContext:$,terminalRolePrompt:T,rolePrompt:P,mcpServers:M,requiredMcpTools:F=[],prepareCwd:O=null,command:q="thinkpool",args:j=["acp"],clientFactory:A=v,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(S)?S:"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,Ce=()=>"plan"===Q?F.filter(e=>I.has(e)):F,Re=e=>"acceptEdits"===e?"accept_edits":"bypassPermissions"===e?"dont_ask":"default",He=y(e=>{try{R?.(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:C("allow",t.options)};if(s===a)return{outcome:C("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:C("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:C(e,t.options)}}if("plan"===Q)return{outcome:{outcome:"cancelled"}};if(p({toolName:i.toolName,input:i.input,mode:Q}))return{outcome:C("allow",t.options)};let c="deny";try{c=await(H?.(i))}catch{}return{outcome:C(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&&b.has(o?.sessionUpdate)||ee?.push(t)}async function Pe(){const e=[...new Set((Array.isArray(Ce())?Ce():[]).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(E),n=q,r=j;if("thinkpool"===q&&A===v){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:Ce()})}}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=A({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=Re(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:x(p?.models),flow:"thinkpool"===q&&A===v})})().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=t?.turnCause||null,i=_({promptIndex:o,forceFull:n,turnCause:r}),l=[{type:"text",text:buildHermesPromptText({text:s,firstTurn:le,terminalRolePrompt:T,rolePrompt:P,roomContext:()=>de({force:i}),promptIndex:o,forceFullReminder:n,turnCause:r})}];for(const e of Array.isArray(t.images)?t.images:[])try{l.push(k(e))}catch{}return l}async function je(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 je(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):(je(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(()=>je(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 park(){if(ne||se||Se.length||ke||be||_e||oe)return!1;try{await ue}catch{}if(ne||se||Se.length||ke||be||_e||oe)return!1;const e=Z;he(),Z=null,ee=null,re=!1,ie=!1,me="";try{e?.end()}catch{return!1}return!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:Re(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 m,hermesRoleFor as d}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";import{HERMES_REASONING_EFFORT_IDS as E}from"./reasoning-effort.mjs";import{inspectCatastrophicTool as I}from"./catastrophic-command-guard.mjs";import{hermesProjectSkillEnv as S}from"./runtime-skill-parity.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(E);export const HERMES_EFFORT_CONFIG_ID="thinkpool_reasoning_effort";const k=new Set([...l,"read_review_file"]),b=/(?:TOKEN|SECRET|PASSWORD|PRIVATE_KEY|API_KEY|ACCESS_KEY|CREDENTIAL)/i,x=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])=>!b.test(e)));const v=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")}},C=e=>(e?.availableModels||[]).map(e=>({value:e.modelId,displayName:e.name||e.modelId,description:e.description||""})),R=e=>new n(e);export function buildHermesPromptText({text:e,firstTurn:t=!1,terminalRolePrompt:o,rolePrompt:n,roomContext:s,promptIndex:r=0,forceFullReminder:i=!1,turnCause:l=null}={}){const a="function"==typeof s?s():s,c=!!(o||n||a),m=_({promptIndex:r,forceFull:i,turnCause:l}),d=[t||m?o:"",c?g({text:e,promptIndex:r,forceFull:i,turnCause:l}):"",t||m?n:"",a].filter(Boolean).join("\n\n"),u=String(e??"");return d?`${d}\n\n${u}`:u}const H=(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:E=process.env,mode:b="default",onEvent:$,requestPermission:T,roomContext:P,terminalRolePrompt:M,rolePrompt:F,mcpServers:O,requiredMcpTools:q=[],prepareCwd:j=null,command:A="thinkpool",args:N=["acp"],clientFactory:D=R,mcpHttpFactory:L=u,lazy:V=!1,hermesRole:B=null,crossPostGate:U=null,didSpawnTarget:z=null,crossRoomPostGate:G=null,effort:K="high",admitStart:Y=null,onTurnStart:W=null}={}){let J=e;const Q=n||null;let X=Q,Z=HERMES_SUPPORTED_MODES.has(b)?b:"default",ee=g||null,te=null,oe=null,ne=null,se=null,re=!1,ie=!1,le=!1,ae=!1,ce=!0,me=0,de=!0;const ue=w(P);let pe="",fe=Promise.resolve(),he=0;const ge=new Set;let we=()=>{},_e=!1,ye=null,Ee=!1,Ie=!1,Se=!1,ke=null===K?null:HERMES_EFFORT_LEVELS.has(K)?K:"high";const be=[];let xe=!1,ve=!1;const Ce=B||d({}),Re=()=>"plan"===Z?"plan":Ce,He=()=>"plan"===Z?q.filter(e=>k.has(e)):q,$e=e=>"acceptEdits"===e?"accept_edits":"bypassPermissions"===e?"dont_ask":"default",Te=y(e=>{try{$?.(e)}catch{}});async function Pe(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||oe?.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(I({toolName:i.toolName,input:i.input,cwd:J}).blocked)return{outcome:{outcome:"cancelled"}};if(l.includes(s))return{outcome:H("allow",t.options)};if(s===a)return{outcome:H("allow",t.options)};if("post_to_terminal"===s){if(!(U?U():{ok:!0}).ok)return{outcome:{outcome:"cancelled"}};const e=!!z&&!!z(n.input?.terminal);if(!h({mode:Z,spawnedByMe:e}))return{outcome:H("allow",t.options)}}if("post_to_session"===s){if(!(G?G():{ok:!0}).ok)return{outcome:{outcome:"cancelled"}};let e="deny";try{e=await(T?.(i))}catch{}return{outcome:H(e,t.options)}}if("plan"===Z)return{outcome:{outcome:"cancelled"}};if(p({toolName:i.toolName,input:i.input,mode:Z}))return{outcome:H("allow",t.options)};let c="deny";try{c=await(T?.(i))}catch{}return{outcome:H(c,t.options)}}function Me(e,t){if("session/update"!==e)return;const o=t?.update||t;ye&&"agent_message_chunk"===o?.sessionUpdate&&"text"===o?.content?.type?ye.push(String(o.content.text||"")):_e&&x.has(o?.sessionUpdate)||oe?.push(t)}async function Fe(){const e=[...new Set((Array.isArray(He())?He():[]).map(e=>String(e||"").trim()).filter(Boolean))];if(!e.length&&"plan"!==Re())return{inventory:"",missing:[],forbidden:[]};const t=c(Re(),{mcpTools:e}),n=t.builtinTools,s=t.requiredBuiltinTools,r=t.mcpTools,i=[];ye=i;try{await te.request("session/prompt",{sessionId:ee,prompt:[{type:"text",text:"/tools"}],messageId:o()},3e4)}finally{ye=null}const l=i.join(""),a=e=>new RegExp(`(?:^|[^a-z0-9_])${e}(?:$|[^a-z0-9_])`,"i").test(l),m=[...l.matchAll(/\bmcp(?:__|_)[a-z0-9_]+/gi)].map(e=>e[0]),d=new Set(r.flatMap(e=>[`mcp__thinkpool__${e}`,`mcp_thinkpool_${e}`])),u=[...new Set(m.filter(e=>!d.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 Oe(){const e=await Fe();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 qe(){if(re)throw new Error("Hermes session is closed");if(ae)throw new Error("Hermes ACP process crashed; this lane must be restarted before sending another turn");if(te?.alive&&oe)return;if(se)return se;const e="function"==typeof Y?Y():{ok:!0};if(!1===e?.ok)throw new Error(e.reason||"Host memory is critically low. This Hermes runtime was not started.");return se=(async()=>{if(!le&&j)try{J=j()||J}catch{}const e=O?.thinkpool;e&&!ne&&(ne=await L({sdkServer:e}));let o=hermesChildEnv(E),n=A,r=N;if("thinkpool"===A&&D===R){const e=i({command:A,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,...S(J),HERMES_HOME:e.profile,THINKPOOL_HERMES_ACP_POLICY:m(Re(),{mcpTools:He()})}}let l=!1;we=()=>{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,we=()=>{l=!0},te=D({command:n,args:r,cwd:J,env:o,onNotification:Me,onRequest:Pe,onStderr:e=>{pe=(pe+e).slice(-2e3)},onClose:e=>{re||ae||l||(ae=!0,ie=!1,Te({kind:"error",message:`Hermes ACP closed unexpectedly: ${e?.message||e}`,recoverable:!0}))}}),te.start();try{c=await te.request("initialize",a,15e3);break}catch(t){if(Ie||0!==e||!/^initialize timed out\b/i.test(String(t?.message||t)))throw t;we();const o=te;te=null,o?.end(),pe=""}}if(1!==c?.protocolVersion)throw new Error(`Unsupported Hermes ACP protocol ${c?.protocolVersion??"unknown"}; expected 1`);const d={cwd:t.resolve(J),mcpServers:ne?[(u=ne.url,{type:"http",name:"thinkpool",url:u,headers:[]})]:[]};var u;let p;if(oe=new s({onEvent:Te,sessionId:ee,model:X}),ee){if(!c?.agentCapabilities?.sessionCapabilities?.resume)throw new Error(`Hermes session ${ee} cannot be resumed by this ACP release; no fresh session was substituted`);try{_e=!0,p=await te.request("session/resume",{...d,sessionId:ee},3e4)}catch(e){throw new Error(`Hermes session ${ee} is not available; no fresh session was substituted (${e?.message||e})`)}finally{_e=!1}}else p=await te.request("session/new",d,3e4);if(ee||(ee=p?.sessionId||null),!ee)throw new Error("Hermes ACP did not return a session id");let f=await Fe(),h=f.missing;if(h.length&&c?.agentCapabilities?.sessionCapabilities?.resume){try{_e=!0,p=await te.request("session/resume",{...d,sessionId:ee},3e4)}finally{_e=!1}f=await Fe(),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}`)}X=Q||p?.models?.currentModelId||null||X,Q&&(await te.request("session/set_model",{sessionId:ee,modelId:X}),await Oe()),ke&&await te.request("session/set_config_option",{sessionId:ee,configId:HERMES_EFFORT_CONFIG_ID,value:ke});const g=p?.models?{...p.models,currentModelId:X}:X?{currentModelId:X,availableModels:[]}:p?.models;Se?(oe.sessionId=ee,oe.model=X,Se=!1):oe.startSession({sessionId:ee,models:g,modes:p?.modes,commands:[]}),le=!0;const w=$e(Z);p?.modes?.availableModes?.some(e=>e.id===w)&&p.modes.currentModeId!==w&&await te.request("session/set_mode",{sessionId:ee,modeId:w}),Te({kind:"capabilities",runtime:"hermes",protocol:"acp",protocolVersion:c?.protocolVersion,capabilities:c?.agentCapabilities||{},models:C(p?.models),flow:"thinkpool"===A&&D===R})})().catch(async e=>{ae=!0,te?.end();try{await(ne?.close())}catch{}throw ne=null,Ie||Te({kind:"error",message:`Hermes ACP startup failed: ${e?.message||e}${pe?`: ${pe.trim().slice(-240)}`:""}`,recoverable:!0}),e}).finally(()=>{se=null,Ie=!1}),se}function je(){oe?oe.finishTurn({stopReason:"cancelled"}):Te({kind:"result",subtype:"aborted",sessionId:ee,model:X,costUsd:null,usage:null,numTurns:1,durationMs:void 0,denials:0,resultText:null})}function Ae(e,t={},{promptIndex:o=0,forceFull:n=!1}={}){const s=String(e);if(/^\s*\//.test(s))return[{type:"text",text:s}];const r=t?.turnCause||null,i=_({promptIndex:o,forceFull:n,turnCause:r}),l=[{type:"text",text:buildHermesPromptText({text:s,firstTurn:ce,terminalRolePrompt:M,rolePrompt:F,roomContext:()=>ue({force:i}),promptIndex:o,forceFullReminder:n,turnCause:r})}];for(const e of Array.isArray(t.images)?t.images:[])try{l.push(v(e))}catch{}return l}async function Ne(e,t={},{steering:n=!1,turnId:s=he,promptIndex:r=0,forceFull:i=!1}={}){try{await qe()}catch(e){if(ge.has(s))return{stopReason:"cancelled"};throw e}if(ge.has(s))return{stopReason:"cancelled"};const l=n?`/steer ${String(e)}`:String(e);let a;try{a=await te.request("session/prompt",{sessionId:ee,prompt:Ae(l,t,{promptIndex:r,forceFull:i}),messageId:o()},0)}catch(e){if(ge.has(s))return re||await async function(){const e=te;we(),te=null,oe=null,e?.end(),await qe()}(),{stopReason:"cancelled"};throw e}if(!n){if(ge.has(s))return a;ie=!1,ce=!1;const t=/^\s*\/compact\b/i.test(String(e||""))?oe.compactOutcome():null;t&&Te({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(oe.text):new RegExp(`Session reasoning set to ${e}`,"i").test(oe.text))&&(ke="reset"===e?null:e,Te({kind:"effort",level:ke}))}ve=be.length>0&&!re&&!ge.has(s),oe.finishTurn({stopReason:a?.stopReason,usage:a?.usage}),ve&&async function(){if(!(xe||re||ie)){xe=!0;try{for(;be.length&&!re&&!ie;){ve=!1;const e=be.shift(),t=++he;ie=!0;try{W?.(e.options||{})}catch{}try{await Ne(e.text,e.options,{steering:!1,turnId:t,promptIndex:e.promptIndex,forceFull:e.forceFull})}catch(e){ie=!1,Te({kind:"error",message:`Hermes queued turn failed: ${e?.message||e}`,recoverable:!0})}}}finally{xe=!1,ve=!1}}}()}return a}return V||queueMicrotask(()=>{qe().catch(()=>{})}),{get sessionId(){return ee},get turnActive(){return ie},get queuedDepth(){return be.length},get canSteer(){return!!te?.alive},get started(){return le},get models(){return[]},get effort(){return ke},sendTurn(e,t={}){if(re)return!1;if(!te?.alive&&!se){const e="function"==typeof Y?Y():{ok:!0};if(!1===e?.ok)return Te({kind:"error",message:e.reason||"Host memory is critically low. This Hermes runtime was not started.",recoverable:!0}),!1}const o=me++,n=de;de=/^\s*\/(?:compact|reset|clear)\b/i.test(String(e||"")),ae&&!re&&(we(),te?.end(),te=null,oe=null,ae=!1,pe="");const s=String(e||"").match(/^\s*\/queue\s+([\s\S]+)$/i);if(ve)return be.push({text:s?s[1]:String(e||""),options:t,promptIndex:o,forceFull:n}),Te({kind:"queue-add",queued:!0,depth:be.length}),!0;if(ie)return s?(be.push({text:s[1],options:t,promptIndex:o,forceFull:n}),Te({kind:"queue-add",queued:!0,depth:be.length}),!0):(Ne(e,t,{steering:!0,turnId:he,promptIndex:o,forceFull:n}).catch(e=>Te({kind:"error",message:`Hermes steering failed: ${e?.message||e}`,recoverable:!0})),!0);const r=++he;return ie=!0,fe=fe.then(()=>Ne(e,t,{steering:!1,turnId:r,promptIndex:o,forceFull:n})).catch(e=>{ie=!1,Te({kind:"error",message:`Hermes turn failed: ${e?.message||e}`,recoverable:!0})}),!0},async park(){if(re||ie||be.length||ve||xe||Ee||se)return!1;try{await fe}catch{}if(re||ie||be.length||ve||xe||Ee||se)return!1;const e=te;we(),te=null,oe=null,le=!1,ae=!1,pe="";try{e?.end()}catch{return!1}return!0},async abort(){if(!ie)return;be.length=0,ve=!1;const e=he;return ge.add(e),ge.size>32&&ge.delete(ge.values().next().value),ie=!1,ce=!1,se?(Ie=!0,we(),te?.end(),je(),void await fe.catch(()=>{})):ee&&te?.alive?(je(),await te.notify("session/cancel",{sessionId:ee}).catch(e=>{ae=!0,te?.end(),Te({kind:"error",message:`Hermes cancellation delivery failed; ACP process stopped: ${e?.message||e}`,recoverable:!0})}),void await fe.catch(()=>{})):(je(),void await fe.catch(()=>{}))},end(){re=!0,ie=!1,Te.cancel(),be.length=0,ve=!1,ee&&te?.alive?te.request("session/close",{sessionId:ee},1500).catch(()=>{}).finally(()=>te?.end()):te?.end(),ne?.close().catch(()=>{})},setModel(e){if(!e||ie||Ee)return!1;const t=String(e);return Ee=!0,qe().then(()=>te.request("session/set_model",{sessionId:ee,modelId:t})).then(async()=>{await Oe(),re||(X=t,oe&&(oe.model=X),Te({kind:"model-switch",status:"success",model:X,requested:t}))}).catch(e=>{re||Te({kind:"model-switch",status:"failed",model:X,requested:t,message:`Hermes model switch failed: ${e?.message||e}`})}).finally(()=>{Ee=!1}),!0},setMode(e){if(ie||!HERMES_SUPPORTED_MODES.has(e))return!1;const t=Z;if("plan"===t!=("plan"===e)){if(Z=e,Se=le,le){we();const e=te;te=null,oe=null,le=!1,ae=!1,pe="",e?.end()}return qe().then(()=>Te({kind:"mode",mode:Z})).catch(e=>{Z=t,Se=!1,Te({kind:"error",message:`Hermes mode switch failed: ${e?.message||e}`,recoverable:!0})}),!0}return qe().then(()=>te.request("session/set_mode",{sessionId:ee,modeId:$e(e)})).then(()=>{Z=e,Te({kind:"mode",mode:Z})}).catch(e=>Te({kind:"error",message:`Hermes mode switch failed: ${e?.message||e}`,recoverable:!0})),!0},setEffort(e){if(ie||!HERMES_EFFORT_LEVELS.has(e))return!1;const t=String(e);return qe().then(()=>te.request("session/set_config_option",{sessionId:ee,configId:HERMES_EFFORT_CONFIG_ID,value:t})).then(()=>{ke=t,Te({kind:"effort",level:ke})}).catch(e=>Te({kind:"error",message:`Hermes reasoning change failed: ${e?.message||e}`,recoverable:!0})),!0},clearContext(){return!ie&&this.sendTurn("/reset")},warm(){qe().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:
|
|
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";export const HERMES_CATASTROPHIC_FILENAME="catastrophic-command-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)}export function hermesCatastrophicGuardPath(e={}){return t.join(t.dirname(hermesGuardPath(e)),HERMES_CATASTROPHIC_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:E=process.env,homeDir:g=o.homedir(),platform:w=process.platform,execFile:k=r,fsImpl:_=e,guardSource:S=e.readFileSync(new URL("./hermes-delegation-guard.mjs",import.meta.url),"utf8"),catastrophicSource:y=e.readFileSync(new URL("./catastrophic-command-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 A=(e,o={})=>k(u,e,{encoding:"utf8",env:o.env||E,timeout:o.timeout||2e4,stdio:["ignore","pipe","pipe"]}),H=["-p",f];let v="",M=!1;try{v=A(["profile","show",f])}catch{}if(!/Profile:\s*thinkpool/i.test(v)){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"),A(e,{timeout:6e4}),M=!0,v=A(["profile","show",f])}if(!h(v,f)&&(A(["profile","alias",f,"--name",f]),v=A(["profile","show",f]),!h(v,f)))throw new Error("Hermes did not create the required thinkpool profile alias");const R=s(A([...H,"config","path"]));if(!R)throw new Error("Hermes did not report the thinkpool profile config path");const x=R.startsWith("~")?t.join(g,R.slice(1).replace(/^[/\\]/,"")):t.resolve(R),D=String(v).match(/^Path:\s*(.+)$/im);if(!D)throw new Error("Hermes did not report the isolated thinkpool profile path");const I=D[1].trim().startsWith("~")?t.join(g,D[1].trim().slice(1).replace(/^[/\\]/,"")):t.resolve(D[1].trim()),P=t.relative(I,x);if(P.startsWith("..")||t.isAbsolute(P))throw new Error("Hermes reported a config path outside the isolated thinkpool profile");let $={};if(_.existsSync(x)){const e=i.parse(_.readFileSync(x,"utf8"));if(null!=e&&!a(e))throw new Error("Hermes thinkpool config is not a YAML mapping; refusing to overwrite it");$=e||{}}if(null!=$.hooks&&!a($.hooks))throw new Error("Hermes thinkpool hooks config is not a mapping; refusing to overwrite it");if(null!=$.model&&!a($.model))throw new Error("Hermes thinkpool model config is not a mapping; refusing to overwrite it");const L=$.model||={};L.provider="nous",L.default=HERMES_DEFAULT_MODEL,delete L.base_url,delete L.api_mode;const F=$.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 j=F.pre_tool_call||[];F.pre_tool_call=[...j.filter(e=>!(e=>a(e)&&["^delegate_task$","^(delegate_task|terminal)$"].includes(e.matcher)&&new RegExp(`${HERMES_GUARD_FILENAME.replaceAll(".","\\.")}$`).test(String(e.command||"").replace(/["']/g,"")))(e)),{matcher:"^(delegate_task|terminal)$",command:l(hermesGuardPath({homeDir:g,env:E,platform:w})),timeout:5}];const b=hermesGuardPath({homeDir:g,env:E,platform:w}),O=hermesCatastrophicGuardPath({homeDir:g,env:E,platform:w}),C=t.join(I,"shell-hooks-allowlist.json"),G=c(b,_),T=c(O,_),N=c(x,_),U=c(C,_);try{m(b,S,{fsImpl:_,mode:448}),m(O,y,{fsImpl:_,mode:384}),m(x,i.stringify($,{lineWidth:0}),{fsImpl:_,mode:384});const e={...E,HERMES_ACCEPT_HOOKS:"1"};A([...H,"hooks","revoke",l(b)],{env:e});const o=A([...H,"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");A([...H,"hooks","doctor"],{env:e}),A([...H,"acp","--check"],{env:e});const t=n({command:u,prefixArgs:H,execFile:k,env:e,fsImpl:_});if(!t.available)throw new Error(`Hermes setup did not pass the final runtime probe: ${t.reason||"unavailable"}`);return{...t,profile:f,created:M,mode:M?d:"existing",guardPath:b,configPath:x}}catch(e){try{p(x,N,_),p(b,G,_),p(O,T,_),p(C,U,_)}catch(o){e.message=`${e.message}; rollback also failed: ${s(o.message)}`}throw e}}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "thinkpool-pair",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.396",
|
|
4
4
|
"description": "Connect Claude Code, Codex, or Hermes on your computer to a thinkpool Code room.",
|
|
5
5
|
"license": "SEE LICENSE IN LICENSE",
|
|
6
6
|
"type": "module",
|
|
@@ -51,7 +51,10 @@
|
|
|
51
51
|
"hermes-setup.mjs",
|
|
52
52
|
"hermes-isolation.mjs",
|
|
53
53
|
"hermes-delegation-guard.mjs",
|
|
54
|
+
"catastrophic-command-guard.mjs",
|
|
55
|
+
"codex-catastrophic-command-hook.mjs",
|
|
54
56
|
"runtime-registry.mjs",
|
|
57
|
+
"runtime-skill-parity.mjs",
|
|
55
58
|
"reasoning-effort.mjs",
|
|
56
59
|
"runtime-contract.mjs",
|
|
57
60
|
"command-catalog.mjs",
|
package/publish-manifest.json
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"format": 1,
|
|
3
3
|
"package": "thinkpool-pair",
|
|
4
|
-
"version": "0.7.
|
|
4
|
+
"version": "0.7.396",
|
|
5
5
|
"kind": "minified",
|
|
6
|
-
"minifiedModules":
|
|
7
|
-
"sourceModuleBytes":
|
|
8
|
-
"artifactModuleBytes":
|
|
6
|
+
"minifiedModules": 122,
|
|
7
|
+
"sourceModuleBytes": 1772854,
|
|
8
|
+
"artifactModuleBytes": 803212,
|
|
9
9
|
"files": {
|
|
10
|
-
"package.json": "
|
|
10
|
+
"package.json": "028cc075f4c47743328cd5758099bd1fc6c465c65143bff89b3fe0240cd774f6",
|
|
11
11
|
"LICENSE": "8a030197ca1cad9669909ca14c9ad15cf48d9fe551da526ccaf1fca993eddb23",
|
|
12
12
|
"bridge.mjs": "0c4e67cb580ebaf37e39828f372b57cad5f1b70750ef6c5ec8cf49db965c1bdb",
|
|
13
13
|
"bridge-service.sh": "b64f27c473ca8f3a49190fd08c23947ab14ca9958d70b6fa53c7bddc2be91403",
|
|
@@ -24,34 +24,37 @@
|
|
|
24
24
|
"byok-detect.mjs": "f31f2ca426737581a97c391b3cecfc75f0207cfa31f51c6bf4a9148309c9625d",
|
|
25
25
|
"context-contract.mjs": "ffebb435dc5d75c289329bf16ab909df9d634a2b508154c250c6b25906a02a91",
|
|
26
26
|
"context-windows.mjs": "80af664f84d7e95ef2b0a2a3131090ac4ccd522709448ac9a0d96c378bbec0f7",
|
|
27
|
-
"claude-session.mjs": "
|
|
27
|
+
"claude-session.mjs": "8992a67c66cb5745f7a72c373ea2bf06f3dcc77a68a3735f21dcbe8968623edc",
|
|
28
28
|
"terminal-name.mjs": "f39bb8d1e148ab9a74cbb96438d31e524d6597b8035f00d7a906c28c306c0cf8",
|
|
29
29
|
"claude-command-catalog.mjs": "36bd4342b2ae94b43a0072533ef2d1648c7473003964bbad433fdb1a8b2bfec1",
|
|
30
|
-
"codex-session.mjs": "
|
|
30
|
+
"codex-session.mjs": "e33bbf0bbe7c70db9eca1b3cf80a65d74bbdf86ba6884844b309ebb522866bab",
|
|
31
31
|
"question-response.mjs": "35a7ba53d3eb117717c18004d2a8441f8f37665b231a3c44f4bd9321ff59dd57",
|
|
32
32
|
"thinkpool-room-prompt.mjs": "055598556558fb17e100d85ea7041d910332cf06ca978d1e648a55ad4377331b",
|
|
33
33
|
"thinkpool-prompt-contracts.mjs": "051602d7d2fe9b34df92957bd3922c5eeebedbcc60c892adf415243887d8c62f",
|
|
34
34
|
"thinkpool-capabilities.json": "9884346a9c9d81acb4ecb923e7264dc13b114cc15d9bc509b7c3885066ac33f1",
|
|
35
|
-
"codex-app-server.mjs": "
|
|
35
|
+
"codex-app-server.mjs": "d50e30ba6cb2a383c90d89186bc7a13f4caf900c977dd501572468986009470e",
|
|
36
36
|
"codex-images.mjs": "31605d6d8b3dd6b73395da63d1b0c513445c95ffa2f4306ee466f201d633ffc0",
|
|
37
37
|
"codex-mcp-http.mjs": "7472543648a9bb575ce20dc3e3270c2da84174c6ea6e9faf87dfaafb95b6212a",
|
|
38
38
|
"lane-worktree.mjs": "37583757ab82843412ba565dfba3d7cab517e94782eb33115decd5438d092d8f",
|
|
39
39
|
"lane-work-snapshot.mjs": "ba40233059ea86ba2e08d0b61763427baa9602eb6edf6c445a6b22fd15af212c",
|
|
40
|
-
"codex-event-mapper.mjs": "
|
|
41
|
-
"edit-diff.mjs": "
|
|
40
|
+
"codex-event-mapper.mjs": "a7c423806b5e80957733c21d756078740773ae7230305ed00dba72cecb3a558a",
|
|
41
|
+
"edit-diff.mjs": "258c64416b4e60b1f8243546f1cc25e1b27457f0edeaa0e6c837a3e888f49ba2",
|
|
42
42
|
"cumulative-event-relay.mjs": "cf6e6533e33b3c5bb0b462d15ccc86106f2b56f8b1e66fc1f4867dd8f44d7ae6",
|
|
43
43
|
"codex-commands.mjs": "cae3f617ac42e1316d8980ef97e4255227fe5df5e8e9b556630a3bfb83bb7c2b",
|
|
44
44
|
"acp-client.mjs": "9e7919ff79825883820019ff7a0ca578f120853ed61c7caab21a8ffc3a67ffc9",
|
|
45
|
-
"hermes-session.mjs": "
|
|
45
|
+
"hermes-session.mjs": "b3d521ea59297edb58ecfcc0daad3c0df88d96131cadc455fa36fc1cca954210",
|
|
46
46
|
"hermes-model-cache.mjs": "aee23efdebe1aa5e10053f291a9153981fe9b45065c293c9519ca73477251e72",
|
|
47
47
|
"hermes-policy.mjs": "df6dd384ab130b3626045816b6ac4c1d2aecde37fb7b7a4be0c1bffc375fd8ff",
|
|
48
|
-
"hermes-acp-bootstrap.py": "
|
|
48
|
+
"hermes-acp-bootstrap.py": "9453b48d1ec0057a1d1754fc828acb949ad3f69c687fd055dc3856823d26c19b",
|
|
49
49
|
"hermes-event-mapper.mjs": "7c714e15d06d970c4d602e755461bacf8f4d4136a8afc03406e8b3207ff84cdc",
|
|
50
|
-
"hermes-probe.mjs": "
|
|
51
|
-
"hermes-setup.mjs": "
|
|
50
|
+
"hermes-probe.mjs": "3fc419707f0df9342bc3a5fbfbba2ec2ba1511309dfb1ad8cc79f6f8da4e6b3d",
|
|
51
|
+
"hermes-setup.mjs": "50840b3e0101dc94aa2cd6dd2456efd8377e30bd5d797a00d360fae28e13dddb",
|
|
52
52
|
"hermes-isolation.mjs": "50e960ce815c520805245cc75719c9c53e2c5af51b7304c8ecef8a4946e23c6d",
|
|
53
|
-
"hermes-delegation-guard.mjs": "
|
|
53
|
+
"hermes-delegation-guard.mjs": "f737d05fb93591848fa3839fd5df2c019779bda828d42ab785cd57213665f922",
|
|
54
|
+
"catastrophic-command-guard.mjs": "65158e59266e4be1e2a72479fb9b2c7e7a4ec7902a8b764d3b26017cd0cdbfd5",
|
|
55
|
+
"codex-catastrophic-command-hook.mjs": "5bf1eb5dd837b77044846af0cb8936a81edbc435e454217ddb15981f3626f7dc",
|
|
54
56
|
"runtime-registry.mjs": "d0a3d93edb60443df56273025ed3f10ed4b1d06a16b84759267782691cacdda8",
|
|
57
|
+
"runtime-skill-parity.mjs": "933535bc7b70d7843cf7bf8eb83f306693045ccf7154ad781b6d42be75442f1c",
|
|
55
58
|
"reasoning-effort.mjs": "3a514997656bbcd41ff1aefe9d5e59c5920a90e6bfcc928016d95987db2ceaa3",
|
|
56
59
|
"runtime-contract.mjs": "eb24b177002ba33353ec0335f4f830fb57fe313552b1a8d7aef8163cc743f6c1",
|
|
57
60
|
"command-catalog.mjs": "ecad63189f8803eba99e1b797afa57b355ca1b55d09e1cf212cc74dd952baa65",
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import e from"node:fs";import t from"node:path";export const THINKPOOL_PROJECT_SKILLS_ENV="THINKPOOL_PROJECT_SKILLS_DIR";export const PROJECT_SKILL_RUNTIMES=Object.freeze(["claude","codex","hermes"]);export function resolveProjectSkillsDir(n,{fsImpl:r=e}={}){let c=t.resolve(n||process.cwd());for(;;){const e=t.join(c,".claude","skills");try{if(r.statSync(e).isDirectory())return e}catch{}const n=t.dirname(c);if(n===c)return null;c=n}}export function hermesProjectSkillEnv(e,t={}){const n=resolveProjectSkillsDir(e,t);return n?{[THINKPOOL_PROJECT_SKILLS_ENV]:n}:{}}const n=(e,t)=>{const n=String(e).match(/^---\s*\n([\s\S]*?)\n---(?:\s*\n|$)/)?.[1]||"";return n.match(new RegExp(`^${t}:\\s*(.+)$`,"m"))?.[1]?.trim().replace(/^['"]|['"]$/g,"")||""};export function canonicalProjectSkills(r,{fsImpl:c=e}={}){const i=t.join(t.resolve(r),".claude","skills");let l=[];try{l=c.readdirSync(i).sort()}catch{return[]}return l.flatMap(e=>{const l=t.join(i,e,"SKILL.md");try{if(!c.statSync(l).isFile())return[];const i=c.readFileSync(l,"utf8");return[{name:n(i,"name")||e,directory:e,description:n(i,"description"),canonicalFile:t.relative(r,l),bytes:Buffer.byteLength(i)}]}catch{return[]}})}export function runtimeSkillManifest(n,{fsImpl:r=e,checkedAt:c=(new Date).toISOString()}={}){const i=t.resolve(n),l=canonicalProjectSkills(i,{fsImpl:r}).map(e=>{const n=t.join(i,e.canonicalFile),c=t.join(i,".agents","skills",e.directory,"SKILL.md");let l="";try{l=r.readFileSync(n,"utf8")}catch{}const s=[...l.matchAll(/`(\.claude\/skills\/[^`\s]+)`/g)].map(e=>e[1].replace(/[),.;:]+$/,"")).filter(e=>!/[<>{}*]/.test(e)),o=[...new Set(s.filter(e=>{try{return!r.existsSync(t.join(i,e))}catch{return!0}}))];let a=!1;try{a=r.realpathSync(c)===r.realpathSync(n)}catch{}return{...e,dependencies:{status:o.length?"missing":"project-paths-checked",missing:o},runtimes:{claude:{discoverable:!0,body:"canonical",path:e.canonicalFile},codex:{discoverable:a,body:a?"canonical-symlink":"missing",path:t.relative(i,c)},hermes:{discoverable:!0,body:"canonical-external-dir",path:e.canonicalFile,tools:["skills_list","skill_view"]}}}});return{schemaVersion:1,checkedAt:c,canonicalDirectory:".claude/skills",runtimes:PROJECT_SKILL_RUNTIMES,skills:l}}
|