@reticlehq/server 3.2.0 → 3.4.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-ENTERPRISE +1 -1
- package/README.md +3 -3
- package/SKILL.md +23 -22
- package/dist/chromium-launch-options.d.ts +4 -3
- package/dist/chromium-launch-options.js +6 -2
- package/dist/command/cli/adhoc-suite.d.ts +41 -0
- package/dist/command/cli/adhoc-suite.js +159 -0
- package/dist/command/cli/adhoc-verdict.d.ts +58 -2
- package/dist/command/cli/adhoc-verdict.js +108 -13
- package/dist/command/cli/answers/gate-hook-message.d.ts +6 -0
- package/dist/command/cli/answers/gate-hook-message.js +6 -0
- package/dist/command/cli/auth/cloud-keystore.d.ts +0 -2
- package/dist/command/cli/auth/cloud-keystore.js +0 -3
- package/dist/command/cli/auth/cloud-session.d.ts +4 -0
- package/dist/command/cli/auth/cloud-session.js +2 -0
- package/dist/command/cli/cli-doctor.js +30 -8
- package/dist/command/cli/cli-flow-commands.d.ts +3 -6
- package/dist/command/cli/cli-flow-commands.js +67 -4
- package/dist/command/cli/cli-parse-grammar.d.ts +7 -0
- package/dist/command/cli/cli-parse-grammar.js +7 -0
- package/dist/command/cli/cli-parse-verify.d.ts +2 -5
- package/dist/command/cli/cli-parse-verify.js +86 -13
- package/dist/command/cli/cli-parse.d.ts +14 -1
- package/dist/command/cli/cli-parse.js +54 -8
- package/dist/command/cli/cli-update-commands.js +8 -1
- package/dist/command/cli/cli-verify.d.ts +80 -0
- package/dist/command/cli/cli-verify.js +181 -26
- package/dist/command/cli/cloud-cli.js +5 -2
- package/dist/command/cli/cloud-kit.d.ts +0 -2
- package/dist/command/cli/cloud-kit.js +3 -5
- package/dist/command/cli/cloud-login.js +21 -3
- package/dist/command/cli/demo/demo-page.js +6 -3
- package/dist/command/cli/doctor/browser/chromium-hint.d.ts +5 -0
- package/dist/command/cli/doctor/browser/chromium-hint.js +5 -0
- package/dist/command/cli/doctor/doctor-sessions-line.d.ts +2 -1
- package/dist/command/cli/launch/cli-launch.d.ts +9 -29
- package/dist/command/cli/launch/cli-launch.js +27 -49
- package/dist/command/cli/launch/sdk-version-match.d.ts +84 -0
- package/dist/command/cli/launch/sdk-version-match.js +206 -0
- package/dist/command/cli/lifecycle/daemon-lifecycle.js +2 -16
- package/dist/command/cli/ports/resolve/cli-port.d.ts +38 -0
- package/dist/command/cli/ports/resolve/cli-port.js +169 -15
- package/dist/command/cli/report-command.d.ts +16 -0
- package/dist/command/cli/report-command.js +97 -0
- package/dist/command/cli/setup-mcp-cli.js +13 -13
- package/dist/command/cli/tutorial.d.ts +2 -2
- package/dist/command/cli/tutorial.js +21 -7
- package/dist/command/cli.js +67 -10
- package/dist/command/daemon/binding/daemon-status-probe.d.ts +44 -2
- package/dist/command/daemon/binding/daemon-status-probe.js +79 -6
- package/dist/command/daemon/binding/port-presence.d.ts +1 -1
- package/dist/command/daemon/daemon-resolve.d.ts +9 -0
- package/dist/command/daemon/daemon-resolve.js +1 -1
- package/dist/command/daemon/idle-shutdown.d.ts +2 -1
- package/dist/command/daemon/lifetime/daemon-usefulness.d.ts +7 -1
- package/dist/command/daemon/lifetime/daemon-usefulness.js +30 -7
- package/dist/command/dev/stale-issue-guard.d.ts +3 -4
- package/dist/command/dev/stale-issue-guard.js +8 -8
- package/dist/command/setup/bringup/connect-lease.d.ts +17 -0
- package/dist/command/setup/bringup/connect-lease.js +38 -0
- package/dist/command/setup/bringup/dev-server-files.d.ts +68 -0
- package/dist/command/setup/bringup/dev-server-files.js +131 -0
- package/dist/command/setup/bringup/dev-server-log-cap.d.ts +35 -0
- package/dist/command/setup/bringup/dev-server-log-cap.js +121 -0
- package/dist/command/setup/bringup/ensure-daemon.d.ts +65 -4
- package/dist/command/setup/bringup/ensure-daemon.js +135 -16
- package/dist/command/setup/bringup/static-page-server.d.ts +35 -0
- package/dist/command/setup/bringup/static-page-server.js +151 -0
- package/dist/command/setup/desktop-shape.d.ts +4 -0
- package/dist/command/setup/desktop-shape.js +18 -2
- package/dist/command/setup/init/init-host.js +8 -0
- package/dist/command/setup/init/init-port.d.ts +15 -0
- package/dist/command/setup/init/init-port.js +44 -0
- package/dist/command/setup/init/init-runtime.d.ts +1 -1
- package/dist/command/setup/init/init-runtime.js +39 -2
- package/dist/command/setup/node-effects.d.ts +27 -14
- package/dist/command/setup/node-effects.js +80 -44
- package/dist/command/setup/probe/loopback-probe-urls.d.ts +7 -0
- package/dist/command/setup/probe/loopback-probe-urls.js +40 -0
- package/dist/command/setup/probe/page-probe.d.ts +13 -2
- package/dist/command/setup/probe/page-probe.js +18 -3
- package/dist/command/setup/run-setup.d.ts +26 -1
- package/dist/command/setup/run-setup.js +96 -13
- package/dist/command/setup/session-pick.d.ts +13 -0
- package/dist/command/setup/session-pick.js +20 -0
- package/dist/command/setup/setup-command.d.ts +2 -14
- package/dist/command/setup/setup-command.js +105 -31
- package/dist/command/setup/setup-mcp.js +3 -3
- package/dist/command/setup/terminal/wait-progress.d.ts +8 -0
- package/dist/command/setup/terminal/wait-progress.js +17 -0
- package/dist/features/crawl/crawl-empty.js +1 -1
- package/dist/features/crawl/crawl-tools.d.ts +6 -0
- package/dist/features/crawl/crawl-tools.js +57 -0
- package/dist/features/crawl/crawl.d.ts +2 -0
- package/dist/features/crawl/crawl.js +171 -156
- package/dist/features/exhaust/app-coverage.d.ts +33 -0
- package/dist/features/exhaust/app-coverage.js +31 -0
- package/dist/features/exhaust/code-coverage.d.ts +47 -0
- package/dist/features/exhaust/code-coverage.js +77 -0
- package/dist/features/exhaust/exhaust-run.d.ts +33 -0
- package/dist/features/exhaust/exhaust-run.js +82 -0
- package/dist/features/exhaust/explorer.d.ts +103 -0
- package/dist/features/exhaust/explorer.js +195 -0
- package/dist/features/exhaust/failure-branches.d.ts +33 -0
- package/dist/features/exhaust/failure-branches.js +92 -0
- package/dist/features/exhaust/ledger.d.ts +202 -0
- package/dist/features/exhaust/ledger.js +252 -0
- package/dist/features/exhaust/session-fold.d.ts +31 -0
- package/dist/features/exhaust/session-fold.js +79 -0
- package/dist/features/exhaust/session-port.d.ts +36 -0
- package/dist/features/exhaust/session-port.js +102 -0
- package/dist/features/harness/drive-report.d.ts +0 -9
- package/dist/features/harness/drive-report.js +18 -0
- package/dist/features/harness/driver.js +4 -7
- package/dist/features/harness/fill-values.d.ts +6 -0
- package/dist/features/harness/fill-values.js +3 -0
- package/dist/features/harness/jev-driver.js +36 -22
- package/dist/features/harness/openai-driver.js +4 -7
- package/dist/features/harness/platform-config.d.ts +10 -0
- package/dist/features/harness/platform-config.js +13 -6
- package/dist/index.d.ts +10 -1
- package/dist/index.js +78 -104
- package/dist/judgement/capsule/capsule-store.d.ts +31 -2
- package/dist/judgement/capsule/capsule-store.js +58 -6
- package/dist/judgement/capsule/capsule.js +5 -0
- package/dist/judgement/capsule/causal-summary.d.ts +13 -1
- package/dist/judgement/capsule/causal-summary.js +7 -0
- package/dist/judgement/domain/domain-model.js +40 -9
- package/dist/judgement/oracles/self-instrument.js +4 -2
- package/dist/judgement/outcome/assertion-integrity.d.ts +3 -3
- package/dist/judgement/runs/artifact/gap-report.d.ts +17 -0
- package/dist/judgement/runs/artifact/gap-report.js +46 -0
- package/dist/judgement/runs/artifact/gap-summary.d.ts +58 -0
- package/dist/judgement/runs/artifact/gap-summary.js +85 -0
- package/dist/judgement/runs/artifact/render-report.js +4 -1
- package/dist/judgement/runs/artifact/replay-mapping.d.ts +2 -2
- package/dist/judgement/runs/artifact/replay-mapping.js +44 -9
- package/dist/judgement/runs/artifact/to-artifact.d.ts +7 -2
- package/dist/judgement/runs/artifact/to-artifact.js +1 -1
- package/dist/judgement/runs/context-tools.js +21 -3
- package/dist/judgement/runs/drive-run.js +7 -2
- package/dist/judgement/runs/verification-sync.d.ts +3 -1
- package/dist/judgement/runs/verification-sync.js +1 -1
- package/dist/language/flows/annotate-notes/annotate.d.ts +0 -15
- package/dist/language/flows/annotate-notes/annotate.js +66 -18
- package/dist/language/flows/change/affected.d.ts +9 -0
- package/dist/language/flows/change/affected.js +35 -0
- package/dist/language/flows/change/gate.d.ts +20 -0
- package/dist/language/flows/change/gate.js +12 -1
- package/dist/language/flows/flow-anchor.d.ts +12 -0
- package/dist/language/flows/flow-anchor.js +20 -0
- package/dist/language/flows/flow-author.d.ts +7 -0
- package/dist/language/flows/flow-author.js +19 -0
- package/dist/language/flows/flow-expect-grammar.d.ts +2 -2
- package/dist/language/flows/flow-expect-grammar.js +92 -11
- package/dist/language/flows/flow-intent.js +1 -1
- package/dist/language/flows/flow-journey.d.ts +54 -0
- package/dist/language/flows/flow-journey.js +119 -0
- package/dist/language/flows/flow-learning.d.ts +3 -0
- package/dist/language/flows/flow-learning.js +39 -8
- package/dist/language/flows/flow-replay-run.d.ts +33 -12
- package/dist/language/flows/flow-replay-run.js +131 -52
- package/dist/language/flows/flow-replay-types.d.ts +9 -0
- package/dist/language/flows/flow-replay.d.ts +27 -12
- package/dist/language/flows/flow-replay.js +127 -111
- package/dist/language/flows/flow-result.d.ts +2 -0
- package/dist/language/flows/flow-result.js +25 -0
- package/dist/language/flows/flow-step-runners.d.ts +26 -10
- package/dist/language/flows/flow-step-runners.js +20 -4
- package/dist/language/flows/flow-success.d.ts +21 -6
- package/dist/language/flows/flow-success.js +50 -138
- package/dist/language/flows/flow-tools.js +17 -5
- package/dist/language/flows/flows.d.ts +18 -5
- package/dist/language/flows/flows.js +101 -16
- package/dist/language/flows/heal.d.ts +9 -1
- package/dist/language/flows/heal.js +24 -2
- package/dist/language/flows/learned-sources.d.ts +21 -0
- package/dist/language/flows/learned-sources.js +84 -0
- package/dist/language/flows/recording/recording-backtrack.d.ts +0 -7
- package/dist/language/flows/recording/recording-backtrack.js +1 -1
- package/dist/language/flows/recording/replay-halt.d.ts +2 -1
- package/dist/language/flows/recording/replay-halt.js +3 -1
- package/dist/language/flows/recording/tape/recordings.d.ts +31 -2
- package/dist/language/flows/recording/tape/recordings.js +55 -1
- package/dist/language/flows/replay.d.ts +19 -0
- package/dist/language/flows/replay.js +53 -13
- package/dist/language/flows/stores/annotation-store.d.ts +5 -5
- package/dist/language/flows/stores/assertion-tiers-store.js +2 -3
- package/dist/language/flows/stores/flake-store.js +2 -3
- package/dist/language/flows/suite/coverage.d.ts +1 -1
- package/dist/language/flows/suite/parallel-suite.d.ts +2 -1
- package/dist/language/flows/verify-change-tools.js +19 -2
- package/dist/launch-chromium.d.ts +80 -0
- package/dist/launch-chromium.js +218 -0
- package/dist/memory/cloud/cloud-config.d.ts +11 -2
- package/dist/memory/cloud/cloud-config.js +47 -7
- package/dist/memory/cloud/cloud-sync.d.ts +4 -3
- package/dist/memory/cloud/cloud-sync.js +7 -10
- package/dist/memory/cloud/harness-config.d.ts +2 -0
- package/dist/memory/cloud/harness-offer.js +5 -6
- package/dist/memory/cloud/harness-switch.js +5 -6
- package/dist/memory/cloud/sync-cycle.d.ts +78 -1
- package/dist/memory/cloud/sync-cycle.js +321 -53
- package/dist/memory/cloud/sync-daemon.d.ts +13 -0
- package/dist/memory/cloud/sync-daemon.js +45 -20
- package/dist/memory/cloud/sync-disk.js +27 -8
- package/dist/memory/impact/impact-store.js +6 -0
- package/dist/memory/intent/inline-intent.js +8 -3
- package/dist/memory/intent/intent-shard-store.d.ts +33 -47
- package/dist/memory/intent/intent-shard-store.js +252 -140
- package/dist/memory/intent/intent-shard.d.ts +0 -23
- package/dist/memory/intent/intent-shard.js +1 -11
- package/dist/memory/intent/intent-store.d.ts +13 -8
- package/dist/memory/intent/intent-store.js +46 -90
- package/dist/memory/intent/intent-tools.js +34 -23
- package/dist/memory/journal/ambient-store.js +2 -3
- package/dist/memory/journal/deviation-report.js +4 -1
- package/dist/memory/journal/deviation-service.js +10 -2
- package/dist/memory/journal/drive-flow.d.ts +19 -14
- package/dist/memory/journal/drive-flow.js +65 -48
- package/dist/memory/journal/envelope-store.js +2 -3
- package/dist/memory/journal/journal-recorder.d.ts +21 -0
- package/dist/memory/journal/on-disk/retain-policy.d.ts +34 -0
- package/dist/memory/journal/on-disk/retain-policy.js +44 -0
- package/dist/memory/journal/on-disk/retention.d.ts +14 -10
- package/dist/memory/journal/on-disk/retention.js +13 -12
- package/dist/memory/journal/on-disk/startup-maintenance.d.ts +42 -0
- package/dist/memory/journal/on-disk/startup-maintenance.js +14 -0
- package/dist/memory/journal/on-disk/workspace-gitignore.d.ts +0 -11
- package/dist/memory/journal/on-disk/workspace-gitignore.js +6 -45
- package/dist/memory/journal/on-disk/workspace-tiers.d.ts +52 -0
- package/dist/memory/journal/on-disk/workspace-tiers.js +90 -0
- package/dist/memory/journal/route-template.d.ts +22 -0
- package/dist/memory/journal/route-template.js +44 -0
- package/dist/memory/journal/session-end.d.ts +13 -2
- package/dist/memory/journal/session-end.js +88 -18
- package/dist/memory/journal/session-journal.d.ts +8 -1
- package/dist/memory/journal/session-journal.js +10 -0
- package/dist/memory/project/baselines.d.ts +1 -1
- package/dist/memory/project/dir/reticle-dir.d.ts +3 -1
- package/dist/memory/project/dir/reticle-dir.js +1 -0
- package/dist/memory/project/fs/fs-port.js +44 -5
- package/dist/memory/project/fs/write-atomic.d.ts +26 -0
- package/dist/memory/project/fs/write-atomic.js +48 -0
- package/dist/memory/project/project-tools.js +3 -1
- package/dist/memory/recall/prior/connection-memory.d.ts +6 -0
- package/dist/memory/recall/prior/connection-memory.js +8 -0
- package/dist/memory/recall/registered-projects.d.ts +2 -1
- package/dist/portal/bridge/auth-failure-reason.d.ts +1 -1
- package/dist/portal/bridge/auth-failure-reason.js +9 -4
- package/dist/portal/bridge/bridge.d.ts +8 -0
- package/dist/portal/bridge/bridge.js +43 -2
- package/dist/portal/input/js-coverage.d.ts +25 -0
- package/dist/portal/input/js-coverage.js +30 -0
- package/dist/portal/input/real-input.d.ts +11 -0
- package/dist/portal/input/real-input.js +31 -5
- package/dist/portal/pool/browser-pool.d.ts +10 -0
- package/dist/portal/pool/browser-pool.js +10 -0
- package/dist/portal/pool/playwright-launcher.d.ts +6 -0
- package/dist/portal/pool/playwright-launcher.js +21 -13
- package/dist/portal/pool/pool-contract.d.ts +2 -0
- package/dist/portal/pool/storage-seed.d.ts +2 -1
- package/dist/portal/pool/zero-install.d.ts +27 -0
- package/dist/portal/pool/zero-install.js +54 -0
- package/dist/portal/realm/web-realm.js +6 -0
- package/dist/portal/session/dev-server/dev-command.d.ts +2 -2
- package/dist/portal/session/dev-server/dev-command.js +30 -12
- package/dist/portal/session/dev-server/dev-server-probe.d.ts +2 -1
- package/dist/portal/session/dev-server/dev-server-probe.js +17 -1
- package/dist/portal/session/dev-server/route-status-probe.d.ts +24 -0
- package/dist/portal/session/dev-server/route-status-probe.js +57 -0
- package/dist/portal/session/dev-server/served-document.d.ts +21 -0
- package/dist/portal/session/dev-server/served-document.js +57 -0
- package/dist/portal/session/facts/background-traffic.d.ts +9 -0
- package/dist/portal/session/facts/background-traffic.js +25 -0
- package/dist/portal/session/facts/observed-state.d.ts +11 -0
- package/dist/portal/session/facts/observed-state.js +34 -0
- package/dist/portal/session/live-control-tools.js +48 -3
- package/dist/portal/session/no-session-diagnosis.d.ts +29 -0
- package/dist/portal/session/no-session-diagnosis.js +62 -5
- package/dist/portal/session/no-session-next-action.d.ts +13 -0
- package/dist/portal/session/no-session-next-action.js +48 -9
- package/dist/portal/session/no-session-watch.d.ts +18 -0
- package/dist/portal/session/no-session-watch.js +88 -2
- package/dist/portal/session/presence/attachment-history.d.ts +13 -2
- package/dist/portal/session/session-health.d.ts +61 -0
- package/dist/portal/session/session-health.js +43 -3
- package/dist/portal/session/session-info.d.ts +6 -0
- package/dist/portal/session/session-manager.d.ts +29 -6
- package/dist/portal/session/session-manager.js +42 -5
- package/dist/portal/session/session-reaper.d.ts +28 -0
- package/dist/portal/session/session-reaper.js +49 -1
- package/dist/portal/session/session-successor.d.ts +11 -0
- package/dist/portal/session/session-successor.js +14 -1
- package/dist/portal/session/session-verdict-facts.d.ts +28 -0
- package/dist/portal/session/session-verdict-facts.js +15 -0
- package/dist/portal/session/session.d.ts +10 -0
- package/dist/portal/session/session.js +34 -26
- package/dist/surface/http-server.d.ts +2 -1
- package/dist/surface/http-server.js +4 -2
- package/dist/surface/mcp/mcp-proxy.d.ts +2 -2
- package/dist/surface/mcp/mcp-proxy.js +5 -5
- package/dist/surface/mcp/mcp.js +4 -2
- package/dist/surface/mcp/proxy/proxy-daemon-probe.d.ts +13 -1
- package/dist/surface/mcp/proxy/proxy-daemon-probe.js +27 -2
- package/dist/surface/mcp/proxy-idle-exit.d.ts +2 -1
- package/dist/surface/mcp/server-instructions.js +21 -8
- package/dist/surface/tools/act/after-match.d.ts +17 -0
- package/dist/surface/tools/act/after-match.js +30 -0
- package/dist/surface/tools/act/already-true.d.ts +40 -0
- package/dist/surface/tools/act/already-true.js +36 -0
- package/dist/surface/tools/act/durable.d.ts +69 -0
- package/dist/surface/tools/act/durable.js +139 -0
- package/dist/surface/tools/act/navigation/navigate-arrival.d.ts +19 -6
- package/dist/surface/tools/act/navigation/navigate-arrival.js +60 -10
- package/dist/surface/tools/act/navigation/navigate-result.d.ts +1 -12
- package/dist/surface/tools/act/navigation/navigate-result.js +21 -3
- package/dist/surface/tools/act/proved-controls.d.ts +9 -0
- package/dist/surface/tools/act/proved-controls.js +11 -0
- package/dist/surface/tools/act/sequence-grade.d.ts +5 -0
- package/dist/surface/tools/act/sequence-grade.js +32 -1
- package/dist/surface/tools/act/settle-in-flight.d.ts +6 -2
- package/dist/surface/tools/act/settle-in-flight.js +29 -6
- package/dist/surface/tools/act-capsule.d.ts +7 -0
- package/dist/surface/tools/act-capsule.js +68 -8
- package/dist/surface/tools/act-sequence-tool.js +25 -8
- package/dist/surface/tools/act-tools.js +51 -74
- package/dist/surface/tools/args/numeric-bounds.d.ts +15 -2
- package/dist/surface/tools/args/numeric-bounds.js +30 -3
- package/dist/surface/tools/assert/assert-grade.js +26 -3
- package/dist/surface/tools/assert/assert-verdict.js +32 -6
- package/dist/surface/tools/browser-tools.js +17 -4
- package/dist/surface/tools/coverage-tools.js +27 -1
- package/dist/surface/tools/dynamic-tools.js +6 -0
- package/dist/surface/tools/error-recovery.js +4 -4
- package/dist/surface/tools/feedback-invite.js +6 -4
- package/dist/surface/tools/feedback-tools.d.ts +2 -1
- package/dist/surface/tools/feedback-tools.js +1 -1
- package/dist/surface/tools/harness-explore.d.ts +3 -2
- package/dist/surface/tools/harness-explore.js +7 -6
- package/dist/surface/tools/invoke-tool.js +17 -4
- package/dist/surface/tools/lease-hint.d.ts +8 -0
- package/dist/surface/tools/lease-hint.js +10 -0
- package/dist/surface/tools/lease-tools.d.ts +16 -28
- package/dist/surface/tools/lease-tools.js +68 -11
- package/dist/surface/tools/lineage-tools.js +1 -1
- package/dist/surface/tools/live-call-text.js +4 -1
- package/dist/surface/tools/observe-tools.js +23 -7
- package/dist/surface/tools/real-input-attempt.d.ts +0 -5
- package/dist/surface/tools/real-input-attempt.js +1 -1
- package/dist/surface/tools/tool-kit.d.ts +5 -0
- package/dist/surface/tools/tool-kit.js +6 -1
- package/dist/surface/tools/tool-surface.js +17 -5
- package/dist/surface/tools/tools.js +68 -29
- package/dist/telemetry/argument-shape.d.ts +0 -2
- package/dist/telemetry/argument-shape.js +0 -4
- package/dist/telemetry/error-class.d.ts +2 -3
- package/dist/telemetry/error-class.js +2 -3
- package/dist/telemetry/feedback-local.d.ts +1 -0
- package/dist/telemetry/feedback-local.js +1 -3
- package/dist/telemetry/feedback.d.ts +1 -1
- package/dist/telemetry/harness-drive.d.ts +0 -2
- package/dist/telemetry/harness-drive.js +0 -4
- package/dist/telemetry/hud-metrics.d.ts +32 -0
- package/dist/telemetry/hud-metrics.js +95 -0
- package/dist/telemetry/session-metrics.d.ts +5 -0
- package/dist/telemetry/session-metrics.js +15 -1
- package/dist/wire-journal.d.ts +34 -0
- package/dist/wire-journal.js +104 -0
- package/dist/workspace-packages.d.ts +0 -2
- package/dist/workspace-packages.js +1 -9
- package/docs/README.md +5 -2
- package/docs/actions.mdx +3 -3
- package/docs/agent-cheatsheet.md +4 -4
- package/docs/architecture.md +6 -6
- package/docs/best-practices.mdx +1 -1
- package/docs/capabilities.mdx +32 -1
- package/docs/cli/cloud.mdx +21 -9
- package/docs/cli/doctor.mdx +4 -0
- package/docs/cli/feedback.mdx +2 -2
- package/docs/cli/gate.mdx +9 -4
- package/docs/cli/init.mdx +1 -0
- package/docs/cli/report.mdx +44 -0
- package/docs/cli/update.mdx +10 -0
- package/docs/cli.mdx +9 -0
- package/docs/desktop.mdx +14 -10
- package/docs/environment.mdx +87 -0
- package/docs/faq.mdx +18 -16
- package/docs/first-drive.md +5 -13
- package/docs/flows.md +32 -2
- package/docs/frameworks.mdx +140 -9
- package/docs/getting-started.md +3 -3
- package/docs/http-transport.md +6 -3
- package/docs/human-control.md +2 -2
- package/docs/index.mdx +16 -6
- package/docs/install-agentic.mdx +37 -1
- package/docs/install-manual.mdx +1 -1
- package/docs/integration-patterns.md +3 -3
- package/docs/multi-agent-testing.md +1 -1
- package/docs/packages/babel-plugin.mdx +1 -1
- package/docs/packages/browser.mdx +2 -2
- package/docs/packages/react.mdx +2 -2
- package/docs/packages/tauri.mdx +1 -1
- package/docs/packages/vite-plugin.mdx +2 -2
- package/docs/packages.mdx +9 -9
- package/docs/predicates.mdx +176 -7
- package/docs/quickstart.mdx +2 -2
- package/docs/recipes.mdx +2 -2
- package/docs/state-management.mdx +2 -2
- package/docs/system-map.md +1 -1
- package/docs/telemetry.md +1 -1
- package/docs/token-efficiency.md +3 -3
- package/docs/tools/act-and-wait.mdx +1 -0
- package/docs/tools/act-sequence.mdx +1 -1
- package/docs/tools/console.mdx +1 -1
- package/docs/tools/inspect.mdx +1 -1
- package/docs/tools/network.mdx +1 -1
- package/docs/tools/query.mdx +11 -11
- package/docs/tools/sessions.mdx +1 -1
- package/docs/tools/snapshot.mdx +1 -1
- package/docs/tools/state.mdx +1 -1
- package/docs/tools/wait-for.mdx +1 -1
- package/docs/usage.md +8 -7
- package/docs/use-cases.mdx +157 -0
- package/docs/vs-screenshots.mdx +3 -3
- package/docs/what-is-recorded.md +62 -0
- package/docs/what-is-reticle.mdx +17 -20
- package/docs/why-reticle.mdx +14 -21
- package/package.json +11 -10
- package/dist/judgement/capsule/minimize.d.ts +0 -12
- package/dist/judgement/capsule/minimize.js +0 -22
- package/dist/judgement/outcome/predicate-to-expect.d.ts +0 -44
- package/dist/judgement/outcome/predicate-to-expect.js +0 -148
- package/dist/judgement/phenomena/phenomena.d.ts +0 -26
- package/dist/judgement/phenomena/phenomena.js +0 -101
- package/dist/language/flows/flow-report.d.ts +0 -22
- package/dist/language/flows/flow-report.js +0 -106
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { REPLAY_PROGRAM_VERSION, asFlowName } from '@reticlehq/core';
|
|
1
|
+
import { ActionType, PredicateKind, QueryBy, REPLAY_PROGRAM_VERSION, asFlowName, defaultIsSensitiveKey, } from '@reticlehq/core';
|
|
2
|
+
import { clauseOfKind } from '@reticlehq/core';
|
|
2
3
|
/**
|
|
3
4
|
* What a session drove, saved as a replayable flow — without anybody asking for it.
|
|
4
5
|
*
|
|
@@ -29,67 +30,62 @@ const segment = (value, max) => value
|
|
|
29
30
|
* DOM-shaped claims are the weakest. Naming a flow after the strongest thing it proves is the same
|
|
30
31
|
* judgement the honesty grade makes, reused where a human reads it.
|
|
31
32
|
*
|
|
32
|
-
* Returns '' when a step claims nothing nameable
|
|
33
|
+
* Returns '' when a step claims nothing nameable.
|
|
33
34
|
*/
|
|
34
35
|
function provedSlug(steps) {
|
|
35
36
|
for (const step of steps) {
|
|
36
37
|
const expect = step.expect;
|
|
37
38
|
if (expect === undefined)
|
|
38
39
|
continue;
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
40
|
+
// A predicate names its kind, so the slug reads the tree rather than probing a struct. The
|
|
41
|
+
// signal wins over the request for the same reason it always did: it is the app saying what it
|
|
42
|
+
// did, which is the strongest thing a name can carry.
|
|
43
|
+
const signal = clauseOfKind(expect, PredicateKind.SIGNAL);
|
|
44
|
+
if (signal?.name !== undefined)
|
|
45
|
+
return segment(signal.name, 32);
|
|
46
|
+
const netClause = clauseOfKind(expect, PredicateKind.NET);
|
|
47
|
+
if (netClause !== undefined) {
|
|
48
|
+
const method = segment(netClause.method ?? '', 6);
|
|
49
|
+
const where = segment(netClause.urlContains ?? '', 24);
|
|
44
50
|
const joined = [method, where].filter((part) => '' !== part).join('-');
|
|
45
51
|
if ('' !== joined)
|
|
46
52
|
return joined;
|
|
47
53
|
}
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
if (
|
|
53
|
-
return segment(
|
|
54
|
+
const state = clauseOfKind(expect, PredicateKind.STATE);
|
|
55
|
+
if (state !== undefined)
|
|
56
|
+
return segment(state.path, 32);
|
|
57
|
+
const route = clauseOfKind(expect, PredicateKind.ROUTE);
|
|
58
|
+
if (route?.pathname !== undefined)
|
|
59
|
+
return segment(route.pathname, 24);
|
|
60
|
+
const text = clauseOfKind(expect, PredicateKind.TEXT);
|
|
61
|
+
if (text?.contains !== undefined)
|
|
62
|
+
return segment(text.contains, 24);
|
|
54
63
|
}
|
|
55
64
|
return '';
|
|
56
65
|
}
|
|
66
|
+
/** Every auto-saved flow's name starts here, which is what lets a merge tell them from hand-named ones. */
|
|
67
|
+
export const DRIVE_FLOW_PREFIX = 'drive-';
|
|
57
68
|
/**
|
|
58
|
-
* A
|
|
69
|
+
* A readable name for a journey, led by WHY it was driven.
|
|
59
70
|
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
* because `presenter-controls.ts` renders it straight into the replay button's `textContent` — so
|
|
64
|
-
* the label a person actually sees was `drive-sdc991872-6d66-4adf-8780-f931c62905f9`, twice, for two
|
|
65
|
-
* different journeys. A session id is a fact about the socket, not about the feature.
|
|
71
|
+
* The agent's own `intent` on the action is the best name there is — it is the sentence a person
|
|
72
|
+
* would use — so it leads when one was declared. Without one, what the journey proved names it (the
|
|
73
|
+
* strongest claim first), and the route follows so two journeys stay apart.
|
|
66
74
|
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
75
|
+
* No session id. It used to trail every name so two sessions never overwrote each other, which also
|
|
76
|
+
* made every session's drive of the same journey its own file. Two DIFFERENT journeys that land on
|
|
77
|
+
* one name are now told apart at save by their shape, and two copies of the SAME journey merge —
|
|
78
|
+
* see FlowStore.saveJourney.
|
|
70
79
|
*/
|
|
71
|
-
export function driveFlowName(
|
|
72
|
-
const
|
|
73
|
-
const
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
// Minted here, not at the call site. This function is where a drive's flow name comes into
|
|
81
|
-
// existence, and every character it can emit has just been forced through the safe-segment
|
|
82
|
-
// replacements above — so this IS the validated boundary the brand is meant to be created at.
|
|
83
|
-
// Returning `string` pushed a cast onto each caller, which is the hole `flowPath`'s parameter
|
|
84
|
-
// type was added to close.
|
|
85
|
-
// Nothing nameable was proved, so keep the name this function has always produced.
|
|
86
|
-
if ('' === proved) {
|
|
87
|
-
return asFlowName(0 === leg.length ? `drive-${safe}` : `drive-${safe}-${leg}`);
|
|
88
|
-
}
|
|
89
|
-
// Enough of the session to keep two of them apart, and little enough to read past.
|
|
90
|
-
const discriminator = safe.replace(/-/g, '').slice(0, 8);
|
|
91
|
-
const parts = ['drive', proved, ...(0 === leg.length ? [] : [leg]), discriminator];
|
|
92
|
-
return asFlowName(parts.join('-'));
|
|
80
|
+
export function driveFlowName(route, steps = []) {
|
|
81
|
+
const intent = steps.find((step) => step.intent !== undefined)?.intent;
|
|
82
|
+
const leg = segment(route ?? '', 24);
|
|
83
|
+
const why = intent === undefined ? '' : segment(intent, 48);
|
|
84
|
+
const lead = '' !== why ? why : provedSlug(steps);
|
|
85
|
+
// Minted here: every character has just been forced through `segment`, so this IS the validated
|
|
86
|
+
// boundary the brand is meant to be created at.
|
|
87
|
+
const parts = [lead, ...('' !== why || 0 === leg.length ? [] : [leg])].filter((p) => '' !== p);
|
|
88
|
+
return asFlowName(`${DRIVE_FLOW_PREFIX}${0 === parts.length ? 'journey' : parts.join('-')}`);
|
|
93
89
|
}
|
|
94
90
|
/**
|
|
95
91
|
* Turn this session's ambient tape into a flow, when it is worth one.
|
|
@@ -116,18 +112,39 @@ export function driveFlowName(sessionId, route, steps = []) {
|
|
|
116
112
|
*/
|
|
117
113
|
function segmentsByRoute(steps) {
|
|
118
114
|
const out = [];
|
|
115
|
+
let signedIn = false;
|
|
119
116
|
for (const step of steps) {
|
|
120
117
|
const last = out.at(-1);
|
|
121
|
-
|
|
118
|
+
const previous = last?.steps.at(-1);
|
|
119
|
+
// The same journey when the route is unchanged, or when the previous ACTION moved the app here
|
|
120
|
+
// — cart → shipping → payment is one flow, and cutting it left three fragments that each
|
|
121
|
+
// proved a page and none proved the journey. Not after a sign-in: that is the cut this exists
|
|
122
|
+
// for, and a login that navigates onward must still not open the next flow.
|
|
123
|
+
const caused = previous !== undefined && step.route !== undefined && previous.endPage === step.route;
|
|
124
|
+
if (last !== undefined && (last.route === step.route || (caused && !signedIn))) {
|
|
122
125
|
last.steps.push(step);
|
|
126
|
+
signedIn ||= typesASecret(step);
|
|
123
127
|
continue;
|
|
124
128
|
}
|
|
129
|
+
signedIn = typesASecret(step);
|
|
125
130
|
out.push({ ...(step.route === undefined ? {} : { route: step.route }), steps: [step] });
|
|
126
131
|
}
|
|
127
132
|
return out;
|
|
128
133
|
}
|
|
134
|
+
/**
|
|
135
|
+
* A fill into a field the network channel would redact — the sign-in a journey must not carry.
|
|
136
|
+
* Read off the recorder's anchor args: a testid names the field by `value`, a role by `name`.
|
|
137
|
+
*/
|
|
138
|
+
function typesASecret(step) {
|
|
139
|
+
const action = step.args['action'];
|
|
140
|
+
if (ActionType.FILL !== action && ActionType.TYPE !== action)
|
|
141
|
+
return false;
|
|
142
|
+
const by = step.args['by'];
|
|
143
|
+
const field = QueryBy.TESTID === by ? step.args['value'] : step.args['name'];
|
|
144
|
+
return 'string' === typeof field && defaultIsSensitiveKey(field);
|
|
145
|
+
}
|
|
129
146
|
/** A flow per journey, so two journeys in one session are two regression tests rather than one. */
|
|
130
|
-
export function driveFlowsFrom(
|
|
147
|
+
export function driveFlowsFrom(tape) {
|
|
131
148
|
if (tape === undefined || 0 === tape.steps.length)
|
|
132
149
|
return { programs: [], outcome: {} };
|
|
133
150
|
const programs = [];
|
|
@@ -139,7 +156,7 @@ export function driveFlowsFrom(sessionId, tape) {
|
|
|
139
156
|
}
|
|
140
157
|
const startPath = segment.route ?? tape.startPath;
|
|
141
158
|
programs.push({
|
|
142
|
-
name: driveFlowName(
|
|
159
|
+
name: driveFlowName(startPath, segment.steps),
|
|
143
160
|
version: REPLAY_PROGRAM_VERSION,
|
|
144
161
|
// The route is recorder-internal and has no business on disk — `startPath` is where the
|
|
145
162
|
// on-disk flow says the same thing, in the field replay actually reads.
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
+
import { writeFileAtomic } from '../project/fs/write-atomic.js';
|
|
2
3
|
import { dirname } from 'node:path';
|
|
3
4
|
import { reticleDirPaths } from '../project/dir/reticle-dir.js';
|
|
4
5
|
import { RouteEnvelopeSchema } from './envelope.js';
|
|
@@ -54,8 +55,6 @@ export class EnvelopeStore {
|
|
|
54
55
|
routes[route] = envelope;
|
|
55
56
|
const body = `${JSON.stringify({ version: ENVELOPE_FILE_VERSION, routes }, null, 2)}\n`;
|
|
56
57
|
// Atomic publish: write a temp sibling then rename, so a crash never leaves a half-written baseline.
|
|
57
|
-
|
|
58
|
-
await this.#fs.writeFile(tmp, body);
|
|
59
|
-
await this.#fs.rename(tmp, this.#path);
|
|
58
|
+
await writeFileAtomic(this.#fs, this.#path, body);
|
|
60
59
|
}
|
|
61
60
|
}
|
|
@@ -53,6 +53,27 @@ export interface JournalReader {
|
|
|
53
53
|
* only ever about writes that never happened.
|
|
54
54
|
*/
|
|
55
55
|
readWriteLoss?(): Promise<JournalWriteLoss | undefined>;
|
|
56
|
+
/**
|
|
57
|
+
* How big the event ledger is right now, and the ceiling it is measured against.
|
|
58
|
+
*
|
|
59
|
+
* Synchronous and cached: the size is already tracked for the cap, so reporting it costs a field
|
|
60
|
+
* read. Optional on the same reasoning as `readActions` -- a partial double declares no size,
|
|
61
|
+
* and a caller reads that as "not measured" rather than "empty".
|
|
62
|
+
*
|
|
63
|
+
* It is reported at all because nothing reported it. A repeating uncaught error wrote one
|
|
64
|
+
* session's ledger without limit, and neither the daemon nor
|
|
65
|
+
* `doctor` nor any health counter said a word; the user found out when the disk was full (#986).
|
|
66
|
+
* The cap that now bounds this is only half an answer -- a ledger silently approaching it is
|
|
67
|
+
* still a dev tool writing hard to somebody's repo directory.
|
|
68
|
+
*/
|
|
69
|
+
ledger?(): JournalLedgerSize | undefined;
|
|
70
|
+
}
|
|
71
|
+
/** The event ledger's size on disk against the ceiling that will refuse it. */
|
|
72
|
+
export interface JournalLedgerSize {
|
|
73
|
+
/** Bytes of `events.jsonl` on disk. */
|
|
74
|
+
bytes: number;
|
|
75
|
+
/** The ceiling that refuses the next batch past it. */
|
|
76
|
+
capBytes: number;
|
|
56
77
|
}
|
|
57
78
|
interface JournalRecorderOptions {
|
|
58
79
|
/** Injected elapsed-ms clock (never read from Date here — the clock-injection rule). */
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a project is willing to keep.
|
|
3
|
+
*
|
|
4
|
+
* The only answer `.reticle.json` used to offer a full directory was `journal: false` — turning off
|
|
5
|
+
* the thing the product is FOR, and, until the sweep was ungated, the one setting under which
|
|
6
|
+
* nothing was deleted either. The bounds themselves were four constants no user could reach.
|
|
7
|
+
*
|
|
8
|
+
* `0` is a real answer and means keep none, which the count bounds already do for free: a sweep that
|
|
9
|
+
* keeps zero directories deletes all of them. It is not the same as "unset", which is why every
|
|
10
|
+
* field is read explicitly rather than with `??` over a falsy value.
|
|
11
|
+
*/
|
|
12
|
+
export interface RetainPolicy {
|
|
13
|
+
/** Session journal directories under `sessions/`. */
|
|
14
|
+
sessions: number;
|
|
15
|
+
/** Overlay diffs under `visual/`. Baselines are memory and are never counted here. */
|
|
16
|
+
visual: number;
|
|
17
|
+
/** Local copies of reports the outbox already carries. */
|
|
18
|
+
feedback: number;
|
|
19
|
+
/** The TOTAL the evidence tier may occupy, in bytes. Stated in the file as `budgetMb`. */
|
|
20
|
+
budgetBytes: number;
|
|
21
|
+
}
|
|
22
|
+
export declare const DEFAULT_RETAIN: RetainPolicy;
|
|
23
|
+
/**
|
|
24
|
+
* Read the `retain` block out of an already-parsed `.reticle.json`.
|
|
25
|
+
*
|
|
26
|
+
* Takes the record rather than a path: the config search belongs to the caller that already does it
|
|
27
|
+
* once at startup, and reaching for it from here would point the retention domain at the
|
|
28
|
+
* command-line surface for a single lookup.
|
|
29
|
+
*
|
|
30
|
+
* Never throws, and falls back PER FIELD. A project that mistypes one bound keeps the other three —
|
|
31
|
+
* the alternative silently reverts every bound the user did set, which is the same class of quiet
|
|
32
|
+
* disagreement the whole partition of `.reticle/` exists to prevent.
|
|
33
|
+
*/
|
|
34
|
+
export declare function readRetainPolicy(config: Record<string, unknown> | undefined): RetainPolicy;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { DEFAULT_DIFF_RETENTION, DEFAULT_EVIDENCE_BUDGET_BYTES, DEFAULT_FEEDBACK_RETENTION, DEFAULT_SESSION_RETENTION, } from './retention.js';
|
|
2
|
+
/** The key a project writes in `.reticle.json`. */
|
|
3
|
+
const RETAIN_KEY = 'retain';
|
|
4
|
+
const BYTES_PER_MB = 1024 * 1024;
|
|
5
|
+
export const DEFAULT_RETAIN = {
|
|
6
|
+
sessions: DEFAULT_SESSION_RETENTION,
|
|
7
|
+
visual: DEFAULT_DIFF_RETENTION,
|
|
8
|
+
feedback: DEFAULT_FEEDBACK_RETENTION,
|
|
9
|
+
budgetBytes: DEFAULT_EVIDENCE_BUDGET_BYTES,
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
* A count a sweep can act on, or `undefined` for anything else.
|
|
13
|
+
*
|
|
14
|
+
* Negative and fractional are refused rather than coerced. "Keep -1 sessions" and "keep 1.5" are
|
|
15
|
+
* both a user saying something they did not mean, and a sweep that guesses at what they meant is
|
|
16
|
+
* deleting files on a guess.
|
|
17
|
+
*/
|
|
18
|
+
function count(value) {
|
|
19
|
+
return 'number' === typeof value && Number.isInteger(value) && value >= 0 ? value : undefined;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Read the `retain` block out of an already-parsed `.reticle.json`.
|
|
23
|
+
*
|
|
24
|
+
* Takes the record rather than a path: the config search belongs to the caller that already does it
|
|
25
|
+
* once at startup, and reaching for it from here would point the retention domain at the
|
|
26
|
+
* command-line surface for a single lookup.
|
|
27
|
+
*
|
|
28
|
+
* Never throws, and falls back PER FIELD. A project that mistypes one bound keeps the other three —
|
|
29
|
+
* the alternative silently reverts every bound the user did set, which is the same class of quiet
|
|
30
|
+
* disagreement the whole partition of `.reticle/` exists to prevent.
|
|
31
|
+
*/
|
|
32
|
+
export function readRetainPolicy(config) {
|
|
33
|
+
const raw = config?.[RETAIN_KEY];
|
|
34
|
+
if ('object' !== typeof raw || null === raw || Array.isArray(raw))
|
|
35
|
+
return DEFAULT_RETAIN;
|
|
36
|
+
const retain = raw;
|
|
37
|
+
const mb = count(retain['budgetMb']);
|
|
38
|
+
return {
|
|
39
|
+
sessions: count(retain['sessions']) ?? DEFAULT_RETAIN.sessions,
|
|
40
|
+
visual: count(retain['visual']) ?? DEFAULT_RETAIN.visual,
|
|
41
|
+
feedback: count(retain['feedback']) ?? DEFAULT_RETAIN.feedback,
|
|
42
|
+
budgetBytes: mb === undefined ? DEFAULT_RETAIN.budgetBytes : mb * BYTES_PER_MB,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
@@ -16,9 +16,10 @@ export declare const DEFAULT_FEEDBACK_RETENTION = 20;
|
|
|
16
16
|
* and feedback copies together.
|
|
17
17
|
*
|
|
18
18
|
* Every bound above this line counts things, and a count cannot see a size: twenty session
|
|
19
|
-
* directories is twenty UNBOUNDED directories. A
|
|
20
|
-
* three counts being honoured the whole time, because one drive against a chatty app
|
|
21
|
-
* response bodies and DOM text for as long as the drive lasts, and nothing was looking at
|
|
19
|
+
* directories is twenty UNBOUNDED directories. A workspace in the field grew to multiple gigabytes
|
|
20
|
+
* with all three counts being honoured the whole time, because one drive against a chatty app
|
|
21
|
+
* appends response bodies and DOM text for as long as the drive lasts, and nothing was looking at
|
|
22
|
+
* the total.
|
|
22
23
|
*
|
|
23
24
|
* Why this number and not one an order of magnitude either side. An ordinary session journal is
|
|
24
25
|
* hundreds of kilobytes to a few megabytes; a long drive against an app with large JSON responses
|
|
@@ -40,11 +41,11 @@ export declare const DEFAULT_EVIDENCE_BUDGET_BYTES: number;
|
|
|
40
41
|
/**
|
|
41
42
|
* One candidate entry inside a `.reticle/`, classified by the top-level directory it lives under.
|
|
42
43
|
*
|
|
43
|
-
* `under` is what decides the tier, and it is
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
44
|
+
* `under` is what decides the tier, and it is looked up in the workspace tier table's `durability`
|
|
45
|
+
* axis — never in the gitignore's, which answers a different question. Anything not EVIDENCE is
|
|
46
|
+
* MEMORY: flows, capsules, baselines, the contract, the intent ledger. Those are small, durable
|
|
47
|
+
* and the reason the directory exists, so they are never evicted, never counted, and must keep
|
|
48
|
+
* being written however full the disk is.
|
|
48
49
|
*/
|
|
49
50
|
export interface TierEntry {
|
|
50
51
|
/** Absolute path, and what is removed. */
|
|
@@ -73,8 +74,6 @@ export declare function selectPrunable(dirs: readonly DatedDir[], retention: num
|
|
|
73
74
|
export interface RetentionOptions {
|
|
74
75
|
/** How many session directories survive the count bound. */
|
|
75
76
|
retention?: number;
|
|
76
|
-
/** The TOTAL the evidence tier may occupy. */
|
|
77
|
-
budgetBytes?: number;
|
|
78
77
|
/**
|
|
79
78
|
* The ids of sessions that are STILL OPEN. Neither bound may evict one.
|
|
80
79
|
*
|
|
@@ -94,6 +93,11 @@ export interface RetentionOptions {
|
|
|
94
93
|
* so a stat/rm failure on one dir (or a missing sessions/ dir) is swallowed, never crashing a session.
|
|
95
94
|
* Mirrors RunStore's amortized pruning; run on daemon start and at the end of every session.
|
|
96
95
|
*
|
|
96
|
+
* The COUNT bound only. This used to sweep the tier total as well, from when it was the single
|
|
97
|
+
* sweep both call sites used; `pruneWorkspace` is that single place now and runs the budget last,
|
|
98
|
+
* on purpose, so the leftover call here was a second walk of every evidence entry on every teardown
|
|
99
|
+
* — and it ran FIRST, sizing everything the counts were about to delete anyway.
|
|
100
|
+
*
|
|
97
101
|
* An open session is excluded from BOTH bounds — see `RetentionOptions.live`.
|
|
98
102
|
*
|
|
99
103
|
* ponytail: excluded via `keep`, so an open session is not COUNTED toward the retention cap either.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { basename, join } from 'node:path';
|
|
2
2
|
import { ReticleDir } from '@reticlehq/core';
|
|
3
3
|
import { reticleDirPaths, visualDir } from '../../project/dir/reticle-dir.js';
|
|
4
|
-
import {
|
|
4
|
+
import { EVIDENCE_DIRS } from './workspace-tiers.js';
|
|
5
5
|
/** Keep at most this many session journals on disk; older ones are pruned by recency. */
|
|
6
6
|
export const DEFAULT_SESSION_RETENTION = 20;
|
|
7
7
|
/**
|
|
@@ -21,9 +21,10 @@ const PNG_SUFFIX = '.png';
|
|
|
21
21
|
* and feedback copies together.
|
|
22
22
|
*
|
|
23
23
|
* Every bound above this line counts things, and a count cannot see a size: twenty session
|
|
24
|
-
* directories is twenty UNBOUNDED directories. A
|
|
25
|
-
* three counts being honoured the whole time, because one drive against a chatty app
|
|
26
|
-
* response bodies and DOM text for as long as the drive lasts, and nothing was looking at
|
|
24
|
+
* directories is twenty UNBOUNDED directories. A workspace in the field grew to multiple gigabytes
|
|
25
|
+
* with all three counts being honoured the whole time, because one drive against a chatty app
|
|
26
|
+
* appends response bodies and DOM text for as long as the drive lasts, and nothing was looking at
|
|
27
|
+
* the total.
|
|
27
28
|
*
|
|
28
29
|
* Why this number and not one an order of magnitude either side. An ordinary session journal is
|
|
29
30
|
* hundreds of kilobytes to a few megabytes; a long drive against an app with large JSON responses
|
|
@@ -60,7 +61,7 @@ function isOpenSession(entry, live) {
|
|
|
60
61
|
* `selectPrunable` and `pruneByRecency` already split it.
|
|
61
62
|
*/
|
|
62
63
|
export function selectOverBudget(entries, budgetBytes, live = NO_LIVE_SESSIONS) {
|
|
63
|
-
const evidence = entries.filter((entry) =>
|
|
64
|
+
const evidence = entries.filter((entry) => EVIDENCE_DIRS.includes(entry.under));
|
|
64
65
|
// Live sessions still COUNT — their bytes are on disk and the budget is about the disk. They are
|
|
65
66
|
// only excluded from the eviction list, so a session over budget on its own is left alone rather
|
|
66
67
|
// than deleted under its own writer.
|
|
@@ -78,8 +79,6 @@ export function selectOverBudget(entries, budgetBytes, live = NO_LIVE_SESSIONS)
|
|
|
78
79
|
}
|
|
79
80
|
return doomed;
|
|
80
81
|
}
|
|
81
|
-
/** Where refused feedback reports are copied. Named here because the writer lives in another area. */
|
|
82
|
-
const FEEDBACK_SUBDIR = 'feedback';
|
|
83
82
|
/**
|
|
84
83
|
* Pure: given session dirs with their mtimes, the names to remove so only the `retention` most-recent
|
|
85
84
|
* remain. Oldest-first. Exported for testing (no IO).
|
|
@@ -138,6 +137,11 @@ async function pruneByRecency(fs, dir, retention, keep = () => true) {
|
|
|
138
137
|
* so a stat/rm failure on one dir (or a missing sessions/ dir) is swallowed, never crashing a session.
|
|
139
138
|
* Mirrors RunStore's amortized pruning; run on daemon start and at the end of every session.
|
|
140
139
|
*
|
|
140
|
+
* The COUNT bound only. This used to sweep the tier total as well, from when it was the single
|
|
141
|
+
* sweep both call sites used; `pruneWorkspace` is that single place now and runs the budget last,
|
|
142
|
+
* on purpose, so the leftover call here was a second walk of every evidence entry on every teardown
|
|
143
|
+
* — and it ran FIRST, sizing everything the counts were about to delete anyway.
|
|
144
|
+
*
|
|
141
145
|
* An open session is excluded from BOTH bounds — see `RetentionOptions.live`.
|
|
142
146
|
*
|
|
143
147
|
* ponytail: excluded via `keep`, so an open session is not COUNTED toward the retention cap either.
|
|
@@ -148,9 +152,6 @@ export async function pruneSessions(fs, root, options = {}) {
|
|
|
148
152
|
const live = options.live ?? NO_LIVE_SESSIONS;
|
|
149
153
|
const retention = options.retention ?? DEFAULT_SESSION_RETENTION;
|
|
150
154
|
await pruneByRecency(fs, reticleDirPaths(root).sessions, retention, (name) => !live.has(name));
|
|
151
|
-
// The tier total is swept from here because this is the one sweep that runs on BOTH the daemon's
|
|
152
|
-
// start path and the end of every session, and the end of a session is when the bytes arrive.
|
|
153
|
-
await pruneEvidenceBudget(fs, root, options.budgetBytes ?? DEFAULT_EVIDENCE_BUDGET_BYTES, live);
|
|
154
155
|
}
|
|
155
156
|
/**
|
|
156
157
|
* Bound `.reticle/visual/` — the largest thing in the workspace, and the only one with no delete
|
|
@@ -224,7 +225,7 @@ async function sizeOf(fs, path) {
|
|
|
224
225
|
*/
|
|
225
226
|
async function collectEvidence(fs, root) {
|
|
226
227
|
const entries = [];
|
|
227
|
-
for (const under of
|
|
228
|
+
for (const under of EVIDENCE_DIRS) {
|
|
228
229
|
const isVisual = ReticleDir.VISUAL_SUBDIR === under;
|
|
229
230
|
const dirs = isVisual ? await visualDirs(fs, root) : [join(root, under)];
|
|
230
231
|
for (const dir of dirs) {
|
|
@@ -287,5 +288,5 @@ export async function pruneEvidenceBudget(fs, root, budgetBytes = DEFAULT_EVIDEN
|
|
|
287
288
|
* retention rule is the same rule whether the directory holds journals, diffs or reports.
|
|
288
289
|
*/
|
|
289
290
|
export async function pruneFeedback(fs, root, retention = DEFAULT_FEEDBACK_RETENTION) {
|
|
290
|
-
await pruneByRecency(fs, join(root, FEEDBACK_SUBDIR), retention);
|
|
291
|
+
await pruneByRecency(fs, join(root, ReticleDir.FEEDBACK_SUBDIR), retention);
|
|
291
292
|
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Everything Reticle prunes in ONE `.reticle/` workspace, in one place.
|
|
3
|
+
*
|
|
4
|
+
* Called from two moments and both matter: daemon start (the daemon's own tree) and session end
|
|
5
|
+
* (the tree that SESSION wrote into). Those are usually different directories — a globally
|
|
6
|
+
* registered daemon stands in `$HOME` — and the per-project one is where the bytes actually
|
|
7
|
+
* accumulate, so sweeping only the first swept the wrong tree for the common case.
|
|
8
|
+
*
|
|
9
|
+
* The four bounds were inlined at the call site and one of them was simply missing:
|
|
10
|
+
* `pruneEvidenceBudget` had been written, tested, and never called by anything. The three COUNT
|
|
11
|
+
* bounds were wired and the BYTE bound was not, which is the combination the budget's own header
|
|
12
|
+
* warns about -- "twenty session directories is twenty UNBOUNDED directories", beside a field
|
|
13
|
+
* report of a multi-gigabyte `.reticle/sessions` with all three counts honoured throughout.
|
|
14
|
+
*
|
|
15
|
+
* Counts and bytes answer different questions and neither substitutes for the other. A count says
|
|
16
|
+
* how MANY sessions are kept; it says nothing about how large one is allowed to get. A chatty app
|
|
17
|
+
* writes one enormous journal and never trips a count.
|
|
18
|
+
*
|
|
19
|
+
* Gathered into a function rather than left as four statements so that "what maintenance runs at
|
|
20
|
+
* startup" has one answer that can be TESTED. Inlined, the only way to check the budget was wired
|
|
21
|
+
* was to grep the call site for its name -- and a grep passes on a comment that quotes the code,
|
|
22
|
+
* which has already happened in this repository.
|
|
23
|
+
*
|
|
24
|
+
* Order: counts first, budget last. The counts are cheap and bounded; the budget walks and sizes
|
|
25
|
+
* what the counts leave behind, so running it second means it sizes less.
|
|
26
|
+
*
|
|
27
|
+
* NEVER THROWS. Maintenance must not be the reason a daemon fails to come up, and each prune below
|
|
28
|
+
* already swallows its own errors -- this only has to avoid adding a new way to fail.
|
|
29
|
+
*/
|
|
30
|
+
import type { FileSystemPort } from '../../project/fs/fs-port.js';
|
|
31
|
+
import { type RetainPolicy } from './retain-policy.js';
|
|
32
|
+
/**
|
|
33
|
+
* What this sweep is allowed to keep. Every field optional, defaulting to the built-in bound.
|
|
34
|
+
*
|
|
35
|
+
* Partial rather than a whole `RetainPolicy` for two reasons that happen to agree: a test states
|
|
36
|
+
* only the bound it is exercising instead of writing half a gigabyte to disk, and a project's
|
|
37
|
+
* `.reticle.json` states only the bounds it cares about. Same reason the clock is injected
|
|
38
|
+
* everywhere else here — a bound that can only be exercised by producing the real quantity is a
|
|
39
|
+
* bound nothing exercises.
|
|
40
|
+
*/
|
|
41
|
+
export type PruneWorkspaceOptions = Partial<RetainPolicy>;
|
|
42
|
+
export declare function pruneWorkspace(fs: FileSystemPort, root: string, live: ReadonlySet<string>, options?: PruneWorkspaceOptions): Promise<void>;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { pruneEvidenceBudget, pruneFeedback, pruneSessions, pruneVisualDiffs, } from './retention.js';
|
|
2
|
+
import { DEFAULT_RETAIN } from './retain-policy.js';
|
|
3
|
+
export async function pruneWorkspace(fs, root, live, options = {}) {
|
|
4
|
+
const retain = { ...DEFAULT_RETAIN, ...options };
|
|
5
|
+
// Empty in the ordinary case (nothing has connected yet); this path also runs on a daemon that is
|
|
6
|
+
// already serving sessions, which is why `live` is threaded rather than assumed empty.
|
|
7
|
+
await pruneSessions(fs, root, { live, retention: retain.sessions });
|
|
8
|
+
// The largest thing in the workspace, and for a long time the only one with no delete path.
|
|
9
|
+
await pruneVisualDiffs(fs, root, retain.visual);
|
|
10
|
+
// Write-only local copies of reports the outbox already carries.
|
|
11
|
+
await pruneFeedback(fs, root, retain.feedback);
|
|
12
|
+
// LAST, and the one that was missing: the only bound that limits SIZE rather than count.
|
|
13
|
+
await pruneEvidenceBudget(fs, root, retain.budgetBytes, live);
|
|
14
|
+
}
|
|
@@ -1,15 +1,4 @@
|
|
|
1
1
|
import type { FileSystemPort } from '../../project/fs/fs-port.js';
|
|
2
|
-
/**
|
|
3
|
-
* Local directories, without the trailing slash the ignore file wants.
|
|
4
|
-
*
|
|
5
|
-
* Exported because this is also the EVIDENCE tier: the per-session churn that retention is allowed
|
|
6
|
-
* to delete. Read from here rather than restated next to the byte budget — the partition of what
|
|
7
|
-
* `.reticle/` holds already exists, is guarded as exhaustive, and a second copy of it would be the
|
|
8
|
-
* list that silently disagrees.
|
|
9
|
-
*/
|
|
10
|
-
export declare const TRANSIENT_DIRS: readonly string[];
|
|
11
|
-
/** Every name Reticle treats as machine-local, however it is spelled in the ignore file. */
|
|
12
|
-
export declare const TRANSIENT_NAMES: readonly string[];
|
|
13
2
|
/**
|
|
14
3
|
* Write `.reticle/.gitignore` if it is not already there.
|
|
15
4
|
*
|
|
@@ -1,60 +1,21 @@
|
|
|
1
1
|
import { join } from 'node:path';
|
|
2
2
|
import { ReticleDir } from '@reticlehq/core';
|
|
3
|
+
import { LOCAL_DIRS, LOCAL_FILES } from './workspace-tiers.js';
|
|
3
4
|
/**
|
|
4
5
|
* The `.reticle/` entries that are LOCAL state, and must never be committed by accident.
|
|
5
6
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
7
|
+
* Read from the tier table rather than restated here: which half a name belongs to is one decision,
|
|
8
|
+
* and a second copy of it is the list that silently disagrees. This file asks the table exactly one
|
|
9
|
+
* of its two questions — `sharing` — and has no opinion at all about what retention may delete.
|
|
9
10
|
*
|
|
10
11
|
* The reason this matters beyond tidiness: a session journal carries URLs, request and response
|
|
11
12
|
* bodies, and DOM text from the app under test. `.reticle/` is created by the daemon rather than by
|
|
12
13
|
* the user, so it arrives untracked and unexplained, and one `git add -A` puts an app's traffic into
|
|
13
14
|
* a shared repository. Reticle created the directory; the ignore for it is Reticle's to write.
|
|
14
15
|
*/
|
|
15
|
-
/**
|
|
16
|
-
* The local feedback copies — `.reticle/feedback/`.
|
|
17
|
-
*
|
|
18
|
-
* Named here rather than imported: the directory is spelled inline where it is written, in files
|
|
19
|
-
* another agent owns, and the ignore for a directory Reticle creates is Reticle's to write whether
|
|
20
|
-
* or not that name has been hoisted yet.
|
|
21
|
-
*/
|
|
22
|
-
const FEEDBACK_SUBDIR = 'feedback';
|
|
23
|
-
/**
|
|
24
|
-
* Local directories, without the trailing slash the ignore file wants.
|
|
25
|
-
*
|
|
26
|
-
* Exported because this is also the EVIDENCE tier: the per-session churn that retention is allowed
|
|
27
|
-
* to delete. Read from here rather than restated next to the byte budget — the partition of what
|
|
28
|
-
* `.reticle/` holds already exists, is guarded as exhaustive, and a second copy of it would be the
|
|
29
|
-
* list that silently disagrees.
|
|
30
|
-
*/
|
|
31
|
-
export const TRANSIENT_DIRS = [
|
|
32
|
-
ReticleDir.SESSIONS_SUBDIR,
|
|
33
|
-
ReticleDir.RUNS_SUBDIR,
|
|
34
|
-
ReticleDir.VISUAL_SUBDIR,
|
|
35
|
-
// Write-only: a local copy of what was already sent. The outbox is the record.
|
|
36
|
-
FEEDBACK_SUBDIR,
|
|
37
|
-
];
|
|
38
|
-
/** Local files. Exported with the directories as `TRANSIENT_NAMES`, which the partition guard reads. */
|
|
39
|
-
const TRANSIENT_FILES = [
|
|
40
|
-
ReticleDir.PROJECT_FILE,
|
|
41
|
-
ReticleDir.AMBIENT_FILE,
|
|
42
|
-
ReticleDir.ENVELOPES_FILE,
|
|
43
|
-
ReticleDir.FLAKE_FILE,
|
|
44
|
-
ReticleDir.TIERS_FILE,
|
|
45
|
-
// The user's own record of what Reticle did for them, on THIS machine.
|
|
46
|
-
ReticleDir.IMPACT_FILE,
|
|
47
|
-
// This machine's conversation with the server. Its own doc comment says why committing it is
|
|
48
|
-
// harmful: one machine's pull cursor makes every other machine skip what it has not seen.
|
|
49
|
-
ReticleDir.CLOUD_STATE_FILE,
|
|
50
|
-
// Triage pulled back from the dashboard — a cache of somebody's decisions, re-pullable at will.
|
|
51
|
-
ReticleDir.ISSUES_FILE,
|
|
52
|
-
];
|
|
53
|
-
/** Every name Reticle treats as machine-local, however it is spelled in the ignore file. */
|
|
54
|
-
export const TRANSIENT_NAMES = [...TRANSIENT_DIRS, ...TRANSIENT_FILES];
|
|
55
16
|
const TRANSIENT = [
|
|
56
|
-
...
|
|
57
|
-
...
|
|
17
|
+
...LOCAL_DIRS.map((dir) => `${dir}/`),
|
|
18
|
+
...LOCAL_FILES,
|
|
58
19
|
'*.log',
|
|
59
20
|
'*.tmp',
|
|
60
21
|
];
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* May retention delete this to reclaim disk?
|
|
3
|
+
*
|
|
4
|
+
* MEMORY is why `.reticle/` exists — what a flow replays from, what a deviation is measured
|
|
5
|
+
* against. It is small, it is never evicted, and it must keep being written however full the disk
|
|
6
|
+
* is. EVIDENCE is what one drive left behind: journals, run artifacts, pixel diffs. It is large, it
|
|
7
|
+
* ages out, and losing the oldest of it costs nothing anybody can name.
|
|
8
|
+
*/
|
|
9
|
+
export declare const Durability: {
|
|
10
|
+
readonly MEMORY: "memory";
|
|
11
|
+
readonly EVIDENCE: "evidence";
|
|
12
|
+
};
|
|
13
|
+
export type Durability = (typeof Durability)[keyof typeof Durability];
|
|
14
|
+
/**
|
|
15
|
+
* Does this belong in the repository, or only on the machine that wrote it?
|
|
16
|
+
*
|
|
17
|
+
* COMMIT is the shareable half: a flow nobody else can replay is not a regression check. LOCAL is
|
|
18
|
+
* per-machine churn, and journals carry URLs, request and response bodies and page text from the
|
|
19
|
+
* app under test, so keeping them out of a shared repository is worth doing on its own account.
|
|
20
|
+
*/
|
|
21
|
+
export declare const Sharing: {
|
|
22
|
+
readonly COMMIT: "commit";
|
|
23
|
+
readonly LOCAL: "local";
|
|
24
|
+
};
|
|
25
|
+
export type Sharing = (typeof Sharing)[keyof typeof Sharing];
|
|
26
|
+
/** What one `.reticle/` entry is, on both axes at once. */
|
|
27
|
+
export interface WorkspaceTier {
|
|
28
|
+
readonly durability: Durability;
|
|
29
|
+
readonly sharing: Sharing;
|
|
30
|
+
/** A directory is ignored as `name/` and swept entry by entry; a file is neither. */
|
|
31
|
+
readonly isDirectory: boolean;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Every top-level name `.reticle/` can hold, classified on both axes.
|
|
35
|
+
*
|
|
36
|
+
* One list used to answer both questions with one answer, and they are not the same question.
|
|
37
|
+
* `capsules/` is committed — a fail-to-pass capsule a teammate cannot replay is not a regression
|
|
38
|
+
* check — AND it is evidence a drive left behind, which grows without bound. That cell did not
|
|
39
|
+
* exist, so the byte budget, which read the gitignore's list, structurally could not see capsules
|
|
40
|
+
* however large they got. Splitting the axes is what makes that sayable; it does not by itself say
|
|
41
|
+
* it, and `capsules/` stays MEMORY here until the budget is measured against it.
|
|
42
|
+
*
|
|
43
|
+
* Declaration order is the order the ignore file lists them, so the file a user already has on disk
|
|
44
|
+
* and the one written today are the same file.
|
|
45
|
+
*/
|
|
46
|
+
export declare const WORKSPACE_TIERS: Readonly<Record<string, WorkspaceTier>>;
|
|
47
|
+
/** Machine-local directories, without the trailing slash the ignore file wants. */
|
|
48
|
+
export declare const LOCAL_DIRS: readonly string[];
|
|
49
|
+
/** Machine-local files. */
|
|
50
|
+
export declare const LOCAL_FILES: readonly string[];
|
|
51
|
+
/** The directories retention sweeps, ignored or not. The byte budget and the count bounds read this. */
|
|
52
|
+
export declare const EVIDENCE_DIRS: readonly string[];
|