@reticlehq/server 3.5.0 → 3.6.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/README.md +1 -1
- package/SKILL.md +16 -8
- package/dist/chromium-launch-options.d.ts +3 -1
- package/dist/chromium-launch-options.js +2 -1
- package/dist/command/cli/adhoc-verdict.d.ts +1 -0
- package/dist/command/cli/adhoc-verdict.js +6 -3
- package/dist/command/cli/answers/open-note.d.ts +2 -0
- package/dist/command/cli/answers/open-note.js +22 -0
- package/dist/command/cli/cli-flow-commands.js +4 -8
- package/dist/command/cli/cli-parse.d.ts +1 -1
- package/dist/command/cli/cli-parse.js +10 -10
- package/dist/command/cli/cli-verify.d.ts +1 -1
- package/dist/command/cli/cli-verify.js +35 -25
- package/dist/command/cli/cloud-cli.js +51 -12
- package/dist/command/cli/cloud-login.d.ts +8 -0
- package/dist/command/cli/cloud-login.js +36 -2
- package/dist/command/cli/demo/demo-page.js +11 -3
- package/dist/command/cli/doctor/browser/chromium-hint.d.ts +46 -0
- package/dist/command/cli/doctor/browser/chromium-hint.js +99 -1
- package/dist/command/cli/drive/drive-attach.d.ts +27 -2
- package/dist/command/cli/drive/drive-attach.js +71 -4
- package/dist/command/cli/drive/drive-command.d.ts +2 -1
- package/dist/command/cli/drive/drive-command.js +13 -7
- package/dist/command/cli/lifecycle/daemon-lifecycle.js +4 -1
- package/dist/command/cli/ports/port-holder.d.ts +10 -0
- package/dist/command/cli/ports/port-holder.js +24 -0
- package/dist/command/cli/setup-mcp-cli.js +6 -4
- package/dist/command/cli/tutorial.d.ts +2 -2
- package/dist/command/cli/tutorial.js +6 -9
- package/dist/command/cli.d.ts +6 -0
- package/dist/command/cli.js +28 -22
- package/dist/command/daemon/binding/daemon-status-probe.d.ts +2 -0
- package/dist/command/daemon/binding/daemon-status-probe.js +1 -0
- package/dist/command/daemon/daemon-resolve.d.ts +14 -0
- package/dist/command/daemon/daemon-resolve.js +33 -2
- package/dist/command/setup/agent-writer.d.ts +1 -1
- package/dist/command/setup/agent-writer.js +3 -2
- package/dist/command/setup/bringup/connect-lease.d.ts +1 -0
- package/dist/command/setup/bringup/connect-lease.js +1 -0
- package/dist/command/setup/bringup/dev-server-wait.d.ts +8 -0
- package/dist/command/setup/bringup/dev-server-wait.js +19 -0
- package/dist/command/setup/init/init-port.d.ts +4 -0
- package/dist/command/setup/init/init-port.js +10 -0
- package/dist/command/setup/init/init-runtime.d.ts +10 -0
- package/dist/command/setup/init/init-runtime.js +18 -1
- package/dist/command/setup/node-effects.js +2 -1
- package/dist/command/setup/remaining-steps.js +2 -1
- package/dist/command/setup/run-setup.d.ts +5 -0
- package/dist/command/setup/run-setup.js +35 -27
- package/dist/command/setup/setup-command.d.ts +2 -1
- package/dist/command/setup/setup-command.js +2 -0
- package/dist/command/version/version-skew.js +11 -2
- package/dist/features/crawl/crawl-tools.js +4 -0
- package/dist/features/crawl/crawl.d.ts +6 -0
- package/dist/features/crawl/crawl.js +9 -1
- package/dist/features/exhaust/ledger.d.ts +7 -0
- package/dist/features/exhaust/ledger.js +35 -9
- package/dist/features/harness/app-findings.d.ts +2 -0
- package/dist/features/harness/app-findings.js +52 -0
- package/dist/features/harness/drive-report.d.ts +27 -1
- package/dist/features/harness/drive-report.js +162 -11
- package/dist/features/harness/drivers.d.ts +10 -14
- package/dist/features/harness/drivers.js +10 -14
- package/dist/features/harness/fill-values.d.ts +3 -73
- package/dist/features/harness/fill-values.js +3 -169
- package/dist/features/harness/goals.d.ts +21 -0
- package/dist/features/harness/goals.js +77 -0
- package/dist/features/harness/harness.d.ts +21 -0
- package/dist/features/harness/harness.js +69 -2
- package/dist/features/harness/platform/drive-target.d.ts +30 -0
- package/dist/features/harness/platform/drive-target.js +81 -0
- package/dist/features/harness/platform/local-apps.d.ts +94 -0
- package/dist/features/harness/platform/local-apps.js +204 -0
- package/dist/features/harness/platform/remote-drive.d.ts +180 -0
- package/dist/features/harness/platform/remote-drive.js +365 -0
- package/dist/features/harness/platform/script.d.ts +48 -0
- package/dist/features/harness/platform/script.js +128 -0
- package/dist/features/harness/platform/server-driver.d.ts +41 -0
- package/dist/features/harness/platform/server-driver.js +179 -0
- package/dist/features/harness/platform-config.d.ts +7 -0
- package/dist/features/harness/platform-config.js +14 -2
- package/dist/features/harness/script/app-controls.d.ts +5 -0
- package/dist/features/harness/script/app-controls.js +40 -0
- package/dist/features/harness/script/lane-ids.d.ts +14 -0
- package/dist/features/harness/script/lane-ids.js +16 -0
- package/dist/features/harness/script/script-run.d.ts +41 -0
- package/dist/features/harness/script/script-run.js +299 -0
- package/dist/features/license/license.d.ts +2 -2
- package/dist/features/visual/visual-tools.d.ts +10 -1
- package/dist/features/visual/visual-tools.js +40 -3
- package/dist/hooks/driven-by.d.ts +4 -0
- package/dist/hooks/driven-by.js +16 -0
- package/dist/hooks/hook-commands.js +15 -4
- package/dist/hooks/tool-log.d.ts +9 -0
- package/dist/hooks/tool-log.js +29 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +76 -82
- package/dist/judgement/runs/artifact/build-verification-run.js +6 -0
- package/dist/judgement/runs/artifact/replay-mapping.d.ts +1 -1
- package/dist/judgement/runs/artifact/replay-mapping.js +16 -6
- package/dist/judgement/runs/artifact/run-store.d.ts +1 -1
- package/dist/judgement/runs/artifact/run-store.js +26 -1
- package/dist/judgement/runs/drive-run.d.ts +22 -0
- package/dist/judgement/runs/drive-run.js +99 -1
- package/dist/judgement/runs/repair-prompt.js +1 -1
- package/dist/judgement/runs/reticle-runner.js +8 -1
- package/dist/judgement/runs/verification-sync.d.ts +3 -2
- package/dist/judgement/runs/verification-sync.js +4 -3
- package/dist/language/flows/change/gate.d.ts +10 -0
- package/dist/language/flows/change/gate.js +22 -0
- package/dist/language/flows/decision.js +34 -0
- package/dist/language/flows/flow-anchor.d.ts +54 -8
- package/dist/language/flows/flow-anchor.js +88 -3
- package/dist/language/flows/flow-cross-step.d.ts +15 -0
- package/dist/language/flows/flow-cross-step.js +21 -0
- package/dist/language/flows/flow-learning.js +8 -11
- package/dist/language/flows/flow-mutate-tools.js +5 -3
- package/dist/language/flows/flow-replay-run.d.ts +23 -9
- package/dist/language/flows/flow-replay-run.js +101 -94
- package/dist/language/flows/flow-replay.d.ts +23 -1
- package/dist/language/flows/flow-replay.js +92 -19
- package/dist/language/flows/flow-scope.js +5 -1
- package/dist/language/flows/flow-step-runners.d.ts +20 -2
- package/dist/language/flows/flow-step-runners.js +236 -42
- package/dist/language/flows/flow-store-for-session.d.ts +2 -2
- package/dist/language/flows/flow-store-for-session.js +2 -2
- package/dist/language/flows/flow-tools.js +58 -34
- package/dist/language/flows/flows.js +5 -2
- package/dist/language/flows/heal-run.js +6 -4
- package/dist/language/flows/recording/flake.d.ts +2 -2
- package/dist/language/flows/recording/tape/recordings.d.ts +30 -4
- package/dist/language/flows/recording/tape/recordings.js +40 -8
- package/dist/language/flows/replay-from-hud.d.ts +20 -0
- package/dist/language/flows/replay-from-hud.js +51 -0
- package/dist/language/flows/replay.d.ts +9 -5
- package/dist/language/flows/replay.js +27 -9
- package/dist/language/flows/suite/suite-flakes.js +2 -0
- package/dist/language/flows/suite-selection.d.ts +2 -15
- package/dist/language/flows/suite-selection.js +4 -3
- package/dist/language/flows/verify-change-tools.js +8 -4
- package/dist/launch-chromium.d.ts +27 -5
- package/dist/launch-chromium.js +38 -8
- package/dist/memory/cloud/cloud-config.d.ts +12 -0
- package/dist/memory/cloud/cloud-config.js +15 -0
- package/dist/memory/cloud/cloud-sync.d.ts +17 -14
- package/dist/memory/cloud/cloud-sync.js +12 -1
- package/dist/memory/cloud/harness-config.d.ts +26 -1
- package/dist/memory/cloud/harness-config.js +99 -4
- package/dist/memory/cloud/hud-notices-source.d.ts +21 -0
- package/dist/memory/cloud/hud-notices-source.js +128 -0
- package/dist/memory/cloud/memory-scope.d.ts +144 -0
- package/dist/memory/cloud/memory-scope.js +205 -0
- package/dist/memory/cloud/sync-cycle.d.ts +12 -3
- package/dist/memory/cloud/sync-cycle.js +16 -3
- package/dist/memory/cloud/sync-daemon.js +7 -5
- package/dist/memory/cloud/sync-disk.d.ts +1 -1
- package/dist/memory/cloud/sync-disk.js +5 -8
- package/dist/memory/impact/account-watch.d.ts +19 -0
- package/dist/memory/impact/account-watch.js +34 -0
- package/dist/memory/impact/impact-recorder.d.ts +8 -0
- package/dist/memory/impact/impact-recorder.js +12 -2
- package/dist/memory/impact/impact-store.d.ts +16 -0
- package/dist/memory/impact/impact-store.js +142 -26
- package/dist/memory/intent/intent-shard.d.ts +2 -2
- package/dist/memory/intent/intent-tools.js +80 -2
- package/dist/memory/journal/envelope.d.ts +8 -8
- package/dist/memory/journal/journal-recorder.js +7 -1
- package/dist/memory/journal/on-disk/retention.d.ts +3 -0
- package/dist/memory/journal/on-disk/retention.js +5 -0
- package/dist/memory/journal/on-disk/startup-maintenance.js +3 -1
- package/dist/memory/journal/on-disk/workspace-tiers.js +7 -4
- package/dist/memory/journal/session-end.js +6 -4
- package/dist/memory/project/artifact-root-resolver.d.ts +26 -1
- package/dist/memory/project/artifact-root-resolver.js +94 -13
- package/dist/memory/project/artifact-root.d.ts +42 -5
- package/dist/memory/project/artifact-root.js +87 -8
- package/dist/memory/project/dir/reticle-dir.d.ts +4 -2
- package/dist/memory/project/dir/reticle-dir.js +3 -1
- package/dist/memory/project/project-tools.js +22 -3
- package/dist/memory/project/session-root.d.ts +24 -1
- package/dist/memory/project/session-root.js +41 -5
- package/dist/memory/project/sync-status.d.ts +39 -0
- package/dist/memory/project/sync-status.js +90 -0
- package/dist/memory/recall/memory-tools.js +2 -0
- package/dist/memory/recall/project-memory.d.ts +19 -0
- package/dist/memory/recall/project-memory.js +45 -6
- package/dist/portal/bridge/bridge.d.ts +3 -1
- package/dist/portal/bridge/bridge.js +19 -2
- package/dist/portal/bridge/pairing-token.d.ts +9 -0
- package/dist/portal/bridge/pairing-token.js +17 -0
- package/dist/portal/input/real-input.d.ts +81 -10
- package/dist/portal/input/real-input.js +207 -22
- package/dist/portal/input/viewport-tools.js +3 -3
- package/dist/portal/pool/browser-pool.d.ts +22 -9
- package/dist/portal/pool/browser-pool.js +70 -35
- package/dist/portal/pool/playwright-launcher.d.ts +13 -0
- package/dist/portal/pool/playwright-launcher.js +17 -1
- package/dist/portal/pool/pool-contract.d.ts +7 -4
- package/dist/portal/session/command-payload.d.ts +8 -0
- package/dist/portal/session/command-payload.js +19 -0
- package/dist/portal/session/dev-server/dev-command.d.ts +12 -0
- package/dist/portal/session/dev-server/dev-command.js +61 -12
- package/dist/portal/session/facts/departure-note.d.ts +22 -0
- package/dist/portal/session/facts/departure-note.js +47 -0
- package/dist/portal/session/facts/session-replaced.d.ts +5 -0
- package/dist/portal/session/facts/session-replaced.js +5 -0
- package/dist/portal/session/first-run-wiring.d.ts +42 -0
- package/dist/portal/session/first-run-wiring.js +132 -0
- package/dist/portal/session/human/notes-ledger.d.ts +20 -0
- package/dist/portal/session/human/notes-ledger.js +55 -0
- package/dist/portal/session/human/review-store.d.ts +12 -0
- package/dist/portal/session/human/review-store.js +18 -10
- package/dist/portal/session/live-control-tools.d.ts +0 -13
- package/dist/portal/session/live-control-tools.js +28 -3
- package/dist/portal/session/no-session-diagnosis.d.ts +7 -0
- package/dist/portal/session/no-session-diagnosis.js +15 -0
- package/dist/portal/session/no-session-next-action.d.ts +11 -0
- package/dist/portal/session/no-session-next-action.js +31 -5
- package/dist/portal/session/no-session-watch.d.ts +14 -1
- package/dist/portal/session/no-session-watch.js +63 -10
- package/dist/portal/session/presence/session-mirror.js +3 -0
- package/dist/portal/session/serving-directory.d.ts +27 -0
- package/dist/portal/session/serving-directory.js +45 -0
- package/dist/portal/session/session-health.d.ts +7 -0
- package/dist/portal/session/session-health.js +6 -0
- package/dist/portal/session/session-info.d.ts +8 -1
- package/dist/portal/session/session-info.js +6 -0
- package/dist/portal/session/session-manager.d.ts +21 -0
- package/dist/portal/session/session-manager.js +44 -8
- package/dist/portal/session/session-reaper.js +4 -1
- package/dist/portal/session/session-successor.d.ts +5 -0
- package/dist/portal/session/session-tools.js +26 -3
- package/dist/portal/session/session.d.ts +7 -2
- package/dist/portal/session/session.js +21 -18
- package/dist/start-options.d.ts +6 -0
- package/dist/status-payload.d.ts +10 -0
- package/dist/status-payload.js +23 -0
- package/dist/surface/http-server.js +11 -1
- package/dist/surface/mcp/mcp-proxy.d.ts +5 -0
- package/dist/surface/mcp/mcp-proxy.js +36 -5
- package/dist/surface/mcp/mcp.d.ts +11 -6
- package/dist/surface/mcp/mcp.js +27 -13
- package/dist/surface/mcp/server-instructions.d.ts +4 -2
- package/dist/surface/mcp/server-instructions.js +14 -4
- package/dist/surface/mcp/shared-params.js +2 -2
- package/dist/surface/tools/act/navigation/navigate-arrival.d.ts +8 -0
- package/dist/surface/tools/act/navigation/navigate-arrival.js +56 -2
- package/dist/surface/tools/act/settle-in-flight.js +25 -3
- package/dist/surface/tools/act-sequence-tool.js +2 -2
- package/dist/surface/tools/act-tools.js +23 -7
- package/dist/surface/tools/assert/assert-verdict.d.ts +1 -1
- package/dist/surface/tools/assert/assert-verdict.js +17 -2
- package/dist/surface/tools/browser-tools.js +8 -1
- package/dist/surface/tools/chat-drives.d.ts +26 -0
- package/dist/surface/tools/chat-drives.js +165 -0
- package/dist/surface/tools/empty-session-list.d.ts +41 -0
- package/dist/surface/tools/empty-session-list.js +80 -0
- package/dist/surface/tools/error-recovery.d.ts +6 -0
- package/dist/surface/tools/error-recovery.js +37 -4
- package/dist/surface/tools/explore-tools.d.ts +10 -1
- package/dist/surface/tools/explore-tools.js +82 -13
- package/dist/surface/tools/feedback-tools.js +14 -0
- package/dist/surface/tools/harness-explore.d.ts +96 -14
- package/dist/surface/tools/harness-explore.js +227 -206
- package/dist/surface/tools/harness-plan.d.ts +12 -0
- package/dist/surface/tools/harness-plan.js +31 -0
- package/dist/surface/tools/harness-script.d.ts +8 -0
- package/dist/surface/tools/harness-script.js +342 -0
- package/dist/surface/tools/harness-toolset.d.ts +14 -1
- package/dist/surface/tools/harness-toolset.js +77 -6
- package/dist/surface/tools/invoke-tool.js +52 -1
- package/dist/surface/tools/lease-seed.d.ts +29 -0
- package/dist/surface/tools/lease-seed.js +123 -0
- package/dist/surface/tools/lease-tools.d.ts +3 -23
- package/dist/surface/tools/lease-tools.js +30 -131
- package/dist/surface/tools/live-call-text.js +2 -2
- package/dist/surface/tools/next-step.d.ts +19 -0
- package/dist/surface/tools/next-step.js +159 -0
- package/dist/surface/tools/observe-tools.js +2 -4
- package/dist/surface/tools/platform-moment.d.ts +12 -0
- package/dist/surface/tools/platform-moment.js +168 -0
- package/dist/surface/tools/query-hint-schema.d.ts +7 -0
- package/dist/surface/tools/query-hint-schema.js +10 -0
- package/dist/surface/tools/read-tools.js +8 -2
- package/dist/surface/tools/real-input-attempt.js +96 -13
- package/dist/surface/tools/tool-kit.d.ts +15 -1
- package/dist/surface/tools/tool-kit.js +4 -6
- package/dist/surface/tools/tools.js +20 -48
- package/dist/telemetry/hud-metrics.d.ts +2 -0
- package/dist/telemetry/hud-metrics.js +17 -0
- package/dist/telemetry/telemetry.d.ts +12 -0
- package/dist/telemetry/telemetry.js +43 -14
- package/dist/telemetry/verification-of.js +1 -0
- package/dist/wire-journal.js +4 -1
- package/docs/README.md +1 -1
- package/docs/agent-cheatsheet.md +1 -1
- package/docs/autodrive.md +29 -25
- package/docs/cli/doctor.mdx +6 -0
- package/docs/cli/drive.mdx +8 -2
- package/docs/cli/init.mdx +11 -13
- package/docs/cli/status.mdx +5 -1
- package/docs/cli/version.mdx +3 -3
- package/docs/cli.mdx +1 -1
- package/docs/desktop.mdx +5 -1
- package/docs/environment.mdx +3 -7
- package/docs/getting-started.md +13 -11
- package/docs/human-control.md +1 -2
- package/docs/index.mdx +4 -3
- package/docs/install-manual.mdx +2 -2
- package/docs/packages/tauri.mdx +2 -2
- package/docs/packages/vite-plugin.mdx +22 -1
- package/docs/packages.mdx +1 -1
- package/docs/quickstart.mdx +17 -7
- package/docs/telemetry.md +1 -1
- package/docs/tools/session-and-feedback.mdx +9 -9
- package/docs/usage.md +5 -2
- package/docs/what-is-recorded.md +13 -3
- package/docs/what-is-reticle.mdx +3 -1
- package/package.json +8 -12
- package/dist/features/harness/driver.d.ts +0 -50
- package/dist/features/harness/driver.js +0 -236
- package/dist/features/harness/jev-driver.d.ts +0 -188
- package/dist/features/harness/jev-driver.js +0 -678
- package/dist/features/harness/openai-driver.d.ts +0 -58
- package/dist/features/harness/openai-driver.js +0 -206
- package/dist/memory/project/dir/fill-value-store.d.ts +0 -32
- package/dist/memory/project/dir/fill-value-store.js +0 -88
- package/docs/local-registry.md +0 -143
|
@@ -6,10 +6,13 @@
|
|
|
6
6
|
* needs no clock.
|
|
7
7
|
*/
|
|
8
8
|
import { asRunId, ReticleVerificationRunSchema, RUN_RETENTION, RUN_RETENTION_SLACK, RunReadError, } from '@reticlehq/core';
|
|
9
|
+
import { PromptContextSchema } from '@reticlehq/core/artifacts';
|
|
9
10
|
import { reticleDirPaths, isValidRunId, runPath } from '../../../memory/project/dir/reticle-dir.js';
|
|
10
11
|
const JSON_INDENT = 2;
|
|
11
12
|
const JSON_EXT = '.json';
|
|
12
13
|
const TMP_EXT = '.tmp';
|
|
14
|
+
/** A request older than this is about some other task, not the run being written now. */
|
|
15
|
+
const REQUEST_RELEVANT_MS = 6 * 60 * 60 * 1000;
|
|
13
16
|
export class RunStore {
|
|
14
17
|
#fs;
|
|
15
18
|
#root;
|
|
@@ -28,11 +31,15 @@ export class RunStore {
|
|
|
28
31
|
* Guards runId as a safe path segment BEFORE building the path — a runId can originate from a caller
|
|
29
32
|
* (an OEM may set it), so an unsafe value must never escape .reticle/runs/ (mirrors the read guard).
|
|
30
33
|
*/
|
|
31
|
-
async write(
|
|
34
|
+
async write(given) {
|
|
35
|
+
let run = given;
|
|
32
36
|
if (!isValidRunId(run.runId)) {
|
|
33
37
|
throw new Error(`refusing to write run with unsafe runId: ${JSON.stringify(run.runId)}`);
|
|
34
38
|
}
|
|
35
39
|
await this.#fs.mkdir(reticleDirPaths(this.#root).runs);
|
|
40
|
+
const context = run.context ?? (await this.#recentRequest(run.createdAt));
|
|
41
|
+
if (context !== undefined)
|
|
42
|
+
run = { ...run, context };
|
|
36
43
|
// Atomic publish: write a temp file then rename, so a crash mid-write never leaves a half-written
|
|
37
44
|
// artifact (a partial.json would otherwise read back as MALFORMED).
|
|
38
45
|
const path = runPath(this.#root, run.runId);
|
|
@@ -49,6 +56,24 @@ export class RunStore {
|
|
|
49
56
|
}
|
|
50
57
|
await this.#pruneOld();
|
|
51
58
|
}
|
|
59
|
+
/**
|
|
60
|
+
* The user's request this run answers, when the agent relayed one recently enough to be about it.
|
|
61
|
+
* Read from .reticle/intent/request.json (written by `reticle_intent { declare, request }`).
|
|
62
|
+
*/
|
|
63
|
+
async #recentRequest(at) {
|
|
64
|
+
try {
|
|
65
|
+
const parsed = PromptContextSchema.safeParse(JSON.parse(await this.#fs.readFile(reticleDirPaths(this.#root).request)));
|
|
66
|
+
if (!parsed.success)
|
|
67
|
+
return undefined;
|
|
68
|
+
const given = parsed.data.at;
|
|
69
|
+
return given !== undefined && 0 <= at - given && at - given <= REQUEST_RELEVANT_MS
|
|
70
|
+
? parsed.data
|
|
71
|
+
: undefined;
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
return undefined; // no request recorded: the common case
|
|
75
|
+
}
|
|
76
|
+
}
|
|
52
77
|
/**
|
|
53
78
|
* Keep .reticle/runs/ bounded. Only acts once the count exceeds RUN_RETENTION + SLACK, then deletes the
|
|
54
79
|
* oldest (by createdAt) back down to RUN_RETENTION — so the read-all is amortized, not per-write.
|
|
@@ -59,3 +59,25 @@ export declare function driveRunId(sessionId: string): string;
|
|
|
59
59
|
* artifact are the caller's job, which is what keeps the verdict rules testable without a disk.
|
|
60
60
|
*/
|
|
61
61
|
export declare function driveRunFrom(actions: readonly JournalAction[], deps: DriveRunDeps): VerificationRunInput | undefined;
|
|
62
|
+
/** What a Harness run says drove it, in the dashboard's "Driven by" line. */
|
|
63
|
+
export declare function harnessAgentId(driver: string): string;
|
|
64
|
+
/**
|
|
65
|
+
* The id of the run a Harness drive syncs as, so whoever started the drive can find it again (the
|
|
66
|
+
* platform's chat folds the run into the check it asked for). Undefined for an id no run can carry.
|
|
67
|
+
*/
|
|
68
|
+
export declare function harnessRunId(harness: string): string | undefined;
|
|
69
|
+
/**
|
|
70
|
+
* What the Harness judged of a drive's goal, noted the moment the drive finishes, before its
|
|
71
|
+
* session ends and is graded. A drive whose checks held but whose goal was not reached synced as
|
|
72
|
+
* "Proved" while the drive itself said it had failed. Unmet once is unmet: a later journey of the
|
|
73
|
+
* same drive reaching its goal does not undo it.
|
|
74
|
+
*/
|
|
75
|
+
export declare function noteHarnessGoal(harness: string, met: boolean | undefined): void;
|
|
76
|
+
/**
|
|
77
|
+
* Every run a session's journal holds: the connected agent's drive, and one per Harness drive.
|
|
78
|
+
*
|
|
79
|
+
* They used to be one. A Harness drive dispatches through the same tools in the same tab, so its
|
|
80
|
+
* verdicts folded into the agent's run, and the dashboard showed one blended row that credited the
|
|
81
|
+
* agent with what the Harness proved and named neither.
|
|
82
|
+
*/
|
|
83
|
+
export declare function driveRunsFrom(actions: readonly JournalAction[], deps: DriveRunDeps): VerificationRunInput[];
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
*/
|
|
25
25
|
import { isValidRunId } from '../../memory/project/dir/reticle-dir.js';
|
|
26
26
|
import { defaultRunId } from './runner-port.js';
|
|
27
|
-
import { JournalVerdictEffectSchema, RunAgentKind, RunFramework, RunProfile, RunTrigger, } from '@reticlehq/core';
|
|
27
|
+
import { JournalVerdictEffectSchema, RunAgentKind, RunFramework, RunProfile, RunTrigger, RunFlowStatus, Verified, } from '@reticlehq/core';
|
|
28
28
|
import { withCorrections } from './late-answer.js';
|
|
29
29
|
/** The author of record when no MCP peer introduced itself. Mirrors verification-sync's default. */
|
|
30
30
|
const UNNAMED_AGENT = 'reticle-mcp';
|
|
@@ -133,3 +133,101 @@ export function driveRunFrom(actions, deps) {
|
|
|
133
133
|
evidence: { consoleErrors: [], networkAnomalies: [], stateAssertions: [], timeline: [] },
|
|
134
134
|
};
|
|
135
135
|
}
|
|
136
|
+
/** Marks a run as one Harness drive, so it never collides with the session's own `drive-` run. */
|
|
137
|
+
const HARNESS_RUN_PREFIX = 'harness-';
|
|
138
|
+
/** What a Harness run says drove it, in the dashboard's "Driven by" line. */
|
|
139
|
+
export function harnessAgentId(driver) {
|
|
140
|
+
return 'server' === driver ? 'Reticle Harness (platform)' : `Reticle Harness (${driver})`;
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* The id of the run a Harness drive syncs as, so whoever started the drive can find it again (the
|
|
144
|
+
* platform's chat folds the run into the check it asked for). Undefined for an id no run can carry.
|
|
145
|
+
*/
|
|
146
|
+
export function harnessRunId(harness) {
|
|
147
|
+
const candidate = `${HARNESS_RUN_PREFIX}${harness}`;
|
|
148
|
+
return isValidRunId(candidate) ? candidate : undefined;
|
|
149
|
+
}
|
|
150
|
+
/** The journey a Harness drive was named for, or what it did when nobody named one. */
|
|
151
|
+
const UNNAMED_JOURNEY = 'Harness drive';
|
|
152
|
+
/** Goal verdicts kept for the drives a session has not yet been graded on. */
|
|
153
|
+
const MAX_GOALS_HELD = 200;
|
|
154
|
+
const goalsMissed = new Map();
|
|
155
|
+
/**
|
|
156
|
+
* What the Harness judged of a drive's goal, noted the moment the drive finishes, before its
|
|
157
|
+
* session ends and is graded. A drive whose checks held but whose goal was not reached synced as
|
|
158
|
+
* "Proved" while the drive itself said it had failed. Unmet once is unmet: a later journey of the
|
|
159
|
+
* same drive reaching its goal does not undo it.
|
|
160
|
+
*/
|
|
161
|
+
export function noteHarnessGoal(harness, met) {
|
|
162
|
+
if (met === undefined)
|
|
163
|
+
return;
|
|
164
|
+
const before = goalsMissed.get(harness);
|
|
165
|
+
if (false === before)
|
|
166
|
+
return;
|
|
167
|
+
if (MAX_GOALS_HELD <= goalsMissed.size && !goalsMissed.has(harness)) {
|
|
168
|
+
const oldest = goalsMissed.keys().next().value;
|
|
169
|
+
if (oldest !== undefined)
|
|
170
|
+
goalsMissed.delete(oldest);
|
|
171
|
+
}
|
|
172
|
+
goalsMissed.set(harness, met);
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* One Harness drive, as its own run: named for the journey it was asked to complete, and credited to
|
|
176
|
+
* the Harness rather than to the agent whose tab it shared. The journey reads as one flow, failed
|
|
177
|
+
* when any check it declared came back no, passed when one came back yes and none failed.
|
|
178
|
+
*/
|
|
179
|
+
function harnessRunFrom(by, actions, deps) {
|
|
180
|
+
const input = driveRunFrom(actions, {
|
|
181
|
+
...deps,
|
|
182
|
+
runId: harnessRunId(by.harness) ?? defaultRunId(),
|
|
183
|
+
});
|
|
184
|
+
if (input === undefined)
|
|
185
|
+
return undefined;
|
|
186
|
+
const statuses = input.checks.map((check) => check.status);
|
|
187
|
+
const status = statuses.includes(Verified.NO) || false === goalsMissed.get(by.harness)
|
|
188
|
+
? RunFlowStatus.FAIL
|
|
189
|
+
: statuses.includes(Verified.YES)
|
|
190
|
+
? RunFlowStatus.PASS
|
|
191
|
+
: RunFlowStatus.SKIPPED;
|
|
192
|
+
const first = actions.reduce((min, action) => Math.min(min, action.at), Number.MAX_SAFE_INTEGER);
|
|
193
|
+
return {
|
|
194
|
+
...input,
|
|
195
|
+
durationMs: Math.max(0, input.durationMs - first),
|
|
196
|
+
agent: { id: harnessAgentId(by.driver), kind: RunAgentKind.CODING_AGENT },
|
|
197
|
+
trigger: {
|
|
198
|
+
kind: RunTrigger.EDIT,
|
|
199
|
+
note: `driven by the Harness${by.persona === undefined ? '' : `: ${by.persona}`}`,
|
|
200
|
+
},
|
|
201
|
+
flows: [
|
|
202
|
+
{
|
|
203
|
+
name: by.persona ?? UNNAMED_JOURNEY,
|
|
204
|
+
status,
|
|
205
|
+
steps: actions.length,
|
|
206
|
+
durationMs: Math.max(0, input.durationMs - first),
|
|
207
|
+
},
|
|
208
|
+
],
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Every run a session's journal holds: the connected agent's drive, and one per Harness drive.
|
|
213
|
+
*
|
|
214
|
+
* They used to be one. A Harness drive dispatches through the same tools in the same tab, so its
|
|
215
|
+
* verdicts folded into the agent's run, and the dashboard showed one blended row that credited the
|
|
216
|
+
* agent with what the Harness proved and named neither.
|
|
217
|
+
*/
|
|
218
|
+
export function driveRunsFrom(actions, deps) {
|
|
219
|
+
const own = actions.filter((action) => action.drivenBy === undefined);
|
|
220
|
+
const drives = new Map();
|
|
221
|
+
for (const action of actions) {
|
|
222
|
+
const by = action.drivenBy;
|
|
223
|
+
if (by === undefined)
|
|
224
|
+
continue;
|
|
225
|
+
const held = drives.get(by.harness) ?? { by, actions: [] };
|
|
226
|
+
held.actions.push(action);
|
|
227
|
+
drives.set(by.harness, held);
|
|
228
|
+
}
|
|
229
|
+
return [
|
|
230
|
+
driveRunFrom(own, deps),
|
|
231
|
+
...[...drives.values()].map((drive) => harnessRunFrom(drive.by, drive.actions, deps)),
|
|
232
|
+
].filter((run) => run !== undefined);
|
|
233
|
+
}
|
|
@@ -46,7 +46,7 @@ function deviationHeadline(deviation) {
|
|
|
46
46
|
return undefined;
|
|
47
47
|
}
|
|
48
48
|
export function buildRepairPacket(replay) {
|
|
49
|
-
if (replay.status === ReplayStatus.OK)
|
|
49
|
+
if (replay.status === ReplayStatus.OK || replay.status === ReplayStatus.UNVERIFIABLE)
|
|
50
50
|
return undefined;
|
|
51
51
|
const decision = replay.decision;
|
|
52
52
|
const baseActual = decision?.whatChanged ?? replay.error?.message ?? 'the flow failed';
|
|
@@ -85,7 +85,14 @@ export class ReticleRunner {
|
|
|
85
85
|
flows,
|
|
86
86
|
checks: [],
|
|
87
87
|
risks,
|
|
88
|
-
|
|
88
|
+
// What the replay windows saw. An empty list written here regardless read as a clean
|
|
89
|
+
// console on a run whose steps had counted errors (#1344).
|
|
90
|
+
evidence: {
|
|
91
|
+
consoleErrors: replays.flatMap((r) => r.steps.flatMap((step) => step.consoleErrors ?? [])),
|
|
92
|
+
networkAnomalies: [],
|
|
93
|
+
stateAssertions: [],
|
|
94
|
+
timeline: [],
|
|
95
|
+
},
|
|
89
96
|
...(failurePackets.length > 0 ? { repair: { failurePackets } } : {}),
|
|
90
97
|
};
|
|
91
98
|
const run = buildVerificationRun(input, () => this.#port.now());
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { type
|
|
1
|
+
import { type FlowFile, type FlowReplayResult } from '@reticlehq/core';
|
|
2
|
+
import { type ProjectTarget } from '../../memory/project/session-root.js';
|
|
2
3
|
import type { ToolDeps } from '../../surface/tools/tool-kit.js';
|
|
3
4
|
/** A replay plus the wall-clock time it took — the shape the verify handler already collects. */
|
|
4
5
|
export interface TimedReplay {
|
|
@@ -12,4 +13,4 @@ export interface TimedReplay {
|
|
|
12
13
|
* tool — a disk or network failure is logged and swallowed so the verdict the agent asked for is
|
|
13
14
|
* unaffected. Returns the runId (for logging/tests); undefined if nothing was produced (empty suite).
|
|
14
15
|
*/
|
|
15
|
-
export declare function persistAndSyncVerificationRun(deps: ToolDeps, timed: TimedReplay[],
|
|
16
|
+
export declare function persistAndSyncVerificationRun(deps: ToolDeps, timed: TimedReplay[], target: ProjectTarget): Promise<string | undefined>;
|
|
@@ -18,7 +18,7 @@ import { RunStore } from './artifact/run-store.js';
|
|
|
18
18
|
import { cloudFetch, syncRunToCloud, SyncOutcome } from '../../memory/cloud/cloud-sync.js';
|
|
19
19
|
import { resolveProjectCloud } from '../../memory/cloud/cloud-config.js';
|
|
20
20
|
import { log } from '../../log.js';
|
|
21
|
-
import {
|
|
21
|
+
import { rootForTarget } from '../../memory/project/session-root.js';
|
|
22
22
|
/** The author of record when no MCP peer introduced itself — a CLI run, or a client that skipped the handshake. */
|
|
23
23
|
const RETICLE_MCP_AGENT_ID = 'reticle-mcp';
|
|
24
24
|
/** Assemble the ReticleVerificationRun artifact for a verify suite (pure — no IO). */
|
|
@@ -57,7 +57,8 @@ function assembleRun(deps, timed, projectId) {
|
|
|
57
57
|
* tool — a disk or network failure is logged and swallowed so the verdict the agent asked for is
|
|
58
58
|
* unaffected. Returns the runId (for logging/tests); undefined if nothing was produced (empty suite).
|
|
59
59
|
*/
|
|
60
|
-
export async function persistAndSyncVerificationRun(deps, timed,
|
|
60
|
+
export async function persistAndSyncVerificationRun(deps, timed, target) {
|
|
61
|
+
const { projectId } = target;
|
|
61
62
|
if (0 === timed.length)
|
|
62
63
|
return undefined;
|
|
63
64
|
let run;
|
|
@@ -66,7 +67,7 @@ export async function persistAndSyncVerificationRun(deps, timed, projectId) {
|
|
|
66
67
|
// decides WHICH DASHBOARD this run is pushed to. The daemon's own directory has no link file, so
|
|
67
68
|
// a misrouted read silently meant "not attached" — and where it DID have one, the run was pushed
|
|
68
69
|
// to a dashboard belonging to whoever that checkout was linked to.
|
|
69
|
-
const root =
|
|
70
|
+
const root = rootForTarget(deps, target);
|
|
70
71
|
try {
|
|
71
72
|
run = assembleRun(deps, timed, projectId);
|
|
72
73
|
await new RunStore(deps.fs, root).write(run);
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* unexplained flake that blocks a merge destroys trust in the red. Pure decision; the CLI computes the
|
|
6
6
|
* inputs (git diff → affected → run artifacts + flake ledger) and maps `pass` to the exit code.
|
|
7
7
|
*/
|
|
8
|
+
import { type RunFlowResult } from '@reticlehq/core';
|
|
8
9
|
/** A flow whose assertions were WEAKENED since its last passing run. */
|
|
9
10
|
interface DowngradedFlow {
|
|
10
11
|
flow: string;
|
|
@@ -56,4 +57,13 @@ interface GateResult {
|
|
|
56
57
|
unexecuted: string[];
|
|
57
58
|
}
|
|
58
59
|
export declare function gateDecision(input: GateInput): GateResult;
|
|
60
|
+
/**
|
|
61
|
+
* The flows a run lets the gate count as passing.
|
|
62
|
+
*
|
|
63
|
+
* Every PASS or HEALED flow by its own name, as before. A generated copy of a template also credits
|
|
64
|
+
* the template, but only on a STRICT pass that proved a consequence (#1321): a suite replays runtime
|
|
65
|
+
* copies with fixture parameters and deletes them, so their own names match no saved flow. A healed
|
|
66
|
+
* anchor, a failed or unverifiable replay, and a pass that checked nothing credit nothing.
|
|
67
|
+
*/
|
|
68
|
+
export declare function passingFlowNames(flows: readonly RunFlowResult[]): string[];
|
|
59
69
|
export {};
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* unexplained flake that blocks a merge destroys trust in the red. Pure decision; the CLI computes the
|
|
6
6
|
* inputs (git diff → affected → run artifacts + flake ledger) and maps `pass` to the exit code.
|
|
7
7
|
*/
|
|
8
|
+
import { RunFlowStatus } from '@reticlehq/core';
|
|
8
9
|
export function gateDecision(input) {
|
|
9
10
|
const passing = new Set(input.passing);
|
|
10
11
|
const flaky = new Set(input.flaky ?? []);
|
|
@@ -42,3 +43,24 @@ export function gateDecision(input) {
|
|
|
42
43
|
unexecuted,
|
|
43
44
|
};
|
|
44
45
|
}
|
|
46
|
+
/**
|
|
47
|
+
* The flows a run lets the gate count as passing.
|
|
48
|
+
*
|
|
49
|
+
* Every PASS or HEALED flow by its own name, as before. A generated copy of a template also credits
|
|
50
|
+
* the template, but only on a STRICT pass that proved a consequence (#1321): a suite replays runtime
|
|
51
|
+
* copies with fixture parameters and deletes them, so their own names match no saved flow. A healed
|
|
52
|
+
* anchor, a failed or unverifiable replay, and a pass that checked nothing credit nothing.
|
|
53
|
+
*/
|
|
54
|
+
export function passingFlowNames(flows) {
|
|
55
|
+
const names = new Set();
|
|
56
|
+
for (const f of flows) {
|
|
57
|
+
if (f.status !== RunFlowStatus.PASS && f.status !== RunFlowStatus.HEALED)
|
|
58
|
+
continue;
|
|
59
|
+
names.add(f.name);
|
|
60
|
+
const proved = f.oracle !== undefined ||
|
|
61
|
+
(f.stepResults ?? []).some((s) => s.ok && s.consequence !== undefined);
|
|
62
|
+
if (f.template !== undefined && f.status === RunFlowStatus.PASS && proved)
|
|
63
|
+
names.add(f.template);
|
|
64
|
+
}
|
|
65
|
+
return [...names];
|
|
66
|
+
}
|
|
@@ -123,6 +123,35 @@ invoked = new Map()) {
|
|
|
123
123
|
: 'the action ran and the expected consequence never appeared — check the handler behind it, or whether this flow needs a state it did not start in. The locator is not the problem.',
|
|
124
124
|
};
|
|
125
125
|
}
|
|
126
|
+
/*
|
|
127
|
+
* An anchor that matched SEVERAL elements is not an element that is gone, and the advice below
|
|
128
|
+
* says it is.
|
|
129
|
+
*
|
|
130
|
+
* `ANCHOR_AMBIGUOUS` carries `nearest: null` and `ambiguous: true`, so it fell through to the
|
|
131
|
+
* last line of this branch — "the anchored element is gone; rebind or update the flow" — for a
|
|
132
|
+
* locator that resolved several times over and lost nothing. Rebind TO WHAT is the question the
|
|
133
|
+
* drift just refused to answer; sending the reader looking for a replacement locator is the
|
|
134
|
+
* opposite of the fix, which is a NARROWER anchor on the one they already have.
|
|
135
|
+
*
|
|
136
|
+
* The same sentence was already wrong for a testid ambiguity (it is why `ambiguous: true` exists
|
|
137
|
+
* on that drift at all), and the role and component runners make it reachable for two more
|
|
138
|
+
* anchor kinds. The distinction this branch has to keep is the one it was written for: a
|
|
139
|
+
* CONSEQUENCE that never appeared has no anchor problem, an AMBIGUOUS anchor has nothing else.
|
|
140
|
+
*/
|
|
141
|
+
if (DriftReason.ANCHOR_AMBIGUOUS === drift.reasonKind) {
|
|
142
|
+
return {
|
|
143
|
+
verdict: 'drift',
|
|
144
|
+
summary: withIntent(`"${name}" drifted at step ${step.step} (${step.anchor}) — the anchor is ambiguous.`, intentSaid),
|
|
145
|
+
whatChanged: drift.reason,
|
|
146
|
+
// The step's OWN anchor is the thing at fault here, so its source file is the right place to
|
|
147
|
+
// look — unlike the consequence case above, where the recorded source belongs to the element
|
|
148
|
+
// that was acted on and the anchor is not what failed.
|
|
149
|
+
...(where !== undefined ? { whereInSource: where } : {}),
|
|
150
|
+
nextAction: 'the anchor matched several elements, so which one the recording meant is not decidable. ' +
|
|
151
|
+
'Narrow the anchor on this step — a unique testid, or a role+name that only one control ' +
|
|
152
|
+
'has — rather than rebinding it; nothing here says the element was renamed or removed.',
|
|
153
|
+
};
|
|
154
|
+
}
|
|
126
155
|
const fix = drift.nearest !== null && drift.ambiguous !== true
|
|
127
156
|
? `rebind the anchor to "${drift.nearest}" (closest survivor)`
|
|
128
157
|
: drift.nearest !== null
|
|
@@ -207,6 +236,11 @@ isolation) {
|
|
|
207
236
|
const unverifiable = [];
|
|
208
237
|
let passed = 0;
|
|
209
238
|
for (const { replay, flow } of runs) {
|
|
239
|
+
if (replay.status === ReplayStatus.UNVERIFIABLE) {
|
|
240
|
+
const reason = replay.unverifiable?.reason ?? unverifiableReason(flow) ?? 'the flow could not be graded';
|
|
241
|
+
unverifiable.push({ flow: replay.name, reason });
|
|
242
|
+
continue;
|
|
243
|
+
}
|
|
210
244
|
if (replay.status === ReplayStatus.OK) {
|
|
211
245
|
/*
|
|
212
246
|
* The REPLAY's own answer first, then the grader's.
|
|
@@ -29,17 +29,63 @@ export declare function nearestIsAmbiguous(missing: string, present: string[]):
|
|
|
29
29
|
/** Build the legible-drift record for a testid anchor that resolved to zero live elements. */
|
|
30
30
|
export declare function testidDrift(value: string, hint: QueryEmptyHint | undefined): Drift;
|
|
31
31
|
/**
|
|
32
|
-
*
|
|
32
|
+
* What the stale-ref retry found when it ran the locator again.
|
|
33
33
|
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
34
|
+
* A discriminated result rather than the bare `string | undefined` this started as, because the two
|
|
35
|
+
* ways of getting no ref back are different answers and were arriving as one. `{ drift }` says the
|
|
36
|
+
* locator now names SEVERAL elements — the re-render turned an unambiguous anchor into an ambiguous
|
|
37
|
+
* one, and the fix is a NARROWER anchor. `undefined` says it named none, or named one and the second
|
|
38
|
+
* dispatch failed too; both of those are already told truthfully by the stale-ref error the first
|
|
39
|
+
* attempt produced, so there is nothing to add.
|
|
37
40
|
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
+
* Collapsed together, the ambiguous case was reported as "ref 'e-orig' no longer resolves to an
|
|
42
|
+
* element" — a sentence about an element that is GONE, sent for an element that is now three. An
|
|
43
|
+
* agent reading it hunts for a rename or re-records the flow; the truth is that the anchor needs
|
|
44
|
+
* narrowing, which is the whole reason `ANCHOR_AMBIGUOUS` exists as its own reason kind.
|
|
41
45
|
*/
|
|
42
|
-
export
|
|
46
|
+
export type Reresolved = {
|
|
47
|
+
readonly ref: string;
|
|
48
|
+
} | {
|
|
49
|
+
readonly drift: Drift;
|
|
50
|
+
};
|
|
51
|
+
/**
|
|
52
|
+
* The ambiguity rule itself: an anchor that matched several live elements is DRIFT, never a guess.
|
|
53
|
+
*
|
|
54
|
+
* Returns null when the locator named zero or one element, so a runner reads it as
|
|
55
|
+
* `const drift = ambiguityDrift(anchor, refs); if (drift !== null) return { …, ok: false, drift };`
|
|
56
|
+
* and never has to reconstruct the record itself.
|
|
57
|
+
*
|
|
58
|
+
* ONE function, because three anchor kinds ask the same question and each used to answer for itself.
|
|
59
|
+
* testid was fixed first and role second, and role's fix was written as a copy of testid's — so "is
|
|
60
|
+
* this ambiguous" had two answers and COMPONENT had none at all. `runComponentStep` took `refs[0]`,
|
|
61
|
+
* dispatched and returned `ok: true` whenever `by:'component'` matched several elements, which is the
|
|
62
|
+
* ordinary case for a component rendered once per row (a table row's action button). The verdict
|
|
63
|
+
* reads `drift` and `ok` and nothing else, so that green was indistinguishable from a replay that did
|
|
64
|
+
* what it did before — the #1227 defect, one anchor kind over.
|
|
65
|
+
*
|
|
66
|
+
* The caller must not dispatch before checking. Acting and then reporting drift would leave the app
|
|
67
|
+
* changed by a click nobody can attribute, which is worse than the ambiguity it reports.
|
|
68
|
+
*/
|
|
69
|
+
export declare function ambiguityDrift(anchor: FlowAnchor, refs: readonly string[]): Drift | null;
|
|
70
|
+
/**
|
|
71
|
+
* The one ref a step means: the only match, or the recorded position among same-named controls
|
|
72
|
+
* while the page still has exactly as many of them as when it was recorded. Otherwise undefined.
|
|
73
|
+
*/
|
|
74
|
+
export declare function refFor(anchor: object, refs: readonly string[]): string | undefined;
|
|
75
|
+
/**
|
|
76
|
+
* The anchor as the stale-ref RETRY found it: one ref to dispatch at, an ambiguity to report, or
|
|
77
|
+
* neither.
|
|
78
|
+
*
|
|
79
|
+
* The same rule as `ambiguityDrift`, one layer deeper, and shared for the same reason: a re-render
|
|
80
|
+
* can turn an unambiguous anchor into an ambiguous one between the two dispatches, and dispatching
|
|
81
|
+
* at the first of the new matches would re-make the guess the caller's own check exists to refuse —
|
|
82
|
+
* in a place the caller cannot see. Written once so the three runners cannot answer it differently,
|
|
83
|
+
* which is how the component runner came to have no answer at all.
|
|
84
|
+
*
|
|
85
|
+
* `refs[0]` is read only after `ambiguityDrift` has cleared the several-match case, so the only way
|
|
86
|
+
* it is undefined here is zero matches.
|
|
87
|
+
*/
|
|
88
|
+
export declare function reresolved(anchor: FlowAnchor, refs: readonly string[]): Reresolved | undefined;
|
|
43
89
|
/**
|
|
44
90
|
* Build the drift for a step whose `expect.element` testid was absent after the action ran.
|
|
45
91
|
*
|
|
@@ -139,6 +139,34 @@ export function testidDrift(value, hint) {
|
|
|
139
139
|
drift.ambiguous = true;
|
|
140
140
|
return drift;
|
|
141
141
|
}
|
|
142
|
+
/**
|
|
143
|
+
* How an anchor is ADDRESSED — the sentence that names it, and the label a result row carries.
|
|
144
|
+
*
|
|
145
|
+
* Two different strings, and neither can be derived from the other: `value` is a raw testid for one
|
|
146
|
+
* anchor kind and a human-readable label for the others, so one sentence cannot name both. Calling a
|
|
147
|
+
* role anchor `testid "button "View result""` sends the reader to edit a locator that does not exist
|
|
148
|
+
* on that step, which is the same misdirection the drift reason exists to remove.
|
|
149
|
+
*
|
|
150
|
+
* Derived from the anchor rather than passed in by the caller. The first version of the role fix took
|
|
151
|
+
* a `described` argument and wrote `ambiguousAnchorDrift(label, n, \`role anchor ${label}\`)` — the
|
|
152
|
+
* same string twice, so the `anchor` field and the sentence could drift apart with nothing to catch
|
|
153
|
+
* it, and the next anchor kind would have had to remember the convention on its own.
|
|
154
|
+
*/
|
|
155
|
+
function anchorAddress(anchor) {
|
|
156
|
+
if (anchor.kind === AnchorKind.TESTID) {
|
|
157
|
+
return { described: `testid "${anchor.value}"`, label: anchor.value };
|
|
158
|
+
}
|
|
159
|
+
if (anchor.kind === AnchorKind.ROLE) {
|
|
160
|
+
// Kind named explicitly: `button "View result"` alone reads as a testid that happens to contain
|
|
161
|
+
// a space, and the reader would go looking for a locator nothing on the step declares.
|
|
162
|
+
const label = `${anchor.role} "${String(anchor.name)}"`;
|
|
163
|
+
return { described: `role anchor ${label}`, label };
|
|
164
|
+
}
|
|
165
|
+
// A component needs no such qualifier: `componentLabel` already answers
|
|
166
|
+
// `NewDeployButton@Deployments.tsx:107`, which no reader mistakes for a testid.
|
|
167
|
+
const label = anchor.kind === AnchorKind.COMPONENT ? componentLabel(anchor) : anchorLabel(anchor);
|
|
168
|
+
return { described: `anchor ${label}`, label };
|
|
169
|
+
}
|
|
142
170
|
/**
|
|
143
171
|
* Build the drift for a step whose anchor matched MORE THAN ONE live element.
|
|
144
172
|
*
|
|
@@ -149,16 +177,73 @@ export function testidDrift(value, hint) {
|
|
|
149
177
|
* `nearest` is null on purpose. There is no nearest: the anchor was found, several times over, and
|
|
150
178
|
* proposing one of the matches as "the" match would re-make the guess this reason exists to stop.
|
|
151
179
|
* The fix is a NARROWER anchor, and only a human or an agent looking at the page can choose it.
|
|
180
|
+
*
|
|
181
|
+
* Private: callers reach it through `ambiguityDrift`, which is the RULE rather than the record.
|
|
182
|
+
* Exported, it had three call sites and a fourth anchor kind that never called it at all.
|
|
152
183
|
*/
|
|
153
|
-
|
|
184
|
+
function ambiguousAnchorDrift(anchor, matches) {
|
|
185
|
+
const { described, label } = anchorAddress(anchor);
|
|
154
186
|
return {
|
|
155
187
|
reasonKind: DriftReason.ANCHOR_AMBIGUOUS,
|
|
156
|
-
reason:
|
|
157
|
-
anchor:
|
|
188
|
+
reason: `${described} matched ${String(matches)} live elements — which one the recording meant is not decidable; anchor this step to something unique`,
|
|
189
|
+
anchor: label,
|
|
158
190
|
nearest: null,
|
|
159
191
|
ambiguous: true,
|
|
160
192
|
};
|
|
161
193
|
}
|
|
194
|
+
/**
|
|
195
|
+
* The ambiguity rule itself: an anchor that matched several live elements is DRIFT, never a guess.
|
|
196
|
+
*
|
|
197
|
+
* Returns null when the locator named zero or one element, so a runner reads it as
|
|
198
|
+
* `const drift = ambiguityDrift(anchor, refs); if (drift !== null) return { …, ok: false, drift };`
|
|
199
|
+
* and never has to reconstruct the record itself.
|
|
200
|
+
*
|
|
201
|
+
* ONE function, because three anchor kinds ask the same question and each used to answer for itself.
|
|
202
|
+
* testid was fixed first and role second, and role's fix was written as a copy of testid's — so "is
|
|
203
|
+
* this ambiguous" had two answers and COMPONENT had none at all. `runComponentStep` took `refs[0]`,
|
|
204
|
+
* dispatched and returned `ok: true` whenever `by:'component'` matched several elements, which is the
|
|
205
|
+
* ordinary case for a component rendered once per row (a table row's action button). The verdict
|
|
206
|
+
* reads `drift` and `ok` and nothing else, so that green was indistinguishable from a replay that did
|
|
207
|
+
* what it did before — the #1227 defect, one anchor kind over.
|
|
208
|
+
*
|
|
209
|
+
* The caller must not dispatch before checking. Acting and then reporting drift would leave the app
|
|
210
|
+
* changed by a click nobody can attribute, which is worse than the ambiguity it reports.
|
|
211
|
+
*/
|
|
212
|
+
export function ambiguityDrift(anchor, refs) {
|
|
213
|
+
return refs.length > 1 && refFor(anchor, refs) === undefined
|
|
214
|
+
? ambiguousAnchorDrift(anchor, refs.length)
|
|
215
|
+
: null;
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* The one ref a step means: the only match, or the recorded position among same-named controls
|
|
219
|
+
* while the page still has exactly as many of them as when it was recorded. Otherwise undefined.
|
|
220
|
+
*/
|
|
221
|
+
export function refFor(anchor, refs) {
|
|
222
|
+
if (1 === refs.length)
|
|
223
|
+
return refs[0];
|
|
224
|
+
const { nth, of } = { ...anchor };
|
|
225
|
+
return 'number' === typeof nth && of === refs.length ? refs[nth] : undefined;
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* The anchor as the stale-ref RETRY found it: one ref to dispatch at, an ambiguity to report, or
|
|
229
|
+
* neither.
|
|
230
|
+
*
|
|
231
|
+
* The same rule as `ambiguityDrift`, one layer deeper, and shared for the same reason: a re-render
|
|
232
|
+
* can turn an unambiguous anchor into an ambiguous one between the two dispatches, and dispatching
|
|
233
|
+
* at the first of the new matches would re-make the guess the caller's own check exists to refuse —
|
|
234
|
+
* in a place the caller cannot see. Written once so the three runners cannot answer it differently,
|
|
235
|
+
* which is how the component runner came to have no answer at all.
|
|
236
|
+
*
|
|
237
|
+
* `refs[0]` is read only after `ambiguityDrift` has cleared the several-match case, so the only way
|
|
238
|
+
* it is undefined here is zero matches.
|
|
239
|
+
*/
|
|
240
|
+
export function reresolved(anchor, refs) {
|
|
241
|
+
const drift = ambiguityDrift(anchor, refs);
|
|
242
|
+
if (drift !== null)
|
|
243
|
+
return { drift };
|
|
244
|
+
const ref = refFor(anchor, refs);
|
|
245
|
+
return ref === undefined ? undefined : { ref };
|
|
246
|
+
}
|
|
162
247
|
/**
|
|
163
248
|
* Build the drift for a step whose `expect.element` testid was absent after the action ran.
|
|
164
249
|
*
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { Contradiction, FlowStepResult } from '@reticlehq/core';
|
|
2
|
+
/** Cross-step findings belong to no single step; -1 is the address the suite verdict already uses. */
|
|
3
|
+
export declare const CROSS_STEP_INDEX = -1;
|
|
4
|
+
/**
|
|
5
|
+
* The contradictions the whole-span pass found that no individual step could.
|
|
6
|
+
*
|
|
7
|
+
* A step's window closes when the step ends, so a request fired at step 2 and still unanswered at
|
|
8
|
+
* step 5 is invisible to every per-step window: step 2's closed before the answer came and step 5
|
|
9
|
+
* never saw it start. Re-running the detectors over the whole replay span finds those — and re-finds
|
|
10
|
+
* everything the steps already reported, which is what the subtraction is for. Reporting a finding
|
|
11
|
+
* twice teaches a reader that the count is noise.
|
|
12
|
+
*
|
|
13
|
+
* Exported for its own test: the subtraction is the whole rule, and it is pure.
|
|
14
|
+
*/
|
|
15
|
+
export declare function crossStepOnly(whole: readonly Contradiction[], steps: readonly FlowStepResult[]): Contradiction[];
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/** A contradiction's identity for de-duplication: the rule that fired, and the evidence it fired on. */
|
|
2
|
+
function contradictionId(found) {
|
|
3
|
+
return `${found.kind}|${found.detail}`;
|
|
4
|
+
}
|
|
5
|
+
/** Cross-step findings belong to no single step; -1 is the address the suite verdict already uses. */
|
|
6
|
+
export const CROSS_STEP_INDEX = -1;
|
|
7
|
+
/**
|
|
8
|
+
* The contradictions the whole-span pass found that no individual step could.
|
|
9
|
+
*
|
|
10
|
+
* A step's window closes when the step ends, so a request fired at step 2 and still unanswered at
|
|
11
|
+
* step 5 is invisible to every per-step window: step 2's closed before the answer came and step 5
|
|
12
|
+
* never saw it start. Re-running the detectors over the whole replay span finds those — and re-finds
|
|
13
|
+
* everything the steps already reported, which is what the subtraction is for. Reporting a finding
|
|
14
|
+
* twice teaches a reader that the count is noise.
|
|
15
|
+
*
|
|
16
|
+
* Exported for its own test: the subtraction is the whole rule, and it is pure.
|
|
17
|
+
*/
|
|
18
|
+
export function crossStepOnly(whole, steps) {
|
|
19
|
+
const seen = new Set(steps.flatMap((step) => (step.contradictions ?? []).map(contradictionId)));
|
|
20
|
+
return whole.filter((found) => !seen.has(contradictionId(found)));
|
|
21
|
+
}
|
|
@@ -1,15 +1,11 @@
|
|
|
1
1
|
import { asString } from '@reticlehq/core';
|
|
2
2
|
import { flowsForSession } from './flow-store-for-session.js';
|
|
3
|
+
import { safeSessionTarget } from '../../memory/project/session-root.js';
|
|
3
4
|
import { replayNamedFlow } from './flow-replay-run.js';
|
|
4
5
|
import { recordingSources, withLearnedSources } from './learned-sources.js';
|
|
5
6
|
import { ReplayStatus } from '@reticlehq/core';
|
|
6
|
-
function
|
|
7
|
-
|
|
8
|
-
return deps.sessions.resolve(asString(args['sessionId'])).projectId;
|
|
9
|
-
}
|
|
10
|
-
catch {
|
|
11
|
-
return undefined;
|
|
12
|
-
}
|
|
7
|
+
function targetOf(deps, args) {
|
|
8
|
+
return safeSessionTarget(deps, asString(args['sessionId']));
|
|
13
9
|
}
|
|
14
10
|
/**
|
|
15
11
|
* Write back what a replay taught the flow, once the replay has finished writing its own.
|
|
@@ -36,8 +32,8 @@ export async function persistLearning(deps, args, result) {
|
|
|
36
32
|
if (0 === name.length)
|
|
37
33
|
return result;
|
|
38
34
|
try {
|
|
39
|
-
const
|
|
40
|
-
await flowsForSession(deps,
|
|
35
|
+
const target = targetOf(deps, args);
|
|
36
|
+
await flowsForSession(deps, target).flows.recordLearned(name, learned, target.projectId);
|
|
41
37
|
}
|
|
42
38
|
catch {
|
|
43
39
|
// Bookkeeping only.
|
|
@@ -93,8 +89,9 @@ export async function persistSources(deps, args, sources) {
|
|
|
93
89
|
if (0 === name.length)
|
|
94
90
|
return;
|
|
95
91
|
try {
|
|
96
|
-
const
|
|
97
|
-
const
|
|
92
|
+
const target = targetOf(deps, args);
|
|
93
|
+
const { projectId } = target;
|
|
94
|
+
const store = flowsForSession(deps, target).flows;
|
|
98
95
|
const current = await store.load(name, projectId);
|
|
99
96
|
// Read first so a flow that already names every file is never rewritten.
|
|
100
97
|
if (!current.ok || withLearnedSources(current.value, sources) === undefined)
|