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