@north-light/crouter 0.3.231 → 0.3.232
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api/client.d.ts +3 -1
- package/dist/api/client.js +4 -0
- package/dist/api/dto/canvas.d.ts +10 -0
- package/dist/api/dto/common.d.ts +1 -1
- package/dist/api/dto/health.d.ts +2 -1
- package/dist/api/dto/lifecycle.d.ts +3 -4
- package/dist/api/dto/messages.d.ts +5 -4
- package/dist/api/dto/nodes.d.ts +2 -0
- package/dist/api/dto/profiles.d.ts +5 -0
- package/dist/api/routes.d.ts +1 -0
- package/dist/api/routes.js +1 -0
- package/dist/builtin-memory/00-runtime-base/00-authoring.md +8 -0
- package/dist/builtin-memory/00-runtime-base/01-escalation.md +1 -1
- package/dist/builtin-memory/01-spine/00-has-manager.md +1 -1
- package/dist/builtin-memory/02-turn-lifecycle/00-ending-a-turn.md +5 -0
- package/dist/builtin-memory/02-turn-lifecycle/02-resident.md +5 -3
- package/dist/builtin-memory/04-orchestration-kernel.md +2 -2
- package/dist/builtin-memory/insights/capture.md +3 -2
- package/dist/builtin-pi-packages/pi-crtr-extensions/README.md +6 -1
- package/dist/clients/attach/render/diagram.js +13 -5
- package/dist/clients/attach/render/page-block.d.ts +0 -1
- package/dist/clients/attach/render/page-block.js +4 -57
- package/dist/clients/attach/viewer.js +570 -563
- package/dist/clients/inbox/__tests__/integration/inbox-controller.test.js +9 -0
- package/dist/clients/inbox/__tests__/integration/mount-panel.test.js +62 -1
- package/dist/clients/inbox/controller.d.ts +10 -0
- package/dist/clients/inbox/controller.js +56 -12
- package/dist/clients/inbox/tui/input.js +38 -10
- package/dist/clients/inbox/tui/page-body.d.ts +10 -0
- package/dist/clients/inbox/tui/page-body.js +66 -0
- package/dist/clients/inbox/tui/panel.js +6 -4
- package/dist/clients/inbox/tui/render.js +89 -17
- package/dist/clients/inbox/tui/types.d.ts +4 -4
- package/dist/commands/__tests__/human.test.js +18 -3
- package/dist/commands/__tests__/node-message.test.js +3 -3
- package/dist/commands/api-client.js +1 -7
- package/dist/commands/canvas-config.js +6 -14
- package/dist/commands/canvas-use.js +4 -6
- package/dist/commands/cron.js +16 -22
- package/dist/commands/human/prompts.d.ts +1 -1
- package/dist/commands/human/prompts.js +118 -112
- package/dist/commands/human/request.js +13 -14
- package/dist/commands/human/review.js +3 -4
- package/dist/commands/human/shared.d.ts +6 -0
- package/dist/commands/human/shared.js +43 -5
- package/dist/commands/human.js +1 -1
- package/dist/commands/memory/delete.js +4 -6
- package/dist/commands/memory/edit.js +0 -4
- package/dist/commands/memory/move.js +3 -5
- package/dist/commands/memory/shared.d.ts +1 -1
- package/dist/commands/memory/shared.js +9 -5
- package/dist/commands/memory/write.js +60 -29
- package/dist/commands/memory.js +1 -1
- package/dist/commands/node/bash.js +6 -9
- package/dist/commands/node/create.js +89 -22
- package/dist/commands/node/inspect.js +3 -3
- package/dist/commands/node/lifecycle.js +30 -27
- package/dist/commands/node/message.js +24 -45
- package/dist/commands/node/subscription.js +6 -15
- package/dist/commands/node/wait.js +2 -3
- package/dist/commands/node-lifecycle-revive.js +1 -12
- package/dist/commands/pkg/browse/actions.js +2 -3
- package/dist/commands/pkg/market-manage.js +2 -5
- package/dist/commands/pkg/plugin-manage.js +7 -8
- package/dist/commands/profile/default.js +5 -5
- package/dist/commands/profile/delete.js +1 -1
- package/dist/commands/profile/env.js +9 -15
- package/dist/commands/profile/kind.js +3 -7
- package/dist/commands/profile/meta.js +3 -5
- package/dist/commands/profile/new.js +0 -6
- package/dist/commands/profile/pause.js +4 -8
- package/dist/commands/profile/project.js +5 -9
- package/dist/commands/profile/rename.js +3 -7
- package/dist/commands/profile/show.js +3 -3
- package/dist/commands/profile.js +4 -3
- package/dist/commands/surface-tmux-spread.js +1 -3
- package/dist/commands/sys/config.js +3 -4
- package/dist/commands/sys/support/prepare.js +8 -4
- package/dist/commands/sys/support/submit.js +2 -3
- package/dist/commands/sys/sync-deps.js +1 -9
- package/dist/commands/sys/sync-project-guidance.js +1 -7
- package/dist/commands/sys/sync-skills.js +1 -11
- package/dist/core/__tests__/cron-node-sink-parked-root.test.d.ts +1 -0
- package/dist/core/__tests__/cron-node-sink-parked-root.test.js +147 -0
- package/dist/core/__tests__/history-inbox.test.js +11 -1
- package/dist/core/__tests__/human-deliver.test.js +2 -1
- package/dist/core/__tests__/integration/command-plugins.test.js +0 -1
- package/dist/core/__tests__/integration/deferred-no-wake.test.js +0 -1
- package/dist/core/__tests__/lifecycle.test.js +30 -2
- package/dist/core/__tests__/revive-parked-fresh.test.d.ts +1 -0
- package/dist/core/__tests__/revive-parked-fresh.test.js +109 -0
- package/dist/core/__tests__/seam/dormancy-release.test.js +32 -5
- package/dist/core/canvas/attention.d.ts +2 -0
- package/dist/core/canvas/attention.js +25 -18
- package/dist/core/canvas/extensions.d.ts +1 -1
- package/dist/core/canvas/extensions.js +7 -1
- package/dist/core/canvas/history.js +20 -2
- package/dist/core/canvas/types.d.ts +1 -1
- package/dist/core/command.js +33 -8
- package/dist/core/help.d.ts +28 -2
- package/dist/core/help.js +46 -10
- package/dist/core/human/__tests__/page-html-markdown.test.d.ts +1 -0
- package/dist/core/human/__tests__/page-html-markdown.test.js +48 -0
- package/dist/core/human/component-docs.js +4 -4
- package/dist/core/human/page-html-markdown.d.ts +8 -0
- package/dist/core/human/page-html-markdown.js +260 -0
- package/dist/core/memory/lint.d.ts +15 -0
- package/dist/core/memory/lint.js +150 -90
- package/dist/core/profiles/__tests__/fuzzy-match.test.d.ts +1 -0
- package/dist/core/profiles/__tests__/fuzzy-match.test.js +51 -0
- package/dist/core/profiles/fuzzy-match.d.ts +19 -0
- package/dist/core/profiles/fuzzy-match.js +92 -0
- package/dist/core/profiles/manifest.d.ts +14 -7
- package/dist/core/profiles/manifest.js +62 -12
- package/dist/core/profiles/select.d.ts +3 -1
- package/dist/core/profiles/select.js +5 -3
- package/dist/core/profiles/state-block.js +4 -3
- package/dist/core/runtime/boot-root.d.ts +3 -2
- package/dist/core/runtime/canvas-extensions.d.ts +7 -1
- package/dist/core/runtime/canvas-extensions.js +8 -1
- package/dist/core/runtime/lifecycle.d.ts +11 -2
- package/dist/core/runtime/lifecycle.js +15 -2
- package/dist/core/runtime/model-selection.d.ts +4 -0
- package/dist/core/runtime/model-selection.js +5 -0
- package/dist/core/runtime/nodes.js +5 -0
- package/dist/core/runtime/reopen.d.ts +6 -0
- package/dist/core/runtime/reopen.js +12 -1
- package/dist/core/runtime/revive.d.ts +6 -0
- package/dist/core/runtime/revive.js +22 -2
- package/dist/core/runtime/spawn.d.ts +5 -2
- package/dist/core/runtime/spawn.js +18 -32
- package/dist/core/runtime/structured-output.d.ts +6 -0
- package/dist/core/runtime/structured-output.js +6 -0
- package/dist/core/substrate/__tests__/surface-match-command.test.d.ts +1 -0
- package/dist/core/substrate/__tests__/surface-match-command.test.js +89 -0
- package/dist/core/substrate/on-read.js +2 -1
- package/dist/core/substrate/surface-match.js +164 -12
- package/dist/core/termrender/version.d.ts +1 -1
- package/dist/core/termrender/version.js +1 -1
- package/dist/core/user-settings.js +1 -1
- package/dist/daemon/api/__tests__/broker-settle-park.test.d.ts +1 -0
- package/dist/daemon/api/__tests__/broker-settle-park.test.js +102 -0
- package/dist/daemon/api/__tests__/canvas-snapshot-fields.test.d.ts +1 -0
- package/dist/daemon/api/__tests__/canvas-snapshot-fields.test.js +188 -0
- package/dist/daemon/api/__tests__/node-create-description.test.d.ts +1 -0
- package/dist/daemon/api/__tests__/node-create-description.test.js +83 -0
- package/dist/daemon/api/__tests__/profile-metadata-route.test.d.ts +1 -0
- package/dist/daemon/api/__tests__/profile-metadata-route.test.js +92 -0
- package/dist/daemon/api/__tests__/reopen-delivery.test.d.ts +1 -0
- package/dist/daemon/api/__tests__/reopen-delivery.test.js +173 -0
- package/dist/daemon/api/handlers/broker-ops.js +21 -0
- package/dist/daemon/api/handlers/canvas.js +10 -0
- package/dist/daemon/api/handlers/messages.js +25 -16
- package/dist/daemon/api/handlers/nodes.js +5 -0
- package/dist/daemon/api/handlers/profiles.js +22 -1
- package/dist/daemon/cron-run.js +19 -1
- package/dist/daemon/manage.d.ts +16 -1
- package/dist/daemon/manage.js +20 -1
- package/dist/daemon/park-pending.d.ts +13 -0
- package/dist/daemon/park-pending.js +42 -0
- package/dist/daemon/reconcilers/broker-supervision.d.ts +13 -0
- package/dist/daemon/reconcilers/broker-supervision.js +139 -21
- package/dist/daemon/reconcilers/live-obligation.d.ts +10 -4
- package/dist/daemon/reconcilers/live-obligation.js +7 -3
- package/dist/daemon/reconcilers/storage-maintenance.d.ts +0 -4
- package/dist/daemon/reconcilers/storage-maintenance.js +1 -33
- package/dist/pi-extensions/canvas-prompt-scrub.d.ts +13 -0
- package/dist/pi-extensions/canvas-prompt-scrub.js +53 -0
- package/dist/shared/generated-context.d.ts +7 -0
- package/dist/shared/generated-context.js +11 -0
- package/package.json +4 -4
- package/runtime.lock.json +2 -2
- 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
|
|
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: [...
|
|
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: [...
|
|
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: [...
|
|
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
|
|
7
|
-
|
|
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
|
|
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() {
|
package/dist/commands/human.js
CHANGED
|
@@ -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:
|
|
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
|
-
|
|
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: '
|
|
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}
|
|
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
|
|
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). 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 } =
|
|
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
|
-
|
|
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
|
}
|
|
@@ -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
|
|
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). 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
|
|
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: '
|
|
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: '
|
|
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
|
-
//
|
|
170
|
-
//
|
|
171
|
-
//
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
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
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
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
|
});
|
package/dist/commands/memory.js
CHANGED
|
@@ -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',
|
|
@@ -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,
|
|
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 ${
|
|
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,
|
|
169
|
+
return { node_id: node.node_id, signaled: stopped.signaled };
|
|
173
170
|
},
|
|
174
|
-
render: (result) => `Canceled background bash job
|
|
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',
|