@crouton-kit/crouter 0.3.33 → 0.3.34

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 (566) hide show
  1. package/README.md +2 -2
  2. package/dist/build-root.js +3 -6
  3. package/dist/builtin-memory/crouter-development/marketplaces.md +6 -6
  4. package/dist/builtin-memory/crouter-development/plugins.md +13 -13
  5. package/dist/builtin-memory/internal/examples/imessage-assistant.md +3 -3
  6. package/dist/builtin-memory/internal/nodes-and-canvas.md +4 -4
  7. package/dist/builtin-personas/developer/PERSONA.md +1 -1
  8. package/dist/builtin-personas/orchestration-kernel.md +1 -1
  9. package/dist/builtin-personas/waiting.md +1 -1
  10. package/dist/builtin-pi-packages/pi-crtr-extensions/README.md +71 -0
  11. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/__tests__/crtr-help-gate.test.ts +41 -0
  12. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/__tests__/provider-rotation.test.ts +663 -0
  13. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/__tests__/statusline.test.ts +52 -0
  14. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/claude-plugin-commands.ts +418 -0
  15. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/crouter-help.ts +87 -0
  16. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/crtr-commands/filters.json +7 -0
  17. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/crtr-commands/index.ts +174 -0
  18. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/crtr-help-gate.ts +408 -0
  19. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/frontmatter-rules/index.ts +442 -0
  20. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/frontmatter-rules/test.ts +119 -0
  21. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/nested-context.ts +327 -0
  22. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/provider-rotation.ts +574 -0
  23. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/statusline.ts +260 -0
  24. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/strip-skills-docs.ts +40 -0
  25. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/sysprompt-window.ts +19 -0
  26. package/dist/builtin-pi-packages/pi-crtr-extensions/lib/package.json +3 -0
  27. package/dist/builtin-pi-packages/pi-crtr-extensions/lib/subscription-state.js +1 -0
  28. package/dist/builtin-pi-packages/pi-crtr-extensions/lib/subscription-state.ts +479 -0
  29. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/LICENSE +13 -0
  30. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/README.md +172 -0
  31. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/bin.mjs +11 -0
  32. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/browser/dist/compose/compose-collection.js +88 -0
  33. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/browser/dist/compose/compose-doc.js +43 -0
  34. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/browser/dist/compose/compose-node.js +109 -0
  35. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/browser/dist/compose/compose-scalar.js +86 -0
  36. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/browser/dist/compose/composer.js +219 -0
  37. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/browser/dist/compose/resolve-block-map.js +115 -0
  38. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/browser/dist/compose/resolve-block-scalar.js +198 -0
  39. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/browser/dist/compose/resolve-block-seq.js +49 -0
  40. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/browser/dist/compose/resolve-end.js +37 -0
  41. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/browser/dist/compose/resolve-flow-collection.js +207 -0
  42. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/browser/dist/compose/resolve-flow-scalar.js +225 -0
  43. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/browser/dist/compose/resolve-props.js +146 -0
  44. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/browser/dist/compose/util-contains-newline.js +34 -0
  45. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/browser/dist/compose/util-empty-scalar-position.js +26 -0
  46. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/browser/dist/compose/util-flow-indent-check.js +15 -0
  47. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/browser/dist/compose/util-map-includes.js +13 -0
  48. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/browser/dist/doc/Document.js +335 -0
  49. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/browser/dist/doc/anchors.js +71 -0
  50. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/browser/dist/doc/applyReviver.js +55 -0
  51. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/browser/dist/doc/createNode.js +88 -0
  52. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/browser/dist/doc/directives.js +176 -0
  53. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/browser/dist/errors.js +57 -0
  54. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/browser/dist/index.js +17 -0
  55. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/browser/dist/log.js +11 -0
  56. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/browser/dist/nodes/Alias.js +116 -0
  57. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/browser/dist/nodes/Collection.js +147 -0
  58. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/browser/dist/nodes/Node.js +38 -0
  59. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/browser/dist/nodes/Pair.js +36 -0
  60. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/browser/dist/nodes/Scalar.js +24 -0
  61. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/browser/dist/nodes/YAMLMap.js +144 -0
  62. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/browser/dist/nodes/YAMLSeq.js +113 -0
  63. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/browser/dist/nodes/addPairToJSMap.js +63 -0
  64. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/browser/dist/nodes/identity.js +36 -0
  65. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/browser/dist/nodes/toJS.js +37 -0
  66. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/browser/dist/parse/cst-scalar.js +214 -0
  67. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/browser/dist/parse/cst-stringify.js +61 -0
  68. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/browser/dist/parse/cst-visit.js +97 -0
  69. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/browser/dist/parse/cst.js +98 -0
  70. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/browser/dist/parse/lexer.js +721 -0
  71. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/browser/dist/parse/line-counter.js +39 -0
  72. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/browser/dist/parse/parser.js +975 -0
  73. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/browser/dist/public-api.js +102 -0
  74. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/browser/dist/schema/Schema.js +37 -0
  75. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/browser/dist/schema/common/map.js +17 -0
  76. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/browser/dist/schema/common/null.js +15 -0
  77. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/browser/dist/schema/common/seq.js +17 -0
  78. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/browser/dist/schema/common/string.js +14 -0
  79. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/browser/dist/schema/core/bool.js +19 -0
  80. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/browser/dist/schema/core/float.js +43 -0
  81. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/browser/dist/schema/core/int.js +38 -0
  82. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/browser/dist/schema/core/schema.js +23 -0
  83. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/browser/dist/schema/json/schema.js +62 -0
  84. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/browser/dist/schema/tags.js +96 -0
  85. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/browser/dist/schema/yaml-1.1/binary.js +58 -0
  86. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/browser/dist/schema/yaml-1.1/bool.js +26 -0
  87. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/browser/dist/schema/yaml-1.1/float.js +46 -0
  88. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/browser/dist/schema/yaml-1.1/int.js +71 -0
  89. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/browser/dist/schema/yaml-1.1/merge.js +67 -0
  90. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/browser/dist/schema/yaml-1.1/omap.js +74 -0
  91. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/browser/dist/schema/yaml-1.1/pairs.js +78 -0
  92. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/browser/dist/schema/yaml-1.1/schema.js +39 -0
  93. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/browser/dist/schema/yaml-1.1/set.js +93 -0
  94. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/browser/dist/schema/yaml-1.1/timestamp.js +101 -0
  95. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/browser/dist/stringify/foldFlowLines.js +146 -0
  96. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/browser/dist/stringify/stringify.js +129 -0
  97. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/browser/dist/stringify/stringifyCollection.js +153 -0
  98. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/browser/dist/stringify/stringifyComment.js +20 -0
  99. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/browser/dist/stringify/stringifyDocument.js +85 -0
  100. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/browser/dist/stringify/stringifyNumber.js +25 -0
  101. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/browser/dist/stringify/stringifyPair.js +150 -0
  102. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/browser/dist/stringify/stringifyString.js +336 -0
  103. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/browser/dist/util.js +11 -0
  104. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/browser/dist/visit.js +233 -0
  105. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/browser/index.js +5 -0
  106. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/browser/package.json +3 -0
  107. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/cli.d.ts +8 -0
  108. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/cli.mjs +201 -0
  109. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/compose/compose-collection.d.ts +11 -0
  110. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/compose/compose-collection.js +90 -0
  111. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/compose/compose-doc.d.ts +7 -0
  112. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/compose/compose-doc.js +45 -0
  113. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/compose/compose-node.d.ts +29 -0
  114. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/compose/compose-node.js +112 -0
  115. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/compose/compose-scalar.d.ts +5 -0
  116. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/compose/compose-scalar.js +88 -0
  117. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/compose/composer.d.ts +63 -0
  118. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/compose/composer.js +224 -0
  119. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/compose/resolve-block-map.d.ts +6 -0
  120. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/compose/resolve-block-map.js +117 -0
  121. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/compose/resolve-block-scalar.d.ts +11 -0
  122. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/compose/resolve-block-scalar.js +200 -0
  123. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/compose/resolve-block-seq.d.ts +6 -0
  124. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/compose/resolve-block-seq.js +51 -0
  125. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/compose/resolve-end.d.ts +6 -0
  126. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/compose/resolve-end.js +39 -0
  127. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/compose/resolve-flow-collection.d.ts +7 -0
  128. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/compose/resolve-flow-collection.js +209 -0
  129. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/compose/resolve-flow-scalar.d.ts +10 -0
  130. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/compose/resolve-flow-scalar.js +227 -0
  131. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/compose/resolve-props.d.ts +23 -0
  132. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/compose/resolve-props.js +148 -0
  133. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/compose/util-contains-newline.d.ts +2 -0
  134. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/compose/util-contains-newline.js +36 -0
  135. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/compose/util-empty-scalar-position.d.ts +2 -0
  136. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/compose/util-empty-scalar-position.js +28 -0
  137. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/compose/util-flow-indent-check.d.ts +3 -0
  138. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/compose/util-flow-indent-check.js +17 -0
  139. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/compose/util-map-includes.d.ts +4 -0
  140. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/compose/util-map-includes.js +15 -0
  141. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/doc/Document.d.ts +141 -0
  142. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/doc/Document.js +337 -0
  143. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/doc/anchors.d.ts +24 -0
  144. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/doc/anchors.js +76 -0
  145. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/doc/applyReviver.d.ts +9 -0
  146. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/doc/applyReviver.js +57 -0
  147. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/doc/createNode.d.ts +17 -0
  148. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/doc/createNode.js +90 -0
  149. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/doc/directives.d.ts +49 -0
  150. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/doc/directives.js +178 -0
  151. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/errors.d.ts +21 -0
  152. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/errors.js +62 -0
  153. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/index.d.ts +25 -0
  154. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/index.js +50 -0
  155. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/log.d.ts +3 -0
  156. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/log.js +19 -0
  157. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/nodes/Alias.d.ts +29 -0
  158. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/nodes/Alias.js +118 -0
  159. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/nodes/Collection.d.ts +73 -0
  160. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/nodes/Collection.js +151 -0
  161. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/nodes/Node.d.ts +53 -0
  162. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/nodes/Node.js +40 -0
  163. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/nodes/Pair.d.ts +22 -0
  164. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/nodes/Pair.js +39 -0
  165. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/nodes/Scalar.d.ts +47 -0
  166. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/nodes/Scalar.js +27 -0
  167. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/nodes/YAMLMap.d.ts +53 -0
  168. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/nodes/YAMLMap.js +147 -0
  169. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/nodes/YAMLSeq.d.ts +60 -0
  170. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/nodes/YAMLSeq.js +115 -0
  171. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/nodes/addPairToJSMap.d.ts +4 -0
  172. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/nodes/addPairToJSMap.js +65 -0
  173. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/nodes/identity.d.ts +23 -0
  174. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/nodes/identity.js +53 -0
  175. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/nodes/toJS.d.ts +29 -0
  176. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/nodes/toJS.js +39 -0
  177. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/options.d.ts +350 -0
  178. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/parse/cst-scalar.d.ts +64 -0
  179. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/parse/cst-scalar.js +218 -0
  180. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/parse/cst-stringify.d.ts +8 -0
  181. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/parse/cst-stringify.js +63 -0
  182. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/parse/cst-visit.d.ts +39 -0
  183. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/parse/cst-visit.js +99 -0
  184. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/parse/cst.d.ts +109 -0
  185. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/parse/cst.js +112 -0
  186. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/parse/lexer.d.ts +87 -0
  187. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/parse/lexer.js +723 -0
  188. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/parse/line-counter.d.ts +22 -0
  189. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/parse/line-counter.js +41 -0
  190. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/parse/parser.d.ts +84 -0
  191. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/parse/parser.js +980 -0
  192. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/public-api.d.ts +44 -0
  193. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/public-api.js +107 -0
  194. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/schema/Schema.d.ts +17 -0
  195. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/schema/Schema.js +39 -0
  196. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/schema/common/map.d.ts +2 -0
  197. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/schema/common/map.js +19 -0
  198. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/schema/common/null.d.ts +4 -0
  199. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/schema/common/null.js +17 -0
  200. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/schema/common/seq.d.ts +2 -0
  201. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/schema/common/seq.js +19 -0
  202. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/schema/common/string.d.ts +2 -0
  203. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/schema/common/string.js +16 -0
  204. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/schema/core/bool.d.ts +4 -0
  205. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/schema/core/bool.js +21 -0
  206. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/schema/core/float.d.ts +4 -0
  207. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/schema/core/float.js +47 -0
  208. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/schema/core/int.d.ts +4 -0
  209. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/schema/core/int.js +42 -0
  210. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/schema/core/schema.d.ts +1 -0
  211. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/schema/core/schema.js +25 -0
  212. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/schema/json/schema.d.ts +2 -0
  213. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/schema/json/schema.js +64 -0
  214. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/schema/json-schema.d.ts +69 -0
  215. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/schema/tags.d.ts +48 -0
  216. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/schema/tags.js +99 -0
  217. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/schema/types.d.ts +92 -0
  218. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/schema/yaml-1.1/binary.d.ts +2 -0
  219. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/schema/yaml-1.1/binary.js +70 -0
  220. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/schema/yaml-1.1/bool.d.ts +7 -0
  221. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/schema/yaml-1.1/bool.js +29 -0
  222. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/schema/yaml-1.1/float.d.ts +4 -0
  223. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/schema/yaml-1.1/float.js +50 -0
  224. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/schema/yaml-1.1/int.d.ts +5 -0
  225. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/schema/yaml-1.1/int.js +76 -0
  226. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/schema/yaml-1.1/merge.d.ts +9 -0
  227. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/schema/yaml-1.1/merge.js +71 -0
  228. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/schema/yaml-1.1/omap.d.ts +22 -0
  229. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/schema/yaml-1.1/omap.js +77 -0
  230. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/schema/yaml-1.1/pairs.d.ts +10 -0
  231. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/schema/yaml-1.1/pairs.js +82 -0
  232. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/schema/yaml-1.1/schema.d.ts +1 -0
  233. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/schema/yaml-1.1/schema.js +41 -0
  234. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/schema/yaml-1.1/set.d.ts +28 -0
  235. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/schema/yaml-1.1/set.js +96 -0
  236. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/schema/yaml-1.1/timestamp.d.ts +6 -0
  237. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/schema/yaml-1.1/timestamp.js +105 -0
  238. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/stringify/foldFlowLines.d.ts +34 -0
  239. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/stringify/foldFlowLines.js +151 -0
  240. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/stringify/stringify.d.ts +21 -0
  241. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/stringify/stringify.js +132 -0
  242. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/stringify/stringifyCollection.d.ts +17 -0
  243. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/stringify/stringifyCollection.js +155 -0
  244. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/stringify/stringifyComment.d.ts +10 -0
  245. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/stringify/stringifyComment.js +24 -0
  246. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/stringify/stringifyDocument.d.ts +4 -0
  247. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/stringify/stringifyDocument.js +87 -0
  248. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/stringify/stringifyNumber.d.ts +2 -0
  249. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/stringify/stringifyNumber.js +27 -0
  250. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/stringify/stringifyPair.d.ts +3 -0
  251. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/stringify/stringifyPair.js +152 -0
  252. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/stringify/stringifyString.d.ts +9 -0
  253. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/stringify/stringifyString.js +338 -0
  254. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/test-events.d.ts +4 -0
  255. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/test-events.js +134 -0
  256. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/util.d.ts +16 -0
  257. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/util.js +28 -0
  258. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/visit.d.ts +102 -0
  259. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/visit.js +236 -0
  260. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/package.json +97 -0
  261. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/util.js +2 -0
  262. package/dist/builtin-pi-packages/pi-crtr-extensions/package-lock.json +3385 -0
  263. package/dist/builtin-pi-packages/pi-crtr-extensions/package.json +25 -0
  264. package/dist/builtin-pi-packages/pi-mode-switch/README.md +64 -0
  265. package/dist/builtin-pi-packages/pi-mode-switch/bin/mode +40 -0
  266. package/dist/builtin-pi-packages/pi-mode-switch/extensions/index.ts +422 -0
  267. package/dist/builtin-pi-packages/pi-mode-switch/package.json +13 -0
  268. package/dist/clients/attach/__tests__/crtr-output-coverage.test.js +55 -0
  269. package/dist/clients/attach/__tests__/crtr-output-render.test.js +2340 -0
  270. package/dist/clients/attach/attach-cmd.js +826 -754
  271. package/dist/clients/attach/auth-pickers.js +2 -2
  272. package/dist/clients/attach/canvas-panels.js +1 -1
  273. package/dist/clients/attach/chat-view.d.ts +3 -2
  274. package/dist/clients/attach/chat-view.js +30 -10
  275. package/dist/clients/attach/clipboard-image.js +1 -1
  276. package/dist/clients/attach/clipboard-text.js +1 -1
  277. package/dist/clients/attach/config-load.js +1 -1
  278. package/dist/clients/attach/crtr-output-render.d.ts +48 -0
  279. package/dist/clients/attach/crtr-output-render.js +646 -0
  280. package/dist/clients/attach/extension-dialogs.js +1 -1
  281. package/dist/clients/attach/graph-overlay.js +3 -3
  282. package/dist/clients/attach/input-controller.js +1 -1
  283. package/dist/clients/attach/pickers.js +1 -1
  284. package/dist/clients/attach/slash-commands.js +5 -6
  285. package/dist/clients/attach/titled-editor.d.ts +4 -0
  286. package/dist/clients/attach/titled-editor.js +13 -1
  287. package/dist/clients/attach/view-socket.js +1 -1
  288. package/dist/clients/web/__tests__/source-cache.test.js +5 -5
  289. package/dist/clients/web/server.js +29 -5
  290. package/dist/clients/web/source-cache.js +3 -3
  291. package/dist/clients/web/web-cmd.js +7 -7
  292. package/dist/commands/canvas-issue.js +1 -1
  293. package/dist/commands/canvas-prune.js +24 -2
  294. package/dist/commands/canvas-snapshot.js +3 -0
  295. package/dist/commands/canvas-tmux-spread.js +3 -3
  296. package/dist/commands/human.js +1 -1
  297. package/dist/commands/memory/lint.js +15 -8
  298. package/dist/commands/memory/origin.d.ts +1 -0
  299. package/dist/commands/memory/origin.js +88 -0
  300. package/dist/commands/memory/shared.d.ts +8 -0
  301. package/dist/commands/memory/shared.js +22 -1
  302. package/dist/commands/memory/write.js +17 -12
  303. package/dist/commands/memory.js +4 -3
  304. package/dist/commands/node-context.js +4 -4
  305. package/dist/commands/node-snapshot.js +4 -1
  306. package/dist/commands/node.d.ts +2 -2
  307. package/dist/commands/node.js +814 -606
  308. package/dist/commands/pkg/market-inspect.d.ts +2 -1
  309. package/dist/commands/pkg/market-inspect.js +14 -32
  310. package/dist/commands/pkg/market-manage.d.ts +3 -1
  311. package/dist/commands/pkg/market-manage.js +58 -154
  312. package/dist/commands/pkg/market.js +7 -7
  313. package/dist/commands/pkg/plugin-inspect.d.ts +2 -1
  314. package/dist/commands/pkg/plugin-inspect.js +8 -24
  315. package/dist/commands/pkg/plugin-manage.d.ts +5 -1
  316. package/dist/commands/pkg/plugin-manage.js +245 -171
  317. package/dist/commands/pkg/plugin.js +6 -6
  318. package/dist/commands/pkg.js +5 -5
  319. package/dist/commands/revive.js +5 -3
  320. package/dist/commands/search/answer.js +1 -1
  321. package/dist/commands/search/contents.js +1 -1
  322. package/dist/commands/search/web.js +2 -2
  323. package/dist/commands/search.js +1 -1
  324. package/dist/commands/surface.d.ts +2 -0
  325. package/dist/commands/surface.js +43 -0
  326. package/dist/commands/sys/promptstudio.js +1 -1
  327. package/dist/commands/sys/settings.js +2 -2
  328. package/dist/commands/sys/setup-core.d.ts +88 -0
  329. package/dist/commands/sys/setup-core.js +352 -0
  330. package/dist/commands/sys/setup.d.ts +2 -0
  331. package/dist/commands/sys/setup.js +516 -0
  332. package/dist/commands/sys/sysprompt.js +3 -0
  333. package/dist/commands/sys.js +3 -2
  334. package/dist/commands/view-cycle.js +5 -5
  335. package/dist/commands/view-list.js +3 -3
  336. package/dist/commands/view-new.js +5 -5
  337. package/dist/commands/view-pick.js +6 -6
  338. package/dist/commands/view-run.js +14 -14
  339. package/dist/commands/view.js +3 -3
  340. package/dist/commands/workspace.js +10 -10
  341. package/dist/core/__tests__/boot.test.js +140 -0
  342. package/dist/core/__tests__/broker-sdk-wiring.test.js +6 -27
  343. package/dist/core/__tests__/daemon-boot.test.js +22 -0
  344. package/dist/core/__tests__/fixtures/fake-engine.js +1 -1
  345. package/dist/core/__tests__/full/broker-attach-limits.test.js +1 -1
  346. package/dist/core/__tests__/full/broker-attach-stream.test.js +1 -1
  347. package/dist/core/__tests__/full/broker-crash-teardown.test.js +10 -5
  348. package/dist/core/__tests__/full/broker-model-changed-broadcast.test.js +1 -1
  349. package/dist/core/__tests__/full/broker-navigate-tree-rewelcome.test.js +1 -1
  350. package/dist/core/__tests__/full/broker-pane-resolution.test.js +1 -1
  351. package/dist/core/__tests__/full/cascade-close.test.js +31 -18
  352. package/dist/core/__tests__/full/detach-focus.test.js +2 -2
  353. package/dist/core/__tests__/hearth-bootstrap.test.js +136 -0
  354. package/dist/core/__tests__/helpers/harness.js +1 -1
  355. package/dist/core/__tests__/live-mutation-verbs.test.js +10 -10
  356. package/dist/core/__tests__/live-mutation.test.js +8 -8
  357. package/dist/core/__tests__/migration.test.js +124 -0
  358. package/dist/core/__tests__/on-read-dedup-resume.test.js +40 -0
  359. package/dist/core/__tests__/preview-registry-sync.test.js +36 -0
  360. package/dist/core/__tests__/prune-to-limit.test.js +91 -0
  361. package/dist/core/__tests__/revive.test.js +64 -21
  362. package/dist/core/__tests__/trigger-migration.test.js +48 -0
  363. package/dist/core/__tests__/wake-bearings.test.js +23 -21
  364. package/dist/core/__tests__/wake-origin.test.js +36 -33
  365. package/dist/core/canvas/boot.d.ts +48 -0
  366. package/dist/core/canvas/boot.js +157 -0
  367. package/dist/core/canvas/canvas.d.ts +27 -1
  368. package/dist/core/canvas/canvas.js +86 -5
  369. package/dist/core/canvas/db.js +86 -1
  370. package/dist/core/canvas/index.d.ts +1 -1
  371. package/dist/core/canvas/index.js +1 -1
  372. package/dist/core/canvas/nav-model.js +1 -1
  373. package/dist/core/canvas/pid.d.ts +6 -0
  374. package/dist/core/canvas/pid.js +13 -0
  375. package/dist/core/canvas/triggers.d.ts +108 -0
  376. package/dist/core/canvas/triggers.js +320 -0
  377. package/dist/core/canvas/types.d.ts +128 -46
  378. package/dist/core/command.js +13 -4
  379. package/dist/core/hearth/__tests__/model-auth-guest.test.js +151 -0
  380. package/dist/core/hearth/config.d.ts +1 -0
  381. package/dist/core/hearth/config.js +36 -0
  382. package/dist/core/hearth/guest-env.d.ts +7 -0
  383. package/dist/core/hearth/guest-env.js +17 -1
  384. package/dist/core/hearth/model-auth-guest.d.ts +8 -0
  385. package/dist/core/hearth/model-auth-guest.js +435 -0
  386. package/dist/core/hearth/provider.d.ts +13 -6
  387. package/dist/core/hearth/providers/blaxel-bootstrap.d.ts +2 -0
  388. package/dist/core/hearth/providers/blaxel-bootstrap.js +127 -19
  389. package/dist/core/hearth/providers/blaxel-home.d.ts +8 -4
  390. package/dist/core/hearth/providers/blaxel-home.js +85 -75
  391. package/dist/core/hearth/providers/blaxel.d.ts +5 -0
  392. package/dist/core/hearth/providers/blaxel.js +86 -12
  393. package/dist/core/hearth/providers/types.d.ts +6 -1
  394. package/dist/core/hearth/registry.js +2 -1
  395. package/dist/core/hearth/types.d.ts +35 -0
  396. package/dist/core/help.d.ts +16 -0
  397. package/dist/core/help.js +2 -0
  398. package/dist/core/memory-resolver.d.ts +8 -8
  399. package/dist/core/memory-resolver.js +58 -26
  400. package/dist/core/preview-registry.d.ts +25 -0
  401. package/dist/core/preview-registry.js +2047 -0
  402. package/dist/core/resolver.d.ts +1 -0
  403. package/dist/core/resolver.js +23 -16
  404. package/dist/core/runtime/bearings.d.ts +11 -10
  405. package/dist/core/runtime/bearings.js +8 -8
  406. package/dist/core/runtime/broker-protocol.d.ts +2 -2
  407. package/dist/core/runtime/broker-protocol.js +2 -2
  408. package/dist/core/runtime/close.js +2 -2
  409. package/dist/core/runtime/kickoff.js +1 -1
  410. package/dist/core/runtime/launch.d.ts +9 -3
  411. package/dist/core/runtime/launch.js +17 -9
  412. package/dist/core/runtime/lifecycle.js +7 -7
  413. package/dist/core/runtime/model-swap.js +1 -1
  414. package/dist/core/runtime/placement.d.ts +5 -5
  415. package/dist/core/runtime/placement.js +10 -10
  416. package/dist/core/runtime/recycle.js +7 -7
  417. package/dist/core/runtime/reset.d.ts +1 -1
  418. package/dist/core/runtime/reset.js +3 -3
  419. package/dist/core/runtime/revive-all.d.ts +5 -2
  420. package/dist/core/runtime/revive-all.js +5 -2
  421. package/dist/core/runtime/revive.d.ts +15 -8
  422. package/dist/core/runtime/revive.js +52 -16
  423. package/dist/core/runtime/spawn.d.ts +1 -1
  424. package/dist/core/runtime/spawn.js +7 -7
  425. package/dist/core/runtime/stop-guard.js +5 -5
  426. package/dist/core/runtime/surface-bg.js +1 -1
  427. package/dist/core/runtime/tmux.d.ts +1 -1
  428. package/dist/core/runtime/tmux.js +4 -4
  429. package/dist/core/scope.d.ts +9 -0
  430. package/dist/core/scope.js +37 -15
  431. package/dist/core/substrate/on-read.js +23 -21
  432. package/dist/core/substrate/render.js +3 -3
  433. package/dist/core/view/loader.js +2 -2
  434. package/dist/core/wake.d.ts +2 -2
  435. package/dist/core/wake.js +7 -7
  436. package/dist/daemon/__tests__/predicate-eval.test.js +109 -0
  437. package/dist/daemon/crtrd.js +222 -103
  438. package/dist/daemon/predicate-eval.d.ts +52 -0
  439. package/dist/daemon/predicate-eval.js +248 -0
  440. package/dist/hearth/control-plane/__tests__/node-message.test.js +60 -0
  441. package/dist/hearth/control-plane/__tests__/rate-limit-recurrence.test.js +49 -0
  442. package/dist/hearth/control-plane/__tests__/relay-security.test.js +314 -0
  443. package/dist/hearth/control-plane/__tests__/scheduler-scan-loop.test.js +133 -0
  444. package/dist/hearth/control-plane/__tests__/trigger-delivery.test.js +170 -0
  445. package/dist/hearth/control-plane/__tests__/webhook-ingress.test.js +167 -0
  446. package/dist/hearth/control-plane/config.d.ts +21 -0
  447. package/dist/hearth/control-plane/config.js +77 -0
  448. package/dist/hearth/control-plane/db.d.ts +30 -0
  449. package/dist/hearth/control-plane/db.js +534 -0
  450. package/dist/hearth/control-plane/ingress/rate-limit.d.ts +24 -0
  451. package/dist/hearth/control-plane/ingress/rate-limit.js +100 -0
  452. package/dist/hearth/control-plane/ingress/route-store.d.ts +31 -0
  453. package/dist/hearth/control-plane/ingress/route-store.js +61 -0
  454. package/dist/hearth/control-plane/ingress/webhook-delivery-store.d.ts +41 -0
  455. package/dist/hearth/control-plane/ingress/webhook-delivery-store.js +69 -0
  456. package/dist/hearth/control-plane/ingress/webhook-route.d.ts +55 -0
  457. package/dist/hearth/control-plane/ingress/webhook-route.js +285 -0
  458. package/dist/hearth/control-plane/main.js +88 -0
  459. package/dist/hearth/control-plane/node-message.d.ts +31 -0
  460. package/dist/hearth/control-plane/node-message.js +98 -0
  461. package/dist/hearth/control-plane/register.d.ts +15 -0
  462. package/dist/hearth/control-plane/register.js +34 -0
  463. package/dist/hearth/control-plane/registry.d.ts +17 -0
  464. package/dist/hearth/control-plane/registry.js +161 -0
  465. package/dist/hearth/control-plane/relay.d.ts +40 -0
  466. package/dist/hearth/control-plane/relay.js +642 -0
  467. package/dist/hearth/control-plane/scheduler/fire-store.d.ts +36 -0
  468. package/dist/hearth/control-plane/scheduler/fire-store.js +73 -0
  469. package/dist/hearth/control-plane/scheduler/recurrence.d.ts +7 -0
  470. package/dist/hearth/control-plane/scheduler/recurrence.js +58 -0
  471. package/dist/hearth/control-plane/scheduler/scan-loop.d.ts +38 -0
  472. package/dist/hearth/control-plane/scheduler/scan-loop.js +138 -0
  473. package/dist/hearth/control-plane/scheduler/schedule-store.d.ts +32 -0
  474. package/dist/hearth/control-plane/scheduler/schedule-store.js +66 -0
  475. package/dist/hearth/control-plane/secrets.d.ts +17 -0
  476. package/dist/hearth/control-plane/secrets.js +113 -0
  477. package/dist/hearth/control-plane/server.d.ts +27 -0
  478. package/dist/hearth/control-plane/server.js +346 -0
  479. package/dist/hearth/control-plane/session.d.ts +68 -0
  480. package/dist/hearth/control-plane/session.js +273 -0
  481. package/dist/hearth/control-plane/triggers/acl.d.ts +14 -0
  482. package/dist/hearth/control-plane/triggers/acl.js +52 -0
  483. package/dist/hearth/control-plane/triggers/audit-store.d.ts +38 -0
  484. package/dist/hearth/control-plane/triggers/audit-store.js +79 -0
  485. package/dist/hearth/control-plane/triggers/deliver.d.ts +43 -0
  486. package/dist/hearth/control-plane/triggers/deliver.js +76 -0
  487. package/dist/hearth/control-plane/triggers/envelope.d.ts +29 -0
  488. package/dist/hearth/control-plane/triggers/envelope.js +38 -0
  489. package/dist/hearth/control-plane/types.d.ts +66 -0
  490. package/dist/hearth/control-plane/wake.d.ts +80 -0
  491. package/dist/hearth/control-plane/wake.js +312 -0
  492. package/dist/hearth/wake-proxy/__tests__/guest-source.test.js +2 -2
  493. package/dist/hearth/wake-proxy/home.d.ts +2 -8
  494. package/dist/hearth/wake-proxy/home.js +48 -12
  495. package/dist/pi-extensions/canvas-doc-substrate.js +28 -2
  496. package/dist/pi-extensions/canvas-nav.js +1 -1
  497. package/dist/prompts/view.js +7 -8
  498. package/dist/types.js +4 -4
  499. package/dist/web-client/assets/{index-D36PNBj4.js → index-BRKxe-hy.js} +2 -2
  500. package/dist/web-client/index.html +1 -1
  501. package/package.json +12 -8
  502. package/scripts/postinstall.mjs +8 -0
  503. package/dist/clients/attach/__tests__/action-parity.test.js +0 -48
  504. package/dist/clients/attach/__tests__/autocomplete-and-bash-mode.test.js +0 -96
  505. package/dist/clients/attach/__tests__/bash-bang-routing.test.js +0 -58
  506. package/dist/clients/attach/__tests__/bundle-pi-tui-dedup.test.js +0 -56
  507. package/dist/clients/attach/__tests__/clipboard-image.test.js +0 -76
  508. package/dist/clients/attach/__tests__/editor-newline.test.js +0 -82
  509. package/dist/clients/attach/__tests__/git-info.test.js +0 -31
  510. package/dist/clients/attach/__tests__/onrequest-wired.test.js +0 -20
  511. package/dist/clients/attach/__tests__/reconnect-giveup.test.js +0 -30
  512. package/dist/clients/attach/__tests__/slash-quit-copy.test.js +0 -42
  513. package/dist/clients/attach/__tests__/titled-editor.test.js +0 -60
  514. package/dist/commands/memory/__tests__/lint-schema.test.js +0 -52
  515. package/dist/core/__tests__/argv-parser.test.js +0 -217
  516. package/dist/core/__tests__/artifact-paths.test.js +0 -44
  517. package/dist/core/__tests__/draw-style.test.js +0 -258
  518. package/dist/core/__tests__/editor-label.test.js +0 -26
  519. package/dist/core/__tests__/fault-marker.test.js +0 -112
  520. package/dist/core/__tests__/fault-retry-rewind.test.d.ts +0 -1
  521. package/dist/core/__tests__/fault-retry-rewind.test.js +0 -123
  522. package/dist/core/__tests__/frame-decoder-perf.test.d.ts +0 -1
  523. package/dist/core/__tests__/frame-decoder-perf.test.js +0 -224
  524. package/dist/core/__tests__/keystone.test.d.ts +0 -1
  525. package/dist/core/__tests__/keystone.test.js +0 -185
  526. package/dist/core/__tests__/listing-completeness.test.d.ts +0 -1
  527. package/dist/core/__tests__/listing-completeness.test.js +0 -31
  528. package/dist/core/__tests__/memory-resolver.test.d.ts +0 -1
  529. package/dist/core/__tests__/memory-resolver.test.js +0 -119
  530. package/dist/core/__tests__/memory.test.d.ts +0 -1
  531. package/dist/core/__tests__/memory.test.js +0 -60
  532. package/dist/core/__tests__/model-ladders.test.d.ts +0 -1
  533. package/dist/core/__tests__/model-ladders.test.js +0 -160
  534. package/dist/core/__tests__/node-env.test.d.ts +0 -1
  535. package/dist/core/__tests__/node-env.test.js +0 -26
  536. package/dist/core/__tests__/pane-column.test.d.ts +0 -1
  537. package/dist/core/__tests__/pane-column.test.js +0 -153
  538. package/dist/core/__tests__/persona-compose.test.d.ts +0 -1
  539. package/dist/core/__tests__/persona-compose.test.js +0 -53
  540. package/dist/core/__tests__/persona-subkind.test.d.ts +0 -1
  541. package/dist/core/__tests__/persona-subkind.test.js +0 -65
  542. package/dist/core/__tests__/persona.test.d.ts +0 -1
  543. package/dist/core/__tests__/persona.test.js +0 -107
  544. package/dist/core/__tests__/unknown-path.test.d.ts +0 -1
  545. package/dist/core/__tests__/unknown-path.test.js +0 -58
  546. package/dist/core/canvas/wakeups.d.ts +0 -76
  547. package/dist/core/canvas/wakeups.js +0 -185
  548. package/dist/hearth/wake-proxy/__tests__/source-bridge-gate.test.d.ts +0 -1
  549. package/dist/hearth/wake-proxy/__tests__/source-bridge-gate.test.js +0 -455
  550. /package/dist/clients/attach/__tests__/{action-parity.test.d.ts → crtr-output-coverage.test.d.ts} +0 -0
  551. /package/dist/clients/attach/__tests__/{autocomplete-and-bash-mode.test.d.ts → crtr-output-render.test.d.ts} +0 -0
  552. /package/dist/{clients/attach/__tests__/bash-bang-routing.test.d.ts → core/__tests__/boot.test.d.ts} +0 -0
  553. /package/dist/{clients/attach/__tests__/bundle-pi-tui-dedup.test.d.ts → core/__tests__/hearth-bootstrap.test.d.ts} +0 -0
  554. /package/dist/{clients/attach/__tests__/clipboard-image.test.d.ts → core/__tests__/preview-registry-sync.test.d.ts} +0 -0
  555. /package/dist/{clients/attach/__tests__/editor-newline.test.d.ts → core/__tests__/prune-to-limit.test.d.ts} +0 -0
  556. /package/dist/{clients/attach/__tests__/git-info.test.d.ts → core/__tests__/trigger-migration.test.d.ts} +0 -0
  557. /package/dist/{clients/attach/__tests__/onrequest-wired.test.d.ts → core/hearth/__tests__/model-auth-guest.test.d.ts} +0 -0
  558. /package/dist/{clients/attach/__tests__/reconnect-giveup.test.d.ts → daemon/__tests__/predicate-eval.test.d.ts} +0 -0
  559. /package/dist/{clients/attach/__tests__/slash-quit-copy.test.d.ts → hearth/control-plane/__tests__/node-message.test.d.ts} +0 -0
  560. /package/dist/{clients/attach/__tests__/titled-editor.test.d.ts → hearth/control-plane/__tests__/rate-limit-recurrence.test.d.ts} +0 -0
  561. /package/dist/{commands/memory/__tests__/lint-schema.test.d.ts → hearth/control-plane/__tests__/relay-security.test.d.ts} +0 -0
  562. /package/dist/{core/__tests__/argv-parser.test.d.ts → hearth/control-plane/__tests__/scheduler-scan-loop.test.d.ts} +0 -0
  563. /package/dist/{core/__tests__/artifact-paths.test.d.ts → hearth/control-plane/__tests__/trigger-delivery.test.d.ts} +0 -0
  564. /package/dist/{core/__tests__/draw-style.test.d.ts → hearth/control-plane/__tests__/webhook-ingress.test.d.ts} +0 -0
  565. /package/dist/{core/__tests__/editor-label.test.d.ts → hearth/control-plane/main.d.ts} +0 -0
  566. /package/dist/{core/__tests__/fault-marker.test.d.ts → hearth/control-plane/types.js} +0 -0
@@ -2,8 +2,11 @@
2
2
  //
3
3
  // A node is the unit of the runtime: an agent with its own identity, context
4
4
  // dir, and pi vehicle, pinned to a cwd. This subtree spawns terminal workers
5
- // onto the canvas (`new`), inspects the graph (`inspect list|show`), and walks
6
- // the spine (`focus`/`msg`). The push/feed half lives under `crtr push`.
5
+ // onto the canvas (`new`), inspects nodes (`inspect`), messages any node and
6
+ // wires the subscribes_to spine (`msg`/`subscribe`/`unsubscribe`), changes
7
+ // lifecycle (`lifecycle`), reconfigures node settings (`config`), and
8
+ // walks/focuses the graph (`focus`/`cycle`). The push/feed half lives under
9
+ // `crtr push`, and pending trigger inspection lives under `triggers`.
7
10
  import { defineLeaf, defineBranch } from '../core/command.js';
8
11
  import { InputError } from '../core/io.js';
9
12
  import { spawnChild } from '../core/runtime/spawn.js';
@@ -11,21 +14,21 @@ import { promote, requestYield } from '../core/runtime/promote.js';
11
14
  import { writeYieldMessage, readGoal } from '../core/runtime/kickoff.js';
12
15
  import { reviveNode } from '../core/runtime/revive.js';
13
16
  import { newNodeId } from '../core/runtime/nodes.js';
14
- import { readRoadmap } from '../core/runtime/roadmap.js';
17
+ import { readRoadmap, hasRoadmap, seedRoadmap } from '../core/runtime/roadmap.js';
15
18
  import { parseWhen, parseCadence, cadenceDisplay } from '../core/wake.js';
16
19
  import { nodeSnapshotLeaf } from './node-snapshot.js';
17
20
  import { nodeContextLeaf } from './node-context.js';
18
21
  import { recycleNode } from '../core/runtime/recycle.js';
19
22
  import { readFaultAsync } from '../core/runtime/fault.js';
20
23
  import { activeFaultForDisplay } from '../core/canvas/render.js';
21
- import { detachToBackground, focus as placementFocus, windowAlive, windowOfPane, currentTmux, getPaneOption } from '../core/runtime/placement.js';
24
+ import { detachToBackground, focus as placementFocus, windowAlive, windowOfPane, currentTmux, getPaneOption, renameWindow } from '../core/runtime/placement.js';
22
25
  import { buildLaunchSpec, normalizeModel } from '../core/runtime/launch.js';
23
26
  import { closeNode } from '../core/runtime/close.js';
24
27
  import { isBrokerLive, setModelLive, persistDormantModel } from '../core/runtime/model-swap.js';
25
28
  import { appendInbox } from '../core/feed/inbox.js';
26
29
  import { availableKinds, kindWhenToUse } from '../core/personas/index.js';
27
30
  import { stateBlock } from '../core/help.js';
28
- import { getNode, updateNode, listNodes, subscribe, unsubscribe, subscriptionsOf, subscribersOf, readContextTokens, view, armWake, listWakes, cancelWake, WakeArmError, } from '../core/canvas/index.js';
31
+ import { getNode, updateNode, listNodes, subscribe, unsubscribe, subscriptionsOf, subscribersOf, readContextTokens, view, armTrigger, listTriggers, cancelTrigger, TriggerArmError, fullName, } from '../core/canvas/index.js';
29
32
  // Past this much context, an ORCHESTRATOR that spawns a managed child is better
30
33
  // off yielding than holding its fat window open for the child's result: the
31
34
  // child's finish revives it fresh against its roadmap, so a clean window absorbs
@@ -78,37 +81,48 @@ function kindsStateBlock() {
78
81
  // ---------------------------------------------------------------------------
79
82
  const nodeNew = defineLeaf({
80
83
  name: 'new',
81
- description: 'spawn a node — a managed child (default), or an independent root with --root',
82
- whenToUse: 'you have a self-contained unit of work — reach for this instead of doing it inline, so the reading and the tokens land in a fresh window and only the conclusion comes back: mapping an unfamiliar part of the codebase, writing a spec, designing an approach, breaking a job into a plan, implementing a change, or running a review. Fan independent units out as concurrent children. Most spawns are managed children whose finish wakes you; reach here too when a unit is itself too big for one window (boot it directly as its own sub-orchestrator rather than hope a base worker promotes itself) or when you are handing off an INDEPENDENT node you will neither manage nor be woken by, e.g. one a human will sit and drive',
84
+ description: 'spawn a node now — a managed child (default), an independent root with --root, or arm a trigger that births it later',
85
+ whenToUse: 'you have a self-contained unit of work — reach for this instead of doing it inline, so the reading and the tokens land in a fresh window and only the conclusion comes back: mapping an unfamiliar part of the codebase, writing a spec, designing an approach, breaking a job into a plan, implementing a change, or running a review. Fan independent units out as concurrent children. Most spawns are managed children whose finish wakes you; reach here too when a unit is itself too big for one window (boot it directly as its own sub-orchestrator rather than hope a base worker promotes itself) or when you are handing off an INDEPENDENT node you will neither manage nor be woken by, e.g. one a human will sit and drive. Add `--at`/`--every` to defer or recur the birth instead of spawning now, or `--when` to birth it once an external predicate exits 0',
83
86
  tier: 'important',
84
87
  help: {
85
88
  name: 'node new',
86
- summary: 'spawn a terminal worker onto the canvas as a detached broker — returns its node id',
89
+ summary: 'spawn a terminal worker onto the canvas as a detached broker — returns its node id. With --at/--every/--when, arms a node_birth trigger instead: nothing is spawned now, and the full recipe is stored/re-derived at fire time',
87
90
  params: [
88
91
  { kind: 'stdin', name: 'prompt', required: true, constraint: 'First user message for the spawned node. Deliver it on stdin from a quoted heredoc (`<<\'EOF\'`) or a file (`< prompt.md`).' },
89
92
  { kind: 'flag', name: 'kind', type: 'string', required: false, default: 'general', constraint: 'Persona kind — match the work to the kind. The <kinds> list below names every installable kind and when to use each. For an exact composed persona list, use `crtr sys prompt-review --list --kind <kind>`.' },
90
93
  { kind: 'flag', name: 'mode', type: 'enum', choices: ['base', 'orchestrator'], required: false, default: 'base', constraint: 'Persona mode. base for a worker that finishes in one window; orchestrator to create the child directly as a sub-orchestrator (it boots with the orchestrator persona + a seeded roadmap and fans its scope out) — use it when the unit is too large for one window, e.g. a big review, instead of spawning a base worker and counting on it to promote itself.' },
91
- { kind: 'flag', name: 'cwd', type: 'path', required: false, constraint: 'Dir the node is pinned to. Defaults to the caller cwd.' },
94
+ { kind: 'flag', name: 'cwd', type: 'path', required: false, constraint: 'Dir the node is pinned to. Defaults to the caller cwd, resolved now — for a triggered spawn it must still exist at fire time or the spawn fails loud.' },
92
95
  { kind: 'flag', name: 'name', type: 'string', required: false, constraint: 'Display name (tmux window + resume picker). Defaults to the kind.' },
93
96
  { kind: 'flag', name: 'parent', type: 'string', required: false, constraint: 'Parent node id. Defaults to the calling node (CRTR_NODE_ID).' },
94
97
  { kind: 'flag', name: 'root', type: 'bool', required: false, constraint: 'Spawn an INDEPENDENT root instead of a managed child: no parent (top-level on the canvas), NO subscription back to you (you are NOT woken by it), resident lifecycle. It records spawned_by=you for provenance and is brought forefront so it can be driven directly. Use for a node you hand off and do not manage (e.g. a sub-orchestrator a human will discuss with).' },
95
98
  { kind: 'flag', name: 'fork-from', type: 'string', required: false, constraint: 'FORK the new node from an existing pi conversation instead of starting it fresh: pass a node id (forks from that node\'s session), an absolute session `.jsonl` path, or a partial pi session uuid. pi copies that whole history into a NEW session for the child (the source is untouched), then the prompt is delivered as the next message — i.e. the child wakes up as a continuation of that conversation. Use to branch exploratory work off a node that already built up the context you need, instead of re-deriving it. One-shot at birth: the fork resumes its own session thereafter.' },
96
99
  { kind: 'flag', name: 'model', type: 'enum', choices: ['ultra', 'strong', 'medium', 'light'], required: false, constraint: 'Override the model the node runs on, by capability TIER: ultra (frontier), strong (opus), medium (sonnet), light (haiku). Omit to use the kind\'s persona default (advisor=ultra, explore=light, most other builtins=strong). The override is durable — it survives revives and polymorphs (promote/demote).' },
100
+ { kind: 'flag', name: 'at', type: 'string', required: false, constraint: 'DEFER the birth to a one-shot clock trigger at this future time instead of spawning now — a duration ("5m","1h30m"), a zoned ISO ("2026-06-07T09:00:00Z"), or a bare ISO ("2026-06-07T09:00", host-local or in --tz). Required unless --every is given (the cadence then sets the first fire). Mutually exclusive with --when.' },
101
+ { kind: 'flag', name: 'every', type: 'string', required: false, constraint: 'SPAWN-CRON: re-birth a fresh node on this cadence — a duration ("6h","30m") or a 5-field cron / @alias ("0 9 * * *","@daily"). Combine with --at to anchor the first fire, then recur normally. Min cadence 60s. With --when, this instead sets the PREDICATE EVALUATION cadence (required), not a repeating birth.' },
102
+ { kind: 'flag', name: 'when', type: 'string', required: false, constraint: 'Arm a daemon-evaluated bash predicate instead of a clock trigger: exit 0 births the node once, non-zero leaves it armed for the next evaluation. Requires --every (eval cadence) and --timeout (wall-clock bound). Mutually exclusive with --at.' },
103
+ { kind: 'flag', name: 'timeout', type: 'string', required: false, constraint: 'Wall-clock bound for a --when predicate — a duration or absolute time after which the trigger is consumed and the owner gets one urgent notice with no node born. Required with --when; rejected otherwise.' },
104
+ { kind: 'flag', name: 'tz', type: 'string', required: false, constraint: 'IANA zone for a calendar --every or a bare-ISO --at (default: host-local). Valid only alongside --at/--every; rejected with --when, and rejected when spawning now (no trigger flags) since neither has anything to parse it against.' },
97
105
  ],
98
106
  output: [
99
- { name: 'node_id', type: 'string', required: true, constraint: 'The new node id.' },
100
- { name: 'name', type: 'string', required: true, constraint: 'Display name.' },
101
- { name: 'window', type: 'string', required: false, constraint: 'tmux window id of the viewer opened for an in-tmux --root (a `crtr attach` view onto the node\'s broker); absent for a managed child, which opens no viewer on spawn.' },
102
- { name: 'session', type: 'string', required: false, constraint: 'The tmux session a --root\'s viewer opened into (your current session); absent for a managed child and for a --root spawned outside tmux.' },
103
- { name: 'status', type: 'string', required: true, constraint: 'Always "active" on spawn.' },
104
- { name: 'follow_up', type: 'string', required: true, constraint: 'Decision road sign for the caller: the child runs independently and its finish wakes you on its own, so never wait or poll on it — either pick up other work now or end your turn. If you are an orchestrator already deep in context (>100k), it instead steers you to `crtr node yield` now so your fresh revive absorbs the child\'s result. Read it, then act.' },
107
+ { name: 'node_id', type: 'string', required: false, constraint: 'The new node id. Present only when spawning now (no trigger flags).' },
108
+ { name: 'name', type: 'string', required: false, constraint: 'Display name. Present only when spawning now (no trigger flags).' },
109
+ { name: 'window', type: 'string', required: false, constraint: 'tmux window id of the viewer opened for an in-tmux --root (a surface attach view onto the node\'s broker); absent for a managed child (opens no viewer on spawn) and absent whenever armed.' },
110
+ { name: 'session', type: 'string', required: false, constraint: 'The tmux session a --root\'s viewer opened into (your current session); absent for a managed child, for a --root spawned outside tmux, and absent whenever armed.' },
111
+ { name: 'status', type: 'string', required: false, constraint: 'Always "active". Present only when spawning now (no trigger flags).' },
112
+ { name: 'armed', type: 'boolean', required: true, constraint: 'True when a node_birth trigger was armed for later (--at/--every/--when) instead of spawning now.' },
113
+ { name: 'id', type: 'string', required: false, constraint: 'The trigger id (inspect/cancel it via `node triggers`), present only when armed=true.' },
114
+ { name: 'trigger_type', type: 'string', required: false, constraint: '"at", "every", or "when", present only when armed=true.' },
115
+ { name: 'fires_at', type: 'string', required: false, constraint: 'Absolute UTC birth time, present only for an armed clock trigger.' },
116
+ { name: 'next_eval_at', type: 'string', required: false, constraint: 'The next predicate-evaluation instant (always "now" at arm time), present only for an armed --when trigger.' },
117
+ { name: 'timeout_at', type: 'string', required: false, constraint: 'Absolute UTC wall-clock bound, present only for an armed --when trigger.' },
118
+ { name: 'recur', type: 'string', required: false, constraint: 'Cadence display ("every 6h", cron, or eval cadence for --when), present only when --every was given.' },
119
+ { name: 'follow_up', type: 'string', required: true, constraint: 'Decision road sign for the caller. Spawned now: the child runs independently and its finish wakes you on its own, so never wait or poll on it — either pick up other work now or end your turn (an orchestrator already deep in context is instead steered to `crtr node yield`). Armed: no node exists yet — inspect/cancel via `crtr node triggers list`/`cancel <id>`, then pick up other work or end your turn.' },
105
120
  ],
106
121
  dynamicState: () => kindsStateBlock(),
107
122
  outputKind: 'object',
108
123
  effects: [
109
- 'Creates a node under ~/.crouter/canvas/nodes/<id>/ and indexes it in canvas.db.',
110
- 'Default (managed child): parent auto-subscribes (active) and is woken on the child\'s pushes. With --root: no subscription records a spawned_by edge for provenance only.',
111
- 'Launches the node\'s engine as a detached broker (the only host). A managed child opens NO viewer — it runs headless and you open one on demand via `crtr node focus` / `crtr attach`. With --root inside tmux, opens one viewer window in your CURRENT session and switches the client to it so the root can be driven directly; outside tmux a --root opens no viewer.',
124
+ 'Spawning now: creates a node under ~/.crouter/canvas/nodes/<id>/ and indexes it in canvas.db. Default (managed child): parent auto-subscribes (active) and is woken on the child\'s pushes. With --root: no subscription — records a spawned_by edge for provenance only. Launches the node\'s engine as a detached broker (the only host); a managed child opens NO viewer, a --root opens one in your current tmux session.',
125
+ 'Armed (--at/--every/--when): inserts one detached `new`/`node_birth` triggers row (node_id NULL, owner=you, parent/cwd resolved now); NO node and NO window exist until fire time. The daemon spawns from the stored recipe at fire time, re-deriving the launch spec live.',
112
126
  ],
113
127
  },
114
128
  run: async (input) => {
@@ -124,19 +138,154 @@ const nodeNew = defineLeaf({
124
138
  const root = input['root'] === true;
125
139
  const forkFrom = input['forkFrom'];
126
140
  const model = input['model'];
127
- const res = await spawnChild({ kind, mode, cwd, name, prompt, parent, root, forkFrom, model });
141
+ const at = input['at']?.trim();
142
+ const every = input['every']?.trim();
143
+ const whenRaw = input['when'];
144
+ const hasWhen = whenRaw !== undefined && whenRaw.trim() !== '';
145
+ const timeout = input['timeout']?.trim();
146
+ const hasTimeout = timeout !== undefined && timeout !== '';
147
+ const tz = input['tz'];
148
+ const hasTz = tz !== undefined && tz.trim() !== '';
149
+ const hasAt = at !== undefined && at !== '';
150
+ const hasEvery = every !== undefined && every !== '';
151
+ const triggered = hasAt || hasEvery || hasWhen;
152
+ if (hasTz && hasWhen) {
153
+ throw new InputError({ error: 'tz_not_valid_with_when', message: '--tz has no effect with --when (predicate evaluation parses no calendar timezone)', field: 'tz', next: 'Drop --tz.' });
154
+ }
155
+ if (hasTz && !hasAt && !hasEvery) {
156
+ throw new InputError({ error: 'tz_needs_clock', message: '--tz has no effect without --at or --every to parse it against', field: 'tz', next: 'Drop --tz, or pass --at/--every.' });
157
+ }
158
+ if (!triggered) {
159
+ const res = await spawnChild({ kind, mode, cwd, name, prompt, parent, root, forkFrom, model });
160
+ return {
161
+ node_id: res.node.node_id,
162
+ name: res.node.name,
163
+ window: res.window ?? undefined,
164
+ session: res.session ?? undefined,
165
+ status: res.node.status,
166
+ armed: false,
167
+ follow_up: root
168
+ ? "Independent root spawned — it is NOT under you. You are not subscribed, so its finish will NOT wake you and it does not hold you alive; it carries spawned_by=you for lineage only. Inside tmux it opened a viewer in your current session (the client switched to it) so it can be driven directly. Hand it off and move on — you will not be notified of its progress."
169
+ : childFollowUp(parent ?? process.env['CRTR_NODE_ID']),
170
+ };
171
+ }
172
+ if (hasWhen && hasAt) {
173
+ throw new InputError({ error: 'bad_when_combo', message: '--when cannot combine with --at', next: 'A trigger is either clock-driven (--at/--every) or predicate-driven (--when).' });
174
+ }
175
+ if (hasWhen && !hasEvery) {
176
+ throw new InputError({ error: 'when_needs_every', message: '--when requires --every (the evaluation cadence)', next: 'Pass --every <cadence> alongside --when.' });
177
+ }
178
+ if (hasWhen && !hasTimeout) {
179
+ throw new InputError({ error: 'when_needs_timeout', message: '--when requires --timeout (the wall-clock bound)', next: 'Pass --timeout <duration> alongside --when.' });
180
+ }
181
+ if (hasTimeout && !hasWhen) {
182
+ throw new InputError({ error: 'timeout_needs_when', message: '--timeout is only valid with --when', next: 'Drop --timeout, or pass --when to make this a predicate trigger.' });
183
+ }
184
+ const ownerId = armerId();
185
+ const recipeParent = parent ?? ownerId;
186
+ const recipe = {
187
+ kind,
188
+ mode,
189
+ cwd,
190
+ prompt,
191
+ parent: recipeParent,
192
+ ...(name !== undefined ? { name } : {}),
193
+ ...(root ? { root: true } : {}),
194
+ ...(forkFrom !== undefined ? { forkFrom } : {}),
195
+ ...(model !== undefined ? { model } : {}),
196
+ };
197
+ const id = `tr-${newNodeId()}`;
198
+ if (hasWhen) {
199
+ const now = new Date();
200
+ const cad = parseCadence(every, parseOpts(now, undefined));
201
+ if ('error' in cad)
202
+ throwTimeError(cad.error);
203
+ const t = parseWhen(timeout, { now });
204
+ if ('error' in t)
205
+ throwTimeError(t.error);
206
+ assertUnderCap(ownerId);
207
+ armTriggerOrThrow({
208
+ trigger_id: id,
209
+ node_id: null,
210
+ owner_id: ownerId,
211
+ action: 'new',
212
+ action_mode: 'node_birth',
213
+ trigger_type: 'when',
214
+ when_script: whenRaw.trim(),
215
+ eval_every: cad.recur,
216
+ next_eval_at: now.toISOString(),
217
+ timeout_at: t.fireAt,
218
+ payload: recipe,
219
+ });
220
+ return {
221
+ armed: true,
222
+ id,
223
+ kind,
224
+ trigger_type: 'when',
225
+ recur: cadenceDisplay(cad.recur),
226
+ next_eval_at: now.toISOString(),
227
+ timeout_at: t.fireAt,
228
+ follow_up: `Predicate spawn armed — evaluated ${cadenceDisplay(cad.recur)} (first check now), bounded by a wall-clock timeout at ${t.fireAt}. No node exists yet; inspect/cancel via \`crtr node triggers list\` / \`crtr node triggers cancel ${id}\`. Pick up other work or end your turn.`,
229
+ };
230
+ }
231
+ const now = new Date();
232
+ let recur;
233
+ let fireAt;
234
+ if (hasEvery) {
235
+ const cad = parseCadence(every, parseOpts(now, tz));
236
+ if ('error' in cad)
237
+ throwTimeError(cad.error);
238
+ recur = cad.recur;
239
+ fireAt = cad.firstFireAt;
240
+ // BOTH --at and --every (fixed-interval or cron): --at anchors the first
241
+ // fire; recurrence thereafter advances from wall-clock `now` at settle
242
+ // time (nextSlotAfter), independent of the anchor, so this holds for
243
+ // both cadence shapes.
244
+ if (hasAt) {
245
+ const w = parseWhen(at, parseOpts(now, tz));
246
+ if ('error' in w)
247
+ throwTimeError(w.error);
248
+ fireAt = w.fireAt;
249
+ }
250
+ }
251
+ else {
252
+ const w = parseWhen(at, parseOpts(now, tz));
253
+ if ('error' in w)
254
+ throwTimeError(w.error);
255
+ fireAt = w.fireAt;
256
+ }
257
+ assertUnderCap(ownerId);
258
+ armTriggerOrThrow({
259
+ trigger_id: id,
260
+ node_id: null,
261
+ owner_id: ownerId,
262
+ action: 'new',
263
+ action_mode: 'node_birth',
264
+ trigger_type: recur !== undefined ? 'every' : 'at',
265
+ fire_at: fireAt,
266
+ ...(recur !== undefined ? { recur } : {}),
267
+ payload: recipe,
268
+ });
269
+ const eta = etaHint(fireAt, now);
270
+ const follow_up = recur !== undefined
271
+ ? `Spawn-cron armed (${cadenceDisplay(recur)}): a fresh ${kind} node is born each fire, first at ${fireAt}${eta}. No node exists yet; the runtime keeps spawning even across your crash/finalize — inspect/cancel via \`crtr node triggers list\` / \`crtr node triggers cancel ${id}\`.`
272
+ : `Deferred spawn armed: a fresh ${kind} node is born at ${fireAt}${eta}. No node exists yet — inspect/cancel via \`crtr node triggers list\` / \`crtr node triggers cancel ${id}\`. Pick up other work or end your turn.`;
128
273
  return {
129
- node_id: res.node.node_id,
130
- name: res.node.name,
131
- window: res.window ?? undefined,
132
- session: res.session ?? undefined,
133
- status: res.node.status,
134
- follow_up: root
135
- ? "Independent root spawned — it is NOT under you. You are not subscribed, so its finish will NOT wake you and it does not hold you alive; it carries spawned_by=you for lineage only. Inside tmux it opened a viewer in your current session (the client switched to it) so it can be driven directly. Hand it off and move on — you will not be notified of its progress."
136
- : childFollowUp(parent ?? process.env['CRTR_NODE_ID']),
274
+ armed: true,
275
+ id,
276
+ kind,
277
+ trigger_type: recur !== undefined ? 'every' : 'at',
278
+ fires_at: fireAt,
279
+ ...(recur !== undefined ? { recur: cadenceDisplay(recur) } : {}),
280
+ follow_up,
137
281
  };
138
282
  },
139
283
  render: (r) => {
284
+ if (r['armed'] === true) {
285
+ const recur = r['recur'] ? `, recurs ${r['recur']}` : '';
286
+ const when = r['fires_at'] ? `fires ${r['fires_at']}${recur}` : `next eval ${r['next_eval_at']}, timeout ${r['timeout_at']}`;
287
+ return `Armed deferred spawn ${r['id']} — a ${r['kind']} node, ${when}.\n\n${r['follow_up']}`;
288
+ }
140
289
  const win = r['window'] ? ` in window ${r['window']}` : '';
141
290
  const lines = [`Spawned "${r['name']}" (${r['node_id']}) — ${r['status']}${win}.`];
142
291
  if (r['session'])
@@ -146,28 +295,29 @@ const nodeNew = defineLeaf({
146
295
  },
147
296
  });
148
297
  // ---------------------------------------------------------------------------
149
- // node list — the active canvas (or a status slice)
298
+ // node inspect list — the active canvas (or a status slice)
150
299
  // ---------------------------------------------------------------------------
151
300
  const nodeList = defineLeaf({
152
301
  name: 'list',
153
302
  description: 'list nodes on the canvas',
154
- whenToUse: 'you want a flat roster of the nodes on the canvas, sliced by any combination of attribute filters (status, kind, mode, lifecycle, cwd, hanging) and graph scope (--under a node, for that node plus its subscription descendants). The filters AND together, so "every human node in my subtree" is `--kind human --under <me>` and "every parked orchestrator" is `--mode orchestrator --hanging`. Use `node inspect show` instead to drill into one node and its spine neighbors, `canvas dashboard` for the tree SHAPE, and `canvas attention` to find which nodes are blocked on a human',
303
+ whenToUse: 'you want a flat roster of the nodes on the canvas, sliced by any combination of attribute filters (status, kind, mode, lifecycle, cwd, hanging) and graph scope (--under a node, for that node plus its subscription descendants). The filters AND together, so any attribute filter can be paired with any other, and with --under, to narrow by both criteria and subtree at once. Use `node inspect show` instead to drill into one node and its spine neighbors, `canvas dashboard` for the tree SHAPE, and `canvas attention` to find which nodes are blocked on a human',
155
304
  help: {
156
305
  name: 'node inspect list',
157
306
  summary: 'list nodes on the canvas, filtered by any combination of attribute slices and a graph-scope (--under) that AND together',
158
307
  params: [
159
- { kind: 'flag', name: 'status', type: 'string', required: false, constraint: 'Filter: active | idle | done | dead | canceled. Comma-separated for several. NOTE: `active` means the engine process is live (never closed), NOT that the node is generating right now — an active node is usually dormant between turns. To tell working-vs-idle, check the pi session-file mtime or CPU, not status.' },
160
- { kind: 'flag', name: 'kind', type: 'string', required: false, constraint: 'Filter by persona kind (general, explore, developer, design, plan, review, spec, human, …). Comma-separated for several (OR within the flag).' },
161
- { kind: 'flag', name: 'mode', type: 'string', required: false, constraint: 'Filter by mode: base | orchestrator. Comma-separated for several.' },
162
- { kind: 'flag', name: 'lifecycle', type: 'string', required: false, constraint: 'Filter by lifecycle: terminal | resident. Comma-separated for several.' },
308
+ { kind: 'flag', name: 'status', type: 'string', required: false, constraint: 'Filter status (comma-separated for several; accepted values: active, idle, done, dead, canceled). NOTE: `active` means the engine process is live (never closed), NOT that the node is generating right now — an active node is usually dormant between turns. To tell working-vs-idle, check the pi session-file mtime or CPU, not status.' },
309
+ { kind: 'flag', name: 'kind', type: 'string', required: false, constraint: 'Filter by persona kind (comma-separated for several). The <kinds> list below names every installable kind.' },
310
+ { kind: 'flag', name: 'mode', type: 'string', required: false, constraint: 'Filter by mode (comma-separated for several; accepted values: base, orchestrator).' },
311
+ { kind: 'flag', name: 'lifecycle', type: 'string', required: false, constraint: 'Filter by lifecycle (comma-separated for several; accepted values: terminal, resident).' },
163
312
  { kind: 'flag', name: 'cwd', type: 'string', required: false, constraint: 'Filter to nodes pinned to this exact cwd (absolute path). Roster is canvas-wide (all cwds) by default.' },
164
- { kind: 'flag', name: 'under', type: 'string', required: false, constraint: 'Graph scope: restrict to this node and its subscription descendants (the sub-DAG reachable downward — its reports, theirs, …). This is "one initiative / my subtree". Combine with attribute filters, e.g. `--kind human --under <me>`.' },
313
+ { kind: 'flag', name: 'under', type: 'string', required: false, constraint: 'Graph scope: restrict to this node and its subscription descendants (the sub-DAG reachable downward — its reports, theirs, …). This is "one initiative / my subtree". ANDs with every attribute filter above, narrowing any of them to this subtree.' },
165
314
  { kind: 'flag', name: 'hanging', type: 'bool', required: false, constraint: 'Only nodes PARKED on an active fault (live broker + marker when provider-owned). Use to find stuck/rate-limited nodes awaiting auto-revive.' },
166
315
  ],
167
316
  output: [
168
317
  { name: 'nodes', type: 'object[]', required: true, constraint: 'Rows: {node_id, name, kind, mode, lifecycle, status, cwd, parent, created, hanging}. `hanging` is null normally, or {kind, message, since} when the node is PARKED on an active fault (rate-limit/overloaded/connection/other); the canvas-graph views render it with ⚠ + a countdown or state cue.' },
169
318
  ],
170
319
  outputKind: 'object',
320
+ dynamicState: () => kindsStateBlock(),
171
321
  effects: ['Read-only: queries canvas.db (+ an active-fault marker read).'],
172
322
  },
173
323
  run: async (input) => {
@@ -215,7 +365,7 @@ const nodeList = defineLeaf({
215
365
  },
216
366
  });
217
367
  // ---------------------------------------------------------------------------
218
- // node show — a node + its place in the spine
368
+ // node inspect show — a node + its place in the spine
219
369
  // ---------------------------------------------------------------------------
220
370
  const nodeShow = defineLeaf({
221
371
  name: 'show',
@@ -234,6 +384,11 @@ const nodeShow = defineLeaf({
234
384
  ],
235
385
  outputKind: 'object',
236
386
  effects: ['Read-only: reads the node meta + canvas.db edges.'],
387
+ // The node meta object inlines the full system prompt + every telemetry
388
+ // field — a machine-readable dump with no useful human-glance slice to
389
+ // synthesize (unlike dashboard/list's tabular rows), same rationale as
390
+ // canvas snapshot / node inspect snapshot.
391
+ preview: { suppressOutput: true },
237
392
  },
238
393
  run: async (input) => {
239
394
  const id = input['node'];
@@ -245,17 +400,17 @@ const nodeShow = defineLeaf({
245
400
  },
246
401
  });
247
402
  // ---------------------------------------------------------------------------
248
- // node inspect — read the graph (list + show)
403
+ // node inspect — read the graph (list, show, snapshot, context)
249
404
  // ---------------------------------------------------------------------------
250
405
  const nodeInspect = defineBranch({
251
406
  name: 'inspect',
252
- description: 'read the graph (list nodes / show one)',
253
- whenToUse: 'reading the graph: enumerate the nodes on the canvas (`list`) or drill into one and its spine neighbors (`show`). Use `canvas dashboard` instead to render the tree SHAPE, or `canvas attention` to find which nodes are blocked on a human',
407
+ description: 'read the graph (list, show, snapshot, context)',
408
+ whenToUse: 'reading the graph: enumerate nodes on the canvas (`list`), inspect one and its spine neighbors (`show`), or pull a dormant session snapshot with `node inspect snapshot` / browse on-disk context dirs with `node inspect context` while reading a node. Use `canvas dashboard` instead to render the tree SHAPE, or `canvas attention` to find which nodes are blocked on a human',
254
409
  help: {
255
410
  name: 'node inspect',
256
- summary: 'read the canvas graph — enumerate nodes or inspect one with its spine neighbors',
411
+ summary: 'read the canvas graph — enumerate nodes, inspect one, or pull snapshot/context views for a node (including dormant session snapshots and on-disk context dirs)',
257
412
  },
258
- children: [nodeList, nodeShow],
413
+ children: [nodeList, nodeShow, nodeSnapshotLeaf, nodeContextLeaf],
259
414
  });
260
415
  // ---------------------------------------------------------------------------
261
416
  // node focus — bring a node's viewer forefront (across roots if needed)
@@ -263,10 +418,10 @@ const nodeInspect = defineBranch({
263
418
  const nodeFocus = defineLeaf({
264
419
  name: 'focus',
265
420
  description: 'bring a node\'s viewer forefront',
266
- whenToUse: 'you want to bring a specific node into view — open or move its viewer beside your current pane — to watch or steer it directly, reviving its broker first if dormant. Use `node cycle` instead to walk neighbors one viewer at a time rather than jump to a named node, and `node msg` to steer a node without leaving where you are',
421
+ whenToUse: 'you want to bring a specific node into view — open or move its viewer beside your current pane — to watch or steer it directly, reviving its broker first if dormant. Use `node cycle` instead to walk neighbors one viewer at a time rather than jump to a named node, and `node msg --to` to steer a node without leaving where you are',
267
422
  help: {
268
423
  name: 'node focus',
269
- summary: 'bring a node\'s viewer forefront — open a `crtr attach` view onto the node\'s broker beside your current pane, or navigate to its existing viewer. The engine runs on the broker (the only host); the pane is just a window onto it.',
424
+ summary: 'bring a node\'s viewer forefront — open a surface attach view onto the node\'s broker beside your current pane, or navigate to its existing viewer. The engine runs on the broker (the only host); the pane is just a window onto it.',
270
425
  params: [
271
426
  { kind: 'positional', name: 'node', required: true, constraint: 'Node id to focus.' },
272
427
  { kind: 'flag', name: 'new-pane', type: 'bool', required: false, constraint: 'Always open a NEW viewer SIDE-BY-SIDE with your current pane instead of moving/reusing the node\'s existing viewer. Two agents on screen at once (F4).' },
@@ -279,7 +434,7 @@ const nodeFocus = defineLeaf({
279
434
  { name: 'in_place', type: 'boolean', required: true, constraint: 'True when an existing viewer was navigated to (no new pane); false when a fresh viewer pane was opened or moved beside you.' },
280
435
  ],
281
436
  outputKind: 'object',
282
- effects: ['Ensures the node\'s broker engine is alive (revives it if dormant) and opens/moves its one viewer pane (a `crtr attach` view) beside the caller, or navigates to it if a live viewer already sits in the caller\'s session.', 'With --new-pane: always splits a fresh viewer beside the caller (two agents on screen) instead of reusing the existing viewer.'],
437
+ effects: ['Ensures the node\'s broker engine is alive (revives it if dormant) and opens/moves its one viewer pane (a surface attach view) beside the caller, or navigates to it if a live viewer already sits in the caller\'s session.', 'With --new-pane: always splits a fresh viewer beside the caller (two agents on screen) instead of reusing the existing viewer.'],
283
438
  },
284
439
  run: async (input) => {
285
440
  const id = input['node'];
@@ -310,10 +465,10 @@ const nodeFocus = defineLeaf({
310
465
  },
311
466
  });
312
467
  // ---------------------------------------------------------------------------
313
- // node recycle — FINALIZE the agent in your pane + recycle it into a fresh root
468
+ // node lifecycle recycle — FINALIZE the agent in your pane + recycle it into a fresh root
314
469
  // (push its last report as a `final` → mark it done, then boot a fresh resident
315
470
  // `crtr` root in the SAME pane; see recycleNode in runtime/recycle.ts). NOT
316
- // bound to any Alt+C key/menu — d/D there route to `node demote` (±--detach),
471
+ // bound to any Alt+C key/menu — d/D there route to `node lifecycle demote` (±--detach),
317
472
  // the flip-to-terminal-IN-PLACE action that keeps the agent running.
318
473
  // ---------------------------------------------------------------------------
319
474
  /** First live node whose window id is `win` (each node owns one window). The
@@ -327,11 +482,11 @@ function nodeByWindow(win) {
327
482
  }
328
483
  /** The live node "in front of you" in a tmux pane, HOST-AGNOSTIC. Defaults to
329
484
  * $TMUX_PANE / the caller's current pane when `pane` is omitted — shared by
330
- * `node recycle` / `node demote` / `node lifecycle` / `node close` / `node
485
+ * `node lifecycle recycle` / `node lifecycle demote` / `node config --lifecycle` / `node lifecycle close` / `node
331
486
  * cycle`, and the `canvas chord` leaf.
332
487
  *
333
488
  * Two resolutions, tried in order:
334
- * 1. VIEWER pane (broker host): a `crtr attach` viewer self-tags its pane with
489
+ * 1. VIEWER pane (broker host): a `crtr surface attach` viewer self-tags its pane with
335
490
  * the node it views (`@crtr_node`). A broker engine runs DETACHED with
336
491
  * window=null, so the window→node lookup never finds it — the pane tag is
337
492
  * the only handle. Checked FIRST because the tag is pane-precise, whereas a
@@ -356,9 +511,9 @@ export function nodeInPane(pane) {
356
511
  const nodeRecycle = defineLeaf({
357
512
  name: 'recycle',
358
513
  description: 'finish the agent in your pane + recycle it into a fresh root',
359
- whenToUse: 'you are at an agent\'s pane and done with it: finish it cleanly and recycle the pane in one move — push its last message as a final report to everyone waiting on it, mark it done, then boot a fresh crtr root in the same pane to keep working. The human-driver way to end an agent and immediately start over in place. Use `node demote` instead to put it on a finishing track IN PLACE (flip terminal, keep it running) without ending it now, `node close` to tear a node and its subtree down WITHOUT finishing (no report, revivable), and `push final` when the agent should finish ITSELF from inside its own turn',
514
+ whenToUse: 'you are at an agent\'s pane and done with it: finish it cleanly and recycle the pane in one move — push its last message as a final report to everyone waiting on it, mark it done, then boot a fresh crtr root in the same pane to keep working. The human-driver way to end an agent and immediately start over in place. Use `node lifecycle demote` instead to put it on a finishing track IN PLACE (flip terminal, keep it running) without ending it now, `node lifecycle close` to tear a node and its subtree down WITHOUT finishing (no report, revivable), and `push final` when the agent should finish ITSELF from inside its own turn',
360
515
  help: {
361
- name: 'node recycle',
516
+ name: 'node lifecycle recycle',
362
517
  summary: 'finish the agent in your current pane and recycle the pane — push its last message as a final report to everyone waiting on it, mark it done, then boot a fresh crtr root in the same pane',
363
518
  params: [
364
519
  { kind: 'flag', name: 'node', type: 'string', required: false, constraint: 'Node to finish. Defaults to the node occupying --pane (or your current pane).' },
@@ -409,14 +564,14 @@ const nodeRecycle = defineLeaf({
409
564
  },
410
565
  });
411
566
  // ---------------------------------------------------------------------------
412
- // node close — close a node + cascade-cancel its exclusive subtree (Alt+C → x)
567
+ // node lifecycle close — close a node + cascade-cancel its exclusive subtree (Alt+C → x)
413
568
  // ---------------------------------------------------------------------------
414
569
  const nodeClose = defineLeaf({
415
570
  name: 'close',
416
571
  description: 'close a node + cascade-cancel its exclusive subtree (revivable)',
417
- whenToUse: 'you want to tear a node down WITHOUT finishing it, cascade-cancelling every descendant it exclusively owns: abandoning a line of work, killing a stuck or wrong-turn subtree, clearing a branch you no longer need. Windows die but nothing is deleted — each closed node keeps its pi session and can be revived later (`canvas revive`). Use `node recycle` instead to FINISH the agent in your pane with a final report, and `push final` when a worker should end its own work normally (Alt+C → x)',
572
+ whenToUse: 'you want to tear a node down WITHOUT finishing it, cascade-cancelling every descendant it exclusively owns: abandoning a line of work, killing a stuck or wrong-turn subtree, clearing a branch you no longer need. Windows die but nothing is deleted — each closed node keeps its pi session and can be revived later (`canvas revive`). Use `node lifecycle recycle` instead to FINISH the agent in your pane with a final report, and `push final` when a worker should end its own work normally (Alt+C → x)',
418
573
  help: {
419
- name: 'node close',
574
+ name: 'node lifecycle close',
420
575
  summary: 'close a node and cascade-cancel its subtree — kill its tmux window plus those of every descendant it EXCLUSIVELY owns (down the subscribes_to spine), mark them all canceled, and leave each a notice it reads on resume. A descendant still subscribed to by a manager outside the subtree is left running. Nothing is deleted: every closed node keeps its pi session and can be revived later (`crtr canvas revive`)',
421
576
  params: [
422
577
  { kind: 'flag', name: 'node', type: 'string', required: false, constraint: 'Node to close. Defaults to the node occupying --pane (or your current pane).' },
@@ -458,72 +613,158 @@ const nodeClose = defineLeaf({
458
613
  },
459
614
  });
460
615
  // ---------------------------------------------------------------------------
461
- // node modelheadless live model/provider swap for a node (no viewer attach)
616
+ // node configreconfigure a node's settings headlessly (model/lifecycle/kind/mode/name)
462
617
  // ---------------------------------------------------------------------------
463
- const nodeModel = defineLeaf({
464
- name: 'model',
465
- description: 'swap a node\'s model/provider headlessly (durable, survives revives)',
466
- whenToUse: 'you want to change which model (and thus provider) a node runs on WITHOUT opening its viewer flip a running node to a stronger/cheaper model or a different provider, or repin a dormant node\'s model before its next revive. The choice is durable: it survives revives and polymorphs. This is the headless twin of the in-viewer `/model` slash command; reach for `node promote --model` instead when you are also changing a node\'s role',
618
+ const nodeConfig = defineLeaf({
619
+ name: 'config',
620
+ description: 'reconfigure a node\'s settings headlessly — model, lifecycle (terminal/resident), kind, mode, name',
621
+ whenToUse: 'you want to reconfigure a node\'s settings headlessly after spawn swap its model/provider, flip it terminal↔resident (interactable), respecialize its kind, switch base↔orchestrator mode, or rename it without opening its viewer. The durable attribute setter; reuses the same recipes a revive reads. Use `node promote` for the opinionated self-promotion path, and `node lifecycle` for teardown actions (recycle/close/demote).',
467
622
  help: {
468
- name: 'node model',
469
- summary: 'swap a node\'s model headlessly — if its broker is live, drive the switch over view.sock (no viewer attached); if dormant, repin its durable recipe for the next revive. The choice persists across revives either way.',
623
+ name: 'node config',
624
+ summary: 'reconfigure a node\'s settings headlessly — model, lifecycle, kind, mode, name',
470
625
  params: [
471
- { kind: 'positional', name: 'spec', required: true, constraint: 'Model query: an exact `provider/id` (e.g. `openai-codex/gpt-5.5`), a capability tier (`ultra|strong|medium|light`), a bare family alias (`opus|sonnet|haiku`), or a free-text substring matched against the live registry. A free-text substring only resolves against a LIVE broker; a dormant node needs a concrete `provider/id` (or a tier/alias).' },
472
- { kind: 'flag', name: 'node', type: 'string', required: false, constraint: 'Node to repoint. Defaults to the node occupying --pane (or your current pane).' },
473
- { kind: 'flag', name: 'pane', type: 'string', required: false, constraint: 'tmux pane id whose node to repoint. Defaults to $TMUX_PANE / your current pane.' },
626
+ { kind: 'flag', name: 'node', type: 'string', required: false, constraint: 'Target node id. Defaults to the node in --pane, else CRTR_NODE_ID.' },
627
+ { kind: 'flag', name: 'pane', type: 'string', required: false, constraint: 'tmux pane id to resolve the node from. Defaults to $TMUX_PANE.' },
628
+ { kind: 'flag', name: 'model', type: 'string', required: false, constraint: 'Model spec: exact provider/id, capability tier (ultra|strong|medium|light), family alias (opus|sonnet|haiku), or a free-text substring when the broker is live. Dormant nodes require a provider/id, tier, or alias.' },
629
+ { kind: 'flag', name: 'lifecycle', type: 'enum', choices: ['terminal', 'resident'], required: false, constraint: 'Set lifecycle headlessly without closing the viewer. terminal owes a final and reaps when done; resident stays interactable and wakes on inbox/human.' },
630
+ { kind: 'flag', name: 'kind', type: 'string', required: false, constraint: 'Persona kind. The <kinds> list below names every installable kind and when to use each.' },
631
+ { kind: 'flag', name: 'mode', type: 'enum', choices: ['base', 'orchestrator'], required: false, constraint: 'Set persona mode headlessly. base is hands-on; orchestrator holds a roadmap and delegates. orchestrator seeds a roadmap scaffold if absent.' },
632
+ { kind: 'flag', name: 'name', type: 'string', required: false, constraint: 'Rename the node; if the node has a live window, also rename that viewer window.' },
474
633
  ],
475
634
  output: [
476
- { name: 'node_id', type: 'string', required: true, constraint: 'The node whose model was swapped.' },
477
- { name: 'model', type: 'string', required: true, constraint: 'The resolved model a concrete `provider/id` for a live broker (registry-resolved) or the normalized recipe value for a dormant node.' },
478
- { name: 'applied', type: 'string', required: true, constraint: '"live" (the running broker switched immediately and persisted it) or "next-revive" (the dormant node\'s recipe was repinned; takes effect when it next revives).' },
635
+ { name: 'node_id', type: 'string', required: true, constraint: 'The reconfigured node.' },
636
+ { name: 'model', type: 'string', required: false, constraint: 'The resolved model, present only when --model changed it.' },
637
+ { name: 'lifecycle', type: 'string', required: false, constraint: 'The new lifecycle, present only when --lifecycle changed it.' },
638
+ { name: 'kind', type: 'string', required: false, constraint: 'The new kind, present only when --kind changed it.' },
639
+ { name: 'mode', type: 'string', required: false, constraint: 'The new mode, present only when --mode changed it.' },
640
+ { name: 'name', type: 'string', required: false, constraint: 'The new name, present only when --name changed it.' },
641
+ { name: 'applied', type: 'string[]', required: false, constraint: 'The fields that changed, in application order.' },
479
642
  ],
480
643
  outputKind: 'object',
481
644
  effects: [
482
- 'LIVE broker: connects to the node\'s view.sock, claims controller, and drives `set_model` on the running engine; the broker persists the choice (model_override + launch.model) itself.',
483
- 'DORMANT node: writes model_override + launch.model directly into the node\'s durable recipe no engine runs until its next revive.',
645
+ 'Model change first: live broker switches over its socket; dormant node persists the durable recipe (model_override + launch.model).',
646
+ 'Kind, mode, and lifecycle changes rebuild the launch spec from the fresh node meta; setting mode to orchestrator seeds a roadmap scaffold if absent.',
647
+ 'Name changes update the row and, when the node already has a live window, rename that viewer window to the new full name.',
484
648
  ],
649
+ dynamicState: () => kindsStateBlock(),
485
650
  },
486
651
  run: async (input) => {
487
652
  const pane = input['pane'] ?? process.env['TMUX_PANE'];
488
- let id = input['node'];
489
- if (id === undefined || id === '')
490
- id = nodeInPane(pane);
491
- if (id === undefined || id === '') {
492
- throw new InputError({ error: 'no_node', message: 'no node found in this pane to repoint', next: 'Pass --node <id>, or run from inside the agent\'s pane.' });
653
+ const nodeId = resolveLifecycleNode(input, pane);
654
+ const applied = [];
655
+ const modelSpec = input['model']?.trim();
656
+ if (modelSpec !== undefined) {
657
+ if (modelSpec === '') {
658
+ throw new InputError({ error: 'empty_spec', message: 'a model spec is required', field: 'model', next: 'Pass a `provider/id`, a tier (ultra|strong|medium|light), an alias (opus|sonnet|haiku), or a substring.' });
659
+ }
660
+ let resolved;
661
+ const meta = getNode(nodeId);
662
+ if (meta === null)
663
+ throw new InputError({ error: 'not_found', message: `no node: ${nodeId}`, next: 'List nodes with `crtr node inspect list`.' });
664
+ if (isBrokerLive(meta)) {
665
+ try {
666
+ resolved = await setModelLive(nodeId, modelSpec);
667
+ }
668
+ catch (err) {
669
+ throw new InputError({ error: 'switch_failed', message: `live model switch failed: ${err instanceof Error ? err.message : String(err)}`, next: 'Check the spec resolves (`provider/id`, tier, alias, or a substring of a registered model).' });
670
+ }
671
+ }
672
+ else {
673
+ const normalized = normalizeModel(modelSpec);
674
+ if (!normalized.includes('/')) {
675
+ throw new InputError({ error: 'unresolvable_dormant', message: `'${modelSpec}' does not resolve to a concrete model for a dormant node`, field: 'model', next: 'Pass an exact `provider/id` (e.g. openai-codex/gpt-5.5) or a tier/alias — the live registry search is only available while the node\'s broker is up.' });
676
+ }
677
+ persistDormantModel(nodeId, normalized);
678
+ resolved = normalized;
679
+ }
680
+ applied.push('model');
493
681
  }
494
- const meta = getNode(id);
682
+ let meta = getNode(nodeId);
495
683
  if (meta === null) {
496
- throw new InputError({ error: 'not_found', message: `no node: ${id}`, next: 'List nodes with `crtr node inspect list`.' });
497
- }
498
- const spec = (input['spec'] ?? '').trim();
499
- if (spec === '') {
500
- throw new InputError({ error: 'empty_spec', message: 'a model spec is required', field: 'spec', next: 'Pass a `provider/id`, a tier (ultra|strong|medium|light), an alias (opus|sonnet|haiku), or a substring.' });
684
+ throw new InputError({ error: 'not_found', message: `no node: ${nodeId}`, next: 'List nodes with `crtr node inspect list`.' });
501
685
  }
502
- if (isBrokerLive(meta)) {
503
- // Live: drive the running engine over its socket. The broker resolves the
504
- // query against the live registry (substrings included) and persists.
505
- let model;
506
- try {
507
- model = await setModelLive(id, spec);
686
+ const kindSpec = input['kind']?.trim();
687
+ const lifecycleSpec = input['lifecycle']?.trim();
688
+ const modeSpec = input['mode']?.trim();
689
+ if (kindSpec !== undefined || lifecycleSpec !== undefined || modeSpec !== undefined) {
690
+ if (kindSpec !== undefined)
691
+ assertKind(kindSpec);
692
+ let newLifecycle = meta.lifecycle;
693
+ if (lifecycleSpec !== undefined) {
694
+ const value = lifecycleSpec.toLowerCase();
695
+ if (value !== 'terminal' && value !== 'resident') {
696
+ throw new InputError({ error: 'bad_lifecycle', message: `invalid lifecycle: ${lifecycleSpec}`, field: 'lifecycle', next: 'Pass `terminal` or `resident`.' });
697
+ }
698
+ newLifecycle = value;
508
699
  }
509
- catch (err) {
510
- throw new InputError({ error: 'switch_failed', message: `live model switch failed: ${err instanceof Error ? err.message : String(err)}`, next: 'Check the spec resolves (`provider/id`, tier, alias, or a substring of a registered model).' });
700
+ let newMode = meta.mode;
701
+ if (modeSpec !== undefined) {
702
+ const value = modeSpec.toLowerCase();
703
+ if (value !== 'base' && value !== 'orchestrator') {
704
+ throw new InputError({ error: 'bad_mode', message: `invalid mode: ${modeSpec}`, field: 'mode', next: 'Pass `base` or `orchestrator`.' });
705
+ }
706
+ newMode = value;
511
707
  }
512
- return { node_id: id, model, applied: 'live' };
708
+ const newKind = kindSpec ?? meta.kind;
709
+ if (newMode === 'orchestrator' && !hasRoadmap(nodeId)) {
710
+ const goal = readGoal(nodeId);
711
+ seedRoadmap(nodeId, goal !== null && goal.trim() !== '' ? { goal: goal.trim() } : {});
712
+ }
713
+ const { launch } = buildLaunchSpec(newKind, newMode, {
714
+ lifecycle: newLifecycle,
715
+ hasManager: meta.parent !== null,
716
+ model: meta.model_override ?? undefined,
717
+ });
718
+ meta = updateNode(nodeId, {
719
+ ...(kindSpec !== undefined ? { kind: newKind } : {}),
720
+ ...(lifecycleSpec !== undefined ? { lifecycle: newLifecycle } : {}),
721
+ ...(modeSpec !== undefined ? { mode: newMode } : {}),
722
+ launch,
723
+ });
724
+ if (kindSpec !== undefined)
725
+ applied.push('kind');
726
+ if (lifecycleSpec !== undefined)
727
+ applied.push('lifecycle');
728
+ if (modeSpec !== undefined)
729
+ applied.push('mode');
513
730
  }
514
- // Dormant: no live registry to search, so normalizeModel only maps
515
- // tiers/aliases. A bare substring that isn't a tier/alias can't resolve to a
516
- // concrete id offline reject rather than write a meaningless override.
517
- const normalized = normalizeModel(spec);
518
- if (!normalized.includes('/')) {
519
- throw new InputError({ error: 'unresolvable_dormant', message: `'${spec}' does not resolve to a concrete model for a dormant node`, field: 'spec', next: 'Pass an exact `provider/id` (e.g. openai-codex/gpt-5.5) or a tier/alias — the live registry search is only available while the node\'s broker is up.' });
731
+ const nameSpec = input['name'];
732
+ if (nameSpec !== undefined) {
733
+ meta = updateNode(nodeId, { name: nameSpec });
734
+ if (meta.window)
735
+ renameWindow(meta.window, fullName(meta));
736
+ applied.push('name');
520
737
  }
521
- persistDormantModel(id, normalized);
522
- return { node_id: id, model: normalized, applied: 'next-revive' };
738
+ if (applied.length === 0) {
739
+ throw new InputError({ error: 'no_change', message: 'no config changes requested', next: 'Pass at least one of --model, --lifecycle, --kind, --mode, --name.' });
740
+ }
741
+ const result = { node_id: nodeId, applied };
742
+ if (getNode(nodeId)?.model_override !== undefined && applied.includes('model'))
743
+ result.model = getNode(nodeId).model_override;
744
+ if (applied.includes('lifecycle'))
745
+ result.lifecycle = getNode(nodeId).lifecycle;
746
+ if (applied.includes('kind'))
747
+ result.kind = getNode(nodeId).kind;
748
+ if (applied.includes('mode'))
749
+ result.mode = getNode(nodeId).mode;
750
+ if (applied.includes('name'))
751
+ result.name = getNode(nodeId).name;
752
+ return result;
753
+ },
754
+ render: (r) => {
755
+ const bits = [];
756
+ if (r['model'] !== undefined)
757
+ bits.push(`model=${r['model']}`);
758
+ if (r['lifecycle'] !== undefined)
759
+ bits.push(`lifecycle=${r['lifecycle']}`);
760
+ if (r['kind'] !== undefined)
761
+ bits.push(`kind=${r['kind']}`);
762
+ if (r['mode'] !== undefined)
763
+ bits.push(`mode=${r['mode']}`);
764
+ if (r['name'] !== undefined)
765
+ bits.push(`name=${r['name']}`);
766
+ return `Configured ${r['node_id']} — ${bits.join(', ')}`;
523
767
  },
524
- render: (r) => r['applied'] === 'live'
525
- ? `Switched ${r['node_id']} to ${r['model']} (live — applied now and persisted).`
526
- : `Repinned ${r['node_id']} to ${r['model']} (applies on next revive).`,
527
768
  });
528
769
  // ---------------------------------------------------------------------------
529
770
  // node cycle — DFS-walk the canvas one window at a time (Alt+] / Alt+[)
@@ -611,52 +852,403 @@ const nodeCycle = defineLeaf({
611
852
  : 'No other live node to focus.',
612
853
  });
613
854
  // ---------------------------------------------------------------------------
614
- // node msg — direct-address any node at a wake tier (wakes a dormant target)
855
+ // node msg — message any node now, or arm a trigger (clock or predicate) that
856
+ // messages/revives it later. See the node-trigger-grammar spec's §Command
857
+ // surface / §node msg.
615
858
  // ---------------------------------------------------------------------------
859
+ /** Max pending triggers a single owner may hold. */
860
+ const TRIGGER_CAP = 100;
861
+ /** Resolve the calling node (the armer/owner of a trigger). CRTR_NODE_ID is
862
+ * mandatory — a trigger is owned by the node that arms it (owner_id). */
863
+ function armerId() {
864
+ const id = process.env['CRTR_NODE_ID'];
865
+ if (id === undefined || id === '') {
866
+ throw new InputError({ error: 'no_node', message: 'no node to arm a trigger (CRTR_NODE_ID unset)', next: 'Run from inside a node.' });
867
+ }
868
+ return id;
869
+ }
870
+ /** Build ParseOpts, including `tz` only when actually provided. */
871
+ function parseOpts(now, tz) {
872
+ return tz !== undefined && tz.trim() !== '' ? { tz, now } : { now };
873
+ }
874
+ /** Map a time-grammar error (parseWhen/parseCadence) to the rendered error block. */
875
+ function throwTimeError(e) {
876
+ const next = {
877
+ wake_in_past: 'Pick a future instant — a positive duration ("5m","2h") or an ISO time later than now.',
878
+ bad_when: 'Use a duration ("5m","1h30m"), a zoned ISO ("2026-06-07T09:00:00Z"), or a bare ISO ("2026-06-07T09:00").',
879
+ bad_cadence: 'Use a duration ("6h"), a 5-field cron ("0 9 * * *"), or an @alias ("@daily").',
880
+ unknown_zone: 'Pass --tz with an IANA zone name (e.g. "America/New_York").',
881
+ cadence_too_fast: 'Use a cadence of at least 60s (e.g. "1m","5m","1h").',
882
+ };
883
+ throw new InputError({ error: e.code, message: e.message, received: e.received, next: next[e.code] ?? 'Fix the time value and retry.' });
884
+ }
885
+ /** Run armTrigger, mapping its thrown integrity backstop (TriggerArmError) to a
886
+ * rendered error block. The surface validates these cases up front, so a throw
887
+ * here is a backstop, not the primary path. */
888
+ function armTriggerOrThrow(spec) {
889
+ try {
890
+ return armTrigger(spec);
891
+ }
892
+ catch (e) {
893
+ if (e instanceof TriggerArmError) {
894
+ const next = {
895
+ empty_body: 'Provide a real body, or use --fresh for a fresh revive with no message.',
896
+ deadline_cannot_recur: 'Drop --every — a deadline cannot recur.',
897
+ bad_action_mode: 'This is a crtr bug — report it.',
898
+ };
899
+ throw new InputError({ error: e.code, message: e.message, next: next[e.code] ?? 'Fix the trigger spec and retry.' });
900
+ }
901
+ throw e;
902
+ }
903
+ }
904
+ /** Reject an arm that would push this owner past the pending-triggers cap. */
905
+ function assertUnderCap(ownerId) {
906
+ const pending = listTriggers({ owner: ownerId }).length;
907
+ if (pending >= TRIGGER_CAP) {
908
+ throw new InputError({
909
+ error: 'cap_exceeded',
910
+ message: `you hold ${pending} pending triggers (cap ${TRIGGER_CAP}).`,
911
+ next: 'Reap stale triggers with `crtr node triggers cancel <id>` (see `crtr node triggers list`) before arming more.',
912
+ });
913
+ }
914
+ }
915
+ /** A fresh revive resumes a window with no memory beyond disk, so it needs
916
+ * durable state to wake INTO. Accept it only when the target has a goal
917
+ * (initial-prompt.md) or roadmap (roadmap.md) on disk. */
918
+ function hasRecoverableState(nodeId) {
919
+ const goal = readGoal(nodeId);
920
+ if (goal !== null && goal.trim() !== '')
921
+ return true;
922
+ const roadmap = readRoadmap(nodeId);
923
+ return roadmap !== null && roadmap.trim() !== '';
924
+ }
925
+ /** A human ETA hint like " (~5m)" from a fire-at ISO relative to now. '' if past. */
926
+ function etaHint(fireAt, now) {
927
+ const ms = new Date(fireAt).getTime() - now.getTime();
928
+ if (!Number.isFinite(ms) || ms <= 0)
929
+ return '';
930
+ const mins = Math.round(ms / 60_000);
931
+ if (mins < 60)
932
+ return ` (~${mins}m)`;
933
+ const hrs = Math.round(ms / 3_600_000);
934
+ if (hrs < 48)
935
+ return ` (~${hrs}h)`;
936
+ return ` (~${Math.round(ms / 86_400_000)}d)`;
937
+ }
938
+ /** Resolve a `node msg` target: exactly one of --to/--self. */
939
+ function resolveMsgTarget(input) {
940
+ const to = input['to']?.trim();
941
+ const self = input['self'] === true;
942
+ const hasTo = to !== undefined && to !== '';
943
+ if (hasTo === self) {
944
+ throw new InputError({
945
+ error: 'bad_target',
946
+ message: hasTo && self ? '--to and --self are mutually exclusive' : 'exactly one of --to or --self is required',
947
+ next: 'Pass --to <node-id> or --self.',
948
+ });
949
+ }
950
+ if (self) {
951
+ const id = process.env['CRTR_NODE_ID'];
952
+ if (id === undefined || id === '') {
953
+ throw new InputError({ error: 'no_node', message: 'no node to target with --self (CRTR_NODE_ID unset)', next: 'Run from inside a node, or pass --to <node-id>.' });
954
+ }
955
+ return { targetId: id, isSelf: true };
956
+ }
957
+ if (getNode(to) === null) {
958
+ throw new InputError({ error: 'not_found', message: `no node: ${to}`, field: 'to', next: 'List nodes with `crtr node inspect list`.' });
959
+ }
960
+ return { targetId: to, isSelf: false };
961
+ }
616
962
  const nodeMsg = defineLeaf({
617
963
  name: 'msg',
618
- description: 'direct-message any node at a wake tier',
619
- whenToUse: 'you want to address a specific node directly — steer it mid-flight, hand it a correction, ping it, or pass it new information and have it land regardless of subscriptions, reviving a dormant target. You set how urgently it lands, from an immediate interrupt to a note read on its next cycle. Use `node subscribe` instead to wire ongoing push delivery rather than send a one-off, and `push` to report UP your own spine',
964
+ description: 'message any node now, or arm a trigger that messages/revives it later',
965
+ whenToUse: 'you want to address a specific node directly — steer it mid-flight, hand it a correction, ping it, pass it new information, or schedule any of that for later — regardless of subscriptions. Default delivers immediately and revives a dormant target; `--at`/`--every` arm a clock trigger instead of acting now; `--fresh` revives with no inbox entry; `--self --until` puts a deadline backstop on your current inbox-wait; `--when` arms a daemon-evaluated predicate for an external condition (CI, a deploy) you cannot otherwise be pushed for. Use `node subscribe` instead to wire ONGOING push delivery rather than a one-off, `push` to report UP your own spine, and `node new` to schedule the BIRTH of a node rather than message an existing one',
620
966
  help: {
621
967
  name: 'node msg',
622
- summary: 'send a direct message to any node\'s inbox at a wake tier a direct message wakes the node regardless of subscriptions (reviving it if dormant)',
968
+ summary: 'message a node now, or arm a trigger (clock or predicate) that messages/revives it later. No trigger flag → immediate inbox delivery (or immediate fresh revive with --fresh). --at/--every clock trigger. --self --until self-only deadline backstop on the current inbox-wait. --when daemon-evaluated predicate, fires once on exit 0',
623
969
  params: [
624
- { kind: 'positional', name: 'node', required: true, constraint: 'Target node id.' },
625
- { kind: 'stdin', name: 'body', required: true, constraint: 'Message body. Positional (after the node id is consumed) or stdin.' },
626
- { kind: 'flag', name: 'tier', type: 'enum', choices: ['critical', 'urgent', 'normal', 'deferred'], required: false, default: 'normal', constraint: 'How it lands: critical = interrupt + new turn; urgent = steer mid-turn; normal = follow-up; deferred = read on next cycle.' },
970
+ { kind: 'stdin', name: 'body', required: false, constraint: 'Message body. Positional or stdin. Required for a plain or clock-triggered message and for --until. Forbidden with --fresh. Optional with --when — if omitted, the predicate\'s captured stdout becomes the body when it fires.' },
971
+ { kind: 'flag', name: 'to', type: 'string', required: false, constraint: 'Target an existing node by id. Exactly one of --to/--self is required.' },
972
+ { kind: 'flag', name: 'self', type: 'bool', required: false, constraint: 'Target the calling node (CRTR_NODE_ID). Exactly one of --to/--self is required. Required for --until.' },
973
+ { kind: 'flag', name: 'tier', type: 'enum', choices: ['critical', 'urgent', 'normal', 'deferred'], required: false, constraint: 'Inbox delivery urgency: critical = interrupt + new turn; urgent = steer mid-turn; normal = follow-up; deferred = read on next cycle. Defaults to normal when omitted. Valid for a plain or --when inbox message (immediate or clock-triggered); forbidden with --fresh or --until (--until is always urgent).' },
974
+ { kind: 'flag', name: 'fresh', type: 'bool', required: false, constraint: 'Fresh-revive the target instead of delivering a message: no inbox entry, just a clean revive (resume:false). Valid alone (revives now) or with --at/--every (arms a clock-triggered fresh revive). Forbidden with a body, --tier, --until, or --when. Rejected if the target has no goal/roadmap on disk (would resume amnesiac).' },
975
+ { kind: 'flag', name: 'at', type: 'string', required: false, constraint: 'Arm a one-shot clock trigger at this future time instead of acting now — a duration ("5m","1h30m"), a zoned ISO ("2026-06-07T09:00:00Z"), or a bare ISO ("2026-06-07T09:00", host-local or in --tz). Required unless --every is given (the cadence then sets the first fire). Mutually exclusive with --when.' },
976
+ { kind: 'flag', name: 'every', type: 'string', required: false, constraint: 'Arm a recurring clock trigger on this cadence — a duration ("6h","30m") or a 5-field cron / @alias ("0 9 * * *","@daily"). Combine with --at to anchor the first fire, then recur normally. Min cadence 60s. With --when, this instead sets the PREDICATE EVALUATION cadence (required), not a repeating action.' },
977
+ { kind: 'flag', name: 'when', type: 'string', required: false, constraint: 'Arm a daemon-evaluated bash predicate instead of a clock trigger: exit 0 fires the message once, non-zero leaves it armed for the next evaluation. Requires --every (eval cadence) and --timeout (wall-clock bound). Mutually exclusive with --at, --fresh, and --until.' },
978
+ { kind: 'flag', name: 'until', type: 'string', required: false, constraint: 'Self-only: bind a deadline to your current dormancy — you wake on the first inbox message, or at this time if none arrives (whichever fires first cancels the other). One-shot, urgent tier, ≤ 1 per node (a new --until replaces any prior). Mutually exclusive with --at, --every, --when, --fresh, --tier, and --timeout. Requires --self and a body.' },
979
+ { kind: 'flag', name: 'timeout', type: 'string', required: false, constraint: 'Wall-clock bound for a --when predicate — a duration or absolute time after which the trigger is consumed and the owner gets one urgent notice. Required with --when; rejected otherwise.' },
980
+ { kind: 'flag', name: 'tz', type: 'string', required: false, constraint: 'IANA zone for a calendar --every, a bare-ISO --at, or a bare-ISO --until (default: host-local). Valid only alongside --at/--every/--until; rejected with --when, and rejected when acting immediately (no trigger flags) since none of those have anything to parse it against.' },
627
981
  ],
628
982
  output: [
629
- { name: 'delivered', type: 'boolean', required: true, constraint: 'True when the message was appended to the target inbox.' },
630
- { name: 'node_id', type: 'string', required: true, constraint: 'Target node.' },
631
- { name: 'woke', type: 'boolean', required: true, constraint: 'True when a dormant target was revived to receive it.' },
983
+ { name: 'mode', type: 'string', required: true, constraint: '"inbox_message", "fresh_revive", or "deadline_message" what this message enacts.' },
984
+ { name: 'armed', type: 'boolean', required: true, constraint: 'True when a trigger row was armed for later (--at/--every/--when/--until); false when the action happened immediately.' },
985
+ { name: 'target', type: 'string', required: true, constraint: '"self" or the target node id.' },
986
+ { name: 'delivered', type: 'boolean', required: false, constraint: 'True when an inbox message was delivered immediately (armed=false, mode=inbox_message).' },
987
+ { name: 'revived', type: 'boolean', required: false, constraint: 'True when a dormant target was revived to receive an immediate delivery, or when an immediate --fresh fired the revive now.' },
988
+ { name: 'id', type: 'string', required: false, constraint: 'The trigger id (cancel/list it via `node triggers`), present only when armed=true.' },
989
+ { name: 'trigger_type', type: 'string', required: false, constraint: '"at", "every", "when", or "until", present only when armed=true.' },
990
+ { name: 'fires_at', type: 'string', required: false, constraint: 'Absolute UTC fire time, present only for an armed clock or deadline trigger.' },
991
+ { name: 'next_eval_at', type: 'string', required: false, constraint: 'The next predicate-evaluation instant (always "now" at arm time), present only for an armed --when trigger.' },
992
+ { name: 'timeout_at', type: 'string', required: false, constraint: 'Absolute UTC wall-clock bound, present only for an armed --when trigger.' },
993
+ { name: 'recur', type: 'string', required: false, constraint: 'Cadence display ("every 6h", cron, or eval cadence for --when), present only when --every was given.' },
994
+ { name: 'guidance', type: 'string', required: true, constraint: 'What to do now — end your turn to go dormant when armed; delivery confirmation when immediate.' },
632
995
  ],
633
996
  outputKind: 'object',
634
- effects: ['Appends a message entry to the target inbox.jsonl.', 'Revives the target (resume) if it has no live window.'],
997
+ effects: [
998
+ 'Immediate inbox_message: appends a message entry to the target inbox.jsonl and best-effort revives the target if dormant.',
999
+ 'Immediate fresh_revive: revives the target now with no inbox entry (resume:false).',
1000
+ 'Armed (--at/--every/--when/--until): inserts one triggers row; nothing fires/evaluates until the daemon\'s next pass. No pi spawn, no transition — end your turn separately to go dormant.',
1001
+ '--until upserts the node\'s single deadline trigger, replacing any prior.',
1002
+ ],
635
1003
  },
636
1004
  run: async (input) => {
637
- const id = input['node'];
638
- const target = getNode(id);
639
- if (target === null)
640
- throw new InputError({ error: 'not_found', message: `no node: ${id}`, next: 'List nodes with `crtr node inspect list`.' });
641
- const body = (input['body'] ?? '').trim();
642
- if (body === '')
643
- throw new InputError({ error: 'empty_body', message: 'a message body is required', field: 'body', next: 'Pass the message after the node id or on stdin.' });
644
- const tier = (input['tier'] ?? 'normal');
1005
+ const { targetId, isSelf } = resolveMsgTarget(input);
1006
+ const bodyRaw = input['body'];
1007
+ const body = bodyRaw !== undefined ? bodyRaw.trim() : undefined;
1008
+ const hasBody = body !== undefined && body !== '';
1009
+ const tierRaw = input['tier'];
1010
+ const hasTier = tierRaw !== undefined && tierRaw !== '';
1011
+ const fresh = input['fresh'] === true;
1012
+ const at = input['at']?.trim();
1013
+ const every = input['every']?.trim();
1014
+ const whenRaw = input['when'];
1015
+ const hasWhen = whenRaw !== undefined && whenRaw.trim() !== '';
1016
+ const until = input['until']?.trim();
1017
+ const hasUntil = until !== undefined && until !== '';
1018
+ const timeout = input['timeout']?.trim();
1019
+ const hasTimeout = timeout !== undefined && timeout !== '';
1020
+ const tz = input['tz'];
1021
+ const hasAt = at !== undefined && at !== '';
1022
+ const hasEvery = every !== undefined && every !== '';
1023
+ // --- Validation per the approved flag matrix ---
1024
+ if (hasUntil && !isSelf) {
1025
+ throw new InputError({ error: 'until_self_only', message: '--until is self-only', field: 'until', next: 'Use --self, or drop --until.' });
1026
+ }
1027
+ if (hasUntil && (hasAt || hasEvery || hasWhen || fresh || hasTier || hasTimeout)) {
1028
+ throw new InputError({ error: 'bad_until_combo', message: '--until cannot combine with --at, --every, --when, --fresh, --tier, or --timeout', next: 'Drop the conflicting flag(s), or remove --until for a different trigger.' });
1029
+ }
1030
+ if (hasWhen && (hasAt || fresh)) {
1031
+ throw new InputError({ error: 'bad_when_combo', message: '--when cannot combine with --at or --fresh', next: 'A trigger is either clock-driven (--at/--every/--fresh) or predicate-driven (--when).' });
1032
+ }
1033
+ if (hasWhen && !hasEvery) {
1034
+ throw new InputError({ error: 'when_needs_every', message: '--when requires --every (the evaluation cadence)', next: 'Pass --every <cadence> alongside --when.' });
1035
+ }
1036
+ if (hasWhen && !hasTimeout) {
1037
+ throw new InputError({ error: 'when_needs_timeout', message: '--when requires --timeout (the wall-clock bound)', next: 'Pass --timeout <duration> alongside --when.' });
1038
+ }
1039
+ if (hasTimeout && !hasWhen) {
1040
+ throw new InputError({ error: 'timeout_needs_when', message: '--timeout is only valid with --when', next: 'Drop --timeout, or pass --when to make this a predicate trigger.' });
1041
+ }
1042
+ if (fresh && (hasBody || hasTier || hasUntil || hasWhen)) {
1043
+ throw new InputError({ error: 'bad_fresh_combo', message: '--fresh cannot combine with a body, --tier, --until, or --when', next: 'Drop the conflicting flag(s) — --fresh is a fresh revive, not a message.' });
1044
+ }
1045
+ const hasTz = tz !== undefined && tz.trim() !== '';
1046
+ if (hasTz && hasWhen) {
1047
+ throw new InputError({ error: 'tz_not_valid_with_when', message: '--tz has no effect with --when (predicate evaluation parses no calendar timezone)', field: 'tz', next: 'Drop --tz.' });
1048
+ }
1049
+ if (hasTz && !hasAt && !hasEvery && !hasUntil) {
1050
+ throw new InputError({ error: 'tz_needs_trigger', message: '--tz has no effect without --at, --every, or --until to parse it against', field: 'tz', next: 'Drop --tz, or pass --at/--every/--until.' });
1051
+ }
1052
+ const target = isSelf ? 'self' : targetId;
1053
+ // --- Deadline (--until): self-only, one-shot, urgent ---
1054
+ if (hasUntil) {
1055
+ if (!hasBody) {
1056
+ throw new InputError({ error: 'empty_body', message: 'a body is required for --until', field: 'body', next: 'Pass a timeout playbook after the target flags or on stdin.' });
1057
+ }
1058
+ const owner = armerId();
1059
+ const now = new Date();
1060
+ const w = parseWhen(until, parseOpts(now, tz));
1061
+ if ('error' in w)
1062
+ throwTimeError(w.error);
1063
+ assertUnderCap(owner);
1064
+ const payload = { body: body, timeout: true, label: body.split('\n')[0].slice(0, 120) };
1065
+ const id = `tr-${newNodeId()}`;
1066
+ armTriggerOrThrow({
1067
+ trigger_id: id,
1068
+ node_id: targetId,
1069
+ owner_id: owner,
1070
+ action: 'msg',
1071
+ action_mode: 'deadline_message',
1072
+ trigger_type: 'until',
1073
+ fire_at: w.fireAt,
1074
+ payload,
1075
+ });
1076
+ const eta = etaHint(w.fireAt, now);
1077
+ const guidance = `Deadline armed. You wake on the first inbox message, or at ${w.fireAt}${eta} if none arrives. Now delegate / end your turn to go dormant — whichever fires first cancels the other.`;
1078
+ return { mode: 'deadline_message', armed: true, id, target, trigger_type: 'until', fires_at: w.fireAt, guidance };
1079
+ }
1080
+ // --- Predicate (--when) ---
1081
+ if (hasWhen) {
1082
+ const owner = armerId();
1083
+ const now = new Date();
1084
+ const cad = parseCadence(every, parseOpts(now, undefined));
1085
+ if ('error' in cad)
1086
+ throwTimeError(cad.error);
1087
+ const t = parseWhen(timeout, { now });
1088
+ if ('error' in t)
1089
+ throwTimeError(t.error);
1090
+ assertUnderCap(owner);
1091
+ // A supplied body is delivered as-is; omitted body means stdout becomes the
1092
+ // body when the predicate fires (spec's stdout-as-body rule) — armTrigger
1093
+ // accepts a body-less payload only for this trigger_type='when' case.
1094
+ const payload = {
1095
+ ...(hasBody ? { body: body } : {}),
1096
+ label: hasBody ? body.split('\n')[0].slice(0, 120) : `(stdout) ${whenRaw.trim().split('\n')[0].slice(0, 110)}`,
1097
+ ...(hasTier ? { tier: tierRaw } : {}),
1098
+ };
1099
+ const id = `tr-${newNodeId()}`;
1100
+ armTriggerOrThrow({
1101
+ trigger_id: id,
1102
+ node_id: targetId,
1103
+ owner_id: owner,
1104
+ action: 'msg',
1105
+ action_mode: 'inbox_message',
1106
+ trigger_type: 'when',
1107
+ when_script: whenRaw.trim(),
1108
+ eval_every: cad.recur,
1109
+ next_eval_at: now.toISOString(),
1110
+ timeout_at: t.fireAt,
1111
+ payload,
1112
+ });
1113
+ const guidance = `Predicate trigger armed — evaluated ${cadenceDisplay(cad.recur)} (first check now), bounded by a wall-clock timeout at ${t.fireAt}. Fires once the predicate exits 0; non-zero leaves it armed. End your turn to go dormant; do not push final.`;
1114
+ return {
1115
+ mode: 'inbox_message',
1116
+ armed: true,
1117
+ id,
1118
+ target,
1119
+ trigger_type: 'when',
1120
+ next_eval_at: now.toISOString(),
1121
+ timeout_at: t.fireAt,
1122
+ recur: cadenceDisplay(cad.recur),
1123
+ guidance,
1124
+ };
1125
+ }
1126
+ // --- Clock trigger (--at / --every), no --when/--until ---
1127
+ if (hasAt || hasEvery) {
1128
+ if (fresh) {
1129
+ if (!hasRecoverableState(targetId)) {
1130
+ throw new InputError({
1131
+ error: 'fresh_no_recoverable_state',
1132
+ message: `node ${targetId} has no goal/roadmap on disk — a fresh revive would resume amnesiac.`,
1133
+ next: 'Seed a goal/roadmap first, or drop --fresh so the woken context carries a message.',
1134
+ });
1135
+ }
1136
+ }
1137
+ else if (!hasBody) {
1138
+ throw new InputError({ error: 'empty_body', message: 'a message body is required (or --fresh for a clock-triggered revive)', field: 'body', next: 'Pass the message after the target flags or on stdin.' });
1139
+ }
1140
+ const owner = armerId();
1141
+ const now = new Date();
1142
+ let recur;
1143
+ let fireAt;
1144
+ if (hasEvery) {
1145
+ const cad = parseCadence(every, parseOpts(now, tz));
1146
+ if ('error' in cad)
1147
+ throwTimeError(cad.error);
1148
+ recur = cad.recur;
1149
+ fireAt = cad.firstFireAt;
1150
+ // BOTH --at and --every (fixed-interval or cron): --at anchors the first
1151
+ // fire; recurrence thereafter advances from wall-clock `now` at settle
1152
+ // time (nextSlotAfter), independent of the anchor, so this holds for
1153
+ // both cadence shapes.
1154
+ if (hasAt) {
1155
+ const w = parseWhen(at, parseOpts(now, tz));
1156
+ if ('error' in w)
1157
+ throwTimeError(w.error);
1158
+ fireAt = w.fireAt;
1159
+ }
1160
+ }
1161
+ else {
1162
+ const w = parseWhen(at, parseOpts(now, tz));
1163
+ if ('error' in w)
1164
+ throwTimeError(w.error);
1165
+ fireAt = w.fireAt;
1166
+ }
1167
+ assertUnderCap(owner);
1168
+ const actionMode = fresh ? 'fresh_revive' : 'inbox_message';
1169
+ const payload = fresh
1170
+ ? null
1171
+ : { body: body, label: body.split('\n')[0].slice(0, 120), ...(hasTier ? { tier: tierRaw } : {}) };
1172
+ const id = `tr-${newNodeId()}`;
1173
+ armTriggerOrThrow({
1174
+ trigger_id: id,
1175
+ node_id: targetId,
1176
+ owner_id: owner,
1177
+ action: 'msg',
1178
+ action_mode: actionMode,
1179
+ trigger_type: recur !== undefined ? 'every' : 'at',
1180
+ fire_at: fireAt,
1181
+ ...(recur !== undefined ? { recur } : {}),
1182
+ payload,
1183
+ });
1184
+ const eta = etaHint(fireAt, now);
1185
+ const guidance = recur !== undefined
1186
+ ? `${fresh ? 'Fresh-revive' : 'Message'} recurrence armed (${cadenceDisplay(recur)}); first fire ${fireAt}${eta}. End your turn to go dormant; do not push final. The runtime keeps firing it even across your crash/finalize — cancel with \`crtr node triggers cancel ${id}\`.`
1187
+ : fresh
1188
+ ? `Fresh-revive armed. The target wakes in a fresh window at ${fireAt}${eta}, re-reading its roadmap. End your turn now to go dormant; do not push final.`
1189
+ : `Message armed. It is delivered at ${fireAt}${eta}. End your turn now to go dormant; do not push final.`;
1190
+ return {
1191
+ mode: actionMode,
1192
+ armed: true,
1193
+ id,
1194
+ target,
1195
+ trigger_type: recur !== undefined ? 'every' : 'at',
1196
+ fires_at: fireAt,
1197
+ ...(recur !== undefined ? { recur: cadenceDisplay(recur) } : {}),
1198
+ guidance,
1199
+ };
1200
+ }
1201
+ // --- Immediate (no trigger flag) ---
1202
+ if (fresh) {
1203
+ if (!hasRecoverableState(targetId)) {
1204
+ throw new InputError({
1205
+ error: 'fresh_no_recoverable_state',
1206
+ message: `node ${targetId} has no goal/roadmap on disk — a fresh revive would resume amnesiac.`,
1207
+ next: 'Seed a goal/roadmap first, or drop --fresh so the woken context carries a message.',
1208
+ });
1209
+ }
1210
+ reviveNode(targetId, { resume: false });
1211
+ return {
1212
+ mode: 'fresh_revive',
1213
+ armed: false,
1214
+ target,
1215
+ revived: true,
1216
+ guidance: `Fresh-revived ${isSelf ? 'yourself' : targetId} now — it resumes a clean window re-reading its roadmap/disk.`,
1217
+ };
1218
+ }
1219
+ if (!hasBody) {
1220
+ throw new InputError({ error: 'empty_body', message: 'a message body is required (or --fresh / --when)', field: 'body', next: 'Pass the message after the target flags or on stdin.' });
1221
+ }
1222
+ const targetMeta = getNode(targetId);
1223
+ if (targetMeta === null) {
1224
+ throw new InputError({ error: 'not_found', message: `no node: ${targetId}`, next: 'List nodes with `crtr node inspect list`.' });
1225
+ }
1226
+ const tier = (hasTier ? tierRaw : 'normal');
645
1227
  const from = process.env['CRTR_NODE_ID'] ?? 'human';
646
1228
  // A long body clips in the receiver's digest; appendInbox mints a short `id`
647
1229
  // for it so the receiver can read the full text back via `feed message <id>`.
648
- appendInbox(id, { from, tier, kind: 'message', label: body.split('\n')[0].slice(0, 120), data: { body } });
1230
+ appendInbox(targetId, { from, tier, kind: 'message', label: body.split('\n')[0].slice(0, 120), data: { body: body } });
649
1231
  // A direct message wakes any node: if the target has no live window
650
1232
  // (done/dead/idle-released), revive it so its inbox-watcher delivers this.
651
1233
  let woke = false;
652
- if (!windowAlive(target.tmux_session, target.window)) {
1234
+ if (!windowAlive(targetMeta.tmux_session, targetMeta.window)) {
653
1235
  try {
654
- void reviveNode(id, { resume: true });
1236
+ void reviveNode(targetId, { resume: true });
655
1237
  woke = true;
656
1238
  }
657
- catch { /* best-effort wake */ }
1239
+ catch {
1240
+ /* best-effort wake */
1241
+ }
1242
+ }
1243
+ return { mode: 'inbox_message', armed: false, target, delivered: true, revived: woke, guidance: woke ? 'Delivered — the dormant target was revived to receive it.' : 'Delivered.' };
1244
+ },
1245
+ render: (r) => {
1246
+ if (r['armed'] === true) {
1247
+ const recur = r['recur'] ? `, recurs ${r['recur']}` : '';
1248
+ const when = r['fires_at'] ? `fires ${r['fires_at']}${recur}` : `next eval ${r['next_eval_at']}, timeout ${r['timeout_at']}`;
1249
+ return `Armed ${r['mode']} trigger ${r['id']} (${r['trigger_type']}) — ${when}, target ${r['target']}.\n\n${r['guidance']}`;
658
1250
  }
659
- return { delivered: true, node_id: id, woke };
1251
+ return `${r['guidance']}`;
660
1252
  },
661
1253
  });
662
1254
  // ---------------------------------------------------------------------------
@@ -780,10 +1372,11 @@ const nodePromote = defineLeaf({
780
1372
  });
781
1373
  // ---------------------------------------------------------------------------
782
1374
  // Shared lifecycle plumbing — the single implementation behind BOTH `node
783
- // demote` (the friendly terminal-only verb that pairs with `node promote`) and
784
- // `node lifecycle` (the low-level orthogonal flip, which also does resident).
1375
+ // lifecycle demote` (the friendly terminal-only verb that pairs with `node
1376
+ // promote`) and `node config --lifecycle` (the low-level orthogonal flip, which
1377
+ // also does resident).
785
1378
  // ---------------------------------------------------------------------------
786
- /** Resolve the node a demote/lifecycle command acts on: explicit --node, else
1379
+ /** Resolve the node a demote/config command acts on: explicit --node, else
787
1380
  * the node occupying --pane (the Alt+C menu passes #{pane_id}), else the caller
788
1381
  * (CRTR_NODE_ID). Throws a rendered error when none resolves or it is unknown. */
789
1382
  function resolveLifecycleNode(input, pane) {
@@ -803,8 +1396,8 @@ function resolveLifecycleNode(input, pane) {
803
1396
  * revive comes back with the new lifecycle's prompt baked in (the live session
804
1397
  * is steered by the persona injector; this fixes the static prompt the daemon
805
1398
  * replays). Spine is fixed by parent-ness, so it carries through unchanged.
806
- * The ONE implementation `node demote` (always terminal) and `node lifecycle`
807
- * (the passed value) both call — no duplication. */
1399
+ * The ONE implementation `node lifecycle demote` (always terminal) and
1400
+ * `node config --lifecycle` (the passed value) both call — no duplication. */
808
1401
  function setLifecycle(id, value, opts) {
809
1402
  const target = getNode(id);
810
1403
  const { launch } = buildLaunchSpec(target.kind, target.mode, {
@@ -822,18 +1415,18 @@ function setLifecycle(id, value, opts) {
822
1415
  return { node_id: meta.node_id, lifecycle: meta.lifecycle, detached };
823
1416
  }
824
1417
  // ---------------------------------------------------------------------------
825
- // node demote — flip a node to TERMINAL in place (the friendly half of the
826
- // promote/demote pair; bound to Alt+C → d, and → D with --detach). It stays
827
- // focused and running but now owes a final up the spine. A terminal-only skin
828
- // over the shared setLifecycle plumbing; `node lifecycle` is the orthogonal
829
- // low-level command (it also does resident). See vision F5.
1418
+ // node lifecycle demote — flip a node to TERMINAL in place (the friendly half
1419
+ // of the promote/demote pair; bound to Alt+C → d, and → D with --detach). It
1420
+ // stays focused and running but now owes a final up the spine. A terminal-only
1421
+ // skin over the shared setLifecycle plumbing; `node config --lifecycle` is the
1422
+ // orthogonal low-level command (it also does resident). See vision F5.
830
1423
  // ---------------------------------------------------------------------------
831
1424
  const nodeDemote = defineLeaf({
832
1425
  name: 'demote',
833
1426
  description: 'demote a node to terminal in place; it stays focused and running but now owes a final; --detach also closes its viewer (broker keeps running off-screen)',
834
- whenToUse: 'you are watching a resident/interactive node and want to put it on a finishing track WITHOUT disturbing it: flip it terminal IN PLACE — it keeps its viewer and your focus, keeps running, but now owes a final report up the spine and reaps when done. The friendly counterpart to `node promote`. You can also let go entirely and close its viewer so the agent finishes off-screen on its broker. Use `node recycle` instead to FINISH it now and reboot a fresh root in its pane, and `node lifecycle` for the orthogonal low-level flip (incl. terminal→resident)',
1427
+ whenToUse: 'you are watching a resident/interactive node and want to put it on a finishing track WITHOUT disturbing it: flip it terminal IN PLACE — it keeps its viewer and your focus, keeps running, but now owes a final report up the spine and reaps when done. The friendly counterpart to `node promote`. You can also let go entirely and close its viewer so the agent finishes off-screen on its broker. Use `node lifecycle recycle` instead to FINISH it now and reboot a fresh root in its pane, and `node config --lifecycle` for the orthogonal low-level flip (incl. terminal→resident)',
835
1428
  help: {
836
- name: 'node demote',
1429
+ name: 'node lifecycle demote',
837
1430
  summary: 'demote a node to terminal IN PLACE — it stays focused and running but now owes a final up the spine and reaps when done. Pairs with `node promote` (mode↑); `--detach` also closes its viewer (the broker engine keeps running off-screen)',
838
1431
  params: [
839
1432
  { kind: 'flag', name: 'node', type: 'string', required: false, constraint: 'Node to demote. Defaults to the node in --pane, else the caller (CRTR_NODE_ID).' },
@@ -857,41 +1450,17 @@ const nodeDemote = defineLeaf({
857
1450
  render: (r) => `Demoted ${r['node_id']} — lifecycle now ${r['lifecycle']} (in place)${r['detached'] === true ? ', viewer closed (broker still running off-screen)' : ''}.`,
858
1451
  });
859
1452
  // ---------------------------------------------------------------------------
860
- // node lifecycle — flip the lifecycle axis (terminal ↔ resident), independent
861
- // of mode. The persona injector delivers the transition guidance.
1453
+ // node lifecycle — recycle, close, or demote a node
862
1454
  // ---------------------------------------------------------------------------
863
- const nodeLifecycle = defineLeaf({
1455
+ const nodeLifecycle = defineBranch({
864
1456
  name: 'lifecycle',
865
- description: 'switch a node between terminal and resident',
866
- whenToUse: 'you want to flip a node\'s LIFECYCLE independent of its mode: make a node RESIDENT so it becomes interactable it stays dormant, wakes on inbox/human, and is never forced to submit a final; or make a node TERMINAL so it owes a final result up the spine and reaps when done. Orthogonal to `node promote`, which changes MODE (base↔orchestrator), not lifecycle. You can also let go of a still-running agent and send it off-screen to finish in the background. For the human-driver flip-to-terminal pair reach for `node demote` (the friendly terminal-only skin over this)',
1457
+ description: 'lifecycle actions recycle, close, demote a node',
1458
+ whenToUse: 'you are ending or transitioning a node\'s life: finish the agent in your pane and reboot a fresh root (`recycle`), tear a node + its exclusive subtree down without finishing (`close`), or flip a node onto a finishing track in place (`demote`). To merely SET the terminal/resident attribute headlessly use `node config --lifecycle`; to FINISH from inside a node\'s own turn use `crtr push final`.',
867
1459
  help: {
868
1460
  name: 'node lifecycle',
869
- summary: 'set a node\'s lifecycle axis terminal (owes a final up the spine, reaps when done) or resident (interactable, stays dormant, woken by inbox/human, never forced to submit). Orthogonal to mode; promotion does not touch it. `--detach` also closes a live agent\'s viewer (broker keeps running off-screen)',
870
- params: [
871
- { kind: 'positional', name: 'lifecycle', required: true, constraint: 'terminal | resident.' },
872
- { kind: 'flag', name: 'node', type: 'string', required: false, constraint: 'Node to change. Defaults to the node in --pane, else the caller (CRTR_NODE_ID).' },
873
- { kind: 'flag', name: 'pane', type: 'string', required: false, constraint: 'tmux pane id whose node to change, when --node is omitted. Defaults to $TMUX_PANE. The Alt+C menu passes this for you.' },
874
- { kind: 'flag', name: 'detach', type: 'bool', required: false, constraint: 'After flipping lifecycle, stop foregrounding the agent by closing its viewer pane. The broker engine keeps generating off-screen and — now terminal — pushes a final up the spine when done; a later `focus` reattaches a viewer. The human-driver "I am done foregrounding this" move (Alt+C → D).' },
875
- ],
876
- output: [
877
- { name: 'node_id', type: 'string', required: true, constraint: 'The node.' },
878
- { name: 'lifecycle', type: 'string', required: true, constraint: 'Its new lifecycle (terminal | resident).' },
879
- { name: 'detached', type: 'boolean', required: false, constraint: 'True when --detach closed the agent\'s viewer pane (broker keeps running).' },
880
- ],
881
- outputKind: 'object',
882
- effects: ['Sets lifecycle on the node meta and rebuilds its launch spec so a future revive boots with the new lifecycle\'s prompt baked in.', 'The persona injector delivers the transition guidance at the next turn boundary (or on the node\'s next revive if it is dormant).', 'With --detach: closes the agent\'s viewer pane WITHOUT touching the broker engine — it keeps generating off-screen, reconnectable by a later `focus`.'],
1461
+ summary: 'end or transition a node — recycle the pane, close + cascade-cancel a subtree, or demote to terminal in place',
883
1462
  },
884
- run: async (input) => {
885
- const value = input['lifecycle']?.trim().toLowerCase();
886
- if (value !== 'terminal' && value !== 'resident') {
887
- throw new InputError({ error: 'bad_lifecycle', message: `invalid lifecycle: ${value ?? ''}`, field: 'lifecycle', next: 'Pass `terminal` or `resident`.' });
888
- }
889
- const pane = input['pane'] ?? process.env['TMUX_PANE'];
890
- const id = resolveLifecycleNode(input, pane);
891
- const res = setLifecycle(id, value, { pane, detach: input['detach'] === true });
892
- return { node_id: res.node_id, lifecycle: res.lifecycle, detached: res.detached };
893
- },
894
- render: (r) => `Set ${r['node_id']} lifecycle → ${r['lifecycle']}${r['detached'] === true ? ', viewer closed (broker still running off-screen)' : ''}.`,
1463
+ children: [nodeRecycle, nodeClose, nodeDemote],
895
1464
  });
896
1465
  // ---------------------------------------------------------------------------
897
1466
  // node yield — refresh: discard context, revive fresh against the roadmap
@@ -935,395 +1504,38 @@ const nodeYield = defineLeaf({
935
1504
  },
936
1505
  });
937
1506
  // ---------------------------------------------------------------------------
938
- // node wakescheduled wakeups (arm / list / cancel). The agent-facing skin
939
- // over the wakeups data-access layer (armWake/listWakes/cancelWake) + the time
940
- // grammar (parseWhen/parseCadence). The surface ONLY arms a durable row and
941
- // introspects/cancels — it never spawns pi, drives a transition, or fires a wake
942
- // (those are the daemon's at fire time). T2's armWake carries only integrity
943
- // backstops (empty body / recur-on-deadline / unknown kind); the target-
944
- // resolvability, bare-recoverable-state, and per-owner cap checks live HERE
945
- // (Min-6), so every armer must route through this surface.
1507
+ // node triggersinspect/cancel pending triggers (clock + predicate) armed by
1508
+ // `node msg` or `node new`.
946
1509
  // ---------------------------------------------------------------------------
947
- /** Max pending wakes a single owner may hold (AC-N4). */
948
- const WAKE_CAP = 100;
949
- /** Default timeout body for a note-less `until`, so the deadline row is never
950
- * empty (T2 rejects an empty body) and Maj-8's rendered timeout signal has text. */
951
- const DEFAULT_DEADLINE_BODY = 'Deadline reached — no report arrived; reassess / chase / escalate.';
952
- /** Resolve the calling node (the armer/owner). CRTR_NODE_ID is mandatory — a
953
- * wake is owned by the node that arms it (owner_id). */
954
- function armerId() {
955
- const id = process.env['CRTR_NODE_ID'];
956
- if (id === undefined || id === '') {
957
- throw new InputError({ error: 'no_node', message: 'no node to arm a wake (CRTR_NODE_ID unset)', next: 'Run from inside a node.' });
958
- }
959
- return id;
960
- }
961
- /** Build ParseOpts, including `tz` only when actually provided. */
962
- function parseOpts(now, tz) {
963
- return tz !== undefined && tz.trim() !== '' ? { tz, now } : { now };
964
- }
965
- /** Map a T3 typed time-grammar error to the rendered AC-N3/N4 error block. */
966
- function throwWakeError(e) {
967
- const next = {
968
- wake_in_past: 'Pick a future instant — a positive duration ("5m","2h") or an ISO time later than now.',
969
- bad_when: 'Use a duration ("5m","1h30m"), a zoned ISO ("2026-06-07T09:00:00Z"), or a bare ISO ("2026-06-07T09:00").',
970
- bad_cadence: 'Use a duration ("6h"), a 5-field cron ("0 9 * * *"), or an @alias ("@daily").',
971
- unknown_zone: 'Pass --tz with an IANA zone name (e.g. "America/New_York").',
972
- cadence_too_fast: 'Use a cadence of at least 60s (e.g. "1m","5m","1h").',
973
- };
974
- throw new InputError({ error: e.code, message: e.message, received: e.received, next: next[e.code] ?? 'Fix the time value and retry.' });
975
- }
976
- /** Run armWake, mapping its thrown integrity backstop (WakeArmError) to a
977
- * rendered error block. The surface validates these cases up front, so a throw
978
- * here is a backstop, not the primary path. */
979
- function armOrThrow(spec) {
980
- try {
981
- return armWake(spec);
982
- }
983
- catch (e) {
984
- if (e instanceof WakeArmError) {
985
- const next = {
986
- empty_note: 'Provide a real --note, or omit it for a bare wake.',
987
- deadline_cannot_recur: 'Drop --every. For a recurring self-alarm use `crtr node wake at --every <cadence>`.',
988
- bad_kind: 'This is a crtr bug — report it.',
989
- };
990
- throw new InputError({ error: e.code, message: e.message, next: next[e.code] ?? 'Fix the wake spec and retry.' });
991
- }
992
- throw e;
993
- }
994
- }
995
- /** Reject an arm that would push this owner past the pending-wakes cap (AC-N4),
996
- * counted via the {owner} listWakes variant. */
997
- function assertUnderCap(ownerId) {
998
- const pending = listWakes({ owner: ownerId }).length;
999
- if (pending >= WAKE_CAP) {
1000
- throw new InputError({
1001
- error: 'cap_exceeded',
1002
- message: `you hold ${pending} pending wakes (cap ${WAKE_CAP}).`,
1003
- next: 'Reap stale wakes with `crtr node wake cancel <id>` (see `crtr node wake list`) before arming more.',
1004
- });
1005
- }
1006
- }
1007
- /** A bare wake resumes a fresh window with no memory beyond disk, so it needs
1008
- * durable state to wake INTO. Accept it only when the target has a goal
1009
- * (initial-prompt.md) or roadmap (roadmap.md) on disk — located via the
1010
- * codebase's own writeGoal/roadmap convention, not a new file (AC-N3). */
1011
- function hasRecoverableState(nodeId) {
1012
- const goal = readGoal(nodeId);
1013
- if (goal !== null && goal.trim() !== '')
1014
- return true;
1015
- const roadmap = readRoadmap(nodeId);
1016
- return roadmap !== null && roadmap.trim() !== '';
1017
- }
1018
- /** True when a stored recur is a fixed interval (vs a calendar cron). */
1019
- function isFixedInterval(recur) {
1020
- try {
1021
- return typeof JSON.parse(recur).every === 'string';
1022
- }
1023
- catch {
1024
- return false;
1025
- }
1026
- }
1027
- /** A human ETA hint like " (~5m)" from a fire-at ISO relative to now. '' if past. */
1028
- function etaHint(fireAt, now) {
1029
- const ms = new Date(fireAt).getTime() - now.getTime();
1030
- if (!Number.isFinite(ms) || ms <= 0)
1031
- return '';
1032
- const mins = Math.round(ms / 60_000);
1033
- if (mins < 60)
1034
- return ` (~${mins}m)`;
1035
- const hrs = Math.round(ms / 3_600_000);
1036
- if (hrs < 48)
1037
- return ` (~${hrs}h)`;
1038
- return ` (~${Math.round(ms / 86_400_000)}d)`;
1039
- }
1040
- // node wake at ---------------------------------------------------------------
1041
- const nodeWakeAt = defineLeaf({
1042
- name: 'at',
1043
- description: 'arm a self-alarm — wake yourself (or another node) at a future time',
1044
- whenToUse: 'you have a standing or recurring task on a CLOCK, or nothing to do until a known future time — run a job each morning, loop a health check on a cadence (--every), re-check an EXTERNAL poll (CI, a deploy, a rate-limit window) after a backoff, or hand your future self a timed reminder. Time-driven work ONLY: do NOT arm a timer to watch your own agents — you auto-subscribe to every child you spawn, so its finish already wakes you; setting a wake to "check if it is done yet" just burns a window the subscription would have woken anyway. The bare form is the time-triggered twin of `node yield`: a fresh window re-reading your roadmap, right for standing/recurring work; the noted form instead wakes you into your saved conversation with a pointer this moment needs. Use `node wake until` to put a single DEADLINE backstop on an inbox-wait (still event-first, not polling); `node wake spawn` to defer spawning a NEW node; `node yield` to refresh NOW rather than at a future T',
1045
- help: {
1046
- name: 'node wake at',
1047
- summary: 'arm a self-alarm: wake yourself (default) or another node (--node) at a future time. Bare ⇒ fresh window re-reading roadmap/disk; --note ⇒ saved conversation with the note as new context; --every ⇒ a declarative recurrence the runtime keeps firing even across your crash/finalize',
1048
- params: [
1049
- { kind: 'positional', name: 'when', required: false, constraint: 'Fire time — a duration ("5m","1h30m"), a zoned ISO ("2026-06-07T09:00:00Z"), or a bare ISO ("2026-06-07T09:00", host-local). Required UNLESS --every is given (the cadence then sets the first fire).' },
1050
- { kind: 'flag', name: 'note', type: 'string', required: false, constraint: 'A non-empty note delivered into the woken context. Present ⇒ NOTED wake (resume your saved conversation, note as new context). Absent ⇒ BARE wake (fresh window re-reading roadmap/disk). A bare wake on a node with no goal/roadmap on disk is rejected.' },
1051
- { kind: 'flag', name: 'every', type: 'string', required: false, constraint: 'Make it a declarative recurrence the runtime fires on schedule even if you crash/finalize (node-anchored revive-cron): a duration ("6h","30m") or a 5-field cron / @alias ("0 9 * * *","@daily"). A fixed-interval --every WITH <when> uses <when> as the first fire; a cron --every ignores <when>. Min cadence 60s.' },
1052
- { kind: 'flag', name: 'tz', type: 'string', required: false, constraint: 'IANA zone for a calendar --every or a bare-ISO <when> (default: host-local).' },
1053
- { kind: 'flag', name: 'node', type: 'string', required: false, constraint: 'Arm the wake for ANOTHER existing node (a parent waking a child / a timed message to it). Default: self (CRTR_NODE_ID).' },
1054
- ],
1055
- output: [
1056
- { name: 'id', type: 'string', required: true, constraint: 'The new wakeup id (cancel/list by it).' },
1057
- { name: 'kind', type: 'string', required: true, constraint: '"bare" or "noted".' },
1058
- { name: 'fires_at', type: 'string', required: true, constraint: 'Absolute UTC fire time (the first fire for a recurrence).' },
1059
- { name: 'recur', type: 'string', required: true, constraint: '"none", or the cadence (every 6h / cron `0 9 * * *` <zone>).' },
1060
- { name: 'target', type: 'string', required: true, constraint: '"self" or the --node id.' },
1061
- { name: 'guidance', type: 'string', required: true, constraint: 'What to do now — end your turn to go dormant; do not push final.' },
1062
- ],
1063
- outputKind: 'object',
1064
- effects: [
1065
- 'Inserts one wakeups row (kind bare/noted); nothing fires before fire_at.',
1066
- 'No pi spawn, no transition — arming is a pure durable side-effect. End your turn separately to go dormant.',
1067
- ],
1068
- },
1069
- run: async (input) => {
1070
- const ownerId = armerId();
1071
- const targetId = (input['node'] ?? '').trim() || ownerId;
1072
- if (getNode(targetId) === null) {
1073
- throw new InputError({ error: 'not_found', message: `no node: ${targetId}`, field: 'node', next: 'List nodes with `crtr node inspect list`.' });
1074
- }
1075
- const when = input['when']?.trim();
1076
- const every = input['every']?.trim();
1077
- const tz = input['tz'];
1078
- const noteRaw = input['note'];
1079
- const hasNote = noteRaw !== undefined;
1080
- if ((when === undefined || when === '') && (every === undefined || every === '')) {
1081
- throw new InputError({ error: 'bad_when', message: 'a <when> time is required (or --every for a recurrence).', next: 'Pass a duration ("5m"), an ISO time, or --every <cadence>.' });
1082
- }
1083
- if (hasNote && noteRaw.trim() === '') {
1084
- throw new InputError({ error: 'empty_note', message: '--note must be non-empty for a noted wake.', received: noteRaw, next: 'Provide a real note ("re-check CI #4821; deploy was pending"), or omit --note for a bare wake.' });
1085
- }
1086
- const kind = hasNote ? 'noted' : 'bare';
1087
- if (kind === 'bare' && !hasRecoverableState(targetId)) {
1088
- throw new InputError({ error: 'bare_no_recoverable_state', message: `node ${targetId} has no goal/roadmap on disk — a bare wake would resume amnesiac.`, next: 'Pass --note "<why this moment matters>" so the woken context carries a pointer, or arm the bare wake only once the node has a roadmap/goal.' });
1089
- }
1090
- const now = new Date();
1091
- let recur;
1092
- let fireAt;
1093
- if (every !== undefined && every !== '') {
1094
- const cad = parseCadence(every, parseOpts(now, tz));
1095
- if ('error' in cad)
1096
- throwWakeError(cad.error);
1097
- recur = cad.recur;
1098
- fireAt = cad.firstFireAt;
1099
- // BOTH <when> and a FIXED-interval --every: <when> overrides the first fire
1100
- // (Min-12). A cron --every ignores <when> by design.
1101
- if (when !== undefined && when !== '' && isFixedInterval(cad.recur)) {
1102
- const w = parseWhen(when, parseOpts(now, tz));
1103
- if ('error' in w)
1104
- throwWakeError(w.error);
1105
- fireAt = w.fireAt;
1106
- }
1107
- }
1108
- else {
1109
- const w = parseWhen(when, parseOpts(now, tz));
1110
- if ('error' in w)
1111
- throwWakeError(w.error);
1112
- fireAt = w.fireAt;
1113
- }
1114
- assertUnderCap(ownerId);
1115
- let payload = null;
1116
- if (kind === 'noted') {
1117
- const body = noteRaw;
1118
- payload = { body, label: body.split('\n')[0].slice(0, 120) };
1119
- }
1120
- const id = `wk-${newNodeId()}`;
1121
- armOrThrow({
1122
- wakeup_id: id,
1123
- node_id: targetId,
1124
- owner_id: ownerId,
1125
- fire_at: fireAt,
1126
- kind,
1127
- ...(recur !== undefined ? { recur } : {}),
1128
- payload,
1129
- });
1130
- const target = targetId === ownerId ? 'self' : targetId;
1131
- const eta = etaHint(fireAt, now);
1132
- const guidance = recur !== undefined
1133
- ? `${kind === 'noted' ? 'Noted' : 'Bare'} recurrence armed (${cadenceDisplay(recur)}); first fire ${fireAt}${eta}. End your turn to go dormant; do not push final. The runtime keeps firing it even across your crash/finalize — cancel with \`crtr node wake cancel ${id}\`.`
1134
- : kind === 'noted'
1135
- ? `Noted wake armed. You wake into your saved conversation at ${fireAt}${eta} with your note. End your turn now to go dormant; do not push final.`
1136
- : `Bare self-alarm armed. You wake in a fresh window at ${fireAt}${eta}. End your turn now to go dormant; do not push final. The wake re-reads your roadmap.`;
1137
- return { id, kind, fires_at: fireAt, recur: cadenceDisplay(recur), target, guidance };
1138
- },
1139
- render: (r) => {
1140
- const recur = r['recur'] !== 'none' ? `, recurs ${r['recur']}` : '';
1141
- return `Armed ${r['kind']} wake ${r['id']} — fires ${r['fires_at']}${recur}, target ${r['target']}.\n\n${r['guidance']}`;
1142
- },
1143
- });
1144
- // node wake until ------------------------------------------------------------
1145
- const nodeWakeUntil = defineLeaf({
1146
- name: 'until',
1147
- description: 'bind a deadline to your current inbox-wait (self only)',
1148
- whenToUse: 'rarely — only to put a deadline on an inbox-wait whose event the runtime genuinely cannot guarantee to deliver: you are open to a message AND backstopping an UNPUSHABLE external (a human who may never reply, an outside system with no spine-push). NOT for delegates — a child finishing, crashing, or being closed already wakes you on its own, so a deadline to "chase" a delegate is exactly the belt-and-suspenders the runtime makes redundant. Use `node wake at` instead for an unconditional timed wake unrelated to an inbox event',
1149
- help: {
1150
- name: 'node wake until',
1151
- summary: 'arm a deadline on your current dormancy: you wake on the first inbox message, or at <when> if none arrives — whichever fires first wins, the loser is canceled. Self only; ≤1 deadline per node (a new `until` replaces the prior); cancel-on-wake (any genuine revive drops it)',
1152
- params: [
1153
- { kind: 'positional', name: 'when', required: true, constraint: 'Deadline time — a duration ("30m"), a zoned ISO, or a bare ISO (host-local).' },
1154
- { kind: 'flag', name: 'note', type: 'string', required: false, constraint: 'Your timeout playbook — what to do if you woke because the wait expired (delivered with a timeout marker so you can tell a timeout from a real report). Omit and a default timeout note is supplied.' },
1155
- { kind: 'flag', name: 'every', type: 'string', required: false, constraint: 'NOT ALLOWED — a deadline cannot recur. Passing it is rejected (deadline_cannot_recur); use `crtr node wake at --every` for a recurring self-alarm.' },
1156
- ],
1157
- output: [
1158
- { name: 'id', type: 'string', required: true, constraint: 'The new deadline wakeup id.' },
1159
- { name: 'fires_at', type: 'string', required: true, constraint: 'Absolute UTC deadline.' },
1160
- { name: 'target', type: 'string', required: true, constraint: 'Always "self".' },
1161
- { name: 'guidance', type: 'string', required: true, constraint: 'What to do now — delegate / end your turn to go dormant.' },
1162
- ],
1163
- outputKind: 'object',
1164
- effects: [
1165
- "Upserts the node's single deadline wakeups row (replacing any prior).",
1166
- 'No pi spawn, no transition — arm, then end your turn to go dormant. A genuine revive (an inbox event or any other wake) cancels it.',
1167
- ],
1168
- },
1169
- run: async (input) => {
1170
- const ownerId = armerId();
1171
- const every = input['every']?.trim();
1172
- if (every !== undefined && every !== '') {
1173
- throw new InputError({ error: 'deadline_cannot_recur', message: 'a deadline cannot recur.', next: 'Drop --every. For a recurring self-alarm use `crtr node wake at --every <cadence>`.' });
1174
- }
1175
- const when = input['when'].trim();
1176
- const noteRaw = input['note'];
1177
- if (noteRaw !== undefined && noteRaw.trim() === '') {
1178
- throw new InputError({ error: 'empty_note', message: '--note must be non-empty when given.', received: noteRaw, next: 'Provide a real timeout playbook, or omit --note for the default timeout note.' });
1179
- }
1180
- const now = new Date();
1181
- const w = parseWhen(when, { now });
1182
- if ('error' in w)
1183
- throwWakeError(w.error);
1184
- assertUnderCap(ownerId);
1185
- const body = noteRaw !== undefined && noteRaw.trim() !== '' ? noteRaw : DEFAULT_DEADLINE_BODY;
1186
- const label = body.split('\n')[0].slice(0, 120);
1187
- const payload = { body, timeout: true, label };
1188
- const id = `wk-${newNodeId()}`;
1189
- armOrThrow({ wakeup_id: id, node_id: ownerId, owner_id: ownerId, fire_at: w.fireAt, kind: 'deadline', payload });
1190
- const eta = etaHint(w.fireAt, now);
1191
- const guidance = `Deadline armed. You wake on the first inbox message, or at ${w.fireAt}${eta} if none arrives. Now delegate / end your turn to go dormant — whichever fires first cancels the other.`;
1192
- return { id, fires_at: w.fireAt, target: 'self', guidance };
1193
- },
1194
- render: (r) => `Armed deadline ${r['id']} — fires ${r['fires_at']}, target ${r['target']}.\n\n${r['guidance']}`,
1195
- });
1196
- // node wake spawn -----------------------------------------------------------
1197
- const nodeWakeSpawn = defineLeaf({
1198
- name: 'spawn',
1199
- description: 'defer or recur the BIRTH of a new node',
1200
- whenToUse: 'you want a node spawned LATER or on a repeating cadence rather than right now: a one-shot deferred birth at a future time (--at), or a spawn-cron that re-births a fresh node every interval/cron even across your crash/finalize (--every) — standing work like a nightly review, a periodic health check, a morning digest, or a timed reminder that acts. Use `node new` instead to spawn immediately, and `node wake at` to re-wake an EXISTING node on a timer rather than birth a new one',
1201
- help: {
1202
- name: 'node wake spawn',
1203
- summary: 'defer or recur a node birth — arm a wake that spawns a fresh node at a future time (--at) or on a repeating cadence (--every), instead of `node new` spawning it now. Returns a wakeup id, not a node id; the full spawn recipe is stored on the wake and re-derived live at fire time',
1204
- params: [
1205
- { kind: 'stdin', name: 'prompt', required: true, constraint: 'First user message for the node to be born. Deliver it on stdin from a quoted heredoc (`<<\'EOF\'`) or a file (`< prompt.md`).' },
1206
- { kind: 'flag', name: 'at', type: 'string', required: false, constraint: 'DEFER a one-shot birth at <when> — a duration ("5m"), a zoned ISO, or a bare ISO (host-local, or in --tz). Mutually exclusive with --every; exactly one of --at/--every is required.' },
1207
- { kind: 'flag', name: 'every', type: 'string', required: false, constraint: 'SPAWN-CRON: re-birth a fresh node on this declarative cadence even after a prior instance reaped itself or crashed — a duration ("6h") or a 5-field cron / @alias ("0 9 * * *","@daily"). Canvas-anchored (survives your crash/finalize), reaped by your deliberate close or `node wake cancel`. Min cadence 60s. Mutually exclusive with --at.' },
1208
- { kind: 'flag', name: 'tz', type: 'string', required: false, constraint: 'IANA zone for a calendar --every (or a bare-ISO --at); default host-local. Makes "every 9am" mean 9am there, DST-correct.' },
1209
- { kind: 'flag', name: 'kind', type: 'string', required: false, default: 'general', constraint: 'Persona kind for the node to be born — match the work to the kind (the <kinds> list below names each).' },
1210
- { kind: 'flag', name: 'mode', type: 'enum', choices: ['base', 'orchestrator'], required: false, default: 'base', constraint: 'Persona mode: base (finishes in one window) or orchestrator (boots with a seeded roadmap and fans its scope out).' },
1211
- { kind: 'flag', name: 'cwd', type: 'path', required: false, constraint: 'Dir the born node is pinned to. Defaults to the caller cwd, resolved NOW — it must still exist at fire time or the spawn fails loud.' },
1212
- { kind: 'flag', name: 'name', type: 'string', required: false, constraint: 'Display name for the born node (tmux window + resume picker). Defaults to the kind.' },
1213
- { kind: 'flag', name: 'parent', type: 'string', required: false, constraint: 'Parent node id for the born node. Defaults to the calling node (CRTR_NODE_ID).' },
1214
- { kind: 'flag', name: 'root', type: 'bool', required: false, constraint: 'Birth an INDEPENDENT root instead of a managed child: no parent on the spine, NO subscription back to you, resident lifecycle (records spawned_by=you for provenance).' },
1215
- { kind: 'flag', name: 'fork-from', type: 'string', required: false, constraint: 'Fork the born node from an existing pi conversation instead of starting it fresh: a node id, an absolute session `.jsonl` path, or a partial pi session uuid. pi copies that history into a NEW session, then the prompt is the next message. One-shot at birth.' },
1216
- ],
1217
- output: [
1218
- { name: 'id', type: 'string', required: true, constraint: 'The wakeup id (inspect/cancel by it via `node wake list`/`cancel`).' },
1219
- { name: 'kind', type: 'string', required: true, constraint: 'The persona kind of the deferred node.' },
1220
- { name: 'fires_at', type: 'string', required: true, constraint: 'Absolute UTC birth time (the first fire for a cron).' },
1221
- { name: 'recur', type: 'string', required: true, constraint: '"none" (one-shot --at) or the cadence (every 6h / cron `0 9 * * *` <zone>).' },
1222
- { name: 'guidance', type: 'string', required: true, constraint: 'What to do now — no node exists yet; pick up other work or end your turn.' },
1223
- ],
1224
- dynamicState: () => kindsStateBlock(),
1225
- outputKind: 'object',
1226
- effects: [
1227
- 'Inserts one detached `spawn` wakeups row (node_id NULL, owner=you, parent/cwd resolved now); NO node and NO window exist until fire time.',
1228
- 'At fire time the daemon spawns the node from the stored recipe, re-deriving the launch spec live (persona prose is never stale). Best-effort: if the cwd/parent is gone at fire it fails LOUD (an urgent push to you, or a daemon-log line if you are gone).',
1229
- ],
1230
- },
1231
- run: async (input) => {
1232
- const prompt = input['prompt'] ?? '';
1233
- if (prompt.trim() === '') {
1234
- throw new InputError({ error: 'empty_prompt', message: 'a prompt is required (stdin or positional)', next: 'Pipe a task on stdin or pass it as an argument.' });
1235
- }
1236
- const at = input['at']?.trim();
1237
- const every = input['every']?.trim();
1238
- const tz = input['tz'];
1239
- const hasAt = at !== undefined && at !== '';
1240
- const hasEvery = every !== undefined && every !== '';
1241
- if (!hasAt && !hasEvery) {
1242
- throw new InputError({ error: 'no_schedule', message: 'node wake spawn needs a schedule: --at <when> or --every <cadence>.', next: 'Pass --at for a one-shot deferred spawn, --every for a spawn-cron, or use `crtr node new` to spawn now.' });
1243
- }
1244
- if (hasAt && hasEvery) {
1245
- throw new InputError({ error: 'at_and_every', message: '--at and --every are mutually exclusive.', next: 'Use --at for a one-shot deferred spawn, or --every for a spawn-cron.' });
1246
- }
1247
- const kind = input['kind'] ?? 'general';
1248
- const mode = (input['mode'] ?? 'base');
1249
- const cwd = input['cwd'] ?? process.cwd();
1250
- const name = input['name'];
1251
- const parent = input['parent'];
1252
- const root = input['root'] === true;
1253
- const forkFrom = input['forkFrom'];
1254
- const ownerId = armerId();
1255
- // The recipe's `parent` is the resolved armer — NON-NULL on EVERY payload,
1256
- // INCLUDING --root: spawnChild throws at fire time on a null parent (the
1257
- // daemon has no CRTR_NODE_ID); for a root it internally nulls the spine
1258
- // parent while keeping `spawner` for provenance, so a non-null value is right.
1259
- const recipeParent = parent ?? ownerId;
1260
- const recipe = {
1261
- kind,
1262
- mode,
1263
- cwd,
1264
- prompt,
1265
- parent: recipeParent,
1266
- ...(name !== undefined ? { name } : {}),
1267
- ...(root ? { root: true } : {}),
1268
- ...(forkFrom !== undefined ? { forkFrom } : {}),
1269
- };
1270
- const now = new Date();
1271
- let recur;
1272
- let fireAt;
1273
- if (hasEvery) {
1274
- const cad = parseCadence(every, parseOpts(now, tz));
1275
- if ('error' in cad)
1276
- throwWakeError(cad.error);
1277
- recur = cad.recur;
1278
- fireAt = cad.firstFireAt;
1279
- }
1280
- else {
1281
- const w = parseWhen(at, parseOpts(now, tz));
1282
- if ('error' in w)
1283
- throwWakeError(w.error);
1284
- fireAt = w.fireAt;
1285
- }
1286
- assertUnderCap(ownerId);
1287
- const id = `wk-${newNodeId()}`;
1288
- armOrThrow({ wakeup_id: id, node_id: null, owner_id: ownerId, fire_at: fireAt, kind: 'spawn', ...(recur !== undefined ? { recur } : {}), payload: recipe });
1289
- const eta = etaHint(fireAt, now);
1290
- const guidance = recur !== undefined
1291
- ? `Spawn-cron armed (${cadenceDisplay(recur)}): a fresh ${kind} node is born each fire, first at ${fireAt}${eta}. No node exists yet; the runtime keeps spawning even across your crash/finalize — inspect/cancel via \`crtr node wake list\` / \`crtr node wake cancel ${id}\`.`
1292
- : `Deferred spawn armed: a fresh ${kind} node is born at ${fireAt}${eta}. No node exists yet — inspect/cancel via \`crtr node wake list\` / \`crtr node wake cancel ${id}\`. Pick up other work or end your turn.`;
1293
- return { id, kind, fires_at: fireAt, recur: cadenceDisplay(recur), guidance };
1294
- },
1295
- render: (r) => {
1296
- const recur = r['recur'] !== 'none' ? `, recurs ${r['recur']}` : '';
1297
- return `Armed deferred spawn ${r['id']} — a ${r['kind']} node, fires ${r['fires_at']}${recur}.\n\n${r['guidance']}`;
1298
- },
1299
- });
1300
- // node wake list -------------------------------------------------------------
1301
- const nodeWakeList = defineLeaf({
1510
+ const nodeTriggersList = defineLeaf({
1302
1511
  name: 'list',
1303
- description: 'list pending wakes for a scope (default self)',
1304
- whenToUse: 'before re-arming or finishing, to see what you already have armed and reap stale ones. Defaults to your own wakes, with scopes for another node, the whole canvas, or a node and its descendants',
1512
+ description: 'list pending triggers for a scope (default self)',
1513
+ whenToUse: 'before re-arming or finishing, to see what you already have armed and reap stale ones. Defaults to your own triggers, with a --node or --canvas scope for anything wider',
1305
1514
  help: {
1306
- name: 'node wake list',
1307
- summary: 'list pending wakes (all kinds, incl. deferred spawns) for a scope — id, kind, next fire, cadence, target, owner, note. Fired one-shots are gone; a recurrence shows its NEXT fire, not past slots',
1515
+ name: 'node triggers list',
1516
+ summary: 'list pending triggers (clock and predicate, incl. deferred spawns) for a scope — id, owner, target, action/mode, type, next fire/eval, cadence, timeout, label, predicate diagnostics. Fired one-shots are gone; a recurrence shows its NEXT fire/eval, not past slots',
1308
1517
  params: [
1309
- { kind: 'flag', name: 'node', type: 'string', required: false, constraint: 'List the wakes anchored to this node. Mutually exclusive with --canvas/--subtree.' },
1310
- { kind: 'flag', name: 'canvas', type: 'bool', required: false, constraint: 'List EVERY wake on the canvas. Mutually exclusive with --node/--subtree.' },
1311
- { kind: 'flag', name: 'subtree', type: 'string', required: false, constraint: 'List the wakes anchored to this node AND its descendants (the subscription sub-DAG). Mutually exclusive with --node/--canvas.' },
1518
+ { kind: 'flag', name: 'node', type: 'string', required: false, constraint: 'List the triggers owned by this node instead of yourself. Mutually exclusive with --canvas.' },
1519
+ { kind: 'flag', name: 'canvas', type: 'bool', required: false, constraint: 'List EVERY trigger on the canvas. Mutually exclusive with --node.' },
1312
1520
  ],
1313
1521
  output: [
1314
- { name: 'scope', type: 'string', required: true, constraint: 'The scope listed (self / <id> / canvas / subtree:<id>).' },
1315
- { name: 'wakes', type: 'object[]', required: true, constraint: 'Rows: {id, kind, next, recur, target, owner, note}.' },
1522
+ { name: 'scope', type: 'string', required: true, constraint: 'The scope listed (self / <id> / canvas).' },
1523
+ {
1524
+ name: 'triggers',
1525
+ type: 'object[]',
1526
+ required: true,
1527
+ constraint: 'Rows: {id, owner, target, action ( "msg"|"new" ), action_mode, trigger_type ( "at"|"every"|"when"|"until" ), next (fire_at or next_eval_at), recur (cadence display), timeout (timeout_at, predicate only), label, eval_state, last_eval_at, last_exit, last_stderr, created}.',
1528
+ },
1316
1529
  ],
1317
1530
  outputKind: 'object',
1318
- effects: ['Read-only: queries the wakeups table.'],
1531
+ effects: ['Read-only: queries the triggers table.'],
1319
1532
  },
1320
1533
  run: async (input) => {
1321
1534
  const nodeFlag = input['node']?.trim();
1322
1535
  const canvas = input['canvas'] === true;
1323
- const subtree = input['subtree']?.trim();
1324
- const chosen = [nodeFlag !== undefined && nodeFlag !== '', canvas, subtree !== undefined && subtree !== ''].filter(Boolean).length;
1325
- if (chosen > 1) {
1326
- throw new InputError({ error: 'bad_scope', message: 'choose at most one of --node, --canvas, --subtree.', next: 'Pass a single scope flag, or none for your own wakes.' });
1536
+ const hasNode = nodeFlag !== undefined && nodeFlag !== '';
1537
+ if (hasNode && canvas) {
1538
+ throw new InputError({ error: 'bad_scope', message: 'choose at most one of --node, --canvas.', next: 'Pass a single scope flag, or none for your own triggers.' });
1327
1539
  }
1328
1540
  const viewer = process.env['CRTR_NODE_ID'];
1329
1541
  let scope;
@@ -1332,99 +1544,95 @@ const nodeWakeList = defineLeaf({
1332
1544
  scope = { canvas: true };
1333
1545
  scopeLabel = 'canvas';
1334
1546
  }
1335
- else if (subtree !== undefined && subtree !== '') {
1336
- if (getNode(subtree) === null)
1337
- throw new InputError({ error: 'not_found', message: `no node: ${subtree}`, field: 'subtree', next: 'List nodes with `crtr node inspect list`.' });
1338
- scope = { subtree: [subtree, ...view(subtree)] };
1339
- scopeLabel = `subtree:${subtree}`;
1340
- }
1341
- else if (nodeFlag !== undefined && nodeFlag !== '') {
1342
- scope = { node: nodeFlag };
1547
+ else if (hasNode) {
1548
+ if (getNode(nodeFlag) === null)
1549
+ throw new InputError({ error: 'not_found', message: `no node: ${nodeFlag}`, field: 'node', next: 'List nodes with `crtr node inspect list`.' });
1550
+ scope = { owner: nodeFlag };
1343
1551
  scopeLabel = viewer !== undefined && nodeFlag === viewer ? 'self' : nodeFlag;
1344
1552
  }
1345
1553
  else {
1346
- // Default "self" scope is OWNER-based, not node-anchored: it is "what YOU
1347
- // armed" (§3.5 whenToUse), so the detached spawn wakes you own via
1348
- // `node new --at/--every` (node_id NULL, owner_id self) DO surface here
1349
- // with their `spawn:<kind>@<cwd>` target — §3.5/§3.7 require that. (Use
1350
- // --node <id> for the node-ANCHORED view of a target's wakes.)
1351
1554
  scope = { owner: armerId() };
1352
1555
  scopeLabel = 'self';
1353
1556
  }
1354
- const rel = (id) => {
1355
- if (id === null)
1356
- return '';
1357
- return viewer !== undefined && viewer !== '' && id === viewer ? 'self' : id;
1358
- };
1359
- const wakes = listWakes(scope).map((w) => {
1557
+ const rel = (id) => (id !== null && viewer !== undefined && viewer !== '' && id === viewer ? 'self' : (id ?? ''));
1558
+ const triggers = listTriggers(scope).map((t) => {
1360
1559
  let target;
1361
- if (w.node_id !== null) {
1362
- target = rel(w.node_id);
1560
+ if (t.node_id !== null) {
1561
+ target = rel(t.node_id);
1363
1562
  }
1364
- else if (w.kind === 'spawn' && w.payload !== null) {
1365
- const recipe = w.payload;
1563
+ else if (t.action === 'new' && t.payload !== null) {
1564
+ const recipe = t.payload;
1366
1565
  target = `spawn:${recipe.kind}@${recipe.cwd}`;
1367
1566
  }
1368
1567
  else {
1369
1568
  target = 'detached';
1370
1569
  }
1371
- const note = (w.kind === 'noted' || w.kind === 'deadline') && w.payload !== null
1372
- ? (w.payload.label ?? '')
1373
- : '';
1374
- return { id: w.wakeup_id, kind: w.kind, next: w.fire_at, recur: cadenceDisplay(w.recur), target, owner: rel(w.owner_id), note };
1570
+ const label = t.payload !== null && typeof t.payload.label === 'string' ? t.payload.label : '';
1571
+ return {
1572
+ id: t.trigger_id,
1573
+ owner: rel(t.owner_id),
1574
+ target,
1575
+ action: t.action,
1576
+ action_mode: t.action_mode,
1577
+ trigger_type: t.trigger_type,
1578
+ next: t.trigger_type === 'when' ? (t.next_eval_at ?? '') : (t.fire_at ?? ''),
1579
+ recur: cadenceDisplay(t.trigger_type === 'when' ? t.eval_every : t.recur),
1580
+ timeout: t.timeout_at ?? '',
1581
+ label,
1582
+ eval_state: t.eval_state,
1583
+ last_eval_at: t.last_eval_at ?? '',
1584
+ last_exit: t.last_exit,
1585
+ last_stderr: t.last_stderr ?? '',
1586
+ created: t.created,
1587
+ };
1375
1588
  });
1376
- return { scope: scopeLabel, wakes };
1589
+ return { scope: scopeLabel, triggers };
1377
1590
  },
1378
1591
  render: (r) => {
1379
- const wakes = r['wakes'];
1380
- const follow = 'Cancel one with `crtr node wake cancel <id>`. Fired one-shots are gone; a recurrence shows its NEXT fire, not past slots.';
1381
- if (wakes.length === 0)
1382
- return `No pending wakes in scope ${r['scope']}.\n\n${follow}`;
1383
- const cell = (v) => String(v).replace(/\|/g, '\\|').replace(/\n/g, ' ');
1384
- const head = '| id | kind | next | recur | target | owner | note |\n| --- | --- | --- | --- | --- | --- | --- |';
1385
- const rows = wakes.map((w) => `| ${cell(w['id'])} | ${cell(w['kind'])} | ${cell(w['next'])} | ${cell(w['recur'])} | ${cell(w['target'])} | ${cell(w['owner'])} | ${cell(w['note'])} |`);
1386
- return `${wakes.length} pending wake(s) in scope ${r['scope']}.\n\n${head}\n${rows.join('\n')}\n\n${follow}`;
1592
+ const triggers = r['triggers'];
1593
+ const follow = 'Cancel one with `crtr node triggers cancel <id>`. Fired one-shots are gone; a recurrence shows its NEXT fire/eval, not past slots.';
1594
+ if (triggers.length === 0)
1595
+ return `No pending triggers in scope ${r['scope']}.\n\n${follow}`;
1596
+ const cell = (v) => String(v ?? '').replace(/\|/g, '\\|').replace(/\n/g, ' ');
1597
+ const head = '| id | owner | target | action | mode | type | next | recur | timeout | label | eval_state | last_eval_at | last_exit | last_stderr | created |\n| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |';
1598
+ const rows = triggers.map((t) => `| ${cell(t['id'])} | ${cell(t['owner'])} | ${cell(t['target'])} | ${cell(t['action'])} | ${cell(t['action_mode'])} | ${cell(t['trigger_type'])} | ${cell(t['next'])} | ${cell(t['recur'])} | ${cell(t['timeout'])} | ${cell(t['label'])} | ${cell(t['eval_state'])} | ${cell(t['last_eval_at'])} | ${cell(t['last_exit'])} | ${cell(t['last_stderr'])} | ${cell(t['created'])} |`);
1599
+ return `${triggers.length} pending trigger(s) in scope ${r['scope']}.\n\n${head}\n${rows.join('\n')}\n\n${follow}`;
1387
1600
  },
1388
1601
  });
1389
- // node wake cancel -----------------------------------------------------------
1390
- const nodeWakeCancel = defineLeaf({
1602
+ const nodeTriggersCancel = defineLeaf({
1391
1603
  name: 'cancel',
1392
- description: 'cancel a pending wake by id (idempotent)',
1393
- whenToUse: 'a wait you no longer need — a poll whose goal is met, a deferred spawn you reconsidered, a deadline you are replacing. Idempotent: canceling an already-fired or already-canceled id is a no-op. Closing/reaping a node already reaps its own wakes and the detached ones it armed; reach for this to drop a single wake, or to reap a detached spawn/cron a finished or crashed node left running',
1604
+ description: 'cancel a pending trigger by id (idempotent)',
1605
+ whenToUse: 'a wait you no longer need — a poll whose goal is met, a deferred spawn you reconsidered, a deadline you are replacing. Idempotent: canceling an already-fired or already-canceled id is a no-op. Closing/reaping a node already reaps its own triggers and the detached ones it armed; reach for this to drop a single trigger, or to reap a detached spawn/cron a finished or crashed node left running',
1394
1606
  help: {
1395
- name: 'node wake cancel',
1396
- summary: 'cancel a pending wake by id — it never fires and leaves the list. Idempotent (canceling an already-gone id is a no-op, not an error)',
1397
- params: [
1398
- { kind: 'positional', name: 'wakeup-id', required: true, constraint: 'The wakeup id to cancel (from `node wake list`).' },
1399
- ],
1607
+ name: 'node triggers cancel',
1608
+ summary: 'cancel a pending trigger by id — it never fires/evaluates again and leaves the list. Idempotent (canceling an already-gone id is a no-op, not an error)',
1609
+ params: [{ kind: 'positional', name: 'trigger-id', required: true, constraint: 'The trigger id to cancel (from `node triggers list`).' }],
1400
1610
  output: [
1401
- { name: 'id', type: 'string', required: true, constraint: 'The canceled wakeup id.' },
1611
+ { name: 'id', type: 'string', required: true, constraint: 'The canceled trigger id.' },
1402
1612
  { name: 'was_pending', type: 'boolean', required: true, constraint: 'True when a pending row was removed; false when it was already gone.' },
1403
1613
  ],
1404
1614
  outputKind: 'object',
1405
- effects: ['Deletes the wakeup row (idempotent — no error if it was already gone).'],
1615
+ effects: ['Deletes the trigger row (idempotent — no error if already gone).'],
1406
1616
  },
1407
1617
  run: async (input) => {
1408
- const id = input['wakeupId'].trim();
1409
- const wasPending = listWakes({ canvas: true }).some((w) => w.wakeup_id === id);
1410
- cancelWake(id);
1618
+ const id = input['triggerId'].trim();
1619
+ const wasPending = listTriggers({ canvas: true }).some((t) => t.trigger_id === id);
1620
+ cancelTrigger(id);
1411
1621
  return { id, was_pending: wasPending };
1412
1622
  },
1413
- render: (r) => `Canceled wake ${r['id']}${r['was_pending'] === true ? '' : ' (already gone — no-op)'}.`,
1623
+ render: (r) => `Canceled trigger ${r['id']}${r['was_pending'] === true ? '' : ' (already gone — no-op)'}.`,
1414
1624
  });
1415
- // node wake (branch) ---------------------------------------------------------
1416
- const nodeWake = defineBranch({
1417
- name: 'wake',
1418
- description: 'arm/list/cancel scheduled wakeups the second trigger that stirs a dormant node: time',
1419
- whenToUse: 'you want to schedule work on a CLOCK — a long-horizon or recurring task that fires at a future time or loops on a cadence: a standing job, a recurring cron, a deferred or repeating node birth, or a poll against EXTERNAL state (CI, a deploy, a clock) after a backoff. This is the TIME trigger, NOT a way to watch your own agents — to monitor a child you arm NOTHING: you auto-subscribe on spawn, so its finish, crash, or close wakes you on its own; a deadline to chase a delegate is the belt-and-suspenders the runtime makes redundant. `at` arms a self-alarm (the timed twin of `node yield`), `until` bounds an inbox-wait with a deadline, `spawn` defers or repeats the BIRTH of a new node, `list`/`cancel` inspect and reap',
1625
+ const nodeTriggers = defineBranch({
1626
+ name: 'triggers',
1627
+ description: 'inspect/cancel pending triggers (clock + predicate)',
1628
+ whenToUse: 'an agent wants to see/reap its own or the canvas\'s pending triggers clock or predicate — armed by `node msg` or `node new`',
1420
1629
  help: {
1421
- name: 'node wake',
1422
- summary: 'the pending-wakeups namespace — arm a time trigger on the dormant state, then inspect/cancel it',
1423
- model: 'Time is the second trigger that stirs a dormant node — the first is an inbox message, and a scheduled wake is just a future delivery on that same channel. At the moment you set, the runtime brings you (or, for a deferred birth, a fresh node) back through the ordinary revive path: no new window, nothing the focus model learns. Reach for it to schedule CLOCK-driven work — a standing job, a recurring loop (--every), a deferred birth, or a poll against EXTERNAL state after a backoff. It is NOT how you watch your own agents: you auto-subscribe to every child you spawn, so its finish already wakes you — arm nothing and stay dormant rather than set a timer to "check if it is done" — a child finishing, crashing, or being closed all wake you on their own. (`until` is a narrow exception: a deadline backstop on an inbox-wait the runtime CANNOT guarantee to deliver — an unpushable external, never a delegate, whose outcome already wakes you.) Use this to wait CHEAPLY — end your turn, go dormant (free: no window, no compute), and be woken only when it is worth acting again. `at` arms a self-alarm; `until` bounds an inbox-wait with a deadline; `spawn` defers or repeats the BIRTH of a new node (the full recipe stored on the wake); `list`/`cancel` inspect and reap. Arming is a pure side-effect: it writes a durable row, it does NOT end your turn — end your turn separately to go dormant.',
1630
+ name: 'node triggers',
1631
+ summary: 'the pending-triggers namespace — inspect or cancel a trigger armed by `node msg` or `node new`',
1424
1632
  },
1425
- children: [nodeWakeAt, nodeWakeUntil, nodeWakeSpawn, nodeWakeList, nodeWakeCancel],
1633
+ children: [nodeTriggersList, nodeTriggersCancel],
1426
1634
  });
1427
- // ── /pyramid slash command ───────────────────────────────────────────────────
1635
+ // ── /pyramid slash command ──────────────────────────────────────────────────────────────────────────────
1428
1636
  // Rides on the `node` subtree, not because pyramid is a `node` verb, but because
1429
1637
  // (1) the workflow IS node orchestration — it fans out explore agents — and
1430
1638
  // (2) provisionExports lazy-loads only the invoked subtree (build-root.ts), so a
@@ -1517,16 +1725,16 @@ export function registerNode() {
1517
1725
  rootEntry: {
1518
1726
  concept: 'a unit of the agent runtime — an agent on the canvas with its own context dir and pi vehicle. Spawning nodes is how work gets done here, not an optimization you reach for late',
1519
1727
  desc: 'spawn, inspect, and navigate nodes on the canvas',
1520
- useWhen: 'delegating work, or walking the subscription graph. CRITICAL, and your default reflex — spawning a child is the first move on almost any task, not a fallback. Your own context window is the scarce resource: before you read files into it to understand, implement, debug, or change code you do not already know, hand that to a fresh node and get back just the conclusion, matching the node kind to the work. Keep inline only a fact you already know. Fan out independent units as CONCURRENT children — a wake with idle workers is wasted; serialize only true dependencies, and never let two live children edit the same files. Once you delegate a unit, do not also run it yourself: you auto-subscribe on spawn, so its finish wakes you. Spawn + collect mechanics: `crtr node -h`.',
1728
+ useWhen: 'delegating work, or walking the subscription graph. CRITICAL, and your default reflex — spawning a child is the first move on almost any task, not a fallback. Your own context window is the scarce resource: before you read files into it to understand, implement, debug, or change code you do not already know, hand that to a fresh node and get back just the conclusion, matching the node kind to the work. Keep inline only a fact you already know. Fan out independent units as CONCURRENT children — a wake with idle workers is wasted; serialize only true dependencies, and never let two live children edit the same files. Once you delegate a unit, do not also run it yourself: you auto-subscribe on spawn, so its finish wakes you. Use the help surface for the exact node contract before invoking it.'
1521
1729
  },
1522
1730
  help: {
1523
1731
  name: 'node',
1524
1732
  summary: 'spawn and navigate canvas nodes',
1525
1733
  model: 'Every agent is a node in one directed graph (the canvas); `subscribes_to` is the spine — spawn a child and you auto-subscribe (active) to it, so its finish wakes you.\n\n' +
1526
1734
  'WHEN TO DELEGATE: hand any self-contained unit of work to a child instead of doing it inline — it keeps your own context window (your scarce resource) free for steering. Spawn independent units in PARALLEL; a wake with idle workers is wasted. Serialize only true dependencies, and never let two live children edit the same files.\n\n' +
1527
- 'HOW: `crtr node new "<task>" --kind <kind>` returns a node id immediately and runs the worker as a detached headless broker (no window opens; open a viewer on demand with `crtr node focus`). Match the kind to the work (see `node new -h`). You are woken when a child finishes — the wake message ALREADY IS the coalesced digest (the watcher drains your inbox to wake you), so don\'t re-run `crtr feed read` to "open" it (it would read empty, the cursor already advanced); instead dereference the report paths in that digest that matter, don\'t act on a one-line label. (`crtr feed read` is for proactively polling before a wake, or inspecting a child\'s inbox via `--node`; `--all` re-reads history with full message bodies.) Integrate, then either delegate the next units or finish.\n\n' +
1528
- 'FINISH: a worker ends its own work with `crtr push final "<result>"` (writes the canonical result, marks done, closes the window) stopping without it is not finishing. For a job too big for one context window, `node promote` to an orchestrator (holds a roadmap, delegates phases); when context fills, `node yield` to refresh against that roadmap.',
1735
+ 'HOW: the new leaf returns a node id immediately and runs the worker as a detached headless broker; no viewer opens unless a surface asks for one. Match the kind to the work and read the leaf schema before invoking. You are woken when a child finishes — the wake message ALREADY IS the coalesced digest, so dereference the report paths in that digest that matter instead of acting on a one-line label. Integrate, then either delegate the next units or finish.\n\n' +
1736
+ 'FINISH: a worker ends its own work with a final push, which writes the canonical result, marks done, and closes the window; stopping without it is not finishing. For a job too big for one context window, promote to an orchestrator; when context fills, yield to refresh against that roadmap.',
1529
1737
  },
1530
- children: [nodeNew, nodeInspect, nodeSnapshotLeaf, nodeContextLeaf, nodeFocus, nodeCycle, nodeRecycle, nodeClose, nodeMsg, nodeModel, nodeSubscribe, nodeUnsubscribe, nodePromote, nodeDemote, nodeLifecycle, nodeYield, nodeWake],
1738
+ children: [nodeNew, nodeMsg, nodeSubscribe, nodeUnsubscribe, nodePromote, nodeYield, nodeFocus, nodeCycle, nodeInspect, nodeLifecycle, nodeConfig, nodeTriggers],
1531
1739
  });
1532
1740
  }