@north-light/crouter 0.3.220 → 0.3.222
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 +29 -0
- package/dist/api/client.js +44 -0
- package/dist/api/dto/chat-inventory.d.ts +56 -0
- package/dist/api/dto/chat-inventory.js +11 -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/profiles.d.ts +19 -5
- package/dist/api/dto/profiles.js +2 -1
- package/dist/api/dto/reviews.d.ts +2 -0
- package/dist/api/index.d.ts +2 -0
- package/dist/api/index.js +2 -0
- package/dist/api/routes.d.ts +8 -0
- package/dist/api/routes.js +11 -0
- package/dist/build-root.d.ts +2 -6
- package/dist/build-root.js +51 -4
- 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/agent-shaping.md +3 -1
- package/dist/builtin-memory/internal/memory-loading.md +4 -0
- 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 -22
- package/dist/builtin-memory/spec/guide.md +23 -6
- 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 +34 -16
- 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/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/session/keys.d.ts +1 -1
- package/dist/clients/attach/session/profile-files.js +1 -1
- package/dist/clients/attach/viewer.js +698 -696
- package/dist/clients/inbox/controller.js +1 -1
- package/dist/clients/inbox/resolve.d.ts +1 -0
- 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 +7 -1
- package/dist/clients/inbox/review-adapter.d.ts +1 -8
- package/dist/clients/inbox/review-adapter.js +4 -52
- 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/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/config.js +2 -2
- package/dist/commands/sys/doctor.js +87 -5
- package/dist/commands/sys/migrate.js +38 -19
- package/dist/commands/sys/setup-core.js +1 -1
- package/dist/commands/sys/sync-project-guidance.js +1 -1
- package/dist/core/__tests__/broker-extension-canvas-db-boundary.test.js +7 -4
- 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__/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 +36 -2
- package/dist/core/__tests__/profile-project-memory-delivery.test.d.ts +1 -0
- package/dist/core/__tests__/profile-project-memory-delivery.test.js +217 -0
- 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/__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/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/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/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/help.d.ts +7 -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/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 +26 -20
- package/dist/core/profiles/manifest.d.ts +13 -2
- package/dist/core/profiles/manifest.js +84 -18
- package/dist/core/profiles/select.d.ts +2 -0
- package/dist/core/profiles/select.js +29 -12
- 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/event-projection.js +7 -0
- package/dist/core/runtime/broker/frame-dispatch.d.ts +1 -1
- package/dist/core/runtime/broker/frame-dispatch.js +13 -11
- 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 +4 -0
- package/dist/core/runtime/broker-inventory.js +116 -0
- package/dist/core/runtime/broker-persona-guidance.js +1 -1
- package/dist/core/runtime/broker-protocol.d.ts +9 -2
- package/dist/core/runtime/broker.js +10 -1
- 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 +38 -0
- package/dist/core/runtime/command-surface.js +117 -0
- package/dist/core/runtime/launch-target.d.ts +25 -0
- package/dist/core/runtime/launch-target.js +54 -0
- package/dist/core/runtime/node-read.js +5 -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/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 +30 -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 +65 -24
- package/dist/core/substrate/schema.d.ts +16 -2
- package/dist/core/substrate/schema.js +14 -5
- 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 +56 -7
- 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/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/profiles.js +7 -1
- 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/map.d.ts +2 -1
- package/dist/daemon/api/map.js +3 -2
- package/dist/daemon/api/server.js +6 -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/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 +23 -6
- 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/clients/attach/__tests__/file-review-focus.test.js +0 -49
- /package/dist/builtin-memory/{02-lifecycle/00-terminal.md → 02-turn-lifecycle/01-terminal.md} +0 -0
- /package/dist/{clients/attach/__tests__/file-review-focus.test.d.ts → core/__tests__/fixtures/memory-slash-live-probe.d.ts} +0 -0
|
@@ -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,
|
|
@@ -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) {
|
|
@@ -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
|
});
|
|
@@ -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();
|
|
@@ -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
|
+
});
|