@north-light/crouter 0.3.179 → 0.3.181
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 +27 -2
- package/dist/api/client.js +41 -1
- package/dist/api/dto/broker.d.ts +32 -0
- package/dist/api/dto/crons.d.ts +17 -0
- package/dist/api/dto/human.d.ts +1 -7
- package/dist/api/dto/inbox.d.ts +71 -1
- package/dist/api/dto/inbox.js +9 -1
- package/dist/api/dto/memory.d.ts +17 -0
- package/dist/api/dto/memory.js +6 -0
- package/dist/api/dto/messages.d.ts +5 -0
- package/dist/api/dto/reviews.d.ts +8 -4
- package/dist/api/index.d.ts +1 -0
- package/dist/api/index.js +1 -0
- package/dist/api/routes.d.ts +5 -0
- package/dist/api/routes.js +8 -1
- package/dist/build-root.d.ts +7 -0
- package/dist/build-root.js +21 -0
- package/dist/builtin-memory/insights/init.md +48 -3
- package/dist/builtin-pi-packages/pi-crtr-extensions/__tests__/insights-active-init.test.ts +98 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/claude-plugin-commands.ts +7 -50
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/memory-slash-commands.ts +16 -1
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/pi-shell-runner.ts +34 -0
- package/dist/cli.js +1 -2
- package/dist/clients/attach/__tests__/context-message.test.js +5 -2
- package/dist/clients/attach/assets/README.md +7 -0
- package/dist/clients/attach/assets/whip-06.mp3 +0 -0
- package/dist/clients/attach/assets/whip-crack.mp3 +0 -0
- package/dist/clients/attach/assets/whip-snap.mp3 +0 -0
- package/dist/clients/attach/chrome/canvas-panels.d.ts +7 -1
- package/dist/clients/attach/chrome/canvas-panels.js +20 -3
- package/dist/clients/attach/chrome/review-wait.d.ts +6 -0
- package/dist/clients/attach/chrome/review-wait.js +22 -0
- package/dist/clients/attach/chrome/roster.js +23 -2
- package/dist/clients/attach/chrome/widgets.js +1 -1
- package/dist/clients/attach/input/controller.js +4 -3
- package/dist/clients/attach/overlays/mcp.js +3 -1
- package/dist/clients/attach/render/chat-view.js +1 -1
- package/dist/clients/attach/session/whip.d.ts +1 -0
- package/dist/clients/attach/session/whip.js +26 -0
- package/dist/clients/attach/slash/dispatch.js +2 -0
- package/dist/clients/attach/viewer.js +578 -573
- package/dist/clients/inbox/review/document-surface.d.ts +1 -1
- package/dist/clients/inbox/review/document-surface.js +4 -4
- package/dist/clients/inbox/review/launch.js +16 -4
- package/dist/clients/inbox/review/review-client.d.ts +9 -4
- package/dist/clients/inbox/review/review-client.js +3 -0
- package/dist/commands/cron.js +30 -8
- package/dist/commands/human/prompts.d.ts +7 -2
- package/dist/commands/human/prompts.js +15 -10
- package/dist/commands/human.js +1 -2
- package/dist/commands/memory/find.js +11 -8
- package/dist/commands/memory/read.js +111 -11
- package/dist/commands/memory/write.js +1 -1
- package/dist/commands/memory.js +1 -1
- package/dist/commands/pkg/market-manage.d.ts +13 -0
- package/dist/commands/pkg/market-manage.js +39 -33
- package/dist/commands/pkg/plugin-inspect.js +4 -3
- package/dist/commands/pkg/plugin-manage.js +12 -11
- package/dist/commands/surface/node/focus.js +1 -2
- package/dist/commands/sys/doctor.js +4 -4
- package/dist/commands/sys/setup-core.d.ts +14 -7
- package/dist/commands/sys/setup-core.js +66 -11
- package/dist/commands/sys/setup-wizard.js +2 -2
- package/dist/commands/sys/setup.js +1 -1
- package/dist/core/__tests__/cron-held-settlement.test.d.ts +1 -0
- package/dist/core/__tests__/cron-held-settlement.test.js +222 -0
- package/dist/core/__tests__/helpers/harness.js +1 -2
- package/dist/core/__tests__/phase4-review-store.test.js +1 -0
- package/dist/core/__tests__/serial/command-plugins.test.js +88 -1
- package/dist/core/__tests__/session-model.test.js +5 -3
- package/dist/core/bootstrap.d.ts +0 -4
- package/dist/core/bootstrap.js +1 -55
- package/dist/core/canvas/crons.d.ts +54 -2
- package/dist/core/canvas/crons.js +48 -4
- package/dist/core/canvas/db.js +23 -0
- package/dist/core/command-manifests/manifest.d.ts +11 -0
- package/dist/core/command-manifests/manifest.js +45 -4
- package/dist/core/command-manifests/schema.d.ts +1 -1
- package/dist/core/command-plugins/bundle.d.ts +1 -0
- package/dist/core/command-plugins/bundle.js +3 -3
- package/dist/core/command-plugins/discovery.d.ts +5 -2
- package/dist/core/command-plugins/discovery.js +5 -5
- package/dist/core/command-plugins/help-addenda.d.ts +12 -0
- package/dist/core/command-plugins/help-addenda.js +30 -0
- package/dist/core/command.js +25 -2
- package/dist/core/config.js +0 -1
- package/dist/core/human/convention.d.ts +0 -1
- package/dist/core/human/convention.js +0 -6
- package/dist/core/keybindings/inbox.d.ts +6 -8
- package/dist/core/keybindings/inbox.js +6 -15
- package/dist/core/keybindings/index.d.ts +1 -1
- package/dist/core/keybindings/index.js +1 -1
- package/dist/core/memory/doc-link-grammar.js +4 -1
- package/dist/core/memory-resolver.d.ts +28 -4
- package/dist/core/memory-resolver.js +51 -39
- package/dist/core/review/stage.js +1 -0
- package/dist/core/review/store.d.ts +5 -0
- package/dist/core/review/store.js +10 -0
- package/dist/core/review/types.d.ts +4 -0
- package/dist/core/runtime/broker/event-projection.d.ts +8 -1
- package/dist/core/runtime/broker/event-projection.js +25 -1
- package/dist/core/runtime/broker/frame-dispatch.d.ts +2 -0
- package/dist/core/runtime/broker/frame-dispatch.js +50 -8
- package/dist/core/runtime/broker/message-ledger.d.ts +53 -0
- package/dist/core/runtime/broker/message-ledger.js +143 -0
- package/dist/core/runtime/broker/rebind.js +14 -0
- package/dist/core/runtime/broker-protocol.d.ts +46 -1
- package/dist/core/runtime/broker.js +11 -2
- package/dist/core/runtime/interactive-deliver.d.ts +5 -2
- package/dist/core/runtime/interactive-deliver.js +6 -3
- package/dist/core/runtime/shell-expansion.d.ts +32 -0
- package/dist/core/runtime/shell-expansion.js +102 -0
- package/dist/core/session-model/session-state.d.ts +9 -4
- package/dist/core/session-model/session-state.js +5 -1
- package/dist/daemon/api/handlers/broker-ops.js +8 -0
- package/dist/daemon/api/handlers/crons.js +14 -1
- package/dist/daemon/api/handlers/inbox.js +346 -2
- package/dist/daemon/api/handlers/memory.d.ts +2 -0
- package/dist/daemon/api/handlers/memory.js +48 -0
- package/dist/daemon/api/handlers/messages.js +7 -1
- package/dist/daemon/api/handlers/reviews.js +7 -5
- package/dist/daemon/api/map.js +3 -0
- package/dist/daemon/api/server.js +2 -0
- package/dist/daemon/cron-run.js +71 -3
- package/dist/daemon/crtrd.js +3 -0
- package/dist/daemon/reconcilers/pending-review-submit.d.ts +7 -0
- package/dist/daemon/reconcilers/pending-review-submit.js +35 -0
- package/dist/daemon/review/companion.d.ts +8 -0
- package/dist/daemon/review/companion.js +35 -0
- package/dist/daemon/review/deliver.js +2 -1
- package/dist/daemon/review/finish.d.ts +29 -2
- package/dist/daemon/review/finish.js +75 -2
- package/dist/shared/generated-context.d.ts +3 -4
- package/dist/shared/generated-context.js +24 -6
- package/dist/types.d.ts +0 -1
- package/package.json +1 -1
- package/runtime.lock.json +2 -2
|
@@ -119,7 +119,7 @@ export declare class ReviewDocumentSurface implements SurfaceComponent {
|
|
|
119
119
|
* only after the close, because the host's restoring close disposes every
|
|
120
120
|
* notice raised over the closing surface — a notice raised first would die
|
|
121
121
|
* with the surface and the disposition would never paint. */
|
|
122
|
-
teardown(notice: string): void;
|
|
122
|
+
teardown(notice: string, opts?: SurfaceNoticeOptions): void;
|
|
123
123
|
/** Releases this surface's render cache and stops every projected repaint.
|
|
124
124
|
* This surface closes the source it was handed; that close stops its watcher.
|
|
125
125
|
* The companion pane deliberately survives: only {@link teardown} closes it. */
|
|
@@ -264,7 +264,7 @@ export class ReviewDocumentSurface {
|
|
|
264
264
|
const notice = this.tornDownNotice;
|
|
265
265
|
this.tornDownNotice = undefined;
|
|
266
266
|
if (notice !== undefined)
|
|
267
|
-
this.services.notice(notice);
|
|
267
|
+
this.services.notice(notice.text, notice.opts);
|
|
268
268
|
}
|
|
269
269
|
}
|
|
270
270
|
/** The status row's left segment: this document's state in one line. */
|
|
@@ -333,7 +333,7 @@ export class ReviewDocumentSurface {
|
|
|
333
333
|
* only after the close, because the host's restoring close disposes every
|
|
334
334
|
* notice raised over the closing surface — a notice raised first would die
|
|
335
335
|
* with the surface and the disposition would never paint. */
|
|
336
|
-
teardown(notice) {
|
|
336
|
+
teardown(notice, opts) {
|
|
337
337
|
if (this.tornDown)
|
|
338
338
|
return;
|
|
339
339
|
this.tornDown = true;
|
|
@@ -342,9 +342,9 @@ export class ReviewDocumentSurface {
|
|
|
342
342
|
this.paneId = undefined;
|
|
343
343
|
this.paneOrigin = undefined;
|
|
344
344
|
if (this.handle === undefined)
|
|
345
|
-
this.tornDownNotice = notice;
|
|
345
|
+
this.tornDownNotice = { text: notice, ...(opts === undefined ? {} : { opts }) };
|
|
346
346
|
this.handle?.close();
|
|
347
|
-
this.services.notice(notice);
|
|
347
|
+
this.services.notice(notice, opts);
|
|
348
348
|
}
|
|
349
349
|
/** Releases this surface's render cache and stops every projected repaint.
|
|
350
350
|
* This surface closes the source it was handed; that close stops its watcher.
|
|
@@ -19,7 +19,7 @@ export async function openReviewSurface(host, opts) {
|
|
|
19
19
|
const reviewClient = createReviewClient({
|
|
20
20
|
onTerminalConflict: (notice) => {
|
|
21
21
|
terminalConflict = true;
|
|
22
|
-
|
|
22
|
+
finish(notice);
|
|
23
23
|
},
|
|
24
24
|
});
|
|
25
25
|
const comments = reviewClient.comments(review.reviewId);
|
|
@@ -52,8 +52,17 @@ export async function openReviewSurface(host, opts) {
|
|
|
52
52
|
return;
|
|
53
53
|
submitting = true;
|
|
54
54
|
try {
|
|
55
|
-
const
|
|
56
|
-
|
|
55
|
+
const disposition = await reviewClient.submit(review.reviewId);
|
|
56
|
+
if (disposition.outcome === 'approved') {
|
|
57
|
+
finish(approvalNotice(disposition));
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
// The companion is mid-edit. The review is off the human's hands either
|
|
61
|
+
// way: the daemon approves the document as it reads once the companion
|
|
62
|
+
// stops, and the origin has already been told the approval is coming.
|
|
63
|
+
// Sticky, because the wait outlives a three-second notice — the review's
|
|
64
|
+
// cockpit row below the editor carries it from there.
|
|
65
|
+
finish('Review submitted before the companion finished — the agent resumes when the review completes.', { sticky: true });
|
|
57
66
|
}
|
|
58
67
|
catch (error) {
|
|
59
68
|
if (terminalConflict)
|
|
@@ -62,13 +71,16 @@ export async function openReviewSurface(host, opts) {
|
|
|
62
71
|
host.notice(`couldn't submit review: ${describeError(error)}`);
|
|
63
72
|
}
|
|
64
73
|
}
|
|
74
|
+
function finish(notice, opts) {
|
|
75
|
+
surface.teardown(notice, opts);
|
|
76
|
+
}
|
|
65
77
|
async function cancel() {
|
|
66
78
|
if (canceling)
|
|
67
79
|
return;
|
|
68
80
|
canceling = true;
|
|
69
81
|
try {
|
|
70
82
|
await reviewClient.cancel(review.reviewId, 'canceled from the review surface');
|
|
71
|
-
|
|
83
|
+
finish('Review canceled.');
|
|
72
84
|
}
|
|
73
85
|
catch (error) {
|
|
74
86
|
if (terminalConflict)
|
|
@@ -19,12 +19,17 @@ export interface ReviewClient {
|
|
|
19
19
|
file: string;
|
|
20
20
|
}): Promise<ResolvedReview>;
|
|
21
21
|
comments(reviewId: string): CommentAuthority;
|
|
22
|
-
submit(reviewId: string): Promise<
|
|
23
|
-
changed: boolean;
|
|
24
|
-
resultPath: string;
|
|
25
|
-
}>;
|
|
22
|
+
submit(reviewId: string): Promise<SubmitDisposition>;
|
|
26
23
|
cancel(reviewId: string, reason: string): Promise<void>;
|
|
27
24
|
}
|
|
25
|
+
/** An approval that landed, or one the daemon is holding for the companion. */
|
|
26
|
+
export type SubmitDisposition = {
|
|
27
|
+
outcome: 'approved';
|
|
28
|
+
changed: boolean;
|
|
29
|
+
resultPath: string;
|
|
30
|
+
} | {
|
|
31
|
+
outcome: 'awaiting_companion';
|
|
32
|
+
};
|
|
28
33
|
export interface ReviewClientOptions {
|
|
29
34
|
onTerminalConflict?: (notice: string) => void;
|
|
30
35
|
}
|
|
@@ -54,7 +54,10 @@ export function createReviewClient(opts = {}) {
|
|
|
54
54
|
},
|
|
55
55
|
async submit(reviewId) {
|
|
56
56
|
const submitted = await request(() => cliClient().submitReview(reviewId));
|
|
57
|
+
if (submitted.outcome === 'awaiting_companion')
|
|
58
|
+
return { outcome: 'awaiting_companion' };
|
|
57
59
|
return {
|
|
60
|
+
outcome: 'approved',
|
|
58
61
|
changed: requireChanged(submitted.review),
|
|
59
62
|
resultPath: requireResultPath(submitted.review),
|
|
60
63
|
};
|
package/dist/commands/cron.js
CHANGED
|
@@ -134,12 +134,12 @@ const addLeaf = defineLeaf({
|
|
|
134
134
|
{ kind: 'stdin', name: 'command', required: true, constraint: "Arbitrary bash the daemon runs at each fire. Pipe it from a single-quoted heredoc (`<<'EOF'`) to preserve literal bytes. Runs via `bash -c` in this cwd with $CRTR_CRON_ID, $CRTR_CRON_NAME, and $CRTR_RUN_ID in the env ($CRTR_ANCHOR too when anchored). The scheduler does not interpret it as a node action; a bash predicate can decide whether this fire acts, not arm a native predicate trigger." },
|
|
135
135
|
{ kind: 'flag', name: 'name', type: 'string', required: true, constraint: 'Agent-legible label — how the cron shows in `cron list`.' },
|
|
136
136
|
{ kind: 'flag', name: 'at', type: 'string', required: false, constraint: 'One-shot: run the stored bash once at <when> — a duration ("90s","1h30m"), a zoned ISO ("2026-06-07T09:00:00Z"), or a bare ISO ("2026-06-07T09:00", interpreted in --tz else host zone). Exactly one of --at / --every is required. The row is deleted after its run unless the run\'s disposition pauses it.' },
|
|
137
|
-
{ kind: 'flag', name: 'every', type: 'string', required: false, constraint: 'Recurring: run the stored bash at each cadence — a fixed interval ("6h" — first fire one interval from now), a 5-field cron ("0 9 * * *"), or an @alias ("@daily"). Each fire is another bash run. Minimum cadence 60s. Exactly one of --at / --every is required.' },
|
|
137
|
+
{ kind: 'flag', name: 'every', type: 'string', required: false, constraint: 'Recurring: run the stored bash at each cadence — a fixed interval ("6h" — first fire one interval from now), a 5-field cron ("0 9 * * *"), or an @alias ("@daily"). Each fire is another bash run. Minimum cadence 60s. Exactly one of --at / --every is required. For a gated cron (a command that exits 75 while ineligible), pick a LONG cadence: each natural slot is only the backstop re-check, and the daemon poke supplies the latency — "6h" plus poke beats "15m" polling on both latency and cost.' },
|
|
138
138
|
{ kind: 'flag', name: 'tz', type: 'string', required: false, constraint: 'IANA zone (e.g. "America/New_York") for a bare-ISO --at or a calendar --every. Defaults to the host zone.' },
|
|
139
139
|
{ kind: 'flag', name: 'on-output', type: 'enum', choices: ['silent', 'on-failure', 'always', 'on-change'], required: false, default: 'on-failure', constraint: 'What each run does with its output. silent: record in the run log, tell nobody — never escalate. on-failure (default): no success delivery; a failing run pauses the cron and spawns a node to deal with it. always: deliver stdout every run to --sink at --tier. on-change: deliver stdout only when it differs from the previous run\'s — the shape polling wants.' },
|
|
140
140
|
{ kind: 'flag', name: 'sink', type: 'string', required: false, constraint: 'Where always/on-change stdout deliveries go: "node:<id>" (an existing node\'s inbox — must exist now; gone/finalized at fire time is a failure), "spawn:<kind>" (each delivered output creates a self-finishing node with stdout as kickoff — a managed child while its creator exists, otherwise a terminal parentless root), or "human" (the humanloop inbox). Required by always/on-change, rejected otherwise.' },
|
|
141
141
|
{ kind: 'flag', name: 'tier', type: 'enum', choices: ['normal', 'urgent', 'critical'], required: false, default: 'urgent', constraint: 'Inbox priority of a node-sink delivery. urgent (default): steers the target mid-turn, or wakes it when dormant — a fire is news the node asked for on a schedule, so it should land now. normal: waits for the running turn to settle. critical: aborts the running turn outright.' },
|
|
142
|
-
{ kind: 'flag', name: 'expires', type: 'string', required: false, constraint: 'Clock bound (same grammar as --at): the row is deleted once this instant passes. The self-limiting half of a poll-until pattern.' },
|
|
142
|
+
{ kind: 'flag', name: 'expires', type: 'string', required: false, constraint: 'Clock bound (same grammar as --at): the row is deleted once this instant passes. The self-limiting half of a poll-until pattern. For a held one-shot (last run exited 75), expiry bounds the wait for eligibility — the row is deleted UNFIRED, never fired blind at expiry.' },
|
|
143
143
|
{ kind: 'flag', name: 'anchor', type: 'string', required: false, constraint: 'Couple the cron\'s lifetime to a node: the row is deleted when that node is deleted. Without an anchor it outlives every node; it ends through its one-shot run, --expires, explicit cancellation, or self-cancellation from its bash.' },
|
|
144
144
|
{ kind: 'flag', name: 'anchor-self', type: 'bool', required: false, constraint: 'Anchor to the calling node ($CRTR_NODE_ID). Errors outside a node.' },
|
|
145
145
|
{ kind: 'flag', name: 'cancel-on-wake', type: 'bool', required: false, constraint: 'Also delete the row when the anchor node revives: use only when the cron belongs to that current dormancy, not for standing work. Requires an anchor; at most one cancel-on-wake cron per anchor — arming a second replaces the anchor’s pending one.' },
|
|
@@ -287,7 +287,7 @@ const listLeaf = defineLeaf({
|
|
|
287
287
|
summary: 'every cron in scope, next-fire order',
|
|
288
288
|
params: [],
|
|
289
289
|
output: [
|
|
290
|
-
{ name: 'crons', type: 'object[]', required: true, constraint: 'One row per cron: cron_id, name, fire_at (next fire, UTC), recur (cadence display), state (active|paused), on_output, sink, expires_at, last_run (the most recent settled run: finished, exit_code, delivered — null if it never ran).' },
|
|
290
|
+
{ name: 'crons', type: 'object[]', required: true, constraint: 'One row per cron: cron_id, name, fire_at (next fire, UTC), recur (cadence display), state (active|paused), held (true while parked by an exit-75 gate — fires on daemon poke, else at fire_at), on_output, sink, expires_at, last_run (the most recent settled run: finished, exit_code, delivered — null if it never ran).' },
|
|
291
291
|
],
|
|
292
292
|
outputKind: 'object',
|
|
293
293
|
effects: [],
|
|
@@ -301,6 +301,7 @@ const listLeaf = defineLeaf({
|
|
|
301
301
|
fire_at: c.fire_at,
|
|
302
302
|
recur: cadenceDisplay(c.recur),
|
|
303
303
|
state: c.state,
|
|
304
|
+
held: c.held,
|
|
304
305
|
on_output: c.on_output,
|
|
305
306
|
sink: c.sink,
|
|
306
307
|
expires_at: c.expires_at,
|
|
@@ -321,7 +322,16 @@ const listLeaf = defineLeaf({
|
|
|
321
322
|
];
|
|
322
323
|
if (c.expires_at !== null)
|
|
323
324
|
bits.push(`expires ${c.expires_at}`);
|
|
324
|
-
|
|
325
|
+
// The state cell derives from both fields: an active held row must never
|
|
326
|
+
// present as paused (paused is user-owned and blocks firing; held fires
|
|
327
|
+
// the moment a poke lands), and the held fact is never hidden.
|
|
328
|
+
const marker = c.state === 'paused'
|
|
329
|
+
? c.held
|
|
330
|
+
? ' [PAUSED (held) — resume or cancel]'
|
|
331
|
+
: ' [PAUSED — resume or cancel]'
|
|
332
|
+
: c.held
|
|
333
|
+
? ' [HELD — last run exited 75; fires on daemon poke]'
|
|
334
|
+
: '';
|
|
325
335
|
return `- ${c.name} (${c.cron_id}) — ${bits.join(', ')}${marker}`;
|
|
326
336
|
});
|
|
327
337
|
return `${crons.length} cron(s):\n\n${lines.join('\n')}`;
|
|
@@ -355,11 +365,23 @@ const showLeaf = defineLeaf({
|
|
|
355
365
|
render: (r) => {
|
|
356
366
|
const c = r['cron'];
|
|
357
367
|
const runs = r['runs'];
|
|
358
|
-
const schedule = c.recur !== null ? cadenceDisplay(c.recur) : `one-shot at ${c.fire_at}`;
|
|
368
|
+
const schedule = c.recur !== null ? cadenceDisplay(c.recur) : c.held ? 'one-shot' : `one-shot at ${c.fire_at}`;
|
|
369
|
+
const tzPart = c.tz !== null ? ` (tz ${c.tz})` : '';
|
|
370
|
+
// A held row's next-fire is its BACKSTOP, not a promise: a daemon poke
|
|
371
|
+
// fires it earlier. A held one-shot has no clock slot at all — it waits
|
|
372
|
+
// for a poke until --expires deletes it (or forever, without one).
|
|
373
|
+
const scheduleLine = !c.held
|
|
374
|
+
? `- schedule: ${schedule}${tzPart} — next fire ${c.fire_at}`
|
|
375
|
+
: c.recur !== null
|
|
376
|
+
? `- schedule: ${schedule}${tzPart} — backstop ${c.fire_at} (held — fires early on daemon poke; last run exited 75)`
|
|
377
|
+
: c.expires_at !== null
|
|
378
|
+
? `- schedule: ${schedule}${tzPart} — backstop none — expires ${c.expires_at} (held — fires on daemon poke; deleted unfired at expiry)`
|
|
379
|
+
: `- schedule: ${schedule}${tzPart} — backstop none (held — fires only on daemon poke)`;
|
|
380
|
+
const stateTag = c.state === 'paused' ? (c.held ? ' — PAUSED (held)' : ' — PAUSED') : c.held ? ' — HELD' : '';
|
|
359
381
|
const lines = [
|
|
360
|
-
`# ${c.name} (${c.cron_id})${
|
|
382
|
+
`# ${c.name} (${c.cron_id})${stateTag}`,
|
|
361
383
|
'',
|
|
362
|
-
|
|
384
|
+
scheduleLine,
|
|
363
385
|
`- disposition: ${c.on_output}${c.sink !== null ? ` → ${c.sink} (tier ${c.tier})` : ''}`,
|
|
364
386
|
`- context: cwd ${c.cwd}${c.profile !== null ? `, profile ${c.profile}` : ''}, scope ${c.scope}${c.env_keys.length > 0 ? `, env ${c.env_keys.join(', ')}` : ''}`,
|
|
365
387
|
`- run: timeout ${c.run_timeout_s}s, overlap ${c.overlap}${c.run_state === 'running' ? ' — a run is IN FLIGHT now' : ''}`,
|
|
@@ -521,7 +543,7 @@ export function registerCron() {
|
|
|
521
543
|
help: {
|
|
522
544
|
name: 'cron',
|
|
523
545
|
summary: 'scheduled bash commands, daemon-run',
|
|
524
|
-
model: 'A cron stores and daemon-runs arbitrary bash at its clock. Use ordinary bash for ordinary work; choose node:<id> for stdout as inbox information to an existing node, an existing node\'s lifecycle fresh-revive action in the bash for a clean re-check without inbox output, or spawn:<kind> to route each output-producing fire to a fresh self-finishing node (a managed child while its creator exists, otherwise a terminal parentless root). Per-fire fresh agent work belongs on the existing spawn:<kind> sink, not an independent resident-root birth. A bash
|
|
546
|
+
model: 'A cron stores and daemon-runs arbitrary bash at its clock. Use ordinary bash for ordinary work; choose node:<id> for stdout as inbox information to an existing node, an existing node\'s lifecycle fresh-revive action in the bash for a clean re-check without inbox output, or spawn:<kind> to route each output-producing fire to a fresh self-finishing node (a managed child while its creator exists, otherwise a terminal parentless root). Per-fire fresh agent work belongs on the existing spawn:<kind> sink, not an independent resident-root birth. A gate is bash, at the top of the command. Exit 0 after deciding not to act and the occurrence is simply spent — right when the recurrence is your polling cadence. Exit 75 and the occurrence is OWED: the row is held, fires again the moment the daemon receives an eligibility poke from the host platform, and otherwise re-checks at its next natural slot (a held one-shot instead waits for a poke until --expires deletes it unfired). Any other nonzero exit is a real failure and escalates per --on-output. Use 75 for "not yet — retry when conditions change" (a device coming online); keep gate ERRORS nonzero so a broken probe is loud instead of silently parked. Beyond the exit-code contract, cron has no native predicate-trigger semantics. --at runs once and deletes the row unless the run\'s disposition pauses it; --every runs each cadence. Output disposition (--on-output) records silently, escalates failures, or routes stdout to its --sink. Anchor a cron to delete it with a node; --cancel-on-wake instead deletes it when the anchor wakes. An unanchored recurring cron ends through --expires, explicit cancellation, or self-cancellation from its bash. Every run lands in a bounded per-cron run log (`cron show`); cwd, env, and profile are snapshotted at arm time, and --scope controls who lists and cancels it.',
|
|
525
547
|
},
|
|
526
548
|
children: [addLeaf, listLeaf, showLeaf, runLeaf, pauseLeaf, resumeLeaf, cancelLeaf],
|
|
527
549
|
});
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import type { DeckSource } from '../../core/human/types.js';
|
|
2
|
-
/**
|
|
3
|
-
*
|
|
2
|
+
/** Provenance for crouter tickets. Ticket *identity* comes from the bridge node
|
|
3
|
+
* directory, never ticket data; `nodeId` carries the originating node so a
|
|
4
|
+
* consumer can relate a ticket back to whatever that node belongs to, and
|
|
5
|
+
* `sessionName` is a presentation label only. Review tickets already stamp
|
|
6
|
+
* `nodeId` the same way (`core/review/realize.ts`), so decks match them.
|
|
7
|
+
* `nodeId` is stamped even when the daemon is unreachable — losing the label is
|
|
8
|
+
* survivable, losing machine attribution is not. */
|
|
4
9
|
export declare function sourceStamp(): Promise<DeckSource>;
|
|
5
10
|
export declare const humanAsk: import("../../core/command.js").LeafDef;
|
|
6
11
|
export declare const humanNotify: import("../../core/command.js").LeafDef;
|
|
@@ -17,23 +17,27 @@ import { ticketDir } from '../../core/human/root.js';
|
|
|
17
17
|
import { submitDeck } from '../../core/human/tickets.js';
|
|
18
18
|
import { display } from '../../core/termrender/display.js';
|
|
19
19
|
import { BODY_PATH_CONTRACT, DECK_SCHEMA_HINT, resolveMaxPanes } from './shared.js';
|
|
20
|
-
import { inboxHint, inboxOpenInstruction } from '../../core/keybindings/index.js';
|
|
21
20
|
/** The asking node's id, or null when run from a bare shell (no parent to route to). */
|
|
22
21
|
function askingNode() {
|
|
23
22
|
return process.env['CRTR_NODE_ID'] ?? null;
|
|
24
23
|
}
|
|
25
|
-
/**
|
|
26
|
-
*
|
|
24
|
+
/** Provenance for crouter tickets. Ticket *identity* comes from the bridge node
|
|
25
|
+
* directory, never ticket data; `nodeId` carries the originating node so a
|
|
26
|
+
* consumer can relate a ticket back to whatever that node belongs to, and
|
|
27
|
+
* `sessionName` is a presentation label only. Review tickets already stamp
|
|
28
|
+
* `nodeId` the same way (`core/review/realize.ts`), so decks match them.
|
|
29
|
+
* `nodeId` is stamped even when the daemon is unreachable — losing the label is
|
|
30
|
+
* survivable, losing machine attribution is not. */
|
|
27
31
|
export async function sourceStamp() {
|
|
28
32
|
const nodeId = askingNode();
|
|
29
33
|
if (nodeId === null || nodeId === '')
|
|
30
34
|
return {};
|
|
31
35
|
try {
|
|
32
36
|
const sessionName = (await cliClient().getNode(nodeId)).name.trim();
|
|
33
|
-
return sessionName === '' ? {} : { sessionName };
|
|
37
|
+
return sessionName === '' ? { nodeId } : { nodeId, sessionName };
|
|
34
38
|
}
|
|
35
39
|
catch {
|
|
36
|
-
return {};
|
|
40
|
+
return { nodeId };
|
|
37
41
|
}
|
|
38
42
|
}
|
|
39
43
|
function requireAskTitle(title) {
|
|
@@ -100,11 +104,12 @@ function canonicalizeAskKinds(deck) {
|
|
|
100
104
|
})),
|
|
101
105
|
};
|
|
102
106
|
}
|
|
103
|
-
/** The non-blocking status peek returned by ask/review.
|
|
104
|
-
*
|
|
105
|
-
*
|
|
107
|
+
/** The non-blocking status peek returned by ask/review. How the human reaches
|
|
108
|
+
* their inbox is theirs, not the agent's — naming a shortcut here only teaches
|
|
109
|
+
* agents to repeat a screen-specific gesture back to a reader who may be on a
|
|
110
|
+
* different surface entirely. */
|
|
106
111
|
function queuedFollowUp() {
|
|
107
|
-
return
|
|
112
|
+
return 'Queued for the human. Their answer is pushed to your inbox and wakes you when they respond — never poll, verify it opened, or block.';
|
|
108
113
|
}
|
|
109
114
|
// ---------------------------------------------------------------------------
|
|
110
115
|
// ask
|
|
@@ -131,7 +136,7 @@ export const humanAsk = defineLeaf({
|
|
|
131
136
|
outputKind: 'object',
|
|
132
137
|
effects: [
|
|
133
138
|
'Creates a kind:"human" node under you and enqueues a decision deck in the human inbox.',
|
|
134
|
-
|
|
139
|
+
'Returns immediately; nothing opens on screen. The human answers from their inbox on their own time.',
|
|
135
140
|
],
|
|
136
141
|
},
|
|
137
142
|
run: async (input, context) => {
|
package/dist/commands/human.js
CHANGED
|
@@ -9,7 +9,6 @@ import { humanReviewBranch } from './human/review.js';
|
|
|
9
9
|
import { humanList, humanDeck, humanResolve, humanCancel } from './human/queue.js';
|
|
10
10
|
import { humanInbox } from './human/inbox.js';
|
|
11
11
|
import { humanDoc } from './human/doc.js';
|
|
12
|
-
import { inboxOpenInstruction } from '../core/keybindings/index.js';
|
|
13
12
|
export function registerHuman() {
|
|
14
13
|
return defineBranch({
|
|
15
14
|
name: 'human',
|
|
@@ -21,7 +20,7 @@ export function registerHuman() {
|
|
|
21
20
|
help: {
|
|
22
21
|
name: 'human',
|
|
23
22
|
summary: 'human-in-the-loop decisions, document review, and live display',
|
|
24
|
-
model: `Every body and displayed file is directive-flavored markdown rendered by termrender (panels, columns, trees, callouts, mermaid) — see \`crtr human doc check\` to validate a body before submitting it. ask creates a kind:'human' bridge node under you and returns instantly, never blocking. Nothing opens on screen — the ticket is queued in the human inbox; the human opens it
|
|
23
|
+
model: `Every body and displayed file is directive-flavored markdown rendered by termrender (panels, columns, trees, callouts, mermaid) — see \`crtr human doc check\` to validate a body before submitting it. ask creates a kind:'human' bridge node under you and returns instantly, never blocking. Nothing opens on screen — the ticket is queued in the human inbox; the human opens it on their own time, and their response is pushed to your inbox when they answer, so keep working (or just end your turn) and you'll be woken with it. ask covers everything from a yes/no sign-off gate (two options) to an open-ended judgment call. review is a branch: new queues a live document for the human to review in the attach viewer alongside a companion agent, while comment verbs operate on daemon-owned anchored comments; approval is pushed back as an approval fact, and a changed document instructs the origin to re-read it. notify enqueues a durable acknowledgement with no node; show is passive live display. inbox opens or toggles the queue on this screen; doc validates or renders a directive-flavored markdown body.`,
|
|
25
24
|
},
|
|
26
25
|
children: [
|
|
27
26
|
humanAsk,
|
|
@@ -3,7 +3,7 @@ import { usage } from '../../core/errors.js';
|
|
|
3
3
|
import { listAllMemoryDocs } from '../../core/memory-resolver.js';
|
|
4
4
|
import { parseSubstrateDoc } from '../../core/substrate/schema.js';
|
|
5
5
|
import { paginate } from '../../core/pagination.js';
|
|
6
|
-
import { MEMORY_KINDS } from './shared.js';
|
|
6
|
+
import { MEMORY_KINDS, MEMORY_SCOPES } from './shared.js';
|
|
7
7
|
function substrateUnit(d) {
|
|
8
8
|
return {
|
|
9
9
|
name: d.name,
|
|
@@ -16,14 +16,15 @@ function substrateUnit(d) {
|
|
|
16
16
|
};
|
|
17
17
|
}
|
|
18
18
|
/** The candidate set: every substrate memory document (native + plugin, supplied
|
|
19
|
-
* by listAllMemoryDocs), optionally narrowed to one kind
|
|
20
|
-
* EVERYTHING — it never applies
|
|
21
|
-
* §11#3)
|
|
19
|
+
* by listAllMemoryDocs), optionally narrowed to one kind and/or one scope.
|
|
20
|
+
* Within that explicit narrowing find searches EVERYTHING — it never applies
|
|
21
|
+
* gate or visibility-rung filtering (design §11#3); --kind/--scope are caller
|
|
22
|
+
* intent, not a visibility policy.
|
|
22
23
|
*
|
|
23
24
|
* Dedup by (scope, name): native and plugin docs of the same scope can collide
|
|
24
25
|
* on identity. The FIRST unit encountered for a given identity wins — native
|
|
25
26
|
* before plugin, which mirrors the memory resolver's own precedence. */
|
|
26
|
-
function candidates(kindFilter) {
|
|
27
|
+
function candidates(kindFilter, scopeFilter) {
|
|
27
28
|
const seen = new Set(); // "scope/name" → first wins
|
|
28
29
|
const add = (u) => {
|
|
29
30
|
const id = `${u.scope}/${u.name}`;
|
|
@@ -33,7 +34,7 @@ function candidates(kindFilter) {
|
|
|
33
34
|
units.push(u);
|
|
34
35
|
};
|
|
35
36
|
const units = [];
|
|
36
|
-
for (const doc of listAllMemoryDocs()) {
|
|
37
|
+
for (const doc of listAllMemoryDocs(scopeFilter)) {
|
|
37
38
|
const sub = parseSubstrateDoc(doc);
|
|
38
39
|
if (sub === null)
|
|
39
40
|
continue;
|
|
@@ -46,13 +47,14 @@ function candidates(kindFilter) {
|
|
|
46
47
|
export const findLeaf = defineLeaf({
|
|
47
48
|
name: 'find',
|
|
48
49
|
description: 'relevance search across memory documents',
|
|
49
|
-
whenToUse: 'you
|
|
50
|
+
whenToUse: 'you already have a topic or keyword in mind and need to find which document covers it — ranks documents by relevance, weighted over name, the read-routing line, and short-form (add --body to also weigh body text). Searches every scope by default regardless of any visibility gate; --scope narrows to one. Reach for `crtr memory list` instead when you want the whole inventory rather than a topic match, and --grep when you need an exact regex or literal-string match across document bodies.',
|
|
50
51
|
help: {
|
|
51
52
|
name: 'memory find',
|
|
52
53
|
summary: 'bounded relevance search across memory documents, weighted over name/routing-line/short-form (and body with --body)',
|
|
53
54
|
params: [
|
|
54
55
|
{ kind: 'positional', name: 'query', required: true, constraint: 'With ranked search (default): whitespace-separated terms, matched case-insensitively and weighted over name, the read-routing line, and short-form (plus body with --body); documents matching more/stronger fields rank higher. By default returns at most 10 hits scoring at least half the top score. With --grep: an ECMAScript regex applied to each document body line.' },
|
|
55
56
|
{ kind: 'flag', name: 'kind', type: 'enum', choices: [...MEMORY_KINDS], required: false, constraint: 'Filter to a single kind. Default: all kinds.' },
|
|
57
|
+
{ kind: 'flag', name: 'scope', type: 'enum', choices: [...MEMORY_SCOPES], required: false, constraint: 'Filter to a single scope. Default: all resolved scopes, builtin included.' },
|
|
56
58
|
{ kind: 'flag', name: 'grep', type: 'bool', required: false, constraint: 'Treat the query as an ECMAScript regex and match it against document bodies, instead of weighted relevance ranking. Mutually exclusive with --body.' },
|
|
57
59
|
{ kind: 'flag', name: 'body', type: 'bool', required: false, constraint: 'Also weigh document body text in the relevance ranking (in addition to name/when/why/short-form). Ignored under --grep, which always scans bodies.' },
|
|
58
60
|
{ kind: 'flag', name: 'limit', type: 'int', required: false, constraint: 'Maximum hits to return per page after relevance filtering. Default 10, hard max 100.' },
|
|
@@ -73,6 +75,7 @@ export const findLeaf = defineLeaf({
|
|
|
73
75
|
run: async (input) => {
|
|
74
76
|
const query = input['query'];
|
|
75
77
|
const kindFilter = input['kind'];
|
|
78
|
+
const scopeFilter = input['scope'];
|
|
76
79
|
const grep = input['grep'];
|
|
77
80
|
const weighBody = input['body'];
|
|
78
81
|
const limitArg = input['limit'];
|
|
@@ -90,7 +93,7 @@ export const findLeaf = defineLeaf({
|
|
|
90
93
|
if (minScore !== undefined && minScore < 1)
|
|
91
94
|
throw usage('--min-score must be at least 1.');
|
|
92
95
|
const limit = limitArg ?? 10;
|
|
93
|
-
const units = candidates(kindFilter);
|
|
96
|
+
const units = candidates(kindFilter, scopeFilter);
|
|
94
97
|
// --- grep mode: regex over every unit's body, one row per matching line ---
|
|
95
98
|
if (grep) {
|
|
96
99
|
let regex;
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { defineLeaf } from '../../core/command.js';
|
|
2
2
|
import { CrtrError, notFound } from '../../core/errors.js';
|
|
3
|
-
import { createMemoryDocSnapshot, resolveMemoryDoc, resolveMemoryDocs } from '../../core/memory-resolver.js';
|
|
4
|
-
import { effectiveDocKind } from '../../core/substrate/schema.js';
|
|
5
|
-
import { displayName } from '../../core/substrate/ceiling.js';
|
|
3
|
+
import { createMemoryDocSnapshot, listAllMemoryDocs, resolveMemoryDoc, resolveMemoryDocs } from '../../core/memory-resolver.js';
|
|
4
|
+
import { effectiveDocKind, parseSubstrateDoc, previewLine } from '../../core/substrate/schema.js';
|
|
5
|
+
import { displayName, indexDirOf, isIndexName } from '../../core/substrate/ceiling.js';
|
|
6
6
|
import { interpolateNodePaths } from '../../core/canvas/paths.js';
|
|
7
7
|
import { readText } from '../../core/fs-utils.js';
|
|
8
8
|
import { parseFrontmatterGeneric } from '../../core/frontmatter.js';
|
|
9
9
|
import { docLinkNames } from '../../core/memory/doc-link-grammar.js';
|
|
10
|
+
import { expandShellBlocks, hasShellBlocks, makeNodeShellRunner } from '../../core/runtime/shell-expansion.js';
|
|
10
11
|
import { MEMORY_KINDS } from './shared.js';
|
|
11
12
|
export { createMemoryDocSnapshot, resolveMemoryDocs };
|
|
12
13
|
/** Load the body at an already-resolved memory path with the same frontmatter
|
|
@@ -19,6 +20,89 @@ export function readMemoryDocContent(path, includeFrontmatter = false) {
|
|
|
19
20
|
const nodeId = process.env['CRTR_NODE_ID'];
|
|
20
21
|
return nodeId ? interpolateNodePaths(content, nodeId) : content;
|
|
21
22
|
}
|
|
23
|
+
/** The routing list appended to a directory-INDEX read: one line per document
|
|
24
|
+
* the INDEX gates, `[[name]]: <when-and-why-to-read>`. Membership is the
|
|
25
|
+
* directory's docs unioned with the body's resolvable `[[links]]`, deduped —
|
|
26
|
+
* a nested INDEX stands in for its whole subtree with a single line, while a
|
|
27
|
+
* subdirectory without an INDEX is transparent and its leaves render directly
|
|
28
|
+
* (mirroring how boot ceilings govern to the nearest INDEX). Every member
|
|
29
|
+
* renders regardless of its own visibility rung: rungs price unsolicited
|
|
30
|
+
* surfaces, and a read is a deliberate act. The root INDEX is excluded — the
|
|
31
|
+
* top-level namespace merges across stores, so its "directory" would
|
|
32
|
+
* enumerate the entire corpus. */
|
|
33
|
+
function indexRoutes(doc) {
|
|
34
|
+
// A dir INDEX resolves two ways: the explicit `dir/INDEX` name, or the bare
|
|
35
|
+
// dir name (which carries the dir as its identity while the path is the
|
|
36
|
+
// INDEX.md). Detect from the path so both shapes route identically.
|
|
37
|
+
if (!doc.path.endsWith('/INDEX.md'))
|
|
38
|
+
return [];
|
|
39
|
+
const dir = isIndexName(doc.name) ? indexDirOf(doc.name) : doc.name;
|
|
40
|
+
if (dir === '')
|
|
41
|
+
return [];
|
|
42
|
+
const canonicalName = `${dir}/INDEX`;
|
|
43
|
+
let corpus;
|
|
44
|
+
try {
|
|
45
|
+
corpus = listAllMemoryDocs(undefined, true);
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
return [];
|
|
49
|
+
}
|
|
50
|
+
// First occurrence wins: listAllMemoryDocs emits in resolver-precedence
|
|
51
|
+
// order, so the doc that renders a name's line is the doc a read of that
|
|
52
|
+
// name would actually return.
|
|
53
|
+
const byName = new Map();
|
|
54
|
+
for (const d of corpus)
|
|
55
|
+
if (!byName.has(d.name))
|
|
56
|
+
byName.set(d.name, d);
|
|
57
|
+
const prefix = dir + '/';
|
|
58
|
+
const members = new Map();
|
|
59
|
+
for (const [name, d] of byName) {
|
|
60
|
+
if (!name.startsWith(prefix) || name === canonicalName)
|
|
61
|
+
continue;
|
|
62
|
+
// The shallowest INDEX between the gating dir and this doc stands in for
|
|
63
|
+
// it (an INDEX finds itself here, keeping its own single line).
|
|
64
|
+
const segs = name.slice(prefix.length).split('/');
|
|
65
|
+
let member = d;
|
|
66
|
+
for (let i = 1; i < segs.length; i++) {
|
|
67
|
+
const gate = byName.get(`${prefix}${segs.slice(0, i).join('/')}/INDEX`);
|
|
68
|
+
if (gate !== undefined) {
|
|
69
|
+
member = gate;
|
|
70
|
+
break;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
if (!members.has(member.name))
|
|
74
|
+
members.set(member.name, member);
|
|
75
|
+
}
|
|
76
|
+
// No members means this INDEX gates no cluster in the corpus namespace — a
|
|
77
|
+
// store-root front door whose explicit frontmatter name dodges the root
|
|
78
|
+
// check above lands here. Fall back to the plain links field.
|
|
79
|
+
if (members.size === 0)
|
|
80
|
+
return [];
|
|
81
|
+
// Union the body's authored links so one list carries everything this INDEX
|
|
82
|
+
// routes to — the routes list subsumes `links` on an INDEX read.
|
|
83
|
+
for (const linkName of docLinkNames(doc.body)) {
|
|
84
|
+
let linked;
|
|
85
|
+
try {
|
|
86
|
+
linked = resolveMemoryDoc(linkName);
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
continue; // dangling link — lint's finding, not read's
|
|
90
|
+
}
|
|
91
|
+
if (displayName(linked.name) !== linkName || linked.name === canonicalName || linked.name === doc.name)
|
|
92
|
+
continue;
|
|
93
|
+
if (!members.has(linked.name))
|
|
94
|
+
members.set(linked.name, linked);
|
|
95
|
+
}
|
|
96
|
+
return [...members.values()]
|
|
97
|
+
.map((m) => {
|
|
98
|
+
const label = displayName(m.name);
|
|
99
|
+
const sub = parseSubstrateDoc(m);
|
|
100
|
+
const line = sub === null ? '' : previewLine(sub);
|
|
101
|
+
return { label, line: line === '' ? `[[${label}]]` : `[[${label}]]: ${line}` };
|
|
102
|
+
})
|
|
103
|
+
.sort((a, b) => a.label.localeCompare(b.label))
|
|
104
|
+
.map((e) => e.line);
|
|
105
|
+
}
|
|
22
106
|
export const readLeaf = defineLeaf({
|
|
23
107
|
name: 'read',
|
|
24
108
|
description: 'load a memory document body by name',
|
|
@@ -36,12 +120,15 @@ export const readLeaf = defineLeaf({
|
|
|
36
120
|
{ name: 'kind', type: 'string', required: true, constraint: 'Resolved kind: knowledge or preference.' },
|
|
37
121
|
{ name: 'scope', type: 'string', required: true, constraint: 'Scope the document was resolved from: node, project, profile, user, or builtin.' },
|
|
38
122
|
{ name: 'path', type: 'string', required: true, constraint: 'Absolute path to the document on disk — direct edits here are for a small body-only tweak; any routing/frontmatter/visibility change goes through `crtr memory write`.' },
|
|
39
|
-
{ name: 'content', type: 'string', required: true, constraint: 'Document body. Frontmatter stripped unless --frontmatter is set.' },
|
|
40
|
-
{ name: 'links', type: 'string[]', required: false, constraint: 'Canonical names this document links to via `[[name]]` that resolve in the current corpus — further reading, loaded only on demand with `crtr memory read <name>`. Omitted when the body carries no resolvable links.' },
|
|
123
|
+
{ name: 'content', type: 'string', required: true, constraint: 'Document body. Frontmatter stripped unless --frontmatter is set. A document may embed shell as `!`cmd`` or a ```! fenced block; each runs once per read, in the current working directory, and is replaced by its output. Read `path` off disk when you need the literal unexecuted text.' },
|
|
124
|
+
{ name: 'links', type: 'string[]', required: false, constraint: 'Canonical names this document links to via `[[name]]` that resolve in the current corpus — further reading, loaded only on demand with `crtr memory read <name>`. Omitted when the body carries no resolvable links, and subsumed by `routes` on a directory-INDEX read.' },
|
|
125
|
+
{ name: 'routes', type: 'string[]', required: false, constraint: 'Present only when the document is a directory INDEX: one routing line per document the INDEX gates — the directory’s members plus the body’s resolvable links, a nested INDEX standing in for its subtree with a single line. Follow one with `crtr memory read <name>` when the task in front of you matches its line.' },
|
|
41
126
|
{ name: 'follow_up', type: 'string', required: true, constraint: 'Hints at variant flags or next commands.' },
|
|
42
127
|
],
|
|
43
128
|
outputKind: 'object',
|
|
44
|
-
effects: [
|
|
129
|
+
effects: [
|
|
130
|
+
'Reads the document. A document that embeds shell (`!`cmd`` or a ```! block) runs those commands once, in the current working directory.',
|
|
131
|
+
],
|
|
45
132
|
},
|
|
46
133
|
run: async (input) => {
|
|
47
134
|
const nameRaw = input['name'];
|
|
@@ -63,11 +150,21 @@ export const readLeaf = defineLeaf({
|
|
|
63
150
|
}
|
|
64
151
|
if (doc !== undefined) {
|
|
65
152
|
const kind = effectiveDocKind(doc);
|
|
66
|
-
const
|
|
153
|
+
const raw = readMemoryDocContent(doc.path, includeFrontmatter);
|
|
154
|
+
// A read is an explicit act by an agent that can already run shell, so a
|
|
155
|
+
// document's embedded commands run here and their output lands inline —
|
|
156
|
+
// the same contract as invoking the document as a slash command. Nothing
|
|
157
|
+
// executes on the auto-load path, which never reaches this leaf.
|
|
158
|
+
const content = hasShellBlocks(raw)
|
|
159
|
+
? await expandShellBlocks(raw, makeNodeShellRunner({ cwd: process.cwd() }))
|
|
160
|
+
: raw;
|
|
67
161
|
// `[[name]]` doc links are pointers, never transclusion: surface which
|
|
68
162
|
// linked names actually resolve so the reader can follow one when the
|
|
69
163
|
// task needs that depth, without ever auto-loading a linked body.
|
|
70
|
-
|
|
164
|
+
// A directory INDEX is a gate: its read appends one routing line per doc
|
|
165
|
+
// it gates, subsuming the links field. Non-INDEX reads keep plain links.
|
|
166
|
+
const routes = indexRoutes(doc);
|
|
167
|
+
const links = routes.length > 0 ? [] : docLinkNames(doc.body).filter((linkName) => {
|
|
71
168
|
try {
|
|
72
169
|
// `resolveMemoryDoc` permits leaf-name fallback for interactive reads;
|
|
73
170
|
// a stored graph edge does not. Compare the resolved canonical name
|
|
@@ -84,10 +181,13 @@ export const readLeaf = defineLeaf({
|
|
|
84
181
|
scope: doc.scope,
|
|
85
182
|
path: doc.path,
|
|
86
183
|
content,
|
|
184
|
+
...(routes.length > 0 ? { routes } : {}),
|
|
87
185
|
...(links.length > 0 ? { links } : {}),
|
|
88
|
-
follow_up: (
|
|
89
|
-
? '
|
|
90
|
-
:
|
|
186
|
+
follow_up: (routes.length > 0
|
|
187
|
+
? 'This INDEX gates the docs listed in `routes` — follow one with `crtr memory read <name>` when the task in front of you matches its line. '
|
|
188
|
+
: links.length > 0
|
|
189
|
+
? 'The `[[name]]` links in the body are further reading — follow one with `crtr memory read <name>` only when the task needs that depth. '
|
|
190
|
+
: '') +
|
|
91
191
|
'Use --frontmatter on this same command to inspect the YAML frontmatter, or edit `path` directly for a body-only tweak. Browse the inventory with `crtr memory list`.',
|
|
92
192
|
};
|
|
93
193
|
}
|
|
@@ -24,7 +24,7 @@ export const writeLeaf = defineLeaf({
|
|
|
24
24
|
'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' +
|
|
25
25
|
'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 — the same identifier `crtr memory read` takes (a directory INDEX is linked by its bare directory name). 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, and `crtr memory read` lists a doc’s resolvable links alongside its body. There is no alias or label form.\n\n' +
|
|
26
26
|
'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. Save reference leaves at `none` visibility on both axes — the link from the main doc is how they are found, so any higher rung just double-charges every boot or read for depth the graph already routes. Keep every doc as short as its job allows; `crtr memory lint` caps body length by rung and its findings carry the split guidance.\n\n' +
|
|
27
|
-
'A directory INDEX earns existence only where the aggregate view beats the sum of per-file rungs. Two shapes pass: synthesis — an operating guide or the cluster’s mechanics, ordering, and relationships, content no single child can carry — and
|
|
27
|
+
'A directory INDEX earns existence only where the aggregate view beats the sum of per-file rungs. Aggregated child routing is automatic: `crtr memory read` of a directory INDEX appends every member’s routing line (`[[name]]: <when-and-why-to-read>`), so never author that list in the body. Two shapes pass: synthesis — an operating guide or the cluster’s mechanics, ordering, conditions, and relationships, content no single child can carry — and a pure gate — one preview line covering a cluster with one shared routing condition, the body adding nothing the automatic list does not. Otherwise skip the INDEX and let each doc’s own rung route it: frontmatter routing lines are self-maintaining, while an INDEX body describing its children is a hand-maintained copy that drifts. Never write an INDEX body that restates child names, paraphrases their routing lines, fronts a small directory of sharply named docs, or summarizes children a reader should open.\n\n' +
|
|
28
28
|
'Find before write. Prefer slightly expanding an existing document, 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 automatic on create and preserved on update. Run `crtr memory lint` after authoring.\n\n' +
|
|
29
29
|
'--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.',
|
|
30
30
|
params: [
|
package/dist/commands/memory.js
CHANGED
|
@@ -19,7 +19,7 @@ export function registerMemory() {
|
|
|
19
19
|
help: {
|
|
20
20
|
name: 'memory',
|
|
21
21
|
summary: 'list, read, search, and write memory documents — knowledge and preferences',
|
|
22
|
-
model: 'Documents have path-derived identities and resolve across layered scopes in precedence order: node > project stack > profile > user > builtin.
|
|
22
|
+
model: 'Documents have path-derived identities and resolve across layered scopes in precedence order: node > project stack > profile > user > builtin. Browse the inventory with `list` to see what is stored; address a document directly with `read`/`write` once you know its name. A directory may carry an `INDEX.md` with the same frontmatter schema as any doc — it renders as one boot-catalog entry whose system-prompt rung caps that subtree. File context is separate and explicit: `applies-to: "."` opens project context with the workspace; other globs fire after matching reads.',
|
|
23
23
|
},
|
|
24
24
|
children: [listLeaf, readLeaf, findLeaf, writeLeaf, deleteLeaf, originLeaf, lintLeaf],
|
|
25
25
|
});
|
|
@@ -1,3 +1,16 @@
|
|
|
1
|
+
import type { Scope } from '../../types.js';
|
|
2
|
+
/** Clone a marketplace repo into `scope` and register it in that scope's
|
|
3
|
+
* config. Shared by the `pkg market add` leaf and `crtr sys setup`, which
|
|
4
|
+
* offers the official marketplace as a checked-by-default entry. */
|
|
5
|
+
export declare function addMarketplace(opts: {
|
|
6
|
+
url: string;
|
|
7
|
+
ref?: string;
|
|
8
|
+
scope: Scope;
|
|
9
|
+
}): {
|
|
10
|
+
name: string;
|
|
11
|
+
scope: Scope;
|
|
12
|
+
path: string;
|
|
13
|
+
};
|
|
1
14
|
export declare const marketAdd: import("../../core/command.js").LeafDef;
|
|
2
15
|
export declare const marketRemove: import("../../core/command.js").LeafDef;
|
|
3
16
|
export declare const marketUpdate: import("../../core/command.js").LeafDef;
|