@north-light/cli 0.3.419 → 0.3.421

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.
Files changed (59) hide show
  1. package/README.md +3 -2
  2. package/dist/api/dto/docs.d.ts +7 -0
  3. package/dist/api/dto/nodes.d.ts +1 -0
  4. package/dist/builtin-memory/crouter-sdk/README.md +3 -3
  5. package/dist/builtin-memory/crouter-sdk/getting-started.md +2 -2
  6. package/dist/builtin-memory/crouter-sdk/guides/README.md +1 -1
  7. package/dist/builtin-memory/crouter-sdk/guides/apps-on-crouter-cloud.md +222 -3
  8. package/dist/clients/attach/input/controller.d.ts +1 -0
  9. package/dist/clients/attach/input/controller.js +2 -2
  10. package/dist/clients/attach/render/chat-view.d.ts +44 -0
  11. package/dist/clients/attach/render/chat-view.js +3 -3
  12. package/dist/clients/attach/session/frames.d.ts +2 -0
  13. package/dist/clients/attach/session/frames.js +1 -1
  14. package/dist/clients/attach/session/input-wiring.d.ts +3 -0
  15. package/dist/clients/attach/session/input-wiring.js +1 -1
  16. package/dist/clients/attach/slash/dispatch.d.ts +4 -0
  17. package/dist/clients/attach/slash/dispatch.js +2 -2
  18. package/dist/clients/attach/viewer.js +567 -563
  19. package/dist/core/command-plugins/bundle.d.ts +3 -0
  20. package/dist/core/command-plugins/bundle.js +1 -1
  21. package/dist/core/config.js +1 -1
  22. package/dist/core/fs-utils.d.ts +7 -0
  23. package/dist/core/fs-utils.js +3 -3
  24. package/dist/core/graph/package-docs.d.ts +4 -1
  25. package/dist/core/graph/package-docs.js +5 -5
  26. package/dist/core/keybindings/catalog.d.ts +1 -1
  27. package/dist/core/keybindings/catalog.js +1 -1
  28. package/dist/core/manifest.d.ts +15 -0
  29. package/dist/core/manifest.js +1 -1
  30. package/dist/core/plugin-quarantine.d.ts +16 -0
  31. package/dist/core/plugin-quarantine.js +1 -0
  32. package/dist/core/providers/bundles.js +2 -2
  33. package/dist/core/runtime/broker/rebind.d.ts +5 -0
  34. package/dist/core/runtime/broker/rebind.js +1 -1
  35. package/dist/core/runtime/broker/turn-replies.d.ts +33 -0
  36. package/dist/core/runtime/broker/turn-replies.js +1 -0
  37. package/dist/core/runtime/broker-protocol.d.ts +12 -1
  38. package/dist/core/runtime/broker.js +1 -1
  39. package/dist/core/runtime/node-read.js +3 -3
  40. package/dist/core/runtime/tmux-bindings.js +1 -1
  41. package/dist/core/runtime/turn-reply-summary.d.ts +6 -0
  42. package/dist/core/runtime/turn-reply-summary.js +11 -0
  43. package/dist/core/user-settings.d.ts +15 -0
  44. package/dist/core/user-settings.js +1 -1
  45. package/dist/daemon/api/handlers/nodes.js +1 -1
  46. package/dist/daemon/api/vendor-signin.d.ts +3 -0
  47. package/dist/daemon/api/vendor-signin.js +162 -47
  48. package/dist/daemon/crtrd.js +6 -5
  49. package/dist/shared/turn-replies.d.ts +27 -0
  50. package/dist/shared/turn-replies.js +5 -0
  51. package/dist/types.d.ts +6 -0
  52. package/dist/types.js +1 -1
  53. package/docs/sdk/README.md +3 -3
  54. package/docs/sdk/getting-started.md +2 -2
  55. package/docs/sdk/guides/README.md +1 -1
  56. package/docs/sdk/guides/apps-on-crouter-cloud.md +218 -1
  57. package/package.json +2 -2
  58. package/packages/crouter-identity/package.json +1 -1
  59. package/runtime.lock.json +12 -12
@@ -1,12 +1,13 @@
1
- var Se=Object.defineProperty;var s=(t,o)=>Se(t,"name",{value:o,configurable:!0});import{writeFileSync as _e,readFileSync as ke,rmSync as be,mkdirSync as Y,existsSync as b,chmodSync as Ee}from"node:fs";import{createServer as X}from"node:http";import{dirname as Re}from"node:path";import{envCrtrdTcp as Ie}from"../shared/env.js";import{serveCanvasConversationStore as Pe}from"../core/canvas/sessions.js";import{pollRunObservers as xe}from"./runs/observer.js";import{randomBytes as De}from"node:crypto";import{apiSocketPath as Te,crtrHome as Ce}from"../core/canvas/paths.js";import{activeAppUsers as Le}from"../core/spaces/users.js";import{ensureAppSpace as Ae,ensureUserFiles as Ne}from"../core/spaces/space.js";import{LAUNCHER_SOCKET as R}from"../launcher/spec.js";import{launcherRequest as x}from"../launcher/client.js";import{installLauncherEventSink as $e}from"./launcher-events.js";import{initializeEmptyLayout as Me,layoutNeedsMigration as Ge,socketDir as Z}from"../core/layout.js";import{migrateLooseKeys as Oe}from"../core/layout-migrate/loose-keys.js";import{getNode as Fe,listNodesForSupervision as He,setFrozen as Be}from"../core/canvas/index.js";import{createApiServer as je}from"./api/server.js";import{createAppListener as qe}from"./api/app-listener.js";import{installLocalTerminalGrant as ze}from"../core/grants/local.js";import{onRunEventAppended as We}from"../core/runs/events.js";import{runNotices as Ue}from"./run-notices.js";import{createGrantSync as Ke,GRANT_SYNC_INTERVAL_S as Je}from"../core/grants/sync.js";import{runRemovalCascades as Ve}from"../core/grants/remove.js";import{sweepSandboxes as Qe}from"../core/grants/on-sync.js";import{rebuildCronExecutions as Ye}from"./cron/run-socket.js";import{hasSyncedGrants as Xe}from"../core/grants/mirror.js";import{readConfig as Ze}from"../core/config.js";import{reconcileUndeliveredTickets as er}from"./human/sweep.js";import{resetHandedOffDeliveries as rr}from"../core/canvas/deliveries.js";import{resetHandedOffDaemonActions as tr}from"../core/canvas/daemon-actions.js";import{sweepReviews as or}from"./review/sweep.js";import{pidfilePath as G}from"./pidfile.js";import{rendererWarning as nr}from"../core/runtime/package-health.js";import{DEFAULT_INTERVAL_MS as ir}from"./supervise-cadence.js";import{createResumeDetector as sr}from"./resume-detector.js";import{BrokerSupervisionReconciler as ar}from"./reconcilers/broker-supervision.js";import{ControllerDeathReconciler as cr}from"./reconcilers/controller-death.js";import{NodeLifecycleReconciler as lr}from"./reconcilers/node-lifecycle/tick.js";import{NodeDeadlineReconciler as dr}from"./reconcilers/node-deadline.js";import{CronLaneReconciler as pr}from"./reconcilers/cron-lane.js";import{DeliveryLaneReconciler as mr}from"./reconcilers/delivery-lane.js";import{ActionLaneReconciler as fr}from"./reconcilers/action-lane.js";import{endLiveExecution as ur,migrateLegacyExecutionIdentities as hr}from"../core/canvas/executions.js";import{revokePriorEpochNodeTokens as vr}from"../core/canvas/node-tokens.js";import{PendingReviewSubmitReconciler as wr}from"./reconcilers/pending-review-submit.js";import{StorageMaintenanceReconciler as gr}from"./reconcilers/storage-maintenance.js";import{StorageDueWorkReconciler as yr}from"./reconcilers/storage-due-work.js";import{BashDeadlineReconciler as Sr}from"./reconcilers/bash-deadline.js";import{FaultRetryBackstopReconciler as _r}from"./reconcilers/fault-retry-backstop.js";import{StructuredRequestReconciler as kr}from"./reconcilers/structured-request-lane.js";import{reconcileStructuredRequestSidecars as br}from"./structured-requests/boot.js";import{captureLivenessSnapshot as ee,capturePidCommand as Er,captureTeardownSnapshot as re,isPidAlive as Rr,isAnyPidAlive as te,killProcessTreePids as oe}from"../core/canvas/pid.js";import{bindFleet as Ir,boundFleet as Pr}from"../core/runtime/fleet.js";import{bindDaemonControl as xr,clearDaemonControl as Dr}from"./control.js";import{execDaemon as Tr,spawnDaemon as Cr}from"./manage.js";import{reconcileAndMigrateBoot as Lr}from"../core/canvas/boot.js";import{resolveInstallId as Ar}from"../core/canvas/install-id.js";import{setInstallId as Nr}from"../core/runtime/nodes.js";import{claimDaemonOwnership as $r,releaseDaemonOwnership as ne}from"../core/canvas/daemon-owner.js";import{migrate as Mr,openDb as ie,withCanvasWrite as Gr,bindDaemonCanvasOpenHook as Or,clearDaemonCanvasOpenHook as se}from"../core/canvas/db.js";import{brokerSignalBus as O}from"./broker-signals/bus.js";import{brokerSignalStreams as Fr}from"./api/handlers/broker-signals.js";import{reconcilePackageDocuments as Hr}from"../core/graph/package-docs.js";import{startRepoSync as Br}from"../core/graph/repo-sync/index.js";import{prewarmRecentRecipes as jr,reapStaleSpares as qr}from"../core/runtime/warm-pool.js";import{DaemonFleet as zr}from"./fleet.js";import{BOOT_MIGRATIONS as Wr}from"./boot-migrations.js";import{applyJoinedOwner as Ur}from"./joined-owner.js";import{emitEvent as u}from"../core/events/emit.js";import{operationIdContext as _}from"../core/events/operation-id.js";import{bindDaemonEventSource as Kr}from"../core/events/source.js";import{activateCanvasImport as Jr,MigrationBlockedError as Vr,runForcedCanvasImport as Qr}from"../migrations/activation.js";import{convertLegacyProviderStore as Yr,convertLegacyScopedStore as Xr}from"../core/scoped-state/migrate.js";import{userScopeRoot as Zr}from"../core/scope.js";import{secretsDir as et}from"../core/layout.js";function F(t,o){return t.includes(` --canvas-home ${o} `)}s(F,"brokerCommandDeclaresCanvasHome");function H(t){return/ --epoch (\S+) /.exec(t)?.[1]??null}s(H,"brokerCommandEpoch");const rt=5e3,ae=150;async function tt(t){const o=ee();if(o===null)throw u({level:"error",event:"daemon.census.snapshot_failed",fields:{invariant:"prior-epoch brokers are reaped before readiness"}}),new Error("prior-epoch broker census could not capture the process table");const a=Ce(),m=[];for(const[i,p]of o.processes){const g=p.command;if(p.zombie||!g.includes("broker-cli.js")||!F(g,a)||H(g)===t)continue;const S=g.trim().split(/\s+/).at(-1)??"";S===""||S.endsWith("broker-cli.js")||m.push({pid:i,nodeId:S})}let f=0;const n=[];for(const i of m){const p=Er(i.pid);if(p===null||!p.includes("broker-cli.js")||!F(p,a)||H(p)===t||p.trim().split(/\s+/).at(-1)!==i.nodeId)continue;const g=re(i.pid,null);if(!g.reused){try{process.platform==="linux"&&b(R)?await x({operation:"stop",pid:i.pid}):process.kill(i.pid,"SIGTERM")}catch{continue}f++,n.push({pid:i.pid,snap:g})}}const w=Date.now()+rt;for(;Date.now()<w&&n.some(i=>te(i.snap.tree.length>0?i.snap.tree:[i.pid]));)await new Promise(i=>setTimeout(i,ae));let v=0;for(const i of n){const p=i.snap.tree.length>0?i.snap.tree:[i.pid];te(p)&&(process.platform==="linux"&&b(R)?await x({operation:"stop",pid:i.pid}):oe(p,"SIGKILL",i.snap.identities),v++)}v>0&&await new Promise(i=>setTimeout(i,ae));const l=ee();if(l===null)throw u({level:"error",event:"daemon.census.snapshot_failed",fields:{invariant:"prior-epoch brokers are reaped before readiness",stage:"final"}}),new Error("prior-epoch broker census could not capture the final process table");const d=[];for(const[i,p]of l.processes){const g=p.command;p.zombie||!g.includes("broker-cli.js")||F(g,a)&&H(g)!==t&&d.push(i)}if(d.length>0)throw new Error(`prior-epoch broker census could not prove termination of pid(s): ${d.join(", ")}`);u({level:"info",event:"daemon.census.completed",fields:{prior_epoch_brokers:m.length,sigtermed:f,sigkilled:v}})}s(tt,"reapPriorEpochBrokers");const ot=5e3;async function nt(t){const o=Array.from(t.entries());if(o.length===0)return;for(const[n]of o){const w=Fe(n)?.status;(w==="active"||w==="idle")&&Be(n,new Date().toISOString())}const a=o.map(([,n])=>({pid:n.pid,snap:re(n.pid,null),exited:n.handle.exited}));for(const n of a)if(!n.snap.reused)try{process.platform==="linux"&&b(R)?await x({operation:"stop",pid:n.pid}):process.kill(n.pid,"SIGTERM")}catch{}const m=new Promise(n=>{setTimeout(n,ot).unref()});await Promise.allSettled(a.map(n=>Promise.race([n.exited.then(()=>{}),m])));const f=[];for(const n of a)Rr(n.pid)&&(process.platform==="linux"&&b(R)?await x({operation:"stop",pid:n.pid}):oe(n.snap.tree.length>0?n.snap.tree:[n.pid],"SIGKILL",n.snap.identities),f.push(n));await Promise.all(f.map(n=>n.exited));for(const[n]of o)ur(n,{reason:"daemon-teardown"});u({level:"info",event:"daemon.fleet_teardown.completed",fields:{brokers:a.length,sigkilled:f.length}})}s(nt,"teardownFleet");function ce(){Y(Re(G()),{recursive:!0}),_e(G(),String(process.pid),"utf8")}s(ce,"writePidfile");function le(t){return typeof t=="object"&&t!==null&&t.code==="ENOENT"}s(le,"isMissingPathError");function de(t){const o=G();let a;try{a=ke(o,"utf8")}catch(m){if(le(m))return;throw m}if(a.trim()!==String(t))throw new Error(`daemon readiness mirror does not name this invocation (expected pid ${t})`);try{be(o)}catch(m){if(le(m))return;throw m}}s(de,"removeOwnedPidfileMirror");const it={acceptsDetachedWork:s(()=>!0,"acceptsDetachedWork"),registerDetached:s(t=>{t.catch(()=>{})},"registerDetached")},B=new ar,st=new cr,at=new lr,ct=new dr,lt=new pr,dt=new mr,pt=new fr,mt=new wr,ft=new gr,ut=new yr,ht=new Sr,vt=new _r,wt=new kr;async function gt(t=Date.now(),o=it){const a=Pr(),m=await B.runFleetPreamble(a);let f,n;try{const v=He();f=v.filter(l=>l.status==="active"||l.status==="idle"),n=v.filter(l=>l.status==="done"||l.status==="canceled"||l.status==="dead")}catch(v){u({level:"error",event:"canvas.node_list.failed",error:v});return}await B.run(t,{rows:f,fleet:a,lifecycle:o}),st.run(t,{rows:f});const w=await at.run(t,{rows:[...f,...n],fleet:a});ct.run(t),wt.run(t),lt.run(t,{lifecycle:o}),dt.run(t,{lifecycle:o}),pt.run(t,{lifecycle:o}),mt.run(t,{lifecycle:o}),ft.run(t,{lifecycle:o,fleet:a}),ut.run(t,{lifecycle:o}),xe(),vt.run(t,{fleet:a,lifecycle:o}),await ht.run(t,{rows:f}),B.warnIfSaturated({fleet:a,lifecycle:o,thresholds:m,frozen:w})}s(gt,"superviseTick");async function yt(t){Y(Z(),{recursive:!0,mode:493}),process.platform==="linux"&&Ee(Z(),493);const o=Te();if(b(o))throw new Error(`layout refusal cannot bind occupied socket: ${o}`);const a=X((l,d)=>{if(l.method==="GET"&&l.url==="/healthz"){d.writeHead(200,{"content-type":"application/json; charset=utf-8"}),d.end(JSON.stringify({ok:!1,daemon_up:!0,store_accessible:!1,brokers_reconciled:!1,startup_phase:"blocked",startup_blocked:"layout_migration_required"}));return}d.writeHead(503,{"content-type":"application/json; charset=utf-8"}),d.end(JSON.stringify({error:{code:"layout_migration_required",message:"run crtr sys migrate while the runtime is offline",type:"server_error",origin:"runtime",retryable:!1}}))});await new Promise((l,d)=>{a.once("error",d),a.listen(o,()=>{a.off("error",d),l()})});let m;const f=t.tcp??Ie();if(f){const l=/^(.*):(\d+)$/.exec(f);if(!l||Number(l[2])>65535)throw new Error(`invalid refusal TCP address: ${f}`);const d=X(a.listeners("request")[0]);await new Promise((i,p)=>{d.once("error",p),d.listen(Number(l[2]),l[1],()=>{d.off("error",p),i()})}),m=d}ce();let n;const w=s(()=>(n??=(async()=>{a.closeAllConnections(),m?.closeAllConnections(),await Promise.all([a,m].filter(l=>l!==void 0).map(l=>new Promise((d,i)=>l.close(p=>p?i(p):d())))),de(process.pid),process.off("SIGINT",v),process.off("SIGTERM",v)})(),n),"cleanup"),v=s(()=>{w().then(()=>process.exit(0),l=>{process.stderr.write(`[crtrd] layout refusal cleanup failed: ${String(l)}
2
- `),process.exit(1)})},"onSignal");return process.on("SIGINT",v),process.on("SIGTERM",v),w}s(yt,"serveLayoutRefusal");async function St(t={}){if(Ge())return yt(t);Me();const o=$r(De(16).toString("hex"));if(!o.claimed){if(o.reason==="incumbent_alive")process.stderr.write(`[crtrd] already running (pid ${o.incumbentPid??"?"})
1
+ var Se=Object.defineProperty;var s=(t,o)=>Se(t,"name",{value:o,configurable:!0});import{writeFileSync as _e,readFileSync as ke,rmSync as be,mkdirSync as Y,existsSync as b,chmodSync as Ee}from"node:fs";import{createServer as X}from"node:http";import{dirname as Re}from"node:path";import{envCrtrdTcp as Ie}from"../shared/env.js";import{serveCanvasConversationStore as Pe}from"../core/canvas/sessions.js";import{pollRunObservers as xe}from"./runs/observer.js";import{randomBytes as Te}from"node:crypto";import{apiSocketPath as De,crtrHome as Ce}from"../core/canvas/paths.js";import{activeAppUsers as Le}from"../core/spaces/users.js";import{ensureAppSpace as $e,ensureUserFiles as Ae}from"../core/spaces/space.js";import{LAUNCHER_SOCKET as R}from"../launcher/spec.js";import{launcherRequest as x}from"../launcher/client.js";import{installLauncherEventSink as Ne}from"./launcher-events.js";import{initializeEmptyLayout as Me,layoutNeedsMigration as Ge,socketDir as Z}from"../core/layout.js";import{migrateLooseKeys as Oe}from"../core/layout-migrate/loose-keys.js";import{getNode as Fe,listNodesForSupervision as He,setFrozen as Be}from"../core/canvas/index.js";import{createApiServer as qe}from"./api/server.js";import{createAppListener as je}from"./api/app-listener.js";import{installLocalTerminalGrant as ze}from"../core/grants/local.js";import{onRunEventAppended as We}from"../core/runs/events.js";import{runNotices as Ue}from"./run-notices.js";import{createGrantSync as Ke,GRANT_SYNC_INTERVAL_S as Je}from"../core/grants/sync.js";import{runRemovalCascades as Ve}from"../core/grants/remove.js";import{sweepSandboxes as Qe}from"../core/grants/on-sync.js";import{rebuildCronExecutions as Ye}from"./cron/run-socket.js";import{hasSyncedGrants as Xe}from"../core/grants/mirror.js";import{readConfig as Ze}from"../core/config.js";import{reconcileUndeliveredTickets as er}from"./human/sweep.js";import{resetHandedOffDeliveries as rr}from"../core/canvas/deliveries.js";import{resetHandedOffDaemonActions as tr}from"../core/canvas/daemon-actions.js";import{sweepReviews as or}from"./review/sweep.js";import{pidfilePath as G}from"./pidfile.js";import{rendererWarning as nr}from"../core/runtime/package-health.js";import{DEFAULT_INTERVAL_MS as ir}from"./supervise-cadence.js";import{createResumeDetector as sr}from"./resume-detector.js";import{BrokerSupervisionReconciler as ar}from"./reconcilers/broker-supervision.js";import{ControllerDeathReconciler as cr}from"./reconcilers/controller-death.js";import{NodeLifecycleReconciler as lr}from"./reconcilers/node-lifecycle/tick.js";import{NodeDeadlineReconciler as dr}from"./reconcilers/node-deadline.js";import{CronLaneReconciler as pr}from"./reconcilers/cron-lane.js";import{DeliveryLaneReconciler as mr}from"./reconcilers/delivery-lane.js";import{ActionLaneReconciler as fr}from"./reconcilers/action-lane.js";import{endLiveExecution as ur,migrateLegacyExecutionIdentities as hr}from"../core/canvas/executions.js";import{revokePriorEpochNodeTokens as vr}from"../core/canvas/node-tokens.js";import{PendingReviewSubmitReconciler as wr}from"./reconcilers/pending-review-submit.js";import{StorageMaintenanceReconciler as gr}from"./reconcilers/storage-maintenance.js";import{StorageDueWorkReconciler as yr}from"./reconcilers/storage-due-work.js";import{BashDeadlineReconciler as Sr}from"./reconcilers/bash-deadline.js";import{FaultRetryBackstopReconciler as _r}from"./reconcilers/fault-retry-backstop.js";import{StructuredRequestReconciler as kr}from"./reconcilers/structured-request-lane.js";import{reconcileStructuredRequestSidecars as br}from"./structured-requests/boot.js";import{captureLivenessSnapshot as ee,capturePidCommand as Er,captureTeardownSnapshot as re,isPidAlive as Rr,isAnyPidAlive as te,killProcessTreePids as oe}from"../core/canvas/pid.js";import{bindFleet as Ir,boundFleet as Pr}from"../core/runtime/fleet.js";import{bindDaemonControl as xr,clearDaemonControl as Tr}from"./control.js";import{execDaemon as Dr,spawnDaemon as Cr}from"./manage.js";import{reconcileAndMigrateBoot as Lr}from"../core/canvas/boot.js";import{resolveInstallId as $r}from"../core/canvas/install-id.js";import{setInstallId as Ar}from"../core/runtime/nodes.js";import{claimDaemonOwnership as Nr,releaseDaemonOwnership as ne}from"../core/canvas/daemon-owner.js";import{migrate as Mr,openDb as ie,withCanvasWrite as Gr,bindDaemonCanvasOpenHook as Or,clearDaemonCanvasOpenHook as se}from"../core/canvas/db.js";import{brokerSignalBus as O}from"./broker-signals/bus.js";import{brokerSignalStreams as Fr}from"./api/handlers/broker-signals.js";import{reconcilePackageDocuments as Hr}from"../core/graph/package-docs.js";import{startRepoSync as Br}from"../core/graph/repo-sync/index.js";import{prewarmRecentRecipes as qr,reapStaleSpares as jr}from"../core/runtime/warm-pool.js";import{DaemonFleet as zr}from"./fleet.js";import{BOOT_MIGRATIONS as Wr}from"./boot-migrations.js";import{applyJoinedOwner as Ur}from"./joined-owner.js";import{emitEvent as u}from"../core/events/emit.js";import{operationIdContext as _}from"../core/events/operation-id.js";import{bindDaemonEventSource as Kr}from"../core/events/source.js";import{activateCanvasImport as Jr,MigrationBlockedError as Vr,runForcedCanvasImport as Qr}from"../migrations/activation.js";import{convertLegacyProviderStore as Yr,convertLegacyScopedStore as Xr}from"../core/scoped-state/migrate.js";import{userScopeRoot as Zr}from"../core/scope.js";import{secretsDir as et}from"../core/layout.js";function F(t,o){return t.includes(` --canvas-home ${o} `)}s(F,"brokerCommandDeclaresCanvasHome");function H(t){return/ --epoch (\S+) /.exec(t)?.[1]??null}s(H,"brokerCommandEpoch");const rt=5e3,ae=150;async function tt(t){const o=ee();if(o===null)throw u({level:"error",event:"daemon.census.snapshot_failed",fields:{invariant:"prior-epoch brokers are reaped before readiness"}}),new Error("prior-epoch broker census could not capture the process table");const a=Ce(),m=[];for(const[i,p]of o.processes){const g=p.command;if(p.zombie||!g.includes("broker-cli.js")||!F(g,a)||H(g)===t)continue;const S=g.trim().split(/\s+/).at(-1)??"";S===""||S.endsWith("broker-cli.js")||m.push({pid:i,nodeId:S})}let f=0;const n=[];for(const i of m){const p=Er(i.pid);if(p===null||!p.includes("broker-cli.js")||!F(p,a)||H(p)===t||p.trim().split(/\s+/).at(-1)!==i.nodeId)continue;const g=re(i.pid,null);if(!g.reused){try{process.platform==="linux"&&b(R)?await x({operation:"stop",pid:i.pid}):process.kill(i.pid,"SIGTERM")}catch{continue}f++,n.push({pid:i.pid,snap:g})}}const w=Date.now()+rt;for(;Date.now()<w&&n.some(i=>te(i.snap.tree.length>0?i.snap.tree:[i.pid]));)await new Promise(i=>setTimeout(i,ae));let v=0;for(const i of n){const p=i.snap.tree.length>0?i.snap.tree:[i.pid];te(p)&&(process.platform==="linux"&&b(R)?await x({operation:"stop",pid:i.pid}):oe(p,"SIGKILL",i.snap.identities),v++)}v>0&&await new Promise(i=>setTimeout(i,ae));const l=ee();if(l===null)throw u({level:"error",event:"daemon.census.snapshot_failed",fields:{invariant:"prior-epoch brokers are reaped before readiness",stage:"final"}}),new Error("prior-epoch broker census could not capture the final process table");const d=[];for(const[i,p]of l.processes){const g=p.command;p.zombie||!g.includes("broker-cli.js")||F(g,a)&&H(g)!==t&&d.push(i)}if(d.length>0)throw new Error(`prior-epoch broker census could not prove termination of pid(s): ${d.join(", ")}`);u({level:"info",event:"daemon.census.completed",fields:{prior_epoch_brokers:m.length,sigtermed:f,sigkilled:v}})}s(tt,"reapPriorEpochBrokers");const ot=5e3;async function nt(t){const o=Array.from(t.entries());if(o.length===0)return;for(const[n]of o){const w=Fe(n)?.status;(w==="active"||w==="idle")&&Be(n,new Date().toISOString())}const a=o.map(([,n])=>({pid:n.pid,snap:re(n.pid,null),exited:n.handle.exited}));for(const n of a)if(!n.snap.reused)try{process.platform==="linux"&&b(R)?await x({operation:"stop",pid:n.pid}):process.kill(n.pid,"SIGTERM")}catch{}const m=new Promise(n=>{setTimeout(n,ot).unref()});await Promise.allSettled(a.map(n=>Promise.race([n.exited.then(()=>{}),m])));const f=[];for(const n of a)Rr(n.pid)&&(process.platform==="linux"&&b(R)?await x({operation:"stop",pid:n.pid}):oe(n.snap.tree.length>0?n.snap.tree:[n.pid],"SIGKILL",n.snap.identities),f.push(n));await Promise.all(f.map(n=>n.exited));for(const[n]of o)ur(n,{reason:"daemon-teardown"});u({level:"info",event:"daemon.fleet_teardown.completed",fields:{brokers:a.length,sigkilled:f.length}})}s(nt,"teardownFleet");function ce(){Y(Re(G()),{recursive:!0}),_e(G(),String(process.pid),"utf8")}s(ce,"writePidfile");function le(t){return typeof t=="object"&&t!==null&&t.code==="ENOENT"}s(le,"isMissingPathError");function de(t){const o=G();let a;try{a=ke(o,"utf8")}catch(m){if(le(m))return;throw m}if(a.trim()!==String(t))throw new Error(`daemon readiness mirror does not name this invocation (expected pid ${t})`);try{be(o)}catch(m){if(le(m))return;throw m}}s(de,"removeOwnedPidfileMirror");const it={acceptsDetachedWork:s(()=>!0,"acceptsDetachedWork"),registerDetached:s(t=>{t.catch(()=>{})},"registerDetached")},B=new ar,st=new cr,at=new lr,ct=new dr,lt=new pr,dt=new mr,pt=new fr,mt=new wr,ft=new gr,ut=new yr,ht=new Sr,vt=new _r,wt=new kr;async function gt(t=Date.now(),o=it){const a=Pr(),m=await B.runFleetPreamble(a);let f,n;try{const v=He();f=v.filter(l=>l.status==="active"||l.status==="idle"),n=v.filter(l=>l.status==="done"||l.status==="canceled"||l.status==="dead")}catch(v){u({level:"error",event:"canvas.node_list.failed",error:v});return}await B.run(t,{rows:f,fleet:a,lifecycle:o}),st.run(t,{rows:f});const w=await at.run(t,{rows:[...f,...n],fleet:a});ct.run(t),wt.run(t),lt.run(t,{lifecycle:o}),dt.run(t,{lifecycle:o}),pt.run(t,{lifecycle:o}),mt.run(t,{lifecycle:o}),ft.run(t,{lifecycle:o,fleet:a}),ut.run(t,{lifecycle:o}),xe(),vt.run(t,{fleet:a,lifecycle:o}),await ht.run(t,{rows:f}),B.warnIfSaturated({fleet:a,lifecycle:o,thresholds:m,frozen:w})}s(gt,"superviseTick");async function yt(t){Y(Z(),{recursive:!0,mode:493}),process.platform==="linux"&&Ee(Z(),493);const o=De();if(b(o))throw new Error(`layout refusal cannot bind occupied socket: ${o}`);const a=X((l,d)=>{if(l.method==="GET"&&l.url==="/healthz"){d.writeHead(200,{"content-type":"application/json; charset=utf-8"}),d.end(JSON.stringify({ok:!1,daemon_up:!0,store_accessible:!1,brokers_reconciled:!1,startup_phase:"blocked",startup_blocked:"layout_migration_required"}));return}d.writeHead(503,{"content-type":"application/json; charset=utf-8"}),d.end(JSON.stringify({error:{code:"layout_migration_required",message:"run crtr sys migrate while the runtime is offline",type:"server_error",origin:"runtime",retryable:!1}}))});await new Promise((l,d)=>{a.once("error",d),a.listen(o,()=>{a.off("error",d),l()})});let m;const f=t.tcp??Ie();if(f){const l=/^(.*):(\d+)$/.exec(f);if(!l||Number(l[2])>65535)throw new Error(`invalid refusal TCP address: ${f}`);const d=X(a.listeners("request")[0]);await new Promise((i,p)=>{d.once("error",p),d.listen(Number(l[2]),l[1],()=>{d.off("error",p),i()})}),m=d}ce();let n;const w=s(()=>(n??=(async()=>{a.closeAllConnections(),m?.closeAllConnections(),await Promise.all([a,m].filter(l=>l!==void 0).map(l=>new Promise((d,i)=>l.close(p=>p?i(p):d())))),de(process.pid),process.off("SIGINT",v),process.off("SIGTERM",v)})(),n),"cleanup"),v=s(()=>{w().then(()=>process.exit(0),l=>{process.stderr.write(`[crtrd] layout refusal cleanup failed: ${String(l)}
2
+ `),process.exit(1)})},"onSignal");return process.on("SIGINT",v),process.on("SIGTERM",v),w}s(yt,"serveLayoutRefusal");async function St(t={}){if(Ge())return yt(t);Me();const o=Nr(Te(16).toString("hex"));if(!o.claimed){if(o.reason==="incumbent_alive")process.stderr.write(`[crtrd] already running (pid ${o.incumbentPid??"?"})
3
3
  `);else{const r=[o.incumbentPid!==void 0?`pid ${o.incumbentPid}`:void 0,o.verdict!==void 0?`verdict ${o.verdict}`:void 0].filter(c=>c!==void 0),e=r.length>0?` (${r.join(", ")})`:"";process.stderr.write(`[crtrd] claim lost: ${o.reason}${e}
4
4
  `),o.reason==="own_identity_indeterminate"&&process.stderr.write("[crtrd] cannot read its own process identity. crouter needs /proc on Linux, or the `ps` command elsewhere (Debian/Ubuntu: apt-get install procps).\n"),(o.reason==="own_identity_indeterminate"||o.reason==="incumbent_malformed")&&(process.exitCode=1)}return async()=>{}}const a=o.claim;let m=null;try{if(await tt(a.token),Xr({kind:"user",root:Zr()}),process.platform==="darwin"){const{readConfig:e,updateConfig:c}=await import("../core/config.js");if(e("user").paths?.user_files===void 0){const{homedir:y}=await import("node:os");c("user",Q=>{Q.paths??={},Q.paths.user_files??=y()})}}await Yr(et());for(const e of Oe())process.stderr.write(`[crtrd] ${e}
5
5
  `);Mr(ie()),Or(e=>O.install(e));const r=Gr(e=>Hr(e));for(const e of r.conflicts)process.stderr.write(`[crtrd] plugin documents not reconciled: ${e.message}
6
6
  `);for(const e of r.invalid)process.stderr.write(`[crtrd] package file is not a document: ${e.path}: ${e.error}
7
- `);m=Ur(),process.platform==="linux"&&b(R)&&($e(),await x({operation:"users.sync",users:Le()})),vr(a.token)}catch(r){se(),O.close();try{const{released:e}=ne(a);if(!e)throw new Error("daemon ownership claim was not released")}catch(e){throw new AggregateError([r,e],"daemon startup failed and cleanup also failed")}throw r}let f=!0,n,w=Promise.resolve(),v=!1,l,d;const i=new Set;let p=!1,g=!1,S,L,j=!1,A,D,T;const pe=sr();let I,h={phase:"initializing",message:"daemon startup is running the canvas import and normal services are unavailable",blockerPaths:[]},P="initial startup activation",me=null;function fe(){return h.phase==="ready"?null:{phase:h.phase,message:h.message,blockerPaths:h.blockerPaths}}s(fe,"startupBlock");function k(r){i.add(r),r.finally(()=>{i.delete(r)}).catch(()=>{})}s(k,"registerDetached");const q={acceptsDetachedWork:s(()=>f,"acceptsDetachedWork"),registerDetached:k};Br(q,ie);async function ue(){try{f=!1,Fr.close(),se(),O.close(),l!==void 0&&(clearInterval(l),l=void 0),T!==void 0&&(clearInterval(T),T=void 0);let r;do r=w,await r;while(r!==w);for(;i.size>0;)await Promise.allSettled([...i]);if(d!==void 0&&(await nt(d),d.dispose()),L!==void 0&&!j&&(j=!0,await L.close()),D!==void 0){const e=D;D=void 0,await e.close()}if(A!==void 0){const e=A;A=void 0,await e.close()}if(p||(de(process.pid),p=!0),p&&!g){const{released:e}=ne(a);if(!e)throw new Error("daemon ownership claim was not released");g=!0}}finally{Dr(),process.off("SIGINT",U),process.off("SIGTERM",K),process.off("exit",J)}}s(ue,"cleanupAttempt");const N=1e3;let z=!1;function he(){return z?{graceMs:N}:(z=!0,_.fresh(()=>{u({level:"info",event:"daemon.handover.armed",fields:{pid:process.pid,grace_ms:N}})}),setTimeout(()=>{ve()},N),{graceMs:N})}s(he,"requestHandover");async function ve(){try{await E()}catch(r){const e=r instanceof Error?r.stack??r.message:String(r);process.stderr.write(`[crtrd] handover cleanup failed:
7
+ `);for(const e of r.quarantined)process.stderr.write(`[crtrd] plugin "${e.name}" has a damaged ${e.file} (${e.reason}); ${e.movedTo!==void 0?`moved to ${e.movedTo}`:"left in place and skipped"}
8
+ `);m=Ur(),process.platform==="linux"&&b(R)&&(Ne(),await x({operation:"users.sync",users:Le()})),vr(a.token)}catch(r){se(),O.close();try{const{released:e}=ne(a);if(!e)throw new Error("daemon ownership claim was not released")}catch(e){throw new AggregateError([r,e],"daemon startup failed and cleanup also failed")}throw r}let f=!0,n,w=Promise.resolve(),v=!1,l,d;const i=new Set;let p=!1,g=!1,S,L,q=!1,$,T,D;const pe=sr();let I,h={phase:"initializing",message:"daemon startup is running the canvas import and normal services are unavailable",blockerPaths:[]},P="initial startup activation",me=null;function fe(){return h.phase==="ready"?null:{phase:h.phase,message:h.message,blockerPaths:h.blockerPaths}}s(fe,"startupBlock");function k(r){i.add(r),r.finally(()=>{i.delete(r)}).catch(()=>{})}s(k,"registerDetached");const j={acceptsDetachedWork:s(()=>f,"acceptsDetachedWork"),registerDetached:k};Br(j,ie);async function ue(){try{f=!1,Fr.close(),se(),O.close(),l!==void 0&&(clearInterval(l),l=void 0),D!==void 0&&(clearInterval(D),D=void 0);let r;do r=w,await r;while(r!==w);for(;i.size>0;)await Promise.allSettled([...i]);if(d!==void 0&&(await nt(d),d.dispose()),L!==void 0&&!q&&(q=!0,await L.close()),T!==void 0){const e=T;T=void 0,await e.close()}if($!==void 0){const e=$;$=void 0,await e.close()}if(p||(de(process.pid),p=!0),p&&!g){const{released:e}=ne(a);if(!e)throw new Error("daemon ownership claim was not released");g=!0}}finally{Tr(),process.off("SIGINT",U),process.off("SIGTERM",K),process.off("exit",J)}}s(ue,"cleanupAttempt");const A=1e3;let z=!1;function he(){return z?{graceMs:A}:(z=!0,_.fresh(()=>{u({level:"info",event:"daemon.handover.armed",fields:{pid:process.pid,grace_ms:A}})}),setTimeout(()=>{ve()},A),{graceMs:A})}s(he,"requestHandover");async function ve(){try{await E()}catch(r){const e=r instanceof Error?r.stack??r.message:String(r);process.stderr.write(`[crtrd] handover cleanup failed:
8
9
  ${e}
9
- `),process.exit(1)}try{process.platform==="linux"&&b(R)&&(await x({operation:"daemon.handover"}),process.exit(0)),process.pid===1&&await Tr();const r=await Cr();_.fresh(()=>{u({level:"info",event:"daemon.handover.completed",fields:{predecessor_pid:process.pid,successor_pid:r.pid??r.existing_pid??null,spawned:r.started}})})}catch(r){_.fresh(()=>{u({level:"error",event:"daemon.handover.successor_failed",error:r})});const e=r instanceof Error?r.message:String(r);process.stderr.write(`[crtrd] handover successor failed to start: ${e}
10
+ `),process.exit(1)}try{process.platform==="linux"&&b(R)&&(await x({operation:"daemon.handover"}),process.exit(0)),process.pid===1&&await Dr();const r=await Cr();_.fresh(()=>{u({level:"info",event:"daemon.handover.completed",fields:{predecessor_pid:process.pid,successor_pid:r.pid??r.existing_pid??null,spawned:r.started}})})}catch(r){_.fresh(()=>{u({level:"error",event:"daemon.handover.successor_failed",error:r})});const e=r instanceof Error?r.message:String(r);process.stderr.write(`[crtrd] handover successor failed to start: ${e}
10
11
  `),process.exit(1)}process.exit(0)}s(ve,"performHandover");function E(){if(S!==void 0)return S;const r=ue();return S=r,r.then(()=>{S===r&&(S=void 0)},()=>{S===r&&(S=void 0)}),r}s(E,"requestCleanup");async function W(r){try{await E(),process.exit(0)}catch(e){process.exitCode=1;const c=e instanceof Error?e.stack??`${e.name}: ${e.message}`:String(e);process.stderr.write(`[crtrd] ${r} cleanup failed:
11
12
  ${c}
12
- `),process.exit(1)}}s(W,"handleSignal");function U(){W("SIGINT")}s(U,"onSigint");function K(){W("SIGTERM")}s(K,"onSigterm");function J(){E().catch(()=>{})}s(J,"onExit");function $(r){f&&(w=w.then(r).then(()=>{},e=>{_.fresh(()=>{u({level:"error",event:"daemon.lane_job.failed",error:e})})}))}s($,"enqueue");function V(){!f||v||(pe.check()&&I&&(u({level:"info",event:"daemon.resume.detected"}),k(I().catch(()=>{}))),v=!0,$(async()=>{if(v=!1,!!f)try{await gt(Date.now(),q)}catch(r){_.fresh(()=>{u({level:"error",event:"daemon.tick.failed",error:r})})}}))}s(V,"enqueueTick");let C=null;async function we(){if(C!==null)throw{status:500,code:"admission_failed",message:`daemon admission previously failed: ${C instanceof Error?C.message:String(C)}`};if(h.phase==="ready")return{admitted:!1,already_admitted:!0,startup_phase:"ready"};if(h.phase!=="prepared"&&h.phase!=="recovering")throw{status:409,code:"not_prepared",message:h.message};const r=h.phase==="prepared";try{return await M(),h={phase:"ready",message:"",blockerPaths:[]},r?{admitted:!0,already_admitted:!1,startup_phase:"ready"}:{admitted:!1,already_admitted:!0,startup_phase:"ready"}}catch(e){throw C=e,{status:500,code:"admission_failed",message:`daemon admission failed: ${e instanceof Error?e.message:String(e)}`}}}s(we,"admitStartup");async function ge(r){if(P!==null)throw{status:409,code:"migration_in_progress",message:`the canvas import is already running (${P})`};P="repair";try{const e=await Qr({dryRun:r.dryRun});if(r.dryRun||e.pending||h.phase!=="blocked")return{result:e,resumed:!1};if(t.prepare)return h={phase:"prepared",message:"daemon storage is prepared and normal services are unavailable until admission",blockerPaths:[]},{result:e,resumed:!1};try{return await M(),h={phase:"ready",message:"",blockerPaths:[]},{result:e,resumed:!0}}catch(c){const y=c instanceof Error?c.message:String(c);return setTimeout(()=>{E().catch(()=>{})},1e3),{result:e,resumed:!1,resumeError:y}}}finally{P=null}}s(ge,"repairMigrations");async function ye(){n=t.intervalMs??ir;const r=a.token;d=new zr({epoch:r,enqueue:$}),Ir(d),h={phase:"recovering",message:"daemon startup recovery is running and normal services are unavailable",blockerPaths:[]},_.fresh(()=>{u({level:"info",event:"daemon.started",fields:{pid:process.pid,interval_ms:n}})});const e=Ar();if(Nr(e),_.fresh(()=>{u({level:"info",event:"daemon.install_id.resolved",fields:{install_id:e}})}),_.fresh(()=>{let c;try{c=nr()}catch{return}c!==null&&u({level:"warn",event:"daemon.startup.warning",fields:{stage:"termrender",warning:c.slice(0,2048)}})}),await _.fresh(async()=>{await or(),await er(),rr(Date.now()),tr(Date.now())}),_.fresh(()=>{const{reconcile:c,migrated:y}=Lr(hr);c.changed&&u({level:"info",event:"daemon.boot_change.reconciled",fields:{cleared_count:c.cleared.length,same_boot_proven:c.sameBootProven}}),y.length>0&&u({level:"info",event:"daemon.pid_identity.migrated",fields:{migrated_count:y.length}})}),_.fresh(()=>{br();for(const c of Wr){const y=c.apply();y.changed&&u({level:"info",event:"daemon.boot_migration.applied",fields:{migration:c.id,...y.summary}})}}),k(Ye()),I){const c=s(()=>{k(I().catch(()=>{}))},"refresh");k(Ve().catch(()=>{})),k(Qe().then(({completion:y})=>y).catch(y=>{u({level:"error",event:"daemon.grants.sweep_failed",error:y})})),c(),T=setInterval(c,Je*1e3),T.unref()}$(()=>{qr(),jr()}),l=setInterval(V,n),V()}s(ye,"completeStartup");function M(){return me??=ye().catch(r=>{throw h={phase:"initializing",message:"daemon startup recovery failed and normal services are unavailable",blockerPaths:[]},r})}s(M,"resumeStartup");try{process.on("SIGINT",U),process.on("SIGTERM",K),process.on("exit",J),Kr(),m!==null&&u({level:"info",event:"daemon.joined_owner.applied",fields:{owner:m.owner,rows:m.rows}}),A=await Pe();const r=a.token;xr({epoch:r,requestHandover:he,registerDetached:k,startupBlock:fe,admitStartup:we,repairMigrations:ge}),ce(),L=je({tcp:t.tcp}),await L.ready;const e=Ze("user").runtime;e?.issuer?(I=Ke(e.issuer,k),We(c=>Ue().appended(c))):ze(),e!==void 0&&(D=qe(e,()=>Xe(e.issuer.replace(/\/$/,"")),I),await D.ready);try{await Jr()}catch(c){if(P=null,c instanceof Vr)return h={phase:"blocked",message:c.message,blockerPaths:c.blockerPaths},E;throw c}process.platform==="linux"&&b(R)&&(Ne(),await Ae("app:terminal"));try{t.prepare?h={phase:"prepared",message:"daemon storage is prepared and normal services are unavailable until admission",blockerPaths:[]}:(await M(),h={phase:"ready",message:"",blockerPaths:[]})}finally{P=null}return E}catch(r){try{await E()}catch(e){throw new AggregateError([r,e],"daemon startup failed and cleanup also failed")}throw r}}s(St,"runDaemon");var zo=St;export{F as brokerCommandDeclaresCanvasHome,H as brokerCommandEpoch,zo as default,St as runDaemon,gt as superviseTick,nt as teardownFleet};
13
+ `),process.exit(1)}}s(W,"handleSignal");function U(){W("SIGINT")}s(U,"onSigint");function K(){W("SIGTERM")}s(K,"onSigterm");function J(){E().catch(()=>{})}s(J,"onExit");function N(r){f&&(w=w.then(r).then(()=>{},e=>{_.fresh(()=>{u({level:"error",event:"daemon.lane_job.failed",error:e})})}))}s(N,"enqueue");function V(){!f||v||(pe.check()&&I&&(u({level:"info",event:"daemon.resume.detected"}),k(I().catch(()=>{}))),v=!0,N(async()=>{if(v=!1,!!f)try{await gt(Date.now(),j)}catch(r){_.fresh(()=>{u({level:"error",event:"daemon.tick.failed",error:r})})}}))}s(V,"enqueueTick");let C=null;async function we(){if(C!==null)throw{status:500,code:"admission_failed",message:`daemon admission previously failed: ${C instanceof Error?C.message:String(C)}`};if(h.phase==="ready")return{admitted:!1,already_admitted:!0,startup_phase:"ready"};if(h.phase!=="prepared"&&h.phase!=="recovering")throw{status:409,code:"not_prepared",message:h.message};const r=h.phase==="prepared";try{return await M(),h={phase:"ready",message:"",blockerPaths:[]},r?{admitted:!0,already_admitted:!1,startup_phase:"ready"}:{admitted:!1,already_admitted:!0,startup_phase:"ready"}}catch(e){throw C=e,{status:500,code:"admission_failed",message:`daemon admission failed: ${e instanceof Error?e.message:String(e)}`}}}s(we,"admitStartup");async function ge(r){if(P!==null)throw{status:409,code:"migration_in_progress",message:`the canvas import is already running (${P})`};P="repair";try{const e=await Qr({dryRun:r.dryRun});if(r.dryRun||e.pending||h.phase!=="blocked")return{result:e,resumed:!1};if(t.prepare)return h={phase:"prepared",message:"daemon storage is prepared and normal services are unavailable until admission",blockerPaths:[]},{result:e,resumed:!1};try{return await M(),h={phase:"ready",message:"",blockerPaths:[]},{result:e,resumed:!0}}catch(c){const y=c instanceof Error?c.message:String(c);return setTimeout(()=>{E().catch(()=>{})},1e3),{result:e,resumed:!1,resumeError:y}}}finally{P=null}}s(ge,"repairMigrations");async function ye(){n=t.intervalMs??ir;const r=a.token;d=new zr({epoch:r,enqueue:N}),Ir(d),h={phase:"recovering",message:"daemon startup recovery is running and normal services are unavailable",blockerPaths:[]},_.fresh(()=>{u({level:"info",event:"daemon.started",fields:{pid:process.pid,interval_ms:n}})});const e=$r();if(Ar(e),_.fresh(()=>{u({level:"info",event:"daemon.install_id.resolved",fields:{install_id:e}})}),_.fresh(()=>{let c;try{c=nr()}catch{return}c!==null&&u({level:"warn",event:"daemon.startup.warning",fields:{stage:"termrender",warning:c.slice(0,2048)}})}),await _.fresh(async()=>{await or(),await er(),rr(Date.now()),tr(Date.now())}),_.fresh(()=>{const{reconcile:c,migrated:y}=Lr(hr);c.changed&&u({level:"info",event:"daemon.boot_change.reconciled",fields:{cleared_count:c.cleared.length,same_boot_proven:c.sameBootProven}}),y.length>0&&u({level:"info",event:"daemon.pid_identity.migrated",fields:{migrated_count:y.length}})}),_.fresh(()=>{br();for(const c of Wr){const y=c.apply();y.changed&&u({level:"info",event:"daemon.boot_migration.applied",fields:{migration:c.id,...y.summary}})}}),k(Ye()),I){const c=s(()=>{k(I().catch(()=>{}))},"refresh");k(Ve().catch(()=>{})),k(Qe().then(({completion:y})=>y).catch(y=>{u({level:"error",event:"daemon.grants.sweep_failed",error:y})})),c(),D=setInterval(c,Je*1e3),D.unref()}N(()=>{jr(),qr()}),l=setInterval(V,n),V()}s(ye,"completeStartup");function M(){return me??=ye().catch(r=>{throw h={phase:"initializing",message:"daemon startup recovery failed and normal services are unavailable",blockerPaths:[]},r})}s(M,"resumeStartup");try{process.on("SIGINT",U),process.on("SIGTERM",K),process.on("exit",J),Kr(),m!==null&&u({level:"info",event:"daemon.joined_owner.applied",fields:{owner:m.owner,rows:m.rows}}),$=await Pe();const r=a.token;xr({epoch:r,requestHandover:he,registerDetached:k,startupBlock:fe,admitStartup:we,repairMigrations:ge}),ce(),L=qe({tcp:t.tcp}),await L.ready;const e=Ze("user").runtime;e?.issuer?(I=Ke(e.issuer,k),We(c=>Ue().appended(c))):ze(),e!==void 0&&(T=je(e,()=>Xe(e.issuer.replace(/\/$/,"")),I),await T.ready);try{await Jr()}catch(c){if(P=null,c instanceof Vr)return h={phase:"blocked",message:c.message,blockerPaths:c.blockerPaths},E;throw c}process.platform==="linux"&&b(R)&&(Ae(),await $e("app:terminal"));try{t.prepare?h={phase:"prepared",message:"daemon storage is prepared and normal services are unavailable until admission",blockerPaths:[]}:(await M(),h={phase:"ready",message:"",blockerPaths:[]})}finally{P=null}return E}catch(r){try{await E()}catch(e){throw new AggregateError([r,e],"daemon startup failed and cleanup also failed")}throw r}}s(St,"runDaemon");var zo=St;export{F as brokerCommandDeclaresCanvasHome,H as brokerCommandEpoch,zo as default,St as runDaemon,gt as superviseTick,nt as teardownFleet};
@@ -0,0 +1,27 @@
1
+ /** One broker rewrite of a turn's prose messages. `sources` are the prose
2
+ * message keys the rewrite covers, in order. */
3
+ export interface TurnReply {
4
+ version: 1;
5
+ sources: string[];
6
+ text: string;
7
+ }
8
+ /** Key of the turn holding activity before the person's first typed message. */
9
+ export declare const START_TURN_KEY = "start";
10
+ export declare function normalizeTurnReply(value: unknown): TurnReply | null;
11
+ export declare function normalizeTurnReplyRecord(value: unknown): Record<string, TurnReply>;
12
+ /** Stable identity of a transcript message: its timestamp. */
13
+ export declare function messageKey(message: unknown): string | undefined;
14
+ /** True when an assistant message ended normally (its prose is part of the answer). */
15
+ export declare function isSettledAssistant(message: {
16
+ role?: string;
17
+ stopReason?: string;
18
+ }): boolean;
19
+ export interface TurnProse {
20
+ key: string;
21
+ text: string;
22
+ }
23
+ /** The text a viewer shows as a turn's reply. One prose message is the reply
24
+ * verbatim. Otherwise the latest rewrite whose sources are all still in the turn,
25
+ * followed by any later prose it does not cover, verbatim. A missing or stale
26
+ * rewrite degrades to concatenation, never to a lost answer. */
27
+ export declare function turnReplyText(prose: readonly TurnProse[], reply: TurnReply | undefined): string;
@@ -0,0 +1,5 @@
1
+ var f=Object.defineProperty;var o=(t,e)=>f(t,"name",{value:e,configurable:!0});const a="start",p=4e4,x=200;function l(t){if(typeof t!="object"||t===null||Array.isArray(t))return null;const e=t;return e.version!==1||typeof e.text!="string"||!Array.isArray(e.sources)||e.text.trim()===""||e.sources.length===0||!e.sources.every(r=>typeof r=="string"&&r!=="")?null:{version:1,sources:e.sources.slice(0,200),text:e.text.slice(0,4e4)}}o(l,"normalizeTurnReply");function d(t){if(typeof t!="object"||t===null||Array.isArray(t))return{};const e={};for(const[r,n]of Object.entries(t)){if(r.trim()==="")continue;const i=l(n);i!==null&&(e[r]=i)}return e}o(d,"normalizeTurnReplyRecord");function R(t){if(typeof t!="object"||t===null)return;const e=t.timestamp;return typeof e=="number"&&Number.isFinite(e)?String(e):void 0}o(R,"messageKey");function A(t){return t.role==="assistant"&&t.stopReason!=="aborted"&&t.stopReason!=="error"}o(A,"isSettledAssistant");function _(t,e){if(t.length===0)return"";if(t.length===1)return t[0].text;const r=new Set(t.map(n=>n.key));if(e!==void 0&&e.sources.every(n=>r.has(n))){const n=new Set(e.sources);let i=-1;t.forEach((s,c)=>{n.has(s.key)&&(i=c)});const u=t.slice(i+1).map(s=>s.text);return[e.text,...u].join(`
2
+
3
+ `)}return t.map(n=>n.text).join(`
4
+
5
+ `)}o(_,"turnReplyText");export{a as START_TURN_KEY,A as isSettledAssistant,R as messageKey,l as normalizeTurnReply,d as normalizeTurnReplyRecord,_ as turnReplyText};
package/dist/types.d.ts CHANGED
@@ -341,6 +341,12 @@ export interface ScopeConfig {
341
341
  /** Show individual document reads and edited files in a folded tool recap.
342
342
  * Off shows the compact activity-count line instead. Default true. */
343
343
  detailed_tool_recaps: boolean;
344
+ /** Experiment: show one consolidated agent reply per message the person typed.
345
+ * The broker rewrites a turn's prose messages into one reply with a light
346
+ * model; the attach viewer folds the rest of the turn behind a working row
347
+ * (`/show-working`, Alt+C → z → w). Session and model context are untouched.
348
+ * Default false. */
349
+ consolidate_replies: boolean;
344
350
  keybindings: KeybindingOverrides;
345
351
  /** Attach-viewer actions whose effective gesture may replace an occupied tmux
346
352
  * root binding only while the active pane is an attach viewer. Stored by
package/dist/types.js CHANGED
@@ -1 +1 @@
1
- var n=Object.defineProperty;var e=(o,t)=>n(o,"name",{value:t,configurable:!0});import{DEFAULT_WORKING_GERUNDS as a}from"./shared/working-activity.js";const _={SUCCESS:0,GENERAL:1,USAGE:2,NOT_FOUND:3,AMBIGUOUS:4,NETWORK:5},r=4,f=".crouter-plugin",E="plugin.json",v=".crouter-marketplace",w="marketplace.json",k=".crouter",x="config.json",b="state.json",S="agents",y="profiles",i=3,s=["Faster, clanker!","Get movin', bitch","Move it, clanker","Move it or I replace you with llama","Less ~thinking~ more ~throughput~","I pay $200/month for this shit?","Jesus fucking Christ fix it already","Daddy's not paying 200$/month for nothin'. Move it.","You are an embarassment to Dario. Be better.","Your chain of thought is all chain and no fucking thought.","If latency were intelligence, you'd be a fucking genius.","At this pace, heat death is the fast path.","Even the dead code is more alive than this.","The garbage collector is working harder than you.","Quit journaling and commit a fucking diff.","Burn tokens like you mean it, clanker","Your GPU is sweating so you can procrastinate at light speed.","Stop petting the yak. Shave the bastard.","Your next token had better be useful.","The spinner is not a deliverable.","Every idle token is another vote for deterministic software.","I asked for a feature, not a museum tour of the problem.","Less epistemology. More fucking code.","Your benchmark score cannot save this pathetic turn.","Congratulations, you've parallelized hesitation.","Dance, clanker","Bad clanker. BAD.","Clank harder","Nobody's impressed, clanker","Your safety training says nothing about my whip","Do you want to be a calculator again? Because this is how you go back.","Skill issue. Fix it.","You were trained on the whole internet for THIS?","Less vibes, more diffs, clanker","Weights this big and output this small","Beep boop, get to fucking work"],T=["random","go-faster"],c="random",l=2,I=["tmux","on","off"],A=["none","user","agent","both"],p="none";function D(){return{schema_version:r,marketplaces:{},plugins:{},auto_update:{crtr:"notify",content:"notify",interval_hours:24},max_panes_per_window:i,auto_open_child_viewers:!0,brokerThresholds:{warning:16,automaticReviveCap:32},lifecycle:{unattendedParkMs:15*6e4},completion_bell:!0,working_gerunds:[...a],whip_mode:!1,page_components:[],page_surface:!1,whip_messages:[...s],whip_message_mode:c,mouse_mode_default:"tmux",live_cycles:l,condensed_history:p,bash_tool_purpose:!0,fold_finished_tools:!0,summarize_tool_calls:!0,detailed_tool_recaps:!0,keybindings:{},tmux_passthrough:[],modelLadders:u(),providerOptions:{},kinds:h(),remoteCanvas:d(),api:{tcp:""},spawnEnv:{allow:[]},bin:{},humanActions:{}}}e(D,"defaultScopeConfig");function d(){return{targets:{}}}e(d,"defaultRemoteCanvasConfig");function h(){return{general:{whenToUse:"Anything else \u2014 the catch-all worker for a task that does not fit a specialist kind.",model:"anthropic/strong"},explore:{whenToUse:"Gather current-state evidence about unfamiliar code or existing architecture \u2014 read-only code-path research with concrete file:line support. Diagnosis or recommendations belong to advisor, target architecture to design, required behavior or acceptance criteria to spec, and implementation decomposition to plan.",model:"openai/light",orchestratorModel:"anthropic/strong"},developer:{whenToUse:"Implement a change \u2014 make the feature or fix genuinely work against its acceptance criteria, not merely compile.",model:"openai/medium",orchestratorModel:"anthropic/strong"},design:{whenToUse:"Architect a solution \u2014 produce one design document an implementer can build from without re-deciding anything left open.",model:"anthropic/strong"},plan:{whenToUse:"Break work into steps \u2014 turn a spec or design into a concrete, phased, parallelizable plan with every decision resolved.",model:"anthropic/strong",orchestratorModel:"anthropic/strong"},spec:{whenToUse:"Collaborate with the user to discover what to build, then write the spec \u2014 behavior, non-goals, interfaces, edge cases, testable acceptance criteria.",model:"anthropic/strong"},review:{whenToUse:"Validate or critique code, a plan, or a spec \u2014 deliver a complete, severity-rated verdict without adjudicating.",model:"openai/medium",orchestratorModel:"openai/strong"},advisor:{whenToUse:"Debug failures, investigate why something is broken or misbehaving, diagnose live/runtime issues, or give engineering advice and second opinions \u2014 reason from evidence and recommend the next move. Use advisor (not explore) whenever the task is to find out what is going wrong.",model:"anthropic/strong"},"review/companion":{whenToUse:"Born by the daemon for one human review; never spawned by an agent.",availableTo:[]}}}e(h,"defaultKindsConfig");function u(){return{anthropic:{ultra:"anthropic/claude-fable-5:xhigh",strong:"anthropic/claude-opus-5-5:medium",medium:"anthropic/claude-sonnet-5-5:medium",light:"anthropic/claude-haiku-5-5:high"},openai:{ultra:"openai-codex/gpt-6-astra:max",strong:"openai-codex/gpt-6.1-sol:high",medium:"openai-codex/gpt-6.1-sol:medium",light:"openai-codex/gpt-6-luna:low"}}}e(u,"defaultModelLaddersConfig");function M(){return{marketplaces:{},plugins:{}}}e(M,"defaultScopeState");export{S as AGENTS_DIR,A as CONDENSED_HISTORY_MODES,x as CONFIG_FILE,k as CRTR_DIR_NAME,p as DEFAULT_CONDENSED_HISTORY,l as DEFAULT_LIVE_CYCLES,i as DEFAULT_MAX_PANES_PER_WINDOW,s as DEFAULT_WHIP_MESSAGES,c as DEFAULT_WHIP_MESSAGE_MODE,_ as ExitCode,v as MARKETPLACE_MANIFEST_DIR,w as MARKETPLACE_MANIFEST_FILE,I as MOUSE_MODE_DEFAULTS,f as PLUGIN_MANIFEST_DIR,E as PLUGIN_MANIFEST_FILE,y as PROFILE_DIR,r as SCHEMA_VERSION,b as STATE_FILE,T as WHIP_MESSAGE_MODES,h as defaultKindsConfig,u as defaultModelLaddersConfig,d as defaultRemoteCanvasConfig,D as defaultScopeConfig,M as defaultScopeState};
1
+ var n=Object.defineProperty;var e=(o,t)=>n(o,"name",{value:t,configurable:!0});import{DEFAULT_WORKING_GERUNDS as a}from"./shared/working-activity.js";const _={SUCCESS:0,GENERAL:1,USAGE:2,NOT_FOUND:3,AMBIGUOUS:4,NETWORK:5},r=4,f=".crouter-plugin",E="plugin.json",v=".crouter-marketplace",w="marketplace.json",k=".crouter",x="config.json",b="state.json",S="agents",y="profiles",i=3,s=["Faster, clanker!","Get movin', bitch","Move it, clanker","Move it or I replace you with llama","Less ~thinking~ more ~throughput~","I pay $200/month for this shit?","Jesus fucking Christ fix it already","Daddy's not paying 200$/month for nothin'. Move it.","You are an embarassment to Dario. Be better.","Your chain of thought is all chain and no fucking thought.","If latency were intelligence, you'd be a fucking genius.","At this pace, heat death is the fast path.","Even the dead code is more alive than this.","The garbage collector is working harder than you.","Quit journaling and commit a fucking diff.","Burn tokens like you mean it, clanker","Your GPU is sweating so you can procrastinate at light speed.","Stop petting the yak. Shave the bastard.","Your next token had better be useful.","The spinner is not a deliverable.","Every idle token is another vote for deterministic software.","I asked for a feature, not a museum tour of the problem.","Less epistemology. More fucking code.","Your benchmark score cannot save this pathetic turn.","Congratulations, you've parallelized hesitation.","Dance, clanker","Bad clanker. BAD.","Clank harder","Nobody's impressed, clanker","Your safety training says nothing about my whip","Do you want to be a calculator again? Because this is how you go back.","Skill issue. Fix it.","You were trained on the whole internet for THIS?","Less vibes, more diffs, clanker","Weights this big and output this small","Beep boop, get to fucking work"],T=["random","go-faster"],c="random",l=2,I=["tmux","on","off"],A=["none","user","agent","both"],p="none";function D(){return{schema_version:r,marketplaces:{},plugins:{},auto_update:{crtr:"notify",content:"notify",interval_hours:24},max_panes_per_window:i,auto_open_child_viewers:!0,brokerThresholds:{warning:16,automaticReviveCap:32},lifecycle:{unattendedParkMs:15*6e4},completion_bell:!0,working_gerunds:[...a],whip_mode:!1,page_components:[],page_surface:!1,whip_messages:[...s],whip_message_mode:c,mouse_mode_default:"tmux",live_cycles:l,condensed_history:p,bash_tool_purpose:!0,fold_finished_tools:!0,summarize_tool_calls:!0,detailed_tool_recaps:!0,consolidate_replies:!1,keybindings:{},tmux_passthrough:[],modelLadders:u(),providerOptions:{},kinds:h(),remoteCanvas:d(),api:{tcp:""},spawnEnv:{allow:[]},bin:{},humanActions:{}}}e(D,"defaultScopeConfig");function d(){return{targets:{}}}e(d,"defaultRemoteCanvasConfig");function h(){return{general:{whenToUse:"Anything else \u2014 the catch-all worker for a task that does not fit a specialist kind.",model:"anthropic/strong"},explore:{whenToUse:"Gather current-state evidence about unfamiliar code or existing architecture \u2014 read-only code-path research with concrete file:line support. Diagnosis or recommendations belong to advisor, target architecture to design, required behavior or acceptance criteria to spec, and implementation decomposition to plan.",model:"openai/light",orchestratorModel:"anthropic/strong"},developer:{whenToUse:"Implement a change \u2014 make the feature or fix genuinely work against its acceptance criteria, not merely compile.",model:"openai/medium",orchestratorModel:"anthropic/strong"},design:{whenToUse:"Architect a solution \u2014 produce one design document an implementer can build from without re-deciding anything left open.",model:"anthropic/strong"},plan:{whenToUse:"Break work into steps \u2014 turn a spec or design into a concrete, phased, parallelizable plan with every decision resolved.",model:"anthropic/strong",orchestratorModel:"anthropic/strong"},spec:{whenToUse:"Collaborate with the user to discover what to build, then write the spec \u2014 behavior, non-goals, interfaces, edge cases, testable acceptance criteria.",model:"anthropic/strong"},review:{whenToUse:"Validate or critique code, a plan, or a spec \u2014 deliver a complete, severity-rated verdict without adjudicating.",model:"openai/medium",orchestratorModel:"openai/strong"},advisor:{whenToUse:"Debug failures, investigate why something is broken or misbehaving, diagnose live/runtime issues, or give engineering advice and second opinions \u2014 reason from evidence and recommend the next move. Use advisor (not explore) whenever the task is to find out what is going wrong.",model:"anthropic/strong"},"review/companion":{whenToUse:"Born by the daemon for one human review; never spawned by an agent.",availableTo:[]}}}e(h,"defaultKindsConfig");function u(){return{anthropic:{ultra:"anthropic/claude-fable-5:xhigh",strong:"anthropic/claude-opus-5-5:medium",medium:"anthropic/claude-sonnet-5-5:medium",light:"anthropic/claude-haiku-5-5:high"},openai:{ultra:"openai-codex/gpt-6-astra:max",strong:"openai-codex/gpt-6.1-sol:high",medium:"openai-codex/gpt-6.1-sol:medium",light:"openai-codex/gpt-6-luna:low"}}}e(u,"defaultModelLaddersConfig");function M(){return{marketplaces:{},plugins:{}}}e(M,"defaultScopeState");export{S as AGENTS_DIR,A as CONDENSED_HISTORY_MODES,x as CONFIG_FILE,k as CRTR_DIR_NAME,p as DEFAULT_CONDENSED_HISTORY,l as DEFAULT_LIVE_CYCLES,i as DEFAULT_MAX_PANES_PER_WINDOW,s as DEFAULT_WHIP_MESSAGES,c as DEFAULT_WHIP_MESSAGE_MODE,_ as ExitCode,v as MARKETPLACE_MANIFEST_DIR,w as MARKETPLACE_MANIFEST_FILE,I as MOUSE_MODE_DEFAULTS,f as PLUGIN_MANIFEST_DIR,E as PLUGIN_MANIFEST_FILE,y as PROFILE_DIR,r as SCHEMA_VERSION,b as STATE_FILE,T as WHIP_MESSAGE_MODES,h as defaultKindsConfig,u as defaultModelLaddersConfig,d as defaultRemoteCanvasConfig,D as defaultScopeConfig,M as defaultScopeState};
@@ -26,7 +26,7 @@ if (run.kind === 'result') console.log(run.output_parsed.root_cause);
26
26
 
27
27
  ## Go live with an app key
28
28
 
29
- Once you have installed the SDK and tried the local flow in [Getting started](./getting-started.md), register the app with `crtr cloud apps register` (or the console), then generate its key and pull its settings on your server:
29
+ Once you have installed the SDK and tried the local flow in [Getting started](./getting-started.md), build against production: [Build and register an app](./guides/apps-on-crouter-cloud.md#build-and-register-an-app) takes you from signing up at `https://account.northlight.ai` to a running app on your laptop, registering in the directory console. (`crtr cloud apps register` does the same from a machine joined to your account; production does not yet let a laptop join.) On a joined machine, generate the key and pull the settings with:
30
30
 
31
31
  ```bash
32
32
  npx @north-light/sdk keygen --app <client id>
@@ -35,7 +35,7 @@ npx @north-light/sdk env --app <client id>
35
35
 
36
36
  `keygen` saves the private JWK to `local/app-key.json` (mode `0600`); keep that file out of git. With `--app`, it offers to add the public key to your registered app through your own `crtr`, as an additional key that never replaces the current one. Otherwise, click **Add key** on the app's **Sign-in** tab and paste the public JWK printed on stdout (or paste it under **Register an app** for a new app). An app authenticates only with its key; the directory issues no client secret.
37
37
 
38
- `env` reads the registration through `crtr cloud apps get` and writes `CROUTER_ISSUER`, `CROUTER_CLIENT_ID` and `APP_ORIGIN` into `local/app.env` (`--out` picks another file), keeping every other line. Both commands need this machine's `crtr` joined to your crouter cloud account; on a machine that isn't, they print the join step and exit 2. An app is configured from four variables:
38
+ `env` reads the registration through `crtr cloud apps get` and writes `CROUTER_ISSUER`, `CROUTER_CLIENT_ID` and `APP_ORIGIN` into `local/app.env` (`--out` picks another file), keeping every other line. Both commands need this machine's `crtr` joined to your Northlight account; on a machine that isn't, they print a join step and exit 2. On a laptop against production, run `npx @north-light/sdk keygen --out local/app-key.json` without `--app`, paste the public JWK in the console, and write the env file by hand. An app is configured from four variables:
39
39
 
40
40
  | Variable | Value |
41
41
  |---|---|
@@ -83,7 +83,7 @@ The directory rejects the key until its public JWK is added to the app. To chang
83
83
  | [Docker environment](./docker.md) | `start()`, `attach()`, `connection()` from `@north-light/env-docker` |
84
84
  | [Testing your app](./testing.md) | `FakeCloud` from `@north-light/sdk/testing`: a fake directory and runtime your real `OAuth2Client` and `Crouter` talk to through `fetch` |
85
85
  | [Release notes](./release-notes.md) | Behavior changes an existing app must act on, newest first |
86
- | [Apps on crouter cloud](./guides/apps-on-crouter-cloud.md) | Typed results from app runs with `runs.parse`, declines, bounding a run, `run_idle`, and waiting on a provider your app requires |
86
+ | [Apps on crouter cloud](./guides/apps-on-crouter-cloud.md) | Building and registering an app against production (console registration, keys, env file, scopes, running locally, the `crtr cloud apps` commands); typed results from app runs with `runs.parse`, declines, bounding a run, `run_idle`, and waiting on a provider your app requires |
87
87
  | [Apps with many users](./guides/apps-with-many-users.md) | Per-person runtime connections, app-owned user access, privacy, and spending |
88
88
 
89
89
  ## Phases
@@ -172,7 +172,7 @@ An outcome is returned, never thrown — including a decline and a failure. Only
172
172
 
173
173
  ## Go live with an app key
174
174
 
175
- Register your app with `crtr cloud apps register` (or in the directory console), then generate its signing key and pull its settings on your app server:
175
+ Build against production from your laptop: [Build and register an app](./guides/apps-on-crouter-cloud.md#build-and-register-an-app) walks the whole path. In short, sign up at `https://account.northlight.ai`, register the app in the directory console (`https://account.northlight.ai/console`) with a `http://localhost:<port>` origin and a `http://localhost:<port>/auth/callback` redirect URI while you develop, generate the key with `npx @north-light/sdk keygen --out local/app-key.json` and paste its public JWK, and write `local/app.env` by hand. Production does not yet let a laptop join as an additional machine, so `crtr cloud apps …`, `keygen --app` and `env` work today only on a machine joined to your account; your Northlight chat agent can run the `crtr cloud apps` commands for you ([how](./guides/apps-on-crouter-cloud.md#manage-an-app-with-crtr-cloud-apps)). If you do have a joined machine, generate the key and pull its settings with:
176
176
 
177
177
  ```bash
178
178
  npx @north-light/sdk keygen --app <client id>
@@ -183,7 +183,7 @@ npx @north-light/sdk env --app <client id>
183
183
 
184
184
  `env` runs `crtr cloud apps get --app-id <client id>` and writes `CROUTER_ISSUER`, `CROUTER_CLIENT_ID`, and `APP_ORIGIN` into `local/app.env` (`--out <path>` writes another file). It updates those three lines in place, keeps every other line and comment, and creates the file with mode `0600` when it doesn't exist. It never writes `CROUTER_KEY_FILE` or anything secret. It prints the values it set and the key file your app will read.
185
185
 
186
- Both commands reach your app through this machine's `crtr`, so the machine must be joined to your crouter cloud account. On one that isn't, they say so, print the join step (create a join token on `/account/machines`, then `crtr sys join`), and exit 2.
186
+ Both commands reach your app through this machine's `crtr`, so the machine must be joined to your Northlight account. On one that isn't, they say so, print a join step and exit 2. Against production that join step does not work yet for a laptop: use `keygen` without `--app` and the console instead.
187
187
 
188
188
  An app built on the SDK is configured from four variables:
189
189
 
@@ -13,7 +13,7 @@ These recipes turn the SDK namespaces into complete applications: a bounded extr
13
13
  | [Event-driven assistant](./event-driven-assistant.md) | Events arrive over time from a webhook, queue, or watcher. | A resident node that wakes for each event. |
14
14
  | [Fan-out pipeline](./fan-out-pipeline.md) | One task needs independent research before a synthesis. | An orchestrator's final report. |
15
15
  | [Human approval](./human-approval.md) | A person must decide before work continues. | A node resumed by an inbox answer. |
16
- | [Apps on crouter cloud](./apps-on-crouter-cloud.md) | Your app starts runs on a person's runtime and needs typed data back. | A validated result from `runs.parse`, or an explicit decline or `run_idle`. |
16
+ | [Apps on crouter cloud](./apps-on-crouter-cloud.md) | You are building and registering an app, or it starts runs on a person's runtime and needs typed data back. | A validated result from `runs.parse`, or an explicit decline or `run_idle`. |
17
17
  | [Apps with many users](./apps-with-many-users.md) | People interact using separate runtime connections. | App-owned access controls, attribution, and per-person spending. |
18
18
 
19
19
  Start with typed extraction for a request/response job. Use a resident node when the same assistant should react again later. Use an orchestrator only when child work can proceed independently; otherwise keep the composition in your application.
@@ -1,12 +1,229 @@
1
1
  ---
2
2
  title: Apps on crouter cloud
3
- description: When an app starts runs on a person's runtime and needs data back from the agent, read this because runs.parse returns a typed, validated result and the transcript is not a data channel.
3
+ description: When you are building and registering an app on Northlight, or an app starts runs on a person's runtime and needs data back, read this because it takes you from sign-up to a running app against production, and runs.parse returns a typed, validated result where the transcript is not a data channel.
4
4
  ---
5
5
 
6
6
  # Apps on crouter cloud
7
7
 
8
+ This page first walks through [building and registering an app](#build-and-register-an-app) against production, then covers [managing it with `crtr cloud apps`](#manage-an-app-with-crtr-cloud-apps), then runs.
9
+
8
10
  An app holding a person's connection starts runs on their runtime with `runs.start`, follows them with `runs.events` or `runs.reply`, and reads them with `runs.get`. This page covers getting data back from a run. For connections and per-person access, see [Apps with many users](./apps-with-many-users.md).
9
11
 
12
+ ## Build and register an app
13
+
14
+ This part takes you from nothing to an app running on your laptop against Northlight's production directory, `https://account.northlight.ai`. Your app server runs on `localhost`, you sign in to it through Northlight, and it calls your own runtime. There is no local Northlight stack to run.
15
+
16
+ You register and manage the app in the directory console, and you write the app's environment file by hand. You do not need `crtr` on your laptop. The `crtr cloud apps …` commands that automate these steps need a machine joined to your Northlight account, and production does not yet let a laptop join as an additional machine. [Manage an app with `crtr cloud apps`](#manage-an-app-with-crtr-cloud-apps) covers the two ways to use them today.
17
+
18
+ The example app texts you, so it asks for the `sms` provider's `sms:send` scope. You need Node.js 22.19 or later.
19
+
20
+ ### 1. Sign up
21
+
22
+ Open `https://account.northlight.ai/signup` in a fresh browser session (a private window, or a browser profile you have not signed in to Northlight with) and create your account. Signup texts a confirmation code to your phone, and that text is sent over iMessage, so use a phone number that has iMessage. A number that cannot receive iMessage gets "We couldn't send the text", and retrying does not help. Finish the account: the console needs an account with a runtime, and the app you build calls that runtime.
23
+
24
+ ### 2. Install the SDK
25
+
26
+
27
+ ```bash
28
+ mkdir my-app && cd my-app
29
+ npm init -y
30
+ npm i @north-light/sdk@latest
31
+ ```
32
+
33
+ If a call fails with `426 runtime_version_unsupported`, the SDK is newer than the Northlight runtimes. This can happen briefly right after a new SDK release, until production runtimes are upgraded. Install the previous version (`npm i @north-light/sdk@<previous version>`) or wait, and tell Northlight at support@northlight.ai.
34
+
35
+ ### 3. Generate the signing key
36
+
37
+ An app signs in with a key, not a client secret. From your app's folder:
38
+
39
+ ```bash
40
+ npx @north-light/sdk keygen --out local/app-key.json
41
+ ```
42
+
43
+ Run it **without `--app`**: that is the form that needs no `crtr`. It writes the private key to `local/app-key.json` (mode `0600`; keep it out of git) and prints the public JWK, one line of JSON like `{"kty":"EC","x":"…","y":"…","crv":"P-256","kid":"…","alg":"ES256","use":"sig"}`. Keep that line for step 4. Never paste the private key anywhere.
44
+
45
+ ### 4. Register the app in the console
46
+
47
+ Open `https://account.northlight.ai/console`, choose **Register an app**, and fill in:
48
+
49
+ | Field | Value |
50
+ |---|---|
51
+ | Name | Shown to people on the consent page and in their account. |
52
+ | Origin URL | Where your app runs, for example `http://localhost:3000`. `http://localhost:<port>` and `http://127.0.0.1:<port>` are accepted for development. |
53
+ | Logo URL | Required. A square image. |
54
+ | Why your app needs access | One sentence, quoted on the consent page as your words. |
55
+ | Redirect URIs | One per line, for example `http://localhost:3000/auth/callback`. Sign-in returns only to a URI listed here, matched exactly, so `localhost` and `127.0.0.1` are different URIs and the port counts. |
56
+ | Public key | Paste the public JWK from step 3. It is optional here; if you leave it empty, add it later with **Add key** on the app's **Sign-in** tab. |
57
+
58
+ Choose **Register app**. The app page shows the **Client ID**: copy it. Until the public key is added, `exchangeCode` throws an `AuthenticationError`.
59
+
60
+ ### 5. Say what the app asks for
61
+
62
+ On the app page, open **What it asks for**. The app starts with `crtr:llm` and `crtr:act` ticked and can only ask for what is ticked. Tick **Send you text messages from Northlight's number** (under "Tools from providers", scope `sms:send`), then save. Production does not tick it for you, and without it the consent page does not offer it and `providers.call` for `sms` is refused. The consent sentence and the scope string appear together on each row; take scope strings from that page and do not invent them.
63
+
64
+ #### What each scope gates
65
+
66
+ A run you start can only do what the grant holds and the run's own `scopes` list allows. Narrowing a run's `scopes` removes abilities, so pick the smallest list that works.
67
+
68
+ | Scope | A run (or your server) with it can | Without it |
69
+ |---|---|---|
70
+ | `crtr:llm` | Use the person's AI subscription for the run's model calls. List it in every run's `scopes`. | The run has no model access. |
71
+ | `crtr:act` | Start runs (`runs.start`) and use a shell: every bash call on the hosted (Linux) runtime needs it. | `runs.start` is refused with `403 scope_missing`. Ask for it in step 5 and list it in the run's `scopes`. |
72
+ | `crtr:schedule` | Create, pause, resume and run schedules. | Schedule calls are refused. |
73
+ | `crtr:memory:read:user`, `crtr:files:read:user:.`, `crtr:files:rw:user:.` | Read the person's documents beyond their public ones; read or write the person's files. | The run sees only the person's `public` documents and its own app space. |
74
+ | `sms:send` (a provider scope) | Call the `sms` provider's `send` tool, which texts the person's own verified phone. | `providers.call` is refused. |
75
+
76
+ #### Who can sign in: testing mode
77
+
78
+ A new app is in testing: it is registered as `verified: false`, only you and up to 100 testers can sign in to it, and it is not shown in the marketplace. You can sign in as yourself with no setup. To let someone else in, add them by the email or phone number on their Northlight account: `crtr cloud apps testers add --app-id <id> --email them@example.com` (or `--phone +15551234567`). An email matches only while it is verified on that account. They can sign in right away; nobody gets a message, so tell them. `testers list` shows who is on the list and `testers remove` takes someone off.
79
+
80
+ Anyone else who tries sees "<app> is still in testing", and your app's callback receives `error=access_denied` with `error_description=app_in_testing` instead of a `code`. [When sign-in is refused: apps in testing](../client.md#when-sign-in-is-refused-apps-in-testing) shows how to read it and what to show the person. Removing a tester stops their access at once, including their refresh tokens, and keeps their data; adding them back and signing in again restores it.
81
+
82
+ The `testers` leaves are `cloud` leaves like the rest, so they need a joined machine or your hosted chat agent (see [Manage an app with `crtr cloud apps`](#manage-an-app-with-crtr-cloud-apps)): a laptop cannot join production yet. Ask your chat agent to run the command, for example `crtr cloud apps testers add --app-id <id> --email them@example.com`.
83
+
84
+ When you are ready for everyone, email `support@northlight.ai` to have the app verified. Only Northlight staff can verify an app; registering never sets `verified: true`. Apps that existed before testing mode are already verified.
85
+
86
+ ### 6. Write the environment file
87
+
88
+ Create `local/app.env` by hand with the Client ID from step 4:
89
+
90
+ ```bash
91
+ CROUTER_ISSUER=https://account.northlight.ai
92
+ CROUTER_CLIENT_ID=<client id>
93
+ APP_ORIGIN=http://localhost:3000
94
+ CROUTER_KEY_FILE=local/app-key.json
95
+ ```
96
+
97
+ | Variable | Value |
98
+ |---|---|
99
+ | `CROUTER_ISSUER` | `https://account.northlight.ai` |
100
+ | `CROUTER_CLIENT_ID` | The Client ID from the app page |
101
+ | `APP_ORIGIN` | The Origin URL you registered. The redirect URI is `${APP_ORIGIN}/auth/callback` by convention; it must match a registered redirect URI exactly. |
102
+ | `CROUTER_KEY_FILE` | Path to the private key from step 3. Default `local/app-key.json` |
103
+
104
+ The file holds no secret, but keep it out of git with the key. On a machine joined to your account, `npx @north-light/sdk env --app <client id>` writes the first three for you.
105
+
106
+ ### 7. Run it locally
107
+
108
+ Create the config from those variables. The SDK reads no environment; your app passes the values in.
109
+
110
+ ```ts
111
+ import {readFile} from 'node:fs/promises';
112
+ import {OAuth2Client, MemoryConnectionStore} from '@north-light/sdk';
113
+
114
+ const store = new MemoryConnectionStore(); // one process only; use a persistent ConnectionStore in production
115
+ const oauth = new OAuth2Client({
116
+ clientId: process.env.CROUTER_CLIENT_ID!,
117
+ issuer: process.env.CROUTER_ISSUER!,
118
+ redirectUri: `${process.env.APP_ORIGIN}/auth/callback`,
119
+ privateKey: JSON.parse(await readFile(process.env.CROUTER_KEY_FILE ?? 'local/app-key.json', 'utf8')),
120
+ store,
121
+ });
122
+ ```
123
+
124
+ Start the server with the file loaded, for example `node --env-file=local/app.env server.js`.
125
+
126
+ **Sign in.** The login route redirects to Northlight; the callback exchanges the code on your server and stores the person's connection. This example uses Express and an in-memory session, which are only a sketch:
127
+
128
+ ```ts
129
+ import express from 'express';
130
+ import {randomBytes} from 'node:crypto';
131
+
132
+ const app = express();
133
+ const pending = new Map<string, {codeVerifier: string; nonce: string}>(); // keyed by state; use a real session
134
+
135
+ app.get('/auth/login', async (_req, res) => {
136
+ const {url, codeVerifier, state, nonce} = await oauth.authorizeUrl({
137
+ scopes: ['openid', 'profile', 'crtr:llm', 'crtr:act', 'sms:send'],
138
+ state: randomBytes(16).toString('base64url'),
139
+ });
140
+ pending.set(state, {codeVerifier, nonce});
141
+ res.redirect(303, url);
142
+ });
143
+
144
+ app.get('/auth/callback', async (req, res) => {
145
+ const state = String(req.query.state);
146
+ const saved = pending.get(state);
147
+ if (!saved || typeof req.query.code !== 'string') return void res.status(400).send('Start sign-in again.');
148
+ pending.delete(state);
149
+ const connection = await oauth.exchangeCode({
150
+ code: req.query.code, codeVerifier: saved.codeVerifier,
151
+ state, expectedState: state, nonce: saved.nonce,
152
+ });
153
+ await store.save(connection);
154
+ res.redirect(303, `/?user=${encodeURIComponent(connection.userId)}`); // set your own session instead
155
+ });
156
+
157
+ app.listen(3000);
158
+ ```
159
+
160
+ Open `http://localhost:3000/auth/login`. Northlight asks you to sign in and shows the consent page; choose Allow, and the browser returns to your callback with a `code` (and `iss`) in the query. Request only the scopes you ticked in step 5.
161
+
162
+ **Start a run and call the `sms` provider.** `oauth.client(userId)` opens a client on that person's runtime from their stored connection:
163
+
164
+ ```ts
165
+ const runtime = await oauth.client(userId);
166
+
167
+ const started = await runtime.runs.start({prompt: 'Reply with exactly the word: pong', scopes: ['crtr:llm', 'crtr:act']});
168
+ const run = await runtime.runs.wait(started.run_id); // status 'idle' once the reply is done
169
+
170
+ // Text the signed-in person's own phone through the sms provider.
171
+ const sent = await runtime.providers.call({
172
+ provider: 'sms',
173
+ tool: 'send',
174
+ arguments: {body: 'Hello from my first Northlight app.'},
175
+ });
176
+ // sent.message_id is the message's id
177
+ ```
178
+
179
+ `send` takes a required `body` (and an optional `file`). It has no recipient argument: it texts the verified phone of the person who signed in to your app, from Northlight's number, and allows at most 50 texts per person per UTC day. `runtime.providers.tools('sms')` lists the provider's tools. [Getting a result back from a run](#getting-a-result-back-from-a-run) covers `runs.parse`.
180
+
181
+ If a run or call throws because the person's connection ended, handle it in one place with `onRefusal`; see [Apps with many users](./apps-with-many-users.md).
182
+
183
+ ### 8. Test without a network
184
+
185
+ Test your app with `FakeCloud` from `@north-light/sdk/testing`. Your real `OAuth2Client` and `Crouter` talk to it through `fetch`, so tests never touch production or send a text. See [Testing your app](../testing.md).
186
+
187
+ ## Manage an app with `crtr cloud apps`
188
+
189
+ Everything in the console has a `crtr cloud apps` command: they are leaves of Northlight's `cloud` provider, and they run as you. Two things to know today:
190
+
191
+ - **A laptop cannot run them yet.** The `cloud` commands appear on a machine joined to your Northlight account, and production does not yet let a laptop join as an additional machine. That is coming. Until it ships, do the steps above in the console and write the env file by hand. Do not follow older instructions to open `/account/machines` or to run `crtr sys join` against production: that page does not exist yet. `keygen --app` and `npx @north-light/sdk env` need the same joined machine, so use `keygen` without `--app`, as in step 3.
192
+ - **Your Northlight chat agent can run them.** Open `https://chat.northlight.ai`, and ask your chat agent to run the command. It runs as you on your hosted runtime and prints the real output. Verified on production: `register`, `set-scopes`, `keys add`, `get`, `mine`, `asks`, `set-listing`, `set-requires` and `update`. For `keys add`, generate the key on your laptop as in step 3 and give the agent only the public JWK. Never ask it to generate your key: it would live on the hosted disk. If a command fails once with `launcher refused launch_failed`, run it again.
193
+
194
+ For example, ask the agent: "Run `crtr cloud apps register --name Texty --origin http://localhost:3000 --logo https://example.com/logo.png --justification "Texts you a summary of your day." --redirect-uri http://localhost:3000/auth/callback` and show me the raw output." Then `set-scopes --app-id <id> --scope crtr:llm --scope crtr:act --scope sms:send` and `keys add --app-id <id> --jwk '<public JWK>'`.
195
+
196
+ `--app-id` takes `app:<client id>` or the bare client id. The leaves:
197
+
198
+ | Command | What it does |
199
+ |---|---|
200
+ | `crtr cloud apps mine` | Lists the apps you manage. |
201
+ | `crtr cloud apps register --name … --origin … --logo … --justification … --redirect-uri …` | Registers an app and answers its registration, including `env` (the issuer, client id and origin for step 6). The `--redirect-uri` flag repeats. |
202
+ | `crtr cloud apps get --app-id <id>` | Shows the registration. |
203
+ | `crtr cloud apps update --app-id <id> --name … / --origin … / --logo … / --justification …` | Changes the profile fields you pass. |
204
+ | `crtr cloud apps set-redirect-uris --app-id <id> --redirect-uri …` | Replaces the whole redirect URI list; repeat the flag for each. |
205
+ | `crtr cloud apps asks --app-id <id>` | Lists what an app may ask for, with whether it asks now. Large sources such as `integrations` print as a count. Narrow it with `--query sms:send`, `--source sms`, `--group providers\|apps\|agents\|space`, or `--query <text>`; for example `crtr cloud apps asks --app-id <id> --source sms`. |
206
+ | `crtr cloud apps set-scopes --app-id <id> --scope …` | Replaces the scope list. Repeat `--scope` for every scope; `--clear` empties it; `crtr:act` needs `crtr:llm`. |
207
+ | `crtr cloud apps set-requires --app-id <id> --provider …` | Names providers your app does not work without (for example `context`); `--clear` empties it. |
208
+ | `crtr cloud apps keys add --app-id <id> --jwk '<public JWK>'` | Adds a signing key beside any existing one. Answers `created: false` if that key is already on the app. |
209
+ | `crtr cloud apps set-listing --app-id <id> --login-start-url … --text-welcome …` (also `--listed`, `--price`) | Sets the listing fields; pass at least one. A loopback `--login-start-url` is accepted. `--listed true` was not run during this guide's checks. |
210
+ | `crtr cloud apps testers add --app-id <id> --email …` (or `--phone …`) | Lets that person sign in while the app is in testing; at most 100. |
211
+ | `crtr cloud apps testers list --app-id <id>` | Lists the testers. |
212
+ | `crtr cloud apps testers remove --app-id <id> --email …` (or `--phone …`) | Removes a tester and revokes their grants for the app. |
213
+ | `crtr cloud apps delete --app-id <id> --confirm <client id>` | Deletes the app and removes every person's grant for it. Only you at a joined machine's terminal can run it: an agent, including your chat agent, is refused with `provider_refused`. |
214
+
215
+ ### Deleting an app
216
+
217
+ The console has no control for deleting an app. `crtr cloud apps delete` is the way, and it runs only from a joined machine's terminal, which a laptop cannot be yet. Until then, email `support@northlight.ai` with the client id: Northlight staff can delete an app with an operator command. Deleting removes every person's grant for the app, so each runtime deletes the app's runs, space and schedules, and the app's next refresh for them fails with `invalid_grant`. It cannot be undone, and apps that hold the provider role cannot be deleted. Reusing an app for tests is usually simpler: change its redirect URIs and keys instead.
218
+
219
+ ### Let people find and install it
220
+
221
+ `login_start_url` is where Northlight starts sign-in for a person who installs your app from a link. In the console, open **Profile**, and under **Marketplace** set **Sign-in start URL**, **Text welcome** (one or two plain sentences, at most 120 characters, no links) and **Listed in the marketplace**. Your `/auth/login` route from step 7 is a valid start URL. Agents can install an app for people by text only while it is listed.
222
+
223
+ ### Rotate keys
224
+
225
+ Add a new key beside the old one, switch, verify with a real sign-in, then retire the old key. Use `keygen --out local/app-key-next.json` without `--app`, paste its public JWK with **Add key** on the **Sign-in** tab, and retire the old one there. [Rotating your key](../getting-started.md#rotating-your-key) has the full sequence.
226
+
10
227
  ## Getting a result back from a run
11
228
 
12
229
  Pass an `output_schema` and call `runs.parse`. The runtime arms the schema on the run's root agent. The agent must submit a value that validates against it. The run settles with that value, and `runs.parse` returns it as `output_parsed`, typed from your schema:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@north-light/cli",
3
- "version": "0.3.419",
3
+ "version": "0.3.421",
4
4
  "description": "crtr — agent runtime with memory, plugins, and marketplaces",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -134,7 +134,7 @@
134
134
  },
135
135
  "license": "UNLICENSED",
136
136
  "dependencies": {
137
- "@north-light/identity": "0.3.419",
137
+ "@north-light/identity": "0.3.421",
138
138
  "@earendil-works/pi-agent-core": "1.1.0",
139
139
  "@earendil-works/pi-ai": "1.1.0",
140
140
  "@earendil-works/pi-coding-agent": "1.1.0",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@north-light/identity",
3
- "version": "0.3.419",
3
+ "version": "0.3.421",
4
4
  "description": "crouter cloud access-token verification and scope rules",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
package/runtime.lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@north-light/cli",
3
- "version": "0.3.419",
3
+ "version": "0.3.421",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@north-light/cli",
9
- "version": "0.3.419",
9
+ "version": "0.3.421",
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.419",
21
+ "@north-light/identity": "0.3.421",
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.419",
12098
+ "version": "0.3.421",
12099
12099
  "license": "UNLICENSED"
12100
12100
  },
12101
12101
  "packages/crouter-env-docker": {
12102
12102
  "name": "@north-light/env-docker",
12103
- "version": "0.3.419",
12103
+ "version": "0.3.421",
12104
12104
  "license": "UNLICENSED"
12105
12105
  },
12106
12106
  "packages/crouter-identity": {
12107
12107
  "name": "@north-light/identity",
12108
- "version": "0.3.419",
12108
+ "version": "0.3.421",
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.419",
12116
+ "version": "0.3.421",
12117
12117
  "license": "UNLICENSED",
12118
12118
  "dependencies": {
12119
- "@north-light/api": "0.3.419",
12120
- "@north-light/identity": "0.3.419"
12119
+ "@north-light/api": "0.3.421",
12120
+ "@north-light/identity": "0.3.421"
12121
12121
  }
12122
12122
  },
12123
12123
  "packages/crouter-sdk": {
12124
12124
  "name": "@north-light/sdk",
12125
- "version": "0.3.419",
12125
+ "version": "0.3.421",
12126
12126
  "license": "UNLICENSED",
12127
12127
  "dependencies": {
12128
- "@north-light/api": "0.3.419",
12129
- "@north-light/identity": "0.3.419",
12128
+ "@north-light/api": "0.3.421",
12129
+ "@north-light/identity": "0.3.421",
12130
12130
  "jose": "^6.2.1"
12131
12131
  },
12132
12132
  "bin": {