@north-light/cli 0.3.413 → 0.3.415
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/api/client.d.ts +20 -0
- package/dist/api/client.js +3 -3
- package/dist/api/dto/common.d.ts +1 -1
- package/dist/api/dto/crons.d.ts +7 -1
- package/dist/api/dto/human-requests.d.ts +1 -1
- package/dist/api/dto/run-events.d.ts +1 -1
- package/dist/api/dto/usage.d.ts +27 -0
- package/dist/api/dto/usage.js +0 -0
- package/dist/api/errors.js +1 -1
- package/dist/api/index.d.ts +3 -2
- package/dist/api/index.js +1 -1
- package/dist/builtin-memory/crouter-sdk/README.md +22 -4
- package/dist/builtin-memory/crouter-sdk/client.md +32 -1
- package/dist/builtin-memory/crouter-sdk/errors.md +3 -0
- package/dist/builtin-memory/crouter-sdk/getting-started.md +26 -4
- package/dist/builtin-memory/crouter-sdk/release-notes.md +23 -0
- package/dist/builtin-memory/crouter-sdk/testing.md +9 -3
- package/dist/builtin-memory/internal/nodes-and-canvas.md +4 -0
- package/dist/clients/attach/viewer.js +606 -606
- package/dist/commands/cron.js +1 -1
- package/dist/commands/node/create.js +1 -1
- package/dist/core/canvas/crons.d.ts +15 -1
- package/dist/core/canvas/crons.js +13 -13
- package/dist/core/canvas/delivery-contract.d.ts +1 -1
- package/dist/core/canvas/migrations.js +73 -66
- package/dist/core/canvas/types.d.ts +1 -1
- package/dist/core/command.d.ts +7 -0
- package/dist/core/command.js +9 -9
- package/dist/core/grants/mirror.d.ts +4 -0
- package/dist/core/grants/mirror.js +2 -2
- package/dist/core/layout.d.ts +2 -0
- package/dist/core/layout.js +2 -2
- package/dist/core/llm/run.d.ts +5 -1
- package/dist/core/llm/run.js +3 -3
- package/dist/core/providers/call.d.ts +2 -0
- package/dist/core/providers/call.js +1 -1
- package/dist/core/providers/errors.js +1 -1
- package/dist/core/providers/receipts.d.ts +2 -0
- package/dist/core/providers/receipts.js +2 -3
- package/dist/core/runs/operations.d.ts +2 -0
- package/dist/core/runs/operations.js +4 -4
- package/dist/core/runtime/broker/model-adapter.js +2 -2
- package/dist/core/runtime/run-command.d.ts +6 -0
- package/dist/core/runtime/run-command.js +1 -1
- package/dist/core/spaces/mounts.d.ts +2 -0
- package/dist/core/spaces/mounts.js +1 -1
- package/dist/core/spaces/process.d.ts +14 -2
- package/dist/core/spaces/process.js +1 -1
- package/dist/core/usage/ledger.d.ts +49 -0
- package/dist/core/usage/ledger.js +10 -0
- package/dist/daemon/api/app-listener.js +12 -12
- package/dist/daemon/api/handlers/crons.js +1 -1
- package/dist/daemon/api/handlers/llm.d.ts +8 -0
- package/dist/daemon/api/handlers/llm.js +3 -3
- package/dist/daemon/api/map.d.ts +1 -1
- package/dist/daemon/api/map.js +1 -1
- package/dist/daemon/api/operations.js +1 -1
- package/dist/daemon/cron/armed-context.d.ts +21 -4
- package/dist/daemon/cron/armed-context.js +1 -1
- package/dist/daemon/cron/authority.d.ts +27 -0
- package/dist/daemon/cron/authority.js +1 -0
- package/dist/daemon/cron/disposition.d.ts +2 -0
- package/dist/daemon/cron/disposition.js +1 -1
- package/dist/daemon/cron/escalation.js +3 -3
- package/dist/daemon/cron/folders.d.ts +5 -0
- package/dist/daemon/cron/folders.js +1 -0
- package/dist/daemon/cron/lease-recovery.js +1 -1
- package/dist/daemon/cron/settle.d.ts +10 -0
- package/dist/daemon/cron/settle.js +1 -1
- package/dist/daemon/cron/sinks.d.ts +2 -1
- package/dist/daemon/cron/sinks.js +2 -2
- package/dist/daemon/cron-run.js +1 -1
- package/dist/daemon/reconcilers/action-lane.js +1 -1
- package/dist/daemon/reconcilers/broker-supervision.d.ts +10 -0
- package/dist/daemon/reconcilers/broker-supervision.js +4 -4
- package/dist/daemon/reconcilers/cron-lane.d.ts +1 -0
- package/dist/daemon/reconcilers/cron-lane.js +1 -1
- package/docs/sdk/README.md +22 -4
- package/docs/sdk/client.md +32 -1
- package/docs/sdk/errors.md +3 -0
- package/docs/sdk/getting-started.md +26 -4
- package/docs/sdk/release-notes.md +23 -0
- package/docs/sdk/testing.md +9 -3
- package/package.json +3 -3
- package/packages/crouter-identity/dist/index.d.ts +29 -2
- package/packages/crouter-identity/dist/index.js +81 -12
- package/packages/crouter-identity/package.json +1 -1
- package/runtime.lock.json +12 -12
package/dist/commands/cron.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
var W=Object.defineProperty;var o=(n,e)=>W(n,"name",{value:e,configurable:!0});import{envNodeId as N,envProfileId as U}from"../shared/env.js";import{existsSync as M,statSync as Y}from"node:fs";import{resolve as H}from"node:path";import{defineBranch as B,defineLeaf as h}from"../core/command.js";import{InputError as c,readStdinRaw as V}from"../core/io.js";import{parseWhen as z,parseCadence as Z,cadenceDisplay as k}from"../core/wake.js";import{profilesStateBlock as j,resolveProfileViaDaemon as J}from"./profile/daemon-profiles.js";import{cliClient as l,rethrowAsCliError as u}from"./api-client.js";async function L(n){return(await J(n.trim())).id}o(L,"canonicalProfileId");function b(n,e){return e!==void 0&&e.trim()!==""?{tz:e,now:n}:{now:n}}o(b,"parseOpts");function $(n){const e={wake_in_past:'Pick a future instant \u2014 a positive duration ("5m","2h") or an ISO time later than now.',bad_when:'Use a duration ("5m","1h30m"), a zoned ISO ("2026-06-07T09:00:00Z"), or a bare ISO ("2026-06-07T09:00").',bad_cadence:'Use a duration ("6h"), a 5-field cron ("0 9 * * *"), or an @alias ("@daily").',unknown_zone:'Pass --tz with an IANA zone name (e.g. "America/New_York").',cadence_too_fast:'Use a cadence of at least 60s (e.g. "1m","5m","1h").'};throw new c({error:n.code,message:n.message,received:n.received,next:e[n.code]??"Fix the time value and retry."})}o($,"throwTimeError");function G(n){const e=/^(\d+)(s|m|h)?$/.exec(n.trim());if(e===null)throw new c({error:"bad_run_timeout",message:`"${n}" is not a valid run timeout`,field:"run-timeout",next:'Use a duration like "90s", "5m", "1h", or bare seconds ("300").'});const r=Number(e[1]),t=e[2]==="h"?3600:e[2]==="m"?60:1,i=r*t;if(i<=0)throw new c({error:"bad_run_timeout",message:"run timeout must be positive",field:"run-timeout",next:'Use a positive duration, e.g. "5m".'});return i}o(G,"parseRunTimeout");function f(n){const e=n??U();return e!==void 0&&e.trim()!==""?{profile:e}:void 0}o(f,"scopeQuery");function Q(n){if(n===void 0)return process.cwd();const e=H(n);if(!M(e)||!Y(e).isDirectory())throw new c({error:"bad_cwd",message:`${e} is not a directory`,field:"cwd",next:"Pass --cwd with an existing directory, or omit it to inherit this one."});return e}o(Q,"resolveCwd");function X(n){const e=Array.isArray(n)?n.map(String):typeof n=="string"&&n!==""?[n]:[];if(e.length===0)return null;const r={};for(const t of e){const i=t.indexOf("="),s=i===-1?"":t.slice(0,i);if(!/^[A-Za-z_][A-Za-z0-9_]*$/.test(s))throw new c({error:"bad_env",message:`"${t}" is not KEY=VALUE`,field:"env",next:"Pass --env KEY=VALUE (repeat the flag per variable); KEY must be a shell-legal name."});r[s]=t.slice(i+1)}return JSON.stringify(r)}o(X,"resolveEnvJson");function p(n){const e=typeof n.cronId=="string"?n.cronId.trim():"";if(e==="")throw new c({error:"missing_cron_id",message:"a cron id is required",field:"cron-id",next:"Pass the cron id from `crtr cron list`."});return e}o(p,"requireCronId");function ee(n){if(n===null)return"never run";const e=n.finished??"",r=n.exit_code===0?"ok":n.exit_code===-1?"TIMEOUT":n.exit_code===null?"no exit":`exit ${n.exit_code}`;return e===""?r:`${r} ${e}`}o(ee,"healthDisplay");function ne(n){const e=n.exit_code===null?"no exit":n.exit_code===-1?"TIMEOUT":`exit ${n.exit_code}`,r=n.duration_ms!==null?`${n.duration_ms}ms`:"\u2014";return`- ${n.started} \u2014 ${e}, ${r} \u2014 ${n.delivered??"(no disposition recorded)"}`}o(ne,"renderRun");const te=h({name:"add",description:"arm a scheduled command (bash on stdin; one of --at / --every)",whenToUse:"you need to arm a new bash schedule, rather than inspect or control an existing cron.",help:{name:"cron add",summary:"arm a scheduled command",params:[{kind:"stdin",name:"command",required:!0,constraint:"Arbitrary bash the daemon runs at each fire. Pipe it from a single-quoted heredoc (`<<'EOF'`) to preserve literal bytes. Runs via `bash -c` in this cwd with $CRTR_CRON_ID, $CRTR_CRON_NAME, and $CRTR_RUN_ID in the env ($CRTR_ANCHOR too when anchored). The scheduler does not interpret it as a node action; a bash predicate can decide whether this fire acts, not arm a native predicate trigger. A run that must branch on canvas state should call `crtr --json <cmd>` and read the leaf's declared outputs, rather than parsing the prose output."},{kind:"flag",name:"name",type:"string",required:!0,constraint:"Agent-legible label \u2014 how the cron shows in `cron list`."},{kind:"flag",name:"at",type:"string",required:!1,constraint:`One-shot: run the stored bash once at <when> \u2014 a duration ("90s","1h30m"), a zoned ISO ("2026-06-07T09:00:00Z"), or a bare ISO ("2026-06-07T09:00", interpreted in --tz else host zone). Exactly one of --at / --every is required. The row is deleted after its run unless the run's disposition pauses it.`},{kind:"flag",name:"every",type:"string",required:!1,constraint:'Recurring: run the stored bash at each cadence \u2014 a fixed interval ("6h" \u2014 first fire one interval from now), a 5-field cron ("0 9 * * *"), or an @alias ("@daily"). Each fire is another bash run. Minimum cadence 60s. Exactly one of --at / --every is required. Use this for a bash condition that only the local filesystem knows: if it exits 75, the next cadence re-checks it. A host platform can also re-check it early by calling the daemon\'s `POST /v1/crons/poke` endpoint.'},{kind:"flag",name:"tz",type:"string",required:!1,constraint:'IANA zone (e.g. "America/New_York") for a bare-ISO --at or a calendar --every. Defaults to the host zone.'},{kind:"flag",name:"on-output",type:"enum",choices:["silent","on-failure","always","on-change"],required:!1,constraint:"What each run does with its output. Defaults to on-change for a node:<id> sink; otherwise on-failure. silent: record in the run log, tell nobody \u2014 never escalate. on-failure: no success delivery; a failing run pauses the cron and spawns a node to deal with it. always: deliver stdout every run to --sink at --tier. on-change: deliver stdout only when it differs from the previous run's \u2014 the shape polling wants."},{kind:"flag",name:"sink",type:"string",required:!1,constraint:`Where always/on-change stdout deliveries go: "node:<id>" (an existing node's inbox \u2014 must exist now; gone/finalized at fire time is a failure), "spawn:<kind>" (each delivered output creates a self-finishing node with stdout as kickoff \u2014 a managed child while its creator exists, otherwise a terminal parentless root), or "human" (the humanloop inbox). Required by always/on-change, rejected otherwise.`},{kind:"flag",name:"tier",type:"enum",choices:["normal","urgent","critical"],required:!1,default:"urgent",constraint:"Inbox priority of a node-sink delivery. urgent (default): steers the target mid-turn, or wakes it when dormant \u2014 a fire is news the node asked for on a schedule, so it should land now. normal: waits for the running turn to settle. critical: aborts the running turn outright."},{kind:"flag",name:"expires",type:"string",required:!1,constraint:"Clock bound (same grammar as --at): the row is deleted once this instant passes. The self-limiting half of a poll-until pattern. For a held one-shot (last run exited 75), expiry bounds the wait for eligibility \u2014 the row is deleted UNFIRED, never fired blind at expiry."},{kind:"flag",name:"anchor",type:"string",required:!1,constraint:"Couple the cron's lifetime to a node: the row is deleted when that node is deleted. Without an anchor it outlives every node; it ends through its one-shot run, --expires, explicit cancellation, or self-cancellation from its bash."},{kind:"flag",name:"anchor-self",type:"bool",required:!1,constraint:"Anchor to the calling node ($CRTR_NODE_ID). Errors outside a node."},{kind:"flag",name:"cancel-on-wake",type:"bool",required:!1,constraint:"Also delete the row when the anchor node revives: use only when the cron belongs to that current dormancy, not for standing work. Requires an anchor; at most one cancel-on-wake cron per anchor \u2014 arming a second replaces the anchor\u2019s pending one."},{kind:"flag",name:"run-timeout",type:"string",required:!1,default:"5m",constraint:'Kill a run that exceeds this ("90s","5m","1h", or bare seconds). A timeout is a failure.'},{kind:"flag",name:"overlap",type:"enum",choices:["skip","queue","replace"],required:!1,default:"skip",constraint:"A fire landing while the previous run is still going. skip (default): log the skip, do not start a second run. queue: run once more after the current finishes (depth 1; further fires collapse). replace: kill the running instance and start fresh."},{kind:"flag",name:"cwd",type:"path",required:!1,constraint:"Directory every run executes in. Defaults to this one, must exist now, and is snapshotted onto the row \u2014 a cron never re-resolves its context. In a hosted runtime, runs are sandboxed and see only the app's own folders: an omitted --cwd that is outside them becomes the node's directory or the app files directory, and an explicit --cwd outside them is refused now (`cron show` prints the directory the row got)."},{kind:"flag",name:"env",type:"string",required:!1,repeatable:!0,constraint:"KEY=VALUE added to every run's env (repeat per variable). Snapshotted at arm time; overlays the operational env, alongside $CRTR_CRON_ID/$CRTR_CRON_NAME/$CRTR_RUN_ID."},{kind:"flag",name:"profile",type:"string",required:!1,constraint:"Profile the runs resolve config and credentials under, and \u2014 under --scope profile \u2014 the profile this cron lists and cancels within. A profile id or manifest name from the <profiles> list below; a close-enough name resolves to the nearest profile. Defaults to the calling node's ($CRTR_PROFILE_ID)."},{kind:"flag",name:"scope",type:"enum",choices:["profile","global"],required:!1,constraint:"Who lists and cancels this cron: profile (default when a profile is resolved) confines it to that profile; global makes it canvas-home-wide, visible from every profile."}],output:[{name:"cron_id",type:"string",required:!0,constraint:"The server-minted armed cron id \u2014 inspect with `cron show <cron_id>`, cancel with `cron cancel <cron_id>`."},{name:"fire_at",type:"string",required:!0,constraint:"Next fire (UTC ISO)."},{name:"recur",type:"string",required:!0,constraint:'Cadence display; "none" for a one-shot.'}],outputKind:"object",effects:["Arms one cron row (daemon-run; survives this process and every node).","Each fire runs the command via bash in this cwd and records a run-log entry.","A failing run (nonzero exit, timeout, undeliverable sink) pauses the cron and spawns a node to deal with it \u2014 unless --on-output silent."]},helpState:{leaf:j},run:o(async n=>{let e=typeof n.command=="string"?n.command.trim():"";if(e===""&&(e=(await V()).trim()),e==="")throw new c({error:"missing_command",message:"no command on stdin",field:"command",next:"Pipe the bash command on stdin (e.g. from a single-quoted heredoc)."});const r=typeof n.name=="string"?n.name.trim():"";if(r==="")throw new c({error:"missing_name",message:"--name is required",field:"name",next:"Pass --name <label>."});const t=typeof n.at=="string"&&n.at!==""?n.at:void 0,i=typeof n.every=="string"&&n.every!==""?n.every:void 0;if(t===void 0==(i===void 0))throw new c({error:"bad_schedule",message:t===void 0?"one of --at / --every is required":"--at and --every are mutually exclusive",next:"Pass exactly one: --at <when> for a one-shot, or --every <cadence> for a recurrence."});const s=typeof n.tz=="string"&&n.tz!==""?n.tz:void 0,m=new Date;let d,T=null;if(i!==void 0){const a=Z(i,b(m,s));"error"in a&&$(a.error),d=a.firstFireAt,T=a.recur}else{const a=z(t,b(m,s));"error"in a&&$(a.error),d=a.fireAt}let C=null;const R=typeof n.expires=="string"&&n.expires!==""?n.expires:void 0;if(R!==void 0){const a=z(R,b(m,s));"error"in a&&$(a.error),C=a.fireAt}const q=typeof n.anchor=="string"&&n.anchor!==""?n.anchor:void 0,A=n.anchorSelf===!0;if(q!==void 0&&A)throw new c({error:"bad_anchor",message:"--anchor and --anchor-self are mutually exclusive",next:"Pass one anchor or the other."});let w=q??null;if(A){const a=N();if(a===void 0||a==="")throw new c({error:"no_self_node",message:"--anchor-self requires a calling node ($CRTR_NODE_ID is unset)",next:"Run from inside a node, or pass --anchor <node-id> explicitly."});w=a}const I=n.cancelOnWake===!0;if(I&&w===null)throw new c({error:"cancel_on_wake_needs_anchor",message:"--cancel-on-wake requires an anchor",next:"Add --anchor <node-id> or --anchor-self."});const O=typeof n.runTimeout=="string"&&n.runTimeout!==""?G(n.runTimeout):void 0,S=typeof n.cwd=="string"&&n.cwd!==""?n.cwd:void 0,g=N(),E=g?await l().getNode(g).catch(u):null,K=Q(S??(E?.isolated?E.cwd:void 0)),F=X(n.env),D=typeof n.profile=="string"&&n.profile!==""?n.profile:void 0,y=U(),_=D!==void 0?await L(D):y!==void 0&&y.trim()!==""?y:void 0,P=n.scope??(_!==void 0?"profile":"global");if(P==="profile"&&_===void 0)throw new c({error:"scope_needs_profile",message:"--scope profile names the profile this cron lists and cancels within, but no profile is resolved",field:"profile",next:"Pass --profile <id>, or use --scope global for a canvas-home-wide cron."});const v=g,x=await l().armCron({name:r,command:e,fire_at:d,recur:T,tz:s??null,created_by:v!==void 0&&v!==""?v:null,cwd:K,...S===void 0?{cwd_is_default:!0}:{},env_json:F,profile:_??null,scope:P,on_output:n.onOutput,sink:typeof n.sink=="string"&&n.sink!==""?n.sink:null,tier:n.tier??"urgent",expires_at:C,anchor_node:w,cancel_on_wake:I,...O!==void 0?{run_timeout_s:O}:{},overlap:n.overlap??"skip"}).catch(u);return{cron_id:x.cron_id,fire_at:x.fire_at,recur:k(x.recur)}},"run"),render:o(n=>{const e=String(n.recur),r=e==="none"?"one-shot (row deleted after the run)":e;return`Cron armed \u2014 ${String(n.cron_id)}.
|
|
1
|
+
var W=Object.defineProperty;var o=(n,e)=>W(n,"name",{value:e,configurable:!0});import{envNodeId as P,envProfileId as U}from"../shared/env.js";import{existsSync as M,statSync as H}from"node:fs";import{resolve as Y}from"node:path";import{defineBranch as B,defineLeaf as h}from"../core/command.js";import{InputError as c,readStdinRaw as V}from"../core/io.js";import{parseWhen as z,parseCadence as Z,cadenceDisplay as k}from"../core/wake.js";import{profilesStateBlock as j,resolveProfileViaDaemon as J}from"./profile/daemon-profiles.js";import{cliClient as l,rethrowAsCliError as u}from"./api-client.js";async function L(n){return(await J(n.trim())).id}o(L,"canonicalProfileId");function b(n,e){return e!==void 0&&e.trim()!==""?{tz:e,now:n}:{now:n}}o(b,"parseOpts");function $(n){const e={wake_in_past:'Pick a future instant \u2014 a positive duration ("5m","2h") or an ISO time later than now.',bad_when:'Use a duration ("5m","1h30m"), a zoned ISO ("2026-06-07T09:00:00Z"), or a bare ISO ("2026-06-07T09:00").',bad_cadence:'Use a duration ("6h"), a 5-field cron ("0 9 * * *"), or an @alias ("@daily").',unknown_zone:'Pass --tz with an IANA zone name (e.g. "America/New_York").',cadence_too_fast:'Use a cadence of at least 60s (e.g. "1m","5m","1h").'};throw new c({error:n.code,message:n.message,received:n.received,next:e[n.code]??"Fix the time value and retry."})}o($,"throwTimeError");function G(n){const e=/^(\d+)(s|m|h)?$/.exec(n.trim());if(e===null)throw new c({error:"bad_run_timeout",message:`"${n}" is not a valid run timeout`,field:"run-timeout",next:'Use a duration like "90s", "5m", "1h", or bare seconds ("300").'});const r=Number(e[1]),t=e[2]==="h"?3600:e[2]==="m"?60:1,i=r*t;if(i<=0)throw new c({error:"bad_run_timeout",message:"run timeout must be positive",field:"run-timeout",next:'Use a positive duration, e.g. "5m".'});return i}o(G,"parseRunTimeout");function f(n){const e=n??U();return e!==void 0&&e.trim()!==""?{profile:e}:void 0}o(f,"scopeQuery");function Q(n){if(n===void 0)return process.cwd();const e=Y(n);if(!M(e)||!H(e).isDirectory())throw new c({error:"bad_cwd",message:`${e} is not a directory`,field:"cwd",next:"Pass --cwd with an existing directory, or omit it to inherit this one."});return e}o(Q,"resolveCwd");function X(n){const e=Array.isArray(n)?n.map(String):typeof n=="string"&&n!==""?[n]:[];if(e.length===0)return null;const r={};for(const t of e){const i=t.indexOf("="),s=i===-1?"":t.slice(0,i);if(!/^[A-Za-z_][A-Za-z0-9_]*$/.test(s))throw new c({error:"bad_env",message:`"${t}" is not KEY=VALUE`,field:"env",next:"Pass --env KEY=VALUE (repeat the flag per variable); KEY must be a shell-legal name."});r[s]=t.slice(i+1)}return JSON.stringify(r)}o(X,"resolveEnvJson");function p(n){const e=typeof n.cronId=="string"?n.cronId.trim():"";if(e==="")throw new c({error:"missing_cron_id",message:"a cron id is required",field:"cron-id",next:"Pass the cron id from `crtr cron list`."});return e}o(p,"requireCronId");function ee(n){if(n===null)return"never run";const e=n.finished??"",r=n.exit_code===0?"ok":n.exit_code===-1?"TIMEOUT":n.exit_code===null?"no exit":`exit ${n.exit_code}`;return e===""?r:`${r} ${e}`}o(ee,"healthDisplay");function ne(n){const e=n.exit_code===null?"no exit":n.exit_code===-1?"TIMEOUT":`exit ${n.exit_code}`,r=n.duration_ms!==null?`${n.duration_ms}ms`:"\u2014";return`- ${n.started} \u2014 ${e}, ${r} \u2014 ${n.delivered??"(no disposition recorded)"}`}o(ne,"renderRun");const te=h({name:"add",description:"arm a scheduled command (bash on stdin; one of --at / --every)",whenToUse:"you need to arm a new bash schedule, rather than inspect or control an existing cron.",help:{name:"cron add",summary:"arm a scheduled command",params:[{kind:"stdin",name:"command",required:!0,constraint:"Arbitrary bash the daemon runs at each fire. Pipe it from a single-quoted heredoc (`<<'EOF'`) to preserve literal bytes. Runs via `bash -c` in this cwd with $CRTR_CRON_ID, $CRTR_CRON_NAME, and $CRTR_RUN_ID in the env ($CRTR_ANCHOR and the anchor node's $CRTR_CONTEXT_DIR too when anchored; an unanchored cron has no context dir). Use absolute paths: `~` is the app's HOME, not the arming node's. The scheduler does not interpret it as a node action; a bash predicate can decide whether this fire acts, not arm a native predicate trigger. A run that must branch on canvas state should call `crtr --json <cmd>` and read the leaf's declared outputs, rather than parsing the prose output."},{kind:"flag",name:"name",type:"string",required:!0,constraint:"Agent-legible label \u2014 how the cron shows in `cron list`."},{kind:"flag",name:"at",type:"string",required:!1,constraint:`One-shot: run the stored bash once at <when> \u2014 a duration ("90s","1h30m"), a zoned ISO ("2026-06-07T09:00:00Z"), or a bare ISO ("2026-06-07T09:00", interpreted in --tz else host zone). Exactly one of --at / --every is required. The row is deleted after its run unless the run's disposition pauses it.`},{kind:"flag",name:"every",type:"string",required:!1,constraint:'Recurring: run the stored bash at each cadence \u2014 a fixed interval ("6h" \u2014 first fire one interval from now), a 5-field cron ("0 9 * * *"), or an @alias ("@daily"). Each fire is another bash run. Minimum cadence 60s. Exactly one of --at / --every is required. Use this for a bash condition that only the local filesystem knows: if it exits 75, the next cadence re-checks it. A host platform can also re-check it early by calling the daemon\'s `POST /v1/crons/poke` endpoint.'},{kind:"flag",name:"tz",type:"string",required:!1,constraint:'IANA zone (e.g. "America/New_York") for a bare-ISO --at or a calendar --every. Defaults to the host zone.'},{kind:"flag",name:"on-output",type:"enum",choices:["silent","on-failure","always","on-change"],required:!1,constraint:"What each run does with its output. Defaults to on-change for a node:<id> sink; otherwise on-failure. silent: record in the run log, tell nobody \u2014 never escalate. on-failure: no success delivery; a failing run pauses the cron and spawns a node to deal with it. always: deliver stdout every run to --sink at --tier. on-change: deliver stdout only when it differs from the previous run's \u2014 the shape polling wants."},{kind:"flag",name:"sink",type:"string",required:!1,constraint:`Where always/on-change stdout deliveries go: "node:<id>" (an existing node's inbox \u2014 must exist now; gone/finalized at fire time is a failure), "spawn:<kind>" (each delivered output creates a self-finishing node with stdout as kickoff \u2014 a managed child while its creator exists, otherwise a terminal parentless root), or "human" (the humanloop inbox). Required by always/on-change, rejected otherwise.`},{kind:"flag",name:"tier",type:"enum",choices:["normal","urgent","critical"],required:!1,default:"urgent",constraint:"Inbox priority of a node-sink delivery. urgent (default): steers the target mid-turn, or wakes it when dormant \u2014 a fire is news the node asked for on a schedule, so it should land now. normal: waits for the running turn to settle. critical: aborts the running turn outright."},{kind:"flag",name:"expires",type:"string",required:!1,constraint:"Clock bound (same grammar as --at): the row is deleted once this instant passes. The self-limiting half of a poll-until pattern. For a held one-shot (last run exited 75), expiry bounds the wait for eligibility \u2014 the row is deleted UNFIRED, never fired blind at expiry."},{kind:"flag",name:"anchor",type:"string",required:!1,constraint:"Couple the cron's lifetime to a node: the row is deleted when that node is deleted. Without an anchor it outlives every node; it ends through its one-shot run, --expires, explicit cancellation, or self-cancellation from its bash."},{kind:"flag",name:"anchor-self",type:"bool",required:!1,constraint:"Anchor to the calling node ($CRTR_NODE_ID). Errors outside a node."},{kind:"flag",name:"cancel-on-wake",type:"bool",required:!1,constraint:"Also delete the row when the anchor node revives: use only when the cron belongs to that current dormancy, not for standing work. Requires an anchor; at most one cancel-on-wake cron per anchor \u2014 arming a second replaces the anchor\u2019s pending one."},{kind:"flag",name:"run-timeout",type:"string",required:!1,default:"5m",constraint:'Kill a run that exceeds this ("90s","5m","1h", or bare seconds). A timeout is a failure.'},{kind:"flag",name:"overlap",type:"enum",choices:["skip","queue","replace"],required:!1,default:"skip",constraint:"A fire landing while the previous run is still going. skip (default): log the skip, do not start a second run. queue: run once more after the current finishes (depth 1; further fires collapse). replace: kill the running instance and start fresh."},{kind:"flag",name:"cwd",type:"path",required:!1,constraint:"Directory every run executes in. Defaults to this one, must exist now, and is snapshotted onto the row \u2014 a cron never re-resolves its context. In a hosted runtime, runs are sandboxed and see only the app's own folders: an omitted --cwd that is outside them becomes the node's directory or the app files directory, and an explicit --cwd outside them is refused now (`cron show` prints the directory the row got)."},{kind:"flag",name:"env",type:"string",required:!1,repeatable:!0,constraint:"KEY=VALUE added to every run's env (repeat per variable). Snapshotted at arm time; overlays the operational env, alongside $CRTR_CRON_ID/$CRTR_CRON_NAME/$CRTR_RUN_ID."},{kind:"flag",name:"profile",type:"string",required:!1,constraint:"Profile the runs resolve config and credentials under, and \u2014 under --scope profile \u2014 the profile this cron lists and cancels within. A profile id or manifest name from the <profiles> list below; a close-enough name resolves to the nearest profile. Defaults to the calling node's ($CRTR_PROFILE_ID)."},{kind:"flag",name:"scope",type:"enum",choices:["profile","global"],required:!1,constraint:"Who lists and cancels this cron: profile (default when a profile is resolved) confines it to that profile; global makes it canvas-home-wide, visible from every profile."}],output:[{name:"cron_id",type:"string",required:!0,constraint:"The server-minted armed cron id \u2014 inspect with `cron show <cron_id>`, cancel with `cron cancel <cron_id>`."},{name:"fire_at",type:"string",required:!0,constraint:"Next fire (UTC ISO)."},{name:"recur",type:"string",required:!0,constraint:'Cadence display; "none" for a one-shot.'}],outputKind:"object",effects:["Arms one cron row (daemon-run; survives this process and every node).","Each fire runs the command via bash in this cwd and records a run-log entry.","A failing run (nonzero exit, timeout, undeliverable sink) pauses the cron and spawns a node to deal with it \u2014 unless --on-output silent."]},helpState:{leaf:j},run:o(async n=>{let e=typeof n.command=="string"?n.command.trim():"";if(e===""&&(e=(await V()).trim()),e==="")throw new c({error:"missing_command",message:"no command on stdin",field:"command",next:"Pipe the bash command on stdin (e.g. from a single-quoted heredoc)."});const r=typeof n.name=="string"?n.name.trim():"";if(r==="")throw new c({error:"missing_name",message:"--name is required",field:"name",next:"Pass --name <label>."});const t=typeof n.at=="string"&&n.at!==""?n.at:void 0,i=typeof n.every=="string"&&n.every!==""?n.every:void 0;if(t===void 0==(i===void 0))throw new c({error:"bad_schedule",message:t===void 0?"one of --at / --every is required":"--at and --every are mutually exclusive",next:"Pass exactly one: --at <when> for a one-shot, or --every <cadence> for a recurrence."});const s=typeof n.tz=="string"&&n.tz!==""?n.tz:void 0,m=new Date;let d,T=null;if(i!==void 0){const a=Z(i,b(m,s));"error"in a&&$(a.error),d=a.firstFireAt,T=a.recur}else{const a=z(t,b(m,s));"error"in a&&$(a.error),d=a.fireAt}let R=null;const C=typeof n.expires=="string"&&n.expires!==""?n.expires:void 0;if(C!==void 0){const a=z(C,b(m,s));"error"in a&&$(a.error),R=a.fireAt}const q=typeof n.anchor=="string"&&n.anchor!==""?n.anchor:void 0,A=n.anchorSelf===!0;if(q!==void 0&&A)throw new c({error:"bad_anchor",message:"--anchor and --anchor-self are mutually exclusive",next:"Pass one anchor or the other."});let w=q??null;if(A){const a=P();if(a===void 0||a==="")throw new c({error:"no_self_node",message:"--anchor-self requires a calling node ($CRTR_NODE_ID is unset)",next:"Run from inside a node, or pass --anchor <node-id> explicitly."});w=a}const I=n.cancelOnWake===!0;if(I&&w===null)throw new c({error:"cancel_on_wake_needs_anchor",message:"--cancel-on-wake requires an anchor",next:"Add --anchor <node-id> or --anchor-self."});const O=typeof n.runTimeout=="string"&&n.runTimeout!==""?G(n.runTimeout):void 0,E=typeof n.cwd=="string"&&n.cwd!==""?n.cwd:void 0,g=P(),S=g?await l().getNode(g).catch(u):null,K=Q(E??(S?.isolated?S.cwd:void 0)),F=X(n.env),D=typeof n.profile=="string"&&n.profile!==""?n.profile:void 0,y=U(),_=D!==void 0?await L(D):y!==void 0&&y.trim()!==""?y:void 0,N=n.scope??(_!==void 0?"profile":"global");if(N==="profile"&&_===void 0)throw new c({error:"scope_needs_profile",message:"--scope profile names the profile this cron lists and cancels within, but no profile is resolved",field:"profile",next:"Pass --profile <id>, or use --scope global for a canvas-home-wide cron."});const v=g,x=await l().armCron({name:r,command:e,fire_at:d,recur:T,tz:s??null,created_by:v!==void 0&&v!==""?v:null,cwd:K,...E===void 0?{cwd_is_default:!0}:{},env_json:F,profile:_??null,scope:N,on_output:n.onOutput,sink:typeof n.sink=="string"&&n.sink!==""?n.sink:null,tier:n.tier??"urgent",expires_at:R,anchor_node:w,cancel_on_wake:I,...O!==void 0?{run_timeout_s:O}:{},overlap:n.overlap??"skip"}).catch(u);return{cron_id:x.cron_id,fire_at:x.fire_at,recur:k(x.recur)}},"run"),render:o(n=>{const e=String(n.recur),r=e==="none"?"one-shot (row deleted after the run)":e;return`Cron armed \u2014 ${String(n.cron_id)}.
|
|
2
2
|
|
|
3
3
|
- next fire: ${String(n.fire_at)}
|
|
4
4
|
- cadence: ${r}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
var K=Object.defineProperty;var a=(e,o)=>K(e,"name",{value:o,configurable:!0});import{envNodeId as Y}from"../../shared/env.js";import{defineLeaf as G}from"../../core/command.js";import{InputError as n}from"../../core/io.js";import{requireScope as V}from"../../core/errors.js";import{mark as q}from"../../core/timing.js";import{OUTPUT_SCHEMA_CONSTRAINTS as X,OUTPUT_SCHEMA_TRANSPORT as Q,parseOutputSchemaValue as Z}from"../../core/runtime/structured-output.js";import{MODEL_SPEC_FORMS as ee,MODEL_SPEC_NEXT as te}from"../../core/runtime/model-selection.js";import{parseDurationMs as oe}from"../../core/wake.js";import{kindsStateBlock as ne,profilesStateBlock as re,resolveProfileViaDaemon as ie}from"../profile/daemon-profiles.js";import{cliClient as F,getNodeOrNull as ae,rethrowAsCliError as L}from"../api-client.js";import{inTmux as $}from"../../core/runtime/placement-tmux.js";import{openSpawnViewer as se}from"../surface/node/placement.js";import{randomUUID as de}from"node:crypto";import{resolve as le}from"node:path";import{isDaemonTransportApiError as H}from"../../api/errors.js";import{ExitCode as ce}from"../../types.js";const ue=1e5,z='--output-schema writes the accepted result to the node\u2019s context/result.json and pushes it as the node\u2019s final report; a node that cannot honestly satisfy the schema declines with `crtr push result --decline "<reason>" --code <token>`, which finishes it with a failure outcome (reason `declined`) whose `declined` field carries { reason, code, retryable } and no result. Name the code tokens you accept in the prompt; crouter carries the code verbatim and you validate it.',W=["A resident is told never to push a final, so the spawner gets no report wake when the work is done.","If it stops mid-task, nothing prompts it to continue.","After 15 unattended minutes it is concluded with only a deferred update, which does not wake the spawner."],B="To re-task a terminal child after its final, use `crtr node message send --reopen`; to make an existing node resident later, use `crtr node config --lifecycle resident`.",N="Do not wait, poll, or duplicate this child's assignment in your own context \u2014 there is no result to await, stopping will not strand you, and duplicated work wastes the delegated context. You're auto-subscribed, so its finish wakes you on its own. Two moves only: do other independent work right now, or stop and end your turn \u2014 the wake brings you back.";async function he(e){if(e===void 0||e==="")return N;const o=await ae(e);if(o===null||o.mode!=="orchestrator")return N;const t=o.context_tokens??null;return t===null||t<ue?N:`Child spawned \u2014 you're auto-subscribed, so its finish wakes you on its own; never wait, poll, or duplicate the child's assignment in your own context. But you're an orchestrator already carrying ~${Math.round(t/1e3)}k of context: rather than hold this window open for the result, bring your document \`roadmap\` current (\`crtr doc edit roadmap\`) and \`crtr node yield\` now. Yielding ends this turn, and the child's completion then revives you fresh against your roadmap \u2014 let that clean revive absorb the result instead of this bloated context.`}a(he,"childFollowUp");function fe(e,o){throw H(e)||L(e),new n({error:"node_create_effect_unknown",message:`Node creation did not receive an acknowledgement after automatic recovery with request key ${o}; its effect is unknown.`,next:"Do not create a replacement node until the daemon is reachable and this request can be recovered."},ce.NETWORK)}a(fe,"createTransportFailure");function me(){return[{kind:"stdin",name:"prompt",required:!1,constraint:"First user message."},{kind:"flag",name:"kind",type:"string",required:!1,constraint:"Persona kind. Defaults to the profile's default kind; `general` when the profile has none. The <kinds> list below names every top-level installable kind."},{kind:"flag",name:"mode",type:"enum",choices:["base","orchestrator"],required:!1,default:"base",constraint:"Persona mode. base for one hands-on unit; orchestrator when the unit itself needs decomposition across children. When you hold several parallel units of one kind, spawn ONE orchestrator child of that kind to own the fan-out rather than spawning the units yourself \u2014 unless you are already an orchestrator of that kind, whose job is exactly that fan-out."},{kind:"flag",name:"cwd",type:"path",required:!1,constraint:"Pin the node to this directory. Without --worktree, defaults to the spawner\u2019s directory for a managed child and the invocation directory for a root. With --worktree, selects the repository used for creation, defaulting to the invocation directory; the child runs in the new checkout."},{kind:"flag",name:"name",type:"string",required:!1,constraint:"Display name."},{kind:"flag",name:"parent",type:"string",required:!1,constraint:"Parent node id. Defaults to the calling node."},{kind:"flag",name:"node-id",type:"string",required:!1,constraint:"Exact id for the new node; lowercase letters, digits, and hyphens only, at most 128 characters. Rejected if already present."},{kind:"flag",name:"root",type:"bool",required:!1,constraint:"Spawn an independent root with no parent or subscription. Mutually exclusive with --worktree."},{kind:"flag",name:"resident",type:"bool",required:!1,constraint:"Birth the node as resident, a conversation partner for a person rather than a worker. Omit unless the user explicitly asks for a node they can talk with back and forth.",focusedHelp:{whenToUse:"Pass it ONLY when the user explicitly asks for a node they can talk with back and forth. Never infer it from task size, from expecting the user to look at the node, or from wanting to follow up later. "+B,value:"Boolean. Roots default to resident, so with --root it changes nothing. Managed children default to terminal; for a managed child the command first refuses, explains the cost, and says how to confirm.",effects:[...W,"It ends by going dormant or by being closed."]}},{kind:"flag",name:"terminal",type:"bool",required:!1,constraint:"Birth the node as terminal: it finalizes when its work is done and leaves nothing wakeable behind. Only meaningful with --root, whose default is resident; managed children are already terminal. Pass it for bounded programmatic work no one will open a viewer on. Mutually exclusive with --resident."},{kind:"flag",name:"force",type:"bool",required:!1,default:!1,hidden:!0,constraint:"Confirm a resident managed child (--resident without --root) after the user explicitly asked for one."},{kind:"flag",name:"worktree",type:"string",required:!1,constraint:"Existing local base branch for this child\u2019s managed worktree. When invoking from a managed worktree, also pass --cwd pointing at the repository\u2019s primary checkout; keep --worktree set to the desired parent branch. Mutually exclusive with --root."},{kind:"flag",name:"fork-from",type:"string",required:!1,constraint:"Fork the new node from an existing node id, absolute session path, or partial pi session uuid."},{kind:"flag",name:"model",type:"string",required:!1,constraint:"Durable model override. Omit unless the task or governing guidance requires a specific model.",focusedHelp:{whenToUse:"Use only when the user, task, or governing agent guidance already requires a specific provider, model, capability tier, or family. Omit it for routine delegation: the selected persona kind and profile own normal model selection, and review personas may enforce a quality floor.",value:`Accepted forms: ${ee}. Concrete provider/id requests must be registered. Portable provider/tier, bare-tier, and family-alias requests resolve through the configured model ladders.`,effects:["The resolved concrete model is stored on the node and in its launch recipe.","A concrete provider/id runs exactly that model on every launch and revive. A portable provider/tier, bare tier, or family alias keeps its tier: when that model's provider is rate-limited or rejects its credential, the node moves to the next model in its route: another provider's model at the same tier first, then the configured fallback tier (strong by default).","A persona quality floor may raise a weaker request before its resolved model is stored."]}},{kind:"flag",name:"profile",type:"string",required:!1,constraint:"Profile override. Use only when the task needs different project purview, owned documents, or environment; otherwise omit for normal inheritance or selection.",focusedHelp:{whenToUse:"Use only when the work must run under a different profile because it needs that profile\u2019s project purview, owned documents, stored environment, or defaults. Do not use it merely to change the working directory, persona kind, model, or graph ownership; those have separate parameters.",value:"Pass a profile id or name from the <profiles> list below; a close-enough name resolves to the nearest profile and the choice is reported on stderr. Omit it inside a node to inherit the caller\u2019s profile; outside a node, omission runs normal cwd-based profile selection.",effects:["The selected profile id is stored on the node and supplies its profile-scoped config, documents, environment, and injected bearings.","It does not change the parent or subscription; --cwd separately pins the working directory."]}},{kind:"flag",name:"situational-context",type:"string",required:!1,constraint:"Hidden ambient context appended to injected bearings, never visible chat."},{kind:"flag",name:"no-kickoff",type:"bool",required:!1,hidden:!0,constraint:"With --root, start the broker and session idle without a first message or model turn. Mutually exclusive with a prompt and --mode orchestrator."},{kind:"flag",name:"deadline",type:"string",required:!1,constraint:"Positive, representable wall-clock duration from spawn. Grammar: one or more positive integer-unit parts; units: s, m, h, d; adjacent parts are summed. Omit for no deadline."},{kind:"flag",name:"outcome-action",type:"string",required:!1,constraint:"Declared human-action name that receives the node outcome. Grammar: 1\u2013128 ASCII letters, digits, dots, underscores, or hyphens; starts with a letter or digit. Omit when no external delivery target is needed."},{kind:"flag",name:"outcome-payload",type:"string",required:!1,constraint:"JSON value, serialized at most 64 KiB, frozen and echoed to --outcome-action. Requires --outcome-action."},{kind:"flag",name:"output-schema",type:"string",required:!1,constraint:"Replace prose completion with a schema-constrained final result. Use only when its consumer requires fixed fields.",focusedHelp:{whenToUse:"Use only when the node\u2019s consumer needs a machine-checkable final object with fixed fields. Omit it for ordinary delegation or when a prose report is enough; structured output replaces the completion protocol, and an over-constrained schema can force retries.",value:`Pass the JSON Schema the result must satisfy. ${Q} ${X}`,effects:["The node must answer with `crtr push result` and cannot finish until the result is accepted; a plain `crtr push final` is blocked while the request is pending.",z,"The managed parent wakes through its existing subscription when the final report arrives; an independent root has no subscriber to wake.","An accepted result, or a decline, removes the request and terminates the node."]}}]}a(me,"nodeNewParams");function pe(){return[{name:"node_id",type:"string",required:!0,constraint:"New node id."},{name:"name",type:"string",required:!0,constraint:"Display name."},{name:"frozen_at",type:"string",required:!1,constraint:"Present when a managed child exists but capacity deferred its broker start."},...$()?[{name:"window",type:"string",required:!1,constraint:"tmux window id when a viewer opens."},{name:"session",type:"string",required:!1,constraint:"tmux session when a viewer opens."}]:[],{name:"follow_up",type:"string",required:!0,constraint:"Managed child: continue unrelated work or stop; its final report wakes you. Root: hand it off; no report wakes you."}]}a(pe,"nodeNewOutput");function we(){return["Creates nodes/<id>/ in the canvas home, indexes the node in canvas.db, and starts its detached broker engine.","A managed child creates an active parent subscription; its pushes wake the parent. --root creates an independent node with spawned_by provenance and no parent subscription.","--worktree creates a managed worktree at ~/.crouter/canvas/worktrees/<node-id>/ on a new crtr/<node-id> branch.",z,"--deadline persists a wall-clock deadline from spawn; expiry cancels the node and records a failure outcome with reason deadline_exceeded.","--outcome-action registers one resolved local human-action target. When the node settles, crouter retries delivery of its outcome document until the target accepts it or permanently rejects it; --outcome-payload is frozen and echoed in that document.",...$()?["--root opens a viewer for the new node in the current tmux session."]:[]]}a(we,"nodeNewEffects");async function ke(e){V("crtr:act","crtr node new");const o=e.prompt??"",t=e.noKickoff===!0;if(!t&&o.trim()==="")throw new n({error:"empty_prompt",message:"a prompt is required (stdin or positional)",next:"Pipe a task on stdin or pass it as an argument, or pass --no-kickoff to spawn an idle node with none."});if(t&&o.trim()!=="")throw new n({error:"no_kickoff_with_prompt",message:"--no-kickoff cannot be combined with a prompt",field:"no-kickoff",next:"Drop --no-kickoff, or drop the prompt."});const l=e.kind,T=e.mode??"base",c=e.cwd?.trim(),u=c!==void 0&&c!==""?le(c):void 0,S=e.name,J=e.parent,r=e.root===!0,h=e.resident===!0,O=e.terminal===!0;if(h&&O)throw new n({error:"lifecycle_conflict",message:"--resident and --terminal cannot both be passed",field:"terminal",next:"Pass the one birth lifecycle you want, or neither to take the default (resident for --root, terminal for a managed child)."});if(h&&!r&&e.force!==!0)throw new n({error:"resident_child_requires_force",message:`--resident on a managed child makes it a conversation partner for a person, not a worker that reports back. ${W.join(" ")}`,field:"resident",next:`If the user did not explicitly ask for a node they can talk with back and forth, drop --resident; a terminal child's final report wakes you. ${B} If the user did explicitly ask for one, re-run with --force.`});const f=e.worktree;if(f!==void 0&&f.trim()==="")throw new n({error:"worktree_base_required",message:"--worktree requires a base branch",field:"worktree",next:"Pass an existing local branch that landing can fast-forward."});const m=f?.trim(),P=e.forkFrom,p=e.model?.trim();if(p==="")throw new n({error:"empty_spec",message:"a model spec is required",field:"model",next:`Run \`crtr node new --model -h\` and read the focused contract. ${te}`});const E=e.profile,D=E===void 0?void 0:(await ie(E)).id,w=e.situationalContext?.trim(),C=w!==void 0&&w!==""?w:void 0,A=Z(e.outputSchema),U=A!==null?JSON.stringify(A):null,d=e.deadline;if(d!==void 0){const s=oe(d),x=s===null?Number.NaN:new Date(Date.now()+s).getTime();if(s===null||!Number.isFinite(s)||s<=0||!Number.isFinite(x))throw new n({error:"invalid_deadline",message:"--deadline must be a positive representable duration",field:"deadline",next:"Pass positive integer-unit parts using s, m, h, or d."})}const k=e.outcomeAction,y=e.outcomePayload;if(y!==void 0&&k===void 0)throw new n({error:"outcome_payload_requires_action",message:"--outcome-payload requires --outcome-action",field:"outcome-payload",next:"Pass the declared delivery action with --outcome-action."});let g;if(y!==void 0)try{g=JSON.parse(y)}catch{throw new n({error:"invalid_outcome_payload",message:"--outcome-payload must be valid JSON",field:"outcome-payload",next:"Pass one valid JSON value."})}const v=e.nodeId?.trim();if(r&&m!==void 0)throw new n({error:"worktree_root_conflict",message:"--worktree is mutually exclusive with --root",field:"worktree",next:"Drop either --root or --worktree."});if(t&&!r)throw new n({error:"no_kickoff_requires_root",message:"--no-kickoff is only valid with --root",field:"no-kickoff",next:"Add --root, or drop --no-kickoff and provide a prompt."});if(t&&T==="orchestrator")throw new n({error:"no_kickoff_orchestrator_unsupported",message:"--no-kickoff cannot be combined with --mode orchestrator",field:"no-kickoff",next:"Drop --no-kickoff and provide a prompt, or spawn in base mode."});const b=Y(),I=J??b;q("node_new.create_start",{root:r});const R=de(),M={...l!==void 0?{kind:l}:{},mode:T,cwd:process.cwd(),...u!==void 0&&u!==""?{pin_cwd:u}:{},...S!==void 0?{name:S}:{},prompt:o,...r?{root:!0}:{parent:I??null},...b!==void 0?{creator:b}:{},...h?{root_lifecycle:"resident"}:{},...O?{root_lifecycle:"terminal"}:{},...m!==void 0?{worktree:m}:{},...P!==void 0?{fork_from:P}:{},...p!==void 0?{model:p}:{},...D!==void 0?{profile:D}:{},...C!==void 0?{situational_context:C}:{},...t?{no_kickoff:!0}:{},...U!==null?{output_schema:U}:{},...d!==void 0?{deadline:d}:{},...k!==void 0?{outcome_delivery:{action:k,...g===void 0?{}:{payload:g}}}:{},...v!==void 0&&v!==""?{node_id:v}:{},...r?{prefer_warm:!0}:{},request_key:R};let i;try{i=await F().createNode(M)}catch(s){H(s)||L(s);try{i=await F().createNode(M)}catch(x){fe(x,R)}}q("node_new.node_created",{node:i.node_id});let _={};r&&(_=se(i.node_id,i.cwd,i.name)),r&&q("node_new.viewer_window_opened");const j=!r&&i.frozen_at!==null;return{node_id:i.node_id,name:i.name,...j?{frozen_at:i.frozen_at}:{},window:_.window,session:_.session,follow_up:r?t?"Independent root spawned idle. No kickoff turn ran; hand it off or attach and prompt it. No report wakes you.":"Independent root spawned. You are not subscribed, so its finish does not wake you; hand it off and move on.":j?"Child row exists, but no broker started because capacity is full. It is frozen and will launch when a broker slot frees; its final report wakes you then.":await he(I)}}a(ke,"runNodeCreation");const Re=G({name:"new",description:"create and start a node immediately",whenToUse:"a unit of work is genuinely separable from what you are doing: a bounded subtask you would otherwise grind out inline, several independent units worth running in parallel (usually via one orchestrator child of the matching kind rather than a flat fan-out from you), a scouting pass over unfamiliar code, a change that wants its own git worktree, or an answer you need in a fixed shape rather than as prose",tier:"important",help:{name:"node new",summary:"create a node immediately and start its detached broker",guide:"The prompt is the child\u2019s entire brief: it wakes with this prompt, its persona, and ambient bearings \u2014 none of your conversation carries over. State the outcome and constraints. Pass an implementation plan only when it is already settled or necessary to preserve an approved decision; otherwise, let the child investigate and choose how. Give the reason along with the request: the larger goal this serves and what the child's output enables, so the child resolves ambiguity toward your intent instead of guessing. Point at every document the task depends on (a spec, an explore map, an earlier child's findings) as `[[<node-id>/<name>]]`, or `[[name]]` for a repo, profile or user document; the child reads each with `crtr canvas read` and receives nothing the prompt does not point at. When the report must carry something specific\u2014findings inline, or pointers to documents it wrote\u2014say so in the prompt.",params:me(),output:pe(),outputKind:"object",effects:we()},helpState:{leaf:ne,flags:{profile:re}},run:a(e=>ke(e),"run"),render:a(e=>{const o=e.window?` in window ${e.window}`:"",t=[`Spawned "${e.name}" (${e.node_id})${o}.`];return e.session&&t.push(`- session: ${e.session}`),t.push("",String(e.follow_up)),t.join(`
|
|
1
|
+
var K=Object.defineProperty;var a=(e,o)=>K(e,"name",{value:o,configurable:!0});import{envNodeId as Y}from"../../shared/env.js";import{defineLeaf as G}from"../../core/command.js";import{InputError as n}from"../../core/io.js";import{requireScope as V}from"../../core/errors.js";import{mark as q}from"../../core/timing.js";import{OUTPUT_SCHEMA_CONSTRAINTS as X,OUTPUT_SCHEMA_TRANSPORT as Q,parseOutputSchemaValue as Z}from"../../core/runtime/structured-output.js";import{MODEL_SPEC_FORMS as ee,MODEL_SPEC_NEXT as te}from"../../core/runtime/model-selection.js";import{parseDurationMs as oe}from"../../core/wake.js";import{kindsStateBlock as ne,profilesStateBlock as re,resolveProfileViaDaemon as ie}from"../profile/daemon-profiles.js";import{cliClient as F,getNodeOrNull as ae,rethrowAsCliError as H}from"../api-client.js";import{inTmux as L}from"../../core/runtime/placement-tmux.js";import{openSpawnViewer as se}from"../surface/node/placement.js";import{randomUUID as de}from"node:crypto";import{resolve as le}from"node:path";import{isDaemonTransportApiError as $}from"../../api/errors.js";import{ExitCode as ce}from"../../types.js";const ue=1e5,z='--output-schema writes the accepted result to the node\u2019s context/result.json and pushes it as the node\u2019s final report; a node that cannot honestly satisfy the schema declines with `crtr push result --decline "<reason>" --code <token>`, which finishes it with a failure outcome (reason `declined`) whose `declined` field carries { reason, code, retryable } and no result. Name the code tokens you accept in the prompt; crouter carries the code verbatim and you validate it.',W=["A resident is told never to push a final, so the spawner gets no report wake when the work is done.","If it stops mid-task, nothing prompts it to continue.","After 15 unattended minutes it is concluded with only a deferred update, which does not wake the spawner."],B="To re-task a terminal child after its final, use `crtr node message send --reopen`; to make an existing node resident later, use `crtr node config --lifecycle resident`.",N="Do not wait, poll, or duplicate this child's assignment in your own context \u2014 there is no result to await, stopping will not strand you, and duplicated work wastes the delegated context. You're auto-subscribed, so its finish wakes you on its own. Two moves only: do other independent work right now, or stop and end your turn \u2014 the wake brings you back.";async function he(e){if(e===void 0||e==="")return N;const o=await ae(e);if(o===null||o.mode!=="orchestrator")return N;const t=o.context_tokens??null;return t===null||t<ue?N:`Child spawned \u2014 you're auto-subscribed, so its finish wakes you on its own; never wait, poll, or duplicate the child's assignment in your own context. But you're an orchestrator already carrying ~${Math.round(t/1e3)}k of context: rather than hold this window open for the result, bring your document \`roadmap\` current (\`crtr doc edit roadmap\`) and \`crtr node yield\` now. Yielding ends this turn, and the child's completion then revives you fresh against your roadmap \u2014 let that clean revive absorb the result instead of this bloated context.`}a(he,"childFollowUp");function fe(e,o){throw $(e)||H(e),new n({error:"node_create_effect_unknown",message:`Node creation did not receive an acknowledgement after automatic recovery with request key ${o}; its effect is unknown.`,next:"Do not create a replacement node until the daemon is reachable and this request can be recovered."},ce.NETWORK)}a(fe,"createTransportFailure");function me(){return[{kind:"stdin",name:"prompt",required:!1,constraint:"First user message."},{kind:"flag",name:"kind",type:"string",required:!1,constraint:"Persona kind. Defaults to the profile's default kind; `general` when the profile has none. The <kinds> list below names every top-level installable kind."},{kind:"flag",name:"mode",type:"enum",choices:["base","orchestrator"],required:!1,default:"base",constraint:"Persona mode. base for one hands-on unit; orchestrator when the unit itself needs decomposition across children. When you hold several parallel units of one kind, spawn ONE orchestrator child of that kind to own the fan-out rather than spawning the units yourself \u2014 unless you are already an orchestrator of that kind, whose job is exactly that fan-out."},{kind:"flag",name:"cwd",type:"path",required:!1,constraint:"Pin the node to this directory. Without --worktree, defaults to the spawner\u2019s directory for a managed child and the invocation directory for a root. With --worktree, selects the repository used for creation, defaulting to the invocation directory; the child runs in the new checkout."},{kind:"flag",name:"name",type:"string",required:!1,constraint:"Display name."},{kind:"flag",name:"parent",type:"string",required:!1,constraint:"Parent node id. Defaults to the calling node."},{kind:"flag",name:"node-id",type:"string",required:!1,constraint:"Exact id for the new node; lowercase letters, digits, and hyphens only, at most 128 characters. Rejected if already present."},{kind:"flag",name:"root",type:"bool",required:!1,constraint:"Spawn an independent root with no parent or subscription. Mutually exclusive with --worktree."},{kind:"flag",name:"resident",type:"bool",required:!1,constraint:"Birth the node as resident, a conversation partner for a person rather than a worker. Omit unless the user explicitly asks for a node they can talk with back and forth.",focusedHelp:{whenToUse:"Pass it ONLY when the user explicitly asks for a node they can talk with back and forth. Never infer it from task size, from expecting the user to look at the node, or from wanting to follow up later. "+B,value:"Boolean. Roots default to resident, so with --root it changes nothing. Managed children default to terminal; for a managed child the command first refuses, explains the cost, and says how to confirm.",effects:[...W,"It ends by going dormant or by being closed."]}},{kind:"flag",name:"terminal",type:"bool",required:!1,constraint:"Birth the node as terminal: it finalizes when its work is done and leaves nothing wakeable behind. Only meaningful with --root, whose default is resident; managed children are already terminal. Pass it for bounded programmatic work no one will open a viewer on. Mutually exclusive with --resident."},{kind:"flag",name:"force",type:"bool",required:!1,default:!1,hidden:!0,constraint:"Confirm a resident managed child (--resident without --root) after the user explicitly asked for one."},{kind:"flag",name:"worktree",type:"string",required:!1,constraint:"Existing local base branch for this child\u2019s managed worktree. When invoking from a managed worktree, also pass --cwd pointing at the repository\u2019s primary checkout; keep --worktree set to the desired parent branch. Mutually exclusive with --root."},{kind:"flag",name:"fork-from",type:"string",required:!1,constraint:"Fork the new node from an existing node id, absolute session path, or partial pi session uuid."},{kind:"flag",name:"model",type:"string",required:!1,constraint:"Durable model override. Omit unless the task or governing guidance requires a specific model.",focusedHelp:{whenToUse:"Use only when the user, task, or governing agent guidance already requires a specific provider, model, capability tier, or family. Omit it for routine delegation: the selected persona kind and profile own normal model selection, and review personas may enforce a quality floor.",value:`Accepted forms: ${ee}. Concrete provider/id requests must be registered. Portable provider/tier, bare-tier, and family-alias requests resolve through the configured model ladders.`,effects:["The resolved concrete model is stored on the node and in its launch recipe.","A concrete provider/id runs exactly that model on every launch and revive. A portable provider/tier, bare tier, or family alias keeps its tier: when that model's provider is rate-limited or rejects its credential, the node moves to the next model in its route: another provider's model at the same tier first, then the configured fallback tier (strong by default).","A persona quality floor may raise a weaker request before its resolved model is stored."]}},{kind:"flag",name:"profile",type:"string",required:!1,constraint:"Profile override. Use only when the task needs different project purview, owned documents, or environment; otherwise omit for normal inheritance or selection.",focusedHelp:{whenToUse:"Use only when the work must run under a different profile because it needs that profile\u2019s project purview, owned documents, stored environment, or defaults. Do not use it merely to change the working directory, persona kind, model, or graph ownership; those have separate parameters.",value:"Pass a profile id or name from the <profiles> list below; a close-enough name resolves to the nearest profile and the choice is reported on stderr. Omit it inside a node to inherit the caller\u2019s profile; outside a node, omission runs normal cwd-based profile selection.",effects:["The selected profile id is stored on the node and supplies its profile-scoped config, documents, environment, and injected bearings.","It does not change the parent or subscription; --cwd separately pins the working directory."]}},{kind:"flag",name:"situational-context",type:"string",required:!1,constraint:"Hidden ambient context appended to injected bearings, never visible chat."},{kind:"flag",name:"no-kickoff",type:"bool",required:!1,hidden:!0,constraint:"With --root, start the broker and session idle without a first message or model turn. Mutually exclusive with a prompt and --mode orchestrator."},{kind:"flag",name:"deadline",type:"string",required:!1,constraint:"Positive, representable wall-clock duration from spawn. Grammar: one or more positive integer-unit parts; units: s, m, h, d; adjacent parts are summed. Omit for no deadline."},{kind:"flag",name:"outcome-action",type:"string",required:!1,constraint:"Declared human-action name that receives the node outcome. Grammar: 1\u2013128 ASCII letters, digits, dots, underscores, or hyphens; starts with a letter or digit. Omit when no external delivery target is needed."},{kind:"flag",name:"outcome-payload",type:"string",required:!1,constraint:"JSON value, serialized at most 64 KiB, frozen and echoed to --outcome-action. Requires --outcome-action."},{kind:"flag",name:"output-schema",type:"string",required:!1,constraint:"Replace prose completion with a schema-constrained final result. Use only when its consumer requires fixed fields.",focusedHelp:{whenToUse:"Use only when the node\u2019s consumer needs a machine-checkable final object with fixed fields. Omit it for ordinary delegation or when a prose report is enough; structured output replaces the completion protocol, and an over-constrained schema can force retries.",value:`Pass the JSON Schema the result must satisfy. ${Q} ${X}`,effects:["The node must answer with `crtr push result` and cannot finish until the result is accepted; a plain `crtr push final` is blocked while the request is pending.",z,"The managed parent wakes through its existing subscription when the final report arrives; an independent root has no subscriber to wake.","An accepted result, or a decline, removes the request and terminates the node."]}}]}a(me,"nodeNewParams");function pe(){return[{name:"node_id",type:"string",required:!0,constraint:"New node id."},{name:"name",type:"string",required:!0,constraint:"Display name."},{name:"frozen_at",type:"string",required:!1,constraint:"Present when a managed child exists but capacity deferred its broker start."},...L()?[{name:"window",type:"string",required:!1,constraint:"tmux window id when a viewer opens."},{name:"session",type:"string",required:!1,constraint:"tmux session when a viewer opens."}]:[],{name:"follow_up",type:"string",required:!0,constraint:"Managed child: continue unrelated work or stop; its final report wakes you. Root: hand it off; no report wakes you."}]}a(pe,"nodeNewOutput");function we(){return["Creates nodes/<id>/ in the canvas home, indexes the node in canvas.db, and starts its detached broker engine.","A managed child creates an active parent subscription; its pushes wake the parent. --root creates an independent node with spawned_by provenance and no parent subscription.","--worktree creates a managed worktree at ~/.crouter/canvas/worktrees/<node-id>/ on a new crtr/<node-id> branch.",z,"--deadline persists a wall-clock deadline from spawn; expiry cancels the node and records a failure outcome with reason deadline_exceeded.","--outcome-action registers one resolved local human-action target. When the node settles, crouter retries delivery of its outcome document until the target accepts it or permanently rejects it; --outcome-payload is frozen and echoed in that document.",...L()?["--root opens a viewer for the new node in the current tmux session."]:[]]}a(we,"nodeNewEffects");async function ke(e){V("crtr:act","crtr node new");const o=e.prompt??"",t=e.noKickoff===!0;if(!t&&o.trim()==="")throw new n({error:"empty_prompt",message:"a prompt is required (stdin or positional)",next:"Pipe a task on stdin or pass it as an argument, or pass --no-kickoff to spawn an idle node with none."});if(t&&o.trim()!=="")throw new n({error:"no_kickoff_with_prompt",message:"--no-kickoff cannot be combined with a prompt",field:"no-kickoff",next:"Drop --no-kickoff, or drop the prompt."});const l=e.kind,T=e.mode??"base",c=e.cwd?.trim(),u=c!==void 0&&c!==""?le(c):void 0,O=e.name,J=e.parent,r=e.root===!0,h=e.resident===!0,S=e.terminal===!0;if(h&&S)throw new n({error:"lifecycle_conflict",message:"--resident and --terminal cannot both be passed",field:"terminal",next:"Pass the one birth lifecycle you want, or neither to take the default (resident for --root, terminal for a managed child)."});if(h&&!r&&e.force!==!0)throw new n({error:"resident_child_requires_force",message:`--resident on a managed child makes it a conversation partner for a person, not a worker that reports back. ${W.join(" ")}`,field:"resident",next:`If the user did not explicitly ask for a node they can talk with back and forth, drop --resident; a terminal child's final report wakes you. ${B} If the user did explicitly ask for one, re-run with --force.`});const f=e.worktree;if(f!==void 0&&f.trim()==="")throw new n({error:"worktree_base_required",message:"--worktree requires a base branch",field:"worktree",next:"Pass an existing local branch that landing can fast-forward."});const m=f?.trim(),P=e.forkFrom,p=e.model?.trim();if(p==="")throw new n({error:"empty_spec",message:"a model spec is required",field:"model",next:`Run \`crtr node new --model -h\` and read the focused contract. ${te}`});const E=e.profile,D=E===void 0?void 0:(await ie(E)).id,w=e.situationalContext?.trim(),C=w!==void 0&&w!==""?w:void 0,A=Z(e.outputSchema),U=A!==null?JSON.stringify(A):null,d=e.deadline;if(d!==void 0){const s=oe(d),x=s===null?Number.NaN:new Date(Date.now()+s).getTime();if(s===null||!Number.isFinite(s)||s<=0||!Number.isFinite(x))throw new n({error:"invalid_deadline",message:"--deadline must be a positive representable duration",field:"deadline",next:"Pass positive integer-unit parts using s, m, h, or d."})}const k=e.outcomeAction,y=e.outcomePayload;if(y!==void 0&&k===void 0)throw new n({error:"outcome_payload_requires_action",message:"--outcome-payload requires --outcome-action",field:"outcome-payload",next:"Pass the declared delivery action with --outcome-action."});let g;if(y!==void 0)try{g=JSON.parse(y)}catch{throw new n({error:"invalid_outcome_payload",message:"--outcome-payload must be valid JSON",field:"outcome-payload",next:"Pass one valid JSON value."})}const v=e.nodeId?.trim();if(r&&m!==void 0)throw new n({error:"worktree_root_conflict",message:"--worktree is mutually exclusive with --root",field:"worktree",next:"Drop either --root or --worktree."});if(t&&!r)throw new n({error:"no_kickoff_requires_root",message:"--no-kickoff is only valid with --root",field:"no-kickoff",next:"Add --root, or drop --no-kickoff and provide a prompt."});if(t&&T==="orchestrator")throw new n({error:"no_kickoff_orchestrator_unsupported",message:"--no-kickoff cannot be combined with --mode orchestrator",field:"no-kickoff",next:"Drop --no-kickoff and provide a prompt, or spawn in base mode."});const b=Y(),I=J??b;q("node_new.create_start",{root:r});const R=de(),M={...l!==void 0?{kind:l}:{},mode:T,cwd:process.cwd(),...u!==void 0&&u!==""?{pin_cwd:u}:{},...O!==void 0?{name:O}:{},prompt:o,...r?{root:!0}:{parent:I??null},...b!==void 0?{creator:b}:{},...h?{root_lifecycle:"resident"}:{},...S?{root_lifecycle:"terminal"}:{},...m!==void 0?{worktree:m}:{},...P!==void 0?{fork_from:P}:{},...p!==void 0?{model:p}:{},...D!==void 0?{profile:D}:{},...C!==void 0?{situational_context:C}:{},...t?{no_kickoff:!0}:{},...U!==null?{output_schema:U}:{},...d!==void 0?{deadline:d}:{},...k!==void 0?{outcome_delivery:{action:k,...g===void 0?{}:{payload:g}}}:{},...v!==void 0&&v!==""?{node_id:v}:{},...r?{prefer_warm:!0}:{},request_key:R};let i;try{i=await F().createNode(M)}catch(s){$(s)||H(s);try{i=await F().createNode(M)}catch(x){fe(x,R)}}q("node_new.node_created",{node:i.node_id});let _={};r&&(_=se(i.node_id,i.cwd,i.name)),r&&q("node_new.viewer_window_opened");const j=!r&&i.frozen_at!==null;return{node_id:i.node_id,name:i.name,...j?{frozen_at:i.frozen_at}:{},window:_.window,session:_.session,follow_up:r?t?"Independent root spawned idle. No kickoff turn ran; hand it off or attach and prompt it. No report wakes you.":"Independent root spawned. You are not subscribed, so its finish does not wake you; hand it off and move on.":j?"Child row exists, but no broker started because capacity is full. It is frozen and will launch when a broker slot frees; its final report wakes you then.":await he(I)}}a(ke,"runNodeCreation");const Re=G({name:"new",description:"create and start a node immediately",whenToUse:"a unit of work is genuinely separable from what you are doing: a bounded subtask you would otherwise grind out inline, several independent units worth running in parallel (usually via one orchestrator child of the matching kind rather than a flat fan-out from you), a scouting pass over unfamiliar code, a change that wants its own git worktree, or an answer you need in a fixed shape rather than as prose",tier:"important",help:{name:"node new",summary:"create a node immediately and start its detached broker",guide:"The prompt is the child\u2019s entire brief: it wakes with this prompt, its persona, and ambient bearings \u2014 none of your conversation carries over. State the outcome and constraints. Pass an implementation plan only when it is already settled or necessary to preserve an approved decision; otherwise, let the child investigate and choose how. Give the reason along with the request: the larger goal this serves and what the child's output enables, so the child resolves ambiguity toward your intent instead of guessing. Point at every document the task depends on (a spec, an explore map, an earlier child's findings) as `[[<node-id>/<name>]]`, or `[[name]]` for a repo, profile or user document; the child reads each with `crtr canvas read` and receives nothing the prompt does not point at. Name every file or directory as an absolute host path, never `~`-relative: the child's `~` is its own HOME, not yours, and a hook cwd outside its mounts (e.g. `/tmp`) is refused. When the report must carry something specific\u2014findings inline, or pointers to documents it wrote\u2014say so in the prompt.",params:me(),output:pe(),outputKind:"object",effects:we()},helpState:{leaf:ne,flags:{profile:re}},run:a(e=>ke(e),"run"),render:a(e=>{const o=e.window?` in window ${e.window}`:"",t=[`Spawned "${e.name}" (${e.node_id})${o}.`];return e.session&&t.push(`- session: ${e.session}`),t.push("",String(e.follow_up)),t.join(`
|
|
2
2
|
`)},"render")});export{he as childFollowUp,Re as nodeNew};
|
|
@@ -20,6 +20,16 @@ export interface Cron {
|
|
|
20
20
|
name: string;
|
|
21
21
|
created_by: string | null;
|
|
22
22
|
grantee: string;
|
|
23
|
+
/** The user the app acts for (the cron principal's `sub`). */
|
|
24
|
+
sub: string;
|
|
25
|
+
/** The grant the cron runs under; null only for `app:terminal` with no issuer, or a migrated row with no grant. */
|
|
26
|
+
grant_id: string | null;
|
|
27
|
+
/** Scopes the cron is bounded to; null only for person-armed terminal crons (the grant's whole current set). */
|
|
28
|
+
scopes: string[] | null;
|
|
29
|
+
/** The canvas run of the arming node (confinement; `run.cancel` later). Not the per-fire execution id. */
|
|
30
|
+
origin_run_id: string | null;
|
|
31
|
+
/** The running fire's execution id (`CRTR_RUN_ID`) while leased; null when idle. */
|
|
32
|
+
run_execution_id: string | null;
|
|
23
33
|
/** Frozen from the creating node; never recomputed from a later node state. */
|
|
24
34
|
isolated: boolean;
|
|
25
35
|
command: string;
|
|
@@ -77,6 +87,10 @@ export interface ArmCronSpec {
|
|
|
77
87
|
name: string;
|
|
78
88
|
created_by: string | null;
|
|
79
89
|
grantee?: string;
|
|
90
|
+
sub?: string;
|
|
91
|
+
grant_id?: string | null;
|
|
92
|
+
scopes?: readonly string[] | null;
|
|
93
|
+
origin_run_id?: string | null;
|
|
80
94
|
isolated?: boolean;
|
|
81
95
|
command: string;
|
|
82
96
|
fire_at: string;
|
|
@@ -223,7 +237,7 @@ export declare function dueClockCrons(nowIso: string): Cron[];
|
|
|
223
237
|
*
|
|
224
238
|
* `outOfBand` stamps the lease as a `cron run` (manual verification) so the
|
|
225
239
|
* overlap pass leaves it alone — see `dueOverlapCrons`. */
|
|
226
|
-
export declare function acquireCronRunLease(cron_id: string, leaseOwner: string, startedAtIso: string, outOfBand: boolean): boolean;
|
|
240
|
+
export declare function acquireCronRunLease(cron_id: string, leaseOwner: string, startedAtIso: string, outOfBand: boolean, executionId?: string | null): boolean;
|
|
227
241
|
/** Attach the launcher's pid only to the run that claimed this lease. */
|
|
228
242
|
export declare function attachCronRunPid(cron_id: string, leaseOwner: string, startedAtIso: string, pid: number): boolean;
|
|
229
243
|
/** What a settling run does with the row's `held` state, applied in the SAME
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
var R=Object.defineProperty;var o=(n,r)=>R(n,"name",{value:r,configurable:!0});import{prepared as i}from"../sqlite-statements.js";import{openDb as
|
|
2
|
-
cron_id, name, created_by, grantee, isolated, command, fire_at, recur, tz, expires_at,
|
|
1
|
+
var R=Object.defineProperty;var o=(n,r)=>R(n,"name",{value:r,configurable:!0});import{prepared as i}from"../sqlite-statements.js";import{openDb as a,withCanvasWrite as d}from"./db.js";import{capturePidIdentities as E,identitiesMatch as m,killProcessGroup as D}from"./pid.js";import{isProfilePaused as S}from"../profiles/manifest.js";import{stopAppProcess as h}from"../spaces/process.js";import{emitEvent as N}from"../events/emit.js";const O=20;function L(n){if(typeof n!="string")return null;try{const r=JSON.parse(n);return Array.isArray(r)?r.filter(e=>typeof e=="string"):null}catch{return null}}o(L,"scopesFrom");function _(n){return{cron_id:n.cron_id,name:n.name,created_by:n.created_by??null,grantee:n.grantee,sub:n.sub??"user:local",grant_id:n.grant_id??null,scopes:L(n.scopes_json),origin_run_id:n.origin_run_id??null,run_execution_id:n.run_execution_id??null,isolated:Number(n.isolated)!==0,command:n.command,fire_at:n.fire_at,recur:n.recur??null,tz:n.tz??null,expires_at:n.expires_at??null,anchor_node:n.anchor_node??null,cancel_on_wake:Number(n.cancel_on_wake)!==0,cwd:n.cwd,env_json:n.env_json??null,profile:n.profile??null,scope:n.scope,run_timeout_s:Number(n.run_timeout_s),overlap:n.overlap,on_output:n.on_output,sink:n.sink,tier:n.tier,last_output_hash:n.last_output_hash??null,held:Number(n.held)!==0,last_poke_at:n.last_poke_at??null,state:n.state,run_state:n.run_state,run_pid:n.run_pid??null,run_pid_identity:n.run_pid_identity??null,run_started_at:n.run_started_at??null,run_lease_owner:n.run_lease_owner??null,run_out_of_band:Number(n.run_out_of_band)!==0,created:n.created,updated:n.updated}}o(_,"cronFrom");function p(n){try{const r=JSON.parse(n);if(typeof r.node=="string")return{kind:"node",node:r.node};if(typeof r.spawn=="string")return{kind:"spawn",spawn:r.spawn};if(r.human===!0)return{kind:"human"}}catch{}return null}o(p,"parseCronSink");function M(n){const r=p(n);if(r===null)return null;switch(r.kind){case"node":return`node:${r.node}`;case"spawn":return`spawn:${r.spawn}`;case"human":return"human"}}o(M,"cronSinkDisplay");async function g(n,r="SIGTERM"){if(!l(n))return!1;if(process.platform!=="linux")return D(n.run_pid,r),!0;try{return await h(n.run_pid,r,n.anchor_node??void 0),!0}catch(e){if(!l(n))return!1;throw e}}o(g,"stopLeasedCronRun");function c(n,r="SIGTERM"){return l(n)?(g(n,r).catch(e=>{N({level:"error",event:"cron.run.stop_failed",error:e,fields:{cron_id:n.cron_id,pid:n.run_pid}})}),!0):!1}o(c,"killLeasedCronRun");function l(n){const r=n.run_pid;if(r==null||n.run_pid_identity==null)return!1;const e=E([r]);if(e===null)return!1;const t=e.get(r);return t===void 0?!1:m(t,n.run_pid_identity)}o(l,"verifyLeasedCronRun");function v(n){return d(r=>{const e=[];if(n.cancel_on_wake&&n.anchor_node!==null){const t=i(r,"SELECT * FROM crons WHERE anchor_node = ? AND cancel_on_wake = 1").all(n.anchor_node).map(_);for(const u of t)u.run_state==="running"&&c(u),i(r,"DELETE FROM crons WHERE cron_id = ?").run(u.cron_id),e.push(u.cron_id)}return C(r,n),{cron_id:n.cron_id,replaced:e}})}o(v,"armCron");function C(n,r){const e=new Date().toISOString();i(n,`INSERT INTO crons (
|
|
2
|
+
cron_id, name, created_by, grantee, sub, grant_id, scopes_json, origin_run_id, isolated, command, fire_at, recur, tz, expires_at,
|
|
3
3
|
anchor_node, cancel_on_wake, cwd, env_json, profile, scope,
|
|
4
4
|
run_timeout_s, overlap, on_output, sink, tier, created, updated
|
|
5
|
-
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(r.cron_id,r.name,r.created_by,r.grantee??"app:terminal",r.isolated?1:0,r.command,r.fire_at,r.recur,r.tz,r.expires_at,r.anchor_node,r.cancel_on_wake?1:0,r.cwd,r.env_json,r.profile,r.scope,r.run_timeout_s,r.overlap,r.on_output,r.sink,r.tier,e,e)}o(C,"insertCron");function
|
|
5
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(r.cron_id,r.name,r.created_by,r.grantee??"app:terminal",r.sub??"user:local",r.grant_id??null,r.scopes==null?null:JSON.stringify(r.scopes),r.origin_run_id??null,r.isolated?1:0,r.command,r.fire_at,r.recur,r.tz,r.expires_at,r.anchor_node,r.cancel_on_wake?1:0,r.cwd,r.env_json,r.profile,r.scope,r.run_timeout_s,r.overlap,r.on_output,r.sink,r.tier,e,e)}o(C,"insertCron");function y(n){const r=i(a(),"SELECT * FROM crons WHERE cron_id = ?").get(n);return r===void 0?null:_(r)}o(y,"getCron");function I(n){const r=a();let e;return"profile"in n?e=i(r,"SELECT * FROM crons WHERE scope = 'profile' AND profile = ? ORDER BY fire_at").all(n.profile):"global"in n?e=i(r,"SELECT * FROM crons WHERE scope = 'global' ORDER BY fire_at").all():e=i(r,"SELECT * FROM crons ORDER BY fire_at").all(),e.map(_)}o(I,"listCrons");function U(n,r){return d(e=>{const t=i(e,"SELECT * FROM crons WHERE cron_id = ?").get(n);if(t===void 0)return!1;const u=_(t);return u.run_state==="running"&&r?.preserveRunningProcess!==!0&&c(u),i(e,"DELETE FROM crons WHERE cron_id = ?").run(n),!0})}o(U,"cancelCron");function f(n,r){const e=[...new Set(r)];return e.length===0?{sql:"profile = ?",params:[n]}:{sql:`profile = ? OR anchor_node IN (${e.map(()=>"?").join(",")})`,params:[n,...e]}}o(f,"profileCronWhere");function P(n,r){const e=f(n,r);return a().prepare(`SELECT cron_id FROM crons WHERE ${e.sql} ORDER BY created, cron_id`).all(...e.params).map(({cron_id:t})=>t)}o(P,"cronIdsForProfileDeletion");function b(n,r){return d(e=>{const t=f(n,r),u=e.prepare(`SELECT * FROM crons WHERE ${t.sql} ORDER BY created, cron_id`).all(...t.params).map(_);for(const s of u)s.run_state==="running"&&c(s);return e.prepare(`DELETE FROM crons WHERE ${t.sql}`).run(...t.params),u.map(({cron_id:s})=>s)})}o(b,"deleteCronsForProfile");function B(n){return i(a(),"SELECT cron_id FROM crons WHERE profile = ? ORDER BY created, cron_id").all(n).map(({cron_id:r})=>r)}o(B,"cronIdsForProfileDetach");function Y(n){return d(r=>{const e=i(r,"SELECT cron_id FROM crons WHERE profile = ? ORDER BY created, cron_id").all(n).map(({cron_id:t})=>t);return e.length>0&&i(r,"UPDATE crons SET profile = NULL, scope = 'global', updated = ? WHERE profile = ?").run(new Date().toISOString(),n),e})}o(Y,"globalizeCronsForProfile");function $(n){return i(a(),"DELETE FROM crons WHERE cron_id = ?").run(n).changes>0}o($,"consumeCron");function j(n,r){return i(a(),"UPDATE crons SET state = ?, updated = ? WHERE cron_id = ?").run(r,new Date().toISOString(),n).changes>0}o(j,"setCronState");function q(n,r){i(a(),"UPDATE crons SET last_output_hash = ?, updated = ? WHERE cron_id = ?").run(r,new Date().toISOString(),n)}o(q,"setCronLastOutputHash");function z(n){const r=i(a(),`SELECT anchor_node, cancel_on_wake, sink
|
|
6
6
|
FROM crons
|
|
7
7
|
WHERE (anchor_node = ? AND cancel_on_wake = 1 AND state = 'active')
|
|
8
8
|
OR (
|
|
@@ -11,26 +11,26 @@ var R=Object.defineProperty;var o=(n,r)=>R(n,"name",{value:r,configurable:!0});i
|
|
|
11
11
|
run_state = 'running'
|
|
12
12
|
OR (run_state = 'idle' AND state = 'active' AND (expires_at IS NULL OR expires_at > ?))
|
|
13
13
|
)
|
|
14
|
-
)`).all(n,new Date().toISOString());for(const e of r){if(e.anchor_node===n&&Number(e.cancel_on_wake)!==0)return!0;const t=p(e.sink);if(t?.kind==="node"&&t.node===n)return!0}return!1}o(
|
|
14
|
+
)`).all(n,new Date().toISOString());for(const e of r){if(e.anchor_node===n&&Number(e.cancel_on_wake)!==0)return!0;const t=p(e.sink);if(t?.kind==="node"&&t.node===n)return!0}return!1}o(z,"hasPendingCronWake");function G(n){return d(r=>{const e=i(r,"SELECT * FROM crons WHERE anchor_node = ? AND cancel_on_wake = 1").all(n).map(_);for(const t of e)t.run_state==="running"&&c(t),i(r,"DELETE FROM crons WHERE cron_id = ?").run(t.cron_id);return e.map(t=>t.cron_id)})}o(G,"cancelCronsOnWake");function J(n,r){i(a(),"UPDATE crons SET fire_at = ?, updated = ? WHERE cron_id = ?").run(r,new Date().toISOString(),n)}o(J,"advanceCron");function V(n){return i(a(),`SELECT * FROM crons
|
|
15
15
|
WHERE state = 'active' AND run_state = 'idle' AND fire_at <= ?
|
|
16
|
-
ORDER BY fire_at, created, cron_id`).all(n).map(
|
|
16
|
+
ORDER BY fire_at, created, cron_id`).all(n).map(_).filter(r=>!S(r.profile))}o(V,"dueClockCrons");function K(n,r,e,t,u=null){const s=i(a(),`UPDATE crons
|
|
17
17
|
SET run_state = 'running', run_started_at = ?, run_pid = NULL, run_pid_identity = NULL,
|
|
18
|
-
run_lease_owner = ?, run_out_of_band = ?, updated = ?
|
|
19
|
-
WHERE cron_id = ? AND run_state = 'idle'`).run(e,r,t?1:0,new Date().toISOString(),n);return Number(
|
|
18
|
+
run_lease_owner = ?, run_out_of_band = ?, run_execution_id = ?, updated = ?
|
|
19
|
+
WHERE cron_id = ? AND run_state = 'idle'`).run(e,r,t?1:0,u,new Date().toISOString(),n);return Number(s.changes)>0}o(K,"acquireCronRunLease");function Q(n,r,e,t){const u=E([t])?.get(t)??null,s=i(a(),`UPDATE crons SET run_pid = ?, run_pid_identity = ?, updated = ?
|
|
20
20
|
WHERE cron_id = ? AND run_state = 'running' AND run_lease_owner = ?
|
|
21
|
-
AND run_started_at = ? AND run_pid IS NULL`).run(t,
|
|
21
|
+
AND run_started_at = ? AND run_pid IS NULL`).run(t,u,new Date().toISOString(),n,r,e);return Number(s.changes)>0}o(Q,"attachCronRunPid");function X(n,r){let e="";const t=[];r!==void 0&&(r.kind==="clear"?e=", held = 0":r.kind==="park"?(e=r.fireAt!==void 0?", held = 1, fire_at = ?":", held = 1",r.fireAt!==void 0&&t.push(r.fireAt)):(e=", held = 0, fire_at = ?",t.push(r.nowIso))),a().prepare(`UPDATE crons
|
|
22
22
|
SET run_state = 'idle', run_started_at = NULL, run_pid = NULL, run_pid_identity = NULL,
|
|
23
|
-
run_lease_owner = NULL, run_out_of_band = 0, updated = ?${e}
|
|
24
|
-
WHERE cron_id = ?`).run(new Date().toISOString(),...t,n)}o(
|
|
23
|
+
run_lease_owner = NULL, run_out_of_band = 0, run_execution_id = NULL, updated = ?${e}
|
|
24
|
+
WHERE cron_id = ?`).run(new Date().toISOString(),...t,n)}o(X,"releaseCronRunLease");function Z(n,r=null){return d(e=>{const t=r===null?"":" AND grantee = ?",u=r===null?[]:[r];e.prepare(`UPDATE crons SET last_poke_at = ? WHERE 1 = 1${t}`).run(n,...u);const s=e.prepare(`UPDATE crons SET fire_at = ?, held = 0, updated = ? WHERE held = 1 AND state = 'active' AND run_state = 'idle'${t}`).run(n,n,...u);return Number(s.changes)})}o(Z,"pokeHeldCrons");function nn(n){return i(a(),`SELECT * FROM crons
|
|
25
25
|
WHERE state = 'active' AND run_state = 'running' AND run_out_of_band = 0
|
|
26
26
|
AND recur IS NOT NULL AND fire_at <= ?
|
|
27
|
-
ORDER BY fire_at, created, cron_id`).all(n).map(
|
|
27
|
+
ORDER BY fire_at, created, cron_id`).all(n).map(_)}o(nn,"dueOverlapCrons");function rn(n){return i(a(),`SELECT * FROM crons
|
|
28
28
|
WHERE expires_at IS NOT NULL AND expires_at <= ? AND run_state = 'idle'
|
|
29
|
-
ORDER BY expires_at`).all(n).map(
|
|
29
|
+
ORDER BY expires_at`).all(n).map(_)}o(rn,"expiredIdleCrons");function en(n){return i(a(),"SELECT * FROM cron_runs WHERE cron_id = ? ORDER BY started DESC, run_id DESC").all(n).map(r=>({run_id:r.run_id,cron_id:r.cron_id,started:r.started,finished:r.finished,duration_ms:Number(r.duration_ms),exit_code:r.exit_code??null,stdout_head:r.stdout_head??null,stderr_head:r.stderr_head??null,delivered:r.delivered??null}))}o(en,"listCronRuns");function tn(){return i(a(),"SELECT * FROM crons WHERE run_state = 'running'").all().map(_)}o(tn,"runningCrons");function T(n,r){return i(n,"SELECT 1 FROM crons WHERE cron_id = ?").get(r.cron_id)===void 0?!1:(i(n,`INSERT INTO cron_runs (
|
|
30
30
|
run_id, cron_id, started, finished, duration_ms, exit_code,
|
|
31
31
|
stdout_head, stderr_head, delivered
|
|
32
32
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(r.run_id,r.cron_id,r.started,r.finished,r.duration_ms,r.exit_code,r.stdout_head,r.stderr_head,r.delivered),i(n,`DELETE FROM cron_runs
|
|
33
33
|
WHERE cron_id = ? AND run_id NOT IN (
|
|
34
34
|
SELECT run_id FROM cron_runs WHERE cron_id = ?
|
|
35
35
|
ORDER BY started DESC, run_id DESC LIMIT ?
|
|
36
|
-
)`).run(r.cron_id,r.cron_id,O),!0)}o(
|
|
36
|
+
)`).run(r.cron_id,r.cron_id,O),!0)}o(T,"recordCronRunLocked");function on(n){return d(r=>T(r,n))}o(on,"recordCronRun");export{O as CRON_RUN_RING,K as acquireCronRunLease,J as advanceCron,v as armCron,Q as attachCronRunPid,U as cancelCron,G as cancelCronsOnWake,$ as consumeCron,P as cronIdsForProfileDeletion,B as cronIdsForProfileDetach,M as cronSinkDisplay,b as deleteCronsForProfile,V as dueClockCrons,nn as dueOverlapCrons,rn as expiredIdleCrons,y as getCron,Y as globalizeCronsForProfile,z as hasPendingCronWake,c as killLeasedCronRun,en as listCronRuns,I as listCrons,p as parseCronSink,Z as pokeHeldCrons,on as recordCronRun,T as recordCronRunLocked,X as releaseCronRunLease,tn as runningCrons,q as setCronLastOutputHash,j as setCronState,g as stopLeasedCronRun,l as verifyLeasedCronRun};
|