@reticlehq/server 3.1.0 → 3.3.0
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/SKILL.md +30 -28
- package/dist/command/cli/adhoc-suite.d.ts +19 -0
- package/dist/command/cli/adhoc-suite.js +88 -0
- package/dist/command/cli/adhoc-verdict.d.ts +23 -2
- package/dist/command/cli/adhoc-verdict.js +18 -10
- package/dist/command/cli/auth/device-flow.d.ts +2 -2
- package/dist/command/cli/cli-doctor.js +37 -5
- package/dist/command/cli/cli-flow-commands.d.ts +2 -1
- package/dist/command/cli/cli-kill.js +1 -1
- package/dist/command/cli/cli-parse-grammar.d.ts +7 -0
- package/dist/command/cli/cli-parse-grammar.js +7 -0
- package/dist/command/cli/cli-parse-verify.d.ts +2 -5
- package/dist/command/cli/cli-parse-verify.js +86 -13
- package/dist/command/cli/cli-parse.d.ts +8 -1
- package/dist/command/cli/cli-parse.js +38 -7
- package/dist/command/cli/cli-verify.d.ts +59 -0
- package/dist/command/cli/cli-verify.js +131 -19
- package/dist/command/cli/cloud-cli.js +7 -6
- package/dist/command/cli/cloud-kit.d.ts +0 -2
- package/dist/command/cli/cloud-kit.js +3 -7
- package/dist/command/cli/config/config-discovery.d.ts +12 -0
- package/dist/command/cli/config/config-discovery.js +19 -0
- package/dist/command/cli/doctor/doctor-rows.d.ts +12 -0
- package/dist/command/cli/doctor/doctor-rows.js +12 -0
- package/dist/command/cli/doctor/doctor-sessions-line.d.ts +2 -1
- package/dist/command/cli/drive/drive-command.js +1 -1
- package/dist/command/cli/launch/cli-launch.d.ts +3 -23
- package/dist/command/cli/launch/cli-launch.js +29 -70
- package/dist/command/cli/lifecycle/daemon-lifecycle.d.ts +33 -0
- package/dist/command/cli/lifecycle/daemon-lifecycle.js +198 -0
- package/dist/command/cli/mcp-command.js +5 -3
- package/dist/command/cli/ports/resolve/cli-port.d.ts +15 -1
- package/dist/command/cli/ports/resolve/cli-port.js +50 -2
- package/dist/command/cli/report-command.d.ts +16 -0
- package/dist/command/cli/report-command.js +97 -0
- package/dist/command/cli/status/status-lines.d.ts +28 -0
- package/dist/command/cli/status/status-lines.js +117 -0
- package/dist/command/cli.d.ts +1 -1
- package/dist/command/cli.js +29 -177
- package/dist/command/daemon/binding/daemon-status-probe.d.ts +32 -0
- package/dist/command/daemon/binding/daemon-status-probe.js +93 -0
- package/dist/command/daemon/binding/loopback-reach.d.ts +42 -0
- package/dist/command/daemon/binding/loopback-reach.js +63 -0
- package/dist/command/daemon/binding/port-presence.d.ts +1 -1
- package/dist/command/daemon/daemon-resilience.d.ts +13 -1
- package/dist/command/daemon/daemon-resilience.js +12 -1
- package/dist/command/daemon/daemon.d.ts +2 -11
- package/dist/command/daemon/daemon.js +8 -11
- package/dist/command/daemon/idle-shutdown.d.ts +2 -1
- package/dist/command/daemon/lifetime/heartbeat.d.ts +8 -0
- package/dist/command/daemon/lifetime/heartbeat.js +10 -0
- package/dist/command/daemon/lifetime/log-cap.d.ts +68 -0
- package/dist/command/daemon/lifetime/log-cap.js +115 -0
- package/dist/command/setup/agent-notice-memory.d.ts +16 -0
- package/dist/command/setup/agent-notice-memory.js +65 -0
- package/dist/command/setup/bringup/dev-server-wait.d.ts +1 -0
- package/dist/command/setup/bringup/dev-server-wait.js +16 -0
- package/dist/command/setup/bringup/ensure-daemon.js +1 -1
- package/dist/command/setup/init/init-host.js +8 -0
- package/dist/command/setup/init/init-runtime.js +13 -1
- package/dist/command/setup/node-effects.js +1 -1
- package/dist/command/setup/probe/page-probe.js +9 -2
- package/dist/command/setup/run-setup.d.ts +25 -7
- package/dist/command/setup/run-setup.js +51 -9
- package/dist/command/setup/setup-command.js +31 -3
- package/dist/command/setup/terminal/confirm.js +2 -1
- package/dist/command/update/updater.d.ts +27 -0
- package/dist/command/update/updater.js +35 -2
- package/dist/features/crawl/crawl-tools.js +3 -1
- package/dist/features/crawl/crawl.d.ts +2 -0
- package/dist/features/crawl/crawl.js +3 -1
- package/dist/features/harness/drive-report.d.ts +73 -0
- package/dist/features/harness/drive-report.js +196 -0
- package/dist/features/harness/driver.js +29 -8
- package/dist/features/harness/drivers.d.ts +19 -0
- package/dist/features/harness/drivers.js +19 -0
- package/dist/features/harness/fill-values.d.ts +67 -0
- package/dist/features/harness/fill-values.js +153 -0
- package/dist/features/harness/jev-driver.d.ts +188 -0
- package/dist/features/harness/jev-driver.js +661 -0
- package/dist/features/harness/openai-driver.d.ts +46 -0
- package/dist/features/harness/openai-driver.js +197 -0
- package/dist/features/harness/platform-config.d.ts +62 -0
- package/dist/features/harness/platform-config.js +97 -0
- package/dist/features/visual/visual-tools.js +26 -2
- package/dist/index.d.ts +1 -1
- package/dist/index.js +80 -130
- package/dist/judgement/capsule/capsule-store.d.ts +31 -2
- package/dist/judgement/capsule/capsule-store.js +58 -6
- package/dist/judgement/capsule/capsule.js +5 -0
- package/dist/judgement/capsule/causal-summary.d.ts +13 -1
- package/dist/judgement/capsule/causal-summary.js +7 -0
- package/dist/judgement/domain/domain-model.js +38 -9
- package/dist/judgement/domain/domain-tools.js +4 -2
- package/dist/judgement/outcome/assertion-integrity.d.ts +3 -3
- package/dist/judgement/phenomena/phenomena.d.ts +2 -1
- package/dist/judgement/runs/artifact/gap-report.d.ts +17 -0
- package/dist/judgement/runs/artifact/gap-report.js +46 -0
- package/dist/judgement/runs/artifact/gap-summary.d.ts +58 -0
- package/dist/judgement/runs/artifact/gap-summary.js +85 -0
- package/dist/judgement/runs/artifact/render-report.js +4 -1
- package/dist/judgement/runs/artifact/to-artifact.d.ts +7 -2
- package/dist/judgement/runs/artifact/to-artifact.js +1 -1
- package/dist/judgement/runs/context-tools.js +21 -3
- package/dist/judgement/runs/drive-run.js +7 -2
- package/dist/judgement/runs/verification-sync.d.ts +2 -2
- package/dist/judgement/runs/verification-sync.js +9 -2
- package/dist/language/flows/affected-tools.js +6 -3
- package/dist/language/flows/annotate-notes/annotate.d.ts +0 -15
- package/dist/language/flows/annotate-notes/annotate.js +66 -18
- package/dist/language/flows/flow-anchor.d.ts +12 -0
- package/dist/language/flows/flow-anchor.js +20 -0
- package/dist/language/flows/flow-expect-grammar.d.ts +2 -2
- package/dist/language/flows/flow-expect-grammar.js +92 -11
- package/dist/language/flows/flow-intent.js +1 -1
- package/dist/language/flows/flow-learning.d.ts +3 -0
- package/dist/language/flows/flow-learning.js +39 -8
- package/dist/language/flows/flow-replay-run.d.ts +55 -10
- package/dist/language/flows/flow-replay-run.js +217 -58
- package/dist/language/flows/flow-replay-types.d.ts +9 -0
- package/dist/language/flows/flow-replay.d.ts +29 -11
- package/dist/language/flows/flow-replay.js +221 -203
- package/dist/language/flows/flow-result.d.ts +18 -1
- package/dist/language/flows/flow-result.js +18 -2
- package/dist/language/flows/flow-step-runners.d.ts +26 -10
- package/dist/language/flows/flow-step-runners.js +20 -4
- package/dist/language/flows/flow-store-for-session.d.ts +2 -1
- package/dist/language/flows/flow-success.d.ts +21 -6
- package/dist/language/flows/flow-success.js +50 -138
- package/dist/language/flows/flow-tools.js +8 -4
- package/dist/language/flows/flows.d.ts +15 -10
- package/dist/language/flows/flows.js +17 -4
- package/dist/language/flows/heal.d.ts +9 -1
- package/dist/language/flows/heal.js +24 -2
- package/dist/language/flows/learned-sources.d.ts +21 -0
- package/dist/language/flows/learned-sources.js +84 -0
- package/dist/language/flows/narrow-write.d.ts +4 -4
- package/dist/language/flows/recording/recording-backtrack.d.ts +0 -7
- package/dist/language/flows/recording/recording-backtrack.js +1 -1
- package/dist/language/flows/recording/replay-halt.d.ts +2 -1
- package/dist/language/flows/recording/tape/recordings.d.ts +2 -2
- package/dist/language/flows/replay.js +13 -9
- package/dist/language/flows/stores/annotation-store.d.ts +5 -5
- package/dist/language/flows/stores/assertion-tiers-store.js +2 -3
- package/dist/language/flows/stores/flake-store.js +2 -3
- package/dist/language/flows/suite/coverage.d.ts +1 -1
- package/dist/language/flows/suite/parallel-suite.d.ts +2 -1
- package/dist/language/flows/suite-selection.d.ts +15 -1
- package/dist/language/flows/suite-selection.js +1 -1
- package/dist/language/flows/verify-change-tools.js +21 -4
- package/dist/memory/cloud/account-state.d.ts +1 -1
- package/dist/memory/cloud/account-state.js +5 -4
- package/dist/memory/cloud/cloud-config.d.ts +9 -0
- package/dist/memory/cloud/cloud-config.js +9 -0
- package/dist/memory/cloud/cloud-sync.d.ts +7 -4
- package/dist/memory/cloud/cloud-sync.js +11 -6
- package/dist/memory/cloud/harness-config.d.ts +12 -0
- package/dist/memory/cloud/harness-config.js +58 -0
- package/dist/memory/cloud/harness-offer.d.ts +44 -0
- package/dist/memory/cloud/harness-offer.js +146 -0
- package/dist/memory/cloud/harness-switch.d.ts +11 -0
- package/dist/memory/cloud/harness-switch.js +53 -0
- package/dist/memory/cloud/sync-cycle.d.ts +12 -0
- package/dist/memory/cloud/sync-cycle.js +29 -4
- package/dist/memory/cloud/sync-daemon.d.ts +35 -2
- package/dist/memory/cloud/sync-daemon.js +45 -0
- package/dist/memory/cloud/sync-disk.js +11 -6
- package/dist/memory/impact/impact-recorder.d.ts +8 -0
- package/dist/memory/impact/impact-recorder.js +2 -1
- package/dist/memory/impact/impact-store.d.ts +5 -0
- package/dist/memory/impact/impact-store.js +25 -0
- package/dist/memory/intent/inline-intent.js +8 -3
- package/dist/memory/intent/intent-shard-store.d.ts +33 -47
- package/dist/memory/intent/intent-shard-store.js +253 -141
- package/dist/memory/intent/intent-shard.d.ts +22 -23
- package/dist/memory/intent/intent-shard.js +1 -1
- package/dist/memory/intent/intent-store.d.ts +13 -8
- package/dist/memory/intent/intent-store.js +46 -90
- package/dist/memory/intent/intent-tools.js +34 -23
- package/dist/memory/journal/ambient-store.js +2 -3
- package/dist/memory/journal/attach-journal.d.ts +11 -0
- package/dist/memory/journal/attach-journal.js +1 -1
- package/dist/memory/journal/deviation-report.js +4 -1
- package/dist/memory/journal/deviation-service.js +10 -2
- package/dist/memory/journal/drive-flow.d.ts +2 -2
- package/dist/memory/journal/drive-flow.js +21 -12
- package/dist/memory/journal/envelope-store.js +2 -3
- package/dist/memory/journal/journal-query.d.ts +35 -1
- package/dist/memory/journal/journal-query.js +46 -0
- package/dist/memory/journal/journal-recorder.d.ts +71 -2
- package/dist/memory/journal/journal-recorder.js +45 -7
- package/dist/memory/journal/on-disk/retain-policy.d.ts +34 -0
- package/dist/memory/journal/on-disk/retain-policy.js +44 -0
- package/dist/memory/journal/on-disk/retention.d.ts +91 -2
- package/dist/memory/journal/on-disk/retention.js +181 -11
- package/dist/memory/journal/on-disk/startup-maintenance.d.ts +42 -0
- package/dist/memory/journal/on-disk/startup-maintenance.js +14 -0
- package/dist/memory/journal/on-disk/workspace-gitignore.d.ts +0 -2
- package/dist/memory/journal/on-disk/workspace-gitignore.js +6 -38
- package/dist/memory/journal/on-disk/workspace-tiers.d.ts +52 -0
- package/dist/memory/journal/on-disk/workspace-tiers.js +88 -0
- package/dist/memory/journal/route-template.d.ts +22 -0
- package/dist/memory/journal/route-template.js +44 -0
- package/dist/memory/journal/self-observation.d.ts +3 -0
- package/dist/memory/journal/self-observation.js +67 -0
- package/dist/memory/journal/session-end.d.ts +17 -4
- package/dist/memory/journal/session-end.js +85 -7
- package/dist/memory/journal/session-journal.d.ts +122 -5
- package/dist/memory/journal/session-journal.js +489 -32
- package/dist/memory/project/artifact-root-resolver.d.ts +3 -0
- package/dist/memory/project/artifact-root-resolver.js +89 -0
- package/dist/memory/project/artifact-root.d.ts +39 -2
- package/dist/memory/project/artifact-root.js +58 -1
- package/dist/memory/project/baselines.d.ts +1 -1
- package/dist/memory/project/dir/fill-value-store.d.ts +32 -0
- package/dist/memory/project/dir/fill-value-store.js +88 -0
- package/dist/memory/project/dir/reticle-dir.d.ts +15 -5
- package/dist/memory/project/dir/reticle-dir.js +11 -0
- package/dist/memory/project/fs/fs-port.d.ts +22 -1
- package/dist/memory/project/fs/fs-port.js +72 -5
- package/dist/memory/project/fs/write-atomic.d.ts +26 -0
- package/dist/memory/project/fs/write-atomic.js +48 -0
- package/dist/memory/project/learned-routes.d.ts +16 -2
- package/dist/memory/project/learned-routes.js +26 -10
- package/dist/memory/project/project-for-root.d.ts +30 -0
- package/dist/memory/project/project-for-root.js +43 -0
- package/dist/memory/project/project-tools.js +10 -5
- package/dist/memory/project/session-root.d.ts +35 -6
- package/dist/memory/project/session-root.js +70 -8
- package/dist/memory/recall/prior/connection-memory.d.ts +6 -0
- package/dist/memory/recall/prior/connection-memory.js +8 -0
- package/dist/memory/recall/registered-projects.d.ts +2 -1
- package/dist/portal/bridge/auth-failure-reason.d.ts +9 -0
- package/dist/portal/bridge/auth-failure-reason.js +20 -3
- package/dist/portal/bridge/bridge.d.ts +6 -1
- package/dist/portal/bridge/bridge.js +36 -2
- package/dist/portal/input/real-input.js +2 -1
- package/dist/portal/pool/browser-pool.d.ts +10 -0
- package/dist/portal/pool/browser-pool.js +10 -0
- package/dist/portal/pool/playwright-launcher.js +1 -0
- package/dist/portal/pool/pool-contract.d.ts +2 -0
- package/dist/portal/pool/storage-seed.d.ts +2 -1
- package/dist/portal/pool/zero-install.d.ts +27 -0
- package/dist/portal/pool/zero-install.js +54 -0
- package/dist/portal/realm/web-realm.js +6 -0
- package/dist/portal/session/dev-server/dev-server-probe.d.ts +2 -1
- package/dist/portal/session/dev-server/served-document.d.ts +21 -0
- package/dist/portal/session/dev-server/served-document.js +57 -0
- package/dist/portal/session/facts/background-traffic.d.ts +9 -0
- package/dist/portal/session/facts/background-traffic.js +25 -0
- package/dist/portal/session/facts/session-replaced.d.ts +19 -0
- package/dist/portal/session/facts/session-replaced.js +22 -0
- package/dist/portal/session/live-control-tools.js +48 -3
- package/dist/portal/session/no-session-diagnosis.d.ts +1 -0
- package/dist/portal/session/no-session-diagnosis.js +67 -60
- package/dist/portal/session/no-session-next-action.js +3 -2
- package/dist/portal/session/no-session-watch.js +14 -10
- package/dist/portal/session/presence/attachment-history.d.ts +13 -2
- package/dist/portal/session/rebind.d.ts +18 -0
- package/dist/portal/session/rebind.js +38 -0
- package/dist/portal/session/session-health.d.ts +61 -0
- package/dist/portal/session/session-health.js +40 -1
- package/dist/portal/session/session-info.d.ts +6 -0
- package/dist/portal/session/session-manager.d.ts +13 -0
- package/dist/portal/session/session-manager.js +42 -3
- package/dist/portal/session/session-successor.d.ts +11 -0
- package/dist/portal/session/session-successor.js +14 -1
- package/dist/portal/session/session-verdict-facts.d.ts +28 -0
- package/dist/portal/session/session-verdict-facts.js +15 -0
- package/dist/portal/session/session.d.ts +26 -10
- package/dist/portal/session/session.js +48 -49
- package/dist/status-payload.d.ts +12 -1
- package/dist/status-payload.js +4 -1
- package/dist/surface/http-server.js +3 -5
- package/dist/surface/mcp/faults/mcp-outage.d.ts +2 -1
- package/dist/surface/mcp/faults/mcp-outage.js +20 -4
- package/dist/surface/mcp/mcp-proxy.d.ts +10 -1
- package/dist/surface/mcp/mcp-proxy.js +23 -12
- package/dist/surface/mcp/mcp.d.ts +10 -1
- package/dist/surface/mcp/mcp.js +59 -29
- package/dist/surface/mcp/proxy/proxy-daemon-probe.d.ts +15 -2
- package/dist/surface/mcp/proxy/proxy-daemon-probe.js +22 -5
- package/dist/surface/mcp/proxy/proxy-log.d.ts +5 -11
- package/dist/surface/mcp/proxy/proxy-log.js +7 -22
- package/dist/surface/mcp/proxy-idle-exit.d.ts +2 -1
- package/dist/surface/mcp/server-instructions.js +12 -1
- package/dist/surface/tools/act/act-preflight.js +6 -1
- package/dist/surface/tools/act/already-true.d.ts +40 -0
- package/dist/surface/tools/act/already-true.js +36 -0
- package/dist/surface/tools/act/navigation/navigate-arrival.d.ts +6 -0
- package/dist/surface/tools/act/navigation/navigate-arrival.js +13 -0
- package/dist/surface/tools/act/sequence-grade.js +13 -0
- package/dist/surface/tools/act/settle-in-flight.d.ts +6 -2
- package/dist/surface/tools/act/settle-in-flight.js +29 -6
- package/dist/surface/tools/act-capsule.js +49 -8
- package/dist/surface/tools/act-tools.js +25 -24
- package/dist/surface/tools/args/numeric-bounds.d.ts +15 -2
- package/dist/surface/tools/args/numeric-bounds.js +30 -3
- package/dist/surface/tools/assert/assert-grade.js +14 -2
- package/dist/surface/tools/assert/assert-verdict.js +74 -21
- package/dist/surface/tools/browser-tools.js +4 -3
- package/dist/surface/tools/coverage-tools.js +5 -2
- package/dist/surface/tools/dynamic-tools.js +43 -46
- package/dist/surface/tools/error-recovery.d.ts +6 -0
- package/dist/surface/tools/error-recovery.js +15 -0
- package/dist/surface/tools/explore-tools.js +80 -7
- package/dist/surface/tools/feedback-tools.d.ts +2 -1
- package/dist/surface/tools/feedback-tools.js +50 -3
- package/dist/surface/tools/first-sentence.d.ts +24 -0
- package/dist/surface/tools/first-sentence.js +0 -0
- package/dist/surface/tools/harness-explore.d.ts +125 -1
- package/dist/surface/tools/harness-explore.js +401 -15
- package/dist/surface/tools/harness-plan.d.ts +76 -0
- package/dist/surface/tools/harness-plan.js +156 -0
- package/dist/surface/tools/harness-toolset.js +12 -1
- package/dist/surface/tools/invoke-tool.js +82 -9
- package/dist/surface/tools/lease-hint.d.ts +8 -0
- package/dist/surface/tools/lease-hint.js +10 -0
- package/dist/surface/tools/lease-tools.d.ts +16 -28
- package/dist/surface/tools/lease-tools.js +53 -5
- package/dist/surface/tools/lineage-tools.js +1 -1
- package/dist/surface/tools/live-call-text.d.ts +6 -0
- package/dist/surface/tools/live-call-text.js +36 -2
- package/dist/surface/tools/observe-tools.js +69 -15
- package/dist/surface/tools/read/query-windowed.d.ts +1 -1
- package/dist/surface/tools/read/query-windowed.js +1 -1
- package/dist/surface/tools/read-tools.js +1 -2
- package/dist/surface/tools/real-input-attempt.d.ts +0 -5
- package/dist/surface/tools/real-input-attempt.js +1 -1
- package/dist/surface/tools/tool-kit.d.ts +23 -3
- package/dist/surface/tools/tool-kit.js +6 -1
- package/dist/surface/tools/tool-surface.js +17 -5
- package/dist/surface/tools/tools-helpers.d.ts +8 -7
- package/dist/surface/tools/tools-helpers.js +11 -13
- package/dist/surface/tools/tools.js +70 -30
- package/dist/telemetry/bug-found.js +32 -2
- package/dist/telemetry/feedback-context.js +3 -3
- package/dist/telemetry/feedback-local.d.ts +1 -0
- package/dist/telemetry/feedback-local.js +1 -3
- package/dist/telemetry/feedback.d.ts +1 -1
- package/dist/telemetry/harness-drive.d.ts +6 -0
- package/dist/telemetry/harness-drive.js +39 -0
- package/dist/telemetry/hud-metrics.d.ts +32 -0
- package/dist/telemetry/hud-metrics.js +95 -0
- package/dist/telemetry/mcp-connection.d.ts +14 -1
- package/dist/telemetry/mcp-connection.js +17 -2
- package/dist/telemetry/onboarding-firsts.d.ts +30 -1
- package/dist/telemetry/onboarding-firsts.js +54 -12
- package/dist/telemetry/session-metrics.d.ts +5 -0
- package/dist/telemetry/session-metrics.js +15 -1
- package/dist/telemetry/verification-of.js +31 -3
- package/dist/wire-journal.d.ts +31 -0
- package/dist/wire-journal.js +100 -0
- package/docs/README.md +1 -0
- package/docs/agent-cheatsheet.md +7 -3
- package/docs/autodrive.md +42 -2
- package/docs/benchmarks.md +1 -1
- package/docs/capabilities.mdx +1 -1
- package/docs/cli/cloud.mdx +11 -9
- package/docs/cli/doctor.mdx +1 -0
- package/docs/cli/init.mdx +1 -0
- package/docs/cli/report.mdx +44 -0
- package/docs/cli/status.mdx +35 -20
- package/docs/cli.mdx +8 -18
- package/docs/desktop.mdx +1 -1
- package/docs/faq.mdx +1 -1
- package/docs/flows.md +11 -0
- package/docs/frameworks.mdx +113 -1
- package/docs/install-agentic.mdx +36 -0
- package/docs/install-manual.mdx +1 -1
- package/docs/packages/tauri.mdx +1 -1
- package/docs/packages.mdx +1 -1
- package/docs/predicates.mdx +144 -4
- package/docs/system-map.md +6 -4
- package/docs/telemetry.md +2 -1
- package/docs/token-efficiency.md +2 -2
- package/docs/tools/overview.mdx +1 -1
- package/docs/tools/query.mdx +10 -10
- package/docs/troubleshooting.mdx +8 -2
- package/docs/usage.md +2 -2
- package/docs/what-is-recorded.md +62 -0
- package/package.json +5 -5
- package/dist/judgement/outcome/predicate-to-expect.d.ts +0 -44
- package/dist/judgement/outcome/predicate-to-expect.js +0 -148
|
@@ -26,20 +26,35 @@ export function routesFromEvents(events) {
|
|
|
26
26
|
return route === undefined ? [] : [route];
|
|
27
27
|
});
|
|
28
28
|
}
|
|
29
|
-
/**
|
|
30
|
-
|
|
29
|
+
/**
|
|
30
|
+
* Persist initial and subsequent routes for existing and future browser sessions.
|
|
31
|
+
*
|
|
32
|
+
* `storeFor` resolves the store for ONE session's artifact root, rather than the daemon being handed
|
|
33
|
+
* a single store bound to its own `process.cwd()`. That binding was right only when the daemon
|
|
34
|
+
* happened to be started inside the app it was driving — and a user-scoped MCP registration, the
|
|
35
|
+
* common case, starts it wherever the editor's cwd is. The routes of every app then landed in that
|
|
36
|
+
* directory, which is how a `.reticle/` kept reappearing in a backend nobody had instrumented,
|
|
37
|
+
* while the driven project's own memory never filled.
|
|
38
|
+
*/
|
|
39
|
+
export function attachRouteLearning(bridge, storeFor) {
|
|
31
40
|
const attached = new WeakSet();
|
|
32
|
-
|
|
41
|
+
// Keyed by STORE rather than by root path: `storeFor` already answers one store per root, so this
|
|
42
|
+
// cannot disagree with it about which roots are distinct.
|
|
43
|
+
const pending = new Map();
|
|
33
44
|
let flushTimer;
|
|
34
45
|
const flush = () => {
|
|
35
46
|
flushTimer = undefined;
|
|
36
|
-
const
|
|
47
|
+
const batches = [...pending];
|
|
37
48
|
pending.clear();
|
|
38
|
-
|
|
39
|
-
|
|
49
|
+
for (const [store, routes] of batches) {
|
|
50
|
+
if (routes.size > 0)
|
|
51
|
+
void store.recordRoutes([...routes]).catch(() => undefined);
|
|
52
|
+
}
|
|
40
53
|
};
|
|
41
|
-
const queue = (route) => {
|
|
42
|
-
pending.
|
|
54
|
+
const queue = (store, route) => {
|
|
55
|
+
const routes = pending.get(store) ?? new Set();
|
|
56
|
+
routes.add(route);
|
|
57
|
+
pending.set(store, routes);
|
|
43
58
|
if (flushTimer !== undefined)
|
|
44
59
|
clearTimeout(flushTimer);
|
|
45
60
|
flushTimer = setTimeout(flush, ROUTE_PERSIST_DEBOUNCE_MS);
|
|
@@ -48,13 +63,14 @@ export function attachRouteLearning(bridge, project) {
|
|
|
48
63
|
if (attached.has(session))
|
|
49
64
|
return;
|
|
50
65
|
attached.add(session);
|
|
66
|
+
const store = storeFor(session.artifactRoot);
|
|
51
67
|
const initial = routeFromUrl(session.url);
|
|
52
68
|
if (initial !== undefined)
|
|
53
|
-
queue(initial);
|
|
69
|
+
queue(store, initial);
|
|
54
70
|
session.onEvent((event) => {
|
|
55
71
|
const route = routeFromEvent(event);
|
|
56
72
|
if (route !== undefined)
|
|
57
|
-
queue(route);
|
|
73
|
+
queue(store, route);
|
|
58
74
|
});
|
|
59
75
|
};
|
|
60
76
|
for (const session of bridge.sessions.all())
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The project memory store for ONE artifact root.
|
|
3
|
+
*
|
|
4
|
+
* `deps.project` is bound to the daemon's own root at construction, which is right only when the
|
|
5
|
+
* daemon happens to have been started inside the app it is driving — and a user-scoped MCP
|
|
6
|
+
* registration, the common case, starts it wherever the editor's cwd is. So every run record, every
|
|
7
|
+
* learned route and every observability high-water mark was written into that directory: a
|
|
8
|
+
* `.reticle/` appearing in a backend nobody instrumented, and a driven project whose own memory
|
|
9
|
+
* stayed empty. The cloud half of `recordReplayRun` had already been moved onto the session's root
|
|
10
|
+
* and its local half left behind, which is how a replay outcome could reach the dashboard and not
|
|
11
|
+
* the project it belonged to.
|
|
12
|
+
*
|
|
13
|
+
* Reads and writes both go through here, deliberately: a write that resolves to the app while a
|
|
14
|
+
* read still answers from the daemon's directory is worse than either alone — the record lands and
|
|
15
|
+
* is then invisible to the tool that exists to report it.
|
|
16
|
+
*
|
|
17
|
+
* Rebinding is cheap (a filesystem port, a root and a clock) and the single-project case allocates
|
|
18
|
+
* nothing: when the resolved root IS the daemon's, `deps.project` comes back unchanged.
|
|
19
|
+
*/
|
|
20
|
+
import { ProjectStore } from './project-store.js';
|
|
21
|
+
import type { FileSystemPort } from './fs/fs-port.js';
|
|
22
|
+
import type { ToolDeps } from '../../surface/tools/tool-kit.js';
|
|
23
|
+
export declare function projectForRoot(deps: ToolDeps, root: string): ProjectStore;
|
|
24
|
+
/**
|
|
25
|
+
* The same question asked by the daemon rather than by a tool: one store per artifact root, so a
|
|
26
|
+
* session's learned routes land in the app that was driven and not in whatever directory the daemon
|
|
27
|
+
* was launched from. The daemon's own store is seeded in, so the single-project case allocates
|
|
28
|
+
* nothing and keeps the instance every other caller already holds.
|
|
29
|
+
*/
|
|
30
|
+
export declare function projectStoreResolver(fs: FileSystemPort, daemonStore: ProjectStore, daemonRoot: string, now: () => number): (root: string | undefined) => ProjectStore;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The project memory store for ONE artifact root.
|
|
3
|
+
*
|
|
4
|
+
* `deps.project` is bound to the daemon's own root at construction, which is right only when the
|
|
5
|
+
* daemon happens to have been started inside the app it is driving — and a user-scoped MCP
|
|
6
|
+
* registration, the common case, starts it wherever the editor's cwd is. So every run record, every
|
|
7
|
+
* learned route and every observability high-water mark was written into that directory: a
|
|
8
|
+
* `.reticle/` appearing in a backend nobody instrumented, and a driven project whose own memory
|
|
9
|
+
* stayed empty. The cloud half of `recordReplayRun` had already been moved onto the session's root
|
|
10
|
+
* and its local half left behind, which is how a replay outcome could reach the dashboard and not
|
|
11
|
+
* the project it belonged to.
|
|
12
|
+
*
|
|
13
|
+
* Reads and writes both go through here, deliberately: a write that resolves to the app while a
|
|
14
|
+
* read still answers from the daemon's directory is worse than either alone — the record lands and
|
|
15
|
+
* is then invisible to the tool that exists to report it.
|
|
16
|
+
*
|
|
17
|
+
* Rebinding is cheap (a filesystem port, a root and a clock) and the single-project case allocates
|
|
18
|
+
* nothing: when the resolved root IS the daemon's, `deps.project` comes back unchanged.
|
|
19
|
+
*/
|
|
20
|
+
import { ProjectStore } from './project-store.js';
|
|
21
|
+
export function projectForRoot(deps, root) {
|
|
22
|
+
if (root === deps.reticleRoot)
|
|
23
|
+
return deps.project;
|
|
24
|
+
return new ProjectStore(deps.fs, root, { now: deps.now });
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* The same question asked by the daemon rather than by a tool: one store per artifact root, so a
|
|
28
|
+
* session's learned routes land in the app that was driven and not in whatever directory the daemon
|
|
29
|
+
* was launched from. The daemon's own store is seeded in, so the single-project case allocates
|
|
30
|
+
* nothing and keeps the instance every other caller already holds.
|
|
31
|
+
*/
|
|
32
|
+
export function projectStoreResolver(fs, daemonStore, daemonRoot, now) {
|
|
33
|
+
const byRoot = new Map([[daemonRoot, daemonStore]]);
|
|
34
|
+
return (root) => {
|
|
35
|
+
const key = root ?? daemonRoot;
|
|
36
|
+
let found = byRoot.get(key);
|
|
37
|
+
if (found === undefined) {
|
|
38
|
+
found = new ProjectStore(fs, key, { now });
|
|
39
|
+
byRoot.set(key, found);
|
|
40
|
+
}
|
|
41
|
+
return found;
|
|
42
|
+
};
|
|
43
|
+
}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
+
import { sessionRoot } from './session-root.js';
|
|
3
|
+
import { projectForRoot } from './project-for-root.js';
|
|
2
4
|
import { ProjectReadError, RunStatus } from '@reticlehq/core';
|
|
3
5
|
import { ReticleTool } from '@reticlehq/core';
|
|
4
6
|
import { countSchema } from '../../surface/tools/args/numeric-bounds.js';
|
|
@@ -67,9 +69,11 @@ function lastTwoFor(runs, name) {
|
|
|
67
69
|
* The per-flow diff between the two most-recent verification ARTIFACTS (.reticle/runs), or undefined when
|
|
68
70
|
* fewer than two exist. Never throws — a missing/unreadable artifact must not break reading run history.
|
|
69
71
|
*/
|
|
70
|
-
async function lastTwoRunArtifacts(deps
|
|
72
|
+
async function lastTwoRunArtifacts(deps,
|
|
73
|
+
/** The session's root — the same address `persistAndSyncVerificationRun` wrote the pair to. */
|
|
74
|
+
root) {
|
|
71
75
|
try {
|
|
72
|
-
const pair = await new RunStore(deps.fs,
|
|
76
|
+
const pair = await new RunStore(deps.fs, root).latestTwo();
|
|
73
77
|
return pair === undefined ? undefined : diffVerificationRuns(pair[0], pair[1]);
|
|
74
78
|
}
|
|
75
79
|
catch {
|
|
@@ -106,7 +110,8 @@ export const PROJECT_TOOLS = [
|
|
|
106
110
|
handler: async (deps, args) => {
|
|
107
111
|
const cloud = await cloudRegression(deps, asString(args['sessionId']));
|
|
108
112
|
const withCloud = (obj) => cloud === undefined ? obj : { ...obj, cloud };
|
|
109
|
-
const
|
|
113
|
+
const project = projectForRoot(deps, sessionRoot(deps, asString(args['sessionId'])));
|
|
114
|
+
const read = await project.read();
|
|
110
115
|
if (!read.ok) {
|
|
111
116
|
return withCloud({
|
|
112
117
|
error: read.reason === ProjectReadError.MISSING
|
|
@@ -133,14 +138,14 @@ export const PROJECT_TOOLS = [
|
|
|
133
138
|
learned: read.file.learned,
|
|
134
139
|
});
|
|
135
140
|
}
|
|
136
|
-
const lastRun = await
|
|
141
|
+
const lastRun = await project.lastRun(name);
|
|
137
142
|
const pair = lastTwoFor(read.file.runs, name);
|
|
138
143
|
//: the RICH run diff (per-flow duration deltas past a noise floor, status changes, new/removed
|
|
139
144
|
// flows, verdict change) over the last two verification ARTIFACTS. That lives alongside — not
|
|
140
145
|
// instead of — the lightweight RunRecord diff above: `diff` answers "did this named run behave like
|
|
141
146
|
// last time?" from project.json, while `runDiff` answers "what changed between the last two full
|
|
142
147
|
// verification runs?" from .reticle/runs. Best-effort: no artifacts simply means no runDiff.
|
|
143
|
-
const runDiff = await lastTwoRunArtifacts(deps);
|
|
148
|
+
const runDiff = await lastTwoRunArtifacts(deps, sessionRoot(deps, asString(args['sessionId'])));
|
|
144
149
|
const forName = read.file.runs.filter((r) => r.name === name);
|
|
145
150
|
return withCloud({
|
|
146
151
|
runs: recent(forName),
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { ProjectId } from '@reticlehq/core';
|
|
1
2
|
import type { ToolDeps } from '../../surface/tools/tool-kit.js';
|
|
2
3
|
/**
|
|
3
4
|
* The `.reticle` directory a tool call's artifacts belong in.
|
|
@@ -9,15 +10,43 @@ import type { ToolDeps } from '../../surface/tools/tool-kit.js';
|
|
|
9
10
|
* fixed — the flow would save successfully and then be invisible to the tool that exists to replay
|
|
10
11
|
* it, which reads as "no flows covered this change" rather than as an error.
|
|
11
12
|
*
|
|
12
|
-
* Falls back to `deps.reticleRoot` whenever resolution cannot name a project
|
|
13
|
-
*
|
|
13
|
+
* Falls back to `deps.reticleRoot` whenever resolution cannot name a project — EXCEPT when the
|
|
14
|
+
* caller named a session that resolves to nothing, which is the one case where the fallback is a
|
|
15
|
+
* wrong answer rather than a cautious one. See `sessionProjectId`.
|
|
14
16
|
*/
|
|
15
17
|
export declare function sessionRoot(deps: ToolDeps, sessionId: string | undefined): string;
|
|
18
|
+
/**
|
|
19
|
+
* The same answer for a caller that already holds the projectId rather than a sessionId.
|
|
20
|
+
*
|
|
21
|
+
* The suite paths resolve the project ONCE and thread it down -- the run artifact, the cloud link,
|
|
22
|
+
* the flake ledger and the flows all belong to that one project -- so asking them to go back to the
|
|
23
|
+
* session manager for an id they already have is how a caller ends up taking the root from one
|
|
24
|
+
* place and the id from another. `sessionRoot` is now this function plus a lookup, so the two
|
|
25
|
+
* cannot disagree about what a resolved root is.
|
|
26
|
+
*/
|
|
27
|
+
export declare function rootForProjectId(deps: ToolDeps, projectId: ProjectId | undefined): string;
|
|
28
|
+
/**
|
|
29
|
+
* The PROJECT directory -- one level above `.reticle` -- for a caller that runs a tool in a tree
|
|
30
|
+
* rather than writing a file into one. `git diff` is the case: it was being run in the daemon's
|
|
31
|
+
* `.reticle` subdirectory, which answers about the wrong repository (or, from a globally-registered
|
|
32
|
+
* daemon at `/` or `$HOME`, about none) while reporting a clean "nothing changed".
|
|
33
|
+
*/
|
|
34
|
+
export declare function projectDirFor(deps: ToolDeps, sessionId: string | undefined): string;
|
|
16
35
|
/**
|
|
17
36
|
* The connected session's projectId, or undefined when there is no session to ask.
|
|
18
37
|
*
|
|
19
|
-
* `sessions.resolve` throws
|
|
20
|
-
* several
|
|
21
|
-
*
|
|
38
|
+
* `sessions.resolve` throws for three reasons, and they do not all mean the same thing. Nothing
|
|
39
|
+
* connected, and several connected with none named, both mean "we cannot tell which project this
|
|
40
|
+
* call is about" — on a call that named no session that is not a reason to fail the caller's tool,
|
|
41
|
+
* which may not need a session at all, so it degrades to the daemon's own root.
|
|
42
|
+
*
|
|
43
|
+
* A sessionId that names NO session is the third, and it is a different fact. An agent that passes
|
|
44
|
+
* an id has named the project too, so swallowing that throw does not degrade to a cautious answer:
|
|
45
|
+
* it silently retargets the call at whichever checkout the daemon was started in (#994).
|
|
46
|
+
* The same fallback can expose another project's ledger on reads as well as misdirect writes.
|
|
47
|
+
*
|
|
48
|
+
* So the ambiguous case still declines (the resolver has its own `AMBIGUOUS` branch for the same
|
|
49
|
+
* reason) and the mis-addressed case refuses. There is no root that is right for a dead id, so
|
|
50
|
+
* there is no answer either.
|
|
22
51
|
*/
|
|
23
|
-
export declare function sessionProjectId(deps: ToolDeps, sessionId: string | undefined):
|
|
52
|
+
export declare function sessionProjectId(deps: ToolDeps, sessionId: string | undefined): ProjectId | undefined;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { dirname } from 'node:path';
|
|
1
2
|
/**
|
|
2
3
|
* The `.reticle` directory a tool call's artifacts belong in.
|
|
3
4
|
*
|
|
@@ -8,18 +9,62 @@
|
|
|
8
9
|
* fixed — the flow would save successfully and then be invisible to the tool that exists to replay
|
|
9
10
|
* it, which reads as "no flows covered this change" rather than as an error.
|
|
10
11
|
*
|
|
11
|
-
* Falls back to `deps.reticleRoot` whenever resolution cannot name a project
|
|
12
|
-
*
|
|
12
|
+
* Falls back to `deps.reticleRoot` whenever resolution cannot name a project — EXCEPT when the
|
|
13
|
+
* caller named a session that resolves to nothing, which is the one case where the fallback is a
|
|
14
|
+
* wrong answer rather than a cautious one. See `sessionProjectId`.
|
|
13
15
|
*/
|
|
14
16
|
export function sessionRoot(deps, sessionId) {
|
|
15
|
-
|
|
17
|
+
// Skip the LOOKUP, never the refusal.
|
|
18
|
+
//
|
|
19
|
+
// `artifactRootFor?.(…)` short-circuits its own argument, so an unwired caller used to skip
|
|
20
|
+
// resolution entirely and hand a named-but-dead session the daemon's own directory without ever
|
|
21
|
+
// checking the id -- which is how one app's intents, flows and runs land in another app's
|
|
22
|
+
// checkout. Resolving as an ARGUMENT fixes that, because arguments are evaluated first.
|
|
23
|
+
//
|
|
24
|
+
// But `resolve(undefined)` is not free: it builds the ranked no-session diagnosis and records
|
|
25
|
+
// which branch it took. With no resolver wired AND no id named there is nothing to refuse and
|
|
26
|
+
// nothing its answer could change, so that one case skips the lookup and costs what it always
|
|
27
|
+
// did. Every embedder of this engine, and every older construction of ToolDeps, is on it.
|
|
28
|
+
const nothingToRefuse = deps.artifactRootFor === undefined && sessionId === undefined;
|
|
29
|
+
return rootForProjectId(deps, nothingToRefuse ? undefined : sessionProjectId(deps, sessionId));
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* The same answer for a caller that already holds the projectId rather than a sessionId.
|
|
33
|
+
*
|
|
34
|
+
* The suite paths resolve the project ONCE and thread it down -- the run artifact, the cloud link,
|
|
35
|
+
* the flake ledger and the flows all belong to that one project -- so asking them to go back to the
|
|
36
|
+
* session manager for an id they already have is how a caller ends up taking the root from one
|
|
37
|
+
* place and the id from another. `sessionRoot` is now this function plus a lookup, so the two
|
|
38
|
+
* cannot disagree about what a resolved root is.
|
|
39
|
+
*/
|
|
40
|
+
export function rootForProjectId(deps, projectId) {
|
|
41
|
+
return deps.artifactRootFor?.(projectId).root ?? deps.reticleRoot;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* The PROJECT directory -- one level above `.reticle` -- for a caller that runs a tool in a tree
|
|
45
|
+
* rather than writing a file into one. `git diff` is the case: it was being run in the daemon's
|
|
46
|
+
* `.reticle` subdirectory, which answers about the wrong repository (or, from a globally-registered
|
|
47
|
+
* daemon at `/` or `$HOME`, about none) while reporting a clean "nothing changed".
|
|
48
|
+
*/
|
|
49
|
+
export function projectDirFor(deps, sessionId) {
|
|
50
|
+
return dirname(sessionRoot(deps, sessionId));
|
|
16
51
|
}
|
|
17
52
|
/**
|
|
18
53
|
* The connected session's projectId, or undefined when there is no session to ask.
|
|
19
54
|
*
|
|
20
|
-
* `sessions.resolve` throws
|
|
21
|
-
* several
|
|
22
|
-
*
|
|
55
|
+
* `sessions.resolve` throws for three reasons, and they do not all mean the same thing. Nothing
|
|
56
|
+
* connected, and several connected with none named, both mean "we cannot tell which project this
|
|
57
|
+
* call is about" — on a call that named no session that is not a reason to fail the caller's tool,
|
|
58
|
+
* which may not need a session at all, so it degrades to the daemon's own root.
|
|
59
|
+
*
|
|
60
|
+
* A sessionId that names NO session is the third, and it is a different fact. An agent that passes
|
|
61
|
+
* an id has named the project too, so swallowing that throw does not degrade to a cautious answer:
|
|
62
|
+
* it silently retargets the call at whichever checkout the daemon was started in (#994).
|
|
63
|
+
* The same fallback can expose another project's ledger on reads as well as misdirect writes.
|
|
64
|
+
*
|
|
65
|
+
* So the ambiguous case still declines (the resolver has its own `AMBIGUOUS` branch for the same
|
|
66
|
+
* reason) and the mis-addressed case refuses. There is no root that is right for a dead id, so
|
|
67
|
+
* there is no answer either.
|
|
23
68
|
*/
|
|
24
69
|
/*
|
|
25
70
|
* Exported alongside `sessionRoot`, and that is the point rather than a convenience. A caller that
|
|
@@ -32,7 +77,24 @@ export function sessionProjectId(deps, sessionId) {
|
|
|
32
77
|
try {
|
|
33
78
|
return deps.sessions.resolve(sessionId).projectId;
|
|
34
79
|
}
|
|
35
|
-
catch {
|
|
36
|
-
|
|
80
|
+
catch (cause) {
|
|
81
|
+
if (sessionId === undefined)
|
|
82
|
+
return undefined;
|
|
83
|
+
throw unresolvedTargetRefusal(sessionId, cause);
|
|
37
84
|
}
|
|
38
85
|
}
|
|
86
|
+
/**
|
|
87
|
+
* Why a named-but-dead session gets no directory, and what to do instead.
|
|
88
|
+
*
|
|
89
|
+
* The manager's own message is carried verbatim at the end because it is the actionable half — it
|
|
90
|
+
* names the sessions that ARE connected, with their ids and urls, or says that none are. Rebuilding
|
|
91
|
+
* a worse version of that here is how the two no-session paths drifted apart the last time.
|
|
92
|
+
*/
|
|
93
|
+
function unresolvedTargetRefusal(sessionId, cause) {
|
|
94
|
+
const because = cause instanceof Error ? cause.message : String(cause);
|
|
95
|
+
return new Error(`sessionId '${sessionId}' names no session, so there is no project to resolve this call's ` +
|
|
96
|
+
'.reticle directory against, and nothing was read or written. Reticle refuses rather than ' +
|
|
97
|
+
"falling back to the daemon's own directory, which belongs to whatever project the daemon " +
|
|
98
|
+
"was started in — that is how one app's intents, flows and runs land in another app's " +
|
|
99
|
+
`checkout. Omit sessionId to use the connected app, or name one that is live. ${because}`);
|
|
100
|
+
}
|
|
@@ -63,3 +63,9 @@ export declare function hasAnyAppConnectedBefore(stateDir: string, port: number,
|
|
|
63
63
|
* diagnosis, which is why it now shares this function instead of keeping a second copy of the rule.
|
|
64
64
|
*/
|
|
65
65
|
export declare function hasProjectConnectedBefore(stateDir: string, port: number, projectId: string | undefined): boolean;
|
|
66
|
+
/**
|
|
67
|
+
* Has any of these projects connected before: the first-move question for a directory that speaks
|
|
68
|
+
* for more than one app (a monorepo root), or for none by name (an app wired by the plugin alone).
|
|
69
|
+
* The ids come from `projectIdsAt`; each is still asked the narrow question.
|
|
70
|
+
*/
|
|
71
|
+
export declare function hasAnyProjectConnectedBefore(stateDir: string, port: number, projectIds: readonly string[]): boolean;
|
|
@@ -112,3 +112,11 @@ export function hasProjectConnectedBefore(stateDir, port, projectId) {
|
|
|
112
112
|
return false;
|
|
113
113
|
return hasAnyAppConnectedBefore(stateDir, port, projectId);
|
|
114
114
|
}
|
|
115
|
+
/**
|
|
116
|
+
* Has any of these projects connected before: the first-move question for a directory that speaks
|
|
117
|
+
* for more than one app (a monorepo root), or for none by name (an app wired by the plugin alone).
|
|
118
|
+
* The ids come from `projectIdsAt`; each is still asked the narrow question.
|
|
119
|
+
*/
|
|
120
|
+
export function hasAnyProjectConnectedBefore(stateDir, port, projectIds) {
|
|
121
|
+
return projectIds.some((id) => hasProjectConnectedBefore(stateDir, port, id));
|
|
122
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/** One project this machine has paired before, in the shape the no-session facts already carry. */
|
|
2
|
-
|
|
2
|
+
interface RegisteredProject {
|
|
3
3
|
directory: string;
|
|
4
4
|
projectId?: string;
|
|
5
5
|
}
|
|
@@ -52,3 +52,4 @@ export declare function isUnscopedRoot(directory: string, home: string): boolean
|
|
|
52
52
|
* reporting it as such would tell a correctly-scoped daemon to go and look somewhere else.
|
|
53
53
|
*/
|
|
54
54
|
export declare function registeredElsewhere(home: string, directory: string): RegisteredProject[];
|
|
55
|
+
export {};
|
|
@@ -20,4 +20,13 @@
|
|
|
20
20
|
* With no evidence — a fresh daemon, or a HELLO naming no project — nothing is inferred and the
|
|
21
21
|
* plain reason stands, unless the page sent no token.
|
|
22
22
|
*/
|
|
23
|
+
/**
|
|
24
|
+
* Whether a recorded close is a pairing-token refusal.
|
|
25
|
+
*
|
|
26
|
+
* The bridge records the specific sentence it sent on the socket, not one fixed string, so a
|
|
27
|
+
* comparison with a single constant misses a wrong token, a missing token, and a daemon that
|
|
28
|
+
* belongs to another project. All three are refusals. A later good session is a separate question
|
|
29
|
+
* the caller answers with `connectedSinceLastClosure`.
|
|
30
|
+
*/
|
|
31
|
+
export declare function isAuthRefusalReason(reason: string | undefined): boolean;
|
|
23
32
|
export declare function authFailureReason(servedProjects: ReadonlySet<string>, helloProject: string | undefined, helloToken?: string): string;
|
|
@@ -28,10 +28,27 @@ const PLAIN = 'authentication failed';
|
|
|
28
28
|
* before `~/.reticle` was replaced. Both need the same first move: ask `status`, which can say which.
|
|
29
29
|
*/
|
|
30
30
|
const WRONG_TOKEN = 'authentication failed: wrong pairing token — run `reticle status` for the cause';
|
|
31
|
-
/**
|
|
32
|
-
|
|
31
|
+
/**
|
|
32
|
+
* A paste-in snippet or a Next config that never saw a token. Reload cannot mint one; restarting the
|
|
33
|
+
* dev server re-runs the substitution that should have put it there (#1047).
|
|
34
|
+
*/
|
|
35
|
+
const NO_TOKEN = 'authentication failed: no pairing token on the page; the build did not substitute it, so restart the dev server';
|
|
33
36
|
/** WebSocket close reasons are capped at 123 bytes; a longer one throws and closes with nothing. */
|
|
34
37
|
const MAX_REASON_BYTES = 123;
|
|
38
|
+
const DIFFERENT_PROJECT_PREFIX = 'this daemon serves a different project';
|
|
39
|
+
/**
|
|
40
|
+
* Whether a recorded close is a pairing-token refusal.
|
|
41
|
+
*
|
|
42
|
+
* The bridge records the specific sentence it sent on the socket, not one fixed string, so a
|
|
43
|
+
* comparison with a single constant misses a wrong token, a missing token, and a daemon that
|
|
44
|
+
* belongs to another project. All three are refusals. A later good session is a separate question
|
|
45
|
+
* the caller answers with `connectedSinceLastClosure`.
|
|
46
|
+
*/
|
|
47
|
+
export function isAuthRefusalReason(reason) {
|
|
48
|
+
if (reason === undefined)
|
|
49
|
+
return false;
|
|
50
|
+
return reason.startsWith('authentication failed') || reason.startsWith(DIFFERENT_PROJECT_PREFIX);
|
|
51
|
+
}
|
|
35
52
|
export function authFailureReason(servedProjects, helloProject, helloToken) {
|
|
36
53
|
// ONE served project, not merely some. A daemon that has accepted sessions from SEVERAL projects
|
|
37
54
|
// is demonstrably not owned by any one of them, so "that daemon is not yours, stop it" is the
|
|
@@ -45,7 +62,7 @@ export function authFailureReason(servedProjects, helloProject, helloToken) {
|
|
|
45
62
|
if (helloProject !== undefined &&
|
|
46
63
|
1 === servedProjects.size &&
|
|
47
64
|
!servedProjects.has(helloProject)) {
|
|
48
|
-
const reason =
|
|
65
|
+
const reason = `${DIFFERENT_PROJECT_PREFIX} — run \`reticle stop\` and retry`;
|
|
49
66
|
return Buffer.byteLength(reason, 'utf8') <= MAX_REASON_BYTES ? reason : PLAIN;
|
|
50
67
|
}
|
|
51
68
|
if (helloToken === undefined || 0 === helloToken.length)
|
|
@@ -20,7 +20,7 @@ export declare const WS_CLOSE_REASON: {
|
|
|
20
20
|
* this stays as the close-code table's label and as the fallback when the skew cannot be read.
|
|
21
21
|
*/
|
|
22
22
|
readonly PROTOCOL_MISMATCH: "protocol version mismatch — @reticlehq/browser and server disagree";
|
|
23
|
-
readonly AUTH_FAILED: "authentication failed —
|
|
23
|
+
readonly AUTH_FAILED: "authentication failed — restart the dev server; a reload keeps the old pairing token";
|
|
24
24
|
/**
|
|
25
25
|
* The daemon threw while handling a message. Closing is deliberate: an exception leaves the
|
|
26
26
|
* session in a state neither side can describe, and a socket left OPEN and unresponsive is the
|
|
@@ -97,6 +97,11 @@ export declare class Bridge {
|
|
|
97
97
|
* is to make something happen sooner than it already would.
|
|
98
98
|
*/
|
|
99
99
|
attachSyncRequest(handler: () => void): void;
|
|
100
|
+
/**
|
|
101
|
+
* Register a handler for the panel's harness switch. Optional for the same reason as the above: a
|
|
102
|
+
* bridge built without one ignores the request rather than refusing it.
|
|
103
|
+
*/
|
|
104
|
+
attachHarnessRequest(handler: (enabled: boolean) => void): void;
|
|
100
105
|
/** Register a handler to run when a session connects. Additive — every handler runs. */
|
|
101
106
|
attachSessionCreate(handler: (session: Session) => void): void;
|
|
102
107
|
close(): Promise<void>;
|
|
@@ -48,7 +48,7 @@ export const WS_CLOSE_REASON = {
|
|
|
48
48
|
* this stays as the close-code table's label and as the fallback when the skew cannot be read.
|
|
49
49
|
*/
|
|
50
50
|
PROTOCOL_MISMATCH: 'protocol version mismatch — @reticlehq/browser and server disagree',
|
|
51
|
-
AUTH_FAILED: 'authentication failed —
|
|
51
|
+
AUTH_FAILED: 'authentication failed — restart the dev server; a reload keeps the old pairing token',
|
|
52
52
|
/**
|
|
53
53
|
* The daemon threw while handling a message. Closing is deliberate: an exception leaves the
|
|
54
54
|
* session in a state neither side can describe, and a socket left OPEN and unresponsive is the
|
|
@@ -119,6 +119,24 @@ function replayRequest(event) {
|
|
|
119
119
|
const name = event.data['text'];
|
|
120
120
|
return 'string' === typeof name && name.length > 0 ? name : undefined;
|
|
121
121
|
}
|
|
122
|
+
/**
|
|
123
|
+
* The desired harness state if this event is the panel's switch, else undefined.
|
|
124
|
+
*
|
|
125
|
+
* Carries `on`/`off` rather than meaning "toggle": a double-click on a flaky connection sends two,
|
|
126
|
+
* and two toggles land back where they started while two `on`s are the same as one.
|
|
127
|
+
*/
|
|
128
|
+
function harnessRequest(event) {
|
|
129
|
+
if (event.type !== EventType.HUMAN_CONTROL)
|
|
130
|
+
return undefined;
|
|
131
|
+
if (event.data['kind'] !== HumanControlKind.HARNESS)
|
|
132
|
+
return undefined;
|
|
133
|
+
const want = event.data['text'];
|
|
134
|
+
if ('on' === want)
|
|
135
|
+
return true;
|
|
136
|
+
if ('off' === want)
|
|
137
|
+
return false;
|
|
138
|
+
return undefined;
|
|
139
|
+
}
|
|
122
140
|
/** True when this event is the panel's "sync now" button. Pure boundary narrowing, like the above. */
|
|
123
141
|
function isSyncRequest(event) {
|
|
124
142
|
return event.type === EventType.HUMAN_CONTROL && event.data['kind'] === HumanControlKind.SYNC;
|
|
@@ -225,6 +243,7 @@ export class Bridge {
|
|
|
225
243
|
#onSessionEnd;
|
|
226
244
|
/** Wired by the daemon: push to the dashboard now, because somebody asked in the panel. */
|
|
227
245
|
#onSyncRequest;
|
|
246
|
+
#onHarnessRequest;
|
|
228
247
|
constructor(options) {
|
|
229
248
|
const host = options.host ?? LOOPBACK_HOST;
|
|
230
249
|
if ((options.token?.length ?? 0) > TRANSPORT_LIMITS.MAX_TOKEN_LENGTH) {
|
|
@@ -479,7 +498,7 @@ export class Bridge {
|
|
|
479
498
|
presented: parsed.token !== undefined && 0 < parsed.token.length,
|
|
480
499
|
tokenSource: this.#tokenSource,
|
|
481
500
|
});
|
|
482
|
-
this.sessions.noteClosure(
|
|
501
|
+
this.sessions.noteClosure(reason, this.#clock());
|
|
483
502
|
socket.close(WS_CLOSE.AUTH_FAILED[0], reason);
|
|
484
503
|
return;
|
|
485
504
|
}
|
|
@@ -616,6 +635,10 @@ export class Bridge {
|
|
|
616
635
|
// A panel ▶ replay needs the daemon's flow store, which the Session can't reach — route it to
|
|
617
636
|
// the daemon-wired handler instead of the in-session control path. Everything else is normal.
|
|
618
637
|
const replay = replayRequest(parsed.event);
|
|
638
|
+
// Narrowed before the chain, not inside it: the result is needed by the branch AND by the
|
|
639
|
+
// test that decides whether this is a panel control at all, and calling it twice would
|
|
640
|
+
// invite the two calls to disagree.
|
|
641
|
+
const harness = harnessRequest(parsed.event);
|
|
619
642
|
if (replay !== undefined)
|
|
620
643
|
this.#onReplay?.(session.id, replay);
|
|
621
644
|
// Same shape as ▶ replay, and routed the same way: what to push lives in the daemon's
|
|
@@ -623,6 +646,10 @@ export class Bridge {
|
|
|
623
646
|
// us to hurry, not asking for a result.
|
|
624
647
|
else if (isSyncRequest(parsed.event))
|
|
625
648
|
this.#onSyncRequest?.();
|
|
649
|
+
// The switch is written through to the platform by the daemon, for the same reason: the
|
|
650
|
+
// Session has no cloud credential and should not grow one.
|
|
651
|
+
else if (harness !== undefined)
|
|
652
|
+
this.#onHarnessRequest?.(harness);
|
|
626
653
|
// Pass the raw frame's byte length so the buffer doesn't re-serialize every event for accounting.
|
|
627
654
|
else
|
|
628
655
|
session.pushEvent(parsed.event, Buffer.byteLength(text, 'utf8'));
|
|
@@ -758,6 +785,13 @@ export class Bridge {
|
|
|
758
785
|
attachSyncRequest(handler) {
|
|
759
786
|
this.#onSyncRequest = handler;
|
|
760
787
|
}
|
|
788
|
+
/**
|
|
789
|
+
* Register a handler for the panel's harness switch. Optional for the same reason as the above: a
|
|
790
|
+
* bridge built without one ignores the request rather than refusing it.
|
|
791
|
+
*/
|
|
792
|
+
attachHarnessRequest(handler) {
|
|
793
|
+
this.#onHarnessRequest = handler;
|
|
794
|
+
}
|
|
761
795
|
/** Register a handler to run when a session connects. Additive — every handler runs. */
|
|
762
796
|
attachSessionCreate(handler) {
|
|
763
797
|
this.#onSessionCreate.push(handler);
|
|
@@ -7,6 +7,7 @@ import { classifyConnectFailure } from '../../telemetry/connect-failure.js';
|
|
|
7
7
|
import { ActionType, DriveErrorCode, DRIVE_PLAYWRIGHT_MISSING_MSG } from '@reticlehq/core';
|
|
8
8
|
import { installNetworkMocks } from './network-mock.js';
|
|
9
9
|
import { attachNetworkDetail } from './network-detail.js';
|
|
10
|
+
import { injectedConnectArgs } from '../pool/zero-install.js';
|
|
10
11
|
/** Structured, code-tagged failure so callers branch on cause, not message text. */
|
|
11
12
|
export class DriveError extends Error {
|
|
12
13
|
code;
|
|
@@ -338,7 +339,7 @@ export class LaunchedRealInputProvider {
|
|
|
338
339
|
await page.waitForFunction('!!globalThis.__reticleInstance', {
|
|
339
340
|
timeout: INJECT_CONNECT_WAIT_MS,
|
|
340
341
|
});
|
|
341
|
-
const arg = JSON.stringify(
|
|
342
|
+
const arg = JSON.stringify(injectedConnectArgs(opts));
|
|
342
343
|
await page.evaluate(`globalThis.__reticleInstance.connect(${arg})`);
|
|
343
344
|
}
|
|
344
345
|
catch {
|
|
@@ -18,6 +18,11 @@ export interface Lease {
|
|
|
18
18
|
readonly url: string;
|
|
19
19
|
readonly navStatus?: number;
|
|
20
20
|
release(): Promise<void>;
|
|
21
|
+
/**
|
|
22
|
+
* Hand the page the zero-install reader: evaluated in the live document, and added ahead of every
|
|
23
|
+
* document this lease loads next. False when there is no reader, or the page cannot run one.
|
|
24
|
+
*/
|
|
25
|
+
injectReader?(): Promise<boolean>;
|
|
21
26
|
}
|
|
22
27
|
/** Default lease time-to-live: a lease untouched for this long is presumed orphaned and reclaimed. */
|
|
23
28
|
export declare const DEFAULT_LEASE_TTL_MS: number;
|
|
@@ -32,6 +37,11 @@ interface BrowserPoolOptions {
|
|
|
32
37
|
leaseTtlMs?: number;
|
|
33
38
|
/** Per-lease navigation timeout — a page that won't load fails its own lease, never blocks a slot. */
|
|
34
39
|
navTimeoutMs?: number;
|
|
40
|
+
/**
|
|
41
|
+
* The zero-install reader, built where the daemon knows its own port and pairing token. A thunk so
|
|
42
|
+
* the ~450KB build is only read if some page ever needs it; undefined when there is none to give.
|
|
43
|
+
*/
|
|
44
|
+
zeroInstallScript?: () => string | undefined;
|
|
35
45
|
}
|
|
36
46
|
/**
|
|
37
47
|
* Owns a single browser and leases isolated contexts out of it, capped at `maxContexts`.
|
|
@@ -30,6 +30,7 @@ export class BrowserPool {
|
|
|
30
30
|
#genId;
|
|
31
31
|
#now;
|
|
32
32
|
#ttl;
|
|
33
|
+
#zeroInstall;
|
|
33
34
|
#navTimeout;
|
|
34
35
|
#browser;
|
|
35
36
|
#launching;
|
|
@@ -70,6 +71,7 @@ export class BrowserPool {
|
|
|
70
71
|
this.#now = opts.now ?? (() => Date.now());
|
|
71
72
|
this.#ttl = opts.leaseTtlMs ?? DEFAULT_LEASE_TTL_MS;
|
|
72
73
|
this.#navTimeout = opts.navTimeoutMs ?? DEFAULT_NAV_TIMEOUT_MS;
|
|
74
|
+
this.#zeroInstall = opts.zeroInstallScript;
|
|
73
75
|
}
|
|
74
76
|
/** The TTL configured for leases — how long an untouched lease lives before the reaper reclaims it. */
|
|
75
77
|
leaseTtlMs() {
|
|
@@ -405,6 +407,14 @@ export class BrowserPool {
|
|
|
405
407
|
url,
|
|
406
408
|
...(navStatus !== undefined ? { navStatus } : {}),
|
|
407
409
|
release: () => this.#release(sessionId),
|
|
410
|
+
injectReader: async () => {
|
|
411
|
+
const script = this.#zeroInstall?.();
|
|
412
|
+
if (script === undefined || page.evaluate === undefined)
|
|
413
|
+
return false;
|
|
414
|
+
await page.addInitScript?.(script);
|
|
415
|
+
await page.evaluate(script);
|
|
416
|
+
return true;
|
|
417
|
+
},
|
|
408
418
|
};
|
|
409
419
|
}
|
|
410
420
|
catch (err) {
|
|
@@ -41,6 +41,7 @@ function wrapBrowser(browser) {
|
|
|
41
41
|
return {
|
|
42
42
|
goto: (url, opts) => page.goto(url, gotoOptions(opts?.timeoutMs)),
|
|
43
43
|
close: () => page.close(),
|
|
44
|
+
evaluate: (script) => page.evaluate(script),
|
|
44
45
|
// Playwright returns a Buffer; Uint8Array is what the visual store and differ take.
|
|
45
46
|
screenshot: async (opts) => new Uint8Array(await page.screenshot({ fullPage: true === opts?.fullPage })),
|
|
46
47
|
// Three moves, same as performGesture: a single move to the center can be a no-op if
|
|
@@ -34,6 +34,8 @@ export interface PooledPage {
|
|
|
34
34
|
* reports dispatched/settled while the styles never ran.
|
|
35
35
|
*/
|
|
36
36
|
hover?(x: number, y: number): Promise<void>;
|
|
37
|
+
/** Evaluate a script in the page's current document. OPTIONAL: without it no reader is injected. */
|
|
38
|
+
evaluate?(script: string): Promise<unknown>;
|
|
37
39
|
/** Add an init script to run after document creation but before any page scripts run. */
|
|
38
40
|
addInitScript?<Arg>(script: ((arg: Arg) => void) | string, arg?: Arg): Promise<InitScriptHandle | void>;
|
|
39
41
|
/**
|