@reticlehq/server 3.8.2 → 3.10.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/LICENSE +201 -104
- package/NOTICE +5 -0
- package/README.md +1 -1
- package/SKILL.md +13 -12
- package/bin/launch.js +13 -0
- package/bin/package.json +3 -0
- package/bin/reticle.js +66 -8
- package/dist/chromium-launch-options.d.ts +25 -0
- package/dist/chromium-launch-options.js +21 -1
- package/dist/command/cli/adhoc-suite.js +2 -0
- package/dist/command/cli/adhoc-verdict.d.ts +2 -43
- package/dist/command/cli/adhoc-verdict.js +50 -160
- package/dist/command/cli/answers/gate-exit.d.ts +6 -2
- package/dist/command/cli/answers/gate-exit.js +12 -2
- package/dist/command/cli/answers/mcp-connection-hint.d.ts +2 -0
- package/dist/command/cli/answers/mcp-connection-hint.js +6 -0
- package/dist/command/cli/ci-hint.d.ts +44 -0
- package/dist/command/cli/ci-hint.js +72 -0
- package/dist/command/cli/cli-doctor.d.ts +6 -1
- package/dist/command/cli/cli-doctor.js +46 -5
- package/dist/command/cli/cli-flow-commands.d.ts +7 -1
- package/dist/command/cli/cli-flow-commands.js +39 -24
- package/dist/command/cli/cli-kill.d.ts +21 -0
- package/dist/command/cli/cli-kill.js +47 -4
- package/dist/command/cli/cli-parse-compare.d.ts +22 -0
- package/dist/command/cli/cli-parse-compare.js +75 -0
- package/dist/command/cli/cli-parse-grade.d.ts +36 -0
- package/dist/command/cli/cli-parse-grade.js +75 -0
- package/dist/command/cli/cli-parse-grammar.d.ts +2 -0
- package/dist/command/cli/cli-parse-grammar.js +18 -20
- package/dist/command/cli/cli-parse-verify.d.ts +10 -0
- package/dist/command/cli/cli-parse-verify.js +63 -14
- package/dist/command/cli/cli-parse.d.ts +22 -17
- package/dist/command/cli/cli-parse.js +54 -32
- package/dist/command/cli/cli-update-commands.d.ts +1 -1
- package/dist/command/cli/cli-update-commands.js +29 -20
- package/dist/command/cli/cli-usage.js +61 -31
- package/dist/command/cli/cli-verify.d.ts +17 -3
- package/dist/command/cli/cli-verify.js +59 -21
- package/dist/command/cli/cloud-cli.js +2 -119
- package/dist/command/cli/daemon-link/daemon-client.d.ts +45 -0
- package/dist/command/cli/daemon-link/daemon-client.js +205 -0
- package/dist/command/cli/doctor/browser/chromium-hint.d.ts +6 -1
- package/dist/command/cli/doctor/browser/chromium-hint.js +11 -5
- package/dist/command/cli/doctor/doctor-daemon-line.js +6 -3
- package/dist/command/cli/doctor/doctor-fix.d.ts +44 -0
- package/dist/command/cli/doctor/doctor-fix.js +89 -0
- package/dist/command/cli/doctor/doctor-launcher-line.d.ts +23 -0
- package/dist/command/cli/doctor/doctor-launcher-line.js +60 -0
- package/dist/command/cli/doctor/doctor-rows.d.ts +4 -0
- package/dist/command/cli/doctor/doctor-rows.js +4 -0
- package/dist/command/cli/doctor-repairs.d.ts +64 -0
- package/dist/command/cli/doctor-repairs.js +214 -0
- package/dist/command/cli/drive/drive-command.js +3 -7
- package/dist/command/cli/ensure-daemon.d.ts +26 -0
- package/dist/command/cli/ensure-daemon.js +57 -0
- package/dist/command/cli/finished-run/grade-command.d.ts +23 -0
- package/dist/command/cli/finished-run/grade-command.js +180 -0
- package/dist/command/cli/first-use-checks.d.ts +41 -0
- package/dist/command/cli/first-use-checks.js +127 -0
- package/dist/command/cli/first-verdict-node.d.ts +3 -0
- package/dist/command/cli/first-verdict-node.js +189 -0
- package/dist/command/cli/first-verdict.d.ts +126 -0
- package/dist/command/cli/first-verdict.js +267 -0
- package/dist/command/cli/launch/sdk-version-match.d.ts +3 -2
- package/dist/command/cli/launch/sdk-version-match.js +3 -2
- package/dist/command/cli/lifecycle/daemon-lifecycle.d.ts +1 -1
- package/dist/command/cli/lifecycle/daemon-lifecycle.js +7 -6
- package/dist/command/cli/mcp-command.js +13 -4
- package/dist/command/cli/ports/port-holder.d.ts +11 -0
- package/dist/command/cli/ports/port-holder.js +36 -4
- package/dist/command/cli/setup-mcp-cli.d.ts +27 -1
- package/dist/command/cli/setup-mcp-cli.js +98 -23
- package/dist/command/cli/status/status-lines.d.ts +2 -0
- package/dist/command/cli/status/status-lines.js +72 -17
- package/dist/command/cli/status-fields.d.ts +5 -1
- package/dist/command/cli/status-fields.js +24 -6
- package/dist/command/cli/try-command.d.ts +7 -0
- package/dist/command/cli/try-command.js +11 -3
- package/dist/command/cli/two-builds/compare-command.d.ts +48 -0
- package/dist/command/cli/two-builds/compare-command.js +314 -0
- package/dist/command/cli/verify-against.d.ts +61 -0
- package/dist/command/cli/verify-against.js +187 -0
- package/dist/command/cli/verify-no-session.d.ts +2 -0
- package/dist/command/cli/verify-no-session.js +13 -0
- package/dist/command/cli/verify-results.d.ts +35 -2
- package/dist/command/cli/verify-results.js +76 -10
- package/dist/command/cli.js +112 -70
- package/dist/command/daemon/binding/daemon-status-probe.d.ts +3 -1
- package/dist/command/daemon/binding/daemon-status-probe.js +4 -2
- package/dist/command/daemon/daemon-argv.d.ts +9 -0
- package/dist/command/daemon/daemon-argv.js +11 -0
- package/dist/command/daemon/daemon-resolve.d.ts +8 -1
- package/dist/command/daemon/daemon-resolve.js +27 -1
- package/dist/command/daemon/daemon.d.ts +22 -4
- package/dist/command/daemon/daemon.js +52 -10
- package/dist/command/daemon/lifetime/daemon-exit.d.ts +24 -0
- package/dist/command/daemon/lifetime/daemon-exit.js +39 -0
- package/dist/command/daemon/lifetime/death-cause.d.ts +15 -0
- package/dist/command/daemon/lifetime/death-cause.js +149 -0
- package/dist/command/daemon/lifetime/heartbeat.d.ts +7 -15
- package/dist/command/daemon/lifetime/heartbeat.js +11 -15
- package/dist/command/daemon/lifetime/startup-failure.d.ts +29 -0
- package/dist/command/daemon/lifetime/startup-failure.js +62 -0
- package/dist/command/daemon/port-state.d.ts +14 -0
- package/dist/command/daemon/port-state.js +31 -0
- package/dist/command/setup/agent-configs.d.ts +2 -0
- package/dist/command/setup/agent-configs.js +42 -15
- package/dist/command/setup/agent-next-steps.d.ts +77 -0
- package/dist/command/setup/agent-next-steps.js +284 -0
- package/dist/command/setup/agent-writer.js +11 -6
- package/dist/command/setup/bringup/dev-server-files.d.ts +9 -0
- package/dist/command/setup/bringup/dev-server-files.js +17 -0
- package/dist/command/setup/bringup/static-page-server.d.ts +14 -2
- package/dist/command/setup/bringup/static-page-server.js +43 -9
- package/dist/command/setup/init/init-host.js +2 -0
- package/dist/command/setup/init/init-runtime.d.ts +6 -1
- package/dist/command/setup/init/init-runtime.js +63 -21
- package/dist/command/setup/mcp-launcher.d.ts +3 -0
- package/dist/command/setup/mcp-launcher.js +11 -0
- package/dist/command/setup/node-effects.d.ts +3 -2
- package/dist/command/setup/node-effects.js +9 -3
- package/dist/command/setup/remaining-steps.d.ts +8 -0
- package/dist/command/setup/remaining-steps.js +10 -0
- package/dist/command/setup/run-setup.d.ts +2 -0
- package/dist/command/setup/run-setup.js +9 -1
- package/dist/command/setup/setup-command.js +36 -15
- package/dist/command/setup/setup-mcp.d.ts +8 -0
- package/dist/command/setup/setup-mcp.js +47 -20
- package/dist/command/setup/start-dev-server.d.ts +8 -0
- package/dist/command/setup/start-dev-server.js +43 -0
- package/dist/command/update/updater.js +6 -0
- package/dist/command/version/identity/server-version.d.ts +2 -0
- package/dist/command/version/identity/server-version.js +2 -0
- package/dist/command/version/version-skew.d.ts +7 -0
- package/dist/command/version/version-skew.js +14 -3
- package/dist/features/compare/action-plan.d.ts +260 -0
- package/dist/features/compare/action-plan.js +543 -0
- package/dist/features/compare/behaviour-compare.d.ts +24 -0
- package/dist/features/compare/behaviour-compare.js +85 -0
- package/dist/features/compare/behaviour-diff.d.ts +97 -0
- package/dist/features/compare/behaviour-diff.js +202 -0
- package/dist/features/compare/behaviour-report.d.ts +40 -0
- package/dist/features/compare/behaviour-report.js +128 -0
- package/dist/features/compare/build-readings.d.ts +106 -0
- package/dist/features/compare/build-readings.js +250 -0
- package/dist/features/compare/compare-diff.d.ts +62 -0
- package/dist/features/compare/compare-diff.js +177 -0
- package/dist/features/compare/compare-report-html.d.ts +6 -0
- package/dist/features/compare/compare-report-html.js +102 -0
- package/dist/features/compare/compare-report.d.ts +15 -0
- package/dist/features/compare/compare-report.js +73 -0
- package/dist/features/compare/compare-run.d.ts +60 -0
- package/dist/features/compare/compare-run.js +123 -0
- package/dist/features/compare/outside/effect-lines.d.ts +26 -0
- package/dist/features/compare/outside/effect-lines.js +103 -0
- package/dist/features/compare/outside/state-sources.d.ts +76 -0
- package/dist/features/compare/outside/state-sources.js +164 -0
- package/dist/features/compare/outside/value-dictionary.d.ts +51 -0
- package/dist/features/compare/outside/value-dictionary.js +227 -0
- package/dist/features/compare/visible-content.d.ts +20 -0
- package/dist/features/compare/visible-content.js +125 -0
- package/dist/features/crawl/crawl-empty.js +2 -1
- package/dist/features/crawl/crawl-exhaustive.d.ts +55 -0
- package/dist/features/crawl/crawl-exhaustive.js +138 -0
- package/dist/features/crawl/crawl-tools.d.ts +9 -2
- package/dist/features/crawl/crawl-tools.js +28 -12
- package/dist/features/crawl/crawl.d.ts +8 -9
- package/dist/features/crawl/crawl.js +61 -65
- package/dist/features/exhaust/app-coverage.d.ts +5 -1
- package/dist/features/exhaust/app-coverage.js +3 -1
- package/dist/features/exhaust/code-coverage.js +8 -4
- package/dist/features/exhaust/edge-cases.d.ts +133 -0
- package/dist/features/exhaust/edge-cases.js +310 -0
- package/dist/features/exhaust/edges/edge-drive.d.ts +42 -0
- package/dist/features/exhaust/edges/edge-drive.js +364 -0
- package/dist/features/exhaust/edges/edge-run.d.ts +59 -0
- package/dist/features/exhaust/edges/edge-run.js +169 -0
- package/dist/features/exhaust/explorer.d.ts +87 -14
- package/dist/features/exhaust/explorer.js +159 -25
- package/dist/features/exhaust/failure-branches.d.ts +5 -0
- package/dist/features/exhaust/failure-branches.js +29 -11
- package/dist/features/exhaust/invariants.d.ts +112 -0
- package/dist/features/exhaust/invariants.js +188 -0
- package/dist/features/exhaust/ledger.d.ts +68 -0
- package/dist/features/exhaust/ledger.js +121 -13
- package/dist/features/exhaust/session-fold.d.ts +10 -0
- package/dist/features/exhaust/session-fold.js +46 -3
- package/dist/features/exhaust/session-port.d.ts +3 -1
- package/dist/features/exhaust/session-port.js +97 -28
- package/dist/features/grade/grade-answer.d.ts +99 -0
- package/dist/features/grade/grade-answer.js +112 -0
- package/dist/features/grade/grade-noise.d.ts +27 -0
- package/dist/features/grade/grade-noise.js +55 -0
- package/dist/features/grade/grade-renumber.d.ts +12 -0
- package/dist/features/grade/grade-renumber.js +86 -0
- package/dist/features/grade/grade-state.d.ts +50 -0
- package/dist/features/grade/grade-state.js +93 -0
- package/dist/features/grade/grade-suggest.d.ts +33 -0
- package/dist/features/grade/grade-suggest.js +98 -0
- package/dist/features/license/license.d.ts +11 -71
- package/dist/features/license/license.js +43 -136
- package/dist/features/visual/desktop-capture.d.ts +33 -0
- package/dist/features/visual/desktop-capture.js +94 -0
- package/dist/features/visual/step-shots.js +139 -35
- package/dist/features/visual/visual-tools.d.ts +0 -18
- package/dist/features/visual/visual-tools.js +67 -99
- package/dist/import-graph.js +5 -6
- package/dist/index.d.ts +3 -1
- package/dist/index.js +19 -7
- package/dist/judgement/capsule/causal-summary.js +9 -2
- package/dist/judgement/runs/artifact/replay-mapping.d.ts +13 -1
- package/dist/judgement/runs/artifact/replay-mapping.js +1 -1
- package/dist/judgement/runs/reticle-runner.js +16 -2
- package/dist/judgement/runs/run-ledger.d.ts +7 -5
- package/dist/judgement/runs/run-ledger.js +98 -69
- package/dist/judgement/runs/runner-port.d.ts +2 -0
- package/dist/judgement/runs/runner-port.js +23 -5
- package/dist/judgement/runs/verification-sync.d.ts +6 -3
- package/dist/judgement/runs/verification-sync.js +21 -10
- package/dist/language/flows/affected-tools.d.ts +1 -1
- package/dist/language/flows/affected-tools.js +49 -30
- package/dist/language/flows/change/git-changed.d.ts +2 -1
- package/dist/language/flows/change/git-changed.js +19 -2
- package/dist/language/flows/fields/flow-secret-field.d.ts +1 -13
- package/dist/language/flows/fields/flow-secret-field.js +77 -21
- package/dist/language/flows/flow-anchor.d.ts +60 -2
- package/dist/language/flows/flow-anchor.js +125 -20
- package/dist/language/flows/flow-classify.d.ts +20 -0
- package/dist/language/flows/flow-classify.js +90 -12
- package/dist/language/flows/flow-cross-step.d.ts +7 -1
- package/dist/language/flows/flow-cross-step.js +25 -1
- package/dist/language/flows/flow-journey.d.ts +38 -4
- package/dist/language/flows/flow-journey.js +138 -8
- package/dist/language/flows/flow-replay-run.d.ts +0 -16
- package/dist/language/flows/flow-replay-run.js +76 -14
- package/dist/language/flows/flow-replay-types.d.ts +4 -2
- package/dist/language/flows/flow-replay.d.ts +19 -1
- package/dist/language/flows/flow-replay.js +169 -52
- package/dist/language/flows/flow-replayable.js +7 -1
- package/dist/language/flows/flow-start-path.d.ts +34 -5
- package/dist/language/flows/flow-start-path.js +202 -15
- package/dist/language/flows/flow-step-runners.d.ts +14 -13
- package/dist/language/flows/flow-step-runners.js +184 -109
- package/dist/language/flows/flow-store-for-session.d.ts +26 -25
- package/dist/language/flows/flow-store-for-session.js +51 -5
- package/dist/language/flows/flow-tools.js +46 -33
- package/dist/language/flows/flows.js +176 -76
- package/dist/language/flows/recording/tape/recordings.d.ts +21 -16
- package/dist/language/flows/recording/tape/recordings.js +112 -49
- package/dist/language/flows/replay.d.ts +36 -12
- package/dist/language/flows/replay.js +193 -50
- package/dist/language/flows/steps/page-steps.d.ts +68 -0
- package/dist/language/flows/steps/page-steps.js +198 -0
- package/dist/language/flows/suite/sequential-run.d.ts +68 -0
- package/dist/language/flows/suite/sequential-run.js +96 -0
- package/dist/language/flows/verify-change-tools.js +26 -15
- package/dist/launch-chromium.d.ts +1 -0
- package/dist/launch-chromium.js +49 -12
- package/dist/memory/cloud/background-sync.d.ts +13 -0
- package/dist/memory/cloud/background-sync.js +24 -3
- package/dist/memory/cloud/sync-daemon.js +26 -27
- package/dist/memory/cloud/sync-screenshots.js +6 -1
- package/dist/memory/journal/attach-journal.js +2 -1
- package/dist/memory/journal/drive-flow.d.ts +7 -0
- package/dist/memory/journal/drive-flow.js +44 -13
- package/dist/memory/journal/journal-recorder.d.ts +4 -2
- package/dist/memory/journal/session-journal.d.ts +7 -0
- package/dist/memory/journal/session-journal.js +58 -10
- package/dist/memory/project/root-claims.d.ts +9 -0
- package/dist/memory/project/root-claims.js +9 -4
- package/dist/portal/bridge/bridge.d.ts +13 -4
- package/dist/portal/bridge/bridge.js +10 -9
- package/dist/portal/input/download-capture.d.ts +7 -0
- package/dist/portal/input/download-capture.js +92 -0
- package/dist/portal/input/network-detail.d.ts +6 -0
- package/dist/portal/input/network-detail.js +12 -1
- package/dist/portal/input/network-mock-tools.js +49 -13
- package/dist/portal/input/network-mock.d.ts +8 -0
- package/dist/portal/input/network-mock.js +30 -10
- package/dist/portal/input/page-capture.d.ts +24 -0
- package/dist/portal/input/page-capture.js +33 -0
- package/dist/portal/input/real-input.d.ts +35 -18
- package/dist/portal/input/real-input.js +99 -33
- package/dist/portal/input/viewport-tools.d.ts +8 -0
- package/dist/portal/input/viewport-tools.js +81 -21
- package/dist/portal/pool/browser-pool.d.ts +16 -30
- package/dist/portal/pool/browser-pool.js +64 -52
- package/dist/portal/pool/context-permissions.d.ts +34 -17
- package/dist/portal/pool/context-permissions.js +94 -1
- package/dist/portal/pool/csp-widen.d.ts +62 -0
- package/dist/portal/pool/csp-widen.js +132 -0
- package/dist/portal/pool/determinism.d.ts +52 -0
- package/dist/portal/pool/determinism.js +259 -0
- package/dist/portal/pool/lease-emulation.d.ts +34 -1
- package/dist/portal/pool/lease-emulation.js +18 -0
- package/dist/portal/pool/lease-marker-stamp.d.ts +6 -0
- package/dist/portal/pool/lease-marker-stamp.js +15 -1
- package/dist/portal/pool/lease-page-error.d.ts +56 -0
- package/dist/portal/pool/lease-page-error.js +160 -0
- package/dist/portal/pool/page-error-journal.d.ts +63 -0
- package/dist/portal/pool/page-error-journal.js +208 -0
- package/dist/portal/pool/playwright-launcher.js +71 -15
- package/dist/portal/pool/pool-contract.d.ts +49 -1
- package/dist/portal/pool/pool-lease.d.ts +70 -2
- package/dist/portal/pool/pool-lease.js +23 -0
- package/dist/portal/pool/zero-install.d.ts +8 -0
- package/dist/portal/pool/zero-install.js +91 -1
- package/dist/portal/session/dev-server/dev-command.d.ts +11 -2
- package/dist/portal/session/dev-server/dev-command.js +110 -16
- package/dist/portal/session/dev-server/dev-server-probe.d.ts +7 -0
- package/dist/portal/session/dev-server/dev-server-probe.js +9 -0
- package/dist/portal/session/dev-server/project-ports.d.ts +21 -0
- package/dist/portal/session/dev-server/project-ports.js +62 -0
- package/dist/portal/session/facts/departed-network.d.ts +66 -0
- package/dist/portal/session/facts/departed-network.js +142 -0
- package/dist/portal/session/no-session-diagnosis.d.ts +9 -0
- package/dist/portal/session/no-session-diagnosis.js +76 -27
- package/dist/portal/session/no-session-next-action.d.ts +14 -0
- package/dist/portal/session/no-session-next-action.js +56 -7
- package/dist/portal/session/no-session-watch.d.ts +6 -0
- package/dist/portal/session/no-session-watch.js +41 -4
- package/dist/portal/session/page-commands/undeclared-command.d.ts +9 -3
- package/dist/portal/session/page-commands/undeclared-command.js +16 -7
- package/dist/portal/session/recorded-gaps.d.ts +1 -1
- package/dist/portal/session/recorded-gaps.js +5 -2
- package/dist/portal/session/serving-directory.d.ts +10 -0
- package/dist/portal/session/serving-directory.js +39 -8
- package/dist/portal/session/session-manager.d.ts +10 -0
- package/dist/portal/session/session-manager.js +211 -81
- package/dist/portal/session/session.js +4 -3
- package/dist/portal/session/zero-install-first-call.d.ts +20 -1
- package/dist/portal/session/zero-install-first-call.js +34 -2
- package/dist/start-wiring.d.ts +30 -2
- package/dist/start-wiring.js +74 -9
- package/dist/surface/mcp/mcp-proxy.js +10 -2
- package/dist/surface/mcp/mcp.js +8 -5
- package/dist/surface/mcp/peer/agents-connected.d.ts +6 -0
- package/dist/surface/mcp/peer/agents-connected.js +58 -0
- package/dist/surface/mcp/proxy/proxy-daemon-probe.js +17 -5
- package/dist/surface/mcp/shared-params.d.ts +2 -0
- package/dist/surface/mcp/shared-params.js +31 -0
- package/dist/surface/tools/act/act-observation.d.ts +40 -2
- package/dist/surface/tools/act/act-observation.js +118 -12
- package/dist/surface/tools/act/act-preflight.js +38 -18
- package/dist/surface/tools/act/act-sequence-retry.d.ts +23 -2
- package/dist/surface/tools/act/act-sequence-retry.js +61 -5
- package/dist/surface/tools/act/act-target.js +3 -1
- package/dist/surface/tools/act/after-match.d.ts +10 -0
- package/dist/surface/tools/act/after-match.js +40 -0
- package/dist/surface/tools/act/already-true.js +1 -1
- package/dist/surface/tools/act/navigation/reload-result.d.ts +12 -1
- package/dist/surface/tools/act/navigation/reload-result.js +25 -2
- package/dist/surface/tools/act/resolve-target.d.ts +17 -1
- package/dist/surface/tools/act/resolve-target.js +59 -9
- package/dist/surface/tools/act/sequence-grade.d.ts +15 -1
- package/dist/surface/tools/act/sequence-grade.js +45 -27
- package/dist/surface/tools/act/settle-in-flight.js +27 -15
- package/dist/surface/tools/act-command.d.ts +23 -0
- package/dist/surface/tools/act-command.js +50 -0
- package/dist/surface/tools/act-sequence-tool.js +81 -50
- package/dist/surface/tools/act-tools.d.ts +1 -13
- package/dist/surface/tools/act-tools.js +83 -95
- package/dist/surface/tools/agent/build-staleness.d.ts +1 -1
- package/dist/surface/tools/agent/build-staleness.js +1 -1
- package/dist/surface/tools/assert/assert-verdict.js +18 -12
- package/dist/surface/tools/browser-tools.js +87 -13
- package/dist/surface/tools/compare-tools.d.ts +11 -0
- package/dist/surface/tools/compare-tools.js +94 -0
- package/dist/surface/tools/coverage-tools.js +1 -1
- package/dist/surface/tools/dynamic-tools.js +11 -3
- package/dist/surface/tools/empty-session-list.d.ts +2 -0
- package/dist/surface/tools/empty-session-list.js +4 -2
- package/dist/surface/tools/error-recovery.d.ts +12 -1
- package/dist/surface/tools/error-recovery.js +131 -40
- package/dist/surface/tools/explore-tools.js +1 -1
- package/dist/surface/tools/invoke-tool.js +29 -21
- package/dist/surface/tools/lease-coverage-tools.d.ts +49 -0
- package/dist/surface/tools/lease-coverage-tools.js +128 -0
- package/dist/surface/tools/lease-hint.d.ts +42 -1
- package/dist/surface/tools/lease-hint.js +52 -3
- package/dist/surface/tools/lease-readiness.d.ts +47 -0
- package/dist/surface/tools/lease-readiness.js +126 -0
- package/dist/surface/tools/lease-seed.d.ts +16 -1
- package/dist/surface/tools/lease-seed.js +37 -4
- package/dist/surface/tools/lease-tools.d.ts +3 -41
- package/dist/surface/tools/lease-tools.js +104 -124
- package/dist/surface/tools/lease-url-identity.d.ts +46 -0
- package/dist/surface/tools/lease-url-identity.js +97 -0
- package/dist/surface/tools/merge-tools.js +99 -2
- package/dist/surface/tools/merged-name-redirect.js +5 -1
- package/dist/surface/tools/observe-tools.js +15 -9
- package/dist/surface/tools/read/console-pre-attach.d.ts +19 -0
- package/dist/surface/tools/read/console-pre-attach.js +23 -0
- package/dist/surface/tools/read-tools.js +33 -30
- package/dist/surface/tools/real-input-attempt.d.ts +0 -8
- package/dist/surface/tools/real-input-attempt.js +143 -28
- package/dist/surface/tools/repeat-hint.d.ts +40 -0
- package/dist/surface/tools/repeat-hint.js +171 -0
- package/dist/surface/tools/run-report.d.ts +2 -2
- package/dist/surface/tools/run-report.js +27 -15
- package/dist/surface/tools/session-ports.d.ts +14 -0
- package/dist/surface/tools/session-ports.js +53 -0
- package/dist/surface/tools/tool-kit.d.ts +18 -0
- package/dist/surface/tools/tool-kit.js +9 -3
- package/dist/surface/tools/tool-surface.js +0 -1
- package/dist/surface/tools/tools.js +54 -18
- package/dist/surface/tools/type-spelling.d.ts +13 -0
- package/dist/surface/tools/type-spelling.js +36 -0
- package/dist/telemetry/agent-client.d.ts +6 -0
- package/dist/telemetry/agent-client.js +10 -0
- package/dist/telemetry/argument-shape.js +17 -7
- package/dist/telemetry/cli-telemetry.d.ts +10 -1
- package/dist/telemetry/cli-telemetry.js +15 -0
- package/dist/telemetry/init-telemetry.d.ts +2 -1
- package/dist/telemetry/init-telemetry.js +47 -2
- package/dist/telemetry/install-trace.js +23 -4
- package/dist/telemetry/onboarding-funnel.d.ts +19 -2
- package/dist/telemetry/onboarding-funnel.js +25 -3
- package/dist/telemetry/proxy-telemetry.d.ts +2 -1
- package/dist/telemetry/proxy-telemetry.js +2 -2
- package/dist/telemetry/session-metrics.d.ts +1 -1
- package/dist/telemetry/session-metrics.js +67 -27
- package/dist/telemetry/telemetry.d.ts +3 -1
- package/dist/telemetry/telemetry.js +60 -28
- package/dist/telemetry/tool-refused.d.ts +6 -3
- package/dist/telemetry/tool-refused.js +15 -3
- package/docs/actions.mdx +3 -3
- package/docs/agent-cheatsheet.md +1 -1
- package/docs/architecture.md +3 -6
- package/docs/capabilities.mdx +61 -1
- package/docs/cli/affected.mdx +9 -7
- package/docs/cli/cloud.mdx +3 -3
- package/docs/cli/compare.mdx +94 -0
- package/docs/cli/doctor.mdx +41 -2
- package/docs/cli/grade.mdx +97 -0
- package/docs/cli/help.mdx +4 -2
- package/docs/cli/license.mdx +13 -12
- package/docs/cli/restart.mdx +7 -5
- package/docs/cli/rollback.mdx +5 -5
- package/docs/cli/setup.mdx +13 -2
- package/docs/cli/status.mdx +4 -0
- package/docs/cli/stop.mdx +3 -3
- package/docs/cli/update.mdx +2 -2
- package/docs/cli/verify.mdx +23 -1
- package/docs/cli.mdx +34 -24
- package/docs/deploy-checks.md +25 -0
- package/docs/desktop.mdx +1 -1
- package/docs/enterprise.md +17 -22
- package/docs/environment.mdx +1 -0
- package/docs/first-drive.md +1 -1
- package/docs/flows.md +11 -1
- package/docs/frameworks.mdx +9 -9
- package/docs/getting-started.md +1 -1
- package/docs/install-manual.mdx +16 -1
- package/docs/license-activation.mdx +3 -3
- package/docs/packages/engine.mdx +1 -1
- package/docs/packages/server.mdx +1 -1
- package/docs/packages/tauri.mdx +1 -1
- package/docs/packages/test.mdx +1 -1
- package/docs/packages.mdx +1 -1
- package/docs/platform-integration.md +1 -1
- package/docs/predicates.mdx +1 -1
- package/docs/quickstart.mdx +68 -49
- package/docs/skill-setup.md +1 -1
- package/docs/skill.md +14 -13
- package/docs/telemetry.md +6 -4
- package/docs/tools/navigate.mdx +1 -1
- package/docs/tools/verify.mdx +4 -1
- package/docs/troubleshooting.mdx +1 -1
- package/docs/usage.md +17 -14
- package/package.json +18 -14
- package/LICENSE-ENTERPRISE +0 -37
- package/dist/features/ee/audit-log.d.ts +0 -14
- package/dist/features/ee/audit-log.js +0 -11
- package/dist/features/exhaust/exhaust-run.d.ts +0 -33
- package/dist/features/exhaust/exhaust-run.js +0 -82
|
@@ -41,7 +41,7 @@ import { describeSync } from './sync-describe.js';
|
|
|
41
41
|
import { diskSink, diskSource, policyScreenshots, readCloudState } from './sync-disk.js';
|
|
42
42
|
import { resolveProjectCloud } from './cloud-config.js';
|
|
43
43
|
import { watch } from 'node:fs';
|
|
44
|
-
import { BackgroundSyncState, backgroundSyncGate, } from './background-sync.js';
|
|
44
|
+
import { BackgroundSyncState, backgroundSyncGate, closeCycleGate, } from './background-sync.js';
|
|
45
45
|
import { unmatchedRoots, unsyncedRoots } from './unsynced-roots.js';
|
|
46
46
|
/**
|
|
47
47
|
* The backstop: how often the daemon cycles when nothing under `.reticle` has changed. Long, because
|
|
@@ -186,38 +186,39 @@ export function startSyncDaemon(deps) {
|
|
|
186
186
|
let wasLinked;
|
|
187
187
|
/** The project the last cycle synced to: a re-link to another one is announced too. */
|
|
188
188
|
let wasProject;
|
|
189
|
-
/** The last
|
|
190
|
-
|
|
189
|
+
/** The aggregate last logged: unlinked roots are announced as one line per change, never per tick. */
|
|
190
|
+
let announcedUnsynced;
|
|
191
191
|
let unsynced = [];
|
|
192
192
|
/** Which roots pushed this cycle, and whether each is linked by its own link file. */
|
|
193
193
|
let rootsLinked = new Map();
|
|
194
194
|
/** The linked roots that send flows: a link with `sync.flows` off keeps every flow local. */
|
|
195
195
|
let rootsSendFlows = new Set();
|
|
196
|
-
/**
|
|
197
|
-
* Count what every root still owes, and say once per change where runs sit that nothing sends.
|
|
198
|
-
* Linked roots are listed too (the platform report and `doctor` read them), but only an unlinked
|
|
199
|
-
* one earns a log line: a linked root's pending runs are the next cycle's job.
|
|
200
|
-
*/
|
|
196
|
+
/** Count what every root owes; unlinked roots are one aggregate line per change, never per tick. */
|
|
201
197
|
const refreshUnsynced = async () => {
|
|
202
198
|
const linked = rootsLinked;
|
|
203
199
|
const sendsFlows = rootsSendFlows;
|
|
204
200
|
unsynced = await unsyncedRoots(
|
|
205
201
|
// With what no project claimed, which no cycle pushes: the dashboard's unsent list left it out.
|
|
206
202
|
[...linked.keys(), ...unmatchedRoots(homedir())], (r) => Promise.resolve(true === linked.get(r)), (r) => Promise.resolve(sendsFlows.has(r)));
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
linked: entry.linked,
|
|
218
|
-
fix: "run `reticle connect` in that folder's project: this work is on this machine only",
|
|
219
|
-
});
|
|
203
|
+
const unlinked = unsynced.filter((entry) => !entry.linked);
|
|
204
|
+
const runs = unlinked.reduce((total, entry) => total + entry.runs, 0);
|
|
205
|
+
const flows = unlinked.reduce((total, entry) => total + entry.flows, 0);
|
|
206
|
+
const signature = JSON.stringify([unlinked.map((e) => [e.root, e.runs, e.flows]), runs, flows]);
|
|
207
|
+
if (0 === unlinked.length) {
|
|
208
|
+
announcedUnsynced = undefined;
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
if (announcedUnsynced === signature) {
|
|
212
|
+
return;
|
|
220
213
|
}
|
|
214
|
+
announcedUnsynced = signature;
|
|
215
|
+
log('reticle_cloud_unsynced_root', {
|
|
216
|
+
count: unlinked.length,
|
|
217
|
+
runs,
|
|
218
|
+
flows,
|
|
219
|
+
examples: unlinked.slice(0, 3).map((entry) => entry.root),
|
|
220
|
+
fix: 'run `reticle connect` in each project that owns these folders: this work is on this machine only',
|
|
221
|
+
});
|
|
221
222
|
};
|
|
222
223
|
/**
|
|
223
224
|
* The cloud as this tick may use it: unchanged when the gate lets the root send, else with no
|
|
@@ -778,12 +779,10 @@ export function attachCloudSync(deps) {
|
|
|
778
779
|
// and on the backstop timer.
|
|
779
780
|
// Watching them too means one recursive watcher per linked repo — add it if siblings lag.
|
|
780
781
|
watch: (onChange) => watchReticleRoot(deps.reticleRoot, onChange),
|
|
781
|
-
// Sent only
|
|
782
|
-
|
|
783
|
-
?
|
|
784
|
-
: {
|
|
785
|
-
gate: (root, cloud) => backgroundSyncGate({ reticleRoot: root, cloud, env: deps.env, signedIn }),
|
|
786
|
-
}),
|
|
782
|
+
// Sent only to a linked project that did not switch it off; see `closeCycleGate` for no sign-in.
|
|
783
|
+
gate: (root, cloud) => signedIn === undefined
|
|
784
|
+
? closeCycleGate(root, deps.env)
|
|
785
|
+
: backgroundSyncGate({ reticleRoot: root, cloud, env: deps.env, signedIn }),
|
|
787
786
|
...(deps.request === undefined ? {} : { request: deps.request }),
|
|
788
787
|
...(deps.intervalMs === undefined ? {} : { intervalMs: deps.intervalMs }),
|
|
789
788
|
});
|
|
@@ -20,6 +20,11 @@ const HTTP_OK = 200;
|
|
|
20
20
|
const NOT_THERE = new Set([404, 405]);
|
|
21
21
|
/** Refused for good: too large (413), too many for one run (409). Retrying cannot change it. */
|
|
22
22
|
const REFUSED_FOR_GOOD = new Set([409, 413]);
|
|
23
|
+
/**
|
|
24
|
+
* The platform takes only JPEG and answers 400 to anything else, which stops the cycle. A desktop
|
|
25
|
+
* capture from a page SDK before 3.7 is a PNG, so it stays on this machine.
|
|
26
|
+
*/
|
|
27
|
+
const isJpeg = (image) => 0xff === image[0] && 0xd8 === image[1];
|
|
23
28
|
/** The screenshots a run artifact lists, read defensively: a malformed entry is skipped. */
|
|
24
29
|
function listed(payload) {
|
|
25
30
|
const raw = 'object' === typeof payload && null !== payload
|
|
@@ -78,7 +83,7 @@ export async function syncScreenshots(deps) {
|
|
|
78
83
|
done.add(keyOf(shot));
|
|
79
84
|
sent[run.runId] = [...done];
|
|
80
85
|
};
|
|
81
|
-
if (image.byteLength > STEP_SCREENSHOT.MAX_BYTES) {
|
|
86
|
+
if (image.byteLength > STEP_SCREENSHOT.MAX_BYTES || !isJpeg(image)) {
|
|
82
87
|
markDone();
|
|
83
88
|
continue;
|
|
84
89
|
}
|
|
@@ -19,7 +19,8 @@ export function makeJournalAttach(deps) {
|
|
|
19
19
|
log('journal_skipped_unsafe_session_id', { sessionId: session.id });
|
|
20
20
|
return;
|
|
21
21
|
}
|
|
22
|
-
|
|
22
|
+
// The session's clock stamps each record's `t`, so it also bounds a cut that left nothing to parse.
|
|
23
|
+
const journal = new SessionJournal(deps.fs, session.artifactRoot ?? deps.reticleRoot, session.id, { now: () => session.elapsed() });
|
|
23
24
|
// Same SessionJournal is both the write sink and the read fall-through for queries after eviction.
|
|
24
25
|
session.setJournal(new JournalRecorder(journal, { now: () => session.elapsed() }), journal);
|
|
25
26
|
};
|
|
@@ -44,6 +44,13 @@ export interface DriveProgram {
|
|
|
44
44
|
*
|
|
45
45
|
* So it is not a tool and not a rule. It runs at teardown, in code, on every session.
|
|
46
46
|
*/
|
|
47
|
+
/**
|
|
48
|
+
* The claims one tape step carries: its own `expect`, and the `expect` of each of its sub-steps.
|
|
49
|
+
*
|
|
50
|
+
* A `reticle_act { steps }` journey is one tape step, and each claim is on its sub-step. When only
|
|
51
|
+
* the step's own `expect` was read, a journey with four held claims was "unproven" and was not saved.
|
|
52
|
+
*/
|
|
53
|
+
export declare function claimsOf(step: TapeStep): Predicate[];
|
|
47
54
|
/** The one condition on saving, and the reason the whole thing is safe. */
|
|
48
55
|
export declare function carriesAnAssertion(steps: readonly TapeStep[]): boolean;
|
|
49
56
|
/** Every auto-saved flow's name starts here, which is what lets a merge tell them from hand-named ones. */
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ActionType, QueryBy, REPLAY_PROGRAM_VERSION, asFlowName,
|
|
1
|
+
import { ActionType, FlowPredicateSchema, QueryBy, REPLAY_PROGRAM_VERSION, asFlowName, asRecord, isCredentialFieldName, } from '@reticlehq/core';
|
|
2
2
|
import { journeySentence } from '@reticlehq/core';
|
|
3
3
|
/**
|
|
4
4
|
* What a session drove, saved as a replayable flow — without anybody asking for it.
|
|
@@ -11,9 +11,23 @@ import { journeySentence } from '@reticlehq/core';
|
|
|
11
11
|
*
|
|
12
12
|
* So it is not a tool and not a rule. It runs at teardown, in code, on every session.
|
|
13
13
|
*/
|
|
14
|
+
/**
|
|
15
|
+
* The claims one tape step carries: its own `expect`, and the `expect` of each of its sub-steps.
|
|
16
|
+
*
|
|
17
|
+
* A `reticle_act { steps }` journey is one tape step, and each claim is on its sub-step. When only
|
|
18
|
+
* the step's own `expect` was read, a journey with four held claims was "unproven" and was not saved.
|
|
19
|
+
*/
|
|
20
|
+
export function claimsOf(step) {
|
|
21
|
+
const subs = Array.isArray(step.args['steps']) ? step.args['steps'] : [];
|
|
22
|
+
const subClaims = subs.flatMap((sub) => {
|
|
23
|
+
const parsed = FlowPredicateSchema.safeParse(asRecord(sub)['expect']);
|
|
24
|
+
return parsed.success ? [parsed.data] : [];
|
|
25
|
+
});
|
|
26
|
+
return step.expect === undefined ? subClaims : [step.expect, ...subClaims];
|
|
27
|
+
}
|
|
14
28
|
/** The one condition on saving, and the reason the whole thing is safe. */
|
|
15
29
|
export function carriesAnAssertion(steps) {
|
|
16
|
-
return steps.some((step) =>
|
|
30
|
+
return steps.some((step) => 0 < claimsOf(step).length);
|
|
17
31
|
}
|
|
18
32
|
/** Squeeze any string into one safe, lower-case filename segment. */
|
|
19
33
|
const segment = (value, max) => value
|
|
@@ -28,10 +42,12 @@ const segment = (value, max) => value
|
|
|
28
42
|
* said nothing about what it was for.
|
|
29
43
|
*/
|
|
30
44
|
function provedSentence(steps) {
|
|
31
|
-
return journeySentence(steps.flatMap(
|
|
45
|
+
return journeySentence(steps.flatMap(claimsOf));
|
|
32
46
|
}
|
|
33
47
|
/** Every auto-saved flow's name starts here, which is what lets a merge tell them from hand-named ones. */
|
|
34
48
|
export const DRIVE_FLOW_PREFIX = 'drive-';
|
|
49
|
+
/** The most characters one segment of core's FLOW_NAME_PATTERN holds. */
|
|
50
|
+
const FLOW_NAME_MAX_LENGTH = 64;
|
|
35
51
|
/**
|
|
36
52
|
* A readable name for a journey, led by WHY it was driven.
|
|
37
53
|
*
|
|
@@ -52,7 +68,10 @@ export function driveFlowName(route, steps = []) {
|
|
|
52
68
|
// Minted here: every character has just been forced through `segment`, so this IS the validated
|
|
53
69
|
// boundary the brand is meant to be created at.
|
|
54
70
|
const parts = [lead, ...('' !== why || 0 === leg.length ? [] : [leg])].filter((p) => '' !== p);
|
|
55
|
-
|
|
71
|
+
// Cut to the flow-name limit: a sentence and a route together went over it, the save refused
|
|
72
|
+
// the name, and session end dropped the flow with nothing said.
|
|
73
|
+
const body = segment(parts.join('-'), FLOW_NAME_MAX_LENGTH - DRIVE_FLOW_PREFIX.length);
|
|
74
|
+
return asFlowName(`${DRIVE_FLOW_PREFIX}${'' === body ? 'journey' : body}`);
|
|
56
75
|
}
|
|
57
76
|
/** Why session end saved no flow, as reported on the `flow_recorded` step. */
|
|
58
77
|
export const DriveFlowSkipReason = {
|
|
@@ -106,16 +125,25 @@ function segmentsByRoute(steps) {
|
|
|
106
125
|
return out;
|
|
107
126
|
}
|
|
108
127
|
/**
|
|
109
|
-
* A fill into a field
|
|
128
|
+
* A fill into a field whose name says it holds a credential — the sign-in a journey must not carry.
|
|
110
129
|
* Read off the recorder's anchor args: a testid names the field by `value`, a role by `name`.
|
|
111
130
|
*/
|
|
112
131
|
function typesASecret(step) {
|
|
113
|
-
|
|
114
|
-
|
|
132
|
+
// A sign-in sent as one reticle_act sequence carries its fills as sub-steps.
|
|
133
|
+
const subs = step.args['steps'];
|
|
134
|
+
if (Array.isArray(subs)) {
|
|
135
|
+
return subs.some((sub) => fillsASecret(asRecord(sub)));
|
|
136
|
+
}
|
|
137
|
+
return fillsASecret(step.args);
|
|
138
|
+
}
|
|
139
|
+
function fillsASecret(args) {
|
|
140
|
+
const action = args['action'];
|
|
141
|
+
if (ActionType.FILL !== action && ActionType.TYPE !== action) {
|
|
115
142
|
return false;
|
|
116
|
-
|
|
117
|
-
const
|
|
118
|
-
|
|
143
|
+
}
|
|
144
|
+
const by = args['by'];
|
|
145
|
+
const field = QueryBy.TESTID === by ? args['value'] : args['name'];
|
|
146
|
+
return 'string' === typeof field && isCredentialFieldName(field);
|
|
119
147
|
}
|
|
120
148
|
/**
|
|
121
149
|
* The tape without the attempts an agent made again. An action that declared nothing, followed at
|
|
@@ -126,11 +154,13 @@ function typesASecret(step) {
|
|
|
126
154
|
function withoutRetries(steps) {
|
|
127
155
|
return steps.filter((step, i) => {
|
|
128
156
|
const next = steps[i + 1];
|
|
129
|
-
if (next === undefined || step.expect !== undefined || next.expect === undefined)
|
|
157
|
+
if (next === undefined || step.expect !== undefined || next.expect === undefined) {
|
|
130
158
|
return true;
|
|
159
|
+
}
|
|
131
160
|
// Tried again where it was tried: a page later it is a step of the journey, not a retry.
|
|
132
|
-
if (step.route !== next.route || step.page !== next.page || step.mocked !== next.mocked)
|
|
161
|
+
if (step.route !== next.route || step.page !== next.page || step.mocked !== next.mocked) {
|
|
133
162
|
return true;
|
|
163
|
+
}
|
|
134
164
|
const { expect: _declared, ...again } = next.args;
|
|
135
165
|
const { expect: _none, ...first } = step.args;
|
|
136
166
|
return step.tool !== next.tool || JSON.stringify(first) !== JSON.stringify(again);
|
|
@@ -138,8 +168,9 @@ function withoutRetries(steps) {
|
|
|
138
168
|
}
|
|
139
169
|
/** A flow per journey, so two journeys in one session are two regression tests rather than one. */
|
|
140
170
|
export function driveFlowsFrom(tape) {
|
|
141
|
-
if (tape === undefined || 0 === tape.steps.length)
|
|
171
|
+
if (tape === undefined || 0 === tape.steps.length) {
|
|
142
172
|
return { programs: [], outcome: {} };
|
|
173
|
+
}
|
|
143
174
|
const programs = [];
|
|
144
175
|
let unproven = 0;
|
|
145
176
|
let mocked = 0;
|
|
@@ -30,8 +30,10 @@ export interface JournalReadLoss {
|
|
|
30
30
|
/** Parsed records evicted from the retained set. Exact, unlike `droppedBytes`. */
|
|
31
31
|
droppedEvents: number;
|
|
32
32
|
/**
|
|
33
|
-
* Latest
|
|
34
|
-
*
|
|
33
|
+
* Latest `t` the loss can reach, inclusive: a parsed record that was evicted, or the bound of a
|
|
34
|
+
* byte cut. Omitted while a cut has no bound, which means every window is lost. A cut has no
|
|
35
|
+
* bound until a record after it is parsed, and none at all when the clock can have started
|
|
36
|
+
* again across it.
|
|
35
37
|
*/
|
|
36
38
|
lostThroughT?: number;
|
|
37
39
|
/** Human-and-agent readable summary. Present so a consumer never has to compose one. */
|
|
@@ -42,6 +42,13 @@ export interface SessionJournalOptions {
|
|
|
42
42
|
maxReadBytes?: number;
|
|
43
43
|
maxRetainedBytes?: number;
|
|
44
44
|
maxRetainedEvents?: number;
|
|
45
|
+
/**
|
|
46
|
+
* The session's elapsed-ms clock, the same clock that stamps each record's `t`. It bounds a cut
|
|
47
|
+
* that left no readable record: every skipped byte was on disk at the read, so its `t` is at or
|
|
48
|
+
* before the time of the read. Without it, such a cut stays unbounded until a later read parses
|
|
49
|
+
* a record.
|
|
50
|
+
*/
|
|
51
|
+
now?: () => number;
|
|
45
52
|
}
|
|
46
53
|
/**
|
|
47
54
|
* Default byte budget for one serialized writer's event ledger (#986).
|
|
@@ -146,11 +146,13 @@ export class SessionJournal {
|
|
|
146
146
|
#droppedBytes = 0;
|
|
147
147
|
/** Parsed records evicted from the retained set. Exact — these were counted, not estimated. */
|
|
148
148
|
#droppedEvents = 0;
|
|
149
|
-
/**
|
|
150
|
-
* Latest parsed eviction timestamp, or +Infinity after unparsed bytes were skipped.
|
|
151
|
-
* -Infinity means no loss. Later intact reads cannot narrow an unknown discarded time range.
|
|
152
|
-
*/
|
|
149
|
+
/** Latest `t` that a cut or an eviction can reach, inclusive. -Infinity means no loss. */
|
|
153
150
|
#lostThroughT = Number.NEGATIVE_INFINITY;
|
|
151
|
+
/** A cut skipped bytes and no record after it has been parsed yet, so its bound is unknown. */
|
|
152
|
+
#cutPending = false;
|
|
153
|
+
/** The last record parsed, so a cut can tell whether the clock started again across it. */
|
|
154
|
+
#lastParsed;
|
|
155
|
+
#now;
|
|
154
156
|
constructor(fs, root, sessionId, options = {}) {
|
|
155
157
|
if (!isValidSessionId(sessionId)) {
|
|
156
158
|
throw new Error(`refusing to journal an unsafe session id: ${sessionId}`);
|
|
@@ -171,6 +173,7 @@ export class SessionJournal {
|
|
|
171
173
|
this.#maxRetainedBytes = boundedOption(options.maxRetainedBytes, JOURNAL_READ_LIMITS.MAX_RETAINED_BYTES, 'maxRetainedBytes');
|
|
172
174
|
this.#maxRetainedEvents = boundedOption(options.maxRetainedEvents, JOURNAL_READ_LIMITS.MAX_RETAINED_EVENTS, 'maxRetainedEvents');
|
|
173
175
|
this.#eventBytesCap = cap;
|
|
176
|
+
this.#now = options.now;
|
|
174
177
|
}
|
|
175
178
|
/**
|
|
176
179
|
* Append a batch, or refuse it at the byte cap and say so.
|
|
@@ -416,10 +419,15 @@ export class SessionJournal {
|
|
|
416
419
|
// never skips, so its start IS the offset we asked for.
|
|
417
420
|
const from = chunk.from ?? this.#eventBytesConsumed;
|
|
418
421
|
const skipped = Math.max(0, from - this.#eventBytesConsumed);
|
|
419
|
-
|
|
422
|
+
// Before the ingest, so the first record parsed after the cut bounds it.
|
|
420
423
|
if (skipped > 0) {
|
|
421
424
|
this.#noteCut(skipped);
|
|
422
425
|
}
|
|
426
|
+
this.#ingestTail(chunk.text);
|
|
427
|
+
const now = this.#now;
|
|
428
|
+
if (this.#cutPending && now !== undefined) {
|
|
429
|
+
this.#boundCut({ t: now() });
|
|
430
|
+
}
|
|
423
431
|
// Resume at the end of the last COMPLETE line, counted forward over the bytes that actually
|
|
424
432
|
// ARRIVED. Not from `chunk.size`: that is the file's end as `fstat` reported it, and a read
|
|
425
433
|
// truncated or cut short returns less — a cursor there would skip every unread byte, silently
|
|
@@ -494,6 +502,10 @@ export class SessionJournal {
|
|
|
494
502
|
}
|
|
495
503
|
const result = ReticleEventSchema.safeParse(parsed);
|
|
496
504
|
if (result.success) {
|
|
505
|
+
if (this.#cutPending) {
|
|
506
|
+
this.#boundCut(result.data);
|
|
507
|
+
}
|
|
508
|
+
this.#lastParsed = result.data;
|
|
497
509
|
this.#eventCache.push(result.data);
|
|
498
510
|
// Its cost in the file, newline included — so the retained bound is in the same unit the
|
|
499
511
|
// read ceiling and the loss report are.
|
|
@@ -543,13 +555,37 @@ export class SessionJournal {
|
|
|
543
555
|
}
|
|
544
556
|
}
|
|
545
557
|
/**
|
|
546
|
-
* Skipped bytes
|
|
547
|
-
*
|
|
548
|
-
*
|
|
558
|
+
* Skipped bytes are not parsed, so their `t` is not known at the cut. The first record parsed
|
|
559
|
+
* after the cut, or the time of the read, can bound them (see `#boundCut`). Until then, the cut
|
|
560
|
+
* reaches every window.
|
|
549
561
|
*/
|
|
550
562
|
#noteCut(skippedBytes) {
|
|
551
563
|
this.#droppedBytes += skippedBytes;
|
|
552
|
-
this.#
|
|
564
|
+
this.#cutPending = true;
|
|
565
|
+
}
|
|
566
|
+
/**
|
|
567
|
+
* Bound the pending cut at `next`: the first record parsed after it, or the time of the read.
|
|
568
|
+
*
|
|
569
|
+
* The file is append-only, and one connection stamps `t` from one monotonic clock
|
|
570
|
+
* (`Session.elapsed`). So when the clock did not start again across the cut, each skipped record
|
|
571
|
+
* has a `t` at or before `next.t`.
|
|
572
|
+
*
|
|
573
|
+
* A reload or a reconnect keeps the session id and starts the clock again, and one window can
|
|
574
|
+
* span it: a window opened at t=150 on the old connection can hold skipped old records at t=200
|
|
575
|
+
* while the new connection's records start at t=10. A bound from the new clock then clears a
|
|
576
|
+
* window that lost evidence. So the cut stays unbounded, for good, when the reset cannot be
|
|
577
|
+
* ruled out: no record was parsed before the cut, or `t` or `seq` went down, or the document
|
|
578
|
+
* changed. This check is conservative but not complete. A reconnect in the same document whose
|
|
579
|
+
* new clock already passed the old `t` shows none of the three signs.
|
|
580
|
+
*/
|
|
581
|
+
#boundCut(next) {
|
|
582
|
+
const last = this.#lastParsed;
|
|
583
|
+
this.#cutPending = false;
|
|
584
|
+
if (last === undefined || clockStartedAgain(last, next)) {
|
|
585
|
+
this.#lostThroughT = Number.POSITIVE_INFINITY;
|
|
586
|
+
return;
|
|
587
|
+
}
|
|
588
|
+
this.#lostThroughT = Math.max(this.#lostThroughT, next.t);
|
|
553
589
|
}
|
|
554
590
|
/**
|
|
555
591
|
* What these reads could not produce, or `undefined` when nothing was lost.
|
|
@@ -562,7 +598,7 @@ export class SessionJournal {
|
|
|
562
598
|
if (0 === this.#droppedBytes && 0 === this.#droppedEvents) {
|
|
563
599
|
return undefined;
|
|
564
600
|
}
|
|
565
|
-
const bounded = Number.isFinite(this.#lostThroughT);
|
|
601
|
+
const bounded = !this.#cutPending && Number.isFinite(this.#lostThroughT);
|
|
566
602
|
return {
|
|
567
603
|
droppedBytes: this.#droppedBytes,
|
|
568
604
|
droppedEvents: this.#droppedEvents,
|
|
@@ -621,6 +657,18 @@ export class SessionJournal {
|
|
|
621
657
|
return out;
|
|
622
658
|
}
|
|
623
659
|
}
|
|
660
|
+
/** Did the clock start again between `last` and `next`? A field one record lacks proves nothing. */
|
|
661
|
+
function clockStartedAgain(last, next) {
|
|
662
|
+
if (next.t < last.t) {
|
|
663
|
+
return true;
|
|
664
|
+
}
|
|
665
|
+
if (last.seq !== undefined && next.seq !== undefined && next.seq < last.seq) {
|
|
666
|
+
return true;
|
|
667
|
+
}
|
|
668
|
+
return (last.documentId !== undefined &&
|
|
669
|
+
next.documentId !== undefined &&
|
|
670
|
+
next.documentId !== last.documentId);
|
|
671
|
+
}
|
|
624
672
|
/**
|
|
625
673
|
* The closure report a session's sidecar holds — details when it can be read, the bare fact when not.
|
|
626
674
|
*
|
|
@@ -1,3 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `.reticle` folder a caller meant by `root`. Agents pass the project folder, the one they work
|
|
3
|
+
* in, so a folder that holds a `.reticle` stands for it — and so does any existing directory, since
|
|
4
|
+
* a fresh worktree has none until the first run writes one, the same way a `.reticle` path is
|
|
5
|
+
* accepted before it exists. A path nowhere on disk is the caller's to fix, and the refusals name
|
|
6
|
+
* the path to pass. Openings exported so error-recovery classifies both as the caller's mistake.
|
|
7
|
+
*/
|
|
8
|
+
export declare const ARTIFACT_ROOT_ABSOLUTE_REFUSAL: string;
|
|
9
|
+
export declare const ARTIFACT_ROOT_MISSING_REFUSAL: string;
|
|
1
10
|
/** Claim `root` for the session a lease registers. Takes a `.reticle` folder or the project folder holding one. */
|
|
2
11
|
export declare function claimArtifactRoot(leaseId: string, root: string): void;
|
|
3
12
|
/** Every root a lease claimed in this process, released or not. Sync reads it to find the work. */
|
|
@@ -17,20 +17,25 @@ const claims = new Map();
|
|
|
17
17
|
const everClaimed = new Set();
|
|
18
18
|
/**
|
|
19
19
|
* The `.reticle` folder a caller meant by `root`. Agents pass the project folder, the one they work
|
|
20
|
-
* in, so a folder that holds a `.reticle` stands for it
|
|
20
|
+
* in, so a folder that holds a `.reticle` stands for it — and so does any existing directory, since
|
|
21
|
+
* a fresh worktree has none until the first run writes one, the same way a `.reticle` path is
|
|
22
|
+
* accepted before it exists. A path nowhere on disk is the caller's to fix, and the refusals name
|
|
23
|
+
* the path to pass. Openings exported so error-recovery classifies both as the caller's mistake.
|
|
21
24
|
*/
|
|
25
|
+
export const ARTIFACT_ROOT_ABSOLUTE_REFUSAL = `root must be an absolute path to a ${ReticleDir.ROOT} folder`;
|
|
26
|
+
export const ARTIFACT_ROOT_MISSING_REFUSAL = `root must be an existing directory, or the ${ReticleDir.ROOT} folder inside one`;
|
|
22
27
|
function resolveArtifactRoot(root) {
|
|
23
28
|
if (!isAbsolute(root)) {
|
|
24
|
-
throw new Error(
|
|
29
|
+
throw new Error(`${ARTIFACT_ROOT_ABSOLUTE_REFUSAL}, got ${root}`);
|
|
25
30
|
}
|
|
26
31
|
if (ReticleDir.ROOT === basename(root)) {
|
|
27
32
|
return root;
|
|
28
33
|
}
|
|
29
34
|
const inside = join(root, ReticleDir.ROOT);
|
|
30
|
-
if (isDirectory(inside)) {
|
|
35
|
+
if (isDirectory(inside) || isDirectory(root)) {
|
|
31
36
|
return inside;
|
|
32
37
|
}
|
|
33
|
-
throw new Error(
|
|
38
|
+
throw new Error(`${ARTIFACT_ROOT_MISSING_REFUSAL}, got ${root}. Pass ${inside}`);
|
|
34
39
|
}
|
|
35
40
|
function isDirectory(path) {
|
|
36
41
|
try {
|
|
@@ -57,6 +57,18 @@ export declare const WS_CLOSE_REASON: {
|
|
|
57
57
|
*/
|
|
58
58
|
readonly INVALID_HELLO_CLOSE: "invalid hello: version skew. Put SDK, build plugin and server on one release.";
|
|
59
59
|
};
|
|
60
|
+
/** WS close codes + reasons the bridge sends to the SDK (1008 = policy violation, 1013 = try again later). */
|
|
61
|
+
export declare const WS_CLOSE: {
|
|
62
|
+
readonly TOO_MANY_HANDSHAKES: readonly [1013, "too many pending handshakes"];
|
|
63
|
+
readonly HELLO_TIMEOUT: readonly [1008, "hello timeout"];
|
|
64
|
+
readonly INVALID_MESSAGE: readonly [1008, "invalid message"];
|
|
65
|
+
readonly INVALID_HELLO: readonly [1008, "invalid hello: version skew. Put SDK, build plugin and server on one release."];
|
|
66
|
+
readonly HELLO_DUPLICATE: readonly [1008, "hello already received"];
|
|
67
|
+
readonly AUTH_FAILED: readonly [1008, "authentication failed — restart the dev server; a reload keeps the old pairing token"];
|
|
68
|
+
readonly SESSION_LIMIT: readonly [1013, "session limit reached"];
|
|
69
|
+
readonly PROTOCOL_MISMATCH: readonly [1008, "protocol version mismatch — @reticlehq/browser and server disagree"];
|
|
70
|
+
readonly HANDLER_FAILED: readonly [1011, "the Reticle daemon failed handling a message — reload the page to reconnect"];
|
|
71
|
+
};
|
|
60
72
|
interface BridgeOptions {
|
|
61
73
|
port: number;
|
|
62
74
|
host?: string;
|
|
@@ -107,10 +119,7 @@ export declare class Bridge {
|
|
|
107
119
|
*/
|
|
108
120
|
attachSessionReady(handler: SessionReadyHandler): void;
|
|
109
121
|
/** Register a callback fired the instant a session is created — used to attach the durable journal. */
|
|
110
|
-
/**
|
|
111
|
-
* Teardown handler fired when a session is removed (tab closed/disconnected). Without it the journal's
|
|
112
|
-
* batched tail (< one flush batch) never reaches disk and the learned ambient map is discarded.
|
|
113
|
-
*/
|
|
122
|
+
/** Teardown when a session is removed (tab closed/disconnected). Additive — every handler runs. */
|
|
114
123
|
attachSessionEnd(handler: (session: Session) => Promise<void>): void;
|
|
115
124
|
/**
|
|
116
125
|
* Register what to do when the panel's sync button is pressed.
|
|
@@ -101,7 +101,7 @@ export const WS_CLOSE_REASON = {
|
|
|
101
101
|
INVALID_HELLO_CLOSE: 'invalid hello: version skew. Put SDK, build plugin and server on one release.',
|
|
102
102
|
};
|
|
103
103
|
/** WS close codes + reasons the bridge sends to the SDK (1008 = policy violation, 1013 = try again later). */
|
|
104
|
-
const WS_CLOSE = {
|
|
104
|
+
export const WS_CLOSE = {
|
|
105
105
|
TOO_MANY_HANDSHAKES: [1013, 'too many pending handshakes'],
|
|
106
106
|
HELLO_TIMEOUT: [1008, 'hello timeout'],
|
|
107
107
|
INVALID_MESSAGE: [1008, 'invalid message'],
|
|
@@ -190,8 +190,10 @@ export class Bridge {
|
|
|
190
190
|
*/
|
|
191
191
|
#onSessionCreate = [];
|
|
192
192
|
#onProjectAdopted = [];
|
|
193
|
-
|
|
194
|
-
|
|
193
|
+
// Fired when a session is removed. ADDITIVE: a single slot meant one teardown silently displaced
|
|
194
|
+
// the other — the same trap the create-side note describes. The durable journal flushes its tail;
|
|
195
|
+
// the page-error journal drops the session's dedupe memory.
|
|
196
|
+
#onSessionEnd = [];
|
|
195
197
|
/** Wired by the daemon: push to the dashboard now, because somebody asked in the panel. */
|
|
196
198
|
#onSyncRequest;
|
|
197
199
|
#onHarnessRequest;
|
|
@@ -737,7 +739,9 @@ export class Bridge {
|
|
|
737
739
|
log('session_disconnected', { sessionId: ended.id });
|
|
738
740
|
// Best-effort teardown: flush the journal tail + persist ambient learning. Never awaited here
|
|
739
741
|
// (the socket is already closing) and never allowed to reject into the close handler.
|
|
740
|
-
|
|
742
|
+
for (const handler of this.#onSessionEnd) {
|
|
743
|
+
void handler(ended).catch(() => undefined);
|
|
744
|
+
}
|
|
741
745
|
// AFTER teardown is started, and deliberately not awaited on it. The payload says a
|
|
742
746
|
// session ended, which is already true here; making it wait for the journal flush would
|
|
743
747
|
// put a hook in the path of a socket close for no gain to the consumer.
|
|
@@ -835,12 +839,9 @@ export class Bridge {
|
|
|
835
839
|
this.#onSessionReady.push(handler);
|
|
836
840
|
}
|
|
837
841
|
/** Register a callback fired the instant a session is created — used to attach the durable journal. */
|
|
838
|
-
/**
|
|
839
|
-
* Teardown handler fired when a session is removed (tab closed/disconnected). Without it the journal's
|
|
840
|
-
* batched tail (< one flush batch) never reaches disk and the learned ambient map is discarded.
|
|
841
|
-
*/
|
|
842
|
+
/** Teardown when a session is removed (tab closed/disconnected). Additive — every handler runs. */
|
|
842
843
|
attachSessionEnd(handler) {
|
|
843
|
-
this.#onSessionEnd
|
|
844
|
+
this.#onSessionEnd.push(handler);
|
|
844
845
|
}
|
|
845
846
|
/**
|
|
846
847
|
* Register what to do when the panel's sync button is pressed.
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { type NetworkDetail } from './network-detail.js';
|
|
2
|
+
/** The part of a CDP session this needs, kept structural so it can be tested with a fake. */
|
|
3
|
+
export interface CdpLike {
|
|
4
|
+
send(method: string, params?: Record<string, unknown>): Promise<unknown>;
|
|
5
|
+
on(event: string, handler: (event: unknown) => void): void;
|
|
6
|
+
}
|
|
7
|
+
export declare function attachDownloadCapture(cdp: CdpLike, emit: (detail: NetworkDetail) => void, pageUrl: () => string): Promise<void>;
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { buildNetworkDetail } from './network-detail.js';
|
|
2
|
+
import { drivenRedactionPolicy } from './driven-redaction.js';
|
|
3
|
+
/**
|
|
4
|
+
* Capture a file the server sends as a download, on a page Reticle drives.
|
|
5
|
+
*
|
|
6
|
+
* A click that becomes a download leaves the page as a navigation the browser turns into a file.
|
|
7
|
+
* Measured on Chromium, nothing the page can see records it: no page `request`/`response` event, no
|
|
8
|
+
* CDP `Network.responseReceived` on the page target, and a Playwright route does not intercept it.
|
|
9
|
+
* So a `net` check on a working export missed. CDP request interception at the response stage does
|
|
10
|
+
* see it, with the status and headers, so this listens there.
|
|
11
|
+
*
|
|
12
|
+
* Document responses only, and every paused response is let through at once: the capture must never
|
|
13
|
+
* change or delay what the page receives. Chromium only; a browser without the Fetch domain gets no
|
|
14
|
+
* capture rather than an error.
|
|
15
|
+
*/
|
|
16
|
+
const DOCUMENT_RESPONSES = { patterns: [{ resourceType: 'Document', requestStage: 'Response' }] };
|
|
17
|
+
/**
|
|
18
|
+
* The body is read only for a text attachment of known, small size. Reading it holds the whole file
|
|
19
|
+
* in memory, so a large or binary download keeps its status and headers and gets no body: a body
|
|
20
|
+
* check on it stays undecidable rather than costing the run its memory.
|
|
21
|
+
*/
|
|
22
|
+
const MAX_BODY_READ_BYTES = 1024 * 1024;
|
|
23
|
+
const TEXTUAL = /text\/|json|csv|xml/i;
|
|
24
|
+
function readableBodySize(headers) {
|
|
25
|
+
const length = Number(headers['content-length']);
|
|
26
|
+
return (Number.isFinite(length) &&
|
|
27
|
+
length <= MAX_BODY_READ_BYTES &&
|
|
28
|
+
TEXTUAL.test(headers['content-type'] ?? ''));
|
|
29
|
+
}
|
|
30
|
+
async function readBody(cdp, requestId) {
|
|
31
|
+
try {
|
|
32
|
+
const got = (await cdp.send('Fetch.getResponseBody', { requestId }));
|
|
33
|
+
if ('string' !== typeof got.body) {
|
|
34
|
+
return undefined;
|
|
35
|
+
}
|
|
36
|
+
return true === got.base64Encoded ? Buffer.from(got.body, 'base64').toString('utf8') : got.body;
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
return undefined;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
function headersOf(raw) {
|
|
43
|
+
const headers = {};
|
|
44
|
+
if (!Array.isArray(raw)) {
|
|
45
|
+
return headers;
|
|
46
|
+
}
|
|
47
|
+
for (const entry of raw) {
|
|
48
|
+
const { name, value } = (entry ?? {});
|
|
49
|
+
if ('string' === typeof name && 'string' === typeof value) {
|
|
50
|
+
headers[name.toLowerCase()] = value;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return headers;
|
|
54
|
+
}
|
|
55
|
+
function isAttachment(headers) {
|
|
56
|
+
return /^\s*attachment\b/i.test(headers['content-disposition'] ?? '');
|
|
57
|
+
}
|
|
58
|
+
async function onPaused(cdp, paused, emit, pageUrl) {
|
|
59
|
+
const requestId = 'string' === typeof paused.requestId ? paused.requestId : undefined;
|
|
60
|
+
try {
|
|
61
|
+
const headers = headersOf(paused.responseHeaders);
|
|
62
|
+
const url = paused.request?.url;
|
|
63
|
+
const status = paused.responseStatusCode;
|
|
64
|
+
if (isAttachment(headers) && 'string' === typeof url && 'number' === typeof status) {
|
|
65
|
+
const method = paused.request?.method;
|
|
66
|
+
const responseBody = requestId !== undefined && readableBodySize(headers)
|
|
67
|
+
? await readBody(cdp, requestId)
|
|
68
|
+
: undefined;
|
|
69
|
+
emit(buildNetworkDetail({
|
|
70
|
+
url,
|
|
71
|
+
...('string' === typeof method ? { method } : {}),
|
|
72
|
+
status,
|
|
73
|
+
headers,
|
|
74
|
+
resourceType: 'document',
|
|
75
|
+
...(responseBody === undefined ? {} : { responseBody }),
|
|
76
|
+
pageUrl: pageUrl(),
|
|
77
|
+
}, drivenRedactionPolicy()));
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
// Recording is best-effort; letting the response through below is not.
|
|
82
|
+
}
|
|
83
|
+
if (requestId !== undefined) {
|
|
84
|
+
await cdp.send('Fetch.continueRequest', { requestId }).catch(() => undefined);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
export async function attachDownloadCapture(cdp, emit, pageUrl) {
|
|
88
|
+
cdp.on('Fetch.requestPaused', (event) => {
|
|
89
|
+
void onPaused(cdp, event ?? {}, emit, pageUrl);
|
|
90
|
+
});
|
|
91
|
+
await cdp.send('Fetch.enable', DOCUMENT_RESPONSES);
|
|
92
|
+
}
|