@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
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { ReticleDir } from '@reticlehq/core';
|
|
2
|
+
/**
|
|
3
|
+
* May retention delete this to reclaim disk?
|
|
4
|
+
*
|
|
5
|
+
* MEMORY is why `.reticle/` exists — what a flow replays from, what a deviation is measured
|
|
6
|
+
* against. It is small, it is never evicted, and it must keep being written however full the disk
|
|
7
|
+
* is. EVIDENCE is what one drive left behind: journals, run artifacts, pixel diffs. It is large, it
|
|
8
|
+
* ages out, and losing the oldest of it costs nothing anybody can name.
|
|
9
|
+
*/
|
|
10
|
+
export const Durability = {
|
|
11
|
+
MEMORY: 'memory',
|
|
12
|
+
EVIDENCE: 'evidence',
|
|
13
|
+
};
|
|
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 const Sharing = {
|
|
22
|
+
COMMIT: 'commit',
|
|
23
|
+
LOCAL: 'local',
|
|
24
|
+
};
|
|
25
|
+
const localDir = { durability: Durability.EVIDENCE, sharing: Sharing.LOCAL, isDirectory: true };
|
|
26
|
+
const localFile = { durability: Durability.MEMORY, sharing: Sharing.LOCAL, isDirectory: false };
|
|
27
|
+
const sharedDir = { durability: Durability.MEMORY, sharing: Sharing.COMMIT, isDirectory: true };
|
|
28
|
+
const sharedFile = { durability: Durability.MEMORY, sharing: Sharing.COMMIT, isDirectory: false };
|
|
29
|
+
/**
|
|
30
|
+
* Every top-level name `.reticle/` can hold, classified on both axes.
|
|
31
|
+
*
|
|
32
|
+
* One list used to answer both questions with one answer, and they are not the same question.
|
|
33
|
+
* `capsules/` is committed — a fail-to-pass capsule a teammate cannot replay is not a regression
|
|
34
|
+
* check — AND it is evidence a drive left behind, which grows without bound. That cell did not
|
|
35
|
+
* exist, so the byte budget, which read the gitignore's list, structurally could not see capsules
|
|
36
|
+
* however large they got. Splitting the axes is what makes that sayable; it does not by itself say
|
|
37
|
+
* it, and `capsules/` stays MEMORY here until the budget is measured against it.
|
|
38
|
+
*
|
|
39
|
+
* Declaration order is the order the ignore file lists them, so the file a user already has on disk
|
|
40
|
+
* and the one written today are the same file.
|
|
41
|
+
*/
|
|
42
|
+
export const WORKSPACE_TIERS = {
|
|
43
|
+
[ReticleDir.SESSIONS_SUBDIR]: localDir,
|
|
44
|
+
[ReticleDir.RUNS_SUBDIR]: localDir,
|
|
45
|
+
[ReticleDir.VISUAL_SUBDIR]: localDir,
|
|
46
|
+
// Write-only: a local copy of what was already sent. The outbox is the record.
|
|
47
|
+
[ReticleDir.FEEDBACK_SUBDIR]: localDir,
|
|
48
|
+
[ReticleDir.PROJECT_FILE]: localFile,
|
|
49
|
+
[ReticleDir.AMBIENT_FILE]: localFile,
|
|
50
|
+
[ReticleDir.ENVELOPES_FILE]: localFile,
|
|
51
|
+
[ReticleDir.FLAKE_FILE]: localFile,
|
|
52
|
+
// What THIS machine's drives covered, and the best each level reached — the gate's ratchet.
|
|
53
|
+
[ReticleDir.COVERAGE_FILE]: localFile,
|
|
54
|
+
[ReticleDir.TIERS_FILE]: localFile,
|
|
55
|
+
// The user's own record of what Reticle did for them, on THIS machine.
|
|
56
|
+
[ReticleDir.IMPACT_FILE]: localFile,
|
|
57
|
+
// This machine's conversation with the server. Its own doc comment says why committing it is
|
|
58
|
+
// harmful: one machine's pull cursor makes every other machine skip what it has not seen.
|
|
59
|
+
[ReticleDir.CLOUD_STATE_FILE]: localFile,
|
|
60
|
+
// Triage pulled back from the dashboard — a cache of somebody's decisions, re-pullable at will.
|
|
61
|
+
[ReticleDir.ISSUES_FILE]: localFile,
|
|
62
|
+
[ReticleDir.CONTRACT_FILE]: sharedFile,
|
|
63
|
+
[ReticleDir.FLOWS_SUBDIR]: sharedDir,
|
|
64
|
+
[ReticleDir.BASELINES_SUBDIR]: sharedDir,
|
|
65
|
+
[ReticleDir.CAPSULES_SUBDIR]: sharedDir,
|
|
66
|
+
[ReticleDir.INTENT_FILE]: sharedFile,
|
|
67
|
+
// The sharded form of the same ledger, and shared for the same reason.
|
|
68
|
+
[ReticleDir.INTENT_SUBDIR]: sharedDir,
|
|
69
|
+
[ReticleDir.CLOUD_LINK_FILE]: sharedFile,
|
|
70
|
+
// A hook is a decision the whole team shares, exactly like a package script: one that exists only
|
|
71
|
+
// on the machine that wrote it is a rule nobody else is following. It names COMMANDS and never a
|
|
72
|
+
// credential, so it is safe to commit — the same reasoning that puts `cloud.json` here while its
|
|
73
|
+
// API key stays in ~/.reticle.
|
|
74
|
+
[ReticleDir.HOOKS_FILE]: sharedFile,
|
|
75
|
+
// What a drive types into each labelled field. Committed for two reasons: a replay must send
|
|
76
|
+
// exactly what the recording sent or it is not a replay, and a value a model wrote once should be
|
|
77
|
+
// reviewable and editable by the team rather than regenerated differently on every machine.
|
|
78
|
+
[ReticleDir.FILL_VALUES_FILE]: sharedFile,
|
|
79
|
+
};
|
|
80
|
+
function namesWhere(match) {
|
|
81
|
+
return Object.entries(WORKSPACE_TIERS)
|
|
82
|
+
.filter(([, tier]) => match(tier))
|
|
83
|
+
.map(([name]) => name);
|
|
84
|
+
}
|
|
85
|
+
/** Machine-local directories, without the trailing slash the ignore file wants. */
|
|
86
|
+
export const LOCAL_DIRS = namesWhere((tier) => Sharing.LOCAL === tier.sharing && tier.isDirectory);
|
|
87
|
+
/** Machine-local files. */
|
|
88
|
+
export const LOCAL_FILES = namesWhere((tier) => Sharing.LOCAL === tier.sharing && !tier.isDirectory);
|
|
89
|
+
/** The directories retention sweeps, ignored or not. The byte budget and the count bounds read this. */
|
|
90
|
+
export const EVIDENCE_DIRS = namesWhere((tier) => Durability.EVIDENCE === tier.durability && tier.isDirectory);
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The key an envelope's statistics accumulate under.
|
|
3
|
+
*
|
|
4
|
+
* The envelope map was keyed on the raw `pathname`. On any app whose URLs carry ids — most of them —
|
|
5
|
+
* `/orders/1001` and `/orders/1002` are different keys, so every visit mints a fresh envelope
|
|
6
|
+
* holding one sample.
|
|
7
|
+
*
|
|
8
|
+
* The cost is not the file size. `deviation-report` skips any envelope below
|
|
9
|
+
* `MIN_ENVELOPE_SAMPLES`, so on such an app no envelope ever qualifies, every report answers
|
|
10
|
+
* "envelope too new", and the deviation feature never turns on at all — silently, permanently, with
|
|
11
|
+
* nothing anywhere saying why. The unbounded file and the dead feature are one defect.
|
|
12
|
+
*
|
|
13
|
+
* CONSERVATIVE on purpose. Over-collapsing is the worse error: merging two genuinely different
|
|
14
|
+
* routes builds a baseline from two different pages, and a confident comparison against the wrong
|
|
15
|
+
* population is more expensive than no comparison. So a segment is an id only when it could not
|
|
16
|
+
* plausibly be a route NAME.
|
|
17
|
+
*
|
|
18
|
+
* This is the statistics key alone. The observed path is untouched, because a report has to be able
|
|
19
|
+
* to say "/orders/1001 was slow" — the template tells you where to look, the path tells you what to
|
|
20
|
+
* open.
|
|
21
|
+
*/
|
|
22
|
+
export declare function routeTemplate(pathname: string): string;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The key an envelope's statistics accumulate under.
|
|
3
|
+
*
|
|
4
|
+
* The envelope map was keyed on the raw `pathname`. On any app whose URLs carry ids — most of them —
|
|
5
|
+
* `/orders/1001` and `/orders/1002` are different keys, so every visit mints a fresh envelope
|
|
6
|
+
* holding one sample.
|
|
7
|
+
*
|
|
8
|
+
* The cost is not the file size. `deviation-report` skips any envelope below
|
|
9
|
+
* `MIN_ENVELOPE_SAMPLES`, so on such an app no envelope ever qualifies, every report answers
|
|
10
|
+
* "envelope too new", and the deviation feature never turns on at all — silently, permanently, with
|
|
11
|
+
* nothing anywhere saying why. The unbounded file and the dead feature are one defect.
|
|
12
|
+
*
|
|
13
|
+
* CONSERVATIVE on purpose. Over-collapsing is the worse error: merging two genuinely different
|
|
14
|
+
* routes builds a baseline from two different pages, and a confident comparison against the wrong
|
|
15
|
+
* population is more expensive than no comparison. So a segment is an id only when it could not
|
|
16
|
+
* plausibly be a route NAME.
|
|
17
|
+
*
|
|
18
|
+
* This is the statistics key alone. The observed path is untouched, because a report has to be able
|
|
19
|
+
* to say "/orders/1001 was slow" — the template tells you where to look, the path tells you what to
|
|
20
|
+
* open.
|
|
21
|
+
*/
|
|
22
|
+
/** Stands in for any id segment. Not a valid path segment itself, so it cannot collide with one. */
|
|
23
|
+
const ID = ':id';
|
|
24
|
+
/** 8+ hex characters: a token, a hash, a short uuid. Below that it is as likely to be a word. */
|
|
25
|
+
const LONG_HEX = /^[0-9a-f]{8,}$/i;
|
|
26
|
+
const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
27
|
+
const ALL_DIGITS = /^\d+$/;
|
|
28
|
+
/**
|
|
29
|
+
* A year collapses too, and that is a deliberate trade rather than an oversight: `/reports/2024`
|
|
30
|
+
* and `/reports/2025` are the same page with different data, which is exactly what one envelope
|
|
31
|
+
* should describe. Distinguishing a year from an id needs to know the app, and guessing would
|
|
32
|
+
* reintroduce the split this exists to remove.
|
|
33
|
+
*/
|
|
34
|
+
function isIdSegment(segment) {
|
|
35
|
+
return ALL_DIGITS.test(segment) || UUID.test(segment) || LONG_HEX.test(segment);
|
|
36
|
+
}
|
|
37
|
+
export function routeTemplate(pathname) {
|
|
38
|
+
if (0 === pathname.length)
|
|
39
|
+
return pathname;
|
|
40
|
+
return pathname
|
|
41
|
+
.split('/')
|
|
42
|
+
.map((segment) => (isIdSegment(segment) ? ID : segment))
|
|
43
|
+
.join('/');
|
|
44
|
+
}
|
|
@@ -3,6 +3,7 @@ import { type OnboardingStep } from '@reticlehq/core/telemetry';
|
|
|
3
3
|
import type { AmbientCounts } from '@reticlehq/engine/window/ambient.js';
|
|
4
4
|
import { type ProjectId, type JournalAction } from '@reticlehq/core';
|
|
5
5
|
import type { FileSystemPort } from '../project/fs/fs-port.js';
|
|
6
|
+
import { type PruneWorkspaceOptions } from './on-disk/startup-maintenance.js';
|
|
6
7
|
/**
|
|
7
8
|
* Session teardown: the durable half of ending a session. Two things must happen when a tab disconnects,
|
|
8
9
|
* and neither did before — the journal batches events and only writes at its flush threshold, so the tail
|
|
@@ -44,8 +45,10 @@ export interface SessionEndTarget {
|
|
|
44
45
|
interface SessionEndDeps {
|
|
45
46
|
fs: FileSystemPort;
|
|
46
47
|
reticleRoot: string;
|
|
47
|
-
/** Journaling/persistence off (opt-out) → teardown is a no-op. */
|
|
48
|
+
/** Journaling/persistence off (opt-out) → teardown is a no-op. Retention still runs. */
|
|
48
49
|
enabled: boolean;
|
|
50
|
+
/** What this project is willing to keep — the `retain` block of its `.reticle.json`. */
|
|
51
|
+
retain?: PruneWorkspaceOptions;
|
|
49
52
|
/**
|
|
50
53
|
* The one clock in this file, injected — the run artifact stamps `createdAt` from it.
|
|
51
54
|
*
|
|
@@ -76,8 +79,16 @@ interface SessionEndDeps {
|
|
|
76
79
|
* this handler follows.
|
|
77
80
|
*/
|
|
78
81
|
flows?: {
|
|
79
|
-
|
|
82
|
+
saveJourney: (program: DriveProgram, projectId: ProjectId | undefined, prefix: string) => Promise<unknown>;
|
|
80
83
|
};
|
|
84
|
+
/**
|
|
85
|
+
* The flow store for a session's own `.reticle`. Drive flows used to go to `flows` above — the
|
|
86
|
+
* daemon's launch directory — while replay reads the session's project, so a daemon started
|
|
87
|
+
* anywhere else saved flows nothing could replay. Absent, `flows` is used.
|
|
88
|
+
*/
|
|
89
|
+
flowsAt?: (root: string) => NonNullable<SessionEndDeps['flows']>;
|
|
90
|
+
/** Who is driving — stamped on each saved flow. Injected: teardown does not reach into MCP. */
|
|
91
|
+
author?: () => DriveProgram['author'];
|
|
81
92
|
/**
|
|
82
93
|
* Called when a run artifact is written, so cloud sync can cycle instead of waiting for its timer.
|
|
83
94
|
*
|
|
@@ -1,8 +1,17 @@
|
|
|
1
|
-
import { driveFlowsFrom } from './drive-flow.js';
|
|
1
|
+
import { DRIVE_FLOW_PREFIX, driveFlowsFrom, } from './drive-flow.js';
|
|
2
|
+
import { log } from '../../log.js';
|
|
3
|
+
/** What a session's teardown writes to the daemon log when a by-product fails to save. */
|
|
4
|
+
const SESSION_END_LOG = {
|
|
5
|
+
FLOW_SAVE_FAILED: 'reticle_drive_flow_save_failed',
|
|
6
|
+
RUN_RECORD_FAILED: 'reticle_drive_run_record_failed',
|
|
7
|
+
};
|
|
8
|
+
const errorMessage = (error) => error instanceof Error ? error.message : String(error);
|
|
9
|
+
import { journalActionsPath, sessionDirPath } from '../project/dir/reticle-dir.js';
|
|
10
|
+
import { asSessionId } from '@reticlehq/core';
|
|
2
11
|
import { OnboardingPhase, OnboardingStepStatus, } from '@reticlehq/core/telemetry';
|
|
3
12
|
import { AmbientStore } from './ambient-store.js';
|
|
4
13
|
import { subjectOf } from '@reticlehq/core';
|
|
5
|
-
import {
|
|
14
|
+
import { pruneWorkspace } from './on-disk/startup-maintenance.js';
|
|
6
15
|
import { buildVerificationRun } from '../../judgement/runs/artifact/build-verification-run.js';
|
|
7
16
|
import { driveRunFrom, driveRunId } from '../../judgement/runs/drive-run.js';
|
|
8
17
|
import { RunStore } from '../../judgement/runs/artifact/run-store.js';
|
|
@@ -13,8 +22,28 @@ import { RunStore } from '../../judgement/runs/artifact/run-store.js';
|
|
|
13
22
|
*/
|
|
14
23
|
export function makeSessionEnd(deps) {
|
|
15
24
|
return async (session) => {
|
|
16
|
-
|
|
25
|
+
/*
|
|
26
|
+
* Retention is maintenance of a directory, not a part of journalling, so it runs either way.
|
|
27
|
+
*
|
|
28
|
+
* Teardown used to return here, and this is the one setting under which that mattered most:
|
|
29
|
+
* somebody switches journalling off BECAUSE `.reticle/` got too big, and switching it off was
|
|
30
|
+
* what stopped anything ever deleting what was already there. It also stopped the sweep of
|
|
31
|
+
* visual diffs, feedback copies and run artifacts, none of which need the journal to be written
|
|
32
|
+
* at all. The daemon's own start-path sweep was gated on the same flag, so there was no second
|
|
33
|
+
* site still running — both are ungated now.
|
|
34
|
+
*/
|
|
35
|
+
const sweep = async () => {
|
|
36
|
+
try {
|
|
37
|
+
await pruneWorkspace(deps.fs, session.artifactRoot ?? deps.reticleRoot, deps.liveSessionIds?.() ?? new Set(), deps.retain ?? {});
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
// retention is best-effort maintenance; never surface at teardown
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
if (!deps.enabled) {
|
|
44
|
+
await sweep();
|
|
17
45
|
return;
|
|
46
|
+
}
|
|
18
47
|
try {
|
|
19
48
|
await session.flushJournal();
|
|
20
49
|
}
|
|
@@ -56,24 +85,27 @@ export function makeSessionEnd(deps) {
|
|
|
56
85
|
// would buy under a minute of latency for a mutable handle held across two wiring sites.
|
|
57
86
|
// A DRIVE IS A REGRESSION TEST, and it used to need somebody to remember to say so.
|
|
58
87
|
//
|
|
59
|
-
// `record{start}` + `flow_save` is the agent-facing route and agents do not take it:
|
|
60
|
-
//
|
|
61
|
-
//
|
|
62
|
-
//
|
|
88
|
+
// `record{start}` + `flow_save` is the agent-facing route and agents do not take it: few saved
|
|
89
|
+
// flows are mutation-testable and few steps declare a consequence, while the engine catches
|
|
90
|
+
// nearly every bug it structurally can. The ceiling is how many flows EXIST that could go red,
|
|
91
|
+
// so a drive that is never saved is a regression test paid for and thrown away.
|
|
63
92
|
//
|
|
64
93
|
// Saved only when the tape declared a consequence — see drive-flow.ts for why the alternative is
|
|
65
94
|
// a false-green factory running once per session.
|
|
66
95
|
try {
|
|
67
96
|
await saveDrivenFlow(deps, session);
|
|
68
97
|
}
|
|
69
|
-
catch {
|
|
70
|
-
//
|
|
98
|
+
catch (error) {
|
|
99
|
+
// A flow is a by-product of the session; failing to write one never fails the teardown. It is
|
|
100
|
+
// logged, because a drive that leaves no flow and no reason is otherwise undiagnosable.
|
|
101
|
+
log(SESSION_END_LOG.FLOW_SAVE_FAILED, { error: errorMessage(error) });
|
|
71
102
|
}
|
|
72
103
|
try {
|
|
73
104
|
await recordDriveRun(deps, session);
|
|
74
105
|
}
|
|
75
|
-
catch {
|
|
76
|
-
//
|
|
106
|
+
catch (error) {
|
|
107
|
+
// An artifact is a report ABOUT the session; failing to write one never fails the teardown.
|
|
108
|
+
log(SESSION_END_LOG.RUN_RECORD_FAILED, { error: errorMessage(error) });
|
|
77
109
|
}
|
|
78
110
|
try {
|
|
79
111
|
// Bound the journal on disk HERE, not only at daemon start.
|
|
@@ -101,13 +133,31 @@ export function makeSessionEnd(deps) {
|
|
|
101
133
|
// The root this session actually journalled into. Pruning the daemon's tree instead meant a
|
|
102
134
|
// per-project workspace was never swept at all, so the one place journals really accumulate
|
|
103
135
|
// was the one place retention never ran.
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
136
|
+
/*
|
|
137
|
+
* A session nobody ever drove keeps no journal.
|
|
138
|
+
*
|
|
139
|
+
* The observers run from the moment the SDK attaches, so an idle tab on a busy page
|
|
140
|
+
* accumulates megabytes without a single tool call. The cost is not the disk: those
|
|
141
|
+
* directories occupy retention SLOTS, so a journal that could answer a verdict question is
|
|
142
|
+
* evicted by one that was never asked one.
|
|
143
|
+
*
|
|
144
|
+
* Decided at the END and never by gating CAPTURE. Whether a tool call will happen is not
|
|
145
|
+
* knowable while the events that would answer it are being recorded, and gating capture on it
|
|
146
|
+
* would leave the first assertion of a session with nothing to read.
|
|
147
|
+
*/
|
|
148
|
+
await dropUndrivenJournal(deps.fs, session.artifactRoot ?? deps.reticleRoot, asSessionId(session.id));
|
|
107
149
|
}
|
|
108
150
|
catch {
|
|
109
|
-
//
|
|
151
|
+
// teardown must never throw: the tab is already gone
|
|
110
152
|
}
|
|
153
|
+
// EVERY tier, not just sessions. Visual diffs and feedback copies were pruned only at daemon
|
|
154
|
+
// start, against the DAEMON's root — which for a globally registered daemon is `$HOME` and not
|
|
155
|
+
// the project at all, so the two tiers that only ever grow in a project workspace were the two
|
|
156
|
+
// never swept there. Same defect as the one this line already fixed for sessions, one move
|
|
157
|
+
// behind. The byte budget rides along for the same reason: it was wired at daemon start too.
|
|
158
|
+
//
|
|
159
|
+
// Outside the try above, so a failure anywhere in teardown still leaves the directory swept.
|
|
160
|
+
await sweep();
|
|
111
161
|
};
|
|
112
162
|
}
|
|
113
163
|
/**
|
|
@@ -156,13 +206,15 @@ export async function recordDriveRun(deps, session) {
|
|
|
156
206
|
/** Persist what this session drove as a replayable flow, when it declared anything provable. */
|
|
157
207
|
async function saveDrivenFlow(deps, session) {
|
|
158
208
|
const takeTape = deps.takeAmbientTape;
|
|
159
|
-
const flows = deps.flows;
|
|
209
|
+
const flows = deps.flowsAt?.(session.artifactRoot ?? deps.reticleRoot) ?? deps.flows;
|
|
160
210
|
if (takeTape === undefined || flows === undefined)
|
|
161
211
|
return;
|
|
162
212
|
// One flow per journey the session contained, not one flow per session — see drive-flow.ts.
|
|
163
|
-
const { programs, outcome } = driveFlowsFrom(
|
|
213
|
+
const { programs, outcome } = driveFlowsFrom(takeTape());
|
|
214
|
+
const author = deps.author?.();
|
|
164
215
|
for (const program of programs) {
|
|
165
|
-
|
|
216
|
+
// Folded into any saved copy of the same journey, so each session does not add a near-copy.
|
|
217
|
+
await flows.saveJourney(author === undefined ? program : { ...program, author }, session.projectId, DRIVE_FLOW_PREFIX);
|
|
166
218
|
}
|
|
167
219
|
// The funnel step that makes the SECOND run cheap.
|
|
168
220
|
//
|
|
@@ -177,3 +229,21 @@ async function saveDrivenFlow(deps, session) {
|
|
|
177
229
|
...(outcome.unprovenSteps === undefined ? {} : { reason: 'no_declared_consequence' }),
|
|
178
230
|
});
|
|
179
231
|
}
|
|
232
|
+
/**
|
|
233
|
+
* Remove this session's journal when it served no tool call.
|
|
234
|
+
*
|
|
235
|
+
* "Served no tool call" is read off the actions ledger, which is written one line per call: absent
|
|
236
|
+
* or empty means nothing was ever driven here. Best-effort like every other maintenance step —
|
|
237
|
+
* a session that cannot be tidied is not a session that failed.
|
|
238
|
+
*/
|
|
239
|
+
async function dropUndrivenJournal(fs, root, sessionId) {
|
|
240
|
+
try {
|
|
241
|
+
const actions = await fs.readFile(journalActionsPath(root, sessionId)).catch(() => '');
|
|
242
|
+
if (actions.trim().length > 0)
|
|
243
|
+
return;
|
|
244
|
+
await fs.rm(sessionDirPath(root, sessionId));
|
|
245
|
+
}
|
|
246
|
+
catch {
|
|
247
|
+
// tidying is never the reason a teardown fails
|
|
248
|
+
}
|
|
249
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type JournalAction, type JournalWriteLoss, type ReticleEvent } from '@reticlehq/core';
|
|
2
2
|
import type { FileSystemPort } from '../project/fs/fs-port.js';
|
|
3
|
-
import type { JournalReadLoss } from './journal-recorder.js';
|
|
3
|
+
import type { JournalLedgerSize, JournalReadLoss } from './journal-recorder.js';
|
|
4
4
|
/**
|
|
5
5
|
* The two ceilings a durable read is held to.
|
|
6
6
|
*
|
|
@@ -115,6 +115,13 @@ export declare class SessionJournal {
|
|
|
115
115
|
* file, as opposed to what a bounded read declined to hand back.
|
|
116
116
|
*/
|
|
117
117
|
readWriteLoss(): Promise<JournalWriteLoss | undefined>;
|
|
118
|
+
/**
|
|
119
|
+
* The ledger's size against its ceiling, once the size is known.
|
|
120
|
+
*
|
|
121
|
+
* `undefined` before the first append, because `#eventBytes` is seeded from `stat` at that point
|
|
122
|
+
* and a zero reported before it is measured would be a claim rather than a reading.
|
|
123
|
+
*/
|
|
124
|
+
ledger(): JournalLedgerSize | undefined;
|
|
118
125
|
appendAction(action: JournalAction): Promise<void>;
|
|
119
126
|
readEvents(): Promise<ReticleEvent[]>;
|
|
120
127
|
/**
|
|
@@ -281,6 +281,16 @@ export class SessionJournal {
|
|
|
281
281
|
async readWriteLoss() {
|
|
282
282
|
return this.#closureReport();
|
|
283
283
|
}
|
|
284
|
+
/**
|
|
285
|
+
* The ledger's size against its ceiling, once the size is known.
|
|
286
|
+
*
|
|
287
|
+
* `undefined` before the first append, because `#eventBytes` is seeded from `stat` at that point
|
|
288
|
+
* and a zero reported before it is measured would be a claim rather than a reading.
|
|
289
|
+
*/
|
|
290
|
+
ledger() {
|
|
291
|
+
const bytes = this.#eventBytes;
|
|
292
|
+
return bytes === undefined ? undefined : { bytes, capBytes: this.#eventBytesCap };
|
|
293
|
+
}
|
|
284
294
|
/**
|
|
285
295
|
* Close the ledger for good, and declare the loss twice — durably, then in-band.
|
|
286
296
|
*
|
|
@@ -15,7 +15,7 @@ export interface ReticleDirPaths {
|
|
|
15
15
|
project: string;
|
|
16
16
|
/**.../.reticle/impact.json (what Reticle has done for this user, local only) */
|
|
17
17
|
impact: string;
|
|
18
|
-
/**.../.reticle/intent.json
|
|
18
|
+
/**.../.reticle/intent.json — the LEGACY single ledger, read until the first write migrates it into .reticle/intent/ */
|
|
19
19
|
intent: string;
|
|
20
20
|
/**.../.reticle/fill-values.json (what a drive types into each labelled field — git-checked) */
|
|
21
21
|
fillValues: string;
|
|
@@ -33,6 +33,8 @@ export interface ReticleDirPaths {
|
|
|
33
33
|
capsules: string;
|
|
34
34
|
/**.../.reticle/flake.json (per-flow flake ledger) */
|
|
35
35
|
flake: string;
|
|
36
|
+
/**.../.reticle/coverage.json (app-wide coverage ledger, all six levels) */
|
|
37
|
+
coverage: string;
|
|
36
38
|
/**.../.reticle/assertion-tiers.json (last-passing assertion tiers; anti-reward-hacking baseline) */
|
|
37
39
|
tiers: string;
|
|
38
40
|
}
|
|
@@ -18,6 +18,7 @@ export function reticleDirPaths(root) {
|
|
|
18
18
|
ambient: join(root, ReticleDir.AMBIENT_FILE),
|
|
19
19
|
capsules: join(root, ReticleDir.CAPSULES_SUBDIR),
|
|
20
20
|
flake: join(root, ReticleDir.FLAKE_FILE),
|
|
21
|
+
coverage: join(root, ReticleDir.COVERAGE_FILE),
|
|
21
22
|
tiers: join(root, ReticleDir.TIERS_FILE),
|
|
22
23
|
};
|
|
23
24
|
}
|
|
@@ -1,11 +1,46 @@
|
|
|
1
1
|
import { access, appendFile, mkdir, open, readdir, readFile, realpath, rename, rm, stat, writeFile, } from 'node:fs/promises';
|
|
2
2
|
/** The JSONL record terminator, as a byte — the only boundary a capped tail read can align to. */
|
|
3
3
|
const NEWLINE_BYTE = 0x0a;
|
|
4
|
+
/**
|
|
5
|
+
* The largest single `read(2)` this port will ask for: 2^31 - 1, the int32 ceiling.
|
|
6
|
+
*
|
|
7
|
+
* Past it `fs.read` does not throw — it ABORTS the process on a V8 assertion
|
|
8
|
+
* (`node::fs::Read ... Assertion failed: args[3]->IsInt32()`, where args[3] is the length). That is
|
|
9
|
+
* not catchable, so one oversized journal takes the daemon down and with it every session on the
|
|
10
|
+
* machine, including unrelated ones. Reported from the field exactly that way.
|
|
11
|
+
*
|
|
12
|
+
* Reachable in practice because `Buffer.allocUnsafe` happily allocates well past this, so a
|
|
13
|
+
* `size - start` over 2 GiB allocates fine and then kills node at the syscall. A `.reticle/sessions`
|
|
14
|
+
* has been observed in the multi-gigabyte range, so "no journal is ever that big" is not a bound
|
|
15
|
+
* anybody is enforcing.
|
|
16
|
+
*
|
|
17
|
+
* Clamping rather than throwing is deliberate: a caller reading a tail wants the newest records, and
|
|
18
|
+
* returning the last 2 GiB of them is a better answer than an error. The boundary realignment below
|
|
19
|
+
* already handles a window that opens mid-record, which is exactly what a clamp produces.
|
|
20
|
+
*/
|
|
21
|
+
const MAX_SINGLE_READ_BYTES = 2_147_483_647;
|
|
22
|
+
/**
|
|
23
|
+
* Refuse an offset that cannot survive the arithmetic below.
|
|
24
|
+
*
|
|
25
|
+
* `length = size - start`, so a non-integer or negative offset produces a non-integer or negative
|
|
26
|
+
* length, and those fail in two different ways one layer apart: `Buffer.allocUnsafe` throws a
|
|
27
|
+
* RangeError for a fractional length, while an oversized one reaches `fs.read` and aborts the
|
|
28
|
+
* process. Checking here means the caller gets one predictable rejection instead of either.
|
|
29
|
+
*/
|
|
30
|
+
function requireByteCount(label, value) {
|
|
31
|
+
if (value === undefined)
|
|
32
|
+
return;
|
|
33
|
+
if (!Number.isSafeInteger(value) || value < 0) {
|
|
34
|
+
throw new RangeError(`${label} must be a non-negative safe integer, got ${String(value)} — a fractional or negative byte count cannot reach a read syscall safely`);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
4
37
|
/** Production adapter — the single import site of node:fs/promises in the server. */
|
|
5
38
|
export function createNodeFileSystem() {
|
|
6
39
|
return {
|
|
7
40
|
readFile: (path) => readFile(path, 'utf8'),
|
|
8
41
|
readFileFrom: async (path, byteOffset, maxBytes) => {
|
|
42
|
+
requireByteCount('byteOffset', byteOffset);
|
|
43
|
+
requireByteCount('maxBytes', maxBytes);
|
|
9
44
|
const fh = await open(path, 'r');
|
|
10
45
|
try {
|
|
11
46
|
const { size } = await fh.stat();
|
|
@@ -18,14 +53,18 @@ export function createNodeFileSystem() {
|
|
|
18
53
|
// One byte BEFORE that cut, so a cut landing exactly on a record boundary is recognised as
|
|
19
54
|
// one instead of sacrificing the record after it.
|
|
20
55
|
const start = capped ? cut - 1 : byteOffset;
|
|
21
|
-
|
|
56
|
+
// Clamped to the int32 ceiling: past it `fs.read` aborts the process rather than throwing.
|
|
57
|
+
// Keep the NEWEST bytes, matching what every caller of a tail read is asking for.
|
|
58
|
+
const want = size - start;
|
|
59
|
+
const length = Math.min(want, MAX_SINGLE_READ_BYTES);
|
|
60
|
+
const readFrom = start + (want - length);
|
|
22
61
|
const buf = Buffer.allocUnsafe(length);
|
|
23
|
-
const { bytesRead } = await fh.read(buf, 0, length,
|
|
62
|
+
const { bytesRead } = await fh.read(buf, 0, length, readFrom);
|
|
24
63
|
// ONLY what the read returned. `allocUnsafe` hands back whatever was last in that heap
|
|
25
64
|
// block, and `fstat` runs one syscall ahead of `read` — so a file truncated in between, or
|
|
26
65
|
// any short read, would otherwise decode that memory and hand it back as journal text.
|
|
27
66
|
const got = buf.subarray(0, bytesRead);
|
|
28
|
-
if (!capped)
|
|
67
|
+
if (!capped && readFrom === start)
|
|
29
68
|
return { text: got.toString('utf8'), size, from: start };
|
|
30
69
|
// The ceiling moved the start, so the window may open inside a record whose head was never
|
|
31
70
|
// read. Drop up to the first boundary in BYTES, before decoding: `\n` is one byte and
|
|
@@ -35,11 +74,11 @@ export function createNodeFileSystem() {
|
|
|
35
74
|
// No boundary in the whole window: there is no record here that can be read, and half of
|
|
36
75
|
// one is worse than none. `from` still says how far the read reached.
|
|
37
76
|
if (-1 === newline)
|
|
38
|
-
return { text: '', size, from:
|
|
77
|
+
return { text: '', size, from: readFrom + bytesRead };
|
|
39
78
|
return {
|
|
40
79
|
text: got.subarray(newline + 1).toString('utf8'),
|
|
41
80
|
size,
|
|
42
|
-
from:
|
|
81
|
+
from: readFrom + newline + 1,
|
|
43
82
|
};
|
|
44
83
|
}
|
|
45
84
|
finally {
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Write a file so a reader never sees a half-written one, and two writers never see each other's.
|
|
3
|
+
*
|
|
4
|
+
* Write to a temporary sibling, then rename. The rename is what makes it atomic: on a POSIX
|
|
5
|
+
* filesystem it either replaces the destination entirely or does not happen, so a reader gets the
|
|
6
|
+
* old file or the new one and never a truncated middle.
|
|
7
|
+
*
|
|
8
|
+
* TWO properties, and the second is the one that was missing. Five stores in this package built
|
|
9
|
+
* this by hand and named the temp file after the DESTINATION — `${path}.tmp` — which is shared by
|
|
10
|
+
* every process writing that path. Two daemons on one repository then write the same temp file:
|
|
11
|
+
* one truncates the other mid-write and renames the remains into place, and the survivor is a file
|
|
12
|
+
* neither of them produced. Two other stores already pid-suffixed theirs, so the fix was known and
|
|
13
|
+
* had simply never been carried to the rest.
|
|
14
|
+
*
|
|
15
|
+
* Running parallel agent sessions against one checkout is the ordinary way to reach this, not an
|
|
16
|
+
* exotic one.
|
|
17
|
+
*
|
|
18
|
+
* The temp file stays BESIDE its destination rather than in the system temp directory, because a
|
|
19
|
+
* rename across devices is not atomic and `/tmp` is routinely a different device.
|
|
20
|
+
*
|
|
21
|
+
* One helper rather than five corrected copies: the sixth store gets this for free and cannot get
|
|
22
|
+
* it wrong, which is the only version of this fix that stays fixed.
|
|
23
|
+
*/
|
|
24
|
+
import type { FileSystemPort } from './fs-port.js';
|
|
25
|
+
export declare function tempNameFor(path: string, pid?: number, seq?: number): string;
|
|
26
|
+
export declare function writeFileAtomic(fs: FileSystemPort, path: string, contents: string): Promise<void>;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unique per WRITE, not per process.
|
|
3
|
+
*
|
|
4
|
+
* The pid separates two daemons. It does not separate two concurrent writes inside ONE daemon —
|
|
5
|
+
* `Promise.all` over two store saves, or two scheduled flushes of the same ledger — and those share
|
|
6
|
+
* a pid, so a pid-only name puts them straight back on the same temp file. The first version of
|
|
7
|
+
* this helper had exactly that hole and its own concurrency test found it.
|
|
8
|
+
*
|
|
9
|
+
* A counter rather than `Math.random()`: rule 7 keeps the clock and the random source out of pure
|
|
10
|
+
* logic, and a monotonic counter is deterministic, collision-free within a process, and needs
|
|
11
|
+
* neither.
|
|
12
|
+
*/
|
|
13
|
+
let writeSeq = 0;
|
|
14
|
+
export function tempNameFor(path, pid = process.pid, seq) {
|
|
15
|
+
writeSeq += 1;
|
|
16
|
+
return `${path}.${String(pid)}.${String(seq ?? writeSeq)}.tmp`;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* What Windows answers, for a moment, when another writer is still replacing the destination.
|
|
20
|
+
* POSIX never refuses such a rename; Windows does, and the save has to wait it out rather than
|
|
21
|
+
* fail. Seen on Windows CI with two writers on one path.
|
|
22
|
+
*/
|
|
23
|
+
const TRANSIENT_RENAME = new Set(['EPERM', 'EBUSY', 'EACCES']);
|
|
24
|
+
const RENAME_ATTEMPTS = 6;
|
|
25
|
+
const RENAME_BACKOFF_MS = 15;
|
|
26
|
+
function isTransientRename(error) {
|
|
27
|
+
const code = error?.code;
|
|
28
|
+
return 'string' === typeof code && TRANSIENT_RENAME.has(code);
|
|
29
|
+
}
|
|
30
|
+
export async function writeFileAtomic(fs, path, contents) {
|
|
31
|
+
const tmp = tempNameFor(path);
|
|
32
|
+
await fs.writeFile(tmp, contents);
|
|
33
|
+
for (let attempt = 1;; attempt += 1) {
|
|
34
|
+
try {
|
|
35
|
+
await fs.rename(tmp, path);
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
catch (error) {
|
|
39
|
+
if (attempt < RENAME_ATTEMPTS && isTransientRename(error)) {
|
|
40
|
+
await new Promise((resolve) => setTimeout(resolve, RENAME_BACKOFF_MS * attempt));
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
// The write failed; its temp file must not outlive it.
|
|
44
|
+
await fs.rm(tmp).catch(() => undefined);
|
|
45
|
+
throw error;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -38,7 +38,9 @@ function numericDelta(before, after) {
|
|
|
38
38
|
* "what's broken vs before" from the cloud. Absent creds / unreachable → undefined (agent stays local).
|
|
39
39
|
*/
|
|
40
40
|
async function cloudRegression(deps, sessionId) {
|
|
41
|
-
|
|
41
|
+
// The same resolver every caller uses (stored key for a linked repo, else the exported one);
|
|
42
|
+
// the environment alone only for an embedder that wires no linked-credential port.
|
|
43
|
+
const config = await (deps.linkedCloud?.() ?? Promise.resolve(resolveCloudConfig(process.env)));
|
|
42
44
|
if (null === config)
|
|
43
45
|
return undefined;
|
|
44
46
|
let projectId;
|
|
@@ -63,3 +63,9 @@ export declare function hasAnyAppConnectedBefore(stateDir: string, port: number,
|
|
|
63
63
|
* diagnosis, which is why it now shares this function instead of keeping a second copy of the rule.
|
|
64
64
|
*/
|
|
65
65
|
export declare function hasProjectConnectedBefore(stateDir: string, port: number, projectId: string | undefined): boolean;
|
|
66
|
+
/**
|
|
67
|
+
* Has any of these projects connected before: the first-move question for a directory that speaks
|
|
68
|
+
* for more than one app (a monorepo root), or for none by name (an app wired by the plugin alone).
|
|
69
|
+
* The ids come from `projectIdsAt`; each is still asked the narrow question.
|
|
70
|
+
*/
|
|
71
|
+
export declare function hasAnyProjectConnectedBefore(stateDir: string, port: number, projectIds: readonly string[]): boolean;
|
|
@@ -112,3 +112,11 @@ export function hasProjectConnectedBefore(stateDir, port, projectId) {
|
|
|
112
112
|
return false;
|
|
113
113
|
return hasAnyAppConnectedBefore(stateDir, port, projectId);
|
|
114
114
|
}
|
|
115
|
+
/**
|
|
116
|
+
* Has any of these projects connected before: the first-move question for a directory that speaks
|
|
117
|
+
* for more than one app (a monorepo root), or for none by name (an app wired by the plugin alone).
|
|
118
|
+
* The ids come from `projectIdsAt`; each is still asked the narrow question.
|
|
119
|
+
*/
|
|
120
|
+
export function hasAnyProjectConnectedBefore(stateDir, port, projectIds) {
|
|
121
|
+
return projectIds.some((id) => hasProjectConnectedBefore(stateDir, port, id));
|
|
122
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/** One project this machine has paired before, in the shape the no-session facts already carry. */
|
|
2
|
-
|
|
2
|
+
interface RegisteredProject {
|
|
3
3
|
directory: string;
|
|
4
4
|
projectId?: string;
|
|
5
5
|
}
|
|
@@ -52,3 +52,4 @@ export declare function isUnscopedRoot(directory: string, home: string): boolean
|
|
|
52
52
|
* reporting it as such would tell a correctly-scoped daemon to go and look somewhere else.
|
|
53
53
|
*/
|
|
54
54
|
export declare function registeredElsewhere(home: string, directory: string): RegisteredProject[];
|
|
55
|
+
export {};
|