@reticlehq/server 2.4.0 → 2.6.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 +719 -0
- package/dist/bridge/auth-failure-reason.d.ts +19 -0
- package/dist/bridge/auth-failure-reason.js +29 -0
- package/dist/{bridge-security.d.ts → bridge/bridge-security.d.ts} +1 -1
- package/dist/{bridge-security.js → bridge/bridge-security.js} +1 -1
- package/dist/{bridge.d.ts → bridge/bridge.d.ts} +13 -1
- package/dist/{bridge.js → bridge/bridge.js} +95 -15
- package/dist/{bridge.test-harness.d.ts → bridge/bridge.test-harness.d.ts} +9 -11
- package/dist/{bridge.test-harness.js → bridge/bridge.test-harness.js} +18 -10
- package/dist/{pairing-token.js → bridge/pairing-token.js} +1 -1
- package/dist/capsule/capsule-store.d.ts +8 -3
- package/dist/capsule/capsule-store.js +7 -2
- package/dist/capsule/capsule.js +3 -3
- package/dist/capsule/causal-summary.d.ts +8 -0
- package/dist/capsule/causal-summary.js +22 -12
- package/dist/capsule/divergence.d.ts +9 -4
- package/dist/capsule/divergence.js +8 -8
- package/dist/capsule/predicate-to-links.js +11 -8
- package/dist/cli/cli-doctor.d.ts +1 -0
- package/dist/cli/cli-doctor.js +135 -0
- package/dist/{cli-flow-commands.d.ts → cli/cli-flow-commands.d.ts} +2 -2
- package/dist/{cli-flow-commands.js → cli/cli-flow-commands.js} +15 -15
- package/dist/{cli-launch.d.ts → cli/cli-launch.d.ts} +32 -3
- package/dist/cli/cli-launch.js +171 -0
- package/dist/cli/cli-parse-feedback.d.ts +31 -0
- package/dist/cli/cli-parse-feedback.js +56 -0
- package/dist/{cli-parse.d.ts → cli/cli-parse.d.ts} +6 -19
- package/dist/{cli-parse.js → cli/cli-parse.js} +118 -81
- package/dist/{cli-port.d.ts → cli/cli-port.d.ts} +12 -0
- package/dist/{cli-port.js → cli/cli-port.js} +9 -9
- package/dist/cli/cli-update-commands.d.ts +4 -0
- package/dist/cli/cli-update-commands.js +93 -0
- package/dist/{cli-verify.js → cli/cli-verify.js} +14 -14
- package/dist/{cloud-cli.js → cli/cloud-cli.js} +29 -24
- package/dist/cli/doctor-daemon-line.d.ts +37 -0
- package/dist/cli/doctor-daemon-line.js +35 -0
- package/dist/cli/port-holder.d.ts +43 -0
- package/dist/cli/port-holder.js +99 -0
- package/dist/cli.d.ts +2 -2
- package/dist/cli.js +302 -100
- package/dist/cloud/cloud-config.d.ts +0 -1
- package/dist/cloud/cloud-config.js +9 -9
- package/dist/cloud/cloud-sync.d.ts +1 -7
- package/dist/cloud/cloud-sync.js +12 -12
- package/dist/crawl/crawl-empty.d.ts +16 -0
- package/dist/crawl/crawl-empty.js +24 -0
- package/dist/crawl/crawl-tools.js +4 -1
- package/dist/crawl/crawl.js +26 -7
- package/dist/daemon/daemon-resilience.d.ts +73 -0
- package/dist/daemon/daemon-resilience.js +279 -0
- package/dist/daemon/daemon-usefulness.d.ts +61 -0
- package/dist/daemon/daemon-usefulness.js +62 -0
- package/dist/{daemon.d.ts → daemon/daemon.d.ts} +33 -8
- package/dist/{daemon.js → daemon/daemon.js} +105 -48
- package/dist/daemon/heartbeat.d.ts +115 -0
- package/dist/daemon/heartbeat.js +141 -0
- package/dist/daemon/idle-grace.d.ts +32 -0
- package/dist/daemon/idle-grace.js +41 -0
- package/dist/{idle-shutdown.d.ts → daemon/idle-shutdown.d.ts} +23 -2
- package/dist/{idle-shutdown.js → daemon/idle-shutdown.js} +37 -6
- package/dist/daemon/port-presence.d.ts +76 -0
- package/dist/daemon/port-presence.js +93 -0
- package/dist/daemon/state-dir.d.ts +7 -0
- package/dist/daemon/state-dir.js +59 -0
- package/dist/daemon/unclean-predecessor.d.ts +23 -0
- package/dist/daemon/unclean-predecessor.js +27 -0
- package/dist/domain/domain-model.js +2 -2
- package/dist/events/body-failures.js +8 -8
- package/dist/events/contradictions.d.ts +1 -1
- package/dist/events/contradictions.js +60 -10
- package/dist/events/echo-mismatch.js +10 -10
- package/dist/events/event-filters.js +6 -6
- package/dist/events/json-salvage.d.ts +2 -1
- package/dist/events/json-salvage.js +6 -6
- package/dist/events/observed-in-window.d.ts +12 -0
- package/dist/events/observed-in-window.js +21 -0
- package/dist/events/predicate-eval.d.ts +53 -20
- package/dist/events/predicate-eval.js +179 -55
- package/dist/events/predicate-parse.d.ts +23 -0
- package/dist/events/predicate-parse.js +54 -0
- package/dist/events/predicate.d.ts +7 -2
- package/dist/events/predicate.js +126 -35
- package/dist/events/reconcile.js +5 -5
- package/dist/events/ring-buffer.js +2 -17
- package/dist/events/stale-response.js +6 -6
- package/dist/events/testid-near-miss.d.ts +22 -0
- package/dist/events/testid-near-miss.js +33 -0
- package/dist/events/unit-mismatch.js +8 -8
- package/dist/flows/affected-tools.js +19 -4
- package/dist/flows/affected.js +1 -1
- package/dist/flows/annotate-target.d.ts +1 -0
- package/dist/flows/annotate-target.js +20 -0
- package/dist/flows/annotate-tools.js +25 -3
- package/dist/flows/annotate.js +17 -4
- package/dist/flows/assertion-integrity.d.ts +2 -1
- package/dist/flows/assertion-tiers-store.d.ts +3 -66
- package/dist/flows/assertion-tiers-store.js +2 -2
- package/dist/flows/attributed-failure.d.ts +17 -0
- package/dist/flows/attributed-failure.js +20 -0
- package/dist/flows/buddy-status.d.ts +2 -1
- package/dist/flows/buddy-status.js +2 -2
- package/dist/flows/coverage.d.ts +3 -2
- package/dist/flows/coverage.js +2 -2
- package/dist/flows/decision.js +24 -9
- package/dist/flows/empty-flow.d.ts +6 -0
- package/dist/flows/empty-flow.js +24 -0
- package/dist/flows/flake-store.d.ts +0 -1
- package/dist/flows/flake-store.js +1 -1
- package/dist/flows/flake.d.ts +2 -1
- package/dist/flows/flake.js +2 -2
- package/dist/flows/flow-classify.js +12 -0
- package/dist/flows/flow-replay-run.js +4 -4
- package/dist/flows/flow-replay.d.ts +6 -8
- package/dist/flows/flow-replay.js +132 -48
- package/dist/flows/flow-report.js +1 -1
- package/dist/flows/flow-scope.d.ts +1 -6
- package/dist/flows/flow-step-runners.d.ts +26 -6
- package/dist/flows/flow-step-runners.js +156 -6
- package/dist/flows/flow-success.js +20 -15
- package/dist/flows/flow-tools.js +63 -21
- package/dist/flows/flows.js +15 -11
- package/dist/flows/gate.d.ts +4 -3
- package/dist/flows/gate.js +1 -1
- package/dist/flows/heal-run.d.ts +0 -8
- package/dist/flows/heal-run.js +5 -5
- package/dist/flows/heal.js +3 -3
- package/dist/flows/predicate-to-expect.d.ts +38 -0
- package/dist/flows/predicate-to-expect.js +95 -0
- package/dist/flows/replay.d.ts +16 -1
- package/dist/flows/replay.js +58 -22
- package/dist/flows/role-anchor-nearest.d.ts +6 -0
- package/dist/flows/role-anchor-nearest.js +45 -0
- package/dist/flows/role-drift-reason.d.ts +20 -0
- package/dist/flows/role-drift-reason.js +39 -0
- package/dist/flows/server-verify.d.ts +1 -1
- package/dist/flows/server-verify.js +5 -5
- package/dist/flows/suite-flakes.d.ts +33 -0
- package/dist/flows/suite-flakes.js +32 -0
- package/dist/flows/verify-change-tools.js +32 -10
- package/dist/flows/verify-change-verdict.d.ts +15 -0
- package/dist/flows/verify-change-verdict.js +25 -0
- package/dist/flows/watch-batcher.d.ts +3 -2
- package/dist/honesty/already-true.d.ts +2 -0
- package/dist/honesty/already-true.js +31 -0
- package/dist/honesty/blind-spots.d.ts +35 -3
- package/dist/honesty/blind-spots.js +57 -12
- package/dist/honesty/honesty.d.ts +3 -2
- package/dist/honesty/honesty.js +4 -4
- package/dist/honesty/uncaptured-bodies.js +1 -1
- package/dist/honesty/unread-outcome.js +2 -2
- package/dist/honesty/verified.d.ts +31 -3
- package/dist/honesty/verified.js +64 -8
- package/dist/http-server.js +23 -7
- package/dist/hunt/hunt-report.d.ts +2 -1
- package/dist/hunt/hunt-report.js +2 -2
- package/dist/index.d.ts +9 -6
- package/dist/index.js +41 -25
- package/dist/init/agent-rules.d.ts +20 -1
- package/dist/init/agent-rules.js +64 -8
- package/dist/init/app-choice.d.ts +20 -0
- package/dist/init/app-choice.js +25 -0
- package/dist/init/astro-host.d.ts +21 -0
- package/dist/init/astro-host.js +27 -0
- package/dist/init/astro-patch.d.ts +16 -0
- package/dist/init/astro-patch.js +240 -0
- package/dist/init/capabilities.js +4 -1
- package/dist/init/cra.d.ts +50 -0
- package/dist/init/cra.js +106 -0
- package/dist/init/cursor.js +39 -4
- package/dist/init/desktop-doctor.d.ts +3 -2
- package/dist/init/desktop-doctor.js +4 -4
- package/dist/init/detect.d.ts +9 -2
- package/dist/init/detect.js +24 -5
- package/dist/init/mcp-clients.d.ts +81 -0
- package/dist/init/mcp-clients.js +248 -0
- package/dist/init/mcp-registered.d.ts +12 -0
- package/dist/init/mcp-registered.js +14 -0
- package/dist/init/mcp.d.ts +27 -2
- package/dist/init/mcp.js +40 -4
- package/dist/init/next-patch.d.ts +2 -4
- package/dist/init/next-patch.js +109 -19
- package/dist/init/node-io.js +29 -6
- package/dist/init/plan-framework.d.ts +16 -1
- package/dist/init/plan-framework.js +166 -8
- package/dist/init/plan.d.ts +85 -6
- package/dist/init/plan.js +196 -33
- package/dist/init/project-id.d.ts +5 -9
- package/dist/init/project-id.js +10 -22
- package/dist/init/run.d.ts +23 -1
- package/dist/init/run.js +338 -47
- package/dist/init/slash-command.d.ts +9 -0
- package/dist/init/slash-command.js +10 -1
- package/dist/init/snippets.d.ts +7 -10
- package/dist/init/snippets.js +44 -15
- package/dist/init/vite-config.js +1 -1
- package/dist/init/windows-quote.d.ts +38 -0
- package/dist/init/windows-quote.js +70 -0
- package/dist/init/workspace-apps.d.ts +23 -0
- package/dist/init/workspace-apps.js +68 -0
- package/dist/input/driven-redaction.js +1 -1
- package/dist/input/network-detail.js +8 -8
- package/dist/input/network-mock-tools.js +2 -2
- package/dist/input/network-mock.js +4 -4
- package/dist/input/real-input.js +8 -3
- package/dist/input/scroll-find.js +2 -2
- package/dist/input/scroll-tools.d.ts +0 -5
- package/dist/input/scroll-tools.js +11 -4
- package/dist/input/viewport-tools.js +2 -2
- package/dist/journal/ambient-store.d.ts +0 -2
- package/dist/journal/ambient-store.js +1 -1
- package/dist/journal/ambient.js +1 -1
- package/dist/journal/attach-journal.d.ts +2 -1
- package/dist/journal/deviation-report.js +6 -6
- package/dist/journal/deviation-service.js +1 -1
- package/dist/journal/envelope-store.d.ts +0 -2
- package/dist/journal/envelope-store.js +1 -1
- package/dist/journal/envelope.d.ts +2 -3
- package/dist/journal/envelope.js +3 -3
- package/dist/journal/journal-query.js +1 -1
- package/dist/journal/journal-recorder.d.ts +2 -1
- package/dist/journal/journal-recorder.js +2 -2
- package/dist/journal/rollups.js +9 -9
- package/dist/journal/session-end.d.ts +2 -1
- package/dist/journal/session-journal.js +4 -4
- package/dist/license/license.d.ts +0 -14
- package/dist/license/license.js +8 -8
- package/dist/log.d.ts +10 -0
- package/dist/log.js +11 -1
- package/dist/mcp/mcp-is-error.d.ts +13 -0
- package/dist/mcp/mcp-is-error.js +18 -0
- package/dist/mcp/mcp-outage.d.ts +32 -0
- package/dist/mcp/mcp-outage.js +55 -0
- package/dist/mcp/mcp-proxy.d.ts +134 -0
- package/dist/mcp/mcp-proxy.js +720 -0
- package/dist/{mcp.d.ts → mcp/mcp.d.ts} +13 -10
- package/dist/{mcp.js → mcp/mcp.js} +139 -37
- package/dist/mcp/proxy-handshake.d.ts +3 -0
- package/dist/mcp/proxy-handshake.js +54 -0
- package/dist/mcp/proxy-lifecycle.d.ts +64 -0
- package/dist/mcp/proxy-lifecycle.js +70 -0
- package/dist/mcp/tool-catalog-cache.d.ts +29 -0
- package/dist/mcp/tool-catalog-cache.js +55 -0
- package/dist/oracles/propose-consequences.d.ts +2 -1
- package/dist/oracles/propose-consequences.js +16 -11
- package/dist/oracles/self-instrument.d.ts +2 -1
- package/dist/oracles/self-instrument.js +1 -1
- package/dist/phenomena/phenomena.js +3 -3
- package/dist/platform.d.ts +16 -0
- package/dist/platform.js +16 -0
- package/dist/pool/browser-pool.d.ts +9 -0
- package/dist/pool/browser-pool.js +38 -9
- package/dist/pool/playwright-launcher.d.ts +15 -2
- package/dist/pool/playwright-launcher.js +21 -2
- package/dist/project/fs-port.js +1 -1
- package/dist/project/project-tools.js +2 -2
- package/dist/runs/build-verification-run.js +1 -1
- package/dist/runs/repair-prompt.d.ts +0 -6
- package/dist/runs/repair-prompt.js +3 -3
- package/dist/runs/risk-classify.js +1 -1
- package/dist/runs/run-diff.d.ts +0 -2
- package/dist/runs/run-diff.js +3 -3
- package/dist/runs/run-tools.js +2 -2
- package/dist/runs/verification-sync.js +2 -2
- package/dist/runs/verify-http.js +1 -1
- package/dist/runs/verify-server.js +1 -1
- package/dist/session/attachment-history.d.ts +46 -0
- package/dist/session/attachment-history.js +65 -0
- package/dist/session/command-timeout.d.ts +11 -3
- package/dist/session/command-timeout.js +2 -2
- package/dist/session/control-envelope.d.ts +6 -0
- package/dist/session/control-envelope.js +7 -1
- package/dist/session/dev-server-probe.d.ts +13 -0
- package/dist/session/dev-server-probe.js +52 -0
- package/dist/session/last-act.d.ts +17 -3
- package/dist/session/last-act.js +16 -4
- package/dist/session/live-control-tools.js +7 -2
- package/dist/session/live-control.js +1 -1
- package/dist/session/looks-like-dev-server.d.ts +14 -0
- package/dist/session/looks-like-dev-server.js +25 -0
- package/dist/session/no-session-diagnosis.d.ts +45 -0
- package/dist/session/no-session-diagnosis.js +120 -0
- package/dist/session/no-session-watch.d.ts +21 -0
- package/dist/session/no-session-watch.js +69 -0
- package/dist/session/observed-state.d.ts +4 -0
- package/dist/session/observed-state.js +19 -1
- package/dist/session/output-budget.js +1 -1
- package/dist/session/pending-commands.js +1 -0
- package/dist/session/session-health.d.ts +8 -1
- package/dist/session/session-health.js +8 -7
- package/dist/session/session-info.d.ts +43 -8
- package/dist/session/session-info.js +46 -1
- package/dist/session/session-lease.d.ts +0 -11
- package/dist/session/session-lease.js +1 -1
- package/dist/session/session-manager.d.ts +6 -0
- package/dist/session/session-manager.js +124 -12
- package/dist/session/session-readiness.d.ts +1 -1
- package/dist/session/session-readiness.js +6 -1
- package/dist/session/session-reaper.js +1 -1
- package/dist/session/session-reconnect.d.ts +32 -0
- package/dist/session/session-reconnect.js +42 -0
- package/dist/session/session.d.ts +15 -2
- package/dist/session/session.js +77 -26
- package/dist/session/state-select.d.ts +1 -1
- package/dist/session/state-select.js +1 -1
- package/dist/status-payload.d.ts +19 -0
- package/dist/status-payload.js +11 -0
- package/dist/telemetry/argument-shape.js +10 -1
- package/dist/telemetry/browser-mode.d.ts +27 -0
- package/dist/telemetry/browser-mode.js +33 -0
- package/dist/telemetry/bug-found.d.ts +17 -21
- package/dist/telemetry/bug-found.js +120 -8
- package/dist/telemetry/cli-telemetry.d.ts +2 -12
- package/dist/telemetry/cli-telemetry.js +27 -4
- package/dist/telemetry/crash-cause.d.ts +47 -0
- package/dist/telemetry/crash-cause.js +104 -0
- package/dist/telemetry/daemon-telemetry.d.ts +38 -1
- package/dist/telemetry/daemon-telemetry.js +37 -12
- package/dist/telemetry/dev-repo.js +4 -4
- package/dist/telemetry/error-class.d.ts +26 -0
- package/dist/telemetry/error-class.js +52 -0
- package/dist/telemetry/error-fingerprint.js +21 -2
- package/dist/telemetry/feedback-cli.d.ts +8 -3
- package/dist/telemetry/feedback-cli.js +52 -8
- package/dist/telemetry/feedback-context.d.ts +23 -6
- package/dist/telemetry/feedback-context.js +105 -5
- package/dist/telemetry/feedback-delivery.d.ts +20 -0
- package/dist/telemetry/feedback-delivery.js +30 -0
- package/dist/telemetry/feedback-outbox.d.ts +22 -0
- package/dist/telemetry/feedback-outbox.js +87 -0
- package/dist/telemetry/feedback.d.ts +22 -1
- package/dist/telemetry/feedback.js +67 -8
- package/dist/telemetry/git-facts.d.ts +7 -0
- package/dist/telemetry/git-facts.js +5 -5
- package/dist/telemetry/project-profile.js +2 -1
- package/dist/telemetry/session-metrics.d.ts +47 -3
- package/dist/telemetry/session-metrics.js +315 -15
- package/dist/telemetry/telemetry.d.ts +45 -2
- package/dist/telemetry/telemetry.js +150 -37
- package/dist/telemetry/verification-of.d.ts +22 -0
- package/dist/telemetry/verification-of.js +84 -0
- package/dist/tools/act-capsule.js +2 -2
- package/dist/tools/act-danger.d.ts +12 -0
- package/dist/tools/act-danger.js +20 -3
- package/dist/tools/act-helpers.js +4 -4
- package/dist/tools/act-tools.d.ts +0 -8
- package/dist/tools/act-tools.js +114 -118
- package/dist/tools/act-view.js +7 -7
- package/dist/tools/alias-args.d.ts +17 -0
- package/dist/tools/alias-args.js +25 -0
- package/dist/tools/assert-grade.js +9 -9
- package/dist/tools/assert-verdict.d.ts +9 -1
- package/dist/tools/assert-verdict.js +21 -6
- package/dist/tools/browser-tools.js +36 -11
- package/dist/tools/contract-tools.d.ts +0 -5
- package/dist/tools/contract-tools.js +27 -1
- package/dist/tools/coverage-identity.d.ts +34 -0
- package/dist/tools/coverage-identity.js +46 -0
- package/dist/tools/coverage-tools.js +10 -14
- package/dist/tools/dynamic-tools.d.ts +2 -1
- package/dist/tools/dynamic-tools.js +96 -19
- package/dist/tools/error-recovery.d.ts +34 -2
- package/dist/tools/error-recovery.js +255 -4
- package/dist/tools/feedback-invite.d.ts +48 -0
- package/dist/tools/feedback-invite.js +71 -0
- package/dist/tools/feedback-tools.d.ts +12 -1
- package/dist/tools/feedback-tools.js +28 -5
- package/dist/tools/invoke-tool.d.ts +0 -12
- package/dist/tools/invoke-tool.js +126 -33
- package/dist/tools/lease-hint.d.ts +17 -0
- package/dist/tools/lease-hint.js +24 -0
- package/dist/tools/lease-tools.js +6 -5
- package/dist/tools/merge-tools.d.ts +10 -1
- package/dist/tools/merge-tools.js +9 -3
- package/dist/tools/merged-name-redirect.d.ts +13 -0
- package/dist/tools/merged-name-redirect.js +55 -0
- package/dist/tools/navigate-arrival.d.ts +26 -0
- package/dist/tools/navigate-arrival.js +60 -0
- package/dist/tools/navigate-result.d.ts +31 -0
- package/dist/tools/navigate-result.js +38 -0
- package/dist/tools/observe-tools.js +72 -20
- package/dist/tools/observed-nothing.d.ts +25 -0
- package/dist/tools/observed-nothing.js +33 -0
- package/dist/tools/query-paginate.js +2 -2
- package/dist/tools/query-shape.d.ts +18 -0
- package/dist/tools/query-shape.js +39 -0
- package/dist/tools/read-tools.js +81 -20
- package/dist/tools/real-input-attempt.d.ts +28 -0
- package/dist/tools/real-input-attempt.js +93 -0
- package/dist/tools/reconcile-tools.js +7 -7
- package/dist/tools/reload-result.d.ts +14 -0
- package/dist/tools/reload-result.js +29 -0
- package/dist/tools/snapshot-delta.d.ts +4 -5
- package/dist/tools/snapshot-delta.js +0 -0
- package/dist/tools/tool-kit.d.ts +16 -0
- package/dist/tools/tool-surface.d.ts +83 -0
- package/dist/tools/tool-surface.js +217 -0
- package/dist/tools/tools-helpers.js +6 -6
- package/dist/tools/tools.d.ts +15 -0
- package/dist/tools/tools.js +53 -17
- package/dist/tools/unadvertised-help.d.ts +5 -0
- package/dist/tools/unadvertised-help.js +39 -0
- package/dist/trace.d.ts +69 -0
- package/dist/trace.js +150 -0
- package/dist/update/nudge-credit.d.ts +4 -0
- package/dist/update/nudge-credit.js +53 -0
- package/dist/update/sdk-sync.d.ts +21 -0
- package/dist/update/sdk-sync.js +42 -0
- package/dist/update/update-checker.js +3 -3
- package/dist/update/update-nudge.d.ts +63 -6
- package/dist/update/update-nudge.js +128 -15
- package/dist/update/updater.d.ts +1 -9
- package/dist/update/updater.js +13 -8
- package/dist/version/peer-announce.d.ts +4 -0
- package/dist/version/peer-announce.js +30 -0
- package/dist/{server-version.js → version/server-version.js} +5 -1
- package/dist/version/version-nudge.d.ts +28 -0
- package/dist/version/version-nudge.js +40 -0
- package/dist/version/version-skew.d.ts +66 -0
- package/dist/version/version-skew.js +71 -0
- package/dist/visual/visual-diff.js +2 -2
- package/dist/visual/visual-tools.js +4 -4
- package/docs/README.md +55 -0
- package/docs/agent-cheatsheet.md +85 -0
- package/docs/architecture.md +113 -0
- package/docs/benchmarks.md +255 -0
- package/docs/debugging.md +102 -0
- package/docs/deploy-checks.md +69 -0
- package/docs/desktop-apps.md +256 -0
- package/docs/enterprise.md +62 -0
- package/docs/fixtures-dispatch-receiver.yml +76 -0
- package/docs/fixtures.md +45 -0
- package/docs/flows.md +166 -0
- package/docs/gate-plan.md +98 -0
- package/docs/gates.md +117 -0
- package/docs/getting-started.md +469 -0
- package/docs/human-control.md +63 -0
- package/docs/integration-patterns.md +171 -0
- package/docs/local-registry.md +126 -0
- package/docs/matrix/2.5.0/claude-code-machine.json +19 -0
- package/docs/matrix/2.5.0/codex-machine.json +20 -0
- package/docs/matrix/2.5.0/cursor-machine.json +22 -0
- package/docs/matrix/2.5.0/gemini-machine.json +22 -0
- package/docs/matrix/2.5.0/opencode-machine.json +22 -0
- package/docs/matrix/2.5.0/vscode-machine.json +22 -0
- package/docs/matrix/2.5.0/windsurf-machine.json +22 -0
- package/docs/matrix/MATRIX.md +26 -0
- package/docs/matrix/README.md +59 -0
- package/docs/mint.json +61 -0
- package/docs/multi-agent-testing.md +55 -0
- package/docs/platform-integration.md +122 -0
- package/docs/system-map.md +152 -0
- package/docs/telemetry-contract.md +214 -0
- package/docs/telemetry.md +191 -0
- package/docs/testing.md +82 -0
- package/docs/token-efficiency.md +80 -0
- package/docs/usage.md +1046 -0
- package/package.json +8 -5
- package/dist/cli-doctor.d.ts +0 -6
- package/dist/cli-doctor.js +0 -60
- package/dist/cli-launch.js +0 -112
- package/dist/daemon-resilience.d.ts +0 -22
- package/dist/daemon-resilience.js +0 -85
- package/dist/mcp-proxy.d.ts +0 -67
- package/dist/mcp-proxy.js +0 -337
- package/dist/tools/profiles.d.ts +0 -37
- package/dist/tools/profiles.js +0 -139
- /package/dist/{pairing-token.d.ts → bridge/pairing-token.d.ts} +0 -0
- /package/dist/{token-auth.d.ts → bridge/token-auth.d.ts} +0 -0
- /package/dist/{token-auth.js → bridge/token-auth.js} +0 -0
- /package/dist/{cli-verify.d.ts → cli/cli-verify.d.ts} +0 -0
- /package/dist/{cloud-cli.d.ts → cli/cloud-cli.d.ts} +0 -0
- /package/dist/{server-version.d.ts → version/server-version.d.ts} +0 -0
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { isOpaqueOrigin } from '@reticlehq/core';
|
|
1
|
+
import { AppRuntime, isOpaqueOrigin } from '@reticlehq/core';
|
|
2
2
|
/**
|
|
3
3
|
* Inbound silence beyond this means the page may genuinely be wedged; inside it, the SDK has spoken
|
|
4
4
|
* recently enough that "the page is hung" is provably the wrong story. A page that is streaming
|
|
@@ -18,7 +18,7 @@ const HIDDEN_ADVICE = 'The page last reported itself hidden, and this is a WKWeb
|
|
|
18
18
|
'loaded. If neither fits, the page is reachable but not executing, which is worth reporting.';
|
|
19
19
|
/** True when this session is a WebKit desktop shell — the only runtime that suffers this. */
|
|
20
20
|
function isWebKitDesktop(context) {
|
|
21
|
-
if (
|
|
21
|
+
if (AppRuntime.TAURI === context.runtime)
|
|
22
22
|
return true;
|
|
23
23
|
// A `tauri://` origin is unambiguous even before the first health report lands.
|
|
24
24
|
if (context.runtime !== undefined)
|
|
@@ -12,6 +12,12 @@ interface ControlEnvelope {
|
|
|
12
12
|
* recovery path (address the guidance, then reticle_session{action:"resume"}) lives here in exactly one place.
|
|
13
13
|
*/
|
|
14
14
|
export declare const PAUSE_HINT = "Paused by the human. Address the guidance, then call reticle_session{action:\"resume\"} (or wait for the human to resume).";
|
|
15
|
+
/**
|
|
16
|
+
* `because` for the verdict a paused verification returns. Nothing was driven and nothing was
|
|
17
|
+
* asserted, which is the definition of unknown — never `no`, which would report the human's own
|
|
18
|
+
* pause as the app failing.
|
|
19
|
+
*/
|
|
20
|
+
export declare const PAUSED_NO_VERDICT = "the human paused this session, so nothing was driven and nothing was asserted";
|
|
15
21
|
/** Shape returned by the short-circuit when an action tool refuses while paused. */
|
|
16
22
|
interface PausedResult {
|
|
17
23
|
paused: true;
|
|
@@ -5,6 +5,12 @@ import { z } from 'zod';
|
|
|
5
5
|
* recovery path (address the guidance, then reticle_session{action:"resume"}) lives here in exactly one place.
|
|
6
6
|
*/
|
|
7
7
|
export const PAUSE_HINT = 'Paused by the human. Address the guidance, then call reticle_session{action:"resume"} (or wait for the human to resume).';
|
|
8
|
+
/**
|
|
9
|
+
* `because` for the verdict a paused verification returns. Nothing was driven and nothing was
|
|
10
|
+
* asserted, which is the definition of unknown — never `no`, which would report the human's own
|
|
11
|
+
* pause as the app failing.
|
|
12
|
+
*/
|
|
13
|
+
export const PAUSED_NO_VERDICT = 'the human paused this session, so nothing was driven and nothing was asserted';
|
|
8
14
|
/**
|
|
9
15
|
* outputSchema fields every action tool that can short-circuit on pause MUST declare, or a validating
|
|
10
16
|
* profile strips the WHOLE pause payload — and because `guidance` is drained-once, that loses the
|
|
@@ -28,7 +34,7 @@ export const pausedOutputShape = {
|
|
|
28
34
|
export function buildControlEnvelope(session) {
|
|
29
35
|
const state = session.getState();
|
|
30
36
|
const guidance = session.drainInbox().map((m) => m.text);
|
|
31
|
-
if (state === SessionState.ACTIVE && guidance.length
|
|
37
|
+
if (state === SessionState.ACTIVE && 0 === guidance.length)
|
|
32
38
|
return undefined;
|
|
33
39
|
return { state, guidance };
|
|
34
40
|
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which of the usual dev-server ports have something listening on them.
|
|
3
|
+
*
|
|
4
|
+
* The one fact that separates "the app is not running" from "the app is running and never dialled
|
|
5
|
+
* us" — the difference between two completely different next actions for the agent, which the
|
|
6
|
+
* no-session message could not tell apart before. See no-session-diagnosis.ts.
|
|
7
|
+
*
|
|
8
|
+
* Kept out of the hot path: `SessionManager.resolve` is synchronous and runs on every tool call, so
|
|
9
|
+
* the probe refreshes in the background and the resolve path reads a cached answer. A stale answer
|
|
10
|
+
* is fine here — it is a hint in an error message, not a decision.
|
|
11
|
+
*/
|
|
12
|
+
/** Every candidate port serving a document, in ascending order. Never rejects. */
|
|
13
|
+
export declare function probeDevServers(ports?: readonly number[], probe?: (port: number) => Promise<boolean>): Promise<number[]>;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which of the usual dev-server ports have something listening on them.
|
|
3
|
+
*
|
|
4
|
+
* The one fact that separates "the app is not running" from "the app is running and never dialled
|
|
5
|
+
* us" — the difference between two completely different next actions for the agent, which the
|
|
6
|
+
* no-session message could not tell apart before. See no-session-diagnosis.ts.
|
|
7
|
+
*
|
|
8
|
+
* Kept out of the hot path: `SessionManager.resolve` is synchronous and runs on every tool call, so
|
|
9
|
+
* the probe refreshes in the background and the resolve path reads a cached answer. A stale answer
|
|
10
|
+
* is fine here — it is a hint in an error message, not a decision.
|
|
11
|
+
*/
|
|
12
|
+
import { request } from 'node:http';
|
|
13
|
+
import { DEV_SERVER_PORTS } from '../cli/cli-port.js';
|
|
14
|
+
import { looksLikeDevServer } from './looks-like-dev-server.js';
|
|
15
|
+
/** Give up fast: an HTTP answer from localhost either lands quickly or nothing is there. */
|
|
16
|
+
const HTTP_PROBE_TIMEOUT_MS = 400;
|
|
17
|
+
/**
|
|
18
|
+
* `localhost`, not `127.0.0.1` — so BOTH address families are tried.
|
|
19
|
+
*
|
|
20
|
+
* Measured: a plain `vite --port 4311` on this machine listens on `[::1]` only, and a probe pinned to
|
|
21
|
+
* the IPv4 loopback cannot see it. The old TCP probe had the same blind spot, which meant the
|
|
22
|
+
* diagnostic could miss the very dev server it exists to find while still reporting an unrelated
|
|
23
|
+
* service that happens to bind both stacks (macOS AirPlay does).
|
|
24
|
+
*/
|
|
25
|
+
const PROBE_HOST = 'localhost';
|
|
26
|
+
/**
|
|
27
|
+
* Does this port serve a DOCUMENT — i.e. is it a dev server rather than merely something listening?
|
|
28
|
+
*
|
|
29
|
+
* A bare TCP connect reported macOS ControlCenter (AirPlay Receiver, on by default on every Mac,
|
|
30
|
+
* port 5000) as the user's dev server. See looks-like-dev-server for what it actually answers and
|
|
31
|
+
* why "a socket accepted" is the wrong question.
|
|
32
|
+
*/
|
|
33
|
+
function servesDocument(port, host = PROBE_HOST) {
|
|
34
|
+
return new Promise((resolve) => {
|
|
35
|
+
const req = request({ host, port, path: '/', method: 'GET', timeout: HTTP_PROBE_TIMEOUT_MS }, (res) => {
|
|
36
|
+
const answer = looksLikeDevServer(res.statusCode ?? 0, res.headers['content-type']);
|
|
37
|
+
res.resume(); // drain, so the socket closes rather than lingering
|
|
38
|
+
resolve(answer);
|
|
39
|
+
});
|
|
40
|
+
req.once('timeout', () => {
|
|
41
|
+
req.destroy();
|
|
42
|
+
resolve(false);
|
|
43
|
+
});
|
|
44
|
+
req.once('error', () => resolve(false));
|
|
45
|
+
req.end();
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
/** Every candidate port serving a document, in ascending order. Never rejects. */
|
|
49
|
+
export async function probeDevServers(ports = [...DEV_SERVER_PORTS], probe = (p) => servesDocument(p)) {
|
|
50
|
+
const results = await Promise.all(ports.map((p) => probe(p).then((up) => (up ? p : null))));
|
|
51
|
+
return results.filter((p) => p !== null).sort((a, b) => a - b);
|
|
52
|
+
}
|
|
@@ -14,18 +14,32 @@
|
|
|
14
14
|
* the "this click did nothing" check is unreachable on the ordinary act-then-observe flow: an
|
|
15
15
|
* empty-window test is a statement about the PAGE, and no real app has a quiet page.
|
|
16
16
|
*/
|
|
17
|
-
|
|
17
|
+
interface ActEffect {
|
|
18
18
|
action?: string | undefined;
|
|
19
19
|
/** DOM mutations inside the acted element's own subtree. Undefined means nobody measured. */
|
|
20
20
|
mutatedWithin?: number | undefined;
|
|
21
21
|
}
|
|
22
22
|
export declare class LastAct {
|
|
23
23
|
#private;
|
|
24
|
-
|
|
24
|
+
/**
|
|
25
|
+
* Record an action that ACTUALLY DISPATCHED — cursor and effect together, never one without the
|
|
26
|
+
* other.
|
|
27
|
+
*
|
|
28
|
+
* It used to be two setters called BEFORE the ref was resolved, so a refused act (stale ref, a
|
|
29
|
+
* disabled control, a paused page) left a cursor and an empty effect standing. The next observe
|
|
30
|
+
* saw a cursor inside its window, asked for the effect, found `mutatedWithin: undefined` over a
|
|
31
|
+
* quiet window and reported "the click was dispatched and the page settled … the target does not
|
|
32
|
+
* react" — about a click nobody dispatched, blaming a control that was fine, on exactly the
|
|
33
|
+
* re-query-and-retry path the stale-ref message sends the agent down.
|
|
34
|
+
*
|
|
35
|
+
* Marking only on the success path is what makes that unrepeatable: a failure mode added later
|
|
36
|
+
* cannot forget to clean up state that was never written.
|
|
37
|
+
*/
|
|
38
|
+
markActed(cursor: number, action: string | undefined, mutatedWithin: number | undefined): void;
|
|
25
39
|
cursor(): number | undefined;
|
|
26
40
|
markSource(source: string | undefined): void;
|
|
27
41
|
source(): string | undefined;
|
|
28
|
-
markEffect(action: string | undefined, mutatedWithin: number | undefined): void;
|
|
29
42
|
/** Empty when nothing has acted — callers then fall back to checks that need no action context. */
|
|
30
43
|
effect(): ActEffect;
|
|
31
44
|
}
|
|
45
|
+
export {};
|
package/dist/session/last-act.js
CHANGED
|
@@ -2,8 +2,23 @@ export class LastAct {
|
|
|
2
2
|
#cursor;
|
|
3
3
|
#source;
|
|
4
4
|
#effect;
|
|
5
|
-
|
|
5
|
+
/**
|
|
6
|
+
* Record an action that ACTUALLY DISPATCHED — cursor and effect together, never one without the
|
|
7
|
+
* other.
|
|
8
|
+
*
|
|
9
|
+
* It used to be two setters called BEFORE the ref was resolved, so a refused act (stale ref, a
|
|
10
|
+
* disabled control, a paused page) left a cursor and an empty effect standing. The next observe
|
|
11
|
+
* saw a cursor inside its window, asked for the effect, found `mutatedWithin: undefined` over a
|
|
12
|
+
* quiet window and reported "the click was dispatched and the page settled … the target does not
|
|
13
|
+
* react" — about a click nobody dispatched, blaming a control that was fine, on exactly the
|
|
14
|
+
* re-query-and-retry path the stale-ref message sends the agent down.
|
|
15
|
+
*
|
|
16
|
+
* Marking only on the success path is what makes that unrepeatable: a failure mode added later
|
|
17
|
+
* cannot forget to clean up state that was never written.
|
|
18
|
+
*/
|
|
19
|
+
markActed(cursor, action, mutatedWithin) {
|
|
6
20
|
this.#cursor = cursor;
|
|
21
|
+
this.#effect = { action, mutatedWithin };
|
|
7
22
|
}
|
|
8
23
|
cursor() {
|
|
9
24
|
return this.#cursor;
|
|
@@ -14,9 +29,6 @@ export class LastAct {
|
|
|
14
29
|
source() {
|
|
15
30
|
return this.#source;
|
|
16
31
|
}
|
|
17
|
-
markEffect(action, mutatedWithin) {
|
|
18
|
-
this.#effect = { action, mutatedWithin };
|
|
19
|
-
}
|
|
20
32
|
/** Empty when nothing has acted — callers then fall back to checks that need no action context. */
|
|
21
33
|
effect() {
|
|
22
34
|
return this.#effect ?? {};
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
+
import { noteEmptyRead } from '../tools/observed-nothing.js';
|
|
2
3
|
import { AGENT_ASK_NOTICE, AGENT_WAITING_NOTICE, PresenterTone, SessionState, } from '@reticlehq/core';
|
|
3
4
|
import { ReticleTool } from '../tools/tool-names.js';
|
|
4
5
|
import { sessionIdShape } from '../tools/tool-kit.js';
|
|
@@ -95,7 +96,11 @@ export const LIVE_CONTROL_TOOLS = [
|
|
|
95
96
|
outputSchema: { messages: z.array(z.unknown()) },
|
|
96
97
|
handler: (deps, args) => {
|
|
97
98
|
const session = deps.sessions.resolve(asString(args['sessionId']));
|
|
98
|
-
return
|
|
99
|
+
// An empty inbox and a panel that is not wired both return `[]`. The first means the human has
|
|
100
|
+
// said nothing; the second means an agent is waiting on a channel that does not exist.
|
|
101
|
+
return Promise.resolve(noteEmptyRead({ messages: session.drainInbox() }, 'messages', {
|
|
102
|
+
noun: 'messages from the human since the last poll',
|
|
103
|
+
}));
|
|
99
104
|
},
|
|
100
105
|
},
|
|
101
106
|
{
|
|
@@ -135,7 +140,7 @@ export const LIVE_CONTROL_TOOLS = [
|
|
|
135
140
|
session.pushNarration(`✓ fixed: ${mark.note}`);
|
|
136
141
|
}
|
|
137
142
|
}
|
|
138
|
-
const source = args['all']
|
|
143
|
+
const source = true === args['all'] ? session.allMarks() : session.pendingMarks();
|
|
139
144
|
const marks = source.map((m) => ({ ...m, fix: buildFixHint(m) }));
|
|
140
145
|
const out = {
|
|
141
146
|
marks,
|
|
@@ -27,7 +27,7 @@ export class LiveControl {
|
|
|
27
27
|
/** Queue a human note. Empty/whitespace-only text is ignored so a stray enter cannot spam the agent. */
|
|
28
28
|
push(text, t) {
|
|
29
29
|
const trimmed = text.trim();
|
|
30
|
-
if (trimmed.length
|
|
30
|
+
if (0 === trimmed.length)
|
|
31
31
|
return;
|
|
32
32
|
this.#inbox.push({ text: trimmed, t });
|
|
33
33
|
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Is the thing on this port a DEV SERVER serving the app, or just something on a port?
|
|
3
|
+
*
|
|
4
|
+
* The probe used to answer "something accepted a TCP connection", which on macOS means every Mac
|
|
5
|
+
* reports Apple's AirPlay Receiver on port 5000 as the user's dev server. Reproduced here: it
|
|
6
|
+
* answers `HTTP/1.1 403 Forbidden`, `Server: AirTunes/950.7.1`, no body. The agent was then told
|
|
7
|
+
* "something IS listening on port 5000 and this project is wired for Reticle" while the app was on
|
|
8
|
+
* 3100 — a diagnostic confidently naming a service with nothing to do with the project.
|
|
9
|
+
*
|
|
10
|
+
* A dev server serving an app answers `GET /` with a document. That is the whole test. A false
|
|
11
|
+
* negative costs a less specific message; a false positive costs trust in every diagnostic Reticle
|
|
12
|
+
* prints, which is far more expensive.
|
|
13
|
+
*/
|
|
14
|
+
export declare function looksLikeDevServer(status: number, contentType: string | undefined): boolean;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Is the thing on this port a DEV SERVER serving the app, or just something on a port?
|
|
3
|
+
*
|
|
4
|
+
* The probe used to answer "something accepted a TCP connection", which on macOS means every Mac
|
|
5
|
+
* reports Apple's AirPlay Receiver on port 5000 as the user's dev server. Reproduced here: it
|
|
6
|
+
* answers `HTTP/1.1 403 Forbidden`, `Server: AirTunes/950.7.1`, no body. The agent was then told
|
|
7
|
+
* "something IS listening on port 5000 and this project is wired for Reticle" while the app was on
|
|
8
|
+
* 3100 — a diagnostic confidently naming a service with nothing to do with the project.
|
|
9
|
+
*
|
|
10
|
+
* A dev server serving an app answers `GET /` with a document. That is the whole test. A false
|
|
11
|
+
* negative costs a less specific message; a false positive costs trust in every diagnostic Reticle
|
|
12
|
+
* prints, which is far more expensive.
|
|
13
|
+
*/
|
|
14
|
+
/** Statuses that mean "I will not serve you", which no local dev server does for its own app root. */
|
|
15
|
+
const REFUSES = new Set([401, 403, 407]);
|
|
16
|
+
export function looksLikeDevServer(status, contentType) {
|
|
17
|
+
if (REFUSES.has(status))
|
|
18
|
+
return false;
|
|
19
|
+
// A redirect IS a server routing: a dev server on a base path answers `/` with one.
|
|
20
|
+
if (status >= 300 && status < 400)
|
|
21
|
+
return true;
|
|
22
|
+
// Otherwise it has to be serving a document. A 404 that renders HTML is still a dev server — the
|
|
23
|
+
// server is there, only the route is missing.
|
|
24
|
+
return contentType !== undefined && contentType.toLowerCase().includes('text/html');
|
|
25
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Turning "no browser session connected" from a dead end into a next action.
|
|
3
|
+
*
|
|
4
|
+
* This is the most consequential sentence in the product. Measured over a day of telemetry: 74% of
|
|
5
|
+
* sessions never call a Reticle tool, and of the ones that do, half make exactly ONE call — usually
|
|
6
|
+
* `reticle_sessions` — and stop. Ten of those thirteen never touched a browser, and every recorded
|
|
7
|
+
* session error is this one. The agent asks whether anything is connected, is told no, and leaves.
|
|
8
|
+
*
|
|
9
|
+
* The old message asked the agent to check two things it cannot see from where it stands ("is your
|
|
10
|
+
* app running with the SDK enabled?", "does it point at this port?"). The daemon can actually tell
|
|
11
|
+
* the three cases apart, and each has a different, concrete fix:
|
|
12
|
+
*
|
|
13
|
+
* - a session was here and went away -> the tab closed or reloaded; reopen it
|
|
14
|
+
* - something is listening, never dialled, project not wired -> run `reticle init` in THAT app
|
|
15
|
+
* - something is listening, project wired -> port mismatch or a stale build; restart the dev server
|
|
16
|
+
* - nothing is listening anywhere -> there is no app running; start it
|
|
17
|
+
*
|
|
18
|
+
* Pure: everything it needs is passed in, so the probe that discovers listening ports stays out of
|
|
19
|
+
* the hot resolve() path and this stays unit-testable.
|
|
20
|
+
*/
|
|
21
|
+
interface NoSessionFacts {
|
|
22
|
+
/** Whether ANY session has connected to this daemon since it booted. */
|
|
23
|
+
everConnected: boolean;
|
|
24
|
+
/** Whether this project has been through `reticle init` (a .reticle.json / projectId is present). */
|
|
25
|
+
initialized: boolean;
|
|
26
|
+
/** Localhost ports with something listening that looks like a dev server. */
|
|
27
|
+
listening: readonly number[];
|
|
28
|
+
/** The port this daemon is on — half of the mismatch the old message asked about. */
|
|
29
|
+
port: number;
|
|
30
|
+
/**
|
|
31
|
+
* This daemon has reaped at least one EXPIRED pooled lease.
|
|
32
|
+
*
|
|
33
|
+
* Deliberately not "the session that just vanished was that lease" — nothing knows that. It is
|
|
34
|
+
* evidence, not proof, and the message below is worded accordingly.
|
|
35
|
+
*/
|
|
36
|
+
leaseExpired?: boolean;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Every branch ends with this, and `recoveryFor` keys on it to suppress the generic no-session
|
|
40
|
+
* recovery: a message that already carries its own next action must not be handed a second, more
|
|
41
|
+
* generic one that contradicts it.
|
|
42
|
+
*/
|
|
43
|
+
export declare const SELF_RECOVERING_MARKER = "Then call reticle_sessions again \u2014 it will appear within a second of the page loading.";
|
|
44
|
+
export declare function diagnoseNoSession(facts: NoSessionFacts): string;
|
|
45
|
+
export {};
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Turning "no browser session connected" from a dead end into a next action.
|
|
3
|
+
*
|
|
4
|
+
* This is the most consequential sentence in the product. Measured over a day of telemetry: 74% of
|
|
5
|
+
* sessions never call a Reticle tool, and of the ones that do, half make exactly ONE call — usually
|
|
6
|
+
* `reticle_sessions` — and stop. Ten of those thirteen never touched a browser, and every recorded
|
|
7
|
+
* session error is this one. The agent asks whether anything is connected, is told no, and leaves.
|
|
8
|
+
*
|
|
9
|
+
* The old message asked the agent to check two things it cannot see from where it stands ("is your
|
|
10
|
+
* app running with the SDK enabled?", "does it point at this port?"). The daemon can actually tell
|
|
11
|
+
* the three cases apart, and each has a different, concrete fix:
|
|
12
|
+
*
|
|
13
|
+
* - a session was here and went away -> the tab closed or reloaded; reopen it
|
|
14
|
+
* - something is listening, never dialled, project not wired -> run `reticle init` in THAT app
|
|
15
|
+
* - something is listening, project wired -> port mismatch or a stale build; restart the dev server
|
|
16
|
+
* - nothing is listening anywhere -> there is no app running; start it
|
|
17
|
+
*
|
|
18
|
+
* Pure: everything it needs is passed in, so the probe that discovers listening ports stays out of
|
|
19
|
+
* the hot resolve() path and this stays unit-testable.
|
|
20
|
+
*/
|
|
21
|
+
import { DEV_SERVER_PORTS } from '../cli/cli-port.js';
|
|
22
|
+
/**
|
|
23
|
+
* Every branch ends with this, and `recoveryFor` keys on it to suppress the generic no-session
|
|
24
|
+
* recovery: a message that already carries its own next action must not be handed a second, more
|
|
25
|
+
* generic one that contradicts it.
|
|
26
|
+
*/
|
|
27
|
+
export const SELF_RECOVERING_MARKER = 'Then call reticle_sessions again — it will appear within a second of the page loading.';
|
|
28
|
+
const RETRY = SELF_RECOVERING_MARKER;
|
|
29
|
+
/**
|
|
30
|
+
* The way out that needs no human at all.
|
|
31
|
+
*
|
|
32
|
+
* `reticle_lease` opens a browser Reticle drives itself, instead of waiting for somebody's tab to
|
|
33
|
+
* dial in. Measured over a day of telemetry it is the single strongest predictor of a session that
|
|
34
|
+
* works: the 5 sessions that used it had a MEDIAN of 30 tool calls and produced 46% of every bug
|
|
35
|
+
* found, against a median of 1 call for the 20 active sessions that did not — and not one
|
|
36
|
+
* single-call bounce used one. It is also advertised on no profile except `full`, so an agent only
|
|
37
|
+
* ever finds it if it already knew it existed. Naming it HERE puts it in front of the agent at the
|
|
38
|
+
* one moment it is the answer, and costs nothing on the turns when it is not.
|
|
39
|
+
*
|
|
40
|
+
* Only offered when the app is known to carry the SDK: leasing an uninstrumented app just burns a
|
|
41
|
+
* browser and comes back `ready:false`.
|
|
42
|
+
*/
|
|
43
|
+
const SELF_SERVE = 'You do not have to wait for the human: reticle_lease {action:"acquire", url} opens a browser ' +
|
|
44
|
+
'Reticle drives itself, and returns a sessionId you can use immediately (reach it with ' +
|
|
45
|
+
'reticle_run {tool:"reticle_lease"} if it is not advertised directly; release it when you finish).';
|
|
46
|
+
/**
|
|
47
|
+
* The ports the scan actually covers, rendered for the message.
|
|
48
|
+
*
|
|
49
|
+
* Derived from DEV_SERVER_PORTS rather than re-typed: a message that lists ports the scan does not
|
|
50
|
+
* check, or omits ones it does, is a new version of the same defect — a confident claim about
|
|
51
|
+
* evidence that was never gathered.
|
|
52
|
+
*/
|
|
53
|
+
/**
|
|
54
|
+
* The second half of the answer for a project that never went through `init`.
|
|
55
|
+
*
|
|
56
|
+
* Held separately because it must appear ONLY when the project is uninstrumented — telling a wired
|
|
57
|
+
* project to re-run `init` sends the reader back to a step that already succeeded, which is its own
|
|
58
|
+
* kind of wrong answer.
|
|
59
|
+
*/
|
|
60
|
+
const UNINSTRUMENTED = 'Separately: this project has not been through `reticle init`, which is what installs the SDK ' +
|
|
61
|
+
'and wires it into the build — so even once the dev server is up, the app will carry no SDK and ' +
|
|
62
|
+
"no session will appear. Run `reticle init` in the app's directory too.";
|
|
63
|
+
const SCANNED_PORTS = [...DEV_SERVER_PORTS].join(', ');
|
|
64
|
+
export function diagnoseNoSession(facts) {
|
|
65
|
+
const { everConnected, initialized, listening, port } = facts;
|
|
66
|
+
const ports = listening.join(', ');
|
|
67
|
+
if (everConnected) {
|
|
68
|
+
// A reaped lease first, because it is the one cause we have POSITIVE evidence for. Reported
|
|
69
|
+
// from the field (#157): an aged-out lease produced "the tab was closed … ask the human to
|
|
70
|
+
// reopen the app", which is wrong on every clause — there is no human tab, and the recovery it
|
|
71
|
+
// names is unavailable to the caller while the one that works goes unmentioned. The reporter
|
|
72
|
+
// went looking for a port mismatch. Hedged rather than swapped: a human tab may ALSO have
|
|
73
|
+
// closed, and this does not know which session went.
|
|
74
|
+
if (true === facts.leaseExpired) {
|
|
75
|
+
return ('no browser session connected — but one WAS connected to this daemon earlier, so the wiring ' +
|
|
76
|
+
'is correct. This daemon has expired at least one pooled lease, so the likeliest cause is ' +
|
|
77
|
+
'that a lease you were using aged out; a lease is a headless context, not a human tab, and ' +
|
|
78
|
+
'it takes its cookies with it (so an authenticated app needs signing in again). Re-acquire ' +
|
|
79
|
+
'with reticle_lease {action:"acquire", url} and carry on. If you were driving a human tab ' +
|
|
80
|
+
`instead, it went away — reopen it or run \`reticle open\`. ${RETRY}`);
|
|
81
|
+
}
|
|
82
|
+
return ('no browser session connected — but one WAS connected to this daemon earlier, so the wiring ' +
|
|
83
|
+
'is correct. The tab was closed, navigated away, or hard-reloaded. Ask the human to reopen ' +
|
|
84
|
+
`the app (or run \`reticle open\`), or reload the tab. ${SELF_SERVE} ${RETRY}`);
|
|
85
|
+
}
|
|
86
|
+
if (0 === listening.length) {
|
|
87
|
+
return ('no browser session connected, and nothing is listening on the ports Reticle scans ' +
|
|
88
|
+
`(${SCANNED_PORTS}). The likeliest cause by far is that the dev server is not running: ask ` +
|
|
89
|
+
'the human to start it (`npm run dev`), then open the app in a browser. ' +
|
|
90
|
+
// The caveat is here rather than omitted because the scan is NARROW, and the old sentence
|
|
91
|
+
// spent its confidence as though an empty result from eleven ports were proof of absence.
|
|
92
|
+
// Reported from a scripted drive of 2.5.0: this branch asserted the app was not running while
|
|
93
|
+
// it served 200 on :7699. A dev server on any other port — 4173 from `vite preview`, 1420
|
|
94
|
+
// from Tauri, 5175 from a second Vite, anything passed to --port — is invisible to it.
|
|
95
|
+
'That scan is narrow, so it is not proof: a server on any other port is invisible to it. If ' +
|
|
96
|
+
// Deliberately NOT offering reticle_lease here, and a test pins that: a lease opens a URL, and
|
|
97
|
+
// if nothing is listening there is nothing at any URL to open. Asking for the real one is the
|
|
98
|
+
// only move that can recover the :7699 case.
|
|
99
|
+
'the app IS running, ask the human for its URL rather than assuming it is down. ' +
|
|
100
|
+
// BOTH facts at once when the project was never wired. This branch fires before the
|
|
101
|
+
// `!initialized` one, so an uninstrumented project used to be told only "start your dev
|
|
102
|
+
// server" — the reader starts it, calls again, and is told ONLY THEN that no app carries the
|
|
103
|
+
// SDK. Two round trips to learn two things the daemon knew on the first call, and this is the
|
|
104
|
+
// largest cohort in the funnel (#171): 77 users attached an agent and never instrumented an
|
|
105
|
+
// app. They have a daemon (they registered the MCP server) and no SDK anywhere.
|
|
106
|
+
`${initialized ? '' : UNINSTRUMENTED} ${RETRY}`);
|
|
107
|
+
}
|
|
108
|
+
if (!initialized) {
|
|
109
|
+
return (`no browser session connected, but something IS listening on port ${ports} — so a server is ` +
|
|
110
|
+
'up and has never dialled this daemon. This project has not been through `reticle init`, ' +
|
|
111
|
+
'which is what installs the SDK and wires it into the build, so the likeliest cause is that ' +
|
|
112
|
+
"the app carries no Reticle SDK. Ask the human to run `reticle init` in the app's directory " +
|
|
113
|
+
`and restart the dev server. ${RETRY}`);
|
|
114
|
+
}
|
|
115
|
+
return (`no browser session connected, but something IS listening on port ${ports} and this project is ` +
|
|
116
|
+
`wired for Reticle — so the app is either serving a build made before the wiring landed, or ` +
|
|
117
|
+
`dialling a different daemon than this one (this daemon is on ${String(port)}). Ask the human ` +
|
|
118
|
+
'to restart the dev server and hard-reload the page; if it still does not appear, check that ' +
|
|
119
|
+
`the app's reticle port matches ${String(port)}. ${SELF_SERVE} ${RETRY}`);
|
|
120
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Keeps the "why is nothing connected" diagnosis fresh, without putting a probe on the hot path.
|
|
3
|
+
*
|
|
4
|
+
* `SessionManager.resolve` is synchronous and runs on every tool call, so it cannot await a port
|
|
5
|
+
* scan. This refreshes the answer in the background and hands the manager a closure that reads the
|
|
6
|
+
* cached result — a hint in an error message is exactly the sort of thing that may be a few seconds
|
|
7
|
+
* stale.
|
|
8
|
+
*
|
|
9
|
+
* It only probes while NOTHING is connected. Once a session is live the question is moot, and a
|
|
10
|
+
* daemon that outlives the agent by hours has no business scanning ports it does not need.
|
|
11
|
+
*/
|
|
12
|
+
import type { SessionManager } from './session-manager.js';
|
|
13
|
+
/**
|
|
14
|
+
* The daemon's whole session-scoping decision in one call: scope auto-selection to the active
|
|
15
|
+
* project, and keep the no-session diagnosis fresh. Both derive from the same one fact — whether
|
|
16
|
+
* this directory has been through `reticle init` — so they belong together rather than as two
|
|
17
|
+
* adjacent blocks in the bootstrap.
|
|
18
|
+
*/
|
|
19
|
+
export declare function wireSessionScope(sessions: SessionManager, activeProjectId: string | undefined, port: number,
|
|
20
|
+
/** Reader for the pool's aged-out-lease count; omitted when this daemon runs no pool. */
|
|
21
|
+
reapedLeases?: () => number): () => void;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Keeps the "why is nothing connected" diagnosis fresh, without putting a probe on the hot path.
|
|
3
|
+
*
|
|
4
|
+
* `SessionManager.resolve` is synchronous and runs on every tool call, so it cannot await a port
|
|
5
|
+
* scan. This refreshes the answer in the background and hands the manager a closure that reads the
|
|
6
|
+
* cached result — a hint in an error message is exactly the sort of thing that may be a few seconds
|
|
7
|
+
* stale.
|
|
8
|
+
*
|
|
9
|
+
* It only probes while NOTHING is connected. Once a session is live the question is moot, and a
|
|
10
|
+
* daemon that outlives the agent by hours has no business scanning ports it does not need.
|
|
11
|
+
*/
|
|
12
|
+
import { probeDevServers } from './dev-server-probe.js';
|
|
13
|
+
import { diagnoseNoSession } from './no-session-diagnosis.js';
|
|
14
|
+
/** Slow enough to be free, fast enough that a dev server started 15s ago is already reflected. */
|
|
15
|
+
const REFRESH_MS = 15_000;
|
|
16
|
+
/** Start the watch. Returns a stop function; the timer is unref'd so it never holds the daemon up. */
|
|
17
|
+
function startNoSessionWatch(options) {
|
|
18
|
+
const probe = options.probe ?? (() => probeDevServers());
|
|
19
|
+
let listening = [];
|
|
20
|
+
let running = false;
|
|
21
|
+
const refresh = () => {
|
|
22
|
+
// Nothing to diagnose while a session is live, and no reason to scan.
|
|
23
|
+
if (running || options.sessions.count() > 0)
|
|
24
|
+
return;
|
|
25
|
+
running = true;
|
|
26
|
+
void probe()
|
|
27
|
+
.then((ports) => {
|
|
28
|
+
listening = ports;
|
|
29
|
+
})
|
|
30
|
+
.catch(() => {
|
|
31
|
+
/* a diagnostic hint must never take the daemon down */
|
|
32
|
+
})
|
|
33
|
+
.finally(() => {
|
|
34
|
+
running = false;
|
|
35
|
+
});
|
|
36
|
+
};
|
|
37
|
+
refresh();
|
|
38
|
+
const timer = setInterval(refresh, REFRESH_MS);
|
|
39
|
+
timer.unref();
|
|
40
|
+
options.sessions.setNoSessionHint(() => diagnoseNoSession({
|
|
41
|
+
everConnected: options.sessions.everConnected(),
|
|
42
|
+
initialized: options.initialized,
|
|
43
|
+
listening,
|
|
44
|
+
port: options.port,
|
|
45
|
+
leaseExpired: (options.reapedLeases?.() ?? 0) > 0,
|
|
46
|
+
}));
|
|
47
|
+
return () => {
|
|
48
|
+
clearInterval(timer);
|
|
49
|
+
options.sessions.setNoSessionHint(undefined);
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* The daemon's whole session-scoping decision in one call: scope auto-selection to the active
|
|
54
|
+
* project, and keep the no-session diagnosis fresh. Both derive from the same one fact — whether
|
|
55
|
+
* this directory has been through `reticle init` — so they belong together rather than as two
|
|
56
|
+
* adjacent blocks in the bootstrap.
|
|
57
|
+
*/
|
|
58
|
+
export function wireSessionScope(sessions, activeProjectId, port,
|
|
59
|
+
/** Reader for the pool's aged-out-lease count; omitted when this daemon runs no pool. */
|
|
60
|
+
reapedLeases) {
|
|
61
|
+
if (activeProjectId !== undefined)
|
|
62
|
+
sessions.setDefaultScope({ projectId: activeProjectId });
|
|
63
|
+
return startNoSessionWatch({
|
|
64
|
+
sessions,
|
|
65
|
+
port,
|
|
66
|
+
initialized: activeProjectId !== undefined,
|
|
67
|
+
...(reapedLeases === undefined ? {} : { reapedLeases }),
|
|
68
|
+
});
|
|
69
|
+
}
|
|
@@ -42,8 +42,12 @@ export declare class ObservedState {
|
|
|
42
42
|
seedAmbient(counts: AmbientCounts): void;
|
|
43
43
|
/** Record a driven ref. Idempotent. */
|
|
44
44
|
recordActedRef(ref: string): void;
|
|
45
|
+
/** Record a driven control's re-render-surviving label. Idempotent; empty labels are not identities. */
|
|
46
|
+
recordActedLabel(label: string): void;
|
|
45
47
|
/** Every ref driven so far this session. */
|
|
46
48
|
actedRefs(): ReadonlySet<string>;
|
|
49
|
+
/** Every driven control's label, for matching across re-renders. */
|
|
50
|
+
actedLabels(): ReadonlySet<string>;
|
|
47
51
|
/**
|
|
48
52
|
* Record bridge-side sampling as a blind spot.
|
|
49
53
|
*
|
|
@@ -34,7 +34,7 @@ export class ObservedState {
|
|
|
34
34
|
if (event.type === EventType.BLIND_SPOT) {
|
|
35
35
|
const kind = event.data['kind'];
|
|
36
36
|
const count = event.data['count'];
|
|
37
|
-
if (typeof kind
|
|
37
|
+
if ('string' === typeof kind && 'number' === typeof count)
|
|
38
38
|
this.#blindSpots[kind] = count;
|
|
39
39
|
}
|
|
40
40
|
}
|
|
@@ -78,15 +78,33 @@ export class ObservedState {
|
|
|
78
78
|
* "recently untouched" — a number that reads as thorough while drifting toward the opposite.
|
|
79
79
|
*/
|
|
80
80
|
#actedRefs = new Set();
|
|
81
|
+
/**
|
|
82
|
+
* The same drives, keyed by something that SURVIVES a re-render.
|
|
83
|
+
*
|
|
84
|
+
* A ref is invalidated whenever the DOM changes, so on a framework that replaces nodes rather than
|
|
85
|
+
* reconciling in place (Next's app router, for one) every driven ref is stale by the time coverage
|
|
86
|
+
* is asked and the number reads 0 forever. The snapshot label — `button "Deploy"` — names the same
|
|
87
|
+
* control on both sides of a re-render.
|
|
88
|
+
*/
|
|
89
|
+
#actedLabels = new Set();
|
|
81
90
|
/** Record a driven ref. Idempotent. */
|
|
82
91
|
recordActedRef(ref) {
|
|
83
92
|
if (ref.length > 0)
|
|
84
93
|
this.#actedRefs.add(ref);
|
|
85
94
|
}
|
|
95
|
+
/** Record a driven control's re-render-surviving label. Idempotent; empty labels are not identities. */
|
|
96
|
+
recordActedLabel(label) {
|
|
97
|
+
if (label.length > 0)
|
|
98
|
+
this.#actedLabels.add(label);
|
|
99
|
+
}
|
|
86
100
|
/** Every ref driven so far this session. */
|
|
87
101
|
actedRefs() {
|
|
88
102
|
return this.#actedRefs;
|
|
89
103
|
}
|
|
104
|
+
/** Every driven control's label, for matching across re-renders. */
|
|
105
|
+
actedLabels() {
|
|
106
|
+
return this.#actedLabels;
|
|
107
|
+
}
|
|
90
108
|
/**
|
|
91
109
|
* Record bridge-side sampling as a blind spot.
|
|
92
110
|
*
|
|
@@ -58,7 +58,7 @@ export function sizeCost(payload) {
|
|
|
58
58
|
* Non-object results (e.g. a thrown-error envelope) pass through unchanged.
|
|
59
59
|
*/
|
|
60
60
|
export function withSizeCost(result) {
|
|
61
|
-
if (typeof result !== 'object' ||
|
|
61
|
+
if (typeof result !== 'object' || null === result)
|
|
62
62
|
return result;
|
|
63
63
|
return { ...result, cost: sizeCost(result) };
|
|
64
64
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { BrowserBrand } from '@reticlehq/core';
|
|
1
2
|
import type { Session } from './session.js';
|
|
2
3
|
/**
|
|
3
4
|
* The health block spliced onto act/assert results. Defined here rather than in session.ts: this is
|
|
@@ -45,12 +46,18 @@ export declare function healthEnvelope(session: Session): HealthEnvelope;
|
|
|
45
46
|
*/
|
|
46
47
|
export declare function refuseIfThrottled(session: Session, refuse: unknown): void;
|
|
47
48
|
/** The page's own visibility/runtime report, narrowed out of an untrusted PAGE_HEALTH payload. */
|
|
48
|
-
|
|
49
|
+
interface HealthReport {
|
|
49
50
|
hidden: boolean | undefined;
|
|
50
51
|
focused: boolean | undefined;
|
|
51
52
|
runtime: string | undefined;
|
|
52
53
|
/** Coarse rendering engine (blink/gecko/webkit) — context for a feedback report, not a health input. */
|
|
53
54
|
engine: string | undefined;
|
|
55
|
+
/**
|
|
56
|
+
* Which browser the page is. Narrowed to `BrowserBrand` HERE rather than trusted: the SDK already
|
|
57
|
+
* normalises, so anything else is a stale or hand-forged payload and is dropped — the point of the
|
|
58
|
+
* closed list is that an unbounded string can never reach telemetry through it.
|
|
59
|
+
*/
|
|
60
|
+
brand: BrowserBrand | undefined;
|
|
54
61
|
}
|
|
55
62
|
/**
|
|
56
63
|
* Narrow a PAGE_HEALTH payload. Every field is optional on the wire — an older SDK does not report
|