@north-light/crouter 0.3.231 → 0.3.233
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api/client.d.ts +3 -1
- package/dist/api/client.js +4 -0
- package/dist/api/dto/canvas.d.ts +10 -0
- package/dist/api/dto/common.d.ts +1 -1
- package/dist/api/dto/health.d.ts +2 -1
- package/dist/api/dto/lifecycle.d.ts +3 -4
- package/dist/api/dto/messages.d.ts +5 -4
- package/dist/api/dto/nodes.d.ts +2 -0
- package/dist/api/dto/profiles.d.ts +5 -0
- package/dist/api/routes.d.ts +1 -0
- package/dist/api/routes.js +1 -0
- package/dist/builtin-memory/00-runtime-base/00-authoring.md +8 -0
- package/dist/builtin-memory/00-runtime-base/01-escalation.md +1 -1
- package/dist/builtin-memory/01-spine/00-has-manager.md +1 -1
- package/dist/builtin-memory/02-turn-lifecycle/00-ending-a-turn.md +5 -0
- package/dist/builtin-memory/02-turn-lifecycle/02-resident.md +5 -3
- package/dist/builtin-memory/04-orchestration-kernel.md +2 -2
- package/dist/builtin-memory/insights/capture.md +3 -2
- package/dist/builtin-memory/internal/memory-loading.md +4 -3
- package/dist/builtin-pi-packages/pi-crtr-extensions/README.md +6 -1
- package/dist/clients/attach/render/diagram.js +13 -5
- package/dist/clients/attach/render/page-block.d.ts +0 -1
- package/dist/clients/attach/render/page-block.js +4 -57
- package/dist/clients/attach/viewer.js +570 -563
- package/dist/clients/inbox/__tests__/integration/inbox-controller.test.js +9 -0
- package/dist/clients/inbox/__tests__/integration/mount-panel.test.js +62 -1
- package/dist/clients/inbox/controller.d.ts +10 -0
- package/dist/clients/inbox/controller.js +56 -12
- package/dist/clients/inbox/tui/input.js +38 -10
- package/dist/clients/inbox/tui/page-body.d.ts +10 -0
- package/dist/clients/inbox/tui/page-body.js +66 -0
- package/dist/clients/inbox/tui/panel.js +6 -4
- package/dist/clients/inbox/tui/render.js +89 -17
- package/dist/clients/inbox/tui/types.d.ts +4 -4
- package/dist/commands/__tests__/human.test.js +18 -3
- package/dist/commands/__tests__/node-message.test.js +3 -3
- package/dist/commands/api-client.js +1 -7
- package/dist/commands/canvas-config.js +6 -14
- package/dist/commands/canvas-use.js +4 -6
- package/dist/commands/cron.js +16 -22
- package/dist/commands/human/prompts.d.ts +1 -1
- package/dist/commands/human/prompts.js +118 -112
- package/dist/commands/human/request.js +13 -14
- package/dist/commands/human/review.js +3 -4
- package/dist/commands/human/shared.d.ts +6 -0
- package/dist/commands/human/shared.js +43 -5
- package/dist/commands/human.js +1 -1
- package/dist/commands/memory/delete.js +4 -6
- package/dist/commands/memory/edit.js +0 -4
- package/dist/commands/memory/move.js +3 -5
- package/dist/commands/memory/shared.d.ts +1 -1
- package/dist/commands/memory/shared.js +11 -7
- package/dist/commands/memory/write.js +60 -29
- package/dist/commands/memory.js +1 -1
- package/dist/commands/node/bash.js +6 -9
- package/dist/commands/node/create.js +89 -22
- package/dist/commands/node/inspect.js +3 -3
- package/dist/commands/node/lifecycle.js +30 -27
- package/dist/commands/node/message.js +24 -45
- package/dist/commands/node/subscription.js +6 -15
- package/dist/commands/node/wait.js +2 -3
- package/dist/commands/node-lifecycle-revive.js +1 -12
- package/dist/commands/pkg/browse/actions.js +2 -3
- package/dist/commands/pkg/market-manage.js +2 -5
- package/dist/commands/pkg/plugin-manage.js +7 -8
- package/dist/commands/profile/default.js +5 -5
- package/dist/commands/profile/delete.js +1 -1
- package/dist/commands/profile/env.js +9 -15
- package/dist/commands/profile/kind.js +3 -7
- package/dist/commands/profile/meta.js +3 -5
- package/dist/commands/profile/new.js +0 -6
- package/dist/commands/profile/pause.js +4 -8
- package/dist/commands/profile/project.js +5 -9
- package/dist/commands/profile/rename.js +3 -7
- package/dist/commands/profile/show.js +3 -3
- package/dist/commands/profile.js +4 -3
- package/dist/commands/surface-tmux-spread.js +1 -3
- package/dist/commands/sys/config.js +3 -4
- package/dist/commands/sys/support/prepare.js +8 -4
- package/dist/commands/sys/support/submit.js +2 -3
- package/dist/commands/sys/sync-deps.js +1 -9
- package/dist/commands/sys/sync-project-guidance.js +1 -7
- package/dist/commands/sys/sync-skills.js +1 -11
- package/dist/core/__tests__/cron-node-sink-parked-root.test.d.ts +1 -0
- package/dist/core/__tests__/cron-node-sink-parked-root.test.js +147 -0
- package/dist/core/__tests__/history-inbox.test.js +11 -1
- package/dist/core/__tests__/human-deliver.test.js +2 -1
- package/dist/core/__tests__/integration/command-plugins.test.js +0 -1
- package/dist/core/__tests__/integration/deferred-no-wake.test.js +0 -1
- package/dist/core/__tests__/lifecycle.test.js +30 -2
- package/dist/core/__tests__/revive-parked-fresh.test.d.ts +1 -0
- package/dist/core/__tests__/revive-parked-fresh.test.js +109 -0
- package/dist/core/__tests__/seam/dormancy-release.test.js +32 -5
- package/dist/core/canvas/attention.d.ts +2 -0
- package/dist/core/canvas/attention.js +25 -18
- package/dist/core/canvas/extensions.d.ts +1 -1
- package/dist/core/canvas/extensions.js +7 -1
- package/dist/core/canvas/history.js +20 -2
- package/dist/core/canvas/types.d.ts +1 -1
- package/dist/core/command.js +33 -8
- package/dist/core/help.d.ts +28 -2
- package/dist/core/help.js +46 -10
- package/dist/core/human/__tests__/page-html-markdown.test.d.ts +1 -0
- package/dist/core/human/__tests__/page-html-markdown.test.js +48 -0
- package/dist/core/human/component-docs.js +4 -4
- package/dist/core/human/page-html-markdown.d.ts +8 -0
- package/dist/core/human/page-html-markdown.js +260 -0
- package/dist/core/memory/lint.d.ts +15 -0
- package/dist/core/memory/lint.js +150 -90
- package/dist/core/profiles/__tests__/fuzzy-match.test.d.ts +1 -0
- package/dist/core/profiles/__tests__/fuzzy-match.test.js +51 -0
- package/dist/core/profiles/fuzzy-match.d.ts +19 -0
- package/dist/core/profiles/fuzzy-match.js +92 -0
- package/dist/core/profiles/manifest.d.ts +14 -7
- package/dist/core/profiles/manifest.js +62 -12
- package/dist/core/profiles/select.d.ts +3 -1
- package/dist/core/profiles/select.js +5 -3
- package/dist/core/profiles/state-block.js +4 -3
- package/dist/core/runtime/boot-root.d.ts +3 -2
- package/dist/core/runtime/canvas-extensions.d.ts +7 -1
- package/dist/core/runtime/canvas-extensions.js +8 -1
- package/dist/core/runtime/lifecycle.d.ts +11 -2
- package/dist/core/runtime/lifecycle.js +15 -2
- package/dist/core/runtime/model-selection.d.ts +4 -0
- package/dist/core/runtime/model-selection.js +5 -0
- package/dist/core/runtime/nodes.js +5 -0
- package/dist/core/runtime/reopen.d.ts +6 -0
- package/dist/core/runtime/reopen.js +12 -1
- package/dist/core/runtime/revive.d.ts +6 -0
- package/dist/core/runtime/revive.js +22 -2
- package/dist/core/runtime/spawn.d.ts +5 -2
- package/dist/core/runtime/spawn.js +18 -32
- package/dist/core/runtime/structured-output.d.ts +6 -0
- package/dist/core/runtime/structured-output.js +6 -0
- package/dist/core/substrate/__tests__/surface-match-command.test.d.ts +1 -0
- package/dist/core/substrate/__tests__/surface-match-command.test.js +89 -0
- package/dist/core/substrate/__tests__/surface-match-pre-command.test.d.ts +1 -0
- package/dist/core/substrate/__tests__/surface-match-pre-command.test.js +92 -0
- package/dist/core/substrate/frontmatter-validation.js +1 -1
- package/dist/core/substrate/injected-store.d.ts +6 -0
- package/dist/core/substrate/injected-store.js +24 -0
- package/dist/core/substrate/on-read.d.ts +17 -1
- package/dist/core/substrate/on-read.js +38 -3
- package/dist/core/substrate/schema.d.ts +3 -3
- package/dist/core/substrate/schema.js +4 -4
- package/dist/core/substrate/surface-match.d.ts +19 -0
- package/dist/core/substrate/surface-match.js +224 -12
- package/dist/core/termrender/version.d.ts +1 -1
- package/dist/core/termrender/version.js +1 -1
- package/dist/core/user-settings.js +1 -1
- package/dist/daemon/api/__tests__/broker-settle-park.test.d.ts +1 -0
- package/dist/daemon/api/__tests__/broker-settle-park.test.js +102 -0
- package/dist/daemon/api/__tests__/canvas-snapshot-fields.test.d.ts +1 -0
- package/dist/daemon/api/__tests__/canvas-snapshot-fields.test.js +188 -0
- package/dist/daemon/api/__tests__/node-create-description.test.d.ts +1 -0
- package/dist/daemon/api/__tests__/node-create-description.test.js +83 -0
- package/dist/daemon/api/__tests__/profile-metadata-route.test.d.ts +1 -0
- package/dist/daemon/api/__tests__/profile-metadata-route.test.js +92 -0
- package/dist/daemon/api/__tests__/reopen-delivery.test.d.ts +1 -0
- package/dist/daemon/api/__tests__/reopen-delivery.test.js +173 -0
- package/dist/daemon/api/handlers/broker-ops.js +21 -0
- package/dist/daemon/api/handlers/canvas.js +10 -0
- package/dist/daemon/api/handlers/messages.js +25 -16
- package/dist/daemon/api/handlers/nodes.js +5 -0
- package/dist/daemon/api/handlers/profiles.js +22 -1
- package/dist/daemon/cron-run.js +19 -1
- package/dist/daemon/manage.d.ts +16 -1
- package/dist/daemon/manage.js +20 -1
- package/dist/daemon/park-pending.d.ts +13 -0
- package/dist/daemon/park-pending.js +42 -0
- package/dist/daemon/reconcilers/broker-supervision.d.ts +13 -0
- package/dist/daemon/reconcilers/broker-supervision.js +139 -21
- package/dist/daemon/reconcilers/live-obligation.d.ts +10 -4
- package/dist/daemon/reconcilers/live-obligation.js +7 -3
- package/dist/daemon/reconcilers/storage-maintenance.d.ts +0 -4
- package/dist/daemon/reconcilers/storage-maintenance.js +1 -33
- package/dist/pi-extensions/__tests__/pre-command-gate.test.d.ts +1 -0
- package/dist/pi-extensions/__tests__/pre-command-gate.test.js +220 -0
- package/dist/pi-extensions/canvas-doc-substrate.d.ts +14 -0
- package/dist/pi-extensions/canvas-doc-substrate.js +75 -2
- package/dist/pi-extensions/canvas-prompt-scrub.d.ts +13 -0
- package/dist/pi-extensions/canvas-prompt-scrub.js +53 -0
- package/dist/shared/generated-context.d.ts +7 -0
- package/dist/shared/generated-context.js +11 -0
- package/package.json +4 -4
- package/runtime.lock.json +2 -2
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/strip-skills-docs.ts +0 -47
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
import { accessSync, constants, cpSync, readdirSync, existsSync, rmSync } from 'node:fs';
|
|
13
13
|
import { isAbsolute, resolve, join } from 'node:path';
|
|
14
14
|
import { spawnNode, currentNodeContext, rootOfSpine, newNodeId, preflightNodeId } from './nodes.js';
|
|
15
|
-
import {
|
|
15
|
+
import { resolveProfileOperand } from '../profiles/manifest.js';
|
|
16
16
|
import { selectProfileForCwd, selectProfileForCwdReadOnly } from '../profiles/select.js';
|
|
17
17
|
import { buildLaunchSpecAsync, buildPiArgv } from './launch.js';
|
|
18
18
|
import { createManagedWorktree, rollbackManagedWorktree } from '../worktree.js';
|
|
@@ -23,7 +23,7 @@ import { hasRoadmap, seedRoadmap } from './roadmap.js';
|
|
|
23
23
|
import { buildWakeBearings } from './bearings.js';
|
|
24
24
|
import { canonicalSessionFile, contextDir, findNodeBySessionFile, getNode, fullName, recordPid } from '../canvas/index.js';
|
|
25
25
|
import { jobDir } from '../canvas/paths.js';
|
|
26
|
-
import { openViewerWindow, focusOf, windowOfPane,
|
|
26
|
+
import { openViewerWindow, focusOf, windowOfPane, } from './placement.js';
|
|
27
27
|
import { waitForBrokerViewSocket } from './placement-tmux.js';
|
|
28
28
|
import { transition } from './lifecycle.js';
|
|
29
29
|
import { beginBootFaultAttempt } from './fault.js';
|
|
@@ -33,6 +33,7 @@ import { ensureDaemon } from '../../daemon/manage.js';
|
|
|
33
33
|
import { readConfig } from '../config.js';
|
|
34
34
|
import { modelRequestFromConfig, unregisteredConcreteModel } from '../model-routes.js';
|
|
35
35
|
import { openModelRegistry } from './model-registry.js';
|
|
36
|
+
import { MODEL_SPEC_NEXT } from './model-selection.js';
|
|
36
37
|
import { initializeForkContextExposure } from '../substrate/injected-store.js';
|
|
37
38
|
// ---------------------------------------------------------------------------
|
|
38
39
|
// Shared broker-launch wrapper — catch launch/preflight failures at the call
|
|
@@ -147,7 +148,7 @@ export function resolveForkSource(value) {
|
|
|
147
148
|
return resolveSessionUuid(v);
|
|
148
149
|
}
|
|
149
150
|
/** Resolve the profile a spawned child runs under: an explicit `--profile`
|
|
150
|
-
* operand (id or
|
|
151
|
+
* operand (id, name, or closest match) resolved through `resolveProfileOperand`, else INHERIT
|
|
151
152
|
* the spawner's current `profile_id` (null when the spawner has none). `--root`
|
|
152
153
|
* never resets this to null on its own; only an explicit override does. When
|
|
153
154
|
* there is NO spawner at all — `crtr node new --root` run directly from a
|
|
@@ -158,7 +159,10 @@ export async function resolveProfileId(explicit, spawner, cwd, options = {}) {
|
|
|
158
159
|
if (explicit === null)
|
|
159
160
|
return null;
|
|
160
161
|
if (explicit !== undefined && explicit !== '') {
|
|
161
|
-
|
|
162
|
+
// `explicit` originates as a user-typed `--profile`, so tolerate a near
|
|
163
|
+
// miss. The CLI normally resolves it to an id first; this covers callers
|
|
164
|
+
// that reach the daemon with a name.
|
|
165
|
+
return resolveProfileOperand(explicit).profileId;
|
|
162
166
|
}
|
|
163
167
|
if (spawner !== null)
|
|
164
168
|
return getNode(spawner)?.profile_id ?? null;
|
|
@@ -183,7 +187,7 @@ export async function assertLaunchModelRegistered(request, options, registry) {
|
|
|
183
187
|
return;
|
|
184
188
|
throw usage(`model '${unknown.request.spec}' is not registered in this daemon.`, {
|
|
185
189
|
received: unknown.request.spec,
|
|
186
|
-
next:
|
|
190
|
+
next: MODEL_SPEC_NEXT,
|
|
187
191
|
});
|
|
188
192
|
}
|
|
189
193
|
/** Resolve who a spawn is attributed to. A managed child needs a spine parent
|
|
@@ -272,6 +276,7 @@ export async function spawnChildPrepared(opts, beforeBrokerLaunch) {
|
|
|
272
276
|
lifecycle,
|
|
273
277
|
cwd: spawnCwd,
|
|
274
278
|
name: opts.name ?? opts.kind,
|
|
279
|
+
description: opts.description,
|
|
275
280
|
// A root has no spine parent (top-level, nobody subscribes); it still
|
|
276
281
|
// records spawned_by=spawner when a node (not a human shell) spawned it.
|
|
277
282
|
// A child's parent IS its manager.
|
|
@@ -331,11 +336,6 @@ export async function spawnChildPrepared(opts, beforeBrokerLaunch) {
|
|
|
331
336
|
}
|
|
332
337
|
// (The three scoped long-term memory stores are seeded for EVERY node at birth
|
|
333
338
|
// in spawnNode — no orchestrator-gated seeding needed here.)
|
|
334
|
-
// An ATTENDED --root spawned from inside tmux opens its foreground viewer
|
|
335
|
-
// in the CALLER'S CURRENT session (below), so it appears where the spawner is
|
|
336
|
-
// working; a managed child is a headless broker with NO viewer at all, and
|
|
337
|
-
// so is an unattended terminal root (nobody asked to watch it).
|
|
338
|
-
const here = attendedRoot ? currentTmux() : null;
|
|
339
339
|
const inv = buildPiArgv(meta, { prompt: kickoff, forkFrom });
|
|
340
340
|
// CRTR_SUBTREE (the spine root) rides the detached broker's env so it can group
|
|
341
341
|
// the subtree; the host sets CRTR_FRONT_DOOR itself.
|
|
@@ -377,30 +377,16 @@ export async function spawnChildPrepared(opts, beforeBrokerLaunch) {
|
|
|
377
377
|
? 'Resolve the cause named above, then retry.'
|
|
378
378
|
: `The broker engine exited before binding its view socket. Inspect ${jobDir(meta.node_id)}/broker.log for the underlying cause (a missing dependency, model auth, or a crash), then retry.`);
|
|
379
379
|
}
|
|
380
|
-
//
|
|
381
|
-
//
|
|
382
|
-
//
|
|
383
|
-
//
|
|
384
|
-
//
|
|
385
|
-
//
|
|
386
|
-
//
|
|
387
|
-
// after the readiness proof.
|
|
380
|
+
// A --root opens NO viewer from here. spawnChild runs daemon-side only, and
|
|
381
|
+
// the daemon is paneless by construction (manage.ts strips TMUX/TMUX_PANE
|
|
382
|
+
// from its env precisely so this code cannot resolve someone else's pane),
|
|
383
|
+
// so a root's foreground viewer is placed by whoever asked for it — the
|
|
384
|
+
// front door, or `node new`'s own openSpawnViewer in the CLI process. Only a
|
|
385
|
+
// managed child is placed here, and placement stays non-fatal after the
|
|
386
|
+
// readiness proof.
|
|
388
387
|
let window = null;
|
|
389
388
|
let session = null;
|
|
390
|
-
if (
|
|
391
|
-
const viewer = openViewerWindow(meta.node_id, here.session, { name: fullName(meta), cwd: opts.cwd });
|
|
392
|
-
if (viewer !== null && viewer.pane !== null && viewer.pane !== '') {
|
|
393
|
-
session = here.session;
|
|
394
|
-
window = windowOfPane(viewer.pane);
|
|
395
|
-
if (window !== null) {
|
|
396
|
-
try {
|
|
397
|
-
focusWindow(here.session, window);
|
|
398
|
-
}
|
|
399
|
-
catch { /* best-effort */ }
|
|
400
|
-
}
|
|
401
|
-
}
|
|
402
|
-
}
|
|
403
|
-
else if (!root && spawner !== null && readConfig('user').auto_open_child_viewers) {
|
|
389
|
+
if (!root && spawner !== null && readConfig('user').auto_open_child_viewers) {
|
|
404
390
|
// When enabled, a managed child opens beside a HUMAN actively watching
|
|
405
391
|
// its spawner. Turning the setting off leaves every child headless until
|
|
406
392
|
// someone explicitly focuses or attaches it. The broker is headless
|
|
@@ -13,6 +13,12 @@ export type StructuredOutputState = {
|
|
|
13
13
|
state: 'invalid';
|
|
14
14
|
error: string;
|
|
15
15
|
};
|
|
16
|
+
/** The `--output-schema` transport clause, rendered verbatim by every leaf that
|
|
17
|
+
* accepts one. Declared once so the call sites cannot drift apart. */
|
|
18
|
+
export declare const OUTPUT_SCHEMA_TRANSPORT = "A .json file path, or inline JSON beginning with `{`.";
|
|
19
|
+
/** Provider-constrained generation supports the structural subset below; every
|
|
20
|
+
* other schema keyword remains model-visible and is checked after generation. */
|
|
21
|
+
export declare const OUTPUT_SCHEMA_CONSTRAINTS = "Generation is constrained by structure alone \u2014 type, properties, required, items, anyOf, and the standard string formats; every other keyword (enum, pattern, minLength, numeric bounds) reaches the model as description text and is checked only after it answers, so an over-constrained schema costs retries.";
|
|
16
22
|
export declare function outputSchemaPath(nodeId: string): string;
|
|
17
23
|
export declare function outputResultPath(nodeId: string): string;
|
|
18
24
|
export declare function parseOutputSchemaValue(value: string | undefined): Record<string, unknown> | null;
|
|
@@ -4,6 +4,12 @@ import { randomUUID } from 'node:crypto';
|
|
|
4
4
|
import { nodeDir, contextDir } from '../canvas/paths.js';
|
|
5
5
|
import { InputError } from '../io.js';
|
|
6
6
|
import { isRecord } from '../../shared/predicates.js';
|
|
7
|
+
/** The `--output-schema` transport clause, rendered verbatim by every leaf that
|
|
8
|
+
* accepts one. Declared once so the call sites cannot drift apart. */
|
|
9
|
+
export const OUTPUT_SCHEMA_TRANSPORT = 'A .json file path, or inline JSON beginning with `{`.';
|
|
10
|
+
/** Provider-constrained generation supports the structural subset below; every
|
|
11
|
+
* other schema keyword remains model-visible and is checked after generation. */
|
|
12
|
+
export const OUTPUT_SCHEMA_CONSTRAINTS = 'Generation is constrained by structure alone — type, properties, required, items, anyOf, and the standard string formats; every other keyword (enum, pattern, minLength, numeric bounds) reaches the model as description text and is checked only after it answers, so an over-constrained schema costs retries.';
|
|
7
13
|
export function outputSchemaPath(nodeId) {
|
|
8
14
|
return `${nodeDir(nodeId)}/output-schema.json`;
|
|
9
15
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
// Deliberate, user-approved exception to crouter's standing policy of not using unit tests: glob matching is pure string logic with a large space of near-miss inputs, where a table of real cases is the only economical way to pin behavior, and where BOTH false negatives and false positives are defects.
|
|
2
|
+
//
|
|
3
|
+
// Run: node --conditions=crtr-src --import tsx/esm --test src/core/substrate/__tests__/surface-match-command.test.ts
|
|
4
|
+
import { test } from 'node:test';
|
|
5
|
+
import assert from 'node:assert/strict';
|
|
6
|
+
import { commandDeliveryRung, matchesCommandEntry } from '../surface-match.js';
|
|
7
|
+
const subject = {
|
|
8
|
+
kind: 'developer',
|
|
9
|
+
mode: 'base',
|
|
10
|
+
lifecycle: 'terminal',
|
|
11
|
+
hasManager: false,
|
|
12
|
+
cwd: '/tmp/project',
|
|
13
|
+
scope: 'project',
|
|
14
|
+
orchestration: { depth: 0 },
|
|
15
|
+
profile: null,
|
|
16
|
+
};
|
|
17
|
+
function entry(match, on = 'command', at = 'preview') {
|
|
18
|
+
return { on, at, match };
|
|
19
|
+
}
|
|
20
|
+
test('real command globs match the direct invocation they describe', () => {
|
|
21
|
+
const cases = [
|
|
22
|
+
{ name: 'linear issues', glob: 'linear *', command: 'linear issues list', expected: true },
|
|
23
|
+
{ name: 'capture screenshot', glob: '*capture *', command: 'capture screenshot --url https://example.com', expected: true },
|
|
24
|
+
{ name: 'grove status', glob: '*grove*', command: 'grove status', expected: true },
|
|
25
|
+
{ name: 'crtr human send', glob: '*crtr human *', command: 'crtr human send --message "Approve this change"', expected: true },
|
|
26
|
+
{ name: 'git commit', glob: '*git commit*', command: 'git commit -m "ship fix"', expected: true },
|
|
27
|
+
{ name: 'git push', glob: '*git push*', command: 'git push origin main', expected: true },
|
|
28
|
+
{ name: 'npm publish', glob: '*npm publish*', command: 'npm publish --access public', expected: true },
|
|
29
|
+
{ name: 'pnpm publish', glob: '*pnpm publish*', command: 'pnpm publish --access public', expected: true },
|
|
30
|
+
{ name: 'termrender doc check', glob: '*termrender*', command: 'termrender doc check ./README.md', expected: true },
|
|
31
|
+
{ name: 'single-character question mark', glob: '*git p?sh*', command: 'git push origin main', expected: true },
|
|
32
|
+
{ name: 'trailing star permits bare tool', glob: 'linear *', command: 'linear', expected: true },
|
|
33
|
+
{ name: 'literal glob is an exact command', glob: 'git status', command: 'git status', expected: true },
|
|
34
|
+
{ name: 'literal glob does not imply arguments', glob: 'git status', command: 'git status --short', expected: false },
|
|
35
|
+
];
|
|
36
|
+
for (const testCase of cases) {
|
|
37
|
+
assert.equal(matchesCommandEntry(entry([testCase.glob]), subject, testCase.command), testCase.expected, testCase.name);
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
test('a command is found after ordinary shell composition and whitespace variation', () => {
|
|
41
|
+
const cases = [
|
|
42
|
+
{ name: 'after directory change', glob: 'linear *', command: 'cd apps/core && linear issues list', expected: true },
|
|
43
|
+
{ name: 'after semicolon', glob: '*grove*', command: 'printf ready; grove status', expected: true },
|
|
44
|
+
{ name: 'after or-list', glob: '*crtr human *', command: 'test -f answer.txt || crtr human send --message "Choose one"', expected: true },
|
|
45
|
+
{ name: 'leading environment assignment', glob: 'linear *', command: 'CI=1 linear issues list', expected: true },
|
|
46
|
+
{ name: 'tool inside a pipeline', glob: '*termrender*', command: 'termrender doc check README.md | grep -q error', expected: true },
|
|
47
|
+
{ name: 'tool on the right side of a pipe', glob: '*termrender*', command: 'cat README.md | termrender', expected: true },
|
|
48
|
+
{ name: 'two spaces between tokens', glob: 'linear *', command: 'linear issues list', expected: true },
|
|
49
|
+
{ name: 'tab between tokens', glob: 'linear *', command: 'linear\tissues list', expected: true },
|
|
50
|
+
{ name: 'trailing newline', glob: 'linear *', command: 'linear issues list\n', expected: true },
|
|
51
|
+
];
|
|
52
|
+
for (const testCase of cases) {
|
|
53
|
+
assert.equal(matchesCommandEntry(entry([testCase.glob]), subject, testCase.command), testCase.expected, testCase.name);
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
test('command text and token near-misses do not wake a surface', () => {
|
|
57
|
+
const cases = [
|
|
58
|
+
{ name: 'linear only appears in quoted text', glob: 'linear *', command: 'echo "linear issues list"', expected: false },
|
|
59
|
+
{ name: 'capture only appears in quoted text', glob: '*capture *', command: 'echo "capture screenshot"', expected: false },
|
|
60
|
+
{ name: 'git push only appears in a commit message', glob: '*git push*', command: 'git commit -m "please git push later"', expected: false },
|
|
61
|
+
{ name: 'grove is part of a longer word', glob: '*grove*', command: 'echo grover', expected: false },
|
|
62
|
+
{ name: 'capture is part of a longer word', glob: '*capture *', command: 'printf "%s\\n" mycapture', expected: false },
|
|
63
|
+
{ name: 'linear is part of a filename', glob: 'linear *', command: 'python linear-regression.py', expected: false },
|
|
64
|
+
{ name: 'git commit is a sibling subcommand', glob: '*git commit*', command: 'git commit-tree -m "ship fix"', expected: false },
|
|
65
|
+
{ name: 'termrender only appears in quoted text', glob: '*termrender*', command: 'echo "termrender"', expected: false },
|
|
66
|
+
// A path mention is not an invocation: an unrelated listing should not wake the grove doc.
|
|
67
|
+
{ name: 'grove only appears in an unrelated path', glob: '*grove*', command: 'ls /tmp/grove/config', expected: false },
|
|
68
|
+
];
|
|
69
|
+
for (const testCase of cases) {
|
|
70
|
+
assert.equal(matchesCommandEntry(entry([testCase.glob]), subject, testCase.command), testCase.expected, testCase.name);
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
test('entry and match-list composition stays within the command event', () => {
|
|
74
|
+
const anyGlobCases = [
|
|
75
|
+
{ name: 'second glob satisfies the list', globs: ['*npm publish*', '*pnpm publish*'], command: 'pnpm publish --access public', expected: true },
|
|
76
|
+
{ name: 'no glob satisfies the list', globs: ['*npm publish*', '*pnpm publish*'], command: 'yarn publish', expected: false },
|
|
77
|
+
];
|
|
78
|
+
for (const testCase of anyGlobCases) {
|
|
79
|
+
assert.equal(matchesCommandEntry(entry(testCase.globs), subject, testCase.command), testCase.expected, testCase.name);
|
|
80
|
+
}
|
|
81
|
+
assert.equal(commandDeliveryRung({ surfaces: [entry(['*npm publish*'], 'command', 'name'), entry(['*pnpm publish*'], 'command', 'content')] }, subject, 'pnpm publish --access public'), 'content', 'matching command entries OR together and retain the highest matching rung');
|
|
82
|
+
const otherEventCases = [
|
|
83
|
+
{ name: 'read entry never fires for a command', on: 'read' },
|
|
84
|
+
{ name: 'memory-read entry never fires for a command', on: 'memory-read' },
|
|
85
|
+
];
|
|
86
|
+
for (const testCase of otherEventCases) {
|
|
87
|
+
assert.equal(matchesCommandEntry(entry(['linear *'], testCase.on), subject, 'linear issues list'), false, testCase.name);
|
|
88
|
+
}
|
|
89
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
// Regression: `pre-command` matching holds a command BEFORE it runs, so it
|
|
2
|
+
// costs the agent a whole turn when it fires. Two ways it can be wrong:
|
|
3
|
+
//
|
|
4
|
+
// • It fires on a mention rather than an invocation. A doc-writing command
|
|
5
|
+
// whose heredoc body merely quotes a matched invocation must run untouched
|
|
6
|
+
// — only the heredoc's opening line is a real command.
|
|
7
|
+
// • It ignores the entry's own `gate`. A doc whose entry excludes this node
|
|
8
|
+
// must never hold that node's commands.
|
|
9
|
+
//
|
|
10
|
+
// It also pins the shared-routing invariant: the pre-command matcher is the
|
|
11
|
+
// post-execution `command` matcher plus heredoc stripping, and nothing else.
|
|
12
|
+
// One command-glob semantics, not two.
|
|
13
|
+
//
|
|
14
|
+
// Run: node --conditions=crtr-src --import tsx/esm --test src/core/substrate/__tests__/surface-match-pre-command.test.ts
|
|
15
|
+
import { test } from 'node:test';
|
|
16
|
+
import assert from 'node:assert/strict';
|
|
17
|
+
import { matchesCommandEntry, matchesPreCommandEntry, preCommandDeliveryRung } from '../surface-match.js';
|
|
18
|
+
const SUBJECT = {
|
|
19
|
+
kind: 'developer',
|
|
20
|
+
mode: 'base',
|
|
21
|
+
lifecycle: 'terminal',
|
|
22
|
+
hasManager: true,
|
|
23
|
+
cwd: '/tmp/project',
|
|
24
|
+
scope: 'project',
|
|
25
|
+
orchestration: { depth: 1 },
|
|
26
|
+
profile: 'northlight',
|
|
27
|
+
};
|
|
28
|
+
function entry(match, gate) {
|
|
29
|
+
return { on: 'pre-command', at: 'content', match, ...(gate !== undefined ? { gate } : {}) };
|
|
30
|
+
}
|
|
31
|
+
// --- the behaviour this event adds -----------------------------------------
|
|
32
|
+
test('a matched invocation inside a heredoc BODY does not hold the command', () => {
|
|
33
|
+
const command = [
|
|
34
|
+
"crtr memory write approvals --body <<'EOF'",
|
|
35
|
+
'Before you run `crtr integration run --action send`, get approval.',
|
|
36
|
+
'EOF',
|
|
37
|
+
].join('\n');
|
|
38
|
+
assert.equal(matchesPreCommandEntry(entry(['crtr integration run*']), SUBJECT, command), false);
|
|
39
|
+
});
|
|
40
|
+
test('the same invocation on the heredoc OPENING line still holds the command', () => {
|
|
41
|
+
const command = ["crtr integration run --action send --body <<'EOF'", 'hello', 'EOF'].join('\n');
|
|
42
|
+
assert.equal(matchesPreCommandEntry(entry(['crtr integration run*']), SUBJECT, command), true);
|
|
43
|
+
});
|
|
44
|
+
test('a `<<-` heredoc body is stripped too, tabs and all', () => {
|
|
45
|
+
const command = ['cat <<-EOF', '\tcrtr integration run --action send', '\tEOF'].join('\n');
|
|
46
|
+
assert.equal(matchesPreCommandEntry(entry(['crtr integration run*']), SUBJECT, command), false);
|
|
47
|
+
});
|
|
48
|
+
test('an unterminated heredoc drops its body rather than leaking it back in', () => {
|
|
49
|
+
const command = ["crtr memory write d --body <<'EOF'", 'crtr integration run --action send'].join('\n');
|
|
50
|
+
assert.equal(matchesPreCommandEntry(entry(['crtr integration run*']), SUBJECT, command), false);
|
|
51
|
+
});
|
|
52
|
+
test('an entry whose gate excludes the subject never holds a command', () => {
|
|
53
|
+
const excluded = entry(['crtr integration run*'], { lifecycle: 'resident' });
|
|
54
|
+
assert.equal(matchesPreCommandEntry(excluded, SUBJECT, 'crtr integration run --action send'), false);
|
|
55
|
+
const included = entry(['crtr integration run*'], { lifecycle: 'terminal' });
|
|
56
|
+
assert.equal(matchesPreCommandEntry(included, SUBJECT, 'crtr integration run --action send'), true);
|
|
57
|
+
});
|
|
58
|
+
test('a `command` entry is never consumed by the pre-command matcher, nor the reverse', () => {
|
|
59
|
+
const post = { on: 'command', at: 'content', match: ['crtr integration run*'] };
|
|
60
|
+
assert.equal(matchesPreCommandEntry(post, SUBJECT, 'crtr integration run --action send'), false);
|
|
61
|
+
assert.equal(matchesCommandEntry(entry(['crtr integration run*']), SUBJECT, 'crtr integration run --action send'), false);
|
|
62
|
+
});
|
|
63
|
+
// --- the rung fold ---------------------------------------------------------
|
|
64
|
+
test('the rung fold returns the highest `at` across matching entries, and none when nothing matches', () => {
|
|
65
|
+
const doc = {
|
|
66
|
+
surfaces: [
|
|
67
|
+
entry(['crtr crustdata person search*']),
|
|
68
|
+
entry(['crtr integration run*']),
|
|
69
|
+
{ on: 'command', at: 'content', match: ['*'] },
|
|
70
|
+
],
|
|
71
|
+
};
|
|
72
|
+
assert.equal(preCommandDeliveryRung(doc, SUBJECT, 'crtr integration run --action send'), 'content');
|
|
73
|
+
assert.equal(preCommandDeliveryRung(doc, SUBJECT, 'ls -la'), 'none');
|
|
74
|
+
});
|
|
75
|
+
// --- shared routing: one command-glob semantics, not two --------------------
|
|
76
|
+
test('outside a heredoc, pre-command matching is exactly the `command` matcher', () => {
|
|
77
|
+
// The guard against a second command tokenizer drifting away from the first.
|
|
78
|
+
// Whatever `command` globs mean today, `pre-command` means the same thing —
|
|
79
|
+
// so a sharpening of command-position matching reaches both events at once.
|
|
80
|
+
const glob = 'crtr integration run*';
|
|
81
|
+
for (const command of [
|
|
82
|
+
'crtr integration run --action send',
|
|
83
|
+
'cd /tmp && crtr integration run --action send',
|
|
84
|
+
'echo hi\ncrtr integration run --action send',
|
|
85
|
+
'NL_ENV=1 crtr integration run --action send',
|
|
86
|
+
'crtr integration list',
|
|
87
|
+
'ls -la',
|
|
88
|
+
'echo "crtr integration run --action send"',
|
|
89
|
+
]) {
|
|
90
|
+
assert.equal(matchesPreCommandEntry(entry([glob]), SUBJECT, command), matchesCommandEntry({ on: 'command', at: 'content', match: [glob] }, SUBJECT, command), `pre-command and command disagreed on: ${JSON.stringify(command)}`);
|
|
91
|
+
}
|
|
92
|
+
});
|
|
@@ -47,7 +47,7 @@ export function lintSubstrateSurfaces(value) {
|
|
|
47
47
|
return `invalid surfaces entry \`match-frontmatter\`: ${JSON.stringify(matchFrontmatter)} (expected a field→matcher object)`;
|
|
48
48
|
}
|
|
49
49
|
}
|
|
50
|
-
if ((on === 'read' || on === 'memory-read' || on === 'command') && !hasMatch && matchFrontmatter === undefined) {
|
|
50
|
+
if ((on === 'read' || on === 'memory-read' || on === 'command' || on === 'pre-command') && !hasMatch && matchFrontmatter === undefined) {
|
|
51
51
|
return `invalid surfaces entry: a \`${on}\` entry requires \`match\`${on === 'read' ? ' or `match-frontmatter`' : ''}`;
|
|
52
52
|
}
|
|
53
53
|
const gate = entry['gate'];
|
|
@@ -24,6 +24,12 @@ export declare function contentExposureIdentity(body: string): string | null;
|
|
|
24
24
|
export declare function documentExposedAtOrAbove(state: ContextExposureState, path: string, body: string, rung: Rung): boolean;
|
|
25
25
|
/** Register both the physical document and its non-empty content alias. */
|
|
26
26
|
export declare function registerDocumentExposure(target: ExposureTarget, path: string, body: string, rung: Rung): void;
|
|
27
|
+
/** Forget that a document was delivered into the TRANSCRIPT — both its physical
|
|
28
|
+
* identity and its content alias — while leaving any `system` rank intact,
|
|
29
|
+
* because the system prompt is rebuilt on every agent run and survives
|
|
30
|
+
* compaction. This is what re-arms a `pre-command` hold for guidance that
|
|
31
|
+
* compaction dropped out of the transcript. */
|
|
32
|
+
export declare function demoteDocumentTranscriptExposure(state: ContextExposureState, path: string, body: string): void;
|
|
27
33
|
export declare function freezePreferenceSnapshot(state: ContextExposureState, rendered: string): string;
|
|
28
34
|
export declare function cloneContextExposureState(source: ContextExposureState): ContextExposureState;
|
|
29
35
|
export declare function hasTranscriptExposure(state: ContextExposureState): boolean;
|
|
@@ -49,6 +49,30 @@ export function registerDocumentExposure(target, path, body, rung) {
|
|
|
49
49
|
if (contentIdentity !== null)
|
|
50
50
|
registerExposure(target, contentIdentity, rung);
|
|
51
51
|
}
|
|
52
|
+
/** Drop one identity's transcript rank, deleting the entry outright when no
|
|
53
|
+
* `system` rank remains. The v3 loader rejects an entry carrying neither rank,
|
|
54
|
+
* and the writer serializes the map as-is, so leaving `{}` behind would crash
|
|
55
|
+
* the node's substrate on its next load. */
|
|
56
|
+
function dropTranscriptExposure(state, identity) {
|
|
57
|
+
const ranks = state.exposures.get(identity);
|
|
58
|
+
if (ranks === undefined)
|
|
59
|
+
return;
|
|
60
|
+
if (validRank(ranks.system))
|
|
61
|
+
state.exposures.set(identity, { system: ranks.system });
|
|
62
|
+
else
|
|
63
|
+
state.exposures.delete(identity);
|
|
64
|
+
}
|
|
65
|
+
/** Forget that a document was delivered into the TRANSCRIPT — both its physical
|
|
66
|
+
* identity and its content alias — while leaving any `system` rank intact,
|
|
67
|
+
* because the system prompt is rebuilt on every agent run and survives
|
|
68
|
+
* compaction. This is what re-arms a `pre-command` hold for guidance that
|
|
69
|
+
* compaction dropped out of the transcript. */
|
|
70
|
+
export function demoteDocumentTranscriptExposure(state, path, body) {
|
|
71
|
+
dropTranscriptExposure(state, realpathOrSelf(path));
|
|
72
|
+
const contentIdentity = contentExposureIdentity(body);
|
|
73
|
+
if (contentIdentity !== null)
|
|
74
|
+
dropTranscriptExposure(state, contentIdentity);
|
|
75
|
+
}
|
|
52
76
|
export function freezePreferenceSnapshot(state, rendered) {
|
|
53
77
|
if (state.preferenceSnapshot === null)
|
|
54
78
|
state.preferenceSnapshot = rendered;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type ExposureTarget } from './injected-store.js';
|
|
1
|
+
import { type ContextExposureState, type ExposureTarget } from './injected-store.js';
|
|
2
2
|
import { type Rung, type SubstrateDoc } from './schema.js';
|
|
3
3
|
import type { NodeConfigSubject } from './subject-fields.js';
|
|
4
4
|
interface EventCandidate {
|
|
@@ -30,4 +30,20 @@ export declare function memoryReadDocBlocks(subject: NodeConfigSubject | null, e
|
|
|
30
30
|
* The corpus is the resolved cwd/profile set; a command has no file from which
|
|
31
31
|
* to discover enclosing project stores. */
|
|
32
32
|
export declare function renderOnCommandDocsForSubject(subject: NodeConfigSubject, command: string, target?: ExposureTarget): string;
|
|
33
|
+
/** Surface docs whose `pre-command` entries match a bash command that has NOT
|
|
34
|
+
* run yet. The exact twin of the post-execution renderer above, and its empty
|
|
35
|
+
* string is load-bearing: `''` means every matching doc is already exposed at
|
|
36
|
+
* its matching rung or above, which is the release — the caller lets the
|
|
37
|
+
* command through. */
|
|
38
|
+
export declare function renderPreCommandDocsForSubject(subject: NodeConfigSubject, command: string, target?: ExposureTarget): string;
|
|
39
|
+
/** Does ANY doc in this session's corpus carry a `pre-command` entry? The
|
|
40
|
+
* short-circuit a pre-execution caller checks first: with no such doc — the
|
|
41
|
+
* common case — no command need ever pay for a subject lookup. */
|
|
42
|
+
export declare function corpusHasPreCommandSurfaces(): boolean;
|
|
43
|
+
/** Forget the transcript exposure of every `pre-command` doc, re-arming their
|
|
44
|
+
* holds. Compaction drops delivered guidance out of the transcript while the
|
|
45
|
+
* ledger still claims it was delivered, so without this pass a doc could be
|
|
46
|
+
* held once and then silently absent for the rest of the session. Lives here
|
|
47
|
+
* rather than in the extension because the corpus accessor is private. */
|
|
48
|
+
export declare function demotePreCommandExposures(state: ContextExposureState): void;
|
|
33
49
|
export {};
|
|
@@ -20,7 +20,13 @@
|
|
|
20
20
|
// workspace-wide context arrives before the task rather than waiting for
|
|
21
21
|
// an arbitrary first file.
|
|
22
22
|
// • `command` — a `bash` tool call evaluates the executed command string
|
|
23
|
-
// against every doc's `command` entries (
|
|
23
|
+
// against every doc's `command` entries (per shell segment, wildcards at
|
|
24
|
+
// token granularity — see surface-match.ts).
|
|
25
|
+
// • `pre-command` — a `bash` tool call ABOUT to run evaluates the same way
|
|
26
|
+
// against every doc's `pre-command` entries, and the call is held until
|
|
27
|
+
// the matching docs have been delivered. Because delivery registers
|
|
28
|
+
// exposure, the agent's re-issue of the same command finds nothing left to
|
|
29
|
+
// deliver and runs.
|
|
24
30
|
//
|
|
25
31
|
// All channels register through one context exposure target, so a document
|
|
26
32
|
// already loaded at the same or a higher rung does not repeat. Each candidate
|
|
@@ -36,8 +42,8 @@ import { parseFrontmatterGeneric } from '../frontmatter.js';
|
|
|
36
42
|
import { listAllMemoryDocs, listProjectMemoryDocs, loadStoreMemoryDocs, openProjectMemoryStore } from '../memory-resolver.js';
|
|
37
43
|
import { userScopeRoot } from '../scope.js';
|
|
38
44
|
import { gatePasses } from './gate.js';
|
|
39
|
-
import { commandDeliveryRung, memoryReadDeliveryRung, owningRootOf, readDeliveryRung, workspaceOpenRung } from './surface-match.js';
|
|
40
|
-
import { documentExposedAtOrAbove, emptyContextExposureState, exposureTarget, registerDocumentExposure, } from './injected-store.js';
|
|
45
|
+
import { commandDeliveryRung, memoryReadDeliveryRung, owningRootOf, preCommandDeliveryRung, readDeliveryRung, workspaceOpenRung } from './surface-match.js';
|
|
46
|
+
import { demoteDocumentTranscriptExposure, documentExposedAtOrAbove, emptyContextExposureState, exposureTarget, registerDocumentExposure, } from './injected-store.js';
|
|
41
47
|
import { minRung, parseSubstrateDoc, previewLine } from './schema.js';
|
|
42
48
|
import { cachedEventCorpusInclusive } from './session-cache.js';
|
|
43
49
|
const JUNK_DIRS = new Set(['node_modules', '.git', 'dist', 'build', '.next', '.cache', '.yalc']);
|
|
@@ -242,3 +248,32 @@ export function renderOnCommandDocsForSubject(subject, command, target = transie
|
|
|
242
248
|
const candidates = resolvedDocs().map(({ doc }) => ({ doc, rung: commandDeliveryRung(doc, subject, command) }));
|
|
243
249
|
return renderCandidates(subject, candidates, target);
|
|
244
250
|
}
|
|
251
|
+
/** Surface docs whose `pre-command` entries match a bash command that has NOT
|
|
252
|
+
* run yet. The exact twin of the post-execution renderer above, and its empty
|
|
253
|
+
* string is load-bearing: `''` means every matching doc is already exposed at
|
|
254
|
+
* its matching rung or above, which is the release — the caller lets the
|
|
255
|
+
* command through. */
|
|
256
|
+
export function renderPreCommandDocsForSubject(subject, command, target = transientTranscriptTarget()) {
|
|
257
|
+
const candidates = resolvedDocs().map(({ doc }) => ({ doc, rung: preCommandDeliveryRung(doc, subject, command) }));
|
|
258
|
+
return renderCandidates(subject, candidates, target);
|
|
259
|
+
}
|
|
260
|
+
function carriesPreCommandEntry(doc) {
|
|
261
|
+
return doc.surfaces.some((entry) => entry.on === 'pre-command');
|
|
262
|
+
}
|
|
263
|
+
/** Does ANY doc in this session's corpus carry a `pre-command` entry? The
|
|
264
|
+
* short-circuit a pre-execution caller checks first: with no such doc — the
|
|
265
|
+
* common case — no command need ever pay for a subject lookup. */
|
|
266
|
+
export function corpusHasPreCommandSurfaces() {
|
|
267
|
+
return resolvedDocs().some(({ doc }) => carriesPreCommandEntry(doc));
|
|
268
|
+
}
|
|
269
|
+
/** Forget the transcript exposure of every `pre-command` doc, re-arming their
|
|
270
|
+
* holds. Compaction drops delivered guidance out of the transcript while the
|
|
271
|
+
* ledger still claims it was delivered, so without this pass a doc could be
|
|
272
|
+
* held once and then silently absent for the rest of the session. Lives here
|
|
273
|
+
* rather than in the extension because the corpus accessor is private. */
|
|
274
|
+
export function demotePreCommandExposures(state) {
|
|
275
|
+
for (const { doc } of resolvedDocs()) {
|
|
276
|
+
if (carriesPreCommandEntry(doc))
|
|
277
|
+
demoteDocumentTranscriptExposure(state, doc.path, doc.body);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
@@ -15,7 +15,7 @@ export declare function rungAtLeast(r: Rung, min: Rung): boolean;
|
|
|
15
15
|
* authored rung against its project relationship's cap. */
|
|
16
16
|
export declare function minRung(a: Rung, b: Rung): Rung;
|
|
17
17
|
export { normalizeNameSegment, normalizeDocName, resolveLocalName as resolveDocName } from '../memory/identity.js';
|
|
18
|
-
export declare const SURFACE_EVENTS: readonly ["boot", "workspace-open", "read", "memory-read", "command"];
|
|
18
|
+
export declare const SURFACE_EVENTS: readonly ["boot", "workspace-open", "read", "memory-read", "command", "pre-command"];
|
|
19
19
|
export type SurfaceEvent = (typeof SURFACE_EVENTS)[number];
|
|
20
20
|
/** The rungs an entry may deliver at. There is no `none` — silence is the
|
|
21
21
|
* absence of an entry. (`Rung`'s `none` survives only as the internal
|
|
@@ -30,8 +30,8 @@ export type SurfaceRung = (typeof SURFACE_RUNGS)[number];
|
|
|
30
30
|
export interface SurfaceEntry {
|
|
31
31
|
on: SurfaceEvent;
|
|
32
32
|
/** Globs vs the event's subject namespace. Required on read/memory-read/
|
|
33
|
-
* command (a read entry may carry `matchFrontmatter` instead);
|
|
34
|
-
* on boot/workspace-open (presence of the entry is the match). */
|
|
33
|
+
* command/pre-command (a read entry may carry `matchFrontmatter` instead);
|
|
34
|
+
* meaningless on boot/workspace-open (presence of the entry is the match). */
|
|
35
35
|
match?: string[];
|
|
36
36
|
/** Predicate over the read file's own YAML frontmatter — `read` event only.
|
|
37
37
|
* Frontmatter key `match-frontmatter`. */
|
|
@@ -56,7 +56,7 @@ export { normalizeNameSegment, normalizeDocName, resolveLocalName as resolveDocN
|
|
|
56
56
|
// no `surfaces` at all does exactly one thing: appears in its directory's
|
|
57
57
|
// listing.
|
|
58
58
|
// ---------------------------------------------------------------------------
|
|
59
|
-
export const SURFACE_EVENTS = ['boot', 'workspace-open', 'read', 'memory-read', 'command'];
|
|
59
|
+
export const SURFACE_EVENTS = ['boot', 'workspace-open', 'read', 'memory-read', 'command', 'pre-command'];
|
|
60
60
|
/** The rungs an entry may deliver at. There is no `none` — silence is the
|
|
61
61
|
* absence of an entry. (`Rung`'s `none` survives only as the internal
|
|
62
62
|
* ranking floor, e.g. `bootRung` of a doc with no boot entry.) */
|
|
@@ -158,8 +158,8 @@ function parseGate(v) {
|
|
|
158
158
|
: undefined;
|
|
159
159
|
}
|
|
160
160
|
/** Tolerant `surfaces` parse: a non-array yields `[]`; invalid entries are
|
|
161
|
-
* dropped (bad `on`, bad `at`, a read/memory-read/command entry
|
|
162
|
-
* neither `match` nor — read only — `match-frontmatter`). `match-frontmatter`
|
|
161
|
+
* dropped (bad `on`, bad `at`, a read/memory-read/command/pre-command entry
|
|
162
|
+
* left with neither `match` nor — read only — `match-frontmatter`). `match-frontmatter`
|
|
163
163
|
* on a non-read event is dropped from the entry, which survives iff it still
|
|
164
164
|
* carries a `match`; an invalid entry `gate` is dropped. Lint owns strict
|
|
165
165
|
* enforcement; the runtime parser maps over many docs and must never throw. */
|
|
@@ -180,7 +180,7 @@ function parseSurfaces(v) {
|
|
|
180
180
|
const match = parseMatchGlobs(rec['match']);
|
|
181
181
|
const matchFrontmatter = on === 'read' ? parseGate(rec['match-frontmatter']) : undefined;
|
|
182
182
|
const gate = parseGate(rec['gate']);
|
|
183
|
-
if ((on === 'read' || on === 'memory-read' || on === 'command') &&
|
|
183
|
+
if ((on === 'read' || on === 'memory-read' || on === 'command' || on === 'pre-command') &&
|
|
184
184
|
match === undefined &&
|
|
185
185
|
matchFrontmatter === undefined) {
|
|
186
186
|
continue;
|
|
@@ -20,6 +20,21 @@ export declare function matchesReadEntry(entry: SurfaceEntry, doc: Pick<Substrat
|
|
|
20
20
|
export declare function matchesMemoryReadEntry(entry: SurfaceEntry, routingAnchor: string, subject: NodeConfigSubject | null, name: string): boolean;
|
|
21
21
|
/** Does a `command` entry fit the executed command string? */
|
|
22
22
|
export declare function matchesCommandEntry(entry: SurfaceEntry, subject: NodeConfigSubject, command: string): boolean;
|
|
23
|
+
/** Remove heredoc BODIES (the stdin data between `<<DELIM` and its closing
|
|
24
|
+
* `DELIM` line) from a command before it is matched. The opening line is kept
|
|
25
|
+
* — `crtr push final <<'EOF'` is still a real `push final` invocation — but the
|
|
26
|
+
* body lines, which are data fed on stdin and never executed as commands, are
|
|
27
|
+
* dropped. Without this a doc-writing command whose body merely MENTIONS a
|
|
28
|
+
* matched invocation would have its whole call held. Post-execution `command`
|
|
29
|
+
* delivery does not need this (a spurious late injection costs a paragraph);
|
|
30
|
+
* a `pre-command` block costs the agent a turn, so it does. */
|
|
31
|
+
export declare function stripHeredocs(command: string): string;
|
|
32
|
+
/** Does a `pre-command` entry fit a command that is ABOUT to run? The same
|
|
33
|
+
* glob machinery `command` uses — one matcher, one semantics — applied to the
|
|
34
|
+
* command with its heredoc bodies stripped. Subshell parentheses, backticks,
|
|
35
|
+
* and path-prefixed binaries stay unhandled: this is a guardrail against the
|
|
36
|
+
* faithful-but-uninformed action, not an enforcement boundary. */
|
|
37
|
+
export declare function matchesPreCommandEntry(entry: SurfaceEntry, subject: NodeConfigSubject, command: string): boolean;
|
|
23
38
|
/** The doc's delivery rung for a read of `absReadFile` (already realpathed). */
|
|
24
39
|
export declare function readDeliveryRung(doc: Pick<SubstrateDoc, 'surfaces' | 'path' | 'scope'>, subject: NodeConfigSubject, absReadFile: string, readFrontmatter: Record<string, unknown>): Rung;
|
|
25
40
|
/** The doc's workspace-open rung. Presence of an entry is the match; callers
|
|
@@ -33,3 +48,7 @@ export declare function workspaceOpenRung(doc: Pick<SubstrateDoc, 'surfaces'>, s
|
|
|
33
48
|
export declare function memoryReadDeliveryRung(doc: Pick<SubstrateDoc, 'surfaces'>, routingAnchor: string, subject: NodeConfigSubject | null, name: string): Rung;
|
|
34
49
|
/** The doc's delivery rung for an executed command string. */
|
|
35
50
|
export declare function commandDeliveryRung(doc: Pick<SubstrateDoc, 'surfaces'>, subject: NodeConfigSubject, command: string): Rung;
|
|
51
|
+
/** The doc's delivery rung for a command about to run — the highest `at`
|
|
52
|
+
* over the doc's matching `pre-command` entries, folded the same way as every
|
|
53
|
+
* other event. */
|
|
54
|
+
export declare function preCommandDeliveryRung(doc: Pick<SubstrateDoc, 'surfaces'>, subject: NodeConfigSubject, command: string): Rung;
|