@reticlehq/server 3.9.0 → 3.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/SKILL.md +13 -12
- package/dist/command/cli/adhoc-suite.js +2 -0
- package/dist/command/cli/adhoc-verdict.d.ts +2 -43
- package/dist/command/cli/adhoc-verdict.js +50 -187
- package/dist/command/cli/answers/mcp-connection-hint.d.ts +2 -0
- package/dist/command/cli/answers/mcp-connection-hint.js +6 -0
- package/dist/command/cli/cli-doctor.js +1 -1
- package/dist/command/cli/cli-flow-commands.js +2 -9
- package/dist/command/cli/cli-kill.js +1 -1
- package/dist/command/cli/cli-parse-compare.d.ts +22 -0
- package/dist/command/cli/cli-parse-compare.js +75 -0
- package/dist/command/cli/cli-parse-grade.d.ts +36 -0
- package/dist/command/cli/cli-parse-grade.js +75 -0
- package/dist/command/cli/cli-parse-grammar.d.ts +2 -0
- package/dist/command/cli/cli-parse-grammar.js +18 -20
- package/dist/command/cli/cli-parse-verify.d.ts +10 -0
- package/dist/command/cli/cli-parse-verify.js +63 -14
- package/dist/command/cli/cli-parse.d.ts +17 -15
- package/dist/command/cli/cli-parse.js +44 -27
- package/dist/command/cli/cli-update-commands.d.ts +1 -1
- package/dist/command/cli/cli-update-commands.js +29 -20
- package/dist/command/cli/cli-usage.js +58 -29
- package/dist/command/cli/cli-verify.d.ts +11 -2
- package/dist/command/cli/cli-verify.js +27 -8
- package/dist/command/cli/cloud-cli.js +2 -119
- package/dist/command/cli/daemon-link/daemon-client.d.ts +45 -0
- package/dist/command/cli/daemon-link/daemon-client.js +205 -0
- package/dist/command/cli/doctor/doctor-daemon-line.js +6 -3
- package/dist/command/cli/doctor-repairs.js +1 -1
- package/dist/command/cli/ensure-daemon.d.ts +26 -0
- package/dist/command/cli/ensure-daemon.js +57 -0
- package/dist/command/cli/finished-run/grade-command.d.ts +23 -0
- package/dist/command/cli/finished-run/grade-command.js +180 -0
- package/dist/command/cli/lifecycle/daemon-lifecycle.d.ts +1 -1
- package/dist/command/cli/lifecycle/daemon-lifecycle.js +1 -1
- package/dist/command/cli/ports/port-holder.d.ts +9 -0
- package/dist/command/cli/ports/port-holder.js +27 -3
- package/dist/command/cli/status/status-lines.d.ts +1 -0
- package/dist/command/cli/status/status-lines.js +22 -0
- package/dist/command/cli/status-fields.d.ts +5 -1
- package/dist/command/cli/status-fields.js +7 -3
- package/dist/command/cli/two-builds/compare-command.d.ts +48 -0
- package/dist/command/cli/two-builds/compare-command.js +314 -0
- package/dist/command/cli/verify-against.d.ts +61 -0
- package/dist/command/cli/verify-against.js +187 -0
- package/dist/command/cli/verify-no-session.d.ts +2 -0
- package/dist/command/cli/verify-no-session.js +13 -0
- package/dist/command/cli/verify-results.d.ts +35 -2
- package/dist/command/cli/verify-results.js +76 -10
- package/dist/command/cli.js +46 -41
- package/dist/command/daemon/daemon-resolve.d.ts +8 -1
- package/dist/command/daemon/daemon-resolve.js +27 -1
- package/dist/command/daemon/daemon.d.ts +12 -1
- package/dist/command/daemon/daemon.js +35 -4
- package/dist/command/daemon/lifetime/death-cause.js +21 -2
- package/dist/command/daemon/lifetime/heartbeat.js +7 -2
- package/dist/command/daemon/lifetime/startup-failure.d.ts +29 -0
- package/dist/command/daemon/lifetime/startup-failure.js +62 -0
- package/dist/command/setup/bringup/dev-server-files.d.ts +9 -0
- package/dist/command/setup/bringup/dev-server-files.js +17 -0
- package/dist/command/setup/bringup/static-page-server.d.ts +14 -2
- package/dist/command/setup/bringup/static-page-server.js +43 -9
- package/dist/command/setup/init/init-runtime.js +6 -0
- package/dist/command/setup/node-effects.d.ts +3 -2
- package/dist/command/setup/node-effects.js +9 -3
- package/dist/command/setup/remaining-steps.d.ts +8 -0
- package/dist/command/setup/remaining-steps.js +10 -0
- package/dist/command/setup/run-setup.d.ts +2 -0
- package/dist/command/setup/run-setup.js +9 -1
- package/dist/command/setup/setup-command.js +6 -3
- package/dist/command/update/updater.js +6 -0
- package/dist/command/version/version-skew.js +1 -1
- package/dist/features/compare/action-plan.d.ts +260 -0
- package/dist/features/compare/action-plan.js +543 -0
- package/dist/features/compare/behaviour-compare.d.ts +24 -0
- package/dist/features/compare/behaviour-compare.js +85 -0
- package/dist/features/compare/behaviour-diff.d.ts +97 -0
- package/dist/features/compare/behaviour-diff.js +202 -0
- package/dist/features/compare/behaviour-report.d.ts +40 -0
- package/dist/features/compare/behaviour-report.js +128 -0
- package/dist/features/compare/build-readings.d.ts +106 -0
- package/dist/features/compare/build-readings.js +250 -0
- package/dist/features/compare/compare-diff.d.ts +62 -0
- package/dist/features/compare/compare-diff.js +177 -0
- package/dist/features/compare/compare-report-html.d.ts +6 -0
- package/dist/features/compare/compare-report-html.js +102 -0
- package/dist/features/compare/compare-report.d.ts +15 -0
- package/dist/features/compare/compare-report.js +73 -0
- package/dist/features/compare/compare-run.d.ts +60 -0
- package/dist/features/compare/compare-run.js +123 -0
- package/dist/features/compare/outside/effect-lines.d.ts +26 -0
- package/dist/features/compare/outside/effect-lines.js +103 -0
- package/dist/features/compare/outside/state-sources.d.ts +76 -0
- package/dist/features/compare/outside/state-sources.js +164 -0
- package/dist/features/compare/outside/value-dictionary.d.ts +51 -0
- package/dist/features/compare/outside/value-dictionary.js +227 -0
- package/dist/features/compare/visible-content.d.ts +20 -0
- package/dist/features/compare/visible-content.js +125 -0
- package/dist/features/crawl/crawl-empty.js +2 -1
- package/dist/features/crawl/crawl-exhaustive.d.ts +55 -0
- package/dist/features/crawl/crawl-exhaustive.js +138 -0
- package/dist/features/crawl/crawl-tools.d.ts +9 -2
- package/dist/features/crawl/crawl-tools.js +28 -12
- package/dist/features/crawl/crawl.d.ts +8 -9
- package/dist/features/crawl/crawl.js +61 -65
- package/dist/features/exhaust/app-coverage.d.ts +5 -1
- package/dist/features/exhaust/app-coverage.js +3 -1
- package/dist/features/exhaust/code-coverage.js +8 -4
- package/dist/features/exhaust/edge-cases.d.ts +133 -0
- package/dist/features/exhaust/edge-cases.js +310 -0
- package/dist/features/exhaust/edges/edge-drive.d.ts +42 -0
- package/dist/features/exhaust/edges/edge-drive.js +364 -0
- package/dist/features/exhaust/edges/edge-run.d.ts +59 -0
- package/dist/features/exhaust/edges/edge-run.js +169 -0
- package/dist/features/exhaust/explorer.d.ts +87 -14
- package/dist/features/exhaust/explorer.js +159 -25
- package/dist/features/exhaust/failure-branches.d.ts +5 -0
- package/dist/features/exhaust/failure-branches.js +29 -11
- package/dist/features/exhaust/invariants.d.ts +112 -0
- package/dist/features/exhaust/invariants.js +188 -0
- package/dist/features/exhaust/ledger.d.ts +68 -0
- package/dist/features/exhaust/ledger.js +121 -13
- package/dist/features/exhaust/session-fold.d.ts +10 -0
- package/dist/features/exhaust/session-fold.js +46 -3
- package/dist/features/exhaust/session-port.d.ts +3 -1
- package/dist/features/exhaust/session-port.js +97 -28
- package/dist/features/grade/grade-answer.d.ts +99 -0
- package/dist/features/grade/grade-answer.js +112 -0
- package/dist/features/grade/grade-noise.d.ts +27 -0
- package/dist/features/grade/grade-noise.js +55 -0
- package/dist/features/grade/grade-renumber.d.ts +12 -0
- package/dist/features/grade/grade-renumber.js +86 -0
- package/dist/features/grade/grade-state.d.ts +50 -0
- package/dist/features/grade/grade-state.js +93 -0
- package/dist/features/grade/grade-suggest.d.ts +33 -0
- package/dist/features/grade/grade-suggest.js +98 -0
- package/dist/features/visual/visual-tools.js +66 -19
- package/dist/index.js +7 -4
- package/dist/judgement/capsule/causal-summary.js +9 -2
- package/dist/judgement/runs/artifact/replay-mapping.d.ts +13 -1
- package/dist/judgement/runs/artifact/replay-mapping.js +1 -1
- package/dist/judgement/runs/reticle-runner.js +16 -2
- package/dist/judgement/runs/runner-port.d.ts +2 -0
- package/dist/judgement/runs/runner-port.js +23 -5
- package/dist/language/flows/affected-tools.d.ts +1 -1
- package/dist/language/flows/affected-tools.js +49 -30
- package/dist/language/flows/change/git-changed.d.ts +2 -1
- package/dist/language/flows/change/git-changed.js +19 -2
- package/dist/language/flows/fields/flow-secret-field.d.ts +1 -13
- package/dist/language/flows/fields/flow-secret-field.js +77 -21
- package/dist/language/flows/flow-anchor.d.ts +60 -2
- package/dist/language/flows/flow-anchor.js +125 -20
- package/dist/language/flows/flow-classify.d.ts +20 -0
- package/dist/language/flows/flow-classify.js +90 -12
- package/dist/language/flows/flow-cross-step.d.ts +7 -1
- package/dist/language/flows/flow-cross-step.js +25 -1
- package/dist/language/flows/flow-journey.d.ts +38 -4
- package/dist/language/flows/flow-journey.js +138 -8
- package/dist/language/flows/flow-replay-run.d.ts +0 -16
- package/dist/language/flows/flow-replay-run.js +76 -14
- package/dist/language/flows/flow-replay-types.d.ts +4 -2
- package/dist/language/flows/flow-replay.d.ts +19 -1
- package/dist/language/flows/flow-replay.js +169 -52
- package/dist/language/flows/flow-replayable.js +7 -1
- package/dist/language/flows/flow-start-path.d.ts +34 -5
- package/dist/language/flows/flow-start-path.js +202 -15
- package/dist/language/flows/flow-step-runners.d.ts +14 -13
- package/dist/language/flows/flow-step-runners.js +184 -109
- package/dist/language/flows/flow-store-for-session.d.ts +26 -25
- package/dist/language/flows/flow-store-for-session.js +44 -0
- package/dist/language/flows/flow-tools.js +46 -33
- package/dist/language/flows/flows.js +176 -76
- package/dist/language/flows/recording/tape/recordings.d.ts +19 -3
- package/dist/language/flows/recording/tape/recordings.js +71 -5
- package/dist/language/flows/replay.d.ts +19 -5
- package/dist/language/flows/replay.js +99 -7
- package/dist/language/flows/steps/page-steps.d.ts +68 -0
- package/dist/language/flows/steps/page-steps.js +198 -0
- package/dist/language/flows/suite/sequential-run.d.ts +68 -0
- package/dist/language/flows/suite/sequential-run.js +96 -0
- package/dist/language/flows/verify-change-tools.js +26 -15
- package/dist/memory/cloud/background-sync.d.ts +13 -0
- package/dist/memory/cloud/background-sync.js +24 -3
- package/dist/memory/cloud/sync-daemon.js +26 -27
- package/dist/memory/journal/attach-journal.js +2 -1
- package/dist/memory/journal/drive-flow.d.ts +7 -0
- package/dist/memory/journal/drive-flow.js +44 -13
- package/dist/memory/journal/journal-recorder.d.ts +4 -2
- package/dist/memory/journal/session-journal.d.ts +7 -0
- package/dist/memory/journal/session-journal.js +58 -10
- package/dist/memory/project/root-claims.d.ts +9 -0
- package/dist/memory/project/root-claims.js +9 -4
- package/dist/portal/input/download-capture.d.ts +7 -0
- package/dist/portal/input/download-capture.js +92 -0
- package/dist/portal/input/network-detail.d.ts +6 -0
- package/dist/portal/input/network-detail.js +12 -1
- package/dist/portal/input/network-mock-tools.js +49 -13
- package/dist/portal/input/network-mock.d.ts +8 -0
- package/dist/portal/input/network-mock.js +28 -9
- package/dist/portal/input/page-capture.d.ts +24 -0
- package/dist/portal/input/page-capture.js +33 -0
- package/dist/portal/input/real-input.d.ts +22 -20
- package/dist/portal/input/real-input.js +41 -31
- package/dist/portal/input/viewport-tools.d.ts +8 -0
- package/dist/portal/input/viewport-tools.js +75 -18
- package/dist/portal/pool/browser-pool.d.ts +11 -29
- package/dist/portal/pool/browser-pool.js +35 -17
- package/dist/portal/pool/context-permissions.d.ts +17 -3
- package/dist/portal/pool/context-permissions.js +36 -5
- package/dist/portal/pool/determinism.d.ts +52 -0
- package/dist/portal/pool/determinism.js +259 -0
- package/dist/portal/pool/lease-emulation.d.ts +34 -1
- package/dist/portal/pool/lease-emulation.js +18 -0
- package/dist/portal/pool/lease-marker-stamp.d.ts +6 -0
- package/dist/portal/pool/lease-marker-stamp.js +15 -1
- package/dist/portal/pool/lease-page-error.d.ts +44 -17
- package/dist/portal/pool/lease-page-error.js +147 -5
- package/dist/portal/pool/page-error-journal.d.ts +7 -1
- package/dist/portal/pool/page-error-journal.js +27 -0
- package/dist/portal/pool/playwright-launcher.js +51 -8
- package/dist/portal/pool/pool-contract.d.ts +27 -1
- package/dist/portal/pool/pool-lease.d.ts +36 -1
- package/dist/portal/pool/zero-install.d.ts +8 -0
- package/dist/portal/pool/zero-install.js +23 -1
- package/dist/portal/session/dev-server/dev-command.d.ts +7 -6
- package/dist/portal/session/dev-server/dev-command.js +61 -15
- package/dist/portal/session/dev-server/dev-server-probe.d.ts +7 -0
- package/dist/portal/session/dev-server/dev-server-probe.js +9 -0
- package/dist/portal/session/dev-server/project-ports.d.ts +21 -0
- package/dist/portal/session/dev-server/project-ports.js +62 -0
- package/dist/portal/session/facts/departed-network.d.ts +66 -0
- package/dist/portal/session/facts/departed-network.js +142 -0
- package/dist/portal/session/no-session-diagnosis.d.ts +9 -0
- package/dist/portal/session/no-session-diagnosis.js +67 -25
- package/dist/portal/session/no-session-next-action.d.ts +2 -0
- package/dist/portal/session/no-session-next-action.js +12 -3
- package/dist/portal/session/no-session-watch.d.ts +6 -0
- package/dist/portal/session/no-session-watch.js +38 -3
- package/dist/portal/session/serving-directory.d.ts +10 -0
- package/dist/portal/session/serving-directory.js +39 -8
- package/dist/portal/session/session-manager.d.ts +10 -0
- package/dist/portal/session/session-manager.js +174 -63
- package/dist/start-wiring.d.ts +2 -1
- package/dist/start-wiring.js +2 -1
- package/dist/surface/mcp/mcp.js +3 -2
- package/dist/surface/mcp/shared-params.d.ts +2 -0
- package/dist/surface/mcp/shared-params.js +31 -0
- package/dist/surface/tools/act/act-observation.d.ts +40 -2
- package/dist/surface/tools/act/act-observation.js +118 -12
- package/dist/surface/tools/act/act-preflight.js +1 -1
- package/dist/surface/tools/act/act-sequence-retry.d.ts +23 -2
- package/dist/surface/tools/act/act-sequence-retry.js +61 -5
- package/dist/surface/tools/act/after-match.d.ts +4 -1
- package/dist/surface/tools/act/after-match.js +8 -1
- package/dist/surface/tools/act/already-true.js +1 -1
- package/dist/surface/tools/act/navigation/reload-result.d.ts +12 -1
- package/dist/surface/tools/act/navigation/reload-result.js +25 -2
- package/dist/surface/tools/act/resolve-target.d.ts +15 -1
- package/dist/surface/tools/act/resolve-target.js +24 -1
- package/dist/surface/tools/act/sequence-grade.d.ts +12 -0
- package/dist/surface/tools/act/sequence-grade.js +37 -26
- package/dist/surface/tools/act/settle-in-flight.js +27 -15
- package/dist/surface/tools/act-command.d.ts +23 -0
- package/dist/surface/tools/act-command.js +50 -0
- package/dist/surface/tools/act-sequence-tool.js +78 -50
- package/dist/surface/tools/act-tools.d.ts +1 -13
- package/dist/surface/tools/act-tools.js +74 -86
- package/dist/surface/tools/agent/build-staleness.d.ts +1 -1
- package/dist/surface/tools/agent/build-staleness.js +1 -1
- package/dist/surface/tools/assert/assert-verdict.js +16 -37
- package/dist/surface/tools/browser-tools.js +12 -6
- package/dist/surface/tools/compare-tools.d.ts +11 -0
- package/dist/surface/tools/compare-tools.js +94 -0
- package/dist/surface/tools/coverage-tools.js +1 -1
- package/dist/surface/tools/dynamic-tools.js +9 -2
- package/dist/surface/tools/error-recovery.d.ts +12 -1
- package/dist/surface/tools/error-recovery.js +91 -21
- package/dist/surface/tools/explore-tools.js +1 -1
- package/dist/surface/tools/invoke-tool.js +3 -2
- package/dist/surface/tools/lease-coverage-tools.d.ts +49 -0
- package/dist/surface/tools/lease-coverage-tools.js +128 -0
- package/dist/surface/tools/lease-hint.d.ts +35 -1
- package/dist/surface/tools/lease-hint.js +42 -0
- package/dist/surface/tools/lease-readiness.d.ts +26 -0
- package/dist/surface/tools/lease-readiness.js +79 -0
- package/dist/surface/tools/lease-seed.d.ts +14 -1
- package/dist/surface/tools/lease-seed.js +35 -3
- package/dist/surface/tools/lease-tools.d.ts +3 -41
- package/dist/surface/tools/lease-tools.js +62 -110
- package/dist/surface/tools/lease-url-identity.d.ts +46 -0
- package/dist/surface/tools/lease-url-identity.js +97 -0
- package/dist/surface/tools/merge-tools.js +99 -2
- package/dist/surface/tools/merged-name-redirect.js +5 -1
- package/dist/surface/tools/observe-tools.js +15 -9
- package/dist/surface/tools/read/console-pre-attach.d.ts +19 -0
- package/dist/surface/tools/read/console-pre-attach.js +23 -0
- package/dist/surface/tools/read-tools.js +33 -30
- package/dist/surface/tools/real-input-attempt.d.ts +0 -8
- package/dist/surface/tools/real-input-attempt.js +143 -28
- package/dist/surface/tools/session-ports.d.ts +14 -0
- package/dist/surface/tools/session-ports.js +53 -0
- package/dist/surface/tools/tool-kit.d.ts +6 -0
- package/dist/surface/tools/tool-kit.js +2 -3
- package/dist/surface/tools/tool-surface.js +0 -1
- package/dist/surface/tools/tools.js +28 -5
- package/dist/surface/tools/type-spelling.d.ts +13 -0
- package/dist/surface/tools/type-spelling.js +36 -0
- package/dist/telemetry/agent-client.d.ts +6 -0
- package/dist/telemetry/agent-client.js +10 -0
- package/dist/telemetry/argument-shape.js +1 -0
- package/dist/telemetry/tool-refused.d.ts +6 -3
- package/dist/telemetry/tool-refused.js +8 -3
- package/docs/actions.mdx +3 -3
- package/docs/agent-cheatsheet.md +1 -1
- package/docs/capabilities.mdx +61 -1
- package/docs/cli/affected.mdx +9 -7
- package/docs/cli/cloud.mdx +3 -3
- package/docs/cli/compare.mdx +94 -0
- package/docs/cli/doctor.mdx +1 -1
- package/docs/cli/grade.mdx +97 -0
- package/docs/cli/license.mdx +9 -7
- package/docs/cli/restart.mdx +7 -5
- package/docs/cli/rollback.mdx +5 -5
- package/docs/cli/stop.mdx +3 -3
- package/docs/cli/update.mdx +2 -2
- package/docs/cli/verify.mdx +23 -1
- package/docs/cli.mdx +33 -23
- package/docs/deploy-checks.md +1 -1
- package/docs/desktop.mdx +1 -1
- package/docs/enterprise.md +2 -2
- package/docs/environment.mdx +1 -1
- package/docs/flows.md +11 -1
- package/docs/frameworks.mdx +9 -9
- package/docs/getting-started.md +1 -1
- package/docs/install-manual.mdx +1 -1
- package/docs/license-activation.mdx +3 -3
- package/docs/packages/tauri.mdx +1 -1
- package/docs/packages.mdx +1 -1
- package/docs/predicates.mdx +1 -1
- package/docs/quickstart.mdx +68 -49
- package/docs/skill-setup.md +1 -1
- package/docs/skill.md +14 -13
- package/docs/telemetry.md +2 -2
- package/docs/tools/navigate.mdx +1 -1
- package/docs/tools/verify.mdx +4 -1
- package/docs/troubleshooting.mdx +1 -1
- package/docs/usage.md +17 -14
- package/package.json +13 -13
- package/dist/features/exhaust/exhaust-run.d.ts +0 -33
- package/dist/features/exhaust/exhaust-run.js +0 -82
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The sequential half of `reticle_verify { action: "flows" | "change" }`.
|
|
3
|
+
*
|
|
4
|
+
* Flows replay one after the other against one live tab, with no limit on the total time. When the
|
|
5
|
+
* browser session was lost part-way through, every remaining flow waited for a page that never came,
|
|
6
|
+
* and the MCP client gave up (about 300 s) with no result at all - including for the flows that had
|
|
7
|
+
* already run. So the loop now stops at two points, and says so:
|
|
8
|
+
*
|
|
9
|
+
* - the total time budget is spent: no further flow is started;
|
|
10
|
+
* - the session is gone after a flow: the rest cannot run, so they are not attempted.
|
|
11
|
+
*
|
|
12
|
+
* Kept free of IO (the clock, the liveness probe and the replay are injected) so both stops are
|
|
13
|
+
* tested with a fake clock.
|
|
14
|
+
*/
|
|
15
|
+
import { z } from 'zod';
|
|
16
|
+
import { SuiteStopReason, SuiteVerdictStatus } from '@reticlehq/core';
|
|
17
|
+
/** Below the roughly 300 s at which common MCP clients stop waiting, so a result can still be returned. */
|
|
18
|
+
export declare const DEFAULT_SUITE_BUDGET_MS = 240000;
|
|
19
|
+
/** An hour: more than that is not a budget, and a client will have given up long before. */
|
|
20
|
+
export declare const MAX_SUITE_BUDGET_MS = 3600000;
|
|
21
|
+
export interface SuiteStop {
|
|
22
|
+
reason: SuiteStopReason;
|
|
23
|
+
/** For SESSION_LOST: the flow that was running when the session went. */
|
|
24
|
+
flow?: string;
|
|
25
|
+
budgetMs?: number;
|
|
26
|
+
nextAction: string;
|
|
27
|
+
}
|
|
28
|
+
export interface SequentialRun<R> {
|
|
29
|
+
results: R[];
|
|
30
|
+
/** Selected flows that were never started, in order. */
|
|
31
|
+
notRun: string[];
|
|
32
|
+
stopped?: SuiteStop;
|
|
33
|
+
}
|
|
34
|
+
export interface SequentialRunOptions<R> {
|
|
35
|
+
now: () => number;
|
|
36
|
+
budgetMs: number;
|
|
37
|
+
/** True while the session the flows drive is still there. Probed after each flow. */
|
|
38
|
+
sessionAlive: () => boolean;
|
|
39
|
+
run: (name: string) => Promise<R>;
|
|
40
|
+
}
|
|
41
|
+
export declare function runSequentially<R>(names: readonly string[], options: SequentialRunOptions<R>): Promise<SequentialRun<R>>;
|
|
42
|
+
/**
|
|
43
|
+
* Mark a verdict `partial` when the suite stopped early. The ran flows keep their counts and
|
|
44
|
+
* failures; the summary leads with what did not run, because a green count over half a suite must
|
|
45
|
+
* not read as a pass.
|
|
46
|
+
*/
|
|
47
|
+
export declare function withStop<V extends {
|
|
48
|
+
status: SuiteVerdictStatus;
|
|
49
|
+
summary: string;
|
|
50
|
+
}>(verdict: V, notRun: readonly string[], stopped: SuiteStop | undefined): V & {
|
|
51
|
+
notRun?: string[];
|
|
52
|
+
stopped?: SuiteStop;
|
|
53
|
+
};
|
|
54
|
+
/** The `budgetMs` input of the suite tools. */
|
|
55
|
+
export declare const BUDGET_MS_ARG: z.ZodOptional<z.ZodNumber>;
|
|
56
|
+
/** The two output fields a "partial" suite adds. */
|
|
57
|
+
export declare const PARTIAL_OUTPUT: {
|
|
58
|
+
notRun: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
59
|
+
stopped: z.ZodOptional<z.ZodObject<{
|
|
60
|
+
reason: z.ZodEnum<{
|
|
61
|
+
time_budget: "time_budget";
|
|
62
|
+
session_lost: "session_lost";
|
|
63
|
+
}>;
|
|
64
|
+
flow: z.ZodOptional<z.ZodString>;
|
|
65
|
+
budgetMs: z.ZodOptional<z.ZodNumber>;
|
|
66
|
+
nextAction: z.ZodString;
|
|
67
|
+
}, z.core.$strip>>;
|
|
68
|
+
};
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The sequential half of `reticle_verify { action: "flows" | "change" }`.
|
|
3
|
+
*
|
|
4
|
+
* Flows replay one after the other against one live tab, with no limit on the total time. When the
|
|
5
|
+
* browser session was lost part-way through, every remaining flow waited for a page that never came,
|
|
6
|
+
* and the MCP client gave up (about 300 s) with no result at all - including for the flows that had
|
|
7
|
+
* already run. So the loop now stops at two points, and says so:
|
|
8
|
+
*
|
|
9
|
+
* - the total time budget is spent: no further flow is started;
|
|
10
|
+
* - the session is gone after a flow: the rest cannot run, so they are not attempted.
|
|
11
|
+
*
|
|
12
|
+
* Kept free of IO (the clock, the liveness probe and the replay are injected) so both stops are
|
|
13
|
+
* tested with a fake clock.
|
|
14
|
+
*/
|
|
15
|
+
import { z } from 'zod';
|
|
16
|
+
import { SuiteStopReason, SuiteVerdictStatus } from '@reticlehq/core';
|
|
17
|
+
/** Below the roughly 300 s at which common MCP clients stop waiting, so a result can still be returned. */
|
|
18
|
+
export const DEFAULT_SUITE_BUDGET_MS = 240_000;
|
|
19
|
+
/** An hour: more than that is not a budget, and a client will have given up long before. */
|
|
20
|
+
export const MAX_SUITE_BUDGET_MS = 3_600_000;
|
|
21
|
+
export async function runSequentially(names, options) {
|
|
22
|
+
const deadline = options.now() + options.budgetMs;
|
|
23
|
+
const results = [];
|
|
24
|
+
for (const [index, name] of names.entries()) {
|
|
25
|
+
// The first flow always starts: a budget that stops a suite before anything ran proves nothing.
|
|
26
|
+
if (index > 0 && options.now() >= deadline) {
|
|
27
|
+
return {
|
|
28
|
+
results,
|
|
29
|
+
notRun: names.slice(index),
|
|
30
|
+
stopped: {
|
|
31
|
+
reason: SuiteStopReason.TIME_BUDGET,
|
|
32
|
+
budgetMs: options.budgetMs,
|
|
33
|
+
nextAction: 'run the flows listed in notRun in a new call (pass `names`), or raise `budgetMs` if your client waits longer',
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
results.push(await options.run(name));
|
|
38
|
+
if (index < names.length - 1 && !options.sessionAlive()) {
|
|
39
|
+
return {
|
|
40
|
+
results,
|
|
41
|
+
notRun: names.slice(index + 1),
|
|
42
|
+
stopped: {
|
|
43
|
+
reason: SuiteStopReason.SESSION_LOST,
|
|
44
|
+
flow: name,
|
|
45
|
+
nextAction: 'reconnect the app (open it in a tab, or call reticle_sessions to find the live one), then run the flows listed in notRun with `names`',
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return { results, notRun: [] };
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Mark a verdict `partial` when the suite stopped early. The ran flows keep their counts and
|
|
54
|
+
* failures; the summary leads with what did not run, because a green count over half a suite must
|
|
55
|
+
* not read as a pass.
|
|
56
|
+
*/
|
|
57
|
+
export function withStop(verdict, notRun, stopped) {
|
|
58
|
+
if (stopped === undefined) {
|
|
59
|
+
return verdict;
|
|
60
|
+
}
|
|
61
|
+
const why = stopped.reason === SuiteStopReason.TIME_BUDGET
|
|
62
|
+
? `the ${String(Math.round((stopped.budgetMs ?? 0) / 1000))} s total budget ran out`
|
|
63
|
+
: `the browser session was lost while "${stopped.flow ?? ''}" was running`;
|
|
64
|
+
return {
|
|
65
|
+
...verdict,
|
|
66
|
+
status: SuiteVerdictStatus.PARTIAL,
|
|
67
|
+
summary: `PARTIAL - ${why}; ${String(notRun.length)} flow(s) did not run (${notRun.join(', ')}). Of those that ran: ${verdict.summary}`,
|
|
68
|
+
notRun: [...notRun],
|
|
69
|
+
stopped,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
/** The `budgetMs` input of the suite tools. */
|
|
73
|
+
export const BUDGET_MS_ARG = z
|
|
74
|
+
.number()
|
|
75
|
+
.finite()
|
|
76
|
+
.int()
|
|
77
|
+
.positive()
|
|
78
|
+
.max(MAX_SUITE_BUDGET_MS)
|
|
79
|
+
.optional()
|
|
80
|
+
.describe(`Total time budget for the sequential run, in milliseconds (default ${String(DEFAULT_SUITE_BUDGET_MS)}, below the ~300 s at which common MCP clients stop waiting). When it is spent no further flow is started: the call returns status "partial" with the finished results and the flows in notRun.`);
|
|
81
|
+
/** The two output fields a "partial" suite adds. */
|
|
82
|
+
export const PARTIAL_OUTPUT = {
|
|
83
|
+
notRun: z
|
|
84
|
+
.array(z.string())
|
|
85
|
+
.optional()
|
|
86
|
+
.describe('Present on a "partial" suite: the selected flows that were never started because the time budget ran out or the session was lost.'),
|
|
87
|
+
stopped: z
|
|
88
|
+
.object({
|
|
89
|
+
reason: z.enum([SuiteStopReason.TIME_BUDGET, SuiteStopReason.SESSION_LOST]),
|
|
90
|
+
flow: z.string().optional(),
|
|
91
|
+
budgetMs: z.number().optional(),
|
|
92
|
+
nextAction: z.string(),
|
|
93
|
+
})
|
|
94
|
+
.optional()
|
|
95
|
+
.describe('Present on a "partial" suite: why it ended early (time_budget | session_lost), the flow that lost the session, and one next action.'),
|
|
96
|
+
};
|
|
@@ -1,19 +1,21 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
-
import { sessionRoot
|
|
2
|
+
import { sessionRoot } from '../../memory/project/session-root.js';
|
|
3
3
|
import { verdictForSuite } from '../../judgement/outcome/verify-change-verdict.js';
|
|
4
4
|
import { attributedFailures } from '../../judgement/outcome/attributed-failure.js';
|
|
5
5
|
import { Verified, isAdvisory } from '@reticlehq/core';
|
|
6
6
|
import { ReticleTool } from '@reticlehq/core';
|
|
7
7
|
import { asNumber, asRecord, asString } from '@reticlehq/core';
|
|
8
8
|
import { workerCountSchema } from '../../surface/tools/args/numeric-bounds.js';
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
9
|
+
import { resolveChangedFiles } from '../../command/cli/cli-flow-commands.js';
|
|
10
|
+
import { namedFlowsForSession } from './flow-store-for-session.js';
|
|
11
11
|
import { affectedSavedFlows } from './change/flow-sources.js';
|
|
12
12
|
import { FLOW_TOOLS } from './flow-tools.js';
|
|
13
13
|
import { RESULT_FIELD, changeResult } from '../../surface/tools/reticle-result.js';
|
|
14
14
|
import { findContradictions } from '@reticlehq/engine/disagreement/contradictions.js';
|
|
15
|
+
import { isOneActionOnly } from './flow-cross-step.js';
|
|
15
16
|
import { parseControls } from '../../surface/tools/coverage-tools.js';
|
|
16
17
|
import { ReticleCommand, SnapshotMode } from '@reticlehq/core';
|
|
18
|
+
import { BUDGET_MS_ARG } from './suite/sequential-run.js';
|
|
17
19
|
const VERIFY_CHANGE = {
|
|
18
20
|
name: ReticleTool.VERIFY_CHANGE,
|
|
19
21
|
description: 'Did my change break anything? Give it the files you edited (or `since` for a git ref) and it works out which saved flows cover them, replays exactly those, and answers with one `verified` (yes|no|unknown) plus `because`. `unknown` when NO saved flow covers the change — that is the honest answer, never a green: nothing ran, so nothing was proved. Returns { verified, because, changedFiles, flowsRun, suite, unknownProvenance }.',
|
|
@@ -30,6 +32,11 @@ const VERIFY_CHANGE = {
|
|
|
30
32
|
parallel: workerCountSchema
|
|
31
33
|
.optional()
|
|
32
34
|
.describe('Replay this many affected flows at once (needs the browser pool).'),
|
|
35
|
+
ignoreHTTPSErrors: z
|
|
36
|
+
.boolean()
|
|
37
|
+
.optional()
|
|
38
|
+
.describe('Accept a self-signed or local-CA certificate for parallel flow leases. Omit to inherit the source lease setting; otherwise certificate checks stay on.'),
|
|
39
|
+
budgetMs: BUDGET_MS_ARG,
|
|
33
40
|
sessionId: z
|
|
34
41
|
.string()
|
|
35
42
|
.optional()
|
|
@@ -68,16 +75,14 @@ const VERIFY_CHANGE = {
|
|
|
68
75
|
// used to say it was: `deps.reticleRoot` is the daemon's `.reticle` SUBDIRECTORY, and the
|
|
69
76
|
// daemon's cwd is wherever the MCP host started it — `/` or `$HOME` for a globally-registered
|
|
70
77
|
// server. Both answer about the wrong repository while reporting a clean "nothing changed",
|
|
71
|
-
// which reads as a pass. `
|
|
72
|
-
|
|
78
|
+
// which reads as a pass. `projectDir` is the directory holding the resolved root.
|
|
79
|
+
//
|
|
80
|
+
// The flows come from the store `flows` replays from, and flow_save writes to. A read that
|
|
81
|
+
// resolved differently would report "no flows covered this change" over flows that exist,
|
|
82
|
+
// which reads as a clean result rather than an error (#1996).
|
|
83
|
+
const { flows, projectDir } = await namedFlowsForSession(deps, asString(args['sessionId']));
|
|
84
|
+
const changed = await resolveChangedFiles(files, since, projectDir);
|
|
73
85
|
const changedFiles = changed.files;
|
|
74
|
-
// Same root flow_save wrote to. A read that resolved differently would report "no flows
|
|
75
|
-
// covered this change" over flows that exist, which reads as a clean result rather than
|
|
76
|
-
// an error, so the disagreement would be invisible.
|
|
77
|
-
// Both halves of the address from one place. The root was already resolved from the session;
|
|
78
|
-
// the id used to come from the daemon's cwd, so they named different projects.
|
|
79
|
-
const sid = asString(args['sessionId']);
|
|
80
|
-
const flows = await loadNamedFlows(deps.fs, sessionRoot(deps, sid), sessionProjectId(deps, sid));
|
|
81
86
|
const { affected, unknownProvenance } = affectedSavedFlows(flows, changedFiles);
|
|
82
87
|
if (0 === changedFiles.length) {
|
|
83
88
|
return {
|
|
@@ -126,11 +131,16 @@ const VERIFY_CHANGE = {
|
|
|
126
131
|
// flows produced, and never events from whatever the agent did before calling this.
|
|
127
132
|
const cursor = cursorBeforeReplay(deps, args);
|
|
128
133
|
const verify = FLOW_TOOLS.find((tool) => tool.name === ReticleTool.FLOW_VERIFY);
|
|
129
|
-
if (verify === undefined)
|
|
134
|
+
if (verify === undefined) {
|
|
130
135
|
throw new Error('the whole-suite replay tool is not registered');
|
|
136
|
+
}
|
|
131
137
|
const suite = asRecord(await verify.handler(deps, {
|
|
132
138
|
names: affected,
|
|
133
139
|
...(asNumber(args['parallel']) === undefined ? {} : { parallel: args['parallel'] }),
|
|
140
|
+
...(asNumber(args['budgetMs']) === undefined ? {} : { budgetMs: args['budgetMs'] }),
|
|
141
|
+
...('boolean' === typeof args['ignoreHTTPSErrors']
|
|
142
|
+
? { ignoreHTTPSErrors: args['ignoreHTTPSErrors'] }
|
|
143
|
+
: {}),
|
|
134
144
|
...(asString(args['sessionId']) === undefined ? {} : { sessionId: args['sessionId'] }),
|
|
135
145
|
}));
|
|
136
146
|
const failed = suite.failed ?? 0;
|
|
@@ -254,9 +264,10 @@ async function inspectAfterReplay(deps, args, cursor) {
|
|
|
254
264
|
appOrigin: session.url,
|
|
255
265
|
background: session.background,
|
|
256
266
|
namedNetUrls: [],
|
|
257
|
-
// The
|
|
267
|
+
// The cursor is where the replay started. The replay is many user actions, not one, so a
|
|
268
|
+
// finding that only means something inside one action's window is left to the steps (#2013).
|
|
258
269
|
actionSince: cursor,
|
|
259
|
-
});
|
|
270
|
+
}).filter((found) => !isOneActionOnly(found));
|
|
260
271
|
const snapshot = await session.command(ReticleCommand.SNAPSHOT, {
|
|
261
272
|
mode: SnapshotMode.INTERACTIVE,
|
|
262
273
|
});
|
|
@@ -15,6 +15,19 @@ export interface BackgroundSyncGate {
|
|
|
15
15
|
/** Who is sending, when it is ON: a new value after a new sign-in or a new key. */
|
|
16
16
|
credential?: string;
|
|
17
17
|
}
|
|
18
|
+
/**
|
|
19
|
+
* The switch a person set to stop sending without being asked, or undefined when nobody set it.
|
|
20
|
+
*
|
|
21
|
+
* Every send that nobody asked for reads it, the daemon's and the one `start()` runs on close. The
|
|
22
|
+
* close cycle read nothing, so a person who switched it off still sent this project and every
|
|
23
|
+
* other linked one when `reticle drive` or a self-booting `verify` closed.
|
|
24
|
+
*/
|
|
25
|
+
export declare function backgroundSyncOptOut(reticleRoot: string, env: NodeJS.ProcessEnv): BackgroundSyncGate | undefined;
|
|
26
|
+
/**
|
|
27
|
+
* The gate of the one cycle `start()` sends on close, with nobody signed in. CI's key in the
|
|
28
|
+
* environment still sends there, as the docs promise. A switch that a person set still stops it.
|
|
29
|
+
*/
|
|
30
|
+
export declare function closeCycleGate(reticleRoot: string, env: NodeJS.ProcessEnv): BackgroundSyncGate;
|
|
18
31
|
/** May the daemon send this root's work now? Reads the disk, never the network. */
|
|
19
32
|
export declare function backgroundSyncGate(input: {
|
|
20
33
|
reticleRoot: string;
|
|
@@ -29,9 +29,14 @@ export const BackgroundSyncState = {
|
|
|
29
29
|
};
|
|
30
30
|
const OFF_VALUES = new Set(['0', 'false', 'off', 'no']);
|
|
31
31
|
const BACKGROUND_SYNC_SETTING = 'backgroundSync';
|
|
32
|
-
/**
|
|
33
|
-
|
|
34
|
-
|
|
32
|
+
/**
|
|
33
|
+
* The switch a person set to stop sending without being asked, or undefined when nobody set it.
|
|
34
|
+
*
|
|
35
|
+
* Every send that nobody asked for reads it, the daemon's and the one `start()` runs on close. The
|
|
36
|
+
* close cycle read nothing, so a person who switched it off still sent this project and every
|
|
37
|
+
* other linked one when `reticle drive` or a self-booting `verify` closed.
|
|
38
|
+
*/
|
|
39
|
+
export function backgroundSyncOptOut(reticleRoot, env) {
|
|
35
40
|
if (OFF_VALUES.has((env[ReticleEnv.BACKGROUND_SYNC] ?? '').toLowerCase())) {
|
|
36
41
|
return {
|
|
37
42
|
state: BackgroundSyncState.OPTED_OUT,
|
|
@@ -44,6 +49,22 @@ export function backgroundSyncGate(input) {
|
|
|
44
49
|
why: `off in this project ("${BACKGROUND_SYNC_SETTING}": false in .reticle.json)`,
|
|
45
50
|
};
|
|
46
51
|
}
|
|
52
|
+
return undefined;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* The gate of the one cycle `start()` sends on close, with nobody signed in. CI's key in the
|
|
56
|
+
* environment still sends there, as the docs promise. A switch that a person set still stops it.
|
|
57
|
+
*/
|
|
58
|
+
export function closeCycleGate(reticleRoot, env) {
|
|
59
|
+
return (backgroundSyncOptOut(reticleRoot, env) ?? { state: BackgroundSyncState.ON, why: 'on close' });
|
|
60
|
+
}
|
|
61
|
+
/** May the daemon send this root's work now? Reads the disk, never the network. */
|
|
62
|
+
export function backgroundSyncGate(input) {
|
|
63
|
+
const { reticleRoot, cloud, env, signedIn } = input;
|
|
64
|
+
const optedOut = backgroundSyncOptOut(reticleRoot, env);
|
|
65
|
+
if (optedOut !== undefined) {
|
|
66
|
+
return optedOut;
|
|
67
|
+
}
|
|
47
68
|
// `link` is set only from the project's own link file; an environment key leaves it unset.
|
|
48
69
|
if (cloud.link === undefined) {
|
|
49
70
|
return {
|
|
@@ -41,7 +41,7 @@ import { describeSync } from './sync-describe.js';
|
|
|
41
41
|
import { diskSink, diskSource, policyScreenshots, readCloudState } from './sync-disk.js';
|
|
42
42
|
import { resolveProjectCloud } from './cloud-config.js';
|
|
43
43
|
import { watch } from 'node:fs';
|
|
44
|
-
import { BackgroundSyncState, backgroundSyncGate, } from './background-sync.js';
|
|
44
|
+
import { BackgroundSyncState, backgroundSyncGate, closeCycleGate, } from './background-sync.js';
|
|
45
45
|
import { unmatchedRoots, unsyncedRoots } from './unsynced-roots.js';
|
|
46
46
|
/**
|
|
47
47
|
* The backstop: how often the daemon cycles when nothing under `.reticle` has changed. Long, because
|
|
@@ -186,38 +186,39 @@ export function startSyncDaemon(deps) {
|
|
|
186
186
|
let wasLinked;
|
|
187
187
|
/** The project the last cycle synced to: a re-link to another one is announced too. */
|
|
188
188
|
let wasProject;
|
|
189
|
-
/** The last
|
|
190
|
-
|
|
189
|
+
/** The aggregate last logged: unlinked roots are announced as one line per change, never per tick. */
|
|
190
|
+
let announcedUnsynced;
|
|
191
191
|
let unsynced = [];
|
|
192
192
|
/** Which roots pushed this cycle, and whether each is linked by its own link file. */
|
|
193
193
|
let rootsLinked = new Map();
|
|
194
194
|
/** The linked roots that send flows: a link with `sync.flows` off keeps every flow local. */
|
|
195
195
|
let rootsSendFlows = new Set();
|
|
196
|
-
/**
|
|
197
|
-
* Count what every root still owes, and say once per change where runs sit that nothing sends.
|
|
198
|
-
* Linked roots are listed too (the platform report and `doctor` read them), but only an unlinked
|
|
199
|
-
* one earns a log line: a linked root's pending runs are the next cycle's job.
|
|
200
|
-
*/
|
|
196
|
+
/** Count what every root owes; unlinked roots are one aggregate line per change, never per tick. */
|
|
201
197
|
const refreshUnsynced = async () => {
|
|
202
198
|
const linked = rootsLinked;
|
|
203
199
|
const sendsFlows = rootsSendFlows;
|
|
204
200
|
unsynced = await unsyncedRoots(
|
|
205
201
|
// With what no project claimed, which no cycle pushes: the dashboard's unsent list left it out.
|
|
206
202
|
[...linked.keys(), ...unmatchedRoots(homedir())], (r) => Promise.resolve(true === linked.get(r)), (r) => Promise.resolve(sendsFlows.has(r)));
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
linked: entry.linked,
|
|
218
|
-
fix: "run `reticle connect` in that folder's project: this work is on this machine only",
|
|
219
|
-
});
|
|
203
|
+
const unlinked = unsynced.filter((entry) => !entry.linked);
|
|
204
|
+
const runs = unlinked.reduce((total, entry) => total + entry.runs, 0);
|
|
205
|
+
const flows = unlinked.reduce((total, entry) => total + entry.flows, 0);
|
|
206
|
+
const signature = JSON.stringify([unlinked.map((e) => [e.root, e.runs, e.flows]), runs, flows]);
|
|
207
|
+
if (0 === unlinked.length) {
|
|
208
|
+
announcedUnsynced = undefined;
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
if (announcedUnsynced === signature) {
|
|
212
|
+
return;
|
|
220
213
|
}
|
|
214
|
+
announcedUnsynced = signature;
|
|
215
|
+
log('reticle_cloud_unsynced_root', {
|
|
216
|
+
count: unlinked.length,
|
|
217
|
+
runs,
|
|
218
|
+
flows,
|
|
219
|
+
examples: unlinked.slice(0, 3).map((entry) => entry.root),
|
|
220
|
+
fix: 'run `reticle connect` in each project that owns these folders: this work is on this machine only',
|
|
221
|
+
});
|
|
221
222
|
};
|
|
222
223
|
/**
|
|
223
224
|
* The cloud as this tick may use it: unchanged when the gate lets the root send, else with no
|
|
@@ -778,12 +779,10 @@ export function attachCloudSync(deps) {
|
|
|
778
779
|
// and on the backstop timer.
|
|
779
780
|
// Watching them too means one recursive watcher per linked repo — add it if siblings lag.
|
|
780
781
|
watch: (onChange) => watchReticleRoot(deps.reticleRoot, onChange),
|
|
781
|
-
// Sent only
|
|
782
|
-
|
|
783
|
-
?
|
|
784
|
-
: {
|
|
785
|
-
gate: (root, cloud) => backgroundSyncGate({ reticleRoot: root, cloud, env: deps.env, signedIn }),
|
|
786
|
-
}),
|
|
782
|
+
// Sent only to a linked project that did not switch it off; see `closeCycleGate` for no sign-in.
|
|
783
|
+
gate: (root, cloud) => signedIn === undefined
|
|
784
|
+
? closeCycleGate(root, deps.env)
|
|
785
|
+
: backgroundSyncGate({ reticleRoot: root, cloud, env: deps.env, signedIn }),
|
|
787
786
|
...(deps.request === undefined ? {} : { request: deps.request }),
|
|
788
787
|
...(deps.intervalMs === undefined ? {} : { intervalMs: deps.intervalMs }),
|
|
789
788
|
});
|
|
@@ -19,7 +19,8 @@ export function makeJournalAttach(deps) {
|
|
|
19
19
|
log('journal_skipped_unsafe_session_id', { sessionId: session.id });
|
|
20
20
|
return;
|
|
21
21
|
}
|
|
22
|
-
|
|
22
|
+
// The session's clock stamps each record's `t`, so it also bounds a cut that left nothing to parse.
|
|
23
|
+
const journal = new SessionJournal(deps.fs, session.artifactRoot ?? deps.reticleRoot, session.id, { now: () => session.elapsed() });
|
|
23
24
|
// Same SessionJournal is both the write sink and the read fall-through for queries after eviction.
|
|
24
25
|
session.setJournal(new JournalRecorder(journal, { now: () => session.elapsed() }), journal);
|
|
25
26
|
};
|
|
@@ -44,6 +44,13 @@ export interface DriveProgram {
|
|
|
44
44
|
*
|
|
45
45
|
* So it is not a tool and not a rule. It runs at teardown, in code, on every session.
|
|
46
46
|
*/
|
|
47
|
+
/**
|
|
48
|
+
* The claims one tape step carries: its own `expect`, and the `expect` of each of its sub-steps.
|
|
49
|
+
*
|
|
50
|
+
* A `reticle_act { steps }` journey is one tape step, and each claim is on its sub-step. When only
|
|
51
|
+
* the step's own `expect` was read, a journey with four held claims was "unproven" and was not saved.
|
|
52
|
+
*/
|
|
53
|
+
export declare function claimsOf(step: TapeStep): Predicate[];
|
|
47
54
|
/** The one condition on saving, and the reason the whole thing is safe. */
|
|
48
55
|
export declare function carriesAnAssertion(steps: readonly TapeStep[]): boolean;
|
|
49
56
|
/** Every auto-saved flow's name starts here, which is what lets a merge tell them from hand-named ones. */
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ActionType, QueryBy, REPLAY_PROGRAM_VERSION, asFlowName,
|
|
1
|
+
import { ActionType, FlowPredicateSchema, QueryBy, REPLAY_PROGRAM_VERSION, asFlowName, asRecord, isCredentialFieldName, } from '@reticlehq/core';
|
|
2
2
|
import { journeySentence } from '@reticlehq/core';
|
|
3
3
|
/**
|
|
4
4
|
* What a session drove, saved as a replayable flow — without anybody asking for it.
|
|
@@ -11,9 +11,23 @@ import { journeySentence } from '@reticlehq/core';
|
|
|
11
11
|
*
|
|
12
12
|
* So it is not a tool and not a rule. It runs at teardown, in code, on every session.
|
|
13
13
|
*/
|
|
14
|
+
/**
|
|
15
|
+
* The claims one tape step carries: its own `expect`, and the `expect` of each of its sub-steps.
|
|
16
|
+
*
|
|
17
|
+
* A `reticle_act { steps }` journey is one tape step, and each claim is on its sub-step. When only
|
|
18
|
+
* the step's own `expect` was read, a journey with four held claims was "unproven" and was not saved.
|
|
19
|
+
*/
|
|
20
|
+
export function claimsOf(step) {
|
|
21
|
+
const subs = Array.isArray(step.args['steps']) ? step.args['steps'] : [];
|
|
22
|
+
const subClaims = subs.flatMap((sub) => {
|
|
23
|
+
const parsed = FlowPredicateSchema.safeParse(asRecord(sub)['expect']);
|
|
24
|
+
return parsed.success ? [parsed.data] : [];
|
|
25
|
+
});
|
|
26
|
+
return step.expect === undefined ? subClaims : [step.expect, ...subClaims];
|
|
27
|
+
}
|
|
14
28
|
/** The one condition on saving, and the reason the whole thing is safe. */
|
|
15
29
|
export function carriesAnAssertion(steps) {
|
|
16
|
-
return steps.some((step) =>
|
|
30
|
+
return steps.some((step) => 0 < claimsOf(step).length);
|
|
17
31
|
}
|
|
18
32
|
/** Squeeze any string into one safe, lower-case filename segment. */
|
|
19
33
|
const segment = (value, max) => value
|
|
@@ -28,10 +42,12 @@ const segment = (value, max) => value
|
|
|
28
42
|
* said nothing about what it was for.
|
|
29
43
|
*/
|
|
30
44
|
function provedSentence(steps) {
|
|
31
|
-
return journeySentence(steps.flatMap(
|
|
45
|
+
return journeySentence(steps.flatMap(claimsOf));
|
|
32
46
|
}
|
|
33
47
|
/** Every auto-saved flow's name starts here, which is what lets a merge tell them from hand-named ones. */
|
|
34
48
|
export const DRIVE_FLOW_PREFIX = 'drive-';
|
|
49
|
+
/** The most characters one segment of core's FLOW_NAME_PATTERN holds. */
|
|
50
|
+
const FLOW_NAME_MAX_LENGTH = 64;
|
|
35
51
|
/**
|
|
36
52
|
* A readable name for a journey, led by WHY it was driven.
|
|
37
53
|
*
|
|
@@ -52,7 +68,10 @@ export function driveFlowName(route, steps = []) {
|
|
|
52
68
|
// Minted here: every character has just been forced through `segment`, so this IS the validated
|
|
53
69
|
// boundary the brand is meant to be created at.
|
|
54
70
|
const parts = [lead, ...('' !== why || 0 === leg.length ? [] : [leg])].filter((p) => '' !== p);
|
|
55
|
-
|
|
71
|
+
// Cut to the flow-name limit: a sentence and a route together went over it, the save refused
|
|
72
|
+
// the name, and session end dropped the flow with nothing said.
|
|
73
|
+
const body = segment(parts.join('-'), FLOW_NAME_MAX_LENGTH - DRIVE_FLOW_PREFIX.length);
|
|
74
|
+
return asFlowName(`${DRIVE_FLOW_PREFIX}${'' === body ? 'journey' : body}`);
|
|
56
75
|
}
|
|
57
76
|
/** Why session end saved no flow, as reported on the `flow_recorded` step. */
|
|
58
77
|
export const DriveFlowSkipReason = {
|
|
@@ -106,16 +125,25 @@ function segmentsByRoute(steps) {
|
|
|
106
125
|
return out;
|
|
107
126
|
}
|
|
108
127
|
/**
|
|
109
|
-
* A fill into a field
|
|
128
|
+
* A fill into a field whose name says it holds a credential — the sign-in a journey must not carry.
|
|
110
129
|
* Read off the recorder's anchor args: a testid names the field by `value`, a role by `name`.
|
|
111
130
|
*/
|
|
112
131
|
function typesASecret(step) {
|
|
113
|
-
|
|
114
|
-
|
|
132
|
+
// A sign-in sent as one reticle_act sequence carries its fills as sub-steps.
|
|
133
|
+
const subs = step.args['steps'];
|
|
134
|
+
if (Array.isArray(subs)) {
|
|
135
|
+
return subs.some((sub) => fillsASecret(asRecord(sub)));
|
|
136
|
+
}
|
|
137
|
+
return fillsASecret(step.args);
|
|
138
|
+
}
|
|
139
|
+
function fillsASecret(args) {
|
|
140
|
+
const action = args['action'];
|
|
141
|
+
if (ActionType.FILL !== action && ActionType.TYPE !== action) {
|
|
115
142
|
return false;
|
|
116
|
-
|
|
117
|
-
const
|
|
118
|
-
|
|
143
|
+
}
|
|
144
|
+
const by = args['by'];
|
|
145
|
+
const field = QueryBy.TESTID === by ? args['value'] : args['name'];
|
|
146
|
+
return 'string' === typeof field && isCredentialFieldName(field);
|
|
119
147
|
}
|
|
120
148
|
/**
|
|
121
149
|
* The tape without the attempts an agent made again. An action that declared nothing, followed at
|
|
@@ -126,11 +154,13 @@ function typesASecret(step) {
|
|
|
126
154
|
function withoutRetries(steps) {
|
|
127
155
|
return steps.filter((step, i) => {
|
|
128
156
|
const next = steps[i + 1];
|
|
129
|
-
if (next === undefined || step.expect !== undefined || next.expect === undefined)
|
|
157
|
+
if (next === undefined || step.expect !== undefined || next.expect === undefined) {
|
|
130
158
|
return true;
|
|
159
|
+
}
|
|
131
160
|
// Tried again where it was tried: a page later it is a step of the journey, not a retry.
|
|
132
|
-
if (step.route !== next.route || step.page !== next.page || step.mocked !== next.mocked)
|
|
161
|
+
if (step.route !== next.route || step.page !== next.page || step.mocked !== next.mocked) {
|
|
133
162
|
return true;
|
|
163
|
+
}
|
|
134
164
|
const { expect: _declared, ...again } = next.args;
|
|
135
165
|
const { expect: _none, ...first } = step.args;
|
|
136
166
|
return step.tool !== next.tool || JSON.stringify(first) !== JSON.stringify(again);
|
|
@@ -138,8 +168,9 @@ function withoutRetries(steps) {
|
|
|
138
168
|
}
|
|
139
169
|
/** A flow per journey, so two journeys in one session are two regression tests rather than one. */
|
|
140
170
|
export function driveFlowsFrom(tape) {
|
|
141
|
-
if (tape === undefined || 0 === tape.steps.length)
|
|
171
|
+
if (tape === undefined || 0 === tape.steps.length) {
|
|
142
172
|
return { programs: [], outcome: {} };
|
|
173
|
+
}
|
|
143
174
|
const programs = [];
|
|
144
175
|
let unproven = 0;
|
|
145
176
|
let mocked = 0;
|
|
@@ -30,8 +30,10 @@ export interface JournalReadLoss {
|
|
|
30
30
|
/** Parsed records evicted from the retained set. Exact, unlike `droppedBytes`. */
|
|
31
31
|
droppedEvents: number;
|
|
32
32
|
/**
|
|
33
|
-
* Latest
|
|
34
|
-
*
|
|
33
|
+
* Latest `t` the loss can reach, inclusive: a parsed record that was evicted, or the bound of a
|
|
34
|
+
* byte cut. Omitted while a cut has no bound, which means every window is lost. A cut has no
|
|
35
|
+
* bound until a record after it is parsed, and none at all when the clock can have started
|
|
36
|
+
* again across it.
|
|
35
37
|
*/
|
|
36
38
|
lostThroughT?: number;
|
|
37
39
|
/** Human-and-agent readable summary. Present so a consumer never has to compose one. */
|
|
@@ -42,6 +42,13 @@ export interface SessionJournalOptions {
|
|
|
42
42
|
maxReadBytes?: number;
|
|
43
43
|
maxRetainedBytes?: number;
|
|
44
44
|
maxRetainedEvents?: number;
|
|
45
|
+
/**
|
|
46
|
+
* The session's elapsed-ms clock, the same clock that stamps each record's `t`. It bounds a cut
|
|
47
|
+
* that left no readable record: every skipped byte was on disk at the read, so its `t` is at or
|
|
48
|
+
* before the time of the read. Without it, such a cut stays unbounded until a later read parses
|
|
49
|
+
* a record.
|
|
50
|
+
*/
|
|
51
|
+
now?: () => number;
|
|
45
52
|
}
|
|
46
53
|
/**
|
|
47
54
|
* Default byte budget for one serialized writer's event ledger (#986).
|