@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
@@ -0,0 +1,2047 @@
1
+ // Bundle-safe mirror of LeafHelp.preview metadata (see help.ts's PreviewMeta),
2
+ // keyed by the leaf's full help name (e.g. "canvas snapshot", matching
3
+ // LeafHelp.name exactly).
4
+ //
5
+ // This file has NO imports. The attach viewer is bundled as a separate
6
+ // esbuild process from the one that runs a leaf, so it cannot read a leaf's
7
+ // command-owned `preview` field directly — walking the live command tree
8
+ // to find it would pull every subtree's module graph (including `surface`
9
+ // -> the web client -> Tailwind's native oxide binary) into the attach
10
+ // bundle. A flat, zero-dependency static map avoids that entirely.
11
+ //
12
+ // Source of truth: this Set must mirror every leaf that declares
13
+ // `help.preview.suppressOutput: true` in its own command file (currently
14
+ // canvas snapshot, node inspect snapshot, node inspect show, and sys
15
+ // sysprompt). There is no automated sync — when you add or remove a leaf's
16
+ // `preview.suppressOutput`, update this Set in the same commit.
17
+ // `preview-registry-sync.test.ts` (which imports the real command tree — safe
18
+ // in a test, never in the attach bundle) asserts the two stay identical.
19
+ // Exported (not just consumed through the helpers below) so
20
+ // preview-registry-sync.test.ts can diff it against the live command tree.
21
+ export const SUPPRESS_OUTPUT_LEAVES = new Set([
22
+ 'canvas snapshot',
23
+ 'node inspect snapshot',
24
+ 'node inspect show',
25
+ 'sys sysprompt',
26
+ ]);
27
+ /** True when the leaf at this full help-name path (e.g. "canvas snapshot")
28
+ * declares `preview.suppressOutput: true`. */
29
+ export function suppressOutputForLeaf(helpName) {
30
+ return SUPPRESS_OUTPUT_LEAVES.has(helpName);
31
+ }
32
+ /** Same lookup from argv tokens (e.g. ['node', 'inspect', 'snapshot', 'abc'])
33
+ * instead of a pre-joined help name. Matches a registered leaf name against
34
+ * a *prefix* of the tokens, not the whole joined tail, so a leaf's own
35
+ * positional arguments (e.g. the snapshot id in `node inspect snapshot
36
+ * abc`) don't prevent the match. */
37
+ export function suppressOutputForPath(tokens) {
38
+ for (const leaf of SUPPRESS_OUTPUT_LEAVES) {
39
+ const words = leaf.split(' ');
40
+ if (words.length <= tokens.length && words.every((word, i) => tokens[i] === word))
41
+ return true;
42
+ }
43
+ return false;
44
+ }
45
+ // ---------------------------------------------------------------------------
46
+ // Icon registry (rule 12, inline-ui-placement: real Nerd Font glyphs, not the
47
+ // generic ◆ diamond). Every leaf's glyph is looked up first in LEAF_ICON_CP
48
+ // (longest-prefix match, same shape as SUPPRESS_OUTPUT_LEAVES); an unmapped
49
+ // leaf falls back to its root family's glyph in FAMILY_ICON_CP.
50
+ // ---------------------------------------------------------------------------
51
+ /** question-circle — the header glyph for a `-h`/`--help` invocation,
52
+ * regardless of which command's help was requested. */
53
+ const HELP_ICON_CP = 0xf059;
54
+ export const HELP_ICON = String.fromCodePoint(HELP_ICON_CP);
55
+ /** One glyph per root subtree (`crtr <family> ...`) — the fallback for any
56
+ * leaf not listed in LEAF_ICON_CP below. Codepoints are Nerd Font
57
+ * private-use-area icons chosen per family concept (see command-icons.md). */
58
+ const FAMILY_ICON_CP = {
59
+ canvas: 0xf0e8,
60
+ feed: 0xf09e,
61
+ human: 0xf007,
62
+ memory: 0xf02d,
63
+ node: 0xf192,
64
+ pkg: 0xf1b2,
65
+ push: 0xf0ee,
66
+ search: 0xf002,
67
+ surface: 0xf108,
68
+ sys: 0xf013,
69
+ };
70
+ /** Per-leaf overrides, keyed by full leaf help-name path (e.g. "memory
71
+ * lint"), same shape as SUPPRESS_OUTPUT_LEAVES. */
72
+ const LEAF_ICON_CP = {
73
+ 'canvas dashboard': 0xf0e4,
74
+ 'canvas snapshot': 0xf030,
75
+ 'canvas browse': 0xf14e,
76
+ 'canvas attention count': 0xf292,
77
+ 'canvas attention list': 0xf03a,
78
+ 'canvas attention map': 0xf279,
79
+ 'canvas revive': 0xf021,
80
+ 'canvas history search': 0xf002,
81
+ 'canvas history read': 0xf02d,
82
+ 'canvas history show': 0xf1da,
83
+ 'canvas daemon start': 0xf04b,
84
+ 'canvas daemon status': 0xf111,
85
+ 'canvas daemon stop': 0xf04d,
86
+ 'canvas chord': 0xf001,
87
+ 'canvas tmux-spread': 0xf009,
88
+ 'canvas issue new': 0xf188,
89
+ 'canvas prune': 0xf0c4,
90
+ 'canvas rebuild-index': 0xf0ad,
91
+ 'feed read': 0xf02d,
92
+ 'feed message': 0xf0e0,
93
+ 'feed peek': 0xf06e,
94
+ 'human ask': 0xf059,
95
+ 'human review': 0xf044,
96
+ 'human notify': 0xf0f3,
97
+ 'human show': 0xf108,
98
+ 'human cancel': 0xf00d,
99
+ 'human inbox': 0xf01c,
100
+ 'human list': 0xf03a,
101
+ 'human deck': 0xf0db,
102
+ 'human resolve': 0xf00c,
103
+ 'memory list': 0xf03a,
104
+ 'memory read': 0xf02d,
105
+ 'memory find': 0xf002,
106
+ 'memory write': 0xf040,
107
+ 'memory origin': 0xf1bb,
108
+ 'memory lint': 0xf14a,
109
+ 'node new': 0xf067,
110
+ 'node msg': 0xf0e0,
111
+ 'node subscribe': 0xf0f3,
112
+ 'node unsubscribe': 0xf1f6,
113
+ 'node promote': 0xf102,
114
+ 'node yield': 0xf28d,
115
+ 'node focus': 0xf05b,
116
+ 'node cycle': 0xf021,
117
+ 'node inspect list': 0xf03a,
118
+ 'node inspect show': 0xf06e,
119
+ 'node inspect snapshot': 0xf030,
120
+ 'node inspect context': 0xf07c,
121
+ 'node lifecycle recycle': 0xf1b8,
122
+ 'node lifecycle close': 0xf00d,
123
+ 'node lifecycle demote': 0xf103,
124
+ 'node config': 0xf013,
125
+ 'node triggers list': 0xf017,
126
+ 'node triggers cancel': 0xf05e,
127
+ 'pkg plugin install': 0xf019,
128
+ 'pkg plugin remove': 0xf014,
129
+ 'pkg plugin enable': 0xf00c,
130
+ 'pkg plugin disable': 0xf05e,
131
+ 'pkg plugin update': 0xf021,
132
+ 'pkg plugin list': 0xf03a,
133
+ 'pkg plugin show': 0xf06e,
134
+ 'pkg market add': 0xf067,
135
+ 'pkg market remove': 0xf014,
136
+ 'pkg market update': 0xf021,
137
+ 'pkg market list': 0xf03a,
138
+ 'pkg market browse': 0xf14e,
139
+ 'push update': 0xf093,
140
+ 'push urgent': 0xf0e7,
141
+ 'push final': 0xf11e,
142
+ 'search web': 0xf0ac,
143
+ 'search answer': 0xf075,
144
+ 'search contents': 0xf0f6,
145
+ 'surface view list': 0xf03a,
146
+ 'surface view run': 0xf04b,
147
+ 'surface view cycle': 0xf021,
148
+ 'surface view new': 0xf067,
149
+ 'surface attach to': 0xf1e6,
150
+ 'surface workspace enter': 0xf120,
151
+ 'surface web serve': 0xf0ac,
152
+ 'sys config get': 0xf019,
153
+ 'sys config set': 0xf040,
154
+ 'sys config path': 0xf07b,
155
+ 'sys settings': 0xf1de,
156
+ 'sys setup': 0xf0ad,
157
+ 'sys doctor': 0xf0f1,
158
+ 'sys sysprompt': 0xf15c,
159
+ 'sys prompt-review': 0xf002,
160
+ 'sys promptstudio': 0xf008,
161
+ 'sys feedback': 0xf075,
162
+ 'sys update': 0xf021,
163
+ 'sys version': 0xf02b,
164
+ 'sys sync': 0xf079,
165
+ };
166
+ /** Resolve the Nerd Font glyph for a command's argv tokens: the longest
167
+ * matching LEAF_ICON_CP prefix, else the tokens[0] family glyph, else the
168
+ * generic ◆ fallback for an unmapped family. */
169
+ export function iconForPath(tokens) {
170
+ let best;
171
+ for (const [leaf, cp] of Object.entries(LEAF_ICON_CP)) {
172
+ const words = leaf.split(' ');
173
+ if (words.length <= tokens.length && words.every((word, i) => tokens[i] === word)) {
174
+ if (best === undefined || words.length > best.words)
175
+ best = { words: words.length, cp };
176
+ }
177
+ }
178
+ if (best !== undefined)
179
+ return String.fromCodePoint(best.cp);
180
+ const familyCp = tokens.length > 0 ? FAMILY_ICON_CP[tokens[0]] : undefined;
181
+ return familyCp !== undefined ? String.fromCodePoint(familyCp) : '◆';
182
+ }
183
+ const MEMORY_READ_CONTENT_LINES = 3;
184
+ /** `crtr memory read <name>` prints a `- name:/- kind:/- scope:/- path:`
185
+ * metadata block, then `**content:** <body>`, then a `**follow_up:**` hint.
186
+ * The collapsed card is a tidy frontmatter glance (Silas, 2026-07-01,
187
+ * option C): a `name · kind · scope` identity line (path is filesystem
188
+ * noise, dropped; follow_up hint dropped) plus the first few lines of the
189
+ * content as a teaser — the full body is what ctrl+o reveals. Returns null
190
+ * when neither the metadata fields nor the `**content:**` marker are
191
+ * present — an error result (missing/ambiguous doc name, etc.) has none of
192
+ * this shape, and must fall back to the raw output rather than render a
193
+ * blank collapsed card. */
194
+ function summarizeMemoryRead(text) {
195
+ const field = (name) => new RegExp(`^-\\s+${name}:\\s*(.+)$`, 'm').exec(text)?.[1].trim() ?? '';
196
+ const header = [field('name'), field('kind'), field('scope')].filter((f) => f !== '').join(' · ');
197
+ // Content spans from `**content:**` to the `**follow_up:**` hint (or EOF).
198
+ const contentMatch = /\*\*content:\*\*\s*([\s\S]*?)(?:\n\s*\*\*follow_up:\*\*|$)/.exec(text);
199
+ if (header === '' && contentMatch === null)
200
+ return null;
201
+ const contentLines = (contentMatch?.[1] ?? '').replace(/^\s+/, '').replace(/\s+$/, '').split('\n');
202
+ const teaser = contentLines.slice(0, MEMORY_READ_CONTENT_LINES);
203
+ const hasMore = contentLines.slice(MEMORY_READ_CONTENT_LINES).some((l) => l.trim() !== '');
204
+ if (hasMore)
205
+ teaser.push('…');
206
+ const lines = [{ text: header }];
207
+ if (teaser.some((l) => l.trim() !== '')) {
208
+ lines.push({ text: '' });
209
+ for (const l of teaser)
210
+ lines.push({ text: l });
211
+ }
212
+ return lines;
213
+ }
214
+ /** Cap on individual finding rows in the `memory lint` findings-present
215
+ * summary — the count line always shows the true total, this just bounds
216
+ * how many per-finding detail lines get listed before falling back to a
217
+ * muted "… N more" line. */
218
+ const MEMORY_LINT_FINDINGS_CAP = 8;
219
+ /** `crtr memory lint` has two real output shapes (verified against the live
220
+ * command, not assumed):
221
+ * - 0 findings (success return): `- checked: N`, `- corpora: {...}`, a
222
+ * prose line, blank, then `0 findings.`
223
+ * - >0 findings: the leaf THROWS instead of returning, so the CLI error path
224
+ * renders `<error code="error">\nmemory lint: N finding(s) across M
225
+ * files\nNext: ...\n</error>` on stdout, with one `crtr: memory lint:
226
+ * <path>: <error>` line per finding on stderr (merged into the same tool
227
+ * result text when the bash host merges the two streams). Both shapes are
228
+ * parsed defensively; an output that matches neither falls through to the
229
+ * generic truncation path (returns null). */
230
+ function summarizeMemoryLint(text) {
231
+ const checkedMatch = /^- checked:\s*(\d+)\s*$/m.exec(text);
232
+ const successCountMatch = /(\d+)\s+findings\.\s*$/.exec(text.trimEnd());
233
+ if (checkedMatch && successCountMatch) {
234
+ const checked = checkedMatch[1];
235
+ const count = Number(successCountMatch[1]);
236
+ if (count === 0) {
237
+ return [
238
+ { text: 'PASS', tone: 'success' },
239
+ { text: `${checked} docs checked`, tone: 'muted' },
240
+ ];
241
+ }
242
+ return [
243
+ { text: `${count} findings`, tone: 'error' },
244
+ { text: `${checked} docs checked`, tone: 'muted' },
245
+ ];
246
+ }
247
+ const failureMatch = /(\d+)\s+finding\(s\)\s+across\s+(\d+)\s+files/.exec(text);
248
+ if (failureMatch) {
249
+ const count = Number(failureMatch[1]);
250
+ const checked = failureMatch[2];
251
+ const lines = [{ text: `${count} findings`, tone: 'error' }];
252
+ const findingLines = [...text.matchAll(/^crtr: memory lint: (.+)$/gm)].map((m) => m[1]);
253
+ for (const line of findingLines.slice(0, MEMORY_LINT_FINDINGS_CAP)) {
254
+ lines.push({ text: line, tone: 'warning' });
255
+ }
256
+ const remaining = findingLines.length - MEMORY_LINT_FINDINGS_CAP;
257
+ if (remaining > 0)
258
+ lines.push({ text: `… ${remaining} more`, tone: 'muted' });
259
+ lines.push({ text: `${checked} docs checked`, tone: 'muted' });
260
+ return lines;
261
+ }
262
+ return null;
263
+ }
264
+ // ---------------------------------------------------------------------------
265
+ // Shared table/count parsing helpers for the canvas/node/feed runtime cockpit
266
+ // batch below. These parse the exact markdown shapes core/render.ts's generic
267
+ // renderArray/renderResult produce (a `- field: N` bullet for a scalar count,
268
+ // an `N <label>:` count lead-in followed by a GFM table for an array of
269
+ // objects, or a bare `0 <label>.` line for an empty array) — strict recognition
270
+ // of that one shape, never a second lenient fallback. Any deviation (missing
271
+ // table, column-count mismatch, non-numeric field expected to be numeric)
272
+ // returns null so the caller falls through to generic truncation.
273
+ // ---------------------------------------------------------------------------
274
+ /** Split one GFM table row into its cell strings, honoring `\|` as an escaped
275
+ * literal pipe (the only escape core/render.ts's cellEsc introduces) rather
276
+ * than a column separator. Returns null when the line isn't a `| ... |` row. */
277
+ function splitTableRow(line) {
278
+ const trimmed = line.trim();
279
+ if (!trimmed.startsWith('|') || !trimmed.endsWith('|'))
280
+ return null;
281
+ const body = trimmed.slice(1, -1);
282
+ const cells = [];
283
+ let current = '';
284
+ let i = 0;
285
+ while (i < body.length) {
286
+ const ch = body[i];
287
+ if (ch === '\\' && body[i + 1] === '|') {
288
+ current += '|';
289
+ i += 2;
290
+ continue;
291
+ }
292
+ if (ch === '|') {
293
+ cells.push(current.trim());
294
+ current = '';
295
+ i++;
296
+ continue;
297
+ }
298
+ current += ch;
299
+ i++;
300
+ }
301
+ cells.push(current.trim());
302
+ return cells;
303
+ }
304
+ /** Find the `N <headingLabel>:` + GFM table block renderArray emits for an
305
+ * array-of-objects field named `headingLabel` (e.g. `28 rows:`, `63 nodes:`),
306
+ * and parse every data row into a plain `{column: cellText}` record, using the
307
+ * header row's own column names as keys — no positional/column-index
308
+ * assumptions, so an unrelated extra column (e.g. `hanging`) is carried
309
+ * along and simply ignored by callers that only read specific keys. A bare
310
+ * `0 <headingLabel>.` line (renderArray's empty-array shape) parses as an
311
+ * empty (not null) array. Returns null when the heading is missing, the
312
+ * table shape doesn't match (header/separator/row column-count mismatch), or
313
+ * trailing non-blank content follows the table — never a partial parse. */
314
+ function parseTableRows(text, headingLabel) {
315
+ const lines = text.split('\n');
316
+ for (let i = 0; i < lines.length; i++) {
317
+ const trimmed = lines[i].trim();
318
+ if (trimmed === `0 ${headingLabel}.`)
319
+ return [];
320
+ if (!/^\d+ /.test(trimmed) || !trimmed.endsWith(`${headingLabel}:`))
321
+ continue;
322
+ let j = i + 1;
323
+ while (j < lines.length && lines[j].trim() === '')
324
+ j++;
325
+ if (j + 1 >= lines.length)
326
+ return null;
327
+ const header = splitTableRow(lines[j]);
328
+ const separator = splitTableRow(lines[j + 1]);
329
+ if (header === null || separator === null || header.length === 0 || separator.length !== header.length)
330
+ return null;
331
+ if (separator.some((cell) => !/^-+$/.test(cell)))
332
+ return null;
333
+ const rows = [];
334
+ j += 2;
335
+ while (j < lines.length && lines[j].trim() !== '') {
336
+ const cells = splitTableRow(lines[j]);
337
+ if (cells === null || cells.length !== header.length)
338
+ return null;
339
+ const row = {};
340
+ for (let c = 0; c < header.length; c++)
341
+ row[header[c]] = cells[c];
342
+ rows.push(row);
343
+ j++;
344
+ }
345
+ while (j < lines.length && lines[j].trim() === '')
346
+ j++;
347
+ return j === lines.length ? rows : null;
348
+ }
349
+ return null;
350
+ }
351
+ /** Read a `- <field>: <integer>` scalar bullet (core/render.ts's scalar-bullet
352
+ * shape). Returns null when the field isn't present as a plain integer. */
353
+ function parseIntBullet(text, field) {
354
+ const match = new RegExp(`^-\\s+${field}:\\s*(\\d+)\\s*$`, 'm').exec(text);
355
+ return match === null ? null : Number(match[1]);
356
+ }
357
+ /** Compact a token count for a muted aside: 1000+ rounds to the nearest
358
+ * thousand with a `k` suffix (`437000` → `437k`), matching the `ctx 365k`
359
+ * shorthand the dashboard tree already uses elsewhere. */
360
+ function compactThousands(value) {
361
+ return value >= 1000 ? `${Math.round(value / 1000)}k` : String(value);
362
+ }
363
+ /** The trailing path segment of a cwd, for a compact per-cwd label
364
+ * (`/Users/.../cli/crouter` → `crouter`) — a full absolute path is filesystem
365
+ * noise in a collapsed summary line. */
366
+ function basename(path) {
367
+ const trimmed = path.replace(/[\\/]+$/, '');
368
+ const idx = Math.max(trimmed.lastIndexOf('/'), trimmed.lastIndexOf('\\'));
369
+ return idx === -1 ? trimmed : trimmed.slice(idx + 1);
370
+ }
371
+ /** Render a `<count> <key>` digest across a fixed preferred key order (each
372
+ * shown only when its count is > 0), then any other key present sorted
373
+ * alphabetically — the `3 active · 24 done` shape both dashboard and node
374
+ * list summaries share. */
375
+ function summarizeOrderedCounts(counts, order) {
376
+ const inOrder = new Set(order);
377
+ const parts = [];
378
+ for (const key of order) {
379
+ const count = counts.get(key) ?? 0;
380
+ if (count > 0)
381
+ parts.push(`${count} ${key}`);
382
+ }
383
+ const extras = [...counts.entries()]
384
+ .filter(([key, count]) => count > 0 && !inOrder.has(key))
385
+ .sort(([a], [b]) => a.localeCompare(b));
386
+ for (const [key, count] of extras)
387
+ parts.push(`${count} ${key}`);
388
+ return parts.join(' · ');
389
+ }
390
+ /** Render the top `limit` entries of a name→count map, ranked by count
391
+ * descending (name ascending to break ties), with a muted `… N more` tail
392
+ * when entries were dropped — the `developer×2, advisor×1` kind-breakdown
393
+ * shape. */
394
+ function summarizeTopCounts(counts, limit, renderItem) {
395
+ const entries = [...counts.entries()]
396
+ .filter(([, count]) => count > 0)
397
+ .sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]));
398
+ const shown = entries.slice(0, limit).map(([name, count]) => renderItem(name, count));
399
+ const remaining = entries.length - shown.length;
400
+ if (remaining > 0)
401
+ shown.push(`… ${remaining} more`);
402
+ return shown.join(', ');
403
+ }
404
+ /** `crtr canvas dashboard`'s object output carries a `rows` array (one entry
405
+ * per node: {node_id,name,status,kind,mode,ctx_tokens,asks,...}) rendered by
406
+ * renderArray as an `N rows:` table. The collapsed card is a status-count
407
+ * digest (active/idle plain, done/dead/canceled muted by the ordering below),
408
+ * the largest ctx_tokens across the view as a muted aside, and a warning line
409
+ * only when any row carries a pending ask — Silas's 2026-07-01 reframe ("what
410
+ * does a human want to know?"), not a re-rendering of the raw tree/table.
411
+ * Returns null when the rows table isn't in the expected shape, or is empty
412
+ * (an empty canvas has nothing to digest into a status line). */
413
+ function summarizeCanvasDashboard(text) {
414
+ const rows = parseTableRows(text, 'rows');
415
+ if (rows === null || rows.length === 0)
416
+ return null;
417
+ const statuses = new Map();
418
+ let largestCtx = 0;
419
+ let asks = 0;
420
+ for (const row of rows) {
421
+ const status = row['status'];
422
+ const ctx = Number(row['ctx_tokens']);
423
+ const ask = Number(row['asks']);
424
+ if (status === undefined || status === '' || Number.isNaN(ctx) || Number.isNaN(ask))
425
+ return null;
426
+ statuses.set(status, (statuses.get(status) ?? 0) + 1);
427
+ largestCtx = Math.max(largestCtx, ctx);
428
+ asks += ask;
429
+ }
430
+ const digest = summarizeOrderedCounts(statuses, ['active', 'idle', 'done', 'dead', 'canceled']);
431
+ if (digest === '')
432
+ return null;
433
+ const lines = [{ text: digest }];
434
+ lines.push({ text: `largest ctx: ${compactThousands(largestCtx)} tokens`, tone: 'muted' });
435
+ if (asks > 0)
436
+ lines.push({ text: `${asks} pending asks`, tone: 'warning' });
437
+ return lines;
438
+ }
439
+ /** `crtr node inspect list`'s object output carries a `nodes` array (one entry
440
+ * per node: {node_id,name,kind,mode,lifecycle,status,cwd,...}) rendered as an
441
+ * `N nodes:` table. The collapsed card is a status-count digest (same order
442
+ * as the dashboard summary) plus a muted kind breakdown (top 5 kinds by
443
+ * count). Returns null when the table isn't in the expected shape or empty. */
444
+ function summarizeNodeInspectList(text) {
445
+ const rows = parseTableRows(text, 'nodes');
446
+ if (rows === null || rows.length === 0)
447
+ return null;
448
+ const statuses = new Map();
449
+ const kinds = new Map();
450
+ for (const row of rows) {
451
+ const status = row['status'];
452
+ const kind = row['kind'];
453
+ if (status === undefined || status === '' || kind === undefined || kind === '')
454
+ return null;
455
+ statuses.set(status, (statuses.get(status) ?? 0) + 1);
456
+ kinds.set(kind, (kinds.get(kind) ?? 0) + 1);
457
+ }
458
+ const statusDigest = summarizeOrderedCounts(statuses, ['active', 'idle', 'done', 'dead', 'canceled']);
459
+ const kindDigest = summarizeTopCounts(kinds, 5, (kind, count) => `${kind}×${count}`);
460
+ if (statusDigest === '' || kindDigest === '')
461
+ return null;
462
+ return [
463
+ { text: statusDigest },
464
+ { text: `kinds: ${kindDigest}`, tone: 'muted' },
465
+ ];
466
+ }
467
+ /** `crtr canvas attention count`'s object output is a single `- count: N`
468
+ * bullet. Silas's computed-status framing (2026-07-01): 0 pending asks is a
469
+ * genuinely PASSing state worth a green line, not an empty card, and N > 0 is
470
+ * worth an error-toned line naming the count — never suppress this leaf down
471
+ * to nothing regardless of which way the count falls. Returns null only when
472
+ * `count` isn't present as a plain integer (an unrecognized/error shape). */
473
+ function summarizeCanvasAttentionCount(text) {
474
+ const count = parseIntBullet(text, 'count');
475
+ if (count === null)
476
+ return null;
477
+ if (count === 0)
478
+ return [{ text: 'PASS · no pending asks', tone: 'success' }];
479
+ return [{ text: `${count} pending asks`, tone: 'error' }];
480
+ }
481
+ /** `crtr canvas attention list`'s object output is a `- total: N` bullet
482
+ * followed by an `M items:` table ({node_id,name,cwd,count}). The collapsed
483
+ * card leads with `total · cwd count`, calls out the hottest single cwd in
484
+ * warning tone (the one place a human should look first), then a muted
485
+ * digest of the remaining cwd basenames (paths are filesystem noise here).
486
+ * Zero pending asks reads as the same PASS line attention count uses —
487
+ * consistent health framing across both leaves, never an empty card. Returns
488
+ * null when the total/items shape doesn't match, or when any item is missing
489
+ * its cwd/count (an unrecognized row shape). */
490
+ function summarizeCanvasAttentionList(text) {
491
+ const total = parseIntBullet(text, 'total');
492
+ const items = parseTableRows(text, 'items');
493
+ if (total === null || items === null)
494
+ return null;
495
+ if (total === 0 || items.length === 0)
496
+ return [{ text: 'PASS · no pending asks', tone: 'success' }];
497
+ const entries = [];
498
+ for (const row of items) {
499
+ const cwd = row['cwd'];
500
+ const count = Number(row['count']);
501
+ if (cwd === undefined || cwd === '' || Number.isNaN(count))
502
+ return null;
503
+ entries.push({ cwd, count });
504
+ }
505
+ const sorted = entries
506
+ .slice()
507
+ .sort((a, b) => b.count - a.count || basename(a.cwd).localeCompare(basename(b.cwd)) || a.cwd.localeCompare(b.cwd));
508
+ const hottest = sorted[0];
509
+ const restBasenames = sorted.slice(1).map((item) => basename(item.cwd));
510
+ const lines = [
511
+ { text: `${total} total · ${items.length} cwds` },
512
+ { text: `hottest: ${basename(hottest.cwd)} (${hottest.count})`, tone: 'warning' },
513
+ ];
514
+ if (restBasenames.length > 0) {
515
+ const shown = restBasenames.slice(0, 4);
516
+ const remaining = restBasenames.length - shown.length;
517
+ const digest = remaining > 0 ? `${shown.join(', ')}, … ${remaining} more` : shown.join(', ');
518
+ lines.push({ text: `other cwds: ${digest}`, tone: 'muted' });
519
+ }
520
+ return lines;
521
+ }
522
+ /** `crtr canvas attention map` (src/commands/attention.ts:227) has no custom
523
+ * `render`; its `counts` field is a nested plain object (node_id → ask
524
+ * count), not an array, so the generic renderer's `renderResult` emits it
525
+ * as ONE scalar bullet via `scalarStr`'s compact-JSON branch: `- counts:
526
+ * {"id1":0,"id2":3,...}` (output schema at attention.ts:190). No node names
527
+ * are available in this output (only ids), so the collapsed card names the
528
+ * hottest id, not a name. The real shape is `Record<string, number>` — a
529
+ * mixed object (any non-numeric value) is a shape mismatch, not a partial
530
+ * total, so it's rejected wholesale rather than filtered down. Returns
531
+ * null when `counts` isn't parseable JSON, is empty, or has any
532
+ * non-numeric entry. */
533
+ function summarizeCanvasAttentionMap(text) {
534
+ const m = /^- counts:\s*(\{.*\})\s*$/m.exec(text);
535
+ if (m === null)
536
+ return null;
537
+ let counts;
538
+ try {
539
+ counts = JSON.parse(m[1]);
540
+ }
541
+ catch {
542
+ return null;
543
+ }
544
+ if (counts === null || typeof counts !== 'object')
545
+ return null;
546
+ const rawEntries = Object.entries(counts);
547
+ if (rawEntries.length === 0)
548
+ return null;
549
+ if (rawEntries.some(([, v]) => typeof v !== 'number'))
550
+ return null;
551
+ const entries = rawEntries;
552
+ const total = entries.reduce((s, [, v]) => s + v, 0);
553
+ if (total === 0)
554
+ return [{ text: 'PASS · no pending asks', tone: 'success' }];
555
+ const withAsks = entries.filter(([, v]) => v > 0);
556
+ const sorted = withAsks.slice().sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]));
557
+ const hottest = sorted[0];
558
+ return [
559
+ { text: `${total} asks across ${withAsks.length} node${withAsks.length === 1 ? '' : 's'}` },
560
+ { text: `hottest: ${hottest[0]} (${hottest[1]})`, tone: 'warning' },
561
+ ];
562
+ }
563
+ /** Find the first argv token after `skipWords` that isn't a boolean flag
564
+ * (`--foo`) — recovers a leaf's own positional id when its render() output
565
+ * carries no id of its own (see summarizeCanvasRevive below). Safe here
566
+ * specifically because every `canvas revive` flag is a boolean with no
567
+ * separate value token, so a naive forward scan can't mistake a flag's
568
+ * value for the positional. */
569
+ function firstPositionalAfter(tokens, skipWords) {
570
+ for (let i = skipWords; i < tokens.length; i++) {
571
+ if (!tokens[i].startsWith('--'))
572
+ return tokens[i];
573
+ }
574
+ return undefined;
575
+ }
576
+ /** `crtr canvas revive` (src/commands/revive.ts) has no custom `render`, so
577
+ * its object output falls through to generic renderResult. Four shapes
578
+ * depending on flags:
579
+ * - `--all` (preview or executed) carries a `- mode: preview|revived`
580
+ * bullet — skipped (null): `candidates`/`revived`/`failed` are
581
+ * prose-or-bullet depending on whether they contain a newline (multi- vs
582
+ * single-id), which core/render.ts's `isProse` makes genuinely ambiguous
583
+ * to parse strictly, and this is a rare admin path.
584
+ * - `--now` kick: the only shape with `- kicked: true` (revive.ts:192).
585
+ * - plain single-node revive: the only shape with `- resumed: true|false`.
586
+ * `window` (revive.ts:83) is documented "Always null", so it's never
587
+ * bulleted — and there is no node id anywhere in the output, so it's
588
+ * recovered from argv (the leaf's own positional) via
589
+ * firstPositionalAfter.
590
+ * Returns null on any other/unrecognized shape. */
591
+ function summarizeCanvasRevive(text, tokens) {
592
+ if (/^- mode:\s*\S+\s*$/m.test(text))
593
+ return null;
594
+ if (/^- kicked:\s*true\s*$/m.test(text)) {
595
+ return [{ text: 'kicked — daemon resumes in ~20s', tone: 'warning' }];
596
+ }
597
+ const resumedMatch = /^- resumed:\s*(true|false)\s*$/m.exec(text);
598
+ if (resumedMatch === null)
599
+ return null;
600
+ const id = firstPositionalAfter(tokens, 2); // 'canvas','revive' = 2 words
601
+ const lines = [{ text: id !== undefined ? `revived ${id}` : 'revived', tone: 'success' }];
602
+ const readyMatch = /^- ready:\s*(true|false)\s*$/m.exec(text);
603
+ if (readyMatch !== null && readyMatch[1] === 'false') {
604
+ lines.push({ text: 'not yet attachable (socket not ready)', tone: 'warning' });
605
+ }
606
+ return lines;
607
+ }
608
+ /** `crtr canvas history search` (renderSearch, src/commands/canvas-history/
609
+ * search.ts:246) has three shapes: `0 hits.`; `--full` (### heading blocks
610
+ * per hit, not tabular — skipped, returns null); or the normal `N of M
611
+ * hits:` lead-in plus a GFM table (columns `{ref,node,kind,source,ts,
612
+ * score?,snippet}`, or `{ref,node,ts,line,text}` under `--grep`, no
613
+ * `kind`). Reuses this file's own `splitTableRow` helper rather than
614
+ * `parseTableRows` (whose `N <label>:` lead-in must end in a literal colon
615
+ * immediately before the table — `N of M hits:` is a close but different
616
+ * shape). Returns null on `--full` or any other shape mismatch: a
617
+ * non-`---` separator cell, or a missing `node`/`ts` column (`kind` is
618
+ * read only when present, never required — `--grep` mode legitimately
619
+ * lacks it), or a parsed row count that doesn't match the leading `N of M
620
+ * hits:` displayed count `N` (the actual page size — pagination can make
621
+ * it less than `M`, the grand total). */
622
+ function summarizeCanvasHistorySearch(text) {
623
+ if (/^0 hits\.$/m.test(text))
624
+ return [{ text: '0 hits', tone: 'muted' }];
625
+ const lead = /^(\d+) of (\d+) hits:$/m.exec(text);
626
+ if (lead === null)
627
+ return null;
628
+ const displayed = Number(lead[1]);
629
+ const total = Number(lead[2]);
630
+ const lines = text.split('\n');
631
+ const headerIdx = lines.findIndex((l) => l.trim().startsWith('| ref |'));
632
+ if (headerIdx === -1)
633
+ return null;
634
+ const header = splitTableRow(lines[headerIdx]);
635
+ const separator = splitTableRow(lines[headerIdx + 1]);
636
+ if (header === null || separator === null || header.length !== separator.length)
637
+ return null;
638
+ if (separator.some((cell) => !/^-+$/.test(cell)))
639
+ return null;
640
+ if (!header.includes('node') || !header.includes('ts'))
641
+ return null;
642
+ const rows = [];
643
+ let j = headerIdx + 2;
644
+ while (j < lines.length && lines[j].trim().startsWith('|')) {
645
+ const cells = splitTableRow(lines[j]);
646
+ if (cells === null || cells.length !== header.length)
647
+ return null;
648
+ const row = {};
649
+ header.forEach((h, i) => { row[h] = cells[i]; });
650
+ rows.push(row);
651
+ j++;
652
+ }
653
+ if (rows.length !== displayed)
654
+ return null;
655
+ const top = rows.slice(0, 3).map((r) => 'kind' in r ? `${r['node']} · ${r['kind']} · ${r['ts']}` : `${r['node']} · ${r['ts']}`);
656
+ const out = [{ text: `${total} hit${total === 1 ? '' : 's'}` }];
657
+ for (const t of top)
658
+ out.push({ text: t, tone: 'muted' });
659
+ const remaining = rows.length - top.length;
660
+ if (remaining > 0)
661
+ out.push({ text: `… ${remaining} more shown`, tone: 'muted' });
662
+ return out;
663
+ }
664
+ /** `crtr canvas history read` (src/commands/canvas-history/read.ts:61):
665
+ * `${node} | source: ${source} | ts: ${ts} | ref: ${ref}\n\n${content}`
666
+ * — note the DOUBLE SPACES around each `|`. Mirrors summarizeMemoryRead's
667
+ * identity+teaser pattern (reusing MEMORY_READ_CONTENT_LINES). Returns null
668
+ * when the head line doesn't match this exact shape. */
669
+ function summarizeCanvasHistoryRead(text) {
670
+ const first = text.split('\n')[0] ?? '';
671
+ const head = /^(.+?) \| source: (.+?) \| ts: (.+?) \| ref: (.+)$/.exec(first);
672
+ if (head === null)
673
+ return null;
674
+ const [, node, source, ts] = head;
675
+ const rest = text.split('\n').slice(1);
676
+ let idx = 0;
677
+ while (idx < rest.length && rest[idx].trim() === '')
678
+ idx++;
679
+ const contentLines = rest.slice(idx);
680
+ const teaser = contentLines.slice(0, MEMORY_READ_CONTENT_LINES);
681
+ const hasMore = contentLines.slice(MEMORY_READ_CONTENT_LINES).some((l) => l.trim() !== '');
682
+ if (hasMore)
683
+ teaser.push('…');
684
+ const lines = [{ text: `${node} · ${source} · ${ts}` }];
685
+ if (teaser.some((l) => l.trim() !== '')) {
686
+ lines.push({ text: '' });
687
+ for (const l of teaser)
688
+ lines.push({ text: l });
689
+ }
690
+ return lines;
691
+ }
692
+ /** `crtr canvas history show` (src/commands/canvas-history/show.ts:66):
693
+ * `node: <name (id)>\n\n0 artifacts. | N artifacts:\n\n<table
694
+ * cols=ref,source,ts,detail>\n\n<follow_up>`. `source` values are
695
+ * colon-prefixed types (`report:final`, `report:update`, `doc`,
696
+ * `roadmap`). Reuses this file's `summarizeTopCounts` for the type
697
+ * breakdown. Returns null when the `node:` line, the table shape (a
698
+ * non-`---` separator cell), or a missing `source` column doesn't
699
+ * match — or when the parsed row count doesn't match the leading `N
700
+ * artifacts:` count (this render has no pagination, so the two must
701
+ * agree exactly). */
702
+ function summarizeCanvasHistoryShow(text) {
703
+ const nodeMatch = /^node: (.+)$/m.exec(text);
704
+ if (nodeMatch === null)
705
+ return null;
706
+ if (/^0 artifacts\.$/m.test(text))
707
+ return [{ text: `${nodeMatch[1]} · 0 artifacts`, tone: 'muted' }];
708
+ const countMatch = /^(\d+) artifacts:$/m.exec(text);
709
+ if (countMatch === null)
710
+ return null;
711
+ const lines = text.split('\n');
712
+ const headerIdx = lines.findIndex((l) => l.trim().startsWith('| ref |'));
713
+ if (headerIdx === -1)
714
+ return null;
715
+ const header = splitTableRow(lines[headerIdx]);
716
+ const separator = splitTableRow(lines[headerIdx + 1]);
717
+ if (header === null || separator === null || header.length !== separator.length)
718
+ return null;
719
+ if (separator.some((cell) => !/^-+$/.test(cell)))
720
+ return null;
721
+ const sourceIdx = header.indexOf('source');
722
+ if (sourceIdx === -1)
723
+ return null;
724
+ const typeCounts = new Map();
725
+ let rowCount = 0;
726
+ let j = headerIdx + 2;
727
+ while (j < lines.length && lines[j].trim().startsWith('|')) {
728
+ const cells = splitTableRow(lines[j]);
729
+ if (cells === null || cells.length !== header.length)
730
+ return null;
731
+ const type = (cells[sourceIdx] ?? '').split(':')[0];
732
+ typeCounts.set(type, (typeCounts.get(type) ?? 0) + 1);
733
+ rowCount++;
734
+ j++;
735
+ }
736
+ if (rowCount !== Number(countMatch[1]))
737
+ return null;
738
+ const digest = summarizeTopCounts(typeCounts, 5, (name, count) => `${name}s×${count}`);
739
+ return [{ text: `${countMatch[1]} artifacts` }, { text: digest, tone: 'muted' }];
740
+ }
741
+ /** `crtr canvas daemon start` (src/commands/daemon.ts:40, `spawnDaemon()` in
742
+ * src/daemon/manage.ts:89) has no custom `render`; the generic renderer
743
+ * bullets `{started:true,pid}` or `{started:false,existing_pid}`. `pid` is
744
+ * unconditionally set alongside `started:true` (manage.ts:125) — a
745
+ * true-with-no-pid shape is unrecognized, so it returns null rather than a
746
+ * pid-less success line. `existing_pid` genuinely can be absent on the
747
+ * false branch (a `readPidfile() ?? undefined` race), so that one stays
748
+ * optional. Returns null when `started` isn't a plain boolean bullet, or
749
+ * is `true` without a `pid` bullet. */
750
+ function summarizeCanvasDaemonStart(text) {
751
+ const m = /^- started:\s*(true|false)\s*$/m.exec(text);
752
+ if (m === null)
753
+ return null;
754
+ if (m[1] === 'true') {
755
+ const pid = /^- pid:\s*(\d+)\s*$/m.exec(text)?.[1];
756
+ if (pid === undefined)
757
+ return null;
758
+ return [{ text: `started · pid ${pid}`, tone: 'success' }];
759
+ }
760
+ const pid = /^- existing_pid:\s*(\d+)\s*$/m.exec(text)?.[1];
761
+ return [{ text: `already running${pid !== undefined ? ` · pid ${pid}` : ''}`, tone: 'muted' }];
762
+ }
763
+ /** `crtr canvas daemon status` (src/commands/daemon.ts:65) has no custom
764
+ * `render`; the generic renderer bullets `{running:true,pid}` or
765
+ * `{running:false}`. `pid` is unconditionally set alongside `running:true`
766
+ * (daemon.ts:70 — the true branch only fires once `readPidfile()` already
767
+ * returned a pid), so a true-with-no-pid shape is unrecognized. Returns
768
+ * null when `running` isn't a plain boolean bullet, or is `true` without a
769
+ * `pid` bullet. */
770
+ function summarizeCanvasDaemonStatus(text) {
771
+ const m = /^- running:\s*(true|false)\s*$/m.exec(text);
772
+ if (m === null)
773
+ return null;
774
+ if (m[1] === 'true') {
775
+ const pid = /^- pid:\s*(\d+)\s*$/m.exec(text)?.[1];
776
+ if (pid === undefined)
777
+ return null;
778
+ return [{ text: `running · pid ${pid}`, tone: 'success' }];
779
+ }
780
+ return [{ text: 'not running', tone: 'error' }];
781
+ }
782
+ /** `crtr canvas daemon stop` (src/commands/daemon.ts:93) has no custom
783
+ * `render`; the generic renderer bullets `{stopped:true,pid}` or
784
+ * `{stopped:false}`. `pid` is unconditionally set alongside `stopped:true`
785
+ * (daemon.ts:98 — the signaled pid is echoed back in the same return), so a
786
+ * true-with-no-pid shape is unrecognized. Returns null when `stopped` isn't
787
+ * a plain boolean bullet, or is `true` without a `pid` bullet. */
788
+ function summarizeCanvasDaemonStop(text) {
789
+ const m = /^- stopped:\s*(true|false)\s*$/m.exec(text);
790
+ if (m === null)
791
+ return null;
792
+ if (m[1] === 'true') {
793
+ const pid = /^- pid:\s*(\d+)\s*$/m.exec(text)?.[1];
794
+ if (pid === undefined)
795
+ return null;
796
+ return [{ text: `stopped · pid ${pid}` }];
797
+ }
798
+ return [{ text: 'was not running', tone: 'muted' }];
799
+ }
800
+ /** `crtr canvas issue new` (src/commands/canvas-issue.ts) has no custom
801
+ * `render`; the generic renderer bullets `{created:false}` (empty note,
802
+ * line 125), `{created:true,target:'github',type,title,url}` (line 137),
803
+ * or `{created:true,target:'file',type,title,path}` (line 159). `url` is
804
+ * unconditionally set on every `target:'github'` return, and `path` on
805
+ * every `target:'file'` return — so either missing (e.g. a >80-char
806
+ * value core/render.ts's `isProse` rule pushed to prose form instead of a
807
+ * bullet) is an unrecognized shape, not a success to report without the
808
+ * field. Returns null when `created` isn't a plain boolean bullet, or the
809
+ * target's required companion field isn't present as a bullet. */
810
+ function summarizeCanvasIssueNew(text) {
811
+ const createdMatch = /^- created:\s*(true|false)\s*$/m.exec(text);
812
+ if (createdMatch === null)
813
+ return null;
814
+ if (createdMatch[1] === 'false')
815
+ return [{ text: 'nothing captured', tone: 'muted' }];
816
+ const target = /^- target:\s*(\S+)\s*$/m.exec(text)?.[1];
817
+ if (target === 'github') {
818
+ const url = /^- url:\s*(\S+)\s*$/m.exec(text)?.[1];
819
+ if (url === undefined)
820
+ return null;
821
+ const num = /\/issues\/(\d+)$/.exec(url)?.[1];
822
+ return [
823
+ { text: num !== undefined ? `issue #${num}` : 'issue filed', tone: 'success' },
824
+ { text: url, tone: 'muted' },
825
+ ];
826
+ }
827
+ if (target === 'file') {
828
+ const path = /^- path:\s*(\S+)\s*$/m.exec(text)?.[1];
829
+ if (path === undefined)
830
+ return null;
831
+ return [
832
+ { text: 'note saved', tone: 'success' },
833
+ { text: basename(path), tone: 'muted' },
834
+ ];
835
+ }
836
+ return null;
837
+ }
838
+ /** `crtr node new` (nodeNew.render, src/commands/node.ts:318) has two
839
+ * shapes: a spawn (`Spawned "${name}" (${id}) — ${status}${win}.`, with no
840
+ * `kind`/`mode` anywhere in the output) or an armed deferred spawn (`Armed
841
+ * deferred spawn ${id} — a ${kind} node, ${when}.`, one shared template
842
+ * covering --at/--every/--when alike). Returns null when neither template
843
+ * matches. */
844
+ function summarizeNodeNew(text) {
845
+ const spawned = /^Spawned "([^"]*)" \((\S+)\) — (\w+)/.exec(text);
846
+ if (spawned !== null) {
847
+ const [, name, id, status] = spawned;
848
+ return [{ text: `spawned ${name} (${id}) · ${status}`, tone: 'success' }];
849
+ }
850
+ const armed = /^Armed deferred spawn (\S+) — a (\S+) node, (.+?)\.$/m.exec(text.split('\n')[0] ?? '');
851
+ if (armed !== null) {
852
+ const [, id, kind, detail] = armed;
853
+ const fires = /fires ([^\s,]+)/.exec(detail); // stop before a trailing `, recurs …` suffix
854
+ return [
855
+ { text: fires !== null ? `trigger armed · fires ${fires[1]}` : `trigger armed · ${detail}`, tone: 'accent' },
856
+ { text: `${kind} · ${id}`, tone: 'muted' },
857
+ ];
858
+ }
859
+ return null;
860
+ }
861
+ /** `crtr node msg` (nodeMsg.render, src/commands/node.ts:1284) has two
862
+ * shapes: armed (covers --at/--every/--when/--until — one shared template
863
+ * `Armed ${mode} trigger ${id} (${type}) — ${when}, target ${target}.`) or
864
+ * immediate, whose render is JUST `${guidance}` — one of a small fixed set
865
+ * of strings from the code (not user text): `"Delivered."`, `"Delivered —
866
+ * the dormant target was revived to receive it."`, or `Fresh-revived <id>
867
+ * now…`. Anything else returns null (deadline/predicate guidance is long
868
+ * prose, but those cases are armed=true and already handled above). */
869
+ function summarizeNodeMsg(text) {
870
+ const armed = /^Armed (\S+) trigger (\S+) \((\S+)\) — (.+?), target (\S+)\.$/m.exec(text.split('\n')[0] ?? '');
871
+ if (armed !== null) {
872
+ const [, mode, id, triggerType, when, target] = armed;
873
+ const fires = /fires ([^\s,]+)/.exec(when); // stop before a trailing `, recurs …` suffix
874
+ return [
875
+ { text: fires !== null ? `trigger armed · fires ${fires[1]}` : `trigger armed · ${when}`, tone: 'accent' },
876
+ { text: `${mode} · ${triggerType} · → ${target} · ${id}`, tone: 'muted' },
877
+ ];
878
+ }
879
+ if (/^Delivered\.$/.test(text))
880
+ return [{ text: 'delivered', tone: 'success' }];
881
+ if (/^Delivered — the dormant target was revived to receive it\.$/.test(text)) {
882
+ return [{ text: 'delivered · revived', tone: 'success' }];
883
+ }
884
+ const fresh = /^Fresh-revived (\S+) now/.exec(text);
885
+ if (fresh !== null)
886
+ return [{ text: `fresh-revived ${fresh[1]}`, tone: 'success' }];
887
+ return null;
888
+ }
889
+ /** `crtr node yield` (src/commands/node.ts:1513) has no custom `render`;
890
+ * the generic renderer bullets `{node_id, promoted, model?, will_refresh}`
891
+ * — none of these names are PROSE_FIELDS. `will_refresh` is documented
892
+ * "Always true" (the command has no path that returns false), so
893
+ * `- will_refresh: false` is itself an unrecognized/error shape, not an
894
+ * alternate success to report armed. Returns null unless `will_refresh` is
895
+ * exactly `true`, or `node_id` isn't present as a plain bullet. */
896
+ function summarizeNodeYield(text) {
897
+ if (!/^- will_refresh:\s*true\s*$/m.test(text))
898
+ return null;
899
+ const nodeId = /^- node_id:\s*(\S+)\s*$/m.exec(text);
900
+ if (nodeId === null)
901
+ return null;
902
+ const promoted = /^- promoted:\s*true\s*$/m.test(text);
903
+ const model = /^- model:\s*(\S+)\s*$/m.exec(text)?.[1];
904
+ const extras = [];
905
+ if (promoted)
906
+ extras.push('promoted → orchestrator');
907
+ if (model !== undefined)
908
+ extras.push(`model: ${model}`);
909
+ const lines = [{ text: 'yield armed — refreshes on next wake', tone: 'accent' }];
910
+ if (extras.length > 0)
911
+ lines.push({ text: extras.join(' · '), tone: 'muted' });
912
+ return lines;
913
+ }
914
+ /** `crtr node lifecycle close` (nodeClose.render, src/commands/node.ts:650):
915
+ * `Closed ${id} and its exclusive subtree — ${count} node(s)
916
+ * closed${spared>0 ? ", ${spared} spared..." : ''}.` on success, or a
917
+ * short failure line — left as null there, the generic fallback is fine for
918
+ * a one-liner. */
919
+ function summarizeNodeLifecycleClose(text) {
920
+ const m = /^Closed (\S+) and its exclusive subtree — (\d+) node\(s\) closed(?:, (\d+) spared)?/.exec(text);
921
+ if (m === null)
922
+ return null;
923
+ const [, id, countStr, sparedStr] = m;
924
+ const count = Number(countStr);
925
+ const descendants = count - 1;
926
+ const lines = [{ text: `closed ${id}`, tone: 'success' }];
927
+ if (descendants > 0)
928
+ lines.push({ text: `${descendants} descendant${descendants === 1 ? '' : 's'} canceled`, tone: 'warning' });
929
+ if (sparedStr !== undefined)
930
+ lines.push({ text: `${sparedStr} spared (managed elsewhere)`, tone: 'muted' });
931
+ return lines;
932
+ }
933
+ /** `crtr node triggers list` (nodeTriggersList.render, src/commands/
934
+ * node.ts:1633): `No pending triggers in scope ${scope}.` (empty) or `${N}
935
+ * pending trigger(s) in scope ${scope}.` plus a 15-column table
936
+ * (`id|owner|target|action|mode|type|next|recur|timeout|label|eval_state|
937
+ * last_eval_at|last_exit|last_stderr|created` — the trigger-kind column is
938
+ * literally named `type`, not `trigger_type`). Returns null on any shape
939
+ * mismatch: a non-`---` separator cell, a missing `type`/`next`/`target`
940
+ * column, or a row-count/table mismatch (a sanity cross-check against the
941
+ * leading `N pending` count). */
942
+ function summarizeNodeTriggersList(text) {
943
+ if (/^No pending triggers in scope .+\.$/m.test(text.split('\n')[0] ?? '')) {
944
+ return [{ text: 'none pending', tone: 'muted' }];
945
+ }
946
+ const head = /^(\d+) pending trigger\(s\) in scope (.+)\.$/.exec(text.split('\n')[0] ?? '');
947
+ if (head === null)
948
+ return null;
949
+ const n = Number(head[1]);
950
+ const lines = text.split('\n');
951
+ const headerIdx = lines.findIndex((l) => l.trim().startsWith('| id |'));
952
+ if (headerIdx === -1)
953
+ return null;
954
+ const header = splitTableRow(lines[headerIdx]);
955
+ const separator = splitTableRow(lines[headerIdx + 1]);
956
+ if (header === null || separator === null || header.length !== separator.length)
957
+ return null;
958
+ if (separator.some((cell) => !/^-+$/.test(cell)))
959
+ return null;
960
+ if (!header.includes('type') || !header.includes('next') || !header.includes('target'))
961
+ return null;
962
+ const rows = [];
963
+ let j = headerIdx + 2;
964
+ while (j < lines.length && lines[j].trim().startsWith('|')) {
965
+ const cells = splitTableRow(lines[j]);
966
+ if (cells === null || cells.length !== header.length)
967
+ return null;
968
+ const row = {};
969
+ header.forEach((h, i) => { row[h] = cells[i]; });
970
+ rows.push(row);
971
+ j++;
972
+ }
973
+ if (rows.length !== n)
974
+ return null;
975
+ const cap = 5;
976
+ const shown = rows.slice(0, cap).map((r) => `${r['type']} · next ${r['next']} · ${r['target']}`);
977
+ const out = [{ text: `${n} pending` }];
978
+ for (const s of shown)
979
+ out.push({ text: s, tone: 'muted' });
980
+ const remaining = rows.length - shown.length;
981
+ if (remaining > 0)
982
+ out.push({ text: `… ${remaining} more`, tone: 'muted' });
983
+ return out;
984
+ }
985
+ /** `crtr feed read` (src/commands/push.ts:209) has two shapes: drained
986
+ * (`${n} unread pointer(s) drained from ${node_id}'s inbox.\n\n${digest}`,
987
+ * where the digest — `coalesce()`, src/core/feed/inbox.ts:225 — groups by
988
+ * sender as `From ${sender} — ${N} update(s):` sections) or empty (`No
989
+ * unread pointers for ${node_id} (unread=0).\n\n${explanation}`).
990
+ * `coalesce()` always groups drained entries under at least one
991
+ * From-section, so a positive drained count with zero parsed senders is a
992
+ * shape mismatch, not a 0-sender success. Returns null on any other
993
+ * shape. */
994
+ function summarizeFeedRead(text) {
995
+ const lines = text.split('\n');
996
+ const first = lines[0] ?? '';
997
+ const drained = /^(\d+) unread pointers? drained from (.+)'s inbox\.$/.exec(first);
998
+ if (drained !== null) {
999
+ const n = Number(drained[1]);
1000
+ const senders = [...new Set([...text.matchAll(/^From (.+?) — \d+ update/gm)].map((m) => m[1]))];
1001
+ if (n > 0 && senders.length === 0)
1002
+ return null;
1003
+ const out = [{ text: `${n} pointer${n === 1 ? '' : 's'} drained from ${senders.length} node${senders.length === 1 ? '' : 's'}` }];
1004
+ if (senders.length > 0) {
1005
+ const shown = senders.slice(0, 5);
1006
+ const remaining = senders.length - shown.length;
1007
+ out.push({ text: remaining > 0 ? `${shown.join(', ')}, … ${remaining} more` : shown.join(', '), tone: 'muted' });
1008
+ }
1009
+ return out;
1010
+ }
1011
+ if (/^No unread pointers for .+ \(unread=0\)\.$/.test(first)) {
1012
+ return [{ text: 'nothing unread', tone: 'muted' }];
1013
+ }
1014
+ return null;
1015
+ }
1016
+ /** Teaser cap for `feed message`'s body preview — mirrors
1017
+ * MEMORY_READ_CONTENT_LINES's role for a different leaf. */
1018
+ const FEED_MESSAGE_TEASER_LINES = 3;
1019
+ /** `crtr feed message` (src/commands/push.ts:264) has two shapes: found
1020
+ * (`Full message ${id} (from ${from}):\n\n${body}`) or not-found (a short
1021
+ * line — left as null, the generic fallback is fine for one line). */
1022
+ function summarizeFeedMessage(text) {
1023
+ const lines = text.split('\n');
1024
+ const head = /^Full message (\S+) \(from (.+)\):$/.exec(lines[0] ?? '');
1025
+ if (head === null)
1026
+ return null;
1027
+ const [, id, from] = head;
1028
+ const bodyLines = lines.slice(1);
1029
+ let idx = 0;
1030
+ while (idx < bodyLines.length && bodyLines[idx].trim() === '')
1031
+ idx++;
1032
+ const content = bodyLines.slice(idx);
1033
+ const teaser = content.slice(0, FEED_MESSAGE_TEASER_LINES);
1034
+ const hasMore = content.slice(FEED_MESSAGE_TEASER_LINES).some((l) => l.trim() !== '');
1035
+ if (hasMore)
1036
+ teaser.push('…');
1037
+ const out = [{ text: `${id} · from ${from}` }];
1038
+ if (teaser.some((l) => l.trim() !== '')) {
1039
+ out.push({ text: '' });
1040
+ for (const l of teaser)
1041
+ out.push({ text: l });
1042
+ }
1043
+ return out;
1044
+ }
1045
+ /** `crtr feed peek` (src/commands/push.ts:351) has two shapes: zero-kids
1046
+ * (`No nodes below you (0 subscriptions, ${unread} unread). ${tail}` — the
1047
+ * "0" is a hardcoded literal, not a variable) or nonzero (`${verdictLine}
1048
+ * \n\n${N} node(s) below you — ${working} working, ${done} done, ${dead}
1049
+ * dead, ${unread} unread:\n${rows}`). Returns null on any other shape. */
1050
+ function summarizeFeedPeek(text) {
1051
+ const zero = /^No nodes below you \(0 subscriptions, (\d+) unread\)\./.exec(text.split('\n')[0] ?? '');
1052
+ if (zero !== null) {
1053
+ const unread = Number(zero[1]);
1054
+ return unread > 0
1055
+ ? [{ text: `0 below · ${unread} unread`, tone: 'warning' }]
1056
+ : [{ text: 'PASS · nothing below', tone: 'success' }];
1057
+ }
1058
+ const m = /(\d+) nodes? below you — (\d+) working, (\d+) done, (\d+) dead, (\d+) unread:/.exec(text);
1059
+ if (m === null)
1060
+ return null;
1061
+ const [, , workingStr, doneStr, deadStr, unreadStr] = m;
1062
+ const working = Number(workingStr);
1063
+ const done = Number(doneStr);
1064
+ const dead = Number(deadStr);
1065
+ const unread = Number(unreadStr);
1066
+ const parts = [`${working} working`, `${done} done`];
1067
+ if (dead > 0)
1068
+ parts.push(`${dead} dead`);
1069
+ const lines = [{ text: parts.join(' · ') }];
1070
+ if (dead > 0)
1071
+ lines.push({ text: `${dead} dead — will not wake you`, tone: 'warning' });
1072
+ else if (unread > 0)
1073
+ lines.push({ text: `${unread} unread`, tone: 'warning' });
1074
+ return lines;
1075
+ }
1076
+ /** `crtr push final` (makeTierLeaf's shared render, src/commands/push.ts:161)
1077
+ * leads with a fixed sentence ('Result recorded — node finished; ...')
1078
+ * followed unconditionally by `\n\n- report: ${report_path}` (`report_path`
1079
+ * is a required output field, push.ts:94). Returns null when that lead
1080
+ * sentence isn't present, or the `- report:` line is missing (an
1081
+ * unrecognized/error shape either way). */
1082
+ function summarizePushFinal(text) {
1083
+ if (!/^Result recorded — node finished/.test(text))
1084
+ return null;
1085
+ const report = /^- report: (.+)$/m.exec(text)?.[1];
1086
+ if (report === undefined)
1087
+ return null;
1088
+ return [
1089
+ { text: 'final pushed · node done', tone: 'success' },
1090
+ { text: basename(report.trim()), tone: 'muted' },
1091
+ ];
1092
+ }
1093
+ /** `crtr push urgent` (makeTierLeaf's shared render, src/commands/push.ts:161):
1094
+ * `Urgent report fanned to ${n} subscriber(s) — they are force-woken.\n\n-
1095
+ * report: ${report_path}`, with the same unconditional `- report:` line as
1096
+ * push final. Returns null when the lead sentence doesn't match, or the
1097
+ * `- report:` line is missing. */
1098
+ function summarizePushUrgent(text) {
1099
+ const m = /^Urgent report fanned to (\d+) subscriber/.exec(text);
1100
+ if (m === null)
1101
+ return null;
1102
+ if (/^- report: (.+)$/m.exec(text) === null)
1103
+ return null;
1104
+ return [{ text: `urgent → ${m[1]} subscribers woken`, tone: 'warning' }];
1105
+ }
1106
+ // ---------------------------------------------------------------------------
1107
+ // Batch C — human/memory/pkg. Shared helpers for the two output shapes
1108
+ // renderResult (core/render.ts) can give the SAME logical field depending on
1109
+ // string length/newlines (isProse): a short value is a `- field: value`
1110
+ // bullet; a long/multiline one is prose — labeled `**field:** text` when 2+
1111
+ // prose fields are present, or a bare unlabeled paragraph when it's the SOLE
1112
+ // prose field (renderResult only labels when there's more than one). Several
1113
+ // leaves below carry a field (a path, typically) that crosses this threshold
1114
+ // depending on the user's home directory length, so both forms are handled
1115
+ // rather than assumed.
1116
+ // ---------------------------------------------------------------------------
1117
+ /** Read a `- field: value` bullet or a `**field:** text` labeled-prose block
1118
+ * for the same logical field — the two shapes renderResult can produce once
1119
+ * 2+ prose fields are present (see block comment above). Does NOT recognize
1120
+ * a bare unlabeled solo-prose paragraph (see soloProseAfterBullets for
1121
+ * that); a leaf with exactly one long field needs that instead. */
1122
+ function readFieldFlexible(text, field) {
1123
+ const bullet = new RegExp(`^-\\s+${field}:\\s*(.+)$`, 'm').exec(text);
1124
+ if (bullet !== null)
1125
+ return bullet[1].trim();
1126
+ const labeled = new RegExp(`^\\*\\*${field}:\\*\\*\\s*([\\s\\S]*?)(?=\\n\\n|$)`, 'm').exec(text);
1127
+ return labeled !== null ? labeled[1].trim() : undefined;
1128
+ }
1129
+ /** Recover the SOLE prose field's text when it renders as a bare, unlabeled
1130
+ * paragraph after the bullets block — renderResult's shape for exactly one
1131
+ * long/multiline field among otherwise-short scalars (see block comment
1132
+ * above). Requires at least one bullet line above it (otherwise this isn't
1133
+ * the bullets-then-solo-paragraph shape at all) and bails if what follows
1134
+ * looks like a labeled prose block or an array heading instead of a bare
1135
+ * paragraph — never misparse a different shape as this one. */
1136
+ function soloProseAfterBullets(text) {
1137
+ const lines = text.split('\n');
1138
+ let i = 0;
1139
+ while (i < lines.length && /^- /.test(lines[i]))
1140
+ i++;
1141
+ if (i === 0)
1142
+ return undefined;
1143
+ while (i < lines.length && lines[i].trim() === '')
1144
+ i++;
1145
+ if (i >= lines.length)
1146
+ return undefined;
1147
+ if (/^\*\*\w+:\*\*/.test(lines[i]) || /^\d+ \S+:$/.test(lines[i]) || /^0 \S+\.$/.test(lines[i]))
1148
+ return undefined;
1149
+ const para = [];
1150
+ while (i < lines.length && lines[i].trim() !== '') {
1151
+ para.push(lines[i]);
1152
+ i++;
1153
+ }
1154
+ const text2 = para.join(' ').trim();
1155
+ return text2 === '' ? undefined : text2;
1156
+ }
1157
+ /** Find the first argv token after `skip` that isn't a flag, skipping the
1158
+ * value token that follows any flag listed in `valueFlags` (a boolean flag
1159
+ * with no separate value is skipped on its own via the `startsWith('--')`
1160
+ * check). Generalizes firstPositionalAfter (above) for leaves that mix
1161
+ * boolean and value-taking flags ahead of their positional. */
1162
+ function firstPositionalArg(tokens, skip, valueFlags) {
1163
+ for (let i = skip; i < tokens.length; i++) {
1164
+ const t = tokens[i];
1165
+ if (t.startsWith('--')) {
1166
+ if (valueFlags.has(t))
1167
+ i++;
1168
+ continue;
1169
+ }
1170
+ return t;
1171
+ }
1172
+ return undefined;
1173
+ }
1174
+ /** Truncate a single line of text to `max` chars for a muted-aside teaser,
1175
+ * appending an ellipsis when it was cut. */
1176
+ function truncateLine(line, max) {
1177
+ return line.length > max ? `${line.slice(0, max).trimEnd()}…` : line;
1178
+ }
1179
+ const ASK_TEASER_CHARS = 160;
1180
+ /** `crtr human ask` (humanAsk.run, src/commands/human/prompts.ts:97) returns
1181
+ * `{job_id, dir, follow_up}` — `job_id` is always short (a node id) so it
1182
+ * always bullets; `dir`/`follow_up` cross the prose threshold depending on
1183
+ * path length, so the shape is recognized by `job_id` plus one of the two
1184
+ * FIXED follow_up sentences (followUpResult/followUpDrain, shared.ts) being
1185
+ * present anywhere in the text — never by assuming dir/follow_up's bullet
1186
+ * vs. prose form. The question text isn't in the output at all (only the
1187
+ * bare-question invocation echoes it), so it's recovered from argv: the
1188
+ * first non-flag token after `human ask`, skipping `--context-file`'s value
1189
+ * — absent for a `--context-file` deck invocation, which still gets the
1190
+ * road-sign line with no question teaser. Returns null when neither
1191
+ * follow_up sentence is present (an error/unrecognized shape). */
1192
+ function summarizeHumanAsk(text, tokens) {
1193
+ const jobId = /^- job_id:\s*(\S+)\s*$/m.exec(text)?.[1];
1194
+ if (jobId === undefined)
1195
+ return null;
1196
+ const drained = text.includes('Not in tmux: a human must drain it');
1197
+ const delivered = text.includes('no need to poll');
1198
+ if (!drained && !delivered)
1199
+ return null;
1200
+ const lines = drained
1201
+ ? [{ text: 'ask posted — drain via `human inbox`', tone: 'warning' }]
1202
+ : [{ text: 'ask posted → waiting on human', tone: 'accent' }];
1203
+ const question = firstPositionalArg(tokens, 2, new Set(['--context-file']));
1204
+ if (question !== undefined) {
1205
+ const firstLine = truncateLine(question.split('\n')[0].trim(), ASK_TEASER_CHARS);
1206
+ if (firstLine !== '')
1207
+ lines.push({ text: firstLine, tone: 'muted' });
1208
+ }
1209
+ return lines;
1210
+ }
1211
+ /** `crtr human review` (humanReview.run, src/commands/human/prompts.ts:169)
1212
+ * returns `{job_id, dir, output, follow_up}` — same recognition strategy as
1213
+ * summarizeHumanAsk (job_id bullet + one of the two fixed follow_up
1214
+ * sentences, this time followUpReview/followUpDrain). The reviewed file
1215
+ * isn't in the output either, so its basename is recovered from argv (the
1216
+ * file positional, skipping `--output`'s value). Returns null when neither
1217
+ * follow_up sentence is present. */
1218
+ function summarizeHumanReview(text, tokens) {
1219
+ const jobId = /^- job_id:\s*(\S+)\s*$/m.exec(text)?.[1];
1220
+ if (jobId === undefined)
1221
+ return null;
1222
+ const drained = text.includes('Not in tmux: a human must drain it');
1223
+ const live = text.includes("live on the human's screen for anchored, line-by-line review");
1224
+ if (!drained && !live)
1225
+ return null;
1226
+ const file = firstPositionalArg(tokens, 2, new Set(['--output']));
1227
+ const label = file !== undefined ? `review posted: ${basename(file)}` : 'review posted';
1228
+ return drained
1229
+ ? [{ text: `${label} — drain via \`human inbox\``, tone: 'warning' }]
1230
+ : [{ text: `${label} → waiting on human`, tone: 'accent' }];
1231
+ }
1232
+ /** `crtr human list` (humanList.run, src/commands/human/queue.ts:283) has no
1233
+ * custom `render`; the generic renderer bullets `- total: N` then an
1234
+ * `N items:` table (or `0 items.`), columns including `kind` (the deck's
1235
+ * own interaction kind — `decision`/`question`/etc, often empty for a plain
1236
+ * freeform ask; NOT whether it was posed via `ask` vs `review`). Returns
1237
+ * null when the total/items shape doesn't match, or when `total` is
1238
+ * nonzero but the returned page is empty (exhausted pagination, not a
1239
+ * true zero total — collapsing that to "none pending" would misreport). */
1240
+ function summarizeHumanList(text) {
1241
+ const total = parseIntBullet(text, 'total');
1242
+ const rows = parseTableRows(text, 'items');
1243
+ if (total === null || rows === null)
1244
+ return null;
1245
+ if (total === 0)
1246
+ return [{ text: 'none pending', tone: 'success' }];
1247
+ if (rows.length === 0)
1248
+ return null;
1249
+ const kinds = new Map();
1250
+ for (const row of rows) {
1251
+ const kind = row['kind'];
1252
+ if (kind !== undefined && kind !== '')
1253
+ kinds.set(kind, (kinds.get(kind) ?? 0) + 1);
1254
+ }
1255
+ const lines = [{ text: `${total} pending` }];
1256
+ const kindDigest = summarizeTopCounts(kinds, 5, (k, c) => `${k}×${c}`);
1257
+ if (kindDigest !== '')
1258
+ lines.push({ text: kindDigest, tone: 'muted' });
1259
+ return lines;
1260
+ }
1261
+ const DECK_PROMPT_TEASER_CHARS = 160;
1262
+ /** `crtr human deck` (humanDeck.run, src/commands/human/queue.ts:319) bullets
1263
+ * `id`/`interaction_count` (always short) then an `N interactions:` table
1264
+ * (columns `id,kind,prompt,options,allow_freetext`). `options` is a
1265
+ * cellEsc'd array of JSON-stringified `{id,label,...}` objects joined by
1266
+ * `, ` (core/render.ts's scalarStr for an array of objects) — counted via
1267
+ * the `"id":` occurrences each option object carries, not by splitting on
1268
+ * `, ` (a label/description could itself contain a comma). Only the FIRST
1269
+ * interaction is teased — decks built by `ask`/`review` always have exactly
1270
+ * one; a multi-interaction context-file deck gets an `interaction_count`
1271
+ * aside instead of every prompt. Returns null when the `id`/
1272
+ * `interaction_count` bullets or the interactions table don't match, or
1273
+ * when `allow_freetext` isn't exactly `true`/`false` (a malformed/
1274
+ * truncated row must not silently default to "no freetext"). */
1275
+ function summarizeHumanDeck(text) {
1276
+ const id = /^- id:\s*(\S+)\s*$/m.exec(text)?.[1];
1277
+ const interactionCount = parseIntBullet(text, 'interaction_count');
1278
+ if (id === undefined || interactionCount === null)
1279
+ return null;
1280
+ const rows = parseTableRows(text, 'interactions');
1281
+ if (rows === null || rows.length === 0)
1282
+ return null;
1283
+ const first = rows[0];
1284
+ const prompt = first['prompt'];
1285
+ if (prompt === undefined || prompt === '')
1286
+ return null;
1287
+ const optionCount = (first['options'] ?? '').match(/"id":/g)?.length ?? 0;
1288
+ const allowFreetextRaw = first['allow_freetext'];
1289
+ if (allowFreetextRaw !== 'true' && allowFreetextRaw !== 'false')
1290
+ return null;
1291
+ const allowFreetext = allowFreetextRaw === 'true';
1292
+ const lines = [{ text: truncateLine(prompt, DECK_PROMPT_TEASER_CHARS) }];
1293
+ const parts = [];
1294
+ if (optionCount > 0)
1295
+ parts.push(`${optionCount} option${optionCount === 1 ? '' : 's'}`);
1296
+ if (allowFreetext)
1297
+ parts.push('freetext');
1298
+ if (interactionCount > 1)
1299
+ parts.push(`${interactionCount} interactions`);
1300
+ if (parts.length > 0)
1301
+ lines.push({ text: parts.join(' · '), tone: 'muted' });
1302
+ return lines;
1303
+ }
1304
+ /** `crtr memory list` (listLeaf.run, src/commands/memory/list.ts:65) has no
1305
+ * custom `render`; the generic renderer prints the `follow_up` prose then
1306
+ * an `N items:` table (columns `name,title,short_form,kind,scope,path,
1307
+ * is_dir`). No pagination on this leaf — `rows.length` IS the true total.
1308
+ * Returns null when the items table doesn't match, or is empty (an
1309
+ * empty corpus has nothing to digest into a breakdown). */
1310
+ function summarizeMemoryList(text) {
1311
+ const rows = parseTableRows(text, 'items');
1312
+ if (rows === null || rows.length === 0)
1313
+ return null;
1314
+ const kinds = new Map();
1315
+ const scopes = new Map();
1316
+ for (const row of rows) {
1317
+ const kind = row['kind'];
1318
+ const scope = row['scope'];
1319
+ if (kind === undefined || kind === '' || scope === undefined || scope === '')
1320
+ return null;
1321
+ kinds.set(kind, (kinds.get(kind) ?? 0) + 1);
1322
+ scopes.set(scope, (scopes.get(scope) ?? 0) + 1);
1323
+ }
1324
+ const kindDigest = summarizeTopCounts(kinds, 5, (k, c) => `${k}×${c}`);
1325
+ const scopeDigest = summarizeTopCounts(scopes, 5, (s, c) => `${s}×${c}`);
1326
+ if (kindDigest === '' || scopeDigest === '')
1327
+ return null;
1328
+ return [
1329
+ { text: `${rows.length} docs` },
1330
+ { text: `${kindDigest} / ${scopeDigest}`, tone: 'muted' },
1331
+ ];
1332
+ }
1333
+ const MEMORY_FIND_NAME_CAP = 5;
1334
+ /** `crtr memory find` (findLeaf.run, src/commands/memory/find.ts:141) always
1335
+ * bullets `- query: <query>` then prose follow_up, then either `0 hits.` or
1336
+ * an `N hits:` table. Ranked mode's table has a `name` column; `--grep`
1337
+ * mode's does not (`path,line,text` instead) — recognized and rejected
1338
+ * (null, generic fallback) rather than digested, since a grep hit has no
1339
+ * document identity to name. Returns null when the query bullet or the
1340
+ * hits table (in ranked-mode shape) don't match. */
1341
+ function summarizeMemoryFind(text) {
1342
+ const query = /^- query:\s*(.+)$/m.exec(text)?.[1]?.trim();
1343
+ if (query === undefined)
1344
+ return null;
1345
+ if (/^0 hits\.$/m.test(text))
1346
+ return [{ text: `0 hits for "${query}"`, tone: 'muted' }];
1347
+ const rows = parseTableRows(text, 'hits');
1348
+ if (rows === null || rows.length === 0)
1349
+ return null;
1350
+ const names = rows.map((r) => r['name']);
1351
+ if (names.some((n) => n === undefined || n === ''))
1352
+ return null;
1353
+ const shown = names.slice(0, MEMORY_FIND_NAME_CAP);
1354
+ const remaining = names.length - shown.length;
1355
+ const namesLine = remaining > 0 ? `${shown.join(', ')}, … ${remaining} more` : shown.join(', ');
1356
+ return [
1357
+ { text: `${rows.length} hit${rows.length === 1 ? '' : 's'} for "${query}"` },
1358
+ { text: namesLine, tone: 'muted' },
1359
+ ];
1360
+ }
1361
+ /** `crtr memory write` (writeLeaf.run, src/commands/memory/write.ts:123)
1362
+ * bullets `name`/`kind`/`scope`/`created` (always short); `path` and
1363
+ * `follow_up` are always long, so they render as the two labeled prose
1364
+ * blocks (`**path:**`/`**follow_up:**`) — read via readFieldFlexible.
1365
+ * Returns null when `name` or `created` (a plain boolean) isn't present as
1366
+ * a bullet — an error/unrecognized shape. */
1367
+ function summarizeMemoryWrite(text) {
1368
+ const name = /^- name:\s*(\S+)\s*$/m.exec(text)?.[1];
1369
+ const createdMatch = /^- created:\s*(true|false)\s*$/m.exec(text);
1370
+ if (name === undefined || createdMatch === null)
1371
+ return null;
1372
+ const created = createdMatch[1] === 'true';
1373
+ const lines = [{ text: `${created ? 'wrote' : 'updated'} ${name}`, tone: 'success' }];
1374
+ const path = readFieldFlexible(text, 'path');
1375
+ if (path !== undefined)
1376
+ lines.push({ text: path, tone: 'muted' });
1377
+ return lines;
1378
+ }
1379
+ /** `crtr pkg plugin install` (pluginInstall.run, src/commands/pkg/
1380
+ * plugin-manage.ts:280) has no custom `render`; the generic renderer
1381
+ * bullets `name`/`scope` (always short) then `path` — almost always long
1382
+ * enough to cross the prose threshold, and since it's the SOLE prose field
1383
+ * it renders as a bare unlabeled paragraph (see soloProseAfterBullets),
1384
+ * not a labeled one. Falls back to a `- path:` bullet for the rare
1385
+ * short-path case. Returns null when `name`/`scope` aren't present as
1386
+ * bullets — an error/unrecognized shape (path is a muted aside, not
1387
+ * required for a recognized digest). */
1388
+ function summarizePkgPluginInstall(text) {
1389
+ const name = /^- name:\s*(\S+)\s*$/m.exec(text)?.[1];
1390
+ const scope = /^- scope:\s*(\S+)\s*$/m.exec(text)?.[1];
1391
+ if (name === undefined || scope === undefined)
1392
+ return null;
1393
+ const path = readFieldFlexible(text, 'path') ?? soloProseAfterBullets(text);
1394
+ return [
1395
+ { text: `installed ${name}`, tone: 'success' },
1396
+ { text: path !== undefined ? `${scope} · ${path}` : scope, tone: 'muted' },
1397
+ ];
1398
+ }
1399
+ /** `crtr pkg plugin update` (pluginUpdate.run, src/commands/pkg/
1400
+ * plugin-manage.ts:349) has no custom `render`; the generic renderer emits
1401
+ * an `N updated:` table (columns `name,updated,sha`) — one row when
1402
+ * `--name` targets a single plugin, one row per installed plugin
1403
+ * otherwise. There is no version field anywhere in this output (unlike
1404
+ * command-icons.md's hypothetical version-delta wording) — `sha` (short,
1405
+ * 7 chars) is the only content available to show. Returns null when the
1406
+ * table doesn't match this shape, or when any row's `updated` cell isn't
1407
+ * exactly `true`/`false` (the multi-row loop mirrors the single-row
1408
+ * validation above — a malformed cell must not be silently counted as
1409
+ * "already current"). */
1410
+ function summarizePkgPluginUpdate(text) {
1411
+ const rows = parseTableRows(text, 'updated');
1412
+ if (rows === null)
1413
+ return null;
1414
+ if (rows.length === 0)
1415
+ return [{ text: 'nothing to update', tone: 'muted' }];
1416
+ if (rows.length === 1) {
1417
+ const row = rows[0];
1418
+ const name = row['name'];
1419
+ const updated = row['updated'];
1420
+ if (name === undefined || (updated !== 'true' && updated !== 'false'))
1421
+ return null;
1422
+ const sha = (row['sha'] ?? '').slice(0, 7);
1423
+ const lines = [
1424
+ updated === 'true' ? { text: `updated ${name}`, tone: 'success' } : { text: `${name} already current`, tone: 'muted' },
1425
+ ];
1426
+ if (sha !== '')
1427
+ lines.push({ text: sha, tone: 'muted' });
1428
+ return lines;
1429
+ }
1430
+ let updatedCount = 0;
1431
+ for (const row of rows) {
1432
+ const updated = row['updated'];
1433
+ if (updated !== 'true' && updated !== 'false')
1434
+ return null;
1435
+ if (updated === 'true')
1436
+ updatedCount++;
1437
+ }
1438
+ const current = rows.length - updatedCount;
1439
+ return [{ text: `${updatedCount} updated · ${current} already current` }];
1440
+ }
1441
+ const PKG_LIST_NAME_CAP = 8;
1442
+ /** `crtr pkg plugin list` (pluginList.run, src/commands/pkg/
1443
+ * plugin-inspect.ts:12) bullets `- total: N` then an `N items:` table
1444
+ * (columns `name,scope,version,enabled,source_marketplace,path`).
1445
+ * `enabled`/name counts are read from the RETURNED page only — with the
1446
+ * default 50-per-page limit this matches `total` in practice, but a
1447
+ * narrower `--limit` would undercount enabled/shown names against the
1448
+ * true total (the same page-vs-total caveat other paginated summaries in
1449
+ * this file carry). Returns null when the total/items shape doesn't
1450
+ * match, or when `total` is nonzero but the returned page is empty
1451
+ * (exhausted pagination, not a true zero total). */
1452
+ function summarizePkgPluginList(text) {
1453
+ const total = parseIntBullet(text, 'total');
1454
+ const rows = parseTableRows(text, 'items');
1455
+ if (total === null || rows === null)
1456
+ return null;
1457
+ if (total === 0)
1458
+ return [{ text: '0 plugins', tone: 'muted' }];
1459
+ if (rows.length === 0)
1460
+ return null;
1461
+ let enabledCount = 0;
1462
+ const names = [];
1463
+ for (const row of rows) {
1464
+ const name = row['name'];
1465
+ const enabled = row['enabled'];
1466
+ if (name === undefined || name === '' || (enabled !== 'true' && enabled !== 'false'))
1467
+ return null;
1468
+ if (enabled === 'true')
1469
+ enabledCount++;
1470
+ names.push(name);
1471
+ }
1472
+ const shown = names.slice(0, PKG_LIST_NAME_CAP);
1473
+ const remaining = names.length - shown.length;
1474
+ const namesLine = remaining > 0 ? `${shown.join(', ')}, … ${remaining} more` : shown.join(', ');
1475
+ return [
1476
+ { text: `${total} plugins · ${enabledCount} enabled` },
1477
+ { text: namesLine, tone: 'muted' },
1478
+ ];
1479
+ }
1480
+ /** `crtr pkg plugin show` (pluginShow.run, src/commands/pkg/
1481
+ * plugin-inspect.ts:76) bullets `name`/`scope`/`path`/`enabled` (path is
1482
+ * usually short here — a plugin dir, not a nested interaction path — so
1483
+ * stays a bullet) then `manifest` — an OBJECT, so core/render.ts's
1484
+ * scalarStr always JSON.stringifies it to a bullet regardless of length
1485
+ * (isProse only ever triggers for strings) — then an `N docs:` table (or
1486
+ * `0 docs.`). `version` is read from the parsed manifest JSON, not a
1487
+ * top-level field. Returns null when `name`/`enabled`/`manifest` don't
1488
+ * match, `manifest` isn't valid JSON, or the docs count line is missing. */
1489
+ function summarizePkgPluginShow(text) {
1490
+ const name = /^- name:\s*(\S+)\s*$/m.exec(text)?.[1];
1491
+ const enabledMatch = /^- enabled:\s*(true|false)\s*$/m.exec(text);
1492
+ const manifestMatch = /^- manifest:\s*(\{.*\})\s*$/m.exec(text);
1493
+ if (name === undefined || enabledMatch === null || manifestMatch === null)
1494
+ return null;
1495
+ let manifest;
1496
+ try {
1497
+ manifest = JSON.parse(manifestMatch[1]);
1498
+ }
1499
+ catch {
1500
+ return null;
1501
+ }
1502
+ const version = manifest !== null && typeof manifest === 'object' && 'version' in manifest && typeof manifest.version === 'string'
1503
+ ? manifest.version
1504
+ : undefined;
1505
+ const docsCount = /^0 docs\.$/m.test(text) ? 0 : Number(/^(\d+) docs:$/m.exec(text)?.[1] ?? NaN);
1506
+ if (Number.isNaN(docsCount))
1507
+ return null;
1508
+ const enabled = enabledMatch[1] === 'true';
1509
+ return [
1510
+ { text: `${name}${version !== undefined ? `@${version}` : ''} · ${enabled ? 'enabled' : 'disabled'}`, tone: enabled ? 'success' : 'muted' },
1511
+ { text: `${docsCount} substrate doc${docsCount === 1 ? '' : 's'}`, tone: 'muted' },
1512
+ ];
1513
+ }
1514
+ /** `crtr pkg market list` (marketList.run, src/commands/pkg/
1515
+ * market-inspect.ts:14) bullets `- total: N` then an `N items:` table
1516
+ * (columns `name,scope,url,ref,path`). Returns null when the total/items
1517
+ * shape doesn't match, or when `total` is nonzero but the returned page
1518
+ * is empty (exhausted pagination, not a true zero total). */
1519
+ function summarizePkgMarketList(text) {
1520
+ const total = parseIntBullet(text, 'total');
1521
+ const rows = parseTableRows(text, 'items');
1522
+ if (total === null || rows === null)
1523
+ return null;
1524
+ if (total === 0)
1525
+ return [{ text: '0 marketplaces', tone: 'muted' }];
1526
+ if (rows.length === 0)
1527
+ return null;
1528
+ const names = rows.map((r) => r['name']);
1529
+ if (names.some((n) => n === undefined || n === ''))
1530
+ return null;
1531
+ return [
1532
+ { text: `${total} marketplaces` },
1533
+ { text: names.join(', '), tone: 'muted' },
1534
+ ];
1535
+ }
1536
+ const MARKET_BROWSE_NAME_CAP = 8;
1537
+ /** `crtr pkg market browse` (marketBrowse.run, src/commands/pkg/
1538
+ * market-inspect.ts:77) bullets `marketplace`/`total` then an `N items:`
1539
+ * table (columns include `name,ref,source,version,description,keywords,
1540
+ * installed,installed_scope`). Returns null when the marketplace/total
1541
+ * bullets or the items table don't match, or when `total` is nonzero but
1542
+ * the returned page is empty (exhausted pagination, not a true zero
1543
+ * total). */
1544
+ function summarizePkgMarketBrowse(text) {
1545
+ const market = /^- marketplace:\s*(\S+)\s*$/m.exec(text)?.[1];
1546
+ const total = parseIntBullet(text, 'total');
1547
+ const rows = parseTableRows(text, 'items');
1548
+ if (market === undefined || total === null || rows === null)
1549
+ return null;
1550
+ if (total === 0)
1551
+ return [{ text: `0 plugins in ${market}`, tone: 'muted' }];
1552
+ if (rows.length === 0)
1553
+ return null;
1554
+ const names = rows.map((r) => r['name']);
1555
+ if (names.some((n) => n === undefined || n === ''))
1556
+ return null;
1557
+ const shown = names.slice(0, MARKET_BROWSE_NAME_CAP);
1558
+ const remaining = names.length - shown.length;
1559
+ const namesLine = remaining > 0 ? `${shown.join(', ')}, … ${remaining} more` : shown.join(', ');
1560
+ return [
1561
+ { text: `${total} plugins in ${market}` },
1562
+ { text: namesLine, tone: 'muted' },
1563
+ ];
1564
+ }
1565
+ // ---------------------------------------------------------------------------
1566
+ // Batch D — search/sys/surface. search's three leaves share a rendering
1567
+ // convention (a numbered `### i. title` heading per item, an optional meta
1568
+ // line, then highlight bullets or a text blockquote — see exa.ts's
1569
+ // renderResult) and a fixed, unconditional follow_up sentence per leaf that
1570
+ // doubles as a shape boundary; sys/surface leaves have no shared shape and
1571
+ // are handled independently below.
1572
+ // ---------------------------------------------------------------------------
1573
+ const SEARCH_RESULT_CAP = 3;
1574
+ /** Pull a bare host out of a search result's meta line (renderResult,
1575
+ * src/commands/search/exa.ts:150 — `url · date · author · via source`, url
1576
+ * first when present) for a compact `title — domain` digest row. Returns
1577
+ * undefined when the line isn't a `https?://` url — a highlight bullet with
1578
+ * no meta line at all, or a meta line that leads with the date/author
1579
+ * because url was absent. */
1580
+ function extractDomain(metaLine) {
1581
+ const first = (metaLine.split(' · ')[0] ?? '').trim();
1582
+ const m = /^https?:\/\/(?:www\.)?([^/\s]+)/.exec(first);
1583
+ return m?.[1];
1584
+ }
1585
+ /** `search web`'s follow_up (src/commands/search/web.ts:56–57) is a FIXED
1586
+ * string every call appends after the results (or `No results…`) line —
1587
+ * its presence is required below so a corrupted/foreign document that
1588
+ * happens to start with a matching lead line still can't be mistaken for
1589
+ * this leaf's real output. */
1590
+ const SEARCH_WEB_FOLLOWUP_MARKER = 'Fetch full content for selected result URLs with the contents leaf after checking its schema.';
1591
+ /** `crtr search web` (webLeaf.render, src/commands/search/web.ts:70) has two
1592
+ * shapes: `No results for "<query>".` (zero hits) or `${N} results for
1593
+ * "${query}":` followed by one `### i. title` heading per result (each
1594
+ * optionally followed by a meta line — see extractDomain) then highlight
1595
+ * bullets or a text blockquote, blank-line separated, then the fixed
1596
+ * follow_up road sign (see SEARCH_WEB_FOLLOWUP_MARKER). Returns null when
1597
+ * the follow_up marker is absent, the lead line matches neither shape, or
1598
+ * the parsed heading count doesn't equal the declared total (a pi
1599
+ * bash-tool truncation mid-list, or an unrecognized layout). */
1600
+ function summarizeSearchWeb(text) {
1601
+ if (!text.includes(SEARCH_WEB_FOLLOWUP_MARKER))
1602
+ return null;
1603
+ const firstLine = text.split('\n')[0] ?? '';
1604
+ const noResults = /^No results for "(.+)"\.$/.exec(firstLine);
1605
+ if (noResults !== null)
1606
+ return [{ text: `0 results for "${noResults[1]}"`, tone: 'muted' }];
1607
+ const head = /^(\d+) results for ".+":$/.exec(firstLine);
1608
+ if (head === null)
1609
+ return null;
1610
+ const count = Number(head[1]);
1611
+ const lines = text.split('\n');
1612
+ const items = [];
1613
+ for (let i = 0; i < lines.length; i++) {
1614
+ const h = /^### \d+\. (.+)$/.exec(lines[i]);
1615
+ if (h === null)
1616
+ continue;
1617
+ items.push({ title: h[1], domain: extractDomain(lines[i + 1] ?? '') });
1618
+ }
1619
+ if (items.length !== count)
1620
+ return null;
1621
+ const top = items.slice(0, SEARCH_RESULT_CAP).map((it) => (it.domain !== undefined ? `${it.title} — ${it.domain}` : it.title));
1622
+ const out = [{ text: `${count} result${count === 1 ? '' : 's'}` }];
1623
+ for (const t of top)
1624
+ out.push({ text: t, tone: 'muted' });
1625
+ const remaining = items.length - top.length;
1626
+ if (remaining > 0)
1627
+ out.push({ text: `… ${remaining} more`, tone: 'muted' });
1628
+ return out;
1629
+ }
1630
+ /** Fixed lead sentence of `search answer`'s follow_up (src/commands/search/
1631
+ * answer.ts:39) — the SAME string on every call (no branching), so its
1632
+ * presence marks where the answer body ends and the follow_up road sign
1633
+ * begins, regardless of whether citations are present. */
1634
+ const SEARCH_ANSWER_FOLLOWUP_MARKER = 'Read any cited source in full with the contents leaf';
1635
+ /** `crtr search answer` (answerLeaf.render, src/commands/search/answer.ts:52)
1636
+ * has two shapes: `No answer was returned.` (empty) or `${answer}\n\n` then,
1637
+ * when citations exist, `Sources (${N}):` plus one numbered line per
1638
+ * citation and a blank line, then unconditionally the fixed follow_up
1639
+ * sentence. Citations are optional; the follow_up marker is not, so it
1640
+ * locates the answer/Sources boundary either way. Mirrors
1641
+ * summarizeMemoryRead's teaser+ellipsis shape for the answer body. Returns
1642
+ * null when the follow_up marker is absent (unrecognized shape) or the
1643
+ * recovered answer text is empty. */
1644
+ function summarizeSearchAnswer(text) {
1645
+ const followUpIdx = text.indexOf(SEARCH_ANSWER_FOLLOWUP_MARKER);
1646
+ if (followUpIdx === -1)
1647
+ return null;
1648
+ if (/^No answer was returned\.$/.test(text.split('\n')[0] ?? '')) {
1649
+ return [{ text: 'no answer returned', tone: 'muted' }];
1650
+ }
1651
+ const sourcesMatch = /^Sources \((\d+)\):$/m.exec(text);
1652
+ const bodyEnd = sourcesMatch !== null ? text.indexOf(sourcesMatch[0]) : followUpIdx;
1653
+ if (bodyEnd <= 0)
1654
+ return null;
1655
+ const answer = text.slice(0, bodyEnd).trim();
1656
+ if (answer === '')
1657
+ return null;
1658
+ let citationCount = 0;
1659
+ if (sourcesMatch !== null) {
1660
+ citationCount = Number(sourcesMatch[1]);
1661
+ const citationsBlock = text.slice(text.indexOf(sourcesMatch[0]) + sourcesMatch[0].length, followUpIdx);
1662
+ const numberedLines = [...citationsBlock.matchAll(/^\d+\. .+$/gm)];
1663
+ if (numberedLines.length !== citationCount)
1664
+ return null;
1665
+ }
1666
+ const answerLines = answer.split('\n').filter((l) => l.trim() !== '');
1667
+ const teaser = answerLines.slice(0, MEMORY_READ_CONTENT_LINES);
1668
+ const hasMore = answerLines.length > MEMORY_READ_CONTENT_LINES;
1669
+ const lines = teaser.map((l) => ({ text: l }));
1670
+ if (hasMore)
1671
+ lines.push({ text: '…', tone: 'muted' });
1672
+ lines.push({ text: `${citationCount} citation${citationCount === 1 ? '' : 's'}`, tone: 'muted' });
1673
+ return lines;
1674
+ }
1675
+ /** Fixed lead sentence of `search contents`'s follow_up (src/commands/
1676
+ * search/contents.ts:71) — always this exact string, used to bound the
1677
+ * last result block below (its true end would otherwise swallow a trailing
1678
+ * `Failed (…):` block and the follow_up text into the char count). */
1679
+ const SEARCH_CONTENTS_FOLLOWUP_MARKER = 'Stale or empty content? Re-run with --max-age-hours 0';
1680
+ const SEARCH_CONTENTS_NAME_CAP = 5;
1681
+ /** `crtr search contents` (contentsLeaf.render, src/commands/search/
1682
+ * contents.ts:87): `No content extracted.` (0 results) or `Extracted ${N}
1683
+ * URL(s):` plus one `### i. title` block per result (heading, optional
1684
+ * meta line, then highlight bullets or a text blockquote); either way, an
1685
+ * optional `Failed (${M}):` bullet list follows, then unconditionally the
1686
+ * fixed follow_up sentence. Per-URL `chars` is the length of that result's
1687
+ * own rendered content block (title/meta lines excluded) — the visible
1688
+ * extracted text, not Exa's own byte count. Returns null when the
1689
+ * follow_up marker is absent, the heading count doesn't match `N`, or (in
1690
+ * the 0-results case) there were no failures either — that combination
1691
+ * isn't a real shape this leaf produces. */
1692
+ function summarizeSearchContents(text) {
1693
+ const followUpIdx = text.indexOf(SEARCH_CONTENTS_FOLLOWUP_MARKER);
1694
+ if (followUpIdx === -1)
1695
+ return null;
1696
+ const failedMatch = /^Failed \((\d+)\):$/m.exec(text);
1697
+ const failedCount = failedMatch !== null ? Number(failedMatch[1]) : 0;
1698
+ const firstLine = text.split('\n')[0] ?? '';
1699
+ if (/^No content extracted\.$/.test(firstLine)) {
1700
+ if (failedCount === 0)
1701
+ return [{ text: 'no content extracted', tone: 'muted' }];
1702
+ return [{ text: `0 extracted · ${failedCount} failed`, tone: 'error' }];
1703
+ }
1704
+ const head = /^Extracted (\d+) URLs?:$/.exec(firstLine);
1705
+ if (head === null)
1706
+ return null;
1707
+ const count = Number(head[1]);
1708
+ const lines = text.split('\n');
1709
+ const headingIdxs = [];
1710
+ for (let i = 0; i < lines.length; i++)
1711
+ if (/^### \d+\. /.test(lines[i]))
1712
+ headingIdxs.push(i);
1713
+ if (headingIdxs.length !== count)
1714
+ return null;
1715
+ const contentEndOffset = failedMatch !== null ? text.indexOf(failedMatch[0]) : followUpIdx;
1716
+ const contentEndLine = text.slice(0, contentEndOffset).split('\n').length - 1;
1717
+ const items = [];
1718
+ for (let k = 0; k < headingIdxs.length; k++) {
1719
+ const start = headingIdxs[k];
1720
+ const end = k + 1 < headingIdxs.length ? headingIdxs[k + 1] : contentEndLine;
1721
+ const heading = /^### \d+\. (.+)$/.exec(lines[start]);
1722
+ if (heading === null)
1723
+ return null;
1724
+ const body = lines.slice(start + 1, end);
1725
+ // renderResult (exa.ts:184–189) puts the meta line — url first when
1726
+ // present — immediately after the heading; drop it so `chars` reflects
1727
+ // only the extracted content, not the URL/date/author line.
1728
+ const contentLines = body.length > 0 && /^https?:\/\//.test(body[0]) ? body.slice(1) : body;
1729
+ items.push({ title: heading[1], chars: contentLines.join('\n').trim().length });
1730
+ }
1731
+ const shown = items.slice(0, SEARCH_CONTENTS_NAME_CAP).map((it) => `${it.title} · ${it.chars} chars`);
1732
+ const out = [{ text: `extracted ${count} URL${count === 1 ? '' : 's'}` }];
1733
+ for (const s of shown)
1734
+ out.push({ text: s, tone: 'muted' });
1735
+ const remaining = items.length - shown.length;
1736
+ if (remaining > 0)
1737
+ out.push({ text: `… ${remaining} more`, tone: 'muted' });
1738
+ if (failedCount > 0)
1739
+ out.push({ text: `${failedCount} failed`, tone: 'warning' });
1740
+ return out;
1741
+ }
1742
+ /** `crtr sys setup` opens a pi-tui wizard when stdout is a TTY (its `render`
1743
+ * is always `''` — the wizard writes directly via process.stdout, and its
1744
+ * raw ANSI curses frames never match any marker below, so this summarizer
1745
+ * safely returns null in that case). In the headless/agent path
1746
+ * (src/commands/sys/setup.ts's `!process.stdout.isTTY` branch,
1747
+ * `buildSetupStaticInstructions` in setup-core.ts:328 — the shape every
1748
+ * bash-tool call to this leaf actually produces) it instead prints a
1749
+ * static inventory: the headline `crtr sys setup (non-interactive)`, a
1750
+ * `Packages:` section (`- <name> — <description> (installed|not
1751
+ * installed)`), a `System deps:` section (`- <tool>: ✓ installed` or `-
1752
+ * <tool>: <command/hint>`), then an `- Exa key: …` bullet and a footer line
1753
+ * — verified against the live command, 2026-07-01. Returns null when the
1754
+ * headline, both section headers, or the Exa-key marker bounding the
1755
+ * System deps section are missing, or any row's shape doesn't match — a
1756
+ * partial/truncated dump must not report a false step count. */
1757
+ function summarizeSysSetup(text) {
1758
+ if ((text.split('\n')[0] ?? '') !== 'crtr sys setup (non-interactive)')
1759
+ return null;
1760
+ const lines = text.split('\n');
1761
+ const pkgIdx = lines.indexOf('Packages:');
1762
+ const sysIdx = lines.indexOf('System deps:');
1763
+ if (pkgIdx === -1 || sysIdx === -1 || sysIdx <= pkgIdx)
1764
+ return null;
1765
+ const exaIdx = lines.findIndex((l, i) => i > sysIdx && /^- Exa key:/.test(l));
1766
+ if (exaIdx === -1)
1767
+ return null;
1768
+ const pkgLines = lines.slice(pkgIdx + 1, sysIdx).filter((l) => l.trim() !== '');
1769
+ const sysLines = lines.slice(sysIdx + 1, exaIdx).filter((l) => l.trim() !== '');
1770
+ if (pkgLines.length === 0 && sysLines.length === 0)
1771
+ return null;
1772
+ let ok = 0;
1773
+ const pending = [];
1774
+ for (const l of pkgLines) {
1775
+ const m = /^- (.+?) — .+\((installed|not installed)\)$/.exec(l);
1776
+ if (m === null)
1777
+ return null;
1778
+ if (m[2] === 'installed')
1779
+ ok++;
1780
+ else
1781
+ pending.push(m[1]);
1782
+ }
1783
+ for (const l of sysLines) {
1784
+ const m = /^- (\S+): (.+)$/.exec(l);
1785
+ if (m === null)
1786
+ return null;
1787
+ if (m[2] === '✓ installed')
1788
+ ok++;
1789
+ else
1790
+ pending.push(m[1]);
1791
+ }
1792
+ if (pending.length === 0)
1793
+ return [{ text: `${ok} steps ok`, tone: 'success' }];
1794
+ return [
1795
+ { text: `${ok} ok · ${pending.length} pending`, tone: 'warning' },
1796
+ { text: pending.join(', '), tone: 'muted' },
1797
+ ];
1798
+ }
1799
+ const SYS_DOCTOR_ISSUE_CAP = 5;
1800
+ /** `crtr sys doctor` (sysDoctorLeaf.run, src/commands/sys/doctor.ts:283) has
1801
+ * no custom `render`; the generic renderer bullets `ok`/`failed` then an `N
1802
+ * checks:` table (columns scope,name,status,message,remediation, plus a
1803
+ * `fixed` column ONLY when at least one row set it — renderArray's column
1804
+ * set is the union of keys across every row, src/core/render.ts:60).
1805
+ * `failed` counts only UNRESOLVED fails (status:fail with no `fixed:true`)
1806
+ * — mirroring doctor.ts's own `ok`/`failed` computation, which excludes
1807
+ * `fixed:true` rows. Returns null when the ok/failed bullets or the checks
1808
+ * table don't match this shape, or when the open-fail-row count found in
1809
+ * the table doesn't equal the declared `failed` bullet (a truncated table,
1810
+ * or a row missing name/message). */
1811
+ function summarizeSysDoctor(text) {
1812
+ const okMatch = /^- ok:\s*(true|false)\s*$/m.exec(text);
1813
+ const failedMatch = /^- failed:\s*(\d+)\s*$/m.exec(text);
1814
+ if (okMatch === null || failedMatch === null)
1815
+ return null;
1816
+ const rows = parseTableRows(text, 'checks');
1817
+ if (rows === null)
1818
+ return null;
1819
+ const failed = Number(failedMatch[1]);
1820
+ const fixedCount = rows.filter((r) => r['status'] === 'fail' && r['fixed'] === 'true').length;
1821
+ if (failed === 0) {
1822
+ // A fail row with no `fixed:true` here would contradict the declared
1823
+ // `failed: 0` — an inconsistent/foreign shape, not a real PASS.
1824
+ if (rows.some((r) => r['status'] === 'fail' && r['fixed'] !== 'true'))
1825
+ return null;
1826
+ const lines = [{ text: `PASS · ${rows.length} checks`, tone: 'success' }];
1827
+ if (fixedCount > 0)
1828
+ lines.push({ text: `fixed ${fixedCount} automatically`, tone: 'muted' });
1829
+ return lines;
1830
+ }
1831
+ const openIssues = rows.filter((r) => r['status'] === 'fail' && r['fixed'] !== 'true');
1832
+ if (openIssues.length !== failed)
1833
+ return null;
1834
+ const shown = [];
1835
+ for (const row of openIssues.slice(0, SYS_DOCTOR_ISSUE_CAP)) {
1836
+ const name = row['name'];
1837
+ const message = row['message'];
1838
+ if (name === undefined || message === undefined)
1839
+ return null;
1840
+ shown.push(`${name}: ${message}`);
1841
+ }
1842
+ const lines = [{ text: `${failed} issue${failed === 1 ? '' : 's'}`, tone: 'error' }];
1843
+ for (const s of shown)
1844
+ lines.push({ text: s, tone: 'warning' });
1845
+ const remaining = openIssues.length - shown.length;
1846
+ if (remaining > 0)
1847
+ lines.push({ text: `… ${remaining} more`, tone: 'muted' });
1848
+ if (fixedCount > 0)
1849
+ lines.push({ text: `fixed ${fixedCount} automatically`, tone: 'muted' });
1850
+ return lines;
1851
+ }
1852
+ const SYS_UPDATE_OUTDATED_CAP = 5;
1853
+ /** `crtr sys update` (sysUpdateLeaf.run, src/commands/sys/update.ts:19) has
1854
+ * two shapes depending on `--check`: bounded check — a `- up_to_date:
1855
+ * true|false` bullet plus an `N updates:` table (columns name,kind,current,
1856
+ * latest,up_to_date,unreachable — no version field at the top level, so
1857
+ * command-icons.md's `vX.Y.Z` framing doesn't fit this leaf; that's `sys
1858
+ * version`'s shape, not this one); or apply — `- target: self|content|all`
1859
+ * + `- status: done|failed` + `- error: <msg>` (only on failure). Verified
1860
+ * against the live `--check` path, 2026-07-01; the apply path is read from
1861
+ * source only — running it mutates the install (spawns npm/git). Returns
1862
+ * null when neither shape's required bullets/table are present. */
1863
+ function summarizeSysUpdate(text) {
1864
+ const upToDateMatch = /^- up_to_date:\s*(true|false)\s*$/m.exec(text);
1865
+ if (upToDateMatch !== null) {
1866
+ const rows = parseTableRows(text, 'updates');
1867
+ if (rows === null)
1868
+ return null;
1869
+ if (upToDateMatch[1] === 'true') {
1870
+ // sys update's own computation is `up_to_date = updates.every(u =>
1871
+ // u.up_to_date || u.unreachable)` (update.ts:104) — a row that
1872
+ // disagrees (reachable and NOT up to date) contradicts the bullet.
1873
+ if (!rows.every((r) => r['up_to_date'] === 'true' || r['unreachable'] === 'true'))
1874
+ return null;
1875
+ return [{ text: `PASS · ${rows.length} up to date`, tone: 'success' }];
1876
+ }
1877
+ const outdated = [];
1878
+ let unreachable = 0;
1879
+ for (const row of rows) {
1880
+ const name = row['name'];
1881
+ const current = row['current'];
1882
+ const latest = row['latest'];
1883
+ const isUpToDate = row['up_to_date'];
1884
+ const isUnreachable = row['unreachable'];
1885
+ if (name === undefined || isUpToDate === undefined || isUnreachable === undefined)
1886
+ return null;
1887
+ if (isUnreachable === 'true') {
1888
+ unreachable++;
1889
+ continue;
1890
+ }
1891
+ if (isUpToDate === 'false')
1892
+ outdated.push(`${name}: ${current} → ${latest}`);
1893
+ }
1894
+ const lines = [{ text: `${outdated.length} outdated · ${rows.length} checked`, tone: 'warning' }];
1895
+ const shown = outdated.slice(0, SYS_UPDATE_OUTDATED_CAP);
1896
+ for (const s of shown)
1897
+ lines.push({ text: s, tone: 'muted' });
1898
+ const remaining = outdated.length - shown.length;
1899
+ if (remaining > 0)
1900
+ lines.push({ text: `… ${remaining} more`, tone: 'muted' });
1901
+ if (unreachable > 0)
1902
+ lines.push({ text: `${unreachable} unreachable`, tone: 'muted' });
1903
+ return lines;
1904
+ }
1905
+ const statusMatch = /^- status:\s*(done|failed)\s*$/m.exec(text);
1906
+ if (statusMatch === null)
1907
+ return null;
1908
+ const target = /^- target:\s*(\S+)\s*$/m.exec(text)?.[1];
1909
+ if (target === undefined)
1910
+ return null;
1911
+ if (statusMatch[1] === 'done')
1912
+ return [{ text: `updated · ${target}`, tone: 'success' }];
1913
+ const error = /^- error:\s*(.+)$/m.exec(text)?.[1];
1914
+ const lines = [{ text: `update failed · ${target}`, tone: 'error' }];
1915
+ if (error !== undefined)
1916
+ lines.push({ text: error, tone: 'muted' });
1917
+ return lines;
1918
+ }
1919
+ /** `crtr sys feedback` (sysFeedbackLeaf.run, src/commands/sys/feedback.ts:
1920
+ * 187) has no custom `render`; the generic renderer bullets `status`
1921
+ * (always short) then either `url` (filed shape — a short GitHub issue
1922
+ * link) or `path`+`error` (saved-locally shape; `path` is usually short but
1923
+ * can cross the prose threshold with a long home dir, and `error` is a
1924
+ * free-form spawn/gh message that often does — read via
1925
+ * readFieldFlexible/soloProseAfterBullets, the same two-shape pattern
1926
+ * Batch C's human/memory summarizers use for a field whose bullet-vs-prose
1927
+ * form depends on string length). Read from source only — running it files
1928
+ * a real GitHub issue or writes a local report, so it was not invoked
1929
+ * live. Returns null when `status` isn't `filed`/`saved-locally`, or the
1930
+ * shape's required companion field is missing. */
1931
+ function summarizeSysFeedback(text) {
1932
+ const statusMatch = /^- status:\s*(filed|saved-locally)\s*$/m.exec(text);
1933
+ if (statusMatch === null)
1934
+ return null;
1935
+ if (statusMatch[1] === 'filed') {
1936
+ const url = readFieldFlexible(text, 'url') ?? soloProseAfterBullets(text);
1937
+ if (url === undefined)
1938
+ return null;
1939
+ return [
1940
+ { text: 'issue filed', tone: 'success' },
1941
+ { text: url, tone: 'muted' },
1942
+ ];
1943
+ }
1944
+ const path = readFieldFlexible(text, 'path') ?? soloProseAfterBullets(text);
1945
+ if (path === undefined)
1946
+ return null;
1947
+ // The reason (gh missing, unauthenticated, or the API call itself
1948
+ // failing — feedback.ts:188–195) is prose the digest doesn't try to
1949
+ // classify; "not filed" states the fact without guessing the cause.
1950
+ return [
1951
+ { text: 'saved locally — not filed', tone: 'warning' },
1952
+ { text: basename(path), tone: 'muted' },
1953
+ ];
1954
+ }
1955
+ const SURFACE_VIEW_LIST_ID_CAP = 8;
1956
+ /** `crtr surface view list` (viewListLeaf.render, src/commands/view-list.ts:
1957
+ * 59) has a custom render, NOT the generic renderResult: one padded plain
1958
+ * line per view, `<id padded> <title padded> [<scope>]<optional
1959
+ * desc/error>` (verified live, 2026-07-01) — no GFM table markup at all, so
1960
+ * this file's shared parseTableRows/splitTableRow helpers don't apply.
1961
+ * Real ids/titles never contain a run of 2+ spaces, so each real field
1962
+ * boundary (the column padding plus the literal ` ` separator together
1963
+ * always total 2+ spaces) is recoverable by anchoring on the `[scope]`
1964
+ * bracket. Returns null when the roster is empty (`No views found…`, a
1965
+ * single plain line the generic path already renders fine) or any line
1966
+ * fails to match this row shape — including the pi truncation marker,
1967
+ * which never contains a `[scope]` bracket or a 2+-space run. */
1968
+ function summarizeSurfaceViewList(text) {
1969
+ const lines = text.split('\n').filter((l) => l.trim() !== '');
1970
+ if (lines.length === 0)
1971
+ return null;
1972
+ const ids = [];
1973
+ for (const line of lines) {
1974
+ const m = /^(\S+)\s{2,}.+?\s{2,}\[\w[\w-]*\](?:\s{2,}.*)?$/.exec(line);
1975
+ if (m === null)
1976
+ return null;
1977
+ ids.push(m[1]);
1978
+ }
1979
+ const shown = ids.slice(0, SURFACE_VIEW_LIST_ID_CAP);
1980
+ const remaining = ids.length - shown.length;
1981
+ const idsLine = remaining > 0 ? `${shown.join(', ')}, … ${remaining} more` : shown.join(', ');
1982
+ return [
1983
+ { text: `${ids.length} views` },
1984
+ { text: idsLine, tone: 'muted' },
1985
+ ];
1986
+ }
1987
+ const SUMMARIZERS = [
1988
+ { path: 'memory read', run: summarizeMemoryRead },
1989
+ { path: 'memory lint', run: summarizeMemoryLint },
1990
+ { path: 'canvas dashboard', run: summarizeCanvasDashboard },
1991
+ { path: 'node inspect list', run: summarizeNodeInspectList },
1992
+ { path: 'canvas attention count', run: summarizeCanvasAttentionCount },
1993
+ { path: 'canvas attention list', run: summarizeCanvasAttentionList },
1994
+ { path: 'canvas attention map', run: summarizeCanvasAttentionMap },
1995
+ { path: 'canvas revive', run: summarizeCanvasRevive },
1996
+ { path: 'canvas history search', run: summarizeCanvasHistorySearch },
1997
+ { path: 'canvas history read', run: summarizeCanvasHistoryRead },
1998
+ { path: 'canvas history show', run: summarizeCanvasHistoryShow },
1999
+ { path: 'canvas daemon start', run: summarizeCanvasDaemonStart },
2000
+ { path: 'canvas daemon status', run: summarizeCanvasDaemonStatus },
2001
+ { path: 'canvas daemon stop', run: summarizeCanvasDaemonStop },
2002
+ { path: 'canvas issue new', run: summarizeCanvasIssueNew },
2003
+ { path: 'node new', run: summarizeNodeNew },
2004
+ { path: 'node msg', run: summarizeNodeMsg },
2005
+ { path: 'node yield', run: summarizeNodeYield },
2006
+ { path: 'node lifecycle close', run: summarizeNodeLifecycleClose },
2007
+ { path: 'node triggers list', run: summarizeNodeTriggersList },
2008
+ { path: 'feed read', run: summarizeFeedRead },
2009
+ { path: 'feed message', run: summarizeFeedMessage },
2010
+ { path: 'feed peek', run: summarizeFeedPeek },
2011
+ { path: 'push urgent', run: summarizePushUrgent },
2012
+ { path: 'push final', run: summarizePushFinal },
2013
+ { path: 'human ask', run: summarizeHumanAsk },
2014
+ { path: 'human review', run: summarizeHumanReview },
2015
+ { path: 'human list', run: summarizeHumanList },
2016
+ { path: 'human deck', run: summarizeHumanDeck },
2017
+ { path: 'memory list', run: summarizeMemoryList },
2018
+ { path: 'memory find', run: summarizeMemoryFind },
2019
+ { path: 'memory write', run: summarizeMemoryWrite },
2020
+ { path: 'pkg plugin install', run: summarizePkgPluginInstall },
2021
+ { path: 'pkg plugin update', run: summarizePkgPluginUpdate },
2022
+ { path: 'pkg plugin list', run: summarizePkgPluginList },
2023
+ { path: 'pkg plugin show', run: summarizePkgPluginShow },
2024
+ { path: 'pkg market list', run: summarizePkgMarketList },
2025
+ { path: 'pkg market browse', run: summarizePkgMarketBrowse },
2026
+ { path: 'search web', run: summarizeSearchWeb },
2027
+ { path: 'search answer', run: summarizeSearchAnswer },
2028
+ { path: 'search contents', run: summarizeSearchContents },
2029
+ { path: 'sys setup', run: summarizeSysSetup },
2030
+ { path: 'sys doctor', run: summarizeSysDoctor },
2031
+ { path: 'sys update', run: summarizeSysUpdate },
2032
+ { path: 'sys feedback', run: summarizeSysFeedback },
2033
+ { path: 'surface view list', run: summarizeSurfaceViewList },
2034
+ ];
2035
+ /** Dispatch a command's argv tokens + its raw result text to the summarizer
2036
+ * that owns that leaf path (longest-registered-prefix match, same shape as
2037
+ * suppressOutputForPath), or null when no command owns a summary — the
2038
+ * caller falls through to generic truncation in that case. */
2039
+ export function summarizePath(tokens, rawText) {
2040
+ for (const { path, run } of SUMMARIZERS) {
2041
+ const words = path.split(' ');
2042
+ if (words.length <= tokens.length && words.every((word, i) => tokens[i] === word)) {
2043
+ return run(rawText, tokens);
2044
+ }
2045
+ }
2046
+ return null;
2047
+ }