@north-light/crouter 0.3.232 → 0.3.234
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/builtin-memory/internal/memory-loading.md +4 -3
- 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 +532 -534
- 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 +0 -8
- 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 +5 -18
- 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/memory/__tests__/command-selector-and-mutation-guards.test.js +4 -4
- package/dist/commands/memory/find.js +2 -2
- package/dist/commands/memory/lint.js +2 -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 -1
- package/dist/commands/memory/shared.js +7 -10
- package/dist/commands/memory/write.js +4 -3
- package/dist/commands/node/bash.js +2 -1
- package/dist/commands/node/create.js +2 -1
- package/dist/commands/node/inspect.js +0 -6
- package/dist/commands/node/lifecycle.js +4 -13
- package/dist/commands/node/message.js +3 -2
- package/dist/commands/node/subscription.js +2 -1
- package/dist/commands/node/wait.js +4 -3
- 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 +2 -1
- package/dist/commands/pkg/shared.js +0 -2
- package/dist/commands/profile/delete.js +2 -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 +0 -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/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__/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 +0 -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/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 +50 -18
- 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/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.js +0 -2
- package/dist/core/canvas/render-source.js +31 -13
- package/dist/core/command-manifests/manifest.js +0 -10
- package/dist/core/command-manifests/schema.js +0 -4
- package/dist/core/command-plugins/transport/http-invoke.js +0 -14
- package/dist/core/command.js +0 -4
- package/dist/core/config.js +2 -1
- 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.js +0 -2
- 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 +5 -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/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.js +3 -6
- 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/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.d.ts +1 -0
- package/dist/core/substrate/__tests__/surface-match-pre-command.test.js +92 -0
- package/dist/core/substrate/frontmatter-validation.js +1 -1
- package/dist/core/substrate/injected-store.d.ts +6 -0
- package/dist/core/substrate/injected-store.js +24 -0
- package/dist/core/substrate/on-read.d.ts +17 -1
- package/dist/core/substrate/on-read.js +36 -2
- package/dist/core/substrate/render.js +0 -12
- package/dist/core/substrate/schema.d.ts +3 -3
- package/dist/core/substrate/schema.js +4 -16
- package/dist/core/substrate/surface-match.d.ts +19 -0
- package/dist/core/substrate/surface-match.js +60 -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__/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 +4 -8
- package/dist/daemon/api/handlers/nodes.js +10 -21
- package/dist/daemon/api/handlers/reports.js +1 -1
- package/dist/daemon/api/handlers/reviews.js +1 -6
- package/dist/daemon/api/map.js +0 -6
- 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 +26 -29
- 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/__tests__/pre-command-gate.test.d.ts +1 -0
- package/dist/pi-extensions/__tests__/pre-command-gate.test.js +220 -0
- package/dist/pi-extensions/broker-local.js +2 -1
- package/dist/pi-extensions/canvas-bash-valve.js +3 -17
- package/dist/pi-extensions/canvas-context-intro.js +2 -7
- package/dist/pi-extensions/canvas-doc-substrate.d.ts +14 -0
- package/dist/pi-extensions/canvas-doc-substrate.js +77 -5
- 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
|
@@ -64,13 +64,11 @@ test('flagship: full node lifecycle — spawn, dormancy, one-hop wake, promote,
|
|
|
64
64
|
{ timeout: 60_000 }, async () => {
|
|
65
65
|
const h = await createHeadlessHarness({ sessionPrefix: 'crtr-flagship' });
|
|
66
66
|
try {
|
|
67
|
-
// ===================================================================
|
|
68
67
|
// S1 — A's ROW SHAPE: base × RESIDENT (the user's front door). A spawnRoot
|
|
69
68
|
// fixture pins the PERSISTED row shape the scenario builds on; the real
|
|
70
69
|
// resident-birth boot path (bootRoot execs pi inline) is unreachable
|
|
71
70
|
// headlessly, exactly as in the tmux flagship. The resident BEHAVIOR (no
|
|
72
71
|
// idle-release) is exercised in live-mutation.test.ts.
|
|
73
|
-
// ===================================================================
|
|
74
72
|
const A = h.spawnRoot('top-level orchestrator');
|
|
75
73
|
{
|
|
76
74
|
const a = h.node(A);
|
|
@@ -79,11 +77,9 @@ test('flagship: full node lifecycle — spawn, dormancy, one-hop wake, promote,
|
|
|
79
77
|
assert.equal(a.status, 'active', 'A active');
|
|
80
78
|
assert.equal(a.intent ?? null, null, 'A no intent');
|
|
81
79
|
}
|
|
82
|
-
// ===================================================================
|
|
83
80
|
// S2 — A promotes: orchestrator × RESIDENT (top-level orchestrator). The
|
|
84
81
|
// real `node promote` verb; mode flips, lifecycle/status/intent
|
|
85
82
|
// untouched (no transition()).
|
|
86
|
-
// ===================================================================
|
|
87
83
|
{
|
|
88
84
|
const res = h.cli(A, ['node', 'promote']);
|
|
89
85
|
assert.equal(res.code, 0, `promote A exit 0\n${res.stderr}`);
|
|
@@ -93,13 +89,11 @@ test('flagship: full node lifecycle — spawn, dormancy, one-hop wake, promote,
|
|
|
93
89
|
assert.equal(a.status, 'active', 'A still active (promote does not transition)');
|
|
94
90
|
assert.equal(a.intent ?? null, null, 'A intent untouched by promote');
|
|
95
91
|
}
|
|
96
|
-
// ===================================================================
|
|
97
92
|
// S3 — A spawns terminal child B: base × TERMINAL. Fabricated as a broker-
|
|
98
93
|
// hosted node (pi_pid dead + pi_session_id set, so its later dormant
|
|
99
94
|
// wake RESUMES that session); the spine seed is wired explicitly
|
|
100
95
|
// (fabrication does not auto-subscribe). B is born acked to its own
|
|
101
96
|
// persona (commitPersonaAck), mirroring a real birth.
|
|
102
|
-
// ===================================================================
|
|
103
97
|
const B = h.fabricateBrokerNode({
|
|
104
98
|
parent: A, kind: 'developer', mode: 'base', lifecycle: 'terminal',
|
|
105
99
|
status: 'active', pi_pid: deadPid(), pi_session_id: 'sess-B',
|
|
@@ -117,10 +111,8 @@ test('flagship: full node lifecycle — spawn, dormancy, one-hop wake, promote,
|
|
|
117
111
|
assert.deepEqual(h.subscribers(B), [{ node_id: A, active: true }], "A is B's sole ACTIVE subscriber (spine seed)");
|
|
118
112
|
assert.equal(h.inbox(A).length, 0, 'A inbox empty (spawn pushes nothing)');
|
|
119
113
|
}
|
|
120
|
-
// ===================================================================
|
|
121
114
|
// S4 — B spawns child C (base × terminal). B now holds an active live
|
|
122
115
|
// subscription to C → B is legitimately "awaiting" if it stops.
|
|
123
|
-
// ===================================================================
|
|
124
116
|
const C = h.fabricateBrokerNode({ parent: B, mode: 'base', lifecycle: 'terminal', status: 'active' });
|
|
125
117
|
subscribe(B, C, true);
|
|
126
118
|
{
|
|
@@ -132,7 +124,6 @@ test('flagship: full node lifecycle — spawn, dormancy, one-hop wake, promote,
|
|
|
132
124
|
assert.deepEqual(h.subscriptions(B), [{ node_id: C, active: true }], 'B subscribes_to C (active)');
|
|
133
125
|
assert.equal(h.status(B), 'active', 'B still active');
|
|
134
126
|
}
|
|
135
|
-
// ===================================================================
|
|
136
127
|
// S5 — B goes DORMANT. PURE precondition: B is TERMINAL holding an active
|
|
137
128
|
// live sub to C, so the stop-guard classifies its stop 'awaiting'
|
|
138
129
|
// (legitimate idle-release). We then enact the post-stop MODEL state
|
|
@@ -140,7 +131,6 @@ test('flagship: full node lifecycle — spawn, dormancy, one-hop wake, promote,
|
|
|
140
131
|
// broker is a dead pid; pi_pid was fabricated dead at S3). The actual
|
|
141
132
|
// agent_end idle-release COMMIT is locked by canvas-stophook-agentend
|
|
142
133
|
// ('natural stop while awaiting a live worker → idle-release …').
|
|
143
|
-
// ===================================================================
|
|
144
134
|
{
|
|
145
135
|
const stop = evaluateStop(B, { pushedFinal: false, requestedHumanReply: false });
|
|
146
136
|
assert.equal(stop.action, 'allow', 'awaiting terminal B → allow (legit idle-release)');
|
|
@@ -155,11 +145,9 @@ test('flagship: full node lifecycle — spawn, dormancy, one-hop wake, promote,
|
|
|
155
145
|
assert.equal(h.status(C), 'active', 'C still active while B sleeps');
|
|
156
146
|
assert.equal(h.status(A), 'active', 'A still active (a dormant worker does not disturb its manager)');
|
|
157
147
|
assert.equal(h.inbox(A).length, 0, 'A inbox still empty');
|
|
158
|
-
// ===================================================================
|
|
159
148
|
// S6 — C FINISHES (pushFinal). The pointer fans to subscribersOf(C) = {B}
|
|
160
149
|
// ONLY. C → done/done. A is NOT a subscriber of C → A inbox stays
|
|
161
150
|
// empty — the one-hop wake contract at the push site.
|
|
162
|
-
// ===================================================================
|
|
163
151
|
await pushFinal(C, 'C result body');
|
|
164
152
|
{
|
|
165
153
|
const c = h.node(C);
|
|
@@ -169,7 +157,6 @@ test('flagship: full node lifecycle — spawn, dormancy, one-hop wake, promote,
|
|
|
169
157
|
assert.ok(cFinal, "B inbox received C's final pointer (one hop)");
|
|
170
158
|
assert.equal(h.inbox(A).length, 0, 'A inbox empty — A NOT woken by C (one-hop)');
|
|
171
159
|
}
|
|
172
|
-
// ===================================================================
|
|
173
160
|
// S7 — WAKE B. The in-process daemon second pass sees B idle/idle-release,
|
|
174
161
|
// pi dead, an unseen inbox entry → reviveNode(resume). reviveNode
|
|
175
162
|
// bumps cycles + transition('revive') BEFORE the (detached, unawaited)
|
|
@@ -177,7 +164,6 @@ test('flagship: full node lifecycle — spawn, dormancy, one-hop wake, promote,
|
|
|
177
164
|
// awaitBoot. (The real inbox-watcher's redelivery of C's report on the
|
|
178
165
|
// fresh boot is an engine artifact, out of the model tier; the wake
|
|
179
166
|
// DECISION is locked here by cycles + the S6 inbox entry.)
|
|
180
|
-
// ===================================================================
|
|
181
167
|
assert.equal(h.node(B).cycles ?? 0, 0, 'B not yet revived');
|
|
182
168
|
await h.tick();
|
|
183
169
|
{
|
|
@@ -188,10 +174,8 @@ test('flagship: full node lifecycle — spawn, dormancy, one-hop wake, promote,
|
|
|
188
174
|
}
|
|
189
175
|
assert.equal(h.inbox(A).length, 0, 'A inbox STILL empty after B woke (one-hop confirmed)');
|
|
190
176
|
assert.equal(h.status(A), 'active', 'A unchanged');
|
|
191
|
-
// ===================================================================
|
|
192
177
|
// S8 — B PROMOTES: base → orchestrator, lifecycle TERMINAL unchanged
|
|
193
178
|
// (no --resident). orchestrator × terminal (the sub-orchestrator).
|
|
194
|
-
// ===================================================================
|
|
195
179
|
{
|
|
196
180
|
const res = h.cli(B, ['node', 'promote', '--kind', 'developer']);
|
|
197
181
|
assert.equal(res.code, 0, `promote B exit 0\n${res.stderr}`);
|
|
@@ -201,11 +185,9 @@ test('flagship: full node lifecycle — spawn, dormancy, one-hop wake, promote,
|
|
|
201
185
|
assert.equal(b.status, 'active', 'B still active');
|
|
202
186
|
assert.equal(b.intent ?? null, null, 'B intent untouched by promote');
|
|
203
187
|
}
|
|
204
|
-
// ===================================================================
|
|
205
188
|
// S9 — The next provider request must first deliver the pending persona
|
|
206
189
|
// transition. Independently, agent_end would stall this orchestrator
|
|
207
190
|
// because it has no live subscription and has not pushed a final.
|
|
208
|
-
// ===================================================================
|
|
209
191
|
{
|
|
210
192
|
const drift = personaDrift(B);
|
|
211
193
|
assert.ok(drift !== null, 'the provider gate has a pending persona transition');
|
|
@@ -217,13 +199,11 @@ test('flagship: full node lifecycle — spawn, dormancy, one-hop wake, promote,
|
|
|
217
199
|
assert.equal(stop.reason, 'stalled', 'classified stalled');
|
|
218
200
|
assert.equal(stop.action === 'reprompt' ? stop.message : '', formatCard('stop-guard', {}, STALL_REPROMPT), 'the stall reprompt nudge, delivered as a whole-message stop-guard envelope');
|
|
219
201
|
}
|
|
220
|
-
// ===================================================================
|
|
221
202
|
// S10 — B YIELDS. The real plain `node yield` verb sets intent=refresh
|
|
222
203
|
// while preserving mode (already orchestrator); persona survives the request. We
|
|
223
204
|
// then model the daemon's completed refresh-revive with transition
|
|
224
205
|
// ('revive') (the in-place fresh boot is engine; its model effect —
|
|
225
206
|
// active, intent cleared, persona preserved — is asserted here).
|
|
226
|
-
// ===================================================================
|
|
227
207
|
{
|
|
228
208
|
const res = h.cli(B, ['node', 'yield', 'refresh against the roadmap']);
|
|
229
209
|
assert.equal(res.code, 0, `node yield exit 0\n${res.stderr}`);
|
|
@@ -240,9 +220,7 @@ test('flagship: full node lifecycle — spawn, dormancy, one-hop wake, promote,
|
|
|
240
220
|
assert.equal(b.mode, 'orchestrator', 'B mode survives the refresh');
|
|
241
221
|
assert.equal(b.lifecycle, 'terminal', 'B lifecycle survives the refresh');
|
|
242
222
|
}
|
|
243
|
-
// ===================================================================
|
|
244
223
|
// S11 — B spawns child D (base × terminal). New spine seed B→D active.
|
|
245
|
-
// ===================================================================
|
|
246
224
|
const D = h.fabricateBrokerNode({ parent: B, mode: 'base', lifecycle: 'terminal', status: 'active' });
|
|
247
225
|
subscribe(B, D, true);
|
|
248
226
|
{
|
|
@@ -252,20 +230,16 @@ test('flagship: full node lifecycle — spawn, dormancy, one-hop wake, promote,
|
|
|
252
230
|
assert.equal(d.status, 'active', 'D active');
|
|
253
231
|
assert.ok(h.subscriptions(B).some((s) => s.node_id === D && s.active), 'B subscribes_to D (active)');
|
|
254
232
|
}
|
|
255
|
-
// ===================================================================
|
|
256
233
|
// S12 — D FINISHES → pointer to B (its only subscriber).
|
|
257
|
-
// ===================================================================
|
|
258
234
|
await pushFinal(D, 'D result body');
|
|
259
235
|
{
|
|
260
236
|
assert.equal(h.node(D).status, 'done', 'D done');
|
|
261
237
|
const dFinal = h.inbox(B).find((e) => e.from === D && e.kind === 'final');
|
|
262
238
|
assert.ok(dFinal, "B inbox received D's final pointer");
|
|
263
239
|
}
|
|
264
|
-
// ===================================================================
|
|
265
240
|
// S13 — B pushes its FINAL up the spine → done/done; the pointer fans to
|
|
266
241
|
// subscribersOf(B) = {A}. NOW A finally hears B (its own explicit
|
|
267
242
|
// push — the only way the chain propagates a hop).
|
|
268
|
-
// ===================================================================
|
|
269
243
|
await pushFinal(B, 'all work complete');
|
|
270
244
|
{
|
|
271
245
|
const b = h.node(B);
|
|
@@ -47,7 +47,6 @@ function persona(m) {
|
|
|
47
47
|
return { mode: m.mode, lifecycle: m.lifecycle };
|
|
48
48
|
}
|
|
49
49
|
const FULL_CLI_TEST_TIMEOUT_MS = 180_000;
|
|
50
|
-
// ===========================================================================
|
|
51
50
|
// (b) THE demote / recycle SPLIT — two DISTINCT verbs:
|
|
52
51
|
// • `node lifecycle demote` flips a LIVE node's lifecycle→TERMINAL IN PLACE — it keeps
|
|
53
52
|
// its MODE and parentage, keeps running (active), is NOT finalized; it now
|
|
@@ -57,7 +56,6 @@ const FULL_CLI_TEST_TIMEOUT_MS = 180_000;
|
|
|
57
56
|
// node keeps mode=orchestrator (it is merely `done`).
|
|
58
57
|
// This test drives demote via the real CLI verb and recycle via the real
|
|
59
58
|
// recycleNode, pinning each behavior at the model layer.
|
|
60
|
-
// ===========================================================================
|
|
61
59
|
test('node lifecycle demote flips lifecycle→terminal IN PLACE; node lifecycle recycle is FINISH+RECYCLE',
|
|
62
60
|
// Drives several full-CLI subprocess spawns (each a cold tsx compile of the
|
|
63
61
|
// whole CLI) + recycleNode; this is a subprocess-heavy regression and gets a
|
|
@@ -118,13 +116,11 @@ test('node lifecycle demote flips lifecycle→terminal IN PLACE; node lifecycle
|
|
|
118
116
|
await h.dispose();
|
|
119
117
|
}
|
|
120
118
|
});
|
|
121
|
-
// ===========================================================================
|
|
122
119
|
// (b) A4 BOUNDARY — plain yield is mode-preserving; `yield --promote` is the
|
|
123
120
|
// explicit one-step promotion + refresh. A plain yield sets intent=refresh
|
|
124
121
|
// only, leaves the base persona acknowledged, and does NOT seed a roadmap.
|
|
125
122
|
// With `--promote`, the mode flips and a roadmap is seeded; persona drift
|
|
126
123
|
// remains pending until the replacement context persists its baseline.
|
|
127
|
-
// ===========================================================================
|
|
128
124
|
test('A4: a plain yield preserves base mode and does not seed a roadmap',
|
|
129
125
|
// Full-CLI subprocess spawn under load — see the sibling test's budget note.
|
|
130
126
|
{ timeout: FULL_CLI_TEST_TIMEOUT_MS }, async () => {
|
|
@@ -42,13 +42,11 @@ function persona(m) {
|
|
|
42
42
|
return { mode: m.mode, lifecycle: m.lifecycle };
|
|
43
43
|
}
|
|
44
44
|
const SIGNALS = { pushedFinal: false, requestedHumanReply: false };
|
|
45
|
-
// ===========================================================================
|
|
46
45
|
// (a) LIFECYCLE FLIP — `crtr node config --lifecycle` on a LIVE node, both directions,
|
|
47
46
|
// observing the idle-release DECISION change. A round-trip on ONE node
|
|
48
47
|
// (terminal→resident→terminal) proves both directions: a flipped-resident
|
|
49
48
|
// node no longer idle-releases (evaluateStop → dormant); flipped-terminal it
|
|
50
49
|
// idle-releases again (evaluateStop → awaiting, on its active live sub).
|
|
51
|
-
// ===========================================================================
|
|
52
50
|
test('live lifecycle flip: terminal→resident suppresses idle-release; resident→terminal restores it',
|
|
53
51
|
// Full-CLI subprocess spawns under load — 20s blew on the slower CI runner
|
|
54
52
|
// (passes fast locally); 60s = safe margin. Same class as live-mutation-verbs.
|
|
@@ -102,7 +100,7 @@ test('live lifecycle flip: terminal→resident suppresses idle-release; resident
|
|
|
102
100
|
assert.equal(stop.action, 'allow', 'resident B → allow');
|
|
103
101
|
assert.equal(stop.reason, 'dormant', 'resident B → dormant, NOT awaiting — idle-release SUPPRESSED by the live flip');
|
|
104
102
|
}
|
|
105
|
-
//
|
|
103
|
+
// FLIP 2: resident → TERMINAL (live), on the now-resident node.
|
|
106
104
|
{
|
|
107
105
|
const res = h.cli(B, ['node', 'config', '--lifecycle', 'terminal', '--node', B]);
|
|
108
106
|
assert.equal(res.code, 0, `config --lifecycle terminal exit 0\n${res.stderr}`);
|
|
@@ -124,11 +122,9 @@ test('live lifecycle flip: terminal→resident suppresses idle-release; resident
|
|
|
124
122
|
await h.dispose();
|
|
125
123
|
}
|
|
126
124
|
});
|
|
127
|
-
// ===========================================================================
|
|
128
125
|
// (b) MODE FLIP — promote: base → orchestrator on a LIVE node. Promotion leaves
|
|
129
126
|
// persona_ack pending; the next provider request appends and persists the
|
|
130
127
|
// transition before a guarded ack. This test pins that model precondition.
|
|
131
|
-
// ===========================================================================
|
|
132
128
|
test('live mode flip: promote base→orchestrator leaves persona_ack pending for provider-gated delivery',
|
|
133
129
|
// Full-CLI subprocess spawn under load — see the sibling test's budget note.
|
|
134
130
|
{ timeout: 60_000 }, async () => {
|
|
@@ -25,9 +25,7 @@ import { createHeadlessHarness } from '../helpers/harness.js';
|
|
|
25
25
|
import { recordPid } from '../../canvas/canvas.js';
|
|
26
26
|
import { isPidAlive } from '../../canvas/pid.js';
|
|
27
27
|
import { yieldStallVerdict, YIELD_STALL_GRACE_MS } from '../../../daemon/reconcilers/broker-supervision.js';
|
|
28
|
-
// ---------------------------------------------------------------------------
|
|
29
28
|
// D4a — the pure refresh-stall truth table.
|
|
30
|
-
// ---------------------------------------------------------------------------
|
|
31
29
|
test('D4a yieldStallVerdict: kill ONLY a live, not-busy, refresh engine past the grace', () => {
|
|
32
30
|
const G = YIELD_STALL_GRACE_MS;
|
|
33
31
|
// The one 'kill' case: engine alive, intent=refresh, turn over (not busy), past grace.
|
|
@@ -35,9 +33,7 @@ test('D4a yieldStallVerdict: kill ONLY a live, not-busy, refresh engine past the
|
|
|
35
33
|
// A working engine is NEVER killed, even past the grace.
|
|
36
34
|
assert.equal(yieldStallVerdict(true, 'refresh', true, G + 60_000), 'leave', 'busy (mid-turn) → leave');
|
|
37
35
|
});
|
|
38
|
-
// ---------------------------------------------------------------------------
|
|
39
36
|
// D4b — daemon integration: a stuck refresh on a LIVE engine is force-recycled.
|
|
40
|
-
// ---------------------------------------------------------------------------
|
|
41
37
|
const T0 = 7_000_000;
|
|
42
38
|
const T_KILL = T0 + YIELD_STALL_GRACE_MS + 1; // one ms past the grace from t0
|
|
43
39
|
test('D4b daemon force-recycles a stuck refresh: a live, not-busy refresh engine past the grace is SIGTERM\'d', { timeout: 30_000 }, async () => {
|
|
@@ -68,7 +64,7 @@ test('D4b daemon force-recycles a stuck refresh: a live, not-busy refresh engine
|
|
|
68
64
|
h.fleet.register(id, { pid: enginePid, exited: new Promise(() => { }) });
|
|
69
65
|
assert.equal(h.node(id).intent, 'refresh', 'precondition: intent=refresh pending');
|
|
70
66
|
assert.notEqual(h.node(id).pi_pid_identity, null, 'precondition: live engine has a recorded identity');
|
|
71
|
-
//
|
|
67
|
+
// TICK @ t0: arms the stall clock (yieldStallSince). No kill yet.
|
|
72
68
|
await h.tick(T0);
|
|
73
69
|
assert.equal(isPidAlive(enginePid), true, 'within grace: the engine is NOT killed (clock just armed)');
|
|
74
70
|
// --- TICK past the grace: the daemon concludes the refresh stalled and
|
|
@@ -104,9 +104,7 @@ after(() => {
|
|
|
104
104
|
}
|
|
105
105
|
}
|
|
106
106
|
});
|
|
107
|
-
// ---------------------------------------------------------------------------
|
|
108
107
|
// resumeArgs — the pure resume-source selection
|
|
109
|
-
// ---------------------------------------------------------------------------
|
|
110
108
|
test('resumeArgs resumes by the absolute file path when it exists on disk', () => {
|
|
111
109
|
mkdirSync(home, { recursive: true });
|
|
112
110
|
const sessionFile = join(home, 'resume-args-exists.jsonl');
|
|
@@ -144,9 +142,7 @@ test('resumeArgs selects neither source on a refresh revive with no `.jsonl` on
|
|
|
144
142
|
});
|
|
145
143
|
assert.deepEqual(resumeArgs(m, false), {});
|
|
146
144
|
});
|
|
147
|
-
// ---------------------------------------------------------------------------
|
|
148
145
|
// buildPiArgv — the `--session` argument the revive builds
|
|
149
|
-
// ---------------------------------------------------------------------------
|
|
150
146
|
test('buildPiArgv resumes by ABSOLUTE path when pi_session_file exists on disk', () => {
|
|
151
147
|
mkdirSync(home, { recursive: true });
|
|
152
148
|
const sessionFile = join(home, 'build-argv-exists.jsonl');
|
|
@@ -169,10 +165,8 @@ test('buildPiArgv passes no --session on a fresh (no-resume) launch', () => {
|
|
|
169
165
|
assert.ok(!argv.includes('--session'), 'a fresh launch never resumes');
|
|
170
166
|
assert.ok(argv.includes('go'), 'the kickoff prompt is the last positional');
|
|
171
167
|
});
|
|
172
|
-
// ---------------------------------------------------------------------------
|
|
173
168
|
// buildPiArgv — the resumed argv replays the persisted launch spec faithfully
|
|
174
169
|
// (kind/mode/lifecycle come back as the node's *current* self).
|
|
175
|
-
// ---------------------------------------------------------------------------
|
|
176
170
|
test('a resumed revive replays the persisted LaunchSpec (model, tools, extensions, prompt, env)', () => {
|
|
177
171
|
mkdirSync(home, { recursive: true });
|
|
178
172
|
const sessionFile = join(home, 'launchspec-replay.jsonl');
|
|
@@ -211,13 +205,11 @@ test('a resumed revive replays the persisted LaunchSpec (model, tools, extension
|
|
|
211
205
|
assert.equal(env['CRTR_KIND'], 'developer');
|
|
212
206
|
assert.equal(env['CRTR_MODE'], 'orchestrator');
|
|
213
207
|
});
|
|
214
|
-
// ---------------------------------------------------------------------------
|
|
215
208
|
// Double-revive guard — reviveNode no-ops when the node has a live FLEET ENTRY
|
|
216
209
|
// (design D-1: the entry IS liveness). Fabricated as a BROKER node with a
|
|
217
210
|
// fabricated entry registered in the harness's in-process fleet, so the guard
|
|
218
211
|
// is exercised with NO tmux and NO real boot. An entry → guard short-circuits
|
|
219
212
|
// (cycles flat); no entry → revive proceeds (cycles bumped before the launch).
|
|
220
|
-
// ---------------------------------------------------------------------------
|
|
221
213
|
test('reviveNode no-ops when the node has a live fleet entry — double-revive guard', async () => {
|
|
222
214
|
const h = await createHarness({ headless: true, sessionPrefix: 'crtr-revive-guard' });
|
|
223
215
|
try {
|
|
@@ -272,14 +264,12 @@ test('reviveNode PROCEEDS when the node has NO fleet entry — cycle counter bum
|
|
|
272
264
|
await h.dispose();
|
|
273
265
|
}
|
|
274
266
|
});
|
|
275
|
-
// ---------------------------------------------------------------------------
|
|
276
267
|
// BUG LOCKED — a recovery relaunch must not eat the node's armed deadline.
|
|
277
268
|
// A node armed `crtr node wait deadline` and dormed; its broker then exited and
|
|
278
269
|
// the daemon respawned it (fleet.#respawn → reviveNode). reviveNode used to
|
|
279
270
|
// cancel every cancel-on-wake cron unconditionally, so the deadline vanished
|
|
280
271
|
// with nothing having woken the node and the wait could never settle. A wake
|
|
281
272
|
// (inbox delivery, attach, explicit revive) still consumes it.
|
|
282
|
-
// ---------------------------------------------------------------------------
|
|
283
273
|
function armDeadline(anchor, cronId) {
|
|
284
274
|
armCron({
|
|
285
275
|
cron_id: cronId,
|
|
@@ -449,11 +439,9 @@ test('reviveNode crashes and clears pid when launch/preflight throws', async ()
|
|
|
449
439
|
await h.dispose();
|
|
450
440
|
}
|
|
451
441
|
});
|
|
452
|
-
// ---------------------------------------------------------------------------
|
|
453
442
|
// Crash-to-dead doctrine wake — a launch-refusal marks the node dead; a
|
|
454
443
|
// manager waiting dormant on it must be told, or it hangs forever (the
|
|
455
444
|
// daemon no longer supervises a dead node). Mirrors closeNode's step-4 fan-out.
|
|
456
|
-
// ---------------------------------------------------------------------------
|
|
457
445
|
test('reviveNode fans a doctrine wake to subscribers when launch/preflight throws (crash-to-dead)', async () => {
|
|
458
446
|
const h = await createHarness({ headless: true, sessionPrefix: 'crtr-revive-crashwake' });
|
|
459
447
|
const originalLaunch = headlessBrokerHost.launch;
|
|
@@ -59,11 +59,9 @@ function edgeSet(arr) {
|
|
|
59
59
|
test('one daemon tick revives the active subscriber of a finished target and leaves the passive one dormant', { timeout: 30_000 }, async () => {
|
|
60
60
|
const h = await createHarness({ headless: true, sessionPrefix: 'crtr-subdeliv' });
|
|
61
61
|
try {
|
|
62
|
-
// ===================================================================
|
|
63
62
|
// Build the graph by FABRICATION (no boots). A is a resident root
|
|
64
63
|
// (inline, daemon-untouched); B/T/K/P are broker nodes whose liveness
|
|
65
64
|
// fields are set directly. Each parent→child spine edge is wired ACTIVE.
|
|
66
|
-
// ===================================================================
|
|
67
65
|
const A = h.spawnRoot('front door'); // resident root, active, no placement
|
|
68
66
|
// T (the target) and K (keepalive) are LIVE-active workers; B and P start
|
|
69
67
|
// already DORMANT (idle-released, broker exited = dead pid) — exactly the
|
|
@@ -88,20 +86,16 @@ test('one daemon tick revives the active subscriber of a finished target and lea
|
|
|
88
86
|
assert.equal(passive(P).length, 0, 'P passive accumulator empty before the push');
|
|
89
87
|
assert.equal(h.node(B).cycles ?? 0, 0, 'B not yet revived');
|
|
90
88
|
assert.equal(h.node(P).cycles ?? 0, 0, 'P not yet revived');
|
|
91
|
-
// ===================================================================
|
|
92
89
|
// T FINISHES via the model layer — the precondition the daemon decides on:
|
|
93
90
|
// B's pointer lands in inbox.jsonl (the wake channel), P's in passive.jsonl.
|
|
94
|
-
// ===================================================================
|
|
95
91
|
await pushFinal(T, 'TARGET-FINAL-BODY: the worker completed');
|
|
96
92
|
closeDb();
|
|
97
93
|
assert.ok(h.inbox(B).find((e) => e.from === T && e.kind === 'final'), 'ACTIVE subscriber B: T-final pointer delivered to inbox.jsonl');
|
|
98
94
|
assert.equal(h.inbox(P).length, 0, 'PASSIVE subscriber P: inbox.jsonl stays empty (no wake channel)');
|
|
99
95
|
assert.equal(passive(P).length, 1, 'PASSIVE subscriber P: exactly one passive entry');
|
|
100
|
-
// ===================================================================
|
|
101
96
|
// The DAEMON decision pass. Its 2nd pass revives an idle-released node
|
|
102
97
|
// ONLY on an unseen INBOX entry. B has one (active) → REVIVED. P has none
|
|
103
98
|
// (its entry went to passive.jsonl) → stays idle. Decided in ONE tick.
|
|
104
|
-
// ===================================================================
|
|
105
99
|
await h.tick();
|
|
106
100
|
// ACTIVE subscriber: WOKEN — reviveNode bumped cycles + transition(revive).
|
|
107
101
|
{
|
|
@@ -84,12 +84,13 @@ test('§2.2 driver invariant: every placement verb in tmux-driver.ts passes an e
|
|
|
84
84
|
// three (new-window / split-window / respawn-pane).
|
|
85
85
|
assert.ok(found >= 3, `expected to scan ≥3 placement verbs, saw ${found}`);
|
|
86
86
|
});
|
|
87
|
-
//
|
|
88
|
-
//
|
|
89
|
-
//
|
|
90
|
-
//
|
|
91
|
-
//
|
|
92
|
-
//
|
|
87
|
+
// Lint guard — §5.1, ENFORCED. The driver's PLACEMENT verbs (tmux-driver.ts)
|
|
88
|
+
// are imported only by placement.ts, placement-tmux.ts, tmux-chrome.ts, and
|
|
89
|
+
// tmux-bindings.ts. Every other module — runtime, daemon, commands, stophook,
|
|
90
|
+
// and tests — must route through those sanctioned seams, and may import only
|
|
91
|
+
// the two read-only reachability probes: those answer a question, drive no
|
|
92
|
+
// pane, and are the very calls tmux-driver-boundary.test.ts requires every
|
|
93
|
+
// would-be direct `spawnSync('tmux', …)` prober to route through instead.
|
|
93
94
|
/** Every `.ts` file under `src` (recursively), excluding nothing. */
|
|
94
95
|
function allTsFiles(dir) {
|
|
95
96
|
const out = [];
|
|
@@ -102,13 +103,28 @@ function allTsFiles(dir) {
|
|
|
102
103
|
}
|
|
103
104
|
return out;
|
|
104
105
|
}
|
|
105
|
-
/** Files (basename) sanctioned to import the driver, per §2.1. */
|
|
106
|
+
/** Files (basename) sanctioned to import the driver's full surface, per §2.1. */
|
|
106
107
|
const ALLOWED_IMPORTERS = new Set(['placement.ts', 'placement-tmux.ts', 'tmux-chrome.ts', 'tmux-bindings.ts']);
|
|
108
|
+
/** The read-only probes any module may import — they drive no pane. */
|
|
109
|
+
const PROBE_IMPORTS = new Set(['tmuxSessionsReachable', 'tmuxClientsReachable']);
|
|
107
110
|
function importsDriver(file) {
|
|
108
111
|
const src = readFileSync(file, 'utf8');
|
|
109
112
|
// A specifier whose basename is exactly `tmux-driver.js` (so
|
|
110
113
|
// `tmux-bindings.js` is not matched). Covers `from '...'` and `import('...')`.
|
|
111
|
-
|
|
114
|
+
const specifiers = [...src.matchAll(/(?:from|import\s*\()\s*'([^']+)'/g)].filter((m) => basename(m[1]) === 'tmux-driver.js');
|
|
115
|
+
if (specifiers.length === 0)
|
|
116
|
+
return false;
|
|
117
|
+
// Every reference must be a named import clause naming only the probes; any
|
|
118
|
+
// other shape (a placement verb, a namespace, a re-export, a dynamic import)
|
|
119
|
+
// is a direct use of the driver.
|
|
120
|
+
const clauses = [...src.matchAll(/import\s+(?:type\s+)?\{([^}]*)\}\s*from\s*'([^']+)'/g)].filter((m) => basename(m[2]) === 'tmux-driver.js');
|
|
121
|
+
if (clauses.length !== specifiers.length)
|
|
122
|
+
return true;
|
|
123
|
+
return !clauses.every((m) => m[1]
|
|
124
|
+
.split(',')
|
|
125
|
+
.map((name) => name.trim().split(/\s+as\s+/)[0].trim())
|
|
126
|
+
.filter((name) => name !== '')
|
|
127
|
+
.every((name) => PROBE_IMPORTS.has(name)));
|
|
112
128
|
}
|
|
113
129
|
test('§5.1 lint: only sanctioned seams import the tmux driver', () => {
|
|
114
130
|
const offenders = allTsFiles(SRC_ROOT)
|
|
@@ -252,7 +252,6 @@ test('autoDropCleanManagedWorktree refuses when the recorded base branch no long
|
|
|
252
252
|
assert.ok(existsSync(wt.path));
|
|
253
253
|
rmSync(repo, { recursive: true, force: true });
|
|
254
254
|
});
|
|
255
|
-
// ---------------------------------------------------------------------------
|
|
256
255
|
// The Critical fix: the TOCTOU race between proving clean-and-contained and
|
|
257
256
|
// the destructive branch delete. `proveCleanEmptyManagedWorktree` and
|
|
258
257
|
// `autoDropCleanManagedWorktreeLocked` are exported specifically so these
|
|
@@ -260,7 +259,6 @@ test('autoDropCleanManagedWorktree refuses when the recorded base branch no long
|
|
|
260
259
|
// exactly the window the repository lock does NOT close (it only serializes
|
|
261
260
|
// crtr's own worktree mutations, not ordinary `git commit`/`git reset`/an
|
|
262
261
|
// external base rewrite run by any other process against the checkout).
|
|
263
|
-
// ---------------------------------------------------------------------------
|
|
264
262
|
// A concurrent commit lands on the managed branch after the predicate proved
|
|
265
263
|
// it clean-and-contained, but before cleanup deletes it. The atomic
|
|
266
264
|
// compare-and-delete must refuse, leaving the branch (and the new commit)
|
|
@@ -326,7 +324,6 @@ test('the re-verified base containment refuses when the base is rewritten betwee
|
|
|
326
324
|
assert.equal(gitSync(['show-ref', '--verify', '--quiet', `refs/heads/${wt.branch}`], repo).status, 0, 'the branch ref must still exist — nothing was lost');
|
|
327
325
|
rmSync(repo, { recursive: true, force: true });
|
|
328
326
|
});
|
|
329
|
-
// ---------------------------------------------------------------------------
|
|
330
327
|
// The base-ref TOCTOU: a branch-only compare-and-delete refuses when the
|
|
331
328
|
// BRANCH moves, but a base force-reset between the containment proof and the
|
|
332
329
|
// delete could still leave the delete's own condition (the branch equals the
|
|
@@ -338,7 +335,6 @@ test('the re-verified base containment refuses when the base is rewritten betwee
|
|
|
338
335
|
// re-check and its ref-deletion transaction — the same technique used above
|
|
339
336
|
// for the branch-only race, and the same one the independent re-review used
|
|
340
337
|
// to reproduce this exact Critical against `6de1c8d`.
|
|
341
|
-
// ---------------------------------------------------------------------------
|
|
342
338
|
test('close-landed atomic delete refuses when the base is force-reset between landing and the cleanup delete', () => {
|
|
343
339
|
const repo = mkdtempSync(join(tmpdir(), 'crtr-worktree-close-base-window-'));
|
|
344
340
|
run(['init', '-b', 'main'], repo);
|
|
@@ -387,13 +383,11 @@ PATH="$CRTR_TEST_REAL_PATH" exec git "$@"
|
|
|
387
383
|
assert.equal(refCheck.stdout.trim(), result.landed_sha, 'the preserved branch must still name the landed commit — no orphaned landed commit');
|
|
388
384
|
rmSync(repo, { recursive: true, force: true });
|
|
389
385
|
});
|
|
390
|
-
// ---------------------------------------------------------------------------
|
|
391
386
|
// Checkout-removal refusal: `bestEffortRemoveWorktreeCheckout` can report
|
|
392
387
|
// `worktreeRemoved: false` (real Git refused `worktree remove` because a
|
|
393
388
|
// file reappeared after the clean predicate ran). That refusal must itself
|
|
394
389
|
// refuse the whole cleanup — never reach the low-level ref-deletion
|
|
395
390
|
// transaction, which bypasses Git's own checked-out-branch protection.
|
|
396
|
-
// ---------------------------------------------------------------------------
|
|
397
391
|
test('auto-drop refuses and preserves the branch and checkout when checkout removal is refused after the clean predicate', async () => {
|
|
398
392
|
const { proveCleanEmptyManagedWorktree, autoDropCleanManagedWorktreeLocked } = await import('../../worktree.js');
|
|
399
393
|
const nodeId = 'wt-auto-drop-removal-refused';
|
|
@@ -48,9 +48,7 @@ after(() => {
|
|
|
48
48
|
const ALL = ['active', 'idle', 'done', 'dead', 'canceled'];
|
|
49
49
|
const LIVE = ['active', 'idle'];
|
|
50
50
|
const TERMINAL = ['done', 'dead', 'canceled'];
|
|
51
|
-
// ---------------------------------------------------------------------------
|
|
52
51
|
// finalize → done + intent='done', legal only from active|idle
|
|
53
|
-
// ---------------------------------------------------------------------------
|
|
54
52
|
test('finalize: active|idle → done + intent=done', () => {
|
|
55
53
|
for (const from of LIVE) {
|
|
56
54
|
mk(`n_${from}`, from, 'refresh');
|
|
@@ -66,11 +64,9 @@ test('finalize: illegal from done|dead|canceled → throws, status untouched', (
|
|
|
66
64
|
assert.equal(getNode(`n_${from}`)?.status, from, `${from} unchanged on illegal finalize`);
|
|
67
65
|
}
|
|
68
66
|
});
|
|
69
|
-
// ---------------------------------------------------------------------------
|
|
70
67
|
// park → done + intent='parked', legal only from active|idle. The intent IS the
|
|
71
68
|
// durable park marker: it must land atomically with the `done` it qualifies,
|
|
72
69
|
// read only through isParked, and be cleared by revival.
|
|
73
|
-
// ---------------------------------------------------------------------------
|
|
74
70
|
test('park: active|idle → done + intent=parked, and isParked reads exactly that pair', () => {
|
|
75
71
|
for (const from of LIVE) {
|
|
76
72
|
mk(`n_${from}`, from);
|
|
@@ -94,13 +90,11 @@ test('park: revival clears the marker, and no other done is parked', () => {
|
|
|
94
90
|
mk('finished', 'active');
|
|
95
91
|
assert.equal(isParked(transition('finished', 'finish')), false, 'a finished done is not a park');
|
|
96
92
|
});
|
|
97
|
-
// ---------------------------------------------------------------------------
|
|
98
93
|
// cancel → canceled + intent cleared, legal from ANY status (forced teardown).
|
|
99
94
|
// A5 (human-confirmed 2026-06-06): the `reap` event was COLLAPSED into `cancel`
|
|
100
95
|
// — they were identical (status differed only; both intent=null, from=ANY, no
|
|
101
96
|
// side effects in transition()). Every external reap (close cascade AND
|
|
102
97
|
// reset/relaunch) now routes through `cancel`; `done` is reserved for finalize.
|
|
103
|
-
// ---------------------------------------------------------------------------
|
|
104
98
|
test('cancel: any status → canceled + intent cleared', () => {
|
|
105
99
|
for (const from of ALL) {
|
|
106
100
|
mk(`n_${from}`, from, 'idle-release');
|
|
@@ -109,9 +103,7 @@ test('cancel: any status → canceled + intent cleared', () => {
|
|
|
109
103
|
assert.equal(m.intent, null, `cancel from ${from} clears intent`);
|
|
110
104
|
}
|
|
111
105
|
});
|
|
112
|
-
// ---------------------------------------------------------------------------
|
|
113
106
|
// crash → dead, intent UNCHANGED, legal only from active|idle
|
|
114
|
-
// ---------------------------------------------------------------------------
|
|
115
107
|
test('crash: active|idle → dead, intent preserved', () => {
|
|
116
108
|
for (const from of LIVE) {
|
|
117
109
|
mk(`n_${from}`, from, 'refresh');
|
|
@@ -126,9 +118,7 @@ test('crash: illegal from done|dead|canceled → throws', () => {
|
|
|
126
118
|
assert.throws(() => transition(`n_${from}`, 'crash'), /illegal lifecycle transition/);
|
|
127
119
|
}
|
|
128
120
|
});
|
|
129
|
-
// ---------------------------------------------------------------------------
|
|
130
121
|
// yield → intent='refresh', status UNCHANGED, legal only from active|idle
|
|
131
|
-
// ---------------------------------------------------------------------------
|
|
132
122
|
test('yield: active|idle → intent=refresh, status preserved', () => {
|
|
133
123
|
for (const from of LIVE) {
|
|
134
124
|
mk(`n_${from}`, from);
|
|
@@ -143,9 +133,7 @@ test('yield: illegal from done|dead|canceled → throws', () => {
|
|
|
143
133
|
assert.throws(() => transition(`n_${from}`, 'yield'), /illegal lifecycle transition/);
|
|
144
134
|
}
|
|
145
135
|
});
|
|
146
|
-
// ---------------------------------------------------------------------------
|
|
147
136
|
// release → idle + intent='idle-release', legal only from active|idle
|
|
148
|
-
// ---------------------------------------------------------------------------
|
|
149
137
|
test('release: active|idle → idle + intent=idle-release', () => {
|
|
150
138
|
for (const from of LIVE) {
|
|
151
139
|
mk(`n_${from}`, from);
|
|
@@ -160,10 +148,8 @@ test('release: illegal from done|dead|canceled → throws', () => {
|
|
|
160
148
|
assert.throws(() => transition(`n_${from}`, 'release'), /illegal lifecycle transition/);
|
|
161
149
|
}
|
|
162
150
|
});
|
|
163
|
-
// ---------------------------------------------------------------------------
|
|
164
151
|
// refresh-launch → active + intent=refresh, legal from ANY status. This is the
|
|
165
152
|
// provisional launch transition; session_start commits through revive below.
|
|
166
|
-
// ---------------------------------------------------------------------------
|
|
167
153
|
test('refresh-launch: any status → active + intent=refresh', () => {
|
|
168
154
|
for (const from of ALL) {
|
|
169
155
|
mk(`n_${from}`, from, 'idle-release');
|
|
@@ -172,9 +158,7 @@ test('refresh-launch: any status → active + intent=refresh', () => {
|
|
|
172
158
|
assert.equal(m.intent, 'refresh', `refresh-launch from ${from} preserves refresh authority`);
|
|
173
159
|
}
|
|
174
160
|
});
|
|
175
|
-
// ---------------------------------------------------------------------------
|
|
176
161
|
// revive → active + intent cleared, legal from ANY status (the universal "back")
|
|
177
|
-
// ---------------------------------------------------------------------------
|
|
178
162
|
test('revive: any status → active + intent cleared', () => {
|
|
179
163
|
for (const from of ALL) {
|
|
180
164
|
mk(`n_${from}`, from, 'idle-release');
|
|
@@ -183,9 +167,7 @@ test('revive: any status → active + intent cleared', () => {
|
|
|
183
167
|
assert.equal(m.intent, null, `revive from ${from} clears intent`);
|
|
184
168
|
}
|
|
185
169
|
});
|
|
186
|
-
// ---------------------------------------------------------------------------
|
|
187
170
|
// unknown node → throws for every event
|
|
188
|
-
// ---------------------------------------------------------------------------
|
|
189
171
|
test('transition on an unknown node throws', () => {
|
|
190
172
|
for (const ev of ['finish', 'park', 'cancel', 'crash', 'yield', 'release', 'refresh-launch', 'revive']) {
|
|
191
173
|
assert.throws(() => transition('ghost', ev), /unknown node/);
|
|
@@ -66,7 +66,7 @@ beforeEach(() => {
|
|
|
66
66
|
grandchild = join(child, 'grandchild');
|
|
67
67
|
initWorktreeRepository(root, child);
|
|
68
68
|
mkdirSync(grandchild, { recursive: true });
|
|
69
|
-
//
|
|
69
|
+
// main (FARTHER store, namespace root)
|
|
70
70
|
writeDoc(join(root, '.crouter', 'memory', 'INDEX.md'), 'ROOT STORE INDEX', 'namespace: root');
|
|
71
71
|
writeDoc(join(root, '.crouter', 'memory', 'shared.md'), 'ROOT SHARED BODY'); // root/shared
|
|
72
72
|
writeDoc(join(root, '.crouter', 'memory', 'taste', 'INDEX.md'), 'ROOT TASTE INDEX'); // root/taste
|
|
@@ -215,7 +215,7 @@ test('a fresh db has the four additive runtime columns', () => {
|
|
|
215
215
|
assert.equal(rt['window'], null);
|
|
216
216
|
assert.equal(rt['tmux_session'], null);
|
|
217
217
|
});
|
|
218
|
-
//
|
|
218
|
+
// v4: edges FK ON DELETE CASCADE (the one irreversible rebuild)
|
|
219
219
|
/** Build a hand-rolled db at exactly v3 (baseline tables + the four runtime
|
|
220
220
|
* columns, edges with NO foreign key, `user_version = 3`) — the shape the live
|
|
221
221
|
* fleet reaches just before v4. Caller seeds it, we close it; the next openDb()
|
|
@@ -352,7 +352,7 @@ test('v16 repairs a v15 database missing canonical finalization columns', () =>
|
|
|
352
352
|
db.close();
|
|
353
353
|
}
|
|
354
354
|
});
|
|
355
|
-
//
|
|
355
|
+
// v22: legacy schedules → crons
|
|
356
356
|
function buildV21Db() {
|
|
357
357
|
ensureHome();
|
|
358
358
|
const raw = new DatabaseSync(canvasDbPath());
|
|
@@ -94,7 +94,7 @@ test('on-read exposure survives a strict resume and resets for a fresh context',
|
|
|
94
94
|
const readFile = join(work, 'src', 'file.ts');
|
|
95
95
|
mkdirSync(join(work, 'src'), { recursive: true });
|
|
96
96
|
writeFileSync(readFile, 'export const x = 1;\n');
|
|
97
|
-
//
|
|
97
|
+
// pi process #1: cold load; first read surfaces the doc
|
|
98
98
|
const state = loadContextExposureState(node);
|
|
99
99
|
const transcript = exposureTarget(state, 'transcript');
|
|
100
100
|
const first = renderOnReadDocs(node, readFile, transcript);
|
|
@@ -102,11 +102,11 @@ test('on-read exposure survives a strict resume and resets for a fresh context',
|
|
|
102
102
|
saveContextExposureState(node, state);
|
|
103
103
|
// The active ledger suppresses an equal-rung repeat.
|
|
104
104
|
assert.ok(!renderOnReadDocs(node, readFile, transcript).includes(FIXTURE_BODY), 'same-context repeat read is already exposed');
|
|
105
|
-
//
|
|
105
|
+
// dormancy → revive(resume): NEW process rehydrates the state from disk
|
|
106
106
|
const stateAfterRevive = loadContextExposureState(node);
|
|
107
107
|
assert.deepEqual(stateAfterRevive, state, 'revive(resume) rehydrates the persisted context state');
|
|
108
108
|
assert.ok(!renderOnReadDocs(node, readFile, exposureTarget(stateAfterRevive, 'transcript')).includes(FIXTURE_BODY), 'strict resume does not re-deliver the document');
|
|
109
|
-
//
|
|
109
|
+
// fresh launch (revive resume=false, e.g. a refresh-yield) clears the state
|
|
110
110
|
clearContextExposureState(node);
|
|
111
111
|
const stateFresh = loadContextExposureState(node);
|
|
112
112
|
assert.ok(renderOnReadDocs(node, readFile, exposureTarget(stateFresh, 'transcript')).includes(FIXTURE_BODY), 'a fresh transcript surfaces the doc again');
|
|
@@ -47,7 +47,7 @@ test('one side lacking ticks fails open to a base compare (transient /proc miss)
|
|
|
47
47
|
assert.equal(identitiesMatch('Sat Jul 11 17:50:24 2026', 'Sat Jul 11 17:50:24 2026#53459'), true, 'coarse-vs-fine with matching base → match');
|
|
48
48
|
assert.equal(identitiesMatch('Sat Jul 11 17:50:24 2026', 'Sat Jul 11 17:51:45 2026#53459'), false, 'coarse-vs-fine with different base → reuse');
|
|
49
49
|
});
|
|
50
|
-
//
|
|
50
|
+
// boot-scoped (new-format) identities
|
|
51
51
|
const BOOT_A = '4b0c1d2e-3f40-4152-8637-a1b2c3d4e5f6';
|
|
52
52
|
const BOOT_B = '9e8d7c6b-5a40-4938-9271-fedcba098765';
|
|
53
53
|
test('new-format: equal boot_id + equal ticks → match', () => {
|
|
@@ -110,7 +110,7 @@ test('a project-root plugin layers above user config, and the project config abo
|
|
|
110
110
|
assert.equal(kinds['proj-kind']?.model, 'openai/medium'); // project config > project plugin
|
|
111
111
|
assert.equal(kinds['proj-kind']?.whenToUse, 'project plugin kind');
|
|
112
112
|
});
|
|
113
|
-
//
|
|
113
|
+
// archive path
|
|
114
114
|
function minimalCommandsJson() {
|
|
115
115
|
return {
|
|
116
116
|
schemaVersion: 1,
|