@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
|
@@ -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 +
|
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
import { asFlowName, asProjectId, } from '@reticlehq/core';
|
|
2
2
|
import { REDACTED_FILL } from './fields/flow-secret-field.js';
|
|
3
|
-
export { REDACTED_FILL } from './fields/flow-secret-field.js';
|
|
3
|
+
export { REDACTED_FILL, secretEnvKey } from './fields/flow-secret-field.js';
|
|
4
4
|
import { safeProjectId } from './flow-result.js';
|
|
5
5
|
import { changeInPlace } from './narrow-write.js';
|
|
6
|
-
import { AnchorKind, DEGRADED_ANCHOR_ROLE, FLOW_FILE_VERSION, FlowErrorCode, FlowFileSchema, FlowStepTool, QueryBy, defaultIsSensitiveKey, } from '@reticlehq/core';
|
|
6
|
+
import { AnchorKind, DEGRADED_ANCHOR_ROLE, FLOW_FILE_VERSION, flowFileVersionFor, FlowErrorCode, FlowFileSchema, FlowStepTool, QueryBy, defaultIsSensitiveKey, } from '@reticlehq/core';
|
|
7
7
|
import { ReticleTool } from '@reticlehq/core';
|
|
8
8
|
import { asRecord, asString } from '@reticlehq/core';
|
|
9
9
|
import { applyHealChanges } from './heal.js';
|
|
10
10
|
import { withLearnedSources } from './learned-sources.js';
|
|
11
11
|
import { flowIntentGap, linkFlowIntent } from './flow-intent.js';
|
|
12
|
+
import { journeyFingerprint, journeyName, mergeJourney, routeClaims } from './flow-journey.js';
|
|
12
13
|
import { IntentStore } from '../../memory/intent/intent-store.js';
|
|
13
14
|
import { flowDir, flowParentDir, flowPath, reticleDirPaths, isValidFlowName, } from '../../memory/project/dir/reticle-dir.js';
|
|
14
15
|
import { describeFlowZodFailure, parseFlowFileText } from './flow-expect-grammar.js';
|
|
@@ -164,6 +165,14 @@ function buildStep(tool, anchor, action, args, degraded) {
|
|
|
164
165
|
* NEVER silently dropped. ACT_SEQUENCE recurses over its sub-steps.
|
|
165
166
|
*/
|
|
166
167
|
export function recordedStepToFlowStep(step) {
|
|
168
|
+
const out = anchoredStep(step);
|
|
169
|
+
if (step.page !== undefined)
|
|
170
|
+
out.page = step.page;
|
|
171
|
+
if (step.endPage !== undefined && step.endPage !== step.page)
|
|
172
|
+
out.endPage = step.endPage;
|
|
173
|
+
return out;
|
|
174
|
+
}
|
|
175
|
+
function anchoredStep(step) {
|
|
167
176
|
if (step.invoke !== undefined) {
|
|
168
177
|
// An invocation drives nothing: no action, no args, and an anchor only because every step
|
|
169
178
|
// carries one. Falling through to the action path below would give it an `action` and a
|
|
@@ -237,7 +246,8 @@ export class FlowStore {
|
|
|
237
246
|
* of them produces byte-identical on-disk content (locked by the byte-stability tests).
|
|
238
247
|
*/
|
|
239
248
|
#serialize(flow) {
|
|
240
|
-
|
|
249
|
+
const stamped = { ...flow, version: flowFileVersionFor(flow) };
|
|
250
|
+
return `${JSON.stringify(stamped, null, JSON_INDENT)}\n`;
|
|
241
251
|
}
|
|
242
252
|
/**
|
|
243
253
|
* Register the flow's business goal in the intent ledger and stamp the row's id onto the flow.
|
|
@@ -267,16 +277,8 @@ export class FlowStore {
|
|
|
267
277
|
...(gap === undefined ? {} : { intentGap: gap }),
|
|
268
278
|
};
|
|
269
279
|
}
|
|
270
|
-
/**
|
|
271
|
-
|
|
272
|
-
* Optionally fold structured annotations (per-step expect, dynamic[], success) onto
|
|
273
|
-
* the flow before writing. Omitting `annotations` reproduces the same bytes.
|
|
274
|
-
*/
|
|
275
|
-
async save(program, annotations, projectId) {
|
|
276
|
-
if (!isValidFlowName(program.name)) {
|
|
277
|
-
return { ok: false, code: FlowErrorCode.INVALID_NAME };
|
|
278
|
-
}
|
|
279
|
-
const pid = safeProjectId(projectId);
|
|
280
|
+
/** The on-disk flow a compiled recording becomes, before anything is written. */
|
|
281
|
+
#build(program, annotations, pid) {
|
|
280
282
|
const steps = program.steps.map(recordedStepToFlowStep);
|
|
281
283
|
const base = {
|
|
282
284
|
version: FLOW_FILE_VERSION,
|
|
@@ -295,11 +297,85 @@ export class FlowStore {
|
|
|
295
297
|
*/
|
|
296
298
|
...(program.startPath === undefined ? {} : { startPath: program.startPath }),
|
|
297
299
|
};
|
|
298
|
-
const
|
|
300
|
+
const intent = program.steps.find((step) => step.intent !== undefined)?.intent;
|
|
301
|
+
const annotated = withAnnotations(base, annotations);
|
|
302
|
+
return {
|
|
303
|
+
...annotated,
|
|
304
|
+
// The action's own intent, when nobody annotated the flow — see RecordedStep.intent.
|
|
305
|
+
...(annotated.intent === undefined && intent !== undefined ? { intent } : {}),
|
|
306
|
+
...routeClaims(annotated.startPath, annotated.steps),
|
|
307
|
+
...(program.author === undefined ? {} : { author: program.author }),
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
/**
|
|
311
|
+
* Convert a CompiledProgram (testid-normalized) into an anchored, on-disk flow + write it.
|
|
312
|
+
* Optionally fold structured annotations (per-step expect, dynamic[], success) onto
|
|
313
|
+
* the flow before writing. Omitting `annotations` reproduces the same bytes.
|
|
314
|
+
*/
|
|
315
|
+
async save(program, annotations, projectId) {
|
|
316
|
+
if (!isValidFlowName(program.name)) {
|
|
317
|
+
return { ok: false, code: FlowErrorCode.INVALID_NAME };
|
|
318
|
+
}
|
|
319
|
+
const pid = safeProjectId(projectId);
|
|
320
|
+
const base = this.#build(program, annotations, pid);
|
|
321
|
+
const flow = await this.#linkIntent(base);
|
|
299
322
|
await this.#fs.mkdir(flowParentDir(this.#root, program.name, pid));
|
|
300
323
|
await this.#fs.writeFile(flowPath(this.#root, program.name, pid), this.#serialize(flow));
|
|
301
324
|
return { ok: true, value: this.#summary(flow) };
|
|
302
325
|
}
|
|
326
|
+
/**
|
|
327
|
+
* Save a flow the session drove, folded into any saved copy of the same journey.
|
|
328
|
+
*
|
|
329
|
+
* Every session saves its own drive, so one journey arrived as a near-copy per session. Flows
|
|
330
|
+
* under `prefix` (the auto-saved ones — a hand-named flow is never merged or removed) that start
|
|
331
|
+
* on the same page and take the same steps become ONE file: the oldest keeps its name, the others
|
|
332
|
+
* fill its gaps and are removed. The same scan collapses copies saved before this existed.
|
|
333
|
+
*/
|
|
334
|
+
async saveJourney(program, projectId, prefix) {
|
|
335
|
+
if (!isValidFlowName(program.name))
|
|
336
|
+
return { ok: false, code: FlowErrorCode.INVALID_NAME };
|
|
337
|
+
const pid = safeProjectId(projectId);
|
|
338
|
+
const fresh = this.#build(program, undefined, pid);
|
|
339
|
+
const saved = [];
|
|
340
|
+
for (const name of (await this.list(pid)).filter((n) => n.startsWith(prefix))) {
|
|
341
|
+
const loaded = await this.load(name, pid);
|
|
342
|
+
if (loaded.ok)
|
|
343
|
+
saved.push(loaded.value);
|
|
344
|
+
}
|
|
345
|
+
const byPrint = new Map();
|
|
346
|
+
for (const flow of saved) {
|
|
347
|
+
const print = journeyFingerprint(flow);
|
|
348
|
+
byPrint.set(print, [...(byPrint.get(print) ?? []), flow]);
|
|
349
|
+
}
|
|
350
|
+
const print = journeyFingerprint(fresh);
|
|
351
|
+
const taken = new Map(saved.map((flow) => [flow.name, journeyFingerprint(flow)]));
|
|
352
|
+
const named = { ...fresh, name: journeyName(fresh.name, print, taken) };
|
|
353
|
+
byPrint.set(print, [...(byPrint.get(print) ?? []), named]);
|
|
354
|
+
let result = named;
|
|
355
|
+
for (const [groupPrint, group] of byPrint) {
|
|
356
|
+
if (group.length < 2 && groupPrint !== print)
|
|
357
|
+
continue;
|
|
358
|
+
const [kept, ...rest] = [...group].sort((a, b) => a.createdAt - b.createdAt);
|
|
359
|
+
if (kept === undefined)
|
|
360
|
+
continue;
|
|
361
|
+
const merged = await this.#linkIntent(rest.reduce(mergeJourney, kept));
|
|
362
|
+
await this.#writeInPlace(merged, pid);
|
|
363
|
+
for (const dropped of rest) {
|
|
364
|
+
if (dropped.name !== merged.name && dropped !== named)
|
|
365
|
+
await this.remove(dropped.name, pid);
|
|
366
|
+
}
|
|
367
|
+
if (groupPrint === print)
|
|
368
|
+
result = merged;
|
|
369
|
+
}
|
|
370
|
+
return { ok: true, value: this.#summary(result) };
|
|
371
|
+
}
|
|
372
|
+
/** Write a flow where it already lives, or into its project's directory when it is new. */
|
|
373
|
+
async #writeInPlace(flow, pid) {
|
|
374
|
+
const name = asFlowName(flow.name);
|
|
375
|
+
const path = (await this.#resolveReadPath(name, pid)) ?? flowPath(this.#root, name, pid);
|
|
376
|
+
await this.#fs.mkdir(flowParentDir(this.#root, name, pid));
|
|
377
|
+
await this.#fs.writeFile(path, this.#serialize(flow));
|
|
378
|
+
}
|
|
303
379
|
/**
|
|
304
380
|
* Persist an already-anchored FlowFile captured in-page (no recompilation). The
|
|
305
381
|
* browser resolved every semantic anchor at capture time; here we only validate the name +
|
|
@@ -21,7 +21,9 @@ export function haltedFrom(results, total) {
|
|
|
21
21
|
return undefined;
|
|
22
22
|
if (last.ok && last.drift === undefined)
|
|
23
23
|
return undefined;
|
|
24
|
-
|
|
24
|
+
// From the step's own index, not the count of results: a resumed replay does not report its setup
|
|
25
|
+
// prefix, so the results array no longer starts at step 0.
|
|
26
|
+
const notAttempted = total - (last.step + 1);
|
|
25
27
|
// A failure on the FINAL step is not a halt: nothing was left to attempt, and `notAttempted: 0`
|
|
26
28
|
// would send a reader looking for steps that do not exist.
|
|
27
29
|
if (notAttempted <= 0)
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type Predicate } from '@reticlehq/core';
|
|
1
|
+
import { type FlowFile, type Predicate } from '@reticlehq/core';
|
|
2
2
|
/** One captured agent action, normalized for replay. */
|
|
3
3
|
export interface RecordedStep {
|
|
4
4
|
/** ReticleTool.ACT | ReticleTool.ACT_SEQUENCE */
|
|
@@ -27,6 +27,12 @@ export interface RecordedStep {
|
|
|
27
27
|
* `invoke` first and treats the step as a call.
|
|
28
28
|
*/
|
|
29
29
|
invoke?: string;
|
|
30
|
+
/** The page this step ran on. Written to the saved step (unlike `route`, which cuts journeys). */
|
|
31
|
+
page?: string;
|
|
32
|
+
/** The page it ended on once it settled — see `markEnded`. */
|
|
33
|
+
endPage?: string;
|
|
34
|
+
/** Why the agent took this step: the `intent` it declared on the action. Names the saved flow. */
|
|
35
|
+
intent?: string;
|
|
30
36
|
}
|
|
31
37
|
interface ActiveRecording {
|
|
32
38
|
cursor: number;
|
|
@@ -59,6 +65,8 @@ export interface CompiledProgram {
|
|
|
59
65
|
* format change.
|
|
60
66
|
*/
|
|
61
67
|
routes?: string[];
|
|
68
|
+
/** Who made it — see FlowFile.author. Stamped by the caller that knows. */
|
|
69
|
+
author?: FlowFile['author'];
|
|
62
70
|
}
|
|
63
71
|
/**
|
|
64
72
|
* The `tool` written on an invocation step — core's constant, re-exported for this module's callers.
|
|
@@ -105,6 +113,27 @@ export declare class RecordingStore {
|
|
|
105
113
|
* anything should not carry an empty tape, and "did anything happen at all" stays answerable.
|
|
106
114
|
*/
|
|
107
115
|
capture(step: RecordedStep, route?: string): void;
|
|
116
|
+
/**
|
|
117
|
+
* Add a consequence to the last captured step of every active recording.
|
|
118
|
+
*
|
|
119
|
+
* A separate `reticle_assert` after an act is how an agent proves what the act did, and it never
|
|
120
|
+
* reached the recorder — only the act tools called `capture` — so the flow kept the click and lost
|
|
121
|
+
* the proof. What the step already declared is kept; the new check joins it under `allOf`.
|
|
122
|
+
*/
|
|
123
|
+
attachExpect(expect: Predicate): void;
|
|
124
|
+
/**
|
|
125
|
+
* A navigation happened. Navigating records no step, so until the next one is captured, a check
|
|
126
|
+
* proved now is about the page the navigation opened, not about the last step — and kept on that
|
|
127
|
+
* step, replay would ask it right after the click, of a page it was never true on.
|
|
128
|
+
*/
|
|
129
|
+
markNavigated(): void;
|
|
130
|
+
/**
|
|
131
|
+
* The page the step just captured ended on, once its action settled.
|
|
132
|
+
*
|
|
133
|
+
* Fills only a step that has none, so calling it after an action that captured nothing leaves the
|
|
134
|
+
* previous step's own answer alone.
|
|
135
|
+
*/
|
|
136
|
+
markEnded(page: string | undefined): void;
|
|
108
137
|
/**
|
|
109
138
|
* Returns the active recording (cursor + steps) and clears it, or undefined if not recording.
|
|
110
139
|
*
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { FlowStepTool } from '@reticlehq/core';
|
|
1
|
+
import { FlowStepTool, PredicateKind } from '@reticlehq/core';
|
|
2
2
|
/**
|
|
3
3
|
* The `tool` written on an invocation step — core's constant, re-exported for this module's callers.
|
|
4
4
|
*
|
|
@@ -31,6 +31,8 @@ const AMBIENT_STEP_CAP = 400;
|
|
|
31
31
|
export class RecordingStore {
|
|
32
32
|
#active = new Map();
|
|
33
33
|
#compiled = new Map();
|
|
34
|
+
/** A navigation came after the last captured step — see markNavigated. */
|
|
35
|
+
#navigatedSinceStep = false;
|
|
34
36
|
start(name, cursor, startPath) {
|
|
35
37
|
const openedOver = new Map();
|
|
36
38
|
for (const [outer, rec] of this.#active)
|
|
@@ -67,6 +69,7 @@ export class RecordingStore {
|
|
|
67
69
|
* anything should not carry an empty tape, and "did anything happen at all" stays answerable.
|
|
68
70
|
*/
|
|
69
71
|
capture(step, route) {
|
|
72
|
+
this.#navigatedSinceStep = false;
|
|
70
73
|
if (!this.#active.has(AMBIENT_RECORDING)) {
|
|
71
74
|
this.#active.set(AMBIENT_RECORDING, {
|
|
72
75
|
cursor: 0,
|
|
@@ -83,12 +86,63 @@ export class RecordingStore {
|
|
|
83
86
|
// opened on purpose is not capped — they said when it starts and they say when it stops.
|
|
84
87
|
if (AMBIENT_RECORDING === name && rec.steps.length >= AMBIENT_STEP_CAP)
|
|
85
88
|
continue;
|
|
89
|
+
// A step nobody marked ended where the next one began.
|
|
90
|
+
const previous = rec.steps.at(-1);
|
|
91
|
+
if (previous !== undefined && previous.endPage === undefined && step.page !== undefined) {
|
|
92
|
+
previous.endPage = step.page;
|
|
93
|
+
}
|
|
86
94
|
// The route rides on the AMBIENT tape only: a recording somebody opened deliberately is
|
|
87
95
|
// already one journey by construction, and stamping a route on its steps would change what a
|
|
88
96
|
// deliberate recording contains.
|
|
89
97
|
rec.steps.push(AMBIENT_RECORDING === name && route !== undefined ? { ...step, route } : step);
|
|
90
98
|
}
|
|
91
99
|
}
|
|
100
|
+
/**
|
|
101
|
+
* Add a consequence to the last captured step of every active recording.
|
|
102
|
+
*
|
|
103
|
+
* A separate `reticle_assert` after an act is how an agent proves what the act did, and it never
|
|
104
|
+
* reached the recorder — only the act tools called `capture` — so the flow kept the click and lost
|
|
105
|
+
* the proof. What the step already declared is kept; the new check joins it under `allOf`.
|
|
106
|
+
*/
|
|
107
|
+
attachExpect(expect) {
|
|
108
|
+
if (this.#navigatedSinceStep)
|
|
109
|
+
return;
|
|
110
|
+
for (const rec of this.#active.values()) {
|
|
111
|
+
const last = rec.steps.at(-1);
|
|
112
|
+
if (last === undefined)
|
|
113
|
+
continue;
|
|
114
|
+
const held = last.expect;
|
|
115
|
+
last.expect =
|
|
116
|
+
held === undefined
|
|
117
|
+
? expect
|
|
118
|
+
: PredicateKind.ALL_OF === held.kind
|
|
119
|
+
? { ...held, predicates: [...held.predicates, expect] }
|
|
120
|
+
: { kind: PredicateKind.ALL_OF, predicates: [held, expect] };
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* A navigation happened. Navigating records no step, so until the next one is captured, a check
|
|
125
|
+
* proved now is about the page the navigation opened, not about the last step — and kept on that
|
|
126
|
+
* step, replay would ask it right after the click, of a page it was never true on.
|
|
127
|
+
*/
|
|
128
|
+
markNavigated() {
|
|
129
|
+
this.#navigatedSinceStep = true;
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* The page the step just captured ended on, once its action settled.
|
|
133
|
+
*
|
|
134
|
+
* Fills only a step that has none, so calling it after an action that captured nothing leaves the
|
|
135
|
+
* previous step's own answer alone.
|
|
136
|
+
*/
|
|
137
|
+
markEnded(page) {
|
|
138
|
+
if (page === undefined)
|
|
139
|
+
return;
|
|
140
|
+
for (const rec of this.#active.values()) {
|
|
141
|
+
const last = rec.steps.at(-1);
|
|
142
|
+
if (last !== undefined && last.endPage === undefined)
|
|
143
|
+
last.endPage = page;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
92
146
|
/**
|
|
93
147
|
* Returns the active recording (cursor + steps) and clears it, or undefined if not recording.
|
|
94
148
|
*
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type Predicate } from '@reticlehq/core';
|
|
1
2
|
import { type CommandResult } from '@reticlehq/core';
|
|
2
3
|
import type { RecordedStep, CompiledProgram } from './recording/tape/recordings.js';
|
|
3
4
|
import type { Session } from '../../portal/session/session.js';
|
|
@@ -27,6 +28,13 @@ export declare function queryRefs(result: CommandResult): string[];
|
|
|
27
28
|
* an empty box fails identically and tells the reader nothing.
|
|
28
29
|
*/
|
|
29
30
|
export declare function replayActionArgs(value: unknown, confirmDangerous?: boolean, field?: string): Record<string, unknown>;
|
|
31
|
+
/**
|
|
32
|
+
* The pathname of a session's live url, or undefined when it has none yet.
|
|
33
|
+
*
|
|
34
|
+
* Pathname rather than the whole url, because that is what `startPath` is compared against and what
|
|
35
|
+
* a hash-routed app makes meaningless in the document location — see routeOfEvent.
|
|
36
|
+
*/
|
|
37
|
+
export declare function pathOf(url: string | undefined): string | undefined;
|
|
30
38
|
/**
|
|
31
39
|
* Capture an act into every in-flight recording, or do nothing when none is running.
|
|
32
40
|
*
|
|
@@ -40,6 +48,17 @@ export declare function captureAct(recordings: {
|
|
|
40
48
|
}, args: Record<string, unknown>, res: unknown,
|
|
41
49
|
/** Where this step ran, so the ambient tape can be cut into journeys. See RecordedStep.route. */
|
|
42
50
|
route?: string): void;
|
|
51
|
+
/**
|
|
52
|
+
* Capture an act_and_wait once its verdict is known, keeping `until` as the step's expectation only
|
|
53
|
+
* on a yes. Recorded before the verdict, a step kept an `until` that came back `no` or `no-fault` —
|
|
54
|
+
* a regression test asserting something never once observed to hold because of the action. The
|
|
55
|
+
* action itself is always recorded: it happened, and replay needs it to reach the next step.
|
|
56
|
+
*/
|
|
57
|
+
export declare function captureVerdictedAct(recordings: Parameters<typeof captureAct>[0], args: Record<string, unknown>, res: unknown, route: string | undefined, verified: string | undefined): void;
|
|
58
|
+
/** Fold a PASSING standalone assertion into the step it proved. See RecordingStore.attachExpect. */
|
|
59
|
+
export declare function captureAssertion(recordings: {
|
|
60
|
+
attachExpect: (expect: Predicate) => void;
|
|
61
|
+
}, raw: unknown): void;
|
|
43
62
|
/**
|
|
44
63
|
* Compile a single reticle_act invocation into a normalized RecordedStep using the action result. Anchor
|
|
45
64
|
* priority mirrors synthesizeAnchor: a data-testid is the gold standard; failing that, the element's
|