@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
|
@@ -81,7 +81,7 @@ test('two plugins whose kinds derive one JSX tag are rejected, naming both sourc
|
|
|
81
81
|
plantPlugin(root, 'beta', ['foo--bar']);
|
|
82
82
|
assert.throws(() => resolvePageComponents(), /kind "foo--bar" \(plugin "beta"\) derives JSX tag "FooBar", which duplicates kind "foo-bar" from plugin "alpha"/);
|
|
83
83
|
});
|
|
84
|
-
//
|
|
84
|
+
// archive path
|
|
85
85
|
function minimalCommandsJson() {
|
|
86
86
|
return {
|
|
87
87
|
schemaVersion: 1,
|
|
@@ -15,7 +15,7 @@ import { mkdtempSync, rmSync } from 'node:fs';
|
|
|
15
15
|
import { tmpdir } from 'node:os';
|
|
16
16
|
import { join } from 'node:path';
|
|
17
17
|
import { buildRoot } from '../../build-root.js';
|
|
18
|
-
import { SUPPRESS_OUTPUT_LEAVES } from '../preview-registry.js';
|
|
18
|
+
import { SUPPRESS_OUTPUT_LEAVES, LEAF_ICON_CP, SUMMARIZERS } from '../preview-registry.js';
|
|
19
19
|
import { resetScopeCache } from '../scope.js';
|
|
20
20
|
// buildRoot() composes external command-plugin branches from the effective
|
|
21
21
|
// enabled plugins (user scope + project ancestors). Pin HOME + profile to an
|
|
@@ -62,3 +62,44 @@ test('preview-registry.ts matches every leaf declaring help.preview.suppressOutp
|
|
|
62
62
|
assert.deepEqual([...declared].sort(), [...SUPPRESS_OUTPUT_LEAVES].sort(), 'src/core/preview-registry.ts is out of sync with the leaves that declare ' +
|
|
63
63
|
'help.preview.suppressOutput — update SUPPRESS_OUTPUT_LEAVES to match.');
|
|
64
64
|
});
|
|
65
|
+
test('every LEAF_ICON_CP key and SUMMARIZERS path names a leaf or branch that still exists', async () => {
|
|
66
|
+
const root = await buildRoot();
|
|
67
|
+
// LEAF_ICON_CP is matched as a PREFIX against argv tokens (iconForPath), so
|
|
68
|
+
// a key may legitimately name either a leaf or a branch (e.g. "human
|
|
69
|
+
// review", a branch with no default action of its own). SUMMARIZERS keys
|
|
70
|
+
// ARE always a single invocable leaf. Both name sets are collected here.
|
|
71
|
+
const names = new Set();
|
|
72
|
+
const leafNames = new Set();
|
|
73
|
+
const walk = (branch) => {
|
|
74
|
+
names.add(branch.help.name);
|
|
75
|
+
for (const child of branch.children) {
|
|
76
|
+
if (child.kind === 'branch') {
|
|
77
|
+
walk(child);
|
|
78
|
+
}
|
|
79
|
+
else {
|
|
80
|
+
names.add(child.help.name);
|
|
81
|
+
leafNames.add(child.help.name);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
for (const subtree of root.subtrees)
|
|
86
|
+
walk(subtree);
|
|
87
|
+
// The registry pre-declares icons for PLUGIN commands too (`search web` and
|
|
88
|
+
// friends), and a plugin is present only when installed — it is absent on a
|
|
89
|
+
// clean CI runner. So a key is only checkable when its ROOT command exists:
|
|
90
|
+
// root present means the rest of the path must resolve (this is what catches a
|
|
91
|
+
// genuinely stale entry, e.g. a renamed `human review`), while an absent root
|
|
92
|
+
// means an uninstalled plugin and is not evidence of staleness.
|
|
93
|
+
const rootNames = new Set(root.subtrees.map((s) => s.help.name));
|
|
94
|
+
const rootInstalled = (path) => rootNames.has(path.split(' ')[0]);
|
|
95
|
+
for (const path of Object.keys(LEAF_ICON_CP)) {
|
|
96
|
+
if (!rootInstalled(path))
|
|
97
|
+
continue;
|
|
98
|
+
assert.ok(names.has(path), `LEAF_ICON_CP names "${path}", which is not a leaf or branch in the live command tree — update or remove it.`);
|
|
99
|
+
}
|
|
100
|
+
for (const { path } of SUMMARIZERS) {
|
|
101
|
+
if (!rootInstalled(path))
|
|
102
|
+
continue;
|
|
103
|
+
assert.ok(leafNames.has(path), `SUMMARIZERS names "${path}", which is not a leaf in the live command tree — update or remove it.`);
|
|
104
|
+
}
|
|
105
|
+
});
|
|
@@ -73,9 +73,7 @@ after(() => {
|
|
|
73
73
|
rmSync(home, { recursive: true, force: true });
|
|
74
74
|
delete process.env['CRTR_HOME'];
|
|
75
75
|
});
|
|
76
|
-
// ===========================================================================
|
|
77
76
|
// Short-lived exits 1-5 defer with doubling backoff; the 6th terminalizes.
|
|
78
|
-
// ===========================================================================
|
|
79
77
|
test('consecutive short-lived exits defer respawn on a doubling backoff, then terminalize at the cap', async () => {
|
|
80
78
|
// A booted row (saved session, no crash markers) — table row 6, respawn-resume.
|
|
81
79
|
createNode(node('T', { pi_session_id: 'sess-T' }));
|
|
@@ -95,9 +93,7 @@ test('consecutive short-lived exits defer respawn on a doubling backoff, then te
|
|
|
95
93
|
assert.equal(getNode('T').status, 'dead', 'the respawn loop terminalizes at MAX_RESPAWN_ATTEMPTS');
|
|
96
94
|
assert.equal(events().some((e) => e.event === 'broker.respawn.exhausted'), true, 'exhaustion is surfaced as its own event');
|
|
97
95
|
});
|
|
98
|
-
// ===========================================================================
|
|
99
96
|
// A healthy-uptime exit resets the counter: the next respawn is immediate.
|
|
100
|
-
// ===========================================================================
|
|
101
97
|
test('a pre-launch respawn refusal terminalizes the stranded node and wakes its parent', async () => {
|
|
102
98
|
const missingCwd = join(home, 'reaped-worktree');
|
|
103
99
|
createNode(node('P', { lifecycle: 'resident' }));
|
|
@@ -51,7 +51,7 @@ function meta(over = {}) {
|
|
|
51
51
|
...over,
|
|
52
52
|
};
|
|
53
53
|
}
|
|
54
|
-
//
|
|
54
|
+
// engine liveness branch
|
|
55
55
|
test('a node whose engine is RUNNING (live pid) is NOT disconnected — never double-spawn', () => {
|
|
56
56
|
assert.equal(isDisconnected(meta({ pi_pid: 1234 }), ALIVE), false);
|
|
57
57
|
});
|
|
@@ -61,14 +61,14 @@ test('a node whose engine is DEAD but has a saved session IS disconnected', () =
|
|
|
61
61
|
test('a node with a null pid (never/ no longer recorded) and a session IS disconnected', () => {
|
|
62
62
|
assert.equal(isDisconnected(meta({ pi_pid: null }), DEAD), true);
|
|
63
63
|
});
|
|
64
|
-
//
|
|
64
|
+
// resumable-session requirement
|
|
65
65
|
test('a dead node with NO resumable saved session is NOT disconnected', () => {
|
|
66
66
|
assert.equal(isDisconnected(meta({ pi_session_id: null, pi_session_file: null }), DEAD), false);
|
|
67
67
|
});
|
|
68
68
|
test('a session FILE alone (older node, no id) is enough to be disconnected', () => {
|
|
69
69
|
assert.equal(isDisconnected(meta({ pi_session_id: null }), DEAD), true);
|
|
70
70
|
});
|
|
71
|
-
//
|
|
71
|
+
// terminal-by-choice scope
|
|
72
72
|
for (const status of TERMINAL_BY_CHOICE) {
|
|
73
73
|
test(`a dead-engine '${status}' node is EXCLUDED (terminal-by-choice, no opt-in)`, () => {
|
|
74
74
|
assert.equal(isDisconnected(meta({ status: status }), DEAD), false);
|
|
@@ -89,11 +89,11 @@ test('a node with final_report set is EXCLUDED even when status is dead (finishe
|
|
|
89
89
|
test("dormant 'idle' nodes with a saved session are swept by default", () => {
|
|
90
90
|
assert.equal(isDisconnected(meta({ status: 'idle', intent: 'idle-release' }), DEAD), true);
|
|
91
91
|
});
|
|
92
|
-
//
|
|
92
|
+
// human-bridge carve-out
|
|
93
93
|
test("kind:'human' rows (the human bridge, never a pi engine) are never disconnected", () => {
|
|
94
94
|
assert.equal(isDisconnected(meta({ kind: 'human', pi_pid: null }), DEAD), false);
|
|
95
95
|
});
|
|
96
|
-
//
|
|
96
|
+
// listDisconnected over a live canvas (the sweep's selection)
|
|
97
97
|
//
|
|
98
98
|
// Locks that the canvas-wide select returns EXACTLY the disconnected set against
|
|
99
99
|
// the REAL isPidAlive probe — a live-engine node (this process's pid) is kept
|
|
@@ -1,11 +1,17 @@
|
|
|
1
1
|
// Run with: node --conditions=crtr-src --import tsx/esm --test src/core/__tests__/revive-parked-fresh.test.ts
|
|
2
2
|
//
|
|
3
3
|
// Re-entering a parked node starts a FRESH cycle grounded in goal + roadmap
|
|
4
|
-
// rather than replaying the transcript its parking turn already concluded
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
4
|
+
// rather than replaying the transcript its parking turn already concluded —
|
|
5
|
+
// but only when the revival CARRIES NEW INPUT. Wake-worthy mail that the node
|
|
6
|
+
// has not seen is that input: every doorway that wakes a parked node because
|
|
7
|
+
// something arrived appends its durable inbox entry before calling in. An
|
|
8
|
+
// input-less revival (a bare lifecycle revive, a viewer attach, a focus) has
|
|
9
|
+
// nothing to answer, so it quietly resumes the old transcript and injects no
|
|
10
|
+
// kickoff. The rule is a property of the node, evaluated once inside
|
|
11
|
+
// reviveNode, so every doorway gets it from one strict-resume veto. Its other
|
|
12
|
+
// exception is a park with nothing on disk to stand on (reachable only on the
|
|
13
|
+
// degraded path, which writes no summary): resuming an old transcript beats
|
|
14
|
+
// waking amnesiac.
|
|
9
15
|
import { after, afterEach, before, beforeEach, test } from 'node:test';
|
|
10
16
|
import assert from 'node:assert/strict';
|
|
11
17
|
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
|
|
@@ -14,6 +20,7 @@ import { join } from 'node:path';
|
|
|
14
20
|
import { createNode, getNode } from '../canvas/canvas.js';
|
|
15
21
|
import { contextDir } from '../canvas/paths.js';
|
|
16
22
|
import { closeDb } from '../canvas/db.js';
|
|
23
|
+
import { appendInbox, readCursor, writeCursor } from '../feed/inbox.js';
|
|
17
24
|
import { transition } from '../runtime/lifecycle.js';
|
|
18
25
|
import { reviveNode } from '../runtime/revive.js';
|
|
19
26
|
import { headlessBrokerHost } from '../runtime/host.js';
|
|
@@ -49,6 +56,21 @@ function parkResident(id, ground = {}) {
|
|
|
49
56
|
writeFileSync(join(contextDir(id), 'initial-prompt.md'), ground.goal);
|
|
50
57
|
transition(id, 'park');
|
|
51
58
|
}
|
|
59
|
+
/** Durable wake-worthy mail, appended exactly as every message doorway appends
|
|
60
|
+
* it before calling reviveNode. */
|
|
61
|
+
function deliverMail(id, tier = 'normal') {
|
|
62
|
+
return appendInbox(id, { from: null, tier, kind: 'message', label: 'work', data: { body: 'work' } }).entry_id;
|
|
63
|
+
}
|
|
64
|
+
/** Did this launch carry the fresh-revive kickoff? `buildReviveKickoff` always
|
|
65
|
+
* opens its card body with this sentence (src/core/runtime/kickoff.ts:325). */
|
|
66
|
+
function kickedOff(entry) {
|
|
67
|
+
return entry.argv.some((arg) => arg.includes('You have been revived fresh'));
|
|
68
|
+
}
|
|
69
|
+
/** The two facts every row below asserts together: a fresh row is
|
|
70
|
+
* `resuming:false` + kickoff, a quiet row is `resuming:true` + none. */
|
|
71
|
+
function placements() {
|
|
72
|
+
return launches.map(({ nodeId, resuming }) => ({ nodeId, resuming }));
|
|
73
|
+
}
|
|
52
74
|
before(() => {
|
|
53
75
|
home = mkdtempSync(join(tmpdir(), 'crtr-revive-parked-'));
|
|
54
76
|
priorHome = process.env['CRTR_HOME'];
|
|
@@ -62,8 +84,8 @@ beforeEach(() => {
|
|
|
62
84
|
fleet = bindTestFleet();
|
|
63
85
|
launches = [];
|
|
64
86
|
originalLaunch = headlessBrokerHost.launch;
|
|
65
|
-
headlessBrokerHost.launch = ((nodeId,
|
|
66
|
-
launches.push({ nodeId, resuming: opts.resuming });
|
|
87
|
+
headlessBrokerHost.launch = ((nodeId, inv, opts) => {
|
|
88
|
+
launches.push({ nodeId, resuming: opts.resuming, argv: inv.argv });
|
|
67
89
|
const handle = { pid: 0x7ffffffe, exited: new Promise(() => { }) };
|
|
68
90
|
fleet.fleet.register(nodeId, handle);
|
|
69
91
|
return handle;
|
|
@@ -81,25 +103,54 @@ after(() => {
|
|
|
81
103
|
else
|
|
82
104
|
process.env['CRTR_HOME'] = priorHome;
|
|
83
105
|
});
|
|
84
|
-
test('a parked node with
|
|
106
|
+
test('a parked node with mail waiting re-enters fresh even when the caller asked to resume, and the marker clears', () => {
|
|
85
107
|
parkResident('parked', { roadmap: '# Roadmap\n\n- one open item\n' });
|
|
108
|
+
deliverMail('parked');
|
|
86
109
|
const result = reviveNode('parked', { resume: true });
|
|
87
110
|
assert.equal(result.resumed, false, 'the concluded transcript is not replayed');
|
|
88
|
-
assert.deepEqual(
|
|
111
|
+
assert.deepEqual(placements(), [{ nodeId: 'parked', resuming: false }]);
|
|
112
|
+
assert.equal(kickedOff(launches[0]), true, 'the fresh window is handed its bearings');
|
|
89
113
|
const reopened = getNode('parked');
|
|
90
114
|
assert.equal(reopened.status, 'active');
|
|
91
115
|
assert.equal(reopened.intent ?? null, null, 'the park marker clears on revival');
|
|
92
116
|
assert.equal(reopened.cycle_pending, true, 'the fresh cycle is provisional until session_start confirms it');
|
|
93
117
|
});
|
|
94
|
-
test('a parked node grounded only by a goal still re-enters fresh', () => {
|
|
118
|
+
test('a parked node grounded only by a goal still re-enters fresh when mail rides the revive', () => {
|
|
95
119
|
parkResident('goal-only', { goal: 'Watch the deploy lane.' });
|
|
120
|
+
deliverMail('goal-only');
|
|
96
121
|
assert.equal(reviveNode('goal-only', { resume: true }).resumed, false);
|
|
122
|
+
assert.equal(kickedOff(launches[0]), true);
|
|
123
|
+
});
|
|
124
|
+
test('an input-less revival of a parked node quietly resumes and injects no kickoff', () => {
|
|
125
|
+
parkResident('quiet', { roadmap: '# Roadmap\n\n- one open item\n' });
|
|
126
|
+
const result = reviveNode('quiet', { resume: true });
|
|
127
|
+
assert.equal(result.resumed, true, 'nothing arrived, so there is nothing a fresh window would answer');
|
|
128
|
+
assert.deepEqual(placements(), [{ nodeId: 'quiet', resuming: true }]);
|
|
129
|
+
assert.equal(kickedOff(launches[0]), false, 'a quiet resume generates nothing');
|
|
130
|
+
assert.equal(readCursor('quiet'), undefined, 'drainBearings did not run, so nothing was consumed');
|
|
131
|
+
const reopened = getNode('quiet');
|
|
132
|
+
assert.equal(reopened.status, 'active');
|
|
133
|
+
assert.equal(reopened.intent ?? null, null, 'the park marker clears on a quiet resume too');
|
|
134
|
+
});
|
|
135
|
+
test('deferred mail alone is not input, so the revival still resumes quietly', () => {
|
|
136
|
+
parkResident('deferred', { roadmap: '# Roadmap\n\n- one open item\n' });
|
|
137
|
+
deliverMail('deferred', 'deferred');
|
|
138
|
+
assert.equal(reviveNode('deferred', { resume: true }).resumed, true);
|
|
139
|
+
assert.equal(kickedOff(launches[0]), false);
|
|
140
|
+
assert.equal(readCursor('deferred'), undefined, 'the deferred entry rides the next natural cycle, unconsumed');
|
|
141
|
+
});
|
|
142
|
+
test('mail already covered by the cursor is not input, so the revival still resumes quietly', () => {
|
|
143
|
+
parkResident('stale', { roadmap: '# Roadmap\n\n- one open item\n' });
|
|
144
|
+
writeCursor('stale', deliverMail('stale'));
|
|
145
|
+
assert.equal(reviveNode('stale', { resume: true }).resumed, true, 'only the UNSEEN suffix counts as input');
|
|
146
|
+
assert.equal(kickedOff(launches[0]), false);
|
|
97
147
|
});
|
|
98
148
|
test('a parked node with no goal and no roadmap resumes its transcript rather than waking amnesiac', () => {
|
|
99
149
|
parkResident('bare');
|
|
150
|
+
deliverMail('bare');
|
|
100
151
|
const result = reviveNode('bare', { resume: true });
|
|
101
152
|
assert.equal(result.resumed, true, 'the degraded park has nothing on disk to ground a fresh window');
|
|
102
|
-
assert.deepEqual(
|
|
153
|
+
assert.deepEqual(placements(), [{ nodeId: 'bare', resuming: true }]);
|
|
103
154
|
});
|
|
104
155
|
test('an ordinary done node with a roadmap still resumes — only the park marker vetoes', () => {
|
|
105
156
|
parkResident('finished', { roadmap: '# Roadmap\n\n- one open item\n' });
|
|
@@ -79,7 +79,6 @@ const barrier = async (c, label) => {
|
|
|
79
79
|
const isRequest = (f) => f.type === 'extension_ui_request';
|
|
80
80
|
const dismissFor = (reqId) => (f) => f.type === 'extension_ui_dismiss' && f.id === reqId;
|
|
81
81
|
const welcomes = (frames) => frames.filter((f) => f.type === 'welcome');
|
|
82
|
-
// ---------------------------------------------------------------------------
|
|
83
82
|
// G4 — multi-writer admission + read-only gating + broadcast fan-out. Guards: two
|
|
84
83
|
// simultaneous `controller` hellos are BOTH welcomed writable, each one's own
|
|
85
84
|
// submission reaches the engine, every connected client sees BOTH turns, an
|
|
@@ -90,7 +89,6 @@ const welcomes = (frames) => frames.filter((f) => f.type === 'welcome');
|
|
|
90
89
|
// Failure mode: a singleton writer slot demoting the second client, a writer
|
|
91
90
|
// whose prompt is silently dropped, fan-out that misses a viewer, a read-only
|
|
92
91
|
// client that can drive the engine, or a model switch only its requester hears.
|
|
93
|
-
// ---------------------------------------------------------------------------
|
|
94
92
|
test('G4 — two simultaneous clients are both welcomed controller, each submits, all clients see both turns and the model switch; an observer drive → error{read_only}', async () => {
|
|
95
93
|
const [w1, w2] = await Promise.all([
|
|
96
94
|
attach(id, 'controller', 'g4-writer-1'),
|
|
@@ -132,7 +130,6 @@ test('G4 — two simultaneous clients are both welcomed controller, each submits
|
|
|
132
130
|
const om = await obs.waitFrame((f) => f.type === 'model_changed', 'G4 observer receives model_changed');
|
|
133
131
|
assert.ok(om.type === 'model_changed' && om.model?.id === 'fake-fable', 'G4: a read-only observer ALSO hears the switch');
|
|
134
132
|
});
|
|
135
|
-
// ---------------------------------------------------------------------------
|
|
136
133
|
// G5 — dialog fan-out, mid-dialog handoff, and first-response settlement.
|
|
137
134
|
// Guards: ONE blocking dialog reaches EVERY writable client (never a single
|
|
138
135
|
// owner), a writable client attaching MID-dialog is handed it via
|
|
@@ -143,7 +140,6 @@ test('G4 — two simultaneous clients are both welcomed controller, each submits
|
|
|
143
140
|
// Failure modes: a dialog only one viewer can see (the others sit blind), a
|
|
144
141
|
// late-joining writable viewer blind to an in-flight dialog, peers left holding
|
|
145
142
|
// an overlay nothing can answer, or a late peer answer re-settling a dead dialog.
|
|
146
|
-
// ---------------------------------------------------------------------------
|
|
147
143
|
test('G5 — one dialog fans out to every writable client and to a mid-dialog joiner, the first response settles it, all are dismissed, and a late peer response is a no-op', async () => {
|
|
148
144
|
const base = h.dialogResults(id).length;
|
|
149
145
|
const a = await writer('g5-A');
|
|
@@ -184,7 +180,6 @@ test('G5 — one dialog fans out to every writable client and to a mid-dialog jo
|
|
|
184
180
|
assert.equal(settled[base].resolved, true, "G5: the late peer response did not overwrite the first answerer's value");
|
|
185
181
|
assert.equal(isPidAlive(brokerPid()), true, 'G5: the late no-op response did not fault the broker');
|
|
186
182
|
});
|
|
187
|
-
// ---------------------------------------------------------------------------
|
|
188
183
|
// G9 — re-welcome after an IN-PLACE session mutation (real bug, observed
|
|
189
184
|
// 2026-06-11 in `crtr surface attach`: double-Esc/`/tree` opened the session-tree
|
|
190
185
|
// picker, but SELECTING an entry visibly did nothing). session.navigateTree()
|
|
@@ -194,7 +189,6 @@ test('G5 — one dialog fans out to every writable client and to a mid-dialog jo
|
|
|
194
189
|
// attached client rendering the abandoned branch and dropping the navigated-to
|
|
195
190
|
// user message's editorText. Failure mode: no second welcome arrives at all, or
|
|
196
191
|
// the re-welcome serves a stale snapshot.
|
|
197
|
-
// ---------------------------------------------------------------------------
|
|
198
192
|
test('G9 — navigate_tree re-welcomes every viewer onto the rewound transcript and acks the editor text', async () => {
|
|
199
193
|
const c = await writer('nav-ctrl');
|
|
200
194
|
// Accrue one observable turn so the rewind has something to drop.
|
|
@@ -222,7 +216,6 @@ test('G9 — navigate_tree re-welcomes every viewer onto the rewound transcript
|
|
|
222
216
|
assert.ok(!frameHas(second, token), 'G9: re-welcome serves the rewound transcript (turn dropped)');
|
|
223
217
|
assert.equal(second.snapshot.messages.length, first.snapshot.messages.length - 2, 'G9: re-welcome history is exactly one turn shorter (a turn = user + assistant = 2 tree messages)');
|
|
224
218
|
});
|
|
225
|
-
// ---------------------------------------------------------------------------
|
|
226
219
|
// G10 — catch-up is ONE atomic frame. `ctx.ui.setStatus/setWidget/setTitle` are
|
|
227
220
|
// LEVELS with no pi getter, so the broker is their only holder; it ships that
|
|
228
221
|
// held state as `welcome.snapshot.display` and sends NO display edge after the
|
|
@@ -232,7 +225,6 @@ test('G9 — navigate_tree re-welcomes every viewer onto the rewound transcript
|
|
|
232
225
|
// re-emitted N synthetic display frames after the snapshot: catch-up was
|
|
233
226
|
// non-atomic and the viewer had to clear-and-rebuild, painting blank chrome in
|
|
234
227
|
// the gap. Both halves of that cut fail this test.
|
|
235
|
-
// ---------------------------------------------------------------------------
|
|
236
228
|
test('G10 — retained extension display state rides IN welcome.snapshot.display, and no display edge follows the welcome', async () => {
|
|
237
229
|
const DISPLAY_METHODS = new Set(['setStatus', 'setWidget', 'setTitle']);
|
|
238
230
|
const isDisplayFrame = (f) => f.type === 'extension_ui_request' && f.method !== undefined && DISPLAY_METHODS.has(f.method);
|
|
@@ -61,11 +61,9 @@ afterEach(() => {
|
|
|
61
61
|
kit.closeAll();
|
|
62
62
|
});
|
|
63
63
|
const brokerPid = () => h.node(id).pi_pid;
|
|
64
|
-
// ---------------------------------------------------------------------------
|
|
65
64
|
// G1 — controller drive + live relay. Guards: a controller's `prompt` runs the
|
|
66
65
|
// engine AND the streaming AgentSessionEvents are fanned out to the client
|
|
67
66
|
// VERBATIM. Failure mode: a broken relay/fan-out (viewer sees nothing).
|
|
68
|
-
// ---------------------------------------------------------------------------
|
|
69
67
|
test('G1 — controller prompt runs the engine and the streamed AgentSessionEvents relay to the client', async () => {
|
|
70
68
|
const c = await ctrl('g1-ctrl');
|
|
71
69
|
assert.equal(c.welcome.role, 'controller', 'first controller is admitted as controller');
|
|
@@ -80,7 +78,6 @@ test('G1 — controller prompt runs the engine and the streamed AgentSessionEven
|
|
|
80
78
|
}
|
|
81
79
|
assert.ok(c.frames.some((f) => f.type === 'message_start' && frameHas(f, token)), 'G1: a relayed message carries the prompt token');
|
|
82
80
|
});
|
|
83
|
-
// ---------------------------------------------------------------------------
|
|
84
81
|
// G1b — message_update coalescing preserves ordering (regression for the F2
|
|
85
82
|
// attach typing-lag fix, 2026-06-09). The broker holds the latest message_update
|
|
86
83
|
// on a ~75ms timer; any OTHER event must flush it FIRST, so a viewer can never
|
|
@@ -88,7 +85,6 @@ test('G1 — controller prompt runs the engine and the streamed AgentSessionEven
|
|
|
88
85
|
// streaming text over the final message), and the LAST coalesced update must not
|
|
89
86
|
// be silently dropped at end-of-turn. Failure modes: a flush ordered after the
|
|
90
87
|
// non-update event, or a pending update discarded when message_end wins the race.
|
|
91
|
-
// ---------------------------------------------------------------------------
|
|
92
88
|
test('G1b — coalesced message_update never arrives after message_end; updates still relayed', async () => {
|
|
93
89
|
const c = await ctrl('g1b-ctrl');
|
|
94
90
|
const token = tok('G1B');
|
|
@@ -114,10 +110,8 @@ test('G1b — coalesced message_update never arrives after message_end; updates
|
|
|
114
110
|
assert.ok(i < 0 || lastUpdate < i, `G1b: message_update relayed after ${t}`);
|
|
115
111
|
}
|
|
116
112
|
});
|
|
117
|
-
// ---------------------------------------------------------------------------
|
|
118
113
|
// G2 — detach leaves the engine running. Guards: `bye`/close drops ONE listener,
|
|
119
114
|
// never the engine. Failure mode: a detach that disposes the broker/engine.
|
|
120
|
-
// ---------------------------------------------------------------------------
|
|
121
115
|
test('G2 — detach (bye+close) leaves the broker alive + the engine still emitting; engine pid unchanged', async () => {
|
|
122
116
|
const pid = brokerPid();
|
|
123
117
|
const boots = h.bootCount(id);
|
|
@@ -139,7 +133,6 @@ test('G2 — detach (bye+close) leaves the broker alive + the engine still emitt
|
|
|
139
133
|
assert.ok(o.welcome.snapshot.messages.length > 0, 'G2: the engine produced a message after the detach');
|
|
140
134
|
assert.equal(isPidAlive(pid), true, 'G2: broker still alive after the post-detach turn');
|
|
141
135
|
});
|
|
142
|
-
// ---------------------------------------------------------------------------
|
|
143
136
|
// G3 — the interactive `deliver` ack reports the ENGINE CALL's real outcome, not
|
|
144
137
|
// the fact that a call was routed. A `session.prompt()` that REJECTS outright —
|
|
145
138
|
// the shape a broker whose model resolved but whose provider has no installed
|
|
@@ -150,7 +143,6 @@ test('G2 — detach (bye+close) leaves the broker alive + the engine still emitt
|
|
|
150
143
|
// engine's own agent_start) against rejection and records
|
|
151
144
|
// `broker.prompt.deliver_failed` in the node's job/log.jsonl. Shares this file's
|
|
152
145
|
// one broker: the contract is two frames on an attached controller.
|
|
153
|
-
// ---------------------------------------------------------------------------
|
|
154
146
|
test('G3 — a deliver whose prompt rejects acks ok:false and records deliver_failed; a succeeding deliver acks ok:true via prompt', async () => {
|
|
155
147
|
// Both delivers must route as `prompt` (idle), never `steer` — the steer route
|
|
156
148
|
// legitimately acks at routing time — so attach only once the engine is idle.
|
|
@@ -43,11 +43,9 @@ after(async () => {
|
|
|
43
43
|
else
|
|
44
44
|
process.env['PI_FAKE_ENGINE_HOLD_TURN'] = priorHoldTurn;
|
|
45
45
|
});
|
|
46
|
-
// ===========================================================================
|
|
47
46
|
// Items 4 + 5 + 6 — two independent real-process journeys:
|
|
48
47
|
// graceful busy teardown → strict resume + continuation;
|
|
49
48
|
// dirty interrupted-turn crash → recovery cycle → close.
|
|
50
|
-
// ===========================================================================
|
|
51
49
|
test('SIGTERM of a busy broker aborts cleanly and resumes with a continuation', { timeout: 30_000 }, async () => {
|
|
52
50
|
const id = await h.spawnHeadlessChild(root, 'headless worker — graceful crash recovery');
|
|
53
51
|
const boot1 = await h.awaitBoot(id);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
// tmux-driver.ts owns the raw tmux process boundary — every direct
|
|
2
|
+
// `spawnSync('tmux', ...)` elsewhere is a duplicated probe that should call
|
|
3
|
+
// through tmux-driver.ts's exported primitives instead. This grep-enforces
|
|
4
|
+
// that boundary with an explicit allowlist: the owner itself and three
|
|
5
|
+
// pre-existing direct spawners with a genuinely distinct semantic (documented
|
|
6
|
+
// per-entry below).
|
|
7
|
+
import { test } from 'node:test';
|
|
8
|
+
import assert from 'node:assert/strict';
|
|
9
|
+
import { readdirSync, readFileSync, statSync } from 'node:fs';
|
|
10
|
+
import { join, resolve, relative } from 'node:path';
|
|
11
|
+
const SRC_ROOT = resolve(process.cwd(), 'src');
|
|
12
|
+
// Relative to src/, forward-slash form.
|
|
13
|
+
const ALLOWLIST = new Set([
|
|
14
|
+
'core/runtime/tmux-driver.ts', // the owner
|
|
15
|
+
'clients/attach/session/mouse.ts', // mouse-mode detection — a distinct probe, not reachability
|
|
16
|
+
'core/clipboard-text.ts', // OSC-52 paste-buffer write — not a reachability probe
|
|
17
|
+
'core/termrender/display.ts', // pane-count probe — not a reachability probe
|
|
18
|
+
]);
|
|
19
|
+
function tsFilesUnder(dir) {
|
|
20
|
+
const out = [];
|
|
21
|
+
for (const entry of readdirSync(dir)) {
|
|
22
|
+
const path = join(dir, entry);
|
|
23
|
+
const stat = statSync(path);
|
|
24
|
+
if (stat.isDirectory()) {
|
|
25
|
+
if (entry === '__tests__')
|
|
26
|
+
continue;
|
|
27
|
+
out.push(...tsFilesUnder(path));
|
|
28
|
+
}
|
|
29
|
+
else if (entry.endsWith('.ts') && !entry.endsWith('.test.ts')) {
|
|
30
|
+
out.push(path);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return out;
|
|
34
|
+
}
|
|
35
|
+
test('every direct spawnSync("tmux", …) call site is inside the tmux-driver.ts allowlist', () => {
|
|
36
|
+
const offenders = [];
|
|
37
|
+
for (const file of tsFilesUnder(SRC_ROOT)) {
|
|
38
|
+
const relPath = relative(SRC_ROOT, file).split('\\').join('/');
|
|
39
|
+
if (ALLOWLIST.has(relPath))
|
|
40
|
+
continue;
|
|
41
|
+
const source = readFileSync(file, 'utf8');
|
|
42
|
+
const lines = source.split('\n');
|
|
43
|
+
lines.forEach((line, index) => {
|
|
44
|
+
if (/spawnSync\(\s*['"]tmux['"]/.test(line)) {
|
|
45
|
+
offenders.push(`${relPath}:${index + 1}`);
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
assert.deepEqual(offenders, [], `direct spawnSync('tmux', …) call(s) outside the tmux-driver.ts allowlist:\n${offenders.join('\n')}`);
|
|
50
|
+
});
|
|
@@ -14,7 +14,7 @@ after(() => {
|
|
|
14
14
|
rmSync(home, { recursive: true, force: true });
|
|
15
15
|
});
|
|
16
16
|
test('only a watchdog-stamped aborted settlement records an automatic connection fault', () => {
|
|
17
|
-
const settleAbort = (nodeId, stagedWatchdogAbort, stagedRefreshAbort, stopReason = 'aborted') => {
|
|
17
|
+
const settleAbort = (nodeId, stagedWatchdogAbort, stagedRefreshAbort, stopReason = 'aborted', errorText) => {
|
|
18
18
|
const session = {
|
|
19
19
|
sessionManager: { getLeafId: () => 'leaf-entry' },
|
|
20
20
|
model: undefined,
|
|
@@ -27,7 +27,7 @@ test('only a watchdog-stamped aborted settlement records an automatic connection
|
|
|
27
27
|
messages: [{
|
|
28
28
|
role: 'assistant',
|
|
29
29
|
stopReason,
|
|
30
|
-
errorMessage: stopReason === 'error' ? 'This operation was aborted' : 'Request was aborted',
|
|
30
|
+
errorMessage: errorText ?? (stopReason === 'error' ? 'This operation was aborted' : 'Request was aborted'),
|
|
31
31
|
}],
|
|
32
32
|
},
|
|
33
33
|
stagedWatchdogAbort,
|
|
@@ -53,14 +53,19 @@ test('only a watchdog-stamped aborted settlement records an automatic connection
|
|
|
53
53
|
settleAbort('human-node', false, false);
|
|
54
54
|
assert.equal(readFault('human-node'), null, 'an otherwise-identical human abort must not arm recovery');
|
|
55
55
|
// Pi 0.84 currently serializes a host abort from `node yield` as an `error`
|
|
56
|
-
// settlement.
|
|
56
|
+
// settlement.
|
|
57
57
|
const refresh = settleAbort('refresh-node', false, true, 'error');
|
|
58
58
|
assert.equal(readFault('refresh-node'), null, 'a refresh-aborted error settlement must not enter provider fault recovery');
|
|
59
59
|
assert.equal(refresh.generation.stagedRefreshAbort, false, 'settlement consumes the one-shot refresh provenance');
|
|
60
|
-
|
|
60
|
+
// #683 — an abort that lands between requests settles as an error carrying
|
|
61
|
+
// AbortError text. It stopped because something asked it to; a fatal fault
|
|
62
|
+
// here parks the node and fans a false permanent-failure notice.
|
|
63
|
+
settleAbort('aborted-node', false, false, 'error');
|
|
64
|
+
assert.equal(readFault('aborted-node'), null, 'an abort-shaped error settlement must not record a provider fault');
|
|
65
|
+
settleAbort('provider-node', false, false, 'error', 'connection reset by peer');
|
|
61
66
|
const providerFault = readFault('provider-node');
|
|
62
|
-
assert.equal(providerFault?.kind, '
|
|
63
|
-
assert.
|
|
67
|
+
assert.equal(providerFault?.kind, 'connection', 'a real provider error still classifies');
|
|
68
|
+
assert.equal(providerFault?.retry.disposition, 'auto', 'a real provider error still records its fault');
|
|
64
69
|
});
|
|
65
70
|
test('a failed retry settlement carries the episode forward instead of restarting it', () => {
|
|
66
71
|
// The episode marker a daemon retry records before re-prompting: original
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/** One durable-mutation signal file under `crtrHome()/signals/<name>`, shared by
|
|
2
|
+
* every subsystem that needs to tell every open viewer "something changed"
|
|
3
|
+
* without watching each individual record's directory. */
|
|
4
|
+
export declare function activityPath(name: string): string;
|
|
5
|
+
/** Signal one durable mutation to every open watcher of `name`. */
|
|
6
|
+
export declare function signalActivity(name: string): void;
|
|
7
|
+
export declare function watchActivity(name: string, onChange: () => void): () => void;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { basename, dirname, join } from 'node:path';
|
|
2
|
+
import { mkdirSync, watch, writeFileSync } from 'node:fs';
|
|
3
|
+
import { crtrHome } from './canvas/paths.js';
|
|
4
|
+
/** One durable-mutation signal file under `crtrHome()/signals/<name>`, shared by
|
|
5
|
+
* every subsystem that needs to tell every open viewer "something changed"
|
|
6
|
+
* without watching each individual record's directory. */
|
|
7
|
+
export function activityPath(name) {
|
|
8
|
+
return join(crtrHome(), 'signals', name);
|
|
9
|
+
}
|
|
10
|
+
/** Signal one durable mutation to every open watcher of `name`. */
|
|
11
|
+
export function signalActivity(name) {
|
|
12
|
+
const path = activityPath(name);
|
|
13
|
+
try {
|
|
14
|
+
mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
|
|
15
|
+
writeFileSync(path, `${Date.now()}\n`, { mode: 0o600 });
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
// A later read of the underlying record is still authoritative.
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
export function watchActivity(name, onChange) {
|
|
22
|
+
const path = activityPath(name);
|
|
23
|
+
try {
|
|
24
|
+
mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
|
|
25
|
+
const watcher = watch(dirname(path), (_event, filename) => {
|
|
26
|
+
if (String(filename) === basename(path))
|
|
27
|
+
onChange();
|
|
28
|
+
});
|
|
29
|
+
return () => watcher.close();
|
|
30
|
+
}
|
|
31
|
+
catch (error) {
|
|
32
|
+
// The caller keeps a working no-op unwatch — a watch failure must not crash
|
|
33
|
+
// whatever set it up to be best-effort — but it stays surfaced once, since a
|
|
34
|
+
// silently-never-refreshing viewer is a real symptom, not a background detail.
|
|
35
|
+
console.error(`activity-signal: failed to watch "${name}": ${error instanceof Error ? error.message : String(error)}`);
|
|
36
|
+
return () => { };
|
|
37
|
+
}
|
|
38
|
+
}
|
package/dist/core/auto-update.js
CHANGED
|
@@ -3,6 +3,7 @@ import { readConfig, readState, updateState } from './config.js';
|
|
|
3
3
|
import { nowIso } from './fs-utils.js';
|
|
4
4
|
import { info } from './output.js';
|
|
5
5
|
import { SELF_UPDATE_SCRIPT, selfCheck, contentCheck } from './self-update.js';
|
|
6
|
+
import { envDebug } from '../shared/env.js';
|
|
6
7
|
const HOUR_MS = 60 * 60 * 1000;
|
|
7
8
|
const SKIP_SUBCOMMANDS = new Set([
|
|
8
9
|
'update',
|
|
@@ -85,7 +86,7 @@ export function maybeAutoUpdate(argv) {
|
|
|
85
86
|
}
|
|
86
87
|
}
|
|
87
88
|
catch (e) {
|
|
88
|
-
if (
|
|
89
|
+
if (envDebug()) {
|
|
89
90
|
const msg = e instanceof Error ? e.message : String(e);
|
|
90
91
|
process.stderr.write(`crtr: auto-update hook error: ${msg}\n`);
|
|
91
92
|
}
|
package/dist/core/bootstrap.js
CHANGED
|
@@ -3,6 +3,7 @@ import { join } from 'node:path';
|
|
|
3
3
|
import { findProjectScopeRoot, resetScopeCache, userScopeRoot } from './scope.js';
|
|
4
4
|
import { ensureScopeInitialized } from './config.js';
|
|
5
5
|
import { CRTR_DIR_NAME } from '../types.js';
|
|
6
|
+
import { envDebug } from '../shared/env.js';
|
|
6
7
|
const SKIP_SUBCOMMANDS = new Set([
|
|
7
8
|
'help',
|
|
8
9
|
'--help',
|
|
@@ -42,7 +43,7 @@ export function ensureProjectScope(argv) {
|
|
|
42
43
|
resetScopeCache();
|
|
43
44
|
}
|
|
44
45
|
catch (e) {
|
|
45
|
-
if (
|
|
46
|
+
if (envDebug()) {
|
|
46
47
|
const msg = e instanceof Error ? e.message : String(e);
|
|
47
48
|
process.stderr.write(`crtr: project-init error: ${msg}\n`);
|
|
48
49
|
}
|
|
@@ -10,7 +10,7 @@ import { test } from 'node:test';
|
|
|
10
10
|
import assert from 'node:assert/strict';
|
|
11
11
|
import { createServer } from 'node:http';
|
|
12
12
|
import { RemoteCanvasSource } from '../remote-canvas-source.js';
|
|
13
|
-
import { dashboardRowsAllFromSource, renderForestFromSource } from '../render.js';
|
|
13
|
+
import { dashboardRowsAllFromSource, renderForestFromSource } from '../render-source.js';
|
|
14
14
|
/** Stand up a bare `node:http` server on an ephemeral loopback port — the
|
|
15
15
|
* `/__crtr/source` endpoint a `RemoteCanvasSource` POSTs to. `route` decides
|
|
16
16
|
* the response per captured argv; every request is recorded first so a test
|
|
@@ -62,7 +62,7 @@ test('a normal fast response is unaffected by the timeout wiring', async (t) =>
|
|
|
62
62
|
assert.equal(result.ok, true);
|
|
63
63
|
assert.equal(result.stdout, JSON.stringify({ nodes: [] }));
|
|
64
64
|
});
|
|
65
|
-
//
|
|
65
|
+
// Phase 3 review Minor 1: no token bytes may ever reach returned stderr
|
|
66
66
|
test('an unreachable endpoint returns a flat generic network-error message, never the raw exception text', async () => {
|
|
67
67
|
const exec = createRemoteExec('http://127.0.0.1:1', 'valid-token-123');
|
|
68
68
|
const result = await exec('crtr', ['--json', 'node', 'inspect', 'list']);
|
|
@@ -21,7 +21,7 @@ import { tmpdir } from 'node:os';
|
|
|
21
21
|
import { join } from 'node:path';
|
|
22
22
|
import { atomicWriteJson, pageManifestPath } from '#core/human/convention';
|
|
23
23
|
import { RemoteCanvasSource } from '../remote-canvas-source.js';
|
|
24
|
-
import { dashboardRowsAllFromSource, enrichRowsFromSource, loadPreviewFromSource, renderForestFromSource } from '../render.js';
|
|
24
|
+
import { dashboardRowsAllFromSource, enrichRowsFromSource, loadPreviewFromSource, renderForestFromSource } from '../render-source.js';
|
|
25
25
|
import { createNode } from '../canvas.js';
|
|
26
26
|
import { closeDb } from '../db.js';
|
|
27
27
|
import { openFocusRow } from '../focuses.js';
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
// Resume is one action: Enter routes the chosen node through `crtr surface node focus`,
|
|
8
8
|
// which goes via reviveNode() — the ONLY sanctioned open. NEVER spawn
|
|
9
9
|
// `pi --session` directly (see reviveNode() in revive.ts for the desync hazard).
|
|
10
|
+
import { envNodeId } from '../../../shared/env.js';
|
|
10
11
|
import { execFileSync } from 'node:child_process';
|
|
11
12
|
import { resolve } from 'node:path';
|
|
12
13
|
import { dashboardRowsAllFromSource, enrichRowsFromSource, loadPreviewFromSource, renderForestFromSource } from '../render-source.js';
|
|
@@ -452,7 +453,7 @@ export async function runBrowse(opts) {
|
|
|
452
453
|
// ancestors so it is actually visible. No-op outside a node or when it's filtered
|
|
453
454
|
// out of the current tab/scope. Same move the boot performs, on demand (`.`).
|
|
454
455
|
const recenterOnSelf = () => {
|
|
455
|
-
const selfId =
|
|
456
|
+
const selfId = envNodeId();
|
|
456
457
|
if (selfId === undefined || selfId === '' || !tree.nodes.has(selfId))
|
|
457
458
|
return;
|
|
458
459
|
for (let p = tree.nodes.get(selfId)?.parentId ?? null; p !== null; p = tree.nodes.get(p)?.parentId ?? null) {
|
|
@@ -804,7 +805,7 @@ export async function runBrowse(opts) {
|
|
|
804
805
|
// not the top of the tree. Expand its ancestors so the row is actually visible,
|
|
805
806
|
// then home the cursor onto it. No-op when launched outside a node, or when the
|
|
806
807
|
// node is filtered out of the current tab/scope.
|
|
807
|
-
const selfId =
|
|
808
|
+
const selfId = envNodeId();
|
|
808
809
|
if (selfId !== undefined && selfId !== '' && tree.nodes.has(selfId)) {
|
|
809
810
|
for (let p = tree.nodes.get(selfId)?.parentId ?? null; p !== null; p = tree.nodes.get(p)?.parentId ?? null) {
|
|
810
811
|
state.collapsed.delete(p);
|