@north-light/crouter 0.3.162 → 0.3.164
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/builtin-memory/00-runtime-base.md +1 -0
- package/dist/builtin-memory/04-orchestration-kernel.md +1 -0
- package/dist/builtin-memory/init.md +38 -0
- package/dist/builtin-memory/internal/INDEX.md +1 -0
- package/dist/builtin-memory/internal/memory-loading.md +45 -0
- package/dist/builtin-memory/wedged-child-on-runaway-bash.md +2 -2
- package/dist/builtin-pi-packages/pi-crtr-extensions/README.md +0 -1
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/__tests__/provider-rotation.test.ts +34 -2
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/provider-rotation.ts +14 -3
- package/dist/clients/attach/__tests__/crtr-output-coverage.test.js +1 -1
- package/dist/clients/attach/__tests__/crtr-output.test.js +14 -9
- package/dist/clients/attach/__tests__/edit-diff.test.d.ts +1 -0
- package/dist/clients/attach/__tests__/edit-diff.test.js +35 -0
- package/dist/clients/attach/__tests__/editor-frame-title.test.d.ts +1 -0
- package/dist/clients/attach/__tests__/editor-frame-title.test.js +19 -0
- package/dist/clients/attach/render/chat-view.js +54 -38
- package/dist/clients/attach/render/crtr-output.d.ts +2 -1
- package/dist/clients/attach/render/crtr-output.js +33 -4
- package/dist/clients/attach/render/edit-diff.d.ts +21 -6
- package/dist/clients/attach/render/edit-diff.js +104 -81
- package/dist/clients/attach/render/tool-calls.d.ts +21 -20
- package/dist/clients/attach/render/tool-calls.js +89 -43
- package/dist/clients/attach/session/connection.d.ts +1 -1
- package/dist/clients/attach/session/editor-frame.d.ts +5 -1
- package/dist/clients/attach/session/editor-frame.js +21 -15
- package/dist/clients/attach/session/identity.d.ts +1 -1
- package/dist/clients/attach/viewer.js +628 -624
- package/dist/commands/__tests__/api-canvas-source.test.d.ts +1 -0
- package/dist/commands/__tests__/api-canvas-source.test.js +37 -0
- package/dist/commands/__tests__/human.test.js +2 -0
- package/dist/commands/__tests__/search-contents.test.d.ts +1 -0
- package/dist/commands/__tests__/search-contents.test.js +52 -0
- package/dist/commands/api-client.js +1 -0
- package/dist/commands/human/prompts.js +2 -2
- package/dist/commands/human/shared.d.ts +1 -0
- package/dist/commands/human/shared.js +7 -4
- package/dist/commands/memory/lint.d.ts +6 -0
- package/dist/commands/memory/lint.js +177 -35
- package/dist/commands/memory/read.js +22 -1
- package/dist/commands/memory/write.js +18 -4
- package/dist/commands/memory.js +1 -1
- package/dist/commands/search/contents.js +2 -2
- package/dist/commands/sys/__tests__/sync-deps.test.js +12 -21
- package/dist/commands/sys/__tests__/sync-import.test.js +30 -29
- package/dist/commands/sys/setup-wizard.d.ts +8 -1
- package/dist/commands/sys/setup-wizard.js +209 -58
- package/dist/commands/sys/sync-deps.js +14 -18
- package/dist/commands/sys/sync-project-guidance.js +16 -19
- package/dist/core/__tests__/migration.test.js +8 -3
- package/dist/core/__tests__/on-read-crouter-home-fence.test.js +7 -10
- package/dist/core/__tests__/on-read-dedup-resume.test.js +13 -25
- package/dist/core/__tests__/on-read-identity.test.js +8 -15
- package/dist/core/__tests__/revive.test.js +2 -2
- package/dist/core/__tests__/tmux-surface.test.js +10 -4
- package/dist/core/__tests__/worktree.test.js +3 -3
- package/dist/core/canvas/extensions.d.ts +1 -1
- package/dist/core/canvas/extensions.js +18 -11
- package/dist/core/canvas/labels.d.ts +4 -5
- package/dist/core/canvas/labels.js +9 -9
- package/dist/core/command-manifests/schema.js +18 -5
- package/dist/core/command.js +17 -7
- package/dist/core/configured-clis/invoker.js +2 -0
- package/dist/core/help.d.ts +3 -1
- package/dist/core/help.js +5 -2
- package/dist/core/keybindings/__tests__/resolve.test.js +5 -2
- package/dist/core/keybindings/catalog.d.ts +3 -2
- package/dist/core/keybindings/catalog.js +34 -25
- package/dist/core/keybindings/index.d.ts +1 -1
- package/dist/core/keybindings/resolve.js +8 -0
- package/dist/core/keybindings/types.d.ts +6 -1
- package/dist/core/memory/doc-link-grammar.d.ts +20 -0
- package/dist/core/memory/doc-link-grammar.js +110 -0
- package/dist/core/memory-resolver.d.ts +5 -0
- package/dist/core/memory-resolver.js +12 -1
- package/dist/core/runtime/bearings.d.ts +8 -8
- package/dist/core/runtime/bearings.js +20 -16
- package/dist/core/runtime/broker.js +3 -13
- package/dist/core/runtime/canvas-extensions.d.ts +1 -0
- package/dist/core/runtime/canvas-extensions.js +2 -0
- package/dist/core/runtime/launch.d.ts +1 -1
- package/dist/core/runtime/launch.js +1 -1
- package/dist/core/runtime/tmux.js +136 -100
- package/dist/core/scope.js +4 -5
- package/dist/core/substrate/ceiling.d.ts +3 -6
- package/dist/core/substrate/ceiling.js +13 -15
- package/dist/core/substrate/index.d.ts +2 -3
- package/dist/core/substrate/index.js +2 -2
- package/dist/core/substrate/on-read.d.ts +4 -13
- package/dist/core/substrate/on-read.js +142 -262
- package/dist/core/substrate/render.js +4 -4
- package/dist/core/substrate/schema.d.ts +3 -2
- package/dist/core/substrate/schema.js +5 -5
- package/dist/pi-extensions/__tests__/canvas-tool-guide.test.d.ts +1 -0
- package/dist/pi-extensions/__tests__/canvas-tool-guide.test.js +96 -0
- package/dist/pi-extensions/canvas-doc-substrate.js +7 -8
- package/dist/pi-extensions/canvas-tool-guide.d.ts +14 -0
- package/dist/pi-extensions/canvas-tool-guide.js +77 -0
- package/package.json +2 -2
- package/runtime.lock.json +6 -6
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/crouter-help.ts +0 -95
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
// Run with: node --import tsx/esm --test src/pi-extensions/__tests__/canvas-tool-guide.test.ts
|
|
2
|
+
//
|
|
3
|
+
// Regression: canvas brokers must load and inject the root crtr guide themselves;
|
|
4
|
+
// optional pi packages are not part of the canvas runtime contract.
|
|
5
|
+
import { test } from 'node:test';
|
|
6
|
+
import assert from 'node:assert/strict';
|
|
7
|
+
import { chmodSync, mkdirSync, mkdtempSync, readFileSync, realpathSync, rmSync, writeFileSync } from 'node:fs';
|
|
8
|
+
import { tmpdir } from 'node:os';
|
|
9
|
+
import { delimiter, join } from 'node:path';
|
|
10
|
+
import { CANVAS_EXTENSIONS, CANVAS_TOOL_GUIDE_PATH } from '../../core/runtime/canvas-extensions.js';
|
|
11
|
+
import registerCanvasToolGuide from '../canvas-tool-guide.js';
|
|
12
|
+
function fakePi() {
|
|
13
|
+
return {
|
|
14
|
+
on(event, handler) {
|
|
15
|
+
if (event === 'before_agent_start')
|
|
16
|
+
this.beforeAgentStart = handler;
|
|
17
|
+
if (event === 'session_start')
|
|
18
|
+
this.sessionStart = handler;
|
|
19
|
+
},
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
test('canvas nodes load the cwd-specific root guide, share its boot cache, and never capture during a turn', async () => {
|
|
23
|
+
assert.ok(CANVAS_EXTENSIONS.includes(CANVAS_TOOL_GUIDE_PATH), 'the guide is a mandatory canvas extension');
|
|
24
|
+
const root = mkdtempSync(join(tmpdir(), 'crtr-tool-guide-'));
|
|
25
|
+
const bin = join(root, 'bin');
|
|
26
|
+
const cwd = join(root, 'configured-project');
|
|
27
|
+
const countFile = join(root, 'capture-count');
|
|
28
|
+
const capturedCwdFile = join(root, 'capture-cwd');
|
|
29
|
+
const previous = {
|
|
30
|
+
cwd: process.cwd(),
|
|
31
|
+
home: process.env['HOME'],
|
|
32
|
+
nodeId: process.env['CRTR_NODE_ID'],
|
|
33
|
+
path: process.env['PATH'],
|
|
34
|
+
};
|
|
35
|
+
try {
|
|
36
|
+
mkdirSync(bin);
|
|
37
|
+
mkdirSync(cwd);
|
|
38
|
+
const command = join(bin, 'crtr');
|
|
39
|
+
writeFileSync(command, `#!/bin/sh
|
|
40
|
+
count=0
|
|
41
|
+
if [ -f "$CAPTURE_COUNT" ]; then count=$(cat "$CAPTURE_COUNT"); fi
|
|
42
|
+
printf '%s' "$((count + 1))" > "$CAPTURE_COUNT"
|
|
43
|
+
printf '%s' "$PWD" > "$CAPTURE_CWD"
|
|
44
|
+
printf '%s\\n' 'configured root guide'
|
|
45
|
+
`);
|
|
46
|
+
chmodSync(command, 0o755);
|
|
47
|
+
process.chdir(cwd);
|
|
48
|
+
process.env['HOME'] = root;
|
|
49
|
+
process.env['CRTR_NODE_ID'] = 'canvas-tool-guide-test';
|
|
50
|
+
process.env['PATH'] = `${bin}${delimiter}${previous.path ?? ''}`;
|
|
51
|
+
process.env['CAPTURE_COUNT'] = countFile;
|
|
52
|
+
process.env['CAPTURE_CWD'] = capturedCwdFile;
|
|
53
|
+
const first = fakePi();
|
|
54
|
+
registerCanvasToolGuide(first);
|
|
55
|
+
const firstSessionStart = first.sessionStart;
|
|
56
|
+
assert.ok(firstSessionStart, 'session-start capture is registered');
|
|
57
|
+
await firstSessionStart();
|
|
58
|
+
const second = fakePi();
|
|
59
|
+
registerCanvasToolGuide(second);
|
|
60
|
+
const secondSessionStart = second.sessionStart;
|
|
61
|
+
assert.ok(secondSessionStart, 'session-start capture is registered for every broker');
|
|
62
|
+
await secondSessionStart();
|
|
63
|
+
assert.equal(readFileSync(countFile, 'utf8'), '1', 'a sibling broker uses the 15-second disk cache');
|
|
64
|
+
assert.equal(readFileSync(capturedCwdFile, 'utf8'), realpathSync(cwd), 'root help discovers commands from the node cwd');
|
|
65
|
+
const beforeAgentStart = second.beforeAgentStart;
|
|
66
|
+
assert.ok(beforeAgentStart, 'turn-time guide injection is registered');
|
|
67
|
+
const prompt = 'Available tools\n\nGuidelines:\nUse the available tools.';
|
|
68
|
+
assert.equal(beforeAgentStart({ systemPrompt: prompt })?.systemPrompt, 'Available tools\n\n<crtr-tool-guide>\nconfigured root guide\n</crtr-tool-guide>\n\nGuidelines:\nUse the available tools.', 'the captured guide sits in the tool-selection frame');
|
|
69
|
+
rmSync(join(root, '.crouter'), { recursive: true, force: true });
|
|
70
|
+
const unbooted = fakePi();
|
|
71
|
+
registerCanvasToolGuide(unbooted);
|
|
72
|
+
const unbootedBeforeAgentStart = unbooted.beforeAgentStart;
|
|
73
|
+
assert.ok(unbootedBeforeAgentStart, 'turn-time guide injection is registered before boot capture');
|
|
74
|
+
assert.equal(unbootedBeforeAgentStart({ systemPrompt: prompt }), undefined, 'a node without a boot capture starts without a guide');
|
|
75
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
76
|
+
assert.equal(readFileSync(countFile, 'utf8'), '1', 'a turn never starts a root-help subprocess');
|
|
77
|
+
}
|
|
78
|
+
finally {
|
|
79
|
+
process.chdir(previous.cwd);
|
|
80
|
+
if (previous.home === undefined)
|
|
81
|
+
delete process.env['HOME'];
|
|
82
|
+
else
|
|
83
|
+
process.env['HOME'] = previous.home;
|
|
84
|
+
if (previous.nodeId === undefined)
|
|
85
|
+
delete process.env['CRTR_NODE_ID'];
|
|
86
|
+
else
|
|
87
|
+
process.env['CRTR_NODE_ID'] = previous.nodeId;
|
|
88
|
+
if (previous.path === undefined)
|
|
89
|
+
delete process.env['PATH'];
|
|
90
|
+
else
|
|
91
|
+
process.env['PATH'] = previous.path;
|
|
92
|
+
delete process.env['CAPTURE_COUNT'];
|
|
93
|
+
delete process.env['CAPTURE_CWD'];
|
|
94
|
+
rmSync(root, { recursive: true, force: true });
|
|
95
|
+
}
|
|
96
|
+
});
|
|
@@ -12,16 +12,15 @@
|
|
|
12
12
|
// into `event.systemPrompt`, right after pi's native "Available tools" list
|
|
13
13
|
// (before the `\n\nGuidelines:` anchor), so the substrate's standing
|
|
14
14
|
// directives sit in the tool-selection frame the agent reads while choosing
|
|
15
|
-
// a capability
|
|
16
|
-
// back to appending when the anchor is absent. (The `<memory kind="knowledge">`
|
|
15
|
+
// a capability. Falls back to appending when the anchor is absent. (The `<memory kind="knowledge">`
|
|
17
16
|
// catalog rides the bearings message instead — see core/substrate/render.ts.)
|
|
18
17
|
//
|
|
19
18
|
// 2. tool_result (gated on `read`) — the ON-READ half. For each non-error
|
|
20
|
-
// `read`,
|
|
21
|
-
//
|
|
22
|
-
// file-read
|
|
23
|
-
// before the file contents. A
|
|
24
|
-
//
|
|
19
|
+
// `read`, evaluates explicit `applies-to` globs (plus `read-when` metadata)
|
|
20
|
+
// across the resolved corpus and any project stores enclosing the read
|
|
21
|
+
// file. Matches render at their file-read rung in `<auto-loaded-context>`
|
|
22
|
+
// before the file contents. A transcript-scoped realpath set deduplicates
|
|
23
|
+
// repeated reads and resumed broker processes.
|
|
25
24
|
//
|
|
26
25
|
// Both handler bodies are wrapped in try/catch and degrade to INERT on error —
|
|
27
26
|
// a substrate bug must never brick node boot or break a `read`, only quietly
|
|
@@ -42,7 +41,7 @@ import { loadInjectedDocs, saveInjectedDocs } from '../core/substrate/injected-s
|
|
|
42
41
|
/** The stable seam that closes pi's native "Available tools" list. We splice the
|
|
43
42
|
* substrate's boot sections right before it — the agent decides which capability
|
|
44
43
|
* to reach for while reading the tools, so memory docs/preferences must sit
|
|
45
|
-
* there, not far below.
|
|
44
|
+
* there, not far below. Falls back to appending if absent. */
|
|
46
45
|
const TOOLS_ANCHOR = '\n\nGuidelines:';
|
|
47
46
|
const AUTO_CONTEXT_RE = /<auto-loaded-context(?:\s[^>]*)?>\n([\s\S]*?)\n<\/auto-loaded-context>/;
|
|
48
47
|
function mergeAutoLoadedContext(injected, content) {
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
declare function injectToolGuide(systemPrompt: string, helpText: string): string;
|
|
2
|
+
interface BeforeAgentStartEventLike {
|
|
3
|
+
systemPrompt: string;
|
|
4
|
+
}
|
|
5
|
+
interface PiLike {
|
|
6
|
+
on(event: 'session_start', handler: () => void | Promise<void>): void;
|
|
7
|
+
on(event: 'before_agent_start', handler: (event: BeforeAgentStartEventLike) => {
|
|
8
|
+
systemPrompt: string;
|
|
9
|
+
} | void): void;
|
|
10
|
+
}
|
|
11
|
+
/** Register the root crtr tool-guide injection for a canvas node. */
|
|
12
|
+
export declare function registerCanvasToolGuide(pi: PiLike): void;
|
|
13
|
+
export { injectToolGuide };
|
|
14
|
+
export default registerCanvasToolGuide;
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
// canvas-tool-guide.ts — injects the root crtr CLI guide into every canvas node.
|
|
2
|
+
//
|
|
3
|
+
// Capturing `crtr -h` builds the full configured command tree, including commands
|
|
4
|
+
// discovered from the node's cwd. It runs only at session start; before_agent_start
|
|
5
|
+
// injects the last successful capture without starting work or waiting.
|
|
6
|
+
import { execFile } from 'node:child_process';
|
|
7
|
+
import { createHash } from 'node:crypto';
|
|
8
|
+
import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
9
|
+
import { homedir } from 'node:os';
|
|
10
|
+
import { dirname, join } from 'node:path';
|
|
11
|
+
import { promisify } from 'node:util';
|
|
12
|
+
const run = promisify(execFile);
|
|
13
|
+
const TTL_MS = 15_000;
|
|
14
|
+
const TOOLS_ANCHOR = '\n\nGuidelines:';
|
|
15
|
+
function helpCacheFile(cwd) {
|
|
16
|
+
const hash = createHash('sha1').update(cwd).digest('hex').slice(0, 16);
|
|
17
|
+
return join(homedir(), '.crouter', 'cache', `crtr-tool-guide-${hash}.json`);
|
|
18
|
+
}
|
|
19
|
+
function injectToolGuide(systemPrompt, helpText) {
|
|
20
|
+
const block = `<crtr-tool-guide>\n${helpText}\n</crtr-tool-guide>`;
|
|
21
|
+
const anchor = systemPrompt.indexOf(TOOLS_ANCHOR);
|
|
22
|
+
if (anchor === -1)
|
|
23
|
+
return `${systemPrompt}\n\n${block}`;
|
|
24
|
+
return `${systemPrompt.slice(0, anchor)}\n\n${block}${systemPrompt.slice(anchor)}`;
|
|
25
|
+
}
|
|
26
|
+
/** Register the root crtr tool-guide injection for a canvas node. */
|
|
27
|
+
export function registerCanvasToolGuide(pi) {
|
|
28
|
+
const nodeId = process.env['CRTR_NODE_ID'];
|
|
29
|
+
if (nodeId === undefined || nodeId.trim() === '')
|
|
30
|
+
return;
|
|
31
|
+
let helpText = '';
|
|
32
|
+
let capturedAt = 0;
|
|
33
|
+
async function refresh() {
|
|
34
|
+
if (helpText !== '' && Date.now() - capturedAt < TTL_MS)
|
|
35
|
+
return;
|
|
36
|
+
const file = helpCacheFile(process.cwd());
|
|
37
|
+
try {
|
|
38
|
+
const cached = JSON.parse(readFileSync(file, 'utf8'));
|
|
39
|
+
if (typeof cached.text === 'string' && cached.text !== '' && typeof cached.at === 'number' && Date.now() - cached.at < TTL_MS) {
|
|
40
|
+
helpText = cached.text;
|
|
41
|
+
capturedAt = cached.at;
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
// A missing or invalid cache is refreshed below.
|
|
47
|
+
}
|
|
48
|
+
try {
|
|
49
|
+
const { stdout } = await run('crtr', ['-h'], { timeout: 10_000 });
|
|
50
|
+
helpText = stdout.trim();
|
|
51
|
+
capturedAt = Date.now();
|
|
52
|
+
try {
|
|
53
|
+
mkdirSync(dirname(file), { recursive: true });
|
|
54
|
+
writeFileSync(file, JSON.stringify({ text: helpText, at: capturedAt }), 'utf8');
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
// The guide remains available in this broker even when its cache cannot persist.
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
catch (err) {
|
|
61
|
+
// Keep the last successful guide and never delay a turn for a failed capture.
|
|
62
|
+
// Say so once in the broker log: a node silently missing its whole tool guide
|
|
63
|
+
// is otherwise indistinguishable from one that has it.
|
|
64
|
+
console.error('[canvas-tool-guide] crtr -h capture failed:', err);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
// Node startup is the only point a capture may wait. Warm spares pay it before
|
|
68
|
+
// a user claims them; resumed nodes refresh before their next turn.
|
|
69
|
+
pi.on('session_start', refresh);
|
|
70
|
+
pi.on('before_agent_start', (event) => {
|
|
71
|
+
if (helpText === '')
|
|
72
|
+
return;
|
|
73
|
+
return { systemPrompt: injectToolGuide(event.systemPrompt, helpText) };
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
export { injectToolGuide };
|
|
77
|
+
export default registerCanvasToolGuide;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@north-light/crouter",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.164",
|
|
4
4
|
"description": "crtr — agent runtime with memory, plugins, and marketplaces",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -107,7 +107,7 @@
|
|
|
107
107
|
},
|
|
108
108
|
"license": "MIT",
|
|
109
109
|
"dependencies": {
|
|
110
|
-
"@crouton-kit/humanloop": "0.4.
|
|
110
|
+
"@crouton-kit/humanloop": "0.4.21",
|
|
111
111
|
"@earendil-works/pi-agent-core": "0.83.0",
|
|
112
112
|
"@earendil-works/pi-ai": "0.83.0",
|
|
113
113
|
"@earendil-works/pi-coding-agent": "0.83.0",
|
package/runtime.lock.json
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@north-light/crouter",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.164",
|
|
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.164",
|
|
10
10
|
"hasInstallScript": true,
|
|
11
11
|
"license": "MIT",
|
|
12
12
|
"dependencies": {
|
|
13
|
-
"@crouton-kit/humanloop": "0.4.
|
|
13
|
+
"@crouton-kit/humanloop": "0.4.21",
|
|
14
14
|
"@earendil-works/pi-agent-core": "0.83.0",
|
|
15
15
|
"@earendil-works/pi-ai": "0.83.0",
|
|
16
16
|
"@earendil-works/pi-coding-agent": "0.83.0",
|
|
@@ -2073,9 +2073,9 @@
|
|
|
2073
2073
|
}
|
|
2074
2074
|
},
|
|
2075
2075
|
"node_modules/@crouton-kit/humanloop": {
|
|
2076
|
-
"version": "0.4.
|
|
2077
|
-
"resolved": "https://registry.npmjs.org/@crouton-kit/humanloop/-/humanloop-0.4.
|
|
2078
|
-
"integrity": "sha512-
|
|
2076
|
+
"version": "0.4.21",
|
|
2077
|
+
"resolved": "https://registry.npmjs.org/@crouton-kit/humanloop/-/humanloop-0.4.21.tgz",
|
|
2078
|
+
"integrity": "sha512-j/8dVAhl2R4dWvykSjC+yCEEb/n7P+P1FZLEFWD1pVB24tubYVmA3vJSr359nOO8esmJ+h/+S8Oef8eGZ6n6tA==",
|
|
2079
2079
|
"hasInstallScript": true,
|
|
2080
2080
|
"workspaces": [
|
|
2081
2081
|
"web"
|
|
@@ -1,95 +0,0 @@
|
|
|
1
|
-
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import { execFile } from "node:child_process";
|
|
3
|
-
import { promisify } from "node:util";
|
|
4
|
-
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
5
|
-
import { homedir } from "node:os";
|
|
6
|
-
import { createHash } from "node:crypto";
|
|
7
|
-
import { dirname, join } from "node:path";
|
|
8
|
-
|
|
9
|
-
const run = promisify(execFile);
|
|
10
|
-
|
|
11
|
-
// `crtr -h` is the auto-loaded tool guide. Capturing it costs a real subprocess:
|
|
12
|
-
// bare `crtr -h` is the one invocation that must build the FULL command tree, so
|
|
13
|
-
// it loads every subtree's module graph — ~250ms on a laptop, ~320ms in the
|
|
14
|
-
// guest container. The guide's content is static per crouter version + installed
|
|
15
|
-
// plugins, so that cost must never sit in front of a turn. The TTL only exists
|
|
16
|
-
// to pick a tree change (an upgrade, a newly installed plugin) up eventually.
|
|
17
|
-
const TTL_MS = 15_000;
|
|
18
|
-
|
|
19
|
-
// The TTL cache is also persisted to disk, keyed by cwd (skills resolve by
|
|
20
|
-
// scope, so the guide is cwd-specific). Every broker/node is a FRESH process, so
|
|
21
|
-
// the in-memory cache alone is always cold at session_start — each boot would
|
|
22
|
-
// re-spawn `crtr -h`. The disk layer lets a cluster of node boots within one TTL
|
|
23
|
-
// window (daemon revives, child spawns, rapid relaunches) share a single capture.
|
|
24
|
-
function helpCacheFile(cwd: string): string {
|
|
25
|
-
const h = createHash("sha1").update(cwd).digest("hex").slice(0, 16);
|
|
26
|
-
return join(homedir(), ".crouter", "cache", `crouter-help-${h}.json`);
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export default function (pi: ExtensionAPI) {
|
|
30
|
-
let helpText = "";
|
|
31
|
-
let capturedAt = 0;
|
|
32
|
-
|
|
33
|
-
async function refresh(): Promise<void> {
|
|
34
|
-
if (Date.now() - capturedAt < TTL_MS && helpText) return; // in-memory fresh
|
|
35
|
-
const file = helpCacheFile(process.cwd());
|
|
36
|
-
|
|
37
|
-
// Cross-process disk cache: reuse a recent capture from a sibling boot.
|
|
38
|
-
try {
|
|
39
|
-
const c = JSON.parse(readFileSync(file, "utf8")) as { text?: string; at?: number };
|
|
40
|
-
if (typeof c.text === "string" && c.text && typeof c.at === "number" && Date.now() - c.at < TTL_MS) {
|
|
41
|
-
helpText = c.text;
|
|
42
|
-
capturedAt = c.at;
|
|
43
|
-
return; // fresh on disk — skip the spawn
|
|
44
|
-
}
|
|
45
|
-
} catch {
|
|
46
|
-
// miss / corrupt — fall through to re-probe
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
try {
|
|
50
|
-
const { stdout } = await run("crouter", ["-h"], { timeout: 10_000 });
|
|
51
|
-
helpText = stdout.trim();
|
|
52
|
-
capturedAt = Date.now();
|
|
53
|
-
try {
|
|
54
|
-
mkdirSync(dirname(file), { recursive: true });
|
|
55
|
-
writeFileSync(file, JSON.stringify({ text: helpText, at: capturedAt }), "utf8");
|
|
56
|
-
} catch {
|
|
57
|
-
// best-effort: a missing cache just means the next boot re-probes
|
|
58
|
-
}
|
|
59
|
-
} catch {
|
|
60
|
-
// soft-fail: keep last-known text (or empty); never block the turn
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
// Boot-time capture: the one place a refresh may block, because it happens at
|
|
65
|
-
// node mint/revive, never in front of a user's turn. A warm-pool spare has
|
|
66
|
-
// therefore already paid it before it is ever claimed.
|
|
67
|
-
pi.on("session_start", refresh);
|
|
68
|
-
|
|
69
|
-
pi.on("before_agent_start", (event) => {
|
|
70
|
-
// NEVER await here. A stale-by-TTL capture used to re-spawn `crtr -h`
|
|
71
|
-
// synchronously, putting its full subprocess cost between the delivered
|
|
72
|
-
// message and agent_start — measured as the single largest term in a
|
|
73
|
-
// Northlight first-message ack. Serve the last-known text and let the
|
|
74
|
-
// refresh land for the next turn instead.
|
|
75
|
-
if (Date.now() - capturedAt >= TTL_MS) void refresh();
|
|
76
|
-
if (!helpText) return;
|
|
77
|
-
|
|
78
|
-
const block = `<crtr-tool-guide>\n${helpText}\n</crtr-tool-guide>`;
|
|
79
|
-
|
|
80
|
-
// Place the guide in the tool-selection frame, right after pi's native
|
|
81
|
-
// "Available tools" list rather than at the very bottom — the agent decides
|
|
82
|
-
// which capability to reach for while reading the tools, so the crtr
|
|
83
|
-
// commands (agent/skill/job) must be present there, not 3k tokens later.
|
|
84
|
-
// `\n\nGuidelines:` is the stable seam that closes the tools area. Fall back
|
|
85
|
-
// to appending if pi ever changes that marker.
|
|
86
|
-
const anchor = "\n\nGuidelines:";
|
|
87
|
-
const idx = event.systemPrompt.indexOf(anchor);
|
|
88
|
-
if (idx === -1) {
|
|
89
|
-
return { systemPrompt: `${event.systemPrompt}\n\n${block}` };
|
|
90
|
-
}
|
|
91
|
-
return {
|
|
92
|
-
systemPrompt: `${event.systemPrompt.slice(0, idx)}\n\n${block}${event.systemPrompt.slice(idx)}`,
|
|
93
|
-
};
|
|
94
|
-
});
|
|
95
|
-
}
|