@reticlehq/server 3.3.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 +12 -14
- 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 +22 -0
- package/dist/command/cli/adhoc-suite.js +90 -19
- package/dist/command/cli/adhoc-verdict.d.ts +35 -0
- package/dist/command/cli/adhoc-verdict.js +90 -3
- 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 +7 -5
- 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.d.ts +8 -1
- package/dist/command/cli/cli-parse.js +21 -3
- package/dist/command/cli/cli-update-commands.js +8 -1
- package/dist/command/cli/cli-verify.d.ts +24 -3
- package/dist/command/cli/cli-verify.js +54 -11
- package/dist/command/cli/cloud-cli.js +5 -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/launch/cli-launch.d.ts +8 -0
- package/dist/command/cli/launch/cli-launch.js +21 -9
- 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 +25 -0
- package/dist/command/cli/ports/resolve/cli-port.js +125 -15
- 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 +62 -10
- package/dist/command/daemon/binding/daemon-status-probe.d.ts +14 -2
- package/dist/command/daemon/binding/daemon-status-probe.js +34 -6
- package/dist/command/daemon/daemon-resolve.d.ts +9 -0
- package/dist/command/daemon/daemon-resolve.js +1 -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-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.js +171 -157
- 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 +30 -13
- package/dist/features/harness/openai-driver.js +4 -7
- package/dist/features/harness/platform-config.js +5 -6
- package/dist/index.d.ts +9 -0
- package/dist/index.js +40 -11
- package/dist/judgement/domain/domain-model.js +3 -1
- package/dist/judgement/oracles/self-instrument.js +4 -2
- 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/verification-sync.d.ts +3 -1
- package/dist/judgement/runs/verification-sync.js +1 -1
- 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-author.d.ts +7 -0
- package/dist/language/flows/flow-author.js +19 -0
- package/dist/language/flows/flow-expect-grammar.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-replay-run.d.ts +2 -3
- package/dist/language/flows/flow-replay-run.js +13 -47
- package/dist/language/flows/flow-replay.d.ts +22 -8
- package/dist/language/flows/flow-replay.js +55 -9
- package/dist/language/flows/flow-result.d.ts +2 -0
- package/dist/language/flows/flow-result.js +25 -0
- package/dist/language/flows/flow-tools.js +17 -5
- package/dist/language/flows/flows.d.ts +10 -1
- package/dist/language/flows/flows.js +90 -14
- package/dist/language/flows/recording/replay-halt.js +3 -1
- package/dist/language/flows/recording/tape/recordings.d.ts +30 -1
- 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 +42 -6
- package/dist/language/flows/verify-change-tools.js +5 -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 -1
- package/dist/memory/cloud/cloud-sync.js +6 -9
- 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 +68 -3
- package/dist/memory/cloud/sync-cycle.js +298 -55
- package/dist/memory/cloud/sync-daemon.js +24 -20
- package/dist/memory/cloud/sync-disk.js +16 -2
- package/dist/memory/intent/intent-shard.d.ts +10 -32
- package/dist/memory/intent/intent-shard.js +0 -10
- package/dist/memory/journal/drive-flow.d.ts +18 -13
- package/dist/memory/journal/drive-flow.js +45 -37
- package/dist/memory/journal/on-disk/workspace-tiers.js +2 -0
- package/dist/memory/journal/session-end.d.ts +9 -1
- package/dist/memory/journal/session-end.js +24 -12
- package/dist/memory/project/dir/reticle-dir.d.ts +2 -0
- package/dist/memory/project/dir/reticle-dir.js +1 -0
- package/dist/memory/project/project-tools.js +3 -1
- package/dist/portal/bridge/auth-failure-reason.d.ts +1 -1
- package/dist/portal/bridge/auth-failure-reason.js +4 -2
- 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 +29 -4
- package/dist/portal/pool/playwright-launcher.d.ts +6 -0
- package/dist/portal/pool/playwright-launcher.js +20 -13
- 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.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/facts/observed-state.d.ts +11 -0
- package/dist/portal/session/facts/observed-state.js +34 -0
- 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/session-health.js +3 -2
- package/dist/portal/session/session-manager.d.ts +20 -6
- package/dist/portal/session/session-manager.js +19 -3
- package/dist/portal/session/session-reaper.d.ts +28 -0
- package/dist/portal/session/session-reaper.js +49 -1
- package/dist/portal/session/session.d.ts +5 -0
- package/dist/portal/session/session.js +15 -19
- 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 +2 -2
- 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/server-instructions.js +9 -7
- 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/durable.d.ts +69 -0
- package/dist/surface/tools/act/durable.js +139 -0
- package/dist/surface/tools/act/navigation/navigate-arrival.d.ts +13 -6
- package/dist/surface/tools/act/navigation/navigate-arrival.js +47 -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-capsule.d.ts +7 -0
- package/dist/surface/tools/act-capsule.js +46 -1
- package/dist/surface/tools/act-sequence-tool.js +25 -8
- package/dist/surface/tools/act-tools.js +26 -50
- package/dist/surface/tools/assert/assert-grade.js +12 -1
- package/dist/surface/tools/assert/assert-verdict.js +2 -2
- package/dist/surface/tools/browser-tools.js +14 -2
- package/dist/surface/tools/coverage-tools.js +27 -1
- package/dist/surface/tools/error-recovery.js +4 -4
- package/dist/surface/tools/feedback-invite.js +6 -4
- package/dist/surface/tools/harness-explore.d.ts +3 -2
- package/dist/surface/tools/harness-explore.js +7 -6
- package/dist/surface/tools/lease-tools.js +15 -6
- package/dist/surface/tools/live-call-text.js +4 -1
- package/dist/surface/tools/observe-tools.js +9 -1
- package/dist/surface/tools/tools.js +4 -0
- 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/harness-drive.d.ts +0 -2
- package/dist/telemetry/harness-drive.js +0 -4
- package/dist/wire-journal.d.ts +3 -0
- package/dist/wire-journal.js +4 -0
- package/dist/workspace-packages.d.ts +0 -2
- package/dist/workspace-packages.js +1 -9
- package/docs/README.md +4 -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 +14 -4
- package/docs/cli/doctor.mdx +4 -0
- package/docs/cli/feedback.mdx +2 -2
- package/docs/cli/gate.mdx +9 -4
- 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 +21 -2
- package/docs/frameworks.mdx +29 -10
- 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 +1 -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 +50 -5
- 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 +1 -1
- 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 +7 -6
- package/docs/use-cases.mdx +157 -0
- package/docs/vs-screenshots.mdx +3 -3
- package/docs/what-is-recorded.md +2 -2
- package/docs/what-is-reticle.mdx +17 -20
- package/docs/why-reticle.mdx +14 -21
- package/package.json +10 -9
- package/dist/judgement/capsule/minimize.d.ts +0 -12
- package/dist/judgement/capsule/minimize.js +0 -22
- package/dist/judgement/phenomena/phenomena.d.ts +0 -27
- 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
|
@@ -4,21 +4,52 @@
|
|
|
4
4
|
* perspective a flow that drifted no longer behaves, so it must not read as green. The actionable
|
|
5
5
|
* "why" is lifted from the replay's decision envelope (or its error) into failureReason.
|
|
6
6
|
*/
|
|
7
|
-
import { ReplayStatus, RunFlowStatus, } from '@reticlehq/core';
|
|
7
|
+
import { ReplayStatus, RUN_RECORDING_MAX_BYTES, RUN_STEP_RESULTS_MAX, RUN_TEXT_MAX, RunFlowStatus, } from '@reticlehq/core';
|
|
8
8
|
import { SUCCESS_STEP_TOOL } from '../../../language/flows/flow-success.js';
|
|
9
9
|
/** OK → PASS; DRIFT/ERROR → FAIL (a healed flow is produced by the heal path, not plain replay). */
|
|
10
10
|
export function runFlowStatusOf(status) {
|
|
11
11
|
return status === ReplayStatus.OK ? RunFlowStatus.PASS : RunFlowStatus.FAIL;
|
|
12
12
|
}
|
|
13
|
+
const clip = (text) => text.length > RUN_TEXT_MAX ? `${text.slice(0, RUN_TEXT_MAX - 1)}…` : text;
|
|
14
|
+
/** One step as synced: what it did, where, and whether it held. */
|
|
15
|
+
function stepResultOf(step) {
|
|
16
|
+
return {
|
|
17
|
+
step: step.step,
|
|
18
|
+
anchor: clip(step.anchor),
|
|
19
|
+
ok: step.ok,
|
|
20
|
+
...(step.page === undefined ? {} : { page: step.page }),
|
|
21
|
+
...(step.endPage === undefined ? {} : { endPage: step.endPage }),
|
|
22
|
+
...(step.consequence === undefined ? {} : { consequence: clip(step.consequence) }),
|
|
23
|
+
...(step.error === undefined ? {} : { error: clip(step.error) }),
|
|
24
|
+
...(step.drift === undefined ? {} : { drift: step.drift.reasonKind }),
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* The flow as recorded, when it is small enough to travel — what lets a teammate replay this run
|
|
29
|
+
* from the cloud. Fill values were already redacted when the flow was saved.
|
|
30
|
+
*/
|
|
31
|
+
function recordingOf(flow) {
|
|
32
|
+
const recording = {
|
|
33
|
+
...(flow.startPath === undefined ? {} : { startPath: flow.startPath }),
|
|
34
|
+
steps: flow.steps,
|
|
35
|
+
};
|
|
36
|
+
return Buffer.byteLength(JSON.stringify(recording)) > RUN_RECORDING_MAX_BYTES
|
|
37
|
+
? { recordingOmitted: true }
|
|
38
|
+
: { recording };
|
|
39
|
+
}
|
|
13
40
|
/** Map one replay (plus its measured duration) into a RunFlowResult for the artifact's flows[]. */
|
|
14
|
-
export function mapReplayToFlowResult(replay, durationMs) {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
41
|
+
export function mapReplayToFlowResult(replay, durationMs, flow) {
|
|
42
|
+
// A replay that could not start returns OK with no steps. Nothing ran, so it is SKIPPED — which
|
|
43
|
+
// every reader of a run (the verdict, the gate) already refuses to count as a pass.
|
|
44
|
+
const status = replay.unverifiable === undefined ? runFlowStatusOf(replay.status) : RunFlowStatus.SKIPPED;
|
|
45
|
+
const failureReason = replay.unverifiable !== undefined
|
|
46
|
+
? replay.unverifiable.reason
|
|
47
|
+
: status === RunFlowStatus.FAIL
|
|
48
|
+
? (replay.decision?.whatChanged ??
|
|
49
|
+
replay.decision?.summary ??
|
|
50
|
+
replay.error?.message ??
|
|
51
|
+
'flow failed')
|
|
52
|
+
: undefined;
|
|
22
53
|
// A flow with a success oracle gets a synthetic 'success' step appended by replay; surface its label
|
|
23
54
|
// as the run's `oracle` so the verdict counts this flow as consequence-backed (→ HIGH confidence),
|
|
24
55
|
// not a bare smoke click. Without this, an oracle-backed pass reads as MEDIUM and undersells itself.
|
|
@@ -30,5 +61,9 @@ export function mapReplayToFlowResult(replay, durationMs) {
|
|
|
30
61
|
durationMs,
|
|
31
62
|
...(oracle !== undefined ? { oracle } : {}),
|
|
32
63
|
...(failureReason !== undefined ? { failureReason } : {}),
|
|
64
|
+
...(0 === replay.steps.length
|
|
65
|
+
? {}
|
|
66
|
+
: { stepResults: replay.steps.slice(0, RUN_STEP_RESULTS_MAX).map(stepResultOf) }),
|
|
67
|
+
...(flow === undefined ? {} : recordingOf(flow)),
|
|
33
68
|
};
|
|
34
69
|
}
|
|
@@ -1,9 +1,11 @@
|
|
|
1
|
-
import { type ProjectId, type FlowReplayResult } from '@reticlehq/core';
|
|
1
|
+
import { type ProjectId, type FlowFile, type FlowReplayResult } from '@reticlehq/core';
|
|
2
2
|
import type { ToolDeps } from '../../surface/tools/tool-kit.js';
|
|
3
3
|
/** A replay plus the wall-clock time it took — the shape the verify handler already collects. */
|
|
4
4
|
export interface TimedReplay {
|
|
5
5
|
replay: FlowReplayResult;
|
|
6
6
|
durationMs: number;
|
|
7
|
+
/** The flow file that was replayed, so the synced run can carry its recording. */
|
|
8
|
+
flow?: FlowFile;
|
|
7
9
|
}
|
|
8
10
|
/**
|
|
9
11
|
* Persist the suite's run artifact and best-effort mirror it to the cloud. Never throws into the verify
|
|
@@ -23,7 +23,7 @@ import { rootForProjectId } from '../../memory/project/session-root.js';
|
|
|
23
23
|
const RETICLE_MCP_AGENT_ID = 'reticle-mcp';
|
|
24
24
|
/** Assemble the ReticleVerificationRun artifact for a verify suite (pure — no IO). */
|
|
25
25
|
function assembleRun(deps, timed, projectId) {
|
|
26
|
-
const flows = timed.map((t) => mapReplayToFlowResult(t.replay, t.durationMs));
|
|
26
|
+
const flows = timed.map((t) => mapReplayToFlowResult(t.replay, t.durationMs, t.flow));
|
|
27
27
|
const input = {
|
|
28
28
|
runId: defaultRunId(),
|
|
29
29
|
durationMs: flows.reduce((sum, f) => sum + f.durationMs, 0),
|
|
@@ -22,3 +22,12 @@ export interface AffectedResult {
|
|
|
22
22
|
* a repo-relative change matches an absolute stamp and vice-versa.
|
|
23
23
|
*/
|
|
24
24
|
export declare function affectedFlows(flows: readonly FlowSources[], changedFiles: readonly string[]): AffectedResult;
|
|
25
|
+
export declare function isInteractiveSource(text: string): boolean;
|
|
26
|
+
/**
|
|
27
|
+
* Changed interactive components that no saved flow's manifest names.
|
|
28
|
+
*
|
|
29
|
+
* `affectedFlows` starts from the flows, so a file no flow ever touched affects nothing — and a gate
|
|
30
|
+
* built only on it passed a brand-new feature that had never been driven. This starts from the
|
|
31
|
+
* change instead. `isInteractive` is injected so the decision stays pure; the CLI reads the file.
|
|
32
|
+
*/
|
|
33
|
+
export declare function unflowedFiles(flows: readonly FlowSources[], changedFiles: readonly string[], isInteractive: (file: string) => boolean): string[];
|
|
@@ -33,3 +33,38 @@ export function affectedFlows(flows, changedFiles) {
|
|
|
33
33
|
}
|
|
34
34
|
return { affected, unknownProvenance };
|
|
35
35
|
}
|
|
36
|
+
/** A file a source stamper can put on an element — the only files a flow's manifest can ever name. */
|
|
37
|
+
const COMPONENT_FILE = /\.(?:jsx|tsx|vue|svelte|astro)$/;
|
|
38
|
+
/** Same test-file rule the stampers use, so a test is never demanded a flow. */
|
|
39
|
+
const TEST_FILE = /(?:\.(?:test|spec)\.[^./]+$)|(?:^|\/)__tests__\//;
|
|
40
|
+
/**
|
|
41
|
+
* Something a step could act on: a handler prop (`onClick=`, `@click`, `v-on:`, `on:submit`) or a
|
|
42
|
+
* native control.
|
|
43
|
+
*
|
|
44
|
+
* ponytail: a text heuristic, not a parse. A flow's manifest only names the file of the element a
|
|
45
|
+
* step ACTED on, so a display-only component can never be attributed and demanding a flow for one
|
|
46
|
+
* would block forever. A handler passed in from a parent under another name escapes it; upgrade to
|
|
47
|
+
* recording the source of the element an `expect` matched if that proves common.
|
|
48
|
+
*/
|
|
49
|
+
const INTERACTIVE = /\bon[A-Z]\w*=|@[a-z]+[=.]|\bv-on:|\bon:[a-z]+|<(?:button|input|select|textarea|form|a|summary)[\s>/]/;
|
|
50
|
+
export function isInteractiveSource(text) {
|
|
51
|
+
return INTERACTIVE.test(text);
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Changed interactive components that no saved flow's manifest names.
|
|
55
|
+
*
|
|
56
|
+
* `affectedFlows` starts from the flows, so a file no flow ever touched affects nothing — and a gate
|
|
57
|
+
* built only on it passed a brand-new feature that had never been driven. This starts from the
|
|
58
|
+
* change instead. `isInteractive` is injected so the decision stays pure; the CLI reads the file.
|
|
59
|
+
*/
|
|
60
|
+
export function unflowedFiles(flows, changedFiles, isInteractive) {
|
|
61
|
+
const sources = flows.flatMap((flow) => (flow.sources ?? []).map(normalize));
|
|
62
|
+
return changedFiles.filter((file) => {
|
|
63
|
+
const c = normalize(file);
|
|
64
|
+
if (!COMPONENT_FILE.test(c) || TEST_FILE.test(c))
|
|
65
|
+
return false;
|
|
66
|
+
if (sources.some((s) => c === s || c.endsWith(`/${s}`) || s.endsWith(`/${c}`)))
|
|
67
|
+
return false;
|
|
68
|
+
return isInteractive(file);
|
|
69
|
+
});
|
|
70
|
+
}
|
|
@@ -22,6 +22,16 @@ interface GateInput {
|
|
|
22
22
|
downgraded?: readonly DowngradedFlow[];
|
|
23
23
|
/** Flows that covered a changed file but no longer exist — coverage deleted rather than satisfied. */
|
|
24
24
|
deleted?: readonly string[];
|
|
25
|
+
/** Changed interactive components no saved flow touches — a change nothing has driven. */
|
|
26
|
+
unflowed?: readonly string[];
|
|
27
|
+
/** Coverage levels below the best they ever reached — something new appeared, nothing covered it. */
|
|
28
|
+
coverageRegressed?: readonly {
|
|
29
|
+
level: string;
|
|
30
|
+
was: number;
|
|
31
|
+
now: number;
|
|
32
|
+
}[];
|
|
33
|
+
/** Changed files the browser loaded and never ran a function of. */
|
|
34
|
+
unexecuted?: readonly string[];
|
|
25
35
|
}
|
|
26
36
|
interface GateResult {
|
|
27
37
|
/** True when every affected, non-flaky flow has a passing artifact AND nothing was weakened/deleted. */
|
|
@@ -34,6 +44,16 @@ interface GateResult {
|
|
|
34
44
|
downgraded: DowngradedFlow[];
|
|
35
45
|
/** Deleted flows that covered changed files — BLOCKING for the same reason. */
|
|
36
46
|
deleted: string[];
|
|
47
|
+
/** Changed interactive components no flow covers — BLOCKING: the change was never driven. */
|
|
48
|
+
unflowed: string[];
|
|
49
|
+
/** BLOCKING: the app grew something nothing has covered. */
|
|
50
|
+
coverageRegressed: {
|
|
51
|
+
level: string;
|
|
52
|
+
was: number;
|
|
53
|
+
now: number;
|
|
54
|
+
}[];
|
|
55
|
+
/** BLOCKING: changed code that never ran in any drive. */
|
|
56
|
+
unexecuted: string[];
|
|
37
57
|
}
|
|
38
58
|
export declare function gateDecision(input: GateInput): GateResult;
|
|
39
59
|
export {};
|
|
@@ -23,11 +23,22 @@ export function gateDecision(input) {
|
|
|
23
23
|
// trivially gameable, which is the exact failure this gate exists to prevent.
|
|
24
24
|
const downgraded = [...(input.downgraded ?? [])];
|
|
25
25
|
const deleted = [...(input.deleted ?? [])];
|
|
26
|
+
const unflowed = [...(input.unflowed ?? [])];
|
|
27
|
+
const coverageRegressed = [...(input.coverageRegressed ?? [])];
|
|
28
|
+
const unexecuted = [...(input.unexecuted ?? [])];
|
|
26
29
|
return {
|
|
27
|
-
pass: 0 === uncovered.length &&
|
|
30
|
+
pass: 0 === uncovered.length &&
|
|
31
|
+
0 === downgraded.length &&
|
|
32
|
+
0 === deleted.length &&
|
|
33
|
+
0 === unflowed.length &&
|
|
34
|
+
0 === coverageRegressed.length &&
|
|
35
|
+
0 === unexecuted.length,
|
|
28
36
|
uncovered,
|
|
29
37
|
quarantined,
|
|
30
38
|
downgraded,
|
|
31
39
|
deleted,
|
|
40
|
+
unflowed,
|
|
41
|
+
coverageRegressed,
|
|
42
|
+
unexecuted,
|
|
32
43
|
};
|
|
33
44
|
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { FlowFile } from '@reticlehq/core';
|
|
2
|
+
/**
|
|
3
|
+
* Who is making a flow right now: the agent that announced itself over MCP, and the person signed in
|
|
4
|
+
* to Reticle on this machine. A key handed over in the environment names no person, so none is
|
|
5
|
+
* written — the cloud can still attribute the push by the key that sent it.
|
|
6
|
+
*/
|
|
7
|
+
export declare function flowAuthor(env?: NodeJS.ProcessEnv, home?: string): FlowFile['author'];
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { homedir } from 'node:os';
|
|
2
|
+
import { mcpClientIdentity } from '../../surface/mcp/peer/client-identity.js';
|
|
3
|
+
import { readAccountState } from '../../memory/cloud/account-state.js';
|
|
4
|
+
/**
|
|
5
|
+
* Who is making a flow right now: the agent that announced itself over MCP, and the person signed in
|
|
6
|
+
* to Reticle on this machine. A key handed over in the environment names no person, so none is
|
|
7
|
+
* written — the cloud can still attribute the push by the key that sent it.
|
|
8
|
+
*/
|
|
9
|
+
export function flowAuthor(env = process.env, home = homedir()) {
|
|
10
|
+
const agent = mcpClientIdentity().name;
|
|
11
|
+
const account = readAccountState(home, env);
|
|
12
|
+
const person = account.signedIn ? account.email : undefined;
|
|
13
|
+
if (agent === undefined && person === undefined)
|
|
14
|
+
return undefined;
|
|
15
|
+
return {
|
|
16
|
+
...(agent === undefined ? {} : { agent }),
|
|
17
|
+
...(person === undefined ? {} : { person }),
|
|
18
|
+
};
|
|
19
|
+
}
|
|
@@ -215,7 +215,7 @@ export function parseFlowFileText(text) {
|
|
|
215
215
|
ok: false,
|
|
216
216
|
code: FlowErrorCode.WRONG_VERSION,
|
|
217
217
|
detail: `this flow file is version ${String(declared)} and this Reticle reads versions ` +
|
|
218
|
-
`${[...READABLE_FLOW_VERSIONS].join('
|
|
218
|
+
`${[...READABLE_FLOW_VERSIONS].join(', ')}. The file is not damaged, the reader is the ` +
|
|
219
219
|
'wrong one. ' +
|
|
220
220
|
'Upgrade or downgrade Reticle rather than editing the flow.',
|
|
221
221
|
};
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { type FlowFile, type FlowStep, type Predicate } from '@reticlehq/core';
|
|
2
|
+
/** A short, stable identity for a journey's shape: its start page and its steps. */
|
|
3
|
+
export declare function journeyFingerprint(flow: Pick<FlowFile, 'startPath' | 'steps'>): string;
|
|
4
|
+
/**
|
|
5
|
+
* Fold two copies of one journey into one file.
|
|
6
|
+
*
|
|
7
|
+
* Gap-filling, never overwriting: whatever the kept flow already says — its name, its intent, a
|
|
8
|
+
* step's declared consequence, a quarantine a person wrote — survives, and the other copy only
|
|
9
|
+
* supplies what is missing. So a merge can add an assertion or an intent and can never remove one.
|
|
10
|
+
*/
|
|
11
|
+
export declare function mergeJourney(kept: FlowFile, other: FlowFile): FlowFile;
|
|
12
|
+
/**
|
|
13
|
+
* The name a new journey saves under.
|
|
14
|
+
*
|
|
15
|
+
* Its readable name when that is free or already this journey's; otherwise the same name with the
|
|
16
|
+
* fingerprint's head, so two different journeys that share an intent never overwrite each other.
|
|
17
|
+
*/
|
|
18
|
+
export declare function journeyName(base: string, fingerprint: string, taken: ReadonlyMap<string, string>): string;
|
|
19
|
+
/**
|
|
20
|
+
* What must hold before a journey and what it leaves behind, read from where it started and ended.
|
|
21
|
+
*
|
|
22
|
+
* Routes only: the page is the one piece of state every recorded step knows for certain, and a
|
|
23
|
+
* claim written from a guess about the rest would be a precondition nobody met. Written in the
|
|
24
|
+
* exact form `canFollow` compares by value, so A-then-B is answerable from the two files alone.
|
|
25
|
+
*/
|
|
26
|
+
export declare function routeClaims(startPath: string | undefined, steps: readonly FlowStep[]): {
|
|
27
|
+
requires?: Predicate[];
|
|
28
|
+
ensures?: Predicate[];
|
|
29
|
+
};
|
|
30
|
+
/**
|
|
31
|
+
* The preconditions only another flow can establish.
|
|
32
|
+
*
|
|
33
|
+
* A route claim on the page the flow starts on is not one of them: replay navigates there itself, so
|
|
34
|
+
* it neither opts the flow out of the reset nor gates the run. That is the claim a saved drive writes
|
|
35
|
+
* for the map (see routeClaims) — reading it as "starts from leftover state" would stop every
|
|
36
|
+
* auto-saved flow from ever being reset.
|
|
37
|
+
*/
|
|
38
|
+
export declare function establishedState(flow: FlowFile): readonly Predicate[];
|
|
39
|
+
/**
|
|
40
|
+
* Is the tab where the flow asked to start? Up to a trailing slash, and up to the query the flow
|
|
41
|
+
* did not ask about.
|
|
42
|
+
*
|
|
43
|
+
* `startPath` is the SPECIFICATION, so it decides what counts. A query it recorded is compared:
|
|
44
|
+
* `?tab=wrap` and `?tab=summary` are different pages, and a replay that starts on the wrong one
|
|
45
|
+
* proves nothing about the right one. A query it did NOT record is ignored: the tab carrying
|
|
46
|
+
* `?next=%2F` on a login page, or the identity params Reticle puts on a leased tab, are not the flow
|
|
47
|
+
* being elsewhere, and navigating to strip them costs a session for nothing.
|
|
48
|
+
*
|
|
49
|
+
* The asymmetry is the whole point and the reason `observed` and `expected` are named rather than
|
|
50
|
+
* `a` and `b`. Comparing with the query on both sides always re-navigated a query-bearing
|
|
51
|
+
* `startPath` (#1059, which killed the session mid-flow); comparing with it on neither side reads a
|
|
52
|
+
* tab on `?tab=summary` as already at `?tab=wrap`.
|
|
53
|
+
*/
|
|
54
|
+
export declare function samePath(observed: string, expected: string): boolean;
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { PredicateKind } from '@reticlehq/core';
|
|
3
|
+
/**
|
|
4
|
+
* One journey, however many times it was driven.
|
|
5
|
+
*
|
|
6
|
+
* Every browser session saves its own flow at teardown, so a journey driven in five sessions was
|
|
7
|
+
* five near-identical files. Two flows are the SAME journey when they start on the same page and
|
|
8
|
+
* take the same steps: same tool, action, anchor and invocation, in the same order. What was typed
|
|
9
|
+
* and what was claimed do not decide it — a re-drive that fills a different value or declares one
|
|
10
|
+
* more consequence is the same journey, better described.
|
|
11
|
+
*/
|
|
12
|
+
/** The anchor minus its source location, which moves when an unrelated line is edited. */
|
|
13
|
+
function anchorShape(anchor) {
|
|
14
|
+
const { source: _source, ...rest } = anchor;
|
|
15
|
+
return rest;
|
|
16
|
+
}
|
|
17
|
+
function stepShape(step) {
|
|
18
|
+
return [
|
|
19
|
+
step.tool,
|
|
20
|
+
step.action ?? null,
|
|
21
|
+
anchorShape(step.anchor),
|
|
22
|
+
step.invoke ?? null,
|
|
23
|
+
(step.steps ?? []).map(stepShape),
|
|
24
|
+
];
|
|
25
|
+
}
|
|
26
|
+
const FINGERPRINT_LENGTH = 12;
|
|
27
|
+
/** A short, stable identity for a journey's shape: its start page and its steps. */
|
|
28
|
+
export function journeyFingerprint(flow) {
|
|
29
|
+
return createHash('sha256')
|
|
30
|
+
.update(JSON.stringify([flow.startPath ?? null, flow.steps.map(stepShape)]))
|
|
31
|
+
.digest('hex')
|
|
32
|
+
.slice(0, FINGERPRINT_LENGTH);
|
|
33
|
+
}
|
|
34
|
+
/** `kept` wins wherever it says something; `other` fills only what `kept` left unsaid. */
|
|
35
|
+
function fillStep(kept, other) {
|
|
36
|
+
if (other === undefined)
|
|
37
|
+
return kept;
|
|
38
|
+
const merged = { ...other, ...kept };
|
|
39
|
+
if (kept.steps !== undefined) {
|
|
40
|
+
merged.steps = kept.steps.map((sub, i) => fillStep(sub, other.steps?.[i]));
|
|
41
|
+
}
|
|
42
|
+
return merged;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Fold two copies of one journey into one file.
|
|
46
|
+
*
|
|
47
|
+
* Gap-filling, never overwriting: whatever the kept flow already says — its name, its intent, a
|
|
48
|
+
* step's declared consequence, a quarantine a person wrote — survives, and the other copy only
|
|
49
|
+
* supplies what is missing. So a merge can add an assertion or an intent and can never remove one.
|
|
50
|
+
*/
|
|
51
|
+
export function mergeJourney(kept, other) {
|
|
52
|
+
return {
|
|
53
|
+
...other,
|
|
54
|
+
...kept,
|
|
55
|
+
steps: kept.steps.map((step, i) => fillStep(step, other.steps[i])),
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* The name a new journey saves under.
|
|
60
|
+
*
|
|
61
|
+
* Its readable name when that is free or already this journey's; otherwise the same name with the
|
|
62
|
+
* fingerprint's head, so two different journeys that share an intent never overwrite each other.
|
|
63
|
+
*/
|
|
64
|
+
export function journeyName(base, fingerprint, taken) {
|
|
65
|
+
const owner = taken.get(base);
|
|
66
|
+
return owner === undefined || owner === fingerprint ? base : `${base}-${fingerprint.slice(0, 6)}`;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* What must hold before a journey and what it leaves behind, read from where it started and ended.
|
|
70
|
+
*
|
|
71
|
+
* Routes only: the page is the one piece of state every recorded step knows for certain, and a
|
|
72
|
+
* claim written from a guess about the rest would be a precondition nobody met. Written in the
|
|
73
|
+
* exact form `canFollow` compares by value, so A-then-B is answerable from the two files alone.
|
|
74
|
+
*/
|
|
75
|
+
export function routeClaims(startPath, steps) {
|
|
76
|
+
const first = startPath ?? steps[0]?.page;
|
|
77
|
+
const last = steps.at(-1);
|
|
78
|
+
const end = last?.endPage ?? last?.page;
|
|
79
|
+
return {
|
|
80
|
+
...(first === undefined ? {} : { requires: [{ kind: PredicateKind.ROUTE, pathname: first }] }),
|
|
81
|
+
...(end === undefined ? {} : { ensures: [{ kind: PredicateKind.ROUTE, pathname: end }] }),
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* The preconditions only another flow can establish.
|
|
86
|
+
*
|
|
87
|
+
* A route claim on the page the flow starts on is not one of them: replay navigates there itself, so
|
|
88
|
+
* it neither opts the flow out of the reset nor gates the run. That is the claim a saved drive writes
|
|
89
|
+
* for the map (see routeClaims) — reading it as "starts from leftover state" would stop every
|
|
90
|
+
* auto-saved flow from ever being reset.
|
|
91
|
+
*/
|
|
92
|
+
export function establishedState(flow) {
|
|
93
|
+
const start = flow.startPath;
|
|
94
|
+
return (flow.requires ?? []).filter((claim) => !(start !== undefined &&
|
|
95
|
+
PredicateKind.ROUTE === claim.kind &&
|
|
96
|
+
claim.pathname !== undefined &&
|
|
97
|
+
samePath(claim.pathname, start)));
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Is the tab where the flow asked to start? Up to a trailing slash, and up to the query the flow
|
|
101
|
+
* did not ask about.
|
|
102
|
+
*
|
|
103
|
+
* `startPath` is the SPECIFICATION, so it decides what counts. A query it recorded is compared:
|
|
104
|
+
* `?tab=wrap` and `?tab=summary` are different pages, and a replay that starts on the wrong one
|
|
105
|
+
* proves nothing about the right one. A query it did NOT record is ignored: the tab carrying
|
|
106
|
+
* `?next=%2F` on a login page, or the identity params Reticle puts on a leased tab, are not the flow
|
|
107
|
+
* being elsewhere, and navigating to strip them costs a session for nothing.
|
|
108
|
+
*
|
|
109
|
+
* The asymmetry is the whole point and the reason `observed` and `expected` are named rather than
|
|
110
|
+
* `a` and `b`. Comparing with the query on both sides always re-navigated a query-bearing
|
|
111
|
+
* `startPath` (#1059, which killed the session mid-flow); comparing with it on neither side reads a
|
|
112
|
+
* tab on `?tab=summary` as already at `?tab=wrap`.
|
|
113
|
+
*/
|
|
114
|
+
export function samePath(observed, expected) {
|
|
115
|
+
const trimmed = (path) => path.replace(/\/$/, '');
|
|
116
|
+
const withoutQuery = (path) => path.replace(/\?[^#]*/, '');
|
|
117
|
+
const comparable = expected.includes('?') ? observed : withoutQuery(observed);
|
|
118
|
+
return trimmed(comparable) === trimmed(expected);
|
|
119
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { type CommandResult, type FlowFile, type FlowReplayResult, type Contradiction, type FlowStepResult, type ReticleEvent } from '@reticlehq/core';
|
|
2
2
|
import type { ArrivalClock } from '../../surface/tools/act/navigation/navigate-arrival.js';
|
|
3
3
|
import type { SessionManager } from '../../portal/session/session-manager.js';
|
|
4
4
|
import type { Session } from '../../portal/session/session.js';
|
|
@@ -8,8 +8,7 @@ export declare function latestRecordedFlow(events: ReticleEvent[]): {
|
|
|
8
8
|
name: string;
|
|
9
9
|
flow: import('@reticlehq/core').FlowFile;
|
|
10
10
|
} | undefined;
|
|
11
|
-
|
|
12
|
-
export declare function flowErrorMessage(code: FlowErrorCode, detail?: string): string;
|
|
11
|
+
export { flowErrorMessage } from './flow-result.js';
|
|
13
12
|
/** The slice of a session the start-path logic reads: the live URL plus the event buffer. */
|
|
14
13
|
interface StartPathSession {
|
|
15
14
|
url?: string;
|
|
@@ -2,6 +2,7 @@ import { resolveFlowUploads } from './fields/flow-upload-resolve.js';
|
|
|
2
2
|
import { learnFromRun } from '@reticlehq/engine/evidence/learned-guards.js';
|
|
3
3
|
import { EventType, FLOW_SIGNAL_TIMEOUT_MS, FlowErrorCode, RecordedFlowSchema, ReplayStatus, ReticleCommand, RunKind, RunStatus, } from '@reticlehq/core';
|
|
4
4
|
import { haltedFrom } from './recording/replay-halt.js';
|
|
5
|
+
import { establishedState, samePath } from './flow-journey.js';
|
|
5
6
|
import { asRecord, asString } from '@reticlehq/core';
|
|
6
7
|
import { routeOfEvent, routeOfUrl } from '@reticlehq/engine/question/predicate/predicate-route.js';
|
|
7
8
|
import { carryReticleIdentity } from '../../surface/tools/lease-tools.js';
|
|
@@ -11,7 +12,7 @@ import { queryRefs } from './replay.js';
|
|
|
11
12
|
import { assertSuccess, dynamicTestids, successLabel, SUCCESS_STEP_TOOL } from './flow-success.js';
|
|
12
13
|
import { buildDecision, unverifiableReason } from './decision.js';
|
|
13
14
|
import { unsuppliedSecrets } from './fields/flow-secret-field.js';
|
|
14
|
-
import { assertStepExpect, DocumentLostDuringReplay, } from './flow-replay.js';
|
|
15
|
+
import { assertStepExpect, DocumentLostDuringReplay, resumeArg, unresolvedResume, } from './flow-replay.js';
|
|
15
16
|
import { isDocumentGoneError } from '../../portal/session/facts/session-replaced.js';
|
|
16
17
|
import { classifyFlowAssertions, flattenSteps } from './flow-classify.js';
|
|
17
18
|
import { dischargeFlowIntent, flowIntentStatement, flowReplayVerdictId } from './flow-intent.js';
|
|
@@ -31,7 +32,6 @@ import { consultSubjectFor, selectConsulted } from './flow-memory-consult.js';
|
|
|
31
32
|
import { log } from '../../log.js';
|
|
32
33
|
import { flowsForSession } from './flow-store-for-session.js';
|
|
33
34
|
import { projectForRoot } from '../../memory/project/project-for-root.js';
|
|
34
|
-
import { FlowParseNote } from './flow-expect-grammar.js';
|
|
35
35
|
export function latestRecordedFlow(events) {
|
|
36
36
|
for (let i = events.length - 1; i >= 0; i--) {
|
|
37
37
|
const event = events[i];
|
|
@@ -43,28 +43,8 @@ export function latestRecordedFlow(events) {
|
|
|
43
43
|
}
|
|
44
44
|
return undefined;
|
|
45
45
|
}
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
if (FlowErrorCode.PARSE_FAILED === code && undefined !== detail)
|
|
49
|
-
return detail;
|
|
50
|
-
// The detail names both versions and the remedy, so it beats anything generic this could say.
|
|
51
|
-
if (FlowErrorCode.WRONG_VERSION === code && undefined !== detail)
|
|
52
|
-
return detail;
|
|
53
|
-
switch (code) {
|
|
54
|
-
case FlowErrorCode.INVALID_NAME:
|
|
55
|
-
return 'invalid flow name — use a single safe segment (letters/digits/-/_), no path separators';
|
|
56
|
-
case FlowErrorCode.NOT_FOUND:
|
|
57
|
-
return 'no such flow on disk — run reticle_flow{action:"list"} to see saved flows';
|
|
58
|
-
case FlowErrorCode.PARSE_FAILED:
|
|
59
|
-
return FlowParseNote.MALFORMED;
|
|
60
|
-
case FlowErrorCode.NO_RECORDING:
|
|
61
|
-
return 'no compiled recording by that name — record one (reticle_record{action:"start"|"stop"}) first';
|
|
62
|
-
// Never "regenerate it": the file is intact and the reader is the wrong one. Telling somebody
|
|
63
|
-
// to rewrite an undamaged flow is the failure this code was split out of PARSE_FAILED to stop.
|
|
64
|
-
case FlowErrorCode.WRONG_VERSION:
|
|
65
|
-
return 'this flow file was written in a different flow-file format — the file is not damaged, this Reticle cannot read that version. Upgrade or downgrade Reticle rather than editing the flow';
|
|
66
|
-
}
|
|
67
|
-
}
|
|
46
|
+
export { flowErrorMessage } from './flow-result.js';
|
|
47
|
+
import { flowErrorMessage } from './flow-result.js';
|
|
68
48
|
/** Map the wire ReplayStatus onto the persisted RunStatus (ok→pass). */
|
|
69
49
|
function replayToRunStatus(status) {
|
|
70
50
|
switch (status) {
|
|
@@ -133,27 +113,6 @@ function currentPathOf(session) {
|
|
|
133
113
|
const fromUrl = routeOfUrl(session.url);
|
|
134
114
|
return fromUrl === undefined ? undefined : `${fromUrl.docPath}${fromUrl.search}${fromUrl.hash}`;
|
|
135
115
|
}
|
|
136
|
-
/**
|
|
137
|
-
* Is the tab where the flow asked to start? Up to a trailing slash, and up to the query the flow
|
|
138
|
-
* did not ask about.
|
|
139
|
-
*
|
|
140
|
-
* `startPath` is the SPECIFICATION, so it decides what counts. A query it recorded is compared:
|
|
141
|
-
* `?tab=wrap` and `?tab=summary` are different pages, and a replay that starts on the wrong one
|
|
142
|
-
* proves nothing about the right one. A query it did NOT record is ignored: the tab carrying
|
|
143
|
-
* `?next=%2F` on a login page, or the identity params Reticle puts on a leased tab, are not the flow
|
|
144
|
-
* being elsewhere, and navigating to strip them costs a session for nothing.
|
|
145
|
-
*
|
|
146
|
-
* The asymmetry is the whole point and the reason `observed` and `expected` are named rather than
|
|
147
|
-
* `a` and `b`. Comparing with the query on both sides always re-navigated a query-bearing
|
|
148
|
-
* `startPath` (#1059, which killed the session mid-flow); comparing with it on neither side reads a
|
|
149
|
-
* tab on `?tab=summary` as already at `?tab=wrap`.
|
|
150
|
-
*/
|
|
151
|
-
function samePath(observed, expected) {
|
|
152
|
-
const trimmed = (path) => path.replace(/\/$/, '');
|
|
153
|
-
const withoutQuery = (path) => path.replace(/\?[^#]*/, '');
|
|
154
|
-
const comparable = expected.includes('?') ? observed : withoutQuery(observed);
|
|
155
|
-
return trimmed(comparable) === trimmed(expected);
|
|
156
|
-
}
|
|
157
116
|
/**
|
|
158
117
|
* Ask the project what it already knows about this flow.
|
|
159
118
|
*
|
|
@@ -321,7 +280,7 @@ export async function arriveAtStartPath(sessions, session, flow, timeoutMs = STA
|
|
|
321
280
|
if (current === undefined)
|
|
322
281
|
return {};
|
|
323
282
|
// The declared opt-out, both directions. See above.
|
|
324
|
-
if (0 < (flow.
|
|
283
|
+
if (0 < establishedState(flow).length)
|
|
325
284
|
return {};
|
|
326
285
|
const here = samePath(current, target);
|
|
327
286
|
// A route mismatch that step 1 can start from anyway is not worth a page load: navigating away
|
|
@@ -457,7 +416,7 @@ async function loadInvokedFlows(deps, flow, projectId) {
|
|
|
457
416
|
* a consequence of the first being absent.
|
|
458
417
|
*/
|
|
459
418
|
async function firstUnmetPrecondition(session, flow, since) {
|
|
460
|
-
for (const claim of flow
|
|
419
|
+
for (const claim of establishedState(flow)) {
|
|
461
420
|
// Zero budget: a precondition is a claim about the state you are starting FROM. Waiting for one
|
|
462
421
|
// turns "was it true" into "did it become true", which is a different and much weaker question.
|
|
463
422
|
let drift;
|
|
@@ -608,6 +567,12 @@ observe = (s) => s) {
|
|
|
608
567
|
},
|
|
609
568
|
};
|
|
610
569
|
}
|
|
570
|
+
const from = resumeArg(args['from']);
|
|
571
|
+
const badResume = unresolvedResume(from, replayable.steps);
|
|
572
|
+
if (badResume !== undefined) {
|
|
573
|
+
const error = { code: FlowErrorCode.STEP_NOT_FOUND, message: badResume };
|
|
574
|
+
return { name: replayable.name, status: ReplayStatus.ERROR, steps: [], error };
|
|
575
|
+
}
|
|
611
576
|
const unmet = await firstUnmetPrecondition(session, replayable, replayFloor);
|
|
612
577
|
if (unmet !== undefined) {
|
|
613
578
|
return {
|
|
@@ -629,6 +594,7 @@ observe = (s) => s) {
|
|
|
629
594
|
// Bug-sweep mode: keep going past a step whose action ran and whose consequence merely did
|
|
630
595
|
// not hold, so one flow reports one verdict per step instead of stopping at the first defect.
|
|
631
596
|
sweep: true === args['sweep'],
|
|
597
|
+
...(from === undefined ? {} : { from }),
|
|
632
598
|
});
|
|
633
599
|
}
|
|
634
600
|
catch (error) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export {
|
|
1
|
+
export { nearestIsAmbiguous, nearestTestid, resolveQuery } from './flow-anchor.js';
|
|
2
2
|
import type { FlowReplaySession, WaitForSignal, Sleep } from './flow-replay-types.js';
|
|
3
3
|
export type { FlowReplaySession, WaitForSignal, Sleep } from './flow-replay-types.js';
|
|
4
4
|
import { type Drift, type FlowFile, type FlowStep, type FlowStepResult } from '@reticlehq/core';
|
|
@@ -50,15 +50,18 @@ export declare function assertStepExpect(session: FlowReplaySession, expect: Non
|
|
|
50
50
|
* results. This is the "whose fault is it" contract, not a blind "command failed".
|
|
51
51
|
*/
|
|
52
52
|
/**
|
|
53
|
-
* How a replay is loaded and how far it goes
|
|
54
|
-
*
|
|
55
|
-
* It carried a `from` (and a `surface` that existed only to gate it): replay from step N, re-driving
|
|
56
|
-
* the prefix silently. The machinery was built, tested and reachable from nothing at all — no tool
|
|
57
|
-
* argument, no CLI flag, no caller anywhere. Deleted rather than wired: unreachable code is read as
|
|
58
|
-
* current by the next person and maintained forever, its passing tests make it look load-bearing,
|
|
59
|
-
* and nobody has asked for resume-from-step. The history keeps it if it is ever wanted.
|
|
53
|
+
* How a replay is loaded, where it starts reporting, and how far it goes.
|
|
60
54
|
*/
|
|
61
55
|
export interface ReplayFromOptions {
|
|
56
|
+
/**
|
|
57
|
+
* Resume at this step: an index, or a step's `id` (which survives edits that shift indices).
|
|
58
|
+
*
|
|
59
|
+
* There is no state to restore, so the steps before it are re-driven — quickly, as setup: their
|
|
60
|
+
* actions run, their declared consequences are not checked and their results are not reported.
|
|
61
|
+
* The one exception is a setup step that FAILS: the resume never reached the step it was asked
|
|
62
|
+
* for, and swallowing that would report the run as starting where it did not.
|
|
63
|
+
*/
|
|
64
|
+
from?: number | string;
|
|
62
65
|
/**
|
|
63
66
|
* How to load a flow this one INVOKES. Absent means invocations cannot be followed.
|
|
64
67
|
*
|
|
@@ -89,4 +92,15 @@ export interface ReplayFromOptions {
|
|
|
89
92
|
*/
|
|
90
93
|
sweep?: boolean;
|
|
91
94
|
}
|
|
95
|
+
/**
|
|
96
|
+
* The index a replay resumes at, or `undefined` when `from` names no step of this flow.
|
|
97
|
+
*
|
|
98
|
+
* A step declared `commits` in the prefix refuses the resume (0 = replay everything): re-driving it
|
|
99
|
+
* is not setup, it is a second charge, a second email, a second record.
|
|
100
|
+
*/
|
|
101
|
+
export declare function resumeIndex(from: number | string | undefined, steps: readonly FlowStep[]): number | undefined;
|
|
102
|
+
/** The `from` argument, narrowed: a step index or a step id. */
|
|
103
|
+
export declare function resumeArg(raw: unknown): number | string | undefined;
|
|
104
|
+
/** Why `from` cannot be resumed at, or undefined when it can (or was not given). */
|
|
105
|
+
export declare function unresolvedResume(from: number | string | undefined, steps: readonly FlowStep[]): string | undefined;
|
|
92
106
|
export declare function replayFlow(session: FlowReplaySession, flow: FlowFile, waitForSignal: WaitForSignal, signalTimeoutMs: number, confirmDangerous?: boolean, sleep?: Sleep, options?: ReplayFromOptions): Promise<FlowStepResult[]>;
|