@north-light/crouter 0.3.182 → 0.3.183
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/build-root.js +10 -3
- package/dist/builtin-memory/internal/memory-loading.md +3 -1
- package/dist/clients/attach/__tests__/group-activity.test.js +2 -2
- package/dist/clients/attach/__tests__/titled-editor-paste.test.d.ts +1 -0
- package/dist/clients/attach/__tests__/titled-editor-paste.test.js +114 -0
- package/dist/clients/attach/__tests__/titled-editor-preview.test.js +4 -1
- package/dist/clients/attach/input/titled-editor.d.ts +18 -0
- package/dist/clients/attach/input/titled-editor.js +62 -0
- package/dist/clients/attach/render/chat-view.d.ts +3 -0
- package/dist/clients/attach/render/chat-view.js +15 -6
- package/dist/clients/attach/render/context-message.js +2 -2
- package/dist/clients/attach/render/group-activity.d.ts +18 -1
- package/dist/clients/attach/render/group-activity.js +54 -6
- package/dist/clients/attach/render/group-recap.js +13 -2
- package/dist/clients/attach/render/{markdown-headings.d.ts → markdown-source.d.ts} +4 -4
- package/dist/clients/attach/render/markdown-source.js +165 -0
- package/dist/clients/attach/session/bindings.js +5 -2
- package/dist/clients/attach/viewer.js +396 -394
- package/dist/clients/inbox/review/keys.js +2 -9
- package/dist/clients/inbox/tui/keys.js +2 -9
- package/dist/commands/cron.js +1 -1
- package/dist/commands/memory/delete.js +1 -1
- package/dist/commands/memory/find.js +1 -1
- package/dist/commands/memory/lint.js +28 -3
- package/dist/commands/memory/list.js +1 -1
- package/dist/commands/memory/origin.js +1 -1
- package/dist/commands/memory/read.js +4 -4
- package/dist/commands/memory/write.js +1 -1
- package/dist/commands/sys/settings-shell.d.ts +0 -3
- package/dist/commands/sys/settings-shell.js +1 -38
- package/dist/commands/sys/settings.js +2 -3
- package/dist/commands/sys/setup-core.d.ts +2 -17
- package/dist/commands/sys/setup-core.js +9 -33
- package/dist/commands/sys/setup-wizard.d.ts +1 -3
- package/dist/commands/sys/setup-wizard.js +9 -39
- package/dist/commands/sys/setup.js +6 -6
- package/dist/commands/sys/sync-project-guidance.js +62 -12
- package/dist/core/__tests__/fault-classifier.test.js +20 -0
- package/dist/core/__tests__/nested-store-discovery.test.d.ts +1 -0
- package/dist/core/__tests__/nested-store-discovery.test.js +89 -0
- package/dist/core/__tests__/on-read-nested-store.test.d.ts +1 -0
- package/dist/core/__tests__/on-read-nested-store.test.js +117 -0
- package/dist/core/__tests__/plugin-link-symlinked-scope-root.test.d.ts +1 -0
- package/dist/core/__tests__/plugin-link-symlinked-scope-root.test.js +38 -0
- package/dist/core/__tests__/serial/tmux-surface.test.js +8 -5
- package/dist/core/__tests__/watchdog-abort-arms-retry.test.js +34 -1
- package/dist/core/command.js +5 -3
- package/dist/core/fault-classifier.d.ts +1 -1
- package/dist/core/fault-classifier.js +7 -0
- package/dist/core/fs-utils.js +6 -1
- package/dist/core/keybindings/index.d.ts +1 -1
- package/dist/core/keybindings/index.js +1 -1
- package/dist/core/keybindings/match.d.ts +11 -0
- package/dist/core/keybindings/match.js +22 -0
- package/dist/core/memory/doc-link-grammar.js +3 -7
- package/dist/core/memory/inline-ref-guidance.js +1 -1
- package/dist/core/memory-resolver.d.ts +7 -1
- package/dist/core/memory-resolver.js +20 -7
- package/dist/core/nested-stores.d.ts +12 -0
- package/dist/core/nested-stores.js +146 -0
- package/dist/core/preview-registry.js +4 -2
- package/dist/core/runtime/bearings.d.ts +1 -1
- package/dist/core/runtime/bearings.js +2 -2
- package/dist/core/runtime/broker/fault-retry.js +23 -4
- package/dist/core/runtime/broker-extension-render.d.ts +1 -1
- package/dist/core/runtime/broker-extension-render.js +2 -2
- package/dist/core/runtime/broker-protocol.d.ts +1 -1
- package/dist/core/runtime/deliver-live.js +11 -1
- package/dist/core/runtime/fault.js +1 -1
- package/dist/core/runtime/tmux-bindings.js +42 -20
- package/dist/core/runtime/tool-group-summary.js +2 -6
- package/dist/core/substrate/injected-store.d.ts +6 -0
- package/dist/core/substrate/injected-store.js +19 -0
- package/dist/core/substrate/on-read-node.d.ts +4 -2
- package/dist/core/substrate/on-read-node.js +5 -3
- package/dist/core/substrate/on-read.d.ts +4 -2
- package/dist/core/substrate/on-read.js +27 -5
- package/dist/pi-extensions/canvas-context-intro.d.ts +1 -1
- package/dist/pi-extensions/canvas-context-intro.js +14 -3
- package/dist/pi-extensions/canvas-doc-substrate.js +13 -2
- package/dist/shared/generated-context.d.ts +4 -0
- package/dist/shared/generated-context.js +20 -1
- package/dist/shared/tool-groups.d.ts +3 -3
- package/dist/shared/tool-groups.js +1 -4
- package/package.json +1 -1
- package/runtime.lock.json +2 -2
- package/scripts/postinstall.mjs +1 -1
- package/dist/clients/attach/render/markdown-headings.js +0 -93
- package/dist/commands/search/answer.d.ts +0 -1
- package/dist/commands/search/answer.js +0 -50
- package/dist/commands/search/contents.d.ts +0 -1
- package/dist/commands/search/contents.js +0 -96
- package/dist/commands/search/exa.d.ts +0 -53
- package/dist/commands/search/exa.js +0 -157
- package/dist/commands/search/puremd.d.ts +0 -11
- package/dist/commands/search/puremd.js +0 -53
- package/dist/commands/search/web.d.ts +0 -1
- package/dist/commands/search/web.js +0 -65
- package/dist/commands/search.d.ts +0 -2
- package/dist/commands/search.js +0 -24
- package/dist/commands/sys/panels/keys-panel.d.ts +0 -16
- package/dist/commands/sys/panels/keys-panel.js +0 -60
|
@@ -161,23 +161,40 @@ function normalizedStrokeParts(stroke) {
|
|
|
161
161
|
return { key: parts.pop(), modifiers: parts };
|
|
162
162
|
}
|
|
163
163
|
function tmuxKey(stroke) {
|
|
164
|
+
return tmuxKeyNames(stroke)[0];
|
|
165
|
+
}
|
|
166
|
+
/** Every tmux key name one physical chord can arrive as, most specific first.
|
|
167
|
+
*
|
|
168
|
+
* tmux derives the name from the bytes the terminal sent, and a terminal sends
|
|
169
|
+
* different bytes for the same chord depending on whether extended keys are
|
|
170
|
+
* active — which changes per pane, since the pane's application turns them on.
|
|
171
|
+
* tmux does not normalize across those encodings, so a chord bound under one
|
|
172
|
+
* name is simply dead under the others. Verified against tmux 3.7b by writing
|
|
173
|
+
* raw bytes to an attached client; for Alt+Shift+F:
|
|
174
|
+
*
|
|
175
|
+
* ESC F (legacy meta prefix) -> `M-F`
|
|
176
|
+
* ESC [70;4u / ESC [27;4;70~ (shifted keysym) -> `M-S-F`
|
|
177
|
+
* ESC [102;4u / ESC [27;4;102~ (base keysym) -> `M-S-f`
|
|
178
|
+
*
|
|
179
|
+
* Shift alone never survives as a modifier — every encoding of Shift+G lands
|
|
180
|
+
* on `G` — so only chords that pair Shift with Ctrl or Alt carry aliases. */
|
|
181
|
+
function tmuxKeyNames(stroke) {
|
|
164
182
|
let { key, modifiers: modifierParts } = normalizedStrokeParts(stroke);
|
|
165
183
|
const modifiers = new Set(modifierParts);
|
|
166
|
-
|
|
167
|
-
// tmux names a shifted printable with its uppercase character even when an
|
|
168
|
-
// extended-key terminal also preserves Shift as a modifier: Alt+Shift+F is
|
|
169
|
-
// `M-S-F`. Shift by itself needs only the uppercase character (`F`).
|
|
170
|
-
key = key.toUpperCase();
|
|
171
|
-
if (!modifiers.has('ctrl') && !modifiers.has('alt'))
|
|
172
|
-
modifiers.delete('shift');
|
|
173
|
-
}
|
|
174
|
-
key = TMUX_NAMED_KEYS[key] ?? key;
|
|
175
|
-
const prefix = [
|
|
184
|
+
const name = (character, shift) => [
|
|
176
185
|
modifiers.has('ctrl') ? 'C-' : '',
|
|
177
186
|
modifiers.has('alt') ? 'M-' : '',
|
|
178
|
-
|
|
187
|
+
shift ? 'S-' : '',
|
|
188
|
+
TMUX_NAMED_KEYS[character] ?? character,
|
|
179
189
|
].join('');
|
|
180
|
-
|
|
190
|
+
if (modifiers.has('shift') && key.length === 1 && /[a-z]/.test(key)) {
|
|
191
|
+
const shifted = key.toUpperCase();
|
|
192
|
+
// Shift-only: tmux always collapses it into the uppercase character.
|
|
193
|
+
if (!modifiers.has('ctrl') && !modifiers.has('alt'))
|
|
194
|
+
return [name(shifted, false)];
|
|
195
|
+
return [name(shifted, true), name(shifted, false), name(key, true)];
|
|
196
|
+
}
|
|
197
|
+
return [name(key, modifiers.has('shift'))];
|
|
181
198
|
}
|
|
182
199
|
function menuSelector(stroke) {
|
|
183
200
|
const { key, modifiers } = normalizedStrokeParts(stroke);
|
|
@@ -386,7 +403,7 @@ export function occupiedTmuxRootGestures(gestures) {
|
|
|
386
403
|
const parsed = parseTmuxBinding(line);
|
|
387
404
|
return parsed?.table === ROOT_TABLE ? [parsed.key] : [];
|
|
388
405
|
}));
|
|
389
|
-
return gestures.filter((gesture) => !gesture.includes(' > ') && occupied.has(
|
|
406
|
+
return gestures.filter((gesture) => !gesture.includes(' > ') && tmuxKeyNames(gesture).some((key) => occupied.has(key)));
|
|
390
407
|
}
|
|
391
408
|
function isInstalledPair(value) {
|
|
392
409
|
return typeof value === 'object' && value !== null
|
|
@@ -547,9 +564,10 @@ function reconcileTmuxBindings(bindingSnapshot, passthroughSnapshot = []) {
|
|
|
547
564
|
for (const gesture of bindings.gestures(bindingId)) {
|
|
548
565
|
if (gesture.includes(' > '))
|
|
549
566
|
continue;
|
|
550
|
-
const key
|
|
551
|
-
|
|
552
|
-
|
|
567
|
+
for (const key of tmuxKeyNames(gesture)) {
|
|
568
|
+
if (!desiredWrappers.has(key))
|
|
569
|
+
desiredWrappers.set(key, { bindingId, gesture });
|
|
570
|
+
}
|
|
553
571
|
}
|
|
554
572
|
}
|
|
555
573
|
// Restore every wrapper crouter still owns before the ordinary binding sweep.
|
|
@@ -610,7 +628,8 @@ function reconcileTmuxBindings(bindingSnapshot, passthroughSnapshot = []) {
|
|
|
610
628
|
const menuActions = resolvedMenuActions(bindings);
|
|
611
629
|
const menuCommand = buildMenuArgs(' crtr ', menuItems(bindings, menuActions));
|
|
612
630
|
for (const gesture of bindings.gestures('crtr.tmux.menu.open')) {
|
|
613
|
-
|
|
631
|
+
for (const key of tmuxKeyNames(gesture))
|
|
632
|
+
install(ROOT_TABLE, key, menuCommand, 'crtr.tmux.menu.open', gesture);
|
|
614
633
|
}
|
|
615
634
|
for (const definition of BINDING_CATALOG) {
|
|
616
635
|
if (definition.pair?.field !== 'menu')
|
|
@@ -625,12 +644,15 @@ function reconcileTmuxBindings(bindingSnapshot, passthroughSnapshot = []) {
|
|
|
625
644
|
if (command === null)
|
|
626
645
|
throw new Error(`no direct tmux action for paired menu binding ${menuId}`);
|
|
627
646
|
for (const gesture of bindings.gestures(globalId)) {
|
|
628
|
-
|
|
647
|
+
for (const key of tmuxKeyNames(gesture))
|
|
648
|
+
install(ROOT_TABLE, key, command.argv, globalId, gesture);
|
|
629
649
|
}
|
|
630
650
|
}
|
|
631
651
|
for (const id of ['crtr.tmux.node.next', 'crtr.tmux.node.previous', 'crtr.tmux.inbox.toggle']) {
|
|
632
|
-
for (const gesture of bindings.gestures(id))
|
|
633
|
-
|
|
652
|
+
for (const gesture of bindings.gestures(id)) {
|
|
653
|
+
for (const key of tmuxKeyNames(gesture))
|
|
654
|
+
install(ROOT_TABLE, key, actionCommand(id), id, gesture);
|
|
655
|
+
}
|
|
634
656
|
}
|
|
635
657
|
// Wrap only occupied roots. Unoccupied gestures already reach the pane and do
|
|
636
658
|
// not need crouter ownership. Re-read the installed line as the fingerprint;
|
|
@@ -49,20 +49,16 @@ const SUMMARY_SYSTEM_PROMPT = 'Create a structured recap of this coding-agent to
|
|
|
49
49
|
'Omit inbox receipt, delivery, and queueing; node-response and report bookkeeping; help lookups; status checks; and summary-generation activity. ' +
|
|
50
50
|
'A node response may support a substantive outcome, but its arrival or resummarization is never itself an outcome. ' +
|
|
51
51
|
'If no meaningful outcome remains after those omissions, submit the single bullet "No meaningful changes." ' +
|
|
52
|
-
'Count
|
|
52
|
+
'Count distinct files removed from disk only when the supplied tool arguments or results evidence them; otherwise use zero, even when the corresponding mechanics are omitted from bullets. ' +
|
|
53
53
|
'A deleted file is one that no longer exists after the segment, never a file that was merely edited or overwritten, and a recursive or wildcard removal of an unknown number of files counts as one. ' +
|
|
54
54
|
'Do not invent success when a result shows failure. ' +
|
|
55
|
-
'Call submit_summary exactly once with bullets
|
|
55
|
+
'Call submit_summary exactly once with bullets and filesDeleted, then stop.';
|
|
56
56
|
const SUBMIT_SUMMARY_SCHEMA = Type.Object({
|
|
57
57
|
bullets: Type.Array(Type.String({
|
|
58
58
|
minLength: 1,
|
|
59
59
|
maxLength: 120,
|
|
60
60
|
description: 'A very short, plain-English outcome without bullet markers, identifiers, or operational bookkeeping.',
|
|
61
61
|
}), { minItems: 1, maxItems: 5 }),
|
|
62
|
-
nodesSpawned: Type.Integer({
|
|
63
|
-
minimum: 0,
|
|
64
|
-
description: 'Count of nodes created during the supplied segment, or zero when none are evidenced.',
|
|
65
|
-
}),
|
|
66
62
|
filesDeleted: Type.Integer({
|
|
67
63
|
minimum: 0,
|
|
68
64
|
description: 'Count of distinct files removed from disk during the supplied segment, or zero when none are evidenced.',
|
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
/** The process-shared dedup set for a node: loaded from disk once, then the
|
|
2
|
+
* SAME Set instance on every call, so all hooks in this pi process grow and
|
|
3
|
+
* consult one set. Daemon-side deliveries (a warm-spare claim) write the disk
|
|
4
|
+
* file from another process; consumers that must see those merge the plain
|
|
5
|
+
* `loadInjectedDocs` result in before rendering. */
|
|
6
|
+
export declare function sharedInjectedDocs(nodeId: string): Set<string>;
|
|
1
7
|
/** Rehydrate a node's on-read dedup set from disk. Returns an empty set when the
|
|
2
8
|
* file is absent, unreadable, or malformed (a fresh transcript, or a node that
|
|
3
9
|
* has not yet surfaced any on-read doc). */
|
|
@@ -19,6 +19,24 @@
|
|
|
19
19
|
// never a crash — a dedup miss must never break a read or a revive.
|
|
20
20
|
import { readFileSync, writeFileSync, rmSync } from 'node:fs';
|
|
21
21
|
import { injectedDocsPath } from '../canvas/paths.js';
|
|
22
|
+
/** Per-process cache behind `sharedInjectedDocs`: one Set instance per node,
|
|
23
|
+
* shared by every extension in the pi process (context-intro's workspace-open
|
|
24
|
+
* render and doc-substrate's on-read hook), so a doc delivered by one hook is
|
|
25
|
+
* already in the set the other dedups against. */
|
|
26
|
+
const cache = new Map();
|
|
27
|
+
/** The process-shared dedup set for a node: loaded from disk once, then the
|
|
28
|
+
* SAME Set instance on every call, so all hooks in this pi process grow and
|
|
29
|
+
* consult one set. Daemon-side deliveries (a warm-spare claim) write the disk
|
|
30
|
+
* file from another process; consumers that must see those merge the plain
|
|
31
|
+
* `loadInjectedDocs` result in before rendering. */
|
|
32
|
+
export function sharedInjectedDocs(nodeId) {
|
|
33
|
+
let seen = cache.get(nodeId);
|
|
34
|
+
if (seen === undefined) {
|
|
35
|
+
seen = loadInjectedDocs(nodeId);
|
|
36
|
+
cache.set(nodeId, seen);
|
|
37
|
+
}
|
|
38
|
+
return seen;
|
|
39
|
+
}
|
|
22
40
|
/** Rehydrate a node's on-read dedup set from disk. Returns an empty set when the
|
|
23
41
|
* file is absent, unreadable, or malformed (a fresh transcript, or a node that
|
|
24
42
|
* has not yet surfaced any on-read doc). */
|
|
@@ -46,6 +64,7 @@ export function saveInjectedDocs(nodeId, seen) {
|
|
|
46
64
|
/** Drop a node's persisted dedup set. Called by the launch paths that start a
|
|
47
65
|
* FRESH transcript, so the new conversation surfaces docs from scratch. */
|
|
48
66
|
export function clearInjectedDocs(nodeId) {
|
|
67
|
+
cache.delete(nodeId);
|
|
49
68
|
try {
|
|
50
69
|
rmSync(injectedDocsPath(nodeId), { force: true });
|
|
51
70
|
}
|
|
@@ -3,5 +3,7 @@
|
|
|
3
3
|
export declare function renderOnReadDocs(nodeId: string, readFilePath: string, seen?: Set<string>): string;
|
|
4
4
|
/** Surface workspace-wide docs during first-message assembly, using the node's
|
|
5
5
|
* recorded cwd and selected profile. `.` is a reserved applies-to target
|
|
6
|
-
* meaning “when this project store is mounted by cwd/profile
|
|
7
|
-
|
|
6
|
+
* meaning “when this project store is mounted by cwd/profile” (and, on the
|
|
7
|
+
* read event, “a file under the store's owning dir was read”). `seen` is the
|
|
8
|
+
* transcript-scoped dedup set shared with the on-read hook. */
|
|
9
|
+
export declare function renderWorkspaceOpenDocs(nodeId: string, seen?: Set<string>): string;
|
|
@@ -22,14 +22,16 @@ export function renderOnReadDocs(nodeId, readFilePath, seen = new Set()) {
|
|
|
22
22
|
}
|
|
23
23
|
/** Surface workspace-wide docs during first-message assembly, using the node's
|
|
24
24
|
* recorded cwd and selected profile. `.` is a reserved applies-to target
|
|
25
|
-
* meaning “when this project store is mounted by cwd/profile
|
|
26
|
-
|
|
25
|
+
* meaning “when this project store is mounted by cwd/profile” (and, on the
|
|
26
|
+
* read event, “a file under the store's owning dir was read”). `seen` is the
|
|
27
|
+
* transcript-scoped dedup set shared with the on-read hook. */
|
|
28
|
+
export function renderWorkspaceOpenDocs(nodeId, seen = new Set()) {
|
|
27
29
|
try {
|
|
28
30
|
const subject = assembleNodeSubject(nodeId);
|
|
29
31
|
const node = getNode(nodeId);
|
|
30
32
|
if (subject === null || node === null)
|
|
31
33
|
return '';
|
|
32
|
-
return renderWorkspaceOpenDocsForSubject(subject, node.cwd, node.profile_id ?? null);
|
|
34
|
+
return renderWorkspaceOpenDocsForSubject(subject, node.cwd, node.profile_id ?? null, seen);
|
|
33
35
|
}
|
|
34
36
|
catch {
|
|
35
37
|
return '';
|
|
@@ -3,5 +3,7 @@ import type { NodeConfigSubject } from './subject-fields.js';
|
|
|
3
3
|
* daemon snapshot subject. */
|
|
4
4
|
export declare function renderOnReadDocsForSubject(subject: NodeConfigSubject, readFilePath: string, seen?: Set<string>): string;
|
|
5
5
|
/** Workspace boot docs using the snapshot's cwd/profile instead of a canvas
|
|
6
|
-
* lookup.
|
|
7
|
-
|
|
6
|
+
* lookup. `seen` is the transcript-scoped dedup set shared with the on-read
|
|
7
|
+
* hook, so a front door delivered here never re-delivers on a later read
|
|
8
|
+
* beneath its store. */
|
|
9
|
+
export declare function renderWorkspaceOpenDocsForSubject(subject: NodeConfigSubject, cwd: string, profileId: string | null, seen?: Set<string>): string;
|
|
@@ -12,10 +12,16 @@
|
|
|
12
12
|
// visible substrate doc. Project stores encountered between that file and
|
|
13
13
|
// the filesystem root join the resolved cwd/profile corpus, so a nested
|
|
14
14
|
// workspace can contribute guidance exactly when a file beneath it is read.
|
|
15
|
+
// On this event the reserved `applies-to: "."` target means "the read file
|
|
16
|
+
// is under the doc's store's owning directory" — a nested store's front
|
|
17
|
+
// door surfaces on the first read beneath it, delivery driven purely by
|
|
18
|
+
// filesystem truth.
|
|
15
19
|
// • Opening a workspace evaluates the reserved `applies-to: "."` target for
|
|
16
20
|
// every project store mounted by the cwd and selected profile. This happens
|
|
17
21
|
// while the first-message bearings are assembled, so workspace-wide context
|
|
18
22
|
// arrives before the task rather than waiting for an arbitrary first file.
|
|
23
|
+
// Both hooks share one transcript-scoped dedup set, so a doc delivered at
|
|
24
|
+
// workspace open never re-delivers on a later matching read.
|
|
19
25
|
//
|
|
20
26
|
// `read-when` remains an additional OR trigger over a read markdown file's own
|
|
21
27
|
// frontmatter. It does not replace `applies-to`: every non-none file-read rung
|
|
@@ -25,7 +31,7 @@
|
|
|
25
31
|
// is the whole on-read model, so directory INDEX ceilings remain a boot-catalog
|
|
26
32
|
// concern and do not cap a deliberately matched document.
|
|
27
33
|
import { homedir } from 'node:os';
|
|
28
|
-
import { basename, dirname, matchesGlob, parse, relative, sep } from 'node:path';
|
|
34
|
+
import { basename, dirname, isAbsolute, matchesGlob, parse, relative, sep } from 'node:path';
|
|
29
35
|
import { CRTR_DIR_NAME } from '../../types.js';
|
|
30
36
|
import { pathExists, readText, walkFiles } from '../fs-utils.js';
|
|
31
37
|
import { parseFrontmatterGeneric } from '../frontmatter.js';
|
|
@@ -175,6 +181,18 @@ function globMatches(glob, absReadFile, owningRoot) {
|
|
|
175
181
|
}
|
|
176
182
|
});
|
|
177
183
|
}
|
|
184
|
+
/** True when the read file sits beneath the doc's store's owning directory —
|
|
185
|
+
* the read-event meaning of the reserved `applies-to: "."` target. Callers
|
|
186
|
+
* additionally guard on `doc.scope === 'project'`: a profile store's path
|
|
187
|
+
* computes an owning root of `~`, and user/builtin/node docs likewise have no
|
|
188
|
+
* project-owning dir, so `.` outside a project store must never match. */
|
|
189
|
+
function underOwningRoot(doc, absReadFile) {
|
|
190
|
+
const root = owningRootOf(doc);
|
|
191
|
+
if (root === null)
|
|
192
|
+
return false;
|
|
193
|
+
const rel = relative(realpathOrSelf(root), absReadFile);
|
|
194
|
+
return rel !== '' && !rel.startsWith('..') && !isAbsolute(rel);
|
|
195
|
+
}
|
|
178
196
|
function readFileFrontmatter(absReadFile) {
|
|
179
197
|
if (!/\.(md|mdx|markdown)$/i.test(absReadFile))
|
|
180
198
|
return {};
|
|
@@ -186,7 +204,9 @@ function readFileFrontmatter(absReadFile) {
|
|
|
186
204
|
}
|
|
187
205
|
}
|
|
188
206
|
function matchesReadEvent(doc, absReadFile, readFrontmatter) {
|
|
189
|
-
const pathMatch = doc.appliesTo?.some((glob) =>
|
|
207
|
+
const pathMatch = doc.appliesTo?.some((glob) => glob.trim() === '.'
|
|
208
|
+
? doc.scope === 'project' && underOwningRoot(doc, absReadFile)
|
|
209
|
+
: globMatches(glob, absReadFile, owningRootOf(doc))) === true;
|
|
190
210
|
const frontmatterMatch = doc.readWhen !== undefined &&
|
|
191
211
|
Object.keys(readFrontmatter).length > 0 &&
|
|
192
212
|
evalCondition(doc.readWhen, readFrontmatter);
|
|
@@ -245,8 +265,10 @@ export function renderOnReadDocsForSubject(subject, readFilePath, seen = new Set
|
|
|
245
265
|
return renderCandidates(subject, docs, seen);
|
|
246
266
|
}
|
|
247
267
|
/** Workspace boot docs using the snapshot's cwd/profile instead of a canvas
|
|
248
|
-
* lookup.
|
|
249
|
-
|
|
268
|
+
* lookup. `seen` is the transcript-scoped dedup set shared with the on-read
|
|
269
|
+
* hook, so a front door delivered here never re-delivers on a later read
|
|
270
|
+
* beneath its store. */
|
|
271
|
+
export function renderWorkspaceOpenDocsForSubject(subject, cwd, profileId, seen = new Set()) {
|
|
250
272
|
let docs;
|
|
251
273
|
try {
|
|
252
274
|
docs = listProjectMemoryDocs(cwd, profileId)
|
|
@@ -264,5 +286,5 @@ export function renderWorkspaceOpenDocsForSubject(subject, cwd, profileId) {
|
|
|
264
286
|
const depth = aRoot.split(sep).filter(Boolean).length - bRoot.split(sep).filter(Boolean).length;
|
|
265
287
|
return depth || aRoot.localeCompare(bRoot) || a.path.localeCompare(b.path);
|
|
266
288
|
});
|
|
267
|
-
return renderCandidates(subject, docs,
|
|
289
|
+
return renderCandidates(subject, docs, seen);
|
|
268
290
|
}
|
|
@@ -68,7 +68,7 @@ interface PiLike {
|
|
|
68
68
|
}
|
|
69
69
|
/** Build the full bearings baseline for a conversation without inherited
|
|
70
70
|
* crouter context. Exported for testing. */
|
|
71
|
-
export declare function buildContextIntro(nodeId: string): Promise<string>;
|
|
71
|
+
export declare function buildContextIntro(nodeId: string, seen?: Set<string>): Promise<string>;
|
|
72
72
|
/** Build the compact node-scoped update for a fork whose copied branch already
|
|
73
73
|
* carries a full crouter baseline. Exported for testing. */
|
|
74
74
|
export declare function buildForkContextIntro(nodeId: string): Promise<string>;
|
|
@@ -30,6 +30,7 @@ import { CONTEXT_INTRO_CUSTOM_TYPE } from '../shared/generated-context.js';
|
|
|
30
30
|
import { brokerExtensionState } from '../core/runtime/broker/daemon-ops.js';
|
|
31
31
|
import { buildContextBearingsFromState, buildForkBearingsFromState, buildProjectContextBlockForBroker } from '../core/runtime/broker-extension-render.js';
|
|
32
32
|
import { situationalContextBlock } from '../core/runtime/situational-context.js';
|
|
33
|
+
import { saveInjectedDocs, sharedInjectedDocs } from '../core/substrate/injected-store.js';
|
|
33
34
|
import { truncateToWidth } from './truncate.js';
|
|
34
35
|
/** The `customType` stamped on the injected session message. Used both to write
|
|
35
36
|
* the entry and to detect it on resume (the idempotency guard). Defined beside
|
|
@@ -41,9 +42,9 @@ export { CONTEXT_INTRO_CUSTOM_TYPE };
|
|
|
41
42
|
// ---------------------------------------------------------------------------
|
|
42
43
|
/** Build the full bearings baseline for a conversation without inherited
|
|
43
44
|
* crouter context. Exported for testing. */
|
|
44
|
-
export async function buildContextIntro(nodeId) {
|
|
45
|
+
export async function buildContextIntro(nodeId, seen) {
|
|
45
46
|
const state = await brokerExtensionState(nodeId);
|
|
46
|
-
return buildContextBearingsFromState(state, buildProjectContextBlockForBroker(state.node.cwd), situationalContextBlock(nodeId));
|
|
47
|
+
return buildContextBearingsFromState(state, buildProjectContextBlockForBroker(state.node.cwd), situationalContextBlock(nodeId), seen);
|
|
47
48
|
}
|
|
48
49
|
/** Build the compact node-scoped update for a fork whose copied branch already
|
|
49
50
|
* carries a full crouter baseline. Exported for testing. */
|
|
@@ -160,15 +161,25 @@ export function registerCanvasContextIntro(pi) {
|
|
|
160
161
|
// this fork's new node-scoped facts. A non-crouter branch has no such entry.
|
|
161
162
|
const inherited = contextEntries.length > 0;
|
|
162
163
|
const forked = state.node.fork_from !== null;
|
|
164
|
+
// The baseline branch renders workspace-open docs, so it threads the
|
|
165
|
+
// PROCESS-SHARED dedup set: the on-read hook (canvas-doc-substrate) holds
|
|
166
|
+
// the same Set instance, so a front door delivered here never re-delivers
|
|
167
|
+
// on a later read beneath its store. The fork branch renders no workspace
|
|
168
|
+
// docs and needs no set.
|
|
169
|
+
const seen = inherited && forked ? undefined : sharedInjectedDocs(nodeId);
|
|
163
170
|
const content = inherited && forked
|
|
164
171
|
? buildForkBearingsFromState(state, situationalContextBlock(nodeId))
|
|
165
|
-
: buildContextBearingsFromState(state, buildProjectContextBlockForBroker(state.node.cwd), situationalContextBlock(nodeId));
|
|
172
|
+
: buildContextBearingsFromState(state, buildProjectContextBlockForBroker(state.node.cwd), situationalContextBlock(nodeId), seen);
|
|
166
173
|
pi.sendMessage({
|
|
167
174
|
customType: CONTEXT_INTRO_CUSTOM_TYPE,
|
|
168
175
|
content,
|
|
169
176
|
display: true,
|
|
170
177
|
details: { nodeId },
|
|
171
178
|
});
|
|
179
|
+
// Persist what the baseline delivered so the dedup survives a later
|
|
180
|
+
// dormancy → revive(resume) into a fresh process.
|
|
181
|
+
if (seen !== undefined)
|
|
182
|
+
saveInjectedDocs(nodeId, seen);
|
|
172
183
|
});
|
|
173
184
|
}
|
|
174
185
|
export default registerCanvasContextIntro;
|
|
@@ -36,7 +36,7 @@ import { brokerExtensionState } from '../core/runtime/broker/daemon-ops.js';
|
|
|
36
36
|
import { renderPreferencesFromState } from '../core/runtime/broker-extension-render.js';
|
|
37
37
|
import { renderOnReadDocsForSubject } from '../core/substrate/on-read.js';
|
|
38
38
|
import { clearSessionCache } from '../core/substrate/session-cache.js';
|
|
39
|
-
import { loadInjectedDocs, saveInjectedDocs } from '../core/substrate/injected-store.js';
|
|
39
|
+
import { loadInjectedDocs, saveInjectedDocs, sharedInjectedDocs } from '../core/substrate/injected-store.js';
|
|
40
40
|
import { autoLoadedContextInner, mergeAutoLoadedContext } from './envelope-merge.js';
|
|
41
41
|
// ---------------------------------------------------------------------------
|
|
42
42
|
// Extension
|
|
@@ -65,7 +65,12 @@ export function registerCanvasDocSubstrate(pi) {
|
|
|
65
65
|
// dedup must carry forward. The one launch path that begins a FRESH transcript
|
|
66
66
|
// (reviveNode with resume=false) deletes the file, so a new conversation
|
|
67
67
|
// rehydrates empty here. See core/substrate/injected-store.ts.
|
|
68
|
-
|
|
68
|
+
//
|
|
69
|
+
// The set is the PROCESS-SHARED instance (sharedInjectedDocs), so the
|
|
70
|
+
// context-intro extension's workspace-open render grows the same set this
|
|
71
|
+
// hook dedups against — a front door delivered at boot never re-delivers on
|
|
72
|
+
// a later read beneath its store.
|
|
73
|
+
const injectedDocs = sharedInjectedDocs(nodeId);
|
|
69
74
|
pi.on('session_start', () => {
|
|
70
75
|
// Only the per-session substrate PARSE cache resets each session (so the
|
|
71
76
|
// corpus is re-scanned, picking up memory writes). injectedDocs is
|
|
@@ -112,6 +117,12 @@ export function registerCanvasDocSubstrate(pi) {
|
|
|
112
117
|
const expanded = rawPath.startsWith('~') ? join(homedir(), rawPath.slice(1)) : rawPath;
|
|
113
118
|
const absFile = resolve(cwd, expanded);
|
|
114
119
|
const state = await brokerExtensionState(nodeId);
|
|
120
|
+
// Merge the on-disk set: an out-of-process delivery (the daemon's
|
|
121
|
+
// warm-spare claim pushes bearings — including workspace-open docs —
|
|
122
|
+
// into the live session and persists what it delivered) must dedup here
|
|
123
|
+
// too. Cheap: one tiny JSON read per `read`.
|
|
124
|
+
for (const real of loadInjectedDocs(nodeId))
|
|
125
|
+
injectedDocs.add(real);
|
|
115
126
|
const injected = renderOnReadDocsForSubject(state.subject, absFile, injectedDocs);
|
|
116
127
|
if (injected === '')
|
|
117
128
|
return; // nothing surfaced — pass the read through unchanged
|
|
@@ -46,6 +46,10 @@ export declare function generatedContextText(message: GeneratedContextMessageLik
|
|
|
46
46
|
export declare function formatContextNudge(note: string): string;
|
|
47
47
|
export declare function isInboxDigest(text: string): boolean;
|
|
48
48
|
export declare function extractInboxSender(text: string): string | null;
|
|
49
|
+
/** Node ids whose section of this digest reports a finished node. Sections are
|
|
50
|
+
* delimited by their `From <sender>` headers, so a final never attaches to the
|
|
51
|
+
* neighbouring sender. */
|
|
52
|
+
export declare function inboxFinishedSenders(text: string): string[];
|
|
49
53
|
/** Exact producer-owned marker for the continuation a successor broker inserts
|
|
50
54
|
* after a runtime restart. Other recovery bodies intentionally do not match. */
|
|
51
55
|
export declare function isRuntimeRestartContinuation(message: GeneratedContextMessageLike): boolean;
|
|
@@ -101,14 +101,33 @@ function nudgeSummary(body) {
|
|
|
101
101
|
// Senders are node ids, the two reserved words, or the daemon itself (crtrd —
|
|
102
102
|
// review notes and other daemon-originated node messages).
|
|
103
103
|
const INBOX_HEADER_RE = /^From ([a-z0-9]+(?:-[a-z0-9]+)+|system|human|crtrd) — (\d+) update/m;
|
|
104
|
+
const INBOX_HEADERS_RE = /^From ([a-z0-9]+(?:-[a-z0-9]+)+|system|human|crtrd) — (\d+) update/gm;
|
|
105
|
+
// Entry lines inside a section, rendered by core/runtime/broker/inbox.ts as
|
|
106
|
+
// ` [<kind>]`. A final is the one kind that ends the sender's node.
|
|
107
|
+
const FINISHED_ENTRY_RE = /^ {2}\[(?:final|completed)\]/m;
|
|
104
108
|
export function isInboxDigest(text) {
|
|
105
109
|
return INBOX_HEADER_RE.test(text);
|
|
106
110
|
}
|
|
107
111
|
export function extractInboxSender(text) {
|
|
108
112
|
return INBOX_HEADER_RE.exec(text)?.[1] ?? null;
|
|
109
113
|
}
|
|
114
|
+
/** Node ids whose section of this digest reports a finished node. Sections are
|
|
115
|
+
* delimited by their `From <sender>` headers, so a final never attaches to the
|
|
116
|
+
* neighbouring sender. */
|
|
117
|
+
export function inboxFinishedSenders(text) {
|
|
118
|
+
const headers = [...text.matchAll(INBOX_HEADERS_RE)];
|
|
119
|
+
const senders = [];
|
|
120
|
+
for (const [index, header] of headers.entries()) {
|
|
121
|
+
const start = header.index ?? 0;
|
|
122
|
+
const end = headers[index + 1]?.index ?? text.length;
|
|
123
|
+
const sender = header[1];
|
|
124
|
+
if (sender !== undefined && FINISHED_ENTRY_RE.test(text.slice(start, end)))
|
|
125
|
+
senders.push(sender);
|
|
126
|
+
}
|
|
127
|
+
return senders;
|
|
128
|
+
}
|
|
110
129
|
function inboxSummary(body) {
|
|
111
|
-
const headers = [...body.matchAll(
|
|
130
|
+
const headers = [...body.matchAll(INBOX_HEADERS_RE)];
|
|
112
131
|
if (headers.length === 0)
|
|
113
132
|
return 'node update';
|
|
114
133
|
const updates = headers.reduce((total, match) => total + Number(match[2] ?? 0), 0);
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/** Broker-generated activity recap for one completed tool-call group. Changed
|
|
2
|
-
* files are counted deterministically by the viewer's
|
|
3
|
-
* deletions have no tool of their own, so only the model
|
|
2
|
+
* files and spawned/finished nodes are counted deterministically by the viewer's
|
|
3
|
+
* GroupActivityRecorder; deletions have no tool of their own, so only the model
|
|
4
|
+
* can evidence them. */
|
|
4
5
|
export interface ToolGroupSummary {
|
|
5
6
|
bullets: string[];
|
|
6
|
-
nodesSpawned: number;
|
|
7
7
|
filesDeleted: number;
|
|
8
8
|
}
|
|
9
9
|
export interface ToolGroupMessageLike {
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// Keep this independent of renderer state: both live event tracking and snapshot
|
|
3
3
|
// reconstruction must agree on which prose closes a run of tools.
|
|
4
4
|
import { generatedContextPresentation, generatedContextText } from './generated-context.js';
|
|
5
|
-
const TOOL_GROUP_SUMMARY_KEYS = new Set(['bullets', '
|
|
5
|
+
const TOOL_GROUP_SUMMARY_KEYS = new Set(['bullets', 'filesDeleted']);
|
|
6
6
|
const MAX_TOOL_GROUP_BULLETS = 5;
|
|
7
7
|
const MAX_TOOL_GROUP_BULLET_LENGTH = 1_000;
|
|
8
8
|
/** Validate the one durable summary shape and cap its display-sized fields.
|
|
@@ -19,15 +19,12 @@ export function normalizeToolGroupSummary(value) {
|
|
|
19
19
|
return null;
|
|
20
20
|
if (!record.bullets.every((bullet) => typeof bullet === 'string' && bullet.trim() !== ''))
|
|
21
21
|
return null;
|
|
22
|
-
if (typeof record.nodesSpawned !== 'number' || !Number.isSafeInteger(record.nodesSpawned) || record.nodesSpawned < 0)
|
|
23
|
-
return null;
|
|
24
22
|
if (typeof record.filesDeleted !== 'number' || !Number.isSafeInteger(record.filesDeleted) || record.filesDeleted < 0)
|
|
25
23
|
return null;
|
|
26
24
|
return {
|
|
27
25
|
bullets: record.bullets
|
|
28
26
|
.slice(0, MAX_TOOL_GROUP_BULLETS)
|
|
29
27
|
.map((bullet) => bullet.trim().slice(0, MAX_TOOL_GROUP_BULLET_LENGTH).trimEnd()),
|
|
30
|
-
nodesSpawned: record.nodesSpawned,
|
|
31
28
|
filesDeleted: record.filesDeleted,
|
|
32
29
|
};
|
|
33
30
|
}
|
package/package.json
CHANGED
package/runtime.lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@north-light/crouter",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.183",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@north-light/crouter",
|
|
9
|
-
"version": "0.3.
|
|
9
|
+
"version": "0.3.183",
|
|
10
10
|
"hasInstallScript": true,
|
|
11
11
|
"license": "MIT",
|
|
12
12
|
"dependencies": {
|
package/scripts/postinstall.mjs
CHANGED
|
@@ -40,7 +40,7 @@ async function main() {
|
|
|
40
40
|
console.warn('[crtr postinstall] runtime installation failed (non-fatal); no runtime generation was produced. Run `crtr` for remediation guidance or reinstall:', error?.message ?? error);
|
|
41
41
|
}
|
|
42
42
|
await provisionRenderer();
|
|
43
|
-
console.log('crouter installed. Run `crtr sys setup` to install the companion pi extensions, Claude OAuth, tmux/nvim
|
|
43
|
+
console.log('crouter installed. Run `crtr sys setup` to install the companion pi extensions, Claude OAuth, and tmux/nvim.');
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
await main();
|
|
@@ -1,93 +0,0 @@
|
|
|
1
|
-
// Terminal-specific Markdown heading hierarchy for the attach viewer.
|
|
2
|
-
//
|
|
3
|
-
// pi-tui's MarkdownTheme exposes one `heading(text)` callback without the
|
|
4
|
-
// heading depth. Its renderer does know the depth, but does not pass it to the
|
|
5
|
-
// theme, so every heading otherwise receives the same yellow mdHeading paint.
|
|
6
|
-
// Decorate only the source text of H3–H6 before it reaches pi-tui: pi still
|
|
7
|
-
// owns parsing, wrapping, inline formatting, and H1/H2's established styling.
|
|
8
|
-
// This stays attach-local — persisted messages and copied text remain ordinary
|
|
9
|
-
// Markdown without terminal escape sequences.
|
|
10
|
-
//
|
|
11
|
-
// The same per-block copy carries the viewer's other source-text transform:
|
|
12
|
-
// private-use `U+XXXX` references become their glyphs (see glyph-codepoints.ts),
|
|
13
|
-
// so an agent that cannot type a Nerd Font character can still put one on
|
|
14
|
-
// screen.
|
|
15
|
-
import { expandGlyphCodepoints } from './glyph-codepoints.js';
|
|
16
|
-
const ATX_HEADING = /^(\s{0,3})(#{3,6})(?:[ \t]+|$)(.*?)([ \t]+#+[ \t]*)?$/;
|
|
17
|
-
const FENCE_OPEN = /^\s{0,3}(`{3,}|~{3,})/;
|
|
18
|
-
// A constant-lightness ramp from warm yellow to cool blue, with saturation
|
|
19
|
-
// deliberately reduced at each step so the hierarchy recedes rather than
|
|
20
|
-
// becoming a tour of unrelated accent colors.
|
|
21
|
-
const HEADING_COLORS = [
|
|
22
|
-
[232, 194, 104],
|
|
23
|
-
[207, 194, 145],
|
|
24
|
-
[181, 184, 174],
|
|
25
|
-
[157, 170, 180],
|
|
26
|
-
[135, 153, 164],
|
|
27
|
-
[116, 135, 147],
|
|
28
|
-
];
|
|
29
|
-
function headingColor(level) {
|
|
30
|
-
const [r, g, b] = HEADING_COLORS[level - 1];
|
|
31
|
-
return (text) => `\x1b[1;38;2;${r};${g};${b}m${text}\x1b[22;39m`;
|
|
32
|
-
}
|
|
33
|
-
/** Style ATX headings with a scan-friendly terminal hierarchy. The Markdown
|
|
34
|
-
* markers are syntax, not content, so omit them from the rendered line.
|
|
35
|
-
* Fenced examples remain literal Markdown, never decorated terminal output. */
|
|
36
|
-
export function styleAttachMarkdownHeadings(markdown) {
|
|
37
|
-
let fence;
|
|
38
|
-
return markdown.split('\n').map((line) => {
|
|
39
|
-
const fenceMatch = FENCE_OPEN.exec(line);
|
|
40
|
-
if (fence !== undefined) {
|
|
41
|
-
if (fenceMatch?.[1][0] === fence)
|
|
42
|
-
fence = undefined;
|
|
43
|
-
return line;
|
|
44
|
-
}
|
|
45
|
-
if (fenceMatch) {
|
|
46
|
-
fence = fenceMatch[1][0];
|
|
47
|
-
return line;
|
|
48
|
-
}
|
|
49
|
-
const match = ATX_HEADING.exec(line);
|
|
50
|
-
if (!match)
|
|
51
|
-
return line;
|
|
52
|
-
const [, indent, hashes, body, closing = ''] = match;
|
|
53
|
-
if (body === '')
|
|
54
|
-
return line;
|
|
55
|
-
return `${indent}${headingColor(hashes.length)(body)}`;
|
|
56
|
-
}).join('\n');
|
|
57
|
-
}
|
|
58
|
-
/** Every source-text transform the viewer applies to a Markdown-bearing block,
|
|
59
|
-
* in one place: heading hierarchy, then glyph expansion. */
|
|
60
|
-
function renderableText(markdown) {
|
|
61
|
-
return expandGlyphCodepoints(styleAttachMarkdownHeadings(markdown));
|
|
62
|
-
}
|
|
63
|
-
/** Copy a pi message for terminal rendering and decorate only Markdown-bearing
|
|
64
|
-
* text/thinking blocks. The broker's source event is never mutated. */
|
|
65
|
-
export function styleAttachMessageMarkdown(message) {
|
|
66
|
-
if (typeof message !== 'object' || message === null || !('content' in message))
|
|
67
|
-
return message;
|
|
68
|
-
const candidate = message;
|
|
69
|
-
if (typeof candidate.content === 'string') {
|
|
70
|
-
return { ...candidate, content: renderableText(candidate.content) };
|
|
71
|
-
}
|
|
72
|
-
if (!Array.isArray(candidate.content))
|
|
73
|
-
return message;
|
|
74
|
-
return {
|
|
75
|
-
...candidate,
|
|
76
|
-
content: candidate.content.map((part) => {
|
|
77
|
-
if (typeof part !== 'object' || part === null)
|
|
78
|
-
return part;
|
|
79
|
-
const block = part;
|
|
80
|
-
if (block.type === 'text' && typeof block.text === 'string') {
|
|
81
|
-
return { ...block, text: renderableText(block.text) };
|
|
82
|
-
}
|
|
83
|
-
if (block.type === 'thinking' && typeof block.thinking === 'string') {
|
|
84
|
-
return { ...block, thinking: renderableText(block.thinking) };
|
|
85
|
-
}
|
|
86
|
-
return part;
|
|
87
|
-
}),
|
|
88
|
-
};
|
|
89
|
-
}
|
|
90
|
-
/** Copy an expanded pi summary without changing its persisted source. */
|
|
91
|
-
export function styleAttachSummaryMarkdown(message) {
|
|
92
|
-
return { ...message, summary: renderableText(message.summary) };
|
|
93
|
-
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export declare const answerLeaf: import("../../core/command.js").LeafDef;
|