@north-light/cli 0.3.437 → 0.3.439
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/dist/clients/attach/viewer.js +428 -428
- package/dist/core/runtime/host.js +3 -3
- package/dist/core/runtime/situational-context.js +2 -2
- package/dist/core/runtime/structured-output.d.ts +1 -0
- package/dist/core/runtime/structured-output.js +6 -5
- package/dist/pi-extensions/canvas-structured-output.js +9 -5
- package/docs/ci-runner.md +58 -0
- package/package.json +2 -2
- package/packages/crouter-identity/package.json +1 -1
- package/runtime.lock.json +12 -12
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
var
|
|
2
|
-
`;if(t===r&&o!==l){if(JSON.stringify(f)!==JSON.stringify(c(f)))throw new Error("terminal models.json contains credentials; move them into daemon secrets");
|
|
3
|
-
`,{atomic:!0,mode:432}),
|
|
1
|
+
var be=Object.defineProperty;var u=(e,n)=>be(e,"name",{value:n,configurable:!0});import{assertGranteeRunsHere as Se}from"./unsandboxed.js";import{spawn as ye}from"node:child_process";import{mkdirSync as ce,existsSync as $,unlinkSync as ae,openSync as j,closeSync as T,writeSync as Pe,chmodSync as Re,lstatSync as xe}from"node:fs";import{ensureAppSpace as Oe}from"../spaces/space.js";import{sandboxCacheEnv as Te}from"../spaces/cache-env.js";import{compileLaunch as De,launchStillGranted as Ne}from"../spaces/mounts.js";import{grantsGeneration as le}from"../grants/mirror.js";import{LAUNCHER_SOCKET as U}from"../../launcher/spec.js";import{launcherRequest as G}from"../../launcher/client.js";import{piAgentDir as Ae}from"./package-health.js";import{existsBeneath as ue,readFileBeneath as Ce,readTextBeneathIfExists as Le,setModeBeneath as y,withDirBeneath as W,writeFileBeneath as z}from"../spaces/open-beneath.js";import{dirname as V,join as w,resolve as Be,isAbsolute as $e}from"node:path";import{fileURLToPath as fe}from"node:url";import{connectViewSocket as je}from"./view-socket.js";import{appSpace as F}from"../layout.js";import{readConfig as Ge,readMergedLaunchConfig as Fe}from"../config.js";import{readRotationConfig as Ke}from"../subscription-state.js";import{getNode as S}from"../canvas/index.js";import{crtrHome as de,nodeRunDir as X,nodeRunJobDir as Me,jobDir as He,viewSocketPath as Je,nodeAgentDir as qe,contextDir as Ue,nodeHome as We}from"../canvas/paths.js";import{encodeFrame as ze}from"./broker-protocol.js";import{buildBrokerEnv as Ve}from"./spawn-env.js";import{hostExecPathFresh as Xe}from"./branded-host.js";import{isPidAlive as Q,killProcessTreePids as me,isAnyPidAlive as Qe,captureTeardownSnapshot as Ye}from"../canvas/pid.js";import{assertMandatoryCanvasExtensions as Ze}from"./canvas-extensions.js";import{boundFleet as Ie,brokerCapReached as er,brokerThresholdsForDaemon as rr}from"./fleet.js";import{emitEvent as E}from"../events/emit.js";import{bindBrokerEventSource as nr,eventSource as tr}from"../events/source.js";import{envBrokerEngine as or}from"../../shared/env.js";import{conversationIdFromPath as ir,conversationStore as sr,ConversationNotFoundError as he,storeAddress as cr}from"../conversation-store/index.js";import{takeNodeToken as ar}from"../canvas/node-tokens.js";function K(){const e=V(fe(import.meta.url));return w(e,"broker-cli.js")}u(K,"resolveBrokerEntry");const lr="@earendil-works/pi-coding-agent";function ur(e,n){const t=w(Ae(),"models.json"),r=w(n,"models.json"),o=Le(V(t),t);if(o===null)return;const f=JSON.parse(o),c=u(i=>Array.isArray(i)?i.map(c):i===null||typeof i!="object"?i:Object.fromEntries(Object.entries(i).filter(([s])=>!/^(apiKey|authorization|headers|token|secret)$/i.test(s)).map(([s,v])=>[s,c(v)])),"scrub"),l=`${JSON.stringify(c(f),null,2)}
|
|
2
|
+
`;if(t===r&&o!==l){if(JSON.stringify(f)!==JSON.stringify(c(f)))throw new Error("terminal models.json contains credentials; move them into daemon secrets");y(e,r,432);return}z(e,r,l,{atomic:!0,mode:432}),y(e,r,432)}u(ur,"writeBrokerModels");function pe(e){return e.env.CRTR_BROKER_ENGINE??or()}u(pe,"resolveEngineSpec");function we(e){return e.includes("/")||e.endsWith(".jsonl")}u(we,"isPlainLaunchSessionPath");function fr(e,n){return $e(e)?e:Be(n,e)}u(fr,"resolvePlainLaunchPath");function dr(e,n,t,r){if(t==="--session"&&!we(r))throw new Error(`[broker] refusing to launch ${e}: launch recipe passes bare --session id '${r}' \u2014 the broker fatally rejects bare ids; pass the .jsonl path instead.`);if(!we(r))return;const o=fr(r,n),f=ir(o);try{if(f===null)throw new he(o);sr().header(f)}catch(c){if(!(c instanceof he))throw c;const l=t==="--session"?"session file":"fork source";throw new Error(`[broker] refusing to launch ${e}: launch recipe references missing ${l} '${r}' (${o})`)}}u(dr,"preflightLaunchSessionArg");function mr(e,n,t){const r=X(e);ce(r,{recursive:!0}),process.platform==="linux"&&$(U)&&xe(r).uid===process.getuid()&&Re(r,504);const o=w(r,"broker-launch.json"),f={user:Ge("user"),launch:Fe(t,n.env.CRTR_PROFILE_ID||null),rotation:Ke()};z(r,o,JSON.stringify({...n,configSnapshot:f}),{atomic:!0});const c=Ce(r,o).toString("utf8"),l=JSON.parse(c);if(!Array.isArray(l.argv))throw new Error(`[broker] refusing to launch ${e}: broker-launch.json is missing the argv array`);const i=pe(n);try{const d=import.meta.resolve(i);if(d.startsWith("file:")&&!$(fe(d)))throw new Error(`resolved to missing file ${d}`)}catch(d){throw new Error(`[broker] refusing to launch ${e}: cannot resolve broker engine '${i}' from ${K()} \u2014 ${d.message}`)}const s=l.argv,v=[];for(let d=0;d<s.length;d++){const P=s[d];if(P==="-e"){const D=s[++d];D!==void 0&&v.push(D);continue}if(P!=="--session"&&P!=="--fork")continue;const R=s[++d];R!==void 0&&dr(e,t,P,R)}try{Ze(v)}catch(d){throw new Error(`[broker] refusing to launch ${e}: ${d.message}`)}}u(mr,"preflightBrokerLaunch");const hr=2e3,pr=2e3,wr=150;async function M(e,n){const t=Date.now()+n;for(;Qe(e);){const r=t-Date.now();if(r<=0)return!1;await new Promise(o=>setTimeout(o,Math.min(wr,r)))}return!0}u(M,"waitForTreeExit");async function gr(e,n,t){return e.length===0?{confirmed:!0}:t&&await M(e,hr)?{confirmed:!0}:(me(e,"SIGTERM",n),await M(e,pr)?{confirmed:!0}:(me(e,"SIGKILL",n),await M(e,5e3)?{confirmed:!0}:{confirmed:!1,error:new Error(`broker process tree for ${e.join(", ")} remained alive after SIGKILL`)}))}u(gr,"terminateBrokerTree");function ge(e,n){const t=S(e),r=t?.pi_pid,o=r!=null?Ye(r,t?.pi_pid_identity??null,n.excludePid):null;return o?.reused===!0&&E({level:"warn",event:"broker.teardown.pid_reuse_refused",node_id:e,fields:{recorded_pid:r,expected_identity:t?.pi_pid_identity??null}}),{tree:o?.tree??[],identities:o?.identities??null,nodeId:e,launcherPid:$(U)&&o?.tree.includes(r)?r:null}}u(ge,"prepareBrokerTeardown");async function Ee(e){if(e.launcherPid!==null){try{await G({operation:"stop",pid:e.launcherPid})}catch(n){return Q(e.launcherPid)?{confirmed:!1,error:n instanceof Error?n:new Error(String(n))}:{confirmed:!0}}return await M([e.launcherPid],5e3)?{confirmed:!0}:{confirmed:!1,error:new Error(`launcher pid ${e.launcherPid} remained alive`)}}return new Promise(n=>{let t=!1,r=!1;const o=u(i=>{r||(r=!0,n(i))},"settle"),f=u(()=>{try{ae(Je(e.nodeId))}catch{}},"cleanupSocket"),c=u(i=>{gr(e.tree,e.identities,i).then(o,s=>o({confirmed:!1,error:s instanceof Error?s:new Error(String(s))}))},"finish"),l=je(e.nodeId);l.once("error",()=>{t||(f(),c(!1))}),l.once("connect",()=>{t=!0;try{l.write(ze({type:"shutdown"}))}catch{}l.end(),f(),c(!0)})})}u(Ee,"runPreparedBrokerTeardown");const H=new Map;function Xr(e,n={},t){const r=H.get(e);if(r!==void 0)return t!==void 0&&r.after.push(t),r.completion;let o;const f=new Promise(i=>{o=i}),c={completion:f,resolve:o,after:t===void 0?[]:[t]};H.set(e,c);let l;try{l=ge(e,n)}catch(i){const s={confirmed:!1,error:i instanceof Error?i:new Error(String(i))};return H.delete(e),E({level:"error",event:"broker.teardown.unconfirmed",node_id:e,error:s.error}),c.resolve(s),f}return setImmediate(()=>{H.delete(e),Ee(l).then(async i=>{if(i.confirmed)for(const s of c.after)try{await s()}catch(v){E({level:"error",event:"broker.teardown.after_failed",node_id:e,error:v})}else E({level:"error",event:"broker.teardown.unconfirmed",node_id:e,error:i.error});c.resolve(i)},i=>{const s={confirmed:!1,error:i instanceof Error?i:new Error(String(i))};E({level:"error",event:"broker.teardown.unconfirmed",node_id:e,error:s.error}),c.resolve(s)})}),f}u(Xr,"requestBrokerTeardown");function ke(){return process.platform==="linux"&&$(U)}u(ke,"isolatedBrokerLauncherAvailable");const Qr={async launch(e,n,t){if(Se(S(e)?.grantee),S(e)?.isolated===!0&&!ke())throw new Error("isolated broker requires the Linux root launcher");mr(e,n,t.cwd);const r=Ie();if(!r.reserveSlot(e))throw er(rr().automaticReviveCap,r.size());let o=null,f,c=!1;try{const l=He(e);ce(l,{recursive:!0}),o=j(w(l,"broker.log"),"a");const i=pe(n),s={...Ve(n),CRTR_BROKER_ENGINE:i,CRTR_STORE_ADDRESS:cr()};if(i!==lr){const a=n.env.NODE_OPTIONS??process.env.NODE_OPTIONS;a!==void 0&&(s.NODE_OPTIONS=a)}tr()===void 0&&nr(e);const v=S(e)?.run_id;if(v){const a=S(v);if(!a||a.deleting_at)throw new Error("run is deleting")}const d=n.env.CRTR_EXECUTION_ID;if(!d)throw new Error("broker launch has no execution id");const P=ar(d),R=ke();if(S(e)?.isolated===!0&&!R)throw new Error("isolated broker requires the Linux root launcher");if(process.platform==="linux"&&process.env.CRTR_LAUNCHER_SOCKET&&!R)throw new Error("root launcher socket is unavailable; refusing an unsandboxed broker");if(R){const a=S(e);if(!a)throw new Error(`missing node ${e}`);const h=a.grantee??"app:terminal",A=await Oe(h);Object.assign(s,Te(h));const p={uid:process.getuid(),gid:A.gid},g=X(e);for(const[m,k]of[[F(h).nodes,qe(e,h)],[F(h).nodes,Ue(e,h)],[g,Me(e)]])W(m,k,()=>{},!0),y(m,k,1528,p);y(g,w(g,"broker-launch.json"),416,p);const I=w(g,"system-prompt.md");ue(g,I)&&y(g,I,432,p);for(const m of["output-schema.json","situational-context.md"]){const k=w(g,m);ue(g,k)&&y(g,k,432,p)}const ee=a.isolated===!0,_=We(e,h,ee);ee&&(W(F(h).nodes,_,()=>{},!0),y(F(h).nodes,_,1528,p),s.HOME=_,s.PI_CODING_AGENT_DIR=w(_,".pi","agent"));const q=w(_,".pi","agent");W(_,q,()=>{},!0),ur(_,q);const re=w(q,"settings.json"),ve=w(V(K()),"../../builtin-pi-packages/pi-crtr-extensions");z(_,re,`${JSON.stringify({packages:[ve]})}
|
|
3
|
+
`,{atomic:!0,mode:432}),y(_,re,432);const ne=w(X(e),`token-${d}`),C=j(ne,"wx+",384);ae(ne),Pe(C,P);const te=[j(`/proc/self/fd/${C}`,"r"),j(`/proc/self/fd/${C}`,"r")];T(C);const oe={nodeId:e,app:h,sub:a.sub??"user:local",grantId:a.grant_id??null,scopes:a.scopes??null,isolated:a.isolated===!0,...a.isolated===!0?{cwd:a.cwd}:{},command:[process.execPath,K(),"--canvas-home",de(),"--epoch",r.epoch(),e],env:Object.fromEntries(Object.entries({...s,...a.isolated===!0?{PWD:a.cwd}:{}}).filter(m=>m[1]!==void 0)),auditMissing:u(m=>E({level:"warn",event:"broker.mount.missing",node_id:e,fields:{source:m}}),"auditMissing")};try{let m;for(const O of te){const L=le(),b=De(oe),B=await G({operation:"launch",spec:b},[O]);if(!("pid"in B))throw new Error("launcher did not return a pid");if(le()===L||Ne(oe,b)){m=B.pid;break}E({level:"info",event:"broker.launch.regranted",node_id:e,fields:{pid:B.pid}}),await G({operation:"stop",pid:B.pid,signal:"SIGKILL"}).catch(()=>{})}if(m===void 0)throw new Error("grant changed during every launch attempt");const k=m,_e=Date.now(),ie=new Promise(O=>{const L=setInterval(()=>{Q(k)||(clearInterval(L),G({operation:"exit.status",pid:k}).then(b=>"oomKilled"in b&&b.oomKilled,()=>!1).then(b=>{E({level:b?"warn":"info",event:"broker.exited",node_id:e,fields:{pid:k,uptime_ms:Date.now()-_e,oom_killed:b}}),O({code:null,signal:null,...b?{oomKilled:!0}:{}})}))},250);L.unref()});T(o),o=null;const se={pid:k,exited:ie};return r.register(e,se),c=!0,ie.then(O=>r.onChildExit(e,O,d)),se}finally{for(const m of te)T(m)}}const D=await Xe(),x=ye(D,[K(),"--canvas-home",de(),"--epoch",r.epoch(),e],{cwd:t.cwd,detached:!0,stdio:["ignore",o,o,"pipe"],env:s});f=x;const N=x.stdio[3];if(!N||!("end"in N))throw new Error("broker token pipe did not open");N.on("error",a=>E({event:"broker.token_pipe.failed",level:"error",node_id:e,error:a})),N.end(P),T(o),o=null;const Y=Date.now(),Z=new Promise(a=>{let h=!1;const A=u(p=>{h||(h=!0,a(p))},"finish");x.once("exit",(p,g)=>{E({level:p===0?"info":"warn",event:"broker.exited",node_id:e,fields:{code:p,signal:g,uptime_ms:Date.now()-Y}}),A({code:p,signal:g})}),x.once("error",p=>{E({level:"error",event:"broker.launch.failed",node_id:e,error:p,fields:{uptime_ms:Date.now()-Y}}),A({code:null,signal:null})})});x.unref();const J={pid:x.pid??null,exited:Z};return J.pid!==null&&(r.register(e,J),c=!0,Z.then(a=>r.onChildExit(e,a,d))),J}catch(l){if(!c&&f?.pid!=null)try{f.kill("SIGTERM")}catch{}throw l}finally{c||r.releaseReservation(e),o!==null&&T(o)}},isAlive(e){return Q((typeof e=="string"?S(e):e)?.pi_pid)},teardown(e,n={}){try{return Ee(ge(e,n))}catch(t){return Promise.resolve({confirmed:!1,error:t instanceof Error?t:new Error(String(t))})}},signal(e,n){const t=S(e)?.pi_pid;if(t!=null)try{process.kill(t,n)}catch{}}};export{Qr as headlessBrokerHost,ke as isolatedBrokerLauncherAvailable,mr as preflightBrokerLaunch,Xr as requestBrokerTeardown};
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
var a=Object.defineProperty;var n=(t,r)=>a(t,"name",{value:r,configurable:!0});import{mkdirSync as u}from"node:fs";import{readTextBeneathIfExists as
|
|
2
|
-
`,{atomic:!0}))}n(
|
|
1
|
+
var a=Object.defineProperty;var n=(t,r)=>a(t,"name",{value:r,configurable:!0});import{mkdirSync as u}from"node:fs";import{readTextBeneathIfExists as m,writeFileBeneath as l}from"../spaces/open-beneath.js";import{situationalContextPath as o,nodeRunDir as i}from"../canvas/paths.js";import{formatCard as f,parseCard as p}from"../../shared/generated-context.js";import{shareRunFileWithBroker as c}from"./structured-output.js";import{SITUATIONAL_CONTEXT_CUSTOM_TYPE as b}from"../../shared/generated-context.js";function s(t){return t.trim()===""?"":f("situational",{},t)}n(s,"formatSituationalProse");function B(t,r){const e=r.trim();e!==""&&(u(i(t),{recursive:!0}),l(i(t),o(t),e+`
|
|
2
|
+
`,{atomic:!0}),c(t,o(t)))}n(B,"appendSituationalContext");function x(t){const r=m(i(t),o(t))?.trim()??"";return r!==""?r:null}n(x,"readSituationalContext");function O(t){const r=x(t);return r===null?null:p({content:r})===null?s(r):r}n(O,"situationalContextEnvelope");export{b as SITUATIONAL_CONTEXT_CUSTOM_TYPE,B as appendSituationalContext,s as formatSituationalProse,O as situationalContextEnvelope};
|
|
@@ -30,6 +30,7 @@ export declare const OUTPUT_SCHEMA_CONSTRAINTS = "The full schema (including enu
|
|
|
30
30
|
export declare function outputSchemaPath(nodeId: string): string;
|
|
31
31
|
/** Broker-visible copy; the daemon validates only against outputSchemaPath. */
|
|
32
32
|
export declare function brokerOutputSchemaPath(nodeId: string): string;
|
|
33
|
+
export declare function shareRunFileWithBroker(nodeId: string, path: string): void;
|
|
33
34
|
export declare function projectOutputSchema(nodeId: string): void;
|
|
34
35
|
export declare function outputResultPath(nodeId: string, grantee?: string): string;
|
|
35
36
|
/** The node's machine result, read beneath its agent folder; undefined when it wrote none. */
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
var P=Object.defineProperty;var s=(t,e)=>P(t,"name",{value:e,configurable:!0});import{existsSync as
|
|
2
|
-
|
|
3
|
-
`,{
|
|
4
|
-
`,{atomic:!0})
|
|
5
|
-
`,"
|
|
1
|
+
var P=Object.defineProperty;var s=(t,e)=>P(t,"name",{value:e,configurable:!0});import{existsSync as l,mkdirSync as d,readFileSync as a,unlinkSync as f,writeFileSync as h,renameSync as S}from"node:fs";import{dirname as g,resolve as j}from"node:path";import{randomUUID as b}from"node:crypto";import{nodeDir as R,nodeRunDir as O,contextDir as x,nodeAgentDir as y}from"../canvas/paths.js";import{readTextBeneathIfExists as T,unlinkBeneath as E,writeFileBeneath as q}from"../spaces/open-beneath.js";import{InputError as N}from"../io.js";import{isRecord as c}from"../../shared/predicates.js";import{setModeBeneath as A}from"../spaces/open-beneath.js";import{LAUNCHER_SOCKET as M}from"../../launcher/spec.js";const Y="submit_result",Z="A .json file path, or inline JSON beginning with `{`.",tt='The full schema (including enum, pattern, and numeric bounds) is enforced when the target submits its result (its `submit_result` tool, or `crtr push result`); a mismatch rejects with the field errors and records nothing, so an over-constrained schema costs the target retries. A target that cannot honestly satisfy the schema declines (`submit_result` with `decline`, or `crtr push result --decline "<reason>" --code <token> [--retryable]`), which records a failure outcome with reason `declined` carrying the reason, code, and retryable flag instead of a value.';function m(t){return`${R(t)}/output-schema.json`}s(m,"outputSchemaPath");function _(t){return`${O(t)}/output-schema.json`}s(_,"brokerOutputSchemaPath");function U(){const t=a("/etc/group","utf8").split(`
|
|
2
|
+
`).find(r=>r.startsWith("crtr-data:")),e=Number(t?.split(":")[2]);if(!Number.isInteger(e))throw new Error("crtr-data group is required on sandboxed Linux");return e}s(U,"dataGroupId");function k(t,e){process.platform!=="linux"||!l(M)||A(O(t),e,432,{uid:process.getuid(),gid:U()})}s(k,"shareRunFileWithBroker");function B(t){const e=m(t),r=_(t);l(e)&&(q(O(t),r,a(e),{atomic:!0}),k(t,r))}s(B,"projectOutputSchema");function $(t,e){return`${x(t,e)}/result.json`}s($,"outputResultPath");function et(t,e){const r=T(y(t,e),$(t,e));return r===null?void 0:JSON.parse(r)}s(et,"readOutputResult");function D(t,e,r,n){return r==="terminal"?$(t,n):`${x(t,n)}/structured-requests/${e}/result.json`}s(D,"structuredRequestResultPath");function w(t,e){return`${x(t,e)}/declined.json`}s(w,"outputDeclinedPath");function rt(t){const e=t?.trim();if(e===void 0||e==="")return null;let r,n;if(e.startsWith("{"))r=e,n="inline JSON";else{const o=j(e);n=o;try{r=a(o,"utf8")}catch(i){throw new N({error:"bad_output_schema",message:`could not read output schema from ${o}: ${i.message}`,field:"output-schema",next:"Pass a readable JSON Schema file path, or inline JSON beginning with `{`."})}}let u;try{u=JSON.parse(r)}catch(o){throw new N({error:"bad_output_schema",message:`output schema from ${n} is not valid JSON: ${o.message}`,field:"output-schema",next:"Pass valid JSON whose top-level value is an object."})}if(!c(u))throw new N({error:"bad_output_schema",message:`output schema from ${n} must be a JSON object`,field:"output-schema",next:"Pass a JSON Schema object, not an array, string, number, boolean, or null."});return u}s(rt,"parseOutputSchemaValue");function nt(t){if(t.node_id===null)throw new Error(`cannot publish structured request ${t.request_id} before node binding`);const e=t.node_id,r=m(e);d(g(r),{recursive:!0});const n={request_id:t.request_id,mode:t.submission,schema:t.schema,...t.request_message_id==null?{}:{requestMessageId:t.request_message_id}},u=`${r}.${b()}.tmp`;return h(u,`${JSON.stringify(n,null,2)}
|
|
3
|
+
`,"utf8"),S(u,r),B(e),r}s(nt,"writeStructuredRequestSidecar");function C(t){return J(m(t))}s(C,"readOutputRequest");function st(t){return J(_(t))}s(st,"readBrokerOutputRequest");function J(t){if(!l(t))return{state:"absent"};let e;try{e=a(t,"utf8")}catch(p){return{state:"invalid",error:`could not read request file: ${p.message}`}}let r;try{r=JSON.parse(e)}catch(p){return{state:"invalid",error:`request file is not valid JSON: ${p.message}`}}if(!c(r))return{state:"invalid",error:"request file top-level value must be an object"};const n=r.request_id,u=r.mode,o=r.schema;if(typeof n!="string"||n==="")return{state:"invalid",error:"request_id must be a non-empty string"};if(u!=="terminal"&&u!=="oneoff")return{state:"invalid",error:`mode must be 'terminal' or 'oneoff', got ${JSON.stringify(u)}`};if(!c(o))return{state:"invalid",error:`schema must be an object, got ${typeof o}`};const i=r.requestMessageId;return i!==void 0&&typeof i!="string"?{state:"invalid",error:`requestMessageId must be a string, got ${typeof i}`}:{state:"valid",request:{request_id:n,mode:u,schema:o,...i===void 0?{}:{requestMessageId:i}}}}s(J,"readOutputRequestAt");function ut(t){try{const e=JSON.parse(a(m(t),"utf8"));if(!c(e)||Object.hasOwn(e,"request_id"))return null;const r=e.mode,n=e.schema,u=e.requestMessageId;return r!=="terminal"&&r!=="oneoff"||!c(n)?null:{mode:r,schema:n,...typeof u=="string"?{requestMessageId:u}:{}}}catch{return null}}s(ut,"readLegacyOutputRequest");async function ot(t,e){const{validateToolArguments:r}=await import("@earendil-works/pi-ai");return r({name:"submit",description:"",parameters:t},{type:"toolCall",id:"structured-output",name:"submit",arguments:e})}s(ot,"validateStructuredResult");function it(t,e,r){const n=y(t,r),u=$(t,r);q(n,u,`${JSON.stringify(e,null,2)}
|
|
4
|
+
`,{atomic:!0});try{E(n,w(t,r))}catch(o){if(o.code!=="ENOENT")throw o}return u}s(it,"writeOutputResult");function at(t,e,r){const n=w(t,r);return q(y(t,r),n,`${JSON.stringify(e,null,2)}
|
|
5
|
+
`,{atomic:!0}),n}s(at,"writeOutputDeclined");function v(t){return`${R(t)}/output-schema-reprompts.json`}s(v,"schemaRepromptsPath");function ct(t){try{const e=JSON.parse(a(v(t),"utf8"));if(c(e)&&Number.isSafeInteger(e.count)&&Number.isFinite(e.at))return{count:e.count,at:e.at}}catch{}return{count:0,at:0}}s(ct,"readSchemaReprompts");function mt(t,e){const r=v(t);d(g(r),{recursive:!0});const n=`${r}.${b()}.tmp`;h(n,JSON.stringify(e),"utf8"),S(n,r)}s(mt,"writeSchemaReprompts");function F(t){try{f(v(t))}catch{}}s(F,"clearSchemaReprompts");function pt(t,e){const r=m(t);if(e!==void 0){const u=C(t);if(u.state!=="valid"||u.request.request_id!==e)return}try{f(r)}catch{}F(t);const n=_(t);try{f(n)}catch{}}s(pt,"removeOutputSchema");function lt(t,e,r,n,u){const o=D(t,e,r,u);d(g(o),{recursive:!0});const i=`${o}.${b()}.tmp`;return h(i,`${JSON.stringify(n,null,2)}
|
|
6
|
+
`,"utf8"),{path:o,tmpPath:i}}s(lt,"stageStructuredResult");function dt(t){return S(t.tmpPath,t.path),t.path}s(dt,"installStagedStructuredProjection");export{tt as OUTPUT_SCHEMA_CONSTRAINTS,Z as OUTPUT_SCHEMA_TRANSPORT,Y as SUBMIT_RESULT_TOOL,_ as brokerOutputSchemaPath,F as clearSchemaReprompts,dt as installStagedStructuredProjection,w as outputDeclinedPath,$ as outputResultPath,m as outputSchemaPath,rt as parseOutputSchemaValue,B as projectOutputSchema,st as readBrokerOutputRequest,ut as readLegacyOutputRequest,C as readOutputRequest,et as readOutputResult,ct as readSchemaReprompts,pt as removeOutputSchema,v as schemaRepromptsPath,k as shareRunFileWithBroker,lt as stageStructuredResult,D as structuredRequestResultPath,ot as validateStructuredResult,at as writeOutputDeclined,it as writeOutputResult,mt as writeSchemaReprompts,nt as writeStructuredRequestSidecar};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
var
|
|
1
|
+
var b=Object.defineProperty;var d=(e,t)=>b(e,"name",{value:t,configurable:!0});import{Type as i}from"typebox";import{envNodeId as g}from"../shared/env.js";import{localClient as y}from"../api/node-transport.js";import{ApiError as q}from"../api/index.js";import{readBrokerOutputRequest as h,SUBMIT_RESULT_TOOL as a}from"../core/runtime/structured-output.js";function $(e){const t=JSON.stringify(e.schema,null,2),n=`call the ${`\`${a}\` tool with \`request_id: "${e.request_id}"\``} and the JSON in \`result\``,r=`call \`${a}\` with \`decline: { reason, code }\` instead`;return`<structured-output-request>
|
|
2
2
|
${e.mode==="terminal"?`This node owes a terminal structured result. When the work is complete, submit it: ${n}. That records it and pushes your final report in one step, finishing this node. Do NOT call \`crtr push final\` yourself; it is blocked while this request is pending. If the schema cannot be honestly satisfied (blocked, unavailable, bad input), ${r} of inventing a value \u2014 that finishes the node with a declined outcome. You cannot stop or go dormant until the result is accepted or declined.`:`This node owes a one-off structured result. Submit it: ${n}. That records it and pushes an update report to your subscribers, then you continue working normally. If the schema cannot be honestly satisfied, ${r} of inventing a value. You cannot stop or go dormant until the result is accepted or declined.`}
|
|
3
3
|
|
|
4
4
|
The submitted JSON is validated against this schema; a mismatch rejects with field errors and records nothing, so fix it and call the tool again.
|
|
@@ -8,12 +8,16 @@ Required schema:
|
|
|
8
8
|
\`\`\`json
|
|
9
9
|
${t}
|
|
10
10
|
\`\`\`
|
|
11
|
-
</structured-output-request>`}d(
|
|
11
|
+
</structured-output-request>`}d($,"structuredOutputPromptBlock");function w(e){const t=e.declined,s=e.mode==="terminal"?`${t?"Decline recorded":"Result recorded"} \u2014 node finished; its engine shuts down on next stop. Nothing more to do here: STOP your turn immediately. Reply with just "Done." and nothing else.`:`${t?"One-off request declined":"One-off result recorded"} and pushed \u2014 the structured-output gate is cleared; continue working normally.`,n=t===void 0?"":`
|
|
12
12
|
- code: ${t.code} (${t.retryable?"retryable":"not retryable"})`;return`${s}
|
|
13
13
|
|
|
14
14
|
- ${t?"declined":"result"}: ${e.result_path}${n}
|
|
15
|
-
- report: ${e.report}`}d(
|
|
15
|
+
- report: ${e.report}`}d(w,"submitResultText");function O(e,t){if(typeof t!="string")return t;const s=h(e),n=s.state==="valid"?s.request.schema.type:void 0;if(n!=="object"&&n!=="array")return t;try{return JSON.parse(t)}catch{return t}}d(O,"normalizeResult");function l(e){return{content:[{type:"text",text:e}],details:void 0,isError:!0}}d(l,"errorResult");function T(e,t=s=>y({autostart:!1}).submitResult(e,s)){return{name:a,label:"Submit result",description:"Answer this node's pending structured-output request. Pass the request id from the structured-output instruction and either `result` (the JSON value matching the required schema) or `decline` when the schema cannot be honestly satisfied. The result is validated against the schema; a mismatch is rejected with field errors and nothing is recorded.",promptSnippet:"Submit the JSON result for a pending structured-output request (or decline it)",parameters:i.Object({request_id:i.String({description:"The request id named in the structured-output instruction."}),result:i.Optional(i.Unknown({description:"The JSON value matching the required schema. Omit when declining."})),decline:i.Optional(i.Object({reason:i.String({description:"One sentence on why the schema cannot be honestly satisfied."}),code:i.String({description:"A whitespace-free token naming the decline class, as the requester documented."}),retryable:i.Optional(i.Boolean({description:"Whether a retry could succeed. Default false."}))},{description:"Decline the request instead of submitting a result."}))}),defaultActive:!1,async execute(s,n){const r=typeof n.request_id=="string"?n.request_id.trim():"";if(r==="")return l("request_id is required: pass the request id named in the structured-output instruction.");const c=Object.hasOwn(n,"result")&&n.result!==void 0,o=n.decline;if(c&&o!==void 0)return l("Pass either `result` or `decline`, not both.");if(!c&&o===void 0)return l("Pass `result` (the JSON value matching the required schema), or `decline` when the schema cannot be honestly satisfied.");const f=o!==void 0?{request_id:r,decline:o.reason,code:o.code,retryable:o.retryable===!0}:{request_id:r,value:O(e,n.result)};try{const u=await t(f);return{content:[{type:"text",text:w(u)}],details:u}}catch(u){if(u instanceof q){const m=u.details,p=typeof m?.next=="string"?`
|
|
16
16
|
|
|
17
|
-
${m.next}`:"";return l(`${u.code}: ${u.message}${p}`)}return l(`The result was not submitted: ${u.message}`)}}}}d(T,"createSubmitResultTool");function S(e){const t=
|
|
17
|
+
${m.next}`:"";return l(`${u.code}: ${u.message}${p}`)}return l(`The result was not submitted: ${u.message}`)}}}}d(T,"createSubmitResultTool");function S(e){const t=g();if(t===void 0||t.trim()==="")return;e.registerTool(T(t));const s=d(n=>{const r=e.getActiveTools(),c=r.includes(a);n&&!c?e.setActiveTools([...r,a]):!n&&c&&e.setActiveTools(r.filter(o=>o!==a))},"syncTool");e.on("before_agent_start",n=>{const r=h(t);if(s(r.state!=="absent"),r.state==="invalid")return{systemPrompt:`${n.systemPrompt}
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
<structured-output-request>
|
|
20
|
+
This node owes a structured result, but its request file is unreadable to this process (${r.error}). Call the \`${a}\` tool with the request_id and required schema named in the stop-guard message; the server validates it.
|
|
21
|
+
</structured-output-request>`};if(r.state==="valid")return{systemPrompt:`${n.systemPrompt}
|
|
22
|
+
|
|
23
|
+
${$(r.request)}`}}),e.on("turn_end",()=>{s(h(t).state!=="absent")})}d(S,"registerCanvasStructuredOutput");var P=S;export{a as SUBMIT_RESULT_TOOL,T as createSubmitResultTool,P as default,S as registerCanvasStructuredOutput,$ as structuredOutputPromptBlock,w as submitResultText};
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# CI runners and the double-run gate
|
|
2
|
+
|
|
3
|
+
Crouter's suite runs on macOS (see the header of `.github/workflows/test.yml`). GitHub-hosted `macos-latest` is billed per minute and a full run is ~50 billed minutes, so the suite can run on a self-hosted Mac instead, and the push to `main` does not re-run it for a tree that already passed.
|
|
4
|
+
|
|
5
|
+
## Where the macOS jobs run: `CRTR_CI_RUNNER`
|
|
6
|
+
|
|
7
|
+
Repository variable `CRTR_CI_RUNNER` selects `runs-on` for the `build`, `build-output` and `lane` jobs of `test.yml`:
|
|
8
|
+
|
|
9
|
+
| Value | Runs on |
|
|
10
|
+
| --- | --- |
|
|
11
|
+
| unset, or anything other than `self-hosted` | GitHub-hosted `macos-latest` (billed) |
|
|
12
|
+
| `self-hosted` | runners labelled `[self-hosted, macOS, crouter]` (the Mac mini) |
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
gh variable set CRTR_CI_RUNNER --body self-hosted -R Northlight-Systems/crouter # use the mini
|
|
16
|
+
gh variable delete CRTR_CI_RUNNER -R Northlight-Systems/crouter # fall back to macos-latest
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
It takes effect on the next run; no commit is needed. **If the mini is offline, jobs queue until it returns (they do not fall back by themselves) — delete the variable.** Cancel and re-run anything already queued. The variable also changes the shard matrix: `self-hosted` uses 2 unit + 3 integration + 3 seam shards (8 lane jobs, 3 runner instances), hosted uses 3 + 6 + 6 (15).
|
|
20
|
+
|
|
21
|
+
A pull request from a fork always runs on `macos-latest`, whatever the variable says: a self-hosted runner must never execute fork code. The repository is private and forking is disabled today, so this only guards a later change.
|
|
22
|
+
|
|
23
|
+
## The Mac mini runners
|
|
24
|
+
|
|
25
|
+
`scripts/ci-runner/install.sh` installs N runner instances (default 3). The mini has 10 cores and 16 GB; a lane job runs up to 8 test files at once, each with a broker and tmux, so three concurrent jobs is the ceiling before the seam lane's timeouts become the failure.
|
|
26
|
+
|
|
27
|
+
Isolation, per instance `~/ci-runners/crouter-N/`:
|
|
28
|
+
|
|
29
|
+
- its own runner directory and `_work` checkout;
|
|
30
|
+
- its own `HOME` (`~/ci-runners/crouter-N/home`), never the account's. The suite refuses to start if `$HOME/.crouter` exists, so the account's own crouter install (`~/.crouter`, the selected runtime, `~/.local/bin/crtr`, any running `crtrd`) is neither read nor written;
|
|
31
|
+
- a minimal `PATH` (Homebrew + system) that does not include the account's crtr or npm globals;
|
|
32
|
+
- `hook-job-started.sh` / `hook-job-completed.sh` (`ACTIONS_RUNNER_HOOK_JOB_*`), which kill every process whose environment or command line points into that instance's directory (brokers, `crtrd`, test tmux servers, orphaned node processes) and wipe `home/.crouter*` and `_temp`. They match only that instance's path, so one instance's cleanup cannot touch another's job or the account's processes.
|
|
33
|
+
|
|
34
|
+
The tests bind unix sockets under per-test temp dirs and tmux servers under the per-test `HOME`, with no fixed TCP ports, so instances do not collide.
|
|
35
|
+
|
|
36
|
+
### Service
|
|
37
|
+
|
|
38
|
+
Each instance is a boot-time LaunchDaemon (survives reboot and logout, runs as `silasrhyneer`): `ai.northlight.ci-runner.crouter-1` … `-3`.
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
sudo launchctl kickstart -k system/ai.northlight.ci-runner.crouter-1 # restart one
|
|
42
|
+
sudo launchctl print system/ai.northlight.ci-runner.crouter-1 | head # state
|
|
43
|
+
tail -f ~/ci-runners/crouter-1/runner.log # log
|
|
44
|
+
gh api repos/Northlight-Systems/crouter/actions/runners --jq '.runners[]|[.name,.status,.busy]|@tsv'
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Installing and upgrading: get a registration token (`gh api -X POST repos/Northlight-Systems/crouter/actions/runners/registration-token --jq .token`), run `RUNNER_TOKEN=… scripts/ci-runner/install.sh 3` on the mini, then run the `sudo launchctl bootstrap` lines it prints. Prerequisites on the mini: `tmux`, `uv`, `git` (Homebrew); Node comes from `actions/setup-node`.
|
|
48
|
+
|
|
49
|
+
## No double run on `main`
|
|
50
|
+
|
|
51
|
+
`publish.yml` starts with a `gate` job. It reads the pushed commit's **git tree** and looks for a workflow artifact named `tested-tree-<tree>`. `test.yml`'s final `tested` job creates that artifact only after `build`, `build-output` and every `lane` shard pass, on every pull request run and on every run of Publish's own `tests` job.
|
|
52
|
+
|
|
53
|
+
- Rebase-merging a PR whose tested merge ref has the same tree as the resulting `main` commit → artifact found → `tests` is skipped and `publish` proceeds.
|
|
54
|
+
- A direct push to `main`, or a merge after `main` moved (a different tree from the one the PR run tested) → no artifact → the full suite runs exactly as before. A release commit (`chore: release …`, pushed by Publish itself) changes the tree, so a PR not updated since before the latest release runs the suite once on merge; update the branch to avoid it.
|
|
55
|
+
- The gate can only skip; if the lookup errors it runs the suite. `publish` runs only when `tests` passed, or was skipped *because* the gate found a record. A failed or cancelled suite never publishes.
|
|
56
|
+
- Branch protection is not enabled on `main` and the gate does not depend on it: the proof is the tree, not a required check. Artifacts are kept 30 days; older trees are simply re-tested.
|
|
57
|
+
|
|
58
|
+
The OIDC constraint at the top of `publish.yml` is unchanged: every publishing job is still in that file.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@north-light/cli",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.439",
|
|
4
4
|
"description": "crtr — agent runtime with memory, plugins, and marketplaces",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -137,7 +137,7 @@
|
|
|
137
137
|
},
|
|
138
138
|
"license": "UNLICENSED",
|
|
139
139
|
"dependencies": {
|
|
140
|
-
"@north-light/identity": "0.3.
|
|
140
|
+
"@north-light/identity": "0.3.439",
|
|
141
141
|
"@earendil-works/pi-agent-core": "1.1.0",
|
|
142
142
|
"@earendil-works/pi-ai": "1.1.0",
|
|
143
143
|
"@earendil-works/pi-coding-agent": "1.1.0",
|
package/runtime.lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@north-light/cli",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.439",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@north-light/cli",
|
|
9
|
-
"version": "0.3.
|
|
9
|
+
"version": "0.3.439",
|
|
10
10
|
"hasInstallScript": true,
|
|
11
11
|
"license": "UNLICENSED",
|
|
12
12
|
"workspaces": [
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
"@earendil-works/pi-ai": "1.1.0",
|
|
19
19
|
"@earendil-works/pi-coding-agent": "1.1.0",
|
|
20
20
|
"@earendil-works/pi-tui": "1.1.0",
|
|
21
|
-
"@north-light/identity": "0.3.
|
|
21
|
+
"@north-light/identity": "0.3.439",
|
|
22
22
|
"cron-parser": "^5.6.0",
|
|
23
23
|
"esbuild": "^0.27.7",
|
|
24
24
|
"jose": "^6.2.12",
|
|
@@ -12095,17 +12095,17 @@
|
|
|
12095
12095
|
},
|
|
12096
12096
|
"packages/crouter-api": {
|
|
12097
12097
|
"name": "@north-light/api",
|
|
12098
|
-
"version": "0.3.
|
|
12098
|
+
"version": "0.3.439",
|
|
12099
12099
|
"license": "UNLICENSED"
|
|
12100
12100
|
},
|
|
12101
12101
|
"packages/crouter-env-docker": {
|
|
12102
12102
|
"name": "@north-light/env-docker",
|
|
12103
|
-
"version": "0.3.
|
|
12103
|
+
"version": "0.3.439",
|
|
12104
12104
|
"license": "UNLICENSED"
|
|
12105
12105
|
},
|
|
12106
12106
|
"packages/crouter-identity": {
|
|
12107
12107
|
"name": "@north-light/identity",
|
|
12108
|
-
"version": "0.3.
|
|
12108
|
+
"version": "0.3.439",
|
|
12109
12109
|
"license": "UNLICENSED",
|
|
12110
12110
|
"dependencies": {
|
|
12111
12111
|
"jose": "^6.2.1"
|
|
@@ -12113,20 +12113,20 @@
|
|
|
12113
12113
|
},
|
|
12114
12114
|
"packages/crouter-plugin": {
|
|
12115
12115
|
"name": "@north-light/plugin",
|
|
12116
|
-
"version": "0.3.
|
|
12116
|
+
"version": "0.3.439",
|
|
12117
12117
|
"license": "UNLICENSED",
|
|
12118
12118
|
"dependencies": {
|
|
12119
|
-
"@north-light/api": "0.3.
|
|
12120
|
-
"@north-light/identity": "0.3.
|
|
12119
|
+
"@north-light/api": "0.3.439",
|
|
12120
|
+
"@north-light/identity": "0.3.439"
|
|
12121
12121
|
}
|
|
12122
12122
|
},
|
|
12123
12123
|
"packages/crouter-sdk": {
|
|
12124
12124
|
"name": "@north-light/sdk",
|
|
12125
|
-
"version": "0.3.
|
|
12125
|
+
"version": "0.3.439",
|
|
12126
12126
|
"license": "UNLICENSED",
|
|
12127
12127
|
"dependencies": {
|
|
12128
|
-
"@north-light/api": "0.3.
|
|
12129
|
-
"@north-light/identity": "0.3.
|
|
12128
|
+
"@north-light/api": "0.3.439",
|
|
12129
|
+
"@north-light/identity": "0.3.439",
|
|
12130
12130
|
"jose": "^6.2.1"
|
|
12131
12131
|
},
|
|
12132
12132
|
"bin": {
|