@north-light/crouter 0.3.197 → 0.3.199
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 +19 -19
- package/dist/api/client.js +29 -37
- package/dist/api/dto/broker-ops.d.ts +3 -1
- package/dist/api/dto/broker.d.ts +13 -2
- package/dist/api/dto/broker.js +4 -4
- package/dist/api/dto/human.d.ts +6 -5
- package/dist/api/dto/inbox.d.ts +126 -33
- package/dist/api/dto/nodes.d.ts +8 -2
- package/dist/api/dto/profiles.d.ts +23 -0
- package/dist/api/dto/profiles.js +2 -2
- package/dist/api/dto/worktree.d.ts +3 -5
- package/dist/api/routes.d.ts +3 -5
- package/dist/api/routes.js +4 -6
- package/dist/builtin-memory/00-runtime-base.md +8 -8
- package/dist/builtin-memory/01-spine/01-no-manager.md +1 -1
- package/dist/builtin-memory/02-lifecycle/00-terminal.md +3 -3
- package/dist/builtin-memory/04-orchestration-kernel.md +6 -6
- package/dist/builtin-memory/05-kinds/design/00-base.md +1 -1
- package/dist/builtin-memory/05-kinds/plan/reviewers/security.md +2 -2
- package/dist/builtin-memory/05-kinds/review/00-base.md +2 -2
- package/dist/builtin-memory/05-kinds/review/01-orchestrator.md +1 -1
- package/dist/builtin-memory/05-kinds/spec/01-orchestrator.md +1 -1
- package/dist/builtin-memory/insights/capture.md +2 -2
- package/dist/builtin-memory/insights/init.md +3 -3
- package/dist/builtin-memory/internal/INDEX.md +1 -1
- package/dist/builtin-memory/internal/memory-loading.md +1 -1
- package/dist/builtin-memory/internal/nodes-and-canvas.md +3 -3
- package/dist/builtin-memory/internal/plugins.md +59 -2
- package/dist/builtin-memory/internal/storage-tiers.md +3 -1
- package/dist/builtin-memory/spec/roadmap.md +2 -2
- package/dist/builtin-memory/testing.md +1 -1
- package/dist/builtin-memory/wedged-child-on-runaway-bash.md +1 -1
- package/dist/clients/attach/chrome/bash-jobs.js +3 -3
- package/dist/clients/attach/render/chat-view.d.ts +29 -9
- package/dist/clients/attach/render/chat-view.js +64 -35
- package/dist/clients/attach/render/markdown-source.js +0 -6
- package/dist/clients/attach/render/page-block.d.ts +34 -0
- package/dist/clients/attach/render/page-block.js +226 -0
- package/dist/clients/attach/render/tool-calls.js +16 -2
- package/dist/clients/attach/session/whip.d.ts +2 -0
- package/dist/clients/attach/session/whip.js +2 -0
- package/dist/clients/attach/viewer.js +1419 -982
- package/dist/clients/conversation/projection.js +18 -3
- package/dist/clients/inbox/__tests__/serial/inbox-controller.test.js +9 -6
- package/dist/clients/inbox/__tests__/serial/mount-panel.test.js +13 -14
- package/dist/clients/inbox/controller.js +10 -16
- package/dist/clients/inbox/page-adapter.d.ts +3 -4
- package/dist/clients/inbox/page-adapter.js +3 -4
- package/dist/clients/inbox/resolve.d.ts +1 -1
- package/dist/clients/inbox/resolve.js +3 -3
- package/dist/clients/inbox/review/document-surface.js +3 -1
- package/dist/clients/inbox/review/state.d.ts +2 -1
- package/dist/clients/inbox/review/state.js +5 -16
- package/dist/clients/inbox/tui/input.js +37 -19
- package/dist/clients/inbox/tui/panel.js +5 -31
- package/dist/clients/inbox/tui/render.d.ts +11 -0
- package/dist/clients/inbox/tui/render.js +101 -36
- package/dist/clients/inbox/tui/slots.d.ts +4 -4
- package/dist/clients/inbox/tui/slots.js +19 -44
- package/dist/clients/inbox/tui/types.d.ts +6 -10
- package/dist/clients/inbox/tui.js +2 -2
- package/dist/commands/__tests__/human.test.js +118 -24
- package/dist/commands/__tests__/node-message.test.js +14 -0
- package/dist/commands/attention.js +4 -4
- package/dist/commands/canvas-browse.js +1 -1
- package/dist/commands/canvas-snapshot.js +1 -1
- package/dist/commands/canvas.js +1 -1
- package/dist/commands/dashboard.js +1 -1
- package/dist/commands/human/{doc.d.ts → components.d.ts} +1 -1
- package/dist/commands/human/components.js +77 -0
- package/dist/commands/human/feedback.d.ts +2 -0
- package/dist/commands/human/feedback.js +108 -0
- package/dist/commands/human/prompts.d.ts +17 -8
- package/dist/commands/human/prompts.js +217 -226
- package/dist/commands/human/queue.js +57 -88
- package/dist/commands/human/shared.d.ts +4 -15
- package/dist/commands/human/shared.js +19 -61
- package/dist/commands/human.js +32 -19
- package/dist/commands/memory/origin.js +2 -2
- package/dist/commands/node/bash.js +4 -4
- package/dist/commands/node/inspect.js +3 -3
- package/dist/commands/node/lifecycle.js +1 -1
- package/dist/commands/node/message.js +2 -2
- package/dist/commands/node-worktree.js +8 -8
- package/dist/commands/pkg/browse.js +1 -1
- package/dist/commands/pkg/plugin-manage.js +129 -10
- package/dist/commands/profile/delete.js +41 -15
- package/dist/commands/profile.js +1 -1
- package/dist/commands/push.js +1 -1
- package/dist/commands/surface/node/focus.js +29 -14
- package/dist/commands/surface-edit.js +3 -3
- package/dist/commands/surface-inbox.d.ts +2 -0
- package/dist/commands/{human/inbox.js → surface-inbox.js} +14 -18
- package/dist/commands/surface.js +2 -1
- package/dist/commands/sys/doctor.js +60 -3
- package/dist/commands/sys/feedback.js +79 -25
- package/dist/commands/sys/settings.js +1 -1
- package/dist/core/__tests__/cron-broker-capacity.test.js +1 -1
- package/dist/core/__tests__/daemon-boot.test.js +7 -4
- package/dist/core/__tests__/dead-node-policy-table.test.js +20 -36
- package/dist/core/__tests__/extension-abort.test.js +5 -2
- package/dist/core/__tests__/fixtures/fake-engine.d.ts +13 -2
- package/dist/core/__tests__/fixtures/fake-engine.js +47 -3
- package/dist/core/__tests__/human-cancel-guard.test.js +31 -10
- package/dist/core/__tests__/human-deliver.test.js +41 -45
- package/dist/core/__tests__/human-node-not-supervised.test.js +1 -1
- package/dist/core/__tests__/plugin-page-components.test.js +157 -0
- package/dist/core/__tests__/seam/broker-crash-teardown.test.js +13 -11
- package/dist/core/__tests__/seam/dormancy-release.test.js +103 -4
- package/dist/core/__tests__/serial/flagship-lifecycle.test.js +2 -2
- package/dist/core/__tests__/serial/human-deliver-e2e.test.js +12 -16
- package/dist/core/__tests__/serial/live-mutation.test.js +1 -1
- package/dist/core/__tests__/serial/tmux-surface.test.js +4 -2
- package/dist/core/__tests__/serial/worktree.test.js +24 -198
- package/dist/core/__tests__/session-cycles.test.js +22 -0
- package/dist/core/__tests__/stop-guard.test.js +4 -4
- package/dist/core/__tests__/watchdog-abort-arms-retry.test.js +19 -4
- package/dist/core/bash-jobs.d.ts +6 -0
- package/dist/core/bash-jobs.js +10 -0
- package/dist/core/canvas/__tests__/attention.test.js +8 -5
- package/dist/core/canvas/__tests__/render-remote.test.js +5 -5
- package/dist/core/canvas/attention.js +8 -6
- package/dist/core/canvas/browse/app.d.ts +1 -1
- package/dist/core/canvas/browse/app.js +21 -3
- package/dist/core/canvas/browse/model.d.ts +8 -2
- package/dist/core/canvas/browse/model.js +11 -4
- package/dist/core/canvas/browse/render.d.ts +4 -0
- package/dist/core/canvas/browse/render.js +5 -1
- package/dist/core/canvas/canvas.d.ts +33 -0
- package/dist/core/canvas/canvas.js +92 -0
- package/dist/core/canvas/crons.d.ts +9 -0
- package/dist/core/canvas/crons.js +51 -0
- package/dist/core/canvas/node-order.d.ts +22 -1
- package/dist/core/canvas/node-order.js +26 -1
- package/dist/core/canvas/render-source.d.ts +5 -0
- package/dist/core/canvas/render-source.js +1 -0
- package/dist/core/canvas/types.d.ts +9 -3
- package/dist/core/command-plugins/bundle.d.ts +6 -1
- package/dist/core/command-plugins/bundle.js +26 -13
- package/dist/core/command.js +8 -12
- package/dist/core/config.d.ts +47 -1
- package/dist/core/config.js +138 -4
- package/dist/core/help.d.ts +3 -0
- package/dist/core/help.js +1 -1
- package/dist/core/human/__tests__/page-catalog.test.d.ts +1 -0
- package/dist/core/human/__tests__/page-catalog.test.js +20 -0
- package/dist/core/human/__tests__/page-tickets.test.js +68 -34
- package/dist/core/human/__tests__/page.test.js +60 -119
- package/dist/core/human/__tests__/serial/inbox-core.test.js +6 -16
- package/dist/core/human/answer-text.d.ts +3 -0
- package/dist/core/human/answer-text.js +98 -0
- package/dist/core/human/answer.d.ts +53 -0
- package/dist/core/human/answer.js +119 -0
- package/dist/core/human/component-docs.d.ts +18 -0
- package/dist/core/human/component-docs.js +434 -0
- package/dist/core/human/convention.d.ts +17 -3
- package/dist/core/human/convention.js +36 -5
- package/dist/core/human/feedback-companion.d.ts +20 -0
- package/dist/core/human/feedback-companion.js +98 -0
- package/dist/core/human/feedback.d.ts +65 -0
- package/dist/core/human/feedback.js +116 -0
- package/dist/core/human/page-catalog.d.ts +56 -5
- package/dist/core/human/page-catalog.js +151 -22
- package/dist/core/human/page-errors.d.ts +4 -0
- package/dist/core/human/page-errors.js +7 -0
- package/dist/core/human/page-eval.d.ts +20 -0
- package/dist/core/human/page-eval.js +123 -0
- package/dist/core/human/page-schema.d.ts +48 -116
- package/dist/core/human/page-schema.js +100 -72
- package/dist/core/human/page-synth.d.ts +2 -4
- package/dist/core/human/page-synth.js +20 -7
- package/dist/core/human/page.d.ts +13 -21
- package/dist/core/human/page.js +147 -391
- package/dist/core/human/review-schema.d.ts +1 -0
- package/dist/core/human/review-schema.js +1 -1
- package/dist/core/human/root.d.ts +6 -2
- package/dist/core/human/root.js +12 -4
- package/dist/core/human/scan.d.ts +3 -3
- package/dist/core/human/scan.js +21 -21
- package/dist/core/human/summary.d.ts +2 -6
- package/dist/core/human/summary.js +3 -59
- package/dist/core/human/tickets.d.ts +22 -15
- package/dist/core/human/tickets.js +90 -28
- package/dist/core/human/types.d.ts +14 -2
- package/dist/core/keybindings/catalog.d.ts +2 -2
- package/dist/core/keybindings/catalog.js +1 -0
- package/dist/core/preview-registry.js +8 -10
- package/dist/core/profiles/default-binding.d.ts +4 -0
- package/dist/core/profiles/default-binding.js +24 -0
- package/dist/core/profiles/deletion-reservation.d.ts +8 -0
- package/dist/core/profiles/deletion-reservation.js +38 -0
- package/dist/core/profiles/manifest.d.ts +3 -0
- package/dist/core/profiles/manifest.js +12 -0
- package/dist/core/profiles/select.js +2 -2
- package/dist/core/review/realize.js +12 -4
- package/dist/core/review/store.d.ts +6 -0
- package/dist/core/review/store.js +31 -0
- package/dist/core/runtime/bearings.d.ts +0 -4
- package/dist/core/runtime/bearings.js +4 -62
- package/dist/core/runtime/bin-contributions.d.ts +44 -0
- package/dist/core/runtime/bin-contributions.js +186 -0
- package/dist/core/runtime/boot-root.js +6 -1
- package/dist/core/runtime/broker/client-registry.d.ts +4 -0
- package/dist/core/runtime/broker/client-registry.js +13 -1
- package/dist/core/runtime/broker/extension-abort.d.ts +1 -1
- package/dist/core/runtime/broker/extension-abort.js +4 -2
- package/dist/core/runtime/broker/fault-retry.js +4 -0
- package/dist/core/runtime/broker/frame-dispatch.d.ts +3 -1
- package/dist/core/runtime/broker/frame-dispatch.js +5 -6
- package/dist/core/runtime/broker/read-ops.d.ts +1 -1
- package/dist/core/runtime/broker/read-ops.js +2 -2
- package/dist/core/runtime/broker/rebind.d.ts +1 -0
- package/dist/core/runtime/broker/rebind.js +6 -1
- package/dist/core/runtime/broker-extension-render.js +19 -0
- package/dist/core/runtime/broker-protocol.d.ts +14 -0
- package/dist/core/runtime/broker.d.ts +1 -1
- package/dist/core/runtime/broker.js +25 -12
- package/dist/core/runtime/busy.d.ts +4 -3
- package/dist/core/runtime/busy.js +4 -3
- package/dist/core/runtime/front-door-env.d.ts +5 -0
- package/dist/core/runtime/front-door-env.js +16 -0
- package/dist/core/runtime/front-door.d.ts +1 -5
- package/dist/core/runtime/front-door.js +2 -5
- package/dist/core/runtime/host.d.ts +1 -1
- package/dist/core/runtime/invocation.d.ts +6 -0
- package/dist/core/runtime/invocation.js +10 -0
- package/dist/core/runtime/kickoff.js +34 -0
- package/dist/core/runtime/launch.d.ts +2 -6
- package/dist/core/runtime/node-read.js +4 -1
- package/dist/core/runtime/nodes.js +2 -0
- package/dist/core/runtime/promote.d.ts +1 -0
- package/dist/core/runtime/promote.js +3 -1
- package/dist/core/runtime/session-cycles.d.ts +6 -0
- package/dist/core/runtime/session-cycles.js +15 -1
- package/dist/core/runtime/session-visibility.d.ts +17 -8
- package/dist/core/runtime/session-visibility.js +19 -10
- package/dist/core/runtime/spawn-env.d.ts +9 -1
- package/dist/core/runtime/spawn-env.js +62 -2
- package/dist/core/runtime/spawn.js +4 -4
- package/dist/core/runtime/stop-guard.d.ts +2 -2
- package/dist/core/runtime/stop-guard.js +2 -2
- package/dist/core/runtime/stop-signals.d.ts +1 -1
- package/dist/core/runtime/stop-signals.js +2 -2
- package/dist/core/runtime/tmux-bindings.js +1 -1
- package/dist/core/session-model/session-state.d.ts +4 -3
- package/dist/core/session-model/session-state.js +14 -1
- package/dist/core/user-settings.d.ts +36 -2
- package/dist/core/user-settings.js +56 -14
- package/dist/core/worktree.d.ts +15 -45
- package/dist/core/worktree.js +59 -261
- package/dist/daemon/api/__tests__/seam/profile-delete.test.d.ts +1 -0
- package/dist/daemon/api/__tests__/seam/profile-delete.test.js +386 -0
- package/dist/daemon/api/handlers/bash-jobs.js +1 -1
- package/dist/daemon/api/handlers/broker-ops.js +9 -5
- package/dist/daemon/api/handlers/feedback-comments.d.ts +2 -0
- package/dist/daemon/api/handlers/feedback-comments.js +207 -0
- package/dist/daemon/api/handlers/human.js +24 -20
- package/dist/daemon/api/handlers/inbox.d.ts +10 -0
- package/dist/daemon/api/handlers/inbox.js +48 -109
- package/dist/daemon/api/handlers/profiles.js +11 -29
- package/dist/daemon/api/map.d.ts +6 -1
- package/dist/daemon/api/map.js +23 -0
- package/dist/daemon/api/server.js +3 -1
- package/dist/daemon/companion-retire.d.ts +2 -0
- package/dist/daemon/companion-retire.js +33 -0
- package/dist/daemon/cron-run.js +29 -17
- package/dist/daemon/crtrd.js +1 -1
- package/dist/daemon/fleet.d.ts +22 -10
- package/dist/daemon/fleet.js +43 -24
- package/dist/daemon/human/finish.d.ts +12 -6
- package/dist/daemon/human/finish.js +134 -54
- package/dist/daemon/human/sweep.js +9 -3
- package/dist/daemon/profile-delete.d.ts +7 -0
- package/dist/daemon/profile-delete.js +306 -0
- package/dist/daemon/reconcilers/broker-supervision.js +14 -9
- package/dist/daemon/reconcilers/dormant-inbox.js +11 -6
- package/dist/daemon/reconcilers/live-obligation.d.ts +21 -1
- package/dist/daemon/reconcilers/live-obligation.js +30 -2
- package/dist/daemon/reconcilers/storage-maintenance.d.ts +5 -0
- package/dist/daemon/reconcilers/storage-maintenance.js +28 -1
- package/dist/daemon/review/deliver.js +3 -3
- package/dist/daemon/review/finish.d.ts +0 -2
- package/dist/daemon/review/finish.js +1 -29
- package/dist/daemon/review/sweep.js +12 -1
- package/dist/pi-extensions/__tests__/canvas-bash-valve.test.js +56 -1
- package/dist/pi-extensions/__tests__/canvas-stophook-agentend.test.js +4 -4
- package/dist/pi-extensions/canvas-bash-valve.d.ts +9 -1
- package/dist/pi-extensions/canvas-bash-valve.js +31 -13
- package/dist/pi-extensions/canvas-inbox-watcher.js +1 -1
- package/dist/pi-extensions/canvas-review-boundary.d.ts +4 -0
- package/dist/pi-extensions/canvas-review-boundary.js +18 -9
- package/dist/pi-extensions/canvas-stophook.js +4 -4
- package/dist/shared/generated-context.js +2 -2
- package/dist/types.d.ts +61 -5
- package/dist/types.js +8 -2
- package/package.json +4 -4
- package/runtime.lock.json +5 -33
- package/scripts/install-runtime.mjs +19 -24
- package/dist/builtin-memory/init.md +0 -38
- package/dist/builtin-memory/plan.md +0 -19
- package/dist/builtin-memory/spec.md +0 -19
- package/dist/clients/attach/render/html-markdown.d.ts +0 -13
- package/dist/clients/attach/render/html-markdown.js +0 -206
- package/dist/commands/human/doc.js +0 -144
- package/dist/commands/human/inbox.d.ts +0 -2
- package/dist/core/human/__tests__/page-render.test.js +0 -49
- package/dist/core/human/markdown-html.d.ts +0 -8
- package/dist/core/human/markdown-html.js +0 -581
- package/dist/core/human/page-render.d.ts +0 -3
- package/dist/core/human/page-render.js +0 -131
- package/dist/pages/bundle.css +0 -1
- package/dist/pages/bundle.js +0 -1347
- package/dist/pages/comments.d.ts +0 -43
- package/dist/pages/comments.js +0 -125
- package/dist/pages/elements/cards.d.ts +0 -17
- package/dist/pages/elements/cards.js +0 -802
- package/dist/pages/elements/chart.js +0 -648
- package/dist/pages/elements/options.d.ts +0 -21
- package/dist/pages/elements/options.js +0 -629
- package/dist/pages/elements/pages.d.ts +0 -24
- package/dist/pages/elements/pages.js +0 -355
- package/dist/pages/elements/slot.d.ts +0 -21
- package/dist/pages/elements/slot.js +0 -105
- package/dist/pages/elements/table.d.ts +0 -19
- package/dist/pages/elements/table.js +0 -935
- package/dist/pages/elements/text.d.ts +0 -25
- package/dist/pages/elements/text.js +0 -968
- package/dist/pages/entry.d.ts +0 -8
- package/dist/pages/entry.js +0 -8
- package/dist/pages/host.d.ts +0 -158
- package/dist/pages/host.js +0 -182
- package/dist/pages/register.d.ts +0 -2
- package/dist/pages/register.js +0 -3
- package/dist/pages/slot-config.d.ts +0 -36
- package/dist/pages/slot-config.js +0 -50
- package/dist/pages/types.d.ts +0 -98
- package/dist/pages/types.js +0 -23
- /package/dist/{core/human/__tests__/page-render.test.d.ts → commands/__tests__/node-message.test.d.ts} +0 -0
- /package/dist/{pages/elements/chart.d.ts → core/__tests__/plugin-page-components.test.d.ts} +0 -0
package/dist/core/config.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { dirname, join } from 'node:path';
|
|
2
2
|
import { isDeepStrictEqual } from 'node:util';
|
|
3
|
-
import { CONFIG_FILE, STATE_FILE, CONDENSED_HISTORY_MODES, MOUSE_MODE_DEFAULTS, DEFAULT_WHIP_MESSAGES, defaultScopeConfig, defaultScopeState, defaultModelLaddersConfig, defaultKindsConfig, defaultRemoteCanvasConfig } from '../types.js';
|
|
3
|
+
import { CONFIG_FILE, STATE_FILE, CONDENSED_HISTORY_MODES, MOUSE_MODE_DEFAULTS, WHIP_MESSAGE_MODES, DEFAULT_WHIP_MESSAGES, defaultScopeConfig, defaultScopeState, defaultModelLaddersConfig, defaultKindsConfig, defaultRemoteCanvasConfig } from '../types.js';
|
|
4
4
|
import { BINDING_CATALOG, BINDING_IDS, isAttachPaneBinding } from './keybindings/catalog.js';
|
|
5
5
|
import { emitEvent } from './events/emit.js';
|
|
6
6
|
import { atomicWriteJson, readJsonIfExists, writeJson, ensureDir } from './fs-utils.js';
|
|
@@ -10,7 +10,7 @@ import { profileRoot } from './profiles/manifest.js';
|
|
|
10
10
|
import { withExclusiveDirectoryLock } from './exclusive-lock.js';
|
|
11
11
|
import { SCOPE_CONFIG_KEYS } from './user-settings.js';
|
|
12
12
|
import { normalizeWorkingGerunds } from '../shared/working-activity.js';
|
|
13
|
-
import { normalizePageComponents } from './human/page-catalog.js';
|
|
13
|
+
import { mergePageComponentLayers, normalizePageComponents } from './human/page-catalog.js';
|
|
14
14
|
function configPathFor(root) {
|
|
15
15
|
return join(root, CONFIG_FILE);
|
|
16
16
|
}
|
|
@@ -266,6 +266,101 @@ function mergeSpawnEnv(raw, base = []) {
|
|
|
266
266
|
}
|
|
267
267
|
return { allow: [...new Set([...base, ...extra])] };
|
|
268
268
|
}
|
|
269
|
+
/** The one bare-command-name rule, shared by the read path (`normalizeBin`)
|
|
270
|
+
* and the strict install gate (`invalidPluginBinReasons`): a name a shell can
|
|
271
|
+
* invoke verbatim and a filesystem can hold as a single symlink entry — no
|
|
272
|
+
* separator, no leading dot, no whitespace, no quoting ever required. */
|
|
273
|
+
const BIN_NAME_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
|
|
274
|
+
/** Bare names crouter owns on every node's PATH. A contribution may not take
|
|
275
|
+
* one: shadowing the CLI a node drives its own canvas with breaks the runtime
|
|
276
|
+
* contract rather than extending it. */
|
|
277
|
+
const RESERVED_BIN_NAMES = new Set(['crtr', 'crtrd']);
|
|
278
|
+
/** The one bare-command-name validator shared by `bin` and plugin `requires`.
|
|
279
|
+
* `bin` alone reserves crouter's own commands; a plugin may require any safe
|
|
280
|
+
* executable name because it does not contribute or shadow that command. */
|
|
281
|
+
function bareCommandNameIssue(name, field) {
|
|
282
|
+
if (field === 'bin' && RESERVED_BIN_NAMES.has(name))
|
|
283
|
+
return `${field}.${name} is reserved by crouter — pick another bare command name`;
|
|
284
|
+
if (!BIN_NAME_PATTERN.test(name))
|
|
285
|
+
return `${field}.${name} is not a safe bare command name (expected ${BIN_NAME_PATTERN.source})`;
|
|
286
|
+
return undefined;
|
|
287
|
+
}
|
|
288
|
+
/** Validate raw `bin` declaration shape without touching the filesystem.
|
|
289
|
+
* Install uses these findings as a hard gate for plugin manifests; scope
|
|
290
|
+
* resolution reports them through `crtr sys doctor` and continues. */
|
|
291
|
+
export function binDeclarationIssues(raw) {
|
|
292
|
+
if (raw === null || typeof raw !== 'object' || Array.isArray(raw)) {
|
|
293
|
+
return [{ name: 'bin', message: 'bin must be a JSON object mapping bare command name to a relative path' }];
|
|
294
|
+
}
|
|
295
|
+
const issues = [];
|
|
296
|
+
for (const [name, target] of Object.entries(raw)) {
|
|
297
|
+
const nameIssue = bareCommandNameIssue(name, 'bin');
|
|
298
|
+
if (nameIssue !== undefined)
|
|
299
|
+
issues.push({ name, message: nameIssue });
|
|
300
|
+
if (typeof target !== 'string' || target.trim() === '') {
|
|
301
|
+
issues.push({ name, message: `bin.${name} must be a non-empty relative path to an executable inside the contributor root` });
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
return issues;
|
|
305
|
+
}
|
|
306
|
+
/** Normalize one raw `bin` block (a scope `config.json`'s or a plugin
|
|
307
|
+
* manifest's) to name → root-relative path. Invalid entries never become
|
|
308
|
+
* commands; `binDeclarationIssues` keeps them visible to Doctor. */
|
|
309
|
+
export function normalizeBin(raw) {
|
|
310
|
+
if (raw === null || typeof raw !== 'object' || Array.isArray(raw))
|
|
311
|
+
return {};
|
|
312
|
+
const out = {};
|
|
313
|
+
for (const [name, target] of Object.entries(raw)) {
|
|
314
|
+
if (bareCommandNameIssue(name, 'bin') !== undefined)
|
|
315
|
+
continue;
|
|
316
|
+
if (typeof target !== 'string' || target.trim() === '')
|
|
317
|
+
continue;
|
|
318
|
+
out[name] = target;
|
|
319
|
+
}
|
|
320
|
+
return out;
|
|
321
|
+
}
|
|
322
|
+
/** STRICT install-time validation for a plugin-declared `bin` block. Target
|
|
323
|
+
* filesystem state is checked later by Doctor because it can change after
|
|
324
|
+
* install. */
|
|
325
|
+
export function invalidPluginBinReasons(raw) {
|
|
326
|
+
return binDeclarationIssues(raw).map((issue) => issue.message);
|
|
327
|
+
}
|
|
328
|
+
/** Normalize a plugin manifest's advisory executable requirements. Invalid
|
|
329
|
+
* declarations stay out of the read path; source installs reject them through
|
|
330
|
+
* `invalidPluginRequiresReasons`. */
|
|
331
|
+
export function normalizeRequires(raw) {
|
|
332
|
+
if (raw === null || typeof raw !== 'object' || Array.isArray(raw))
|
|
333
|
+
return {};
|
|
334
|
+
const out = {};
|
|
335
|
+
for (const [name, hint] of Object.entries(raw)) {
|
|
336
|
+
if (bareCommandNameIssue(name, 'requires') !== undefined)
|
|
337
|
+
continue;
|
|
338
|
+
if (typeof hint !== 'string' || hint.trim() === '' || /[\r\n]/.test(hint))
|
|
339
|
+
continue;
|
|
340
|
+
out[name] = hint;
|
|
341
|
+
}
|
|
342
|
+
return out;
|
|
343
|
+
}
|
|
344
|
+
/** STRICT install-time validation for a plugin's advisory `requires` block.
|
|
345
|
+
* A missing executable is advisory; only malformed declarations fail install. */
|
|
346
|
+
export function invalidPluginRequiresReasons(raw) {
|
|
347
|
+
if (raw === null || typeof raw !== 'object' || Array.isArray(raw)) {
|
|
348
|
+
return ['requires must be a JSON object mapping a bare executable name to a one-line install hint'];
|
|
349
|
+
}
|
|
350
|
+
const reasons = [];
|
|
351
|
+
for (const [name, hint] of Object.entries(raw)) {
|
|
352
|
+
const nameIssue = bareCommandNameIssue(name, 'requires');
|
|
353
|
+
if (nameIssue !== undefined)
|
|
354
|
+
reasons.push(nameIssue);
|
|
355
|
+
if (typeof hint !== 'string' || hint.trim() === '') {
|
|
356
|
+
reasons.push(`requires.${name} must be a non-empty one-line install hint`);
|
|
357
|
+
}
|
|
358
|
+
else if (/[\r\n]/.test(hint)) {
|
|
359
|
+
reasons.push(`requires.${name} must be a one-line install hint`);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
return reasons;
|
|
363
|
+
}
|
|
269
364
|
/** Collect the valid `KindConfig` fields of one raw `kinds.<name>` entry —
|
|
270
365
|
* every field is optional here, `whenToUse` included; mistyped fields are
|
|
271
366
|
* dropped, never thrown (an invalid field in config.json must not break
|
|
@@ -341,6 +436,40 @@ function layerPluginKinds(scope, scopeRootPath, base) {
|
|
|
341
436
|
out = mergeKinds(plugin.manifest.kinds, out);
|
|
342
437
|
return out;
|
|
343
438
|
}
|
|
439
|
+
/** The ACTIVE page-component catalog every authoring, validation, daemon,
|
|
440
|
+
* inbox, and TUI consumer resolves: the user scope's own `page_components`
|
|
441
|
+
* entries plus the contributions of every ENABLED plugin installed under that
|
|
442
|
+
* same root, plugin name-sorted for determinism (directory listing order is
|
|
443
|
+
* filesystem-dependent). Installing, updating, or removing a plugin therefore
|
|
444
|
+
* adds, changes, or retires its components with no config edit.
|
|
445
|
+
*
|
|
446
|
+
* User scope only, deliberately: this catalog is read from the daemon, the
|
|
447
|
+
* CLI, and the inbox TUI, whose ambient cwds differ, and a page slot whose
|
|
448
|
+
* validity depended on which directory a process happened to start in would
|
|
449
|
+
* validate in one surface and fail in another. `page_components` has always
|
|
450
|
+
* been a user-scope block (`readConfig('user')`); its plugin channel keeps
|
|
451
|
+
* that boundary.
|
|
452
|
+
*
|
|
453
|
+
* Throws on a duplicate kind or derived JSX tag across sources, and on a
|
|
454
|
+
* plugin whose block is malformed — matching the existing catalog validation
|
|
455
|
+
* style, where a bad `page_components` block already fails config read rather
|
|
456
|
+
* than silently dropping a component the product expects to be authorable.
|
|
457
|
+
* Install-time validation (`invalidPageComponentsReasons`) is what keeps a
|
|
458
|
+
* malformed block from ever reaching this path. */
|
|
459
|
+
export function resolvePageComponents() {
|
|
460
|
+
const layers = [{ origin: 'config.json', components: readConfig('user').page_components }];
|
|
461
|
+
const root = scopeRoot('user');
|
|
462
|
+
if (root !== null) {
|
|
463
|
+
const plugins = listInstalledPluginsInRoot('user', root)
|
|
464
|
+
.filter((plugin) => plugin.enabled && plugin.manifest.page_components !== undefined)
|
|
465
|
+
.sort((a, b) => a.name.localeCompare(b.name));
|
|
466
|
+
for (const plugin of plugins) {
|
|
467
|
+
const label = `plugin "${plugin.name}" page_components`;
|
|
468
|
+
layers.push({ origin: `plugin "${plugin.name}"`, components: normalizePageComponents(plugin.manifest.page_components, label) });
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
return mergePageComponentLayers(layers);
|
|
472
|
+
}
|
|
344
473
|
const KIND_CONFIG_KEYS = new Set(['whenToUse', 'model', 'orchestratorModel', 'tools', 'extensions', 'availableTo']);
|
|
345
474
|
/** STRICT install-time validation for a plugin-declared `kinds` block —
|
|
346
475
|
* the loud counterpart to `mergeKinds`'s silent read-time dropping. Read
|
|
@@ -479,9 +608,12 @@ function mergeConfig(partial) {
|
|
|
479
608
|
? [...defaults.working_gerunds]
|
|
480
609
|
: normalizeWorkingGerunds(partial.working_gerunds);
|
|
481
610
|
const whip_mode = typeof partial.whip_mode === 'boolean' ? partial.whip_mode : defaults.whip_mode;
|
|
482
|
-
const html_pages = typeof partial.html_pages === 'boolean' ? partial.html_pages : defaults.html_pages;
|
|
483
611
|
const page_components = normalizePageComponents(partial.page_components);
|
|
612
|
+
const page_surface = typeof partial.page_surface === 'boolean' ? partial.page_surface : defaults.page_surface;
|
|
484
613
|
const whip_messages = normalizeWhipMessages(partial.whip_messages);
|
|
614
|
+
const whip_message_mode = WHIP_MESSAGE_MODES.includes(partial.whip_message_mode)
|
|
615
|
+
? partial.whip_message_mode
|
|
616
|
+
: defaults.whip_message_mode;
|
|
485
617
|
const mouse_mode_default = MOUSE_MODE_DEFAULTS.includes(partial.mouse_mode_default)
|
|
486
618
|
? partial.mouse_mode_default
|
|
487
619
|
: defaults.mouse_mode_default;
|
|
@@ -489,6 +621,7 @@ function mergeConfig(partial) {
|
|
|
489
621
|
const condensed_history = CONDENSED_HISTORY_MODES.includes(partial.condensed_history)
|
|
490
622
|
? partial.condensed_history
|
|
491
623
|
: defaults.condensed_history;
|
|
624
|
+
const bash_tool_purpose = typeof partial.bash_tool_purpose === 'boolean' ? partial.bash_tool_purpose : defaults.bash_tool_purpose;
|
|
492
625
|
const fold_finished_tools = typeof partial.fold_finished_tools === 'boolean' ? partial.fold_finished_tools : defaults.fold_finished_tools;
|
|
493
626
|
const summarize_tool_calls = typeof partial.summarize_tool_calls === 'boolean' ? partial.summarize_tool_calls : defaults.summarize_tool_calls;
|
|
494
627
|
const detailed_tool_recaps = typeof partial.detailed_tool_recaps === 'boolean' ? partial.detailed_tool_recaps : defaults.detailed_tool_recaps;
|
|
@@ -500,7 +633,8 @@ function mergeConfig(partial) {
|
|
|
500
633
|
const kinds = mergeKinds(partial.kinds);
|
|
501
634
|
const remoteCanvas = mergeRemoteCanvas(partial.remoteCanvas);
|
|
502
635
|
const spawnEnv = mergeSpawnEnv(partial.spawnEnv, defaults.spawnEnv?.allow);
|
|
503
|
-
|
|
636
|
+
const bin = normalizeBin(partial.bin);
|
|
637
|
+
return { bin, schema_version, marketplaces, plugins, auto_update, max_panes_per_window, auto_open_child_viewers, brokerThresholds, completion_bell, working_gerunds, whip_mode, page_components, page_surface, whip_messages, whip_message_mode, mouse_mode_default, live_cycles, condensed_history, bash_tool_purpose, fold_finished_tools, summarize_tool_calls, detailed_tool_recaps, keybindings, tmux_passthrough, modelLadders, ...(Object.keys(modelRoutes).length > 0 ? { modelRoutes } : {}), ...(modelRouting !== undefined ? { modelRouting } : {}), kinds, remoteCanvas, spawnEnv };
|
|
504
638
|
}
|
|
505
639
|
/** Raw (un-defaulted) partial config for one scope, or null if the scope has
|
|
506
640
|
* no root or no config.json. Used by `readMergedLaunchConfig` to layer
|
package/dist/core/help.d.ts
CHANGED
|
@@ -64,6 +64,9 @@ export interface StdinParam {
|
|
|
64
64
|
name: string;
|
|
65
65
|
required: boolean;
|
|
66
66
|
constraint: string;
|
|
67
|
+
/** Whether one positional token may supply stdin instead of a pipe. Defaults
|
|
68
|
+
* to true for existing stdin-body leaves; false requires actual stdin. */
|
|
69
|
+
allowPositional?: boolean;
|
|
67
70
|
}
|
|
68
71
|
/** --context-file PATH: reads and JSON-parses the file at PATH. */
|
|
69
72
|
export interface ContextFileParam {
|
package/dist/core/help.js
CHANGED
|
@@ -223,7 +223,7 @@ function paramDesc(p) {
|
|
|
223
223
|
// flag
|
|
224
224
|
const f = p;
|
|
225
225
|
if (f.type === 'bool')
|
|
226
|
-
return
|
|
226
|
+
return `${req} boolean. Presence means true. ${f.constraint}`.trim();
|
|
227
227
|
const dflt = f.default !== undefined ? ` Default: ${String(f.default)}.` : '';
|
|
228
228
|
const choices = f.type === 'enum' && f.choices !== undefined
|
|
229
229
|
? ` One of: ${f.choices.join(', ')}.`
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import test from 'node:test';
|
|
3
|
+
import { normalizePageComponents, pageComponentKinds } from '../page-catalog.js';
|
|
4
|
+
test('page component registrations accept legacy names and documented objects', () => {
|
|
5
|
+
const components = normalizePageComponents([
|
|
6
|
+
'approval',
|
|
7
|
+
{ kind: 'approval-card', description: 'Approval card', useWhen: 'Use for an approval.', doc: 'Product contract.', display: true },
|
|
8
|
+
]);
|
|
9
|
+
assert.deepEqual(components, [
|
|
10
|
+
{ kind: 'approval' },
|
|
11
|
+
{ kind: 'approval-card', description: 'Approval card', useWhen: 'Use for an approval.', doc: 'Product contract.', display: true },
|
|
12
|
+
]);
|
|
13
|
+
assert.deepEqual(pageComponentKinds(components), ['approval', 'approval-card']);
|
|
14
|
+
assert.throws(() => normalizePageComponents([{ kind: 'approval', display: 'yes' }]), /page_components\[0\]\.display must be a boolean/);
|
|
15
|
+
assert.throws(() => normalizePageComponents([{ kind: 'approval', unexpected: true }]), /unknown field\(s\): unexpected/);
|
|
16
|
+
});
|
|
17
|
+
test('page component registrations reserve derived built-in and product JSX tags', () => {
|
|
18
|
+
assert.throws(() => normalizePageComponents(['user-question']), /derives JSX tag "UserQuestion", which collides with a built-in page component/);
|
|
19
|
+
assert.throws(() => normalizePageComponents(['foo-bar', 'foo--bar']), /derives JSX tag "FooBar", which duplicates another product page component/);
|
|
20
|
+
});
|
|
@@ -5,51 +5,74 @@ import { join } from 'node:path';
|
|
|
5
5
|
import test from 'node:test';
|
|
6
6
|
const home = mkdtempSync(join(tmpdir(), 'crtr-page-tickets-'));
|
|
7
7
|
process.env['CRTR_HOME'] = home;
|
|
8
|
-
const { atomicWriteJson, pageManifestPath, pagePath, progressPath, responsePath } = await import('../convention.js');
|
|
8
|
+
const { atomicWriteJson, bindTicketReplyRoute, pageManifestPath, pagePath, progressPath, responsePath } = await import('../convention.js');
|
|
9
9
|
const { claimTicket } = await import('../claim.js');
|
|
10
10
|
const { scanInbox, scanPageHistory } = await import('../scan.js');
|
|
11
11
|
const { ticketDir } = await import('../root.js');
|
|
12
|
-
const { cancelTicket, completePage, readPageProgress, readPageTicket, readTicketResult, submitPage, takeoverAndCompletePage, writePageProgress, } = await import('../tickets.js');
|
|
12
|
+
const { cancelTicket, completePage, readPageProgress, readPageTicket, preparePage, readTicketResult, submitPage, takeoverAndCompletePage, validatePageTicketResponses, writePageProgress, } = await import('../tickets.js');
|
|
13
13
|
const sources = mkdtempSync(join(tmpdir(), 'crtr-page-sources-'));
|
|
14
14
|
function sourceFile(name, body) {
|
|
15
15
|
const path = join(sources, name);
|
|
16
16
|
writeFileSync(path, body);
|
|
17
17
|
return path;
|
|
18
18
|
}
|
|
19
|
-
const askPage = sourceFile('ask.
|
|
20
|
-
|
|
21
|
-
subtitle
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
19
|
+
const askPage = sourceFile('ask.tsx', `export default function Rollout() {
|
|
20
|
+
return (
|
|
21
|
+
<Page title="Rollout scope" subtitle="Pick tonight\u2019s rollout">
|
|
22
|
+
<p>Which rollout do you want?</p>
|
|
23
|
+
<UserQuestion id="rollout" label="Rollout lane" body="Canary limits the blast radius." mode="single" options={[{ id: 'canary', label: 'Canary first' }, { id: 'full', label: 'Full' }]} />
|
|
24
|
+
</Page>
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
`);
|
|
28
|
+
const displayPage = sourceFile('display.tsx', `export default function Nightly() {
|
|
29
|
+
return (
|
|
30
|
+
<Page title="Nightly report">
|
|
31
|
+
<h1>All green</h1>
|
|
32
|
+
</Page>
|
|
33
|
+
);
|
|
34
|
+
}
|
|
30
35
|
`);
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
36
|
+
function submit(id, source, emittedAt, nodeId = 'node-1', replyExpected = true) {
|
|
37
|
+
const dir = submitPage({ dir: ticketDir(id), sourceFile: source, source: { nodeId, emittedAt }, delivery: { placement: 'inline', inbox: replyExpected, reply: replyExpected } }).dir;
|
|
38
|
+
if (replyExpected)
|
|
39
|
+
bindTicketReplyRoute(dir, id);
|
|
40
|
+
return dir;
|
|
34
41
|
}
|
|
35
|
-
test('submitPage publishes the authored source, the derived manifest and the
|
|
36
|
-
const submitted = submitPage({ dir: ticketDir('ask'), sourceFile: askPage,
|
|
42
|
+
test('submitPage publishes the authored source, the derived manifest and the emittedAt stamp', () => {
|
|
43
|
+
const submitted = submitPage({ dir: ticketDir('ask'), sourceFile: askPage, source: { nodeId: 'node-1' }, delivery: { placement: 'inline', inbox: true, reply: true } });
|
|
37
44
|
assert.equal(submitted.kind, 'page');
|
|
38
45
|
assert.equal(submitted.id, 'ask');
|
|
39
|
-
assert.equal(readFileSync(pagePath(submitted.dir
|
|
46
|
+
assert.equal(readFileSync(pagePath(submitted.dir), 'utf8'), readFileSync(askPage, 'utf8'));
|
|
40
47
|
const manifest = JSON.parse(readFileSync(pageManifestPath(submitted.dir), 'utf8'));
|
|
41
|
-
assert.equal(manifest['schema'], 'crtr.page/
|
|
42
|
-
assert.equal(manifest['document'], 'page.
|
|
43
|
-
assert.
|
|
48
|
+
assert.equal(manifest['schema'], 'crtr.page/v2');
|
|
49
|
+
assert.equal(manifest['document'], 'page.tsx');
|
|
50
|
+
assert.deepEqual(manifest['delivery'], { placement: 'inline', inbox: true, reply: true });
|
|
44
51
|
const source = manifest['source'];
|
|
45
52
|
assert.equal(source.nodeId, 'node-1');
|
|
46
|
-
assert.ok(Date.parse(source.
|
|
47
|
-
|
|
53
|
+
assert.ok(Date.parse(source.emittedAt) > 0, 'submission stamps emittedAt');
|
|
54
|
+
// A ticket's request is replaceable in place — what `human send --replace`
|
|
55
|
+
// does. Republishing over the same directory revises the page and leaves one
|
|
56
|
+
// ticket behind, rather than refusing or raising a second one.
|
|
57
|
+
const stamp = { nodeId: 'node-replace', emittedAt: '2026-01-05T00:00:00.000Z' };
|
|
58
|
+
const first = submitPage({ dir: ticketDir('revisable'), sourceFile: askPage, source: stamp });
|
|
59
|
+
const revised = submitPage({ dir: ticketDir('revisable'), sourceFile: displayPage, source: stamp });
|
|
60
|
+
assert.equal(revised.id, first.id);
|
|
61
|
+
assert.equal(revised.manifest.title, 'Nightly report');
|
|
62
|
+
assert.equal(readFileSync(pagePath(revised.dir), 'utf8'), readFileSync(displayPage, 'utf8'));
|
|
63
|
+
assert.deepEqual(scanPageHistory('node-replace').map((entry) => entry.id), ['revisable'], 'a replaced page is one ticket, not two');
|
|
64
|
+
bindTicketReplyRoute(submitted.dir, 'ask');
|
|
48
65
|
const listed = scanInbox().find((item) => item.id === 'ask');
|
|
49
66
|
assert.equal(listed?.kind, 'page');
|
|
50
67
|
assert.equal(listed?.kind === 'page' ? listed.awaitsResponse : undefined, true);
|
|
51
68
|
assert.deepEqual(listed?.kind === 'page' ? listed.slotKinds : undefined, ['options']);
|
|
52
69
|
});
|
|
70
|
+
test('an HTML page derives a slotless manifest from declared metadata', () => {
|
|
71
|
+
const report = sourceFile('report.html', '<!doctype html><html><head><meta content="Summary" name="description"><title>Report</title></head><body>done</body></html>');
|
|
72
|
+
const prepared = preparePage({ sourceFile: report });
|
|
73
|
+
assert.equal(prepared.module, undefined);
|
|
74
|
+
assert.deepEqual(prepared.manifest, { schema: 'crtr.page/v2', dialect: 'html', title: 'Report', subtitle: 'Summary', delivery: { placement: 'inline', inbox: false, reply: false }, document: 'page.html', steps: 1, slots: [], source: prepared.manifest.source });
|
|
75
|
+
});
|
|
53
76
|
test('progress round-trips through validation and rejects malformed saved work', () => {
|
|
54
77
|
const dir = ticketDir('ask');
|
|
55
78
|
assert.equal(readPageProgress(dir), null, 'no progress file reads as null');
|
|
@@ -59,6 +82,11 @@ test('progress round-trips through validation and rejects malformed saved work',
|
|
|
59
82
|
assert.throws(() => writePageProgress(dir, { unknown: { selectedOptionIds: [], comments: [] } }), /unknown/);
|
|
60
83
|
atomicWriteJson(progressPath(dir), { kind: 'page', responses: { rollout: 'not an object' }, savedAt: new Date().toISOString() });
|
|
61
84
|
assert.throws(() => readPageProgress(dir), /malformed/, 'malformed progress is an error, never silently discarded');
|
|
85
|
+
// Saved work can outlive the slot it answered, because the agent may replace
|
|
86
|
+
// the page under it. The stale entry is dropped on read so the ticket stays
|
|
87
|
+
// readable; every entry the current page still declares hydrates.
|
|
88
|
+
atomicWriteJson(progressPath(dir), { kind: 'page', responses: { rollout: { selectedOptionIds: ['canary'], comments: [] }, retired: { selectedOptionIds: ['gone'], comments: [] } }, savedAt: new Date().toISOString() });
|
|
89
|
+
assert.deepEqual(readPageProgress(dir), { rollout: { selectedOptionIds: ['canary'], comments: [] } });
|
|
62
90
|
writePageProgress(dir, { rollout: { selectedOptionIds: ['canary'], comments: [] } });
|
|
63
91
|
});
|
|
64
92
|
test('readPageTicket derives pending, resolved and canceled state and carries the document', () => {
|
|
@@ -77,23 +105,29 @@ test('readPageTicket derives pending, resolved and canceled state and carries th
|
|
|
77
105
|
assert.equal(written['schema'], 'crtr.page-response/v1');
|
|
78
106
|
assert.equal(written['kind'], 'page');
|
|
79
107
|
assert.deepEqual(written['responses'], { rollout: { selectedOptionIds: ['full'], comments: [] } });
|
|
80
|
-
assert.equal(written['summary'], '
|
|
108
|
+
assert.equal(written['summary'], 'Full');
|
|
81
109
|
assert.equal(readTicketResult(dir)?.kind, 'page', 'the strict decoder accepts the page result');
|
|
82
110
|
assert.equal(existsSync(progressPath(dir)), false, 'completion clears owned work');
|
|
83
111
|
const resolved = readPageTicket(dir);
|
|
84
112
|
assert.equal(resolved.state, 'resolved');
|
|
85
113
|
assert.equal(resolved.progress, null);
|
|
86
114
|
assert.equal(scanInbox().some((item) => item.id === 'ask'), false, 'a resolved page leaves the pending list');
|
|
87
|
-
const canceledDir = submit('canceled', askPage, '
|
|
115
|
+
const canceledDir = submit('canceled', askPage, '2026-01-03T00:00:00.000Z');
|
|
88
116
|
cancelTicket(canceledDir, { reason: 'no longer needed' });
|
|
89
117
|
assert.equal(readPageTicket(canceledDir).state, 'canceled');
|
|
90
118
|
});
|
|
91
|
-
test('
|
|
92
|
-
const dir = submit('display', displayPage, '
|
|
119
|
+
test('page scan derives response expectation from manifest delivery', () => {
|
|
120
|
+
const dir = submit('display', displayPage, '2026-01-04T00:00:00.000Z');
|
|
93
121
|
const ticket = readPageTicket(dir);
|
|
94
|
-
assert.equal(ticket.manifest.dialect, '
|
|
122
|
+
assert.equal(ticket.manifest.dialect, 'jsx');
|
|
95
123
|
assert.equal(ticket.manifest.slots.length, 0);
|
|
96
|
-
|
|
124
|
+
const display = scanInbox().find((item) => item.id === 'display');
|
|
125
|
+
assert.equal(display?.kind === 'page' ? display.awaitsResponse : undefined, true, 'a reply-bearing page waits even with no response-bearing slots');
|
|
126
|
+
const announcementDir = submit('announcement-with-options', askPage, '2026-01-04T00:00:01.000Z', 'node-1', false);
|
|
127
|
+
const announcement = scanInbox().find((item) => item.id === 'announcement-with-options');
|
|
128
|
+
assert.equal(announcement, undefined, 'a passive page stays out of the inbox');
|
|
129
|
+
assert.equal(readPageTicket(announcementDir).state, 'passive');
|
|
130
|
+
assert.throws(() => takeoverAndCompletePage(announcementDir, {}, { host: 'crtrd', pid: process.pid }), /page_has_no_response_lifecycle/);
|
|
97
131
|
const acknowledged = takeoverAndCompletePage(dir, {}, { host: 'crtrd', pid: process.pid });
|
|
98
132
|
assert.equal(acknowledged.won, true);
|
|
99
133
|
const written = JSON.parse(readFileSync(responsePath(dir), 'utf8'));
|
|
@@ -102,11 +136,11 @@ test('a display page acknowledges with the empty responses map', () => {
|
|
|
102
136
|
assert.equal(written['summary'], '');
|
|
103
137
|
});
|
|
104
138
|
test('scanPageHistory returns every page of one node, oldest first, regardless of terminal file', () => {
|
|
105
|
-
submit('older', askPage, '
|
|
106
|
-
submit('other-node', askPage, '
|
|
139
|
+
submit('older', askPage, '2026-01-01T00:00:00.000Z');
|
|
140
|
+
submit('other-node', askPage, '2026-01-02T00:00:00.000Z', 'node-2');
|
|
107
141
|
const history = scanPageHistory('node-1');
|
|
108
|
-
assert.deepEqual(history.map((entry) => entry.id), ['older', 'canceled', 'display', 'ask']);
|
|
109
|
-
assert.deepEqual(history.map((entry) => entry.state), ['pending', 'canceled', 'resolved', 'resolved']);
|
|
142
|
+
assert.deepEqual(history.map((entry) => entry.id), ['older', 'canceled', 'display', 'announcement-with-options', 'ask']);
|
|
143
|
+
assert.deepEqual(history.map((entry) => entry.state), ['pending', 'canceled', 'resolved', 'passive', 'resolved']);
|
|
110
144
|
assert.deepEqual(scanPageHistory('node-2').map((entry) => entry.id), ['other-node']);
|
|
111
145
|
assert.deepEqual(scanPageHistory('node-missing'), []);
|
|
112
146
|
});
|
|
@@ -1,97 +1,51 @@
|
|
|
1
1
|
import assert from 'node:assert/strict';
|
|
2
|
-
import { mkdtempSync, rmSync, writeFileSync } from 'node:fs';
|
|
3
|
-
import { tmpdir } from 'node:os';
|
|
4
|
-
import { join } from 'node:path';
|
|
5
2
|
import test from 'node:test';
|
|
6
|
-
import {
|
|
7
|
-
import { isResponseBearingSlot, validatePageResponses, validatePartialPageResponses } from '../page-schema.js';
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
const path = join(dir, name);
|
|
12
|
-
writeFileSync(path, source);
|
|
13
|
-
run(path);
|
|
14
|
-
}
|
|
15
|
-
finally {
|
|
16
|
-
rmSync(dir, { recursive: true, force: true });
|
|
17
|
-
}
|
|
3
|
+
import { derivePageManifestFromDocument } from '../page.js';
|
|
4
|
+
import { isResponseBearingSlot, validatePageManifest, validatePageResponses, validatePartialPageResponses } from '../page-schema.js';
|
|
5
|
+
const OPTIONS = '<UserQuestion id="choice" label="Choose an option" body="One of these ships tonight." mode="single" options={[{ id: "yes", label: "Yes" }]} />';
|
|
6
|
+
function page(body, props = 'title="Test"') {
|
|
7
|
+
return `export default function Page_() {\n return (\n <Page ${props}>\n ${body}\n </Page>\n );\n}\n`;
|
|
18
8
|
}
|
|
19
|
-
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
withSource('input.html', `${head}${body}${htmlTail}`, (path) => { manifest = derivePageManifest(path, 'html'); });
|
|
25
|
-
return manifest;
|
|
26
|
-
}
|
|
27
|
-
function expectHtmlError(body, pattern, head = htmlHead) {
|
|
28
|
-
withSource('input.html', `${head}${body}${htmlTail}`, (path) => assert.throws(() => derivePageManifest(path, 'html'), pattern));
|
|
29
|
-
}
|
|
30
|
-
test('md dialect derives a normalized manifest and preserves top-level slot scanning', () => {
|
|
31
|
-
const source = `---
|
|
32
|
-
title: Rollout
|
|
33
|
-
subtitle: Choose carefully
|
|
34
|
-
placement: inline
|
|
35
|
-
---
|
|
36
|
-
|
|
37
|
-
Before.
|
|
38
|
-
|
|
39
|
-
\`\`\`\`markdown
|
|
40
|
-
\`\`\`slot:text
|
|
41
|
-
{"id":"nested","initialText":"not a slot"}
|
|
42
|
-
\`\`\`
|
|
43
|
-
\`\`\`\`
|
|
44
|
-
|
|
45
|
-
\`\`\`slot:table
|
|
46
|
-
{"columns":[{"id":"day","label":"Day"}],"rows":[{"id":"r1","cells":{"day":"Mon"}}]}
|
|
47
|
-
\`\`\`
|
|
48
|
-
|
|
49
|
-
\`\`\`slot:options
|
|
50
|
-
{"id":"rollout","mode":"single","options":[{"id":"yes","label":"Yes"}]}
|
|
51
|
-
\`\`\`
|
|
52
|
-
`;
|
|
53
|
-
withSource('input.md', source, (path) => {
|
|
54
|
-
const manifest = derivePageManifest(path, 'md');
|
|
55
|
-
// A display-only table's schema defaults must survive the manifest→source round trip.
|
|
56
|
-
assert.deepEqual(parsePageSource(source, manifest).at(-1), { kind: 'slot', slotIndex: 1 });
|
|
57
|
-
assert.equal(manifest.title, 'Rollout');
|
|
58
|
-
assert.equal(manifest.subtitle, 'Choose carefully');
|
|
59
|
-
assert.equal(manifest.placement, 'inline');
|
|
60
|
-
assert.equal(manifest.document, 'page.md');
|
|
61
|
-
assert.equal(manifest.steps, 1);
|
|
62
|
-
assert.deepEqual(manifest.slots.map(({ id, kind, step }) => ({ id, kind, step })), [{ id: undefined, kind: 'table', step: 0 }, { id: 'rollout', kind: 'options', step: 0 }]);
|
|
63
|
-
});
|
|
64
|
-
});
|
|
65
|
-
test('html dialect derives document-order slots and wrapper steps', () => {
|
|
66
|
-
const body = `<crtr-pages><crtr-page><section><crtr-options id="choice">${optionsScript}</crtr-options></section></crtr-page><crtr-page><crtr-chart><script type="application/json">{"kind":"bar","x":"day","y":[{"field":"count"}]}</script></crtr-chart></crtr-page></crtr-pages>`;
|
|
67
|
-
const manifest = deriveHtml(body, '<!doctype html><html placement="inline"><head><meta name="crtr-page-title" content="HTML"><meta name="crtr-page-subtitle" content="Two steps"><meta name="crtr-page-placement" content="inline"></head><body>');
|
|
68
|
-
assert.equal(manifest.title, 'HTML');
|
|
69
|
-
assert.equal(manifest.placement, 'inline');
|
|
9
|
+
test('jsx pages derive page props, steps, and document-order slots', () => {
|
|
10
|
+
const manifest = derivePageManifestFromDocument(page(`<Step>${OPTIONS}</Step>\n <Step><Chart kind="bar" x="day" y={[{ field: "count" }]} /></Step>`, 'title="JSX" subtitle="Two steps"'));
|
|
11
|
+
assert.equal(manifest.title, 'JSX');
|
|
12
|
+
assert.equal(manifest.subtitle, 'Two steps');
|
|
13
|
+
assert.deepEqual(manifest.delivery, { placement: 'inline', inbox: false, reply: false });
|
|
70
14
|
assert.equal(manifest.steps, 2);
|
|
71
|
-
assert.deepEqual(manifest.slots.map(({ kind, step }) => ({ kind, step })), [
|
|
15
|
+
assert.deepEqual(manifest.slots.map(({ kind, step, id }) => ({ kind, step, id })), [
|
|
16
|
+
{ kind: 'options', step: 0, id: 'choice' },
|
|
17
|
+
{ kind: 'chart', step: 1, id: undefined },
|
|
18
|
+
]);
|
|
19
|
+
});
|
|
20
|
+
test('a page without steps is one step, and a slot config computed at render time still reaches the manifest', () => {
|
|
21
|
+
const manifest = derivePageManifestFromDocument(page('<UserText id="note" label="Add a note" initialText="" />'));
|
|
22
|
+
assert.equal(manifest.steps, 1);
|
|
23
|
+
assert.deepEqual(manifest.slots, [{ id: 'note', kind: 'text', step: 0, config: { label: 'Add a note', initialText: '' } }]);
|
|
24
|
+
// The manifest is harvested by rendering the page, so mapped data, helper components,
|
|
25
|
+
// and hook state are all present; handlers are dropped because a manifest is JSON.
|
|
26
|
+
const computed = derivePageManifestFromDocument(`const ROWS = [{ id: 'a', label: 'A' }, { id: 'b', label: 'B' }];
|
|
27
|
+
function Pick({ rows }) { return <UserQuestion id="picks" label="Pick" body="Rows come from data." mode="single" options={rows.map((r) => ({ id: r.id, label: r.label }))} onSelect={() => 1} />; }
|
|
28
|
+
export default function Computed() { const [title] = React.useState('Computed'); return <Page title={title}><Pick rows={ROWS} /></Page>; }
|
|
29
|
+
`);
|
|
30
|
+
assert.equal(computed.title, 'Computed');
|
|
31
|
+
assert.deepEqual(computed.slots, [{ id: 'picks', kind: 'options', step: 0, config: { label: 'Pick', body: 'Rows come from data.', mode: 'single', options: [{ id: 'a', label: 'A' }, { id: 'b', label: 'B' }] } }]);
|
|
32
|
+
});
|
|
33
|
+
test('a page that cannot render is rejected with the reason', () => {
|
|
34
|
+
assert.throws(() => derivePageManifestFromDocument(page(OPTIONS, 'title=""')), /nonempty title prop/);
|
|
35
|
+
assert.throws(() => derivePageManifestFromDocument(page(OPTIONS, 'title="T" placement="drawer"')), /placement is not authored/);
|
|
36
|
+
assert.throws(() => derivePageManifestFromDocument('function Page_() { return <Page title="T" />; }\n'), /must `export default` a component function/);
|
|
37
|
+
assert.throws(() => derivePageManifestFromDocument('export default function Page_() { return (<><Page title="A" /><Page title="B" /></>); }'), /exactly one is allowed/);
|
|
38
|
+
assert.throws(() => derivePageManifestFromDocument('export default () => <Page title="T"><Mystery /></Page>;'), /Mystery is not defined/);
|
|
39
|
+
assert.throws(() => derivePageManifestFromDocument('export default async () => <Page title="T" />;'), /async/);
|
|
72
40
|
});
|
|
73
|
-
test('
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
test('md front matter names unknown, empty-title, and placement errors', () => {
|
|
79
|
-
for (const [source, pattern] of [
|
|
80
|
-
['---\ntitle: T\nother: no\n---\n', /unknown page front matter key "other"/],
|
|
81
|
-
['---\ntitle: \n---\n', /title is required and must be nonempty/],
|
|
82
|
-
['---\ntitle: T\nplacement: sidebar\n---\n', /placement must be exactly inline or panel/],
|
|
83
|
-
])
|
|
84
|
-
withSource('input.md', source, (path) => assert.throws(() => derivePageManifest(path, 'md'), pattern));
|
|
41
|
+
test('module plumbing is rejected with the in-scope teaching', () => {
|
|
42
|
+
const importing = `import React from 'react';\n${page(OPTIONS)}`;
|
|
43
|
+
assert.throws(() => derivePageManifestFromDocument(importing), /page source uses import: .*already in scope/s);
|
|
44
|
+
const requiring = `const x = require('./data.json');\n${page(OPTIONS)}`;
|
|
45
|
+
assert.throws(() => derivePageManifestFromDocument(requiring), /page source uses require\(\)/);
|
|
85
46
|
});
|
|
86
|
-
test('
|
|
87
|
-
|
|
88
|
-
const duplicateSubtitle = '<!doctype html><html><head><meta name="crtr-page-title" content="A"><meta name="crtr-page-subtitle" content="A"><meta name="crtr-page-subtitle" content="B"></head><body>';
|
|
89
|
-
const unknown = '<!doctype html><html><head><meta name="crtr-page-title" content="A"><meta name="crtr-page-extra" content="x"></head><body>';
|
|
90
|
-
const badPlacement = '<!doctype html><html><head><meta name="crtr-page-title" content="A"><meta name="crtr-page-placement" content="drawer"></head><body>';
|
|
91
|
-
expectHtmlError('', /Expected exactly one <meta name="crtr-page-title">, found 2/, duplicateTitle);
|
|
92
|
-
expectHtmlError('', /Expected at most one <meta name="crtr-page-subtitle">, found 2/, duplicateSubtitle);
|
|
93
|
-
expectHtmlError('', /unknown page metadata element <meta name="crtr-page-extra">/, unknown);
|
|
94
|
-
expectHtmlError('', /content must be exactly inline or panel/, badPlacement);
|
|
47
|
+
test('a page that does not compile is reported with its location', () => {
|
|
48
|
+
assert.throws(() => derivePageManifestFromDocument('export default function Page_() { return (<Page title="Broken">; }'), /page does not compile: .*page\.tsx:\d+:\d+/s);
|
|
95
49
|
});
|
|
96
50
|
test('the response-bearing predicate handles table selection and catalog edges', () => {
|
|
97
51
|
const slot = (kind, config, unvalidated) => ({ kind, step: 0, config, ...(unvalidated ? { unvalidated } : {}) });
|
|
@@ -100,40 +54,27 @@ test('the response-bearing predicate handles table selection and catalog edges',
|
|
|
100
54
|
assert.equal(isResponseBearingSlot(slot('chart', { kind: 'bar', x: 'x', y: [{ field: 'y' }] })), false);
|
|
101
55
|
assert.equal(isResponseBearingSlot(slot('product-kind', {}, true)), true);
|
|
102
56
|
});
|
|
57
|
+
test('product components are authored under their PascalCase tag', () => {
|
|
58
|
+
const displayComponent = [{ kind: 'nl-thread', display: true }];
|
|
59
|
+
const manifest = derivePageManifestFromDocument(page('<NlThread messages={[]} />'), displayComponent);
|
|
60
|
+
assert.deepEqual(manifest.slots, [{ kind: 'nl-thread', step: 0, config: { messages: [] }, unvalidated: true, display: true }]);
|
|
61
|
+
assert.deepEqual(validatePageManifest(manifest).slots, manifest.slots);
|
|
62
|
+
assert.equal(isResponseBearingSlot(manifest.slots[0]), false);
|
|
63
|
+
assert.throws(() => derivePageManifestFromDocument(page('<ProductInput />'), [{ kind: 'product-input' }]), /product slot kind "product-input" at slot #0 requires an id prop/);
|
|
64
|
+
});
|
|
65
|
+
test('a question requires its label and body at authoring time', () => {
|
|
66
|
+
assert.throws(() => derivePageManifestFromDocument(page('<UserQuestion id="choice" body="Context." mode="single" options={[{ id: "yes", label: "Yes" }]} />')), /slot "choice" \(options\): label is required/);
|
|
67
|
+
assert.throws(() => derivePageManifestFromDocument(page('<UserQuestion id="choice" label="Choose" mode="single" options={[{ id: "yes", label: "Yes" }]} />')), /slot "choice" \(options\): body is required/);
|
|
68
|
+
});
|
|
69
|
+
test('multi-response pages require a label on every response-bearing slot', () => {
|
|
70
|
+
assert.throws(() => derivePageManifestFromDocument(page(`<UserText id="first" initialText="" />\n <UserText id="note" label="Add a note" initialText="" />`)), /response-bearing slot "first" requires a nonempty config\.label/);
|
|
71
|
+
assert.throws(() => derivePageManifestFromDocument(page(`${OPTIONS}\n <UserTable id="table" label="Pick a row" columns={[{ id: "name", label: "Name" }]} rows={[{ id: "one", cells: { name: "One" } }]} rowSelect="single" caption="old" />`)), /invalid config for slot "table"/);
|
|
72
|
+
});
|
|
103
73
|
test('final and partial response validation share slot-addressed rules', () => {
|
|
104
|
-
const manifest =
|
|
74
|
+
const manifest = derivePageManifestFromDocument(page(`${OPTIONS}\n <UserText id="note" label="Add a note" initialText="" />`));
|
|
105
75
|
assert.deepEqual(validatePartialPageResponses(manifest, { choice: { selectedOptionIds: ['yes'], comments: [] } }), { choice: { selectedOptionIds: ['yes'], comments: [] } });
|
|
106
76
|
assert.throws(() => validatePageResponses(manifest, { choice: { selectedOptionIds: ['yes'], comments: [] } }), /missing response-bearing slot id "note"/);
|
|
107
|
-
assert.deepEqual(validatePageResponses(manifest, { choice: { selectedOptionIds: ['yes'], comments: [] }, note: { text: '', edited: false
|
|
77
|
+
assert.deepEqual(validatePageResponses(manifest, { choice: { selectedOptionIds: ['yes'], comments: [] }, note: { text: '', edited: false } }), { choice: { selectedOptionIds: ['yes'], comments: [] }, note: { text: '', edited: false } });
|
|
108
78
|
assert.throws(() => validatePartialPageResponses(manifest, { choice: { selectedOptionIds: ['maybe'], comments: [] } }), /unknown option id "maybe"/);
|
|
109
79
|
assert.throws(() => validatePartialPageResponses(manifest, { extra: {} }), /unknown or non-response-bearing slot id "extra"/);
|
|
110
80
|
});
|
|
111
|
-
test('PageSourceProjection associates leading, interstitial, and trailing prose', () => {
|
|
112
|
-
const source = `---
|
|
113
|
-
title: Projection
|
|
114
|
-
---
|
|
115
|
-
Leading prose.
|
|
116
|
-
|
|
117
|
-
\`\`\`slot:text
|
|
118
|
-
{"id":"first","initialText":""}
|
|
119
|
-
\`\`\`
|
|
120
|
-
|
|
121
|
-
Interstitial prose.
|
|
122
|
-
|
|
123
|
-
\`\`\`slot:text
|
|
124
|
-
{"id":"second","initialText":""}
|
|
125
|
-
\`\`\`
|
|
126
|
-
|
|
127
|
-
Trailing prose.
|
|
128
|
-
`;
|
|
129
|
-
withSource('input.md', source, (path) => {
|
|
130
|
-
const manifest = derivePageManifest(path, 'md');
|
|
131
|
-
assert.deepEqual(parsePageSource(source, manifest), [
|
|
132
|
-
{ kind: 'prose', markdown: 'Leading prose.' },
|
|
133
|
-
{ kind: 'slot', slotIndex: 0 },
|
|
134
|
-
{ kind: 'prose', markdown: 'Interstitial prose.' },
|
|
135
|
-
{ kind: 'slot', slotIndex: 1 },
|
|
136
|
-
{ kind: 'prose', markdown: 'Trailing prose.' },
|
|
137
|
-
]);
|
|
138
|
-
});
|
|
139
|
-
});
|