@reticlehq/server 2.4.0 → 2.5.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 +702 -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} +1 -1
- package/dist/{bridge.js → bridge/bridge.js} +63 -13
- package/dist/{bridge.test-harness.d.ts → bridge/bridge.test-harness.d.ts} +1 -11
- package/dist/{bridge.test-harness.js → bridge/bridge.test-harness.js} +9 -9
- 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-doctor.js → cli/cli-doctor.js} +9 -2
- 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-parse.d.ts → cli/cli-parse.d.ts} +2 -11
- package/dist/{cli-parse.js → cli/cli-parse.js} +113 -60
- 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.d.ts +2 -2
- package/dist/cli.js +174 -92
- 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 +49 -0
- package/dist/daemon/daemon-resilience.js +191 -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} +80 -48
- 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} +13 -0
- package/dist/{idle-shutdown.js → daemon/idle-shutdown.js} +27 -4
- 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 +43 -20
- package/dist/events/predicate-eval.js +159 -55
- package/dist/events/predicate.d.ts +2 -2
- package/dist/events/predicate.js +112 -35
- package/dist/events/reconcile.js +5 -5
- package/dist/events/ring-buffer.js +1 -1
- 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 +5 -7
- package/dist/flows/flow-replay.js +114 -47
- 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 +157 -6
- package/dist/flows/flow-success.js +20 -15
- package/dist/flows/flow-tools.js +53 -7
- 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/server-verify.d.ts +1 -1
- package/dist/flows/server-verify.js +5 -5
- 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 +16 -3
- package/dist/honesty/blind-spots.js +24 -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 +22 -3
- package/dist/honesty/verified.js +48 -8
- package/dist/http-server.js +14 -7
- package/dist/hunt/hunt-report.d.ts +2 -1
- package/dist/hunt/hunt-report.js +2 -2
- package/dist/index.d.ts +8 -6
- package/dist/index.js +32 -25
- package/dist/init/agent-rules.d.ts +2 -1
- package/dist/init/agent-rules.js +45 -7
- 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 +37 -0
- package/dist/init/cra.js +90 -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 +3 -2
- package/dist/init/detect.js +16 -5
- 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 +23 -6
- package/dist/init/plan-framework.d.ts +16 -1
- package/dist/init/plan-framework.js +132 -8
- package/dist/init/plan.d.ts +73 -8
- package/dist/init/plan.js +133 -34
- package/dist/init/project-id.d.ts +5 -9
- package/dist/init/project-id.js +10 -22
- package/dist/init/run.d.ts +5 -0
- package/dist/init/run.js +210 -36
- 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/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 +31 -0
- package/dist/mcp/mcp-outage.js +48 -0
- package/dist/mcp/mcp-proxy.d.ts +124 -0
- package/dist/mcp/mcp-proxy.js +697 -0
- package/dist/{mcp.d.ts → mcp/mcp.d.ts} +13 -10
- package/dist/{mcp.js → mcp/mcp.js} +110 -35
- 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/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/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 +38 -0
- package/dist/session/no-session-diagnosis.js +70 -0
- package/dist/session/no-session-watch.d.ts +19 -0
- package/dist/session/no-session-watch.js +61 -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/session-health.d.ts +8 -1
- package/dist/session/session-health.js +8 -7
- package/dist/session/session-info.d.ts +26 -7
- package/dist/session/session-info.js +42 -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 +3 -0
- package/dist/session/session-manager.js +52 -9
- 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 +14 -2
- package/dist/session/session.js +69 -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/daemon-telemetry.d.ts +19 -0
- package/dist/telemetry/daemon-telemetry.js +12 -2
- package/dist/telemetry/dev-repo.js +4 -4
- package/dist/telemetry/error-fingerprint.js +21 -2
- package/dist/telemetry/feedback-cli.d.ts +1 -1
- package/dist/telemetry/feedback-cli.js +4 -4
- package/dist/telemetry/feedback-context.d.ts +22 -0
- package/dist/telemetry/feedback-context.js +41 -1
- 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.js +1 -1
- package/dist/telemetry/session-metrics.d.ts +2 -0
- package/dist/telemetry/session-metrics.js +68 -6
- package/dist/telemetry/telemetry.d.ts +27 -2
- package/dist/telemetry/telemetry.js +119 -29
- 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 +84 -115
- 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 +3 -1
- package/dist/tools/assert-verdict.js +8 -5
- package/dist/tools/browser-tools.js +30 -11
- package/dist/tools/contract-tools.js +1 -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 +90 -18
- package/dist/tools/error-recovery.d.ts +20 -2
- package/dist/tools/error-recovery.js +179 -4
- 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 -6
- package/dist/tools/invoke-tool.js +57 -32
- 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-result.d.ts +17 -0
- package/dist/tools/navigate-result.js +31 -0
- package/dist/tools/observe-tools.js +54 -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 +88 -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 +8 -0
- package/dist/tools/tool-surface.d.ts +83 -0
- package/dist/tools/tool-surface.js +204 -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/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.md +28 -0
- package/docs/flows.md +166 -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/mint.json +61 -0
- package/docs/multi-agent-testing.md +55 -0
- package/docs/platform-integration.md +122 -0
- package/docs/telemetry-contract.md +214 -0
- package/docs/telemetry.md +184 -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-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-doctor.d.ts → cli/cli-doctor.d.ts} +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
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export declare const PEER_VERSION_PARAM = "peerVersion";
|
|
2
|
+
export declare const PEER_CONTRACT_PARAM = "peerContract";
|
|
3
|
+
/** Compare an attaching agent process against this daemon, and queue the nudge when they disagree. */
|
|
4
|
+
export declare function noteAgentPeer(version: string | null, contract: string | null): void;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How another Reticle PROCESS announces itself to the daemon.
|
|
3
|
+
*
|
|
4
|
+
* The SDK announces in HELLO and the CLI reads /status, but the MCP server an agent spawns had no
|
|
5
|
+
* announcement at all — and it is the piece the user actually hits: `npx @reticlehq/server mcp`
|
|
6
|
+
* resolves from a cache, so an agent can be running a months-old MCP package against a current
|
|
7
|
+
* daemon with nothing anywhere saying so.
|
|
8
|
+
*
|
|
9
|
+
* It rides the SSE connect the proxy already makes, as two query params. Query params rather than a
|
|
10
|
+
* new message because the connection happens before any protocol exchange, and because an older
|
|
11
|
+
* proxy that sends neither is handled correctly by describeSkew (absent contract + differing version
|
|
12
|
+
* = a build that predates the field).
|
|
13
|
+
*/
|
|
14
|
+
import { CONTRACT_FINGERPRINT } from '@reticlehq/core';
|
|
15
|
+
import { SERVER_VERSION } from './server-version.js';
|
|
16
|
+
import { describeSkew, DAEMON_FIX, SkewPair } from './version-skew.js';
|
|
17
|
+
import { noteVersionSkew } from './version-nudge.js';
|
|
18
|
+
export const PEER_VERSION_PARAM = 'peerVersion';
|
|
19
|
+
export const PEER_CONTRACT_PARAM = 'peerContract';
|
|
20
|
+
/** Compare an attaching agent process against this daemon, and queue the nudge when they disagree. */
|
|
21
|
+
export function noteAgentPeer(version, contract) {
|
|
22
|
+
const skew = describeSkew({
|
|
23
|
+
what: "the agent's MCP server",
|
|
24
|
+
version: version ?? undefined,
|
|
25
|
+
contract: contract ?? undefined,
|
|
26
|
+
fix: DAEMON_FIX,
|
|
27
|
+
}, { version: SERVER_VERSION, contract: CONTRACT_FINGERPRINT });
|
|
28
|
+
if (skew !== undefined)
|
|
29
|
+
noteVersionSkew(SkewPair.DAEMON, skew);
|
|
30
|
+
}
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import { createRequire } from 'node:module';
|
|
2
|
-
|
|
2
|
+
// NOTE: a RUNTIME path, not an import — `tsc` does not check it and a file move will not rewrite
|
|
3
|
+
// it. Two levels up because this file sits in `version/`: from `src/version/` and from
|
|
4
|
+
// `dist/version/` alike, `../../package.json` is the package manifest. Moving this file again means
|
|
5
|
+
// changing this line by hand; nothing else will tell you.
|
|
6
|
+
const _pkg = createRequire(import.meta.url)('../../package.json');
|
|
3
7
|
/** The Reticle server version, read from package.json at startup. */
|
|
4
8
|
export const SERVER_VERSION = _pkg.version;
|
|
5
9
|
/**
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Getting skew in front of the AGENT, on whatever tool it happens to be calling.
|
|
3
|
+
*
|
|
4
|
+
* Every check built before this reported into a place nobody looks: SDK skew landed in
|
|
5
|
+
* `reticle_sessions.versionSkew`, which an agent driving a flow never calls, and daemon skew went to
|
|
6
|
+
* a CLI log line no agent reads. So an agent could work an entire session against a mismatched pair,
|
|
7
|
+
* watch tools behave oddly, and never be told the one fact that explains it.
|
|
8
|
+
*
|
|
9
|
+
* This is the delivery channel — the same one the update nudge uses, for the same reason: an
|
|
10
|
+
* out-of-date install is worth mentioning whatever the agent is doing. It rides out on the next tool
|
|
11
|
+
* result, then goes quiet until the skew CHANGES. Told, not nagged: a banner on every call is one an
|
|
12
|
+
* agent learns to skip, and it would cost those tokens on every call for the rest of the session.
|
|
13
|
+
*/
|
|
14
|
+
import type { SkewPair } from './version-skew.js';
|
|
15
|
+
interface VersionSkewNudge {
|
|
16
|
+
pair: SkewPair;
|
|
17
|
+
action: string;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Record a skew for delivery. Re-arms only when the message DIFFERS from the one already delivered:
|
|
21
|
+
* a second tab on a third version is news; the same tab reconnecting is not.
|
|
22
|
+
*/
|
|
23
|
+
export declare function noteVersionSkew(pair: SkewPair, action: string): void;
|
|
24
|
+
/** The next undelivered skew, once. */
|
|
25
|
+
export declare function takeVersionSkew(): VersionSkewNudge | undefined;
|
|
26
|
+
/** Tests only — drop the module state so each case starts clean. */
|
|
27
|
+
export declare function resetVersionSkew(): void;
|
|
28
|
+
export {};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Getting skew in front of the AGENT, on whatever tool it happens to be calling.
|
|
3
|
+
*
|
|
4
|
+
* Every check built before this reported into a place nobody looks: SDK skew landed in
|
|
5
|
+
* `reticle_sessions.versionSkew`, which an agent driving a flow never calls, and daemon skew went to
|
|
6
|
+
* a CLI log line no agent reads. So an agent could work an entire session against a mismatched pair,
|
|
7
|
+
* watch tools behave oddly, and never be told the one fact that explains it.
|
|
8
|
+
*
|
|
9
|
+
* This is the delivery channel — the same one the update nudge uses, for the same reason: an
|
|
10
|
+
* out-of-date install is worth mentioning whatever the agent is doing. It rides out on the next tool
|
|
11
|
+
* result, then goes quiet until the skew CHANGES. Told, not nagged: a banner on every call is one an
|
|
12
|
+
* agent learns to skip, and it would cost those tokens on every call for the rest of the session.
|
|
13
|
+
*/
|
|
14
|
+
/** Pending per pair — the SDK and the daemon are different upgrades and each is worth saying once. */
|
|
15
|
+
const pending = new Map();
|
|
16
|
+
/** The last message delivered for each pair, so the same skew is never repeated. */
|
|
17
|
+
const delivered = new Map();
|
|
18
|
+
/**
|
|
19
|
+
* Record a skew for delivery. Re-arms only when the message DIFFERS from the one already delivered:
|
|
20
|
+
* a second tab on a third version is news; the same tab reconnecting is not.
|
|
21
|
+
*/
|
|
22
|
+
export function noteVersionSkew(pair, action) {
|
|
23
|
+
if (delivered.get(pair) === action)
|
|
24
|
+
return;
|
|
25
|
+
pending.set(pair, action);
|
|
26
|
+
}
|
|
27
|
+
/** The next undelivered skew, once. */
|
|
28
|
+
export function takeVersionSkew() {
|
|
29
|
+
for (const [pair, action] of pending) {
|
|
30
|
+
pending.delete(pair);
|
|
31
|
+
delivered.set(pair, action);
|
|
32
|
+
return { pair, action };
|
|
33
|
+
}
|
|
34
|
+
return undefined;
|
|
35
|
+
}
|
|
36
|
+
/** Tests only — drop the module state so each case starts clean. */
|
|
37
|
+
export function resetVersionSkew() {
|
|
38
|
+
pending.clear();
|
|
39
|
+
delivered.clear();
|
|
40
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Does this peer speak our contract? The daemon is the only thing that asks.
|
|
3
|
+
*
|
|
4
|
+
* Reticle is three separately-installed pieces — the SDK in the page, this daemon, and the MCP
|
|
5
|
+
* server the agent spawns — upgraded independently and therefore drifting constantly. Each pair
|
|
6
|
+
* fails the same silent way: `protocolVersion` still matches, the connection succeeds, and only
|
|
7
|
+
* BEHAVIOUR disagrees. A user hit exactly that with a 2.2.1 SDK against a 2.3.0 daemon and saw a
|
|
8
|
+
* bare `-32000` with nothing on either side naming a version.
|
|
9
|
+
*
|
|
10
|
+
* Two decisions shape this file.
|
|
11
|
+
*
|
|
12
|
+
* The daemon is the SINGLE judge. Every piece announces itself at a connect it already makes — the
|
|
13
|
+
* SDK in HELLO, the CLI and the MCP server over /status — and comparing happens here, once. Three
|
|
14
|
+
* independent pairwise checks would be three copies to keep in step and three separate complaints
|
|
15
|
+
* about one upgrade; a table in one place can say what is actually true: which pieces disagree, and
|
|
16
|
+
* which one is behind.
|
|
17
|
+
*
|
|
18
|
+
* The signal is the CONTRACT FINGERPRINT, not the version. Version equality is wrong in both
|
|
19
|
+
* directions: it fires on 2.4.0-vs-2.4.1 where nothing changed — so every patch release makes every
|
|
20
|
+
* un-upgraded app cry wolf, and the warning becomes noise — and it cannot fire at all for two
|
|
21
|
+
* different BUILDS of one version number, which is exactly what a stale daemon and a cached npx
|
|
22
|
+
* install are. The fingerprint is derived from core's wire vocabulary, so it moves when a name on
|
|
23
|
+
* the wire changes and stays put when internals do. Versions still ride along, because they are what
|
|
24
|
+
* a human acts on.
|
|
25
|
+
*/
|
|
26
|
+
/** Which pair disagreed. Named so the nudge can report each independently. */
|
|
27
|
+
export declare const SkewPair: {
|
|
28
|
+
/** The SDK in the page vs this daemon. */
|
|
29
|
+
readonly SDK: "sdk";
|
|
30
|
+
/** Another Reticle process — a CLI, or the MCP server an agent spawned — vs this daemon. */
|
|
31
|
+
readonly DAEMON: "daemon";
|
|
32
|
+
};
|
|
33
|
+
export type SkewPair = (typeof SkewPair)[keyof typeof SkewPair];
|
|
34
|
+
/** What a piece says about itself when it connects. */
|
|
35
|
+
export interface PeerIdentity {
|
|
36
|
+
/** Human-readable subject for the message: "the page", "the daemon on this port". */
|
|
37
|
+
what: string;
|
|
38
|
+
/** Package version, when the peer supplied one. Undefined on a hand-wired connect. */
|
|
39
|
+
version: string | undefined;
|
|
40
|
+
/** Contract fingerprint. Undefined means the peer predates the field — itself evidence of age. */
|
|
41
|
+
contract: string | undefined;
|
|
42
|
+
/** What the human runs to bring this piece into line. */
|
|
43
|
+
fix: string;
|
|
44
|
+
}
|
|
45
|
+
/** Everything a build knows about itself. */
|
|
46
|
+
interface SelfIdentity {
|
|
47
|
+
version: string;
|
|
48
|
+
contract: string;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* The one sentence to tell the agent, or undefined when the pair agrees.
|
|
52
|
+
*
|
|
53
|
+
* Silent when the fingerprints MATCH, whatever the versions say — that is the point: a patch bump
|
|
54
|
+
* that changed no wire name is not skew, and warning about it is how a real warning gets ignored.
|
|
55
|
+
*
|
|
56
|
+
* A peer with no fingerprint is treated as skewed ONLY when its version also differs from ours: the
|
|
57
|
+
* field is absent exactly on builds that predate it, so "no fingerprint AND a different version" is
|
|
58
|
+
* a build provably older than this daemon. With neither signal there is nothing to go on, and
|
|
59
|
+
* inventing a warning would be as dishonest as staying silent about a real one.
|
|
60
|
+
*/
|
|
61
|
+
export declare function describeSkew(peer: PeerIdentity, self: SelfIdentity): string | undefined;
|
|
62
|
+
/** The fix line for a page whose SDK disagrees with this daemon. */
|
|
63
|
+
export declare function sdkFix(daemonVersion: string): string;
|
|
64
|
+
/** The fix line for another Reticle process that disagrees with the daemon it attached to. */
|
|
65
|
+
export declare const DAEMON_FIX: string;
|
|
66
|
+
export {};
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Does this peer speak our contract? The daemon is the only thing that asks.
|
|
3
|
+
*
|
|
4
|
+
* Reticle is three separately-installed pieces — the SDK in the page, this daemon, and the MCP
|
|
5
|
+
* server the agent spawns — upgraded independently and therefore drifting constantly. Each pair
|
|
6
|
+
* fails the same silent way: `protocolVersion` still matches, the connection succeeds, and only
|
|
7
|
+
* BEHAVIOUR disagrees. A user hit exactly that with a 2.2.1 SDK against a 2.3.0 daemon and saw a
|
|
8
|
+
* bare `-32000` with nothing on either side naming a version.
|
|
9
|
+
*
|
|
10
|
+
* Two decisions shape this file.
|
|
11
|
+
*
|
|
12
|
+
* The daemon is the SINGLE judge. Every piece announces itself at a connect it already makes — the
|
|
13
|
+
* SDK in HELLO, the CLI and the MCP server over /status — and comparing happens here, once. Three
|
|
14
|
+
* independent pairwise checks would be three copies to keep in step and three separate complaints
|
|
15
|
+
* about one upgrade; a table in one place can say what is actually true: which pieces disagree, and
|
|
16
|
+
* which one is behind.
|
|
17
|
+
*
|
|
18
|
+
* The signal is the CONTRACT FINGERPRINT, not the version. Version equality is wrong in both
|
|
19
|
+
* directions: it fires on 2.4.0-vs-2.4.1 where nothing changed — so every patch release makes every
|
|
20
|
+
* un-upgraded app cry wolf, and the warning becomes noise — and it cannot fire at all for two
|
|
21
|
+
* different BUILDS of one version number, which is exactly what a stale daemon and a cached npx
|
|
22
|
+
* install are. The fingerprint is derived from core's wire vocabulary, so it moves when a name on
|
|
23
|
+
* the wire changes and stays put when internals do. Versions still ride along, because they are what
|
|
24
|
+
* a human acts on.
|
|
25
|
+
*/
|
|
26
|
+
/** Which pair disagreed. Named so the nudge can report each independently. */
|
|
27
|
+
export const SkewPair = {
|
|
28
|
+
/** The SDK in the page vs this daemon. */
|
|
29
|
+
SDK: 'sdk',
|
|
30
|
+
/** Another Reticle process — a CLI, or the MCP server an agent spawned — vs this daemon. */
|
|
31
|
+
DAEMON: 'daemon',
|
|
32
|
+
};
|
|
33
|
+
function versionPhrase(peer, self) {
|
|
34
|
+
return peer.version === undefined
|
|
35
|
+
? `${peer.what} is on an unknown version; this daemon is ${self.version}`
|
|
36
|
+
: `${peer.what} is ${peer.version}; this daemon is ${self.version}`;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* The one sentence to tell the agent, or undefined when the pair agrees.
|
|
40
|
+
*
|
|
41
|
+
* Silent when the fingerprints MATCH, whatever the versions say — that is the point: a patch bump
|
|
42
|
+
* that changed no wire name is not skew, and warning about it is how a real warning gets ignored.
|
|
43
|
+
*
|
|
44
|
+
* A peer with no fingerprint is treated as skewed ONLY when its version also differs from ours: the
|
|
45
|
+
* field is absent exactly on builds that predate it, so "no fingerprint AND a different version" is
|
|
46
|
+
* a build provably older than this daemon. With neither signal there is nothing to go on, and
|
|
47
|
+
* inventing a warning would be as dishonest as staying silent about a real one.
|
|
48
|
+
*/
|
|
49
|
+
export function describeSkew(peer, self) {
|
|
50
|
+
if (peer.contract !== undefined && peer.contract === self.contract)
|
|
51
|
+
return undefined;
|
|
52
|
+
if (peer.contract === undefined) {
|
|
53
|
+
if (peer.version === undefined || peer.version === self.version)
|
|
54
|
+
return undefined;
|
|
55
|
+
return (`version skew: ${versionPhrase(peer, self)}, and it is too old to report which wire contract ` +
|
|
56
|
+
`it speaks. They connect anyway and then disagree about behaviour, which usually surfaces as ` +
|
|
57
|
+
`a bare -32000 with nothing naming a version. ${peer.fix}`);
|
|
58
|
+
}
|
|
59
|
+
return (`version skew: ${versionPhrase(peer, self)}, and they speak DIFFERENT wire contracts ` +
|
|
60
|
+
`(${peer.contract} vs ${self.contract}) — so this is a real mismatch, not a harmless patch ` +
|
|
61
|
+
`difference. Tools will behave in ways neither side reports. ${peer.fix}`);
|
|
62
|
+
}
|
|
63
|
+
/** The fix line for a page whose SDK disagrees with this daemon. */
|
|
64
|
+
export function sdkFix(daemonVersion) {
|
|
65
|
+
return (`Tell the human to install the matching SDK (\`npm i -D @reticlehq/react@${daemonVersion}\`) ` +
|
|
66
|
+
'or run `reticle update`, then restart their dev server so the page reloads with it.');
|
|
67
|
+
}
|
|
68
|
+
/** The fix line for another Reticle process that disagrees with the daemon it attached to. */
|
|
69
|
+
export const DAEMON_FIX = 'A daemon outlives the agents attached to it, so it keeps serving its OWN code until it restarts ' +
|
|
70
|
+
'— anything fixed in the newer package is simply absent. Run `reticle stop` and retry to replace ' +
|
|
71
|
+
'it (other agents on that daemon will need to reconnect).';
|
|
@@ -32,7 +32,7 @@ function changedRegion(out, width, height) {
|
|
|
32
32
|
for (let x = 0; x < width; x += 1) {
|
|
33
33
|
const i = (y * width + x) * RGBA;
|
|
34
34
|
// pixelmatch marks a changed pixel solid red (default diffColor) at full alpha.
|
|
35
|
-
if (out[i]
|
|
35
|
+
if (255 === out[i] && 0 === out[i + 1] && 0 === out[i + 2] && 255 === out[i + 3]) {
|
|
36
36
|
if (x < minX)
|
|
37
37
|
minX = x;
|
|
38
38
|
if (y < minY)
|
|
@@ -80,7 +80,7 @@ export async function diffPng(baselineBytes, currentBytes, opts = {}) {
|
|
|
80
80
|
const threshold = opts.threshold ?? VISUAL_PIXEL_THRESHOLD;
|
|
81
81
|
const changedPixels = pixelmatch(a.data, b.data, out.data, width, height, { threshold });
|
|
82
82
|
const totalPixels = width * height;
|
|
83
|
-
const ratio =
|
|
83
|
+
const ratio = 0 === totalPixels ? 0 : changedPixels / totalPixels;
|
|
84
84
|
const maxRatio = opts.maxRatio ?? 0;
|
|
85
85
|
const region = changedPixels > 0 ? changedRegion(out.data, width, height) : undefined;
|
|
86
86
|
const result = {
|
|
@@ -16,7 +16,7 @@ const rectShape = z.object({
|
|
|
16
16
|
});
|
|
17
17
|
function screenshotProvider(deps) {
|
|
18
18
|
const p = deps.realInput;
|
|
19
|
-
return p !== undefined && typeof p.screenshot
|
|
19
|
+
return p !== undefined && 'function' === typeof p.screenshot
|
|
20
20
|
? p
|
|
21
21
|
: undefined;
|
|
22
22
|
}
|
|
@@ -55,7 +55,7 @@ async function buildOpts(deps, sessionId, args) {
|
|
|
55
55
|
if (box !== undefined)
|
|
56
56
|
return { clip: box };
|
|
57
57
|
}
|
|
58
|
-
return args['fullPage']
|
|
58
|
+
return true === args['fullPage'] ? { fullPage: true } : {};
|
|
59
59
|
}
|
|
60
60
|
/**
|
|
61
61
|
* Ask the desktop shell to photograph its own window.
|
|
@@ -110,7 +110,7 @@ function isCapturePath(path) {
|
|
|
110
110
|
function isCompletePng(bytes) {
|
|
111
111
|
if (bytes.byteLength < 12)
|
|
112
112
|
return false;
|
|
113
|
-
return bytes.subarray(bytes.byteLength - 8, bytes.byteLength - 4).toString('ascii')
|
|
113
|
+
return 'IEND' === bytes.subarray(bytes.byteLength - 8, bytes.byteLength - 4).toString('ascii');
|
|
114
114
|
}
|
|
115
115
|
/**
|
|
116
116
|
* Get pixels for a session, by whichever route exists.
|
|
@@ -137,7 +137,7 @@ async function capture(deps, sessionId, args) {
|
|
|
137
137
|
if (png !== undefined)
|
|
138
138
|
return { png };
|
|
139
139
|
}
|
|
140
|
-
const desktop = await desktopCapture(deps, sessionId, args['fullPage']
|
|
140
|
+
const desktop = await desktopCapture(deps, sessionId, true === args['fullPage']);
|
|
141
141
|
if (desktop.png !== undefined)
|
|
142
142
|
return { png: desktop.png };
|
|
143
143
|
if (desktop.reason !== undefined)
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# Reticle — agent cheat-sheet
|
|
2
|
+
|
|
3
|
+
One screen to get fluent. Reticle is the **proof layer for AI agents** — no screenshots, no vision model, evidence not prose. Everything below returns structured data. Full guide: [usage.md](usage.md).
|
|
4
|
+
|
|
5
|
+
## The core loop: look → act → observe → assert
|
|
6
|
+
|
|
7
|
+
| Verb | Tool | One-liner |
|
|
8
|
+
| --- | --- | --- |
|
|
9
|
+
| **look** | `reticle_snapshot` / `reticle_query` | See the page (semantic tree) / find one specific element. |
|
|
10
|
+
| **act** | `reticle_act` / `reticle_act_and_wait` | Click/fill a `ref` / act + wait for a predicate in one hop. |
|
|
11
|
+
| **observe** | `reticle_observe` / `reticle_wait_for` | Everything the app did after `since` / block until true. |
|
|
12
|
+
| **assert** | `reticle_assert` | Evaluate a predicate → `{ pass, evidence, failureReason? }`. The end of every loop. |
|
|
13
|
+
|
|
14
|
+
`reticle_act` returns a `since` cursor — pass it to `reticle_observe({ since })` to scope the window. Elements are addressed by stable refs (`e7`) from `snapshot`/`query`; they re-resolve across re-renders.
|
|
15
|
+
|
|
16
|
+
**`assert`/`wait_for` are auto-scoped to your last act.** By default they only count events buffered _since_ the most recent act, so a stale signal from a previous step can't fake a pass — pass an explicit `since` to override. **Clicks run the code, not pixels:** `reticle_act` click fires the full pointer sequence on the element (no coordinate gesture for the HUD to intercept), reports `occluded:true` when something covers the target, and stays synthetic even with CDP configured (use `args:{ native:true }` for a trusted native click).
|
|
17
|
+
|
|
18
|
+
**Never sleep — wait deterministically.** Fixed sleeps are the #1 cause of flaky agent tests. Instead:
|
|
19
|
+
|
|
20
|
+
- `reticle_act_and_wait({ ref, action })` with **no `until`** waits for the page to _settle_ (network + structural DOM idle; ambient count-up/spinner churn is ignored so an animated page still settles) before returning — the one-call replacement for "click then sleep 500ms".
|
|
21
|
+
- Need to wait without acting? `reticle_wait_for({ predicate: { kind: "settled", quietMs } })`.
|
|
22
|
+
- Waiting for a specific outcome? Pass that consequence as the predicate (`{ signal }` / `{ net }`), or `allOf` it with `{ kind: "settled" }` to wait for both the event _and_ the page going quiet.
|
|
23
|
+
|
|
24
|
+
**Assert a consequence, not just presence.** `{ signal }` / `{ net }` prove the feature actually did something; `{ element }` / `{ text }` only prove something is on screen — which a stale render or a locator healed to the wrong element can fake. A _passing_ presence-only `reticle_assert` returns `advice` nudging you to a consequence; heed it on anything that matters.
|
|
25
|
+
|
|
26
|
+
## The 4-layer cross-check — never trust a green the state contradicts
|
|
27
|
+
|
|
28
|
+
A claim is real only when the layers agree. Check more than the UI:
|
|
29
|
+
|
|
30
|
+
| Layer | Tool(s) | Question it answers |
|
|
31
|
+
| --- | --- | --- |
|
|
32
|
+
| **UI** | `reticle_snapshot` / `reticle_query` | Is it on screen / in the right state? |
|
|
33
|
+
| **signal** | `reticle_capabilities` / `reticle_observe` | Did the app emit the intent it advertised? |
|
|
34
|
+
| **network** | `reticle_network` | Did `POST /x` actually fire and return 200? |
|
|
35
|
+
| **store** | `reticle_state` | Does live framework/store state match? |
|
|
36
|
+
|
|
37
|
+
> **Rule:** a passing UI assert that the store, network, or signal contradicts is a **false green**.
|
|
38
|
+
|
|
39
|
+
**Session health is universal.** Every live-session tool result carries a `session` block (`throttled`, `focused`, `lastSeenMs`); when `throttled:true` it also adds a `warning` + `recommendation` (refocus, or `reticle drive`). A throttled/backgrounded tab can silently no-op timers/rAF/pointer gestures — if you see `session.throttled`, distrust a green and refocus first.
|
|
40
|
+
|
|
41
|
+
> Store reads (`reticle_state`) are the reliable path; the DOM can lie (optimistic UI, stale render).
|
|
42
|
+
|
|
43
|
+
**Truncation and coverage are declared, never silent.** A big `reticle_state` read carries `truncation` when the transport caps dropped items — its presence means "this is NOT the whole value", so an absence assertion over a truncated read proves nothing; scope with `path`/`depth` instead. A `reticle_assert` verdict carries `coverage` when part of the page was unobservable (cross-origin iframe, closed shadow root) — a green then means "nothing failed in the part I could see", not "the page is correct". Both fields are **omitted when everything was fine**, so their presence is the warning.
|
|
44
|
+
|
|
45
|
+
**Charts report their own geometry faults.** Any element descriptor containing a broken plot carries `chart: [{ kind, tag, attr, sample }]` — `non-finite-coordinates` (a zero-range scale divided by zero; always a bug), `empty-geometry` (mounted but no data reached it), `degenerate-geometry` (every point identical). No extra call and no flag: query the chart as you would anyway. A healthy chart adds nothing. This matters because a chart is the one widget whose correctness is _geometry_ — the store can be right while the polyline is blank, and neither a state read nor a screenshot catches it. Canvas charts are pixels, not DOM: read their data with `canvasChartData(canvasEl, window)` instead.
|
|
46
|
+
|
|
47
|
+
**Reads never go silently empty.** A zero-result read returns a `hint`, not a bare `[]`: `reticle_query` → `{ route, presentTestids, knownEmptyState }`; `reticle_network` → `{ totalInWindow, present[] }` (what DID fire); `reticle_console` → `{ totalInWindow, byLevel }` (so "0 errors" ≠ "silent page"); `reticle_state` lists `storeNames` when a store isn't found. Read the hint before assuming "not there." Scope big stores with `reticle_state({ store, path:"a.b.0", depth })` instead of paying for the whole tree; a wrong `path` returns `{ found:false, availableKeys }` so it's self-correcting.
|
|
48
|
+
|
|
49
|
+
## Core tool set
|
|
50
|
+
|
|
51
|
+
The tools advertised DIRECTLY — what you'll use 90% of the time. Everything else is one `reticle_run` away (see Token note below).
|
|
52
|
+
|
|
53
|
+
`reticle_sessions` · `reticle_navigate` · `reticle_snapshot` · `reticle_query` · `reticle_act` · `reticle_act_and_wait` · `reticle_observe` · `reticle_network` · `reticle_console` · `reticle_wait_for` · `reticle_assert` · `reticle_state` · `reticle_inspect` (DOM node → `src/App.tsx:104`) · `reticle_feedback` (tell the maintainers what is missing) · `reticle_session` (hand back: `{action:"yield"}` the moment you stop driving, `{action:"resume"}` after a human pause).
|
|
54
|
+
|
|
55
|
+
Frequently useful but NOT core, so reach them through `reticle_run({ tool, args })`: `reticle_domain` (learn the app + gaps), `reticle_capabilities` (the app's declared testable surface), `reticle_baseline {action:"diff"}`, `reticle_project` (run history).
|
|
56
|
+
|
|
57
|
+
**Reach past core when…** you need to record/replay a journey (`reticle_record {action:"start"}/stop`, `reticle_replay`), persist a self-healing golden flow (`reticle_flow_save*` / `reticle_flow_replay` / `reticle_flow_heal`), compile annotations (`reticle_annotate`), explore autonomously (`reticle_explore` lists controls; `reticle_crawl` clicks them all and reports anomalies — **destructive**), reveal a virtualized off-screen row (`reticle_scroll_to` — when `reticle_query` finds nothing because a windowed list hasn't rendered it yet), visual-check (`reticle_screenshot` / `reticle_visual_diff`, pinned with `reticle_viewport` for reproducible baselines), test error/edge states by stubbing the network (`reticle_network_mock` — 500 / offline / delay, driven mode), control time for toasts/debounces/auto-dismiss (`reticle_clock { freeze | advanceMs | reset }`), or work with a human (`reticle_session {action:"end"}` / `reticle_session {action:"resume"}` / `reticle_session {action:"messages"}`, and **`reticle_session {action:"review"}`** to drain + fix the bugs the human flagged from the panel).
|
|
58
|
+
|
|
59
|
+
## flows vs baselines vs project.json (the persistence layers)
|
|
60
|
+
|
|
61
|
+
| Artifact | Tool(s) | What it is |
|
|
62
|
+
| --- | --- | --- |
|
|
63
|
+
| **flows** | `reticle_flow_save*` / `reticle_flow_replay` / `reticle_flow_heal` | Replayable **golden journeys**, anchored to testids/signals — drift is legible and self-heals. |
|
|
64
|
+
| **baselines** | `reticle_baseline {action:"save"}` / `reticle_baseline {action:"diff"}` | Structural **"before" snapshots**; `reticle_baseline {action:"diff"}` flags regressions against them. |
|
|
65
|
+
| **project.json** | `reticle_project` | Cross-run **run-history** — "did it behave like last run?" read via `reticle_project`. |
|
|
66
|
+
|
|
67
|
+
> `reticle_project` / `project.json` are the **run-history layer**. flows answer "does the journey still work?"; baselines answer "did the structure change?"; project.json answers "is this run consistent with prior runs?".
|
|
68
|
+
|
|
69
|
+
**Visual layer (opt-in).** `reticle_screenshot` saves a PNG baseline to `.reticle/visual/<name>.png`; `reticle_visual_diff` perceptually compares the live page to it (`{ masks }` to ignore volatile regions, `{ maxRatio }` tolerance) → `{ matched, changedPixels, ratio, region, diffPath }`. It answers "does it **look** right" — complementary to the behavioral layers, never a replacement. Both need a **driven browser** (`reticle drive <url>` / `RETICLE_CDP_URL`); without one they return `{ ok:false, reason:"no-visual-provider" }` (the always-on SDK ships no screenshotter).
|
|
70
|
+
|
|
71
|
+
## Start here
|
|
72
|
+
|
|
73
|
+
0. Just ran `reticle init` / started the dev server? Poll `reticle_sessions()` until your tab appears — readiness is server-internal now, so the first live call already blocks until the SDK connects.
|
|
74
|
+
1. `reticle_sessions` — find the connected tab (omit `sessionId` if there's only one).
|
|
75
|
+
2. `reticle_domain` — learn the app BEFORE testing: the saved flows, what each asserts, and the **gaps** (declared signals/testids that no flow verifies — untested intent). Tells you what to test and where the real risk is without crawling the whole app. Falls back to `reticle_capabilities` for the raw testable surface (`testids`, `signals`, `stores`, `flows`).
|
|
76
|
+
3. Run the loop: **look → act → observe → assert**, cross-checking the 4 layers on anything that matters.
|
|
77
|
+
|
|
78
|
+
## Token note
|
|
79
|
+
|
|
80
|
+
- **Keep observation cheap.** Prefer `reticle_query` / scoped or `interactive` `reticle_snapshot` / `reticle_assert` over dumping the full tree. A full verify loop is ~100 tokens; see [token-efficiency.md](token-efficiency.md) (~73× leaner than full-tree snapshots).
|
|
81
|
+
- **Re-look with `reticle_snapshot({ diff:true })`** after an action — it returns only what changed (`mode:delta`/`unchanged`), ~99% fewer tokens than a full re-snapshot and no stale tree to mis-read. Every snapshot/query result carries `cost:{ bytes, tokens }` — re-scope before reading if it's large.
|
|
82
|
+
- **Cap broad reads.** `reticle_query` takes `limit` (caps descriptors; reports `total`/`truncated`) and `count_only` (just the match count). `reticle_network` / `reticle_console` take `limit` (most-recent-N, reports `droppedOldest`) and carry the same `cost` hint — so a busy page or wide window never floods your context unnoticed.
|
|
83
|
+
- **A saved flow tells you if it's a real test.** `reticle_flow_save` returns `assertions.grade` (`asserted` / `presence-only` / `assertion-free`); if it's not `asserted`, add a consequence (`reticle_annotate` assert-signal/assert-net or a success-state) so it can't pass while broken. On replay, an ambiguous heal (two testids tie) is surfaced, never auto-applied — and an `apply` heal re-replays the rebound flow and **refuses to write** if the success consequence no longer fires (`status:consequence_broken`): it heals the locator, never the intent.
|
|
84
|
+
- **Predicate schema is not bloated.** The recursive predicate DSL used by `reticle_assert` / `reticle_wait_for` / `reticle_act_and_wait` is **factored, not inlined**: when converted to the JSON Schema MCP sends, the predicate body is emitted **once** (~2.7k chars ≈ **~685 tokens** per tool) and recursion is handled by self-`$ref` (`#/properties/predicate`) — no per-recursion duplication. No action needed.
|
|
85
|
+
- **One tool surface.** Reticle advertises the core set directly (sessions/navigate/snapshot/query/act/act_and_wait/observe/network/console/wait_for/assert/state/inspect/feedback/session — the whole detect loop, plus the file-pointer, the feedback channel and the handback) PLUS two meta-tools that keep every other tool one call away: `reticle_tools` (discover — no args lists every tool name + summary; `names:[…]` loads full params on demand) and `reticle_run({ tool, args })` (invoke any tool by name; a top-level `sessionId` is forwarded to the target, so you need not nest it in `args`). So to record/replay/verify a flow, call `reticle_run({ tool:"reticle_flow_verify", sessionId })` (or `reticle_tools` first to see params). There is nothing to pick. `RETICLE_ADVERTISE_ALL_TOOLS=1` (read by the daemon at startup, so restart it) advertises everything with output schemas — a verification switch for suites, not a way to run agents. **Sizes are deliberately not quoted here** — a count in prose goes stale and has three times already. `reticle_tools` reports the live surface, and SKILL.md carries the one gated table.
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# Reticle architecture — how it works and why it's built this way
|
|
2
|
+
|
|
3
|
+
> For engineers evaluating Reticle, integrating it at scale, or contributing. It explains the moving parts, the data flow, and the design decisions behind them. If you just want to get running, start with [getting-started.md](getting-started.md); come back here when you want to know _why_.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## The one-paragraph model
|
|
8
|
+
|
|
9
|
+
Your app, in dev, embeds a tiny **SDK** that instruments the page (DOM, network, console, routing, framework state) and opens a WebSocket to a local **bridge**. The bridge runs inside the Reticle **server**, which also exposes an **MCP server** — the standard protocol AI agents speak. Your coding agent calls MCP tools (`reticle_query`, `reticle_act`, `reticle_assert`, …); the server turns them into commands over the WebSocket; the SDK executes them in the page and streams back structured events. The agent thus **looks, acts, observes, and asserts** on the real running app — never on a screenshot.
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
┌─────────────────┐ MCP (stdio/SSE) ┌──────────────────────────┐ WebSocket ┌────────────────────┐
|
|
13
|
+
│ AI agent │ ──────────────────────► │ @reticlehq/server │ ◄────────────────► │ @reticlehq/browser│
|
|
14
|
+
│ (Claude Code, │ reticle_query/act/... │ bridge + MCP + CLI │ commands/events │ (SDK in your app) │
|
|
15
|
+
│ Cursor, ...) │ ◄────────────────────── │ (Node) │ │ (the DOM) │
|
|
16
|
+
└─────────────────┘ tool results └──────────────────────────┘ └────────────────────┘
|
|
17
|
+
│
|
|
18
|
+
reads/writes .reticle/ (flows, baselines, runs, contract)
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## The packages (and the boundaries between them)
|
|
24
|
+
|
|
25
|
+
Reticle is a pnpm + Turborepo monorepo. The split is not cosmetic — each boundary enforces a rule.
|
|
26
|
+
|
|
27
|
+
| Package | Runs in | Responsibility | Hard rule |
|
|
28
|
+
| --- | --- | --- | --- |
|
|
29
|
+
| `@reticlehq/core` | both | The **wire contract**: every constant + zod schema crossing any boundary | Depends only on `zod` |
|
|
30
|
+
| `@reticlehq/browser` | the browser | Instrument the page; execute commands; emit events | Never imports Node APIs |
|
|
31
|
+
| `@reticlehq/server` | Node | The bridge, the MCP server, the `reticle` CLI, flow/run storage | Never imports DOM APIs |
|
|
32
|
+
| `@reticlehq/react` | the browser | The SDK **kit** you install in a browser app: re-exports the browser sensor (so one install gives both `reticle` and `install`) and maps a DOM node → React component → source `file:line` | Core works without the source-mapping half |
|
|
33
|
+
| `@reticlehq/babel-plugin`, `@reticlehq/next`, `@reticlehq/vite-plugin` | build time | Stamp `data-reticle-source` for source mapping (and, for Vite, inject `connect()`) | Plain tooling |
|
|
34
|
+
|
|
35
|
+
> Note: pre-2.0, `@reticlehq/core` was a single umbrella package that re-exported all of the above under subpaths. In 2.0 the umbrella was retired and `@reticlehq/core` became the bottom-of-graph wire contract.
|
|
36
|
+
|
|
37
|
+
**Why core-as-contract matters:** because the browser and the server are two different runtimes (a DOM and a Node process) that must agree exactly on every message, the temptation is to inline a string like `"net.request"` in both. That's how drift and silent breakage start. Instead, every such string and shape lives once in `@reticlehq/core` as a named constant + a zod schema. The browser and server both import it; neither can invent a message the other doesn't understand. The server zod-parses **every** inbound WebSocket message — malformed input closes the socket rather than flowing into logic.
|
|
38
|
+
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
## The data flow, end to end
|
|
42
|
+
|
|
43
|
+
1. **Connect.** In dev, your app calls `reticle.connect({ session })`. The SDK opens a WebSocket to the bridge (`ws://localhost:4400/reticle` by default) and sends a `HELLO` carrying the session id, protocol version, and (if configured) a pairing token. Each browser tab uses `SESSION_AUTO` — a unique id — so multiple apps/tabs never collide.
|
|
44
|
+
2. **Capture.** The SDK installs observers: a `MutationObserver` for DOM changes, wrappers around `fetch`/`XHR` for network, a console hook, a history hook for routing, and registries the app opts into (`registerStore`, `registerCapabilities`, `reticle.signal`). Events flow into a bounded **ring buffer** — recent history is always available, memory is capped.
|
|
45
|
+
3. **Look / act.** The agent calls an MCP tool. `reticle_query` finds an element by role/text/testid/ component and returns a stable **ref**. `reticle_act` dispatches an action against that ref and returns an **effect** report (did it land, did the DOM mutate, did focus move…). The server sends the command over the WebSocket; the SDK runs it and replies.
|
|
46
|
+
4. **Observe.** After an action, the agent reads what happened — `reticle_network`, `reticle_console`, `reticle_state`, or the reaction digest from `reticle_act_and_wait`. The server pulls the relevant slice of the ring buffer (scoped to a cursor so stale events can't leak in) and returns a compact summary.
|
|
47
|
+
5. **Assert.** `reticle_assert` (and a flow's declared `success`) evaluates a **predicate** over program truth — a network call that returned 200, a store value, a `signal` the app emitted — not just "an element exists." This is the difference between "looks done" and "is done."
|
|
48
|
+
|
|
49
|
+
---
|
|
50
|
+
|
|
51
|
+
## The four design decisions that define Reticle
|
|
52
|
+
|
|
53
|
+
### 1. Assert the consequence, not the appearance
|
|
54
|
+
|
|
55
|
+
Most agent-browser tools can confirm "an element matching X is present." That's the weakest possible oracle: a wrong or healed-to-wrong element satisfies it, and the regression ships anyway. Reticle grades evidence in tiers:
|
|
56
|
+
|
|
57
|
+
- **Tier 1 — an app signal** (`reticle.signal('order:placed')`): the strongest, because a wrong element can't fake it. Available when the app emits it (a ~30-second opt-in).
|
|
58
|
+
- **Tier 2 — network + route + state**: a POST returned 200, the URL changed, a store value updated. Strong, and works on most SPAs with no setup.
|
|
59
|
+
- **Tier 3 — DOM/text presence**: the weak fallback Reticle nudges _away_ from.
|
|
60
|
+
|
|
61
|
+
Reticle is honest about which tier a given assertion used, so "green" carries its own confidence.
|
|
62
|
+
|
|
63
|
+
### 2. Structured reads, not screenshots
|
|
64
|
+
|
|
65
|
+
A screenshot is ~1,365 image tokens per look, slow, non-deterministic, and **blind to everything non-visual** — the failed request, the console error, the route that didn't change. Reticle reads the accessibility tree, the network log, the console, and framework state as compact structured data. That's an order of magnitude cheaper _and_ it sees the bugs pixels can't. See [benchmarks.md](benchmarks.md) for the measured comparison.
|
|
66
|
+
|
|
67
|
+
### 3. Record once, replay deterministically
|
|
68
|
+
|
|
69
|
+
The same verification runs over and over — every commit, every CI run. Reticle records a **flow** once, then replays it with **no AI model**: it re-resolves each element's durable anchor against the live DOM and re-asserts the declared consequence. A CI gate diffs the verdict exactly, at ~0% flake, for a couple hundred tokens — versus an agent re-driving the whole flow with the model every time. Self- healing rebinds a drifted anchor **only if the consequence still fires**, so it never "heals to the wrong element and ships the regression."
|
|
70
|
+
|
|
71
|
+
### 4. Dev-only, localhost-only, your app data stays local
|
|
72
|
+
|
|
73
|
+
The SDK is tree-shaken out of production builds and connects only to a local bridge. The bridge binds to loopback by default; exposing it beyond localhost _requires_ a pairing token (the server refuses to bind a non-loopback host without one). Every environment variable that gates a security control is a single named constant, so a typo can't silently disable auth. Nothing from the app under test — no DOM, network, console, state, or source — ever leaves your machine. The CLI reports anonymous, opt-out usage metrics only (a random id + event names like `invoke`/`session_start`; no code, no PII — see [telemetry](telemetry.md)); opt out with `reticle telemetry disable`, `RETICLE_TELEMETRY=0`, or `DO_NOT_TRACK=1`. The one thing that carries free text is feedback you or your agent deliberately send (`reticle feedback` / `reticle_feedback`) — never collected passively, redacted before sending, and separately disabled with `RETICLE_FEEDBACK=0`.
|
|
74
|
+
|
|
75
|
+
---
|
|
76
|
+
|
|
77
|
+
## State on disk: the `.reticle/` workspace
|
|
78
|
+
|
|
79
|
+
The server persists project state under `.reticle/` in your repo:
|
|
80
|
+
|
|
81
|
+
- **`flows/`** — recorded, replayable flows (the golden journeys).
|
|
82
|
+
- **`baselines/`** — saved snapshots for diffing.
|
|
83
|
+
- **`runs/`** — verification run artifacts (the evidence trail; writes are atomic and bounded so a crash never leaves a half-written artifact, and the directory is pruned).
|
|
84
|
+
- **the capabilities contract** — the testids/signals/stores/flows the app advertises, frozen under a version so the public artifact can't break silently.
|
|
85
|
+
|
|
86
|
+
This is plain, reviewable, version-controllable data — not a black box.
|
|
87
|
+
|
|
88
|
+
---
|
|
89
|
+
|
|
90
|
+
## Running at scale (multiple apps, multiple projects)
|
|
91
|
+
|
|
92
|
+
- **Multiple apps / tabs on one bridge:** fine — each connection has a unique `SESSION_AUTO` id; a tool call targets the focused/most-recent session, or you pass an explicit `sessionId`.
|
|
93
|
+
- **Multiple isolated projects:** give each project its own bridge port via `RETICLE_PORT` (set it in the MCP config and dial the same port from the app). A port already in use fails fast with a clear error rather than hanging. See [getting-started.md → Running multiple apps](getting-started.md#running-multiple-apps-at-once).
|
|
94
|
+
- **CI / no MCP:** `reticle verify <url>` replays the saved flows headlessly and exits non-zero on failure — the same verdict artifact the MCP path produces, with no agent in the loop.
|
|
95
|
+
|
|
96
|
+
---
|
|
97
|
+
|
|
98
|
+
## Open-core licensing (what's free, what's protected)
|
|
99
|
+
|
|
100
|
+
- The embeddable **SDK** (`-core`, `-browser`, `-react`) is **Apache-2.0** — safe to ship inside your own app.
|
|
101
|
+
- The **server / CLI** is under the **Functional Source License (FSL-1.1, Apache-2.0 future)** — source-available, converts to Apache-2.0 over time.
|
|
102
|
+
- Enterprise-only features live behind a license gate and are clearly separated.
|
|
103
|
+
|
|
104
|
+
See [LICENSE](../LICENSE) and each package's own `LICENSE` file. The licensing _mechanism_ is open and inspectable; activation is offline (no phone-home).
|
|
105
|
+
|
|
106
|
+
---
|
|
107
|
+
|
|
108
|
+
## Where to go next
|
|
109
|
+
|
|
110
|
+
- [getting-started.md](getting-started.md) — wire Reticle into your app in a couple of minutes.
|
|
111
|
+
- [benchmarks.md](benchmarks.md) — how we measure, and the honest results vs the alternatives.
|
|
112
|
+
- [usage.md](usage.md) — the full tool reference and advanced modes.
|
|
113
|
+
- [CONTRIBUTING.md](../CONTRIBUTING.md) — the development loop and the rules.
|