@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
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
// A selected profile's project relationship caps AUTOMATIC delivery — boot and
|
|
2
|
+
// workspace-open — and nothing else. The reported failure this suite exists for:
|
|
3
|
+
// a profile project that is also the node's cwd rendered its authored rung, as
|
|
4
|
+
// if the relationship applied only to projects reached through the manifest.
|
|
5
|
+
//
|
|
6
|
+
// One real fixture drives the real scope resolver, the real boot renderer, the
|
|
7
|
+
// real workspace-open renderer, and the real `crtr memory read` leaf: a profile
|
|
8
|
+
// whose four projects sit at none/name/preview/content in manifest order, with
|
|
9
|
+
// the `preview` project as cwd and canonical-name collisions across projects and
|
|
10
|
+
// against the wider user scope.
|
|
11
|
+
//
|
|
12
|
+
// Migration convergence is NOT here — it lives in migration.test.ts.
|
|
13
|
+
//
|
|
14
|
+
// Run: node --conditions=crtr-src --import tsx/esm --test src/core/__tests__/profile-project-memory-delivery.test.ts
|
|
15
|
+
import { test, before, beforeEach, after } from 'node:test';
|
|
16
|
+
import assert from 'node:assert/strict';
|
|
17
|
+
import { mkdtempSync, mkdirSync, realpathSync, rmSync, writeFileSync } from 'node:fs';
|
|
18
|
+
import { tmpdir } from 'node:os';
|
|
19
|
+
import { dirname, join } from 'node:path';
|
|
20
|
+
import { closeDb } from '../canvas/db.js';
|
|
21
|
+
import { resetScopeCache } from '../scope.js';
|
|
22
|
+
import { createProfile } from '../profiles/manifest.js';
|
|
23
|
+
import { spawnNode } from '../runtime/nodes.js';
|
|
24
|
+
import { clearSessionCache } from '../substrate/session-cache.js';
|
|
25
|
+
import { renderKnowledgeBlock } from '../substrate/render-node.js';
|
|
26
|
+
import { renderWorkspaceOpenDocs } from '../substrate/on-read-node.js';
|
|
27
|
+
import { deliveredAtOrAbove, loadInjectedDocs, saveInjectedDocs, } from '../substrate/injected-store.js';
|
|
28
|
+
import { readLeaf } from '../../commands/memory/read.js';
|
|
29
|
+
const B = {
|
|
30
|
+
pnone: 'BODY-T9-PNONE',
|
|
31
|
+
pname: 'BODY-T9-PNAME',
|
|
32
|
+
ppreview: 'BODY-T9-PPREVIEW',
|
|
33
|
+
pcontent: 'BODY-T9-PCONTENT',
|
|
34
|
+
sharedFromPnone: 'BODY-T9-SHARED-FROM-PNONE',
|
|
35
|
+
sharedFromPcontent: 'BODY-T9-SHARED-FROM-PCONTENT',
|
|
36
|
+
nameFromPname: 'BODY-T9-NAMEDUP-FROM-PNAME',
|
|
37
|
+
nameFromPcontent: 'BODY-T9-NAMEDUP-FROM-PCONTENT',
|
|
38
|
+
userDupFromPnone: 'BODY-T9-USERDUP-FROM-PNONE',
|
|
39
|
+
userDupFromUser: 'BODY-T9-USERDUP-FROM-USER',
|
|
40
|
+
};
|
|
41
|
+
let home;
|
|
42
|
+
let canvasHome;
|
|
43
|
+
let profileId;
|
|
44
|
+
let prevHome;
|
|
45
|
+
let prevCanvasHome;
|
|
46
|
+
let prevProfile;
|
|
47
|
+
let prevNode;
|
|
48
|
+
let prevCwd;
|
|
49
|
+
let cwdProject;
|
|
50
|
+
let ppreviewDocPath;
|
|
51
|
+
/** A doc delivered automatically at the top rung by both automatic channels —
|
|
52
|
+
* every cap observed below is the relationship's, never the author's. */
|
|
53
|
+
function autoDoc(when, body) {
|
|
54
|
+
return [
|
|
55
|
+
'---',
|
|
56
|
+
'kind: knowledge',
|
|
57
|
+
`when-and-why-to-read: "${when}"`,
|
|
58
|
+
'surfaces:',
|
|
59
|
+
' - {on: boot, at: content}',
|
|
60
|
+
' - {on: workspace-open, at: content}',
|
|
61
|
+
'---',
|
|
62
|
+
'',
|
|
63
|
+
body,
|
|
64
|
+
'',
|
|
65
|
+
].join('\n');
|
|
66
|
+
}
|
|
67
|
+
/** No surfaces at all: invisible at boot, counted into its dir's `[+N more]`. */
|
|
68
|
+
function bootlessDoc(when, body) {
|
|
69
|
+
return ['---', 'kind: knowledge', `when-and-why-to-read: "${when}"`, '---', '', body, ''].join('\n');
|
|
70
|
+
}
|
|
71
|
+
function writeDoc(storeDir, relName, contents) {
|
|
72
|
+
const path = join(storeDir, `${relName}.md`);
|
|
73
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
74
|
+
writeFileSync(path, contents);
|
|
75
|
+
return path;
|
|
76
|
+
}
|
|
77
|
+
before(() => {
|
|
78
|
+
prevHome = process.env['HOME'];
|
|
79
|
+
prevCanvasHome = process.env['CRTR_HOME'];
|
|
80
|
+
prevProfile = process.env['CRTR_PROFILE_ID'];
|
|
81
|
+
prevNode = process.env['CRTR_NODE_ID'];
|
|
82
|
+
prevCwd = process.cwd();
|
|
83
|
+
// realpath: the manifest owner persists realpath-resolved project paths. A raw
|
|
84
|
+
// /var/folders path on macOS makes the cwd ancestor walk and the profile
|
|
85
|
+
// pointer name two different roots, and the cwd project then reads as uncapped
|
|
86
|
+
// for the wrong reason.
|
|
87
|
+
home = realpathSync(mkdtempSync(join(tmpdir(), 'crtr-ppm-home-')));
|
|
88
|
+
canvasHome = realpathSync(mkdtempSync(join(tmpdir(), 'crtr-ppm-canvas-')));
|
|
89
|
+
process.env['HOME'] = home;
|
|
90
|
+
process.env['CRTR_HOME'] = canvasHome;
|
|
91
|
+
const store = (slug) => {
|
|
92
|
+
const dir = join(home, slug, '.crouter', 'memory');
|
|
93
|
+
mkdirSync(dir, { recursive: true });
|
|
94
|
+
return dir;
|
|
95
|
+
};
|
|
96
|
+
const pnone = store('pnone');
|
|
97
|
+
const pname = store('pname');
|
|
98
|
+
const ppreview = store('ppreview');
|
|
99
|
+
const pcontent = store('pcontent');
|
|
100
|
+
const user = join(home, '.crouter', 'memory');
|
|
101
|
+
mkdirSync(user, { recursive: true });
|
|
102
|
+
writeDoc(pnone, 't9-pnone-doc', autoDoc('When the none project delivers', B.pnone));
|
|
103
|
+
writeDoc(pname, 't9-pname-doc', autoDoc('When the name project delivers', B.pname));
|
|
104
|
+
ppreviewDocPath = writeDoc(ppreview, 't9-ppreview-doc', autoDoc('When the cwd project delivers', B.ppreview));
|
|
105
|
+
writeDoc(pcontent, 't9-pcontent-doc', autoDoc('When the content project delivers', B.pcontent));
|
|
106
|
+
// Collision one: `none` (earlier in manifest order) vs `content`. The cap must
|
|
107
|
+
// apply before name dedupe, so the dropped entry cannot shadow the winner.
|
|
108
|
+
writeDoc(pnone, 't9-shared-doc', autoDoc('When the shared name resolves', B.sharedFromPnone));
|
|
109
|
+
writeDoc(pcontent, 't9-shared-doc', autoDoc('When the shared name resolves', B.sharedFromPcontent));
|
|
110
|
+
// Collision two: `name` vs `content`. A non-none cap keeps its project's
|
|
111
|
+
// precedence and discloses at the capped rung — it does not step aside.
|
|
112
|
+
writeDoc(pname, 't9-namedup-doc', autoDoc('When the name-capped duplicate resolves', B.nameFromPname));
|
|
113
|
+
writeDoc(pcontent, 't9-namedup-doc', autoDoc('When the name-capped duplicate resolves', B.nameFromPcontent));
|
|
114
|
+
// Collision three: `none` vs the wider user scope, which project precedence
|
|
115
|
+
// would otherwise beat.
|
|
116
|
+
writeDoc(pnone, 't9-userdup-doc', autoDoc('When the user duplicate resolves', B.userDupFromPnone));
|
|
117
|
+
writeDoc(user, 't9-userdup-doc', autoDoc('When the user duplicate resolves', B.userDupFromUser));
|
|
118
|
+
// Hidden-count pair: a top-level dir always renders, so a dir holding only a
|
|
119
|
+
// bootless doc shows as `<dir>/` + `[+1 more]` — unless its project is `none`.
|
|
120
|
+
writeDoc(pnone, join('t9-none-hidden', 'probe'), bootlessDoc('When counted', 'HIDDEN-NONE'));
|
|
121
|
+
writeDoc(pcontent, join('t9-hidden-control', 'probe'), bootlessDoc('When counted', 'HIDDEN-CONTROL'));
|
|
122
|
+
profileId = createProfile('t9', [
|
|
123
|
+
{ path: join(home, 'pnone'), memory: 'none' },
|
|
124
|
+
{ path: join(home, 'pname'), memory: 'name' },
|
|
125
|
+
{ path: join(home, 'ppreview'), memory: 'preview' },
|
|
126
|
+
{ path: join(home, 'pcontent'), memory: 'content' },
|
|
127
|
+
]).profileId;
|
|
128
|
+
process.env['CRTR_PROFILE_ID'] = profileId;
|
|
129
|
+
// The node works INSIDE a capped project: the reported failure.
|
|
130
|
+
cwdProject = join(home, 'ppreview');
|
|
131
|
+
process.chdir(cwdProject);
|
|
132
|
+
});
|
|
133
|
+
beforeEach(() => {
|
|
134
|
+
resetScopeCache();
|
|
135
|
+
clearSessionCache();
|
|
136
|
+
});
|
|
137
|
+
after(() => {
|
|
138
|
+
closeDb();
|
|
139
|
+
resetScopeCache();
|
|
140
|
+
clearSessionCache();
|
|
141
|
+
process.chdir(prevCwd);
|
|
142
|
+
rmSync(home, { recursive: true, force: true });
|
|
143
|
+
rmSync(canvasHome, { recursive: true, force: true });
|
|
144
|
+
const restore = (key, value) => {
|
|
145
|
+
if (value === undefined)
|
|
146
|
+
delete process.env[key];
|
|
147
|
+
else
|
|
148
|
+
process.env[key] = value;
|
|
149
|
+
};
|
|
150
|
+
restore('HOME', prevHome);
|
|
151
|
+
restore('CRTR_HOME', prevCanvasHome);
|
|
152
|
+
restore('CRTR_PROFILE_ID', prevProfile);
|
|
153
|
+
restore('CRTR_NODE_ID', prevNode);
|
|
154
|
+
});
|
|
155
|
+
function newNode() {
|
|
156
|
+
return spawnNode({ kind: 'general', cwd: cwdProject, parent: null, profile_id: profileId }).node_id;
|
|
157
|
+
}
|
|
158
|
+
test('boot delivery caps every profile project at its relationship, including the one used as cwd', () => {
|
|
159
|
+
const boot = renderKnowledgeBlock(newNode(), new Map());
|
|
160
|
+
// The control: an uncapped relationship still renders its authored content.
|
|
161
|
+
assert.ok(boot.includes(B.pcontent), `the content-capped project renders its body; got:\n${boot}`);
|
|
162
|
+
// The reported failure. cwd reaches this root before the manifest pointer
|
|
163
|
+
// does, and the relationship still decides the ceiling.
|
|
164
|
+
assert.ok(!boot.includes(B.ppreview), `the cwd project is capped at preview — its body must not render; got:\n${boot}`);
|
|
165
|
+
assert.match(boot, /t9-ppreview-doc {2}# read when: When the cwd project delivers\./, `the cwd project's doc discloses at preview; got:\n${boot}`);
|
|
166
|
+
// `name` discloses the bare identifier and nothing else.
|
|
167
|
+
assert.ok(!boot.includes(B.pname), 'a name-capped project must not render a body');
|
|
168
|
+
assert.match(boot, /[├└]─ t9-pname-doc$/m, `the name-capped doc discloses as a bare name; got:\n${boot}`);
|
|
169
|
+
assert.ok(!boot.includes('When the name project delivers'), 'a name-capped doc discloses no preview line');
|
|
170
|
+
});
|
|
171
|
+
test('a `none` project contributes nothing to boot — no body, no name, no hidden count, no shadow', () => {
|
|
172
|
+
const boot = renderKnowledgeBlock(newNode(), new Map());
|
|
173
|
+
assert.ok(!boot.includes(B.pnone), 'a none-capped body must not render');
|
|
174
|
+
assert.ok(!boot.includes('t9-pnone-doc'), 'a none-capped doc must not appear as a name either');
|
|
175
|
+
// Hidden counts are disclosure too: the control dir proves a bootless doc
|
|
176
|
+
// does surface as a `[+N more]` count, so the none project's absence is the cap.
|
|
177
|
+
assert.match(boot, /t9-hidden-control\/\n.*\[\+1 more\]/, `the control dir shows its hidden count; got:\n${boot}`);
|
|
178
|
+
assert.ok(!boot.includes('t9-none-hidden'), 'a none-capped project contributes no `[+N more]` count');
|
|
179
|
+
// Shadowing: `none` wins name dedupe on precedence, so a cap applied after
|
|
180
|
+
// dedupe would silently delete the winner instead of deferring to it.
|
|
181
|
+
assert.ok(boot.includes(B.sharedFromPcontent) && !boot.includes(B.sharedFromPnone), `the content project wins the collision the none project would have taken; got:\n${boot}`);
|
|
182
|
+
assert.ok(boot.includes(B.userDupFromUser) && !boot.includes(B.userDupFromPnone), `a none project does not shadow the wider user-scope winner; got:\n${boot}`);
|
|
183
|
+
});
|
|
184
|
+
test('a non-none cap keeps its project precedence and discloses the duplicate at the capped rung', () => {
|
|
185
|
+
const boot = renderKnowledgeBlock(newNode(), new Map());
|
|
186
|
+
assert.match(boot, /[├└]─ t9-namedup-doc$/m, `the name-capped duplicate wins dedupe; got:\n${boot}`);
|
|
187
|
+
assert.ok(!boot.includes(B.nameFromPname), 'the winner discloses at `name`, never its body');
|
|
188
|
+
assert.ok(!boot.includes(B.nameFromPcontent), 'the content project does not take a collision the name project already won');
|
|
189
|
+
});
|
|
190
|
+
test('workspace-open clamps each project store the same way boot does', () => {
|
|
191
|
+
const opened = renderWorkspaceOpenDocs(newNode(), new Map());
|
|
192
|
+
assert.ok(opened.includes(B.pcontent), `the content-capped project delivers its body; got:\n${opened}`);
|
|
193
|
+
assert.ok(!opened.includes(B.ppreview), `the cwd project must not deliver its body; got:\n${opened}`);
|
|
194
|
+
assert.match(opened, /name="t9-ppreview-doc"[^>]*>\nWhen the cwd project delivers\./, `the cwd project delivers its preview line; got:\n${opened}`);
|
|
195
|
+
assert.match(opened, /name="t9-pname-doc"[^>]*\/>/, `the name-capped doc delivers name-only; got:\n${opened}`);
|
|
196
|
+
assert.ok(!opened.includes('t9-pnone-doc'), 'the none-capped project delivers nothing at workspace-open');
|
|
197
|
+
assert.ok(!opened.includes(B.sharedFromPnone), 'nor its side of a name collision');
|
|
198
|
+
});
|
|
199
|
+
test('an explicit `crtr memory read` returns and records the full body a cap withheld', async () => {
|
|
200
|
+
const node = newNode();
|
|
201
|
+
process.env['CRTR_NODE_ID'] = node;
|
|
202
|
+
const seen = loadInjectedDocs(node);
|
|
203
|
+
const opened = renderWorkspaceOpenDocs(node, seen);
|
|
204
|
+
assert.ok(!opened.includes(B.ppreview), 'workspace-open disclosed the capped doc at preview only');
|
|
205
|
+
saveInjectedDocs(node, seen);
|
|
206
|
+
const result = (await readLeaf.run({ name: 't9-ppreview-doc', frontmatter: false }));
|
|
207
|
+
assert.equal(result.path, ppreviewDocPath, 'the read resolved the cwd project doc');
|
|
208
|
+
assert.ok(result.content.includes(B.ppreview), `an explicit read is uncapped and returns the full body; got:\n${result.content}`);
|
|
209
|
+
const afterRead = loadInjectedDocs(node);
|
|
210
|
+
assert.ok(deliveredAtOrAbove(afterRead, ppreviewDocPath, 'content'), 'the explicit read records a content delivery, upgrading the preview disclosure');
|
|
211
|
+
// A later automatic event runs at the capped rung, which is now below what the
|
|
212
|
+
// transcript holds, so it re-delivers nothing.
|
|
213
|
+
assert.equal(renderWorkspaceOpenDocs(node, afterRead), '', 'a later lower automatic event re-delivers nothing');
|
|
214
|
+
// ...and the emptiness is the dedup set, not a dead render.
|
|
215
|
+
assert.ok(renderWorkspaceOpenDocs(node, new Map()).includes('t9-ppreview-doc'), 'the same call against a fresh transcript still delivers');
|
|
216
|
+
delete process.env['CRTR_NODE_ID'];
|
|
217
|
+
});
|
|
@@ -19,20 +19,23 @@
|
|
|
19
19
|
// they belong to the compiled seam tier; neither needs tmux.
|
|
20
20
|
import { after, before, test } from 'node:test';
|
|
21
21
|
import assert from 'node:assert/strict';
|
|
22
|
-
import { writeFileSync } from 'node:fs';
|
|
22
|
+
import { readFileSync, writeFileSync } from 'node:fs';
|
|
23
23
|
import { join } from 'node:path';
|
|
24
24
|
import { appendInbox, readCursor, readInboxSince } from '../../feed/inbox.js';
|
|
25
25
|
import { registerWarmSpare, subscribe, updateNode } from '../../canvas/index.js';
|
|
26
26
|
import { isPidAlive } from '../../canvas/pid.js';
|
|
27
27
|
import { reviveNode } from '../../runtime/revive.js';
|
|
28
28
|
import { createHarness } from '../helpers/harness.js';
|
|
29
|
+
import { createAttachKit } from '../helpers/broker-clients.js';
|
|
29
30
|
let h;
|
|
30
31
|
let root;
|
|
32
|
+
const attach = createAttachKit(() => h);
|
|
31
33
|
before(async () => {
|
|
32
34
|
h = await createHarness({ headless: true, sessionPrefix: 'crtr-dormancy' });
|
|
33
35
|
root = h.spawnRoot('dormancy-release suite root');
|
|
34
36
|
});
|
|
35
37
|
after(async () => {
|
|
38
|
+
attach.closeAll();
|
|
36
39
|
if (h !== undefined)
|
|
37
40
|
await h.dispose();
|
|
38
41
|
});
|
|
@@ -187,6 +190,39 @@ test('an unattended resident with no live obligation completes on the daemon clo
|
|
|
187
190
|
process.env['CRTR_TEST_UNATTENDED_PARK_MS'] = priorParkMs;
|
|
188
191
|
}
|
|
189
192
|
});
|
|
193
|
+
test('a non-attending observer does not prevent unattended resident parking', { timeout: 30_000 }, async () => {
|
|
194
|
+
const priorParkMs = process.env['CRTR_TEST_UNATTENDED_PARK_MS'];
|
|
195
|
+
process.env['CRTR_TEST_UNATTENDED_PARK_MS'] = '1500';
|
|
196
|
+
const nodeId = h.fabricateBrokerNode({
|
|
197
|
+
id: 'settled-with-system-observer',
|
|
198
|
+
parent: root,
|
|
199
|
+
lifecycle: 'resident',
|
|
200
|
+
status: 'idle',
|
|
201
|
+
intent: 'idle-release',
|
|
202
|
+
});
|
|
203
|
+
updateNode(nodeId, { launch: { extensions: [], tools: [], systemPrompt: '', env: {} } });
|
|
204
|
+
reviveNode(nodeId, { resume: true });
|
|
205
|
+
const boot = await h.awaitBoot(nodeId);
|
|
206
|
+
await attach.attach(nodeId, 'observer', 'system-observer-test', { attends: false });
|
|
207
|
+
assert.equal(JSON.parse(readFileSync(join(h.home, 'nodes', nodeId, 'job', 'attach.json'), 'utf8')).viewers, 0, 'a non-attending hello is not persisted as human attendance');
|
|
208
|
+
try {
|
|
209
|
+
const seenAt = 12_000;
|
|
210
|
+
await h.tick(seenAt);
|
|
211
|
+
await h.tick(seenAt + 1500);
|
|
212
|
+
await h.waitFor(() => h.node(nodeId)?.status === 'done' && !isPidAlive(boot.pid), {
|
|
213
|
+
timeoutMs: 10_000,
|
|
214
|
+
intervalMs: 20,
|
|
215
|
+
label: 'system-observed unattended completion and broker exit',
|
|
216
|
+
});
|
|
217
|
+
await h.awaitFleetExit(nodeId);
|
|
218
|
+
}
|
|
219
|
+
finally {
|
|
220
|
+
if (priorParkMs === undefined)
|
|
221
|
+
delete process.env['CRTR_TEST_UNATTENDED_PARK_MS'];
|
|
222
|
+
else
|
|
223
|
+
process.env['CRTR_TEST_UNATTENDED_PARK_MS'] = priorParkMs;
|
|
224
|
+
}
|
|
225
|
+
});
|
|
190
226
|
test('a connected broker viewer keeps an obligation-free resident live', { timeout: 30_000 }, async () => {
|
|
191
227
|
const priorParkMs = process.env['CRTR_TEST_UNATTENDED_PARK_MS'];
|
|
192
228
|
process.env['CRTR_TEST_UNATTENDED_PARK_MS'] = '1500';
|
|
@@ -26,9 +26,10 @@ import { tmpdir } from 'node:os';
|
|
|
26
26
|
import { join, dirname } from 'node:path';
|
|
27
27
|
import { fileURLToPath } from 'node:url';
|
|
28
28
|
import { createAgentSessionServices, createAgentSessionFromServices, SessionManager, VERSION, } from '../../runtime/broker-sdk.js';
|
|
29
|
-
import { buildBrokerSession, makeBrokerUiContext } from '../../runtime/broker.js';
|
|
30
|
-
import { isLeadingEngineCommand, resolveSteerCall, chooseGuidanceDeliveryMode, promptWithNextTurnGuidance, isHiddenJsonlBranchEntry, stripHiddenEntriesFromJsonlExport, engineLeadingCommandTokens, } from '../../runtime/broker/frame-dispatch.js';
|
|
29
|
+
import { buildBrokerSession, makeBrokerUiContext, snapshotMessages } from '../../runtime/broker.js';
|
|
30
|
+
import { isLeadingEngineCommand, resolveSteerCall, chooseGuidanceDeliveryMode, promptWithNextTurnGuidance, promptWithAdvertisedCommandInvocation, isHiddenJsonlBranchEntry, stripHiddenEntriesFromJsonlExport, engineLeadingCommandTokens, } from '../../runtime/broker/frame-dispatch.js';
|
|
31
31
|
import { CANVAS_EXTENSIONS } from '../../runtime/launch.js';
|
|
32
|
+
import { prepareAdvertisedCommandPersistence } from '../../runtime/advertised-command-invocation.js';
|
|
32
33
|
// The REAL engine, assembled from broker-sdk's static SDK re-exports — bypasses
|
|
33
34
|
// the CRTR_BROKER_ENGINE seam entirely so these tests can never accidentally hit
|
|
34
35
|
// the fake (the whole point: prove the wiring the mock hid).
|
|
@@ -403,6 +404,105 @@ test('C5 — isLeadingEngineCommand against a REAL session (registered extension
|
|
|
403
404
|
rmSync(cwd, { recursive: true, force: true });
|
|
404
405
|
}
|
|
405
406
|
});
|
|
407
|
+
test('an advertised custom command persists its exact raw invocation without replacing expanded agent input', async () => {
|
|
408
|
+
const cwd = mkdtempSync(join(tmpdir(), 'crtr-advertised-command-'));
|
|
409
|
+
try {
|
|
410
|
+
const { session } = await buildBrokerSession(realEngine, cfg(cwd, { extensionPaths: [C5_EXT] }));
|
|
411
|
+
try {
|
|
412
|
+
const raw = '/gatewaycmd exact spacing ';
|
|
413
|
+
await promptWithAdvertisedCommandInvocation(session, raw, () => session.prompt(raw, {}));
|
|
414
|
+
const custom = session.sessionManager
|
|
415
|
+
.buildSessionContext()
|
|
416
|
+
.messages.find((message) => message.role === 'custom' && message.customType === 'fixture-output');
|
|
417
|
+
assert.deepEqual(custom, {
|
|
418
|
+
role: 'custom',
|
|
419
|
+
customType: 'fixture-output',
|
|
420
|
+
content: 'expanded agent input',
|
|
421
|
+
display: true,
|
|
422
|
+
details: { fixture: true },
|
|
423
|
+
crouterAdvertisedCommandInvocation: { raw },
|
|
424
|
+
timestamp: custom?.timestamp,
|
|
425
|
+
});
|
|
426
|
+
const welcome = snapshotMessages(session);
|
|
427
|
+
assert.ok(welcome);
|
|
428
|
+
const welcomeCustom = welcome.filter(({ message }) => message.role === 'custom' && message.customType === 'fixture-output');
|
|
429
|
+
assert.equal(welcomeCustom.length, 1, 'welcome reconstructs one advertised custom message');
|
|
430
|
+
assert.deepEqual(welcomeCustom[0].message
|
|
431
|
+
.crouterAdvertisedCommandInvocation, { raw }, 'welcome preserves the exact advertised invocation spacing');
|
|
432
|
+
const persisted = session.sessionManager
|
|
433
|
+
.getEntries()
|
|
434
|
+
.find((entry) => entry.type === 'custom_message' &&
|
|
435
|
+
entry.customType === 'fixture-output');
|
|
436
|
+
assert.equal(persisted?.type, 'custom_message');
|
|
437
|
+
if (persisted?.type !== 'custom_message')
|
|
438
|
+
assert.fail('custom message was not persisted');
|
|
439
|
+
assert.equal(persisted.content, 'expanded agent input');
|
|
440
|
+
assert.deepEqual(persisted.details, {
|
|
441
|
+
crouterPersistenceEnvelope: true,
|
|
442
|
+
crouterAdvertisedCommandInvocation: { raw },
|
|
443
|
+
extensionDetails: { fixture: true },
|
|
444
|
+
});
|
|
445
|
+
}
|
|
446
|
+
finally {
|
|
447
|
+
session.dispose();
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
finally {
|
|
451
|
+
rmSync(cwd, { recursive: true, force: true });
|
|
452
|
+
}
|
|
453
|
+
});
|
|
454
|
+
test('advertised invocation metadata survives nextTurn delivery and null content normalization', async () => {
|
|
455
|
+
const cwd = mkdtempSync(join(tmpdir(), 'crtr-advertised-next-turn-'));
|
|
456
|
+
try {
|
|
457
|
+
const { session } = await buildBrokerSession(realEngine, cfg(cwd, { extensionPaths: [C5_EXT] }));
|
|
458
|
+
try {
|
|
459
|
+
const rawA = '/gatewaynext first exact text ';
|
|
460
|
+
const rawB = '/gatewaynext second exact text ';
|
|
461
|
+
for (const raw of [rawA, rawB]) {
|
|
462
|
+
await promptWithAdvertisedCommandInvocation(session, raw, () => session.prompt(raw, {}));
|
|
463
|
+
}
|
|
464
|
+
const pending = session._pendingNextTurnMessages;
|
|
465
|
+
assert.deepEqual(pending.map((queued) => ({
|
|
466
|
+
content: queued.content,
|
|
467
|
+
details: queued.details,
|
|
468
|
+
invocation: queued.crouterAdvertisedCommandInvocation,
|
|
469
|
+
})), [
|
|
470
|
+
{
|
|
471
|
+
content: [],
|
|
472
|
+
details: 'extension-details',
|
|
473
|
+
invocation: { raw: rawA },
|
|
474
|
+
},
|
|
475
|
+
{
|
|
476
|
+
content: [],
|
|
477
|
+
details: 'extension-details',
|
|
478
|
+
invocation: { raw: rawB },
|
|
479
|
+
},
|
|
480
|
+
]);
|
|
481
|
+
// Simulate a queue policy draining B before A. Payload equality cannot
|
|
482
|
+
// identify them; the app-message metadata must bind each persistence.
|
|
483
|
+
for (const queued of [pending[1], pending[0]]) {
|
|
484
|
+
prepareAdvertisedCommandPersistence(session, queued);
|
|
485
|
+
session.sessionManager.appendCustomMessageEntry(queued.customType, queued.content, queued.display, queued.details);
|
|
486
|
+
}
|
|
487
|
+
const restored = session.sessionManager
|
|
488
|
+
.buildSessionContext()
|
|
489
|
+
.messages.slice(-2);
|
|
490
|
+
assert.deepEqual(restored.map((message) => ({
|
|
491
|
+
details: message.details,
|
|
492
|
+
invocation: message.crouterAdvertisedCommandInvocation,
|
|
493
|
+
})), [
|
|
494
|
+
{ details: 'extension-details', invocation: { raw: rawB } },
|
|
495
|
+
{ details: 'extension-details', invocation: { raw: rawA } },
|
|
496
|
+
]);
|
|
497
|
+
}
|
|
498
|
+
finally {
|
|
499
|
+
session.dispose();
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
finally {
|
|
503
|
+
rmSync(cwd, { recursive: true, force: true });
|
|
504
|
+
}
|
|
505
|
+
});
|
|
406
506
|
test('C5 — chooseGuidanceDeliveryMode truth table (pure)', () => {
|
|
407
507
|
// idle prompt (routeCall='prompt', not streaming) -> guidance precedes the
|
|
408
508
|
// human call, in the SAME turn.
|
package/dist/core/bootstrap.js
CHANGED
|
@@ -14,6 +14,12 @@ function shouldSkipForArgv(argv) {
|
|
|
14
14
|
const sub = argv[2];
|
|
15
15
|
if (sub === undefined)
|
|
16
16
|
return true;
|
|
17
|
+
// `sys migrate` converges state that every other read already depends on:
|
|
18
|
+
// resolving a scope here would read the selected profile through the current
|
|
19
|
+
// reader (blind to a pre-migration manifest) and could scaffold a cwd scope
|
|
20
|
+
// the migration then has to treat as real. It owns its own discovery.
|
|
21
|
+
if (sub === 'sys' && argv[3] === 'migrate')
|
|
22
|
+
return true;
|
|
17
23
|
return SKIP_SUBCOMMANDS.has(sub);
|
|
18
24
|
}
|
|
19
25
|
export function ensureProjectScope(argv) {
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
import { execFileSync } from 'node:child_process';
|
|
11
11
|
import { resolve } from 'node:path';
|
|
12
12
|
import { dashboardRowsAllFromSource, enrichRowsFromSource, loadPreviewFromSource, renderForestFromSource } from '../render-source.js';
|
|
13
|
+
import { fullName } from '../labels.js';
|
|
13
14
|
import { setupTerminal, restoreTerminal, getTerminalSize, parseKeypress, } from '../../tui/terminal.js';
|
|
14
15
|
import { buildTree, flatten, pruneNode, TABS } from './model.js';
|
|
15
16
|
import { loadPins, savePins } from './pins.js';
|
|
@@ -483,8 +484,10 @@ export async function runBrowse(opts) {
|
|
|
483
484
|
/* best-effort — a rename failure never wedges the picker */
|
|
484
485
|
}
|
|
485
486
|
const n = tree.nodes.get(id);
|
|
486
|
-
if (n !== undefined)
|
|
487
|
-
n.row.
|
|
487
|
+
if (n !== undefined) {
|
|
488
|
+
n.row.handle = name;
|
|
489
|
+
n.row.name = fullName({ name, kind: n.row.kind, description: n.row.description });
|
|
490
|
+
}
|
|
488
491
|
}
|
|
489
492
|
recompute(state.renameTarget ?? undefined);
|
|
490
493
|
scheduleFlush();
|
|
@@ -70,11 +70,8 @@ export interface SnippetLine {
|
|
|
70
70
|
text: string;
|
|
71
71
|
hi: Set<number>;
|
|
72
72
|
}
|
|
73
|
-
/**
|
|
74
|
-
*
|
|
75
|
-
* scattered subsequence indices, else empty (empty query). This is a different,
|
|
76
|
-
* stricter model than the subsequence super-search on purpose — a contiguous span
|
|
77
|
-
* is what reads as a highlight. */
|
|
73
|
+
/** The literal case-insensitive substring span in a conversation preview.
|
|
74
|
+
* Scattered letters are not a message match and never receive a highlight. */
|
|
78
75
|
export declare function highlightIndices(query: string, text: string): Set<number>;
|
|
79
76
|
/**
|
|
80
77
|
* Build the preview snippet for `text` under the live `query`: up to `maxLines`
|
|
@@ -91,17 +88,27 @@ export declare function previewSnippet(query: string, text: string, width: numbe
|
|
|
91
88
|
* windowed in the preview, so search matches a prompt from ANYWHERE in the
|
|
92
89
|
* conversation, not just the first one. */
|
|
93
90
|
export declare function promptText(row: DashboardRow): string;
|
|
94
|
-
/** Does this row match the live query?
|
|
95
|
-
*
|
|
96
|
-
* in the conversation (`promptText`). Empty query matches everything. */
|
|
91
|
+
/** Does this row match the live query? Empty query matches everything; a
|
|
92
|
+
* non-empty query uses the same fields and match tiers as relevance ranking. */
|
|
97
93
|
export declare function queryMatch(query: string, row: DashboardRow): boolean;
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
/**
|
|
103
|
-
|
|
104
|
-
|
|
94
|
+
export type MatchTier = 0 | 1 | 2 | 3;
|
|
95
|
+
export interface FieldMatch {
|
|
96
|
+
/** 3 = bounded word/phrase, 2 = contiguous partial, 1 = subsequence. */
|
|
97
|
+
tier: MatchTier;
|
|
98
|
+
/** Tie-break quality within one tier. */
|
|
99
|
+
quality: number;
|
|
100
|
+
}
|
|
101
|
+
export interface RelevanceScore {
|
|
102
|
+
tier: MatchTier;
|
|
103
|
+
strength: number;
|
|
104
|
+
}
|
|
105
|
+
/** One field's best match. Match class is deliberately separate from quality:
|
|
106
|
+
* a bounded literal always outranks a contiguous partial, which always outranks
|
|
107
|
+
* a scattered subsequence, regardless of field weights. */
|
|
108
|
+
export declare function fieldMatch(query: string, text: string): FieldMatch;
|
|
109
|
+
/** Rank a row by its strongest match class, then by weighted evidence only from
|
|
110
|
+
* that class. Weaker classes cannot add up to beat one stronger literal match. */
|
|
111
|
+
export declare function scoreRow(query: string, row: DashboardRow): RelevanceScore;
|
|
105
112
|
/** The "most recent message" signal for the attention sort: the pi session-file
|
|
106
113
|
* mtime (ms) the snapshot stamps on each row, falling back to the `created`
|
|
107
114
|
* birth timestamp when no session file exists. Larger = more recent.
|
|
@@ -111,6 +118,9 @@ export declare function scoreRow(query: string, row: DashboardRow): number;
|
|
|
111
118
|
* update this single accessor. `created` is ISO 8601, so its epoch-ms is a
|
|
112
119
|
* monotonic stand-in within the same tier. */
|
|
113
120
|
export declare function attentionMtime(row: DashboardRow): number;
|
|
121
|
+
/** Up to a 10% relevance lift for recent conversation activity, halving after a
|
|
122
|
+
* week. Match tier remains separate, so freshness never promotes fuzzy over literal. */
|
|
123
|
+
export declare function relevanceFreshness(row: DashboardRow, now: number): number;
|
|
114
124
|
/** Attention tier (lower = higher priority, shown first):
|
|
115
125
|
* T0 — attached AND streaming (`viewed && streaming`)
|
|
116
126
|
* T1 — attached, not streaming (`viewed`)
|