@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
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a drive is FOR, decided before it starts and read out of `.reticle` rather than the app.
|
|
3
|
+
*
|
|
4
|
+
* The driver used to open its eyes on a page and pick whatever looked interesting. That makes every
|
|
5
|
+
* run start from zero: it re-drives journeys that are already saved, and it has no idea which
|
|
6
|
+
* declared intent nobody has ever tested. Both are the same missing input — the project's own
|
|
7
|
+
* record of what it claims to do and what has been proved about it.
|
|
8
|
+
*
|
|
9
|
+
* `.reticle` already holds that. `buildDomainModel` reads `.reticle/flows/` and
|
|
10
|
+
* `.reticle/contract.json` and answers with every saved flow, the consequence that MUST hold for
|
|
11
|
+
* each one, and the GAPS: signals and testids the app DECLARED that no flow asserts. So the plan is
|
|
12
|
+
* derivable with no browser, no model and no source code — which is the order this product argues
|
|
13
|
+
* for everywhere else and did not follow here.
|
|
14
|
+
*
|
|
15
|
+
* Two kinds of step, and the split is the whole economics:
|
|
16
|
+
*
|
|
17
|
+
* REPLAY a journey that is already recorded. Deterministic, no model in the loop, a few hundred
|
|
18
|
+
* tokens. Anything already covered belongs here and must never be re-driven.
|
|
19
|
+
* DRIVE a gap. A model is worth paying for exactly where nothing is proved yet.
|
|
20
|
+
*
|
|
21
|
+
* Nothing here calls a model. The plan is the deterministic half, and the model's only job is to
|
|
22
|
+
* choose among steps somebody already decided were worth taking.
|
|
23
|
+
*/
|
|
24
|
+
export const PlanStepKind = {
|
|
25
|
+
/** Already recorded: replay it deterministically instead of paying a model to rediscover it. */
|
|
26
|
+
REPLAY: 'replay',
|
|
27
|
+
/** Declared but never proved: this is what a drive is actually for. */
|
|
28
|
+
DRIVE: 'drive',
|
|
29
|
+
};
|
|
30
|
+
/**
|
|
31
|
+
* How many untested declarations become drive steps.
|
|
32
|
+
*
|
|
33
|
+
* A contract can declare hundreds of testids, and a plan longer than the step budget is not a plan,
|
|
34
|
+
* it is a list. Capped so the drive spends its budget on the front of a ranked queue rather than
|
|
35
|
+
* spreading itself across everything and finishing none of it.
|
|
36
|
+
*/
|
|
37
|
+
const MAX_DRIVE_STEPS = 12;
|
|
38
|
+
/**
|
|
39
|
+
* How many recorded journeys become replay steps.
|
|
40
|
+
*
|
|
41
|
+
* A project with seventy-four flows produced a seventy-four-step plan, which went into the tool
|
|
42
|
+
* result AND the standing instruction — thousands of tokens spent, every turn, listing work the
|
|
43
|
+
* drive had no budget to do. The whole argument for reading `.reticle` first is spending less, and
|
|
44
|
+
* an uncapped plan spends more.
|
|
45
|
+
*
|
|
46
|
+
* `reticle_domain` caps its own per-flow detail at 25 for the same measured reason. Ranked before
|
|
47
|
+
* cutting, so what survives is what is worth replaying first rather than whatever was listed first.
|
|
48
|
+
*/
|
|
49
|
+
const MAX_REPLAY_STEPS = 15;
|
|
50
|
+
/** Signals first: a declared signal is a stronger statement of intent than a testid. */
|
|
51
|
+
function driveSteps(domain) {
|
|
52
|
+
const steps = [];
|
|
53
|
+
for (const signal of domain.gaps.declaredUntestedSignals) {
|
|
54
|
+
steps.push({
|
|
55
|
+
kind: PlanStepKind.DRIVE,
|
|
56
|
+
target: signal,
|
|
57
|
+
why: `the app declares the signal "${signal}" and no saved flow asserts it`,
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
for (const testid of domain.gaps.declaredUntestedTestids) {
|
|
61
|
+
steps.push({
|
|
62
|
+
kind: PlanStepKind.DRIVE,
|
|
63
|
+
target: testid,
|
|
64
|
+
why: `the app declares the control "${testid}" and no saved flow exercises it`,
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
return steps.slice(0, MAX_DRIVE_STEPS);
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Replay steps, worst-risk first when there is run history to rank by.
|
|
71
|
+
*
|
|
72
|
+
* A flow that asserts nothing is listed too, and says so: replaying it proves very little, and the
|
|
73
|
+
* honest plan admits that rather than counting it as coverage.
|
|
74
|
+
*/
|
|
75
|
+
function replaySteps(domain) {
|
|
76
|
+
const byName = new Map(domain.flows.map((flow) => [flow.name, flow]));
|
|
77
|
+
const ordered = 0 === domain.riskRanked.length
|
|
78
|
+
? domain.flows.map((flow) => flow.name)
|
|
79
|
+
: [
|
|
80
|
+
...domain.riskRanked,
|
|
81
|
+
...domain.flows.map((f) => f.name).filter((name) => !domain.riskRanked.includes(name)),
|
|
82
|
+
];
|
|
83
|
+
/*
|
|
84
|
+
* Flows that assert a real consequence come first, whatever the run history says.
|
|
85
|
+
*
|
|
86
|
+
* A flow with no `mustHold` passes as long as its steps still run, so replaying one proves close
|
|
87
|
+
* to nothing — and when the plan has to be cut, proving nothing is the first thing to give up.
|
|
88
|
+
*/
|
|
89
|
+
const ranked = [
|
|
90
|
+
...ordered.filter((name) => byName.get(name)?.mustHold !== undefined),
|
|
91
|
+
...ordered.filter((name) => byName.get(name)?.mustHold === undefined),
|
|
92
|
+
];
|
|
93
|
+
const steps = [];
|
|
94
|
+
for (const name of ranked) {
|
|
95
|
+
const flow = byName.get(name);
|
|
96
|
+
if (flow === undefined)
|
|
97
|
+
continue;
|
|
98
|
+
steps.push({
|
|
99
|
+
kind: PlanStepKind.REPLAY,
|
|
100
|
+
target: name,
|
|
101
|
+
why: flow.mustHold === undefined
|
|
102
|
+
? `already recorded, but it asserts no consequence — replaying it proves only that the steps still run`
|
|
103
|
+
: `already recorded; it must still hold: ${flow.mustHold}`,
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
return steps.slice(0, MAX_REPLAY_STEPS);
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Build the plan.
|
|
110
|
+
*
|
|
111
|
+
* Replays come first, always. They are the cheap half and they establish what is still true before
|
|
112
|
+
* any model is paid to explore what is not yet known — and a regression found by a replay is found
|
|
113
|
+
* for a few hundred tokens rather than a few hundred thousand.
|
|
114
|
+
*/
|
|
115
|
+
export function buildHarnessPlan(domain) {
|
|
116
|
+
const replays = replaySteps(domain);
|
|
117
|
+
const drives = driveSteps(domain);
|
|
118
|
+
const steps = [...replays, ...drives];
|
|
119
|
+
// Counted over EVERYTHING, then listed up to the cap — the same split `reticle_domain` uses, so a
|
|
120
|
+
// capped listing never quietly becomes a smaller analysis.
|
|
121
|
+
const totalRecorded = domain.flows.length;
|
|
122
|
+
const totalGaps = domain.gaps.declaredUntestedSignals.length + domain.gaps.declaredUntestedTestids.length;
|
|
123
|
+
const omitted = totalRecorded - replays.length + (totalGaps - drives.length);
|
|
124
|
+
/*
|
|
125
|
+
* Untested declarations first: a signal no flow asserts is exactly the one a drive should be
|
|
126
|
+
* claiming, and it is the same ranking the drive steps above already use.
|
|
127
|
+
*
|
|
128
|
+
* Then the signals saved flows ALREADY assert, which is not redundant and not a nicety. Most apps
|
|
129
|
+
* declare no capabilities at all -- the production dashboard this was measured on has no
|
|
130
|
+
* `contract.json` -- so a vocabulary built only from declarations is empty for exactly the
|
|
131
|
+
* projects that most need one. A signal a saved flow asserts is better evidence than a
|
|
132
|
+
* declaration anyway: it is a name the app has actually been observed to fire.
|
|
133
|
+
*/
|
|
134
|
+
const vocabulary = [
|
|
135
|
+
...domain.gaps.declaredUntestedSignals,
|
|
136
|
+
...domain.declared.signals,
|
|
137
|
+
...domain.flows.flatMap((flow) => flow.signals),
|
|
138
|
+
].filter((name, index, all) => 0 < name.length && all.indexOf(name) === index);
|
|
139
|
+
const summary = 0 === steps.length
|
|
140
|
+
? 'Nothing is recorded and nothing is declared: there is no plan to follow, so drive the app to find out what it does.'
|
|
141
|
+
: `${String(totalRecorded)} recorded journey(s) and ${String(totalGaps)} untested declaration(s); the plan lists the ${String(steps.length)} worth doing first${0 === omitted ? '' : `, omitting ${String(omitted)}`}.`;
|
|
142
|
+
return { steps, summary, vocabulary };
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* The plan as the model reads it.
|
|
146
|
+
*
|
|
147
|
+
* Intent first and names second, because a model ranking "what next" against a list of identifiers
|
|
148
|
+
* is guessing, and ranking against what the project says each one is FOR is not. The kind is stated
|
|
149
|
+
* so it can prefer finishing the cheap deterministic half before spending itself on the rest.
|
|
150
|
+
*/
|
|
151
|
+
export function planAsText(plan) {
|
|
152
|
+
if (0 === plan.steps.length)
|
|
153
|
+
return 'PLAN: none — nothing recorded, nothing declared.';
|
|
154
|
+
const lines = plan.steps.map((step, index) => ` ${String(index + 1)}. [${step.kind}] ${step.target} — ${step.why}`);
|
|
155
|
+
return `PLAN (from .reticle, not from the source):\n${lines.join('\n')}`;
|
|
156
|
+
}
|
|
@@ -23,7 +23,18 @@ import { withTimeout } from '../../features/harness/with-timeout.js';
|
|
|
23
23
|
* only through a discovery hop — and a hop the model has to take on every journey is a hop it will
|
|
24
24
|
* sometimes skip, which silently turns the cheap path off.
|
|
25
25
|
*/
|
|
26
|
-
|
|
26
|
+
/**
|
|
27
|
+
* Beyond the default surface, the three tools a drive cannot do its job without.
|
|
28
|
+
*
|
|
29
|
+
* RECORD and FLOW_SAVE are how a drive keeps what it drove. FLOW_REPLAY is how it avoids driving
|
|
30
|
+
* the same thing twice — it was absent, so a driver handed a plan full of already-recorded journeys
|
|
31
|
+
* had no way to run one, and every run paid a model to rediscover what was on disk.
|
|
32
|
+
*/
|
|
33
|
+
const HARNESS_EXTRA_TOOLS = [
|
|
34
|
+
ReticleTool.RECORD,
|
|
35
|
+
ReticleTool.FLOW_SAVE,
|
|
36
|
+
ReticleTool.FLOW_REPLAY,
|
|
37
|
+
];
|
|
27
38
|
/**
|
|
28
39
|
* The one deliberate subtraction.
|
|
29
40
|
*
|
|
@@ -14,10 +14,11 @@ import { resultIsError } from '../mcp/faults/mcp-is-error.js';
|
|
|
14
14
|
import { verificationOf } from '../../telemetry/verification-of.js';
|
|
15
15
|
import { emitBugFoundHook, emitVerdictHook } from '../../hooks/hook-emit.js';
|
|
16
16
|
import { reportOnboardingStep } from '../../telemetry/onboarding-funnel.js';
|
|
17
|
-
import { noteFirstVerdict, noteOnboardingFirst } from '../../telemetry/onboarding-firsts.js';
|
|
17
|
+
import { noteActed, noteFirstVerdict, noteOnboardingFirst } from '../../telemetry/onboarding-firsts.js';
|
|
18
|
+
import { withHarnessDrive } from '../../telemetry/harness-drive.js';
|
|
18
19
|
import { OnboardingPhase, OnboardingStepStatus } from '@reticlehq/core/telemetry';
|
|
19
|
-
import { asString } from '@reticlehq/core';
|
|
20
|
-
import { sessionIdFromArgs, spentRefFromArgs } from './tools-helpers.js';
|
|
20
|
+
import { DiscoveryInvite, asString } from '@reticlehq/core';
|
|
21
|
+
import { SESSION_ID_ARG, sessionIdFromArgs, spentRefFromArgs } from './tools-helpers.js';
|
|
21
22
|
import { EnvelopeKey } from './tool-kit.js';
|
|
22
23
|
import { ReticleTool } from '@reticlehq/core';
|
|
23
24
|
import { takeFeedbackPrompt } from './feedback-tools.js';
|
|
@@ -133,7 +134,7 @@ export const SESSION_EXEMPT_TOOLS = new Set([
|
|
|
133
134
|
// FLOW_HEAL is no longer a name runTool ever sees: it became `reticle_verify {action:"heal"}`,
|
|
134
135
|
// and VERIFY is already listed here, so the exemption is inherited from the parent. Left as a
|
|
135
136
|
// dangling name it would be a lie this file's own test 6 is written to catch.
|
|
136
|
-
ReticleTool.INTENT, // reads/writes .reticle/intent
|
|
137
|
+
ReticleTool.INTENT, // reads/writes .reticle/intent/; sessionId only picks the project
|
|
137
138
|
ReticleTool.CONTEXT, // folds the journal + intent ledger; must still answer when nothing is connected
|
|
138
139
|
ReticleTool.PROJECT, // reads .reticle/project.json
|
|
139
140
|
// Reads the team's shared memory over HTTP; sessionId only resolves WHICH project's link file to
|
|
@@ -152,6 +153,17 @@ export const SESSION_EXEMPT_TOOLS = new Set([
|
|
|
152
153
|
// when NO session exists — "nothing ever connected" is feedback we especially want.
|
|
153
154
|
ReticleTool.FEEDBACK,
|
|
154
155
|
]);
|
|
156
|
+
/**
|
|
157
|
+
* The merged action that hands the drive to a model inside the daemon.
|
|
158
|
+
*
|
|
159
|
+
* Derived from the member tool's own name rather than spelled again: the merge dispatches on
|
|
160
|
+
* `action`, and `reticle_verify` plus `explore` IS `reticle_verify_explore`. A literal here would be
|
|
161
|
+
* a third spelling of one name, and the one that silently stops matching when the action is renamed.
|
|
162
|
+
*/
|
|
163
|
+
const HARNESS_DRIVE_ACTION = ReticleTool.VERIFY_EXPLORE.slice(`${ReticleTool.VERIFY}_`.length);
|
|
164
|
+
function isHarnessDrive(toolName, args) {
|
|
165
|
+
return ReticleTool.VERIFY === toolName && HARNESS_DRIVE_ACTION === args['action'];
|
|
166
|
+
}
|
|
155
167
|
function isPlainObject(value) {
|
|
156
168
|
return 'object' === typeof value && value !== null && !Array.isArray(value);
|
|
157
169
|
}
|
|
@@ -299,6 +311,34 @@ function firstOversizedArg(args) {
|
|
|
299
311
|
}
|
|
300
312
|
return undefined;
|
|
301
313
|
}
|
|
314
|
+
/**
|
|
315
|
+
* The args the handler is dispatched with, pinned to the session THIS call resolved.
|
|
316
|
+
*
|
|
317
|
+
* `runTool` resolves the session once and then attributes the whole call to it: the pool lease it
|
|
318
|
+
* refreshes, the health envelope it splices, the tab a ref is recorded as minted in, the project
|
|
319
|
+
* ledger the call lands in, the session a verdict is reported under. The handler was then handed
|
|
320
|
+
* the caller's args untouched, so a call that named no `sessionId` resolved a SECOND time — and a
|
|
321
|
+
* tab that connects in between can win auto-selection on its fresher heartbeat. The action goes to
|
|
322
|
+
* one tab and the answer describes another, which is a verdict about a page that was never driven
|
|
323
|
+
* and reads exactly like a correct one (#983).
|
|
324
|
+
*
|
|
325
|
+
* The wrong-tab ref guard is the sharpest case: it is asked whether the ref belongs to the tab
|
|
326
|
+
* resolution picked, so its permission is only sound if that is also the tab the handler drives.
|
|
327
|
+
*
|
|
328
|
+
* A COPY, never a write into the caller's object. `reticle_run`, the merged families and the
|
|
329
|
+
* programmatic invoker all pass the same record on, and a dispatch decision leaking back into it
|
|
330
|
+
* would outlive this call.
|
|
331
|
+
*
|
|
332
|
+
* Only a GUESSED session is pinned. An explicit id — including one lifted off a sequence step — is
|
|
333
|
+
* passed through exactly as written, so whether a stale id refuses or rebinds stays the resolver's
|
|
334
|
+
* decision; and a tool no session was resolved for (disk-only tools, or nothing connected) is left
|
|
335
|
+
* unscoped, so it answers as it always did.
|
|
336
|
+
*/
|
|
337
|
+
function pinnedArgs(args, explicitSessionId, session) {
|
|
338
|
+
if (explicitSessionId !== undefined || session === undefined)
|
|
339
|
+
return args;
|
|
340
|
+
return { ...args, [SESSION_ID_ARG]: session.id };
|
|
341
|
+
}
|
|
302
342
|
export async function runTool(tool, deps, args) {
|
|
303
343
|
// Both dispatch paths (MCP + programmatic) pass through here — the one place "which tool is mostly
|
|
304
344
|
// used" can be counted. This used to EMIT an event per call; it now increments an in-process counter
|
|
@@ -313,7 +353,7 @@ export async function runTool(tool, deps, args) {
|
|
|
313
353
|
noteToolCall();
|
|
314
354
|
// The ONBOARD firsts, at the chokepoint every call already passes through — so a second dispatch
|
|
315
355
|
// path cannot quietly stop reporting them the way an extra listener would.
|
|
316
|
-
noteOnboardingFirst(tool.name
|
|
356
|
+
noteOnboardingFirst(tool.name);
|
|
317
357
|
// The impact record needs the project's own `.reticle` root, and this is the first place every
|
|
318
358
|
// call knows it. Idempotent: the first root wins for the daemon's lifetime.
|
|
319
359
|
initImpact({ reticleRoot: deps.reticleRoot });
|
|
@@ -343,8 +383,13 @@ export async function runTool(tool, deps, args) {
|
|
|
343
383
|
}
|
|
344
384
|
// An ACTION is what gets abandoned. Counted here, against verifications, so "the agent drove the
|
|
345
385
|
// page and then wandered off" becomes a number instead of an impression.
|
|
346
|
-
if (ACTION_TOOLS.has(tool.name))
|
|
386
|
+
if (ACTION_TOOLS.has(tool.name)) {
|
|
347
387
|
getSessionMetrics().recordAction();
|
|
388
|
+
// The two funnel rungs this branch is the authority on: the first action of the run, and the
|
|
389
|
+
// fact that this install has been driven at all. Reported from the set that already decides
|
|
390
|
+
// what "drove the page" means, so a fourth driving tool inherits both.
|
|
391
|
+
noteActed(args);
|
|
392
|
+
}
|
|
348
393
|
const settleTiming = getSessionMetrics().startToolCall(tool.name, args);
|
|
349
394
|
const startedAt = Date.now();
|
|
350
395
|
const rawSessionId = sessionIdFromArgs(args);
|
|
@@ -430,12 +475,27 @@ export async function runTool(tool, deps, args) {
|
|
|
430
475
|
const leaseId = session?.id ?? rawSessionId;
|
|
431
476
|
if (leaseId !== undefined)
|
|
432
477
|
deps.pool?.touch(leaseId);
|
|
478
|
+
// One identity for the whole call: the handler drives the session everything above is attributed
|
|
479
|
+
// to, rather than resolving again against a registry that can have changed underneath it.
|
|
480
|
+
const dispatchArgs = pinnedArgs(args, rawSessionId, session);
|
|
433
481
|
let raw;
|
|
434
482
|
try {
|
|
435
483
|
// The one dispatch point every tool call passes through, so it is where the trace's root span
|
|
436
484
|
// belongs: with RETICLE_TRACE on, every stage that runs underneath inherits this call's id and
|
|
437
485
|
// nests under it. Free when off — see trace.ts.
|
|
438
|
-
|
|
486
|
+
// `dispatchArgs`, not `args`: an inferred session is pinned into the arguments before the
|
|
487
|
+
// handler sees them, so a tool that resolves the session a second time cannot land on a
|
|
488
|
+
// different one than the dispatcher chose.
|
|
489
|
+
const call = () => span('tool.handler', { tool: tool.name, session: session?.id }, () => tool.handler(deps, dispatchArgs));
|
|
490
|
+
// A drive we run on the user's behalf, marked for its whole span so the verdicts it produces
|
|
491
|
+
// underneath are distinguishable from the ones the user's own agent earned.
|
|
492
|
+
//
|
|
493
|
+
// The outer call emits no verdict of its own, and that is the honest answer rather than a gap:
|
|
494
|
+
// it drives and records, it adjudicates nothing, and its result carries no verified/pass field
|
|
495
|
+
// to read. Deriving one from how far the drive got would be inventing a verdict nobody reached.
|
|
496
|
+
// What was missing was never an event here — it was that a session driven BY US read exactly
|
|
497
|
+
// like one the agent earned, which is what the span fixes.
|
|
498
|
+
raw = await (isHarnessDrive(tool.name, args) ? withHarnessDrive(call) : call());
|
|
439
499
|
}
|
|
440
500
|
catch (error) {
|
|
441
501
|
// The commonest refusal shape by far, and the one nothing could see: the message is built, handed
|
|
@@ -549,7 +609,13 @@ export async function runTool(tool, deps, args) {
|
|
|
549
609
|
? raw
|
|
550
610
|
: {
|
|
551
611
|
...raw,
|
|
552
|
-
...(friction !== undefined
|
|
612
|
+
...(friction !== undefined
|
|
613
|
+
? {
|
|
614
|
+
[EnvelopeKey.FEEDBACK_INVITE]: inviteFor(friction),
|
|
615
|
+
// The moment Reticle got in somebody's way is the moment to offer them the founder.
|
|
616
|
+
[EnvelopeKey.TALK_TO_US]: DiscoveryInvite.AGENT,
|
|
617
|
+
}
|
|
618
|
+
: {}),
|
|
553
619
|
...(prompt !== undefined ? { [EnvelopeKey.FEEDBACK_PROMPT]: prompt } : {}),
|
|
554
620
|
...(update !== undefined ? { [EnvelopeKey.UPDATE_AVAILABLE]: update } : {}),
|
|
555
621
|
...(skew !== undefined ? { [EnvelopeKey.VERSION_SKEW]: skew } : {}),
|
|
@@ -563,7 +629,14 @@ export async function runTool(tool, deps, args) {
|
|
|
563
629
|
return result;
|
|
564
630
|
// Reuse the session resolved above so the health envelope describes the SAME session the handler
|
|
565
631
|
// drove; only re-resolve if the up-front attempt failed but the handler somehow succeeded.
|
|
566
|
-
const
|
|
632
|
+
const driven = session ?? deps.sessions.resolve(rawSessionId);
|
|
633
|
+
// ...unless the call replaced that document. A full navigation or reload registers a NEW Session
|
|
634
|
+
// under the same id (or under the id the result reports it arrived at), and the object resolved
|
|
635
|
+
// before the call is the page that unloaded: it reported itself hidden on the way out, so every
|
|
636
|
+
// response after a navigation said "throttled" about a tab that was fine. Describe what is there
|
|
637
|
+
// now. Optional-called because a test double is a partial SessionManager.
|
|
638
|
+
const arrivedId = 'string' === typeof result['sessionId'] ? result['sessionId'] : driven.id;
|
|
639
|
+
const resolved = deps.sessions.get?.(arrivedId) ?? driven;
|
|
567
640
|
const envelope = {};
|
|
568
641
|
// The health block is idempotent: add it only when the handler didn't already include a `session`.
|
|
569
642
|
if (!('session' in result))
|
|
@@ -36,6 +36,14 @@ export interface LeaseEvidence {
|
|
|
36
36
|
* reported as a marker that was not found.
|
|
37
37
|
*/
|
|
38
38
|
sdkMarker?: boolean;
|
|
39
|
+
/**
|
|
40
|
+
* The page's own Content-Security-Policy, read off the served document, when it refuses the
|
|
41
|
+
* bridge. Proof rather than a guess: the connection cannot happen until the policy allows it.
|
|
42
|
+
*/
|
|
43
|
+
cspBlock?: {
|
|
44
|
+
problem: string;
|
|
45
|
+
fix: string;
|
|
46
|
+
};
|
|
39
47
|
/**
|
|
40
48
|
* The websocket URL the leased page actually tried, read from its own console.
|
|
41
49
|
*
|
|
@@ -85,6 +85,16 @@ export function leaseNotConnectedHint(url, port, evidence = {}) {
|
|
|
85
85
|
`RETICLE_PORT, or the build plugin's \`port\`), and \`VITE_RETICLE_WS_URL\` overrides it ` +
|
|
86
86
|
`outright.${marker} ${RELEASE}`);
|
|
87
87
|
}
|
|
88
|
+
// 0b. The page's own policy forbids the connection. Read off the document it served, so it is
|
|
89
|
+
// proof about the page in hand, and it holds whether or not the app ships an SDK: neither
|
|
90
|
+
// `init` nor the zero-install reader can dial out of a page that forbids it. Every cause
|
|
91
|
+
// below presupposes a page that is allowed to connect, which is why this comes first.
|
|
92
|
+
if (evidence.cspBlock !== undefined) {
|
|
93
|
+
return (`${opening} This page's Content-Security-Policy blocks the connection: ` +
|
|
94
|
+
`${evidence.cspBlock.problem}. That is true whether or not the app has Reticle installed, ` +
|
|
95
|
+
`so installing it will not help until the policy allows the bridge. For development, ` +
|
|
96
|
+
`${evidence.cspBlock.fix}. ${RELEASE}`);
|
|
97
|
+
}
|
|
88
98
|
// 1. A recorded refusal. The only cause with proof behind it, so nothing outranks it.
|
|
89
99
|
if (evidence.refusal !== undefined) {
|
|
90
100
|
return `${opening} A dial DID arrive and was refused: ${evidence.refusal}${marker} ${RELEASE}`;
|
|
@@ -15,8 +15,6 @@ import { type NetworkMutationPort } from '../../portal/input/network-mutation.js
|
|
|
15
15
|
import type { Perturbation } from '@reticlehq/core';
|
|
16
16
|
import { type SeedStorage } from '@reticlehq/core';
|
|
17
17
|
import type { ToolDef } from './tool-kit.js';
|
|
18
|
-
/** The origin of a URL, or undefined when it does not parse. */
|
|
19
|
-
export declare function originOf(url: string): string | undefined;
|
|
20
18
|
/**
|
|
21
19
|
* Append Reticle identity params (__reticle_session, optional __reticle_project) to a URL so the app's own SDK
|
|
22
20
|
* adopts them on connect. Pure; falls back to plain concatenation if the URL can't be parsed.
|
|
@@ -78,31 +76,6 @@ export declare function resolveLeasedSessionId(sessions: {
|
|
|
78
76
|
* a human's own URL.
|
|
79
77
|
*/
|
|
80
78
|
export declare function carryReticleIdentity(fromUrl: string | undefined, toUrl: string): string;
|
|
81
|
-
/** The narrow slice of a session the probe needs. Anything that quacks like this works. */
|
|
82
|
-
interface ProbeableSession {
|
|
83
|
-
command?: (name: string, args: Record<string, unknown>, timeoutMs: number) => Promise<unknown>;
|
|
84
|
-
}
|
|
85
|
-
/**
|
|
86
|
-
* Does this tab still answer?
|
|
87
|
-
*
|
|
88
|
-
* PRESENCE IS NOT LIVENESS. The sessions map still holds a tab that is attached, streaming events,
|
|
89
|
-
* and answering nothing, so `ready` read off a row in that map hands back a lease that `snapshot`,
|
|
90
|
-
* `state` and `console` then reject (#692). It has to mean the tab replied.
|
|
91
|
-
*
|
|
92
|
-
* ANY reply counts, including one that reports the command failed. The question is whether the SDK
|
|
93
|
-
* answers at all, not what it says — so an SDK too old to know the command replies
|
|
94
|
-
* `unknown command '…'`, and that is proof. Only the absence of a reply is evidence of absence:
|
|
95
|
-
* `PendingCommands.track` REJECTS on timeout and on a dropped socket, and resolves on every real
|
|
96
|
-
* answer, so the two cases are already separated for us.
|
|
97
|
-
*
|
|
98
|
-
* `CAPABILITIES` is the probe because it takes no arguments and returns a small fixed list. No new
|
|
99
|
-
* wire command is introduced: this rides an existing round trip.
|
|
100
|
-
*
|
|
101
|
-
* Fails OPEN. A registry entry with no `command` is a shape this code did not put there, and
|
|
102
|
-
* turning a lease that works into a refusal over a probe that could not run would be a worse
|
|
103
|
-
* failure than the one being fixed.
|
|
104
|
-
*/
|
|
105
|
-
export declare function probeLeaseAlive(session: ProbeableSession | undefined, timeoutMs?: number): Promise<boolean>;
|
|
106
79
|
/**
|
|
107
80
|
* Poll until the leased tab's SDK has actually registered on the bridge, so the sessionId we return
|
|
108
81
|
* is immediately usable (the page navigates first, then connects a moment later — without this wait
|
|
@@ -122,6 +95,7 @@ export declare function acquireLeasedSession(pool: {
|
|
|
122
95
|
sessionId: string;
|
|
123
96
|
release: () => Promise<void>;
|
|
124
97
|
navStatus?: number;
|
|
98
|
+
injectReader?: () => Promise<boolean>;
|
|
125
99
|
}>;
|
|
126
100
|
/**
|
|
127
101
|
* The address this lease's page said it could not reach. Optional so a test double need not
|
|
@@ -141,6 +115,21 @@ export declare function acquireLeasedSession(pool: {
|
|
|
141
115
|
release: () => Promise<void>;
|
|
142
116
|
}>;
|
|
143
117
|
export declare function waitForLeasedSession(isConnected: () => boolean, sleeper?: (ms: number) => Promise<void>, attempts?: number, pollMs?: number): Promise<boolean>;
|
|
118
|
+
/**
|
|
119
|
+
* Wait for the leased tab to connect; if its app never does, hand it the zero-install reader.
|
|
120
|
+
*
|
|
121
|
+
* The decision is taken HERE, by the daemon, on the one fact that settles it: no HELLO arrived. A
|
|
122
|
+
* timer inside the page cannot know that — an app can connect from a dynamic import that resolves
|
|
123
|
+
* after `load` — and a reader injected into a page whose app was about to connect would take its
|
|
124
|
+
* place and drop the app's own options. So an app that connected itself is never touched, and a page
|
|
125
|
+
* that stayed silent through the whole readiness wait is the one that gets a reader.
|
|
126
|
+
*/
|
|
127
|
+
export declare function connectOrInject(lease: {
|
|
128
|
+
injectReader?: () => Promise<boolean>;
|
|
129
|
+
}, isConnected: () => boolean, wait?: (isConnected: () => boolean) => Promise<boolean>): Promise<{
|
|
130
|
+
ready: boolean;
|
|
131
|
+
zeroInstall: boolean;
|
|
132
|
+
}>;
|
|
144
133
|
export declare function hasOriginLock(origin: string): boolean;
|
|
145
134
|
export declare const LEASE_ACQUIRE_TOOL: ToolDef;
|
|
146
135
|
export declare const LEASE_TOOLS: ToolDef[];
|
|
@@ -192,4 +181,3 @@ export declare function suiteFixturePort(realInput: RealInputProvider | undefine
|
|
|
192
181
|
* is the reach the boundary guard exists to ask about.
|
|
193
182
|
*/
|
|
194
183
|
export declare function sessionMutationPort(realInput: RealInputProvider | undefined, appUrl: string | undefined): Promise<NetworkMutationPort | undefined>;
|
|
195
|
-
export {};
|
|
@@ -14,6 +14,8 @@ import { mutationPortFor } from '../../portal/input/network-mutation.js';
|
|
|
14
14
|
import { z } from 'zod';
|
|
15
15
|
import { leaseNotConnectedHint } from './lease-hint.js';
|
|
16
16
|
import { probeSdkMarker } from './gaps/sdk-marker-probe.js';
|
|
17
|
+
import { observeWebDocument } from '../../portal/session/dev-server/served-document.js';
|
|
18
|
+
import { diagnoseObservedWebCsp } from '@reticlehq/init';
|
|
17
19
|
import { readProjectFramework, readProjectId } from '../../command/cli/ports/resolve/cli-port.js';
|
|
18
20
|
import { hasAnyAppConnectedBefore } from '../../memory/recall/prior/connection-memory.js';
|
|
19
21
|
import { AGENT_DRIVING_ELSEWHERE, AGENT_DRIVING_HERE_AGAIN, watchersToNotify, } from '../../portal/session/lease-visibility.js';
|
|
@@ -36,14 +38,38 @@ import { chromiumHint } from '../../command/cli/doctor/browser/chromium-hint.js'
|
|
|
36
38
|
* to print a paragraph, and it can only ever add a sentence: the probe returns `undefined` on any
|
|
37
39
|
* doubt and the hint then says nothing about markers.
|
|
38
40
|
*/
|
|
41
|
+
/**
|
|
42
|
+
* Whether the served page's own CSP refuses the bridge's SOCKET, read by the same code `doctor` uses.
|
|
43
|
+
*
|
|
44
|
+
* Only `connect-src` counts here. A lease injects the SDK itself, so the inline-snippet problem a
|
|
45
|
+
* `script-src` finding describes does not apply; what stops a leased page is the socket. Said in the
|
|
46
|
+
* lease's own words, because doctor's advice is about wiring a project, not about a tab.
|
|
47
|
+
*/
|
|
48
|
+
async function observedCspBlock(url, port) {
|
|
49
|
+
const document = await observeWebDocument(url);
|
|
50
|
+
if (document === undefined)
|
|
51
|
+
return undefined;
|
|
52
|
+
const refusesSocket = diagnoseObservedWebCsp(document, port).some((finding) => `${finding.problem} ${finding.fix}`.includes(CONNECT_SRC));
|
|
53
|
+
if (!refusesSocket)
|
|
54
|
+
return undefined;
|
|
55
|
+
const allow = `ws://localhost:${String(port)} ws://127.0.0.1:${String(port)}`;
|
|
56
|
+
return {
|
|
57
|
+
problem: `its ${CONNECT_SRC} does not allow ws://localhost:${String(port)}`,
|
|
58
|
+
fix: `add ${allow} to ${CONNECT_SRC}, behind a development-only check`,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
/** The CSP directive that governs the socket the SDK dials. */
|
|
62
|
+
const CONNECT_SRC = 'connect-src';
|
|
39
63
|
async function leaseEvidence(deps, port, url) {
|
|
40
64
|
const cwd = process.cwd();
|
|
41
65
|
const projectId = readProjectId(cwd);
|
|
42
66
|
const refusal = deps.sessions.lastClosure()?.reason;
|
|
43
67
|
const framework = readProjectFramework(cwd);
|
|
44
68
|
const sdkMarker = await probeSdkMarker(url);
|
|
69
|
+
const cspBlock = await observedCspBlock(url, port);
|
|
45
70
|
return {
|
|
46
71
|
...(refusal === undefined ? {} : { refusal }),
|
|
72
|
+
...(cspBlock === undefined ? {} : { cspBlock }),
|
|
47
73
|
...(framework === undefined ? {} : { framework }),
|
|
48
74
|
...(sdkMarker === undefined ? {} : { sdkMarker }),
|
|
49
75
|
previouslyConnected: hasAnyAppConnectedBefore(reticleStateHome(), port, projectId),
|
|
@@ -52,7 +78,7 @@ async function leaseEvidence(deps, port, url) {
|
|
|
52
78
|
}
|
|
53
79
|
const POOL_UNAVAILABLE = 'browser pool unavailable — the lease tools need the daemon-managed pool (start Reticle via `reticle mcp`).';
|
|
54
80
|
/** The origin of a URL, or undefined when it does not parse. */
|
|
55
|
-
|
|
81
|
+
function originOf(url) {
|
|
56
82
|
try {
|
|
57
83
|
return new URL(url).origin;
|
|
58
84
|
}
|
|
@@ -298,7 +324,7 @@ const LEASE_PROBE_TIMEOUT_MS = 1_500;
|
|
|
298
324
|
* turning a lease that works into a refusal over a probe that could not run would be a worse
|
|
299
325
|
* failure than the one being fixed.
|
|
300
326
|
*/
|
|
301
|
-
|
|
327
|
+
async function probeLeaseAlive(session, timeoutMs = LEASE_PROBE_TIMEOUT_MS) {
|
|
302
328
|
const send = session?.command;
|
|
303
329
|
if (send === undefined)
|
|
304
330
|
return true;
|
|
@@ -338,7 +364,7 @@ export async function acquireLeasedSession(pool, sessions, url, projectId, seedS
|
|
|
338
364
|
// id comes back, so handing it the lease id for an app that named its own session sent every flow
|
|
339
365
|
// in the run at a session that does not exist.
|
|
340
366
|
let registeredId;
|
|
341
|
-
await
|
|
367
|
+
await connectOrInject(lease, () => {
|
|
342
368
|
registeredId = resolveLeasedSessionId(sessions, lease.sessionId);
|
|
343
369
|
return registeredId !== undefined;
|
|
344
370
|
});
|
|
@@ -364,6 +390,23 @@ export async function waitForLeasedSession(isConnected, sleeper = sleep, attempt
|
|
|
364
390
|
}
|
|
365
391
|
return isConnected();
|
|
366
392
|
}
|
|
393
|
+
/**
|
|
394
|
+
* Wait for the leased tab to connect; if its app never does, hand it the zero-install reader.
|
|
395
|
+
*
|
|
396
|
+
* The decision is taken HERE, by the daemon, on the one fact that settles it: no HELLO arrived. A
|
|
397
|
+
* timer inside the page cannot know that — an app can connect from a dynamic import that resolves
|
|
398
|
+
* after `load` — and a reader injected into a page whose app was about to connect would take its
|
|
399
|
+
* place and drop the app's own options. So an app that connected itself is never touched, and a page
|
|
400
|
+
* that stayed silent through the whole readiness wait is the one that gets a reader.
|
|
401
|
+
*/
|
|
402
|
+
export async function connectOrInject(lease, isConnected, wait = waitForLeasedSession) {
|
|
403
|
+
if (await wait(isConnected))
|
|
404
|
+
return { ready: true, zeroInstall: false };
|
|
405
|
+
if (true !== (await lease.injectReader?.()))
|
|
406
|
+
return { ready: false, zeroInstall: false };
|
|
407
|
+
const ready = await wait(isConnected);
|
|
408
|
+
return { ready, zeroInstall: ready };
|
|
409
|
+
}
|
|
367
410
|
/**
|
|
368
411
|
* Named on its own because `reticle drive <url>` runs it too.
|
|
369
412
|
*
|
|
@@ -381,7 +424,7 @@ export function hasOriginLock(origin) {
|
|
|
381
424
|
}
|
|
382
425
|
export const LEASE_ACQUIRE_TOOL = {
|
|
383
426
|
name: ReticleTool.LEASE_ACQUIRE,
|
|
384
|
-
description: 'Lease a fresh isolated headless browser context from the shared pool and navigate it to the app URL (the app must already be running
|
|
427
|
+
description: 'Lease a fresh isolated headless browser context from the shared pool and navigate it to the app URL (the app must already be running; one with no Reticle SDK gets one supplied, reported as zeroInstall). If this origin is already leased and still connected, this returns THAT session rather than minting a second tab — a second acquire on the same origin poisons default session resolution. Returns the sessionId the leased tab registers — pass it to other tools. The pool keeps all leases in ONE browser and caps concurrency; if at capacity this waits for a free slot. Release with reticle_lease{action:"release"} when the flow is done. PREFER AN ALREADY-OPEN TAB: if reticle_sessions lists a non-leased session for this app, drive THAT instead — a lease is invisible to the person watching the app, whose HUD lives in their own tab, and a tab flagged hidden/throttled is often still driveable. Lease for isolation you actually need (a second identity, a clean context, parallel flows) or when driving the open tab has failed — this call answers with `preferExisting` when a live tab was available.',
|
|
385
428
|
inputSchema: {
|
|
386
429
|
url: z
|
|
387
430
|
.string()
|
|
@@ -398,6 +441,10 @@ export const LEASE_ACQUIRE_TOOL = {
|
|
|
398
441
|
ready: z
|
|
399
442
|
.boolean()
|
|
400
443
|
.describe('Whether the leased tab is connected AND answering. On a reused lease this is probed, not assumed — a row in the session map is presence, not liveness. False ⇒ read notReadyReason.'),
|
|
444
|
+
zeroInstall: z
|
|
445
|
+
.boolean()
|
|
446
|
+
.optional()
|
|
447
|
+
.describe('Present when this app ships no Reticle SDK and the lease supplied one. Verdicts work on the DOM, network, console and routes; there is no framework adapter, so no component state and no source file:line. Install with `npx @reticlehq/server init` for those.'),
|
|
401
448
|
notReadyReason: z
|
|
402
449
|
.enum([LeaseNotReadyReason.SDK_NEVER_DIALLED, LeaseNotReadyReason.SDK_STOPPED_ANSWERING])
|
|
403
450
|
.optional()
|
|
@@ -538,7 +585,7 @@ export const LEASE_ACQUIRE_TOOL = {
|
|
|
538
585
|
// Resolved rather than assumed: an app that names its own session registers under that name,
|
|
539
586
|
// and the id we hand back has to be the one the agent can actually drive.
|
|
540
587
|
let registeredId;
|
|
541
|
-
const ready = await
|
|
588
|
+
const { ready, zeroInstall } = await connectOrInject(lease, () => {
|
|
542
589
|
registeredId = resolveLeasedSessionId(deps.sessions, lease.sessionId);
|
|
543
590
|
return registeredId !== undefined;
|
|
544
591
|
});
|
|
@@ -569,6 +616,7 @@ export const LEASE_ACQUIRE_TOOL = {
|
|
|
569
616
|
// word: no SDK ever dialled in (look at the install) versus one dialled in and stopped
|
|
570
617
|
// answering (recover the tab). They want different next actions, so they get names.
|
|
571
618
|
...(ready ? {} : { notReadyReason: LeaseNotReadyReason.SDK_NEVER_DIALLED }),
|
|
619
|
+
...(zeroInstall ? { zeroInstall: true } : {}),
|
|
572
620
|
expiresInMs: pool.leaseTtlMs(),
|
|
573
621
|
leased: pool.activeCount(),
|
|
574
622
|
queued: pool.queuedCount(),
|
|
@@ -19,7 +19,7 @@ export const LINEAGE_TOOLS = [
|
|
|
19
19
|
{
|
|
20
20
|
name: ReticleTool.LINEAGE,
|
|
21
21
|
example: { path: 'user.profile.name' },
|
|
22
|
-
description: 'Trace BACKWARDS from a runtime value: which state change set it, which signal preceded
|
|
22
|
+
description: 'Trace BACKWARDS from a runtime value: which state change set it, which driven action it was observed under, which signal preceded it, and which request preceded that. Read-only — it draws no verdict and changes nothing. The state change is OBSERVED; every link above it is an INFERENCE and says so in its own text — a driven action is the window-tier stamp the SDK put on the event, a signal or request is a timing look-back. A candidate stamped with a DIFFERENT action is excluded; an unstamped one is kept. Where several candidates could still explain a link it names ALL of them rather than choosing, because timing alone cannot justify the choice — so a chain here is a place to look, never a proven cause. Use it after a `no` verdict or a contradiction, instead of correlating reticle_look { action: "state" }, reticle_observe { action: "network" } and reticle_observe by hand.',
|
|
23
23
|
inputSchema: {
|
|
24
24
|
path: z
|
|
25
25
|
.string()
|
|
@@ -15,5 +15,11 @@ export declare function liveCallText(text: string, advertised: ReadonlySet<strin
|
|
|
15
15
|
* was corrected and the result became unreadable, which is a worse trade than the advice was worth.
|
|
16
16
|
*
|
|
17
17
|
* Values, not keys: a key is a contract with the caller and nothing in a key is advice.
|
|
18
|
+
*
|
|
19
|
+
* And not every value is advice either. A field that carries a tool's IDENTITY is an answer to
|
|
20
|
+
* "which one", not a suggestion about what to call — rewriting it destroys the thing the caller
|
|
21
|
+
* asked for. `reticle_tools { names: ["reticle_act_sequence"] }` sets `name` to the name it was
|
|
22
|
+
* asked about, and this rewrote it to `reticle_act`, so a batch query came back with entries the
|
|
23
|
+
* caller could no longer match to their questions. See `IDENTITY_KEYS`.
|
|
18
24
|
*/
|
|
19
25
|
export declare function liveCallValues(value: unknown, advertised: ReadonlySet<string>): unknown;
|