@reticlehq/server 3.3.0 → 3.5.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 +6 -2
- package/dist/command/cli/auth/cloud-session.js +2 -0
- package/dist/command/cli/auth/device-flow.d.ts +2 -2
- 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 +24 -4
- 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 +64 -2
- package/dist/command/cli/cloud-kit.js +3 -5
- package/dist/command/cli/cloud-login.d.ts +1 -1
- package/dist/command/cli/cloud-login.js +27 -8
- 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 +21 -13
- package/dist/command/cli/tutorial.d.ts +2 -2
- package/dist/command/cli/tutorial.js +21 -7
- package/dist/command/cli.js +79 -11
- 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/dev-server-wait.d.ts +2 -0
- package/dist/command/setup/bringup/dev-server-wait.js +1 -1
- 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 +91 -9
- package/dist/command/setup/node-effects.d.ts +27 -14
- package/dist/command/setup/node-effects.js +86 -45
- 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 +28 -1
- package/dist/command/setup/run-setup.js +103 -15
- 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.d.ts +2 -0
- package/dist/command/setup/setup-mcp.js +17 -8
- package/dist/command/setup/terminal/wait-progress.d.ts +8 -0
- package/dist/command/setup/terminal/wait-progress.js +17 -0
- package/dist/command/version/peer-announce.d.ts +6 -2
- package/dist/command/version/peer-announce.js +8 -6
- package/dist/command/version/version-nudge.d.ts +17 -3
- package/dist/command/version/version-nudge.js +25 -4
- 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 +10 -0
- package/dist/features/harness/fill-values.js +50 -11
- package/dist/features/harness/jev-driver.js +30 -13
- package/dist/features/harness/openai-driver.d.ts +12 -0
- package/dist/features/harness/openai-driver.js +21 -12
- package/dist/features/harness/platform-config.js +5 -6
- package/dist/features/license/license.d.ts +2 -2
- package/dist/index.d.ts +9 -0
- package/dist/index.js +56 -19
- package/dist/judgement/capsule/capsule-store.d.ts +4 -4
- 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 +77 -3
- package/dist/memory/cloud/sync-cycle.js +325 -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/intent/intent-tools.js +8 -3
- package/dist/memory/journal/drive-flow.d.ts +18 -13
- package/dist/memory/journal/drive-flow.js +45 -37
- package/dist/memory/journal/envelope.d.ts +8 -8
- package/dist/memory/journal/journal-query.d.ts +14 -0
- package/dist/memory/journal/journal-query.js +15 -0
- 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/artifact-root-resolver.d.ts +10 -0
- package/dist/memory/project/artifact-root-resolver.js +46 -19
- 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 +12 -3
- package/dist/portal/bridge/bridge.js +49 -3
- 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/input/viewport-tools.js +16 -6
- 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 +10 -8
- package/dist/portal/session/session.js +24 -32
- package/dist/surface/http-server.d.ts +3 -2
- package/dist/surface/http-server.js +8 -5
- 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 +5 -3
- package/dist/surface/mcp/proxy/proxy-daemon-probe.d.ts +13 -1
- package/dist/surface/mcp/proxy/proxy-daemon-probe.js +27 -2
- package/dist/surface/mcp/proxy/proxy-log.js +3 -5
- 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 +28 -52
- 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/dynamic-tools.js +29 -2
- package/dist/surface/tools/error-recovery.js +5 -6
- 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 +12 -7
- package/dist/surface/tools/invoke-tool.js +12 -8
- 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/tool-kit.d.ts +10 -0
- package/dist/surface/tools/tools.js +4 -0
- package/dist/telemetry/argument-shape.d.ts +0 -2
- package/dist/telemetry/argument-shape.js +1 -5
- 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/telemetry/onboarding-firsts.d.ts +2 -2
- package/dist/telemetry/onboarding-firsts.js +2 -2
- 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 -4
- 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 +20 -8
- 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 +22 -7
- 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 +15 -5
- 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 +2 -2
- 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/next.mdx +3 -0
- package/docs/packages/react.mdx +2 -2
- package/docs/packages/tauri.mdx +1 -1
- package/docs/packages/vite-plugin.mdx +4 -2
- package/docs/packages.mdx +9 -9
- package/docs/predicates.mdx +50 -5
- package/docs/quickstart.mdx +12 -10
- 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/act.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 -7
- 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
|
@@ -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[]>;
|
|
@@ -2,10 +2,10 @@ import { formatStepAddress } from 'open-verification';
|
|
|
2
2
|
import { assertsState, expectLabel, expectedElementTestid, withoutClauses } from '@reticlehq/core';
|
|
3
3
|
import { span } from '../../trace.js';
|
|
4
4
|
import { anchorLabel, expectElementDrift, resolveTestid, testidDrift, ambiguousAnchorDrift, } from './flow-anchor.js';
|
|
5
|
-
export {
|
|
6
|
-
import { routeOfEvent } from '@reticlehq/engine/question/predicate/predicate-route.js';
|
|
5
|
+
export { nearestIsAmbiguous, nearestTestid, resolveQuery } from './flow-anchor.js';
|
|
6
|
+
import { routeOfEvent, routeOfUrl } from '@reticlehq/engine/question/predicate/predicate-route.js';
|
|
7
7
|
import { stepEffect } from '@reticlehq/engine/evidence/step-effect.js';
|
|
8
|
-
import { AnchorKind, DriftReason, EventType, FlowStepTool, PredicateKind, } from '@reticlehq/core';
|
|
8
|
+
import { AnchorKind, DriftReason, EventType, FlowStepTool, StepEffect, PredicateKind, } from '@reticlehq/core';
|
|
9
9
|
import { asString, isConsequenceDrift } from '@reticlehq/core';
|
|
10
10
|
import { anchorFieldName } from './fields/flow-secret-field.js';
|
|
11
11
|
import { degradedStepResult, isDegradedAnchor, runComponentStep, runRoleStep, runSequenceStep, } from './flow-step-runners.js';
|
|
@@ -48,8 +48,11 @@ const realSleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
|
48
48
|
function currentRoute(session) {
|
|
49
49
|
const routes = session.eventsSince(0).filter((e) => e.type === EventType.ROUTE_CHANGE);
|
|
50
50
|
const last = routes.at(-1);
|
|
51
|
-
|
|
52
|
-
|
|
51
|
+
// A page that loaded and never navigated has no route event; its URL is still where it is.
|
|
52
|
+
if (last === undefined) {
|
|
53
|
+
const loaded = session.url === undefined ? undefined : routeOfUrl(session.url);
|
|
54
|
+
return loaded !== undefined && loaded.routePath.length > 0 ? loaded.routePath : undefined;
|
|
55
|
+
}
|
|
53
56
|
// The ROUTER's path. This field answers "which page did this step run on", and the document
|
|
54
57
|
// pathname is `/` on every page of a hash-routed app — so a whole desktop replay reported `/` for
|
|
55
58
|
// every step. Sixth place the same reading was wrong; see routeOfEvent.
|
|
@@ -280,6 +283,30 @@ async function runSignalStep(session, step, index, name, waitForSignal, signalTi
|
|
|
280
283
|
},
|
|
281
284
|
};
|
|
282
285
|
}
|
|
286
|
+
/**
|
|
287
|
+
* The index a replay resumes at, or `undefined` when `from` names no step of this flow.
|
|
288
|
+
*
|
|
289
|
+
* A step declared `commits` in the prefix refuses the resume (0 = replay everything): re-driving it
|
|
290
|
+
* is not setup, it is a second charge, a second email, a second record.
|
|
291
|
+
*/
|
|
292
|
+
export function resumeIndex(from, steps) {
|
|
293
|
+
if (from === undefined)
|
|
294
|
+
return 0;
|
|
295
|
+
const at = 'number' === typeof from ? from : steps.findIndex((step) => step.id === from);
|
|
296
|
+
if (at < 0 || at >= steps.length || !Number.isInteger(at))
|
|
297
|
+
return undefined;
|
|
298
|
+
return steps.slice(0, at).some((step) => StepEffect.COMMITS === step.effect) ? 0 : at;
|
|
299
|
+
}
|
|
300
|
+
/** The `from` argument, narrowed: a step index or a step id. */
|
|
301
|
+
export function resumeArg(raw) {
|
|
302
|
+
return 'number' === typeof raw || ('string' === typeof raw && raw.length > 0) ? raw : undefined;
|
|
303
|
+
}
|
|
304
|
+
/** Why `from` cannot be resumed at, or undefined when it can (or was not given). */
|
|
305
|
+
export function unresolvedResume(from, steps) {
|
|
306
|
+
if (resumeIndex(from, steps) !== undefined)
|
|
307
|
+
return undefined;
|
|
308
|
+
return `cannot resume at ${JSON.stringify(from)}: this flow has ${String(steps.length)} step(s) and no step by that id`;
|
|
309
|
+
}
|
|
283
310
|
/**
|
|
284
311
|
* Run an `invoke` step: load the named flow and replay it, or fail saying why.
|
|
285
312
|
*
|
|
@@ -291,7 +318,7 @@ async function runSignalStep(session, step, index, name, waitForSignal, signalTi
|
|
|
291
318
|
* own, and the sub-journey's outcome is summarised on the invocation, so a reader sees the shape
|
|
292
319
|
* they recorded rather than a flattened list that has lost every boundary.
|
|
293
320
|
*/
|
|
294
|
-
async function runInvokeStep(session, flow, step, index, waitForSignal, signalTimeoutMs, confirmDangerous, sleep, options) {
|
|
321
|
+
async function runInvokeStep(session, flow, step, index, waitForSignal, signalTimeoutMs, confirmDangerous, sleep, options, setup = false) {
|
|
295
322
|
const name = step.invoke ?? '';
|
|
296
323
|
const via = options.via ?? [];
|
|
297
324
|
const here = { flow: flow.name, step: index, via };
|
|
@@ -313,7 +340,10 @@ async function runInvokeStep(session, flow, step, index, waitForSignal, signalTi
|
|
|
313
340
|
error: `cannot replay "${name}": it was not found, so this journey would report green having never run it`,
|
|
314
341
|
};
|
|
315
342
|
}
|
|
316
|
-
|
|
343
|
+
// `from` is an offset into the CALLER's steps. A sub-journey invoked from the setup prefix runs
|
|
344
|
+
// whole and unchecked (from = its length); one invoked at or after the resume point runs normally.
|
|
345
|
+
const { from: _outer, ...rest } = options;
|
|
346
|
+
const carried = setup ? { ...rest, from: sub.steps.length } : rest;
|
|
317
347
|
const nested = await replayFlow(session, sub, waitForSignal, signalTimeoutMs, confirmDangerous, sleep, {
|
|
318
348
|
...carried,
|
|
319
349
|
via: [{ flow: flow.name, step: index }, ...via],
|
|
@@ -331,6 +361,9 @@ async function runInvokeStep(session, flow, step, index, waitForSignal, signalTi
|
|
|
331
361
|
}
|
|
332
362
|
export async function replayFlow(session, flow, waitForSignal, signalTimeoutMs, confirmDangerous = false, sleep = realSleep, options = {}) {
|
|
333
363
|
const results = [];
|
|
364
|
+
// Steps before this index are setup. `from` was validated by the caller; an unresolvable one
|
|
365
|
+
// resumes nowhere rather than everywhere, so it cannot silently widen into a full replay.
|
|
366
|
+
const from = resumeIndex(options.from, flow.steps) ?? flow.steps.length;
|
|
334
367
|
// testids whose region is LLM-dynamic — their expect-presence is NOT asserted.
|
|
335
368
|
const dynamic = new Set((flow.dynamic ?? [])
|
|
336
369
|
.filter((a) => a.kind === AnchorKind.TESTID)
|
|
@@ -347,8 +380,10 @@ export async function replayFlow(session, flow, waitForSignal, signalTimeoutMs,
|
|
|
347
380
|
try {
|
|
348
381
|
for (const step of flow.steps) {
|
|
349
382
|
if (step.invoke !== undefined) {
|
|
350
|
-
results.push(await runInvokeStep(session, flow, step, index, waitForSignal, signalTimeoutMs, confirmDangerous, sleep, options));
|
|
383
|
+
results.push(await runInvokeStep(session, flow, step, index, waitForSignal, signalTimeoutMs, confirmDangerous, sleep, options, index < from));
|
|
351
384
|
const last = results[results.length - 1];
|
|
385
|
+
if (last !== undefined && index < from && last.ok)
|
|
386
|
+
results.pop();
|
|
352
387
|
if (last !== undefined && false === last.ok)
|
|
353
388
|
break;
|
|
354
389
|
index += 1;
|
|
@@ -404,7 +439,9 @@ export async function replayFlow(session, flow, waitForSignal, signalTimeoutMs,
|
|
|
404
439
|
* replay path and belongs in its own commit.
|
|
405
440
|
*/
|
|
406
441
|
const declared = [step.expect, ...(step.steps ?? []).map((sub) => sub.expect)].filter((expectation) => expectation !== undefined);
|
|
407
|
-
|
|
442
|
+
// A setup step's consequence is not checked: it is re-driven to get somewhere, not to prove it.
|
|
443
|
+
const checked = index < from ? [] : declared;
|
|
444
|
+
for (const expectation of checked) {
|
|
408
445
|
if (!result.ok || result.drift !== undefined)
|
|
409
446
|
break;
|
|
410
447
|
const expectDrift = await assertStepExpect(session, expectation, waitForSignal, waitFor(step), cursorBefore);
|
|
@@ -415,6 +452,9 @@ export async function replayFlow(session, flow, waitForSignal, signalTimeoutMs,
|
|
|
415
452
|
}
|
|
416
453
|
if (page !== undefined)
|
|
417
454
|
result.page = page;
|
|
455
|
+
const endPage = currentRoute(session);
|
|
456
|
+
if (endPage !== undefined && endPage !== page)
|
|
457
|
+
result.endPage = endPage;
|
|
418
458
|
const windowEvents = session.eventsSince(cursorBefore).filter((e) => e.t >= cursorBefore);
|
|
419
459
|
const consequence = summarizeConsequence(windowEvents);
|
|
420
460
|
if (consequence !== undefined)
|
|
@@ -437,6 +477,12 @@ export async function replayFlow(session, flow, waitForSignal, signalTimeoutMs,
|
|
|
437
477
|
// forget the rule and quietly re-introduce the cost. Spelled out only when it is NOT the default.
|
|
438
478
|
if (FlowStepTool.ACT === result.tool)
|
|
439
479
|
delete result.tool;
|
|
480
|
+
if (index < from && result.ok && result.drift === undefined) {
|
|
481
|
+
index += 1;
|
|
482
|
+
continue;
|
|
483
|
+
}
|
|
484
|
+
if (index < from)
|
|
485
|
+
result.note = `setup step for resuming at step ${String(from)} failed`;
|
|
440
486
|
results.push(result);
|
|
441
487
|
// Under `sweep`, a failure whose action still RAN does not stop the run — the page is where the
|
|
442
488
|
// step left it, so the next step is as meaningful as it was going to be. Anything else halts.
|
|
@@ -33,3 +33,5 @@ export type FlowResult<T> = {
|
|
|
33
33
|
* hold the distinction — this is the fourth id joined into a path, and the one that file missed.
|
|
34
34
|
*/
|
|
35
35
|
export declare const safeProjectId: (projectId?: ProjectId) => ProjectId | undefined;
|
|
36
|
+
/** Map a structured FlowErrorCode to a legible one-line message for the agent. */
|
|
37
|
+
export declare function flowErrorMessage(code: FlowErrorCode, detail?: string): string;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { FlowErrorCode } from '@reticlehq/core';
|
|
2
2
|
import { SAFE_SEGMENT_PATTERN } from '@reticlehq/core';
|
|
3
|
+
import { FlowParseNote } from './flow-expect-grammar.js';
|
|
3
4
|
/**
|
|
4
5
|
* A project id that is safe to put in a path, or nothing.
|
|
5
6
|
*
|
|
@@ -24,3 +25,27 @@ import { SAFE_SEGMENT_PATTERN } from '@reticlehq/core';
|
|
|
24
25
|
* hold the distinction — this is the fourth id joined into a path, and the one that file missed.
|
|
25
26
|
*/
|
|
26
27
|
export const safeProjectId = (projectId) => projectId !== undefined && SAFE_SEGMENT_PATTERN.test(projectId) ? projectId : undefined;
|
|
28
|
+
/** Map a structured FlowErrorCode to a legible one-line message for the agent. */
|
|
29
|
+
export function flowErrorMessage(code, detail) {
|
|
30
|
+
if (FlowErrorCode.PARSE_FAILED === code && undefined !== detail)
|
|
31
|
+
return detail;
|
|
32
|
+
// The detail names both versions and the remedy, so it beats anything generic this could say.
|
|
33
|
+
if (FlowErrorCode.WRONG_VERSION === code && undefined !== detail)
|
|
34
|
+
return detail;
|
|
35
|
+
switch (code) {
|
|
36
|
+
case FlowErrorCode.INVALID_NAME:
|
|
37
|
+
return 'invalid flow name — use a single safe segment (letters/digits/-/_), no path separators';
|
|
38
|
+
case FlowErrorCode.NOT_FOUND:
|
|
39
|
+
return 'no such flow on disk — run reticle_flow{action:"list"} to see saved flows';
|
|
40
|
+
case FlowErrorCode.PARSE_FAILED:
|
|
41
|
+
return FlowParseNote.MALFORMED;
|
|
42
|
+
case FlowErrorCode.NO_RECORDING:
|
|
43
|
+
return 'no compiled recording by that name — record one (reticle_record{action:"start"|"stop"}) first';
|
|
44
|
+
// Never "regenerate it": the file is intact and the reader is the wrong one. Telling somebody
|
|
45
|
+
// to rewrite an undamaged flow is the failure this code was split out of PARSE_FAILED to stop.
|
|
46
|
+
case FlowErrorCode.WRONG_VERSION:
|
|
47
|
+
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';
|
|
48
|
+
case FlowErrorCode.STEP_NOT_FOUND:
|
|
49
|
+
return (detail ?? 'the step to resume at is not in this flow — pass a 0-based index or a step id');
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { flowAuthor } from './flow-author.js';
|
|
1
2
|
import { FLOW_MUTATE_TOOL } from './flow-mutate-tools.js';
|
|
2
3
|
import { seedSchema } from '../../surface/tools/args/numeric-bounds.js';
|
|
3
4
|
import { z } from 'zod';
|
|
@@ -217,7 +218,12 @@ export const FLOW_TOOLS = [
|
|
|
217
218
|
if (emptyRefusal !== undefined)
|
|
218
219
|
return Promise.resolve(emptyRefusal);
|
|
219
220
|
const { flows, root } = flowsForSession(deps, projectId);
|
|
220
|
-
const
|
|
221
|
+
const author = flowAuthor();
|
|
222
|
+
const toSave = {
|
|
223
|
+
...program,
|
|
224
|
+
...(saveAs === undefined ? {} : { name: saveAs }),
|
|
225
|
+
...(author === undefined ? {} : { author }),
|
|
226
|
+
};
|
|
221
227
|
return flows.save(toSave, annotations, projectId).then(async (res) => {
|
|
222
228
|
if (!res.ok)
|
|
223
229
|
return { error: flowErrorMessage(res.code, res.detail), code: res.code };
|
|
@@ -382,6 +388,10 @@ export const FLOW_TOOLS = [
|
|
|
382
388
|
.boolean()
|
|
383
389
|
.optional()
|
|
384
390
|
.describe('Set true to allow destructive controls during this replay only.'),
|
|
391
|
+
from: z
|
|
392
|
+
.union([z.number().int().nonnegative(), z.string().min(1)])
|
|
393
|
+
.optional()
|
|
394
|
+
.describe('Resume at this step (index or step id). Earlier steps re-run as unchecked, unreported setup; a setup step marked effect:"commits" refuses the resume.'),
|
|
385
395
|
sweep: z
|
|
386
396
|
.boolean()
|
|
387
397
|
.optional()
|
|
@@ -636,9 +646,7 @@ export const FLOW_TOOLS = [
|
|
|
636
646
|
return flow === undefined ? { replay } : { replay, flow };
|
|
637
647
|
}));
|
|
638
648
|
const timed = outcomes.map((o, i) => ({
|
|
639
|
-
replay:
|
|
640
|
-
? o.value.replay
|
|
641
|
-
: leaseFailureReplay(requested[i] ?? '', o.error),
|
|
649
|
+
...(parallelRuns[i] ?? { replay: leaseFailureReplay(requested[i] ?? '', o.error) }),
|
|
642
650
|
durationMs: o.ok && o.value !== undefined ? o.value.durationMs : 0,
|
|
643
651
|
}));
|
|
644
652
|
const flaky = await recordSuiteFlakes(deps.fs, rootForProjectId(deps, projectId), parallelRuns);
|
|
@@ -671,7 +679,11 @@ export const FLOW_TOOLS = [
|
|
|
671
679
|
.catch(() => null);
|
|
672
680
|
const flow = loaded !== null && loaded.ok ? loaded.value : undefined;
|
|
673
681
|
runs.push(flow === undefined ? { replay } : { replay, flow });
|
|
674
|
-
timed.push({
|
|
682
|
+
timed.push({
|
|
683
|
+
replay,
|
|
684
|
+
durationMs: deps.now() - start,
|
|
685
|
+
...(flow === undefined ? {} : { flow }),
|
|
686
|
+
});
|
|
675
687
|
}
|
|
676
688
|
// The flake ledger the CLI gate already keeps — see FlakeStore. It was only ever written by
|
|
677
689
|
// `reticle flow` on the command line, so an AGENT running this tool a hundred times learned
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type Predicate, type ProjectId } from '@reticlehq/core';
|
|
2
|
-
export { REDACTED_FILL } from './fields/flow-secret-field.js';
|
|
2
|
+
export { REDACTED_FILL, secretEnvKey } from './fields/flow-secret-field.js';
|
|
3
3
|
import { type FlowResult } from './flow-result.js';
|
|
4
4
|
export type { FlowResult } from './flow-result.js';
|
|
5
5
|
import type { FlowAnchor, FlowFile, FlowStep, HealChange, InstrumentationGap } from '@reticlehq/core';
|
|
@@ -65,6 +65,15 @@ export declare class FlowStore {
|
|
|
65
65
|
* the flow before writing. Omitting `annotations` reproduces the same bytes.
|
|
66
66
|
*/
|
|
67
67
|
save(program: CompiledProgram, annotations?: FlowAnnotations, projectId?: ProjectId): Promise<FlowResult<SaveSummary>>;
|
|
68
|
+
/**
|
|
69
|
+
* Save a flow the session drove, folded into any saved copy of the same journey.
|
|
70
|
+
*
|
|
71
|
+
* Every session saves its own drive, so one journey arrived as a near-copy per session. Flows
|
|
72
|
+
* under `prefix` (the auto-saved ones — a hand-named flow is never merged or removed) that start
|
|
73
|
+
* on the same page and take the same steps become ONE file: the oldest keeps its name, the others
|
|
74
|
+
* fill its gaps and are removed. The same scan collapses copies saved before this existed.
|
|
75
|
+
*/
|
|
76
|
+
saveJourney(program: CompiledProgram, projectId: ProjectId | undefined, prefix: string): Promise<FlowResult<SaveSummary>>;
|
|
68
77
|
/**
|
|
69
78
|
* Persist an already-anchored FlowFile captured in-page (no recompilation). The
|
|
70
79
|
* browser resolved every semantic anchor at capture time; here we only validate the name +
|