@north-light/crouter 0.3.170 → 0.3.172
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api/client.d.ts +37 -11
- package/dist/api/client.js +84 -15
- package/dist/api/dto/human.d.ts +53 -39
- package/dist/api/dto/human.js +2 -3
- package/dist/api/dto/inbox.d.ts +0 -1
- package/dist/api/dto/nodes.d.ts +12 -0
- package/dist/api/dto/review-comments.d.ts +137 -0
- package/dist/api/dto/review-comments.js +5 -0
- package/dist/api/dto/reviews.d.ts +109 -0
- package/dist/api/dto/reviews.js +5 -0
- package/dist/api/index.d.ts +2 -0
- package/dist/api/index.js +2 -0
- package/dist/api/routes.d.ts +17 -4
- package/dist/api/routes.js +21 -5
- package/dist/build-root.d.ts +1 -16
- package/dist/build-root.js +1 -25
- package/dist/builtin-memory/00-runtime-base.md +7 -5
- package/dist/builtin-memory/04-base-worker.md +1 -1
- package/dist/builtin-memory/04-orchestration-kernel.md +7 -7
- package/dist/builtin-memory/05-kinds/design/00-base.md +1 -1
- package/dist/builtin-memory/05-kinds/design/01-orchestrator.md +1 -1
- package/dist/builtin-memory/05-kinds/developer/00-base.md +1 -1
- package/dist/builtin-memory/05-kinds/explore/00-base.md +1 -1
- package/dist/builtin-memory/05-kinds/plan/00-base.md +1 -1
- package/dist/builtin-memory/05-kinds/plan/01-orchestrator.md +1 -1
- package/dist/builtin-memory/05-kinds/review/00-base.md +1 -1
- package/dist/builtin-memory/05-kinds/review/companion/00-base.md +20 -0
- package/dist/builtin-memory/05-kinds/spec/00-base.md +3 -7
- package/dist/builtin-memory/05-kinds/spec/01-orchestrator.md +3 -3
- package/dist/builtin-memory/05-kinds/spec/requirements.md +3 -3
- package/dist/builtin-memory/design.md +2 -2
- package/dist/builtin-memory/insights/capture.md +50 -0
- package/dist/builtin-memory/insights/init.md +45 -0
- package/dist/builtin-memory/insights/listen.md +9 -0
- package/dist/builtin-memory/internal/agent-shaping.md +2 -2
- package/dist/builtin-memory/internal/nodes-and-canvas.md +1 -1
- package/dist/builtin-memory/internal/plugins.md +12 -11
- package/dist/builtin-memory/internal/storage-tiers.md +2 -2
- package/dist/builtin-memory/{planning.md → plan/roadmap.md} +3 -3
- package/dist/builtin-memory/plan.md +19 -0
- package/dist/builtin-memory/spec/guide.md +38 -0
- package/dist/builtin-memory/spec/requirements.md +31 -0
- package/dist/builtin-memory/spec/roadmap.md +35 -0
- package/dist/builtin-memory/spec.md +10 -86
- package/dist/builtin-pi-packages/pi-crtr-extensions/README.md +3 -4
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/__tests__/provider-rotation.test.ts +49 -5
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/provider-rotation.d.ts +56 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/provider-rotation.js +920 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/provider-rotation.ts +221 -156
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/statusline.ts +4 -5
- package/dist/cli.js +2 -6
- package/dist/clients/attach/__tests__/attach-chrome-remote.test.js +7 -35
- package/dist/clients/attach/__tests__/attach-keybindings.test.js +32 -16
- package/dist/clients/attach/__tests__/context-message.test.js +142 -6
- package/dist/clients/attach/__tests__/crtr-output.test.js +29 -45
- package/dist/clients/attach/__tests__/diagram.test.js +1 -1
- package/dist/clients/attach/__tests__/file-review-focus.test.js +48 -0
- package/dist/clients/attach/__tests__/group-activity.test.js +266 -0
- package/dist/clients/attach/__tests__/input-controller-extension-command.test.js +32 -0
- package/dist/clients/attach/__tests__/session-identity-refresh.test.js +32 -0
- package/dist/clients/attach/__tests__/titled-editor-preview.test.js +1 -2
- package/dist/clients/attach/chrome/header.d.ts +10 -0
- package/dist/clients/attach/chrome/header.js +30 -0
- package/dist/clients/attach/chrome/recap.d.ts +7 -0
- package/dist/clients/attach/chrome/recap.js +19 -0
- package/dist/clients/attach/chrome/status-line.d.ts +3 -0
- package/dist/clients/attach/chrome/status-line.js +5 -0
- package/dist/clients/attach/chrome/widgets.js +11 -21
- package/dist/clients/attach/command.js +11 -7
- package/dist/clients/attach/config.d.ts +4 -0
- package/dist/clients/attach/config.js +3 -0
- package/dist/clients/attach/input/controller.d.ts +19 -0
- package/dist/clients/attach/input/controller.js +62 -28
- package/dist/clients/attach/input/titled-editor.d.ts +0 -4
- package/dist/clients/attach/input/titled-editor.js +2 -10
- package/dist/clients/attach/overlays/file-review.d.ts +9 -5
- package/dist/clients/attach/overlays/file-review.js +9 -70
- package/dist/clients/attach/overlays/help.d.ts +33 -0
- package/dist/clients/attach/overlays/help.js +204 -0
- package/dist/clients/attach/render/activity-slot.d.ts +4 -0
- package/dist/clients/attach/render/activity-slot.js +13 -0
- package/dist/clients/attach/render/chat-view.d.ts +117 -12
- package/dist/clients/attach/render/chat-view.js +604 -135
- package/dist/clients/attach/render/context-message.d.ts +2 -1
- package/dist/clients/attach/render/context-message.js +17 -13
- package/dist/clients/attach/render/crtr-output.js +1 -3
- package/dist/clients/attach/render/diagram.js +2 -2
- package/dist/clients/attach/render/edit-diff.d.ts +4 -1
- package/dist/clients/attach/render/edit-diff.js +7 -11
- package/dist/clients/attach/render/group-activity.d.ts +32 -0
- package/dist/clients/attach/render/group-activity.js +206 -0
- package/dist/clients/attach/render/group-recap.d.ts +10 -0
- package/dist/clients/attach/render/group-recap.js +93 -0
- package/dist/clients/attach/render/html-markdown.d.ts +17 -0
- package/dist/clients/attach/render/html-markdown.js +228 -0
- package/dist/clients/attach/render/measured-container.d.ts +96 -0
- package/dist/clients/attach/render/measured-container.js +322 -0
- package/dist/clients/attach/render/scroll-animation.d.ts +8 -0
- package/dist/clients/attach/render/scroll-animation.js +80 -0
- package/dist/clients/attach/render/tool-calls.js +6 -6
- package/dist/clients/attach/render/transcript-copy.d.ts +9 -0
- package/dist/clients/attach/render/transcript-copy.js +81 -0
- package/dist/clients/attach/render/viewport.d.ts +29 -0
- package/dist/clients/attach/render/viewport.js +159 -0
- package/dist/clients/attach/session/bindings.d.ts +10 -7
- package/dist/clients/attach/session/bindings.js +23 -8
- package/dist/clients/attach/session/connection.js +4 -5
- package/dist/clients/attach/session/context.d.ts +6 -4
- package/dist/clients/attach/session/frame.d.ts +34 -0
- package/dist/clients/attach/session/frame.js +176 -0
- package/dist/clients/attach/session/frames.d.ts +4 -1
- package/dist/clients/attach/session/frames.js +14 -1
- package/dist/clients/attach/session/identity.js +4 -3
- package/dist/clients/attach/session/input-wiring.d.ts +14 -4
- package/dist/clients/attach/session/input-wiring.js +15 -13
- package/dist/clients/attach/session/invariant.d.ts +9 -0
- package/dist/clients/attach/session/invariant.js +15 -0
- package/dist/clients/attach/session/keys.d.ts +10 -2
- package/dist/clients/attach/session/keys.js +13 -5
- package/dist/clients/attach/session/layout.d.ts +10 -8
- package/dist/clients/attach/session/layout.js +33 -34
- package/dist/clients/attach/session/mouse.d.ts +18 -0
- package/dist/clients/attach/session/mouse.js +64 -0
- package/dist/clients/attach/session/pane-tag.d.ts +3 -0
- package/dist/clients/attach/session/pane-tag.js +51 -8
- package/dist/clients/attach/session/reconnect.d.ts +3 -0
- package/dist/clients/attach/session/reconnect.js +42 -5
- package/dist/clients/attach/session/surface-frame.d.ts +20 -0
- package/dist/clients/attach/session/surface-frame.js +73 -0
- package/dist/clients/attach/session/surface-host.d.ts +64 -0
- package/dist/clients/attach/session/surface-host.js +255 -0
- package/dist/clients/attach/session/whip.d.ts +17 -0
- package/dist/clients/attach/session/whip.js +334 -0
- package/dist/clients/attach/slash/dispatch.d.ts +13 -1
- package/dist/clients/attach/slash/dispatch.js +51 -29
- package/dist/clients/attach/viewer.js +907 -819
- package/dist/clients/conversation/conversation.d.ts +65 -0
- package/dist/clients/conversation/conversation.js +318 -0
- package/dist/clients/conversation/frames.d.ts +30 -0
- package/dist/clients/conversation/frames.js +52 -0
- package/dist/clients/conversation/region.d.ts +51 -0
- package/dist/clients/conversation/region.js +265 -0
- package/dist/clients/conversation/submit.d.ts +38 -0
- package/dist/clients/conversation/submit.js +51 -0
- package/dist/clients/inbox/__tests__/inbox-controller.test.js +247 -0
- package/dist/clients/inbox/__tests__/mount-panel.test.js +776 -0
- package/dist/clients/inbox/controller.d.ts +79 -0
- package/dist/clients/inbox/controller.js +448 -0
- package/dist/clients/inbox/deck-adapter.d.ts +29 -0
- package/dist/clients/inbox/deck-adapter.js +62 -0
- package/dist/clients/inbox/layout.d.ts +8 -0
- package/dist/clients/inbox/layout.js +9 -0
- package/dist/clients/inbox/resolve.d.ts +10 -0
- package/dist/clients/inbox/resolve.js +34 -0
- package/dist/clients/inbox/review/__tests__/editor-roundtrip.test.js +29 -0
- package/dist/clients/inbox/review/__tests__/remap.test.js +196 -0
- package/dist/clients/inbox/review/__tests__/review-core.test.js +341 -0
- package/dist/clients/inbox/review/anchors.d.ts +41 -0
- package/dist/clients/inbox/review/anchors.js +89 -0
- package/dist/clients/inbox/review/columns.d.ts +22 -0
- package/dist/clients/inbox/review/columns.js +40 -0
- package/dist/clients/inbox/review/comments-client.d.ts +66 -0
- package/dist/clients/inbox/review/comments-client.js +98 -0
- package/dist/clients/inbox/review/composition.d.ts +60 -0
- package/dist/clients/inbox/review/composition.js +278 -0
- package/dist/clients/inbox/review/document-region.d.ts +120 -0
- package/dist/clients/inbox/review/document-region.js +902 -0
- package/dist/clients/inbox/review/frame.d.ts +45 -0
- package/dist/clients/inbox/review/frame.js +215 -0
- package/dist/clients/inbox/review/keys.d.ts +67 -0
- package/dist/clients/inbox/review/keys.js +130 -0
- package/dist/clients/inbox/review/launch.d.ts +10 -0
- package/dist/clients/inbox/review/launch.js +129 -0
- package/dist/clients/inbox/review/live-source.d.ts +24 -0
- package/dist/clients/inbox/review/live-source.js +160 -0
- package/dist/clients/inbox/review/nvim.d.ts +13 -0
- package/dist/clients/inbox/review/nvim.js +42 -0
- package/dist/clients/inbox/review/remap.d.ts +51 -0
- package/dist/clients/inbox/review/remap.js +157 -0
- package/dist/clients/inbox/review/review-client.d.ts +30 -0
- package/dist/clients/inbox/review/review-client.js +107 -0
- package/dist/clients/inbox/review/roundtrip.d.ts +5 -0
- package/dist/clients/inbox/review/roundtrip.js +36 -0
- package/dist/clients/inbox/review/state.d.ts +143 -0
- package/dist/clients/inbox/review/state.js +282 -0
- package/dist/clients/inbox/review-adapter.d.ts +29 -0
- package/dist/clients/inbox/review-adapter.js +92 -0
- package/dist/clients/inbox/review-visibility.d.ts +5 -0
- package/dist/clients/inbox/review-visibility.js +19 -0
- package/dist/clients/inbox/surface.d.ts +19 -0
- package/dist/clients/inbox/surface.js +128 -0
- package/dist/clients/inbox/tui/ansi.d.ts +66 -0
- package/dist/clients/inbox/tui/ansi.js +317 -0
- package/dist/clients/inbox/tui/clipboard.d.ts +2 -0
- package/dist/clients/inbox/tui/clipboard.js +26 -0
- package/dist/clients/inbox/tui/input.d.ts +7 -0
- package/dist/clients/inbox/tui/input.js +653 -0
- package/dist/clients/inbox/tui/keys.d.ts +28 -0
- package/dist/clients/inbox/tui/keys.js +111 -0
- package/dist/clients/inbox/tui/panel.d.ts +2 -0
- package/dist/clients/inbox/tui/panel.js +217 -0
- package/dist/clients/inbox/tui/render.d.ts +37 -0
- package/dist/clients/inbox/tui/render.js +476 -0
- package/dist/clients/inbox/tui/types.d.ts +57 -0
- package/dist/clients/inbox/tui/types.js +1 -0
- package/dist/clients/inbox/tui.d.ts +6 -0
- package/dist/clients/inbox/tui.js +74 -0
- package/dist/clients/surfaces/host.d.ts +66 -0
- package/dist/clients/surfaces/host.js +1 -0
- package/dist/clients/surfaces/region.d.ts +16 -0
- package/dist/clients/surfaces/region.js +1 -0
- package/dist/clients/web/__tests__/push-engine.test.js +14 -158
- package/dist/clients/web/dev-server.d.ts +3 -3
- package/dist/clients/web/dev-server.js +9 -23
- package/dist/clients/web/events.js +4 -6
- package/dist/clients/web/push-engine.d.ts +2 -66
- package/dist/clients/web/push-engine.js +30 -161
- package/dist/clients/web/push-registry.d.ts +0 -4
- package/dist/clients/web/push-registry.js +2 -7
- package/dist/clients/web/server.js +3 -35
- package/dist/clients/web/web-client/shared/protocol.d.ts +0 -108
- package/dist/clients/web/web-cmd.js +2 -2
- package/dist/commands/__tests__/human.test.js +27 -15
- package/dist/commands/api-client.js +3 -3
- package/dist/commands/canvas-browse.js +1 -1
- package/dist/commands/canvas-issue.js +1 -1
- package/dist/commands/canvas-use.js +1 -1
- package/dist/commands/human/doc.d.ts +2 -0
- package/dist/commands/human/doc.js +98 -0
- package/dist/commands/human/inbox.d.ts +2 -0
- package/dist/commands/human/inbox.js +101 -0
- package/dist/commands/human/prompts.d.ts +2 -3
- package/dist/commands/human/prompts.js +27 -180
- package/dist/commands/human/queue.d.ts +1 -5
- package/dist/commands/human/queue.js +54 -216
- package/dist/commands/human/review.d.ts +2 -0
- package/dist/commands/human/review.js +298 -0
- package/dist/commands/human/shared.d.ts +2 -69
- package/dist/commands/human/shared.js +1 -181
- package/dist/commands/human.js +15 -20
- package/dist/commands/memory/__tests__/lint.test.d.ts +1 -0
- package/dist/commands/memory/__tests__/lint.test.js +15 -0
- package/dist/commands/memory/delete.js +2 -2
- package/dist/commands/memory/lint.d.ts +1 -0
- package/dist/commands/memory/lint.js +21 -3
- package/dist/commands/memory/read.js +4 -4
- package/dist/commands/node.js +8 -7
- package/dist/commands/pkg/plugin-manage.js +143 -68
- package/dist/commands/surface-inspect.js +2 -2
- package/dist/commands/surface.js +6 -12
- package/dist/commands/sys/__tests__/setup-core.test.js +9 -5
- package/dist/commands/sys/__tests__/setup-front-door.test.js +7 -42
- package/dist/commands/sys/config.js +29 -3
- package/dist/commands/sys/feedback.js +1 -1
- package/dist/commands/sys/setup-core.d.ts +12 -11
- package/dist/commands/sys/setup-core.js +55 -46
- package/dist/commands/sys/setup-wizard.d.ts +4 -1
- package/dist/commands/sys/setup-wizard.js +50 -9
- package/dist/commands/sys/sync-deps.js +1 -1
- package/dist/commands/sys.js +4 -6
- package/dist/core/__tests__/broker-sdk-wiring.test.js +6 -1
- package/dist/core/__tests__/broker-snapshot-history.test.js +9 -1
- package/dist/core/__tests__/dead-node-policy-table.test.js +3 -4
- package/dist/core/__tests__/fixtures/fake-engine.d.ts +8 -5
- package/dist/core/__tests__/fixtures/fake-engine.js +28 -2
- package/dist/core/__tests__/full/broker-provider-retry.test.js +87 -2
- package/dist/core/__tests__/full/broker-whip-interrupt.test.d.ts +1 -0
- package/dist/core/__tests__/full/broker-whip-interrupt.test.js +39 -0
- package/dist/core/__tests__/full/detach-focus.test.js +1 -1
- package/dist/core/__tests__/human-deliver-e2e.test.js +64 -64
- package/dist/core/__tests__/human-deliver.test.js +71 -345
- package/dist/core/__tests__/inline-memory-refs.test.js +2 -12
- package/dist/core/__tests__/memory-resolver-precedence.test.js +12 -15
- package/dist/core/__tests__/migration.test.js +10 -0
- package/dist/core/__tests__/model-pin-durability.test.js +14 -0
- package/dist/core/__tests__/phase4-review-migration.test.d.ts +1 -0
- package/dist/core/__tests__/phase4-review-migration.test.js +100 -0
- package/dist/core/__tests__/phase4-review-store.test.d.ts +1 -0
- package/dist/core/__tests__/phase4-review-store.test.js +143 -0
- package/dist/core/__tests__/remote-canvas-target.test.js +2 -2
- package/dist/core/__tests__/revive.test.js +22 -53
- package/dist/core/__tests__/session-cycles.test.js +48 -18
- package/dist/core/__tests__/spawn-no-kickoff.test.js +8 -12
- package/dist/core/__tests__/stop-guard.test.js +16 -1
- package/dist/core/__tests__/tmux-surface.test.js +21 -25
- package/dist/core/asset-root.d.ts +7 -0
- package/dist/core/asset-root.js +18 -0
- package/dist/core/broker-client/client.d.ts +0 -7
- package/dist/core/broker-client/client.js +4 -10
- package/dist/core/broker-client/transport-relay.d.ts +3 -4
- package/dist/core/broker-client/transport-relay.js +3 -4
- package/dist/core/canvas/__tests__/attention.test.js +1 -1
- package/dist/core/canvas/__tests__/remote-canvas-source.test.js +1 -1
- package/dist/core/canvas/__tests__/remote-transport.test.d.ts +1 -0
- package/dist/core/{view/__tests__/transport-remote.test.js → canvas/__tests__/remote-transport.test.js} +2 -2
- package/dist/core/canvas/__tests__/render-remote.test.js +1 -1
- package/dist/core/canvas/attention.js +4 -4
- package/dist/core/canvas/canvas.d.ts +6 -11
- package/dist/core/canvas/canvas.js +11 -33
- package/dist/core/canvas/db.js +207 -38
- package/dist/core/canvas/extensions.d.ts +1 -1
- package/dist/core/canvas/extensions.js +8 -1
- package/dist/core/canvas/index.d.ts +0 -1
- package/dist/core/canvas/index.js +0 -1
- package/dist/core/canvas/nav-model.d.ts +1 -1
- package/dist/core/canvas/nav-model.js +1 -1
- package/dist/core/canvas/paths.d.ts +5 -0
- package/dist/core/canvas/paths.js +16 -0
- package/dist/core/canvas/remote-canvas-source.js +2 -2
- package/dist/core/canvas/remote-transport.d.ts +7 -0
- package/dist/core/{view/transport-remote.js → canvas/remote-transport.js} +3 -4
- package/dist/core/canvas/source.js +1 -1
- package/dist/core/canvas/status-glyph.js +4 -12
- package/dist/core/canvas/types.d.ts +10 -35
- package/dist/core/clipboard-text.d.ts +8 -0
- package/dist/core/clipboard-text.js +18 -0
- package/dist/core/command-manifests/registry.d.ts +1 -3
- package/dist/core/command-plugins/bundle.d.ts +29 -0
- package/dist/core/command-plugins/bundle.js +198 -0
- package/dist/core/command-plugins/discovery.d.ts +0 -8
- package/dist/core/command-plugins/discovery.js +2 -39
- package/dist/core/command-plugins/transport/http-fetch.d.ts +4 -30
- package/dist/core/command-plugins/transport/http-fetch.js +17 -95
- package/dist/core/command.d.ts +1 -13
- package/dist/core/command.js +1 -16
- package/dist/core/config.d.ts +4 -0
- package/dist/core/config.js +28 -3
- package/dist/core/document-lines.d.ts +4 -0
- package/dist/core/document-lines.js +6 -0
- package/dist/core/feed/feed.d.ts +2 -0
- package/dist/core/feed/feed.js +11 -7
- package/dist/core/human/__tests__/inbox-cancel-worker.d.ts +1 -0
- package/dist/core/human/__tests__/inbox-cancel-worker.js +7 -0
- package/dist/core/human/__tests__/inbox-claim-worker.d.ts +1 -0
- package/dist/core/human/__tests__/inbox-claim-worker.js +17 -0
- package/dist/core/human/__tests__/inbox-core.test.d.ts +1 -0
- package/dist/core/human/__tests__/inbox-core.test.js +144 -0
- package/dist/core/human/__tests__/inbox-stale-claim-worker.d.ts +1 -0
- package/dist/core/human/__tests__/inbox-stale-claim-worker.js +8 -0
- package/dist/core/human/__tests__/visible-paths.test.d.ts +1 -0
- package/dist/{clients/attach → core/human}/__tests__/visible-paths.test.js +9 -0
- package/dist/core/human/claim.d.ts +23 -0
- package/dist/core/human/claim.js +67 -0
- package/dist/core/human/convention.d.ts +30 -0
- package/dist/core/human/convention.js +202 -0
- package/dist/core/human/deck-factories.d.ts +8 -0
- package/dist/core/human/deck-factories.js +15 -0
- package/dist/core/human/deck-schema.d.ts +70 -0
- package/dist/core/human/deck-schema.js +92 -0
- package/dist/core/human/root.d.ts +7 -0
- package/dist/core/human/root.js +37 -0
- package/dist/core/human/scan.d.ts +3 -0
- package/dist/core/human/scan.js +76 -0
- package/dist/core/human/summary.d.ts +8 -0
- package/dist/core/human/summary.js +44 -0
- package/dist/core/human/tickets.d.ts +49 -0
- package/dist/core/human/tickets.js +190 -0
- package/dist/core/human/types.d.ts +136 -0
- package/dist/core/human/types.js +5 -0
- package/dist/{clients/attach → core/human}/visible-paths.d.ts +1 -1
- package/dist/{clients/attach → core/human}/visible-paths.js +3 -2
- package/dist/core/inspector/chrome.d.ts +6 -0
- package/dist/core/inspector/chrome.js +14 -0
- package/dist/core/inspector/contract.d.ts +105 -0
- package/dist/core/inspector/contract.js +1 -0
- package/dist/core/inspector/core.d.ts +4 -5
- package/dist/core/inspector/core.js +3 -5
- package/dist/core/inspector/host.d.ts +46 -0
- package/dist/core/{tui → inspector}/host.js +42 -208
- package/dist/core/inspector/text.d.ts +2 -2
- package/dist/core/inspector/transport.d.ts +6 -0
- package/dist/core/inspector/transport.js +36 -0
- package/dist/core/inspector/tui.d.ts +2 -2
- package/dist/core/inspector/tui.js +15 -15
- package/dist/core/keybindings/__tests__/inbox-affordance.test.js +11 -11
- package/dist/core/keybindings/__tests__/resolve.test.js +10 -19
- package/dist/core/keybindings/attach-control.d.ts +9 -3
- package/dist/core/keybindings/attach-control.js +3 -1
- package/dist/core/keybindings/catalog.d.ts +6 -5
- package/dist/core/keybindings/catalog.js +61 -133
- package/dist/core/keybindings/inbox.d.ts +5 -6
- package/dist/core/keybindings/inbox.js +9 -10
- package/dist/core/keybindings/index.d.ts +3 -3
- package/dist/core/keybindings/index.js +3 -3
- package/dist/core/keybindings/persistence.d.ts +5 -1
- package/dist/core/keybindings/persistence.js +24 -6
- package/dist/core/keybindings/resolve.d.ts +6 -0
- package/dist/core/keybindings/resolve.js +10 -3
- package/dist/core/memory/inline-ref-grammar.js +2 -3
- package/dist/core/memory-resolver.d.ts +27 -29
- package/dist/core/memory-resolver.js +45 -39
- package/dist/core/model-routes.d.ts +33 -0
- package/dist/core/model-routes.js +42 -3
- package/dist/core/preview-registry.js +17 -55
- package/dist/core/profiles/select.js +3 -3
- package/dist/core/{view/remote-canvas-target.d.ts → remote-canvas-target.d.ts} +2 -2
- package/dist/core/{view/remote-canvas-target.js → remote-canvas-target.js} +4 -4
- package/dist/core/review/__tests__/capture-origin.test.d.ts +1 -0
- package/dist/core/review/__tests__/capture-origin.test.js +139 -0
- package/dist/core/review/__tests__/comments.test.d.ts +1 -0
- package/dist/core/review/__tests__/comments.test.js +98 -0
- package/dist/core/review/__tests__/project.test.d.ts +1 -0
- package/dist/core/review/__tests__/project.test.js +71 -0
- package/dist/core/review/__tests__/remap.test.d.ts +1 -0
- package/dist/core/review/__tests__/remap.test.js +159 -0
- package/dist/core/review/__tests__/stage-identity.test.d.ts +1 -0
- package/dist/core/review/__tests__/stage-identity.test.js +140 -0
- package/dist/core/review/birth.d.ts +7 -0
- package/dist/core/review/birth.js +23 -0
- package/dist/core/review/comments.d.ts +69 -0
- package/dist/core/review/comments.js +295 -0
- package/dist/core/review/companion.d.ts +35 -0
- package/dist/core/review/companion.js +205 -0
- package/dist/core/review/document.d.ts +27 -0
- package/dist/core/review/document.js +138 -0
- package/dist/core/review/project.d.ts +13 -0
- package/dist/core/review/project.js +58 -0
- package/dist/core/review/realize.d.ts +19 -0
- package/dist/core/review/realize.js +217 -0
- package/dist/core/review/remap.d.ts +24 -0
- package/dist/core/review/remap.js +156 -0
- package/dist/core/review/signal.d.ts +5 -0
- package/dist/core/review/signal.js +31 -0
- package/dist/core/review/stage.d.ts +26 -0
- package/dist/core/review/stage.js +184 -0
- package/dist/core/review/store.d.ts +46 -0
- package/dist/core/review/store.js +163 -0
- package/dist/core/review/ticket-filter.d.ts +17 -0
- package/dist/core/review/ticket-filter.js +32 -0
- package/dist/core/review/types.d.ts +130 -0
- package/dist/core/review/types.js +23 -0
- package/dist/core/runtime/__tests__/session-visibility.test.d.ts +1 -0
- package/dist/core/runtime/__tests__/session-visibility.test.js +119 -0
- package/dist/core/runtime/bearings.d.ts +5 -17
- package/dist/core/runtime/bearings.js +88 -71
- package/dist/core/runtime/broker-protocol.d.ts +31 -8
- package/dist/core/runtime/broker.d.ts +15 -1
- package/dist/core/runtime/broker.js +573 -141
- package/dist/core/runtime/canvas-extensions.d.ts +4 -5
- package/dist/core/runtime/canvas-extensions.js +5 -5
- package/dist/core/runtime/front-door.js +4 -4
- package/dist/core/runtime/headless-pi.d.ts +24 -0
- package/dist/core/runtime/headless-pi.js +152 -0
- package/dist/core/runtime/kickoff.d.ts +1 -5
- package/dist/core/runtime/kickoff.js +1 -5
- package/dist/core/runtime/model-registry.d.ts +3 -0
- package/dist/core/runtime/model-registry.js +13 -0
- package/dist/core/runtime/naming-persist.d.ts +4 -0
- package/dist/core/runtime/naming-persist.js +27 -1
- package/dist/core/runtime/naming.d.ts +17 -12
- package/dist/core/runtime/naming.js +91 -140
- package/dist/core/runtime/node-read.js +19 -10
- package/dist/core/runtime/nodes.d.ts +9 -14
- package/dist/core/runtime/nodes.js +3 -53
- package/dist/core/runtime/package-health.d.ts +6 -21
- package/dist/core/runtime/package-health.js +12 -123
- package/dist/core/runtime/placement-tmux.d.ts +2 -1
- package/dist/core/runtime/placement-tmux.js +6 -4
- package/dist/core/runtime/placement.d.ts +1 -0
- package/dist/core/runtime/placement.js +4 -8
- package/dist/core/runtime/promote.js +3 -0
- package/dist/core/runtime/recap.d.ts +2 -7
- package/dist/core/runtime/recap.js +35 -69
- package/dist/core/runtime/revive.js +34 -77
- package/dist/core/runtime/session-cycles.d.ts +4 -0
- package/dist/core/runtime/session-cycles.js +14 -1
- package/dist/core/runtime/session-visibility.d.ts +37 -0
- package/dist/core/runtime/session-visibility.js +83 -0
- package/dist/core/runtime/spawn.js +10 -22
- package/dist/core/runtime/stop-guard.d.ts +1 -1
- package/dist/core/runtime/stop-guard.js +18 -13
- package/dist/core/runtime/surface-bg.js +1 -1
- package/dist/core/runtime/tmux-chrome.d.ts +1 -1
- package/dist/core/runtime/tmux-chrome.js +1 -1
- package/dist/core/runtime/tmux.d.ts +35 -7
- package/dist/core/runtime/tmux.js +439 -62
- package/dist/core/runtime/tool-group-summary.d.ts +19 -0
- package/dist/core/runtime/tool-group-summary.js +88 -0
- package/dist/core/scope.d.ts +2 -7
- package/dist/core/scope.js +9 -35
- package/dist/core/substrate/schema.d.ts +2 -2
- package/dist/core/termrender/code-doc.d.ts +12 -0
- package/dist/core/termrender/code-doc.js +92 -0
- package/dist/core/termrender/display.d.ts +12 -0
- package/dist/core/termrender/display.js +19 -0
- package/dist/core/termrender/termrender.d.ts +73 -0
- package/dist/core/termrender/termrender.js +795 -0
- package/dist/core/termrender/version.d.ts +1 -0
- package/dist/core/termrender/version.js +1 -0
- package/dist/core/tui/terminal.js +1 -2
- package/dist/core/user-settings.d.ts +71 -3
- package/dist/core/user-settings.js +83 -1
- package/dist/daemon/api/__tests__/inbox.test.js +44 -133
- package/dist/daemon/api/handlers/human.d.ts +0 -10
- package/dist/daemon/api/handlers/human.js +104 -702
- package/dist/daemon/api/handlers/inbox.js +113 -142
- package/dist/daemon/api/handlers/review-comments.d.ts +2 -0
- package/dist/daemon/api/handlers/review-comments.js +500 -0
- package/dist/daemon/api/handlers/reviews.d.ts +2 -0
- package/dist/daemon/api/handlers/reviews.js +287 -0
- package/dist/daemon/api/map.d.ts +22 -7
- package/dist/daemon/api/map.js +159 -11
- package/dist/daemon/api/server.js +4 -0
- package/dist/daemon/cron-run.js +6 -9
- package/dist/daemon/crtrd.js +17 -33
- package/dist/daemon/fleet.d.ts +1 -8
- package/dist/daemon/fleet.js +4 -30
- package/dist/daemon/human/finish.d.ts +14 -0
- package/dist/daemon/human/finish.js +115 -0
- package/dist/daemon/human/sweep.d.ts +6 -0
- package/dist/daemon/human/sweep.js +49 -0
- package/dist/daemon/messaging/node-message.d.ts +18 -0
- package/dist/daemon/messaging/node-message.js +70 -0
- package/dist/daemon/review/comment-notify.d.ts +26 -0
- package/dist/daemon/review/comment-notify.js +107 -0
- package/dist/daemon/review/deliver.d.ts +7 -0
- package/dist/daemon/review/deliver.js +56 -0
- package/dist/daemon/review/finish.d.ts +22 -0
- package/dist/daemon/review/finish.js +254 -0
- package/dist/daemon/review/sweep.d.ts +11 -0
- package/dist/daemon/review/sweep.js +120 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -2
- package/dist/pi-extensions/__tests__/canvas-context-intro.test.js +42 -64
- package/dist/pi-extensions/__tests__/canvas-recap.test.d.ts +1 -0
- package/dist/pi-extensions/__tests__/canvas-recap.test.js +159 -0
- package/dist/pi-extensions/__tests__/canvas-stophook-agentend.test.js +16 -38
- package/dist/pi-extensions/__tests__/canvas-structured-output.test.d.ts +1 -0
- package/dist/pi-extensions/__tests__/canvas-structured-output.test.js +63 -0
- package/dist/pi-extensions/canvas-context-intro.d.ts +8 -11
- package/dist/pi-extensions/canvas-context-intro.js +50 -104
- package/dist/pi-extensions/canvas-recap.d.ts +15 -2
- package/dist/pi-extensions/canvas-recap.js +192 -69
- package/dist/pi-extensions/canvas-review-boundary.d.ts +59 -0
- package/dist/pi-extensions/canvas-review-boundary.js +81 -0
- package/dist/pi-extensions/canvas-stophook.d.ts +3 -3
- package/dist/pi-extensions/canvas-stophook.js +78 -128
- package/dist/pi-extensions/canvas-structured-output.js +52 -8
- package/dist/shared/generated-context.d.ts +29 -4
- package/dist/shared/generated-context.js +122 -7
- package/dist/shared/tool-groups.d.ts +23 -0
- package/dist/shared/tool-groups.js +60 -0
- package/dist/shared/working-activity.d.ts +8 -0
- package/dist/shared/working-activity.js +26 -0
- package/dist/types.d.ts +36 -2
- package/dist/types.js +51 -0
- package/dist/web-client/assets/index-CCPaLeW7.css +2 -0
- package/dist/web-client/assets/index-kyOWsnZK.js +86 -0
- package/dist/web-client/index.html +2 -2
- package/dist/web-client/sw.js +1 -1
- package/docs/compat/hearth-crtr-v4.md +2 -2
- package/docs/compat/hearth-crtr-v5.md +4 -2
- package/docs/public-api.md +3 -3
- package/package.json +21 -14
- package/runtime.lock.json +4661 -1453
- package/scripts/postinstall.mjs +15 -2
- package/dist/builtin-pi-packages/pi-mode-switch/README.md +0 -36
- package/dist/builtin-pi-packages/pi-mode-switch/bin/mode +0 -36
- package/dist/builtin-pi-packages/pi-mode-switch/extensions/index.ts +0 -449
- package/dist/builtin-pi-packages/pi-mode-switch/package.json +0 -14
- package/dist/builtin-pi-packages/pi-mode-switch/tsconfig.json +0 -9
- package/dist/builtin-views/_lib/states.mjs +0 -161
- package/dist/builtin-views/canvas/core.mjs +0 -657
- package/dist/builtin-views/canvas/text.mjs +0 -58
- package/dist/builtin-views/canvas/tui.mjs +0 -168
- package/dist/builtin-views/canvas/web.jsx +0 -121
- package/dist/builtin-views/chat/core.mjs +0 -684
- package/dist/builtin-views/chat/text.mjs +0 -101
- package/dist/builtin-views/chat/tui.mjs +0 -360
- package/dist/builtin-views/chat/web.jsx +0 -362
- package/dist/builtin-views/git-pr/core.mjs +0 -673
- package/dist/builtin-views/git-pr/text.mjs +0 -84
- package/dist/builtin-views/git-pr/tui.mjs +0 -301
- package/dist/builtin-views/git-pr/web.jsx +0 -216
- package/dist/builtin-views/inbox/_lib/render.mjs +0 -175
- package/dist/builtin-views/inbox/core.mjs +0 -1273
- package/dist/builtin-views/inbox/text.mjs +0 -73
- package/dist/builtin-views/inbox/tui.mjs +0 -314
- package/dist/builtin-views/inbox/web.jsx +0 -188
- package/dist/builtin-views/linkedin/core.mjs +0 -906
- package/dist/builtin-views/linkedin/text.mjs +0 -69
- package/dist/builtin-views/linkedin/tui.mjs +0 -413
- package/dist/builtin-views/linkedin/web.jsx +0 -206
- package/dist/builtin-views/prompt-review/core.mjs +0 -735
- package/dist/builtin-views/prompt-review/text.mjs +0 -15
- package/dist/builtin-views/prompt-review/tui.mjs +0 -196
- package/dist/builtin-views/prompt-review/web.jsx +0 -484
- package/dist/builtin-views/workspace-sidebar/__tests__/core.test.js +0 -50
- package/dist/builtin-views/workspace-sidebar/__tests__/core.test.ts +0 -53
- package/dist/builtin-views/workspace-sidebar/core.mjs +0 -670
- package/dist/builtin-views/workspace-sidebar/text.mjs +0 -53
- package/dist/builtin-views/workspace-sidebar/tui.mjs +0 -141
- package/dist/builtin-views/workspace-sidebar/web.jsx +0 -109
- package/dist/clients/attach/session/mode.d.ts +0 -25
- package/dist/clients/attach/session/mode.js +0 -97
- package/dist/commands/sys/promptstudio.d.ts +0 -2
- package/dist/commands/sys/promptstudio.js +0 -65
- package/dist/commands/view-cycle.d.ts +0 -2
- package/dist/commands/view-cycle.js +0 -130
- package/dist/commands/view-list.d.ts +0 -2
- package/dist/commands/view-list.js +0 -66
- package/dist/commands/view-new.d.ts +0 -2
- package/dist/commands/view-new.js +0 -74
- package/dist/commands/view-pick.d.ts +0 -6
- package/dist/commands/view-pick.js +0 -129
- package/dist/commands/view-run.d.ts +0 -2
- package/dist/commands/view-run.js +0 -214
- package/dist/commands/view.d.ts +0 -2
- package/dist/commands/view.js +0 -31
- package/dist/commands/workspace.d.ts +0 -2
- package/dist/commands/workspace.js +0 -165
- package/dist/core/__tests__/chat-view-reconnect.test.js +0 -105
- package/dist/core/__tests__/full/consult-startup-failure.test.js +0 -124
- package/dist/core/canvas/__tests__/human-work-outbox.test.js +0 -123
- package/dist/core/canvas/human-work-outbox.d.ts +0 -73
- package/dist/core/canvas/human-work-outbox.js +0 -261
- package/dist/core/command-plugins/store.d.ts +0 -16
- package/dist/core/command-plugins/store.js +0 -64
- package/dist/core/keybindings/__tests__/bespoke-consumers.test.js +0 -40
- package/dist/core/runtime/fault-recovery-nudge.d.ts +0 -3
- package/dist/core/runtime/fault-recovery-nudge.js +0 -3
- package/dist/core/tui/__tests__/host-keybindings.test.js +0 -113
- package/dist/core/tui/host.d.ts +0 -66
- package/dist/core/view/__tests__/transport-cache.test.js +0 -62
- package/dist/core/view/bridge.d.ts +0 -10
- package/dist/core/view/bridge.js +0 -31
- package/dist/core/view/chrome.d.ts +0 -9
- package/dist/core/view/chrome.js +0 -22
- package/dist/core/view/contract.d.ts +0 -205
- package/dist/core/view/contract.js +0 -23
- package/dist/core/view/loader.d.ts +0 -31
- package/dist/core/view/loader.js +0 -188
- package/dist/core/view/stream-local.d.ts +0 -3
- package/dist/core/view/stream-local.js +0 -231
- package/dist/core/view/transport-cache.d.ts +0 -8
- package/dist/core/view/transport-cache.js +0 -38
- package/dist/core/view/transport-local.d.ts +0 -7
- package/dist/core/view/transport-local.js +0 -83
- package/dist/core/view/transport-remote.d.ts +0 -2
- package/dist/core/view/transport.d.ts +0 -10
- package/dist/core/view/transport.js +0 -15
- package/dist/pi-extensions/naming-tool.d.ts +0 -24
- package/dist/pi-extensions/naming-tool.js +0 -67
- package/dist/prompts/view.d.ts +0 -7
- package/dist/prompts/view.js +0 -172
- package/dist/web/ViewChrome.d.ts +0 -7
- package/dist/web/ViewChrome.js +0 -28
- package/dist/web/ViewPane.d.ts +0 -39
- package/dist/web/ViewPane.js +0 -48
- package/dist/web/index.d.ts +0 -8
- package/dist/web/index.js +0 -22
- package/dist/web/runtime.d.ts +0 -39
- package/dist/web/runtime.js +0 -232
- package/dist/web/states.d.ts +0 -24
- package/dist/web/states.js +0 -24
- package/dist/web/transport-http.d.ts +0 -5
- package/dist/web/transport-http.js +0 -34
- package/dist/web/transport-stream.d.ts +0 -3
- package/dist/web/transport-stream.js +0 -204
- package/dist/web-client/assets/index-U_NZ66VE.js +0 -79
- package/dist/web-client/assets/index-aaeu4adv.css +0 -2
- /package/dist/{builtin-views/workspace-sidebar/__tests__/core.test.d.ts → clients/attach/__tests__/file-review-focus.test.d.ts} +0 -0
- /package/dist/clients/attach/__tests__/{visible-paths.test.d.ts → group-activity.test.d.ts} +0 -0
- /package/dist/{core/__tests__/chat-view-reconnect.test.d.ts → clients/attach/__tests__/input-controller-extension-command.test.d.ts} +0 -0
- /package/dist/{core/__tests__/full/consult-startup-failure.test.d.ts → clients/attach/__tests__/session-identity-refresh.test.d.ts} +0 -0
- /package/dist/{core/canvas/__tests__/human-work-outbox.test.d.ts → clients/inbox/__tests__/inbox-controller.test.d.ts} +0 -0
- /package/dist/{core/keybindings/__tests__/bespoke-consumers.test.d.ts → clients/inbox/__tests__/mount-panel.test.d.ts} +0 -0
- /package/dist/{core/tui/__tests__/host-keybindings.test.d.ts → clients/inbox/review/__tests__/editor-roundtrip.test.d.ts} +0 -0
- /package/dist/{core/view/__tests__/transport-cache.test.d.ts → clients/inbox/review/__tests__/remap.test.d.ts} +0 -0
- /package/dist/{core/view/__tests__/transport-remote.test.d.ts → clients/inbox/review/__tests__/review-core.test.d.ts} +0 -0
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/** The document's logical lines: a single terminating newline closes the last
|
|
2
|
+
* line rather than opening an empty one. Never empty — an empty document is
|
|
3
|
+
* one empty line. */
|
|
4
|
+
export function documentLines(text) {
|
|
5
|
+
return (text.endsWith('\n') ? text.slice(0, -1) : text).split('\n');
|
|
6
|
+
}
|
package/dist/core/feed/feed.d.ts
CHANGED
|
@@ -29,3 +29,5 @@ export declare function pushUrgent(nodeId: string, body: string, opts?: {
|
|
|
29
29
|
export declare function pushFinal(nodeId: string, body: string, opts?: {
|
|
30
30
|
from?: string;
|
|
31
31
|
}): Promise<PushResult>;
|
|
32
|
+
/** Re-fan a previously committed final to its ticket origin without finalizing again. */
|
|
33
|
+
export declare function redeliverFinalReport(nodeId: string, originNodeId: string, reportBasename: string, body: string): PushResult;
|
package/dist/core/feed/feed.js
CHANGED
|
@@ -92,13 +92,6 @@ function commitCanonicalFinal(nodeId, staged) {
|
|
|
92
92
|
install(staged);
|
|
93
93
|
installed = true;
|
|
94
94
|
db.prepare("UPDATE nodes SET status = 'done', intent = 'done', final_report = ?, finalized_at = ? WHERE node_id = ? AND final_report IS NULL").run(staged.basename, staged.ts, nodeId);
|
|
95
|
-
// Follow-up human-work promotion remains inside this canonical-final
|
|
96
|
-
// transaction: a crash after finalization leaves a durable `pending`
|
|
97
|
-
// receipt for follow-up reconciliation. Visual work deliberately has no
|
|
98
|
-
// final dispatch path in this slice, and its namespaced statuses make this
|
|
99
|
-
// legacy promotion blind to Visual rows.
|
|
100
|
-
db.prepare("UPDATE consult_outbox SET status = 'pending', final_report = ?, updated_at = ? WHERE node_id = ? AND work_kind = 'follow_up' AND status = 'armed'").run(staged.basename, staged.ts, nodeId);
|
|
101
|
-
db.prepare("UPDATE consult_outbox SET status = 'visual_pending', final_report = ?, updated_at = ? WHERE node_id = ? AND work_kind = 'visual' AND status = 'visual_armed'").run(staged.basename, staged.ts, nodeId);
|
|
102
95
|
db.exec('COMMIT');
|
|
103
96
|
return { subscribers };
|
|
104
97
|
}
|
|
@@ -157,3 +150,14 @@ export async function pushUrgent(nodeId, body, opts) {
|
|
|
157
150
|
export async function pushFinal(nodeId, body, opts) {
|
|
158
151
|
return push(nodeId, { kind: 'final', body, ...opts });
|
|
159
152
|
}
|
|
153
|
+
/** Re-fan a previously committed final to its ticket origin without finalizing again. */
|
|
154
|
+
export function redeliverFinalReport(nodeId, originNodeId, reportBasename, body) {
|
|
155
|
+
if (reportBasename === '' || reportBasename.includes('/') || reportBasename.includes('\\')) {
|
|
156
|
+
throw new Error(`invalid canonical final report basename: ${reportBasename}`);
|
|
157
|
+
}
|
|
158
|
+
const { deliveredTo, failedTo } = deliver([{ node_id: originNodeId, active: true, created: '' }], nodeId, 'final', `${nodeId}:reports/${reportBasename}`, body);
|
|
159
|
+
const reportPath = join(reportsDir(nodeId), reportBasename);
|
|
160
|
+
if (failedTo.length > 0)
|
|
161
|
+
throw new FinalDeliveryError(reportPath, deliveredTo, failedTo);
|
|
162
|
+
return { reportPath, deliveredTo };
|
|
163
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { existsSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { cancelTicket } from '../tickets.js';
|
|
3
|
+
const [dir, barrier, ready] = process.argv.slice(2);
|
|
4
|
+
writeFileSync(ready, 'ready');
|
|
5
|
+
while (!existsSync(barrier))
|
|
6
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 1);
|
|
7
|
+
process.stdout.write(JSON.stringify(cancelTicket(dir)));
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { existsSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { claimTicket } from '../claim.js';
|
|
3
|
+
const [dir, barrier, ready, result, release] = process.argv.slice(2);
|
|
4
|
+
const spin = (path) => { while (!existsSync(path))
|
|
5
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 1); };
|
|
6
|
+
writeFileSync(ready, 'ready');
|
|
7
|
+
spin(barrier);
|
|
8
|
+
const claim = claimTicket(dir);
|
|
9
|
+
writeFileSync(result, JSON.stringify(claim));
|
|
10
|
+
process.stdout.write(JSON.stringify(claim));
|
|
11
|
+
// Stay alive until the parent has read BOTH results and releases us. A same-host
|
|
12
|
+
// claim is reclaimable the instant its holder's pid dies, so a winner that exited
|
|
13
|
+
// early would legitimately free its claim for the peer — that is designed
|
|
14
|
+
// stale-recovery, not a double-claim. Blocking here keeps both pids live across
|
|
15
|
+
// the peer's attempt, so the assertion tests exclusion, not exit timing.
|
|
16
|
+
if (release !== undefined)
|
|
17
|
+
spin(release);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { existsSync, mkdirSync, mkdtempSync, readFileSync, realpathSync, rmSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { spawn } from 'node:child_process';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import { join } from 'node:path';
|
|
6
|
+
import { submitDeck, submitReview, completeDeck, cancelTicket, readTicketResult } from '../tickets.js';
|
|
7
|
+
import { claimTicket } from '../claim.js';
|
|
8
|
+
import { scanInbox } from '../scan.js';
|
|
9
|
+
import { atomicWriteJson, progressPath, responsePath } from '../convention.js';
|
|
10
|
+
import { validateDeck } from '../deck-schema.js';
|
|
11
|
+
import { ticketDir } from '../root.js';
|
|
12
|
+
const temp = mkdtempSync(join(tmpdir(), 'crtr-inbox-core-'));
|
|
13
|
+
process.env['CRTR_HOME'] = temp;
|
|
14
|
+
const source = join(temp, 'source.md');
|
|
15
|
+
writeFileSync(source, '# Source\n');
|
|
16
|
+
const deck = (blockedSince) => ({ title: 'A deck', source: { blockedSince, nodeId: 'node-1' }, interactions: [{ id: 'go', title: 'Go?', subtitle: 'Go? requires your attention.', options: [{ id: 'yes', label: 'Yes' }] }] });
|
|
17
|
+
assert.throws(() => validateDeck({ interactions: [{ id: 'go', title: 'Go?', subtitle: 'Go? requires your attention.', options: [] }] }), /title/, 'a deck must carry its own explicit inbox title');
|
|
18
|
+
assert.throws(() => validateDeck({ title: 'Missing subtitle', interactions: [{ id: 'go', title: 'Go?', options: [] }] }), /subtitle/, 'a deck interaction without an authored subtitle is invalid');
|
|
19
|
+
async function waitFor(paths) {
|
|
20
|
+
for (let attempts = 0; !paths.every(existsSync); attempts++) {
|
|
21
|
+
if (attempts > 1_000)
|
|
22
|
+
throw new Error(`workers did not reach barrier: ${paths.join(', ')}`);
|
|
23
|
+
await new Promise((resolvePromise) => setTimeout(resolvePromise, 1));
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
const oldDeck = submitDeck({ dir: ticketDir('z-last'), deck: deck('2025-01-01T00:00:00.000Z') });
|
|
27
|
+
const newDeck = submitDeck({ dir: ticketDir('a-first'), deck: deck('2025-01-02T00:00:00.000Z') });
|
|
28
|
+
const review = submitReview({ dir: ticketDir('review'), review: { file: source, title: 'Review source', subtitle: 'Review findings require your decision.', source: { nodeId: 'node-1' }, blockedSince: '2025-01-02T00:00:00.000Z' } });
|
|
29
|
+
assert.throws(() => submitReview({ dir: ticketDir('missing-review-subtitle'), review: { file: source, title: 'Missing subtitle', source: {} } }), /subtitle/, 'a review without an authored subtitle is invalid');
|
|
30
|
+
assert.throws(() => submitReview({ dir: ticketDir('bad-review'), review: { file: join(temp, 'missing.md'), title: 'Bad', subtitle: 'This invalid review must be rejected.', source: {} } }), /existing absolute path/);
|
|
31
|
+
assert.equal(existsSync(ticketDir('bad-review')), false, 'invalid submission does not strand an id');
|
|
32
|
+
const prepared = ticketDir('prepared');
|
|
33
|
+
mkdirSync(prepared);
|
|
34
|
+
writeFileSync(join(prepared, 'body.md'), 'prepared body');
|
|
35
|
+
const preparedTicket = submitDeck({ dir: prepared, deck: { ...deck('2025-01-02T00:00:00.000Z'), interactions: [{ id: 'go', title: 'Go?', subtitle: 'Go? requires your attention.', bodyPath: 'body.md', options: [{ id: 'yes', label: 'Yes' }] }] } });
|
|
36
|
+
assert.equal(preparedTicket.dir, realpathSync(prepared), 'submission accepts a crouter-precreated direct child and colocated assets');
|
|
37
|
+
// Same timestamp is stable by id; progress is resumable work, never a hidden lease.
|
|
38
|
+
atomicWriteJson(progressPath(newDeck.dir), { kind: 'deck', responses: [], savedAt: new Date().toISOString() });
|
|
39
|
+
const scanned = scanInbox();
|
|
40
|
+
assert.deepEqual(scanned.map((item) => item.id), ['a-first', 'prepared', 'review', 'z-last']);
|
|
41
|
+
assert.equal(scanned.find((item) => item.id === 'a-first')?.claim, undefined);
|
|
42
|
+
assert.equal(scanned.find((item) => item.id === 'review')?.kind, 'review');
|
|
43
|
+
assert.equal(scanned.find((item) => item.id === 'review')?.subtitle, 'Review findings require your decision.', 'review scanning exposes its required subtitle');
|
|
44
|
+
const firstClaim = claimTicket(newDeck.dir);
|
|
45
|
+
assert.notEqual(firstClaim, null);
|
|
46
|
+
assert.equal(claimTicket(newDeck.dir), null, 'a second controller cannot claim a live ticket');
|
|
47
|
+
// A separate process writes a genuine remote claim whose heartbeat is already
|
|
48
|
+
// stale, then exits; this process recovers it — cross-process stale recovery.
|
|
49
|
+
const staleDir = submitDeck({ dir: ticketDir('stale'), deck: deck('2025-01-03T00:00:00.000Z') }).dir;
|
|
50
|
+
const staleClaimReady = join(temp, 'stale-claim-ready');
|
|
51
|
+
await new Promise((resolveWorker, rejectWorker) => spawn(process.execPath, ['--conditions=crtr-src', '--import', 'tsx', 'src/core/human/__tests__/inbox-stale-claim-worker.ts', staleDir, staleClaimReady], { cwd: process.cwd(), stdio: ['ignore', 'ignore', 'inherit'] }).once('exit', (code) => code === 0 ? resolveWorker() : rejectWorker(new Error('stale-claim worker failed'))));
|
|
52
|
+
assert.notEqual(JSON.parse(readFileSync(staleClaimReady, 'utf8')), null, 'a separate process wrote a stale remote claim on disk');
|
|
53
|
+
assert.notEqual(claimTicket(staleDir), null, 'a remote claim written by another process recovers once its heartbeat has expired');
|
|
54
|
+
const malformedDir = submitDeck({ dir: ticketDir('malformed-claim'), deck: deck('2025-01-03T00:00:00.000Z') }).dir;
|
|
55
|
+
writeFileSync(join(malformedDir, 'claim.json'), '{');
|
|
56
|
+
assert.notEqual(claimTicket(malformedDir), null, 'partial claim crash artifact recovers');
|
|
57
|
+
const final = completeDeck(newDeck.dir, [{ id: 'go', selectedOptionId: 'yes' }], firstClaim.token);
|
|
58
|
+
assert.equal(final.won, true);
|
|
59
|
+
assert.equal(readTicketResult(newDeck.dir)?.kind, 'deck');
|
|
60
|
+
assert.equal(claimTicket(newDeck.dir), null, 'terminal tickets cannot be claimed');
|
|
61
|
+
const exactResults = [
|
|
62
|
+
{ schema: 'humanloop.response/v2', kind: 'deck', responses: [], summary: '', completedAt: '2025-01-04T00:00:00.000Z' },
|
|
63
|
+
{ schema: 'humanloop.review-response/v1', kind: 'review', result: { file: source, submitted: true, approved: true, comments: [], submittedAt: '2025-01-04T00:00:00.000Z', savedAt: '2025-01-04T00:00:00.000Z', reviewId: 'review-1', changed: false, baselineSha256: 'baseline', approvedSha256: 'approved', companionNodeId: 'companion-1', commentsTotal: 0, commentsUnresolved: 0 }, completedAt: '2025-01-04T00:00:00.000Z' },
|
|
64
|
+
{ schema: 'humanloop.cancel/v1', kind: 'canceled', canceledAt: '2025-01-04T00:00:00.000Z', reason: 'stop', actor: 'test' },
|
|
65
|
+
];
|
|
66
|
+
for (const [index, result] of exactResults.entries()) {
|
|
67
|
+
const dir = submitDeck({ dir: ticketDir(`strict-${index}`), deck: deck('2025-01-03T00:00:00.000Z') }).dir;
|
|
68
|
+
writeFileSync(responsePath(dir), JSON.stringify(result));
|
|
69
|
+
assert.equal(readTicketResult(dir)?.kind, result.kind, `strict decoder accepts exact ${result.kind} result`);
|
|
70
|
+
}
|
|
71
|
+
const validFeedback = exactResults[1].result;
|
|
72
|
+
const iso = '2025-01-04T00:00:00.000Z';
|
|
73
|
+
const rejectFixtures = [
|
|
74
|
+
{ why: 'a wrong kind for a known schema', result: { schema: 'humanloop.response/v2', kind: 'review', responses: [], summary: '', completedAt: iso } },
|
|
75
|
+
{ why: 'an unknown schema id', result: { schema: 'humanloop.response/v9', kind: 'deck', responses: [], summary: '', completedAt: iso } },
|
|
76
|
+
{ why: 'a deck missing its required completedAt', result: { schema: 'humanloop.response/v2', kind: 'deck', responses: [], summary: '' } },
|
|
77
|
+
{ why: 'a deck with a non-ISO completedAt', result: { schema: 'humanloop.response/v2', kind: 'deck', responses: [], summary: '', completedAt: 'now' } },
|
|
78
|
+
{ why: 'a deck with an unrecognized nested response field', result: { schema: 'humanloop.response/v2', kind: 'deck', responses: [{ id: 'go', bogus: true }], summary: '', completedAt: iso } },
|
|
79
|
+
{ why: 'a deck with an extra top-level field', result: { schema: 'humanloop.response/v2', kind: 'deck', responses: [], summary: '', completedAt: iso, extra: true } },
|
|
80
|
+
{ why: 'a review missing its required result', result: { schema: 'humanloop.review-response/v1', kind: 'review', completedAt: iso } },
|
|
81
|
+
{ why: 'a review whose nested result is not submitted', result: { schema: 'humanloop.review-response/v1', kind: 'review', result: { ...validFeedback, submitted: false }, completedAt: iso } },
|
|
82
|
+
{ why: 'a review with incomplete comment columns', result: { schema: 'humanloop.review-response/v1', kind: 'review', result: { ...validFeedback, approved: true, comments: [{ id: 'c1', line: 1, endLine: 1, colStart: 1, lineText: '# Source', comment: 'x', createdAt: iso }] }, completedAt: iso } },
|
|
83
|
+
{ why: 'a review nested comment whose endLine precedes its line', result: { schema: 'humanloop.review-response/v1', kind: 'review', result: { ...validFeedback, approved: false, comments: [{ id: 'c1', line: 5, endLine: 2, lineText: '# Source', comment: 'x', createdAt: iso }] }, completedAt: iso } },
|
|
84
|
+
{ why: 'a review with an extra top-level field', result: { schema: 'humanloop.review-response/v1', kind: 'review', result: validFeedback, completedAt: iso, extra: true } },
|
|
85
|
+
{ why: 'a canceled result missing its required canceledAt', result: { schema: 'humanloop.cancel/v1', kind: 'canceled', reason: 'stop' } },
|
|
86
|
+
{ why: 'a canceled result with a non-ISO canceledAt', result: { schema: 'humanloop.cancel/v1', kind: 'canceled', canceledAt: 'now' } },
|
|
87
|
+
{ why: 'a canceled result with an extra top-level field', result: { schema: 'humanloop.cancel/v1', kind: 'canceled', canceledAt: iso, extra: true } },
|
|
88
|
+
];
|
|
89
|
+
for (const [index, fixture] of rejectFixtures.entries()) {
|
|
90
|
+
const dir = submitDeck({ dir: ticketDir(`strict-reject-${index}`), deck: deck('2025-01-03T00:00:00.000Z') }).dir;
|
|
91
|
+
writeFileSync(responsePath(dir), JSON.stringify(fixture.result));
|
|
92
|
+
assert.equal(readTicketResult(dir), null, `strict decoder rejects ${fixture.why}`);
|
|
93
|
+
}
|
|
94
|
+
const protocolDir = ticketDir('stale-protocol');
|
|
95
|
+
mkdirSync(protocolDir);
|
|
96
|
+
writeFileSync(responsePath(protocolDir), '{}');
|
|
97
|
+
assert.throws(() => submitDeck({ dir: protocolDir, deck: deck('2025-01-03T00:00:00.000Z') }), /protocol state/, 'precreated assets may not include humanloop lifecycle state');
|
|
98
|
+
const cancelDir = submitDeck({ dir: ticketDir('cancel-race'), deck: deck('2025-01-03T00:00:00.000Z') }).dir;
|
|
99
|
+
const canceled = cancelTicket(cancelDir, { reason: 'stop' });
|
|
100
|
+
assert.equal(canceled.status, 'canceled');
|
|
101
|
+
assert.equal(cancelTicket(cancelDir).status, 'already_resolved', 'first final writer wins');
|
|
102
|
+
const finalRaceDir = submitDeck({ dir: ticketDir('final-race'), deck: deck('2025-01-03T00:00:00.000Z') }).dir;
|
|
103
|
+
const finalRaceClaim = claimTicket(finalRaceDir);
|
|
104
|
+
const finalBarrier = join(temp, 'final-race-barrier');
|
|
105
|
+
const finalReady = join(temp, 'final-race-ready');
|
|
106
|
+
const cancelWorker = spawn(process.execPath, ['--conditions=crtr-src', '--import', 'tsx', 'src/core/human/__tests__/inbox-cancel-worker.ts', finalRaceDir, finalBarrier, finalReady], { cwd: process.cwd(), stdio: ['ignore', 'pipe', 'pipe'] });
|
|
107
|
+
await waitFor([finalReady]);
|
|
108
|
+
writeFileSync(finalBarrier, 'go');
|
|
109
|
+
const finalRace = completeDeck(finalRaceDir, [{ id: 'go', selectedOptionId: 'yes' }], finalRaceClaim.token);
|
|
110
|
+
await new Promise((resolveWorker, rejectWorker) => cancelWorker.once('exit', (code) => code === 0 ? resolveWorker() : rejectWorker(new Error('cancel worker failed'))));
|
|
111
|
+
assert.equal(readTicketResult(finalRaceDir)?.kind, finalRace.won ? 'deck' : 'canceled', 'submit and cancellation race has one canonical winner');
|
|
112
|
+
const concurrentDir = submitDeck({ dir: ticketDir('concurrent-claim'), deck: deck('2025-01-03T00:00:00.000Z') }).dir;
|
|
113
|
+
const claimBarrier = join(temp, 'claim-race-barrier');
|
|
114
|
+
// Both workers block on this release until the parent has read both results, so
|
|
115
|
+
// neither winner's pid dies during the peer's attempt (a dead same-host pid is
|
|
116
|
+
// legitimately reclaimable — that stale-recovery race, not a double claim, is
|
|
117
|
+
// what previously flaked this assertion).
|
|
118
|
+
const claimRelease = join(temp, 'claim-race-release');
|
|
119
|
+
let claimWorkerNumber = 0;
|
|
120
|
+
const claimReadyPaths = [];
|
|
121
|
+
const claimResultPaths = [];
|
|
122
|
+
const runClaimWorker = () => new Promise((resolveWorker, rejectWorker) => {
|
|
123
|
+
const n = claimWorkerNumber++;
|
|
124
|
+
const ready = join(temp, `claim-race-ready-${n}`);
|
|
125
|
+
const result = join(temp, `claim-race-result-${n}`);
|
|
126
|
+
claimReadyPaths.push(ready);
|
|
127
|
+
claimResultPaths.push(result);
|
|
128
|
+
const child = spawn(process.execPath, ['--conditions=crtr-src', '--import', 'tsx', 'src/core/human/__tests__/inbox-claim-worker.ts', concurrentDir, claimBarrier, ready, result, claimRelease], { cwd: process.cwd(), stdio: ['ignore', 'ignore', 'pipe'] });
|
|
129
|
+
let errors = '';
|
|
130
|
+
child.stderr.on('data', (chunk) => { errors += chunk; });
|
|
131
|
+
child.once('exit', (code) => code === 0 ? resolveWorker() : rejectWorker(new Error(errors)));
|
|
132
|
+
});
|
|
133
|
+
const concurrentClaimRuns = [runClaimWorker(), runClaimWorker()];
|
|
134
|
+
await waitFor(claimReadyPaths);
|
|
135
|
+
writeFileSync(claimBarrier, 'go');
|
|
136
|
+
await waitFor(claimResultPaths);
|
|
137
|
+
const concurrentClaims = claimResultPaths.map((path) => JSON.parse(readFileSync(path, 'utf8')));
|
|
138
|
+
assert.equal(concurrentClaims.filter((claim) => claim !== null).length, 1, 'separate processes exclusively claim one ticket');
|
|
139
|
+
writeFileSync(claimRelease, 'go');
|
|
140
|
+
await Promise.all(concurrentClaimRuns);
|
|
141
|
+
assert.throws(() => submitReview({ dir: ticketDir('bad-output'), review: { file: source, output: responsePath(review.dir), title: 'Bad output', subtitle: 'This invalid output path must be rejected.', source: {} } }), /must not alias/, 'review output cannot replace a ticket response');
|
|
142
|
+
assert.throws(() => submitReview({ dir: ticketDir('source-output'), review: { file: source, output: source, title: 'Bad output', subtitle: 'This invalid output path must be rejected.', source: {} } }), /must not alias/, 'review output cannot overwrite its source');
|
|
143
|
+
rmSync(temp, { recursive: true, force: true });
|
|
144
|
+
console.log('inbox core tests passed');
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { writeFileSync } from 'node:fs';
|
|
2
|
+
import { claimTicket } from '../claim.js';
|
|
3
|
+
// Writes a genuine claim from a SEPARATE process, as a remote host whose
|
|
4
|
+
// heartbeat is already older than the remote-stale window, then exits. The
|
|
5
|
+
// parent process then recovers the ticket, proving cross-process stale recovery.
|
|
6
|
+
const [dir, ready] = process.argv.slice(2);
|
|
7
|
+
const claim = claimTicket(dir, { host: 'other-host', pid: 1, now: new Date(Date.now() - 31_000) });
|
|
8
|
+
writeFileSync(ready, JSON.stringify(claim));
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -17,3 +17,12 @@ test('checkReviewableFile rejects non-files and binary text', () => {
|
|
|
17
17
|
assert.deepEqual(checkReviewableFile(root), { ok: false, reason: 'not a file' });
|
|
18
18
|
assert.deepEqual(checkReviewableFile(binary), { ok: false, reason: 'file is not UTF-8 text' });
|
|
19
19
|
});
|
|
20
|
+
test('checkReviewableFile admits a newline-terminated document at the 5,000-line limit and rejects 5,001', () => {
|
|
21
|
+
const root = mkdtempSync(join(tmpdir(), 'crtr-reviewable-limit-'));
|
|
22
|
+
const atLimit = join(root, 'at-limit.md');
|
|
23
|
+
const overLimit = join(root, 'over-limit.md');
|
|
24
|
+
writeFileSync(atLimit, 'line\n'.repeat(5000));
|
|
25
|
+
writeFileSync(overLimit, 'line\n'.repeat(5001));
|
|
26
|
+
assert.deepEqual(checkReviewableFile(atLimit), { ok: true });
|
|
27
|
+
assert.deepEqual(checkReviewableFile(overLimit), { ok: false, reason: 'file is too long to review (5,000 line limit)' });
|
|
28
|
+
});
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export interface TicketClaim {
|
|
2
|
+
token: string;
|
|
3
|
+
host: string;
|
|
4
|
+
pid: number;
|
|
5
|
+
claimedAt: string;
|
|
6
|
+
heartbeatAt: string;
|
|
7
|
+
tmuxClient?: string;
|
|
8
|
+
}
|
|
9
|
+
export interface ClaimOptions {
|
|
10
|
+
host?: string;
|
|
11
|
+
pid?: number;
|
|
12
|
+
tmuxClient?: string;
|
|
13
|
+
now?: Date;
|
|
14
|
+
}
|
|
15
|
+
export declare function readTicketClaim(dir: string): TicketClaim | null;
|
|
16
|
+
export declare function isStaleClaim(claim: TicketClaim, now?: number, localHost?: string): boolean;
|
|
17
|
+
/** Acquire a visible claim only for a still-pending ticket. Malformed crash artifacts are stale claims. */
|
|
18
|
+
export declare function claimTicket(dir: string, opts?: ClaimOptions): TicketClaim | null;
|
|
19
|
+
export declare function heartbeatClaim(dir: string, token: string, now?: Date): boolean;
|
|
20
|
+
export declare function releaseClaimLocked(dir: string, token: string): boolean;
|
|
21
|
+
export declare function releaseClaim(dir: string, token: string): boolean;
|
|
22
|
+
export declare function claimExists(dir: string): boolean;
|
|
23
|
+
export declare function withTicketLock<T>(dir: string, operation: () => T): T;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { existsSync, renameSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { hostname } from 'node:os';
|
|
3
|
+
import { randomUUID } from 'node:crypto';
|
|
4
|
+
import { claimPath, deckPath, readJson, responsePath, reviewPath, withExclusiveDirectoryLock } from './convention.js';
|
|
5
|
+
const REMOTE_STALE_MS = 30_000;
|
|
6
|
+
function parseClaim(raw) {
|
|
7
|
+
if (typeof raw !== 'object' || raw === null)
|
|
8
|
+
return null;
|
|
9
|
+
const claim = raw;
|
|
10
|
+
if (typeof claim.token !== 'string' || typeof claim.host !== 'string' || !Number.isInteger(claim.pid) || typeof claim.claimedAt !== 'string' || typeof claim.heartbeatAt !== 'string')
|
|
11
|
+
return null;
|
|
12
|
+
return { token: claim.token, host: claim.host, pid: claim.pid, claimedAt: claim.claimedAt, heartbeatAt: claim.heartbeatAt, ...(typeof claim.tmuxClient === 'string' ? { tmuxClient: claim.tmuxClient } : {}) };
|
|
13
|
+
}
|
|
14
|
+
export function readTicketClaim(dir) { return parseClaim(readJson(claimPath(dir))); }
|
|
15
|
+
function isLiveLocalPid(pid) { try {
|
|
16
|
+
process.kill(pid, 0);
|
|
17
|
+
return true;
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return false;
|
|
21
|
+
} }
|
|
22
|
+
export function isStaleClaim(claim, now = Date.now(), localHost = hostname()) {
|
|
23
|
+
if (claim.host === localHost)
|
|
24
|
+
return !isLiveLocalPid(claim.pid);
|
|
25
|
+
const heartbeat = Date.parse(claim.heartbeatAt);
|
|
26
|
+
return !Number.isFinite(heartbeat) || now - heartbeat > REMOTE_STALE_MS;
|
|
27
|
+
}
|
|
28
|
+
/** Acquire a visible claim only for a still-pending ticket. Malformed crash artifacts are stale claims. */
|
|
29
|
+
export function claimTicket(dir, opts = {}) {
|
|
30
|
+
return withExclusiveDirectoryLock(`${dir}/.ticket-lock`, () => {
|
|
31
|
+
if (existsSync(responsePath(dir)) || (!existsSync(deckPath(dir)) && !existsSync(reviewPath(dir))))
|
|
32
|
+
return null;
|
|
33
|
+
const path = claimPath(dir);
|
|
34
|
+
const existing = readTicketClaim(dir);
|
|
35
|
+
if (existing !== null && !isStaleClaim(existing, Date.now(), opts.host ?? hostname()))
|
|
36
|
+
return null;
|
|
37
|
+
if (existsSync(path))
|
|
38
|
+
unlinkSync(path);
|
|
39
|
+
const host = opts.host ?? hostname();
|
|
40
|
+
const pid = opts.pid ?? process.pid;
|
|
41
|
+
const now = (opts.now ?? new Date()).toISOString();
|
|
42
|
+
const claim = { token: randomUUID(), host, pid, claimedAt: now, heartbeatAt: now, ...(opts.tmuxClient ? { tmuxClient: opts.tmuxClient } : {}) };
|
|
43
|
+
writeFileSync(path, `${JSON.stringify(claim)}\n`, { flag: 'wx', mode: 0o600 });
|
|
44
|
+
return claim;
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
export function heartbeatClaim(dir, token, now = new Date()) {
|
|
48
|
+
return withExclusiveDirectoryLock(`${dir}/.ticket-lock`, () => {
|
|
49
|
+
const claim = readTicketClaim(dir);
|
|
50
|
+
if (claim === null || claim.token !== token)
|
|
51
|
+
return false;
|
|
52
|
+
const temp = `${claimPath(dir)}.${token}.heartbeat`;
|
|
53
|
+
writeFileSync(temp, `${JSON.stringify({ ...claim, heartbeatAt: now.toISOString() })}\n`, { flag: 'wx', mode: 0o600 });
|
|
54
|
+
renameSync(temp, claimPath(dir));
|
|
55
|
+
return true;
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
export function releaseClaimLocked(dir, token) {
|
|
59
|
+
const claim = readTicketClaim(dir);
|
|
60
|
+
if (claim === null || claim.token !== token)
|
|
61
|
+
return false;
|
|
62
|
+
unlinkSync(claimPath(dir));
|
|
63
|
+
return true;
|
|
64
|
+
}
|
|
65
|
+
export function releaseClaim(dir, token) { return withExclusiveDirectoryLock(`${dir}/.ticket-lock`, () => releaseClaimLocked(dir, token)); }
|
|
66
|
+
export function claimExists(dir) { return existsSync(claimPath(dir)); }
|
|
67
|
+
export function withTicketLock(dir, operation) { return withExclusiveDirectoryLock(`${dir}/.ticket-lock`, operation); }
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { Deck, InteractionResponse } from './types.js';
|
|
2
|
+
export declare function deckPath(dir: string): string;
|
|
3
|
+
export declare function reviewPath(dir: string): string;
|
|
4
|
+
export declare function responsePath(dir: string): string;
|
|
5
|
+
export declare function progressPath(dir: string): string;
|
|
6
|
+
export declare function claimPath(dir: string): string;
|
|
7
|
+
export type InteractionState = 'pending' | 'claimed' | 'resolved' | 'missing';
|
|
8
|
+
export declare function interactionState(dir: string): InteractionState;
|
|
9
|
+
export declare function isResolved(dir: string): boolean;
|
|
10
|
+
export declare function isClaimed(dir: string): boolean;
|
|
11
|
+
/** Resolve and verify a ticket directory without a registry. */
|
|
12
|
+
export declare function requireTicket(dir: string): string;
|
|
13
|
+
export declare function stampCanvasNode(deck: Deck): void;
|
|
14
|
+
export declare function atomicWriteJson(path: string, value: unknown): void;
|
|
15
|
+
/** Publish one immutable JSON record without replacing an existing winner. */
|
|
16
|
+
export declare function publishJsonExclusive(path: string, value: unknown): boolean;
|
|
17
|
+
export declare function readJson<T>(path: string): T | null;
|
|
18
|
+
/** Runs a short filesystem transition under a token-checked, crash-reclaimable directory lock. */
|
|
19
|
+
export declare function withExclusiveDirectoryLock<T>(path: string, operation: () => T, options?: {
|
|
20
|
+
staleMs?: number;
|
|
21
|
+
timeoutMs?: number;
|
|
22
|
+
}): T;
|
|
23
|
+
/** Async counterpart heartbeats while its operation runs, so a valid long handler is never stolen. */
|
|
24
|
+
export declare function withExclusiveDirectoryLockAsync<T>(path: string, operation: () => Promise<T>, options?: {
|
|
25
|
+
staleMs?: number;
|
|
26
|
+
timeoutMs?: number;
|
|
27
|
+
}): Promise<T>;
|
|
28
|
+
export declare function writeResponse(dir: string, responses: InteractionResponse[], completedAt: string, deck?: Deck): string;
|
|
29
|
+
export declare function writeProgress(dir: string, responses: InteractionResponse[]): void;
|
|
30
|
+
export declare function clearProgress(dir: string): void;
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import { existsSync, linkSync, mkdirSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, rmdirSync, statSync, unlinkSync, utimesSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { dirname } from 'node:path';
|
|
3
|
+
import { randomUUID } from 'node:crypto';
|
|
4
|
+
import { buildSummary } from './summary.js';
|
|
5
|
+
export function deckPath(dir) { return `${dir}/deck.json`; }
|
|
6
|
+
export function reviewPath(dir) { return `${dir}/review.json`; }
|
|
7
|
+
export function responsePath(dir) { return `${dir}/response.json`; }
|
|
8
|
+
export function progressPath(dir) { return `${dir}/progress.json`; }
|
|
9
|
+
export function claimPath(dir) { return `${dir}/claim.json`; }
|
|
10
|
+
export function interactionState(dir) {
|
|
11
|
+
if (!existsSync(deckPath(dir)) && !existsSync(reviewPath(dir)))
|
|
12
|
+
return 'missing';
|
|
13
|
+
if (existsSync(responsePath(dir)))
|
|
14
|
+
return 'resolved';
|
|
15
|
+
return existsSync(claimPath(dir)) ? 'claimed' : 'pending';
|
|
16
|
+
}
|
|
17
|
+
export function isResolved(dir) { return existsSync(responsePath(dir)); }
|
|
18
|
+
export function isClaimed(dir) { return existsSync(claimPath(dir)); }
|
|
19
|
+
/** Resolve and verify a ticket directory without a registry. */
|
|
20
|
+
export function requireTicket(dir) {
|
|
21
|
+
const canonicalDir = realpathSync(dir);
|
|
22
|
+
if (!existsSync(deckPath(canonicalDir)) && !existsSync(reviewPath(canonicalDir))) {
|
|
23
|
+
throw new Error('ticket directory has no deck or review');
|
|
24
|
+
}
|
|
25
|
+
return canonicalDir;
|
|
26
|
+
}
|
|
27
|
+
export function stampCanvasNode(deck) {
|
|
28
|
+
const id = process.env['CRTR_NODE_ID'];
|
|
29
|
+
if (id === undefined || id.trim() === '' || deck.source?.nodeId)
|
|
30
|
+
return;
|
|
31
|
+
deck.source = { ...(deck.source ?? {}), nodeId: id };
|
|
32
|
+
}
|
|
33
|
+
export function atomicWriteJson(path, value) {
|
|
34
|
+
const tmp = `${path}.${process.pid}.${Math.random().toString(16).slice(2)}.tmp`;
|
|
35
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
36
|
+
writeFileSync(tmp, `${JSON.stringify(value, null, 2)}\n`, { mode: 0o600 });
|
|
37
|
+
renameSync(tmp, path);
|
|
38
|
+
}
|
|
39
|
+
/** Publish one immutable JSON record without replacing an existing winner. */
|
|
40
|
+
export function publishJsonExclusive(path, value) {
|
|
41
|
+
const tmp = `${path}.${process.pid}.${Math.random().toString(16).slice(2)}.tmp`;
|
|
42
|
+
mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
|
|
43
|
+
writeFileSync(tmp, `${JSON.stringify(value, null, 2)}\n`, { mode: 0o600 });
|
|
44
|
+
try {
|
|
45
|
+
linkSync(tmp, path);
|
|
46
|
+
return true;
|
|
47
|
+
}
|
|
48
|
+
catch (error) {
|
|
49
|
+
if (error.code === 'EEXIST')
|
|
50
|
+
return false;
|
|
51
|
+
throw error;
|
|
52
|
+
}
|
|
53
|
+
finally {
|
|
54
|
+
unlinkSync(tmp);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
export function readJson(path) {
|
|
58
|
+
try {
|
|
59
|
+
return JSON.parse(readFileSync(path, 'utf8'));
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
function pause(ms) { Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms); }
|
|
66
|
+
// The holder's identity lives in the NAME of its single marker file. The marker's
|
|
67
|
+
// mtime is the heartbeat and its presence is ownership, so the observed token, the
|
|
68
|
+
// staleness clock, and the reclaim gate are one atomic fact.
|
|
69
|
+
const MARKER_PREFIX = 'owner.';
|
|
70
|
+
function markerPath(path, token) { return `${path}/${MARKER_PREFIX}${token}`; }
|
|
71
|
+
function observedMarkerToken(path) {
|
|
72
|
+
try {
|
|
73
|
+
return readdirSync(path).find((entry) => entry.startsWith(MARKER_PREFIX))?.slice(MARKER_PREFIX.length) ?? null;
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
function ownsLock(lock) { return existsSync(markerPath(lock.path, lock.token)); }
|
|
80
|
+
function releaseDirectoryLock(lock) { if (ownsLock(lock))
|
|
81
|
+
rmSync(lock.path, { recursive: true, force: true }); }
|
|
82
|
+
function lockAge(path, token) {
|
|
83
|
+
const target = token !== null ? markerPath(path, token) : path;
|
|
84
|
+
try {
|
|
85
|
+
return Date.now() - statSync(existsSync(target) ? target : path).mtimeMs;
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
return 0;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
// Reclamation is bound to the exact instance it observed: it unlinks ONLY that
|
|
92
|
+
// token's marker, then removes the now-empty directory with an empty-guarded
|
|
93
|
+
// rmdir. A successor lock holds a different random token (a different marker
|
|
94
|
+
// name), so a lagging reclaimer that saw the old stale lock can never strip a
|
|
95
|
+
// live successor — its unlink targets a name that no longer exists.
|
|
96
|
+
function reclaimIfStale(path, staleMs) {
|
|
97
|
+
const token = observedMarkerToken(path);
|
|
98
|
+
if (lockAge(path, token) <= staleMs)
|
|
99
|
+
return;
|
|
100
|
+
if (token !== null) {
|
|
101
|
+
try {
|
|
102
|
+
unlinkSync(markerPath(path, token));
|
|
103
|
+
}
|
|
104
|
+
catch (error) {
|
|
105
|
+
if (error.code === 'ENOENT')
|
|
106
|
+
return;
|
|
107
|
+
throw error;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
try {
|
|
111
|
+
rmdirSync(path);
|
|
112
|
+
}
|
|
113
|
+
catch (error) {
|
|
114
|
+
const code = error.code;
|
|
115
|
+
if (code !== 'ENOENT' && code !== 'ENOTEMPTY' && code !== 'EEXIST')
|
|
116
|
+
throw error;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
function tryAcquireDirectoryLock(path, staleMs) {
|
|
120
|
+
const token = randomUUID();
|
|
121
|
+
try {
|
|
122
|
+
mkdirSync(path, { mode: 0o700 });
|
|
123
|
+
writeFileSync(markerPath(path, token), '', { flag: 'wx', mode: 0o600 });
|
|
124
|
+
return { path, token };
|
|
125
|
+
}
|
|
126
|
+
catch (error) {
|
|
127
|
+
if (error.code !== 'EEXIST')
|
|
128
|
+
throw error;
|
|
129
|
+
reclaimIfStale(path, staleMs);
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
function acquireDirectoryLock(path, staleMs, timeoutMs) {
|
|
134
|
+
const startedAt = Date.now();
|
|
135
|
+
while (true) {
|
|
136
|
+
const lock = tryAcquireDirectoryLock(path, staleMs);
|
|
137
|
+
if (lock !== null)
|
|
138
|
+
return lock;
|
|
139
|
+
if (Date.now() - startedAt >= timeoutMs)
|
|
140
|
+
throw new Error('exclusive operation lock acquisition timed out');
|
|
141
|
+
pause(5);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
async function acquireDirectoryLockAsync(path, staleMs, timeoutMs) {
|
|
145
|
+
const startedAt = Date.now();
|
|
146
|
+
while (true) {
|
|
147
|
+
const lock = tryAcquireDirectoryLock(path, staleMs);
|
|
148
|
+
if (lock !== null)
|
|
149
|
+
return lock;
|
|
150
|
+
if (Date.now() - startedAt >= timeoutMs)
|
|
151
|
+
throw new Error('exclusive operation lock acquisition timed out');
|
|
152
|
+
await new Promise((resolvePromise) => setTimeout(resolvePromise, 10));
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
/** Runs a short filesystem transition under a token-checked, crash-reclaimable directory lock. */
|
|
156
|
+
export function withExclusiveDirectoryLock(path, operation, options = {}) {
|
|
157
|
+
const lock = acquireDirectoryLock(path, options.staleMs ?? 30_000, options.timeoutMs ?? 5_000);
|
|
158
|
+
try {
|
|
159
|
+
return operation();
|
|
160
|
+
}
|
|
161
|
+
finally {
|
|
162
|
+
releaseDirectoryLock(lock);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
/** Async counterpart heartbeats while its operation runs, so a valid long handler is never stolen. */
|
|
166
|
+
export async function withExclusiveDirectoryLockAsync(path, operation, options = {}) {
|
|
167
|
+
const staleMs = options.staleMs ?? 35_000;
|
|
168
|
+
const lock = await acquireDirectoryLockAsync(path, staleMs, options.timeoutMs ?? staleMs + 5_000);
|
|
169
|
+
const heartbeat = setInterval(() => {
|
|
170
|
+
if (ownsLock(lock)) {
|
|
171
|
+
try {
|
|
172
|
+
utimesSync(markerPath(lock.path, lock.token), new Date(), new Date());
|
|
173
|
+
}
|
|
174
|
+
catch { /* a reclaimed lock is no longer ours */ }
|
|
175
|
+
}
|
|
176
|
+
}, 1_000);
|
|
177
|
+
try {
|
|
178
|
+
return await operation();
|
|
179
|
+
}
|
|
180
|
+
finally {
|
|
181
|
+
clearInterval(heartbeat);
|
|
182
|
+
releaseDirectoryLock(lock);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
// Kept for the existing panel until H2 routes all finalization through tickets.ts.
|
|
186
|
+
export function writeResponse(dir, responses, completedAt, deck) {
|
|
187
|
+
const summary = deck === undefined ? '' : buildSummary(deck, responses);
|
|
188
|
+
atomicWriteJson(responsePath(dir), { schema: 'humanloop.response/v2', kind: 'deck', responses, summary, completedAt });
|
|
189
|
+
return responsePath(dir);
|
|
190
|
+
}
|
|
191
|
+
export function writeProgress(dir, responses) {
|
|
192
|
+
atomicWriteJson(progressPath(dir), { kind: 'deck', responses, savedAt: new Date().toISOString() });
|
|
193
|
+
}
|
|
194
|
+
export function clearProgress(dir) {
|
|
195
|
+
try {
|
|
196
|
+
unlinkSync(progressPath(dir));
|
|
197
|
+
}
|
|
198
|
+
catch (error) {
|
|
199
|
+
if (error.code !== 'ENOENT')
|
|
200
|
+
throw error;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { Deck } from './types.js';
|
|
2
|
+
export interface NotifyDeckOpts {
|
|
3
|
+
/** One plain-English sentence stating the notification's status and stakes. */
|
|
4
|
+
subtitle: string;
|
|
5
|
+
body?: string;
|
|
6
|
+
}
|
|
7
|
+
/** Build a validated single-option notify deck. id: 'notify', kind: 'notify'. */
|
|
8
|
+
export declare function notifyDeck(title: string, opts: NotifyDeckOpts): Deck;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { validateDeck } from './deck-schema.js';
|
|
2
|
+
/** Build a validated single-option notify deck. id: 'notify', kind: 'notify'. */
|
|
3
|
+
export function notifyDeck(title, opts) {
|
|
4
|
+
return validateDeck({
|
|
5
|
+
title,
|
|
6
|
+
interactions: [{
|
|
7
|
+
id: 'notify',
|
|
8
|
+
title,
|
|
9
|
+
subtitle: opts.subtitle,
|
|
10
|
+
...(opts.body !== undefined ? { body: opts.body } : {}),
|
|
11
|
+
kind: 'notify',
|
|
12
|
+
options: [{ id: 'ok', label: 'OK' }],
|
|
13
|
+
}],
|
|
14
|
+
});
|
|
15
|
+
}
|