@north-light/crouter 0.3.221 → 0.3.223
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 +21 -1
- package/dist/api/client.js +34 -0
- package/dist/api/dto/chat-inventory.d.ts +13 -0
- package/dist/api/dto/human-requests.d.ts +88 -0
- package/dist/api/dto/human-requests.js +4 -0
- package/dist/api/dto/human.d.ts +3 -0
- package/dist/api/dto/reviews.d.ts +2 -0
- package/dist/api/index.d.ts +1 -0
- package/dist/api/index.js +1 -0
- package/dist/api/routes.d.ts +7 -0
- package/dist/api/routes.js +10 -0
- package/dist/builtin-memory/00-runtime-base/00-authoring.md +31 -0
- package/dist/builtin-memory/00-runtime-base/01-escalation.md +14 -0
- package/dist/builtin-memory/{insights/listen.md → 00-runtime-base/02-insight-capture.md} +1 -0
- package/dist/builtin-memory/02-turn-lifecycle/00-ending-a-turn.md +27 -0
- package/dist/builtin-memory/{02-lifecycle/01-resident.md → 02-turn-lifecycle/02-resident.md} +5 -0
- package/dist/builtin-memory/04-base-worker.md +4 -8
- package/dist/builtin-memory/04-orchestration-kernel.md +1 -1
- package/dist/builtin-memory/05-kinds/advisor/01-orchestrator.md +1 -0
- package/dist/builtin-memory/05-kinds/advisor/advice-contract.md +1 -0
- package/dist/builtin-memory/05-kinds/design/00-base.md +2 -1
- package/dist/builtin-memory/05-kinds/design/01-orchestrator.md +2 -1
- package/dist/builtin-memory/05-kinds/design/design-contract.md +19 -0
- package/dist/builtin-memory/05-kinds/developer/00-base.md +1 -0
- package/dist/builtin-memory/05-kinds/developer/01-orchestrator.md +1 -0
- package/dist/builtin-memory/05-kinds/explore/00-base.md +1 -0
- package/dist/builtin-memory/05-kinds/explore/01-orchestrator.md +1 -0
- package/dist/builtin-memory/05-kinds/general/00-base.md +1 -0
- package/dist/builtin-memory/05-kinds/plan/00-base.md +2 -1
- package/dist/builtin-memory/05-kinds/plan/01-orchestrator.md +2 -1
- package/dist/builtin-memory/05-kinds/plan/plan-contract.md +28 -0
- package/dist/builtin-memory/05-kinds/plan/reviewers/architecture-fit.md +1 -0
- package/dist/builtin-memory/05-kinds/plan/reviewers/code-smells.md +1 -0
- package/dist/builtin-memory/05-kinds/plan/reviewers/lens-contract.md +1 -0
- package/dist/builtin-memory/05-kinds/plan/reviewers/pattern-consistency.md +1 -0
- package/dist/builtin-memory/05-kinds/plan/reviewers/requirements-coverage.md +1 -0
- package/dist/builtin-memory/05-kinds/plan/reviewers/security.md +1 -0
- package/dist/builtin-memory/05-kinds/review/00-base.md +1 -0
- package/dist/builtin-memory/05-kinds/review/01-orchestrator.md +1 -0
- package/dist/builtin-memory/05-kinds/review/companion/00-base.md +1 -0
- package/dist/builtin-memory/05-kinds/review/security-findings.md +1 -0
- package/dist/builtin-memory/05-kinds/spec/00-base.md +4 -3
- package/dist/builtin-memory/05-kinds/spec/01-orchestrator.md +1 -0
- package/dist/builtin-memory/05-kinds/spec/requirements.md +1 -0
- package/dist/builtin-memory/design/guide.md +35 -0
- package/dist/builtin-memory/design/roadmap.md +21 -0
- package/dist/builtin-memory/insights/capture.md +1 -1
- package/dist/builtin-memory/internal/memory-loading.md +4 -4
- package/dist/builtin-memory/internal/plugins.md +10 -1
- package/dist/builtin-memory/internal/storage-tiers.md +1 -1
- package/dist/builtin-memory/plan/roadmap.md +6 -28
- package/dist/builtin-memory/spec/guide.md +19 -8
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/memory-slash-commands.ts +28 -15
- package/dist/clients/attach/render/markdown-source.js +106 -1
- package/dist/clients/attach/session/file-links.d.ts +13 -4
- package/dist/clients/attach/session/file-links.js +54 -58
- package/dist/clients/attach/viewer.js +545 -543
- package/dist/clients/inbox/controller.js +1 -1
- package/dist/clients/inbox/page-adapter.js +2 -1
- package/dist/clients/inbox/resolve.d.ts +1 -0
- package/dist/clients/inbox/review/review-client.js +3 -1
- package/dist/clients/inbox/tui/panel.js +23 -4
- package/dist/clients/inbox/tui/render.js +6 -0
- package/dist/clients/inbox/tui/types.d.ts +8 -0
- package/dist/commands/__tests__/human.test.js +2 -2
- package/dist/commands/human/request.d.ts +2 -0
- package/dist/commands/human/request.js +281 -0
- package/dist/commands/human.js +5 -2
- package/dist/commands/memory/shared.d.ts +2 -2
- package/dist/commands/memory/shared.js +11 -6
- package/dist/commands/pkg/browse/doc-view.js +2 -0
- package/dist/commands/sys/config.js +2 -2
- package/dist/commands/sys/doctor.js +54 -2
- package/dist/core/__tests__/broker-extension-canvas-db-boundary.test.js +7 -4
- package/dist/core/__tests__/fixtures/memory-slash-live-probe.d.ts +1 -0
- package/dist/core/__tests__/fixtures/memory-slash-live-probe.js +71 -0
- package/dist/core/__tests__/human-action-delivery.test.d.ts +1 -0
- package/dist/core/__tests__/human-action-delivery.test.js +140 -0
- package/dist/core/__tests__/human-actions.test.d.ts +1 -0
- package/dist/core/__tests__/human-actions.test.js +116 -0
- package/dist/core/__tests__/inline-memory-refs.test.js +1 -1
- package/dist/core/__tests__/profile-project-memory-delivery.test.js +70 -3
- package/dist/core/__tests__/prospective-inventory-capability-parity.test.d.ts +1 -0
- package/dist/core/__tests__/prospective-inventory-capability-parity.test.js +91 -0
- package/dist/core/__tests__/seam/memory-slash-node-relative-inventory.test.d.ts +1 -0
- package/dist/core/__tests__/seam/memory-slash-node-relative-inventory.test.js +127 -0
- package/dist/core/__tests__/seam/prospective-inventory-stdout.test.d.ts +1 -0
- package/dist/core/__tests__/seam/prospective-inventory-stdout.test.js +31 -0
- package/dist/core/canvas/db.js +23 -0
- package/dist/core/canvas/human-deliveries.d.ts +53 -0
- package/dist/core/canvas/human-deliveries.js +75 -0
- package/dist/core/config.d.ts +13 -1
- package/dist/core/config.js +51 -1
- package/dist/core/feed/inbox.d.ts +6 -0
- package/dist/core/feed/inbox.js +9 -1
- package/dist/core/human/action-binding.d.ts +21 -0
- package/dist/core/human/action-binding.js +40 -0
- package/dist/core/human/completion.d.ts +38 -0
- package/dist/core/human/completion.js +27 -0
- package/dist/core/human/convention.d.ts +2 -0
- package/dist/core/human/convention.js +2 -0
- package/dist/core/human/tickets.d.ts +25 -6
- package/dist/core/human/tickets.js +19 -13
- package/dist/core/human/types.d.ts +5 -0
- package/dist/core/human-actions.d.ts +25 -0
- package/dist/core/human-actions.js +101 -0
- package/dist/core/memory-resolver.js +1 -1
- package/dist/core/profiles/select.d.ts +2 -0
- package/dist/core/profiles/select.js +21 -4
- package/dist/core/runtime/broker/frame-dispatch.js +2 -5
- package/dist/core/runtime/broker-inventory.d.ts +1 -2
- package/dist/core/runtime/broker-inventory.js +2 -77
- package/dist/core/runtime/broker-persona-guidance.js +1 -1
- package/dist/core/runtime/broker.js +4 -4
- package/dist/core/runtime/chat-inventory-rows.d.ts +8 -0
- package/dist/core/runtime/chat-inventory-rows.js +105 -0
- package/dist/core/runtime/command-surface.d.ts +8 -3
- package/dist/core/runtime/command-surface.js +42 -6
- package/dist/core/runtime/launch-target.d.ts +25 -0
- package/dist/core/runtime/launch-target.js +54 -0
- package/dist/core/runtime/persona.js +3 -3
- package/dist/core/runtime/prospective-inventory-cli.d.ts +1 -0
- package/dist/core/runtime/prospective-inventory-cli.js +61 -0
- package/dist/core/runtime/prospective-inventory.d.ts +10 -0
- package/dist/core/runtime/prospective-inventory.js +88 -0
- package/dist/core/runtime/spawn.d.ts +3 -1
- package/dist/core/runtime/spawn.js +5 -3
- package/dist/core/substrate/frontmatter-validation.d.ts +2 -5
- package/dist/core/substrate/frontmatter-validation.js +8 -4
- package/dist/core/substrate/gate.d.ts +4 -1
- package/dist/core/substrate/gate.js +5 -0
- package/dist/core/substrate/on-read.js +21 -32
- package/dist/core/substrate/render-node.d.ts +3 -2
- package/dist/core/substrate/render-node.js +3 -2
- package/dist/core/substrate/render.js +64 -37
- package/dist/core/substrate/schema.d.ts +17 -8
- package/dist/core/substrate/schema.js +14 -10
- package/dist/core/substrate/surface-match.d.ts +8 -7
- package/dist/core/substrate/surface-match.js +15 -14
- package/dist/core/user-settings.d.ts +4 -0
- package/dist/core/user-settings.js +1 -0
- package/dist/daemon/api/__tests__/profile-launch-gates.test.js +52 -3
- package/dist/daemon/api/handlers/human-requests.d.ts +2 -0
- package/dist/daemon/api/handlers/human-requests.js +409 -0
- package/dist/daemon/api/handlers/human.js +3 -0
- package/dist/daemon/api/handlers/inbox.js +3 -0
- package/dist/daemon/api/handlers/nodes.d.ts +1 -3
- package/dist/daemon/api/handlers/nodes.js +11 -46
- package/dist/daemon/api/handlers/prospective-chat-inventory.d.ts +2 -0
- package/dist/daemon/api/handlers/prospective-chat-inventory.js +59 -0
- package/dist/daemon/api/handlers/reviews.js +10 -2
- package/dist/daemon/api/server.js +4 -0
- package/dist/daemon/crtrd.js +6 -0
- package/dist/daemon/human/deliver-action.d.ts +16 -0
- package/dist/daemon/human/deliver-action.js +168 -0
- package/dist/daemon/human/finish.d.ts +8 -5
- package/dist/daemon/human/finish.js +45 -6
- package/dist/daemon/human/sweep.js +4 -1
- package/dist/daemon/reconcilers/human-delivery-lane.d.ts +10 -0
- package/dist/daemon/reconcilers/human-delivery-lane.js +41 -0
- package/dist/daemon/review/finish.d.ts +8 -3
- package/dist/daemon/review/finish.js +19 -1
- package/dist/types.d.ts +8 -0
- package/dist/types.js +1 -0
- package/package.json +1 -1
- package/runtime.lock.json +2 -2
- package/dist/builtin-memory/00-runtime-base.md +0 -55
- package/dist/builtin-memory/design.md +0 -55
- /package/dist/builtin-memory/{02-lifecycle/00-terminal.md → 02-turn-lifecycle/01-terminal.md} +0 -0
|
@@ -15,10 +15,12 @@ import { detectPackageManager } from './setup-core.js';
|
|
|
15
15
|
import { validateEffectiveCommandPlugins } from '../../core/command-plugins/discovery.js';
|
|
16
16
|
import { discoverHookRegistry } from '../../core/command-hooks/discovery.js';
|
|
17
17
|
import { resolveBinContributions } from '../../core/runtime/bin-contributions.js';
|
|
18
|
+
import { inspectHumanActions } from '../../core/human-actions.js';
|
|
18
19
|
import { buildBrokerEnv, resolvePathExecutable } from '../../core/runtime/spawn-env.js';
|
|
19
20
|
import { listAllPlugins } from '../../core/resolver.js';
|
|
20
21
|
import { SUBTREE_NAMES, coreCommandPaths, coreHookCatalog } from '../../build-root.js';
|
|
21
22
|
import { processGeneration, processLocatedGeneration, runtimeManifest, selectedGeneration } from './shared.js';
|
|
23
|
+
import { CONFIG_FILE } from '../../types.js';
|
|
22
24
|
function pass(scope, name, message) {
|
|
23
25
|
return { scope, name, status: 'pass', message };
|
|
24
26
|
}
|
|
@@ -198,6 +200,55 @@ function runBinContributionChecks(scopes) {
|
|
|
198
200
|
}
|
|
199
201
|
return results;
|
|
200
202
|
}
|
|
203
|
+
/** Validate every scope-configured completion action without executing it. */
|
|
204
|
+
function runHumanActionChecks(scopes) {
|
|
205
|
+
const inScope = new Set(scopes);
|
|
206
|
+
const userConfig = join(scopeRoot('user'), CONFIG_FILE);
|
|
207
|
+
const results = [];
|
|
208
|
+
const winningOrigins = new Map();
|
|
209
|
+
for (const action of inspectHumanActions(process.cwd())) {
|
|
210
|
+
if ('kind' in action) {
|
|
211
|
+
if (action.kind === 'unknown_action')
|
|
212
|
+
continue;
|
|
213
|
+
const scope = action.configOrigin === userConfig ? 'user' : 'project';
|
|
214
|
+
const block = action.block === true;
|
|
215
|
+
const winningOrigin = block ? undefined : winningOrigins.get(action.name);
|
|
216
|
+
if (!block && winningOrigin === undefined)
|
|
217
|
+
winningOrigins.set(action.name, action.configOrigin);
|
|
218
|
+
if (!inScope.has(scope))
|
|
219
|
+
continue;
|
|
220
|
+
const checkName = block ? `humanActions:block@${action.configOrigin}` : `humanActions:${action.name}@${action.configOrigin}`;
|
|
221
|
+
const remediation = {
|
|
222
|
+
kind: 'human_action',
|
|
223
|
+
description: block
|
|
224
|
+
? `Fix or remove the \`humanActions\` block in ${action.configOrigin}. Actions run argv directly without a shell.`
|
|
225
|
+
: `Fix or remove the \`humanActions.${action.name}\` declaration in ${action.configOrigin}. Actions run argv directly without a shell.`,
|
|
226
|
+
};
|
|
227
|
+
if (winningOrigin !== undefined) {
|
|
228
|
+
results.push({
|
|
229
|
+
scope,
|
|
230
|
+
name: checkName,
|
|
231
|
+
status: 'pass',
|
|
232
|
+
message: `warning: shadowed by ${winningOrigin}; ${action.configOrigin}: ${action.reason}`,
|
|
233
|
+
remediation,
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
else {
|
|
237
|
+
results.push(failCheck(scope, checkName, `${action.configOrigin}: ${action.reason}`, remediation));
|
|
238
|
+
}
|
|
239
|
+
continue;
|
|
240
|
+
}
|
|
241
|
+
const scope = action.configOrigin === userConfig ? 'user' : 'project';
|
|
242
|
+
const winningOrigin = winningOrigins.get(action.name);
|
|
243
|
+
if (winningOrigin === undefined)
|
|
244
|
+
winningOrigins.set(action.name, action.configOrigin);
|
|
245
|
+
if (!inScope.has(scope))
|
|
246
|
+
continue;
|
|
247
|
+
const shadowedPrefix = winningOrigin === undefined ? '' : `shadowed by ${winningOrigin}; `;
|
|
248
|
+
results.push(pass(scope, `humanActions:${action.name}@${action.configOrigin}`, `${shadowedPrefix}${action.configOrigin} → ${action.argv[0]} (cwd ${action.cwd})`));
|
|
249
|
+
}
|
|
250
|
+
return results;
|
|
251
|
+
}
|
|
201
252
|
/** Check each enabled plugin's advisory PATH requirements against the exact
|
|
202
253
|
* broker environment a node launched from this cwd/profile receives. */
|
|
203
254
|
function runRequiresChecks(scopes) {
|
|
@@ -506,12 +557,12 @@ export const sysDoctorLeaf = defineLeaf({
|
|
|
506
557
|
{ kind: 'flag', name: 'remote', type: 'bool', required: false, constraint: 'Check git remotes with ls-remote (slow — makes network calls).' },
|
|
507
558
|
],
|
|
508
559
|
output: [
|
|
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.' },
|
|
560
|
+
{ 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. humanActions:<name>@<config-origin> checks validate each user/project scope completion-action declaration: action-name and argv shape, cwd directory, argv[0] regular-file existence, and execute permission; they never execute an action. A malformed effective declaration fails; a malformed shadowed declaration is a pass-level warning that names the winning and shadowed origins. 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.' },
|
|
510
561
|
{ name: 'ok', type: 'boolean', required: true, constraint: 'True when no unresolved fail checks remain.' },
|
|
511
562
|
],
|
|
512
563
|
outputKind: 'object',
|
|
513
564
|
effects: [
|
|
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.',
|
|
565
|
+
'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, humanActions declarations, and advisory `requires` PATH checks never execute plugin or contributed binaries.',
|
|
515
566
|
'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).',
|
|
516
567
|
'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.',
|
|
517
568
|
],
|
|
@@ -531,6 +582,7 @@ export const sysDoctorLeaf = defineLeaf({
|
|
|
531
582
|
allResults.push(...await runCommandPluginChecks(scopes));
|
|
532
583
|
allResults.push(...await runHookPluginChecks(scopes));
|
|
533
584
|
allResults.push(...runBinContributionChecks(scopes));
|
|
585
|
+
allResults.push(...runHumanActionChecks(scopes));
|
|
534
586
|
allResults.push(...runRequiresChecks(scopes));
|
|
535
587
|
allResults.push(...await runFaultChecks());
|
|
536
588
|
const processLocation = processGeneration();
|
|
@@ -41,10 +41,13 @@ function dbReach(root) {
|
|
|
41
41
|
}
|
|
42
42
|
return null;
|
|
43
43
|
}
|
|
44
|
-
test('every shipped broker extension
|
|
45
|
-
|
|
46
|
-
|
|
44
|
+
test('every shipped broker extension and the prospective preflight are transitively canvas.db-free', () => {
|
|
45
|
+
const roots = [
|
|
46
|
+
...CANVAS_EXTENSION_NAMES.map((name) => join(SOURCE_ROOT, 'pi-extensions', `${name}.ts`)),
|
|
47
|
+
join(SOURCE_ROOT, 'core/runtime/prospective-inventory-cli.ts'),
|
|
48
|
+
];
|
|
49
|
+
for (const root of roots) {
|
|
47
50
|
const reach = dbReach(root);
|
|
48
|
-
assert.equal(reach, null, `${
|
|
51
|
+
assert.equal(reach, null, `${root} reaches canvas.db through:\n${reach?.join('\n')}`);
|
|
49
52
|
}
|
|
50
53
|
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { existsSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { DefaultResourceLoader, ExtensionRunner, } from '@earendil-works/pi-coding-agent';
|
|
4
|
+
import { buildRefInventory } from '../../memory/inline-ref-inventory.js';
|
|
5
|
+
import { gatewayCommandRows, gatewayMemoryRows } from '../../runtime/chat-inventory-rows.js';
|
|
6
|
+
import { captureChatCapabilities, registrationChatSurface } from '../../runtime/command-surface.js';
|
|
7
|
+
import { runProspectiveInventory } from '../../runtime/prospective-inventory.js';
|
|
8
|
+
async function main() {
|
|
9
|
+
const extensionPath = process.argv[2];
|
|
10
|
+
const invocations = JSON.parse(process.argv[3] ?? '[]');
|
|
11
|
+
if (extensionPath === undefined)
|
|
12
|
+
throw new Error('extension path is required');
|
|
13
|
+
const cwd = process.cwd();
|
|
14
|
+
const home = process.env['CRTR_HOME'];
|
|
15
|
+
if (home === undefined || home === '')
|
|
16
|
+
throw new Error('CRTR_HOME is required');
|
|
17
|
+
const stateBeforeProspective = existsSync(home);
|
|
18
|
+
const prospective = await runProspectiveInventory({ cwd, profileId: null, extensions: [extensionPath] });
|
|
19
|
+
const stateAfterProspective = existsSync(home);
|
|
20
|
+
const loader = new DefaultResourceLoader({
|
|
21
|
+
cwd,
|
|
22
|
+
agentDir: join(cwd, '.pi'),
|
|
23
|
+
additionalExtensionPaths: [extensionPath],
|
|
24
|
+
noContextFiles: true,
|
|
25
|
+
});
|
|
26
|
+
await loader.reload();
|
|
27
|
+
const loaded = loader.getExtensions();
|
|
28
|
+
if (loaded.errors.length > 0)
|
|
29
|
+
throw new Error(JSON.stringify(loaded.errors));
|
|
30
|
+
const sent = [];
|
|
31
|
+
let activeName = '';
|
|
32
|
+
loaded.runtime.sendMessage = (message, options) => sent.push({ name: activeName, message, options });
|
|
33
|
+
const runner = new ExtensionRunner(loaded.extensions, loaded.runtime, cwd, undefined, undefined);
|
|
34
|
+
const registered = runner.getRegisteredCommands();
|
|
35
|
+
captureChatCapabilities({ commands: registered, templates: [] });
|
|
36
|
+
const commands = gatewayCommandRows(registered.map((command) => {
|
|
37
|
+
const expansion = command.expansion;
|
|
38
|
+
return {
|
|
39
|
+
name: command.invocationName,
|
|
40
|
+
description: command.description ?? '',
|
|
41
|
+
source: 'command',
|
|
42
|
+
...(expansion === undefined ? {} : { expansion }),
|
|
43
|
+
...registrationChatSurface(command),
|
|
44
|
+
};
|
|
45
|
+
}));
|
|
46
|
+
const memoryRefs = gatewayMemoryRows(buildRefInventory().refs);
|
|
47
|
+
const notifications = [];
|
|
48
|
+
for (const invocation of invocations) {
|
|
49
|
+
const command = runner.getCommand(invocation.name);
|
|
50
|
+
if (command === undefined)
|
|
51
|
+
throw new Error(`command not registered: ${invocation.name}`);
|
|
52
|
+
activeName = invocation.name;
|
|
53
|
+
await command.handler(invocation.args, {
|
|
54
|
+
cwd,
|
|
55
|
+
ui: { notify: (...args) => notifications.push(args) },
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
writeFileSync(3, JSON.stringify({
|
|
59
|
+
prospective,
|
|
60
|
+
stateBeforeProspective,
|
|
61
|
+
stateAfterProspective,
|
|
62
|
+
commands,
|
|
63
|
+
memoryRefs,
|
|
64
|
+
sent,
|
|
65
|
+
notifications,
|
|
66
|
+
}));
|
|
67
|
+
}
|
|
68
|
+
main().catch((error) => {
|
|
69
|
+
process.stderr.write(`${error instanceof Error ? error.stack ?? error.message : String(error)}\n`);
|
|
70
|
+
process.exitCode = 1;
|
|
71
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { after, afterEach, beforeEach, test } from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { chmodSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import { join } from 'node:path';
|
|
6
|
+
import { closeDb } from '../canvas/db.js';
|
|
7
|
+
import { claimActionDelivery, enqueueActionDelivery, readActionDelivery, } from '../canvas/human-deliveries.js';
|
|
8
|
+
import { actionDeliveryBackoffMs, actionDeliverySettlementForOutcome, deliverAction, } from '../../daemon/human/deliver-action.js';
|
|
9
|
+
let home;
|
|
10
|
+
const priorHome = process.env['CRTR_HOME'];
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
closeDb();
|
|
13
|
+
home = mkdtempSync(join(tmpdir(), 'crtr-human-action-delivery-'));
|
|
14
|
+
process.env['CRTR_HOME'] = home;
|
|
15
|
+
});
|
|
16
|
+
afterEach(() => {
|
|
17
|
+
closeDb();
|
|
18
|
+
rmSync(home, { recursive: true, force: true });
|
|
19
|
+
});
|
|
20
|
+
after(() => {
|
|
21
|
+
closeDb();
|
|
22
|
+
rmSync(home, { recursive: true, force: true });
|
|
23
|
+
if (priorHome === undefined)
|
|
24
|
+
delete process.env['CRTR_HOME'];
|
|
25
|
+
else
|
|
26
|
+
process.env['CRTR_HOME'] = priorHome;
|
|
27
|
+
});
|
|
28
|
+
test('action delivery retries follow the durable backoff and exit boundary', () => {
|
|
29
|
+
assert.equal(actionDeliveryBackoffMs(1), 5_000);
|
|
30
|
+
assert.equal(actionDeliveryBackoffMs(2), 10_000);
|
|
31
|
+
assert.equal(actionDeliveryBackoffMs(3), 20_000);
|
|
32
|
+
assert.equal(actionDeliveryBackoffMs(11), 3_600_000);
|
|
33
|
+
assert.equal(actionDeliveryBackoffMs(100), 3_600_000);
|
|
34
|
+
const now = 1_000_000;
|
|
35
|
+
const outcome = (exitCode, signal = null) => actionDeliverySettlementForOutcome({
|
|
36
|
+
exitCode,
|
|
37
|
+
signal,
|
|
38
|
+
timedOut: false,
|
|
39
|
+
stderr: 'diagnostic',
|
|
40
|
+
}, 2, now);
|
|
41
|
+
assert.deepEqual(outcome(0), { state: 'accepted' });
|
|
42
|
+
assert.deepEqual(outcome(77), {
|
|
43
|
+
state: 'pending',
|
|
44
|
+
nextAttemptAt: now + 10_000,
|
|
45
|
+
failure: { kind: 'exit', exitCode: 77, stderr: 'diagnostic' },
|
|
46
|
+
});
|
|
47
|
+
assert.deepEqual(outcome(78), {
|
|
48
|
+
state: 'permanent_failed',
|
|
49
|
+
failure: { kind: 'exit', exitCode: 78, stderr: 'diagnostic' },
|
|
50
|
+
});
|
|
51
|
+
assert.equal(outcome(79).state, 'pending');
|
|
52
|
+
assert.equal(outcome(null, 'SIGTERM').state, 'pending');
|
|
53
|
+
assert.equal(actionDeliverySettlementForOutcome({
|
|
54
|
+
exitCode: null,
|
|
55
|
+
signal: null,
|
|
56
|
+
timedOut: true,
|
|
57
|
+
stderr: '',
|
|
58
|
+
}, 1, now).state, 'pending');
|
|
59
|
+
// The kill escalation's signal is the only way to tell the graceful term from the SIGKILL.
|
|
60
|
+
assert.deepEqual(actionDeliverySettlementForOutcome({
|
|
61
|
+
exitCode: null,
|
|
62
|
+
signal: 'SIGKILL',
|
|
63
|
+
timedOut: true,
|
|
64
|
+
stderr: 'stalled',
|
|
65
|
+
}, 1, now), {
|
|
66
|
+
state: 'pending',
|
|
67
|
+
nextAttemptAt: now + 5_000,
|
|
68
|
+
failure: { kind: 'timeout', signal: 'SIGKILL', stderr: 'stalled' },
|
|
69
|
+
});
|
|
70
|
+
assert.equal(actionDeliverySettlementForOutcome({
|
|
71
|
+
exitCode: null,
|
|
72
|
+
signal: null,
|
|
73
|
+
timedOut: false,
|
|
74
|
+
spawnError: new Error('missing executable'),
|
|
75
|
+
stderr: '',
|
|
76
|
+
}, 1, now).state, 'pending');
|
|
77
|
+
});
|
|
78
|
+
test('the delivery child gets the consent boundary env, never crtrd\'s own', async () => {
|
|
79
|
+
const priorUserHome = process.env['HOME'];
|
|
80
|
+
const project = join(home, 'project');
|
|
81
|
+
mkdirSync(join(project, '.crouter'), { recursive: true });
|
|
82
|
+
writeFileSync(join(project, '.crouter', 'config.json'), JSON.stringify({ spawnEnv: { allow: ['NL_*'] } }));
|
|
83
|
+
const envDump = join(home, 'child-env.txt');
|
|
84
|
+
const stdinDump = join(home, 'child-stdin.json');
|
|
85
|
+
const script = join(home, 'action.sh');
|
|
86
|
+
writeFileSync(script, `#!/bin/sh\nenv > ${envDump}\ncat > ${stdinDump}\n`);
|
|
87
|
+
chmodSync(script, 0o755);
|
|
88
|
+
process.env['HOME'] = home; // no real user-scope config contributes to the boundary
|
|
89
|
+
process.env['NL_CORE_URL'] = 'http://core.test';
|
|
90
|
+
process.env['CRTRD_ONLY_SECRET'] = 'must-not-cross';
|
|
91
|
+
const requestId = 'b'.repeat(64);
|
|
92
|
+
try {
|
|
93
|
+
enqueueActionDelivery({
|
|
94
|
+
requestId,
|
|
95
|
+
argv: [script],
|
|
96
|
+
cwd: project,
|
|
97
|
+
completion: { schema: 'crtr.human-completion/v1', requestId, delivery: { attempt: 0 } },
|
|
98
|
+
});
|
|
99
|
+
const claimed = claimActionDelivery(requestId, Date.now(), 'claimant');
|
|
100
|
+
assert.ok(claimed !== null);
|
|
101
|
+
await deliverAction(claimed, 'claimant');
|
|
102
|
+
}
|
|
103
|
+
finally {
|
|
104
|
+
delete process.env['NL_CORE_URL'];
|
|
105
|
+
delete process.env['CRTRD_ONLY_SECRET'];
|
|
106
|
+
if (priorUserHome === undefined)
|
|
107
|
+
delete process.env['HOME'];
|
|
108
|
+
else
|
|
109
|
+
process.env['HOME'] = priorUserHome;
|
|
110
|
+
}
|
|
111
|
+
assert.equal(readActionDelivery(requestId)?.state, 'accepted');
|
|
112
|
+
const childEnv = new Map(readFileSync(envDump, 'utf8').split('\n').flatMap((line) => {
|
|
113
|
+
const split = line.indexOf('=');
|
|
114
|
+
return split === -1 ? [] : [[line.slice(0, split), line.slice(split + 1)]];
|
|
115
|
+
}));
|
|
116
|
+
assert.equal(childEnv.get('CRTRD_ONLY_SECRET'), undefined, 'crtrd host state must not cross without consent');
|
|
117
|
+
assert.equal(childEnv.get('NL_CORE_URL'), 'http://core.test', 'the target cwd\'s spawnEnv.allow carries its own config across');
|
|
118
|
+
assert.equal(childEnv.get('CRTR_HOME'), home, 'an action that calls crtr must reach the daemon that delivered to it');
|
|
119
|
+
assert.equal(JSON.parse(readFileSync(stdinDump, 'utf8')).delivery.attempt, 1);
|
|
120
|
+
});
|
|
121
|
+
test('the guarded claim admits only one contender', () => {
|
|
122
|
+
const requestId = 'a'.repeat(64);
|
|
123
|
+
enqueueActionDelivery({
|
|
124
|
+
requestId,
|
|
125
|
+
argv: ['/bin/true'],
|
|
126
|
+
cwd: home,
|
|
127
|
+
completion: { schema: 'crtr.human-completion/v1', delivery: { attempt: 0 } },
|
|
128
|
+
});
|
|
129
|
+
const now = Date.now();
|
|
130
|
+
const first = claimActionDelivery(requestId, now, 'claimant-a');
|
|
131
|
+
const second = claimActionDelivery(requestId, now, 'claimant-b');
|
|
132
|
+
assert.equal(first?.state, 'running');
|
|
133
|
+
assert.equal(first?.attempt, 1);
|
|
134
|
+
assert.equal(second, null);
|
|
135
|
+
assert.deepEqual(readActionDelivery(requestId) && {
|
|
136
|
+
state: readActionDelivery(requestId).state,
|
|
137
|
+
attempt: readActionDelivery(requestId).attempt,
|
|
138
|
+
claimOwner: readActionDelivery(requestId).claimOwner,
|
|
139
|
+
}, { state: 'running', attempt: 1, claimOwner: 'claimant-a' });
|
|
140
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
// Run with: node --conditions=crtr-src --import tsx/esm --test src/core/__tests__/human-actions.test.ts
|
|
2
|
+
//
|
|
3
|
+
// This pins the silent execution boundary: a request must select the nearest
|
|
4
|
+
// project declaration before farther projects or user scope, and its relative
|
|
5
|
+
// executable/cwd must resolve from that declaration's authoring root.
|
|
6
|
+
import { after, afterEach, before, beforeEach, test } from 'node:test';
|
|
7
|
+
import assert from 'node:assert/strict';
|
|
8
|
+
import { chmodSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
|
|
9
|
+
import { tmpdir } from 'node:os';
|
|
10
|
+
import { join } from 'node:path';
|
|
11
|
+
import { resolveHumanAction } from '../human-actions.js';
|
|
12
|
+
import { createProfile } from '../profiles/manifest.js';
|
|
13
|
+
import { resetScopeCache } from '../scope.js';
|
|
14
|
+
let home;
|
|
15
|
+
let cwd;
|
|
16
|
+
let previousHome;
|
|
17
|
+
let previousProfile;
|
|
18
|
+
let previousCwd;
|
|
19
|
+
before(() => {
|
|
20
|
+
previousHome = process.env['HOME'];
|
|
21
|
+
previousProfile = process.env['CRTR_PROFILE_ID'];
|
|
22
|
+
previousCwd = process.cwd();
|
|
23
|
+
});
|
|
24
|
+
beforeEach(() => {
|
|
25
|
+
home = mkdtempSync(join(tmpdir(), 'crtr-human-actions-home-'));
|
|
26
|
+
cwd = join(home, 'work', 'outer', 'nested', 'src');
|
|
27
|
+
mkdirSync(cwd, { recursive: true });
|
|
28
|
+
process.env['HOME'] = home;
|
|
29
|
+
delete process.env['CRTR_PROFILE_ID'];
|
|
30
|
+
process.chdir(cwd);
|
|
31
|
+
resetScopeCache();
|
|
32
|
+
});
|
|
33
|
+
afterEach(() => {
|
|
34
|
+
process.chdir(previousCwd);
|
|
35
|
+
rmSync(home, { recursive: true, force: true });
|
|
36
|
+
resetScopeCache();
|
|
37
|
+
});
|
|
38
|
+
after(() => {
|
|
39
|
+
process.chdir(previousCwd);
|
|
40
|
+
if (previousHome === undefined)
|
|
41
|
+
delete process.env['HOME'];
|
|
42
|
+
else
|
|
43
|
+
process.env['HOME'] = previousHome;
|
|
44
|
+
if (previousProfile === undefined)
|
|
45
|
+
delete process.env['CRTR_PROFILE_ID'];
|
|
46
|
+
else
|
|
47
|
+
process.env['CRTR_PROFILE_ID'] = previousProfile;
|
|
48
|
+
resetScopeCache();
|
|
49
|
+
});
|
|
50
|
+
function writeAction(root, relativeExecutable, relativeCwd) {
|
|
51
|
+
const executable = join(root, relativeExecutable);
|
|
52
|
+
mkdirSync(join(executable, '..'), { recursive: true });
|
|
53
|
+
writeFileSync(executable, '');
|
|
54
|
+
chmodSync(executable, 0o755);
|
|
55
|
+
mkdirSync(join(root, relativeCwd), { recursive: true });
|
|
56
|
+
}
|
|
57
|
+
test('nearest project declaration wins and resolves paths from its authoring root', () => {
|
|
58
|
+
const userRoot = join(home, '.crouter');
|
|
59
|
+
writeAction(userRoot, 'user-bin', 'user-cwd');
|
|
60
|
+
mkdirSync(userRoot, { recursive: true });
|
|
61
|
+
writeFileSync(join(userRoot, 'config.json'), JSON.stringify({
|
|
62
|
+
humanActions: { release: { argv: ['user-bin'], cwd: 'user-cwd' }, 'user-only': { argv: ['user-bin'], cwd: 'user-cwd' } },
|
|
63
|
+
}));
|
|
64
|
+
const outer = join(home, 'work', 'outer');
|
|
65
|
+
writeAction(outer, 'outer-bin', 'outer-cwd');
|
|
66
|
+
mkdirSync(join(outer, '.crouter'), { recursive: true });
|
|
67
|
+
writeFileSync(join(outer, '.crouter', 'config.json'), JSON.stringify({
|
|
68
|
+
humanActions: { release: { argv: ['outer-bin'], cwd: 'outer-cwd' } },
|
|
69
|
+
}));
|
|
70
|
+
const nearest = join(outer, 'nested');
|
|
71
|
+
writeAction(nearest, 'scripts/finish-release', 'work');
|
|
72
|
+
mkdirSync(join(nearest, '.crouter'), { recursive: true });
|
|
73
|
+
writeFileSync(join(nearest, '.crouter', 'config.json'), JSON.stringify({
|
|
74
|
+
humanActions: { release: { argv: ['scripts/finish-release', '--nearest'], cwd: 'work' } },
|
|
75
|
+
}));
|
|
76
|
+
resetScopeCache();
|
|
77
|
+
assert.deepEqual(resolveHumanAction('release', cwd), {
|
|
78
|
+
name: 'release',
|
|
79
|
+
configOrigin: join(nearest, '.crouter', 'config.json'),
|
|
80
|
+
argv: [join(nearest, 'scripts/finish-release'), '--nearest'],
|
|
81
|
+
cwd: join(nearest, 'work'),
|
|
82
|
+
});
|
|
83
|
+
assert.deepEqual(resolveHumanAction('user-only', cwd), {
|
|
84
|
+
name: 'user-only',
|
|
85
|
+
configOrigin: join(userRoot, 'config.json'),
|
|
86
|
+
argv: [join(userRoot, 'user-bin')],
|
|
87
|
+
cwd: join(userRoot, 'user-cwd'),
|
|
88
|
+
});
|
|
89
|
+
const profileProject = join(home, 'profile-project');
|
|
90
|
+
writeAction(profileProject, 'profile-bin', 'profile-cwd');
|
|
91
|
+
mkdirSync(join(profileProject, '.crouter'), { recursive: true });
|
|
92
|
+
writeFileSync(join(profileProject, '.crouter', 'config.json'), JSON.stringify({
|
|
93
|
+
humanActions: { 'profile-only': { argv: ['profile-bin'], cwd: 'profile-cwd' } },
|
|
94
|
+
}));
|
|
95
|
+
process.env['CRTR_PROFILE_ID'] = createProfile('human-actions-test', [{ path: profileProject, memory: 'content' }]).profileId;
|
|
96
|
+
resetScopeCache();
|
|
97
|
+
assert.deepEqual(resolveHumanAction('profile-only', cwd), { kind: 'unknown_action', name: 'profile-only' });
|
|
98
|
+
});
|
|
99
|
+
test('a malformed nearer humanActions block does not shadow a farther declaration', () => {
|
|
100
|
+
const outer = join(home, 'work', 'outer');
|
|
101
|
+
writeAction(outer, 'outer-bin', 'outer-cwd');
|
|
102
|
+
mkdirSync(join(outer, '.crouter'), { recursive: true });
|
|
103
|
+
writeFileSync(join(outer, '.crouter', 'config.json'), JSON.stringify({
|
|
104
|
+
humanActions: { release: { argv: ['outer-bin'], cwd: 'outer-cwd' } },
|
|
105
|
+
}));
|
|
106
|
+
const nearest = join(outer, 'nested');
|
|
107
|
+
mkdirSync(join(nearest, '.crouter'), { recursive: true });
|
|
108
|
+
writeFileSync(join(nearest, '.crouter', 'config.json'), JSON.stringify({ humanActions: [] }));
|
|
109
|
+
resetScopeCache();
|
|
110
|
+
assert.deepEqual(resolveHumanAction('release', cwd), {
|
|
111
|
+
name: 'release',
|
|
112
|
+
configOrigin: join(outer, '.crouter', 'config.json'),
|
|
113
|
+
argv: [join(outer, 'outer-bin')],
|
|
114
|
+
cwd: join(outer, 'outer-cwd'),
|
|
115
|
+
});
|
|
116
|
+
});
|
|
@@ -14,7 +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/
|
|
17
|
+
import { gatewayMemoryRows } from '../runtime/chat-inventory-rows.js';
|
|
18
18
|
// ---------------------------------------------------------------------------
|
|
19
19
|
// Wave C4 — buildRefInventory: the authoritative corpus builder. Fixture-
|
|
20
20
|
// backed (node/project/user scope docs on disk), since precedence dedup,
|
|
@@ -21,6 +21,7 @@ import { closeDb } from '../canvas/db.js';
|
|
|
21
21
|
import { resetScopeCache } from '../scope.js';
|
|
22
22
|
import { createProfile } from '../profiles/manifest.js';
|
|
23
23
|
import { spawnNode } from '../runtime/nodes.js';
|
|
24
|
+
import { memoryDir } from '../runtime/memory.js';
|
|
24
25
|
import { clearSessionCache } from '../substrate/session-cache.js';
|
|
25
26
|
import { renderKnowledgeBlock } from '../substrate/render-node.js';
|
|
26
27
|
import { renderWorkspaceOpenDocs } from '../substrate/on-read-node.js';
|
|
@@ -37,6 +38,8 @@ const B = {
|
|
|
37
38
|
nameFromPcontent: 'BODY-T9-NAMEDUP-FROM-PCONTENT',
|
|
38
39
|
userDupFromPnone: 'BODY-T9-USERDUP-FROM-PNONE',
|
|
39
40
|
userDupFromUser: 'BODY-T9-USERDUP-FROM-USER',
|
|
41
|
+
kindSplit: 'BODY-T9-KIND-SPLIT',
|
|
42
|
+
documentGateWins: 'BODY-T9-DOCUMENT-GATE-WINS',
|
|
40
43
|
};
|
|
41
44
|
let home;
|
|
42
45
|
let canvasHome;
|
|
@@ -115,6 +118,30 @@ before(() => {
|
|
|
115
118
|
// would otherwise beat.
|
|
116
119
|
writeDoc(pnone, 't9-userdup-doc', autoDoc('When the user duplicate resolves', B.userDupFromPnone));
|
|
117
120
|
writeDoc(user, 't9-userdup-doc', autoDoc('When the user duplicate resolves', B.userDupFromUser));
|
|
121
|
+
writeDoc(user, 't9-kind-split', [
|
|
122
|
+
'---',
|
|
123
|
+
'kind: knowledge',
|
|
124
|
+
'when-and-why-to-read: "When the node needs this kind-specific guide"',
|
|
125
|
+
'surfaces:',
|
|
126
|
+
' - {on: boot, gate: {kind: {in: [design, spec, plan, developer]}}, at: content}',
|
|
127
|
+
' - {on: boot, gate: {kind: {in: [general, review, advisor]}}, at: preview}',
|
|
128
|
+
'---',
|
|
129
|
+
'',
|
|
130
|
+
B.kindSplit,
|
|
131
|
+
'',
|
|
132
|
+
].join('\n'));
|
|
133
|
+
writeDoc(user, 't9-document-gate-wins', [
|
|
134
|
+
'---',
|
|
135
|
+
'kind: knowledge',
|
|
136
|
+
'when-and-why-to-read: "When document eligibility is tested"',
|
|
137
|
+
'gate: {kind: {ne: developer}}',
|
|
138
|
+
'surfaces:',
|
|
139
|
+
' - {on: boot, gate: {kind: developer}, at: content}',
|
|
140
|
+
'---',
|
|
141
|
+
'',
|
|
142
|
+
B.documentGateWins,
|
|
143
|
+
'',
|
|
144
|
+
].join('\n'));
|
|
118
145
|
// Hidden-count pair: a top-level dir always renders, so a dir holding only a
|
|
119
146
|
// bootless doc shows as `<dir>/` + `[+1 more]` — unless its project is `none`.
|
|
120
147
|
writeDoc(pnone, join('t9-none-hidden', 'probe'), bootlessDoc('When counted', 'HIDDEN-NONE'));
|
|
@@ -152,8 +179,8 @@ after(() => {
|
|
|
152
179
|
restore('CRTR_PROFILE_ID', prevProfile);
|
|
153
180
|
restore('CRTR_NODE_ID', prevNode);
|
|
154
181
|
});
|
|
155
|
-
function newNode() {
|
|
156
|
-
return spawnNode({ kind
|
|
182
|
+
function newNode(kind = 'general') {
|
|
183
|
+
return spawnNode({ kind, cwd: cwdProject, parent: null, profile_id: profileId }).node_id;
|
|
157
184
|
}
|
|
158
185
|
test('boot delivery caps every profile project at its relationship, including the one used as cwd', () => {
|
|
159
186
|
const boot = renderKnowledgeBlock(newNode(), new Map());
|
|
@@ -168,6 +195,46 @@ test('boot delivery caps every profile project at its relationship, including th
|
|
|
168
195
|
assert.match(boot, /[├└]─ t9-pname-doc$/m, `the name-capped doc discloses as a bare name; got:\n${boot}`);
|
|
169
196
|
assert.ok(!boot.includes('When the name project delivers'), 'a name-capped doc discloses no preview line');
|
|
170
197
|
});
|
|
198
|
+
test('surface-entry gates select the rung by node kind while the document gate remains absolute', () => {
|
|
199
|
+
for (const kind of ['design', 'spec', 'plan', 'developer']) {
|
|
200
|
+
const boot = renderKnowledgeBlock(newNode(kind), new Map());
|
|
201
|
+
assert.ok(boot.includes(B.kindSplit), `${kind} receives the content rung; got:\n${boot}`);
|
|
202
|
+
}
|
|
203
|
+
for (const kind of ['general', 'review', 'advisor']) {
|
|
204
|
+
const boot = renderKnowledgeBlock(newNode(kind), new Map());
|
|
205
|
+
assert.match(boot, /t9-kind-split {2}# read when: When the node needs this kind-specific guide\./, `${kind} receives the preview rung; got:\n${boot}`);
|
|
206
|
+
assert.ok(!boot.includes(B.kindSplit), `${kind} does not receive the body`);
|
|
207
|
+
}
|
|
208
|
+
for (const kind of ['explore', 'personal-assistant']) {
|
|
209
|
+
const boot = renderKnowledgeBlock(newNode(kind), new Map());
|
|
210
|
+
assert.ok(!boot.includes('t9-kind-split'), `${kind} receives no kind-split entry; got:\n${boot}`);
|
|
211
|
+
assert.ok(!boot.includes(B.kindSplit), `${kind} receives no kind-split body`);
|
|
212
|
+
}
|
|
213
|
+
const developer = renderKnowledgeBlock(newNode('developer'), new Map());
|
|
214
|
+
assert.ok(!developer.includes('t9-document-gate-wins'), `a failing document gate excludes every entry; got:\n${developer}`);
|
|
215
|
+
assert.ok(!developer.includes(B.documentGateWins), 'a failing document gate excludes its content');
|
|
216
|
+
});
|
|
217
|
+
test('a node-local doc floors to name only without boot entries', () => {
|
|
218
|
+
const node = newNode('explore');
|
|
219
|
+
writeDoc(memoryDir(node), 't9-node-local-bootless', bootlessDoc('When the node-local fallback applies', 'BODY-T9-NODE-LOCAL-BOOTLESS'));
|
|
220
|
+
writeDoc(memoryDir(node), 't9-node-local-entry-gated', [
|
|
221
|
+
'---',
|
|
222
|
+
'kind: knowledge',
|
|
223
|
+
'when-and-why-to-read: "When the node-local entry gates are tested"',
|
|
224
|
+
'surfaces:',
|
|
225
|
+
' - {on: boot, gate: {kind: developer}, at: content}',
|
|
226
|
+
' - {on: boot, gate: {kind: review}, at: preview}',
|
|
227
|
+
'---',
|
|
228
|
+
'',
|
|
229
|
+
'BODY-T9-NODE-LOCAL-ENTRY-GATED',
|
|
230
|
+
'',
|
|
231
|
+
].join('\n'));
|
|
232
|
+
const boot = renderKnowledgeBlock(node, new Map());
|
|
233
|
+
assert.match(boot, /[├└]─ t9-node-local-bootless$/m, `a bootless node-local doc falls to name; got:\n${boot}`);
|
|
234
|
+
assert.ok(!boot.includes('BODY-T9-NODE-LOCAL-BOOTLESS'), 'the fallback discloses no body');
|
|
235
|
+
assert.ok(!boot.includes('t9-node-local-entry-gated'), `ineligible boot entries keep a node-local doc absent; got:\n${boot}`);
|
|
236
|
+
assert.ok(!boot.includes('BODY-T9-NODE-LOCAL-ENTRY-GATED'), 'ineligible boot entries disclose no body');
|
|
237
|
+
});
|
|
171
238
|
test('a `none` project contributes nothing to boot — no body, no name, no hidden count, no shadow', () => {
|
|
172
239
|
const boot = renderKnowledgeBlock(newNode(), new Map());
|
|
173
240
|
assert.ok(!boot.includes(B.pnone), 'a none-capped body must not render');
|
|
@@ -191,7 +258,7 @@ test('workspace-open clamps each project store the same way boot does', () => {
|
|
|
191
258
|
const opened = renderWorkspaceOpenDocs(newNode(), new Map());
|
|
192
259
|
assert.ok(opened.includes(B.pcontent), `the content-capped project delivers its body; got:\n${opened}`);
|
|
193
260
|
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"
|
|
261
|
+
assert.match(opened, /name="t9-ppreview-doc" readWhen="When the cwd project delivers\./, `the cwd project delivers its preview line; got:\n${opened}`);
|
|
195
262
|
assert.match(opened, /name="t9-pname-doc"[^>]*\/>/, `the name-capped doc delivers name-only; got:\n${opened}`);
|
|
196
263
|
assert.ok(!opened.includes('t9-pnone-doc'), 'the none-capped project delivers nothing at workspace-open');
|
|
197
264
|
assert.ok(!opened.includes(B.sharedFromPnone), 'nor its side of a name collision');
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|