@north-light/crouter 0.3.233 → 0.3.235
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.js +16 -15
- package/dist/api/dto/nodes.d.ts +11 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/__tests__/integration/provider-rotation.test.ts +1 -1
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/claude-plugin-commands.ts +14 -17
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/crtr-commands/index.ts +7 -11
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/frontmatter-rules/index.ts +8 -8
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/memory-slash-commands.ts +10 -11
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/pi-shell-runner.ts +2 -2
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/provider-rotation.d.ts +3 -3
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/provider-rotation.js +7 -7
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/provider-rotation.ts +8 -8
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/statusline.ts +6 -6
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/sysprompt-window.ts +3 -3
- package/dist/builtin-pi-packages/pi-crtr-extensions/lib/subscription-state.ts +1 -1
- package/dist/clients/attach/chrome/ticket-panel.js +15 -2
- package/dist/clients/attach/overlays/graph.js +0 -3
- package/dist/clients/attach/overlays/mcp.js +1 -2
- package/dist/clients/attach/render/chat-view.js +0 -12
- package/dist/clients/attach/render/crtr-output.js +0 -2
- package/dist/clients/attach/render/tool-calls.js +0 -6
- package/dist/clients/attach/slash/dispatch.js +8 -11
- package/dist/clients/attach/viewer.js +535 -537
- package/dist/clients/conversation/projection.js +0 -2
- package/dist/clients/inbox/controller.js +22 -3
- package/dist/clients/inbox/review/companion-pane.js +0 -4
- package/dist/clients/inbox/review-adapter.js +0 -7
- package/dist/clients/inbox/tui/slots.d.ts +3 -3
- package/dist/clients/inbox/tui/types.d.ts +2 -2
- package/dist/commands/api-client.js +0 -8
- package/dist/commands/attention.js +1 -9
- package/dist/commands/canvas-browse.js +2 -1
- package/dist/commands/canvas-config.js +0 -10
- package/dist/commands/canvas-history.js +2 -1
- package/dist/commands/cron.js +10 -23
- package/dist/commands/dashboard.js +0 -2
- package/dist/commands/human/feedback.js +2 -1
- package/dist/commands/human/prompts.js +2 -5
- package/dist/commands/human/queue.js +2 -7
- package/dist/commands/human/request.js +0 -14
- package/dist/commands/human/review.js +2 -1
- package/dist/commands/human.js +1 -1
- package/dist/commands/memory/__tests__/command-selector-and-mutation-guards.test.js +4 -4
- package/dist/commands/memory/__tests__/memory-list-and-lint-ignore.test.d.ts +1 -0
- package/dist/commands/memory/__tests__/memory-list-and-lint-ignore.test.js +96 -0
- package/dist/commands/memory/delete.js +1 -1
- package/dist/commands/memory/edit.js +6 -3
- package/dist/commands/memory/find.js +2 -2
- package/dist/commands/memory/lint.js +2 -1
- package/dist/commands/memory/list.d.ts +4 -0
- package/dist/commands/memory/list.js +20 -14
- package/dist/commands/memory/move.js +1 -1
- package/dist/commands/memory/read.d.ts +2 -7
- package/dist/commands/memory/read.js +5 -16
- package/dist/commands/memory/shared.d.ts +1 -0
- package/dist/commands/memory/shared.js +17 -8
- package/dist/commands/memory/write.js +8 -4
- package/dist/commands/node/bash.js +2 -1
- package/dist/commands/node/create.js +2 -1
- package/dist/commands/node/inspect.js +11 -10
- package/dist/commands/node/lifecycle.js +5 -13
- package/dist/commands/node/message.js +11 -6
- package/dist/commands/node/subscription.js +7 -2
- package/dist/commands/node/wait.js +9 -4
- package/dist/commands/node-context.js +2 -1
- package/dist/commands/node-lifecycle-revive.js +0 -2
- package/dist/commands/node-worktree.js +2 -1
- package/dist/commands/pkg/browse/actions.d.ts +1 -1
- package/dist/commands/pkg/browse/actions.js +4 -4
- package/dist/commands/pkg/browse/catalog.d.ts +1 -1
- package/dist/commands/pkg/browse/catalog.js +15 -15
- package/dist/commands/pkg/browse/command-view.d.ts +1 -1
- package/dist/commands/pkg/browse/command-view.js +3 -3
- package/dist/commands/pkg/browse/detail-panel.d.ts +1 -1
- package/dist/commands/pkg/browse/detail-panel.js +8 -8
- package/dist/commands/pkg/browse/doc-view.d.ts +1 -1
- package/dist/commands/pkg/browse/doc-view.js +3 -3
- package/dist/commands/pkg/browse/list-panel.d.ts +1 -1
- package/dist/commands/pkg/browse/list-panel.js +3 -3
- package/dist/commands/pkg/browse/model.d.ts +3 -3
- package/dist/commands/pkg/browse/shell.d.ts +2 -2
- package/dist/commands/pkg/browse/shell.js +13 -13
- package/dist/commands/pkg/plugin-manage.js +3 -2
- package/dist/commands/pkg/shared.js +0 -2
- package/dist/commands/pkg.js +1 -0
- package/dist/commands/profile/delete.js +2 -1
- package/dist/commands/profile/rename.js +1 -1
- package/dist/commands/push.js +2 -3
- package/dist/commands/revive.js +0 -2
- package/dist/commands/surface/node/placement.js +2 -1
- package/dist/commands/surface-edit.js +2 -1
- package/dist/commands/sys/config.js +1 -4
- package/dist/commands/sys/daemon.js +0 -10
- package/dist/commands/sys/doctor.js +3 -2
- package/dist/commands/sys/panels/broker-limits-panel.d.ts +2 -2
- package/dist/commands/sys/panels/broker-limits-panel.js +4 -4
- package/dist/commands/sys/panels/dependencies-panel.d.ts +2 -2
- package/dist/commands/sys/panels/dependencies-panel.js +2 -2
- package/dist/commands/sys/panels/keybindings-panel.d.ts +1 -1
- package/dist/commands/sys/panels/keybindings-panel.js +5 -5
- package/dist/commands/sys/panels/models-panel.d.ts +4 -4
- package/dist/commands/sys/panels/models-panel.js +11 -10
- package/dist/commands/sys/panels/plugins-panel.d.ts +2 -2
- package/dist/commands/sys/panels/plugins-panel.js +2 -2
- package/dist/commands/sys/panels/prefs-panel.d.ts +2 -2
- package/dist/commands/sys/panels/prefs-panel.js +4 -4
- package/dist/commands/sys/panels/profiles-panel.d.ts +1 -1
- package/dist/commands/sys/panels/profiles-panel.js +14 -13
- package/dist/commands/sys/panels/provider-panel.d.ts +3 -3
- package/dist/commands/sys/panels/provider-panel.js +8 -8
- package/dist/commands/sys/settings-shell.d.ts +3 -3
- package/dist/commands/sys/settings-shell.js +15 -15
- package/dist/commands/sys/setup-settings-runner.d.ts +3 -3
- package/dist/commands/sys/setup-settings-runner.js +5 -5
- package/dist/commands/sys/setup-wizard.d.ts +4 -4
- package/dist/commands/sys/setup-wizard.js +8 -8
- package/dist/commands/sys/support/prepare.js +4 -4
- package/dist/commands/sys/support/submit.js +2 -2
- package/dist/commands/sys/sync-deps.js +0 -2
- package/dist/commands/sys/sync-project-guidance.js +2 -1
- package/dist/commands/sys/sysprompt.js +2 -1
- package/dist/commands/sys.js +1 -0
- package/dist/core/__tests__/boot.test.js +0 -8
- package/dist/core/__tests__/broker-extension-canvas-db-boundary.test.js +23 -1
- package/dist/core/__tests__/canvas-db-boundary.test.d.ts +1 -0
- package/dist/core/__tests__/canvas-db-boundary.test.js +46 -0
- package/dist/core/__tests__/canvas.test.js +1 -1
- package/dist/core/__tests__/child-death-wake.test.js +0 -2
- package/dist/core/__tests__/cli-help-contract.test.d.ts +1 -0
- package/dist/core/__tests__/cli-help-contract.test.js +150 -0
- package/dist/core/__tests__/cron-armed-context-failure.test.d.ts +1 -0
- package/dist/core/__tests__/cron-armed-context-failure.test.js +145 -0
- package/dist/core/__tests__/daemon-wedge.test.js +0 -4
- package/dist/core/__tests__/dead-node-policy-table.test.js +0 -4
- package/dist/core/__tests__/fixtures/fake-engine.js +4 -14
- package/dist/core/__tests__/fixtures/fake-pi-host.js +5 -5
- package/dist/core/__tests__/focuses.test.js +0 -6
- package/dist/core/__tests__/fs-utils.test.d.ts +1 -0
- package/dist/core/__tests__/fs-utils.test.js +50 -0
- package/dist/core/__tests__/helpers/harness.js +1 -1
- package/dist/core/__tests__/inline-memory-refs.test.js +0 -2
- package/dist/core/__tests__/integration/broker-fork-seam.test.js +0 -2
- package/dist/core/__tests__/integration/broker-sdk-wiring.test.js +3 -16
- package/dist/core/__tests__/integration/command-plugins.test.js +21 -24
- package/dist/core/__tests__/integration/deferred-no-wake.test.js +0 -6
- package/dist/core/__tests__/integration/flagship-lifecycle.test.js +0 -26
- package/dist/core/__tests__/integration/live-mutation-verbs.test.js +0 -4
- package/dist/core/__tests__/integration/live-mutation.test.js +1 -5
- package/dist/core/__tests__/integration/refresh-stall-recycle.test.js +1 -5
- package/dist/core/__tests__/integration/revive.test.js +0 -12
- package/dist/core/__tests__/integration/subscription-delivery.test.js +0 -6
- package/dist/core/__tests__/integration/tmux-surface.test.js +24 -8
- package/dist/core/__tests__/integration/worktree-reap.test.js +0 -6
- package/dist/core/__tests__/lifecycle.test.js +0 -18
- package/dist/core/__tests__/memory-resolver-precedence.test.js +1 -1
- package/dist/core/__tests__/migration.test.js +2 -2
- package/dist/core/__tests__/on-read-dedup-resume.test.js +3 -3
- package/dist/core/__tests__/pid-identity-match.test.js +1 -1
- package/dist/core/__tests__/plugin-kinds.test.js +1 -1
- package/dist/core/__tests__/plugin-page-components.test.js +1 -1
- package/dist/core/__tests__/preview-registry-sync.test.js +42 -1
- package/dist/core/__tests__/respawn-throttle.test.js +0 -4
- package/dist/core/__tests__/revive-all.test.js +5 -5
- package/dist/core/__tests__/revive-parked-fresh.test.js +62 -11
- package/dist/core/__tests__/seam/broker-attach-multiclient.test.js +0 -8
- package/dist/core/__tests__/seam/broker-attach-stream.test.js +0 -8
- package/dist/core/__tests__/seam/broker-crash-teardown.test.js +0 -2
- package/dist/core/__tests__/tmux-driver-boundary.test.d.ts +1 -0
- package/dist/core/__tests__/tmux-driver-boundary.test.js +50 -0
- package/dist/core/__tests__/watchdog-abort-arms-retry.test.js +11 -6
- package/dist/core/activity-signal.d.ts +7 -0
- package/dist/core/activity-signal.js +38 -0
- package/dist/core/auto-update.js +2 -1
- package/dist/core/bootstrap.js +2 -1
- package/dist/core/canvas/__tests__/remote-canvas-source.test.js +1 -1
- package/dist/core/canvas/__tests__/remote-transport.test.js +1 -1
- package/dist/core/canvas/__tests__/render-remote.test.js +1 -1
- package/dist/core/canvas/browse/app.js +3 -2
- package/dist/core/canvas/browse/model.d.ts +1 -1
- package/dist/core/canvas/browse/model.js +0 -4
- package/dist/core/canvas/canvas.d.ts +22 -0
- package/dist/core/canvas/canvas.js +61 -19
- package/dist/core/canvas/crons.js +0 -6
- package/dist/core/canvas/db.d.ts +14 -20
- package/dist/core/canvas/db.js +30 -1332
- package/dist/core/canvas/focuses.js +0 -4
- package/dist/core/canvas/history.js +2 -1
- package/dist/core/canvas/meta-file.js +18 -27
- package/dist/core/canvas/migrations.d.ts +20 -0
- package/dist/core/canvas/migrations.js +1339 -0
- package/dist/core/canvas/nav-model.js +1 -11
- package/dist/core/canvas/nav-render.js +0 -8
- package/dist/core/canvas/remote-canvas-source.d.ts +8 -0
- package/dist/core/canvas/remote-canvas-source.js +9 -3
- package/dist/core/canvas/render-source.js +31 -13
- package/dist/core/command-manifests/manifest.js +0 -10
- package/dist/core/command-manifests/registry.d.ts +2 -0
- package/dist/core/command-manifests/schema.js +0 -4
- package/dist/core/command-plugins/compose.js +4 -2
- package/dist/core/command-plugins/discovery.d.ts +1 -0
- package/dist/core/command-plugins/discovery.js +1 -1
- package/dist/core/command-plugins/transport/http-invoke.js +0 -14
- package/dist/core/command.js +2 -6
- package/dist/core/config.js +2 -1
- package/dist/core/fault-classifier.js +8 -0
- package/dist/core/feed/inbox.d.ts +10 -0
- package/dist/core/feed/inbox.js +18 -12
- package/dist/core/fs-utils.d.ts +9 -2
- package/dist/core/fs-utils.js +7 -15
- package/dist/core/help.js +0 -8
- package/dist/core/host-exports/export.d.ts +2 -2
- package/dist/core/host-exports/export.js +9 -12
- package/dist/core/human/answer.d.ts +2 -2
- package/dist/core/human/convention.d.ts +2 -5
- package/dist/core/human/convention.js +3 -13
- package/dist/core/human/page-schema.d.ts +2 -2
- package/dist/core/human/page-schema.js +2 -2
- package/dist/core/human/root.js +6 -23
- package/dist/core/human/scan.js +24 -7
- package/dist/core/human/tickets.d.ts +4 -1
- package/dist/core/human/tickets.js +20 -6
- package/dist/core/io.js +0 -12
- package/dist/core/memory/identity.js +0 -4
- package/dist/core/memory/inline-ref-inventory.js +3 -2
- package/dist/core/memory/lint.js +3 -6
- package/dist/core/memory-resolver.d.ts +5 -0
- package/dist/core/memory-resolver.js +14 -12
- package/dist/core/pagination.d.ts +2 -0
- package/dist/core/pagination.js +2 -3
- package/dist/core/preview-registry.d.ts +14 -0
- package/dist/core/preview-registry.js +2 -4
- package/dist/core/preview-result-path.js +2 -1
- package/dist/core/profiles/manifest.d.ts +37 -5
- package/dist/core/profiles/manifest.js +44 -24
- package/dist/core/profiles/select.js +0 -4
- package/dist/core/review/signal.js +4 -23
- package/dist/core/review/ticket-filter.d.ts +3 -3
- package/dist/core/review/ticket-filter.js +4 -9
- package/dist/core/runtime/__tests__/engine-route.test.js +1 -1
- package/dist/core/runtime/__tests__/spawn-env.test.js +0 -8
- package/dist/core/runtime/bearings-render.d.ts +56 -0
- package/dist/core/runtime/bearings-render.js +241 -0
- package/dist/core/runtime/bearings.d.ts +2 -17
- package/dist/core/runtime/bearings.js +12 -198
- package/dist/core/runtime/broker/auth-reload.d.ts +39 -0
- package/dist/core/runtime/broker/auth-reload.js +149 -0
- package/dist/core/runtime/broker/engine-drive.d.ts +26 -0
- package/dist/core/runtime/broker/engine-drive.js +257 -0
- package/dist/core/runtime/broker/engine-routing.d.ts +143 -0
- package/dist/core/runtime/broker/engine-routing.js +211 -0
- package/dist/core/runtime/broker/event-projection.js +3 -2
- package/dist/core/runtime/broker/fault-retry.js +11 -4
- package/dist/core/runtime/broker/frame-client.d.ts +38 -0
- package/dist/core/runtime/broker/frame-client.js +129 -0
- package/dist/core/runtime/broker/frame-dispatch.d.ts +12 -189
- package/dist/core/runtime/broker/frame-dispatch.js +89 -1476
- package/dist/core/runtime/broker/frame-engine-control.d.ts +20 -0
- package/dist/core/runtime/broker/frame-engine-control.js +231 -0
- package/dist/core/runtime/broker/frame-memory-refs.d.ts +19 -0
- package/dist/core/runtime/broker/frame-memory-refs.js +41 -0
- package/dist/core/runtime/broker/frame-model-query.d.ts +13 -0
- package/dist/core/runtime/broker/frame-model-query.js +69 -0
- package/dist/core/runtime/broker/frame-read.d.ts +19 -0
- package/dist/core/runtime/broker/frame-read.js +116 -0
- package/dist/core/runtime/broker/frame-replies.d.ts +24 -0
- package/dist/core/runtime/broker/frame-replies.js +64 -0
- package/dist/core/runtime/broker/frame-session.d.ts +34 -0
- package/dist/core/runtime/broker/frame-session.js +203 -0
- package/dist/core/runtime/broker/rebind.js +50 -0
- package/dist/core/runtime/broker/turn-ignition.d.ts +42 -0
- package/dist/core/runtime/broker/turn-ignition.js +74 -0
- package/dist/core/runtime/broker-extension-render.js +9 -85
- package/dist/core/runtime/broker-protocol.js +0 -4
- package/dist/core/runtime/broker-sdk.js +2 -1
- package/dist/core/runtime/broker.d.ts +1 -1
- package/dist/core/runtime/broker.js +5 -23
- package/dist/core/runtime/fault.js +3 -2
- package/dist/core/runtime/headless-pi.js +3 -2
- package/dist/core/runtime/host.js +2 -3
- package/dist/core/runtime/kickoff.js +0 -4
- package/dist/core/runtime/launch-target.js +2 -1
- package/dist/core/runtime/launch.d.ts +2 -2
- package/dist/core/runtime/launch.js +20 -10
- package/dist/core/runtime/lifecycle.js +17 -26
- package/dist/core/runtime/managed-provider-cooling.d.ts +1 -1
- package/dist/core/runtime/naming.js +0 -2
- package/dist/core/runtime/nodes.js +2 -1
- package/dist/core/runtime/persona.js +0 -6
- package/dist/core/runtime/placement-tmux.js +2 -5
- package/dist/core/runtime/placement.js +4 -11
- package/dist/core/runtime/recap.js +4 -3
- package/dist/core/runtime/reopen.js +4 -7
- package/dist/core/runtime/reset.js +0 -4
- package/dist/core/runtime/resume-root.js +0 -2
- package/dist/core/runtime/revive.js +17 -13
- package/dist/core/runtime/session-list-cache.js +2 -2
- package/dist/core/runtime/shell-expansion.js +0 -2
- package/dist/core/runtime/spawn.js +0 -2
- package/dist/core/runtime/stop-guard.js +28 -16
- package/dist/core/runtime/stream-watchdog.d.ts +0 -1
- package/dist/core/runtime/stream-watchdog.js +13 -18
- package/dist/core/runtime/tmux-bindings.js +0 -2
- package/dist/core/runtime/tmux-driver.d.ts +12 -0
- package/dist/core/runtime/tmux-driver.js +18 -13
- package/dist/core/runtime/warm-pool.js +0 -4
- package/dist/core/scope.js +4 -3
- package/dist/core/spawn.js +2 -3
- package/dist/core/subscription-state.js +10 -17
- package/dist/core/substrate/__tests__/surface-match-pre-command.test.js +3 -3
- package/dist/core/substrate/frontmatter-validation.d.ts +1 -0
- package/dist/core/substrate/frontmatter-validation.js +1 -1
- package/dist/core/substrate/render.js +0 -12
- package/dist/core/substrate/schema.js +0 -12
- package/dist/core/substrate/surface-match.js +0 -2
- package/dist/core/termrender/display.js +2 -1
- package/dist/core/tui/page-host.d.ts +3 -3
- package/dist/core/tui/page-host.js +4 -4
- package/dist/core/tui/panel.js +3 -3
- package/dist/core/wake.js +0 -12
- package/dist/core/worktree.js +0 -2
- package/dist/daemon/__tests__/reconciler-signature.test.d.ts +1 -0
- package/dist/daemon/__tests__/reconciler-signature.test.js +36 -0
- package/dist/daemon/api/__tests__/error-seam.test.js +27 -0
- package/dist/daemon/api/__tests__/seam/api-server.test.js +2 -16
- package/dist/daemon/api/__tests__/seam/b10-attach-modelauth.test.js +3 -11
- package/dist/daemon/api/__tests__/seam/leaf-api-parity.test.js +0 -6
- package/dist/daemon/api/handlers/attach.js +2 -1
- package/dist/daemon/api/handlers/broker-ops.js +3 -6
- package/dist/daemon/api/handlers/canvas.js +12 -18
- package/dist/daemon/api/handlers/feedback-comments.js +0 -8
- package/dist/daemon/api/handlers/health.js +3 -2
- package/dist/daemon/api/handlers/human-requests.js +0 -10
- package/dist/daemon/api/handlers/human.js +1 -5
- package/dist/daemon/api/handlers/inbox.js +0 -18
- package/dist/daemon/api/handlers/memory.js +1 -1
- package/dist/daemon/api/handlers/messages.js +16 -20
- package/dist/daemon/api/handlers/nodes.js +23 -22
- package/dist/daemon/api/handlers/reports.js +1 -1
- package/dist/daemon/api/handlers/reviews.js +1 -6
- package/dist/daemon/api/handlers/worktree.js +6 -22
- package/dist/daemon/api/map.d.ts +4 -2
- package/dist/daemon/api/map.js +22 -8
- package/dist/daemon/api/router.js +0 -4
- package/dist/daemon/api/server.js +2 -5
- package/dist/daemon/boot-migrations.d.ts +14 -0
- package/dist/daemon/boot-migrations.js +31 -0
- package/dist/daemon/cron/armed-context.d.ts +21 -0
- package/dist/daemon/cron/armed-context.js +100 -0
- package/dist/daemon/cron/capture.d.ts +20 -0
- package/dist/daemon/cron/capture.js +45 -0
- package/dist/daemon/cron/disposition.d.ts +19 -0
- package/dist/daemon/cron/disposition.js +52 -0
- package/dist/daemon/cron/escalation.d.ts +12 -0
- package/dist/daemon/cron/escalation.js +124 -0
- package/dist/daemon/cron/held.d.ts +4 -0
- package/dist/daemon/cron/held.js +20 -0
- package/dist/daemon/cron/lease-recovery.d.ts +3 -0
- package/dist/daemon/cron/lease-recovery.js +119 -0
- package/dist/daemon/cron/live-runs.d.ts +11 -0
- package/dist/daemon/cron/live-runs.js +23 -0
- package/dist/daemon/cron/passes.d.ts +11 -0
- package/dist/daemon/cron/passes.js +78 -0
- package/dist/daemon/cron/settle.d.ts +28 -0
- package/dist/daemon/cron/settle.js +37 -0
- package/dist/daemon/cron/sinks.d.ts +2 -0
- package/dist/daemon/cron/sinks.js +100 -0
- package/dist/daemon/cron-run.d.ts +5 -41
- package/dist/daemon/cron-run.js +193 -882
- package/dist/daemon/crtrd-cli.js +2 -1
- package/dist/daemon/crtrd.js +22 -22
- package/dist/daemon/fleet.js +0 -4
- package/dist/daemon/human/deliver-action.js +2 -1
- package/dist/daemon/human/sweep.js +15 -4
- package/dist/daemon/manage.d.ts +2 -2
- package/dist/daemon/manage.js +4 -13
- package/dist/daemon/pidfile.js +2 -2
- package/dist/daemon/reconcilers/broker-supervision.js +27 -30
- package/dist/daemon/reconcilers/dormant-inbox.d.ts +1 -1
- package/dist/daemon/reconcilers/dormant-inbox.js +2 -1
- package/dist/daemon/reconcilers/live-obligation.js +2 -2
- package/dist/daemon/reconcilers/pending-review-submit.d.ts +1 -1
- package/dist/daemon/reconcilers/pending-review-submit.js +2 -1
- package/dist/daemon/reconcilers/storage-maintenance.d.ts +2 -1
- package/dist/daemon/reconcilers/storage-maintenance.js +2 -1
- package/dist/daemon/review/companion.js +2 -2
- package/dist/daemon/review/sweep.js +8 -3
- package/dist/migrations/001-surfaces-frontmatter.js +0 -6
- package/dist/migrations/corpus.js +2 -5
- package/dist/migrations/registry.js +15 -2
- package/dist/pi-extensions/__tests__/canvas-stophook-agentend.test.js +0 -4
- package/dist/pi-extensions/broker-local.js +2 -1
- package/dist/pi-extensions/canvas-bash-valve.js +7 -20
- package/dist/pi-extensions/canvas-context-intro.js +2 -7
- package/dist/pi-extensions/canvas-doc-substrate.js +2 -3
- package/dist/pi-extensions/canvas-goal-capture.js +2 -1
- package/dist/pi-extensions/canvas-inbox-watcher.js +5 -12
- package/dist/pi-extensions/canvas-passive-context.js +2 -5
- package/dist/pi-extensions/canvas-preview-result.js +2 -1
- package/dist/pi-extensions/canvas-prompt-scrub.js +2 -1
- package/dist/pi-extensions/canvas-recap.js +3 -16
- package/dist/pi-extensions/canvas-review-boundary.js +2 -5
- package/dist/pi-extensions/canvas-stophook.js +2 -26
- package/dist/pi-extensions/canvas-structured-output.js +2 -1
- package/dist/pi-extensions/canvas-tool-guide.js +2 -1
- package/dist/shared/__tests__/env-boundary.test.d.ts +1 -0
- package/dist/shared/__tests__/env-boundary.test.js +93 -0
- package/dist/shared/env.d.ts +68 -0
- package/dist/shared/env.js +171 -0
- package/package.json +3 -1
- package/runtime.lock.json +2 -2
- package/dist/core/canvas/render.d.ts +0 -47
- package/dist/core/canvas/render.js +0 -245
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { ModelRegistry, ModelRuntime } from
|
|
2
|
-
import { TUI, type Component, type Focusable } from
|
|
3
|
-
import { type BindingId, type BindingResolution } from
|
|
4
|
-
import { type SetupPackageId, type SetupPackageManifestEntry, type SetupSubmission, type SetupSystemInstallPlan } from
|
|
1
|
+
import { ModelRegistry, ModelRuntime } from '@earendil-works/pi-coding-agent';
|
|
2
|
+
import { TUI, type Component, type Focusable } from '@earendil-works/pi-tui';
|
|
3
|
+
import { type BindingId, type BindingResolution } from '../../core/keybindings/index.js';
|
|
4
|
+
import { type SetupPackageId, type SetupPackageManifestEntry, type SetupSubmission, type SetupSystemInstallPlan } from './setup-core.js';
|
|
5
5
|
/** The host owns step navigation, focus, submission and composition. Each step
|
|
6
6
|
* owns its own mutable UI state. */
|
|
7
7
|
export declare class SetupWizard implements Component, Focusable {
|
|
@@ -5,14 +5,14 @@
|
|
|
5
5
|
// broker limits, updates — lives in settings-shell.ts. Both hosts share the
|
|
6
6
|
// panels in ./panels; this one is a linear stepper that ends in a single
|
|
7
7
|
// confirmed install, that one is a persistent editor that saves as you go.
|
|
8
|
-
import { matchesKey, truncateToWidth, visibleWidth, } from
|
|
9
|
-
import { formatGesture, matchesPiTuiInput, } from
|
|
10
|
-
import { buildSetupInitialState, executeSetupSubmission, formatSetupExecutionSummary, packagePlanAction, reconcileInboxFrontDoor, renderSetupStaticInstructions, } from
|
|
11
|
-
import { runSetupSettingsProcess } from
|
|
12
|
-
import { DependenciesPanel } from
|
|
13
|
-
import { PAGE_CHROME_ROWS, pageFrame, theme, } from
|
|
14
|
-
import { PluginsPanel } from
|
|
15
|
-
import { ProviderPanel } from
|
|
8
|
+
import { matchesKey, truncateToWidth, visibleWidth, } from '@earendil-works/pi-tui';
|
|
9
|
+
import { formatGesture, matchesPiTuiInput, } from '../../core/keybindings/index.js';
|
|
10
|
+
import { buildSetupInitialState, executeSetupSubmission, formatSetupExecutionSummary, packagePlanAction, reconcileInboxFrontDoor, renderSetupStaticInstructions, } from './setup-core.js';
|
|
11
|
+
import { runSetupSettingsProcess } from './setup-settings-runner.js';
|
|
12
|
+
import { DependenciesPanel } from './panels/dependencies-panel.js';
|
|
13
|
+
import { PAGE_CHROME_ROWS, pageFrame, theme, } from '../../core/tui/panel.js';
|
|
14
|
+
import { PluginsPanel } from './panels/plugins-panel.js';
|
|
15
|
+
import { ProviderPanel } from './panels/provider-panel.js';
|
|
16
16
|
const STEPS = ["Providers", "Plugins", "Dependencies"];
|
|
17
17
|
/** The host owns step navigation, focus, submission and composition. Each step
|
|
18
18
|
* owns its own mutable UI state. */
|
|
@@ -16,7 +16,7 @@ const BUNDLE_NAME = 'bundle.jsonl';
|
|
|
16
16
|
const CAP_BYTES = 2_097_152;
|
|
17
17
|
const MAX_RUNTIME_STATE_BYTES = MAX_EVENT_RECORD_BYTES - 1;
|
|
18
18
|
const CATEGORIES = ['events', 'runtime_state', 'raw_residue'];
|
|
19
|
-
//
|
|
19
|
+
// reference generation
|
|
20
20
|
function pad(value, length) {
|
|
21
21
|
return String(value).padStart(length, '0');
|
|
22
22
|
}
|
|
@@ -26,7 +26,7 @@ function generateReference(now) {
|
|
|
26
26
|
const time = `${pad(now.getUTCHours(), 2)}${pad(now.getUTCMinutes(), 2)}${pad(now.getUTCSeconds(), 2)}${pad(now.getUTCMilliseconds(), 3)}`;
|
|
27
27
|
return `${date}T${time}Z-${randomBytes(4).toString('hex')}`;
|
|
28
28
|
}
|
|
29
|
-
//
|
|
29
|
+
// compact bundle records
|
|
30
30
|
function compactLine(record) {
|
|
31
31
|
return Buffer.from(`${JSON.stringify(record)}\n`, 'utf8');
|
|
32
32
|
}
|
|
@@ -123,7 +123,7 @@ function collectRuntimeState(nodeId, ts, sources) {
|
|
|
123
123
|
}
|
|
124
124
|
return records;
|
|
125
125
|
}
|
|
126
|
-
//
|
|
126
|
+
// manifest + cap fitting
|
|
127
127
|
function renderManifestBuffer(base, sources) {
|
|
128
128
|
const manifest = {
|
|
129
129
|
reference: base.reference,
|
|
@@ -157,7 +157,7 @@ function fitToCap(records, sources, base) {
|
|
|
157
157
|
state.truncated = true;
|
|
158
158
|
}
|
|
159
159
|
}
|
|
160
|
-
//
|
|
160
|
+
// core
|
|
161
161
|
/** Output-free: collects and writes the prepared artifact and returns a
|
|
162
162
|
* small result. Never prints; the leaf below owns argv/rendering. */
|
|
163
163
|
export function prepareSupportBundle(nodeId) {
|
|
@@ -43,7 +43,7 @@ export function defaultGhRunner(args, input) {
|
|
|
43
43
|
const included = args.includes('--include') ? splitIncludedResponse(raw) : { body: raw, httpStatus: undefined };
|
|
44
44
|
return { code: result.status, stdout: included.body, stderr, httpStatus: included.httpStatus };
|
|
45
45
|
}
|
|
46
|
-
//
|
|
46
|
+
// symlink-free, TOCTOU-safe filesystem access
|
|
47
47
|
// `~/.crouter/support/<reference>/` is user-writable, so every open here rejects symlinks
|
|
48
48
|
// and confirms the thing opened is the thing later read/uploaded — never an arbitrary path.
|
|
49
49
|
function closeSafe(fd) {
|
|
@@ -194,7 +194,7 @@ function openReference(reference) {
|
|
|
194
194
|
throw error;
|
|
195
195
|
}
|
|
196
196
|
}
|
|
197
|
-
//
|
|
197
|
+
// manifest and bundle validation
|
|
198
198
|
// An ordinary manifest: every field is either fixed input (the reference) or independently
|
|
199
199
|
// checked against its own declared shape. Nothing here is self-referential — the cap is
|
|
200
200
|
// enforced by directly measuring the two files, not by trusting a declared byte count.
|
|
@@ -449,9 +449,7 @@ export function runDepsSync(opts) {
|
|
|
449
449
|
writeText(join(depsDir, '.gitignore'), '*\n');
|
|
450
450
|
return rows;
|
|
451
451
|
}
|
|
452
|
-
// ---------------------------------------------------------------------------
|
|
453
452
|
// The `sys sync deps` leaf.
|
|
454
|
-
// ---------------------------------------------------------------------------
|
|
455
453
|
function renderDepsSummary(rows) {
|
|
456
454
|
const imported = rows.filter((r) => r.status === 'imported').length;
|
|
457
455
|
const wouldImport = rows.filter((r) => r.status === 'would-import').length;
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// CLAUDE.md/AGENTS.md/.claude/rules into repository-root and nested
|
|
3
3
|
// `.crouter/memory/` stores. Only the repository root owns the namespace and
|
|
4
4
|
// workspace front door; nested stores derive their prefix from their path.
|
|
5
|
+
import { envProfileId } from '../../shared/env.js';
|
|
5
6
|
import { existsSync, readdirSync, statSync } from 'node:fs';
|
|
6
7
|
import { basename, join, resolve } from 'node:path';
|
|
7
8
|
import { defineLeaf } from '../../core/command.js';
|
|
@@ -33,7 +34,7 @@ function projectDocRoots(sourceArg) {
|
|
|
33
34
|
if (sourceArg !== undefined)
|
|
34
35
|
return [resolve(sourceArg)];
|
|
35
36
|
const roots = [resolve(process.cwd())];
|
|
36
|
-
const profileId =
|
|
37
|
+
const profileId = envProfileId();
|
|
37
38
|
if (profileId !== undefined && profileId !== '') {
|
|
38
39
|
try {
|
|
39
40
|
const { manifest } = loadProfileManifest(profileId);
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { envNodeId } from '../../shared/env.js';
|
|
1
2
|
import { existsSync, readFileSync, statSync, writeFileSync } from 'node:fs';
|
|
2
3
|
import { tmpdir } from 'node:os';
|
|
3
4
|
import { join } from 'node:path';
|
|
@@ -34,7 +35,7 @@ export const sysSyspromptLeaf = defineLeaf({
|
|
|
34
35
|
preview: { suppressOutput: true },
|
|
35
36
|
},
|
|
36
37
|
run: async (input) => {
|
|
37
|
-
const nodeId = input['node']?.trim() ||
|
|
38
|
+
const nodeId = input['node']?.trim() || envNodeId();
|
|
38
39
|
if (nodeId === undefined || nodeId === '') {
|
|
39
40
|
throw new InputError({ error: 'empty_node', message: 'a node id is required', field: 'node', next: 'Pass a node id or run from inside a node.' });
|
|
40
41
|
}
|
package/dist/commands/sys.js
CHANGED
|
@@ -24,6 +24,7 @@ export function registerSys() {
|
|
|
24
24
|
help: {
|
|
25
25
|
name: 'sys',
|
|
26
26
|
summary: 'crtr system configuration, settings, diagnostics, and self-management',
|
|
27
|
+
model: 'Configuration and settings own durable behavior; diagnostics and support observe it. Setup, migration, sync, and update change installed or generated state, while daemon control owns the supervisor lifecycle.',
|
|
27
28
|
},
|
|
28
29
|
children: [sysSettingsLeaf, configBranch, sysSetupLeaf, sysDoctorLeaf, sysSyspromptLeaf, promptReviewLeaf, sysFeedbackLeaf, sysLogsLeaf, supportBranch, sysUpdateLeaf, sysVersionLeaf, sysMigrateLeaf, sysSyncBranch, daemonBranch],
|
|
29
30
|
});
|
|
@@ -59,9 +59,7 @@ after(() => {
|
|
|
59
59
|
rmSync(home, { recursive: true, force: true });
|
|
60
60
|
delete process.env['CRTR_HOME'];
|
|
61
61
|
});
|
|
62
|
-
// ---------------------------------------------------------------------------
|
|
63
62
|
// bootChanged — pure core
|
|
64
|
-
// ---------------------------------------------------------------------------
|
|
65
63
|
test('bootChanged: no persisted history is never a change (never clears a genuinely live pid)', () => {
|
|
66
64
|
assert.equal(bootChanged(ident('boot-A', 1_000_000), null), false);
|
|
67
65
|
});
|
|
@@ -84,12 +82,8 @@ test('bootChanged: wall-clock fallback (no boot_id) — epochs within tolerance
|
|
|
84
82
|
test('bootChanged: wall-clock fallback (no boot_id) — epochs far apart are a DIFFERENT boot', () => {
|
|
85
83
|
assert.equal(bootChanged(ident(null, 1_000_000 + 10 * 60_000), ident(null, 1_000_000)), true, '10min apart — different boot');
|
|
86
84
|
});
|
|
87
|
-
// ---------------------------------------------------------------------------
|
|
88
85
|
// reconcileBootLiveness — the impure sweep against a real canvas.db
|
|
89
|
-
// ---------------------------------------------------------------------------
|
|
90
|
-
// ---------------------------------------------------------------------------
|
|
91
86
|
// migrateLegacyPidIdentities — one-time in-place format migration at startup
|
|
92
|
-
// ---------------------------------------------------------------------------
|
|
93
87
|
test('migrateLegacyPidIdentities: a live legacy lstart#ticks broker is re-recorded to <bootId>#<ticks>', () => {
|
|
94
88
|
createNode(node('n1'));
|
|
95
89
|
setPidIdentity('n1', 111, 'Sat Jul 11 17:51:45 2026#53459'); // legacy baseline
|
|
@@ -193,7 +187,6 @@ test('reconcileBootLiveness: after reconciling, the SAME boot no longer re-trigg
|
|
|
193
187
|
assert.deepEqual(result, { changed: false, cleared: [], sameBootProven: true });
|
|
194
188
|
assert.equal(getRow('n1')?.pi_pid, 999, 'a pid recorded AFTER reconciliation, within the same boot, is never touched');
|
|
195
189
|
});
|
|
196
|
-
// ---------------------------------------------------------------------------
|
|
197
190
|
// bootProvenSame + the daemon migration gate (Major-1 regression)
|
|
198
191
|
//
|
|
199
192
|
// The legacy identity migration blesses a recorded pid as a current-boot
|
|
@@ -202,7 +195,6 @@ test('reconcileBootLiveness: after reconciling, the SAME boot no longer re-trigg
|
|
|
202
195
|
// NOT that proof — it is also true with no/malformed marker or the wall-clock
|
|
203
196
|
// epoch fallback, where an unrelated cross-boot process can collide on
|
|
204
197
|
// pid+ticks and be permanently baptized.
|
|
205
|
-
// ---------------------------------------------------------------------------
|
|
206
198
|
test('bootProvenSame: exact equal boot_id on both sides is proof', () => {
|
|
207
199
|
assert.equal(bootProvenSame(ident('boot-A', 2_000), ident('boot-A', 1_000)), true);
|
|
208
200
|
});
|
|
@@ -3,12 +3,33 @@
|
|
|
3
3
|
// and rejects any reach to the single DB accessor module.
|
|
4
4
|
import { test } from 'node:test';
|
|
5
5
|
import assert from 'node:assert/strict';
|
|
6
|
-
import { existsSync, readFileSync } from 'node:fs';
|
|
6
|
+
import { existsSync, readdirSync, readFileSync } from 'node:fs';
|
|
7
7
|
import { dirname, join, normalize, resolve } from 'node:path';
|
|
8
8
|
import ts from 'typescript';
|
|
9
9
|
import { CANVAS_EXTENSION_NAMES } from '../canvas/extensions.js';
|
|
10
10
|
const SOURCE_ROOT = resolve(process.cwd(), 'src');
|
|
11
11
|
const DB_MODULE = normalize(join(SOURCE_ROOT, 'core/canvas/db.ts'));
|
|
12
|
+
// The builtin pi-crtr-extensions package loads into the SAME broker process
|
|
13
|
+
// as the shipped pi-extensions/ roots below (its own package.json declares
|
|
14
|
+
// `pi.extensions: ["extensions/*.ts", "extensions/*/index.ts"]`), so it must
|
|
15
|
+
// clear the identical canvas.db-free bar. Read the glob's two shapes directly
|
|
16
|
+
// off disk rather than hardcoding a name list, so a future extension added to
|
|
17
|
+
// the package is covered automatically.
|
|
18
|
+
function builtinPiCrtrExtensionRoots() {
|
|
19
|
+
const extDir = join(SOURCE_ROOT, 'builtin-pi-packages/pi-crtr-extensions/extensions');
|
|
20
|
+
const roots = [];
|
|
21
|
+
for (const entry of readdirSync(extDir, { withFileTypes: true })) {
|
|
22
|
+
if (entry.isFile() && entry.name.endsWith('.ts') && !entry.name.includes('.test.')) {
|
|
23
|
+
roots.push(join(extDir, entry.name));
|
|
24
|
+
}
|
|
25
|
+
else if (entry.isDirectory()) {
|
|
26
|
+
const indexFile = join(extDir, entry.name, 'index.ts');
|
|
27
|
+
if (existsSync(indexFile))
|
|
28
|
+
roots.push(indexFile);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return roots;
|
|
32
|
+
}
|
|
12
33
|
function resolveLocalImport(from, specifier) {
|
|
13
34
|
if (!specifier.startsWith('.'))
|
|
14
35
|
return null;
|
|
@@ -45,6 +66,7 @@ test('every shipped broker extension and the prospective preflight are transitiv
|
|
|
45
66
|
const roots = [
|
|
46
67
|
...CANVAS_EXTENSION_NAMES.map((name) => join(SOURCE_ROOT, 'pi-extensions', `${name}.ts`)),
|
|
47
68
|
join(SOURCE_ROOT, 'core/runtime/prospective-inventory-cli.ts'),
|
|
69
|
+
...builtinPiCrtrExtensionRoots(),
|
|
48
70
|
];
|
|
49
71
|
for (const root of roots) {
|
|
50
72
|
const reach = dbReach(root);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
// canvas.ts declares itself "the one place that reads/writes the node+edge
|
|
2
|
+
// model" for the runtime layer (spawn, push, lifecycle, daemon) that builds on
|
|
3
|
+
// it — every node/edge write from `runtime/` goes through a named canvas.ts
|
|
4
|
+
// accessor, never a raw SQL statement of its own. This grep-enforces that
|
|
5
|
+
// boundary at the one layer the convention actually claims: `core/runtime/`.
|
|
6
|
+
// (Other canvas.db tables — reviews, review_comments — are a SEPARATE
|
|
7
|
+
// subsystem's tables with their own owner in `core/review/`, and `feed.ts`'s
|
|
8
|
+
// `commitCanonicalFinal` is the one documented sanctioned exception outside
|
|
9
|
+
// `runtime/` entirely — see `core/canvas/.crouter/memory/INDEX.md` — so
|
|
10
|
+
// neither is in scope here.)
|
|
11
|
+
import { test } from 'node:test';
|
|
12
|
+
import assert from 'node:assert/strict';
|
|
13
|
+
import { readdirSync, readFileSync, statSync } from 'node:fs';
|
|
14
|
+
import { join, resolve } from 'node:path';
|
|
15
|
+
const RUNTIME_ROOT = resolve(process.cwd(), 'src/core/runtime');
|
|
16
|
+
function tsFilesUnder(dir) {
|
|
17
|
+
const out = [];
|
|
18
|
+
for (const entry of readdirSync(dir)) {
|
|
19
|
+
const path = join(dir, entry);
|
|
20
|
+
const stat = statSync(path);
|
|
21
|
+
if (stat.isDirectory()) {
|
|
22
|
+
if (entry === '__tests__')
|
|
23
|
+
continue;
|
|
24
|
+
out.push(...tsFilesUnder(path));
|
|
25
|
+
}
|
|
26
|
+
else if (entry.endsWith('.ts') && !entry.endsWith('.test.ts')) {
|
|
27
|
+
out.push(path);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return out;
|
|
31
|
+
}
|
|
32
|
+
test('no file under core/runtime/ prepares a raw SQL statement — canvas.ts is the one node+edge DAO', () => {
|
|
33
|
+
const offenders = [];
|
|
34
|
+
for (const file of tsFilesUnder(RUNTIME_ROOT)) {
|
|
35
|
+
const source = readFileSync(file, 'utf8');
|
|
36
|
+
const lines = source.split('\n');
|
|
37
|
+
lines.forEach((line, index) => {
|
|
38
|
+
// Skip comment lines referencing `.prepare(` in prose (backtick-quoted).
|
|
39
|
+
if (/`\.prepare\(`/.test(line))
|
|
40
|
+
return;
|
|
41
|
+
if (line.includes('.prepare('))
|
|
42
|
+
offenders.push(`${file}:${index + 1}`);
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
assert.deepEqual(offenders, [], `raw .prepare( call(s) outside core/canvas/:\n${offenders.join('\n')}`);
|
|
46
|
+
});
|
|
@@ -118,7 +118,7 @@ test('recordSpawn writes the audit-only spawned_by edge', () => {
|
|
|
118
118
|
assert.equal(subscriptionsOf('child').length, 0);
|
|
119
119
|
assert.equal(subscribersOf('parent').length, 0);
|
|
120
120
|
});
|
|
121
|
-
//
|
|
121
|
+
// Durability contract: meta.json is durable and never truncated
|
|
122
122
|
// writeMeta uses atomic rename with fsync on both the temp file and parent
|
|
123
123
|
// directory to ensure atomicity and durability. readMeta/rebuildIndex tolerate
|
|
124
124
|
// an empty/corrupt meta.json as MISSING (returning null) rather than throwing,
|
|
@@ -110,7 +110,6 @@ after(() => {
|
|
|
110
110
|
delete process.env['CRTR_HOME'];
|
|
111
111
|
resetFleetForTests();
|
|
112
112
|
});
|
|
113
|
-
// ===========================================================================
|
|
114
113
|
// BROKER CUT — exit-event authority REPLACES daemon pane-gone reaping. A booted
|
|
115
114
|
// child whose engine pid is dead (and whose viewer pane is gone) is REVIVABLE,
|
|
116
115
|
// not reaped, and the daemon's tick fans NO wake to the dormant parent. This
|
|
@@ -120,7 +119,6 @@ after(() => {
|
|
|
120
119
|
// node recovery rides real exit events), the daemon treats them all identically
|
|
121
120
|
// — no reap, no liveness wake — so the old CRUX (no spurious wake on healthy
|
|
122
121
|
// dormancy) holds by construction.
|
|
123
|
-
// ===========================================================================
|
|
124
122
|
test('exit-event authority: a dead-engine booted child is revivable, NOT reaped, and the tick fans NO wake to its dormant parent', async () => {
|
|
125
123
|
inboxWaitingParent('PARENT');
|
|
126
124
|
deadEngineChild('CHILD');
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
// Help-contract gate for the whole command tree.
|
|
2
|
+
// Run with: node --import tsx/esm --test src/core/__tests__/cli-help-contract.test.ts
|
|
3
|
+
//
|
|
4
|
+
// The cli-design rules that hold for every node are enforced here rather than
|
|
5
|
+
// per command, because help text is written one command at a time and drifts
|
|
6
|
+
// the same way every time: a branch ships its selection prose in the
|
|
7
|
+
// description attribute, a rubric restates the description it sits beside, or
|
|
8
|
+
// a constraint copied from a sibling leaf keeps naming a value this leaf's
|
|
9
|
+
// enum rejects. Each rule below is mechanical — a judgment call about whether
|
|
10
|
+
// a rubric actually helps an agent choose belongs to review, not to this file.
|
|
11
|
+
import { test, before, after } from 'node:test';
|
|
12
|
+
import assert from 'node:assert/strict';
|
|
13
|
+
import { mkdtempSync, rmSync } from 'node:fs';
|
|
14
|
+
import { tmpdir } from 'node:os';
|
|
15
|
+
import { join } from 'node:path';
|
|
16
|
+
import { buildRoot } from '../../build-root.js';
|
|
17
|
+
import { resetScopeCache } from '../scope.js';
|
|
18
|
+
// buildRoot() composes external command-plugin branches from the effective
|
|
19
|
+
// enabled plugins. Pin HOME to an isolated no-plugin home: a third party's
|
|
20
|
+
// help text is not this repo's contract to enforce.
|
|
21
|
+
let home;
|
|
22
|
+
let realHome;
|
|
23
|
+
let realProfile;
|
|
24
|
+
before(() => {
|
|
25
|
+
realHome = process.env['HOME'];
|
|
26
|
+
realProfile = process.env['CRTR_PROFILE_ID'];
|
|
27
|
+
home = mkdtempSync(join(tmpdir(), 'crtr-help-contract-'));
|
|
28
|
+
delete process.env['CRTR_PROFILE_ID'];
|
|
29
|
+
process.env['HOME'] = home;
|
|
30
|
+
resetScopeCache();
|
|
31
|
+
});
|
|
32
|
+
after(() => {
|
|
33
|
+
if (realHome !== undefined)
|
|
34
|
+
process.env['HOME'] = realHome;
|
|
35
|
+
else
|
|
36
|
+
delete process.env['HOME'];
|
|
37
|
+
if (realProfile !== undefined)
|
|
38
|
+
process.env['CRTR_PROFILE_ID'] = realProfile;
|
|
39
|
+
rmSync(home, { recursive: true, force: true });
|
|
40
|
+
resetScopeCache();
|
|
41
|
+
});
|
|
42
|
+
async function everyNode() {
|
|
43
|
+
const root = await buildRoot();
|
|
44
|
+
const out = [];
|
|
45
|
+
const visit = (node, path) => {
|
|
46
|
+
out.push({ node, path });
|
|
47
|
+
if (node.kind === 'branch')
|
|
48
|
+
for (const child of node.children)
|
|
49
|
+
visit(child, `${path} ${child.name}`);
|
|
50
|
+
};
|
|
51
|
+
for (const subtree of root.subtrees)
|
|
52
|
+
visit(subtree, subtree.name);
|
|
53
|
+
return out;
|
|
54
|
+
}
|
|
55
|
+
function normalize(text) {
|
|
56
|
+
if (text === undefined)
|
|
57
|
+
return '';
|
|
58
|
+
return text.toLowerCase().replace(/\s+/g, ' ').replace(/[.\s]+$/, '').trim();
|
|
59
|
+
}
|
|
60
|
+
function enumFlags(leaf) {
|
|
61
|
+
return (leaf.help.params ?? []).filter((param) => param.kind === 'flag' && param.type === 'enum' && param.choices !== undefined);
|
|
62
|
+
}
|
|
63
|
+
test('every branch offering a choice carries the model prose that frames it', async () => {
|
|
64
|
+
const offenders = [];
|
|
65
|
+
for (const { node, path } of await everyNode()) {
|
|
66
|
+
if (node.kind !== 'branch')
|
|
67
|
+
continue;
|
|
68
|
+
const choices = node.children.filter((child) => child.tier !== 'hidden');
|
|
69
|
+
if (choices.length < 2)
|
|
70
|
+
continue;
|
|
71
|
+
const model = node.help.model;
|
|
72
|
+
if (model === undefined || model.trim() === '')
|
|
73
|
+
offenders.push(`${path} (${choices.length} children)`);
|
|
74
|
+
}
|
|
75
|
+
assert.deepEqual(offenders, [], `branches with 2+ visible children and no help.model:\n ${offenders.join('\n ')}`);
|
|
76
|
+
});
|
|
77
|
+
test('no rubric restates the description it is read beside', async () => {
|
|
78
|
+
const offenders = [];
|
|
79
|
+
for (const { node, path } of await everyNode()) {
|
|
80
|
+
const description = normalize(node.description ?? '');
|
|
81
|
+
const rubric = normalize(node.whenToUse ?? '');
|
|
82
|
+
if (description === '' || rubric === '')
|
|
83
|
+
continue;
|
|
84
|
+
if (rubric === description || rubric.includes(description))
|
|
85
|
+
offenders.push(`${path}: whenToUse repeats "${node.description}"`);
|
|
86
|
+
}
|
|
87
|
+
assert.deepEqual(offenders, [], `rubrics restating their own description:\n ${offenders.join('\n ')}`);
|
|
88
|
+
});
|
|
89
|
+
// A constraint may name a value its own enum rejects only while saying so —
|
|
90
|
+
// the copy-paste failure this catches states the foreign value as if it were
|
|
91
|
+
// selectable. Vocabulary is per flag NAME, so it only fires where one flag
|
|
92
|
+
// genuinely carries different choices on different leaves.
|
|
93
|
+
const NEGATION = /\b(?:reject\w*|refus\w*|invalid|unsupported|forbid\w*|not\s+accepted)\b/i;
|
|
94
|
+
test('no enum constraint names a value that enum does not accept', async () => {
|
|
95
|
+
const offenders = [];
|
|
96
|
+
for (const { node, path } of await everyNode()) {
|
|
97
|
+
if (node.kind !== 'branch')
|
|
98
|
+
continue;
|
|
99
|
+
const leaves = node.children.filter((child) => child.kind === 'leaf');
|
|
100
|
+
// Vocabulary is pooled across ONE branch's leaves, because that is where a
|
|
101
|
+
// constraint gets copied: two sibling verbs carrying the same flag with
|
|
102
|
+
// different choices. Pooling tree-wide would compare unrelated commands
|
|
103
|
+
// that merely share a flag name and flag ordinary English words that
|
|
104
|
+
// happen to be some distant enum's value.
|
|
105
|
+
const vocabulary = new Map();
|
|
106
|
+
for (const leaf of leaves) {
|
|
107
|
+
for (const flag of enumFlags(leaf)) {
|
|
108
|
+
const known = vocabulary.get(flag.name) ?? new Set();
|
|
109
|
+
for (const choice of flag.choices)
|
|
110
|
+
known.add(choice.toLowerCase());
|
|
111
|
+
vocabulary.set(flag.name, known);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
for (const leaf of leaves) {
|
|
115
|
+
for (const flag of enumFlags(leaf)) {
|
|
116
|
+
const own = new Set(flag.choices.map((choice) => choice.toLowerCase()));
|
|
117
|
+
const foreign = [...(vocabulary.get(flag.name) ?? [])].filter((value) => !own.has(value));
|
|
118
|
+
if (foreign.length === 0)
|
|
119
|
+
continue;
|
|
120
|
+
for (const sentence of flag.constraint.split(/(?<=[.;:])\s+/)) {
|
|
121
|
+
if (NEGATION.test(sentence))
|
|
122
|
+
continue;
|
|
123
|
+
const named = foreign.filter((value) => new RegExp(`\\b${value}\\b`, 'i').test(sentence));
|
|
124
|
+
if (named.length > 0)
|
|
125
|
+
offenders.push(`${path} ${leaf.name} --${flag.name}: names ${named.join(', ')} — accepts only ${flag.choices.join(', ')}`);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
assert.deepEqual(offenders, [], `enum constraints naming values they reject:\n ${offenders.join('\n ')}`);
|
|
131
|
+
});
|
|
132
|
+
test('every leaf declares its effects, and none contradicts a claim of none', async () => {
|
|
133
|
+
const offenders = [];
|
|
134
|
+
for (const { node, path } of await everyNode()) {
|
|
135
|
+
if (node.kind !== 'leaf')
|
|
136
|
+
continue;
|
|
137
|
+
const effects = node.help.effects;
|
|
138
|
+
if (effects.length === 0 || effects.some((effect) => effect.trim() === '')) {
|
|
139
|
+
offenders.push(`${path}: declares no effects`);
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
// A leaf may describe what read-only means for it — several usefully do.
|
|
143
|
+
// What it may not do is open with "None" and then list effects underneath:
|
|
144
|
+
// an agent reading the first entry stops there.
|
|
145
|
+
if (effects.length > 1 && /^none\b/i.test(effects[0].trim())) {
|
|
146
|
+
offenders.push(`${path}: claims "${effects[0]}" and then lists ${effects.length - 1} more effect(s)`);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
assert.deepEqual(offenders, [], `leaves with a broken effects declaration:\n ${offenders.join('\n ')}`);
|
|
150
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
// Run with: npm test -- --test-name-pattern='armed'
|
|
2
|
+
//
|
|
3
|
+
// A cron run executes under the context it was ARMED under: the `--env`
|
|
4
|
+
// snapshot and the profile stored on the row. When that stored context cannot
|
|
5
|
+
// be rebuilt, the run must FAIL LOUDLY rather than execute the command under a
|
|
6
|
+
// context nobody armed — the class of defect that otherwise runs wrong, every
|
|
7
|
+
// tick, forever, with a clean run log.
|
|
8
|
+
import { after, afterEach, before, beforeEach, test } from 'node:test';
|
|
9
|
+
import assert from 'node:assert/strict';
|
|
10
|
+
import { existsSync, mkdtempSync, mkdirSync, readFileSync, realpathSync, rmSync, writeFileSync } from 'node:fs';
|
|
11
|
+
import { tmpdir } from 'node:os';
|
|
12
|
+
import { join } from 'node:path';
|
|
13
|
+
import { listNodes } from '../canvas/canvas.js';
|
|
14
|
+
import { armCron, getCron, listCronRuns } from '../canvas/crons.js';
|
|
15
|
+
import { closeDb } from '../canvas/db.js';
|
|
16
|
+
import { createProfile, profileManifestPath, profileRoot } from '../profiles/manifest.js';
|
|
17
|
+
import { resetScopeCache } from '../scope.js';
|
|
18
|
+
import { headlessBrokerHost } from '../runtime/host.js';
|
|
19
|
+
import { resetFleetForTests } from '../runtime/fleet.js';
|
|
20
|
+
import { executeCron } from '../../daemon/cron-run.js';
|
|
21
|
+
import { bindTestFleet } from './helpers/fleet.js';
|
|
22
|
+
let userHome;
|
|
23
|
+
let canvasHome;
|
|
24
|
+
let priorHome;
|
|
25
|
+
let priorCanvasHome;
|
|
26
|
+
let fleet;
|
|
27
|
+
let originalLaunch;
|
|
28
|
+
/** The marker a run writes if the command actually executed. */
|
|
29
|
+
function marker(id) {
|
|
30
|
+
return join(canvasHome, `${id}.ran`);
|
|
31
|
+
}
|
|
32
|
+
function arm({ cron_id, ...overrides }) {
|
|
33
|
+
armCron({
|
|
34
|
+
cron_id,
|
|
35
|
+
name: cron_id,
|
|
36
|
+
created_by: null,
|
|
37
|
+
// Records the profile the command actually ran under, so a root fallback is
|
|
38
|
+
// observable rather than inferred.
|
|
39
|
+
command: `printf "%s" "$CRTR_PROFILE_ID" > ${marker(cron_id)}`,
|
|
40
|
+
fire_at: new Date(Date.now() - 1_000).toISOString(),
|
|
41
|
+
recur: null, // a one-shot: retention is observable
|
|
42
|
+
tz: null,
|
|
43
|
+
expires_at: null,
|
|
44
|
+
anchor_node: null,
|
|
45
|
+
cancel_on_wake: false,
|
|
46
|
+
cwd: canvasHome,
|
|
47
|
+
env_json: null,
|
|
48
|
+
profile: null,
|
|
49
|
+
scope: 'profile',
|
|
50
|
+
run_timeout_s: 30,
|
|
51
|
+
overlap: 'skip',
|
|
52
|
+
on_output: 'on-failure',
|
|
53
|
+
sink: '{}',
|
|
54
|
+
tier: 'normal',
|
|
55
|
+
...overrides,
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
before(() => {
|
|
59
|
+
// realpath: the profile store persists realpath-resolved paths, and macOS
|
|
60
|
+
// hands back a symlinked /var/folders prefix.
|
|
61
|
+
userHome = realpathSync(mkdtempSync(join(tmpdir(), 'crtr-cron-armed-user-')));
|
|
62
|
+
canvasHome = realpathSync(mkdtempSync(join(tmpdir(), 'crtr-cron-armed-canvas-')));
|
|
63
|
+
priorHome = process.env['HOME'];
|
|
64
|
+
priorCanvasHome = process.env['CRTR_HOME'];
|
|
65
|
+
process.env['HOME'] = userHome; // the profile store lives under the USER root
|
|
66
|
+
process.env['CRTR_HOME'] = canvasHome;
|
|
67
|
+
resetScopeCache();
|
|
68
|
+
});
|
|
69
|
+
beforeEach(() => {
|
|
70
|
+
closeDb();
|
|
71
|
+
rmSync(canvasHome, { recursive: true, force: true });
|
|
72
|
+
mkdirSync(canvasHome, { recursive: true });
|
|
73
|
+
resetFleetForTests();
|
|
74
|
+
fleet = bindTestFleet();
|
|
75
|
+
originalLaunch = headlessBrokerHost.launch;
|
|
76
|
+
headlessBrokerHost.launch = ((nodeId) => {
|
|
77
|
+
const handle = { pid: 0x7ffffffe, exited: new Promise(() => { }) };
|
|
78
|
+
fleet.fleet.register(nodeId, handle);
|
|
79
|
+
return handle;
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
afterEach(() => {
|
|
83
|
+
headlessBrokerHost.launch = originalLaunch;
|
|
84
|
+
closeDb();
|
|
85
|
+
resetFleetForTests();
|
|
86
|
+
});
|
|
87
|
+
after(() => {
|
|
88
|
+
rmSync(canvasHome, { recursive: true, force: true });
|
|
89
|
+
rmSync(userHome, { recursive: true, force: true });
|
|
90
|
+
if (priorHome === undefined)
|
|
91
|
+
delete process.env['HOME'];
|
|
92
|
+
else
|
|
93
|
+
process.env['HOME'] = priorHome;
|
|
94
|
+
if (priorCanvasHome === undefined)
|
|
95
|
+
delete process.env['CRTR_HOME'];
|
|
96
|
+
else
|
|
97
|
+
process.env['CRTR_HOME'] = priorCanvasHome;
|
|
98
|
+
resetScopeCache();
|
|
99
|
+
});
|
|
100
|
+
/** Every way an armed context fails settles identically: no child, a record
|
|
101
|
+
* naming the cause, a paused row that keeps its one-shot, and one escalation
|
|
102
|
+
* node. */
|
|
103
|
+
function assertSettledAsArmingFailure(id, record, cause, nodesBefore) {
|
|
104
|
+
assert.equal(existsSync(marker(id)), false, `${id}: the command never ran`);
|
|
105
|
+
assert.equal(record?.exit_code, null, `${id}: no process existed to exit`);
|
|
106
|
+
assert.match(record?.stderr_head ?? '', cause, `${id}: the record names the cause`);
|
|
107
|
+
assert.match(record?.delivered ?? '', /^failure: cron paused, escalation node /, `${id}: the failure is escalated`);
|
|
108
|
+
assert.equal(listNodes().length, nodesBefore + 1, `${id}: exactly one escalation node was created`);
|
|
109
|
+
const row = getCron(id);
|
|
110
|
+
assert.notEqual(row, null, `${id}: the one-shot is retained, not consumed`);
|
|
111
|
+
assert.equal(row.state, 'paused', `${id}: the row pauses, so it does not refire every tick`);
|
|
112
|
+
assert.equal(row.run_state, 'idle', `${id}: no lease was ever taken`);
|
|
113
|
+
assert.deepEqual(listCronRuns(id).map((r) => r.run_id), [record.run_id], `${id}: the failure left exactly one record`);
|
|
114
|
+
}
|
|
115
|
+
test('a corrupt armed env snapshot fails the run instead of silently dropping the operator variables', async () => {
|
|
116
|
+
// Both shapes the arm-time validator rejects, so neither can reach a row
|
|
117
|
+
// through the supported path: unparseable JSON, and a non-string value.
|
|
118
|
+
const corrupt = [
|
|
119
|
+
['env-unparseable', '{"API_BASE": '],
|
|
120
|
+
['env-nonstring', '{"API_BASE": 42}'],
|
|
121
|
+
];
|
|
122
|
+
for (const [id, envJson] of corrupt) {
|
|
123
|
+
arm({ cron_id: id, env_json: envJson });
|
|
124
|
+
const nodesBefore = listNodes().length;
|
|
125
|
+
const record = await executeCron(getCron(id), { outOfBand: false });
|
|
126
|
+
assertSettledAsArmingFailure(id, record, /run could not be armed: the armed env snapshot/, nodesBefore);
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
test('an unreadable profile manifest fails the run, while a deleted profile still runs as root', async () => {
|
|
130
|
+
const unreadable = createProfile('cron-unreadable').profileId;
|
|
131
|
+
const deleted = createProfile('cron-deleted').profileId;
|
|
132
|
+
// Corrupt one manifest in place and delete the other profile outright: the
|
|
133
|
+
// two states the old resolver could not tell apart.
|
|
134
|
+
writeFileSync(profileManifestPath(unreadable), '{ not json', 'utf8');
|
|
135
|
+
rmSync(profileRoot(deleted), { recursive: true, force: true });
|
|
136
|
+
arm({ cron_id: 'profile-unreadable', profile: unreadable });
|
|
137
|
+
const nodesBefore = listNodes().length;
|
|
138
|
+
const failed = await executeCron(getCron('profile-unreadable'), { outOfBand: false });
|
|
139
|
+
assertSettledAsArmingFailure('profile-unreadable', failed, /run could not be armed: the armed profile .* manifest could not be read/, nodesBefore);
|
|
140
|
+
arm({ cron_id: 'profile-deleted', profile: deleted });
|
|
141
|
+
const ran = await executeCron(getCron('profile-deleted'), { outOfBand: false });
|
|
142
|
+
assert.equal(ran?.exit_code, 0, 'a deleted profile is the spec root fallback — the run proceeds');
|
|
143
|
+
assert.equal(readFileSync(marker('profile-deleted'), 'utf8'), '', 'the run carries no profile id: it ran as root');
|
|
144
|
+
assert.equal(getCron('profile-deleted'), null, 'an ordinary settled one-shot is consumed');
|
|
145
|
+
});
|
|
@@ -37,12 +37,10 @@ test('wedgeVerdict: fires ONLY when busy + quiet-past-grace + near-zero cpu all
|
|
|
37
37
|
assert.equal(wedgeVerdict({ busy: true, quietForMs: WEDGE_QUIET_MS, cpuPercent: 0 }), 'wedged');
|
|
38
38
|
assert.equal(wedgeVerdict({ busy: true, quietForMs: WEDGE_QUIET_MS + 60_000, cpuPercent: WEDGE_CPU_MAX_PERCENT }), 'wedged', 'at the cpu ceiling (inclusive)');
|
|
39
39
|
});
|
|
40
|
-
// ---------------------------------------------------------------------------
|
|
41
40
|
// sumTreeCpu (pure) — process-tree CPU sampling. Review fix for issue #110:
|
|
42
41
|
// sampling only the broker pid false-fired on a broker awaiting a busy CHILD
|
|
43
42
|
// subprocess. These lock the tree-sum behavior directly, independent of any
|
|
44
43
|
// `ps` shell-out.
|
|
45
|
-
// ---------------------------------------------------------------------------
|
|
46
44
|
test('sumTreeCpu: a busy CHILD subprocess exonerates an idle broker (the false-positive this fixes)', () => {
|
|
47
45
|
const ps = [
|
|
48
46
|
' 100 1 0.0', // broker, idle
|
|
@@ -85,11 +83,9 @@ test('sumTreeCpu: malformed/blank lines are skipped; empty output returns null',
|
|
|
85
83
|
assert.equal(sumTreeCpu('', 100), null);
|
|
86
84
|
assert.equal(sumTreeCpu(' \n \n', 100), null);
|
|
87
85
|
});
|
|
88
|
-
// ---------------------------------------------------------------------------
|
|
89
86
|
// treeHasDescendants (pure) — issue #119: the broker-alone case has no
|
|
90
87
|
// subprocess to kill, so remediation must branch on this instead of assuming
|
|
91
88
|
// a runaway child always exists.
|
|
92
|
-
// ---------------------------------------------------------------------------
|
|
93
89
|
test('treeHasDescendants: broker alone in its own tree — nothing to kill', () => {
|
|
94
90
|
const ps = [' 100 1 0.0'].join('\n');
|
|
95
91
|
assert.equal(treeHasDescendants(ps, 100), false);
|