@crouton-kit/crouter 0.3.60 → 0.3.62
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/builtin-views/canvas/core.mjs +2 -1
- package/dist/clients/attach/__tests__/context-message.test.d.ts +1 -0
- package/dist/clients/attach/__tests__/context-message.test.js +46 -0
- package/dist/clients/attach/__tests__/transport-relay.test.d.ts +1 -0
- package/dist/clients/attach/__tests__/transport-relay.test.js +176 -0
- package/dist/clients/attach/attach-cmd.js +747 -743
- package/dist/clients/attach/canvas-panels.d.ts +4 -4
- package/dist/clients/attach/canvas-panels.js +108 -37
- package/dist/clients/attach/chat-view.js +11 -0
- package/dist/clients/attach/context-message.d.ts +3 -1
- package/dist/clients/attach/context-message.js +30 -21
- package/dist/clients/attach/graph-overlay.d.ts +8 -1
- package/dist/clients/attach/graph-overlay.js +201 -22
- package/dist/clients/attach/transport-relay.d.ts +51 -0
- package/dist/clients/attach/transport-relay.js +196 -0
- package/dist/clients/attach/transport-socket.d.ts +29 -0
- package/dist/clients/attach/transport-socket.js +164 -0
- package/dist/clients/attach/transport.d.ts +32 -0
- package/dist/clients/attach/transport.js +26 -0
- package/dist/clients/attach/view-socket.d.ts +40 -44
- package/dist/clients/attach/view-socket.js +94 -197
- package/dist/clients/web/server.js +1 -0
- package/dist/commands/memory/delete.js +3 -3
- package/dist/commands/memory/origin.js +1 -1
- package/dist/commands/memory/read.js +3 -3
- package/dist/commands/memory/shared.d.ts +8 -6
- package/dist/commands/memory/shared.js +23 -9
- package/dist/commands/memory/write.js +3 -3
- package/dist/commands/node-worktree.js +9 -9
- package/dist/commands/node.js +71 -23
- package/dist/commands/sys/doctor.js +1 -0
- package/dist/core/__tests__/broker-fork-seam.test.js +17 -2
- package/dist/core/__tests__/broker-snapshot-history.test.js +12 -2
- package/dist/core/__tests__/canvas-inbox-watcher.test.js +79 -0
- package/dist/core/__tests__/context-intro.test.js +11 -0
- package/dist/core/__tests__/fixtures/fake-engine.d.ts +9 -4
- package/dist/core/__tests__/fixtures/fake-engine.js +12 -4
- package/dist/core/__tests__/kickoff.test.js +21 -0
- package/dist/core/__tests__/spawn-root.test.js +60 -0
- package/dist/core/__tests__/worktree.test.js +24 -1
- package/dist/core/canvas/browse/app.d.ts +2 -0
- package/dist/core/canvas/browse/app.js +83 -48
- package/dist/core/canvas/db.js +3 -2
- package/dist/core/canvas/index.d.ts +1 -0
- package/dist/core/canvas/index.js +1 -0
- package/dist/core/canvas/nav-model.d.ts +5 -3
- package/dist/core/canvas/nav-model.js +7 -6
- package/dist/core/canvas/paths.d.ts +8 -0
- package/dist/core/canvas/paths.js +10 -0
- package/dist/core/canvas/render.d.ts +6 -0
- package/dist/core/canvas/render.js +117 -20
- package/dist/core/canvas/source.d.ts +26 -0
- package/dist/core/canvas/source.js +29 -0
- package/dist/core/canvas/status-glyph.js +1 -0
- package/dist/core/canvas/types.d.ts +4 -3
- package/dist/core/config.d.ts +18 -2
- package/dist/core/config.js +88 -10
- package/dist/core/fault-classifier.d.ts +2 -2
- package/dist/core/memory-resolver.d.ts +9 -6
- package/dist/core/memory-resolver.js +28 -4
- package/dist/core/profiles/__tests__/root-profile.test.d.ts +1 -0
- package/dist/core/profiles/__tests__/root-profile.test.js +69 -0
- package/dist/core/profiles/manifest.d.ts +2 -0
- package/dist/core/profiles/manifest.js +40 -7
- package/dist/core/profiles/select.d.ts +4 -4
- package/dist/core/profiles/select.js +13 -13
- package/dist/core/runtime/__tests__/spawn-env.test.d.ts +1 -0
- package/dist/core/runtime/__tests__/spawn-env.test.js +268 -0
- package/dist/core/runtime/bearings.d.ts +5 -4
- package/dist/core/runtime/bearings.js +12 -6
- package/dist/core/runtime/broker.js +59 -7
- package/dist/core/runtime/fault-recovery.js +4 -0
- package/dist/core/runtime/front-door.js +1 -1
- package/dist/core/runtime/host.js +43 -3
- package/dist/core/runtime/kickoff.js +6 -0
- package/dist/core/runtime/launch.d.ts +6 -1
- package/dist/core/runtime/launch.js +7 -1
- package/dist/core/runtime/nodes.d.ts +7 -6
- package/dist/core/runtime/nodes.js +3 -3
- package/dist/core/runtime/situational-context.d.ts +12 -0
- package/dist/core/runtime/situational-context.js +48 -0
- package/dist/core/runtime/spawn-env.d.ts +27 -0
- package/dist/core/runtime/spawn-env.js +169 -0
- package/dist/core/runtime/spawn.d.ts +16 -7
- package/dist/core/runtime/spawn.js +8 -2
- package/dist/core/substrate/session-cache.js +7 -1
- package/dist/core/substrate/subject.d.ts +1 -1
- package/dist/core/view/remote-canvas-target.d.ts +3 -0
- package/dist/core/view/remote-canvas-target.js +25 -0
- package/dist/core/worktree.d.ts +4 -4
- package/dist/core/worktree.js +41 -19
- package/dist/daemon/crtrd.js +32 -0
- package/dist/daemon/predicate-eval.js +12 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +4 -0
- package/dist/pi-extensions/canvas-bash-valve.d.ts +2 -0
- package/dist/pi-extensions/canvas-bash-valve.js +238 -0
- package/dist/pi-extensions/canvas-inbox-watcher.d.ts +12 -0
- package/dist/pi-extensions/canvas-inbox-watcher.js +41 -5
- package/dist/types.d.ts +32 -0
- package/dist/types.js +7 -0
- package/dist/web-client/apple-touch-icon.png +0 -0
- package/dist/web-client/assets/index-BnmSLNLa.css +2 -0
- package/dist/web-client/assets/index-BvzxXXGU.js +77 -0
- package/dist/web-client/assets/workbox-window.prod.es5-Bd17z0YL.js +2 -0
- package/dist/web-client/icon-192.png +0 -0
- package/dist/web-client/icon-512.png +0 -0
- package/dist/web-client/icon-maskable-512.png +0 -0
- package/dist/web-client/index.html +6 -3
- package/dist/web-client/manifest.webmanifest +1 -0
- package/dist/web-client/sw.js +1 -0
- package/docs/public-api.md +5 -3
- package/package.json +4 -3
- package/dist/web-client/assets/fraunces-latin-ext-wght-normal-Ca2vKHc0.woff2 +0 -0
- package/dist/web-client/assets/fraunces-latin-wght-normal-ukD16Tqj.woff2 +0 -0
- package/dist/web-client/assets/fraunces-vietnamese-wght-normal-CnvboYUG.woff2 +0 -0
- package/dist/web-client/assets/index-72YQdji_.js +0 -77
- package/dist/web-client/assets/index-bDhImjJy.css +0 -2
- package/dist/web-client/assets/instrument-sans-latin-ext-wght-normal-B5bTHO_g.woff2 +0 -0
- package/dist/web-client/assets/instrument-sans-latin-wght-normal-BbzFLZTg.woff2 +0 -0
|
@@ -1,106 +1,99 @@
|
|
|
1
1
|
// view-socket.ts — the socket client half of `crtr surface attach` (Phase 4, T7).
|
|
2
2
|
//
|
|
3
3
|
// The §0 ONE-WRITER INVARIANT lives in this directory: a viewer has ONLY a
|
|
4
|
-
//
|
|
5
|
-
// over its unix socket (`nodeDir(id)/view.sock`)
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
4
|
+
// Transport (`transport.ts`) to an ALREADY-running headless broker — locally
|
|
5
|
+
// over its unix socket (`SocketTransport`, `nodeDir(id)/view.sock`), or
|
|
6
|
+
// remotely over a relayed WebSocket to the Blaxel preview endpoint
|
|
7
|
+
// (`RelayTransport`, `transport-relay.ts`). ViewSocketClient NEVER spawns pi,
|
|
8
|
+
// NEVER opens the session `.jsonl`, NEVER constructs an engine — if the
|
|
9
|
+
// broker is not running, connect fails and `attach-cmd` exits (focus/T9 is
|
|
9
10
|
// what keeps a broker alive, not attach). It also never uses pi's `RpcClient`
|
|
10
11
|
// (which would spawn its own engine).
|
|
11
12
|
//
|
|
13
|
+
// Everything below is transport-agnostic: request correlation (the pending
|
|
14
|
+
// map), the reconnect give-up predicate, and close handling are the SAME
|
|
15
|
+
// regardless of which Transport is plugged in at construction — only the
|
|
16
|
+
// wire differs. See `transport.ts` for the seam contract.
|
|
17
|
+
//
|
|
12
18
|
// Reconnect: a dropped broker emits `close`; the caller's reconnect supervisor
|
|
13
|
-
// decides whether to re-dial (a yield/revive keeps the SAME
|
|
14
|
-
// `
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
// a clean error+exit, not a crash.
|
|
19
|
+
// decides whether to re-dial (a yield/revive keeps the SAME target — a local
|
|
20
|
+
// `view.sock` path, or a remote preview endpoint — `redial()` re-establishes
|
|
21
|
+
// the connection to it) or give up ("broker gone"). A connect-time
|
|
22
|
+
// unavailable-broker signal surfaces as `BrokerUnavailableError` so the FIRST
|
|
23
|
+
// connect exits non-zero with a clear message; the same signal during a
|
|
24
|
+
// redial rejects the redial promise as retryable.
|
|
20
25
|
import { EventEmitter } from 'node:events';
|
|
21
26
|
import { randomUUID } from 'node:crypto';
|
|
22
|
-
import {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
import { CLIENT_READ_CAPS, encodeFrame, FrameDecoder, FrameOverflowError, } from '../../core/runtime/broker-protocol.js';
|
|
27
|
+
import { SocketTransport } from './transport-socket.js';
|
|
28
|
+
export { BrokerUnavailableError } from './transport.js';
|
|
29
|
+
export { SocketTransport } from './transport-socket.js';
|
|
30
|
+
export { RelayTransport } from './transport-relay.js';
|
|
27
31
|
/** How long {@link ViewSocketClient.request} waits for the correlated reply before
|
|
28
32
|
* rejecting — bounds a pending picker fetch if the broker drops the request
|
|
29
33
|
* (it should always reply with `data` or a correlated `error`). */
|
|
30
34
|
const REQUEST_TIMEOUT_MS = 10_000;
|
|
31
|
-
/** Surfaced when the node has no reachable broker at connect time (no socket
|
|
32
|
-
* file, or a stale socket with nothing listening). The command catches this to
|
|
33
|
-
* exit non-zero with a focus/revive hint. */
|
|
34
35
|
/** The reconnect supervisor's give-up predicate (extracted pure so it is
|
|
35
36
|
* testable without a socket or TUI). After a broker close the viewer KEEPS
|
|
36
|
-
* re-dialing the same
|
|
37
|
+
* re-dialing the same target while the node is still alive — a yield
|
|
37
38
|
* leaves `status='active'` (intent='refresh') and the daemon revives a fresh
|
|
38
39
|
* broker on the same path. It gives up only when the node is genuinely gone:
|
|
39
40
|
* a terminal status (done/dead/canceled) or a reaped row (null). `idle` is NOT
|
|
40
41
|
* terminal — an idle-release node revives on its next inbox wake, so keep
|
|
41
42
|
* trying (the supervisor waits at a relaxed cadence for as long as the row
|
|
42
|
-
* stays live).
|
|
43
|
+
* stays live). Local canvas rows only — a remote target has no local row, so
|
|
44
|
+
* callers driving a remote attach must not consult this (see attach-cmd.ts). */
|
|
43
45
|
export function reconnectShouldGiveUp(row) {
|
|
44
46
|
if (row === null)
|
|
45
47
|
return true;
|
|
46
48
|
return row.status === 'done' || row.status === 'dead' || row.status === 'canceled';
|
|
47
49
|
}
|
|
48
|
-
export class BrokerUnavailableError extends Error {
|
|
49
|
-
nodeId;
|
|
50
|
-
constructor(nodeId) {
|
|
51
|
-
super(`node ${nodeId} has no running broker — focus or revive it first`);
|
|
52
|
-
this.nodeId = nodeId;
|
|
53
|
-
this.name = 'BrokerUnavailableError';
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
50
|
export class ViewSocketClient extends EventEmitter {
|
|
57
51
|
nodeId;
|
|
58
|
-
|
|
59
|
-
decoder = new FrameDecoder(CLIENT_READ_CAPS);
|
|
52
|
+
transport;
|
|
60
53
|
closeEmitted = false;
|
|
54
|
+
/** Tracks live connect↔close state so {@link request} can fail fast on a
|
|
55
|
+
* dead/absent connection rather than parking a promise the reply will
|
|
56
|
+
* never reach (transport-agnostic mirror of the old socket.destroyed
|
|
57
|
+
* check). A post-close request lands here too — {@link rejectAllPending}
|
|
58
|
+
* already ran by the time this flips false. */
|
|
59
|
+
connected = false;
|
|
60
|
+
/** The last transport-level error message, so a close can reject any
|
|
61
|
+
* in-flight requests with the precise reason (e.g. an oversized-frame
|
|
62
|
+
* overflow) instead of a generic one. Cleared on every fresh connect. */
|
|
63
|
+
lastTransportError;
|
|
61
64
|
/** In-flight correlated read-ops, keyed by the `id` minted in {@link request}.
|
|
62
65
|
* Resolved by the matching `data` frame / rejected by the matching `error`
|
|
63
|
-
* frame in {@link
|
|
66
|
+
* frame in {@link onFrame}, the request's timeout, or transport teardown. */
|
|
64
67
|
pending = new Map();
|
|
65
|
-
/**
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
|
|
70
|
-
redialFaultRecorded = false;
|
|
71
|
-
constructor(nodeId) {
|
|
68
|
+
/** `transport` defaults to a {@link SocketTransport} against this node's
|
|
69
|
+
* local `view.sock` — today's behavior, unchanged. Pass a
|
|
70
|
+
* {@link RelayTransport} to attach a remote node instead; every method
|
|
71
|
+
* below behaves identically either way. */
|
|
72
|
+
constructor(nodeId, transport) {
|
|
72
73
|
super();
|
|
73
74
|
this.nodeId = nodeId;
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
this.
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
kind: classified.kind,
|
|
86
|
-
retry: op === 'redial'
|
|
87
|
-
? { disposition: 'auto', by: 'client' }
|
|
88
|
-
: { disposition: 'manual' },
|
|
89
|
-
message: err instanceof Error ? err.message : String(err),
|
|
75
|
+
this.transport = transport ?? new SocketTransport(nodeId);
|
|
76
|
+
this.transport.on('connect', () => {
|
|
77
|
+
this.closeEmitted = false;
|
|
78
|
+
this.connected = true;
|
|
79
|
+
this.lastTransportError = undefined;
|
|
80
|
+
this.emit('connect');
|
|
81
|
+
});
|
|
82
|
+
this.transport.on('frame', (frame) => this.onFrame(frame));
|
|
83
|
+
this.transport.on('error', (err) => {
|
|
84
|
+
this.lastTransportError = err.message;
|
|
85
|
+
this.emitError(err);
|
|
90
86
|
});
|
|
87
|
+
this.transport.on('close', () => this.onClose());
|
|
91
88
|
}
|
|
92
89
|
/** Issue a correlated read-op and resolve with the broker's `data` reply (or
|
|
93
|
-
* reject on the correlated `error`, a timeout, or
|
|
94
|
-
* `id`, sends `{...frame, id}`, and parks a resolver consumed by
|
|
95
|
-
* The reply is narrowed by the caller on its `kind`. */
|
|
90
|
+
* reject on the correlated `error`, a timeout, or transport teardown). Mints
|
|
91
|
+
* the `id`, sends `{...frame, id}`, and parks a resolver consumed by
|
|
92
|
+
* {@link onFrame}. The reply is narrowed by the caller on its `kind`. */
|
|
96
93
|
request(frame) {
|
|
97
94
|
const id = randomUUID();
|
|
98
95
|
return new Promise((resolve, reject) => {
|
|
99
|
-
|
|
100
|
-
// reply will never reach (it would otherwise hang the full timeout). A
|
|
101
|
-
// post-`close` request lands here too — `rejectAllPending` already ran.
|
|
102
|
-
const sock = this.socket;
|
|
103
|
-
if (sock === undefined || sock.destroyed) {
|
|
96
|
+
if (!this.connected) {
|
|
104
97
|
reject(new Error(`cannot issue '${frame.type}': no live broker connection`));
|
|
105
98
|
return;
|
|
106
99
|
}
|
|
@@ -114,8 +107,8 @@ export class ViewSocketClient extends EventEmitter {
|
|
|
114
107
|
this.send({ ...frame, id });
|
|
115
108
|
});
|
|
116
109
|
}
|
|
117
|
-
/** Reject + clear every in-flight request (
|
|
118
|
-
* picker fetch never hangs past the connection it rode on. */
|
|
110
|
+
/** Reject + clear every in-flight request (connection gone / decode error)
|
|
111
|
+
* so a picker fetch never hangs past the connection it rode on. */
|
|
119
112
|
rejectAllPending(reason) {
|
|
120
113
|
for (const { reject, timer } of this.pending.values()) {
|
|
121
114
|
clearTimeout(timer);
|
|
@@ -123,157 +116,61 @@ export class ViewSocketClient extends EventEmitter {
|
|
|
123
116
|
}
|
|
124
117
|
this.pending.clear();
|
|
125
118
|
}
|
|
126
|
-
/**
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
}
|
|
130
|
-
/** Open the connection. Wire up listeners; emits `connect` on success or
|
|
131
|
-
* `error` (a {@link BrokerUnavailableError} for ECONNREFUSED/ENOENT) on
|
|
132
|
-
* failure. Idempotent guard is the caller's job — call once. */
|
|
119
|
+
/** Open the connection. Emits `connect` on success or `error` (a
|
|
120
|
+
* {@link BrokerUnavailableError} when the target has no reachable broker)
|
|
121
|
+
* on failure. Idempotent guard is the caller's job — call once. */
|
|
133
122
|
connect() {
|
|
134
|
-
|
|
135
|
-
this.socket = socket;
|
|
136
|
-
socket.on('connect', () => {
|
|
137
|
-
this.redialFaultRecorded = false;
|
|
138
|
-
clearFault(this.nodeId, { link: 'viewer↔broker' });
|
|
139
|
-
this.emit('connect');
|
|
140
|
-
});
|
|
141
|
-
socket.on('data', (chunk) => this.onData(chunk));
|
|
142
|
-
socket.on('error', (err) => this.onError(err, 'connect'));
|
|
143
|
-
socket.on('close', () => this.onClose());
|
|
123
|
+
this.transport.connect();
|
|
144
124
|
}
|
|
145
|
-
/** Re-establish the
|
|
146
|
-
*
|
|
147
|
-
*
|
|
148
|
-
* guard, then re-dials. Resolves on `connect` (caller re-sends `hello`);
|
|
149
|
-
* rejects on the dial's `error` (ECONNREFUSED while the new broker is
|
|
150
|
-
* mid-boot, or ENOENT before it re-binds the socket) — both retryable. A
|
|
151
|
-
* post-connect error flows through the normal `onError`→`close` path. */
|
|
125
|
+
/** Re-establish the connection after a broker exit (a yield→revive cycle),
|
|
126
|
+
* to the SAME target. Resolves on `connect` (caller re-sends `hello`);
|
|
127
|
+
* rejects on a retryable dial failure. */
|
|
152
128
|
redial() {
|
|
153
|
-
this.
|
|
154
|
-
this.decoder = new FrameDecoder(CLIENT_READ_CAPS);
|
|
155
|
-
this.closeEmitted = false;
|
|
156
|
-
return new Promise((resolve, reject) => {
|
|
157
|
-
const socket = createConnection(this.socketPath);
|
|
158
|
-
this.socket = socket;
|
|
159
|
-
let settled = false;
|
|
160
|
-
socket.on('connect', () => {
|
|
161
|
-
if (settled)
|
|
162
|
-
return;
|
|
163
|
-
settled = true;
|
|
164
|
-
this.redialFaultRecorded = false;
|
|
165
|
-
clearFault(this.nodeId, { link: 'viewer↔broker' });
|
|
166
|
-
this.emit('connect');
|
|
167
|
-
resolve();
|
|
168
|
-
});
|
|
169
|
-
socket.on('data', (chunk) => this.onData(chunk));
|
|
170
|
-
socket.on('error', (err) => {
|
|
171
|
-
if (settled) {
|
|
172
|
-
this.onError(err, 'redial');
|
|
173
|
-
return;
|
|
174
|
-
}
|
|
175
|
-
settled = true;
|
|
176
|
-
this.recordSocketFault('redial', err);
|
|
177
|
-
try {
|
|
178
|
-
socket.destroy();
|
|
179
|
-
}
|
|
180
|
-
catch {
|
|
181
|
-
/* ignore */
|
|
182
|
-
}
|
|
183
|
-
reject(err);
|
|
184
|
-
});
|
|
185
|
-
socket.on('close', () => this.onClose());
|
|
186
|
-
});
|
|
129
|
+
return this.transport.redial();
|
|
187
130
|
}
|
|
188
|
-
/** Encode +
|
|
189
|
-
*
|
|
131
|
+
/** Encode + send one client→broker frame. No-op on a dead/absent
|
|
132
|
+
* connection; never throws. */
|
|
190
133
|
send(frame) {
|
|
191
|
-
|
|
192
|
-
if (sock === undefined || sock.destroyed)
|
|
193
|
-
return;
|
|
194
|
-
try {
|
|
195
|
-
sock.write(encodeFrame(frame));
|
|
196
|
-
}
|
|
197
|
-
catch {
|
|
198
|
-
/* dead socket — 'close' will fire and drive teardown */
|
|
199
|
-
}
|
|
134
|
+
this.transport.send(frame);
|
|
200
135
|
}
|
|
201
|
-
/** Detach:
|
|
136
|
+
/** Detach: tear down the transport. `close` fires → the caller tears down. */
|
|
202
137
|
close() {
|
|
203
|
-
this.destroy();
|
|
138
|
+
this.transport.destroy();
|
|
204
139
|
}
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
this.rejectAllPending(msg);
|
|
217
|
-
this.emitError(new Error(msg));
|
|
218
|
-
this.destroy();
|
|
219
|
-
return;
|
|
220
|
-
}
|
|
221
|
-
for (const raw of frames) {
|
|
222
|
-
const frame = raw;
|
|
223
|
-
// Correlated replies (read-ops + dequeue) are consumed by the pending-by-id
|
|
224
|
-
// resolver, NOT re-emitted as a generic 'frame' (the attach frame router
|
|
225
|
-
// would otherwise treat a `data` frame as an AgentSessionEvent). A `data`
|
|
226
|
-
// frame is ALWAYS a reply, so it is swallowed unconditionally — even a late
|
|
227
|
-
// post-timeout one (no pending entry) is dropped, never leaked to the
|
|
228
|
-
// router. An `error` is correlated only when its `id` matches an in-flight
|
|
229
|
-
// request; an uncorrelated error still flows through.
|
|
230
|
-
if (frame.type === 'data') {
|
|
231
|
-
const entry = typeof frame.id === 'string' ? this.pending.get(frame.id) : undefined;
|
|
232
|
-
if (entry) {
|
|
233
|
-
this.pending.delete(frame.id);
|
|
234
|
-
clearTimeout(entry.timer);
|
|
235
|
-
entry.resolve(frame);
|
|
236
|
-
}
|
|
237
|
-
continue;
|
|
238
|
-
}
|
|
239
|
-
if (frame.type === 'error' && typeof frame.id === 'string' && this.pending.has(frame.id)) {
|
|
240
|
-
const entry = this.pending.get(frame.id);
|
|
140
|
+
onFrame(frame) {
|
|
141
|
+
// Correlated replies (read-ops + dequeue) are consumed by the pending-by-id
|
|
142
|
+
// resolver, NOT re-emitted as a generic 'frame' (the attach frame router
|
|
143
|
+
// would otherwise treat a `data` frame as an AgentSessionEvent). A `data`
|
|
144
|
+
// frame is ALWAYS a reply, so it is swallowed unconditionally — even a late
|
|
145
|
+
// post-timeout one (no pending entry) is dropped, never leaked to the
|
|
146
|
+
// router. An `error` is correlated only when its `id` matches an in-flight
|
|
147
|
+
// request; an uncorrelated error still flows through.
|
|
148
|
+
if (frame.type === 'data') {
|
|
149
|
+
const entry = typeof frame.id === 'string' ? this.pending.get(frame.id) : undefined;
|
|
150
|
+
if (entry) {
|
|
241
151
|
this.pending.delete(frame.id);
|
|
242
152
|
clearTimeout(entry.timer);
|
|
243
|
-
entry.
|
|
244
|
-
continue;
|
|
153
|
+
entry.resolve(frame);
|
|
245
154
|
}
|
|
246
|
-
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
|
-
onError(err, op) {
|
|
250
|
-
this.recordSocketFault(op, err);
|
|
251
|
-
if (err.code === 'ECONNREFUSED' || err.code === 'ENOENT') {
|
|
252
|
-
this.emitError(new BrokerUnavailableError(this.nodeId));
|
|
155
|
+
return;
|
|
253
156
|
}
|
|
254
|
-
|
|
255
|
-
|
|
157
|
+
if (frame.type === 'error' && typeof frame.id === 'string' && this.pending.has(frame.id)) {
|
|
158
|
+
const entry = this.pending.get(frame.id);
|
|
159
|
+
this.pending.delete(frame.id);
|
|
160
|
+
clearTimeout(entry.timer);
|
|
161
|
+
entry.reject(new Error(frame.message || `request failed: ${frame.code}`));
|
|
162
|
+
return;
|
|
256
163
|
}
|
|
257
|
-
|
|
164
|
+
this.emit('frame', frame);
|
|
258
165
|
}
|
|
259
166
|
onClose() {
|
|
260
167
|
if (this.closeEmitted)
|
|
261
168
|
return;
|
|
262
169
|
this.closeEmitted = true;
|
|
263
|
-
this.
|
|
170
|
+
this.connected = false;
|
|
171
|
+
this.rejectAllPending(this.lastTransportError ?? 'broker connection closed');
|
|
264
172
|
this.emit('close');
|
|
265
173
|
}
|
|
266
|
-
destroy() {
|
|
267
|
-
const sock = this.socket;
|
|
268
|
-
if (sock !== undefined && !sock.destroyed) {
|
|
269
|
-
try {
|
|
270
|
-
sock.destroy();
|
|
271
|
-
}
|
|
272
|
-
catch {
|
|
273
|
-
/* ignore */
|
|
274
|
-
}
|
|
275
|
-
}
|
|
276
|
-
}
|
|
277
174
|
/** Emit `error` only when a listener exists — a bare EventEmitter `error`
|
|
278
175
|
* with no listener throws, and this client must never throw uncaught. */
|
|
279
176
|
emitError(err) {
|
|
@@ -83,6 +83,7 @@ const CONTENT_TYPES = {
|
|
|
83
83
|
'.mjs': 'text/javascript; charset=utf-8',
|
|
84
84
|
'.css': 'text/css; charset=utf-8',
|
|
85
85
|
'.json': 'application/json; charset=utf-8',
|
|
86
|
+
'.webmanifest': 'application/manifest+json',
|
|
86
87
|
'.svg': 'image/svg+xml',
|
|
87
88
|
'.png': 'image/png',
|
|
88
89
|
'.jpg': 'image/jpeg',
|
|
@@ -22,17 +22,17 @@ function pruneEmptyParents(filePath, nameSegments) {
|
|
|
22
22
|
export const deleteLeaf = defineLeaf({
|
|
23
23
|
name: 'delete',
|
|
24
24
|
description: 'delete a memory document by name',
|
|
25
|
-
whenToUse: 'a stored document is wrong, obsolete, or superseded and should be removed — the sanctioned way to delete one, so you never rm the markdown off disk. Resolves the name exactly as `read` does (project stack > profile > user precedence, leaf-name fallback) and removes that file: if you can read it, you can delete it. Refuses docs you do not own — builtin docs (shipped with the package) and installed-plugin docs (managed by `crtr pkg`) — and refuses an ambiguous leaf name rather than guessing which doc to destroy; pass the full `area/topic` name or `--scope` to pin one.',
|
|
25
|
+
whenToUse: 'a stored document is wrong, obsolete, or superseded and should be removed — the sanctioned way to delete one, so you never rm the markdown off disk. Resolves the name exactly as `read` does (node > project stack > profile > user precedence, leaf-name fallback) and removes that file: if you can read it, you can delete it. Refuses docs you do not own — builtin docs (shipped with the package) and installed-plugin docs (managed by `crtr pkg`) — and refuses an ambiguous leaf name rather than guessing which doc to destroy; pass the full `area/topic` name or `--scope` to pin one.',
|
|
26
26
|
help: {
|
|
27
27
|
name: 'memory delete',
|
|
28
28
|
summary: 'resolve a path-derived name and remove that document from its scope store',
|
|
29
29
|
params: [
|
|
30
|
-
{ kind: 'positional', name: 'name', required: true, constraint: 'Path-derived memory identifier (e.g. `topic` or `area/topic`), resolved as `read` resolves it: precedence project stack > profile > user, with leaf-name fallback. A leaf that maps to genuinely different docs across scopes is rejected as ambiguous — pass the full name or --scope to disambiguate. May be scope-prefixed (`user/topic`) to pin the store directly.' },
|
|
30
|
+
{ kind: 'positional', name: 'name', required: true, constraint: 'Path-derived memory identifier (e.g. `topic` or `area/topic`), resolved as `read` resolves it: precedence node > project stack > profile > user, with leaf-name fallback. A leaf that maps to genuinely different docs across scopes is rejected as ambiguous — pass the full name or --scope to disambiguate. May be scope-prefixed (`user/topic`) to pin the store directly.' },
|
|
31
31
|
{ kind: 'flag', name: 'scope', type: 'enum', choices: [...MEMORY_SCOPES], required: false, constraint: 'Restrict resolution to this scope before deleting. Use it to disambiguate a leaf name present at multiple scopes, or to assert which store you mean. builtin is not a choice — builtin docs ship with the package and cannot be deleted.' },
|
|
32
32
|
],
|
|
33
33
|
output: [
|
|
34
34
|
{ name: 'name', type: 'string', required: true, constraint: 'Path-derived name of the deleted document.' },
|
|
35
|
-
{ name: 'scope', type: 'string', required: true, constraint: 'Scope the document was deleted from: user, project, or profile.' },
|
|
35
|
+
{ name: 'scope', type: 'string', required: true, constraint: 'Scope the document was deleted from: node, user, project, or profile.' },
|
|
36
36
|
{ name: 'path', type: 'string', required: true, constraint: 'Absolute path of the file that was removed.' },
|
|
37
37
|
{ name: 'deleted', type: 'boolean', required: true, constraint: 'Always true on success — the file was removed. A missing document fails with not_found instead.' },
|
|
38
38
|
{ name: 'follow_up', type: 'string', required: true, constraint: 'Concrete next command — browse what remains.' },
|
|
@@ -11,7 +11,7 @@ export const originLeaf = defineLeaf({
|
|
|
11
11
|
name: 'memory origin',
|
|
12
12
|
summary: 'resolve a doc name to its creation provenance and deref the originating node',
|
|
13
13
|
params: [
|
|
14
|
-
{ kind: 'positional', name: 'name', required: true, constraint: 'Path-derived memory identifier (e.g. `topic` or `area/topic`). Resolved across scopes: project > user > builtin.' },
|
|
14
|
+
{ kind: 'positional', name: 'name', required: true, constraint: 'Path-derived memory identifier (e.g. `topic` or `area/topic`). Resolved across scopes: node > project > profile > user > builtin.' },
|
|
15
15
|
],
|
|
16
16
|
output: [
|
|
17
17
|
{ name: 'name', type: 'string', required: true, constraint: 'Resolved document name.' },
|
|
@@ -8,19 +8,19 @@ import { MEMORY_KINDS } from './shared.js';
|
|
|
8
8
|
export const readLeaf = defineLeaf({
|
|
9
9
|
name: 'read',
|
|
10
10
|
description: 'load a memory document body by name',
|
|
11
|
-
whenToUse: 'a task in front of you matches a stored document and you already know its name — read it before improvising. Resolves the path-derived name across scopes by precedence (project stack > profile > user > builtin), with leaf-name fallback. You name the document by its crtr identifier, never a file path — do not cat or find the markdown off disk. Reach for `crtr memory find` first when you do not yet know which document applies.',
|
|
11
|
+
whenToUse: 'a task in front of you matches a stored document and you already know its name — read it before improvising. Resolves the path-derived name across scopes by precedence (node > project stack > profile > user > builtin), with leaf-name fallback. You name the document by its crtr identifier, never a file path — do not cat or find the markdown off disk. Reach for `crtr memory find` first when you do not yet know which document applies.',
|
|
12
12
|
help: {
|
|
13
13
|
name: 'memory read',
|
|
14
14
|
summary: 'resolve a path-derived name to its document body, frontmatter stripped unless --frontmatter',
|
|
15
15
|
params: [
|
|
16
|
-
{ kind: 'positional', name: 'name', required: true, constraint: 'Path-derived memory identifier (e.g. `topic` or `area/topic`). Resolved across scopes by precedence: project stack > profile > user > builtin, with leaf-name fallback.' },
|
|
16
|
+
{ kind: 'positional', name: 'name', required: true, constraint: 'Path-derived memory identifier (e.g. `topic` or `area/topic`). Resolved across scopes by precedence: node > project stack > profile > user > builtin, with leaf-name fallback.' },
|
|
17
17
|
{ kind: 'flag', name: 'kind', type: 'enum', choices: [...MEMORY_KINDS], required: false, constraint: 'Narrows resolution when the name is ambiguous across kinds.' },
|
|
18
18
|
{ kind: 'flag', name: 'frontmatter', type: 'bool', required: false, constraint: 'When present, includes the YAML frontmatter in the returned body. Off by default — only the body is returned.' },
|
|
19
19
|
],
|
|
20
20
|
output: [
|
|
21
21
|
{ name: 'name', type: 'string', required: true, constraint: 'Resolved document name.' },
|
|
22
22
|
{ name: 'kind', type: 'string', required: true, constraint: 'Resolved kind: knowledge or preference.' },
|
|
23
|
-
{ name: 'scope', type: 'string', required: true, constraint: 'Scope the document was resolved from: project, profile, user, or builtin.' },
|
|
23
|
+
{ name: 'scope', type: 'string', required: true, constraint: 'Scope the document was resolved from: node, project, profile, user, or builtin.' },
|
|
24
24
|
{ name: 'path', type: 'string', required: true, constraint: 'Absolute path to the document on disk — edit this file directly to tweak the doc in place.' },
|
|
25
25
|
{ name: 'content', type: 'string', required: true, constraint: 'Document body. Frontmatter stripped unless --frontmatter is set.' },
|
|
26
26
|
{ name: 'follow_up', type: 'string', required: true, constraint: 'Hints at variant flags or next commands.' },
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import type { MemoryScope } from '../../core/memory-resolver.js';
|
|
2
2
|
export declare const MEMORY_KINDS: readonly ["knowledge", "preference"];
|
|
3
3
|
export declare const VISIBILITY_RUNGS: readonly ["none", "name", "preview", "content"];
|
|
4
|
-
export declare const MEMORY_SCOPES: readonly ["user", "project", "profile"];
|
|
5
|
-
/** Scope sort weight matching resolution precedence (project stack >
|
|
6
|
-
* user > builtin). Used by `list` for its "scope then kind then
|
|
7
|
-
* ordering. */
|
|
4
|
+
export declare const MEMORY_SCOPES: readonly ["user", "project", "profile", "node"];
|
|
5
|
+
/** Scope sort weight matching resolution precedence (node > project stack >
|
|
6
|
+
* profile > user > builtin). Used by `list` for its "scope then kind then
|
|
7
|
+
* name" ordering. */
|
|
8
8
|
export declare function scopeRank(scope: MemoryScope): number;
|
|
9
9
|
/** Resolve the write target scope + its memory dir. Default: project when a
|
|
10
10
|
* project scope exists for the cwd, else user. An explicit `--scope project`
|
|
@@ -16,8 +16,10 @@ export declare function scopeRank(scope: MemoryScope): number;
|
|
|
16
16
|
* `--dir <path>`) pins the EXACT project directory, cwd-free: the target is
|
|
17
17
|
* `<dir>/.crouter/memory/`, scaffolded if absent, never the cwd-ancestor walk
|
|
18
18
|
* — the way a profiled agent writes into any project in its purview, and the
|
|
19
|
-
* only way to target a dir shadowed by an ancestor store.
|
|
20
|
-
*
|
|
19
|
+
* only way to target a dir shadowed by an ancestor store. `--scope node`
|
|
20
|
+
* targets the this-node store (`nodes/<CRTR_NODE_ID>/context/memory/`) and
|
|
21
|
+
* requires a running node. Returns the absolute `<root>/memory` dir to write
|
|
22
|
+
* under. */
|
|
21
23
|
export declare function resolveWriteTarget(scopeArg: string | undefined, profileArg?: string, dirArg?: string): {
|
|
22
24
|
scope: MemoryScope;
|
|
23
25
|
memoryDir: string;
|
|
@@ -10,6 +10,7 @@ import { usage } from '../../core/errors.js';
|
|
|
10
10
|
import { CRTR_DIR_NAME } from '../../types.js';
|
|
11
11
|
import { scopeMemoryDir, projectScopeRoot, ensureProjectScopeRoot, resetScopeCache, } from '../../core/scope.js';
|
|
12
12
|
import { loadProfileManifest, profileMemoryDir } from '../../core/profiles/manifest.js';
|
|
13
|
+
import { memoryDir as nodeMemoryDir } from '../../core/runtime/memory.js';
|
|
13
14
|
// The two memory kinds — knowledge (consult: procedural playbooks + factual
|
|
14
15
|
// references merged) vs preference (behave: standing directives). Used as the
|
|
15
16
|
// `--kind` enum choices everywhere.
|
|
@@ -18,12 +19,13 @@ export const MEMORY_KINDS = ['knowledge', 'preference'];
|
|
|
18
19
|
// content). Shared by --system-prompt-visibility and --file-read-visibility.
|
|
19
20
|
export const VISIBILITY_RUNGS = ['none', 'name', 'preview', 'content'];
|
|
20
21
|
// Scope choices for filtering / targeting (builtin is read-only, not writable).
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
*
|
|
22
|
+
// `node` is the this-node store, writable only inside a running node.
|
|
23
|
+
export const MEMORY_SCOPES = ['user', 'project', 'profile', 'node'];
|
|
24
|
+
/** Scope sort weight matching resolution precedence (node > project stack >
|
|
25
|
+
* profile > user > builtin). Used by `list` for its "scope then kind then
|
|
26
|
+
* name" ordering. */
|
|
25
27
|
export function scopeRank(scope) {
|
|
26
|
-
return scope === 'project' ? 0 : scope === 'profile' ? 1 : scope === 'user' ? 2 : 3;
|
|
28
|
+
return scope === 'node' ? -1 : scope === 'project' ? 0 : scope === 'profile' ? 1 : scope === 'user' ? 2 : 3;
|
|
27
29
|
}
|
|
28
30
|
/** Resolve the write target scope + its memory dir. Default: project when a
|
|
29
31
|
* project scope exists for the cwd, else user. An explicit `--scope project`
|
|
@@ -35,8 +37,10 @@ export function scopeRank(scope) {
|
|
|
35
37
|
* `--dir <path>`) pins the EXACT project directory, cwd-free: the target is
|
|
36
38
|
* `<dir>/.crouter/memory/`, scaffolded if absent, never the cwd-ancestor walk
|
|
37
39
|
* — the way a profiled agent writes into any project in its purview, and the
|
|
38
|
-
* only way to target a dir shadowed by an ancestor store.
|
|
39
|
-
*
|
|
40
|
+
* only way to target a dir shadowed by an ancestor store. `--scope node`
|
|
41
|
+
* targets the this-node store (`nodes/<CRTR_NODE_ID>/context/memory/`) and
|
|
42
|
+
* requires a running node. Returns the absolute `<root>/memory` dir to write
|
|
43
|
+
* under. */
|
|
40
44
|
export function resolveWriteTarget(scopeArg, profileArg, dirArg) {
|
|
41
45
|
if (dirArg !== undefined && dirArg !== '') {
|
|
42
46
|
if (scopeArg !== undefined && scopeArg !== 'project') {
|
|
@@ -63,15 +67,25 @@ export function resolveWriteTarget(scopeArg, profileArg, dirArg) {
|
|
|
63
67
|
return { scope: 'project', memoryDir: join(real, CRTR_DIR_NAME, 'memory') };
|
|
64
68
|
}
|
|
65
69
|
let scope;
|
|
66
|
-
if (scopeArg === 'user' || scopeArg === 'project' || scopeArg === 'profile') {
|
|
70
|
+
if (scopeArg === 'user' || scopeArg === 'project' || scopeArg === 'profile' || scopeArg === 'node') {
|
|
67
71
|
scope = scopeArg;
|
|
68
72
|
}
|
|
69
73
|
else if (scopeArg !== undefined) {
|
|
70
|
-
throw usage(`invalid --scope: ${scopeArg} (expected user|project|profile)`);
|
|
74
|
+
throw usage(`invalid --scope: ${scopeArg} (expected user|project|profile|node)`);
|
|
71
75
|
}
|
|
72
76
|
else {
|
|
73
77
|
scope = projectScopeRoot() !== null ? 'project' : 'user';
|
|
74
78
|
}
|
|
79
|
+
if (scope === 'node') {
|
|
80
|
+
// The this-node store: `nodes/<CRTR_NODE_ID>/context/memory/`. Available
|
|
81
|
+
// only inside a running node; the same dir render.ts's nodeLocalDocs loads
|
|
82
|
+
// at boot, so a doc written here rides into this node's knowledge block.
|
|
83
|
+
const nodeId = process.env['CRTR_NODE_ID'] || '';
|
|
84
|
+
if (nodeId === '') {
|
|
85
|
+
throw usage('node scope requires a running node (CRTR_NODE_ID unset); rerun inside a node');
|
|
86
|
+
}
|
|
87
|
+
return { scope: 'node', memoryDir: nodeMemoryDir(nodeId) };
|
|
88
|
+
}
|
|
75
89
|
if (scope === 'profile') {
|
|
76
90
|
const profileIdOrName = profileArg && profileArg !== '' ? profileArg : process.env['CRTR_PROFILE_ID'] || '';
|
|
77
91
|
if (profileIdOrName === '') {
|
|
@@ -14,7 +14,7 @@ export const writeLeaf = defineLeaf({
|
|
|
14
14
|
'Pick the kind. knowledge is consulted for facts or procedures; preference directs behavior. The kind choice is about how the doc is used, not about how long it is.\n\n' +
|
|
15
15
|
'Store reusable current truth, not session notes. Useful memories are non-obvious procedures, gotchas, durable preferences, cross-repo conventions, and amendments to plans/specs that future agents must honor. Put plan/spec amendments under path names like `projects/<topic>/...`; add `projects/<topic>/INDEX.md` with `name` visibility so the topic is discoverable without loading the whole body. Do not store chat summaries, implementation history, or facts already recorded in the repo.\n\n' +
|
|
16
16
|
'Set both visibility rungs explicitly on create. There is no kind default. `none`, `name`, `preview`, and `content` move from least to most loaded: `none` keeps the doc out of auto-load and on-read surfaces, `name` is the bare doc tag only, `preview` is the name + envelope + routing line (`when-and-why-to-read`), and `content` inlines the whole body when the body is short enough to justify it. Each axis is independent; usually one carries a real rung and the other is `none`. When a doc is short enough to state in a single sentence — a one-line preference, or a knowledge fact that fits in a sentence — skip `preview` and use `name` or `content` directly: the routing line would run longer than the doc itself, so a `preview` rung just adds words. Sentence-length `content` docs are not just acceptable, they are correct. Never pad a memory to be more verbose than the rule or fact it carries.\n\n' +
|
|
17
|
-
'Choose the scope. `project` is for facts any agent in one repo needs. `user` is for person-wide facts and preferences that should follow the user everywhere. `profile` is for the profile’s bundle of dirs: cross-repo conventions, how the pieces relate, or the user’s stance toward that body of work. When unsure, choose the narrowest scope that will still reach the next agent who needs it.\n\n' +
|
|
17
|
+
'Choose the scope. `project` is for facts any agent in one repo needs. `user` is for person-wide facts and preferences that should follow the user everywhere. `profile` is for the profile’s bundle of dirs: cross-repo conventions, how the pieces relate, or the user’s stance toward that body of work. `node` is the narrowest — the this-node store (`nodes/<CRTR_NODE_ID>/context/memory/`), scratch memory only this running node sees; it rides straight into this node’s knowledge block and dies with the node. When unsure, choose the narrowest scope that will still reach the next agent who needs it.\n\n' +
|
|
18
18
|
'Choose the hook — boot vs file-read. Behavior and how-to procedure surface at boot. Knowledge about code belongs next to the code: put the file in that directory’s .crouter/memory/ (`--dir <project-dir>` targets it exactly, from anywhere) and it fires positionally when files there are read, costing nothing at boot. A knowledge doc about a person or process has no code directory to anchor to, so on-read triggering is meaningless — route it through boot instead and set the boot rung to `preview` when you want the routing line to surface.\n\n' +
|
|
19
19
|
'Write the routing line (--when-and-why-to-read) first, before storing anything: "When <circumstance>, this <kind> should be read because <payoff>." The test is whether a stranger mid-task can decide from that one line alone whether to spend the read. If you cannot name the concrete situation that triggers it, you do not yet understand the memory — ask the user one sharp question instead of improvising. Never paraphrase the advice in the routing line; keep it about when to read and why the read is worth it.\n\n' +
|
|
20
20
|
'Gate and read-when 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.\n\n' +
|
|
@@ -32,7 +32,7 @@ export const writeLeaf = defineLeaf({
|
|
|
32
32
|
{ kind: 'flag', name: 'read-when', type: 'string', required: false, constraint: 'Frontmatter read-when — YAML/JSON object predicate over a read file’s own frontmatter using the same field/matcher vocabulary described in the guide.' },
|
|
33
33
|
{ 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 `:`) — the doc body becomes the command’s injected prompt. Default false: most docs are consulted, not invoked.' },
|
|
34
34
|
{ kind: 'flag', name: 'rationale', type: 'string', required: false, constraint: 'Frontmatter rationale — 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 in the raw file or `memory read --frontmatter`. Omitting this flag on an update PRESERVES an existing rationale unchanged.' },
|
|
35
|
-
{ kind: 'flag', name: 'scope', type: 'enum', choices: [...MEMORY_SCOPES], required: false, constraint: 'Target scope. Default: project when inside a project, else user. `project` resolves to the NEAREST ancestor `.crouter/` walking up from cwd — in a nested workspace that can be a parent’s store, not the dir you are standing in; pass --dir to pin the exact project directory. `profile` requires a selected profile (CRTR_PROFILE_ID) or an explicit --profile.' },
|
|
35
|
+
{ kind: 'flag', name: 'scope', type: 'enum', choices: [...MEMORY_SCOPES], required: false, constraint: 'Target scope. Default: project when inside a project, else user. `project` resolves to the NEAREST ancestor `.crouter/` walking up from cwd — in a nested workspace that can be a parent’s store, not the dir you are standing in; pass --dir to pin the exact project directory. `profile` requires a selected profile (CRTR_PROFILE_ID) or an explicit --profile. `node` writes to the this-node store (`nodes/<CRTR_NODE_ID>/context/memory/`) — the nearest scope, seen only by this running node, requires a node context.' },
|
|
36
36
|
{ kind: 'flag', name: 'dir', type: 'string', required: false, constraint: 'Exact project directory to write under — targets `<dir>/.crouter/memory/` regardless of cwd or ancestor stores, scaffolding `.crouter/` there if absent. THE way to place a doc in a specific project’s store (e.g. another project in your profile’s purview) without cd’ing there, and the only way to target a dir shadowed by an ancestor store. Implies --scope project; rejects --scope user/profile.' },
|
|
37
37
|
{ kind: 'flag', name: 'profile', type: 'string', required: false, constraint: 'Profile id or name to write under, for --scope profile. Default: the process CRTR_PROFILE_ID (the node\u2019s selected profile). Resolved through the same profile lookup as `crtr profile show`; ignored for any other --scope.' },
|
|
38
38
|
{ kind: 'stdin', name: 'body', required: true, constraint: 'Document body (markdown, no frontmatter). Piped on stdin, or passed as the bare positional after <name>.' },
|
|
@@ -40,7 +40,7 @@ export const writeLeaf = defineLeaf({
|
|
|
40
40
|
output: [
|
|
41
41
|
{ name: 'name', type: 'string', required: true, constraint: 'The path-derived document name written.' },
|
|
42
42
|
{ name: 'kind', type: 'string', required: true, constraint: 'Kind recorded in frontmatter.' },
|
|
43
|
-
{ name: 'scope', type: 'string', required: true, constraint: 'Scope the document was written to: user, project, or
|
|
43
|
+
{ name: 'scope', type: 'string', required: true, constraint: 'Scope the document was written to: user, project, profile, or node.' },
|
|
44
44
|
{ name: 'path', type: 'string', required: true, constraint: 'Absolute path to the written document — edit this file directly for later body tweaks instead of re-running write.' },
|
|
45
45
|
{ name: 'created', type: 'boolean', required: true, constraint: 'true when a new document was created, false when an existing one was updated in place.' },
|
|
46
46
|
{ name: 'follow_up', type: 'string', required: true, constraint: 'Concrete next commands — read it back or list the inventory.' },
|