@north-light/crouter 0.3.231 → 0.3.233

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (187) hide show
  1. package/dist/api/client.d.ts +3 -1
  2. package/dist/api/client.js +4 -0
  3. package/dist/api/dto/canvas.d.ts +10 -0
  4. package/dist/api/dto/common.d.ts +1 -1
  5. package/dist/api/dto/health.d.ts +2 -1
  6. package/dist/api/dto/lifecycle.d.ts +3 -4
  7. package/dist/api/dto/messages.d.ts +5 -4
  8. package/dist/api/dto/nodes.d.ts +2 -0
  9. package/dist/api/dto/profiles.d.ts +5 -0
  10. package/dist/api/routes.d.ts +1 -0
  11. package/dist/api/routes.js +1 -0
  12. package/dist/builtin-memory/00-runtime-base/00-authoring.md +8 -0
  13. package/dist/builtin-memory/00-runtime-base/01-escalation.md +1 -1
  14. package/dist/builtin-memory/01-spine/00-has-manager.md +1 -1
  15. package/dist/builtin-memory/02-turn-lifecycle/00-ending-a-turn.md +5 -0
  16. package/dist/builtin-memory/02-turn-lifecycle/02-resident.md +5 -3
  17. package/dist/builtin-memory/04-orchestration-kernel.md +2 -2
  18. package/dist/builtin-memory/insights/capture.md +3 -2
  19. package/dist/builtin-memory/internal/memory-loading.md +4 -3
  20. package/dist/builtin-pi-packages/pi-crtr-extensions/README.md +6 -1
  21. package/dist/clients/attach/render/diagram.js +13 -5
  22. package/dist/clients/attach/render/page-block.d.ts +0 -1
  23. package/dist/clients/attach/render/page-block.js +4 -57
  24. package/dist/clients/attach/viewer.js +570 -563
  25. package/dist/clients/inbox/__tests__/integration/inbox-controller.test.js +9 -0
  26. package/dist/clients/inbox/__tests__/integration/mount-panel.test.js +62 -1
  27. package/dist/clients/inbox/controller.d.ts +10 -0
  28. package/dist/clients/inbox/controller.js +56 -12
  29. package/dist/clients/inbox/tui/input.js +38 -10
  30. package/dist/clients/inbox/tui/page-body.d.ts +10 -0
  31. package/dist/clients/inbox/tui/page-body.js +66 -0
  32. package/dist/clients/inbox/tui/panel.js +6 -4
  33. package/dist/clients/inbox/tui/render.js +89 -17
  34. package/dist/clients/inbox/tui/types.d.ts +4 -4
  35. package/dist/commands/__tests__/human.test.js +18 -3
  36. package/dist/commands/__tests__/node-message.test.js +3 -3
  37. package/dist/commands/api-client.js +1 -7
  38. package/dist/commands/canvas-config.js +6 -14
  39. package/dist/commands/canvas-use.js +4 -6
  40. package/dist/commands/cron.js +16 -22
  41. package/dist/commands/human/prompts.d.ts +1 -1
  42. package/dist/commands/human/prompts.js +118 -112
  43. package/dist/commands/human/request.js +13 -14
  44. package/dist/commands/human/review.js +3 -4
  45. package/dist/commands/human/shared.d.ts +6 -0
  46. package/dist/commands/human/shared.js +43 -5
  47. package/dist/commands/human.js +1 -1
  48. package/dist/commands/memory/delete.js +4 -6
  49. package/dist/commands/memory/edit.js +0 -4
  50. package/dist/commands/memory/move.js +3 -5
  51. package/dist/commands/memory/shared.d.ts +1 -1
  52. package/dist/commands/memory/shared.js +11 -7
  53. package/dist/commands/memory/write.js +60 -29
  54. package/dist/commands/memory.js +1 -1
  55. package/dist/commands/node/bash.js +6 -9
  56. package/dist/commands/node/create.js +89 -22
  57. package/dist/commands/node/inspect.js +3 -3
  58. package/dist/commands/node/lifecycle.js +30 -27
  59. package/dist/commands/node/message.js +24 -45
  60. package/dist/commands/node/subscription.js +6 -15
  61. package/dist/commands/node/wait.js +2 -3
  62. package/dist/commands/node-lifecycle-revive.js +1 -12
  63. package/dist/commands/pkg/browse/actions.js +2 -3
  64. package/dist/commands/pkg/market-manage.js +2 -5
  65. package/dist/commands/pkg/plugin-manage.js +7 -8
  66. package/dist/commands/profile/default.js +5 -5
  67. package/dist/commands/profile/delete.js +1 -1
  68. package/dist/commands/profile/env.js +9 -15
  69. package/dist/commands/profile/kind.js +3 -7
  70. package/dist/commands/profile/meta.js +3 -5
  71. package/dist/commands/profile/new.js +0 -6
  72. package/dist/commands/profile/pause.js +4 -8
  73. package/dist/commands/profile/project.js +5 -9
  74. package/dist/commands/profile/rename.js +3 -7
  75. package/dist/commands/profile/show.js +3 -3
  76. package/dist/commands/profile.js +4 -3
  77. package/dist/commands/surface-tmux-spread.js +1 -3
  78. package/dist/commands/sys/config.js +3 -4
  79. package/dist/commands/sys/support/prepare.js +8 -4
  80. package/dist/commands/sys/support/submit.js +2 -3
  81. package/dist/commands/sys/sync-deps.js +1 -9
  82. package/dist/commands/sys/sync-project-guidance.js +1 -7
  83. package/dist/commands/sys/sync-skills.js +1 -11
  84. package/dist/core/__tests__/cron-node-sink-parked-root.test.d.ts +1 -0
  85. package/dist/core/__tests__/cron-node-sink-parked-root.test.js +147 -0
  86. package/dist/core/__tests__/history-inbox.test.js +11 -1
  87. package/dist/core/__tests__/human-deliver.test.js +2 -1
  88. package/dist/core/__tests__/integration/command-plugins.test.js +0 -1
  89. package/dist/core/__tests__/integration/deferred-no-wake.test.js +0 -1
  90. package/dist/core/__tests__/lifecycle.test.js +30 -2
  91. package/dist/core/__tests__/revive-parked-fresh.test.d.ts +1 -0
  92. package/dist/core/__tests__/revive-parked-fresh.test.js +109 -0
  93. package/dist/core/__tests__/seam/dormancy-release.test.js +32 -5
  94. package/dist/core/canvas/attention.d.ts +2 -0
  95. package/dist/core/canvas/attention.js +25 -18
  96. package/dist/core/canvas/extensions.d.ts +1 -1
  97. package/dist/core/canvas/extensions.js +7 -1
  98. package/dist/core/canvas/history.js +20 -2
  99. package/dist/core/canvas/types.d.ts +1 -1
  100. package/dist/core/command.js +33 -8
  101. package/dist/core/help.d.ts +28 -2
  102. package/dist/core/help.js +46 -10
  103. package/dist/core/human/__tests__/page-html-markdown.test.d.ts +1 -0
  104. package/dist/core/human/__tests__/page-html-markdown.test.js +48 -0
  105. package/dist/core/human/component-docs.js +4 -4
  106. package/dist/core/human/page-html-markdown.d.ts +8 -0
  107. package/dist/core/human/page-html-markdown.js +260 -0
  108. package/dist/core/memory/lint.d.ts +15 -0
  109. package/dist/core/memory/lint.js +150 -90
  110. package/dist/core/profiles/__tests__/fuzzy-match.test.d.ts +1 -0
  111. package/dist/core/profiles/__tests__/fuzzy-match.test.js +51 -0
  112. package/dist/core/profiles/fuzzy-match.d.ts +19 -0
  113. package/dist/core/profiles/fuzzy-match.js +92 -0
  114. package/dist/core/profiles/manifest.d.ts +14 -7
  115. package/dist/core/profiles/manifest.js +62 -12
  116. package/dist/core/profiles/select.d.ts +3 -1
  117. package/dist/core/profiles/select.js +5 -3
  118. package/dist/core/profiles/state-block.js +4 -3
  119. package/dist/core/runtime/boot-root.d.ts +3 -2
  120. package/dist/core/runtime/canvas-extensions.d.ts +7 -1
  121. package/dist/core/runtime/canvas-extensions.js +8 -1
  122. package/dist/core/runtime/lifecycle.d.ts +11 -2
  123. package/dist/core/runtime/lifecycle.js +15 -2
  124. package/dist/core/runtime/model-selection.d.ts +4 -0
  125. package/dist/core/runtime/model-selection.js +5 -0
  126. package/dist/core/runtime/nodes.js +5 -0
  127. package/dist/core/runtime/reopen.d.ts +6 -0
  128. package/dist/core/runtime/reopen.js +12 -1
  129. package/dist/core/runtime/revive.d.ts +6 -0
  130. package/dist/core/runtime/revive.js +22 -2
  131. package/dist/core/runtime/spawn.d.ts +5 -2
  132. package/dist/core/runtime/spawn.js +18 -32
  133. package/dist/core/runtime/structured-output.d.ts +6 -0
  134. package/dist/core/runtime/structured-output.js +6 -0
  135. package/dist/core/substrate/__tests__/surface-match-command.test.d.ts +1 -0
  136. package/dist/core/substrate/__tests__/surface-match-command.test.js +89 -0
  137. package/dist/core/substrate/__tests__/surface-match-pre-command.test.d.ts +1 -0
  138. package/dist/core/substrate/__tests__/surface-match-pre-command.test.js +92 -0
  139. package/dist/core/substrate/frontmatter-validation.js +1 -1
  140. package/dist/core/substrate/injected-store.d.ts +6 -0
  141. package/dist/core/substrate/injected-store.js +24 -0
  142. package/dist/core/substrate/on-read.d.ts +17 -1
  143. package/dist/core/substrate/on-read.js +38 -3
  144. package/dist/core/substrate/schema.d.ts +3 -3
  145. package/dist/core/substrate/schema.js +4 -4
  146. package/dist/core/substrate/surface-match.d.ts +19 -0
  147. package/dist/core/substrate/surface-match.js +224 -12
  148. package/dist/core/termrender/version.d.ts +1 -1
  149. package/dist/core/termrender/version.js +1 -1
  150. package/dist/core/user-settings.js +1 -1
  151. package/dist/daemon/api/__tests__/broker-settle-park.test.d.ts +1 -0
  152. package/dist/daemon/api/__tests__/broker-settle-park.test.js +102 -0
  153. package/dist/daemon/api/__tests__/canvas-snapshot-fields.test.d.ts +1 -0
  154. package/dist/daemon/api/__tests__/canvas-snapshot-fields.test.js +188 -0
  155. package/dist/daemon/api/__tests__/node-create-description.test.d.ts +1 -0
  156. package/dist/daemon/api/__tests__/node-create-description.test.js +83 -0
  157. package/dist/daemon/api/__tests__/profile-metadata-route.test.d.ts +1 -0
  158. package/dist/daemon/api/__tests__/profile-metadata-route.test.js +92 -0
  159. package/dist/daemon/api/__tests__/reopen-delivery.test.d.ts +1 -0
  160. package/dist/daemon/api/__tests__/reopen-delivery.test.js +173 -0
  161. package/dist/daemon/api/handlers/broker-ops.js +21 -0
  162. package/dist/daemon/api/handlers/canvas.js +10 -0
  163. package/dist/daemon/api/handlers/messages.js +25 -16
  164. package/dist/daemon/api/handlers/nodes.js +5 -0
  165. package/dist/daemon/api/handlers/profiles.js +22 -1
  166. package/dist/daemon/cron-run.js +19 -1
  167. package/dist/daemon/manage.d.ts +16 -1
  168. package/dist/daemon/manage.js +20 -1
  169. package/dist/daemon/park-pending.d.ts +13 -0
  170. package/dist/daemon/park-pending.js +42 -0
  171. package/dist/daemon/reconcilers/broker-supervision.d.ts +13 -0
  172. package/dist/daemon/reconcilers/broker-supervision.js +139 -21
  173. package/dist/daemon/reconcilers/live-obligation.d.ts +10 -4
  174. package/dist/daemon/reconcilers/live-obligation.js +7 -3
  175. package/dist/daemon/reconcilers/storage-maintenance.d.ts +0 -4
  176. package/dist/daemon/reconcilers/storage-maintenance.js +1 -33
  177. package/dist/pi-extensions/__tests__/pre-command-gate.test.d.ts +1 -0
  178. package/dist/pi-extensions/__tests__/pre-command-gate.test.js +220 -0
  179. package/dist/pi-extensions/canvas-doc-substrate.d.ts +14 -0
  180. package/dist/pi-extensions/canvas-doc-substrate.js +75 -2
  181. package/dist/pi-extensions/canvas-prompt-scrub.d.ts +13 -0
  182. package/dist/pi-extensions/canvas-prompt-scrub.js +53 -0
  183. package/dist/shared/generated-context.d.ts +7 -0
  184. package/dist/shared/generated-context.js +11 -0
  185. package/package.json +4 -4
  186. package/runtime.lock.json +2 -2
  187. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/strip-skills-docs.ts +0 -47
@@ -32,8 +32,6 @@ const nodeBashBackground = defineLeaf({
32
32
  ],
33
33
  output: [
34
34
  { name: 'node_id', type: 'string', required: true, constraint: 'The target node.' },
35
- { name: 'backgrounded', type: 'number', required: true, constraint: 'Number of running bash commands handed off.' },
36
- { name: 'job_ids', type: 'string[]', required: true, constraint: 'File-backed job ids now running independently of the agent tool call.' },
37
35
  { name: 'jobs', type: 'object[]', required: true, constraint: 'Per handed-off job {job_id, elapsed_ms} — the foreground elapsed time in milliseconds at the moment of handoff.' },
38
36
  ],
39
37
  outputKind: 'object',
@@ -53,14 +51,14 @@ const nodeBashBackground = defineLeaf({
53
51
  ? `crtr: bash handed off · ${formatBashElapsed(elapsed[0].elapsed_ms)}`
54
52
  : `crtr: bash handed off (${jobs.length})`;
55
53
  displayMessage(toast, pane);
56
- return { node_id: id, backgrounded: jobs.length, job_ids: jobs.map((job) => job.jobId), jobs: elapsed };
54
+ return { node_id: id, jobs: elapsed };
57
55
  },
58
56
  render: (result) => {
59
- if (result['backgrounded'] === 0)
60
- return `No foreground bash command is running for ${result['node_id']}.`;
61
57
  const jobs = result['jobs'];
58
+ if (jobs.length === 0)
59
+ return `No foreground bash command is running for ${result['node_id']}.`;
62
60
  const oldestElapsedMs = Math.max(...jobs.map((job) => job.elapsed_ms));
63
- return `Handed ${result['backgrounded']} bash command(s) to the background job system for ${result['node_id']} · ran ${formatBashElapsed(oldestElapsedMs)} in the foreground.`;
61
+ return `Handed ${jobs.length} bash command(s) to the background job system for ${result['node_id']} · ran ${formatBashElapsed(oldestElapsedMs)} in the foreground.`;
64
62
  },
65
63
  });
66
64
  const nodeBashList = defineLeaf({
@@ -126,7 +124,6 @@ const nodeBashKill = defineLeaf({
126
124
  ],
127
125
  output: [
128
126
  { name: 'node_id', type: 'string', required: true, constraint: 'The owning node.' },
129
- { name: 'job_id', type: 'string', required: true, constraint: 'The job that was stopped.' },
130
127
  { name: 'signaled', type: 'boolean', required: true, constraint: 'True when the process group was still alive and received SIGTERM.' },
131
128
  ],
132
129
  outputKind: 'object',
@@ -169,9 +166,9 @@ const nodeBashKill = defineLeaf({
169
166
  body: `Background bash job ${jobId} was stopped by the user before it finished. Log: ${paths.jobLog} Command: ${paths.cmdSh}`,
170
167
  tier: 'urgent',
171
168
  });
172
- return { node_id: node.node_id, job_id: jobId, signaled: stopped.signaled };
169
+ return { node_id: node.node_id, signaled: stopped.signaled };
173
170
  },
174
- render: (result) => `Canceled background bash job ${result['job_id']} for ${result['node_id']}${result['signaled'] === true ? '' : ' (its process group was already gone)'}.`,
171
+ render: (result) => `Canceled a background bash job for ${result['node_id']}${result['signaled'] === true ? '' : ' (its process group was already gone)'}.`,
175
172
  });
176
173
  export const nodeBash = defineBranch({
177
174
  name: 'bash',
@@ -2,13 +2,17 @@
2
2
  import { defineLeaf } from '../../core/command.js';
3
3
  import { InputError } from '../../core/io.js';
4
4
  import { mark } from '../../core/timing.js';
5
- import { parseOutputSchemaValue } from '../../core/runtime/structured-output.js';
5
+ import { OUTPUT_SCHEMA_CONSTRAINTS, OUTPUT_SCHEMA_TRANSPORT, parseOutputSchemaValue } from '../../core/runtime/structured-output.js';
6
+ import { MODEL_SPEC_FORMS, MODEL_SPEC_NEXT } from '../../core/runtime/model-selection.js';
6
7
  import { readMergedLaunchConfig } from '../../core/config.js';
7
8
  import { profilesStateBlock } from '../../core/profiles/state-block.js';
9
+ import { resolveProfileOperand } from '../../core/profiles/manifest.js';
8
10
  import { stateBlock } from '../../core/help.js';
9
11
  import { cliClient, getNodeOrNull, rethrowAsCliError } from '../api-client.js';
12
+ import { inTmux } from '../../core/runtime/placement-tmux.js';
10
13
  import { openSpawnViewer } from '../surface/node/placement.js';
11
14
  const YIELD_NUDGE_THRESHOLD = 100_000;
15
+ const NODE_NEW_OUTPUT_SCHEMA_EFFECT = '--output-schema writes the accepted result to the node’s context/result.json and pushes it as the node’s final report.';
12
16
  const STD_CHILD_FOLLOW_UP = "Do not wait, poll, or duplicate this child's assignment in your own context — there is no result to await, stopping will not strand you, and duplicated work wastes the delegated context. You're auto-subscribed, so its finish wakes you on its own. Two moves only: do other independent work right now, or stop and end your turn — the wake brings you back.";
13
17
  /** Decision road sign for a managed (non-root) child. Normally STD_CHILD_FOLLOW_UP,
14
18
  * but when the SPAWNER is an orchestrator whose context has already grown past
@@ -68,33 +72,97 @@ export function kindsStateBlock() {
68
72
  }
69
73
  function nodeNewParams() {
70
74
  return [
71
- { kind: 'stdin', name: 'prompt', required: false, constraint: 'First user message. Required unless --no-kickoff is given.' },
75
+ { kind: 'stdin', name: 'prompt', required: false, constraint: 'First user message.' },
72
76
  { kind: 'flag', name: 'kind', type: 'string', required: false, constraint: "Persona kind. Defaults to the profile's default kind; `general` when the profile has none. The <kinds> list below names every top-level installable kind." },
73
77
  { kind: 'flag', name: 'mode', type: 'enum', choices: ['base', 'orchestrator'], required: false, default: 'base', constraint: 'Persona mode. base for hands-on work; orchestrator when the unit itself needs decomposition across children.' },
74
78
  { kind: 'flag', name: 'cwd', type: 'path', required: false, constraint: 'Pin the node to this directory. Defaults to the spawner\u2019s directory for a managed child, and to the directory this command runs in for a root.' },
75
- { kind: 'flag', name: 'name', type: 'string', required: false, constraint: 'Display name. Defaults to the kind.' },
79
+ { kind: 'flag', name: 'name', type: 'string', required: false, constraint: 'Display name.' },
76
80
  { kind: 'flag', name: 'parent', type: 'string', required: false, constraint: 'Parent node id. Defaults to the calling node.' },
77
81
  { kind: 'flag', name: 'node-id', type: 'string', required: false, constraint: 'Exact id for the new node; lowercase letters, digits, and hyphens only, at most 128 characters. Rejected if already present.' },
78
82
  { kind: 'flag', name: 'root', type: 'bool', required: false, constraint: 'Spawn an independent resident root with no parent or subscription. Mutually exclusive with --worktree.' },
79
83
  { kind: 'flag', name: 'worktree', type: 'string', required: false, constraint: 'Local base branch for a crouter-managed worktree for this child. Mutually exclusive with --root.' },
80
84
  { kind: 'flag', name: 'fork-from', type: 'string', required: false, constraint: 'Fork the new node from an existing node id, absolute session path, or partial pi session uuid.' },
81
- { kind: 'flag', name: 'model', type: 'string', required: false, constraint: 'Durable model override: provider/id; provider/tier where provider is anthropic|openai and tier is ultra|strong|medium|light; bare tier (ultra|strong|medium|light); or family alias (opus|sonnet|haiku).' },
82
- { kind: 'flag', name: 'profile', type: 'string', required: false, constraint: 'Profile id or unique profile name. Omit to inherit the caller profile.' },
85
+ {
86
+ kind: 'flag',
87
+ name: 'model',
88
+ type: 'string',
89
+ required: false,
90
+ constraint: 'Durable model override. Omit unless the task or governing guidance requires a specific model.',
91
+ focusedHelp: {
92
+ whenToUse: 'Use only when the user, task, or governing agent guidance already requires a specific provider, model, capability tier, or family. Omit it for routine delegation: the selected persona kind and profile own normal model selection, and review personas may enforce a quality floor.',
93
+ value: `Accepted forms: ${MODEL_SPEC_FORMS}. Concrete provider/id requests must be registered. Portable provider/tier, bare-tier, and family-alias requests resolve through the configured model ladders.`,
94
+ effects: [
95
+ 'The resolved override is stored on the node and in its launch recipe, so future revives preserve it.',
96
+ 'A concrete provider/id pins that provider. Portable tiers and aliases preserve logical intent for runtime routing; a persona quality floor may raise a weaker request.',
97
+ ],
98
+ },
99
+ },
100
+ {
101
+ kind: 'flag',
102
+ name: 'profile',
103
+ type: 'string',
104
+ required: false,
105
+ constraint: 'Profile override. Use only when the task needs different project purview, memory, or environment; otherwise omit for normal inheritance or selection.',
106
+ focusedHelp: {
107
+ whenToUse: 'Use only when the work must run under a different profile because it needs that profile’s project purview, memory store, stored environment, or defaults. Do not use it merely to change the working directory, persona kind, model, or graph ownership; those have separate parameters.',
108
+ value: 'Pass a profile id or name from the <profiles> list below; a close-enough name resolves to the nearest profile and the choice is reported on stderr. Omit it inside a node to inherit the caller’s profile; outside a node, omission runs normal cwd-based profile selection.',
109
+ effects: [
110
+ 'The selected profile id is stored on the node and supplies its profile-scoped config, memory, environment, and injected bearings.',
111
+ 'It does not change the parent or subscription; --cwd separately pins the working directory.',
112
+ ],
113
+ dynamicState: () => profilesStateBlock(),
114
+ },
115
+ },
83
116
  { kind: 'flag', name: 'situational-context', type: 'string', required: false, constraint: 'Hidden ambient context appended to injected bearings, never visible chat.' },
84
- { kind: 'flag', name: 'no-kickoff', type: 'bool', required: false, constraint: 'With --root, start the broker and session idle without a first message or model turn. Mutually exclusive with a prompt and --mode orchestrator.' },
85
- { kind: 'flag', name: 'output-schema', type: 'string', required: false, constraint: 'Path to a JSON Schema file, or inline JSON beginning with `{`. The node must submit a conforming result to finish.' },
117
+ // Hidden: an idle root is the rare shape, and the empty-prompt error names
118
+ // this flag at exactly the moment it is needed. Advertising it would tax
119
+ // every spawn with a capability almost no caller wants.
120
+ { kind: 'flag', name: 'no-kickoff', type: 'bool', required: false, hidden: true, constraint: 'With --root, start the broker and session idle without a first message or model turn. Mutually exclusive with a prompt and --mode orchestrator.' },
121
+ {
122
+ kind: 'flag',
123
+ name: 'output-schema',
124
+ type: 'string',
125
+ required: false,
126
+ constraint: 'Replace prose completion with a schema-constrained final result. Use only when its consumer requires fixed fields.',
127
+ focusedHelp: {
128
+ whenToUse: 'Use only when the node’s consumer needs a machine-checkable final object with fixed fields. Omit it for ordinary delegation or when a prose report is enough; structured output replaces the completion protocol, and an over-constrained schema can force retries.',
129
+ value: `Pass the JSON Schema the result must satisfy. ${OUTPUT_SCHEMA_TRANSPORT} ${OUTPUT_SCHEMA_CONSTRAINTS}`,
130
+ effects: [
131
+ 'The node receives a terminal submit tool and cannot finish until submit accepts a result; it must not call `crtr push final` itself.',
132
+ NODE_NEW_OUTPUT_SCHEMA_EFFECT,
133
+ 'The managed parent wakes through its existing subscription when the final report arrives; an independent root has no subscriber to wake.',
134
+ 'Successful submit removes the request and terminates the node.',
135
+ ],
136
+ },
137
+ },
86
138
  ];
87
139
  }
140
+ /** A root's viewer opens from THIS process (openSpawnViewer), so the caller's
141
+ * own tmux presence decides whether placement fields and the viewer effect can
142
+ * ever apply. Outside tmux they are dead weight, so help omits them. `inTmux()`
143
+ * is a bare env read — safe on the import-time help path. */
88
144
  function nodeNewOutput() {
89
145
  return [
90
146
  { name: 'node_id', type: 'string', required: true, constraint: 'New node id.' },
91
147
  { name: 'name', type: 'string', required: true, constraint: 'Display name.' },
92
- { name: 'status', type: 'string', required: true, constraint: 'Always "active".' },
93
- { name: 'window', type: 'string', required: false, constraint: 'tmux window id when a viewer opens.' },
94
- { name: 'session', type: 'string', required: false, constraint: 'tmux session when a viewer opens.' },
148
+ ...(inTmux()
149
+ ? [
150
+ { name: 'window', type: 'string', required: false, constraint: 'tmux window id when a viewer opens.' },
151
+ { name: 'session', type: 'string', required: false, constraint: 'tmux session when a viewer opens.' },
152
+ ]
153
+ : []),
95
154
  { name: 'follow_up', type: 'string', required: true, constraint: 'Managed child: continue unrelated work or stop; its final report wakes you. Root: hand it off; no report wakes you.' },
96
155
  ];
97
156
  }
157
+ function nodeNewEffects() {
158
+ return [
159
+ 'Creates nodes/<id>/ in the canvas home, indexes the node in canvas.db, and starts its detached broker engine.',
160
+ 'A managed child creates an active parent subscription; its pushes wake the parent. --root creates an independent node with spawned_by provenance and no parent subscription.',
161
+ '--worktree creates a managed worktree at ~/.crouter/canvas/worktrees/<node-id>/ on a new crtr/<node-id> branch.',
162
+ NODE_NEW_OUTPUT_SCHEMA_EFFECT,
163
+ ...(inTmux() ? ['--root opens a viewer for the new node in the current tmux session.'] : []),
164
+ ];
165
+ }
98
166
  async function runNodeCreation(input) {
99
167
  const prompt = input['prompt'] ?? '';
100
168
  const noKickoff = input['noKickoff'] === true;
@@ -118,9 +186,13 @@ async function runNodeCreation(input) {
118
186
  const forkFrom = input['forkFrom'];
119
187
  const modelSpec = input['model']?.trim();
120
188
  if (modelSpec === '') {
121
- throw new InputError({ error: 'empty_spec', message: 'a model spec is required', field: 'model', next: 'Pass a provider/id, provider/tier where provider is anthropic|openai and tier is ultra|strong|medium|light, a bare tier (ultra|strong|medium|light), or a family alias (opus|sonnet|haiku).' });
189
+ throw new InputError({ error: 'empty_spec', message: 'a model spec is required', field: 'model', next: `Run \`crtr node new --model -h\` and read the focused contract. ${MODEL_SPEC_NEXT}` });
122
190
  }
123
- const profile = input['profile'];
191
+ // Resolved here rather than server-side so the closest-match notice lands on
192
+ // the caller's terminal instead of the daemon log, and so the daemon receives
193
+ // a durable id rather than a name it would have to interpret again.
194
+ const profileOperand = input['profile'];
195
+ const profile = profileOperand === undefined ? undefined : resolveProfileOperand(profileOperand).profileId;
124
196
  const situationalContextRaw = input['situationalContext']?.trim();
125
197
  const situationalContext = situationalContextRaw !== undefined && situationalContextRaw !== '' ? situationalContextRaw : undefined;
126
198
  const outputSchemaParsed = parseOutputSchemaValue(input['outputSchema']);
@@ -171,7 +243,6 @@ async function runNodeCreation(input) {
171
243
  name: detail.name,
172
244
  window: placement.window,
173
245
  session: placement.session,
174
- status: detail.status,
175
246
  follow_up: root
176
247
  ? (noKickoff
177
248
  ? 'Independent root spawned idle. No kickoff turn ran; hand it off or attach and prompt it. No report wakes you.'
@@ -183,26 +254,22 @@ async function runNodeCreation(input) {
183
254
  export const nodeNew = defineLeaf({
184
255
  name: 'new',
185
256
  description: 'create and start a node immediately',
186
- whenToUse: 'you need a managed child or independent root to start immediately',
257
+ whenToUse: 'a unit of work is genuinely separable from what you are doing: a bounded subtask you would otherwise grind out inline, several independent units worth running in parallel, a scouting pass over unfamiliar code, a change that wants its own git worktree, or an answer you need in a fixed shape rather than as prose',
187
258
  tier: 'important',
188
259
  help: {
189
260
  name: 'node new',
190
261
  summary: 'create a node immediately and start its detached broker',
262
+ guide: 'The prompt is the child\u2019s entire brief: it wakes with this prompt, its persona, and ambient bearings \u2014 none of your conversation carries over. State the outcome and constraints. Pass an implementation plan only when it is already settled or necessary to preserve an approved decision; otherwise, let the child investigate and choose how. Give the reason along with the request: the larger goal this serves and what the child\'s output enables, so the child resolves ambiguity toward your intent instead of guessing. Name by absolute path every artifact the task depends on (a spec, an explore report, an earlier child\'s findings); the child inherits only the files the prompt names. When the report must carry something specific—findings inline, or paths to artifacts it wrote—say so in the prompt.',
191
263
  params: nodeNewParams(),
192
264
  output: nodeNewOutput(),
193
- dynamicState: () => [kindsStateBlock(), profilesStateBlock()].join('\n'),
265
+ dynamicState: () => kindsStateBlock(),
194
266
  outputKind: 'object',
195
- effects: [
196
- 'Creates nodes/<id>/ in the canvas home, indexes the node in canvas.db, and starts its detached broker engine.',
197
- 'A managed child creates an active parent subscription; its pushes wake the parent. --root creates an independent node with spawned_by provenance and no parent subscription.',
198
- '--worktree creates a managed worktree at ~/.crouter/canvas/worktrees/<node-id>/ on a new crtr/<node-id> branch. --output-schema writes nodes/<id>/output-schema.json. --no-kickoff starts an idle root without a first model turn.',
199
- 'An in-tmux root opens a viewer in the current tmux session. When auto_open_child_viewers is true, a managed child opens beside a live spawner viewer; false leaves it running in the background.',
200
- ],
267
+ effects: nodeNewEffects(),
201
268
  },
202
269
  run: (input) => runNodeCreation(input),
203
270
  render: (r) => {
204
271
  const win = r['window'] ? ` in window ${r['window']}` : '';
205
- const lines = [`Spawned "${r['name']}" (${r['node_id']}) — ${r['status']}${win}.`];
272
+ const lines = [`Spawned "${r['name']}" (${r['node_id']})${win}.`];
206
273
  if (r['session'])
207
274
  lines.push(`- session: ${r['session']}`);
208
275
  lines.push('', String(r['follow_up']));
@@ -4,7 +4,7 @@ import { InputError } from '../../core/io.js';
4
4
  import { isPidAlive } from '../../core/canvas/pid.js';
5
5
  import { fullName } from '../../core/canvas/labels.js';
6
6
  import { fuzzyMatch } from '../../core/canvas/browse/model.js';
7
- import { loadProfileManifest } from '../../core/profiles/manifest.js';
7
+ import { resolveProfileOperand } from '../../core/profiles/manifest.js';
8
8
  import { profilesStateBlock } from '../../core/profiles/state-block.js';
9
9
  import { nodeSnapshotLeaf } from '../node-snapshot.js';
10
10
  import { nodeTranscriptLeaf } from '../node-transcript.js';
@@ -26,7 +26,7 @@ const nodeList = defineLeaf({
26
26
  { kind: 'flag', name: 'kind', type: 'string', required: false, constraint: 'Filter by persona kind (comma-separated for several). The <kinds> list below names every top-level installable kind; a registered sub-kind filters too by exact path even though it is not listed.' },
27
27
  { kind: 'flag', name: 'mode', type: 'string', required: false, constraint: 'Filter by mode (comma-separated for several; accepted values: base, orchestrator).' },
28
28
  { kind: 'flag', name: 'lifecycle', type: 'string', required: false, constraint: 'Filter by lifecycle (comma-separated for several; accepted values: terminal, resident).' },
29
- { kind: 'flag', name: 'profile', type: 'string', required: false, constraint: 'Filter by profile. On the local canvas, accepts an exact profile id or a unique manifest name from the <profiles> list below. Against a remote canvas, pass the exact profile id carried by its node rows.' },
29
+ { kind: 'flag', name: 'profile', type: 'string', required: false, constraint: 'Filter by profile. On the local canvas, accepts a profile id or manifest name from the <profiles> list below, or a close-enough name that resolves to the nearest profile. Against a remote canvas, pass the exact profile id carried by its node rows.' },
30
30
  { kind: 'flag', name: 'name', type: 'string', required: false, constraint: 'Filter by the displayed node label (handle plus generated description) with the canvas browser’s case-insensitive fuzzy subsequence match.' },
31
31
  { 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.' },
32
32
  { 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.' },
@@ -59,7 +59,7 @@ const nodeList = defineLeaf({
59
59
  // cannot resolve this machine's manifest names.
60
60
  const profileId = canvasSource instanceof RemoteCanvasSource
61
61
  ? profile
62
- : profile === undefined ? undefined : loadProfileManifest(profile).profileId;
62
+ : profile === undefined ? undefined : resolveProfileOperand(profile).profileId;
63
63
  let scope;
64
64
  if (under !== undefined && under !== '') {
65
65
  if (await canvasSource.getNode(under).catch(rethrowAsCliError) === null) {
@@ -4,6 +4,7 @@ import { InputError } from '../../core/io.js';
4
4
  import { cliClient, getNodeOrNull, rethrowAsCliError } from '../api-client.js';
5
5
  import { nodeReviveLeaf } from '../node-lifecycle-revive.js';
6
6
  import { assertKind, kindsStateBlock } from './create.js';
7
+ import { MODEL_SPEC_FORMS } from '../../core/runtime/model-selection.js';
7
8
  import { closePane, paneExists, respawnPaneSync, viewerEnv, viewerPaneOf, nodeInPane } from '../surface/node/placement.js';
8
9
  const nodeRecycle = defineLeaf({
9
10
  name: 'recycle',
@@ -19,8 +20,6 @@ const nodeRecycle = defineLeaf({
19
20
  output: [
20
21
  { name: 'recycled', type: 'boolean', required: true, constraint: 'True when the pane was recycled into a fresh root.' },
21
22
  { name: 'node_id', type: 'string', required: false, constraint: 'The finished node.' },
22
- { name: 'finalized', type: 'boolean', required: false, constraint: 'True when a final report was pushed to its subscribers.' },
23
- { name: 'delivered', type: 'number', required: false, constraint: 'How many subscribers/managers received the final report.' },
24
23
  { name: 'new_root', type: 'string', required: false, constraint: 'The fresh root node booted into the pane.' },
25
24
  ],
26
25
  outputKind: 'object',
@@ -48,21 +47,19 @@ const nodeRecycle = defineLeaf({
48
47
  if (pane !== undefined && pane !== '') {
49
48
  paneRecycled = respawnPaneSync({ pane, cwd: detail.cwd, env: viewerEnv(), command: 'crtr' });
50
49
  }
51
- return { recycled: paneRecycled, node_id: id, finalized: true, delivered: undefined, new_root: newRoot };
50
+ return { recycled: paneRecycled, node_id: id, new_root: newRoot };
52
51
  },
53
52
  render: (r) => {
54
53
  if (r['recycled'] !== true) {
55
54
  // A broker recycle that finalized + spawned a fresh root but whose new
56
55
  // broker never served its socket: the state DID change (node finished, root
57
56
  // born), only the viewer re-attach failed — say so, don't call it a no-op.
58
- if (r['finalized'] === true && r['new_root']) {
57
+ if (r['new_root']) {
59
58
  return `Finished ${r['node_id']} and spawned a fresh broker root (${r['new_root']}), but its viewer did not attach — the new broker did not come up. Re-focus the root to view it.`;
60
59
  }
61
60
  return `Recycle failed for ${r['node_id'] ?? '?'} — not in tmux, or no agent in this pane.`;
62
61
  }
63
- const lines = [`Recycled the pane — finished ${r['node_id']}.`, `- finalized: ${r['finalized']}`];
64
- if (r['finalized'] === true)
65
- lines.push(`- delivered: ${r['delivered']}`);
62
+ const lines = [`Recycled the pane — finished ${r['node_id']}.`];
66
63
  if (r['new_root'])
67
64
  lines.push(`- new root: ${r['new_root']}`);
68
65
  return lines.join('\n');
@@ -83,7 +80,6 @@ const nodeClose = defineLeaf({
83
80
  { kind: 'flag', name: 'pane', type: 'string', required: false, constraint: 'tmux pane id whose node to close. Defaults to $TMUX_PANE / your current pane. The Alt+C menu passes this for you.' },
84
81
  ],
85
82
  output: [
86
- { name: 'closed', type: 'boolean', required: true, constraint: 'True when the node (and its exclusive subtree) was closed.' },
87
83
  { name: 'node_id', type: 'string', required: false, constraint: 'The node that was closed — the cascade root.' },
88
84
  { name: 'count', type: 'number', required: false, constraint: 'How many nodes were closed (root + cascaded descendants).' },
89
85
  { name: 'closed_ids', type: 'string[]', required: false, constraint: 'All closed node ids, kill order (leaves first, root last).' },
@@ -107,11 +103,9 @@ const nodeClose = defineLeaf({
107
103
  const res = await cliClient()
108
104
  .closeNode(id)
109
105
  .catch((err) => rethrowAsCliError(err, 'List nodes with `crtr node inspect list`.'));
110
- return { closed: true, node_id: res.root, count: res.closed.length, closed_ids: res.closed, spared: res.spared };
106
+ return { node_id: res.root, count: res.closed.length, closed_ids: res.closed, spared: res.spared };
111
107
  },
112
108
  render: (r) => {
113
- if (r['closed'] !== true)
114
- return 'Close failed — no node found in this pane to close.';
115
109
  const spared = r['spared']?.length ?? 0;
116
110
  return `Closed ${r['node_id']} and its exclusive subtree — ${r['count']} node(s) closed${spared > 0 ? `, ${spared} spared (still managed from outside the subtree)` : ''}.`;
117
111
  },
@@ -119,6 +113,7 @@ const nodeClose = defineLeaf({
119
113
  // ---------------------------------------------------------------------------
120
114
  // node config — reconfigure a node's settings headlessly (model/lifecycle/kind/mode/name)
121
115
  // ---------------------------------------------------------------------------
116
+ const NODE_CONFIG_MODEL_EFFECT = 'Model change first: a live broker switches immediately over its socket; a dormant node persists the durable model override and launch recipe for its next revive.';
122
117
  export const nodeConfig = defineLeaf({
123
118
  name: 'config',
124
119
  description: 'reconfigure a node\'s settings headlessly — model, lifecycle (terminal/resident), kind, mode, name',
@@ -129,7 +124,21 @@ export const nodeConfig = defineLeaf({
129
124
  params: [
130
125
  { kind: 'flag', name: 'node', type: 'string', required: false, constraint: 'Target node id. Defaults to the node in --pane, else CRTR_NODE_ID.' },
131
126
  { kind: 'flag', name: 'pane', type: 'string', required: false, constraint: 'tmux pane id to resolve the node from. Defaults to $TMUX_PANE.' },
132
- { 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.' },
127
+ {
128
+ kind: 'flag',
129
+ name: 'model',
130
+ type: 'string',
131
+ required: false,
132
+ constraint: 'Durable model change. Omit unless the task or governing guidance requires a different model.',
133
+ focusedHelp: {
134
+ whenToUse: 'Use only when intentionally changing this node’s durable model because the user, task, or governing agent guidance requires a specific provider, model, capability tier, or family. Omit it to keep the current model; do not use it merely to inspect available models or experiment with a live node.',
135
+ value: `Accepted durable forms: ${MODEL_SPEC_FORMS}. A live broker also accepts a free-text substring matched against its loaded model registry; a dormant node cannot resolve a substring and requires a durable form.`,
136
+ effects: [
137
+ NODE_CONFIG_MODEL_EFFECT,
138
+ 'A concrete provider/id pins that provider. Portable tiers and aliases preserve logical intent for runtime routing; a persona quality floor may raise a weaker request.',
139
+ ],
140
+ },
141
+ },
133
142
  { 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 while attached or awaiting something live.' },
134
143
  { kind: 'flag', name: 'kind', type: 'string', required: false, constraint: 'Persona kind. The <kinds> list below names every top-level installable kind and when to use each; a registered sub-kind is valid too by exact path but not listed here.' },
135
144
  { 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.' },
@@ -142,11 +151,10 @@ export const nodeConfig = defineLeaf({
142
151
  { name: 'kind', type: 'string', required: false, constraint: 'The new kind, present only when --kind changed it.' },
143
152
  { name: 'mode', type: 'string', required: false, constraint: 'The new mode, present only when --mode changed it.' },
144
153
  { name: 'name', type: 'string', required: false, constraint: 'The new name, present only when --name changed it.' },
145
- { name: 'applied', type: 'string[]', required: false, constraint: 'The fields that changed, in application order.' },
146
154
  ],
147
155
  outputKind: 'object',
148
156
  effects: [
149
- 'Model change first: live broker switches over its socket; dormant node persists the durable recipe (model_override + launch.model).',
157
+ NODE_CONFIG_MODEL_EFFECT,
150
158
  'Kind, mode, and lifecycle changes rebuild the launch spec from the fresh node meta; setting mode to orchestrator seeds a roadmap scaffold if absent.',
151
159
  'Name changes update the row and, when the node already has a live window, rename that viewer window to the new full name.',
152
160
  ],
@@ -162,7 +170,7 @@ export const nodeConfig = defineLeaf({
162
170
  // node's scope as a backstop.
163
171
  const modelSpec = input['model']?.trim();
164
172
  if (modelSpec === '') {
165
- 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.' });
173
+ throw new InputError({ error: 'empty_spec', message: 'a model spec is required', field: 'model', next: 'Run `crtr node config --model -h` and read the focused contract before retrying.' });
166
174
  }
167
175
  const kindSpec = input['kind']?.trim();
168
176
  const lifecycleSpec = input['lifecycle']?.trim();
@@ -204,7 +212,7 @@ export const nodeConfig = defineLeaf({
204
212
  const detail = await cliClient()
205
213
  .patchConfig(nodeId, patch)
206
214
  .catch((err) => rethrowAsCliError(err, 'List nodes with `crtr node inspect list`.'));
207
- const result = { node_id: nodeId, applied };
215
+ const result = { node_id: nodeId };
208
216
  if (applied.includes('model'))
209
217
  result.model = detail.model_override ?? modelSpec;
210
218
  if (applied.includes('lifecycle'))
@@ -249,7 +257,6 @@ export const nodePromote = defineLeaf({
249
257
  output: [
250
258
  { name: 'node_id', type: 'string', required: true, constraint: 'The promoted node.' },
251
259
  { name: 'kind', type: 'string', required: true, constraint: 'The kind it now orchestrates as.' },
252
- { name: 'mode', type: 'string', required: true, constraint: 'Now "orchestrator".' },
253
260
  { name: 'model', type: 'string', required: false, constraint: 'The model tier it now runs on, present only when you changed it with --model.' },
254
261
  { name: 'lifecycle', type: 'string', required: true, constraint: '"resident" if you passed --resident, else unchanged (typically "terminal").' },
255
262
  { name: 'roadmap_written', type: 'boolean', required: true, constraint: 'True if a roadmap scaffold was seeded by this call.' },
@@ -275,7 +282,6 @@ export const nodePromote = defineLeaf({
275
282
  return {
276
283
  node_id: detail.node_id,
277
284
  kind: detail.kind,
278
- mode: detail.mode,
279
285
  lifecycle: detail.lifecycle,
280
286
  ...(model !== undefined && detail.model_override != null ? { model: detail.model_override } : {}),
281
287
  roadmap_written: detail.roadmap_written ?? false,
@@ -323,7 +329,6 @@ const nodeDemote = defineLeaf({
323
329
  ],
324
330
  output: [
325
331
  { name: 'node_id', type: 'string', required: true, constraint: 'The demoted node.' },
326
- { name: 'lifecycle', type: 'string', required: true, constraint: 'Always "terminal" after a demote.' },
327
332
  { name: 'detached', type: 'boolean', required: false, constraint: 'True when --detach closed the agent\'s viewer pane (broker keeps running).' },
328
333
  ],
329
334
  outputKind: 'object',
@@ -343,9 +348,9 @@ const nodeDemote = defineLeaf({
343
348
  if (viewer !== undefined && viewer !== '' && paneExists(viewer))
344
349
  detached = closePane(viewer);
345
350
  }
346
- return { node_id: detail.node_id, lifecycle: detail.lifecycle, detached };
351
+ return { node_id: detail.node_id, detached };
347
352
  },
348
- render: (r) => `Demoted ${r['node_id']} — lifecycle now ${r['lifecycle']} (in place)${r['detached'] === true ? ', viewer closed (broker still running off-screen)' : ''}.`,
353
+ render: (r) => `Demoted ${r['node_id']} — lifecycle now terminal (in place)${r['detached'] === true ? ', viewer closed (broker still running off-screen)' : ''}.`,
349
354
  });
350
355
  // ---------------------------------------------------------------------------
351
356
  // node lifecycle — recycle, close, or demote a node
@@ -370,7 +375,7 @@ export const nodeYield = defineLeaf({
370
375
  help: {
371
376
  name: 'node yield',
372
377
  summary: 'request a fresh context window against your roadmap or original goal; mode stays unchanged unless --promote is passed',
373
- guide: 'Persist anything needed after refresh and send any pending human question before invoking; in-flight asks survive the refresh.',
378
+ guide: 'Before invoking: (1) Bring `$CRTR_CONTEXT_DIR/roadmap.md` current — it, not the yield note, carries the goal into the fresh window. Keep its seeded sections (Goal, Exit criteria, Phases) and keep it short: strategy and present state only — what is settled, what remains, the current phase’s one-line status, plus dead ends worth not repeating. Delete completed items instead of marking them done; the roadmap should shrink as work completes. (2) Move anything worth remembering but not needed next window into memory (`crtr memory write -h`): node-local for lessons specific to this node’s task, the workspace store for lessons that would benefit other agents working here. (3) Send any pending human question; in-flight asks survive the refresh.',
374
379
  params: [
375
380
  { kind: 'flag', name: 'promote', type: 'bool', required: false, constraint: 'Also become an orchestrator as you refresh. Pass only when (1) the remaining work contains independent units that can run in parallel and (2) the task is large enough that parallel execution will materially improve intelligence, productivity, or elapsed throughput after coordination and synthesis. Task length, context exhaustion, sequential phases, or one helper are not enough; keep hands-on work in base mode and yield again as needed. Promote when coordinating and integrating children becomes your primary job. Already an orchestrator? This is not an error: your mode stays orchestrator and a missing roadmap is seeded.' },
376
381
  { kind: 'flag', name: 'kind', type: 'string', required: false, constraint: 'Respecialize as you refresh. The <kinds> list below names every top-level available kind and when to choose it; a registered sub-kind is valid too by exact path but not listed here. Defaults to your current kind. With --promote it sets the orchestrator kind you become; without --promote it changes the kind while preserving your current mode.' },
@@ -379,17 +384,15 @@ export const nodeYield = defineLeaf({
379
384
  // `crtr node yield`; that invocation fails fast and surfaces yield's full help just in
380
385
  // time, at the point of use. Keep the note required rather than pre-documenting stdin
381
386
  // in kernel guidance.
382
- { kind: 'stdin', name: 'message', required: true, constraint: 'A note to your future self — what to do the moment you wake fresh. Surfaced as <yield-message> in the next revive. Pass as a positional or pipe via heredoc.' },
387
+ { kind: 'stdin', name: 'message', required: true, constraint: 'A note to your future self — what you just tried, where things stand, and the first move on waking: enough to continue efficiently, no more. Durable strategy belongs in the roadmap, not here. Surfaced as <yield-message> in the next revive. Pass as a positional or pipe via heredoc.' },
383
388
  ],
384
389
  output: [
385
390
  { name: 'node_id', type: 'string', required: true, constraint: 'The yielding node.' },
386
- { name: 'promoted', type: 'boolean', required: true, constraint: 'True iff you passed --promote.' },
387
391
  { name: 'model', type: 'string', required: false, constraint: 'The model tier your fresh revive will run on, present only when you changed it with --model.' },
388
- { name: 'will_refresh', type: 'boolean', required: true, constraint: 'Always true: end your turn and you will revive fresh.' },
389
392
  ],
390
393
  dynamicState: () => kindsStateBlock(),
391
394
  outputKind: 'object',
392
- effects: ['Sets intent=refresh; the stophook shuts the process down on next stop and the daemon revives it fresh.', '--kind and/or --model durably reshape the node (kind respecialization, model-tier pin) before the refresh-revive; --promote additionally flips mode→orchestrator and seeds a roadmap scaffold — mode is otherwise left exactly as it was.'],
395
+ effects: ['The stophook shuts the process down on next stop and the daemon revives it fresh.', '--kind and/or --model durably reshape the node (kind respecialization, model-tier pin) before the refresh-revive; --promote additionally flips mode→orchestrator and seeds a roadmap scaffold — mode is otherwise left exactly as it was.'],
393
396
  },
394
397
  run: async (input) => {
395
398
  const id = process.env['CRTR_NODE_ID'];
@@ -410,6 +413,6 @@ export const nodeYield = defineLeaf({
410
413
  const detail = await cliClient()
411
414
  .yieldNode(id, { note: message, ...(kind !== undefined ? { kind } : {}), ...(model !== undefined ? { model } : {}), ...(doPromote ? { promote: true } : {}) })
412
415
  .catch((err) => rethrowAsCliError(err));
413
- return { node_id: detail.node_id, promoted: doPromote, ...(model !== undefined && detail.model_override != null ? { model: detail.model_override } : {}), will_refresh: true };
416
+ return { node_id: detail.node_id, ...(model !== undefined && detail.model_override != null ? { model: detail.model_override } : {}) };
414
417
  },
415
418
  });
@@ -1,7 +1,7 @@
1
1
  // `crtr node message` — immediate inbox delivery and typed-output requests.
2
2
  import { defineBranch, defineLeaf } from '../../core/command.js';
3
3
  import { InputError } from '../../core/io.js';
4
- import { parseOutputSchemaValue } from '../../core/runtime/structured-output.js';
4
+ import { OUTPUT_SCHEMA_CONSTRAINTS, OUTPUT_SCHEMA_TRANSPORT, parseOutputSchemaValue } from '../../core/runtime/structured-output.js';
5
5
  import { cliClient, getNodeOrNull, rethrowAsCliError } from '../api-client.js';
6
6
  function requireNonBlankDiscriminant(input, name) {
7
7
  const value = input[name];
@@ -36,29 +36,27 @@ async function resolveMsgTarget(input) {
36
36
  }
37
37
  function messageOutput() {
38
38
  return [
39
- { name: 'target', type: 'string', required: true, constraint: 'self or the target node id.' },
40
- { name: 'delivered', type: 'boolean', required: false, constraint: 'True when an inbox message was delivered.' },
41
39
  { name: 'revived', type: 'boolean', required: false, constraint: 'True when the target was revived.' },
42
40
  { name: 'guidance', type: 'string', required: true, constraint: 'Immediate action confirmation.' },
43
41
  ];
44
42
  }
45
43
  function messageSendParams() {
46
44
  return [
47
- { kind: 'stdin', name: 'body', required: false, constraint: 'Visible message body. Required unless --fresh, --output-schema, or --situational-context is supplied.' },
45
+ { kind: 'stdin', name: 'body', required: false, constraint: 'Visible message body. Required unless --situational-context is supplied.' },
48
46
  { kind: 'flag', name: 'to', type: 'string', required: false, constraint: 'Target an existing node by id. Exactly one of --to or --self is required.' },
49
47
  { kind: 'flag', name: 'self', type: 'bool', required: false, constraint: 'Target the calling node. Exactly one of --to or --self is required.' },
50
48
  { kind: 'flag', name: 'tier', type: 'enum', choices: ['critical', 'urgent', 'normal', 'deferred'], required: false, default: 'normal', constraint: 'Delivery urgency. Deferred never wakes an idle node.' },
51
49
  { kind: 'flag', name: 'situational-context', type: 'string', required: false, constraint: 'Non-empty hidden ambient context delivered beside the message, or alone.' },
52
- { kind: 'flag', name: 'reopen', type: 'bool', required: false, constraint: 'Required to re-task a finalized target: clears its finalization latch before delivery. Rejected when the target is not finalized.' },
50
+ { kind: 'flag', name: 'reopen', type: 'bool', required: false, constraint: 'Re-task the target: commits it resident and clears any finalization latch before delivery. Valid for live, parked, and finalized targets.' },
53
51
  ];
54
52
  }
55
53
  function messageRequestParams() {
56
54
  return [
57
- { kind: 'stdin', name: 'body', required: false, constraint: 'Visible message body. Required unless --fresh, --output-schema, or --situational-context is supplied.' },
55
+ { kind: 'stdin', name: 'body', required: false, constraint: 'Visible message body. The schema alone is a complete request.' },
58
56
  { kind: 'flag', name: 'to', type: 'string', required: false, constraint: 'Target an existing node by id. Exactly one of --to or --self is required.' },
59
57
  { kind: 'flag', name: 'self', type: 'bool', required: false, constraint: 'Target the calling node. Exactly one of --to or --self is required.' },
60
58
  { kind: 'flag', name: 'tier', type: 'enum', choices: ['critical', 'urgent', 'normal'], required: false, default: 'normal', constraint: 'Delivery urgency. Deferred never wakes an idle node.' },
61
- { kind: 'flag', name: 'output-schema', type: 'string', required: true, constraint: 'Path to a JSON Schema file, or inline JSON beginning with `{`. Grants a one-off submit tool.' },
59
+ { kind: 'flag', name: 'output-schema', type: 'string', required: true, constraint: `JSON Schema the result must satisfy. ${OUTPUT_SCHEMA_TRANSPORT} The target answers through a constrained submit tool, then keeps working. ${OUTPUT_SCHEMA_CONSTRAINTS}` },
62
60
  { kind: 'flag', name: 'situational-context', type: 'string', required: false, constraint: 'Non-empty hidden ambient context delivered beside the message, or alone.' },
63
61
  { kind: 'flag', name: 'reopen', type: 'bool', required: false, hidden: true, constraint: 'Clear a finalized target latch before delivery or fresh revive.' },
64
62
  ];
@@ -69,58 +67,39 @@ async function runMessageEngine(input) {
69
67
  if (situationalContextSupplied && situationalContextRaw === '') {
70
68
  throw new InputError({ error: 'empty_situational_context', message: '--situational-context must contain non-whitespace text', field: 'situational-context', next: 'Pass non-whitespace text, or omit --situational-context.' });
71
69
  }
72
- const { targetId, isSelf } = await resolveMsgTarget(input);
70
+ const { targetId } = await resolveMsgTarget(input);
73
71
  const bodyRaw = input['body'];
74
72
  const body = bodyRaw !== undefined ? bodyRaw.trim() : undefined;
75
73
  const hasBody = body !== undefined && body !== '';
76
74
  const tierRaw = input['tier'];
77
75
  const hasTier = tierRaw !== undefined && tierRaw !== '';
78
- const fresh = input['fresh'] === true;
79
76
  const outputSchemaParsed = parseOutputSchemaValue(input['outputSchema']);
80
77
  const outputSchema = outputSchemaParsed !== null ? JSON.stringify(outputSchemaParsed) : null;
81
78
  const hasSituational = situationalContextRaw !== undefined && situationalContextRaw !== '';
82
79
  const reopen = input['reopen'] === true;
83
- if (fresh && (hasBody || hasTier || outputSchema !== null)) {
84
- throw new InputError({ error: 'bad_fresh_combo', message: '--fresh cannot combine with a body, --tier, or --output-schema', next: 'Drop the conflicting input; --fresh is a revive, not a message.' });
85
- }
86
80
  if (outputSchema !== null && tierRaw === 'deferred') {
87
81
  throw new InputError({ error: 'output_schema_deferred_tier', message: '--output-schema cannot combine with --tier deferred because the target must answer', field: 'tier', next: 'Use normal, urgent, or critical tier, or drop --output-schema.' });
88
82
  }
89
- const target = isSelf ? 'self' : targetId;
90
83
  const senderFrom = process.env['CRTR_NODE_ID'];
91
84
  const client = cliClient();
92
- let result;
93
- if (fresh) {
94
- const res = await client.sendMessage(targetId, {
95
- body: '',
96
- fresh: true,
97
- ...(reopen ? { reopen: true } : {}),
98
- ...(hasSituational ? { situational_context: situationalContextRaw } : {}),
99
- ...(senderFrom !== undefined ? { from: senderFrom } : {}),
100
- }).catch(rethrowAsCliError);
101
- result = { target, revived: res.revived, guidance: `Fresh-revived ${isSelf ? 'yourself' : targetId} now.` };
102
- }
103
- else {
104
- if (!hasBody && !hasSituational && outputSchema === null) {
105
- throw new InputError({ error: 'empty_body', message: 'a message body is required unless --output-schema or --situational-context is supplied', field: 'body', next: 'Pass the message on stdin, or supply one of the documented alternatives.' });
106
- }
107
- const tier = (hasTier ? tierRaw : 'normal');
108
- const res = await client.sendMessage(targetId, {
109
- body: body ?? '',
110
- tier,
111
- ...(reopen ? { reopen: true } : {}),
112
- ...(hasSituational ? { situational_context: situationalContextRaw } : {}),
113
- ...(outputSchema !== null ? { output_schema: outputSchema } : {}),
114
- ...(senderFrom !== undefined ? { from: senderFrom } : {}),
115
- }).catch(rethrowAsCliError);
116
- const guidance = res.revived
117
- ? 'Delivered — the dormant target was revived to receive it.'
118
- : tier === 'deferred'
119
- ? "Delivered — held for the target's next natural cycle; not revived."
120
- : 'Delivered.';
121
- result = { target, delivered: true, revived: res.revived, guidance };
85
+ if (!hasBody && !hasSituational && outputSchema === null) {
86
+ throw new InputError({ error: 'empty_body', message: 'a message body is required unless --output-schema or --situational-context is supplied', field: 'body', next: 'Pass the message on stdin, or supply one of the documented alternatives.' });
122
87
  }
123
- return result;
88
+ const tier = (hasTier ? tierRaw : 'normal');
89
+ const res = await client.sendMessage(targetId, {
90
+ body: body ?? '',
91
+ tier,
92
+ ...(reopen ? { reopen: true } : {}),
93
+ ...(hasSituational ? { situational_context: situationalContextRaw } : {}),
94
+ ...(outputSchema !== null ? { output_schema: outputSchema } : {}),
95
+ ...(senderFrom !== undefined ? { from: senderFrom } : {}),
96
+ }).catch(rethrowAsCliError);
97
+ const guidance = res.revived
98
+ ? 'Delivered — the dormant target was revived to receive it.'
99
+ : tier === 'deferred'
100
+ ? "Delivered — held for the target's next natural cycle; not revived."
101
+ : 'Delivered.';
102
+ return { revived: res.revived, guidance };
124
103
  }
125
104
  const nodeMessageSend = defineLeaf({
126
105
  name: 'send',
@@ -132,7 +111,7 @@ const nodeMessageSend = defineLeaf({
132
111
  params: messageSendParams(),
133
112
  output: messageOutput(),
134
113
  outputKind: 'object',
135
- effects: ['Appends one inbox entry. Critical, urgent, and normal delivery may revive a dormant target; deferred waits for its next natural cycle.', '--reopen clears a finalized target\'s finalization latch before delivery, allowing it to take a new mandate. Without it, a finalized target rejects delivery before an inbox entry is appended or a revive is attempted.'],
114
+ effects: ['Appends one inbox entry. Critical, urgent, and normal delivery may revive a dormant target; deferred waits for its next natural cycle.', '--reopen commits the target resident and clears any finalization latch before delivery, allowing it to take a new mandate; it is valid for live, parked, and finalized targets. Without it, a finalized target rejects delivery before an inbox entry is appended or a revive is attempted.'],
136
115
  },
137
116
  run: (input) => runMessageEngine(input),
138
117
  render: (r) => String(r['guidance']),