@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
@@ -55,9 +55,9 @@ function requestId(input) {
55
55
  }
56
56
  /** The one projection of a request record onto leaf output. Absent fields stay
57
57
  * absent: a dismissal or a withdrawal has no responses to report. */
58
- function requestResult(dto) {
58
+ function requestResult(dto, includeRequestId = true) {
59
59
  return {
60
- request_id: dto.request_id,
60
+ ...(includeRequestId ? { request_id: dto.request_id } : {}),
61
61
  state: dto.state,
62
62
  title: dto.title,
63
63
  ...(dto.subtitle === undefined ? {} : { subtitle: dto.subtitle }),
@@ -71,8 +71,7 @@ function requestResult(dto) {
71
71
  ...(dto.delivery === undefined ? {} : { delivery: dto.delivery }),
72
72
  };
73
73
  }
74
- const RECORD_OUTPUT = [
75
- { name: 'request_id', type: 'string', required: true, constraint: 'The request — the same 64-hex id the human inbox lists it under.' },
74
+ const REQUEST_RESULT_OUTPUT = [
76
75
  { name: 'state', type: 'pending | answered | dismissed | canceled', required: true, constraint: 'answered: the person (or an authorized responder) submitted the typed responses. dismissed: the recipient surface closed it unanswered. canceled: the requester withdrew it.' },
77
76
  { name: 'title', type: 'string', required: true, constraint: 'Page title as currently published.' },
78
77
  { name: 'subtitle', type: 'string', required: false, constraint: 'Page subtitle when the page carries one.' },
@@ -85,6 +84,10 @@ const RECORD_OUTPUT = [
85
84
  { name: 'action', type: 'object', required: false, constraint: '{name, payload} frozen at creation; an omitted payload is frozen as null. Absent when no action is bound.' },
86
85
  { name: 'delivery', type: 'object', required: false, constraint: 'Completion-delivery state for the bound action: {state: none | pending | running | accepted | permanent_failed, attempt, next_attempt_at?, accepted_at?, permanent_failed_at?, last_failure?}. Absent when no action is bound.' },
87
86
  ];
87
+ const RECORD_OUTPUT = [
88
+ { name: 'request_id', type: 'string', required: true, constraint: 'The request — the same 64-hex id the human inbox lists it under.' },
89
+ ...REQUEST_RESULT_OUTPUT,
90
+ ];
88
91
  // ---------------------------------------------------------------------------
89
92
  // create
90
93
  // ---------------------------------------------------------------------------
@@ -106,9 +109,7 @@ const createLeaf = defineLeaf({
106
109
  ],
107
110
  output: [
108
111
  { name: 'request_id', type: 'string', required: true, constraint: 'The created request — pass it to get/replace/respond/cancel. Stable, and the deduplication key every completion destination keys on.' },
109
- { name: 'state', type: 'string', required: true, constraint: 'Always "pending": creation never settles.' },
110
112
  { name: 'action', type: 'object', required: false, constraint: '{name} of the frozen action binding. Absent when the request carries no action.' },
111
- { name: 'delivery_state', type: 'string', required: true, constraint: 'Always "none" at creation: delivery is enqueued at settlement, not before.' },
112
113
  ],
113
114
  outputKind: 'object',
114
115
  effects: [
@@ -126,9 +127,7 @@ const createLeaf = defineLeaf({
126
127
  const created = await cliClient().createHumanRequest(body);
127
128
  return {
128
129
  request_id: created.request_id,
129
- state: created.state,
130
130
  ...(created.action === undefined ? {} : { action: created.action }),
131
- delivery_state: created.delivery_state,
132
131
  };
133
132
  }
134
133
  catch (error) {
@@ -174,7 +173,7 @@ const replaceLeaf = defineLeaf({
174
173
  { kind: 'positional', name: 'request_id', type: 'string', required: true, constraint: 'request_id returned by `human request create`.' },
175
174
  { kind: 'flag', name: 'page', type: 'path', required: true, constraint: 'The revised page: a .tsx module or a complete .html/.htm document. Read here and sent inline.' },
176
175
  ],
177
- output: [...RECORD_OUTPUT],
176
+ output: [...REQUEST_RESULT_OUTPUT],
178
177
  outputKind: 'object',
179
178
  effects: [
180
179
  'Rewrites the page and its typed response contract in place; open surfaces reload it without a second request.',
@@ -186,7 +185,7 @@ const replaceLeaf = defineLeaf({
186
185
  const id = requestId(input);
187
186
  const body = { page: readPageFile(input['page']) };
188
187
  try {
189
- return requestResult(await cliClient().replaceHumanRequest(id, body));
188
+ return requestResult(await cliClient().replaceHumanRequest(id, body), false);
190
189
  }
191
190
  catch (error) {
192
191
  rethrowAsCliError(error);
@@ -213,7 +212,7 @@ const respondLeaf = defineLeaf({
213
212
  constraint: 'JSON file holding the respond object: {"responses":{"<component_id>":{...}}, "actor"?:"<who answered>"}. One typed response per response-bearing component; the daemon validates the complete map against the currently published page.',
214
213
  },
215
214
  ],
216
- output: [...RECORD_OUTPUT],
215
+ output: [...REQUEST_RESULT_OUTPUT],
217
216
  outputKind: 'object',
218
217
  effects: [
219
218
  'Settles the request as answered and drops it from the human inbox.',
@@ -225,7 +224,7 @@ const respondLeaf = defineLeaf({
225
224
  const id = requestId(input);
226
225
  const body = readJsonFile(input['responseFile'], 'response-file', 'the respond object');
227
226
  try {
228
- return requestResult(await cliClient().respondHumanRequest(id, body));
227
+ return requestResult(await cliClient().respondHumanRequest(id, body), false);
229
228
  }
230
229
  catch (error) {
231
230
  rethrowAsCliError(error);
@@ -246,7 +245,7 @@ const cancelLeaf = defineLeaf({
246
245
  { kind: 'positional', name: 'request_id', type: 'string', required: true, constraint: 'request_id returned by `human request create`.' },
247
246
  { kind: 'flag', name: 'reason', type: 'string', required: false, constraint: 'Short note recorded on the terminal result and carried in the completion document.' },
248
247
  ],
249
- output: [...RECORD_OUTPUT],
248
+ output: [...REQUEST_RESULT_OUTPUT],
250
249
  outputKind: 'object',
251
250
  effects: [
252
251
  'Settles the request as canceled and drops it from the human inbox — the person is no longer asked.',
@@ -258,7 +257,7 @@ const cancelLeaf = defineLeaf({
258
257
  const id = requestId(input);
259
258
  const reason = typeof input['reason'] === 'string' && input['reason'] !== '' ? input['reason'] : undefined;
260
259
  try {
261
- return requestResult(await cliClient().cancelHumanRequest(id, reason === undefined ? {} : { reason }));
260
+ return requestResult(await cliClient().cancelHumanRequest(id, reason === undefined ? {} : { reason }), false);
262
261
  }
263
262
  catch (error) {
264
263
  rethrowAsCliError(error);
@@ -46,13 +46,13 @@ function explicitDeliver(input, context) {
46
46
  const deliver = input['deliver'];
47
47
  return deliver === 'wake' || deliver === 'quiet' ? deliver : undefined;
48
48
  }
49
- function mutationOutput(result) {
49
+ function mutationOutput(result, includeAnchor = true) {
50
50
  const { comment } = result;
51
51
  return {
52
52
  comment_id: comment.comment_id,
53
53
  status: comment.status,
54
54
  revision: comment.revision,
55
- anchor: comment.anchor,
55
+ ...(includeAnchor ? { anchor: comment.anchor } : {}),
56
56
  author: comment.author,
57
57
  text: comment.text,
58
58
  changed: result.changed,
@@ -143,7 +143,6 @@ const humanReviewCommentCreate = defineLeaf({
143
143
  { name: 'comment_id', type: 'string', required: true, constraint: 'Daemon-owned comment id.' },
144
144
  { name: 'status', type: 'string', required: true, constraint: 'Current comment status: open, resolved, or deleted.' },
145
145
  { name: 'revision', type: 'integer', required: true, constraint: 'Current per-comment revision.' },
146
- { name: 'anchor', type: 'object', required: true, constraint: 'Current whole-line anchor.' },
147
146
  { name: 'author', type: 'object', required: true, constraint: 'Daemon-derived human or node attribution.' },
148
147
  { name: 'text', type: 'string', required: true, constraint: 'Stored full comment text.' },
149
148
  { name: 'changed', type: 'boolean', required: true, constraint: 'Whether this invocation changed the comment.' },
@@ -162,7 +161,7 @@ const humanReviewCommentCreate = defineLeaf({
162
161
  actor_node_id: nodeActor(),
163
162
  ...(explicitDeliver(input, context) === undefined ? {} : { deliver: explicitDeliver(input, context) }),
164
163
  });
165
- return mutationOutput(result);
164
+ return mutationOutput(result, false);
166
165
  },
167
166
  });
168
167
  const humanReviewCommentList = defineLeaf({
@@ -4,6 +4,12 @@ export interface PageHelpText {
4
4
  componentSelection: readonly string[];
5
5
  pageHint: string;
6
6
  rootUseWhen: string;
7
+ /** `human send` surfaces. HTML is named only where an HTML page can be read. */
8
+ sendSummary: string;
9
+ sendWhenToUse: string;
10
+ pageFlagConstraint: string;
11
+ stdinConstraint: string;
12
+ dirConstraint: string;
7
13
  }
8
14
  /** The one page authoring model for `human -h`. */
9
15
  export declare function pageHelpText(pageSurface?: boolean): PageHelpText;
@@ -3,16 +3,41 @@ import { CORE_COMPONENT_SELECTION_LINES } from '../../core/human/component-docs.
3
3
  // The reader's missing context is the gap syntax cannot close: an inbox page
4
4
  // is opened away from the conversation that produced it, by someone who never
5
5
  // followed the work.
6
- const PAGE_BRIEF = 'Write the page for where it is read. A page projected to the inbox is opened away from this conversation by someone who has not followed the work, so it stands alone; an inline page can lean on what the conversation already said. Title names the topic; subtitle states the decision, your recommendation, and the stakes. Content carries only what is needed to decide, with the evidence behind it below the ask rather than ahead of it. Ask only what changes your next step, and offer only options you would actually take. The reader takes a page one step at a time, so hold each step — and a stepless page — to one question or request for action; several decisions means several `<Step>` children, never one long scroll of stacked context and asks. A page that asks nothing states what happened and what it changes.';
7
- const PAGE_AUTHORING = 'Write one `.tsx` module to `$CRTR_CONTEXT_DIR/pages/<name>.tsx` that default-exports a component whose root element is `<Page title="…" subtitle="…">`; a multi-step page puts its content in `<Step>` children. Or send a complete `.html` file with a nonempty `<title>`; HTML is delivered verbatim and collects no response. Delivery is chosen by `human send` flags, never a Page prop. Registry components and React are already in scope, so the module contains no `import` and no `require`. Full JS is yours: state, expressions, `.map` over your data, event handlers, plain elements with Tailwind classes. `usePageHost` is a reserved hook whose product-supplied members are opaque here. Never render submit or dismiss chrome — the host owns it. Display-only pages are first-class: a page with no questions may be sent without inbox or reply delivery. The module is compiled when you submit it and a compile error rejects the submit; there is no pre-check.';
6
+ const PAGE_BRIEF = 'Write the page for where it is read. A page projected to the inbox is opened away from this conversation by someone who has not followed the work, so it stands alone; an inline page can lean on what the conversation already said. Title names what is being settled, not the topic it sits under. Subtitle is the single line shown beside the title in the inbox list before anything is opened, so it earns the open — your recommendation and what is at stake — rather than summarising the page. Content carries only what is needed to decide. Ask only what changes your next step, and offer only options you would actually take; mark the one you would take `recommended` (at most one per question), because a page whose single question is that picker can be answered straight from the inbox list without ever being opened, which is the cheapest answer you can ask for. The reader takes a page one step at a time, so hold each step — and a stepless page — to one question or request for action; several decisions means several `<Step>` children, never one long scroll of stacked context and asks. A page that asks nothing states what happened and what it changes.';
7
+ // Both hosts render pages and both want them reached for by default; only the
8
+ // reason differs, so neither surface is left sounding like pages are exotic.
9
+ const ENCOURAGEMENT_PAGE_SURFACE = 'Use inline pages as the default way to put structured or interactive content in front of the user; the environment renders rich components, so a page is not a special occasion.';
10
+ const ENCOURAGEMENT_TERMINAL = 'Use inline pages as the default way to put a question or a decision in front of the user; the viewer draws them in the transcript and in the inbox, so a page is not a special occasion.';
11
+ // The default is about form, not frequency: the page is the cheap part and the
12
+ // reader's attention is the expensive one, so say both in the same breath.
13
+ const INTERRUPTION_COST = 'What costs the reader is being asked, not the page it arrives on: send one when their answer changes what you do next, carry everything else in your next report, and put several decisions in one page as separate steps rather than sending several pages.';
14
+ const AUTHORING_MODULE = 'Write one `.tsx` module to `$CRTR_CONTEXT_DIR/pages/<name>.tsx` that default-exports a component whose root element is `<Page title="…" subtitle="…">`; a multi-step page puts its content in `<Step>` children.';
15
+ // Only a page surface can show an HTML document; a terminal host renders its
16
+ // title and nothing else, so naming the dialect there would hand the agent a
17
+ // page its reader could not read.
18
+ const AUTHORING_HTML = 'Or send a complete `.html` file with a nonempty `<title>`; HTML is delivered verbatim and collects no response.';
19
+ const AUTHORING_RULES = 'Delivery is chosen by `human send` flags, never a Page prop. Registry components and React are already in scope, so the module contains no `import` and no `require`. Full JS is yours: state, expressions, `.map` over your data, event handlers, plain elements with Tailwind classes. `usePageHost` is a reserved hook whose product-supplied members are opaque here. Never render submit or dismiss chrome — the host owns it. Display-only pages are first-class: a page with no questions may be sent without inbox or reply delivery. The module is compiled when you submit it and a compile error rejects the submit; there is no pre-check.';
20
+ // Where context belongs is a property of the surface, not of taste: a page
21
+ // surface shows a question with the whole page around it, a terminal shows one
22
+ // question alone on the screen.
23
+ const CONTEXT_PAGE_SURFACE = 'Context can sit anywhere on the page — prose and display components around a question are rendered beside it — so a question\'s `body` carries what that question needs and the page carries the shared background.';
24
+ const CONTEXT_TERMINAL = 'Put the context a question needs in that question\'s own `body`: the reader is shown one question at a time, so page-level prose is not in front of them while they answer, and a question that leans on it cannot be answered.';
25
+ const FLATTENING_TERMINAL = 'This host draws the page from the components you declared rather than running your module, so layout, Tailwind classes, state, and click handlers never reach the reader — what survives is each component\'s `label`, its markdown `body`, and its options, rows, or cards. A chart renders as one line naming it, and a product-registered component cannot be answered here at all.';
8
26
  const DISPLAY_SET_LINE = 'The shadcn display set — `Card`, `Badge`, `Button`, `Table`, `Tabs`, `Separator`, `Alert`, `Progress`, `Accordion` and their parts — is in scope too, for presentation only.';
9
27
  /** The one page authoring model for `human -h`. */
10
28
  export function pageHelpText(pageSurface = false) {
29
+ const authoring = [
30
+ pageSurface ? ENCOURAGEMENT_PAGE_SURFACE : ENCOURAGEMENT_TERMINAL,
31
+ INTERRUPTION_COST,
32
+ AUTHORING_MODULE,
33
+ ...(pageSurface ? [AUTHORING_HTML] : []),
34
+ AUTHORING_RULES,
35
+ pageSurface ? CONTEXT_PAGE_SURFACE : CONTEXT_TERMINAL,
36
+ ...(pageSurface ? [] : [FLATTENING_TERMINAL]),
37
+ ].join(' ');
11
38
  return {
12
39
  brief: PAGE_BRIEF,
13
- authoring: pageSurface
14
- ? `Use inline pages as the default way to put structured or interactive content in front of the user; the environment renders rich components, so a page is not a special occasion. ${PAGE_AUTHORING}`
15
- : PAGE_AUTHORING,
40
+ authoring,
16
41
  componentSelection: [...CORE_COMPONENT_SELECTION_LINES, DISPLAY_SET_LINE],
17
42
  pageHint: 'Write the page module to `$CRTR_CONTEXT_DIR/pages/<name>.tsx`; pass that path.',
18
43
  // The page-surface wording names no document review and no live display:
@@ -22,6 +47,19 @@ export function pageHelpText(pageSurface = false) {
22
47
  rootUseWhen: pageSurface
23
48
  ? 'a person must decide something or receive structured content from you; use inline pages by default because the environment renders rich components.'
24
49
  : 'a person must decide, review a document with you, or receive a live display.',
50
+ sendSummary: pageSurface ? 'send a durable TSX or HTML page' : 'send a durable TSX page',
51
+ sendWhenToUse: pageSurface
52
+ ? 'you need to put structured TSX or HTML content in a conversation and optionally project it to the inbox; a page carrying response-bearing components routes completion back to this node.'
53
+ : 'you need to put structured TSX content in a conversation and optionally project it to the inbox; a page carrying response-bearing components routes completion back to this node.',
54
+ pageFlagConstraint: pageSurface
55
+ ? 'A .tsx file is JSX; .html/.htm is complete HTML. Provide exactly one of --page or stdin.'
56
+ : 'Provide exactly one of --page or stdin.',
57
+ stdinConstraint: pageSurface
58
+ ? 'The page module source (TSX) on stdin. Provide exactly one of stdin or --page PATH. HTML is file-authored.'
59
+ : 'The page module source (TSX) on stdin. Provide exactly one of stdin or --page PATH.',
60
+ dirConstraint: pageSurface
61
+ ? 'Interaction directory holding page.tsx or page.html, optional page.js, page.json, and response lifecycle files when delivery needs them.'
62
+ : 'Interaction directory holding page.tsx, optional page.js, page.json, and response lifecycle files when delivery needs them.',
25
63
  };
26
64
  }
27
65
  export function resolveMaxPanes() {
@@ -46,7 +46,7 @@ export function registerHuman() {
46
46
  },
47
47
  children: [
48
48
  humanComponents,
49
- humanSend,
49
+ humanSend(pageSurface),
50
50
  ...(pageSurface ? [] : [humanReviewBranch]),
51
51
  humanFeedbackBranch,
52
52
  ...(pageSurface ? [] : [humanShow]),
@@ -37,12 +37,10 @@ export const deleteLeaf = defineLeaf({
37
37
  selectorParam('dir', {}, 'Deletes from that one store, including a non-winning duplicate no target view resolves to.'),
38
38
  ],
39
39
  output: [
40
- { name: 'name', type: 'string', required: true, constraint: 'Canonical name of the deleted document.' },
41
40
  { name: 'scope', type: 'string', required: true, constraint: 'Scope the document was deleted from: node, user, project, or profile.' },
42
41
  { name: 'path', type: 'string', required: true, constraint: 'Absolute path of the file that was removed.' },
43
- { name: 'deleted', type: 'boolean', required: true, constraint: 'Always true on success — the file was removed. A missing document fails with not_found instead.' },
44
42
  { name: 'log_path', type: 'string', required: true, constraint: 'Absolute path to the document’s revision log, which OUTLIVES the document — the delete is appended to it as a tombstone carrying the full final text, still readable with `crtr memory history`.' },
45
- { name: 'follow_up', type: 'string', required: true, constraint: 'Concrete next commands — read the life of the removed doc, or browse what remains.' },
43
+ { name: 'follow_up', type: 'string', required: false, constraint: 'Present only when a same-store canonical collision was recovered by selecting the first physical path in lexical order.' },
46
44
  ],
47
45
  outputKind: 'object',
48
46
  effects: [
@@ -89,12 +87,12 @@ export const deleteLeaf = defineLeaf({
89
87
  // this same log — one log per physical path over its whole life.
90
88
  appendHistoryRecord(logPath, buildHistoryRecord({ op: 'delete', before, after: '' }));
91
89
  return {
92
- name: doc.name,
93
90
  scope: doc.scope,
94
91
  path: doc.path,
95
- deleted: true,
96
92
  log_path: logPath,
97
- follow_up: `Removed${selected.collisionPaths.length > 0 ? ` the first physical path in stable lexical order (${doc.path}) to recover a same-store collision.` : '.'} Its revision history survives — read it with \`crtr memory history ${doc.name}\`, or browse what remains with \`crtr memory list\`.`,
93
+ ...(selected.collisionPaths.length > 0
94
+ ? { follow_up: `Removed the first physical path in stable lexical order (${doc.path}) to recover a same-store collision.` }
95
+ : {}),
98
96
  };
99
97
  },
100
98
  });
@@ -55,8 +55,6 @@ export const editLeaf = defineLeaf({
55
55
  { name: 'path', type: 'string', required: true, constraint: 'Absolute path to the revised document.' },
56
56
  { name: 'revision', type: 'number', required: true, constraint: 'This document\u2019s record count after the append \u2014 the revision number to pass to `crtr memory history --revision`.' },
57
57
  { name: 'changed', type: 'string[]', required: true, constraint: '`body` when the body text changed, plus the frontmatter field names whose values changed. `last-updated` is excluded \u2014 it is stamped on every edit, so naming it says nothing.' },
58
- { name: 'log_path', type: 'string', required: true, constraint: 'Absolute path to the document\u2019s append-only revision log (JSONL, one self-contained record per line).' },
59
- { name: 'follow_up', type: 'string', required: true, constraint: 'Concrete next commands \u2014 read the revisions or read the doc back.' },
60
58
  ],
61
59
  outputKind: 'object',
62
60
  dynamicState: () => memoryExtensionFieldCatalogHelp('edit'),
@@ -197,8 +195,6 @@ export const editLeaf = defineLeaf({
197
195
  path: doc.path,
198
196
  revision: readHistoryRecords(logPath).length,
199
197
  changed,
200
- log_path: logPath,
201
- follow_up: `Recorded. Review the revisions with \`crtr memory history ${doc.name} --diff\`, or read the doc back with \`crtr memory read ${doc.name}\`.`,
202
198
  };
203
199
  },
204
200
  });
@@ -57,14 +57,13 @@ export const moveLeaf = defineLeaf({
57
57
  selectorParam('dir', {}, 'Source, destination, and inbound-link rewriting all stay inside that one store, so moving a non-winning duplicate leaves every other store untouched — and rewrites no link at all, because the name still resolves to the candidate that outranks it here.'),
58
58
  ],
59
59
  output: [
60
- { name: 'name', type: 'string', required: true, constraint: 'The document’s new canonical name.' },
60
+ { name: 'name', type: 'string', required: true, constraint: 'The document’s new canonical name — `--to` after canonicalization, so it can differ from what you passed.' },
61
61
  { name: 'previous_name', type: 'string', required: true, constraint: 'The canonical name it answered to before the move.' },
62
62
  { name: 'scope', type: 'string', required: true, constraint: 'Scope of the store the document moved within: node, user, project, or profile.' },
63
63
  { name: 'path', type: 'string', required: true, constraint: 'Absolute path of the document at its new location — `<local name>/INDEX.md` when that canonical directory already has members in the store, else `<local name>.md`.' },
64
- { name: 'moved', type: 'boolean', required: true, constraint: 'Always true on success — the file and its revision log now live at the new name.' },
65
64
  { name: 'log_path', type: 'string', required: true, constraint: 'Absolute path to the revision log at its new location, ending in a `move` record that names the previous canonical name.' },
66
65
  { name: 'refs_rewritten', type: 'object[]', required: true, constraint: 'Documents whose bodies were rewritten from `[[<previous_name>]]` to `[[<name>]]`. Each: {name, scope, path, refs}. Empty when nothing linked to the document — or when the moved file is not what that name resolves to here (another store’s candidate wins the address), in which case inbound refs keep resolving to that winner and are left alone.' },
67
- { name: 'follow_up', type: 'string', required: true, constraint: 'Concrete next commands — read it back at the new name, verify the corpus with lint.' },
66
+ { name: 'follow_up', type: 'string', required: true, constraint: 'Outcome-dependent collision recovery and inbound-reference rewriting signal.' },
68
67
  ],
69
68
  outputKind: 'object',
70
69
  effects: [
@@ -216,10 +215,9 @@ export const moveLeaf = defineLeaf({
216
215
  previous_name: doc.name,
217
216
  scope: doc.scope,
218
217
  path: placement.path,
219
- moved: true,
220
218
  log_path: newLog,
221
219
  refs_rewritten: refsRewritten,
222
- follow_up: `Moved${selected.collisionPaths.length > 0 ? ` the first physical path in stable lexical order (${doc.path}) to recover a same-store collision.` : '.'}${refsNote} Read it back with \`crtr memory read ${newName}\`, and run \`crtr memory lint\` to verify no link dangles.`,
220
+ follow_up: `Moved${selected.collisionPaths.length > 0 ? ` the first physical path in stable lexical order (${doc.path}) to recover a same-store collision.` : '.'}${refsNote}`,
223
221
  };
224
222
  },
225
223
  });
@@ -194,7 +194,7 @@ export declare function overlayParam(name: string, overrides?: Partial<FlagParam
194
194
  * `--doc-rationale`, because `--rationale` there means why THIS REVISION is
195
195
  * happening. Same field, same prose, two flag names that cannot be confused. */
196
196
  export declare const DOC_RATIONALE_CONSTRAINT = "Frontmatter rationale \u2014 the observed agent failure that made this doc necessary. Maintainer-facing only: never ships in any delivered surface (boot render, on-read injection, `memory read` content), visible only via `memory read --frontmatter`. Omitting the flag preserves an existing rationale unchanged.";
197
- export declare const GUIDE_SURFACES = "Every doc appears in its directory\u2019s listing by default (suppress with --unlisted); everything beyond the listing is explicit `surfaces` routing \u2014 entries declaring WHEN the doc delivers and how much. Events: `boot` delivers in the boot catalog every agent sees; `workspace-open` delivers in first-message context when cwd/profile mounts the doc\u2019s project store (project stores only); `read` fires when the agent reads a matching file (globs vs the file\u2019s absolute path and basename, `./`-anchored globs vs its path relative to the store\u2019s owning repo dir, `match-frontmatter` predicates over the read file\u2019s own YAML frontmatter); `memory-read` fires when another memory doc is read (globs vs that doc\u2019s canonical name, `./` anchored to this doc\u2019s routing anchor \u2014 its own canonical name when it is its directory\u2019s document, otherwise the canonical directory it sits in); `command` fires when a matching shell command runs (globs vs the whole command string, `*` crossing `/`). An entry may also carry `gate`, a node-config predicate using the same vocabulary as the document-level `gate`: event constraints and entry gate both must match, then all participating entries fold to the highest `at` (`content` > `preview` > `name`), with no cross-entry deny precedence. The document-level `gate` remains the hard eligibility check: when it fails, no automatic surface can deliver the document. `at` sets how much delivers: `name` the bare tag, `preview` the routing line, `content` the whole body. Explicit `memory read` and listings remain deliberate access and ignore gates/rungs; only companion docs routed by their `memory-read` entries use entry gates. Each entry is paid by every future agent it fires on, so default down: no surfaces at all is correct for reference depth reached through listings and [[links]], and when a doc fits in a single sentence, deliver `content` directly \u2014 a `preview` routing line would run longer than the doc itself. Sentence-length `content` docs are correct; never pad a memory to be more verbose than the rule or fact it carries.";
197
+ export declare const GUIDE_SURFACES = "Every doc appears in its directory\u2019s listing by default (suppress with --unlisted); everything beyond the listing is explicit `surfaces` routing \u2014 entries declaring WHEN the doc delivers and how much. Events: `boot` delivers in the boot catalog every agent sees; `workspace-open` delivers in first-message context when cwd/profile mounts the doc\u2019s project store (project stores only); `read` fires when the agent reads a matching file (globs vs the file\u2019s absolute path and basename, `./`-anchored globs vs its path relative to the store\u2019s owning repo dir, `match-frontmatter` predicates over the read file\u2019s own YAML frontmatter); `memory-read` fires when another memory doc is read (globs vs that doc\u2019s canonical name, `./` anchored to this doc\u2019s routing anchor \u2014 its own canonical name when it is its directory\u2019s document, otherwise the canonical directory it sits in); `command` fires when a matching shell command runs (globs vs each shell segment of the command line \u2014 split on `&&`/`||`/`;`/`|`/newlines outside quotes, leading `VAR=value` assignments dropped \u2014 where `*` spans whole whitespace-separated tokens and never part of one, `?` is one character inside a token, and quoted argument text is opaque, so `*git commit*` fires for `cd x && git commit -m \"\u2026\"` but not for `git commit-tree` or an `echo` that merely mentions it); `pre-command` fires BEFORE a matching bash command runs (same matching): when the memory has not been read yet, the command does not execute \u2014 the memory comes back as the tool result instead, and the agent re-issues the command, which then runs. Use it to guarantee a memory is seen before a sensitive action is taken. Requires `match`. An entry may also carry `gate`, a node-config predicate using the same vocabulary as the document-level `gate`: event constraints and entry gate both must match, then all participating entries fold to the highest `at` (`content` > `preview` > `name`), with no cross-entry deny precedence. The document-level `gate` remains the hard eligibility check: when it fails, no automatic surface can deliver the document. `at` sets how much delivers: `name` the bare tag, `preview` the routing line, `content` the whole body. Explicit `memory read` and listings remain deliberate access and ignore gates/rungs; only companion docs routed by their `memory-read` entries use entry gates. Each entry is paid by every future agent it fires on, so default down: no surfaces at all is correct for reference depth reached through listings and [[links]], and when a doc fits in a single sentence, deliver `content` directly \u2014 a `preview` routing line would run longer than the doc itself. Sentence-length `content` docs are correct; never pad a memory to be more verbose than the rule or fact it carries.";
198
198
  export declare const GUIDE_ROUTING_LINE = "The routing line (--when-and-why-to-read) is the only text an agent reads before deciding to load the doc. The test for its when-clause: it names an observable circumstance in the reader\u2019s current work, not the doc\u2019s topic reworded as an activity. If the WHEN can be inferred from the title alone, it is not a real trigger. Bad on a todo list: \"When planning or prioritizing work across this profile.\" Good: \"When the user mentions something from their todos, or asks what is still outstanding across this profile.\" The test for its because-clause: if it can be derived by paraphrasing the doc\u2019s advice, it is a restatement, not a payoff \u2014 a real payoff names a consequence in the reader\u2019s world that the document itself never asserts. Bad: \"because only genuine first principles belong in taste memory.\" Bad: \"because keeping the test loop fast and free of speculative tests protects the development pace\" \u2014 the doc\u2019s rule as an outcome, derivable straight from its advice. Good: \"because identifying throughlines in user taste lets future decisions be made faster and with less re-litigation.\" Someone mid-task who has not read the doc must be able to decide from this line alone whether the read is worth it. If you cannot name the concrete situation that triggers it, you do not yet understand the memory \u2014 ask the user one sharp question instead of improvising.";
199
199
  export declare const GUIDE_PREDICATE_VOCABULARY = "Document gates, surface-entry gates, and match-frontmatter share the same predicate language: a field map is AND-ed across fields; dotted fields resolve nested values; field matchers may be scalar, array, or object. Scalar matchers do exact equality, with arrays matching any element. Array matchers do membership or intersection. Object matchers accept `eq`, `ne`, `in`, `nin`, `exists`, `contains`, `containsAll`, `containsAny`, `matches`, `imatches`, `gt`, `gte`, `lt`, and `lte`. Combinators are `all`, `any`, and `not`; sibling field matchers next to them are AND-ed in. An empty condition is inert. An unknown op never matches.";
200
200
  export declare const GUIDE_DOC_LINKS = "Link related docs with `[[canonical/name]]`. A doc link in any body is a first-class cross-reference to another memory document by its exact canonical name \u2014 the same identifier `crtr memory read` takes, namespace included for a project document. A bare directory name is a valid link too: following it returns that directory\u2019s own document when it has one, plus the directory listing. Links are pointers, never transclusion: the linked body is loaded only when a reader follows it, so a link costs nothing until needed. `crtr memory lint` fails on a link that resolves to no document or directory, and `crtr memory read` lists a doc\u2019s resolvable links alongside its body. There is no alias, label, or old-name form \u2014 a renamed target needs its links rewritten.";
@@ -14,7 +14,7 @@ import { usage } from '../../core/errors.js';
14
14
  import { memoryExtensionValidationCatalog, } from '../../core/memory/extensions.js';
15
15
  import { CRTR_DIR_NAME } from '../../types.js';
16
16
  import { NEUTRAL_PROJECT_MEMORY, scopeMemoryDir, projectScopeRoot, ensureProjectScopeRoot, resetScopeCache, } from '../../core/scope.js';
17
- import { loadProfileManifest, profileMemoryDir } from '../../core/profiles/manifest.js';
17
+ import { loadProfileManifest, resolveProfileOperand, profileMemoryDir } from '../../core/profiles/manifest.js';
18
18
  import { memoryDir as nodeMemoryDir } from '../../core/runtime/memory.js';
19
19
  import { SURFACE_EVENTS, SURFACE_RUNGS } from '../../core/substrate/schema.js';
20
20
  import { exposureTarget, loadContextExposureState, registerExposure, saveContextExposureState, } from '../../core/substrate/injected-store.js';
@@ -113,7 +113,7 @@ export function resolveReadSelector(input) {
113
113
  // Naming a profile selects THAT profile's store exactly; the ambient
114
114
  // selection would otherwise answer for whichever profile this process runs
115
115
  // under, silently ignoring the flag.
116
- const { profileId } = loadProfileManifest(input.profile);
116
+ const { profileId } = resolveProfileOperand(input.profile);
117
117
  return { scope: 'profile', store: nativeStoreDescriptor('profile', profileMemoryDir(profileId)) };
118
118
  }
119
119
  return { ...(scope === undefined ? {} : { scope }), store: null };
@@ -159,11 +159,15 @@ export function resolveWriteSelector(input) {
159
159
  return { scope, memoryDir, store: nativeStoreDescriptor(scope, memoryDir) };
160
160
  }
161
161
  if (scope === 'profile') {
162
- const profileIdOrName = input.profile && input.profile !== '' ? input.profile : process.env['CRTR_PROFILE_ID'] || '';
162
+ // A typed `--profile` gets fuzzy tolerance; the ambient `CRTR_PROFILE_ID`
163
+ // is a durable id this process was launched under, so it resolves strictly
164
+ // — a stale one must fail rather than write into a neighbouring store.
165
+ const typed = input.profile !== undefined && input.profile !== '';
166
+ const profileIdOrName = typed ? input.profile : process.env['CRTR_PROFILE_ID'] || '';
163
167
  if (profileIdOrName === '') {
164
168
  throw usage('profile scope requires a selected profile; rerun inside a profiled node or pass --profile');
165
169
  }
166
- const { profileId } = loadProfileManifest(profileIdOrName);
170
+ const { profileId } = typed ? resolveProfileOperand(profileIdOrName) : loadProfileManifest(profileIdOrName);
167
171
  const memoryDir = profileMemoryDir(profileId);
168
172
  return { scope, memoryDir, store: nativeStoreDescriptor(scope, memoryDir) };
169
173
  }
@@ -432,7 +436,7 @@ export function coerceSurface(raw) {
432
436
  throw usage(`--surface: invalid \`match-frontmatter\`: ${JSON.stringify(matchFrontmatter)} (expected a field→matcher object)`);
433
437
  }
434
438
  }
435
- if ((on === 'read' || on === 'memory-read' || on === 'command') && match === undefined && matchFrontmatter === undefined) {
439
+ if ((on === 'read' || on === 'memory-read' || on === 'command' || on === 'pre-command') && match === undefined && matchFrontmatter === undefined) {
436
440
  throw usage(`--surface: a \`${on}\` entry requires \`match\`${on === 'read' ? ' or `match-frontmatter`' : ''}`);
437
441
  }
438
442
  const gate = rec['gate'];
@@ -764,7 +768,7 @@ export const FRONTMATTER_OVERLAY_PARAMS = {
764
768
  'when-and-why-to-read': { kind: 'flag', name: 'when-and-why-to-read', type: 'string', required: false, constraint: 'ONE routing sentence: "When <circumstance>, this <kind> should be read because <broader downstream payoff>." WHY is the reader\u2019s payoff \u2014 the consequence they secure for their task by reading \u2014 NEVER the doc summary, its rule, or that rule reworded as an outcome (a benefit-shaped restatement still fails). Rendered verbatim as the preview.' },
765
769
  'short-form': { kind: 'flag', name: 'short-form', type: 'string', required: false, constraint: 'Frontmatter short-form \u2014 a very abbreviated version of the content, the hook shown in `crtr memory list`.' },
766
770
  'unlisted': { kind: 'flag', name: 'unlisted', type: 'bool', required: false, default: false, constraint: 'Suppress this doc from directory listings. Suppression only — explicit reads, [[links]], and surfaces entries still work.' },
767
- 'surface': { kind: 'flag', name: 'surface', type: 'string', required: false, repeatable: true, constraint: 'One routing entry per occurrence, as a YAML/JSON object `{on, at, match?, match-frontmatter?, gate?}`; the flag set replaces the document’s whole surfaces list. `on` is boot|workspace-open|read|memory-read|command; `at` is name|preview|content. `match` holds the event’s globs — required on read/memory-read/command (a read entry may carry `match-frontmatter`, a predicate over the read file’s own frontmatter, instead), meaningless on boot/workspace-open. Optional entry `gate` is a node-config predicate using the same vocabulary as the document gate; its event constraints and gate both match before it participates. A `./`-anchored glob is relative: for `read` to the store’s owning repo dir, for `memory-read` to this doc’s routing anchor — its own canonical name when the doc is its directory’s document (`<dir>/INDEX.md`), otherwise the canonical directory it sits in. Participating entries fold to their highest `at`; there is no cross-entry deny precedence.' },
771
+ 'surface': { kind: 'flag', name: 'surface', type: 'string', required: false, repeatable: true, constraint: 'One routing entry per occurrence, as a YAML/JSON object `{on, at, match?, match-frontmatter?, gate?}`; the flag set replaces the document’s whole surfaces list. `on` is boot|workspace-open|read|memory-read|command|pre-command; `at` is name|preview|content. `match` holds the event’s globs — required on read/memory-read/command/pre-command (a read entry may carry `match-frontmatter`, a predicate over the read file’s own frontmatter, instead), meaningless on boot/workspace-open. Optional entry `gate` is a node-config predicate using the same vocabulary as the document gate; its event constraints and gate both match before it participates. A `./`-anchored glob is relative: for `read` to the store’s owning repo dir, for `memory-read` to this doc’s routing anchor — its own canonical name when the doc is its directory’s document (`<dir>/INDEX.md`), otherwise the canonical directory it sits in. Participating entries fold to their highest `at`; there is no cross-entry deny precedence.' },
768
772
  'gate': { kind: 'flag', name: 'gate', type: 'string', required: false, constraint: 'Frontmatter gate \u2014 YAML/JSON object predicate over node config using the same field/matcher vocabulary described in the guide.' },
769
773
  'slash': { kind: 'flag', name: 'slash', type: 'bool', required: false, default: false, constraint: 'Presence flags this doc invocable as a pi slash command (`/<name>`, `/` in a nested name rendered as `:`) \u2014 the doc body becomes the command\u2019s injected prompt. Default false: most docs are consulted, not invoked.' },
770
774
  };
@@ -782,7 +786,7 @@ export function overlayParam(name, overrides = {}, extraConstraint) {
782
786
  * `--doc-rationale`, because `--rationale` there means why THIS REVISION is
783
787
  * happening. Same field, same prose, two flag names that cannot be confused. */
784
788
  export const DOC_RATIONALE_CONSTRAINT = 'Frontmatter rationale \u2014 the observed agent failure that made this doc necessary. Maintainer-facing only: never ships in any delivered surface (boot render, on-read injection, `memory read` content), visible only via `memory read --frontmatter`. Omitting the flag preserves an existing rationale unchanged.';
785
- export const GUIDE_SURFACES = 'Every doc appears in its directory’s listing by default (suppress with --unlisted); everything beyond the listing is explicit `surfaces` routing — entries declaring WHEN the doc delivers and how much. Events: `boot` delivers in the boot catalog every agent sees; `workspace-open` delivers in first-message context when cwd/profile mounts the doc’s project store (project stores only); `read` fires when the agent reads a matching file (globs vs the file’s absolute path and basename, `./`-anchored globs vs its path relative to the store’s owning repo dir, `match-frontmatter` predicates over the read file’s own YAML frontmatter); `memory-read` fires when another memory doc is read (globs vs that doc’s canonical name, `./` anchored to this doc’s routing anchor — its own canonical name when it is its directory’s document, otherwise the canonical directory it sits in); `command` fires when a matching shell command runs (globs vs the whole command string, `*` crossing `/`). An entry may also carry `gate`, a node-config predicate using the same vocabulary as the document-level `gate`: event constraints and entry gate both must match, then all participating entries fold to the highest `at` (`content` > `preview` > `name`), with no cross-entry deny precedence. The document-level `gate` remains the hard eligibility check: when it fails, no automatic surface can deliver the document. `at` sets how much delivers: `name` the bare tag, `preview` the routing line, `content` the whole body. Explicit `memory read` and listings remain deliberate access and ignore gates/rungs; only companion docs routed by their `memory-read` entries use entry gates. Each entry is paid by every future agent it fires on, so default down: no surfaces at all is correct for reference depth reached through listings and [[links]], and when a doc fits in a single sentence, deliver `content` directly — a `preview` routing line would run longer than the doc itself. Sentence-length `content` docs are correct; never pad a memory to be more verbose than the rule or fact it carries.';
789
+ export const GUIDE_SURFACES = 'Every doc appears in its directory’s listing by default (suppress with --unlisted); everything beyond the listing is explicit `surfaces` routing — entries declaring WHEN the doc delivers and how much. Events: `boot` delivers in the boot catalog every agent sees; `workspace-open` delivers in first-message context when cwd/profile mounts the doc’s project store (project stores only); `read` fires when the agent reads a matching file (globs vs the file’s absolute path and basename, `./`-anchored globs vs its path relative to the store’s owning repo dir, `match-frontmatter` predicates over the read file’s own YAML frontmatter); `memory-read` fires when another memory doc is read (globs vs that doc’s canonical name, `./` anchored to this doc’s routing anchor — its own canonical name when it is its directory’s document, otherwise the canonical directory it sits in); `command` fires when a matching shell command runs (globs vs each shell segment of the command line — split on `&&`/`||`/`;`/`|`/newlines outside quotes, leading `VAR=value` assignments dropped — where `*` spans whole whitespace-separated tokens and never part of one, `?` is one character inside a token, and quoted argument text is opaque, so `*git commit*` fires for `cd x && git commit -m "…"` but not for `git commit-tree` or an `echo` that merely mentions it); `pre-command` fires BEFORE a matching bash command runs (same matching): when the memory has not been read yet, the command does not execute — the memory comes back as the tool result instead, and the agent re-issues the command, which then runs. Use it to guarantee a memory is seen before a sensitive action is taken. Requires `match`. An entry may also carry `gate`, a node-config predicate using the same vocabulary as the document-level `gate`: event constraints and entry gate both must match, then all participating entries fold to the highest `at` (`content` > `preview` > `name`), with no cross-entry deny precedence. The document-level `gate` remains the hard eligibility check: when it fails, no automatic surface can deliver the document. `at` sets how much delivers: `name` the bare tag, `preview` the routing line, `content` the whole body. Explicit `memory read` and listings remain deliberate access and ignore gates/rungs; only companion docs routed by their `memory-read` entries use entry gates. Each entry is paid by every future agent it fires on, so default down: no surfaces at all is correct for reference depth reached through listings and [[links]], and when a doc fits in a single sentence, deliver `content` directly — a `preview` routing line would run longer than the doc itself. Sentence-length `content` docs are correct; never pad a memory to be more verbose than the rule or fact it carries.';
786
790
  export const GUIDE_ROUTING_LINE = 'The routing line (--when-and-why-to-read) is the only text an agent reads before deciding to load the doc. The test for its when-clause: it names an observable circumstance in the reader\u2019s current work, not the doc\u2019s topic reworded as an activity. If the WHEN can be inferred from the title alone, it is not a real trigger. Bad on a todo list: "When planning or prioritizing work across this profile." Good: "When the user mentions something from their todos, or asks what is still outstanding across this profile." The test for its because-clause: if it can be derived by paraphrasing the doc\u2019s advice, it is a restatement, not a payoff \u2014 a real payoff names a consequence in the reader\u2019s world that the document itself never asserts. Bad: "because only genuine first principles belong in taste memory." Bad: "because keeping the test loop fast and free of speculative tests protects the development pace" \u2014 the doc\u2019s rule as an outcome, derivable straight from its advice. Good: "because identifying throughlines in user taste lets future decisions be made faster and with less re-litigation." Someone mid-task who has not read the doc must be able to decide from this line alone whether the read is worth it. If you cannot name the concrete situation that triggers it, you do not yet understand the memory \u2014 ask the user one sharp question instead of improvising.';
787
791
  export const GUIDE_PREDICATE_VOCABULARY = 'Document gates, surface-entry gates, and match-frontmatter share the same predicate language: a field map is AND-ed across fields; dotted fields resolve nested values; field matchers may be scalar, array, or object. Scalar matchers do exact equality, with arrays matching any element. Array matchers do membership or intersection. Object matchers accept `eq`, `ne`, `in`, `nin`, `exists`, `contains`, `containsAll`, `containsAny`, `matches`, `imatches`, `gt`, `gte`, `lt`, and `lte`. Combinators are `all`, `any`, and `not`; sibling field matchers next to them are AND-ed in. An empty condition is inert. An unknown op never matches.';
788
792
  export const GUIDE_DOC_LINKS = 'Link related docs with `[[canonical/name]]`. A doc link in any body is a first-class cross-reference to another memory document by its exact canonical name \u2014 the same identifier `crtr memory read` takes, namespace included for a project document. A bare directory name is a valid link too: following it returns that directory\u2019s own document when it has one, plus the directory listing. Links are pointers, never transclusion: the linked body is loaded only when a reader follows it, so a link costs nothing until needed. `crtr memory lint` fails on a link that resolves to no document or directory, and `crtr memory read` lists a doc\u2019s resolvable links alongside its body. There is no alias, label, or old-name form \u2014 a renamed target needs its links rewritten.';
@@ -1,6 +1,12 @@
1
1
  import { join } from 'node:path';
2
2
  import { defineLeaf } from '../../core/command.js';
3
3
  import { usage } from '../../core/errors.js';
4
+ import { docLinkNames } from '../../core/memory/doc-link-grammar.js';
5
+ import { lintMemoryDocument, resolvableCanonicalNames } from '../../core/memory/lint.js';
6
+ import { descendantStoreRoots } from '../../core/nested-stores.js';
7
+ import { projectScopeRoots } from '../../core/scope.js';
8
+ import { loadMemoryTargetView } from '../../core/memory-resolver.js';
9
+ import { parsedSubstrateSurfaces } from '../../core/substrate/frontmatter-validation.js';
4
10
  import { ensureDir, realpathOrSelf, walkFiles, writeText } from '../../core/fs-utils.js';
5
11
  import { associateRepository } from '../../core/memory/repository-association.js';
6
12
  import { appendHistoryRecord, buildHistoryRecord, historyLogPathFor, } from '../../core/memory/history.js';
@@ -41,7 +47,8 @@ export const writeLeaf = defineLeaf({
41
47
  GUIDE_DOC_LINKS + '\n\n' +
42
48
  'When a doc grows long or information-rich, nest it into a graph instead of letting it become a scroll. The main doc at the topic’s path keeps the high-level, most load-bearing information, most important first; depth splits into reference docs under the topic’s directory (`area/topic/...`), each pointed at with a `[[link]]`. Split by subject: a leaf earns its link by covering a different subject a task might need on its own; a leaf of offloaded “further evidence”, examples, or references is never followed, so supporting material either sits in the main doc next to the point it supports or gets cut. The main doc is the entry point a reader can act from alone; a reference leaf is loaded only when the task needs that depth. Give reference leaves no surfaces at all — the directory listing and the link from the main doc are how they are found, so any routing entry just double-charges every boot or read for depth the graph already routes. State each fact once. Keep facts that affect action or judgment, including constraints, exceptions, numbers, and exact names. Remove known context, inferable conclusions, filler transitions, and examples that resolve no ambiguity. `crtr memory lint` caps body length by delivery rung and its findings carry the split guidance.\n\n' +
43
49
  'A directory needs no index doc: reading a directory name returns its listing — each member’s routing line — so never author a doc that merely lists, fronts, or paraphrases its siblings. Write a directory-level doc only for synthesis: an operating guide or the cluster’s mechanics, ordering, conditions, and relationships, content no single member can carry. Guided entrance into a topic is an ordinary member doc, found through the listing like any other.\n\n' +
44
- 'Find before write. Prefer slightly expanding an existing document with `crtr memory edit`, nesting genuinely separate depth under its topic, and updating the existing `when-and-why-to-read` (plus its INDEX router when present) over creating another similar memory. A new document earns its own identity only when it has a distinct read trigger and a coherent body whose merge into the existing document would make it harder to route or use. Group related docs with path names (area/topic). Provenance is stamped here and preserved by every later revision. Run `crtr memory lint` after authoring.\n\n' +
50
+ 'Find before write. Prefer slightly expanding an existing document with `crtr memory edit`, nesting genuinely separate depth under its topic, and updating the existing `when-and-why-to-read` (plus its INDEX router when present) over creating another similar memory. A new document earns its own identity only when it has a distinct read trigger and a coherent body whose merge into the existing document would make it harder to route or use. Group related docs with path names (area/topic). Provenance is stamped here and preserved by every later revision.\n\n' +
51
+ 'This leaf validates the document it just wrote and reports any finding; a silent result means it is clean, so there is nothing to run afterwards. The check is scoped to this one document — `crtr memory lint` remains the corpus-wide sweep, and only it catches store-level faults such as a canonical collision or a missing workspace front door.\n\n' +
45
52
  '--rationale is the gap this doc exists to close — the observed agent failure that prompted it, captured from user signal (a correction, a mistake you watched happen) rather than inferred from the doc’s own content. If the rationale is guessable from reading the doc, it is not the real one — a guessable gap is one agents do not actually fall into. Omit the flag when you have no observed gap to record.\n\n' +
46
53
  'Revise an existing doc with `crtr memory edit`.',
47
54
  params: [
@@ -61,14 +68,10 @@ export const writeLeaf = defineLeaf({
61
68
  { kind: 'stdin', name: 'body', required: true, constraint: 'Document body (markdown, no frontmatter). Piped on stdin only — this leaf already claims the one positional for NAME, so a second bare argv token is rejected, not silently accepted as the body.' },
62
69
  ],
63
70
  output: [
64
- { name: 'name', type: 'string', required: true, constraint: 'The full canonical document name written.' },
65
- { name: 'kind', type: 'string', required: true, constraint: 'Kind recorded in frontmatter.' },
66
- { name: 'scope', type: 'string', required: true, constraint: 'Scope the document was written to: user, project, profile, or node.' },
71
+ { name: 'name', type: 'string', required: true, constraint: 'The full canonical document name written — the resolved identity, which composes a namespace onto the name you passed when the store declares one.' },
72
+ { name: 'scope', type: 'string', required: true, constraint: 'Scope the document was written to: user, project, profile, or node. The selector rules decide this, so it is the one placement fact the invocation does not already state.' },
67
73
  { name: 'path', type: 'string', required: true, constraint: 'Absolute path to the written document.' },
68
- { name: 'created', type: 'boolean', required: true, constraint: 'Always true on success — this leaf only creates. A name already taken at the resolved scope fails with a usage error naming `crtr memory edit`.' },
69
- { name: 'frontmatter', type: 'object[]', required: true, constraint: 'The frontmatter options selected by this invocation, in document field order. Each: {key, value}.' },
70
- { name: 'log_path', type: 'string', required: true, constraint: 'Absolute path to the document’s append-only revision log, opened here with a `create` record.' },
71
- { name: 'follow_up', type: 'string', required: true, constraint: 'Concrete next commands — read it back, revise it, or list the inventory.' },
74
+ { name: 'findings', type: 'object[]', required: false, constraint: 'Authoring problems in the document just written, each {severity, rule, message}. ABSENT when the document is clean, which is the normal case — a silent result means it validated. Covers every rule that reads one document: yaml, schema, extension, length, short-preview, local-name, namespace-placement, root-name, nested-store-boot, broad-memory-read, and a workspace front door on a non-root doc. Reference rules (dangling-link, memory-read-route) run only when the document actually carries a `[[link]]` or a memory-read route. The document is written either way: a finding reports what to revise with `crtr memory edit`, it does not mean the write failed.' },
72
75
  ],
73
76
  outputKind: 'object',
74
77
  dynamicState: () => memoryExtensionFieldCatalogHelp('write'),
@@ -166,31 +169,59 @@ export const writeLeaf = defineLeaf({
166
169
  writeText(path, after);
167
170
  const logPath = historyLogPathFor(memoryDir, path);
168
171
  appendHistoryRecord(logPath, buildHistoryRecord({ op: 'create', before: '', after }));
169
- // Confirm exactly the frontmatter choices made by this invocation — the
170
- // runtime-stamped fields do not masquerade as selected options in the
171
- // result or its collapsed viewer preview.
172
- const selectedKeys = [
173
- 'kind',
174
- ...(initializing ? ['namespace'] : []),
175
- ...(input['whenAndWhyToRead'] !== undefined ? ['when-and-why-to-read'] : []),
176
- ...(input['shortForm'] !== undefined ? ['short-form'] : []),
177
- ...(input['unlisted'] === true ? ['unlisted'] : []),
178
- ...(input['surface'] !== undefined ? ['surfaces'] : []),
179
- ...(input['gate'] !== undefined ? ['gate'] : []),
180
- ...(input['slash'] === true ? ['slash'] : []),
181
- ...(input['rationale'] !== undefined ? ['rationale'] : []),
182
- ...(extensionChanges.sets.length > 0 ? ['extensions'] : []),
183
- ];
184
- const selectedFrontmatter = selectedKeys.map((key) => ({ key, value: frontmatter[key] }));
172
+ // Validate what was just authored, here, rather than returning a follow_up
173
+ // asking the author to go run lint — an obligation every write carries is
174
+ // the command's job, not a decision to hand back. Scoped to this one
175
+ // document: a corpus-wide sweep would surface unrelated findings from other
176
+ // docs on every write, which teaches an author to ignore the output.
177
+ //
178
+ // Each rule that needs an index beyond this file is paid for ONLY when the
179
+ // document can actually trip it. Reference resolution needs every canonical
180
+ // name in the view — seconds on a large corpus — so it is bought only when
181
+ // the doc carries a reference; nested-store detection walks the project
182
+ // roots, so it is bought only when the doc carries a boot entry.
183
+ const surfaceEntries = parsedSubstrateSurfaces(frontmatter);
184
+ const carriesReference = docLinkNames(body).length > 0 || surfaceEntries.some((entry) => entry.on === 'memory-read');
185
+ const carriesBootEntry = surfaceEntries.some((entry) => entry.on === 'boot');
186
+ const lint = lintMemoryDocument(store, path, {
187
+ ...(carriesReference
188
+ ? {
189
+ resolvable: resolvableCanonicalNames(loadMemoryTargetView({
190
+ cwd: store.ownerDir ?? process.cwd(),
191
+ profileId: process.env['CRTR_PROFILE_ID'] || null,
192
+ nodeId: process.env['CRTR_NODE_ID'] || null,
193
+ }, { quiet: true, includeDescendants: true }).docs),
194
+ }
195
+ : {}),
196
+ ...(carriesBootEntry
197
+ ? {
198
+ nestedStore: descendantStoreRoots(projectScopeRoots(process.cwd(), process.env['CRTR_PROFILE_ID'] || null))
199
+ .map((root) => realpathOrSelf(join(root, 'memory')))
200
+ .includes(realpathOrSelf(memoryDir)),
201
+ }
202
+ : {}),
203
+ });
204
+ // Findings ride the RESULT only. `memory lint` also streams them to stderr
205
+ // because it walks hundreds of files and the stream is progress; one
206
+ // document has no progress to report, and an agent that captures 2>&1
207
+ // would pay for every finding twice.
208
+ // The result confirms only what the caller could not already know: the
209
+ // resolved identity, where the selector rules put it, and anything wrong
210
+ // with what they just wrote. Echoing back the frontmatter flags just spent
211
+ // charges the author a second time for text they typed one call ago.
185
212
  return {
186
213
  name: canonicalName,
187
- kind,
188
214
  scope,
189
215
  path,
190
- created: true,
191
- frontmatter: selectedFrontmatter,
192
- log_path: logPath,
193
- follow_up: `Read it back with \`crtr memory read ${canonicalName}\`, revise it later with \`crtr memory edit ${canonicalName} --rationale "<why>"\`, or browse the inventory with \`crtr memory list\`.`,
216
+ ...(lint.findings.length === 0
217
+ ? {}
218
+ : {
219
+ findings: lint.findings.map((finding) => ({
220
+ severity: finding.severity,
221
+ rule: finding.rule,
222
+ message: finding.message,
223
+ })),
224
+ }),
194
225
  };
195
226
  },
196
227
  });
@@ -18,7 +18,7 @@ export function registerMemory() {
18
18
  rootEntry: {
19
19
  concept: 'a memory document you read on demand — knowledge or a preference',
20
20
  desc: 'list, read, search, and write memory documents',
21
- useWhen: 'durable knowledge and preferences shared across nodes and sessions — use when prior guidance may apply to the current task, or when a non-obvious reusable truth should outlive this conversation. Every document has one canonical name, a crtr identifier rather than a file path — read docs through the command surface, never cat or find the markdown off disk.',
21
+ useWhen: 'durable knowledge and preferences shared across nodes and sessions — use when prior guidance may apply to the current task, or when a non-obvious reusable truth should outlive this conversation. Every document has one canonical name, a crtr identifier rather than a file path — read docs through the command surface, never cat or find the markdown off disk. Slash commands are authored here too — one is a document flagged invocable.',
22
22
  },
23
23
  help: {
24
24
  name: 'memory',