@north-light/crouter 0.3.219 → 0.3.221
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 +9 -0
- package/dist/api/client.js +10 -0
- package/dist/api/dto/chat-inventory.d.ts +43 -0
- package/dist/api/dto/chat-inventory.js +11 -0
- package/dist/api/dto/profiles.d.ts +19 -5
- package/dist/api/dto/profiles.js +2 -1
- package/dist/api/index.d.ts +1 -0
- package/dist/api/index.js +1 -0
- package/dist/api/routes.d.ts +1 -0
- package/dist/api/routes.js +1 -0
- package/dist/build-root.d.ts +2 -6
- package/dist/build-root.js +51 -4
- package/dist/builtin-memory/internal/agent-shaping.md +3 -1
- package/dist/builtin-memory/internal/memory-loading.md +4 -0
- package/dist/builtin-memory/plan/roadmap.md +7 -1
- package/dist/builtin-memory/spec/guide.md +7 -1
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/claude-plugin-commands.ts +1 -1
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/crtr-commands/index.ts +7 -1
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/memory-slash-commands.ts +7 -2
- package/dist/clients/attach/__tests__/ref-autocomplete.test.js +1 -1
- package/dist/clients/attach/__tests__/titled-editor-preview.test.js +1 -1
- package/dist/clients/attach/overlays/file-review.js +2 -2
- package/dist/clients/attach/session/keys.d.ts +1 -1
- package/dist/clients/attach/session/profile-files.js +1 -1
- package/dist/clients/attach/viewer.js +690 -690
- package/dist/clients/inbox/review/launch.d.ts +8 -4
- package/dist/clients/inbox/review/launch.js +55 -5
- package/dist/clients/inbox/review/review-client.d.ts +1 -0
- package/dist/clients/inbox/review/review-client.js +4 -0
- package/dist/clients/inbox/review-adapter.d.ts +1 -8
- package/dist/clients/inbox/review-adapter.js +4 -52
- package/dist/commands/memory/lint.js +2 -1
- package/dist/commands/memory/read.js +1 -0
- package/dist/commands/memory.js +1 -1
- package/dist/commands/pkg/market-manage.js +165 -75
- package/dist/commands/pkg/plugin-inspect.js +19 -2
- package/dist/commands/pkg/plugin-manage.d.ts +8 -3
- package/dist/commands/pkg/plugin-manage.js +72 -24
- package/dist/commands/profile/default.js +6 -10
- package/dist/commands/profile/list.js +5 -3
- package/dist/commands/profile/new.js +21 -8
- package/dist/commands/profile/project.js +25 -19
- package/dist/commands/profile/show.js +3 -3
- package/dist/commands/surface-inbox.js +1 -0
- package/dist/commands/sys/__tests__/migrate.test.js +16 -5
- package/dist/commands/sys/doctor.js +35 -5
- package/dist/commands/sys/migrate.js +38 -19
- package/dist/commands/sys/panels/broker-limits-panel.js +3 -3
- package/dist/commands/sys/setup-core.js +1 -1
- package/dist/commands/sys/sync-project-guidance.js +1 -1
- package/dist/core/__tests__/canvas-inbox-watcher-hold.test.js +1 -1
- package/dist/core/__tests__/fixtures/c5-command-boundary-ext.js +24 -0
- package/dist/core/__tests__/fixtures/fake-engine.d.ts +24 -18
- package/dist/core/__tests__/fixtures/fake-engine.js +8 -1
- package/dist/core/__tests__/helpers/broker-clients.d.ts +1 -0
- package/dist/core/__tests__/helpers/broker-clients.js +1 -0
- package/dist/core/__tests__/inline-memory-refs.test.js +36 -2
- package/dist/core/__tests__/profile-project-memory-delivery.test.js +217 -0
- package/dist/core/__tests__/seam/dormancy-release.test.js +37 -1
- package/dist/core/__tests__/serial/broker-sdk-wiring.test.js +102 -2
- package/dist/core/bootstrap.js +6 -0
- package/dist/core/canvas/browse/app.js +5 -2
- package/dist/core/canvas/browse/model.d.ts +25 -15
- package/dist/core/canvas/browse/model.js +86 -65
- package/dist/core/canvas/render-source.d.ts +6 -0
- package/dist/core/canvas/render-source.js +7 -1
- package/dist/core/canvas/render.js +10 -2
- package/dist/core/command-hooks/artifact.d.ts +10 -0
- package/dist/core/command-hooks/artifact.js +129 -0
- package/dist/core/command-hooks/catalog.d.ts +14 -0
- package/dist/core/command-hooks/catalog.js +38 -0
- package/dist/core/command-hooks/compose.d.ts +15 -0
- package/dist/core/command-hooks/compose.js +99 -0
- package/dist/core/command-hooks/discovery.d.ts +87 -0
- package/dist/core/command-hooks/discovery.js +174 -0
- package/dist/core/command-hooks/help.d.ts +5 -0
- package/dist/core/command-hooks/help.js +18 -0
- package/dist/core/command-hooks/index.d.ts +6 -0
- package/dist/core/command-hooks/index.js +6 -0
- package/dist/core/command-hooks/report.d.ts +23 -0
- package/dist/core/command-hooks/report.js +19 -0
- package/dist/core/command-hooks/schema.d.ts +27 -0
- package/dist/core/command-hooks/schema.js +68 -0
- package/dist/core/command-hooks/transport/exec-invoke.d.ts +22 -0
- package/dist/core/command-hooks/transport/exec-invoke.js +274 -0
- package/dist/core/command-plugins/presence.d.ts +2 -0
- package/dist/core/command-plugins/presence.js +17 -0
- package/dist/core/command-plugins/transport/exec-invoke.d.ts +5 -0
- package/dist/core/command-plugins/transport/exec-invoke.js +58 -5
- package/dist/core/command.d.ts +8 -1
- package/dist/core/command.js +12 -10
- package/dist/core/help.d.ts +7 -1
- package/dist/core/io.d.ts +9 -1
- package/dist/core/io.js +44 -2
- package/dist/core/memory/inline-ref-inventory.d.ts +2 -1
- package/dist/core/memory/inline-ref-inventory.js +15 -8
- package/dist/core/memory-resolver.d.ts +13 -1
- package/dist/core/memory-resolver.js +25 -19
- package/dist/core/profiles/manifest.d.ts +13 -2
- package/dist/core/profiles/manifest.js +84 -18
- package/dist/core/profiles/select.js +9 -9
- package/dist/core/render.js +11 -0
- package/dist/core/runtime/advertised-command-invocation.d.ts +20 -0
- package/dist/core/runtime/advertised-command-invocation.js +233 -0
- package/dist/core/runtime/bearings.js +1 -1
- package/dist/core/runtime/broker/client-registry.d.ts +6 -3
- package/dist/core/runtime/broker/client-registry.js +6 -4
- package/dist/core/runtime/broker/event-projection.js +7 -0
- package/dist/core/runtime/broker/frame-dispatch.d.ts +1 -1
- package/dist/core/runtime/broker/frame-dispatch.js +16 -10
- package/dist/core/runtime/broker/read-ops.d.ts +4 -0
- package/dist/core/runtime/broker/read-ops.js +6 -2
- package/dist/core/runtime/broker-extension-render.js +1 -1
- package/dist/core/runtime/broker-inventory.d.ts +5 -0
- package/dist/core/runtime/broker-inventory.js +191 -0
- package/dist/core/runtime/broker-protocol.d.ts +13 -2
- package/dist/core/runtime/broker.js +10 -1
- package/dist/core/runtime/command-surface.d.ts +33 -0
- package/dist/core/runtime/command-surface.js +81 -0
- package/dist/core/runtime/node-read.js +5 -0
- package/dist/core/scope.d.ts +26 -1
- package/dist/core/scope.js +52 -12
- package/dist/core/substrate/on-read.d.ts +7 -1
- package/dist/core/substrate/on-read.js +13 -4
- package/dist/core/substrate/render.js +14 -5
- package/dist/core/substrate/schema.d.ts +11 -1
- package/dist/core/substrate/schema.js +11 -2
- package/dist/daemon/api/__tests__/profile-launch-gates.test.js +4 -4
- package/dist/daemon/api/handlers/chat-inventory.d.ts +2 -0
- package/dist/daemon/api/handlers/chat-inventory.js +25 -0
- package/dist/daemon/api/handlers/profiles.js +7 -1
- package/dist/daemon/api/map.d.ts +2 -1
- package/dist/daemon/api/map.js +3 -2
- package/dist/daemon/api/server.js +2 -0
- package/dist/hook-authoring.d.ts +75 -0
- package/dist/hook-authoring.js +358 -0
- package/dist/hook-process.d.ts +7 -0
- package/dist/hook-process.js +34 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/migrations/002-profile-project-memory.d.ts +2 -0
- package/dist/migrations/002-profile-project-memory.js +71 -0
- package/dist/migrations/profile-manifests.d.ts +30 -0
- package/dist/migrations/profile-manifests.js +70 -0
- package/dist/migrations/registry.js +10 -5
- package/dist/migrations/types.d.ts +28 -1
- package/dist/migrations/types.js +15 -9
- package/dist/pi-extensions/__tests__/canvas-structured-output.test.js +21 -4
- package/dist/pi-extensions/canvas-structured-output.js +85 -2
- package/dist/types.d.ts +15 -6
- package/dist/types.js +5 -1
- package/package.json +1 -1
- package/runtime.lock.json +2 -2
- package/dist/clients/attach/__tests__/file-review-focus.test.js +0 -49
- /package/dist/{clients/attach/__tests__/file-review-focus.test.d.ts → core/__tests__/profile-project-memory-delivery.test.d.ts} +0 -0
|
@@ -25,16 +25,17 @@ function write(path, content) {
|
|
|
25
25
|
writeFileSync(path, content);
|
|
26
26
|
}
|
|
27
27
|
function manifest(name, projects) {
|
|
28
|
+
const first = projects[0];
|
|
28
29
|
return JSON.stringify({
|
|
29
30
|
schema_version: 1,
|
|
30
31
|
name,
|
|
31
32
|
projects,
|
|
32
|
-
home:
|
|
33
|
+
home: (typeof first === 'string' ? first : first?.path) ?? null,
|
|
33
34
|
created_at: '2026-01-01T00:00:00.000Z',
|
|
34
35
|
last_used_at: null,
|
|
35
36
|
});
|
|
36
37
|
}
|
|
37
|
-
test('sys migrate
|
|
38
|
+
test('sys migrate converges profile manifests, then profile-aware refs and every descendant project/plugin store', () => {
|
|
38
39
|
const home = mkdtempSync(join(tmpdir(), 'crtr-sys-migrate-'));
|
|
39
40
|
const userRoot = join(home, '.crouter');
|
|
40
41
|
const rootProfile = join(userRoot, 'profiles', 'root-00000000');
|
|
@@ -52,8 +53,11 @@ test('sys migrate covers profile-aware refs and every descendant project/plugin
|
|
|
52
53
|
const pluginDoc = join(pluginRoot, 'memory', 'plugin.md');
|
|
53
54
|
const embeddedPluginDoc = join(embeddedPluginRoot, 'memory', 'plugin.md');
|
|
54
55
|
try {
|
|
55
|
-
|
|
56
|
-
|
|
56
|
+
// Root's string entries migrate to `preview`; every other profile's to
|
|
57
|
+
// `content`. The applet manifest is mixed: its already-current entry must
|
|
58
|
+
// survive untouched while the string beside it converts.
|
|
59
|
+
write(join(rootProfile, 'profile.json'), manifest('root', [appletProject]));
|
|
60
|
+
write(join(appletProfile, 'profile.json'), manifest('applet-inactive', [{ path: appletProject, memory: 'name' }, nestedOnlyProject]));
|
|
57
61
|
write(userDoc, `${OLD_DOC}\nSee [[user/scoped]].\n`);
|
|
58
62
|
write(join(userRoot, 'memory', 'scoped', 'INDEX.md'), CURRENT_DOC);
|
|
59
63
|
write(rootDoc, `${OLD_DOC}\nSee [[shared]].\n`);
|
|
@@ -78,8 +82,15 @@ test('sys migrate covers profile-aware refs and every descendant project/plugin
|
|
|
78
82
|
const result = spawnSync(process.execPath, ['--conditions=crtr-src', '--import', TSX_LOADER, CLI, 'sys', 'migrate', '--json'], { cwd: home, env, encoding: 'utf8' });
|
|
79
83
|
assert.equal(result.status, 0, result.stderr);
|
|
80
84
|
const output = JSON.parse(result.stdout);
|
|
85
|
+
assert.equal(output.profiles, 2);
|
|
81
86
|
assert.equal(output.stores, 7);
|
|
82
|
-
assert.equal(output.changed,
|
|
87
|
+
assert.equal(output.changed, 9);
|
|
88
|
+
const readProjects = (dir) => JSON.parse(readFileSync(join(dir, 'profile.json'), 'utf8')).projects;
|
|
89
|
+
assert.deepEqual(readProjects(rootProfile), [{ path: appletProject, memory: 'preview' }]);
|
|
90
|
+
assert.deepEqual(readProjects(appletProfile), [
|
|
91
|
+
{ path: appletProject, memory: 'name' },
|
|
92
|
+
{ path: nestedOnlyProject, memory: 'content' },
|
|
93
|
+
]);
|
|
83
94
|
for (const path of [userDoc, rootDoc, appletDoc, projectDoc, deepDoc, pluginDoc, embeddedPluginDoc]) {
|
|
84
95
|
const migrated = readFileSync(path, 'utf8');
|
|
85
96
|
assert.doesNotMatch(migrated, /system-prompt-visibility|file-read-visibility/);
|
|
@@ -13,10 +13,11 @@ import { readFault } from '../../core/runtime/fault.js';
|
|
|
13
13
|
import { detectNerdFont, nerdFontInstallCommand, NERD_FONT_SELECT_HINT } from '../../core/runtime/nerd-font.js';
|
|
14
14
|
import { detectPackageManager } from './setup-core.js';
|
|
15
15
|
import { validateEffectiveCommandPlugins } from '../../core/command-plugins/discovery.js';
|
|
16
|
+
import { discoverHookRegistry } from '../../core/command-hooks/discovery.js';
|
|
16
17
|
import { resolveBinContributions } from '../../core/runtime/bin-contributions.js';
|
|
17
18
|
import { buildBrokerEnv, resolvePathExecutable } from '../../core/runtime/spawn-env.js';
|
|
18
19
|
import { listAllPlugins } from '../../core/resolver.js';
|
|
19
|
-
import { SUBTREE_NAMES, coreCommandPaths } from '../../build-root.js';
|
|
20
|
+
import { SUBTREE_NAMES, coreCommandPaths, coreHookCatalog } from '../../build-root.js';
|
|
20
21
|
import { processGeneration, processLocatedGeneration, runtimeManifest, selectedGeneration } from './shared.js';
|
|
21
22
|
function pass(scope, name, message) {
|
|
22
23
|
return { scope, name, status: 'pass', message };
|
|
@@ -141,6 +142,34 @@ async function runCommandPluginChecks(scopes) {
|
|
|
141
142
|
}
|
|
142
143
|
return results;
|
|
143
144
|
}
|
|
145
|
+
/** Statically inspect the complete effective hook registry. This shares the
|
|
146
|
+
* runtime target catalog but never enters hook execution, preserving doctor as
|
|
147
|
+
* the recovery path for broken hooks. */
|
|
148
|
+
async function runHookPluginChecks(scopes) {
|
|
149
|
+
const inScope = new Set(scopes);
|
|
150
|
+
const registry = discoverHookRegistry(await coreHookCatalog());
|
|
151
|
+
const results = [];
|
|
152
|
+
for (const validation of registry.validations) {
|
|
153
|
+
const plugin = validation.plugin;
|
|
154
|
+
if (!inScope.has(plugin.scope))
|
|
155
|
+
continue;
|
|
156
|
+
const issues = registry.issues.filter((issue) => issue.plugin === plugin.name && issue.scope === plugin.scope);
|
|
157
|
+
if (issues.length === 0) {
|
|
158
|
+
const declarations = validation.hooks.map((hook) => `${hook.target} [${hook.phase} ${hook.op}]`).join('; ');
|
|
159
|
+
results.push(pass(plugin.scope, `plugin:${plugin.name}:hooks`, `effective hook declaration(s): ${declarations}`));
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
issues.forEach((issue, index) => {
|
|
163
|
+
const source = validation.manifestPath ?? join(plugin.root, '.crouter-plugin', 'plugin.json');
|
|
164
|
+
const detail = issue.path !== undefined ? `${source}:${issue.path}` : source;
|
|
165
|
+
results.push(failCheck(plugin.scope, `plugin:${plugin.name}:hooks[${index}]`, `${issue.code} at ${detail}: ${issue.message} (received ${issue.received}, expected ${issue.expected})`, {
|
|
166
|
+
kind: 'human_action',
|
|
167
|
+
description: `${issue.next} Inspect with \`crtr pkg plugin show ${plugin.name}\`; then disable (\`crtr pkg plugin disable ${plugin.name} --scope ${plugin.scope}\`), update (\`crtr pkg plugin update --name ${plugin.name}\`), or remove (\`crtr pkg plugin remove ${plugin.name}\`) the plugin.`,
|
|
168
|
+
}));
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
return results;
|
|
172
|
+
}
|
|
144
173
|
/**
|
|
145
174
|
* Report every bare-binary contribution (`bin` in a plugin manifest or a scope
|
|
146
175
|
* `config.json`) that failed to reach a node's PATH, plus the ones that did.
|
|
@@ -467,7 +496,7 @@ function runChecksForScope(scope, opts) {
|
|
|
467
496
|
export const sysDoctorLeaf = defineLeaf({
|
|
468
497
|
name: 'doctor',
|
|
469
498
|
description: 'diagnose installation health',
|
|
470
|
-
whenToUse: 'something in your crtr install looks off and you want it diagnosed — a plugin or marketplace manifest is missing, a config entry points at a directory that no longer exists, or a plugin command
|
|
499
|
+
whenToUse: 'something in your crtr install looks off and you want it diagnosed — a plugin or marketplace manifest is missing, a config entry points at a directory that no longer exists, or a plugin command/hook declaration is malformed. Reports each problem with a structured remediation, and can apply the safe repairs for you.',
|
|
471
500
|
help: {
|
|
472
501
|
name: 'sys doctor',
|
|
473
502
|
summary: 'diagnose runtime integrity, missing manifests, and broken config entries',
|
|
@@ -477,13 +506,13 @@ export const sysDoctorLeaf = defineLeaf({
|
|
|
477
506
|
{ kind: 'flag', name: 'remote', type: 'bool', required: false, constraint: 'Check git remotes with ls-remote (slow — makes network calls).' },
|
|
478
507
|
],
|
|
479
508
|
output: [
|
|
480
|
-
{ name: 'checks', type: 'object[]', required: true, constraint: 'Each: {scope, name, status, message, fixed?, remediation?}. status: pass | fail. scope may be user | project | canvas. bin:<name> checks report bare-binary contributions (a `bin` block in a plugin manifest or a scope config.json): a pass names the contributor and the resolved target, a fail names an unsafe/reserved command name, malformed target declaration, missing/non-executable/escaping target, or same-precedence name collision that installed nothing; their remediation is always a human_action. requires:<name> checks report each enabled plugin manifest PATH requirement: a pass names the plugin and resolved executable path; a fail names the plugin and carries its declared install hint as remediation. Runtime checks validate selected and process generation manifests, sealing/containment, and full SHA-256 inventories. terminal:nerd-font (user scope) reports whether a Nerd Font is installed for the viewer\'s icon glyphs; its remediation is always a human_action carrying the platform install command, never auto-fixed. Plugin
|
|
509
|
+
{ name: 'checks', type: 'object[]', required: true, constraint: 'Each: {scope, name, status, message, fixed?, remediation?}. status: pass | fail. scope may be user | project | canvas. bin:<name> checks report bare-binary contributions (a `bin` block in a plugin manifest or a scope config.json): a pass names the contributor and the resolved target, a fail names an unsafe/reserved command name, malformed target declaration, missing/non-executable/escaping target, or same-precedence name collision that installed nothing; their remediation is always a human_action. requires:<name> checks report each enabled plugin manifest PATH requirement: a pass names the plugin and resolved executable path; a fail names the plugin and carries its declared install hint as remediation. Runtime checks validate selected and process generation manifests, sealing/containment, and full SHA-256 inventories. terminal:nerd-font (user scope) reports whether a Nerd Font is installed for the viewer\'s icon glyphs; its remediation is always a human_action carrying the platform install command, never auto-fixed. Plugin checks appear as plugin:<name>:commands and plugin:<name>:hooks; both are validated statically and never execute plugin code. Hook failures identify manifest/path/target/collision problems and carry disable/update/remove/show remediation; healthy hooks list effective target, phase, and op. remediation (when present) is {kind, description, ...payload} where kind is remove_config_key | rm_path | human_action; fault human_action remediations include the canonical logs command. Sorted by scope then name.' },
|
|
481
510
|
{ name: 'ok', type: 'boolean', required: true, constraint: 'True when no unresolved fail checks remain.' },
|
|
482
511
|
],
|
|
483
512
|
outputKind: 'object',
|
|
484
513
|
effects: [
|
|
485
|
-
'Read-only unless --fix is passed. Runtime verification reads and hashes every selected/process generation file; plugin command manifests, bare-binary (`bin`) declarations, and advisory `requires` PATH checks never execute plugin or contributed binaries.',
|
|
486
|
-
'With --fix: applies each non-pass check\'s `remediation` — removes stale config entries and deletes dangling plugin/marketplace directories; canvas fault findings and plugin command findings stay read-only (--fix never chmods or rewrites third-party plugin content).',
|
|
514
|
+
'Read-only unless --fix is passed. Runtime verification reads and hashes every selected/process generation file; plugin command and hook manifests, bare-binary (`bin`) declarations, and advisory `requires` PATH checks never execute plugin or contributed binaries.',
|
|
515
|
+
'With --fix: applies each non-pass check\'s `remediation` — removes stale config entries and deletes dangling plugin/marketplace directories; canvas fault findings and plugin command/hook findings stay read-only (--fix never chmods or rewrites third-party plugin content).',
|
|
487
516
|
'Each non-pass result carries a structured `remediation` describing the fix action (absolute paths, exact config keys, or a human_action with its command) so callers can apply it directly without --fix.',
|
|
488
517
|
],
|
|
489
518
|
},
|
|
@@ -500,6 +529,7 @@ export const sysDoctorLeaf = defineLeaf({
|
|
|
500
529
|
if (scopes.includes('user'))
|
|
501
530
|
allResults.push(runNerdFontCheck());
|
|
502
531
|
allResults.push(...await runCommandPluginChecks(scopes));
|
|
532
|
+
allResults.push(...await runHookPluginChecks(scopes));
|
|
503
533
|
allResults.push(...runBinContributionChecks(scopes));
|
|
504
534
|
allResults.push(...runRequiresChecks(scopes));
|
|
505
535
|
allResults.push(...await runFaultChecks());
|
|
@@ -1,24 +1,32 @@
|
|
|
1
|
-
// `crtr sys migrate` — the
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
1
|
+
// `crtr sys migrate` — the on-disk state-migration runner: converge every
|
|
2
|
+
// stored profile manifest to the current manifest format, then every writable
|
|
3
|
+
// memory store's documents to the current document format. (Journaled
|
|
4
|
+
// canvas-db migrations run in the db's own ordered chain when the daemon opens
|
|
5
|
+
// it; this leaf owns only the on-disk state.)
|
|
6
|
+
//
|
|
7
|
+
// Manifests come FIRST and are read as raw bytes under the profile lock: the
|
|
8
|
+
// current reader treats a pre-migration manifest exactly like corrupt JSON, so
|
|
9
|
+
// `listProfiles()` sees nothing until this lane has run. Store discovery below
|
|
10
|
+
// therefore works from the lane's post-migration snapshots — which are equally
|
|
11
|
+
// coherent under `--dry-run`, where the same fold ran and nothing was written.
|
|
5
12
|
import { dirname, join } from 'node:path';
|
|
6
13
|
import { defineLeaf } from '../../core/command.js';
|
|
7
14
|
import { pathExists, realpathOrSelf } from '../../core/fs-utils.js';
|
|
8
15
|
import { createMemoryDocSnapshot } from '../../core/memory-resolver.js';
|
|
9
16
|
import { listInstalledPlugins, listInstalledPluginsInRoot } from '../../core/resolver.js';
|
|
10
17
|
import { pluginMemoryDir, projectScopeRoots, scopeMemoryDir } from '../../core/scope.js';
|
|
11
|
-
import {
|
|
18
|
+
import { profileMemoryDir, profilesRoot, withProfileManifestLock } from '../../core/profiles/manifest.js';
|
|
12
19
|
import { descendantStoreRootsExhaustive } from '../../core/nested-stores.js';
|
|
13
20
|
import { memoryDir as nodeMemoryDir } from '../../core/runtime/memory.js';
|
|
14
21
|
import { runConvergentMigrations } from '../../migrations/convergent.js';
|
|
22
|
+
import { runProfileManifestMigrations } from '../../migrations/profile-manifests.js';
|
|
15
23
|
function targetKey(target) {
|
|
16
24
|
return `${realpathOrSelf(target.cwd)}\u0000${target.profileId ?? ''}\u0000${target.nodeId ?? ''}`;
|
|
17
25
|
}
|
|
18
26
|
/** Every writable memory store — the lint corpus minus builtin (package-owned,
|
|
19
27
|
* regenerated on every install, so migrating it would fight the package).
|
|
20
28
|
* Physical stores dedupe once while retaining every owning target corpus. */
|
|
21
|
-
function migratableStores() {
|
|
29
|
+
function migratableStores(profiles) {
|
|
22
30
|
const stores = new Map();
|
|
23
31
|
const add = (dir, target) => {
|
|
24
32
|
if (dir === null || dir === '' || !pathExists(dir))
|
|
@@ -49,18 +57,18 @@ function migratableStores() {
|
|
|
49
57
|
};
|
|
50
58
|
const selectedProfileId = process.env['CRTR_PROFILE_ID'] || null;
|
|
51
59
|
addProjectRoots(projectScopeRoots(process.cwd(), null), selectedProfileId);
|
|
52
|
-
const profiles = listProfiles();
|
|
53
60
|
const profileTargets = [];
|
|
54
61
|
for (const { profileId, manifest } of profiles) {
|
|
62
|
+
const home = manifest.home !== null && manifest.home !== '' ? manifest.home : null;
|
|
55
63
|
const profileTarget = {
|
|
56
|
-
cwd:
|
|
64
|
+
cwd: home ?? manifest.projects[0]?.path ?? process.cwd(),
|
|
57
65
|
profileId,
|
|
58
66
|
nodeId: null,
|
|
59
67
|
};
|
|
60
68
|
profileTargets.push(profileTarget);
|
|
61
69
|
add(profileMemoryDir(profileId), profileTarget);
|
|
62
70
|
const roots = [
|
|
63
|
-
...new Set(manifest.projects.flatMap((project) => [join(project, '.crouter'), ...projectScopeRoots(project, null)])),
|
|
71
|
+
...new Set(manifest.projects.flatMap((project) => [join(project.path, '.crouter'), ...projectScopeRoots(project.path, null)])),
|
|
64
72
|
];
|
|
65
73
|
addProjectRoots(roots, profileId);
|
|
66
74
|
}
|
|
@@ -82,23 +90,25 @@ function migratableStores() {
|
|
|
82
90
|
}
|
|
83
91
|
export const sysMigrateLeaf = defineLeaf({
|
|
84
92
|
name: 'migrate',
|
|
85
|
-
description: 'converge every writable memory store to the current
|
|
86
|
-
whenToUse: 'after a crtr update whose release notes name a
|
|
93
|
+
description: 'converge every profile manifest and writable memory store to the current on-disk format',
|
|
94
|
+
whenToUse: 'after a crtr update whose release notes name a state migration, when profiles stop resolving on a home that predates the update, or when old-format docs resurface (a restore, a sync, a hand-authored file). Convergent and idempotent: re-running over current state changes nothing, so use --dry-run first only when you want to review the rewrite.',
|
|
87
95
|
help: {
|
|
88
96
|
name: 'sys migrate',
|
|
89
|
-
summary: 'run the registered
|
|
97
|
+
summary: 'run the registered state migrations over every profile manifest and writable memory store',
|
|
90
98
|
params: [
|
|
91
99
|
{ kind: 'flag', name: 'dry-run', type: 'bool', required: false, constraint: 'Plan and report every rewrite without writing any file.' },
|
|
92
100
|
],
|
|
93
101
|
output: [
|
|
102
|
+
{ name: 'profiles', type: 'number', required: true, constraint: 'Profile manifests scanned.' },
|
|
94
103
|
{ name: 'stores', type: 'number', required: true, constraint: 'Memory stores scanned.' },
|
|
95
|
-
{ name: 'changed', type: 'number', required: true, constraint: 'Files rewritten (or, under --dry-run, that would be).' },
|
|
96
|
-
{ name: 'applied', type: 'object[]', required: true, constraint: 'One row per migration that changed files
|
|
97
|
-
{ name: 'skipped', type: 'object[]', required: true, constraint: '
|
|
104
|
+
{ name: 'changed', type: 'number', required: true, constraint: 'Files rewritten (or, under --dry-run, that would be) — manifests and documents together.' },
|
|
105
|
+
{ name: 'applied', type: 'object[]', required: true, constraint: 'One row per migration that changed files: {store, lane, migration, files}, lane one of profile-manifest, convergent. Empty when the state is already current.' },
|
|
106
|
+
{ name: 'skipped', type: 'object[]', required: true, constraint: 'Files left untouched because they could not be parsed — a doc whose frontmatter is not valid YAML, or a manifest that is not valid JSON: {store, relPath, error}. Fix the file, then re-run.' },
|
|
98
107
|
{ name: 'dryRun', type: 'boolean', required: true, constraint: 'True when nothing was written (preview only).' },
|
|
99
108
|
],
|
|
100
109
|
outputKind: 'object',
|
|
101
110
|
effects: [
|
|
111
|
+
'Rewrites old-format `profile.json` manifests in place (atomic, under the same per-profile lock `profile project add/remove` take). A manifest whose structure cannot be converted fails the whole run — no project setting is ever guessed.',
|
|
102
112
|
'Rewrites old-format memory documents in place (atomic per-file writes) across the writable stores: every profile and every profile-owned project (plus nested descendant stores and installed plugin stores), the current project, user, and this node\'s store. Builtin docs are package-owned and never touched.',
|
|
103
113
|
'Under --dry-run: read-only, writes nothing.',
|
|
104
114
|
],
|
|
@@ -107,7 +117,15 @@ export const sysMigrateLeaf = defineLeaf({
|
|
|
107
117
|
const dryRun = input.dryRun === true;
|
|
108
118
|
const applied = [];
|
|
109
119
|
const skipped = [];
|
|
110
|
-
const
|
|
120
|
+
const profilesDir = profilesRoot();
|
|
121
|
+
const profileRun = runProfileManifestMigrations(profilesDir, { dryRun, withLock: withProfileManifestLock });
|
|
122
|
+
for (const row of profileRun.applied) {
|
|
123
|
+
applied.push({ store: profilesDir, lane: 'profile-manifest', migration: row.migration, files: [row.path] });
|
|
124
|
+
}
|
|
125
|
+
for (const row of profileRun.skipped) {
|
|
126
|
+
skipped.push({ store: profilesDir, relPath: `${row.profileId}/profile.json`, error: row.error });
|
|
127
|
+
}
|
|
128
|
+
const stores = migratableStores(profileRun.snapshots);
|
|
111
129
|
const snapshots = new Map();
|
|
112
130
|
for (const store of stores) {
|
|
113
131
|
const resolvers = store.targets.map((target) => (name) => {
|
|
@@ -137,21 +155,22 @@ export const sysMigrateLeaf = defineLeaf({
|
|
|
137
155
|
},
|
|
138
156
|
});
|
|
139
157
|
for (const row of result.applied)
|
|
140
|
-
applied.push({ store: store.dir, migration: row.migration, files: row.files });
|
|
158
|
+
applied.push({ store: store.dir, lane: 'convergent', migration: row.migration, files: row.files });
|
|
141
159
|
for (const s of result.skipped)
|
|
142
160
|
skipped.push({ store: store.dir, relPath: s.relPath, error: s.error });
|
|
143
161
|
}
|
|
144
162
|
const changed = applied.reduce((n, row) => n + row.files.length, 0);
|
|
145
163
|
return {
|
|
164
|
+
profiles: profileRun.profiles,
|
|
146
165
|
stores: stores.length,
|
|
147
166
|
changed,
|
|
148
167
|
applied,
|
|
149
168
|
skipped,
|
|
150
169
|
dryRun,
|
|
151
170
|
follow_up: skipped.length > 0
|
|
152
|
-
? 'Some
|
|
171
|
+
? 'Some files could not be parsed and were left untouched — fix each named file, then re-run `crtr sys migrate`.'
|
|
153
172
|
: changed === 0
|
|
154
|
-
? '
|
|
173
|
+
? 'State already current — nothing to rewrite.'
|
|
155
174
|
: dryRun
|
|
156
175
|
? 'Preview only — re-run without --dry-run to write these changes.'
|
|
157
176
|
: undefined,
|
|
@@ -82,7 +82,7 @@ export class BrokerLimitsPanel {
|
|
|
82
82
|
}
|
|
83
83
|
render(width) {
|
|
84
84
|
const out = [
|
|
85
|
-
truncateToWidth(`${theme.fg("accent", theme.bold("Broker limits"))}${theme.fg("dim", " user scope · defaults
|
|
85
|
+
truncateToWidth(`${theme.fg("accent", theme.bold("Broker limits"))}${theme.fg("dim", " user scope · defaults 16 / 32")}`, width),
|
|
86
86
|
truncateToWidth(theme.fg("dim", "These bounds govern daemon automatic revive; explicit node revives remain available."), width),
|
|
87
87
|
"",
|
|
88
88
|
];
|
|
@@ -91,8 +91,8 @@ export class BrokerLimitsPanel {
|
|
|
91
91
|
out.push(truncateToWidth(`${selected ? theme.fg("accent", "▶") : " "} ${padAnsi(selected ? theme.fg("accent", label) : label, 24)} ${input.render(Math.max(8, width - 28))[0] ?? ""}`, width));
|
|
92
92
|
out.push(truncateToWidth(theme.fg("dim", ` ${description}`), width));
|
|
93
93
|
};
|
|
94
|
-
line(0, "Warning threshold", "Warn when live brokers reach this count. Default:
|
|
95
|
-
line(1, "Automatic-revive hard cap", "Defer automatic revives at this count. Default:
|
|
94
|
+
line(0, "Warning threshold", "Warn when live brokers reach this count. Default: 16.", this.warningInput);
|
|
95
|
+
line(1, "Automatic-revive hard cap", "Defer automatic revives at this count. Default: 32.", this.hardCapInput);
|
|
96
96
|
out.push("");
|
|
97
97
|
for (const text of wrapText(`Configured limits apply next tick. ${BROKER_LIMIT_OVERRIDE_PRECEDENCE}`, width))
|
|
98
98
|
out.push(theme.fg("dim", text));
|
|
@@ -535,7 +535,7 @@ export async function executeSetupSubmission(submission) {
|
|
|
535
535
|
const registered = ensureMarketplaceRegistered(marketplace);
|
|
536
536
|
if (registered !== null)
|
|
537
537
|
installedPackages.push(registered);
|
|
538
|
-
installFromMarketplace({ marketplace: marketplace, plugin: plugin }, 'user');
|
|
538
|
+
await installFromMarketplace({ marketplace: marketplace, plugin: plugin }, 'user');
|
|
539
539
|
installedPackages.push(entry.name);
|
|
540
540
|
}
|
|
541
541
|
catch (e) {
|
|
@@ -271,7 +271,7 @@ describe('canvas inbox watcher — hold + idle delivery', () => {
|
|
|
271
271
|
homes.push(userHome);
|
|
272
272
|
process.env.HOME = userHome;
|
|
273
273
|
try {
|
|
274
|
-
const profile = createProfile('paused watcher', ['/tmp']);
|
|
274
|
+
const profile = createProfile('paused watcher', [{ path: '/tmp', memory: 'content' }]);
|
|
275
275
|
pauseProfile(profile.profileId);
|
|
276
276
|
process.env['CRTR_PROFILE_ID'] = profile.profileId;
|
|
277
277
|
const pi = makeFakePi();
|
|
@@ -5,4 +5,28 @@ export default function (pi) {
|
|
|
5
5
|
/* never invoked — isLeadingEngineCommand only checks registration */
|
|
6
6
|
},
|
|
7
7
|
});
|
|
8
|
+
pi.registerCommand('gatewaycmd', {
|
|
9
|
+
description: 'Gateway-advertised custom-message fixture',
|
|
10
|
+
gateway: true,
|
|
11
|
+
handler: async () => {
|
|
12
|
+
pi.sendMessage({
|
|
13
|
+
customType: 'fixture-output',
|
|
14
|
+
content: 'expanded agent input',
|
|
15
|
+
display: true,
|
|
16
|
+
details: { fixture: true },
|
|
17
|
+
});
|
|
18
|
+
},
|
|
19
|
+
});
|
|
20
|
+
pi.registerCommand('gatewaynext', {
|
|
21
|
+
description: 'Queued Gateway custom-message fixture',
|
|
22
|
+
gateway: true,
|
|
23
|
+
handler: async () => {
|
|
24
|
+
pi.sendMessage({
|
|
25
|
+
customType: 'fixture-next-output',
|
|
26
|
+
content: null,
|
|
27
|
+
display: true,
|
|
28
|
+
details: 'extension-details',
|
|
29
|
+
}, { deliverAs: 'nextTurn' });
|
|
30
|
+
},
|
|
31
|
+
});
|
|
8
32
|
}
|
|
@@ -107,6 +107,28 @@ interface SessionTreeNode {
|
|
|
107
107
|
entry: TreeEntry;
|
|
108
108
|
children: SessionTreeNode[];
|
|
109
109
|
}
|
|
110
|
+
/** The real SessionManager surface the broker reads and patches. A method
|
|
111
|
+
* missing here is a broken fixture, not a degraded one — the broker throws
|
|
112
|
+
* during session build and never boots. */
|
|
113
|
+
interface FakeSessionManager {
|
|
114
|
+
buildSessionContext: () => {
|
|
115
|
+
messages: AgentMessage[];
|
|
116
|
+
thinkingLevel: string;
|
|
117
|
+
model: null;
|
|
118
|
+
};
|
|
119
|
+
getLeafId: () => string | null;
|
|
120
|
+
getEntry: (id: string) => TreeEntry | undefined;
|
|
121
|
+
getTree: () => SessionTreeNode[];
|
|
122
|
+
getUserMessagesForForking: () => Array<{
|
|
123
|
+
entryId: string;
|
|
124
|
+
text: string;
|
|
125
|
+
timestamp?: string;
|
|
126
|
+
}>;
|
|
127
|
+
getSessionId: () => string;
|
|
128
|
+
getSessionFile: () => string;
|
|
129
|
+
getSessionDir: () => string;
|
|
130
|
+
appendCustomMessageEntry: (customType: string, content: unknown, display: boolean, details?: unknown) => string;
|
|
131
|
+
}
|
|
110
132
|
declare class FakeSession {
|
|
111
133
|
private readonly sm;
|
|
112
134
|
private readonly loader;
|
|
@@ -142,24 +164,8 @@ declare class FakeSession {
|
|
|
142
164
|
private readonly modelRt;
|
|
143
165
|
constructor(sm: SessionManager, loader: ResourceLoader | undefined, model?: FakeModel, modelRuntime?: FakeModelRuntime);
|
|
144
166
|
get messages(): AgentMessage[];
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
messages: AgentMessage[];
|
|
148
|
-
thinkingLevel: string;
|
|
149
|
-
model: null;
|
|
150
|
-
};
|
|
151
|
-
getLeafId: () => string | null;
|
|
152
|
-
getEntry: (id: string) => TreeEntry | undefined;
|
|
153
|
-
getTree: () => SessionTreeNode[];
|
|
154
|
-
getUserMessagesForForking: () => Array<{
|
|
155
|
-
entryId: string;
|
|
156
|
-
text: string;
|
|
157
|
-
timestamp?: string;
|
|
158
|
-
}>;
|
|
159
|
-
getSessionId: () => string;
|
|
160
|
-
getSessionFile: () => string;
|
|
161
|
-
getSessionDir: () => string;
|
|
162
|
-
};
|
|
167
|
+
private smFacade;
|
|
168
|
+
get sessionManager(): FakeSessionManager;
|
|
163
169
|
get sessionId(): string;
|
|
164
170
|
get sessionFile(): string | undefined;
|
|
165
171
|
get model(): FakeModel | undefined;
|
|
@@ -341,8 +341,13 @@ class FakeSession {
|
|
|
341
341
|
// truth, since `session.messages` can be sliced by pi's retry/overflow/
|
|
342
342
|
// compaction recovery. Mirror that surface so the fake's snapshot still serves
|
|
343
343
|
// the accrued history (G2/G3). The fake's persisted analog is the active branch.
|
|
344
|
+
// Built once and cached: the real SessionManager is a single object the
|
|
345
|
+
// broker's advertised-command contract monkey-patches
|
|
346
|
+
// (appendCustomMessageEntry), so a fresh literal per access would silently
|
|
347
|
+
// discard that patch.
|
|
348
|
+
smFacade;
|
|
344
349
|
get sessionManager() {
|
|
345
|
-
|
|
350
|
+
this.smFacade ??= {
|
|
346
351
|
buildSessionContext: () => this.tree.buildSessionContext(),
|
|
347
352
|
getLeafId: () => this.tree.getLeafId(),
|
|
348
353
|
getEntry: (id) => this.tree.getEntry(id),
|
|
@@ -351,7 +356,9 @@ class FakeSession {
|
|
|
351
356
|
getSessionId: () => this.sm.getSessionId(),
|
|
352
357
|
getSessionFile: () => this.sm.getSessionFile(),
|
|
353
358
|
getSessionDir: () => this.sm.getSessionDir(),
|
|
359
|
+
appendCustomMessageEntry: (customType, content, display, details) => this.tree.append({ role: 'custom', customType, content, display, details }).id,
|
|
354
360
|
};
|
|
361
|
+
return this.smFacade;
|
|
355
362
|
}
|
|
356
363
|
get sessionId() {
|
|
357
364
|
return this.sm.getSessionId();
|
|
@@ -27,6 +27,7 @@ export interface AttachKit {
|
|
|
27
27
|
/** Connect a BrokerClient to a node's running broker, hello, await welcome. */
|
|
28
28
|
attach(id: string, role: ClientRole, clientId: string, opts?: {
|
|
29
29
|
snapshotTail?: number;
|
|
30
|
+
attends?: boolean;
|
|
30
31
|
}): Promise<Attached>;
|
|
31
32
|
/** Attach (as `role`) and retry until the welcome satisfies `pred`. */
|
|
32
33
|
attachUntil(id: string, role: ClientRole, clientId: string, pred: (a: Attached) => boolean, label: string): Promise<Attached>;
|
|
@@ -14,6 +14,7 @@ import { resetScopeCache } from '../scope.js';
|
|
|
14
14
|
import { clearSessionCache } from '../substrate/session-cache.js';
|
|
15
15
|
import { memoryDir as nodeMemoryDir } from '../runtime/memory.js';
|
|
16
16
|
import { buildRefInventory } from '../memory/inline-ref-inventory.js';
|
|
17
|
+
import { gatewayMemoryRows } from '../runtime/broker-inventory.js';
|
|
17
18
|
// ---------------------------------------------------------------------------
|
|
18
19
|
// Wave C4 — buildRefInventory: the authoritative corpus builder. Fixture-
|
|
19
20
|
// backed (node/project/user scope docs on disk), since precedence dedup,
|
|
@@ -47,6 +48,21 @@ describe('buildRefInventory (fixture-backed corpus)', () => {
|
|
|
47
48
|
function nodeScopeMemoryDir() {
|
|
48
49
|
return nodeMemoryDir(NODE_ID);
|
|
49
50
|
}
|
|
51
|
+
function plantNorthlightVisibilityExtension() {
|
|
52
|
+
const manifestDir = join(userHome, '.crouter', 'plugins', 'northlight', '.crouter-plugin');
|
|
53
|
+
mkdirSync(manifestDir, { recursive: true });
|
|
54
|
+
writeFileSync(join(manifestDir, 'plugin.json'), JSON.stringify({
|
|
55
|
+
name: 'northlight',
|
|
56
|
+
memory_extensions: {
|
|
57
|
+
visibility: {
|
|
58
|
+
type: 'boolean',
|
|
59
|
+
default: true,
|
|
60
|
+
write_help: 'Control chat visibility.',
|
|
61
|
+
edit_help: 'Control chat visibility.',
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
}));
|
|
65
|
+
}
|
|
50
66
|
before(() => {
|
|
51
67
|
prevCanvasHome = process.env['CRTR_HOME'];
|
|
52
68
|
prevUserHome = process.env['HOME'];
|
|
@@ -148,12 +164,30 @@ describe('buildRefInventory (fixture-backed corpus)', () => {
|
|
|
148
164
|
// A sibling doc in the same corpus is unaffected.
|
|
149
165
|
assert.ok(names.has('crtr-inline-ref-fixture-sibling'));
|
|
150
166
|
});
|
|
151
|
-
test('refs are metadata-only: exactly {name, kind, scope, shortForm}, no body/path', () => {
|
|
167
|
+
test('refs are metadata-only: exactly {name, kind, scope, shortForm, gatewayVisible}, no body/path', () => {
|
|
152
168
|
writeDoc(projectMemoryDir(), 'crtr-inline-ref-fixture-meta.md', "kind: knowledge\nshort-form: 'hook'");
|
|
153
169
|
const { refs } = buildRefInventory();
|
|
154
170
|
const ref = refs.find((r) => r.name === 'crtr-inline-ref-fixture-meta');
|
|
155
171
|
assert.ok(ref);
|
|
156
|
-
assert.deepEqual(Object.keys(ref).sort(), ['kind', 'name', 'scope', 'shortForm']);
|
|
172
|
+
assert.deepEqual(Object.keys(ref).sort(), ['gatewayVisible', 'kind', 'name', 'scope', 'shortForm']);
|
|
173
|
+
});
|
|
174
|
+
test('Gateway eligibility excludes builtins and effective Northlight-hidden refs without changing resolution', () => {
|
|
175
|
+
plantNorthlightVisibilityExtension();
|
|
176
|
+
writeDoc(projectMemoryDir(), 'crtr-inline-ref-fixture-hidden.md', 'kind: knowledge\nextensions:\n northlight:\n visibility: false');
|
|
177
|
+
writeDoc(projectMemoryDir(), 'crtr-inline-ref-fixture-visible.md', 'kind: knowledge');
|
|
178
|
+
const { refs, names } = buildRefInventory();
|
|
179
|
+
const hidden = refs.find((ref) => ref.name === 'crtr-inline-ref-fixture-hidden');
|
|
180
|
+
const visible = refs.find((ref) => ref.name === 'crtr-inline-ref-fixture-visible');
|
|
181
|
+
assert.equal(hidden?.gatewayVisible, false);
|
|
182
|
+
assert.equal(visible?.gatewayVisible, true);
|
|
183
|
+
assert.ok(names.has('crtr-inline-ref-fixture-hidden'));
|
|
184
|
+
assert.ok(names.has('crtr-inline-ref-fixture-visible'));
|
|
185
|
+
assert.ok(refs.some((ref) => ref.scope === 'builtin'));
|
|
186
|
+
assert.ok(refs.filter((ref) => ref.scope === 'builtin').every((ref) => !ref.gatewayVisible));
|
|
187
|
+
const chatRows = gatewayMemoryRows(refs);
|
|
188
|
+
assert.ok(!chatRows.some((ref) => ref.name === 'crtr-inline-ref-fixture-hidden'));
|
|
189
|
+
assert.ok(chatRows.some((ref) => ref.name === 'crtr-inline-ref-fixture-visible'));
|
|
190
|
+
assert.ok(!chatRows.some((ref) => ref.scope === 'builtin'));
|
|
157
191
|
});
|
|
158
192
|
test('a malformed doc (no valid kind) is omitted from the inventory', () => {
|
|
159
193
|
writeDoc(projectMemoryDir(), 'crtr-inline-ref-fixture-malformed.md', "short-form: 'no kind here'");
|