@reticlehq/server 2.3.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} +105 -15
- package/dist/{bridge.test-harness.d.ts → bridge/bridge.test-harness.d.ts} +6 -11
- package/dist/{bridge.test-harness.js → bridge/bridge.test-harness.js} +21 -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/cli-parse.d.ts +143 -0
- package/dist/{cli-parse.js → cli/cli-parse.js} +146 -65
- package/dist/{cli-port.d.ts → cli/cli-port.d.ts} +15 -0
- package/dist/{cli-port.js → cli/cli-port.js} +29 -8
- 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 +182 -93
- 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.d.ts +0 -6
- package/dist/flows/decision.js +64 -8
- 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 +67 -13
- 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.d.ts +17 -0
- package/dist/init/capabilities.js +54 -0
- 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 +26 -0
- package/dist/init/detect.js +78 -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 +39 -0
- package/dist/init/next-patch.js +196 -0
- package/dist/init/node-io.js +48 -6
- package/dist/init/patch-kind.d.ts +20 -0
- package/dist/init/patch-kind.js +10 -0
- package/dist/init/plan-framework.d.ts +50 -0
- package/dist/init/plan-framework.js +341 -0
- package/dist/init/plan.d.ts +119 -6
- package/dist/init/plan.js +230 -131
- package/dist/init/project-id.d.ts +5 -9
- package/dist/init/project-id.js +10 -22
- package/dist/init/run.d.ts +22 -0
- package/dist/init/run.js +393 -28
- package/dist/init/slash-command.d.ts +27 -0
- package/dist/init/slash-command.js +61 -0
- package/dist/init/snippets.d.ts +41 -3
- package/dist/init/snippets.js +185 -10
- package/dist/init/vite-config.d.ts +4 -10
- package/dist/init/vite-config.js +15 -7
- package/dist/init/workspace-apps.d.ts +23 -0
- package/dist/init/workspace-apps.js +68 -0
- package/dist/input/driven-redaction.d.ts +15 -0
- package/dist/input/driven-redaction.js +76 -0
- package/dist/input/network-detail.d.ts +9 -3
- package/dist/input/network-detail.js +43 -23
- 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 +58 -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 +20 -2
- package/dist/session/session.js +76 -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 +85 -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/cli-parse.d.ts +0 -140
- package/dist/daemon-resilience.d.ts +0 -22
- package/dist/daemon-resilience.js +0 -85
- package/dist/mcp-proxy.d.ts +0 -35
- package/dist/mcp-proxy.js +0 -190
- 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
|
@@ -29,7 +29,7 @@ export const CONTRACT_TOOLS = [
|
|
|
29
29
|
governance: z.unknown().optional(),
|
|
30
30
|
},
|
|
31
31
|
handler: async (deps, args) => {
|
|
32
|
-
if (args[FROM_DISK_ARG]
|
|
32
|
+
if (true === args[FROM_DISK_ARG]) {
|
|
33
33
|
const r = await readContract(deps.fs, deps.reticleRoot);
|
|
34
34
|
if (!r.ok)
|
|
35
35
|
throw new Error(r.reason === ContractReadError.MISSING
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which controls has the agent actually driven — across re-renders.
|
|
3
|
+
*
|
|
4
|
+
* Coverage was counted by REF, and a ref is invalidated whenever the DOM changes. Reported from a
|
|
5
|
+
* sweep of a Next app-router project: `total: 34, exercised: 0` after four successful acts. App
|
|
6
|
+
* router replaces a whole route segment on navigation, so every control the agent drove is a new
|
|
7
|
+
* element by the time coverage is asked. The number is then 0 forever on any framework that replaces
|
|
8
|
+
* nodes rather than reconciling in place — and an agent told it has exercised none of 34 controls
|
|
9
|
+
* re-drives ground it already covered, which is the exact opposite of what this tool is for.
|
|
10
|
+
*
|
|
11
|
+
* Bench-app hid it: it reconciles in place, so refs survive there and the number looked right.
|
|
12
|
+
*
|
|
13
|
+
* The LABEL is the identity that survives a re-render — `button "Deploy"` names the same control
|
|
14
|
+
* before and after — and it is already parsed for the untouched list.
|
|
15
|
+
*
|
|
16
|
+
* One caveat, stated because it is a real limitation: two controls sharing a label are
|
|
17
|
+
* indistinguishable here, so driving one marks both. That errs toward "covered", which is the
|
|
18
|
+
* unsafe direction for a coverage number — but it is bounded to same-labelled controls, and the
|
|
19
|
+
* alternative it replaces reported ZERO coverage on entire frameworks.
|
|
20
|
+
*/
|
|
21
|
+
interface CoverageControl {
|
|
22
|
+
ref: string;
|
|
23
|
+
label: string;
|
|
24
|
+
}
|
|
25
|
+
interface CoverageTally {
|
|
26
|
+
/** Controls on the page the agent has driven, by ref or by surviving label. */
|
|
27
|
+
exercised: number;
|
|
28
|
+
/** Controls the agent drove that are on the page no longer, and no label explains. */
|
|
29
|
+
droveGone: number;
|
|
30
|
+
/** The controls still untouched, in document order. */
|
|
31
|
+
untouched: CoverageControl[];
|
|
32
|
+
}
|
|
33
|
+
export declare function exercisedCount(controls: readonly CoverageControl[], actedRefs: ReadonlySet<string>, actedLabels: ReadonlySet<string>): CoverageTally;
|
|
34
|
+
export {};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which controls has the agent actually driven — across re-renders.
|
|
3
|
+
*
|
|
4
|
+
* Coverage was counted by REF, and a ref is invalidated whenever the DOM changes. Reported from a
|
|
5
|
+
* sweep of a Next app-router project: `total: 34, exercised: 0` after four successful acts. App
|
|
6
|
+
* router replaces a whole route segment on navigation, so every control the agent drove is a new
|
|
7
|
+
* element by the time coverage is asked. The number is then 0 forever on any framework that replaces
|
|
8
|
+
* nodes rather than reconciling in place — and an agent told it has exercised none of 34 controls
|
|
9
|
+
* re-drives ground it already covered, which is the exact opposite of what this tool is for.
|
|
10
|
+
*
|
|
11
|
+
* Bench-app hid it: it reconciles in place, so refs survive there and the number looked right.
|
|
12
|
+
*
|
|
13
|
+
* The LABEL is the identity that survives a re-render — `button "Deploy"` names the same control
|
|
14
|
+
* before and after — and it is already parsed for the untouched list.
|
|
15
|
+
*
|
|
16
|
+
* One caveat, stated because it is a real limitation: two controls sharing a label are
|
|
17
|
+
* indistinguishable here, so driving one marks both. That errs toward "covered", which is the
|
|
18
|
+
* unsafe direction for a coverage number — but it is bounded to same-labelled controls, and the
|
|
19
|
+
* alternative it replaces reported ZERO coverage on entire frameworks.
|
|
20
|
+
*/
|
|
21
|
+
export function exercisedCount(controls, actedRefs, actedLabels) {
|
|
22
|
+
// An empty label is not an identity — anonymous controls would otherwise collapse into one bucket
|
|
23
|
+
// and a single act would mark every one of them driven.
|
|
24
|
+
// Matched on the whole label (`button "Save"`) OR on a testid the label contains. The snapshot
|
|
25
|
+
// renders a control's testid inside its label, and a testid is the identity most likely to survive
|
|
26
|
+
// a re-render — matching only the full label meant a control with a testid but no accessible name
|
|
27
|
+
// was never recognised, and coverage read `exercised: 0` after real work.
|
|
28
|
+
const droveLabel = (label) => {
|
|
29
|
+
if (0 === label.length)
|
|
30
|
+
return false;
|
|
31
|
+
if (actedLabels.has(label))
|
|
32
|
+
return true;
|
|
33
|
+
for (const acted of actedLabels) {
|
|
34
|
+
if (acted.length > 0 && !acted.includes('"') && label.includes(acted))
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
return false;
|
|
38
|
+
};
|
|
39
|
+
const untouched = controls.filter((control) => !actedRefs.has(control.ref) && !droveLabel(control.label));
|
|
40
|
+
// A driven ref that is not on the page is only "gone" if no present control carries its label —
|
|
41
|
+
// otherwise it is the same control after a re-render, which is the case this exists to fix.
|
|
42
|
+
const presentRefs = new Set(controls.map((control) => control.ref));
|
|
43
|
+
const explainedByLabel = new Set(controls.filter((control) => droveLabel(control.label)).map((control) => control.label));
|
|
44
|
+
const droveGone = [...actedRefs].filter((ref) => !presentRefs.has(ref) && 0 === explainedByLabel.size).length;
|
|
45
|
+
return { exercised: controls.length - untouched.length, droveGone, untouched };
|
|
46
|
+
}
|
|
@@ -2,6 +2,7 @@ import { z } from 'zod';
|
|
|
2
2
|
import { ReticleCommand, SnapshotMode } from '@reticlehq/core';
|
|
3
3
|
import { ReticleTool } from './tool-names.js';
|
|
4
4
|
import { asString } from './tools-helpers.js';
|
|
5
|
+
import { exercisedCount } from './coverage-identity.js';
|
|
5
6
|
import { commandOrThrow, sessionIdShape } from './tool-kit.js';
|
|
6
7
|
/**
|
|
7
8
|
* `reticle_coverage` — which interactive controls this session has driven, and which it has not.
|
|
@@ -28,7 +29,7 @@ export function parseControls(tree) {
|
|
|
28
29
|
const seen = new Set();
|
|
29
30
|
for (const line of tree.split('\n')) {
|
|
30
31
|
const match = LINE_WITH_REF.exec(line);
|
|
31
|
-
if (
|
|
32
|
+
if (null === match)
|
|
32
33
|
continue;
|
|
33
34
|
const [, label, ref] = match;
|
|
34
35
|
if (label === undefined || ref === undefined || seen.has(ref))
|
|
@@ -71,21 +72,16 @@ export const COVERAGE_TOOLS = [
|
|
|
71
72
|
mode: SnapshotMode.INTERACTIVE,
|
|
72
73
|
});
|
|
73
74
|
const tree = asString(result['tree']) ?? '';
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
// against the actions that WORKED: archive, delete, submit and navigate all remove their own
|
|
80
|
-
// control, so clicking one and asking again reported `exercised: 0` — measured on the Electron
|
|
81
|
-
// demo right after a successful archive. An agent reading "0" concludes it has done nothing
|
|
82
|
-
// and re-drives ground it already covered.
|
|
83
|
-
const drovenGone = [...acted].filter((ref) => !present.has(ref)).length;
|
|
75
|
+
// Matched by ref AND by label — see coverage-identity. A ref dies with the next re-render, so
|
|
76
|
+
// on a framework that replaces nodes this reported `exercised: 0` however much work was done.
|
|
77
|
+
// `droveGone` keeps the other honesty: archive/delete/submit remove their own control, so a
|
|
78
|
+
// drive that WORKED must not read as no coverage at all.
|
|
79
|
+
const { exercised, droveGone, untouched } = exercisedCount(parseControls(tree), session.actedRefs(), session.actedLabels());
|
|
84
80
|
return {
|
|
85
|
-
total:
|
|
86
|
-
exercised
|
|
81
|
+
total: parseControls(tree).length,
|
|
82
|
+
exercised,
|
|
87
83
|
untouched,
|
|
88
|
-
...(
|
|
84
|
+
...(droveGone > 0 ? { alsoDroveGone: droveGone } : {}),
|
|
89
85
|
};
|
|
90
86
|
},
|
|
91
87
|
},
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { ToolDef } from './tools.js';
|
|
2
|
+
import { type ToolSurfaceOrigin } from './tool-surface.js';
|
|
2
3
|
/**
|
|
3
4
|
* Build the two dynamic meta-tools over the full tool table. `reticle_run` dispatches through the same
|
|
4
5
|
* `runTool` chokepoint as a direct call, so session-health splicing and every other invariant hold.
|
|
5
6
|
*/
|
|
6
|
-
export declare function buildDynamicTools(allTools: ToolDef[]): ToolDef[];
|
|
7
|
+
export declare function buildDynamicTools(allTools: ToolDef[], profile?: ToolSurfaceOrigin): ToolDef[];
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import { runTool } from './invoke-tool.js';
|
|
3
|
+
import { buildErrorPayload } from './error-recovery.js';
|
|
4
|
+
import { mergedNameRedirect, mergedNameMessage } from './merged-name-redirect.js';
|
|
3
5
|
import { ReticleTool } from './tool-names.js';
|
|
6
|
+
import { TOOL_PROFILE_ENV } from './tool-surface.js';
|
|
4
7
|
import { getSessionMetrics } from '../telemetry/session-metrics.js';
|
|
5
8
|
/**
|
|
6
9
|
* On-demand tool loading for MCP — the answer to the per-turn tool-definition tax.
|
|
@@ -36,12 +39,39 @@ function paramInfo(shape) {
|
|
|
36
39
|
description: schema.description ?? '',
|
|
37
40
|
}));
|
|
38
41
|
}
|
|
42
|
+
/**
|
|
43
|
+
* The one wording for "you named a parameter that does not exist", shared by both checks in
|
|
44
|
+
* `reticle_run` — the one over the WRAPPED tool's `args`, and the one over reticle_run's own
|
|
45
|
+
* top-level keys. Two spellings of the same refusal is how one of them ends up not existing.
|
|
46
|
+
*/
|
|
47
|
+
function unknownParamsError(toolName, unknown) {
|
|
48
|
+
return `unknown ${1 === unknown.length ? 'parameter' : 'parameters'} for ${toolName}: ${unknown.join(', ')} — NOT applied, so any result would be an answer to a different question`;
|
|
49
|
+
}
|
|
50
|
+
/** The keys not declared by `shape`, in call order. */
|
|
51
|
+
function unknownKeys(args, shape) {
|
|
52
|
+
const declared = new Set(Object.keys(shape));
|
|
53
|
+
return Object.keys(args).filter((key) => !declared.has(key));
|
|
54
|
+
}
|
|
39
55
|
/**
|
|
40
56
|
* Build the two dynamic meta-tools over the full tool table. `reticle_run` dispatches through the same
|
|
41
57
|
* `runTool` chokepoint as a direct call, so session-health splicing and every other invariant hold.
|
|
42
58
|
*/
|
|
43
|
-
export function buildDynamicTools(allTools) {
|
|
59
|
+
export function buildDynamicTools(allTools, profile) {
|
|
44
60
|
const byName = new Map(allTools.map((t) => [t.name, t]));
|
|
61
|
+
// The profile is a DAEMON-startup decision, so an agent that exported RETICLE_TOOL_PROFILE into its
|
|
62
|
+
// own environment sees no change and has, until now, no way to tell. Reported with the catalog.
|
|
63
|
+
const profileBlock = profile === undefined
|
|
64
|
+
? {}
|
|
65
|
+
: {
|
|
66
|
+
profile: {
|
|
67
|
+
...profile,
|
|
68
|
+
// NOT "every tool is reachable via reticle_run under any profile" — measured false:
|
|
69
|
+
// `full` advertises all 46 tools DIRECTLY and carries no meta-tools at all, so
|
|
70
|
+
// reticle_run does not exist there. Saying otherwise sends the agent to a tool the
|
|
71
|
+
// profile does not have.
|
|
72
|
+
note: `The profile is read once at daemon startup — change ${TOOL_PROFILE_ENV} and restart the daemon, or it has no effect. Under this profile every unadvertised tool is reachable through reticle_run; under \`full\` there are no meta-tools because every tool is advertised directly.`,
|
|
73
|
+
},
|
|
74
|
+
};
|
|
45
75
|
const reticleTools = {
|
|
46
76
|
name: ReticleTool.TOOLS,
|
|
47
77
|
description: 'Discover Reticle tools on demand. Call with no arguments to list every tool (name + one-line summary); call with names:["reticle_network", …] to load full descriptions and parameters for specific tools. Then invoke them with reticle_run. This avoids paying for every tool definition on every turn. To make a verification REUSABLE (record once, replay free forever), the flow workflow lives here: reticle_record{action:"start"} → act → reticle_flow_save → reticle_flow_verify (and reticle_flow_heal on drift). Load those names when you want to save or re-run a flow.',
|
|
@@ -53,11 +83,12 @@ export function buildDynamicTools(allTools) {
|
|
|
53
83
|
},
|
|
54
84
|
handler: (_deps, args) => {
|
|
55
85
|
const names = Array.isArray(args['names'])
|
|
56
|
-
? args['names'].filter((n) => typeof n
|
|
86
|
+
? args['names'].filter((n) => 'string' === typeof n)
|
|
57
87
|
: undefined;
|
|
58
|
-
if (names === undefined || names.length
|
|
88
|
+
if (names === undefined || 0 === names.length) {
|
|
59
89
|
return Promise.resolve({
|
|
60
90
|
tools: allTools.map((t) => ({ name: t.name, summary: firstSentence(t.description) })),
|
|
91
|
+
...profileBlock,
|
|
61
92
|
next: 'Load params with reticle_tools { names:[…] }, then call reticle_run { tool, args }.',
|
|
62
93
|
});
|
|
63
94
|
}
|
|
@@ -71,35 +102,76 @@ export function buildDynamicTools(allTools) {
|
|
|
71
102
|
});
|
|
72
103
|
},
|
|
73
104
|
};
|
|
105
|
+
const runShape = {
|
|
106
|
+
tool: z.string().describe('Tool name to invoke, e.g. reticle_network.'),
|
|
107
|
+
args: z.record(z.unknown()).optional().describe('Arguments object for that tool.'),
|
|
108
|
+
// Accepted AND FORWARDED, not merely tolerated. reticle_run is the only way to reach an
|
|
109
|
+
// unadvertised tool, so on a machine running several projects it has to be aimable — and
|
|
110
|
+
// `sessionId` is the shape an agent already uses on every other tool. Reported across 6 of 6
|
|
111
|
+
// apps: it took this key, dropped it, resolved by the daemon's cwd project, and failed with
|
|
112
|
+
// "no browser session for project X" while naming the very session it had been given.
|
|
113
|
+
sessionId: z
|
|
114
|
+
.string()
|
|
115
|
+
.optional()
|
|
116
|
+
.describe('Target tab for the invoked tool. Forwarded to it — an explicit args.sessionId wins.'),
|
|
117
|
+
};
|
|
74
118
|
const reticleRun = {
|
|
75
119
|
name: ReticleTool.RUN,
|
|
76
120
|
description: "Invoke any Reticle tool by name (discover names/params first with reticle_tools). On an unknown tool or bad arguments it returns the available names or the tool's params, so you can correct and retry.",
|
|
77
|
-
inputSchema:
|
|
78
|
-
tool: z.string().describe('Tool name to invoke, e.g. reticle_network.'),
|
|
79
|
-
args: z.record(z.unknown()).optional().describe('Arguments object for that tool.'),
|
|
80
|
-
},
|
|
121
|
+
inputSchema: runShape,
|
|
81
122
|
handler: async (deps, args) => {
|
|
82
|
-
|
|
83
|
-
|
|
123
|
+
// reticle_run's OWN parameters, checked first and with the same wording as the inner check.
|
|
124
|
+
// It refused an unknown key inside `args` and silently dropped one beside them: an agent that
|
|
125
|
+
// wrote `reticle_run { tool, args, sessionId }` — the shape it uses on every other tool — had
|
|
126
|
+
// its sessionId ignored and got a confident answer about whichever session auto-selection
|
|
127
|
+
// picked. Every other tool on the surface refuses this; the escape hatch has to as well.
|
|
128
|
+
const strayTop = unknownKeys(args, runShape);
|
|
129
|
+
if (strayTop.length > 0) {
|
|
130
|
+
return {
|
|
131
|
+
error: unknownParamsError(ReticleTool.RUN, strayTop),
|
|
132
|
+
hint: `pass them inside args: reticle_run { tool, args: { ${strayTop.join(', ')}: … } } if the target tool declares them`,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
const name = 'string' === typeof args['tool'] ? args['tool'] : '';
|
|
136
|
+
const given = 'object' === typeof args['args'] && args['args'] !== null
|
|
84
137
|
? args['args']
|
|
85
138
|
: {};
|
|
139
|
+
const aimed = args['sessionId'];
|
|
86
140
|
const target = byName.get(name);
|
|
87
141
|
if (target === undefined) {
|
|
88
142
|
// A non-zero count here means our advertised surface is confusing the agent — it reached for
|
|
89
143
|
// something it believed existed. That is a docs/naming defect, and it was invisible.
|
|
90
144
|
getSessionMetrics().recordUnknownTool();
|
|
145
|
+
// ...and for 22 of those names the capability DOES exist, it just moved when tools merged.
|
|
146
|
+
// Answering "unknown tool" there is simply wrong; see merged-name-redirect.
|
|
147
|
+
const moved = mergedNameRedirect(name);
|
|
148
|
+
if (moved !== undefined) {
|
|
149
|
+
return {
|
|
150
|
+
error: mergedNameMessage(name, moved),
|
|
151
|
+
tool: moved.tool,
|
|
152
|
+
...(moved.action === undefined ? {} : { action: moved.action }),
|
|
153
|
+
};
|
|
154
|
+
}
|
|
91
155
|
return { error: `unknown tool '${name}'`, available: allTools.map((t) => t.name) };
|
|
92
156
|
}
|
|
157
|
+
// The outer aim, forwarded ONLY to a tool that declares a session — injecting it into one that
|
|
158
|
+
// does not would trip the unknown-key check below and refuse a call the caller got right. An
|
|
159
|
+
// inner args.sessionId wins as the more specific instruction, and an absent aim stays absent so
|
|
160
|
+
// auto-selection still applies.
|
|
161
|
+
const callArgs = 'string' === typeof aimed &&
|
|
162
|
+
given['sessionId'] === undefined &&
|
|
163
|
+
undefined !== target.inputSchema['sessionId']
|
|
164
|
+
? { ...given, sessionId: aimed }
|
|
165
|
+
: given;
|
|
93
166
|
// The escape hatch is where a typo is MOST likely, because an unadvertised tool's parameters
|
|
94
167
|
// are not in front of the agent at all — and it is the one path the SDK's own validation never
|
|
95
168
|
// sees, since `reticle_run`'s own args (`tool`, `args`) are perfectly valid. Left unchecked,
|
|
96
169
|
// `reticle_run { tool: "reticle_clock", args: { action: "freeze" } }` returned
|
|
97
170
|
// `{"frozen":false}`: a well-formed answer to a question nobody asked.
|
|
98
|
-
const
|
|
99
|
-
const unknown = Object.keys(callArgs).filter((key) => !declared.has(key));
|
|
171
|
+
const unknown = unknownKeys(callArgs, target.inputSchema);
|
|
100
172
|
if (unknown.length > 0) {
|
|
101
173
|
return {
|
|
102
|
-
error:
|
|
174
|
+
error: unknownParamsError(name, unknown),
|
|
103
175
|
tool: name,
|
|
104
176
|
params: paramInfo(target.inputSchema),
|
|
105
177
|
...(target.example === undefined ? {} : { example: target.example }),
|
|
@@ -110,12 +182,12 @@ export function buildDynamicTools(allTools) {
|
|
|
110
182
|
return await runTool(target, deps, callArgs);
|
|
111
183
|
}
|
|
112
184
|
catch (error) {
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
};
|
|
185
|
+
// The SAME payload the direct tool path builds — recovery included. Answering every failure
|
|
186
|
+
// with "fix the arguments" threw that away, and under the default profile nearly every tool
|
|
187
|
+
// is reached through here: a stale ref, a paused session, a missing pairing token all came
|
|
188
|
+
// back as the agent's arguments being wrong, which is advice that spends the retry.
|
|
189
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
190
|
+
return { ...buildErrorPayload(message), tool: name };
|
|
119
191
|
}
|
|
120
192
|
},
|
|
121
193
|
};
|
|
@@ -17,10 +17,28 @@ export declare const RECOVERY: {
|
|
|
17
17
|
readonly MISSING_BASELINE: string;
|
|
18
18
|
readonly MISSING_RECORDING: string;
|
|
19
19
|
readonly STALE_REF: string;
|
|
20
|
+
readonly INVALID_NAME: string;
|
|
21
|
+
readonly NO_SUCH_OPTION: string;
|
|
20
22
|
readonly BAD_ARGUMENTS: string;
|
|
23
|
+
readonly WRONG_TARGET: string;
|
|
24
|
+
readonly NOT_EDITABLE: string;
|
|
25
|
+
readonly CONFIRM_DANGEROUS: string;
|
|
26
|
+
readonly UNSUPPORTED_SURFACE: string;
|
|
21
27
|
readonly TOKEN_REQUIRED: string;
|
|
28
|
+
readonly SCOPE_MISMATCH: string;
|
|
29
|
+
readonly SESSION_GONE: string;
|
|
30
|
+
readonly COMMAND_TIMEOUT: string;
|
|
31
|
+
readonly FLOW_STEP_MISSING: string;
|
|
32
|
+
readonly NO_POOL: string;
|
|
22
33
|
};
|
|
23
|
-
/**
|
|
34
|
+
/**
|
|
35
|
+
* The actionable next move for a known error message, or undefined when none is recognized.
|
|
36
|
+
*
|
|
37
|
+
* A message that already carries its own next action gets NO second one. The no-session diagnosis
|
|
38
|
+
* inspects the machine and says which of three causes this actually is; pairing that with the
|
|
39
|
+
* generic "ask the human to start their app" produced a result that contradicted itself — the error
|
|
40
|
+
* saying a server is running and the recovery saying to go start one.
|
|
41
|
+
*/
|
|
24
42
|
export declare function recoveryFor(message: string): string | undefined;
|
|
25
43
|
/**
|
|
26
44
|
* The ask attached to errors we do NOT recognize. An unrecognized error is, by definition, the case
|
|
@@ -43,5 +61,5 @@ interface ErrorPayload {
|
|
|
43
61
|
* known; the feedback ask is added when it is NOT — the two are mutually exclusive by design, so the
|
|
44
62
|
* agent always gets exactly one next move.
|
|
45
63
|
*/
|
|
46
|
-
export declare function buildErrorPayload(
|
|
64
|
+
export declare function buildErrorPayload(rawMessage: string): ErrorPayload;
|
|
47
65
|
export {};
|
|
@@ -8,6 +8,45 @@
|
|
|
8
8
|
* on stable, human-authored substrings of the thrown messages (see session-manager.ts, the flow/
|
|
9
9
|
* baseline stores). No clock, no IO — unit-testable in isolation.
|
|
10
10
|
*/
|
|
11
|
+
import { TRANSPORT_LIMITS } from '@reticlehq/core';
|
|
12
|
+
import { SELF_RECOVERING_MARKER } from '../session/no-session-diagnosis.js';
|
|
13
|
+
/** Marks where an over-long message had its middle removed. Named so tests assert it by value. */
|
|
14
|
+
const ELISION = '… [elided] …';
|
|
15
|
+
/**
|
|
16
|
+
* Bound a message before the agent ever sees it, keeping BOTH ends.
|
|
17
|
+
*
|
|
18
|
+
* Error messages interpolate caller-supplied values — a ref, a baseline name, a selector — and
|
|
19
|
+
* nothing upstream bounds them, so a 100KB argument came back as a 100KB tool result. Capping here
|
|
20
|
+
* rather than at each throw site is deliberate: this is the single funnel every tool error passes
|
|
21
|
+
* through, so it also covers the tools nobody has written yet.
|
|
22
|
+
*
|
|
23
|
+
* The middle is what goes. Head-truncation would be simpler and wrong: the recovery table keys off
|
|
24
|
+
* substrings that sit at the END of the message ("… no longer resolves to an element"), so cutting
|
|
25
|
+
* the tail severs the part that makes an error recognizable and re-labels a known condition as a
|
|
26
|
+
* possible Reticle defect.
|
|
27
|
+
*/
|
|
28
|
+
function capMessage(message) {
|
|
29
|
+
const max = TRANSPORT_LIMITS.MAX_ERROR_LENGTH;
|
|
30
|
+
if (max >= message.length)
|
|
31
|
+
return message;
|
|
32
|
+
const keep = Math.floor((max - ELISION.length) / 2);
|
|
33
|
+
return `${message.slice(0, keep)}${ELISION}${message.slice(message.length - keep)}`;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Messages that already spell out the exact retry, so any hint we add can only argue with them.
|
|
37
|
+
*
|
|
38
|
+
* The no-session diagnosis is one (it inspected the machine and named the cause). The native-input
|
|
39
|
+
* refusal is the other: it names the tool, the argument and the follow-up call to make instead — and
|
|
40
|
+
* it was still collecting the defect ask, inviting a bug report about a refusal that had just told
|
|
41
|
+
* the agent precisely what to do.
|
|
42
|
+
*/
|
|
43
|
+
const SELF_RECOVERING = [/cannot drive native input/i];
|
|
44
|
+
/** A message that already carries its own concrete next action, so nothing should be appended. */
|
|
45
|
+
function isSelfRecovering(message) {
|
|
46
|
+
if (message.includes(SELF_RECOVERING_MARKER))
|
|
47
|
+
return true;
|
|
48
|
+
return SELF_RECOVERING.some((pattern) => pattern.test(message));
|
|
49
|
+
}
|
|
11
50
|
/** The recovery hints, named so they are not free strings and can be asserted in tests. */
|
|
12
51
|
export const RECOVERY = {
|
|
13
52
|
NO_SESSION: 'No app is connected to Reticle. Ask the human to start their app in dev with @reticlehq/browser ' +
|
|
@@ -21,18 +60,71 @@ export const RECOVERY = {
|
|
|
21
60
|
'the tab to the front, or run `reticle drive <url>` for a guaranteed scriptable context.',
|
|
22
61
|
MISSING_BASELINE: 'That baseline does not exist yet. Call reticle_baseline { action: "list" } to see saved names, or ' +
|
|
23
62
|
'reticle_baseline { action: "save", name } to capture one before diffing against it.',
|
|
24
|
-
MISSING_RECORDING:
|
|
25
|
-
|
|
63
|
+
MISSING_RECORDING:
|
|
64
|
+
// `recordingName`, not `name`. The hint said `name`, so an agent that followed it exactly got
|
|
65
|
+
// InvalidParams — a recovery that fails is worse than none, because it spends the retry.
|
|
66
|
+
'No recording by that name is in progress. Start one with ' +
|
|
67
|
+
'reticle_record { action: "start", recordingName } before annotating, stopping, or saving it.',
|
|
26
68
|
STALE_REF: 'That ref is stale: refs are invalidated whenever the DOM re-renders, so any action that ' +
|
|
27
69
|
'navigated, opened a modal, re-sorted a list or changed the page invalidates every ref taken ' +
|
|
28
70
|
'before it. Reticle refuses here rather than clicking whatever now occupies that slot. Call ' +
|
|
29
71
|
'reticle_query again for a fresh ref and retry the action — and prefer reticle_act_and_wait ' +
|
|
30
72
|
'{ until } when an action changes the page, so the next ref is taken after it settles.',
|
|
73
|
+
INVALID_NAME: 'That name is not a usable one. A flow, baseline or recording name must be a single safe path ' +
|
|
74
|
+
'segment — letters, digits, dash and underscore only, starting with a letter or digit, at most ' +
|
|
75
|
+
'64 characters. No slashes, no "..", no leading dot: the name becomes a filename under ' +
|
|
76
|
+
'.reticle/, so anything that could escape that directory is refused. Retry with a slug like ' +
|
|
77
|
+
'"checkout-happy-path". This is an invalid call, not a Reticle defect: there is nothing to report.',
|
|
78
|
+
NO_SUCH_OPTION: 'That <select> has no option with the value you asked for, and the message above lists the ones ' +
|
|
79
|
+
'it does have. Reticle refuses rather than assigning it: an unmatched value deselects everything, ' +
|
|
80
|
+
'and the change event that follows has been observed persisting an empty setting into the app. ' +
|
|
81
|
+
'Retry with one of the listed values — note they are option VALUES, which often differ from the ' +
|
|
82
|
+
'visible labels shown in brackets. This is an invalid call, not a Reticle defect.',
|
|
31
83
|
BAD_ARGUMENTS: "That call did not match the tool's schema — the message above names the tool and the exact " +
|
|
32
84
|
"argument it wanted. Re-read that tool's parameters and retry with the missing or corrected " +
|
|
33
85
|
'argument. This is an invalid call, not a Reticle defect: there is nothing to report.',
|
|
86
|
+
WRONG_TARGET: 'That action does not apply to this element — the ref resolved, the element just cannot take it ' +
|
|
87
|
+
'(you cannot fill a <button>, submit outside a form, or upload to anything but <input ' +
|
|
88
|
+
'type="file">). Call reticle_snapshot or reticle_query to find the control that does, or ' +
|
|
89
|
+
'reticle_inspect on this ref to see what it actually is. This is an invalid call, not a Reticle ' +
|
|
90
|
+
'defect: there is nothing to report.',
|
|
91
|
+
NOT_EDITABLE: 'The field is disabled or readonly, so a user could not edit it either — forcing the value would ' +
|
|
92
|
+
'put the app in a state nobody can reach, which is why Reticle refuses. This is usually the app ' +
|
|
93
|
+
'telling you something: drive whatever ENABLES the field (open the editor, satisfy the ' +
|
|
94
|
+
'precondition) and act again. If the field should have been editable, that is a bug in the app, ' +
|
|
95
|
+
'not in Reticle.',
|
|
96
|
+
CONFIRM_DANGEROUS: 'Reticle blocked a potentially destructive control (delete, remove, revoke…) on purpose. If you ' +
|
|
97
|
+
'mean it, retry the same action with args.confirmDangerous set to true. This is a deliberate ' +
|
|
98
|
+
'refusal, not a defect: there is nothing to report.',
|
|
99
|
+
UNSUPPORTED_SURFACE: 'That surface is not supported yet, and Reticle refuses rather than pretending — a rich-text ' +
|
|
100
|
+
'editor keeps its own document model, so writing to the DOM would look right and submit the old ' +
|
|
101
|
+
'content. Drive the same outcome another way (a plain input, a command, a keyboard action) or ' +
|
|
102
|
+
'assert on the result instead of typing it. Known gap, already reported: no need to file it.',
|
|
34
103
|
TOKEN_REQUIRED: 'The bridge binds beyond localhost and requires a pairing token. Set the same token in the SDK ' +
|
|
35
104
|
'init (@reticlehq/core) and the Reticle server config, then reconnect.',
|
|
105
|
+
SCOPE_MISMATCH: 'A tab IS connected — it just belongs to a different project than the daemon is scoped to, so ' +
|
|
106
|
+
'nothing is wrong with the app and there is nothing to report. The message above lists each ' +
|
|
107
|
+
'connected session with its sessionId: pass that sessionId to this tool to target it, or ' +
|
|
108
|
+
"restart the daemon from that app's directory so it becomes the default. Call reticle_sessions " +
|
|
109
|
+
'for the same list at any time.',
|
|
110
|
+
SESSION_GONE: 'The tab this call was targeting disconnected while the command was in flight, so the command ' +
|
|
111
|
+
'never completed — do not assume it applied or that it did not. Call reticle_sessions: a tab ' +
|
|
112
|
+
'that reloaded comes back under a NEW sessionId (every ref taken before it is gone — re-query), ' +
|
|
113
|
+
'and an empty list means the page was closed and the human has to reopen it.',
|
|
114
|
+
COMMAND_TIMEOUT: 'The page did not answer within the command window. That is a fact about the page, not a Reticle ' +
|
|
115
|
+
'failure: check reticle_sessions for `throttled`/`stale` on this session — a backgrounded tab is ' +
|
|
116
|
+
'throttled and may never answer, so ask the human to bring it to the front or drive your own ' +
|
|
117
|
+
'browser with reticle_lease. If the tab is in front, the page is busy or blocked (a long ' +
|
|
118
|
+
'synchronous task, an alert/confirm dialog); reticle_console usually shows what it hit. Retry ' +
|
|
119
|
+
'once the cause is addressed.',
|
|
120
|
+
FLOW_STEP_MISSING: 'A recorded step points at an element that is not on the page now. Either the app changed under ' +
|
|
121
|
+
'the flow — run reticle_flow_heal to re-anchor it, then reticle_flow_verify — or an earlier step ' +
|
|
122
|
+
'did not do what it used to, leaving the run on the wrong page: check reticle_snapshot to see ' +
|
|
123
|
+
'where it actually is. This is app/flow drift, which is exactly what the flow exists to catch.',
|
|
124
|
+
NO_POOL: 'Reticle cannot launch its own browser here: the daemon-managed pool is absent, or Playwright ' +
|
|
125
|
+
'has no Chromium installed (the message above says which). Run Reticle via `reticle mcp`, and ' +
|
|
126
|
+
'ask the human for `npx playwright install chromium` if that is what is missing. Meanwhile drive ' +
|
|
127
|
+
'a tab the human already has open — reticle_sessions lists them.',
|
|
36
128
|
};
|
|
37
129
|
/** Ordered match rules; the first hit wins. Substrings track the thrown messages they recover. */
|
|
38
130
|
const RULES = [
|
|
@@ -43,9 +135,46 @@ const RULES = [
|
|
|
43
135
|
{ match: /no baseline named/i, hint: RECOVERY.MISSING_BASELINE },
|
|
44
136
|
{ match: /no (?:active|compiled) recording named/i, hint: RECOVERY.MISSING_RECORDING },
|
|
45
137
|
{ match: /pairing token is required/i, hint: RECOVERY.TOKEN_REQUIRED },
|
|
138
|
+
// Three conditions the daemon understands perfectly and still asked for a bug report about. Each
|
|
139
|
+
// needs its own rule: none of them contains "no browser session connected" (the scope miss is
|
|
140
|
+
// "no browser session FOR project 'x'", which is the opposite claim — sessions exist).
|
|
141
|
+
{
|
|
142
|
+
match: /scope mismatch|session\(s\) ARE connected under a different project/i,
|
|
143
|
+
hint: RECOVERY.SCOPE_MISMATCH,
|
|
144
|
+
},
|
|
145
|
+
{ match: /^session disconnected$|session .* never connected/i, hint: RECOVERY.SESSION_GONE },
|
|
146
|
+
{ match: /^command '[^']*' timed out after/i, hint: RECOVERY.COMMAND_TIMEOUT },
|
|
147
|
+
// Flow replay: the recorded anchor is gone. Ordinary drift, and the tool that fixes it exists.
|
|
148
|
+
{ match: /did not resolve in current page/i, hint: RECOVERY.FLOW_STEP_MISSING },
|
|
149
|
+
{
|
|
150
|
+
match: /browser pool (?:unavailable|is shut down)|Chromium is not installed/i,
|
|
151
|
+
hint: RECOVERY.NO_POOL,
|
|
152
|
+
},
|
|
46
153
|
// The commonest post-action condition there is. Unmatched, it fell through to FEEDBACK_ASK and
|
|
47
154
|
// told the agent a successful click's aftermath might be a bug in Reticle.
|
|
48
155
|
{ match: /no longer resolves to an element/i, hint: RECOVERY.STALE_REF },
|
|
156
|
+
{ match: /no <option> with value/i, hint: RECOVERY.NO_SUCH_OPTION },
|
|
157
|
+
// Every guard in the browser's executeAction. These are Reticle-authored refusals of an invalid
|
|
158
|
+
// call, and ALL of them used to fall through to the feedback ask — including the destructive-action
|
|
159
|
+
// block, which carries its own retry instruction and was still telling the agent Reticle might be
|
|
160
|
+
// broken. Order matters: contenteditable and disabled/readonly are `cannot <verb> …` messages too,
|
|
161
|
+
// so they must be tested before the general wrong-target rule.
|
|
162
|
+
{ match: /contenteditable/i, hint: RECOVERY.UNSUPPORTED_SURFACE },
|
|
163
|
+
{ match: /cannot \w+ a (disabled|readonly) </i, hint: RECOVERY.NOT_EDITABLE },
|
|
164
|
+
// Three spellings ship — "action", "native action", "WebMCP tool" — and the rule matched one, so
|
|
165
|
+
// two thirds of the same deliberate refusal still read as a possible defect.
|
|
166
|
+
{
|
|
167
|
+
match: /potentially destructive (?:\w+ )*(?:action|tool) blocked/i,
|
|
168
|
+
hint: RECOVERY.CONFIRM_DANGEROUS,
|
|
169
|
+
},
|
|
170
|
+
{
|
|
171
|
+
match: /^cannot [\w()]+ (?:a|an|into a|on a) <|no form to submit|upload target must be|is not an HTMLElement/i,
|
|
172
|
+
hint: RECOVERY.WRONG_TARGET,
|
|
173
|
+
},
|
|
174
|
+
// Authored by Reticle, about the caller's arguments: the message already names the valid answers.
|
|
175
|
+
{ match: /^unknown action '/i, hint: RECOVERY.BAD_ARGUMENTS },
|
|
176
|
+
{ match: /^unsupported query strategy '/i, hint: RECOVERY.BAD_ARGUMENTS },
|
|
177
|
+
{ match: /^\w+ requires a string `\w+`/i, hint: RECOVERY.BAD_ARGUMENTS },
|
|
49
178
|
// A message naming a `reticle_*` tool is one WE authored about our own API — an invalid call, not
|
|
50
179
|
// an unanticipated failure. Left unmatched it collected the feedback ask, which pushed agents to
|
|
51
180
|
// file reports about their own bad arguments. Kept LAST so a specific rule above always wins.
|
|
@@ -54,14 +183,42 @@ const RULES = [
|
|
|
54
183
|
hint: RECOVERY.BAD_ARGUMENTS,
|
|
55
184
|
},
|
|
56
185
|
];
|
|
57
|
-
/**
|
|
186
|
+
/**
|
|
187
|
+
* The actionable next move for a known error message, or undefined when none is recognized.
|
|
188
|
+
*
|
|
189
|
+
* A message that already carries its own next action gets NO second one. The no-session diagnosis
|
|
190
|
+
* inspects the machine and says which of three causes this actually is; pairing that with the
|
|
191
|
+
* generic "ask the human to start their app" produced a result that contradicted itself — the error
|
|
192
|
+
* saying a server is running and the recovery saying to go start one.
|
|
193
|
+
*/
|
|
58
194
|
export function recoveryFor(message) {
|
|
195
|
+
if (isSelfRecovering(message))
|
|
196
|
+
return undefined;
|
|
59
197
|
for (const rule of RULES) {
|
|
60
198
|
if (rule.match.test(message))
|
|
61
199
|
return rule.hint;
|
|
62
200
|
}
|
|
63
201
|
return undefined;
|
|
64
202
|
}
|
|
203
|
+
/**
|
|
204
|
+
* A schema rejection: the call named a parameter that does not exist, or gave one the wrong type.
|
|
205
|
+
*
|
|
206
|
+
* Matched by the SHAPES the validators actually produce (zod's codes, and the MCP layer's wrapper),
|
|
207
|
+
* because these are our own machinery talking, not the app.
|
|
208
|
+
*/
|
|
209
|
+
const ARGUMENT_REJECTION = /Unrecognized key|unrecognized_keys|invalid_type|Invalid arguments for tool|Unknown parameters? for|^Required |Expected \w+, received/i;
|
|
210
|
+
/**
|
|
211
|
+
* What to say instead of asking for a bug report. The schema already stated the problem precisely;
|
|
212
|
+
* this only says who can fix it and where to look.
|
|
213
|
+
*/
|
|
214
|
+
const ARGUMENT_RECOVERY = "That call did not match the tool's schema — the message above names the parameter. Nothing ran, " +
|
|
215
|
+
'so no result is affected. Call reticle_tools { names: ["<tool>"] } for its exact parameters and ' +
|
|
216
|
+
'retry.';
|
|
217
|
+
/**
|
|
218
|
+
* `invalid <thing> name: <value>` — the shape every name validator in the server throws. Matched on
|
|
219
|
+
* the phrasing rather than per-caller so a new store that validates a name is covered by default.
|
|
220
|
+
*/
|
|
221
|
+
const INVALID_NAME_REJECTION = /^invalid (?:[a-z]+ )*name: /i;
|
|
65
222
|
/**
|
|
66
223
|
* The ask attached to errors we do NOT recognize. An unrecognized error is, by definition, the case
|
|
67
224
|
* where Reticle failed in a way nobody anticipated — the single highest-value moment to hear from the
|
|
@@ -79,7 +236,25 @@ export const FEEDBACK_ASK = 'This error is not one Reticle recognizes, which mea
|
|
|
79
236
|
* known; the feedback ask is added when it is NOT — the two are mutually exclusive by design, so the
|
|
80
237
|
* agent always gets exactly one next move.
|
|
81
238
|
*/
|
|
82
|
-
export function buildErrorPayload(
|
|
239
|
+
export function buildErrorPayload(rawMessage) {
|
|
240
|
+
// Cap FIRST, then classify on the capped text — so what the classifier reads is exactly what the
|
|
241
|
+
// agent reads. Classifying the full message and emitting a shorter one would let a recovery hint
|
|
242
|
+
// reference a phrase that is no longer there.
|
|
243
|
+
const message = capMessage(rawMessage);
|
|
244
|
+
// A self-diagnosing message gets NEITHER a generic recovery nor the feedback ask. It already
|
|
245
|
+
// inspected the machine and named the cause; a second, contradictory hint is noise, and inviting a
|
|
246
|
+
// bug report about a condition Reticle diagnosed itself is exactly backwards.
|
|
247
|
+
if (isSelfRecovering(message))
|
|
248
|
+
return { error: message };
|
|
249
|
+
// An argument rejection is the agent's to fix, not a Reticle defect. Reported from the field: a
|
|
250
|
+
// clean `unrecognized_keys: ["value"]` came back wrapped in "may be a defect in Reticle", which
|
|
251
|
+
// spends the agent's turn and fills the feedback channel with reports about malformed calls.
|
|
252
|
+
if (ARGUMENT_REJECTION.test(message))
|
|
253
|
+
return { error: message, recovery: ARGUMENT_RECOVERY };
|
|
254
|
+
// Reticle's own name validators rejecting a caller's value: an argument rejection that the schema
|
|
255
|
+
// cannot express (the shape is a runtime pattern, not a type), so it needs its own arm.
|
|
256
|
+
if (INVALID_NAME_REJECTION.test(message))
|
|
257
|
+
return { error: message, recovery: RECOVERY.INVALID_NAME };
|
|
83
258
|
const recovery = recoveryFor(message);
|
|
84
259
|
return recovery !== undefined
|
|
85
260
|
? { error: message, recovery }
|
|
@@ -12,7 +12,18 @@ export declare const FEEDBACK_PROMPT: {
|
|
|
12
12
|
readonly ask: "Ask the human, in your own words and only once: how did this verification go, and what would have made Reticle more useful? If they answer, file it with `reticle feedback` (they run it) or relay it yourself.";
|
|
13
13
|
readonly command: "reticle feedback";
|
|
14
14
|
};
|
|
15
|
-
/**
|
|
15
|
+
/**
|
|
16
|
+
* Tools whose completion means "a verification just happened" — the only moment worth asking at,
|
|
17
|
+
* and the set that decides what `verification_completed` counts.
|
|
18
|
+
*
|
|
19
|
+
* ACT_AND_WAIT belongs here and was missing for a long time. It declares `verified` in its
|
|
20
|
+
* outputSchema, returns the whole verdict block, and its own description calls it "one hop for the
|
|
21
|
+
* act->observe->assert loop" — it IS the verification path most agents take. The guard that should
|
|
22
|
+
* have caught the omission keyed on the tool NAME (/assert|verify/), which act_and_wait does not
|
|
23
|
+
* match, so it slipped through in silence. Measured over a day of real telemetry: act_and_wait 14
|
|
24
|
+
* calls, assert ZERO, verification_completed 2. Agents were verifying the entire time and the metric
|
|
25
|
+
* could not see it. The contract test now checks the SHAPE instead.
|
|
26
|
+
*/
|
|
16
27
|
export declare const VERIFICATION_TOOLS: ReadonlySet<string>;
|
|
17
28
|
/**
|
|
18
29
|
* The human prompt, when one is due. Resettable for tests.
|