@north-light/crouter 0.3.332 → 0.3.334

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 (61) hide show
  1. package/README.md +2 -2
  2. package/dist/api/command-manifest/manifest.js +1 -1
  3. package/dist/build-root.js +1 -1
  4. package/dist/builtin-memory/crouter-concepts/INDEX.md +13 -0
  5. package/dist/builtin-memory/crouter-concepts/README.md +19 -0
  6. package/dist/builtin-memory/crouter-concepts/lifecycle-and-wakes.md +28 -0
  7. package/dist/builtin-memory/crouter-concepts/memory.md +36 -0
  8. package/dist/builtin-memory/crouter-concepts/nodes-and-the-canvas.md +30 -0
  9. package/dist/builtin-memory/crouter-concepts/profiles-kinds-and-modes.md +33 -0
  10. package/dist/builtin-memory/crouter-concepts/scopes-and-trust.md +34 -0
  11. package/dist/builtin-memory/crouter-concepts/why-a-daemon.md +31 -0
  12. package/dist/builtin-memory/crouter-sdk/guides/README.md +19 -0
  13. package/dist/builtin-memory/crouter-sdk/guides/app-memory.md +73 -0
  14. package/dist/builtin-memory/crouter-sdk/guides/event-driven-assistant.md +91 -0
  15. package/dist/builtin-memory/crouter-sdk/guides/fan-out-pipeline.md +57 -0
  16. package/dist/builtin-memory/crouter-sdk/guides/human-approval.md +79 -0
  17. package/dist/builtin-memory/crouter-sdk/guides/typed-extraction.md +53 -0
  18. package/dist/clients/attach/input/capabilities.js +1 -1
  19. package/dist/clients/attach/overlays/help.js +1 -1
  20. package/dist/clients/attach/slash/dispatch.js +1 -1
  21. package/dist/clients/attach/viewer.js +413 -413
  22. package/dist/commands/sys/branch.js +1 -1
  23. package/dist/commands/sys/setup-core.d.ts +1 -1
  24. package/dist/commands/sys/setup-core.js +4 -4
  25. package/dist/commands/sys/tutorial/branch.d.ts +1 -0
  26. package/dist/commands/sys/tutorial/branch.js +1 -0
  27. package/dist/commands/sys/tutorial/lessons.d.ts +6 -0
  28. package/dist/commands/sys/tutorial/lessons.js +12 -0
  29. package/dist/commands/sys/tutorial/scenario.d.ts +1 -0
  30. package/dist/commands/sys/tutorial/scenario.js +2 -0
  31. package/dist/commands/sys/tutorial/tracks.d.ts +2 -0
  32. package/dist/commands/sys/tutorial/tracks.js +4 -0
  33. package/dist/commands/sys.js +1 -1
  34. package/dist/core/runtime/boot-root.d.ts +7 -0
  35. package/dist/core/runtime/boot-root.js +4 -4
  36. package/dist/core/runtime/first-run-offer.d.ts +11 -0
  37. package/dist/core/runtime/first-run-offer.js +5 -0
  38. package/dist/core/runtime/front-door.js +3 -3
  39. package/dist/daemon/api/handlers/node-events.js +1 -1
  40. package/dist/types.js +1 -1
  41. package/docs/cli/README.md +16 -0
  42. package/docs/cli/canvas-and-dashboard.md +56 -0
  43. package/docs/cli/first-session.md +55 -0
  44. package/docs/cli/human-inbox.md +34 -0
  45. package/docs/cli/memory-and-preferences.md +50 -0
  46. package/docs/cli/the-viewer.md +79 -0
  47. package/docs/concepts/README.md +17 -0
  48. package/docs/concepts/lifecycle-and-wakes.md +26 -0
  49. package/docs/concepts/memory.md +34 -0
  50. package/docs/concepts/nodes-and-the-canvas.md +28 -0
  51. package/docs/concepts/profiles-kinds-and-modes.md +31 -0
  52. package/docs/concepts/scopes-and-trust.md +32 -0
  53. package/docs/concepts/why-a-daemon.md +29 -0
  54. package/docs/sdk/guides/README.md +18 -0
  55. package/docs/sdk/guides/app-memory.md +22 -0
  56. package/docs/sdk/guides/event-driven-assistant.md +24 -0
  57. package/docs/sdk/guides/fan-out-pipeline.md +24 -0
  58. package/docs/sdk/guides/human-approval.md +24 -0
  59. package/docs/sdk/guides/typed-extraction.md +22 -0
  60. package/package.json +8 -8
  61. package/runtime.lock.json +196 -196
package/README.md CHANGED
@@ -18,11 +18,11 @@ crtr node new "Inspect this repository and report the failing tests."
18
18
 
19
19
  ## Use crouter from an application
20
20
 
21
- Install [`@north-light/crouter-sdk`](https://www.npmjs.com/package/@north-light/crouter-sdk) to create agent runs, receive typed results, stream activity, and use the daemon's other application-facing APIs. It connects to the local daemon or to a remote daemon with a bearer token. The [SDK guide](https://github.com/vallum-security/crouter/tree/main/docs/sdk) has working examples and the full API reference.
21
+ Install [`@north-light/crouter-sdk`](https://www.npmjs.com/package/@north-light/crouter-sdk) to create agent runs, receive typed results, stream activity, and use the daemon's other application-facing APIs. It connects to the local daemon or to a remote daemon with a bearer token. The [SDK guide](https://crouter-docs.vercel.app/docs/sdk) has working examples and the full API reference.
22
22
 
23
23
  ## Extend crouter
24
24
 
25
- - Build an HTTP plugin with [`@north-light/crouter-plugin`](https://www.npmjs.com/package/@north-light/crouter-plugin). Its [authoring guide](https://github.com/vallum-security/crouter/tree/main/docs/plugin) covers commands, parameters, output, errors, deployment, and plugin memory documents.
25
+ - Build an HTTP plugin with [`@north-light/crouter-plugin`](https://www.npmjs.com/package/@north-light/crouter-plugin). Its [authoring guide](https://crouter-docs.vercel.app/docs/plugin) covers commands, parameters, output, errors, deployment, and plugin memory documents.
26
26
  - Install plugins from the [crouter official marketplace](https://github.com/crouton-labs/crouter-official-marketplace). `crtr sys setup` offers to install and register it; then use `crtr pkg market browse crouter-official-marketplace` to see its plugins.
27
27
 
28
28
  ## Source and contributing
@@ -1 +1 @@
1
- var j=Object.defineProperty;var h=(e,n)=>j(e,"name",{value:n,configurable:!0});import{validateCommandNode as U}from"./schema.js";import{isRecord as y}from"../../shared/predicates.js";const g=/^[a-z][a-z0-9]*(-[a-z0-9]+)*$/,E=new Set(["GET","POST","PUT","PATCH","DELETE"]);function b(e){return e===null?"null":Array.isArray(e)?"array":typeof e}h(b,"typeName");function L(e,n){const a=[],o=h((t,v,k,x,A,_)=>{a.push({code:t,message:v,received:k,expected:x,next:A,..._?{path:_}:{}})},"issue");if(!y(e))return o("command_manifest_invalid","manifest must be an object",b(e),n.extensionFragment===!0?"{ schemaVersion, transport, mounts }":n.transport==="http"?"{ schemaVersion, baseUrl?, timeouts?, mounts, helpAddenda? }":"{ schemaVersion, mounts, helpAddenda? }","Provide a valid JSON manifest object."),{issues:a};const s=Object.keys(e),r=n.extensionFragment===!0?new Set(["schemaVersion","transport","mounts"]):n.transport==="http"?new Set(["schemaVersion","baseUrl","timeouts","mounts","helpAddenda"]):new Set(["schemaVersion","mounts","helpAddenda"]),l=s.filter(t=>!r.has(t));if(l.length>0)return o("command_manifest_invalid","unknown top-level keys",l.join(", "),n.extensionFragment===!0?"only: schemaVersion, transport, mounts":n.transport==="http"?"only: schemaVersion, baseUrl, timeouts, mounts, helpAddenda":"only: schemaVersion, mounts, helpAddenda","Remove the unknown keys."),{issues:a};if(e.schemaVersion!==1)return o("command_schema_version","schemaVersion must be exactly 1",String(e.schemaVersion),"1","Update the manifest schema version to 1.","schemaVersion"),{issues:a};let d;if(n.transport==="http"&&e.baseUrl!==void 0){if(typeof e.baseUrl!="string")return o("command_manifest_invalid","baseUrl must be a string",b(e.baseUrl),"an absolute HTTP(S) URL or omitted","Provide a valid baseUrl or remove it.","baseUrl"),{issues:a};try{const t=new URL(e.baseUrl);if(!["http:","https:"].includes(t.protocol))return o("command_manifest_invalid","baseUrl must be http: or https:",t.protocol,"http: or https:","Use an HTTP(S) URL.","baseUrl"),{issues:a};d=e.baseUrl}catch{return o("command_manifest_invalid","baseUrl must be a valid absolute URL",e.baseUrl,"a valid URL","Fix the baseUrl.","baseUrl"),{issues:a}}}let m;if(n.transport==="http"&&e.timeouts!==void 0){const t=$(e.timeouts,o);if(t===null)return{issues:a};m=t}let i;if(e.helpAddenda!==void 0){const t=M(e.helpAddenda,n.coreCommandPaths,o);if(t===null)return{issues:a};Object.keys(t).length>0&&(i=t)}const c=e.mounts;if(!Array.isArray(c))return o("command_manifest_invalid","mounts must be an array",b(c),"a non-empty array of { parent, node } contributions","Provide at least one mount.","mounts"),{issues:a};if(c.length===0)return o("command_manifest_invalid","mounts must be a non-empty array","empty array","at least one contribution","Add at least one mount.","mounts"),{issues:a};const u=[];for(let t=0;t<c.length;t++){const v=R(c[t],t,n.transport,o,n.extensionFragment===!0?{topLevelRootEntry:"forbidden",allowPassthrough:!1,allowExtensible:!1}:void 0);if(v===null)return{issues:a};u.push(v)}const f=u.filter(t=>t.parent.length>0&&n.extensibleCoreBranches?.has(t.parent[0])===!0);for(const t of f){if(t.parent.length!==1)return o("command_parent_invalid","core mount parent must name an extensible core branch directly",t.parent.join("."),`one extensible core branch (one of: ${[...n.extensibleCoreBranches??[]].join(", ")})`,`Mount below one of the extensible core branches: ${[...n.extensibleCoreBranches??[]].join(", ")}.`,t.parent.join(".")),{issues:a};if(n.coreCommandPaths?.has(`${t.parent[0]} ${t.node.name}`)===!0)return o("command_collision",`contributed command "${t.node.name}" conflicts with a child the extensible core branch already owns`,t.node.name,`a name not already owned by the ${t.parent[0]} core branch`,"Rename the contributed command or remove this mount.",`${t.parent.join(".")}.${t.node.name}`),{issues:a}}const p=F(u.filter(t=>!f.includes(t)),n.reservedCoreNames,n.extensibleCoreBranches,o);return p===null?{issues:a}:a.length===0?{manifest:{schemaVersion:1,...d!==void 0?{baseUrl:d}:{},...m!==void 0?{timeouts:m}:{},...i!==void 0?{helpAddenda:i}:{},roots:p,coreMounts:f},issues:[]}:{issues:a}}h(L,"validateCommandManifest");function $(e,n){if(!y(e))return n("command_manifest_invalid","timeouts must be an object",b(e),"{ connectMs?, requestMs?, streamIdleMs? }","Fix timeouts.","timeouts"),null;const a=new Set(["connectMs","requestMs","streamIdleMs"]),o=Object.keys(e).filter(r=>!a.has(r));if(o.length>0)return n("command_manifest_invalid","unknown timeouts keys",o.join(", "),"only: connectMs, requestMs, streamIdleMs","Remove the unknown keys.","timeouts"),null;const s={};for(const r of["connectMs","requestMs","streamIdleMs"])if(e[r]!==void 0){if(typeof e[r]!="number"||!Number.isInteger(e[r])||e[r]<=0)return n("command_manifest_invalid",`${r} must be a positive integer`,String(e[r]),"a positive integer",`Fix ${r}.`,`timeouts.${r}`),null;s[r]=e[r]}return s}h($,"validateTimeouts");function M(e,n,a){if(!y(e))return a("command_manifest_invalid","helpAddenda must be an object",b(e),"a map of core command path \u2192 addendum text","Fix helpAddenda.","helpAddenda"),null;const o={};for(const[s,r]of Object.entries(e)){if(s.split(" ").some(l=>!g.test(l)))return a("command_help_addendum_invalid","helpAddenda key must be a space-separated core command path",s,'kebab tokens separated by single spaces (e.g. "cron" or "cron add")',"Fix the helpAddenda key.",`helpAddenda.${s}`),null;if(typeof r!="string"||r.length===0)return a("command_manifest_invalid","helpAddenda value must be a non-empty string",typeof r=="string"?"empty string":b(r),"non-empty addendum text","Fix the helpAddenda value.",`helpAddenda.${s}`),null;if(n!==void 0&&!n.has(s))return a("command_help_addendum_invalid","helpAddenda key names no crtr core command path",s,'an existing core command path (e.g. "cron", "cron add")',"Fix the key or remove the addendum.",`helpAddenda.${s}`),null;o[s]=r}return o}h(M,"validateHelpAddenda");function R(e,n,a,o,s){const r=`mounts[${n}]`;if(!y(e))return o("command_manifest_invalid","mount must be an object",b(e),"{ parent, node }","Fix the mount.",r),null;const l=new Set(["parent","node"]),d=Object.keys(e).filter(p=>!l.has(p));if(d.length>0)return o("command_manifest_invalid","unknown mount keys",d.join(", "),"only: parent, node","Remove the unknown keys.",r),null;const m=e.parent;if(!Array.isArray(m))return o("command_manifest_invalid","mount parent must be an array",b(m),"an array of kebab tokens, or [] for a top-level mount","Fix parent.",`${r}.parent`),null;for(let p=0;p<m.length;p++){const t=m[p];if(typeof t!="string"||!g.test(t))return o("command_manifest_invalid","parent token must be a kebab token",String(t),"a kebab-case identifier","Fix the parent path.",`${r}.parent[${p}]`),null}const i=m,c=e.node,u=i.length===0,f=U(c,[`${r}.node`],u,a,o,s);return f===null?null:{parent:i,node:f}}h(R,"validateMount");function F(e,n,a,o){const s=e.filter(i=>i.parent.length===0),r=e.filter(i=>i.parent.length>0);for(const i of s)if(n.has(i.node.name))return o("command_collision","node name conflicts with a crtr core command",i.node.name,`a name not in core (not one of: ${[...n].join(", ")})`,"Rename the node or remove this mount.",`${i.node.name}`),null;const l=s.map(i=>i.node.kind==="branch"?{...i.node,children:[...i.node.children]}:{...i.node});for(const i of r)if(n.has(i.parent[0]))return o("command_parent_invalid","parent path starts with a core branch that is not extensible",i.parent[0],`an extensible core branch (one of: ${[...a??[]].join(", ")})`,`Mount below one of the extensible core branches: ${[...a??[]].join(", ")}.`,i.parent.join(".")),null;for(const i of r)if(!S(l,i,o))return null;const d=new Set,m=h((i,c)=>{const u=c.join(".");return d.has(u)?(o("command_node_invalid","duplicate node path",u,"unique paths within the manifest","Remove the duplicate node.",u),!1):(d.add(u),i.kind==="branch"?i.children.every(f=>m(f,[...c,f.name])):!0)},"indexPaths");for(const i of l)if(!m(i,[i.name]))return null;return l}h(F,"materializeForest");function S(e,n,a){const[o,...s]=n.parent,r=e.find(d=>d.name===o);if(!r)return a("command_parent_invalid","parent path starts with unknown branch",o,"a branch this manifest contributes","Fix the parent path or add the parent branch.",n.parent.join(".")),!1;if(r.kind==="leaf")return a("command_parent_invalid","parent path resolves to a leaf",o,"a branch","Change the parent to point to a branch.",n.parent.join(".")),!1;let l=r;for(const d of s){const m=l.children.find(i=>i.name===d);if(!m)return a("command_parent_invalid","parent path contains unknown branch",d,"a branch this manifest contributes","Fix the parent path or add the intermediate branch.",n.parent.join(".")),!1;if(m.kind==="leaf")return a("command_parent_invalid","parent path resolves to a leaf",d,"a branch","Change the parent to point to a branch.",n.parent.join(".")),!1;l=m}return l.passthrough!==void 0?(a("command_parent_invalid","parent path resolves to a passthrough branch",n.parent.join("."),"a branch without passthrough","Remove the nested mount or remove passthrough from its parent branch.",n.parent.join(".")),!1):l.children.some(d=>d.name===n.node.name)?(a("command_node_invalid","duplicate child name",n.node.name,"unique child names within a branch","Rename the node or remove the duplicate.",`${n.parent.join(".")}.${n.node.name}`),!1):(l.children.push(n.node),!0)}h(S,"resolveAndAttachMount");export{L as validateCommandManifest};
1
+ var j=Object.defineProperty;var h=(e,n)=>j(e,"name",{value:n,configurable:!0});import{validateCommandNode as U}from"./schema.js";import{isRecord as y}from"../../shared/predicates.js";const g=/^[a-z][a-z0-9]*(-[a-z0-9]+)*$/,E=new Set(["GET","POST","PUT","PATCH","DELETE"]);function b(e){return e===null?"null":Array.isArray(e)?"array":typeof e}h(b,"typeName");function L(e,n){const t=[],a=h((o,v,k,A,x,_)=>{t.push({code:o,message:v,received:k,expected:A,next:x,..._?{path:_}:{}})},"issue");if(!y(e))return a("command_manifest_invalid","manifest must be an object",b(e),n.extensionFragment===!0?"{ schemaVersion, transport, mounts }":n.transport==="http"?"{ schemaVersion, baseUrl?, timeouts?, mounts, helpAddenda? }":"{ schemaVersion, mounts, helpAddenda? }","Provide a valid JSON manifest object."),{issues:t};const s=Object.keys(e),r=n.extensionFragment===!0?new Set(["schemaVersion","transport","mounts"]):n.transport==="http"?new Set(["schemaVersion","baseUrl","timeouts","mounts","helpAddenda"]):new Set(["schemaVersion","mounts","helpAddenda"]),l=s.filter(o=>!r.has(o));if(l.length>0)return a("command_manifest_invalid","unknown top-level keys",l.join(", "),n.extensionFragment===!0?"only: schemaVersion, transport, mounts":n.transport==="http"?"only: schemaVersion, baseUrl, timeouts, mounts, helpAddenda":"only: schemaVersion, mounts, helpAddenda","Remove the unknown keys."),{issues:t};if(e.schemaVersion!==1)return a("command_schema_version","schemaVersion must be exactly 1",String(e.schemaVersion),"1","Update the manifest schema version to 1.","schemaVersion"),{issues:t};let d;if(n.transport==="http"&&e.baseUrl!==void 0){if(typeof e.baseUrl!="string")return a("command_manifest_invalid","baseUrl must be a string",b(e.baseUrl),"an absolute HTTP(S) URL or omitted","Provide a valid baseUrl or remove it.","baseUrl"),{issues:t};try{const o=new URL(e.baseUrl);if(!["http:","https:"].includes(o.protocol))return a("command_manifest_invalid","baseUrl must be http: or https:",o.protocol,"http: or https:","Use an HTTP(S) URL.","baseUrl"),{issues:t};d=e.baseUrl}catch{return a("command_manifest_invalid","baseUrl must be a valid absolute URL",e.baseUrl,"a valid URL","Fix the baseUrl.","baseUrl"),{issues:t}}}let m;if(n.transport==="http"&&e.timeouts!==void 0){const o=$(e.timeouts,a);if(o===null)return{issues:t};m=o}let i;if(e.helpAddenda!==void 0){const o=M(e.helpAddenda,n.coreCommandPaths,a);if(o===null)return{issues:t};Object.keys(o).length>0&&(i=o)}const c=e.mounts;if(!Array.isArray(c))return a("command_manifest_invalid","mounts must be an array",b(c),"a non-empty array of { parent, node } contributions","Provide at least one mount.","mounts"),{issues:t};if(c.length===0)return a("command_manifest_invalid","mounts must be a non-empty array","empty array","at least one contribution","Add at least one mount.","mounts"),{issues:t};const u=[];for(let o=0;o<c.length;o++){const v=R(c[o],o,n.transport,a,n.extensionFragment===!0?{topLevelRootEntry:"forbidden",allowPassthrough:!1,allowExtensible:!1}:void 0);if(v===null)return{issues:t};u.push(v)}const f=u.filter(o=>o.parent.length>0&&n.extensibleCoreBranches?.has(o.parent.join(" "))===!0);for(const o of f){const v=o.parent.join(" ");if(n.coreCommandPaths?.has(`${v} ${o.node.name}`)===!0)return a("command_collision",`contributed command "${o.node.name}" conflicts with a child the extensible core branch already owns`,o.node.name,`a name not already owned by the ${v} core branch`,"Rename the contributed command or remove this mount.",`${o.parent.join(".")}.${o.node.name}`),{issues:t}}const p=F(u.filter(o=>!f.includes(o)),n.reservedCoreNames,n.extensibleCoreBranches,a);return p===null?{issues:t}:t.length===0?{manifest:{schemaVersion:1,...d!==void 0?{baseUrl:d}:{},...m!==void 0?{timeouts:m}:{},...i!==void 0?{helpAddenda:i}:{},roots:p,coreMounts:f},issues:[]}:{issues:t}}h(L,"validateCommandManifest");function $(e,n){if(!y(e))return n("command_manifest_invalid","timeouts must be an object",b(e),"{ connectMs?, requestMs?, streamIdleMs? }","Fix timeouts.","timeouts"),null;const t=new Set(["connectMs","requestMs","streamIdleMs"]),a=Object.keys(e).filter(r=>!t.has(r));if(a.length>0)return n("command_manifest_invalid","unknown timeouts keys",a.join(", "),"only: connectMs, requestMs, streamIdleMs","Remove the unknown keys.","timeouts"),null;const s={};for(const r of["connectMs","requestMs","streamIdleMs"])if(e[r]!==void 0){if(typeof e[r]!="number"||!Number.isInteger(e[r])||e[r]<=0)return n("command_manifest_invalid",`${r} must be a positive integer`,String(e[r]),"a positive integer",`Fix ${r}.`,`timeouts.${r}`),null;s[r]=e[r]}return s}h($,"validateTimeouts");function M(e,n,t){if(!y(e))return t("command_manifest_invalid","helpAddenda must be an object",b(e),"a map of core command path \u2192 addendum text","Fix helpAddenda.","helpAddenda"),null;const a={};for(const[s,r]of Object.entries(e)){if(s.split(" ").some(l=>!g.test(l)))return t("command_help_addendum_invalid","helpAddenda key must be a space-separated core command path",s,'kebab tokens separated by single spaces (e.g. "cron" or "cron add")',"Fix the helpAddenda key.",`helpAddenda.${s}`),null;if(typeof r!="string"||r.length===0)return t("command_manifest_invalid","helpAddenda value must be a non-empty string",typeof r=="string"?"empty string":b(r),"non-empty addendum text","Fix the helpAddenda value.",`helpAddenda.${s}`),null;if(n!==void 0&&!n.has(s))return t("command_help_addendum_invalid","helpAddenda key names no crtr core command path",s,'an existing core command path (e.g. "cron", "cron add")',"Fix the key or remove the addendum.",`helpAddenda.${s}`),null;a[s]=r}return a}h(M,"validateHelpAddenda");function R(e,n,t,a,s){const r=`mounts[${n}]`;if(!y(e))return a("command_manifest_invalid","mount must be an object",b(e),"{ parent, node }","Fix the mount.",r),null;const l=new Set(["parent","node"]),d=Object.keys(e).filter(p=>!l.has(p));if(d.length>0)return a("command_manifest_invalid","unknown mount keys",d.join(", "),"only: parent, node","Remove the unknown keys.",r),null;const m=e.parent;if(!Array.isArray(m))return a("command_manifest_invalid","mount parent must be an array",b(m),"an array of kebab tokens, or [] for a top-level mount","Fix parent.",`${r}.parent`),null;for(let p=0;p<m.length;p++){const o=m[p];if(typeof o!="string"||!g.test(o))return a("command_manifest_invalid","parent token must be a kebab token",String(o),"a kebab-case identifier","Fix the parent path.",`${r}.parent[${p}]`),null}const i=m,c=e.node,u=i.length===0,f=U(c,[`${r}.node`],u,t,a,s);return f===null?null:{parent:i,node:f}}h(R,"validateMount");function F(e,n,t,a){const s=e.filter(i=>i.parent.length===0),r=e.filter(i=>i.parent.length>0);for(const i of s)if(n.has(i.node.name))return a("command_collision","node name conflicts with a crtr core command",i.node.name,`a name not in core (not one of: ${[...n].join(", ")})`,"Rename the node or remove this mount.",`${i.node.name}`),null;const l=s.map(i=>i.node.kind==="branch"?{...i.node,children:[...i.node.children]}:{...i.node});for(const i of r)if(n.has(i.parent[0]))return a("command_parent_invalid","parent path starts with a core branch that is not extensible",i.parent[0],`an extensible core branch (one of: ${[...t??[]].join(", ")})`,`Mount below one of the extensible core branches: ${[...t??[]].join(", ")}.`,i.parent.join(".")),null;for(const i of r)if(!S(l,i,a))return null;const d=new Set,m=h((i,c)=>{const u=c.join(".");return d.has(u)?(a("command_node_invalid","duplicate node path",u,"unique paths within the manifest","Remove the duplicate node.",u),!1):(d.add(u),i.kind==="branch"?i.children.every(f=>m(f,[...c,f.name])):!0)},"indexPaths");for(const i of l)if(!m(i,[i.name]))return null;return l}h(F,"materializeForest");function S(e,n,t){const[a,...s]=n.parent,r=e.find(d=>d.name===a);if(!r)return t("command_parent_invalid","parent path starts with unknown branch",a,"a branch this manifest contributes","Fix the parent path or add the parent branch.",n.parent.join(".")),!1;if(r.kind==="leaf")return t("command_parent_invalid","parent path resolves to a leaf",a,"a branch","Change the parent to point to a branch.",n.parent.join(".")),!1;let l=r;for(const d of s){const m=l.children.find(i=>i.name===d);if(!m)return t("command_parent_invalid","parent path contains unknown branch",d,"a branch this manifest contributes","Fix the parent path or add the intermediate branch.",n.parent.join(".")),!1;if(m.kind==="leaf")return t("command_parent_invalid","parent path resolves to a leaf",d,"a branch","Change the parent to point to a branch.",n.parent.join(".")),!1;l=m}return l.passthrough!==void 0?(t("command_parent_invalid","parent path resolves to a passthrough branch",n.parent.join("."),"a branch without passthrough","Remove the nested mount or remove passthrough from its parent branch.",n.parent.join(".")),!1):l.children.some(d=>d.name===n.node.name)?(t("command_node_invalid","duplicate child name",n.node.name,"unique child names within a branch","Rename the node or remove the duplicate.",`${n.parent.join(".")}.${n.node.name}`),!1):(l.children.push(n.node),!0)}h(S,"resolveAndAttachMount");export{L as validateCommandManifest};
@@ -1 +1 @@
1
- var b=Object.defineProperty;var o=(e,n)=>b(e,"name",{value:n,configurable:!0});import{defineBranch as P,defineRoot as c,GLOBAL_TOKENS as v}from"./core/command.js";import{createCoreHookCatalog as y}from"./core/command-hooks/catalog.js";import{composeCoreHooks as h}from"./core/command-hooks/compose.js";import{mark as l}from"./core/timing.js";import{envScopes as O,scopeAllowed as R}from"./shared/env.js";const m="crtr: agentic runtime.",d=[{name:"--json",desc:"stdout as the leaf's declared outputs in raw JSON instead of prose \u2014 for scripts (a cron's bash, external tooling) that must branch on canvas state; prose stays the contract for what you read yourself."}],p=Object.assign(Object.create(null),{memory:o(async()=>(await import("./commands/memory.js")).registerMemory(),"memory"),profile:o(async()=>(await import("./commands/profile.js")).registerProfile(),"profile"),pkg:o(async()=>(await import("./commands/pkg.js")).registerPkg(),"pkg"),human:o(async()=>(await import("./commands/human.js")).registerHuman(),"human"),sys:o(async()=>(await import("./commands/sys.js")).registerSys(),"sys"),node:o(async()=>(await import("./commands/node.js")).registerNode(),"node"),push:o(async()=>(await import("./commands/push.js")).registerPush(),"push"),cron:o(async()=>(await import("./commands/cron.js")).registerCron(),"cron"),canvas:o(async()=>(await import("./commands/canvas.js")).registerCanvas(),"canvas"),surface:o(async()=>(await import("./commands/surface.js")).registerSurface(),"surface")}),f=Object.freeze(Object.keys(p)),B={human:["ask"],cron:["schedule"],memory:["memory:read","memory:write"]};function w(e){const n=O();return n===null?e:e.filter(s=>{const r=B[s.name];return r===void 0||r.some(i=>R(n,i))})}o(w,"allowedSubtrees");let S;function u(){return S??=Promise.all(f.map(e=>p[e]())),S}o(u,"loadCoreSubtrees");let x;function T(){return l("cli.hooks.catalog"),x??=u().then(y),x}o(T,"coreHookCatalog");function C(){let e;return()=>(e??=(async()=>{const{compileHookRegistry:n,effectiveHookPlugins:s}=await import("./core/command-hooks/discovery.js"),r=s();return l("cli.hooks.effective_plugins",{count:r.length}),r.length===0?(l("cli.hooks.empty_registry"),n(y([]),[])):n(await T(),r)})(),e)}o(C,"hookRegistryLoader");function g(e){const n=new Set,s=o((r,i)=>{if(!(r.kind==="branch"&&r.passthrough!==void 0)&&(n.add(i),r.kind==="branch"))for(const t of r.children)s(t,`${i} ${t.name}`)},"visit");for(const r of e)s(r,r.name);return n}o(g,"commandPathsFor");async function D(){return g(await u())}o(D,"coreCommandPaths");async function G(){return new Set((await u()).filter(e=>e.extensible===!0).map(e=>e.name))}o(G,"extensibleCoreBranches");async function k(e){const n=new Set(e.filter(t=>t.extensible===!0).map(t=>t.name));if(n.size===0)return e;const[{buildExternalCommandSnapshot:s},{composeCoreBranchMounts:r}]=await Promise.all([import("./core/command-plugins/discovery.js"),import("./core/command-plugins/compose.js")]),i=s(new Set(f),void 0,void 0,g(e),n);return e.map(t=>{const a=r(i.coreMounts,[t.name]);if(a.length===0)return t;const{listing:_,...E}=t.help;return P({name:t.name,...t.description!==void 0?{description:t.description}:{},...t.whenToUse!==void 0?{whenToUse:t.whenToUse}:{},...t.tier!==void 0?{tier:t.tier}:{},...t.rootEntry!==void 0?{rootEntry:t.rootEntry}:{},...t.extensible===!0?{extensible:!0}:{},...t.slash!==void 0?{slash:t.slash}:{},...t.passthrough!==void 0?{passthrough:t.passthrough}:{},help:E,children:[...t.children,...a]})})}o(k,"composeCorePluginMounts");function z(e){const n=e.slice(2).filter(s=>!v.has(s));return n[0]==="sys"&&n[1]==="daemon"}o(z,"isDaemonControlInvocation");async function I(){const{registerDaemonControlSys:e}=await import("./commands/sys/daemon.js");return c({tagline:m,globals:d,subtrees:[e()]})}o(I,"resolveDaemonControlRoot");async function $(e){const n=e!==void 0?p[e]:void 0;if(n!==void 0){const s=w(await k([await n()]));if(s.length>0)return c({tagline:m,globals:d,subtrees:h(s,C())})}return L()}o($,"resolveRoot");async function L(){const e=await u(),[{buildExternalCommandSnapshot:n},{composeExternalSubtrees:s}]=await Promise.all([import("./core/command-plugins/discovery.js"),import("./core/command-plugins/compose.js")]),r=await k(e),i=n(new Set(f),void 0,void 0,g(e),new Set(r.filter(a=>a.extensible===!0).map(a=>a.name))),t=s(i);return c({tagline:m,globals:d,subtrees:[...h(w(r),C()),...t]})}o(L,"buildRoot");export{f as SUBTREE_NAMES,L as buildRoot,D as coreCommandPaths,T as coreHookCatalog,G as extensibleCoreBranches,z as isDaemonControlInvocation,I as resolveDaemonControlRoot,$ as resolveRoot};
1
+ var L=Object.defineProperty;var t=(e,n)=>L(e,"name",{value:n,configurable:!0});import{defineBranch as T,defineRoot as l,GLOBAL_TOKENS as _}from"./core/command.js";import{createCoreHookCatalog as b}from"./core/command-hooks/catalog.js";import{composeCoreHooks as C}from"./core/command-hooks/compose.js";import{mark as m}from"./core/timing.js";import{envScopes as j,scopeAllowed as A}from"./shared/env.js";const p="crtr: agentic runtime.",f=[{name:"--json",desc:"stdout as the leaf's declared outputs in raw JSON instead of prose \u2014 for scripts (a cron's bash, external tooling) that must branch on canvas state; prose stays the contract for what you read yourself."}],d=Object.assign(Object.create(null),{memory:t(async()=>(await import("./commands/memory.js")).registerMemory(),"memory"),profile:t(async()=>(await import("./commands/profile.js")).registerProfile(),"profile"),pkg:t(async()=>(await import("./commands/pkg.js")).registerPkg(),"pkg"),human:t(async()=>(await import("./commands/human.js")).registerHuman(),"human"),sys:t(async()=>(await import("./commands/sys.js")).registerSys(),"sys"),node:t(async()=>(await import("./commands/node.js")).registerNode(),"node"),push:t(async()=>(await import("./commands/push.js")).registerPush(),"push"),cron:t(async()=>(await import("./commands/cron.js")).registerCron(),"cron"),canvas:t(async()=>(await import("./commands/canvas.js")).registerCanvas(),"canvas"),surface:t(async()=>(await import("./commands/surface.js")).registerSurface(),"surface")}),g=Object.freeze(Object.keys(d)),H={human:["ask"],cron:["schedule"],memory:["memory:read","memory:write"]};function x(e){const n=j();return n===null?e:e.filter(r=>{const o=H[r.name];return o===void 0||o.some(s=>A(n,s))})}t(x,"allowedSubtrees");let E;function u(){return E??=Promise.all(g.map(e=>d[e]())),E}t(u,"loadCoreSubtrees");let P;function M(){return m("cli.hooks.catalog"),P??=u().then(b),P}t(M,"coreHookCatalog");function v(){let e;return()=>(e??=(async()=>{const{compileHookRegistry:n,effectiveHookPlugins:r}=await import("./core/command-hooks/discovery.js"),o=r();return m("cli.hooks.effective_plugins",{count:o.length}),o.length===0?(m("cli.hooks.empty_registry"),n(b([]),[])):n(await M(),o)})(),e)}t(v,"hookRegistryLoader");function h(e){const n=new Set,r=t((o,s)=>{if(!(o.kind==="branch"&&o.passthrough!==void 0)&&(n.add(s),o.kind==="branch"))for(const i of o.children)r(i,`${s} ${i.name}`)},"visit");for(const o of e)r(o,o.name);return n}t(h,"commandPathsFor");async function J(){return h(await u())}t(J,"coreCommandPaths");function N(e){const n=[],r=t((o,s)=>{o.extensible===!0&&n.push(s);for(const i of o.children)i.kind==="branch"&&r(i,[...s,i.name])},"visit");for(const o of e)r(o,[o.name]);return n}t(N,"extensibleBranchPaths");async function B(){return new Set(N(await u()).map(e=>e.join(" ")))}t(B,"extensibleCoreBranches");function U(e,n){const{listing:r,...o}=e.help;return T({name:e.name,description:e.description,whenToUse:e.whenToUse,tier:e.tier,help:o,rootEntry:e.rootEntry,extensible:e.extensible,plugin:e.plugin,slash:e.slash,passthrough:e.passthrough,children:n})}t(U,"rebuildBranch");async function O(e,n){if(n.size===0)return e;const[{buildExternalCommandSnapshot:r},{composeCoreBranchMounts:o}]=await Promise.all([import("./core/command-plugins/discovery.js"),import("./core/command-plugins/compose.js")]),s=r(new Set(g),void 0,void 0,h(await u()),n),i=t((a,y)=>{let w=!1;const R=a.children.map(c=>{if(c.kind!=="branch")return c;const k=i(c,[...y,c.name]);return k!==c&&(w=!0),k}),S=a.extensible===!0?o(s.coreMounts,y):[];return w||S.length>0?U(a,[...R,...S]):a},"composeBranch");return e.map(a=>i(a,[a.name]))}t(O,"composeCorePluginMounts");function K(e){const n=e.slice(2).filter(r=>!_.has(r));return n[0]==="sys"&&n[1]==="daemon"}t(K,"isDaemonControlInvocation");async function Q(){const{registerDaemonControlSys:e}=await import("./commands/sys/daemon.js");return l({tagline:p,globals:f,subtrees:[e()]})}t(Q,"resolveDaemonControlRoot");async function V(e){const n=e!==void 0?d[e]:void 0;if(n!==void 0){const r=x(await O([await n()],await B()));if(r.length>0)return l({tagline:p,globals:f,subtrees:C(r,v())})}return D()}t(V,"resolveRoot");async function D(){const e=await u(),[{buildExternalCommandSnapshot:n},{composeExternalSubtrees:r}]=await Promise.all([import("./core/command-plugins/discovery.js"),import("./core/command-plugins/compose.js")]),o=await B(),s=await O(e,o),i=n(new Set(g),void 0,void 0,h(e),o),a=r(i);return l({tagline:p,globals:f,subtrees:[...C(x(s),v()),...a]})}t(D,"buildRoot");export{g as SUBTREE_NAMES,D as buildRoot,J as coreCommandPaths,M as coreHookCatalog,B as extensibleCoreBranches,K as isDaemonControlInvocation,Q as resolveDaemonControlRoot,V as resolveRoot};
@@ -0,0 +1,13 @@
1
+ ---
2
+ kind: knowledge
3
+ description: Why crouter is shaped the way it is — durable nodes, the canvas graph, dormancy, memory tiers, profiles, scoped authority, and one daemon
4
+ when-and-why-to-read: When you are choosing the shape of an agent application on crtr — whether work needs a durable node, whether it should finish or stay resident, where memory belongs, or why only the daemon writes canvas state — this section should be read because picking an SDK method or plugin field before understanding the runtime model produces an application that fights the runtime instead of using it.
5
+ short-form: The concepts behind the crtr runtime, read before choosing an SDK method or plugin field — nodes and the canvas, lifecycle and wakes, memory, profiles/kinds/modes, scopes and trust, and why a daemon.
6
+ surfaces:
7
+ - on: boot
8
+ at: name
9
+ ---
10
+
11
+ # `crouter-concepts/` — choose the crouter runtime shape before using the SDK
12
+
13
+ Read `crouter-concepts/README` for the section overview. These pages explain why crouter uses durable nodes, the canvas graph, free dormancy, memory tiers, profiles, scoped authority, and one daemon before an application chooses an SDK method or plugin field.
@@ -0,0 +1,19 @@
1
+ ---
2
+ kind: knowledge
3
+ when-and-why-to-read: When building with crtr, read this section because its
4
+ pages explain which runtime parts solve each problem before you choose an SDK
5
+ method or plugin field.
6
+ ---
7
+
8
+ # Concepts
9
+
10
+ Use these pages to choose the shape of an agent application before you start wiring it. They explain the durable runtime model behind the SDK and plugin surfaces, then point to the detailed guides for each surface.
11
+
12
+ | Page | Decide |
13
+ |---|---|
14
+ | [[crouter-concepts/nodes-and-the-canvas]] | Whether work needs a durable node rather than one request and response |
15
+ | [[crouter-concepts/lifecycle-and-wakes]] | Whether a node should finish, remain resident, delegate, or wait |
16
+ | [[crouter-concepts/memory]] | What an agent should know or embody, who owns it, and where it belongs |
17
+ | [[crouter-concepts/profiles-kinds-and-modes]] | Which run-shaping dial matches an application concern |
18
+ | [[crouter-concepts/scopes-and-trust]] | What an application or node may do and why only the daemon writes canvas state |
19
+ | [[crouter-concepts/why-a-daemon]] | Why a broker survives a terminal and why viewers are clients rather than hosts |
@@ -0,0 +1,28 @@
1
+ ---
2
+ kind: knowledge
3
+ when-and-why-to-read: When an agent must react to later work, read this because
4
+ lifecycle and wake choices let it sleep without a process while preserving its
5
+ goal and the event that should resume it.
6
+ ---
7
+
8
+ # Lifecycle and wakes
9
+
10
+ Choose a node’s lifecycle from the kind of relationship it has with work. A **terminal** node owes a final report when it is done. A **resident** node is for an ongoing conversation with a person: it can become dormant and be messaged again without first finalizing. Resident does not mean “keep a process running.” Dormant nodes have no live broker or model turn and cost no context window or compute.
11
+
12
+ Mode answers a different question. A **base** node works hands-on and may delegate a clearly separate piece. An **orchestrator** owns enough independent parallel work that decomposition, delegation, and integration are now its main job. Long or sequential work is not enough reason to orchestrate; keep one hands-on owner and give it a fresh context when needed. Lifecycle answers whether a conversation remains open; mode answers who does the work.
13
+
14
+ ```mermaid
15
+ flowchart LR
16
+ Active[Active node] -->|nothing to do now| Dormant[Dormant: no broker]
17
+ Child[Child report] --> Inbox
18
+ Message[Message or human answer] --> Inbox
19
+ Cron[Cron or deadline] --> Inbox
20
+ Inbox -->|wake| Active
21
+ Active -->|terminal final| Finished[Finished]
22
+ ```
23
+
24
+ A node wakes when work actually arrives: a subscribed child pushes a report, another node or an application sends a message, a person answers a human request, or a cron action delivers work. A deadline is a scheduled wake that races an otherwise unpushable wait. The inbox is the durable path for these triggers, so a node can stop between them without losing its goal.
25
+
26
+ Do not keep a node active to poll a child, a person, or a message. Creation automatically subscribes a parent to its child, and the runtime delivers the child’s outcome. Waiting for something the canvas can push is free: end the turn and let the node become dormant. Schedule a cron only for recurring work or an external condition that nothing can push into the canvas, such as checking a CI run. A timer added “just in case” a child does not report duplicates a runtime guarantee and hides a runtime defect.
27
+
28
+ A broker crash does not erase the node. The daemon retains the durable node row, conversation, waits, and outstanding inbox entries, then applies its recovery policy. This is what makes a resident event-driven assistant practical: it can wait for a webhook, be dormant for hours, and resume its saved work only when the webhook produces a message. Use [[crouter-sdk/nodes]] to deliver that external event, and run `crtr memory read internal/nodes-and-canvas` for lifecycle operations.
@@ -0,0 +1,36 @@
1
+ ---
2
+ kind: knowledge
3
+ when-and-why-to-read: When shaping an agent without rewriting its prompt, read
4
+ this because memory documents separate facts to consult from behavior to
5
+ embody and let each owner control who receives them.
6
+ ---
7
+
8
+ # Memory
9
+
10
+ Memory shapes a node without adding the same instructions to every prompt. A memory document says what it contains, who owns it, and when it should enter context. The result is durable guidance that can be discovered when relevant instead of a growing startup prompt.
11
+
12
+ There are two kinds. **Knowledge** is something an agent consults: a procedure, fact, or technical reference. A **preference** is behavior the agent should embody: a standing directive or correction. This is a use-based split. A procedure and a fact are both knowledge because the agent reads either one to answer a question; a preference changes how it acts.
13
+
14
+ | Tier | Owner | Put here |
15
+ |---|---|---|
16
+ | Node | One running node | A note needed across that node’s fresh contexts |
17
+ | Project | One repository or workspace | Repository facts and procedures |
18
+ | Profile | One application identity and its purview | Application-wide conventions and knowledge |
19
+ | User | One person | Facts and preferences that follow them everywhere |
20
+ | Builtin | crouter | Runtime documentation that ships to every user |
21
+
22
+ Choose the narrowest tier that reaches the next agent who needs the document. For an application author, the profile store is the usual home for knowledge shared by that application’s nodes across repositories. [[crouter-sdk/memory]] names a target for every operation, so the daemon resolves the application’s profile rather than its own current directory. A node’s `memory:read` and `memory:write` scopes can respectively permit reading while denying changes.
23
+
24
+ ```mermaid
25
+ flowchart LR
26
+ Doc[Memory document] --> Surface[Surface entry: event, gate, rung]
27
+ Surface -->|event matches| Preview[Name, preview, or full body]
28
+ Preview --> Route[Routing line tells the agent why to read]
29
+ Route --> Read[Explicit full read when needed]
30
+ ```
31
+
32
+ A surface entry is the delivery mechanism. A document is not loaded because it sits in a particular folder or because its routing line resembles the task. Its frontmatter names an event such as boot, workspace-open, file read, memory read, command, or pre-command; that entry can also match a path or gate on the node’s shape and selects a rung: its name, a preview, or its full content. A document with no surface entry stays in its directory listing until an agent deliberately finds or reads it.
33
+
34
+ The routing line is the preview at the middle rung, not a trigger. For example, a profile document can have a file-read surface that delivers a preview when an order record is opened. Its line — “When handling a refund request, read this because the eligibility window is not in the order record” — then tells the agent why an explicit full read is useful. The event delivers the preview; the line helps the agent decide whether to read the body without pretending to replace it.
35
+
36
+ Memory therefore supports progressive disclosure. You can record knowledge freely, but it costs future contexts only when an explicit surface route delivers it. Read `crtr memory write -h` to author a document and `crtr memory read internal/memory-loading` for the routing mechanics. The SDK [[crouter-sdk/memory]] shows how an application creates and revises its profile documents.
@@ -0,0 +1,30 @@
1
+ ---
2
+ kind: knowledge
3
+ when-and-why-to-read: When deciding whether an agent task should outlive one
4
+ request, read this because a node gives the task durable identity, context,
5
+ reports, and a path for later messages.
6
+ ---
7
+
8
+ # Nodes and the canvas
9
+
10
+ Use a node when the work may need a follow-up, a report, a child, or time without a caller holding a request open. A node is a durable unit of agent work: it has an identity, a goal, graph relationships, a context directory for artifacts, and a broker that hosts its agent engine. It is not an HTTP request with an LLM response attached.
11
+
12
+ The canvas is the durable graph of those nodes. It makes an agent's work and its relationship to other work visible after the process that created it has returned. This is why [[crouter-sdk/nodes]] returns a node you can retrieve, message, stream, or wait on instead of only returning generated text. An application can create a root node, show its progress, and later call `nodes.message` when a person or an external event has more work for it.
13
+
14
+ ```mermaid
15
+ flowchart TD
16
+ App[Application] -->|creates or messages| Daemon[crtrd]
17
+ Daemon --> Node[Durable node]
18
+ Node --> Broker[Broker and agent engine]
19
+ Node --> Context[Context directory and artifacts]
20
+ Child[Child node] -->|pushes a report| Node
21
+ Node -->|pushes a report| Parent[Subscriber]
22
+ ```
23
+
24
+ A graph edge is not just a visual parent-child line. The management relationship records who owns a child, while subscriptions carry report delivery. On normal child creation, the parent subscribes to the child, so the child’s final report wakes the parent. A node can have other subscribers too; report delivery is intentionally separate from hierarchy.
25
+
26
+ The one way work reports upward is a **push**. A push writes a durable report and puts a reference in each subscriber’s feed. Nothing is reported merely because a node stopped producing text. That makes a report an explicit claim a subscriber can inspect, rather than an inference from terminal output. The parent can then integrate the child’s result instead of repeating the work.
27
+
28
+ A node owns an outcome, not merely an artifact. It may write files and reports while working, but its terminal result is credible only when it has evidence that the requested goal was met. The canvas supports that responsibility: it preserves the goal, durable artifacts, reports, and relationships across fresh contexts and broker replacement.
29
+
30
+ Use a one-shot SDK call such as [[crouter-sdk/nodes]] when work is bounded and its only useful output is a typed result. Use `nodes.create` when the application needs a continuing conversation or must observe work while it runs. For the operational graph and report model, run `crtr memory read internal/nodes-and-canvas`.
@@ -0,0 +1,33 @@
1
+ ---
2
+ kind: knowledge
3
+ when-and-why-to-read: When designing an agent run, read this because profiles,
4
+ kinds, modes, and memory tiers solve different problems and prevent a long
5
+ prompt from becoming an unstable substitute for an application identity.
6
+ ---
7
+
8
+ # Profiles, kinds, and modes
9
+
10
+ Shape a run by choosing the dial that owns the decision. The four dials are independent: a useful profile does not imply an orchestrator, and a specialist kind does not decide where its knowledge lives.
11
+
12
+ | Dial | It answers | Use it when |
13
+ |---|---|---|
14
+ | Profile | Which application identity, project purview, environment, and profile memory apply? | An application or body of work has stable directories and conventions |
15
+ | Kind | What standing role, model tier, tools, and expertise should the agent have? | The work matches a recurring role such as developer or reviewer |
16
+ | Mode | Does this node work hands-on or coordinate independent children? | Parallel work makes coordination the main job |
17
+ | Memory tier | Who should receive a document? | Guidance must reach one node, project, profile, user, or all crouter users |
18
+
19
+ A profile is not a label on a run. It is a stable agent identity with its own memory store and a purview of project directories. It lets an application create nodes from the same target context even if the caller runs elsewhere. Create a profile when an app has a durable set of directories, environment values, and conventions worth sharing. Do not make a profile for every repository or individual request. Use [[crouter-sdk/resources]] and the SDK’s `profile` create field to select the application’s identity.
20
+
21
+ ```mermaid
22
+ flowchart LR
23
+ Profile[Profile: purview, environment, memory] --> Node
24
+ Kind[Kind: role and tools] --> Node
25
+ Mode[Mode: base or orchestrator] --> Node
26
+ Tier[Memory tier: reach] --> Node
27
+ ```
28
+
29
+ A kind is a recurring role, not a decorative name. It carries a role-specific posture and may choose a suitable model tier and tools. A custom kind beats a long prompt when the role recurs and needs standing discipline that should survive every run: for example, an application’s compliance reviewer that always needs the same tools, expertise, and model choice. A one-off instruction belongs in the node’s prompt, where it does not create a permanent persona to maintain.
30
+
31
+ Base mode is the normal choice: the node owns and performs the work, using a child only for a separable part. Promote to orchestrator only when independent parts can proceed in parallel and the benefits outweigh coordination and integration. A terminal orchestrator still finishes normally; residency is separate and belongs to a person-facing ongoing conversation.
32
+
33
+ These choices keep the application prompt focused. Identity belongs in a profile, standing role in a kind, task-specific intent in the prompt, coordination in mode, and reusable knowledge in the narrowest memory tier. For the full selection rules, run `crtr memory read internal/agent-shaping`; [[crouter-sdk/nodes]] documents the profile, kind, and mode fields an application passes.
@@ -0,0 +1,34 @@
1
+ ---
2
+ kind: knowledge
3
+ when-and-why-to-read: When giving an application or node less authority, read
4
+ this because a scope list is an allow-list enforced by the daemon and a bearer
5
+ token can set the maximum authority for every run it creates.
6
+ ---
7
+
8
+ # Scopes and trust
9
+
10
+ Use scopes to give a node or remote application only the authority it needs. A node carries a scope list; a scoped bearer token carries a ceiling. Omitting a node scope list gives it the inherited runtime vocabulary. Supplying one narrows that authority. A token cannot create a node with scopes outside its own ceiling.
11
+
12
+ | Scope family | It gates |
13
+ |---|---|
14
+ | `ask` | Human requests and review actions |
15
+ | `act` | Node creation on behalf of a node and node messaging |
16
+ | `schedule` | Creating scheduled work |
17
+ | `memory:read` / `memory:write` | Reading or changing memory through a node target |
18
+ | `llm`, `net`, and parameterized forms such as `files:<dir>` | Declared capability categories; some performers are recorded rather than enforced in the current beta |
19
+
20
+ This is an allow-list, not a claim that code will behave. The daemon checks the scope at the route that performs the action and rejects an unavailable one. The SDK’s [[crouter-sdk/nodes]] `scopes` field narrows a run; `client.memory` additionally checks `memory:read` or `memory:write` when you make a request on behalf of a node. A node may be allowed to consult its application’s knowledge while being unable to rewrite it.
21
+
22
+ ```mermaid
23
+ flowchart LR
24
+ Token[Bearer token ceiling] --> Request[SDK request]
25
+ Request --> Daemon[crtrd]
26
+ Daemon -->|within ceiling| Node[Node with narrowed scopes]
27
+ Daemon -->|outside ceiling| Denied[403 scope_denied]
28
+ ```
29
+
30
+ Scopes rely on a more basic trust boundary: `crtrd` is the sole writer of durable canvas state and the sole owner of broker lifecycle. SDK clients, the CLI, and viewers call its API; they do not open the canvas database or launch their own broker. One owner serializes lifecycle changes, makes the same API usable locally and remotely, and keeps a client from silently creating a second state authority.
31
+
32
+ For a browser or remote process, run `crtr sys connect`. It enables the daemon’s TCP listener when needed and returns `base_url` plus a bearer `token` to give the application. `crtr sys connect --scopes …` mints a new scoped token instead of returning the owner token. Treat either token as a credential: the remote app sends it as `Authorization: Bearer <token>`, and its scope list is the ceiling for scope-gated calls and newly created nodes.
33
+
34
+ The remote API is an explicit boundary, not permission to reach around it. Keep application code on the SDK or `/v1` contract, and let the daemon own state transitions. Run `crtr memory read internal/nodes-and-canvas` for the operational ownership model.
@@ -0,0 +1,31 @@
1
+ ---
2
+ kind: knowledge
3
+ when-and-why-to-read: When deciding how an application should host or reconnect
4
+ to an agent, read this because the daemon keeps the durable canvas and broker
5
+ lifecycle in one place while terminals and SDK clients come and go.
6
+ ---
7
+
8
+ # Why a daemon
9
+
10
+ A terminal is not an agent host. A broker is the detached process that hosts one node’s agent engine and session; a viewer is only a terminal presentation attached to that broker. The daemon, `crtrd`, owns the durable canvas and starts, stops, and recovers brokers. This separation lets a node continue after the terminal that created or displayed it has gone away.
11
+
12
+ ```mermaid
13
+ flowchart LR
14
+ CLI[crtr CLI] --> API[/v1 API]
15
+ SDK[SDK application] --> API
16
+ Viewer[Terminal viewer] --> API
17
+ API --> Daemon[crtrd]
18
+ Daemon --> Canvas[Canvas state]
19
+ Daemon --> Broker[Detached broker]
20
+ Viewer -. live session .-> Broker
21
+ ```
22
+
23
+ The broker has one job: host a node’s agent session. The daemon has the broader job: preserve the node graph, state transitions, waits, reports, and broker lifecycle. A viewer may stream a broker’s live session, but it does not become the authority that decides whether that node is active, dormant, or revived. Closing a viewer closes a view, not the node’s durable identity.
24
+
25
+ This is why a process crash is recoverable rather than an automatic loss of work. The daemon retains the node row, session information, artifacts, waits, and inbox state, then applies recovery to the broker execution. A later wake or explicit revival can continue the node’s saved conversation. The application should create nodes through [[crouter-sdk/nodes]], not start an LLM process itself, because the daemon is the component that keeps the engine and durable canvas state coordinated.
26
+
27
+ The costs are real. A daemon must be running, and canvas state has one home instead of being scattered across terminals and application processes. Local clients use its owner-only Unix socket. Remote clients use its configured TCP listener and bearer token. A network interruption can therefore fail a mutation loudly rather than encouraging a client to replay it and risk doing the action twice.
28
+
29
+ Those costs buy a simpler model: one state owner, one broker launcher, and clients that can reconnect. A CLI command, an SDK application, and a terminal viewer all use the same API for canvas state. They can come and go without creating competing writers or losing the graph that explains what each agent is doing.
30
+
31
+ For the lower-level runtime model, run `crtr memory read internal/nodes-and-canvas`. For a complete remote application setup, start with the SDK [[crouter-sdk/getting-started]].
@@ -0,0 +1,19 @@
1
+ ---
2
+ kind: knowledge
3
+ when-and-why-to-read: When composing crouter SDK namespaces into an application,
4
+ read this because it helps you choose a complete runnable starting point.
5
+ ---
6
+
7
+ # SDK recipes
8
+
9
+ These recipes turn the SDK namespaces into complete applications: a bounded extraction, an assistant that waits for events, a research pipeline, a human approval step, and application-owned memory.
10
+
11
+ | Recipe | Use it when | It ends with |
12
+ |---|---|---|
13
+ | [[crouter-sdk/guides/typed-extraction]] | You need one structured answer and no follow-up. | A typed object or an explicit failure. |
14
+ | [[crouter-sdk/guides/event-driven-assistant]] | Events arrive over time from a webhook, queue, or watcher. | A resident node that wakes for each event. |
15
+ | [[crouter-sdk/guides/fan-out-pipeline]] | One task needs independent research before a synthesis. | An orchestrator's final report. |
16
+ | [[crouter-sdk/guides/human-approval]] | A person must decide before work continues. | A node resumed by an inbox answer. |
17
+ | [[crouter-sdk/guides/app-memory]] | Several runs need the same durable application knowledge. | A profile-owned document read by a scoped run. |
18
+
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.
@@ -0,0 +1,73 @@
1
+ ---
2
+ kind: knowledge
3
+ when-and-why-to-read: When an application's agents need shared durable
4
+ knowledge, read this because a profile store gives runs one owned memory
5
+ location and scopes can allow reading without writing.
6
+ ---
7
+
8
+ # Application memory
9
+
10
+ Use this recipe when several runs need the same application guidance. Run `npx tsx examples/guides/app-memory.ts /path/to/repo`; it ensures a profile, creates a profile-owned knowledge document if it does not already exist, then starts a run allowed to read but not write memory.
11
+
12
+ ```ts
13
+ import Crouter, { NotFoundError } from '@north-light/crouter-sdk';
14
+ import { resolve } from 'node:path';
15
+ import { z } from 'zod';
16
+
17
+ const client = new Crouter();
18
+ const cwd = resolve(process.argv[2] ?? process.cwd());
19
+ const profileName = process.env.APP_PROFILE ?? 'release-notes-app';
20
+
21
+ const profile = await client.profiles.ensure(profileName, {
22
+ projects: [{ path: cwd, memory: 'content' }],
23
+ });
24
+
25
+ try {
26
+ await client.memory.retrieve('release/voice', { profile: profile.id, store: 'profile' });
27
+ } catch (error) {
28
+ if (!(error instanceof NotFoundError)) throw error;
29
+ await client.memory.create({
30
+ name: 'release/voice',
31
+ kind: 'knowledge',
32
+ when_and_why_to_read: 'When writing release notes, read this because it defines the product voice and terms customers recognize.',
33
+ body: 'Use short factual sentences. Name the user-visible result before implementation details.',
34
+ frontmatter: { surfaces: ['{"on":"boot","at":"preview"}'] },
35
+ profile: profile.id,
36
+ store: 'profile',
37
+ });
38
+ }
39
+
40
+ const result = await client.nodes.parse({
41
+ cwd,
42
+ model: process.env.GUIDE_MODEL,
43
+ profile: profile.id,
44
+ root: true,
45
+ root_lifecycle: 'terminal',
46
+ deadline: '5m',
47
+ scopes: ['memory:read'],
48
+ prompt: 'Write one short release note announcing that customers can download invoices as CSV from account settings. Do not write or update memory.',
49
+ output_schema: z.object({ release_note: z.string() }),
50
+ });
51
+
52
+ if (result.kind === 'result') {
53
+ console.log(result.output_parsed.release_note);
54
+ } else if (result.reason === 'declined') {
55
+ console.error(`agent declined: ${result.declined?.reason ?? 'no reason supplied'}`);
56
+ process.exitCode = 2;
57
+ } else {
58
+ console.error(`agent failed: ${result.reason}`, result.detail);
59
+ process.exitCode = 1;
60
+ }
61
+ ```
62
+
63
+ A profile is the application's durable identity: it names the memory store and the projects whose context the profile can reach. The routing line is part of the document, not decoration. State both the moment the agent should read it and the reason it matters; a document with a vague routing line will not surface when the agent needs it.
64
+
65
+ The `boot`/`preview` surface exposes the document's routing line when this profile's agent starts. The task asks for a release note without naming `release/voice`; in the local run the agent read that document before producing the note. Without a surface entry, a memory document appears only in its directory listing and its routing line cannot select it at boot. The run passes `scopes: ['memory:read']`. That is an allow-list: node-targeted memory reads are allowed and writes are refused because `memory:write` is absent. The application itself is not narrowed by those node scopes, so keep application credentials and its memory mutation policy separate from the agent's capabilities.
66
+
67
+ Observed against the local daemon with `APP_PROFILE=release-notes-app-guide-boot-preview GUIDE_MODEL=openai-codex/gpt-6-sol:high`:
68
+
69
+ ```text
70
+ You can now download your invoices as a CSV from account settings.
71
+ ```
72
+
73
+ Use profile memory for knowledge shared across the application's related projects. Put facts that belong to one repository in project memory instead, and temporary run notes in node memory. See [[crouter-concepts/memory]] for the ownership tiers and [[crouter-concepts/profiles-kinds-and-modes]] for what a profile changes.
@@ -0,0 +1,91 @@
1
+ ---
2
+ kind: knowledge
3
+ when-and-why-to-read: When an assistant must react to later webhook, queue, or
4
+ file-watcher events, read this because a resident node sleeps until
5
+ nodes.message delivers the next event.
6
+ ---
7
+
8
+ # Event-driven assistant
9
+
10
+ Use this recipe for an assistant that reacts to an event source instead of ending after one request. Run `npx tsx examples/guides/event-driven-assistant.ts /path/to/repo`, then type events into standard input to stand in for a webhook handler or file watcher.
11
+
12
+ ```ts
13
+ import Crouter from '@north-light/crouter-sdk';
14
+ import { resolve } from 'node:path';
15
+ import { createInterface } from 'node:readline';
16
+
17
+ const client = new Crouter();
18
+ const cwd = resolve(process.argv[2] ?? process.cwd());
19
+
20
+ const assistant = await client.nodes.create({
21
+ name: 'event assistant',
22
+ cwd,
23
+ model: process.env.GUIDE_MODEL,
24
+ root: true,
25
+ root_lifecycle: 'resident',
26
+ no_kickoff: true,
27
+ situational_context: `You are a standing assistant. Each inbox message is an event from an external source. For each event, briefly state what happened and one useful next action, then push that response as an update report with crtr push update. Do not push final. End the turn and go dormant after reporting. Do not poll or schedule a timer: another event will arrive as a message.`,
28
+ });
29
+
30
+ console.log(`assistant node: ${assistant.node_id}`);
31
+ console.log('Type an event and press Enter. Press Ctrl-C to stop the assistant.');
32
+
33
+ const input = createInterface({ input: process.stdin, crlfDelay: Infinity });
34
+ let stopping: Promise<void> | undefined;
35
+
36
+ function stop(): Promise<void> {
37
+ if (stopping !== undefined) return stopping;
38
+ input.close();
39
+ stopping = client.nodes.cancel(assistant.node_id).then(() => undefined);
40
+ return stopping;
41
+ }
42
+
43
+ process.once('SIGINT', () => {
44
+ void stop().catch((error: unknown) => {
45
+ console.error(error);
46
+ process.exitCode = 1;
47
+ });
48
+ });
49
+
50
+ for await (const line of input) {
51
+ if (stopping !== undefined) break;
52
+ if (line.trim() === '') continue;
53
+ try {
54
+ const previous = (await client.nodes.reports.list(assistant.node_id, { limit: 1 }))[0]?.path;
55
+ if (stopping !== undefined) break;
56
+ await client.nodes.message(assistant.node_id, { body: line });
57
+ while (stopping === undefined) {
58
+ const report = (await client.nodes.reports.list(assistant.node_id, { limit: 1 }))[0];
59
+ if (stopping !== undefined) break;
60
+ if (report !== undefined && report.path !== previous) {
61
+ console.log(report.body);
62
+ break;
63
+ }
64
+ const state = await client.nodes.retrieve(assistant.node_id);
65
+ if (stopping !== undefined) break;
66
+ if (state.fault?.retry.disposition === 'fatal' || state.status === 'dead' || state.status === 'canceled') {
67
+ throw new Error(`assistant stopped without reporting: ${state.fault?.message ?? state.status}`);
68
+ }
69
+ await new Promise((resolve) => setTimeout(resolve, 1000));
70
+ }
71
+ } catch (error) {
72
+ if (stopping === undefined) throw error;
73
+ }
74
+ }
75
+
76
+ await stop();
77
+ ```
78
+
79
+ The external source owns detection. When it receives an event, it calls `nodes.message(nodeId, { body })`. `no_kickoff` leaves the resident node waiting for the first event, while `situational_context` tells it how to handle every event. A dormant resident node wakes for the message; a running node reads it at its next turn boundary. Keep the returned node id with the application so each later event reaches the same context and memory. The agent pushes one update report per event, which the application reads through `nodes.reports.list` and prints. The application checks for a fatal node fault rather than waiting forever for a report that cannot arrive.
80
+
81
+ The agent does not poll or schedule a timer: the daemon wakes it on each message, and it goes dormant after reporting. This console program polls for the report associated with the event it just sent; an application with its own event loop can also consume the node's event stream. Ctrl-C calls `nodes.cancel()` only because this interactive example needs an explicit way to stop.
82
+
83
+ Observed against the local daemon with `GUIDE_MODEL=openai-codex/gpt-6-sol:high` after sending `Build completed with 2 failing checks: lint and unit tests.`:
84
+
85
+ ```text
86
+ assistant node: 3zl47w7d-mud5amcx-67978c9b
87
+ Type an event and press Enter. Press Ctrl-C to stop the assistant.
88
+ The build completed, but **lint and unit tests failed**. Inspect the two failing check logs first to identify the errors.
89
+ ```
90
+
91
+ Use a resident node only when future events belong to the same ongoing assistant. For bounded work, use [[crouter-sdk/guides/typed-extraction]]. See [[crouter-concepts/lifecycle-and-wakes]] for why waiting is free and [[crouter-concepts/why-a-daemon]] for why the process can outlive the terminal that started it.
@@ -0,0 +1,57 @@
1
+ ---
2
+ kind: knowledge
3
+ when-and-why-to-read: When independent parts of one job need separate agent work
4
+ before one synthesis, read this because an orchestrator can spawn children,
5
+ wait for reports, and publish a final result while the SDK streams progress.
6
+ ---
7
+
8
+ # Fan-out pipeline
9
+
10
+ Use this recipe when one result needs independent investigation first. Run `npx tsx examples/guides/fan-out-pipeline.ts /path/to/repo`; it asks an orchestrator to inspect a package through two children, prints live text and pushed reports, then prints the final reports retained on the node.
11
+
12
+ ```ts
13
+ import Crouter from '@north-light/crouter-sdk';
14
+ import { resolve } from 'node:path';
15
+
16
+ const client = new Crouter();
17
+ const cwd = resolve(process.argv[2] ?? process.cwd());
18
+
19
+ const stream = client.nodes.stream({
20
+ name: 'research pipeline',
21
+ cwd,
22
+ root: true,
23
+ root_lifecycle: 'terminal',
24
+ mode: 'orchestrator',
25
+ deadline: '10m',
26
+ prompt: `Research the package in this directory. Spawn two focused children: one should inspect package.json and one should inspect the README. Wait for their reports, reconcile them, then push a final report with the package name, purpose, and one risk or unknown.`,
27
+ });
28
+
29
+ const node = await stream.node;
30
+ console.log(`orchestrator node: ${node.node_id}`);
31
+
32
+ for await (const event of stream) {
33
+ if (event.type === 'node.output_text.delta') process.stdout.write(event.delta);
34
+ if (event.type === 'node.report.pushed') console.log(`\nreport: ${event.report.body}`);
35
+ }
36
+
37
+ const outcome = await stream.finalOutcome();
38
+ const reports = await client.nodes.reports.list(node.node_id, { limit: 10 });
39
+ console.log(`\nfinal outcome: ${outcome.kind}`);
40
+ for (const report of reports) console.log(`[${report.tier}] ${report.body}`);
41
+
42
+ if (outcome.kind !== 'result') process.exitCode = 1;
43
+ ```
44
+
45
+ `nodes.stream()` creates the orchestrator and observes it. It does not run the orchestration in your process: the node owns its children, waits for their pushed reports, and produces the synthesis. The stream gives your application live output and reports; `nodes.reports.list()` gives it the durable report view after settlement.
46
+
47
+ Keep orchestration inside the canvas when children need the canvas's report delivery, durable waits, and a parent that can be inspected or resumed. Keep it in your application when tasks are simple independent calls and the application already owns their scheduling and aggregation. Do not use an orchestrator merely because a task is long; it earns the extra structure only when child work can run independently.
48
+
49
+ A local run created `j6amiqxs-mud4drnk-330da196`; after its two children reported, `waitForOutcome()` returned:
50
+
51
+ ```text
52
+ { "kind": "result", "reason": "finalized" }
53
+ ```
54
+
55
+ The same run's `nodes.stream()` observer ended during a dormant gap before it could print this outcome. A separate daemon fix is correcting that observer behavior; the example remains the intended streamed shape, but its streamed path has not yet been observed end to end.
56
+
57
+ See [[crouter-concepts/nodes-and-the-canvas]] for reports and durable identities, and [[crouter-concepts/profiles-kinds-and-modes]] for the base-versus-orchestrator decision.