@reticlehq/server 2.8.0 → 2.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/SKILL.md +130 -813
- package/dist/bridge/bridge.d.ts +23 -0
- package/dist/bridge/bridge.js +80 -2
- package/dist/bridge/bridge.test-harness.d.ts +5 -0
- package/dist/bridge/bridge.test-harness.js +6 -4
- package/dist/capsule/causal-summary.d.ts +16 -1
- package/dist/capsule/causal-summary.js +2 -1
- package/dist/cli/cli-doctor.js +41 -13
- package/dist/cli/cli-flow-commands.js +8 -8
- package/dist/cli/cli-launch.d.ts +0 -17
- package/dist/cli/cli-launch.js +0 -28
- package/dist/cli/cli-port.d.ts +24 -0
- package/dist/cli/cli-port.js +66 -35
- package/dist/cli/cli-verify.d.ts +21 -0
- package/dist/cli/cli-verify.js +45 -1
- package/dist/cli/config-discovery.d.ts +35 -0
- package/dist/cli/config-discovery.js +163 -0
- package/dist/cli/doctor-daemon-line.js +4 -1
- package/dist/cli/doctor-project-line.d.ts +37 -0
- package/dist/cli/doctor-project-line.js +35 -0
- package/dist/cli/doctor-rows.d.ts +51 -0
- package/dist/cli/doctor-rows.js +54 -0
- package/dist/cli/doctor-sessions-line.js +6 -3
- package/dist/cli/drive-attach.d.ts +99 -0
- package/dist/cli/drive-attach.js +203 -0
- package/dist/cli/drive-command.d.ts +13 -0
- package/dist/cli/drive-command.js +103 -0
- package/dist/cli/open-note.d.ts +20 -0
- package/dist/cli/open-note.js +37 -0
- package/dist/cli/status-next-action.d.ts +37 -0
- package/dist/cli/status-next-action.js +61 -0
- package/dist/cli.js +65 -70
- package/dist/events/contradictions.d.ts +32 -0
- package/dist/events/contradictions.js +9 -5
- package/dist/events/declared.d.ts +45 -0
- package/dist/events/declared.js +73 -0
- package/dist/events/echo-mismatch.d.ts +8 -2
- package/dist/events/echo-mismatch.js +33 -5
- package/dist/events/event-filters.d.ts +1 -1
- package/dist/events/event-filters.js +7 -4
- package/dist/events/predicate-eval.d.ts +17 -91
- package/dist/events/predicate-eval.js +85 -326
- package/dist/events/predicate-parse.js +31 -3
- package/dist/events/predicate-route.d.ts +32 -0
- package/dist/events/predicate-route.js +102 -0
- package/dist/events/predicate-schema.d.ts +99 -0
- package/dist/events/predicate-schema.js +309 -0
- package/dist/events/predicate.d.ts +8 -0
- package/dist/events/predicate.js +127 -5
- package/dist/flows/annotate-tools.js +3 -0
- package/dist/flows/annotate.js +18 -4
- package/dist/flows/coverage.d.ts +38 -5
- package/dist/flows/coverage.js +30 -4
- package/dist/flows/decision.d.ts +15 -0
- package/dist/flows/decision.js +1 -1
- package/dist/flows/flow-replay-run.js +8 -1
- package/dist/flows/flow-tools.js +9 -1
- package/dist/flows/replay.js +3 -0
- package/dist/honesty/accepted-write.d.ts +3 -0
- package/dist/honesty/accepted-write.js +8 -3
- package/dist/honesty/already-true.js +15 -0
- package/dist/honesty/blind-spots.js +4 -0
- package/dist/honesty/honesty.d.ts +11 -0
- package/dist/honesty/honesty.js +1 -0
- package/dist/honesty/uncaptured-bodies.d.ts +4 -2
- package/dist/honesty/uncaptured-bodies.js +9 -2
- package/dist/honesty/unread-outcome.d.ts +8 -0
- package/dist/honesty/unread-outcome.js +36 -9
- package/dist/honesty/unsettled.d.ts +9 -0
- package/dist/honesty/unsettled.js +7 -3
- package/dist/honesty/verified.d.ts +29 -3
- package/dist/honesty/verified.js +51 -8
- package/dist/http-server.d.ts +19 -0
- package/dist/http-server.js +202 -4
- package/dist/impact/impact-recorder.d.ts +23 -0
- package/dist/impact/impact-recorder.js +76 -0
- package/dist/impact/impact-store.d.ts +47 -0
- package/dist/impact/impact-store.js +171 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +44 -10
- package/dist/init/agent-rules.d.ts +20 -0
- package/dist/init/agent-rules.js +36 -0
- package/dist/init/astro-patch.js +27 -1
- package/dist/init/confirm.d.ts +21 -3
- package/dist/init/confirm.js +32 -5
- package/dist/init/connect-steps.d.ts +2 -0
- package/dist/init/connect-steps.js +50 -0
- package/dist/init/csp-check.d.ts +38 -0
- package/dist/init/csp-check.js +92 -0
- package/dist/init/csp-doctor.d.ts +25 -0
- package/dist/init/csp-doctor.js +49 -0
- package/dist/init/cursor.js +6 -9
- package/dist/init/detect.d.ts +8 -0
- package/dist/init/detect.js +33 -3
- package/dist/init/mcp-clients.d.ts +1 -0
- package/dist/init/mcp-clients.js +51 -7
- package/dist/init/mcp.d.ts +15 -0
- package/dist/init/mcp.js +19 -0
- package/dist/init/plan-framework.d.ts +10 -0
- package/dist/init/plan-framework.js +25 -1
- package/dist/init/plan.d.ts +7 -2
- package/dist/init/plan.js +66 -30
- package/dist/init/run.js +11 -5
- package/dist/init/slash-command.d.ts +1 -1
- package/dist/init/slash-command.js +12 -4
- package/dist/init/snippets.d.ts +1 -1
- package/dist/init/snippets.js +35 -7
- package/dist/init/vite-config.js +24 -2
- package/dist/input/network-mock-tools.js +8 -1
- package/dist/input/scroll-find.d.ts +6 -0
- package/dist/input/scroll-find.js +17 -2
- package/dist/input/scroll-tools.js +4 -0
- package/dist/journal/attach-journal.d.ts +9 -1
- package/dist/journal/attach-journal.js +13 -2
- package/dist/journal/journal-query.d.ts +13 -0
- package/dist/journal/journal-query.js +28 -3
- package/dist/journal/workspace-gitignore.d.ts +10 -0
- package/dist/journal/workspace-gitignore.js +59 -0
- package/dist/mcp/mcp-proxy.d.ts +5 -66
- package/dist/mcp/mcp-proxy.js +180 -163
- package/dist/mcp/mcp.d.ts +10 -1
- package/dist/mcp/mcp.js +47 -27
- package/dist/mcp/proxy-handshake.d.ts +82 -1
- package/dist/mcp/proxy-handshake.js +109 -2
- package/dist/mcp/proxy-lifecycle.d.ts +10 -1
- package/dist/mcp/proxy-lifecycle.js +11 -2
- package/dist/mcp/proxy-log.d.ts +63 -0
- package/dist/mcp/proxy-log.js +132 -0
- package/dist/mcp/server-instructions.d.ts +17 -0
- package/dist/mcp/server-instructions.js +56 -0
- package/dist/mcp/shared-params.d.ts +27 -0
- package/dist/mcp/shared-params.js +36 -0
- package/dist/project/reticle-dir.d.ts +2 -0
- package/dist/project/reticle-dir.js +1 -0
- package/dist/session/connection-memory.d.ts +59 -0
- package/dist/session/connection-memory.js +108 -0
- package/dist/session/dev-command.d.ts +26 -0
- package/dist/session/dev-command.js +89 -0
- package/dist/session/dev-server-probe.d.ts +68 -0
- package/dist/session/dev-server-probe.js +102 -7
- package/dist/session/last-act.d.ts +26 -0
- package/dist/session/last-act.js +35 -1
- package/dist/session/live-control-tools.js +42 -5
- package/dist/session/no-session-diagnosis.d.ts +48 -2
- package/dist/session/no-session-diagnosis.js +176 -20
- package/dist/session/no-session-next-action.d.ts +46 -0
- package/dist/session/no-session-next-action.js +85 -0
- package/dist/session/no-session-watch.d.ts +40 -1
- package/dist/session/no-session-watch.js +165 -11
- package/dist/session/pending-commands.d.ts +12 -0
- package/dist/session/pending-commands.js +13 -1
- package/dist/session/session-manager.d.ts +4 -0
- package/dist/session/session-manager.js +35 -5
- package/dist/session/session.d.ts +10 -1
- package/dist/session/session.js +146 -5
- package/dist/telemetry/app-instrumented.d.ts +8 -0
- package/dist/telemetry/app-instrumented.js +10 -0
- package/dist/telemetry/bug-found.d.ts +18 -0
- package/dist/telemetry/bug-found.js +14 -0
- package/dist/telemetry/daemon-telemetry.js +24 -0
- package/dist/telemetry/install-source.d.ts +20 -0
- package/dist/telemetry/install-source.js +34 -0
- package/dist/telemetry/instrumentation-stall.d.ts +50 -0
- package/dist/telemetry/instrumentation-stall.js +90 -0
- package/dist/telemetry/session-metrics.d.ts +8 -0
- package/dist/telemetry/session-metrics.js +10 -0
- package/dist/telemetry/telemetry.d.ts +3 -1
- package/dist/telemetry/telemetry.js +13 -2
- package/dist/tools/act-tools.js +166 -29
- package/dist/tools/assert-verdict.js +32 -7
- package/dist/tools/browser-tools.js +10 -0
- package/dist/tools/dynamic-tools.js +16 -7
- package/dist/tools/error-recovery.js +9 -5
- package/dist/tools/invoke-tool.js +20 -3
- package/dist/tools/lease-hint.d.ts +30 -8
- package/dist/tools/lease-hint.js +65 -14
- package/dist/tools/lease-tools.d.ts +11 -0
- package/dist/tools/lease-tools.js +141 -96
- package/dist/tools/observe-tools.js +9 -3
- package/dist/tools/react-grace.d.ts +4 -0
- package/dist/tools/react-grace.js +6 -3
- package/dist/tools/read-tools.js +47 -1
- package/dist/tools/resolve-target.d.ts +33 -0
- package/dist/tools/resolve-target.js +53 -0
- package/dist/tools/settle-in-flight.d.ts +15 -0
- package/dist/tools/settle-in-flight.js +42 -2
- package/dist/tools/snapshot-delta.d.ts +3 -0
- package/dist/tools/snapshot-delta.js +67 -6
- package/dist/tools/tool-kit.d.ts +8 -0
- package/dist/tools/tool-surface.d.ts +60 -0
- package/dist/tools/tool-surface.js +85 -19
- package/dist/tools/tools.js +45 -4
- package/dist/tools/unadvertised-help.js +9 -6
- package/docs/README.md +1 -2
- package/docs/actions.mdx +41 -8
- package/docs/agent-cheatsheet.md +4 -2
- package/docs/architecture.md +6 -0
- package/docs/benchmarks.md +60 -12
- package/docs/best-practices.mdx +11 -6
- package/docs/capabilities.mdx +201 -54
- package/docs/cli/affected.mdx +6 -4
- package/docs/cli/capsules.mdx +9 -8
- package/docs/cli/cloud.mdx +30 -4
- package/docs/cli/doctor.mdx +41 -8
- package/docs/cli/drive.mdx +34 -15
- package/docs/cli/gate.mdx +6 -6
- package/docs/cli/help.mdx +8 -6
- package/docs/cli/hunt.mdx +18 -2
- package/docs/cli/identify.mdx +16 -2
- package/docs/cli/init.mdx +35 -8
- package/docs/cli/kill.mdx +5 -5
- package/docs/cli/license.mdx +1 -1
- package/docs/cli/open.mdx +6 -0
- package/docs/cli/restart.mdx +6 -6
- package/docs/cli/serve.mdx +10 -3
- package/docs/cli/status.mdx +29 -5
- package/docs/cli/stop.mdx +8 -0
- package/docs/cli/verify.mdx +20 -1
- package/docs/cli/version.mdx +1 -1
- package/docs/cli/watch.mdx +9 -2
- package/docs/cli.mdx +44 -22
- package/docs/debugging.md +3 -1
- package/docs/deploy-checks.md +30 -1
- package/docs/desktop.mdx +276 -80
- package/docs/docs.json +13 -12
- package/docs/enterprise.md +2 -0
- package/docs/faq.mdx +49 -35
- package/docs/fixtures.md +2 -0
- package/docs/flows.md +3 -1
- package/docs/for-agents.md +3 -3
- package/docs/frameworks.mdx +46 -7
- package/docs/gate-plan.md +4 -2
- package/docs/gates.md +7 -5
- package/docs/getting-started.md +53 -12
- package/docs/human-control.md +2 -0
- package/docs/index.mdx +43 -1
- package/docs/install-agentic.mdx +13 -4
- package/docs/install-manual.mdx +6 -4
- package/docs/instrumentation.mdx +7 -1
- package/docs/integration-patterns.md +3 -1
- package/docs/local-registry.md +4 -2
- package/docs/multi-agent-testing.md +3 -1
- package/docs/packages/babel-plugin.mdx +15 -6
- package/docs/packages/browser.mdx +2 -2
- package/docs/packages/core.mdx +26 -16
- package/docs/packages/electron.mdx +5 -7
- package/docs/packages/eslint-plugin.mdx +5 -3
- package/docs/packages/next.mdx +3 -5
- package/docs/packages/react.mdx +3 -3
- package/docs/packages/server.mdx +2 -2
- package/docs/packages/tauri.mdx +6 -6
- package/docs/packages/test.mdx +18 -7
- package/docs/packages/vite-plugin.mdx +4 -6
- package/docs/packages.mdx +14 -13
- package/docs/platform-integration.md +2 -0
- package/docs/predicates.mdx +76 -7
- package/docs/quickstart.mdx +51 -5
- package/docs/recipes.mdx +62 -22
- package/docs/skill-file.mdx +3 -1
- package/docs/state-management.mdx +2 -0
- package/docs/system-map.md +3 -1
- package/docs/telemetry-contract.md +16 -5
- package/docs/telemetry-events.mdx +63 -18
- package/docs/telemetry.md +8 -5
- package/docs/testing.md +35 -1
- package/docs/token-efficiency.md +8 -2
- package/docs/tools-act-and-wait.mdx +64 -14
- package/docs/tools-act-sequence.mdx +34 -17
- package/docs/tools-act.mdx +24 -11
- package/docs/tools-assert.mdx +69 -10
- package/docs/tools-console.mdx +11 -7
- package/docs/tools-inspect.mdx +11 -8
- package/docs/tools-navigate.mdx +41 -5
- package/docs/tools-network.mdx +42 -9
- package/docs/tools-observe.mdx +66 -10
- package/docs/tools-overview.mdx +26 -23
- package/docs/tools-query.mdx +52 -8
- package/docs/tools-session-and-feedback.mdx +30 -9
- package/docs/tools-sessions.mdx +39 -10
- package/docs/tools-snapshot.mdx +57 -3
- package/docs/tools-state.mdx +24 -1
- package/docs/tools-tools-and-run.mdx +40 -9
- package/docs/tools-wait-for.mdx +34 -15
- package/docs/troubleshooting.mdx +80 -10
- package/docs/usage.md +43 -21
- package/docs/vs-chrome-devtools-mcp.mdx +41 -0
- package/docs/vs-playwright-mcp.mdx +42 -2
- package/docs/vs-screenshots.mdx +42 -1
- package/docs/what-is-reticle.mdx +1 -1
- package/docs/why-reticle.mdx +3 -1
- package/package.json +5 -5
- package/docs/desktop-apps.md +0 -265
package/dist/tools/act-tools.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { normalizeQueryArgs } from './query-shape.js';
|
|
2
|
+
import { resolveTargetRef } from './resolve-target.js';
|
|
1
3
|
/**
|
|
2
4
|
* Action tools — reticle_act, reticle_act_sequence, reticle_act_and_wait. Split out of tools.ts to keep
|
|
3
5
|
* that file under the line cap and assembled back into the tool list there via ...ACT_TOOLS; the
|
|
@@ -14,18 +16,19 @@ import { buildReactionReport, summarizeReaction } from '../events/reaction.js';
|
|
|
14
16
|
import { parsePredicate } from '../events/predicate-parse.js';
|
|
15
17
|
import { causalSummary } from '../capsule/causal-summary.js';
|
|
16
18
|
import { findContradictions } from '../events/contradictions.js';
|
|
17
|
-
import { inFlightRequestLabels, waitForInFlight } from './settle-in-flight.js';
|
|
19
|
+
import { inFlightRequestLabels, repeatedRequestLabels, waitForInFlight, } from './settle-in-flight.js';
|
|
18
20
|
import { waitForReaction } from './react-grace.js';
|
|
19
21
|
import { decideVerified } from '../honesty/verified.js';
|
|
22
|
+
import { declaredExpectations } from '../events/declared.js';
|
|
20
23
|
import { readsDomState } from '../honesty/already-true.js';
|
|
21
24
|
import { describeWaitTarget } from '../honesty/unsettled.js';
|
|
22
25
|
import { saveFailedAssertCapsule } from './act-capsule.js';
|
|
23
26
|
import { buildDivergenceCapsule } from '../capsule/capsule.js';
|
|
24
27
|
import { predicateToExpectedLinks } from '../capsule/predicate-to-links.js';
|
|
25
28
|
import { buildHonestyBlock } from '../honesty/honesty.js';
|
|
26
|
-
import { buildCoverageStatement, blindSpotsFromState, transportGapNote, Coverage, impeachesCapture, } from '../honesty/blind-spots.js';
|
|
29
|
+
import { BlindSpotKind, buildCoverageStatement, blindSpotsFromState, transportGapNote, Coverage, impeachesCapture, } from '../honesty/blind-spots.js';
|
|
27
30
|
import { hasAcceptedWrite } from '../honesty/accepted-write.js';
|
|
28
|
-
import {
|
|
31
|
+
import { unreadWriteLabels } from '../honesty/unread-outcome.js';
|
|
29
32
|
import { evaluatePredicate, waitForPredicate, provenExpectedLinks, PredicateSchema, } from '../events/predicate.js';
|
|
30
33
|
import { healthEnvelope, refuseIfThrottled } from '../session/session-health.js';
|
|
31
34
|
import { pausedShortCircuit, pausedOutputShape, withControl, PAUSED_NO_VERDICT, } from '../session/control-envelope.js';
|
|
@@ -54,20 +57,59 @@ import { tryRealInput } from './real-input-attempt.js';
|
|
|
54
57
|
const ACTION_TYPE_VALUES = Object.values(ActionType);
|
|
55
58
|
const ACTION_TYPE_LIST = ACTION_TYPE_VALUES.join(' | ');
|
|
56
59
|
const actionTypeEnum = z.enum(ACTION_TYPE_VALUES);
|
|
60
|
+
/**
|
|
61
|
+
* Resolve an action's element: an explicit `ref`, or a `target` query resolved in the SAME call.
|
|
62
|
+
*
|
|
63
|
+
* Requiring a ref meant every verification paid a `reticle_query` turn first just to learn one
|
|
64
|
+
* string, and the advertised tool surface is re-sent on every turn — measured on the wire, a
|
|
65
|
+
* two-turn verification spent 10,756 of 11,235 tokens on schema and 479 on the actual answers. The
|
|
66
|
+
* lookup still happens; it just stops costing a round trip through the model.
|
|
67
|
+
*
|
|
68
|
+
* `ref` wins when both are given, because it is the more specific instruction and silently
|
|
69
|
+
* preferring the query would act on something the caller did not name.
|
|
70
|
+
*/
|
|
71
|
+
async function resolveActTarget(session, args) {
|
|
72
|
+
const ref = asString(args['ref']);
|
|
73
|
+
if (ref !== undefined && ref.length > 0)
|
|
74
|
+
return { kind: 'ref', ref };
|
|
75
|
+
const target = args['target'];
|
|
76
|
+
if (target === undefined) {
|
|
77
|
+
return {
|
|
78
|
+
kind: 'error',
|
|
79
|
+
message: 'pass `ref` (from reticle_query/reticle_snapshot) or `target` (e.g. { testid } or { role, name }).',
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
const q = normalizeQueryArgs(asRecord(target));
|
|
83
|
+
const out = await session.command(ReticleCommand.QUERY, {
|
|
84
|
+
by: q['by'],
|
|
85
|
+
value: q['value'],
|
|
86
|
+
name: q['name'],
|
|
87
|
+
scope: q['scope'],
|
|
88
|
+
});
|
|
89
|
+
if (!out.ok)
|
|
90
|
+
return { kind: 'error', message: out.error ?? 'target query failed' };
|
|
91
|
+
const elements = asRecord(out.result)['elements'];
|
|
92
|
+
return resolveTargetRef(Array.isArray(elements) ? elements : []);
|
|
93
|
+
}
|
|
57
94
|
export const ACT_TOOLS = [
|
|
58
95
|
{
|
|
59
96
|
name: ReticleTool.ACT,
|
|
60
97
|
example: { ref: 'e42', action: 'fill', args: { value: 'hello' } },
|
|
61
|
-
description: 'Act WITHOUT checking the result — if the action is supposed to cause something, use reticle_act_and_wait { until } instead and get the verdict in the same call. This tool returns immediately with a `since` cursor; observe the reaction yourself with reticle_observe. Use it for actions with no observable consequence to assert (focus, hover, scrollIntoView, a fill you are about to submit). One action against a ref: click|dblclick|hover|focus|fill|type|clear|select|check|uncheck|submit|press|scrollIntoView. Carries effect:{dispatched,targetMatched,visible,enabled,focusMoved,valueChanged,domMutatedWithin,occluded,occludedBy,scrolledIntoView} to tell "action missed" from "app didn\'t react"; dispatched=landed, settled=a real frame flushed, and a settle timeout never fails the tool. Fields at their uninformative default are OMITTED so a clean action collapses to its consequence: an absent dispatched/targetMatched/visible/enabled means true, an absent occluded/scrolledIntoView/valueChanged/defaultPrevented means false, an absent focusMoved/occludedBy means null. occluded=true means the click point is covered by another element (a real user could not click it) — synthetic dispatch still delivered the event; scrolledIntoView=true means an off-viewport target was scrolled in first. inputMode is "real" (native CDP, no synthetic effect block) or "synthetic"; clicks default to the occlusion-honest synthetic path even when CDP is configured — pass args.native:true to force a trusted native click (file pickers, clipboard). args.holdMs keeps the pointer DOWN for that long between mousedown and mouseup — the only way to drive a hold-to-confirm control (hold-to-delete, hold-to-record, long-press); effect.heldMs reports the hold actually achieved, which a throttled background tab can stretch. args.confirmDangerous is a permission gate and NOT a duration: it allows a destructive control, it does not hold one. inputModeReason explains any real→synthetic choice so it is never silent. Full model (real-input, throttled tabs, `reticle drive`): node_modules/@reticlehq/server/docs/usage.md §18.',
|
|
98
|
+
description: 'Act WITHOUT checking the result — if the action is supposed to cause something, use reticle_act_and_wait { until } instead and get the verdict in the same call. This tool returns immediately with a `since` cursor; observe the reaction yourself with reticle_observe. Use it for actions with no observable consequence to assert (focus, hover, scrollIntoView, a fill you are about to submit). One action against a ref: click|dblclick|hover|focus|fill|type|clear|select|check|uncheck|submit|press|scrollIntoView. Carries effect:{dispatched,targetMatched,visible,enabled,focusMoved,valueChanged,domMutatedWithin,occluded,occludedBy,scrolledIntoView} to tell "action missed" from "app didn\'t react"; dispatched=landed, settled=a real frame flushed, and a settle timeout never fails the tool. Fields at their uninformative default are OMITTED so a clean action collapses to its consequence: an absent dispatched/targetMatched/visible/enabled means true, an absent occluded/scrolledIntoView/valueChanged/defaultPrevented means false, an absent focusMoved/occludedBy means null. occluded=true means the click point is covered by another element (a real user could not click it) — synthetic dispatch still delivered the event; scrolledIntoView=true means an off-viewport target was scrolled in first. effect.alreadyAtValue=true (check/uncheck only) means the box already read as the requested state, so NOTHING was dispatched and the app was never told — the DOM property is not evidence the application holds that value, so assert the app\'s own state (a signal, a request, a derived control), not the box. inputMode is "real" (native CDP, no synthetic effect block) or "synthetic"; clicks default to the occlusion-honest synthetic path even when CDP is configured — pass args.native:true to force a trusted native click (file pickers, clipboard). args.holdMs keeps the pointer DOWN for that long between mousedown and mouseup — the only way to drive a hold-to-confirm control (hold-to-delete, hold-to-record, long-press); effect.heldMs reports the hold actually achieved, which a throttled background tab can stretch. args.confirmDangerous is a permission gate and NOT a duration: it allows a destructive control, it does not hold one. inputModeReason explains any real→synthetic choice so it is never silent. Full model (real-input, throttled tabs, `reticle drive`): node_modules/@reticlehq/server/docs/usage.md §18.',
|
|
62
99
|
inputSchema: {
|
|
63
100
|
ref: z
|
|
64
101
|
.string()
|
|
65
|
-
.
|
|
102
|
+
.optional()
|
|
103
|
+
.describe(`Element ref (e.g. 'e42') from reticle_snapshot/reticle_query — stable until the element leaves the DOM, so no re-snapshot between actions. Give this OR \`target\`.`),
|
|
104
|
+
target: z
|
|
105
|
+
.record(z.unknown())
|
|
106
|
+
.optional()
|
|
107
|
+
.describe('Find the element and act on it in ONE call, instead of a reticle_query round trip first: { testid } | { text } | { role, name } | { label }. Refuses if it matches more than one, rather than guessing.'),
|
|
66
108
|
action: actionTypeEnum.describe(`Action to perform: ${ACTION_TYPE_LIST}`),
|
|
67
109
|
args: z
|
|
68
110
|
.record(z.unknown())
|
|
69
111
|
.optional()
|
|
70
|
-
.describe('Action-specific arguments: { value } for fill/select, { text } for type/press (the key NAME, e.g. Escape or Tab), { toRef } for drag (the ref to drop ON — without it the drag lands nowhere), { native: true } to force a trusted native click, { holdMs: N } to keep the pointer DOWN for N ms (hold-to-confirm controls; effect.heldMs reports what was achieved), { confirmDangerous: true } to allow a potentially destructive control — a permission gate, NOT a duration.'),
|
|
112
|
+
.describe('Action-specific arguments: { value } for fill/select, { text } for type/press (the key NAME, e.g. Escape or Tab), { modifiers: ["Meta","Shift"] } for a press shortcut (Meta/Control/Shift/Alt — a Cmd+K), { toRef } for drag (the ref to drop ON — without it the drag lands nowhere), { native: true } to force a trusted native click, { holdMs: N } to keep the pointer DOWN for N ms (hold-to-confirm controls; effect.heldMs reports what was achieved), { confirmDangerous: true } to allow a potentially destructive control — a permission gate, NOT a duration.'),
|
|
71
113
|
refuseWhenThrottled: z
|
|
72
114
|
.boolean()
|
|
73
115
|
.optional()
|
|
@@ -107,10 +149,15 @@ export const ACT_TOOLS = [
|
|
|
107
149
|
if (paused !== undefined)
|
|
108
150
|
return paused;
|
|
109
151
|
refuseIfThrottled(session, args['refuseWhenThrottled']);
|
|
152
|
+
// Resolve `target` to a ref BEFORE the action window opens, so the lookup is not attributed to
|
|
153
|
+
// the act and cannot be mistaken for something the action caused.
|
|
154
|
+
const targetRef = await resolveActTarget(session, args);
|
|
155
|
+
if ('error' === targetRef.kind)
|
|
156
|
+
throw new Error(targetRef.message);
|
|
110
157
|
const since = session.elapsed();
|
|
111
158
|
// The act cursor + effect are marked only once the action has actually DISPATCHED (below, on
|
|
112
159
|
// each success path) — a refused act must leave nothing behind for the next observe to judge.
|
|
113
|
-
const ref =
|
|
160
|
+
const ref = targetRef.ref;
|
|
114
161
|
// Open the journal's action-attribution window BEFORE dispatching, so it covers the native path
|
|
115
162
|
// too. It used to open only on the synthetic path, and the native path returned above it — so a
|
|
116
163
|
// hover, a drag or any native:true click produced events with no actionId. Session.pushEvent
|
|
@@ -140,7 +187,7 @@ export const ACT_TOOLS = [
|
|
|
140
187
|
});
|
|
141
188
|
}
|
|
142
189
|
const result = await session.command(ReticleCommand.ACT, {
|
|
143
|
-
ref:
|
|
190
|
+
ref: targetRef.ref,
|
|
144
191
|
action: args['action'],
|
|
145
192
|
args: args['args'] ?? {},
|
|
146
193
|
});
|
|
@@ -192,11 +239,18 @@ export const ACT_TOOLS = [
|
|
|
192
239
|
steps: z
|
|
193
240
|
.array(z.record(z.unknown()))
|
|
194
241
|
.describe('Ordered list of { ref, action, args? } objects. Each step is equivalent to one reticle_act call; put confirmDangerous:true in a destructive step args object.'),
|
|
242
|
+
timeout_ms: z
|
|
243
|
+
.number()
|
|
244
|
+
.optional()
|
|
245
|
+
.describe('Per-step timeout in milliseconds. Default: 8000. Each step gets this budget independently.'),
|
|
195
246
|
...sessionIdShape,
|
|
196
247
|
},
|
|
197
248
|
outputSchema: {
|
|
198
249
|
since: z.number(),
|
|
199
250
|
dispatched: z.boolean(),
|
|
251
|
+
completed: z.number(),
|
|
252
|
+
stalled_at: z.number().optional(),
|
|
253
|
+
steps: z.array(z.record(z.unknown())).optional(),
|
|
200
254
|
result: z.unknown().optional(),
|
|
201
255
|
session: z
|
|
202
256
|
.object({ lastSeenMs: z.number(), throttled: z.boolean(), focused: z.boolean() })
|
|
@@ -206,33 +260,84 @@ export const ACT_TOOLS = [
|
|
|
206
260
|
},
|
|
207
261
|
handler: async (deps, args) => {
|
|
208
262
|
const session = deps.sessions.resolve(asString(args['sessionId']));
|
|
209
|
-
// Live-control: refuse to drive the page while the human has paused us (before any work).
|
|
210
263
|
const paused = pausedShortCircuit(session);
|
|
211
264
|
if (paused !== undefined)
|
|
212
265
|
return paused;
|
|
213
266
|
const since = session.elapsed();
|
|
214
267
|
session.beginAction(ReticleTool.ACT_SEQUENCE, asRecord(args));
|
|
215
268
|
try {
|
|
216
|
-
const
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
//
|
|
221
|
-
//
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
269
|
+
const inputSteps = Array.isArray(args['steps']) ? args['steps'] : [];
|
|
270
|
+
const perStepTimeout = 'number' === typeof args['timeout_ms'] ? args['timeout_ms'] : 8000;
|
|
271
|
+
const stepResults = [];
|
|
272
|
+
let stalledAt;
|
|
273
|
+
// DIVERGENCE: live sends N individual ACT commands (for per-step timeout + progress);
|
|
274
|
+
// replay sends one batched ACT_SEQUENCE command (flows/replay.ts:294). A bug in either is
|
|
275
|
+
// invisible from the other — cover both when changing sequence semantics.
|
|
276
|
+
for (let i = 0; i < inputSteps.length; i++) {
|
|
277
|
+
const step = asRecord(inputSteps[i]);
|
|
278
|
+
try {
|
|
279
|
+
const result = await session.command(ReticleCommand.ACT, { ref: step['ref'], action: step['action'], args: step['args'] ?? {} }, perStepTimeout);
|
|
280
|
+
if (!result.ok) {
|
|
281
|
+
stalledAt = i;
|
|
282
|
+
stepResults.push({
|
|
283
|
+
ref: step['ref'],
|
|
284
|
+
action: step['action'],
|
|
285
|
+
dispatched: false,
|
|
286
|
+
error: result.error ?? 'step failed',
|
|
287
|
+
});
|
|
288
|
+
break;
|
|
289
|
+
}
|
|
290
|
+
const r = asRecord(result.result);
|
|
291
|
+
const stepResult = {
|
|
292
|
+
ref: r['ref'] ?? step['ref'],
|
|
293
|
+
action: r['action'] ?? step['action'],
|
|
294
|
+
dispatched: r['dispatched'] ?? true,
|
|
295
|
+
settled: r['settled'] ?? null,
|
|
296
|
+
settleReason: r['settleReason'] ?? null,
|
|
297
|
+
};
|
|
298
|
+
if (r['testid'] !== undefined)
|
|
299
|
+
stepResult['testid'] = r['testid'];
|
|
300
|
+
if (r['component'] !== undefined)
|
|
301
|
+
stepResult['component'] = r['component'];
|
|
302
|
+
if (r['role'] !== undefined)
|
|
303
|
+
stepResult['role'] = r['role'];
|
|
304
|
+
if (r['name'] !== undefined)
|
|
305
|
+
stepResult['name'] = r['name'];
|
|
306
|
+
if (r['source'] !== undefined)
|
|
307
|
+
stepResult['source'] = r['source'];
|
|
308
|
+
if (r['warning'] !== undefined)
|
|
309
|
+
stepResult['warning'] = r['warning'];
|
|
310
|
+
stepResults.push(stepResult);
|
|
311
|
+
}
|
|
312
|
+
catch (err) {
|
|
313
|
+
stalledAt = i;
|
|
314
|
+
stepResults.push({
|
|
315
|
+
ref: step['ref'],
|
|
316
|
+
action: step['action'],
|
|
317
|
+
dispatched: null,
|
|
318
|
+
timedOut: true,
|
|
319
|
+
error: err instanceof Error ? err.message : 'step timed out',
|
|
320
|
+
});
|
|
321
|
+
break;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
const completed = stalledAt ?? inputSteps.length;
|
|
325
|
+
if (completed > 0) {
|
|
326
|
+
session.lastAct.markActed(since, undefined, undefined);
|
|
327
|
+
}
|
|
328
|
+
if (deps.recordings.active().length > 0 && stalledAt === undefined) {
|
|
329
|
+
deps.recordings.capture(compileSequenceStep(args, { count: inputSteps.length, steps: stepResults }));
|
|
225
330
|
}
|
|
226
|
-
const r = asRecord(result.result); // per-step settle status lives in result.steps[]
|
|
227
331
|
return withControl(session, {
|
|
228
332
|
since,
|
|
229
|
-
dispatched:
|
|
230
|
-
|
|
333
|
+
dispatched: completed > 0,
|
|
334
|
+
completed,
|
|
335
|
+
...(stalledAt !== undefined ? { stalled_at: stalledAt } : {}),
|
|
336
|
+
steps: stepResults,
|
|
231
337
|
...healthEnvelope(session),
|
|
232
338
|
});
|
|
233
339
|
}
|
|
234
340
|
finally {
|
|
235
|
-
// Per-step settle lives in steps[]; the sequence action records without a single settle bool.
|
|
236
341
|
session.finishAction();
|
|
237
342
|
}
|
|
238
343
|
},
|
|
@@ -253,12 +358,17 @@ export const ACT_TOOLS = [
|
|
|
253
358
|
inputSchema: {
|
|
254
359
|
ref: z
|
|
255
360
|
.string()
|
|
256
|
-
.
|
|
361
|
+
.optional()
|
|
362
|
+
.describe(`Element ref (e.g. 'e42') from reticle_snapshot/reticle_query — stable until the element leaves the DOM, so no re-snapshot between actions. Give this OR \`target\`.`),
|
|
363
|
+
target: z
|
|
364
|
+
.record(z.unknown())
|
|
365
|
+
.optional()
|
|
366
|
+
.describe('Find the element and act on it in ONE call, instead of a reticle_query round trip first: { testid } | { text } | { role, name } | { label }. Refuses if it matches more than one, rather than guessing.'),
|
|
257
367
|
action: actionTypeEnum.describe(`Action to perform: ${ACTION_TYPE_LIST}`),
|
|
258
368
|
args: z
|
|
259
369
|
.record(z.unknown())
|
|
260
370
|
.optional()
|
|
261
|
-
.describe('Action-specific arguments: { value } for fill/select, { text } for type/press (the key NAME, e.g. Escape or Tab), { toRef } for drag (the ref to drop ON — without it the drag lands nowhere), { confirmDangerous: true } for a potentially destructive control.'),
|
|
371
|
+
.describe('Action-specific arguments: { value } for fill/select, { text } for type/press (the key NAME, e.g. Escape or Tab), { modifiers: ["Meta","Shift"] } for a press shortcut (Meta/Control/Shift/Alt), { toRef } for drag (the ref to drop ON — without it the drag lands nowhere), { confirmDangerous: true } for a potentially destructive control.'),
|
|
262
372
|
predicate: PredicateSchema.optional().describe('Alias for `until` (the name reticle_assert / reticle_wait_for use).'),
|
|
263
373
|
until: PredicateSchema.optional().describe('Predicate to wait for after the action completes (same shape as reticle_assert). OMIT to wait for the page to SETTLE — network + DOM idle — the deterministic default instead of a sleep. To assert a consequence AND settle, allOf them: { kind: "allOf", predicates: [<your predicate>, { kind: "settled" }] }.'),
|
|
264
374
|
timeout_ms: z
|
|
@@ -298,7 +408,7 @@ export const ACT_TOOLS = [
|
|
|
298
408
|
.describe('Reaction digest: { window_ms, summary } of what the app did (DOM/network/route/console/signal counts). The full per-event timeline is one reticle_observe { since } away.'),
|
|
299
409
|
summary: z
|
|
300
410
|
.unknown()
|
|
301
|
-
.describe('Bounded causal summary: net {total,errors,headline}, consoleErrors, statePathsChanged, storageKeysChanged, stateDiffs [{path,from,to}], storageDiffs [{key,from,to}], route, signals, layoutShift, longTasks — real before→after diffs (capped), not just readings. Every list is length-capped so the verdict can never be the field a client truncates; `elided` says which lists lost entries and how many.'),
|
|
411
|
+
.describe('Bounded causal summary: net {total,errors,headline}, consoleErrors, statePathsChanged, storageKeysChanged, stateDiffs [{path,from,to}], storageDiffs [{key,from,to}], route, signals, layoutShift, longTasks — real before→after diffs (capped), not just readings. THIS IS THE ANSWER TO "what did that click do": console, network, storage and state for this action are all here, so do NOT follow an act with reticle_console + reticle_network + reticle_state to find out — call them only to go deeper into something this block already pointed at. Every list is length-capped so the verdict can never be the field a client truncates; `elided` says which lists lost entries and how many. `stateUnwatched: true` means NO subscribable store is registered, so an empty stateDiffs means unwatched, NOT unchanged — no state conclusion is available until one is registered.'),
|
|
302
412
|
// Promoted out of `effect` on RED only — the file:line the failure came from, the first thing a
|
|
303
413
|
// repair wants. Undeclared, it was stripped on the validating profile exactly like the structured
|
|
304
414
|
// cause above was, losing the highest-value pointer on the one path (a failed verdict) that has it.
|
|
@@ -357,6 +467,11 @@ export const ACT_TOOLS = [
|
|
|
357
467
|
// Before anything is driven: this path cannot honour a native-input request, and taking the
|
|
358
468
|
// argument and ignoring it told the agent its trusted click had happened. See act-danger.
|
|
359
469
|
assertNativeInputSupported(asRecord(args['args']));
|
|
470
|
+
// Resolve `target` to a ref BEFORE the action window opens, so the lookup is not attributed to
|
|
471
|
+
// the act and cannot be mistaken for something the action caused.
|
|
472
|
+
const resolved = await resolveActTarget(session, args);
|
|
473
|
+
if ('error' === resolved.kind)
|
|
474
|
+
throw new Error(resolved.message);
|
|
360
475
|
const since = session.elapsed();
|
|
361
476
|
// The cursor + effect are marked after the act dispatches (below) — a refused act leaves none.
|
|
362
477
|
// The attribution window stays open across the settle wait below, so post-dispatch async events
|
|
@@ -372,7 +487,7 @@ export const ACT_TOOLS = [
|
|
|
372
487
|
: false;
|
|
373
488
|
try {
|
|
374
489
|
const actResult = await session.command(ReticleCommand.ACT, {
|
|
375
|
-
ref:
|
|
490
|
+
ref: resolved.ref,
|
|
376
491
|
action: args['action'],
|
|
377
492
|
args: args['args'] ?? {},
|
|
378
493
|
});
|
|
@@ -444,6 +559,9 @@ export const ACT_TOOLS = [
|
|
|
444
559
|
// verdict didn't see everything — say so, never imply full coverage.
|
|
445
560
|
const spots = blindSpotsFromState(session.blindSpots());
|
|
446
561
|
const coverage = buildCoverageStatement(spots);
|
|
562
|
+
// Nothing subscribed ⇒ the state channel is dark, and the summary must say so rather than
|
|
563
|
+
// report an empty diff list that reads like a fact about the app. See CausalSummary.
|
|
564
|
+
const stateUnwatched = spots.some((s) => s.kind === BlindSpotKind.UNWATCHED_STATE && s.count > 0);
|
|
447
565
|
// Only a spot that IMPEACHES the capture belongs in integrity — see impeachesCapture. A
|
|
448
566
|
// structural boundary (virtualized rows, a cross-origin frame) is reported as coverage and
|
|
449
567
|
// must not downgrade a verdict about what WAS observed.
|
|
@@ -475,6 +593,9 @@ export const ACT_TOOLS = [
|
|
|
475
593
|
// evictions that happened outside the window they impeached.
|
|
476
594
|
truncated: bufferLost,
|
|
477
595
|
coveragePartial: Coverage.PARTIAL === coverage.coverage,
|
|
596
|
+
// Settlement no longer vetoes a declared consequence that held, so the fact is carried
|
|
597
|
+
// here instead of only in the verdict it used to decide. Omitted when never measured.
|
|
598
|
+
...(settledOutcome === undefined ? {} : { settled: settledOutcome }),
|
|
478
599
|
...(0 === impeachingNotes.length ? {} : { blindSpots: impeachingNotes }),
|
|
479
600
|
losses,
|
|
480
601
|
});
|
|
@@ -503,18 +624,30 @@ export const ACT_TOOLS = [
|
|
|
503
624
|
//
|
|
504
625
|
// `actionSince` is the window's own floor here, which is the tightest attribution there is —
|
|
505
626
|
// and it is what lets `duplicate-request` claim "one user action was performed" and mean it.
|
|
627
|
+
// What the caller DECLARED before acting, handed to the detector that would otherwise judge
|
|
628
|
+
// the window as if nobody had said what they expected. See events/declared.ts.
|
|
629
|
+
const declared = declaredExpectations(until);
|
|
506
630
|
const contradictions = findContradictions(windowEvents, {
|
|
507
631
|
action: acted,
|
|
508
632
|
prior,
|
|
509
633
|
actionSince: since,
|
|
634
|
+
expectedFailures: declared.netFailures,
|
|
635
|
+
// A consequence that was already true before the action proves nothing about it, so it is
|
|
636
|
+
// not evidence the destination rendered either — `alreadyTrue` decides that, once.
|
|
637
|
+
renderProved: verdict.pass && !alreadyTrue && declared.rendersContent,
|
|
510
638
|
...session.lastAct.effect(),
|
|
511
639
|
});
|
|
512
640
|
// The single field an agent reads. Everything below it is the evidence it was derived from;
|
|
513
641
|
// this is the only one that has to be interpreted, and now it interprets itself.
|
|
514
642
|
const outcomePending = hasAcceptedWrite(windowEvents);
|
|
515
|
-
const outcomeUnread =
|
|
643
|
+
const outcomeUnread = unreadWriteLabels(windowEvents);
|
|
516
644
|
const decision = decideVerified({
|
|
517
645
|
pass: verdict.pass,
|
|
646
|
+
// The caller NAMED the consequence rather than defaulting to "wait for idle". A
|
|
647
|
+
// declaration made before the action is what this tool sells, and idle-settlement was
|
|
648
|
+
// overriding it — see `declaredConsequence`. An explicit `{ kind: "settled" }` is not a
|
|
649
|
+
// declaration about the app's behaviour, it IS the idle wait, so it does not count.
|
|
650
|
+
declaredConsequence: until.kind !== PredicateKind.SETTLED,
|
|
518
651
|
...(alreadyTrue ? { alreadyTrue } : {}),
|
|
519
652
|
// An assertion nobody could evaluate must not be reported as one the app failed.
|
|
520
653
|
...(verdict.inconclusive === undefined ? {} : { inconclusive: verdict.inconclusive }),
|
|
@@ -525,7 +658,7 @@ export const ACT_TOOLS = [
|
|
|
525
658
|
honesty,
|
|
526
659
|
contradictions,
|
|
527
660
|
...(outcomePending ? { outcomePending } : {}),
|
|
528
|
-
...(outcomeUnread ? { outcomeUnread } : {}),
|
|
661
|
+
...(outcomeUnread.length > 0 ? { outcomeUnread } : {}),
|
|
529
662
|
// `settled` is genuinely optional: a wait that declared no predicate never measured it, and
|
|
530
663
|
// passing `false` there would report "never settled" about something never asked to settle.
|
|
531
664
|
...(settledOutcome === undefined ? {} : { settled: settledOutcome }),
|
|
@@ -535,6 +668,8 @@ export const ACT_TOOLS = [
|
|
|
535
668
|
unsettled: {
|
|
536
669
|
waitedFor: describeWaitTarget(until),
|
|
537
670
|
stillInFlight: inFlightRequestLabels(windowEvents),
|
|
671
|
+
// The retry loop that leaves nothing outstanding — see repeatedRequestLabels.
|
|
672
|
+
repeated: repeatedRequestLabels(windowEvents),
|
|
538
673
|
},
|
|
539
674
|
});
|
|
540
675
|
return withControl(session, {
|
|
@@ -549,7 +684,9 @@ export const ACT_TOOLS = [
|
|
|
549
684
|
: { source: `${actedSource.file}:${String(actedSource.line)}` }),
|
|
550
685
|
trace,
|
|
551
686
|
...(capsuleSaved === undefined ? {} : { capsuleSaved }),
|
|
552
|
-
|
|
687
|
+
// The window cannot say whether anything was WATCHING state — that is a level fact the
|
|
688
|
+
// session holds. Without it an empty `stateDiffs` reads as "the app changed nothing".
|
|
689
|
+
summary: causalSummary(windowEvents, { stateUnwatched }),
|
|
553
690
|
// Cross-channel disagreement, reported WITH the action that caused it.
|
|
554
691
|
//
|
|
555
692
|
// This is the one finding here a human structurally cannot make — they watch one channel,
|
|
@@ -1,13 +1,25 @@
|
|
|
1
|
-
import { CaptureLoss } from '@reticlehq/core';
|
|
1
|
+
import { BlindSpotKind, CaptureLoss, PredicateKind } from '@reticlehq/core';
|
|
2
2
|
import { findContradictions } from '../events/contradictions.js';
|
|
3
|
+
import { declaredExpectations } from '../events/declared.js';
|
|
3
4
|
import { blindSpotsFromState, buildCoverageStatement, Coverage, impeachesCapture, transportGapNote, } from '../honesty/blind-spots.js';
|
|
4
5
|
import { buildHonestyBlock } from '../honesty/honesty.js';
|
|
5
6
|
import { hasAcceptedWrite } from '../honesty/accepted-write.js';
|
|
6
|
-
import {
|
|
7
|
+
import { unreadWriteLabels } from '../honesty/unread-outcome.js';
|
|
7
8
|
import { decideVerified } from '../honesty/verified.js';
|
|
8
9
|
import { describeWaitTarget } from '../honesty/unsettled.js';
|
|
9
|
-
import { inFlightRequestLabels } from './settle-in-flight.js';
|
|
10
|
+
import { inFlightRequestLabels, repeatedRequestLabels } from './settle-in-flight.js';
|
|
10
11
|
import { gradeOfPredicate } from './assert-grade.js';
|
|
12
|
+
function absenceBlindSpotNote(predicate, spots) {
|
|
13
|
+
if (predicate.kind !== 'element' || predicate.absent !== true)
|
|
14
|
+
return undefined;
|
|
15
|
+
const relevant = spots.filter((spot) => spot.count > 0 &&
|
|
16
|
+
spot.kind === BlindSpotKind.CROSS_ORIGIN_IFRAME &&
|
|
17
|
+
undefined !== predicate.query.scope);
|
|
18
|
+
if (0 === relevant.length)
|
|
19
|
+
return undefined;
|
|
20
|
+
const statement = buildCoverageStatement(relevant);
|
|
21
|
+
return `the absence assertion targeted a region Reticle could not observe (${statement.note ?? 'partial coverage'}), so a passing DOM check cannot prove absence`;
|
|
22
|
+
}
|
|
11
23
|
/**
|
|
12
24
|
* The honesty verdict for a plain `reticle_assert`.
|
|
13
25
|
*
|
|
@@ -37,6 +49,7 @@ observationLost) {
|
|
|
37
49
|
// that implies coverage it never had, and a needless caveat costs the agent a sentence.
|
|
38
50
|
const spots = blindSpotsFromState(session.blindSpots());
|
|
39
51
|
const statement = buildCoverageStatement(spots);
|
|
52
|
+
const absenceBlindSpot = absenceBlindSpotNote(predicate, spots);
|
|
40
53
|
// Omitted entirely when coverage is full, so an intact page pays nothing and the field's PRESENCE
|
|
41
54
|
// is the warning.
|
|
42
55
|
const coverage = Coverage.PARTIAL === statement.coverage
|
|
@@ -57,12 +70,18 @@ observationLost) {
|
|
|
57
70
|
// could show it did. An assert taken over a window that predates the act attributes nothing, and the
|
|
58
71
|
// rule then stays quiet.
|
|
59
72
|
const actCursor = session.lastAct.cursor();
|
|
73
|
+
// The same declaration the act path reads. `reticle_assert` is the other half of the verdict
|
|
74
|
+
// surface, and an error path declared here deserves the same answer it gets there — a fix that
|
|
75
|
+
// lived only on the act path would leave the sibling caller broken.
|
|
76
|
+
const declared = declaredExpectations(predicate);
|
|
60
77
|
const contradictions = findContradictions(windowEvents, {
|
|
61
78
|
prior,
|
|
79
|
+
expectedFailures: declared.netFailures,
|
|
80
|
+
renderProved: pass && declared.rendersContent,
|
|
62
81
|
...(actCursor !== undefined && actCursor >= since ? { actionSince: actCursor } : {}),
|
|
63
82
|
});
|
|
64
|
-
// Only a spot that IMPEACHES the capture downgrades a verdict.
|
|
65
|
-
//
|
|
83
|
+
// Only a spot that IMPEACHES the capture downgrades a general verdict. Structural boundaries are
|
|
84
|
+
// reported as coverage; the narrower absence exception is computed separately above.
|
|
66
85
|
const impeaching = buildCoverageStatement(spots.filter((sp) => impeachesCapture(sp.kind)));
|
|
67
86
|
// A gap in the WINDOW, as opposed to a standing limit of the page. Both mean the same thing to the
|
|
68
87
|
// rule — part of what happened was not seen — so both belong in `blindSpots`, which is the only
|
|
@@ -70,11 +89,16 @@ observationLost) {
|
|
|
70
89
|
const gap = transportGapNote(windowEvents);
|
|
71
90
|
const impeachingNotes = [impeaching.note, gap].filter((n) => n !== undefined);
|
|
72
91
|
const outcomePending = hasAcceptedWrite(windowEvents);
|
|
73
|
-
const outcomeUnread =
|
|
92
|
+
const outcomeUnread = unreadWriteLabels(windowEvents);
|
|
74
93
|
const decision = decideVerified({
|
|
75
94
|
pass,
|
|
95
|
+
// Same rule as the act path: the caller named a consequence, so a settlement-only finding must
|
|
96
|
+
// not override it. A fix that lived on one half of the verdict surface would leave the other
|
|
97
|
+
// half broken, and this is the tool agents call most.
|
|
98
|
+
declaredConsequence: predicate.kind !== PredicateKind.SETTLED,
|
|
76
99
|
...(inconclusive === undefined ? {} : { inconclusive }),
|
|
77
100
|
...(true === observationLost ? { observationLost: true } : {}),
|
|
101
|
+
...(absenceBlindSpot === undefined ? {} : { absenceBlindSpot }),
|
|
78
102
|
honesty: buildHonestyBlock({
|
|
79
103
|
grade: gradeOfPredicate(predicate),
|
|
80
104
|
attribution: 'window',
|
|
@@ -89,12 +113,13 @@ observationLost) {
|
|
|
89
113
|
}),
|
|
90
114
|
contradictions,
|
|
91
115
|
...(outcomePending ? { outcomePending } : {}),
|
|
92
|
-
...(outcomeUnread ? { outcomeUnread } : {}),
|
|
116
|
+
...(outcomeUnread.length > 0 ? { outcomeUnread } : {}),
|
|
93
117
|
// Same detail the act path supplies: this route reaches UNSETTLED through an absence-derived
|
|
94
118
|
// contradiction, and "the window closed before the app finished" is no more actionable here.
|
|
95
119
|
unsettled: {
|
|
96
120
|
waitedFor: describeWaitTarget(predicate),
|
|
97
121
|
stillInFlight: inFlightRequestLabels(windowEvents),
|
|
122
|
+
repeated: repeatedRequestLabels(windowEvents),
|
|
98
123
|
},
|
|
99
124
|
});
|
|
100
125
|
return { decision: decision, contradictions, coverage };
|
|
@@ -39,6 +39,12 @@ export const BROWSER_TOOLS = [
|
|
|
39
39
|
// reload:true is the absorbed reticle_refresh — same command, one fewer advertised tool.
|
|
40
40
|
if (true === args['reload']) {
|
|
41
41
|
const before = deps.sessions.resolve(asString(args['sessionId']));
|
|
42
|
+
// The verdict floor moves HERE, before the page goes away. A reload destroys the document,
|
|
43
|
+
// so every request that failed under the old one belongs to a page that no longer exists —
|
|
44
|
+
// and a later assert defaulting its window to "the last thing that happened" must not reach
|
|
45
|
+
// back past this point. Reported from the field as an assertion whose clauses all passed
|
|
46
|
+
// coming back `contradicted` by hundreds of 500s against resources that were already gone.
|
|
47
|
+
before.lastAct.markNavigated(before.elapsed());
|
|
42
48
|
await commandOrThrow(deps, asString(args['sessionId']), ReticleCommand.REFRESH, {
|
|
43
49
|
hard: true === args['hard'],
|
|
44
50
|
});
|
|
@@ -65,6 +71,10 @@ export const BROWSER_TOOLS = [
|
|
|
65
71
|
// reconnects), but the action record itself — "navigated to X" — is the causal fact worth keeping.
|
|
66
72
|
const session = deps.sessions.resolve(asString(args['sessionId']));
|
|
67
73
|
session.beginAction(ReticleTool.NAVIGATE, { url });
|
|
74
|
+
// Same floor as the reload path above, for the same reason: going to a new URL replaces the
|
|
75
|
+
// document just as thoroughly. `beginAction` attributes events to this action; it does not move
|
|
76
|
+
// the window a later assert judges over, and those are two different jobs.
|
|
77
|
+
session.lastAct.markNavigated(session.elapsed());
|
|
68
78
|
try {
|
|
69
79
|
const result = (await commandOrThrow(deps, asString(args['sessionId']), ReticleCommand.NAVIGATE, { url }));
|
|
70
80
|
// `ok` is the browser accepting the instruction, not the page arriving — see navigate-result.
|
|
@@ -65,11 +65,12 @@ export function buildDynamicTools(allTools, profile) {
|
|
|
65
65
|
: {
|
|
66
66
|
profile: {
|
|
67
67
|
...profile,
|
|
68
|
-
//
|
|
69
|
-
//
|
|
70
|
-
//
|
|
71
|
-
//
|
|
72
|
-
|
|
68
|
+
// This used to say `full` carries no meta-tools because it advertised everything
|
|
69
|
+
// directly. That stopped being true when the advertised surface was capped: no surface
|
|
70
|
+
// advertises the whole registry any more, so BOTH meta-tools are on every surface and
|
|
71
|
+
// reticle_run is always the way to the tail. The old wording would now send an agent
|
|
72
|
+
// away from the only tool that can reach half the registry.
|
|
73
|
+
note: `The surface is read once at daemon startup: change ${TOOL_PROFILE_ENV} and restart the daemon, or it has no effect. No surface advertises every tool — the advertised count is capped because editors budget tools across all connected MCP servers. Every tool listed here is callable through reticle_run { tool, args } whether or not it is advertised.`,
|
|
73
74
|
},
|
|
74
75
|
};
|
|
75
76
|
const reticleTools = {
|
|
@@ -86,10 +87,18 @@ export function buildDynamicTools(allTools, profile) {
|
|
|
86
87
|
? args['names'].filter((n) => 'string' === typeof n)
|
|
87
88
|
: undefined;
|
|
88
89
|
if (names === undefined || 0 === names.length) {
|
|
90
|
+
// The COUNT is stated, not just implied by the array length. An agent that can see 18 tools
|
|
91
|
+
// has no way to know whether that is all of them; being told the registry holds more is what
|
|
92
|
+
// turns "these are the tools" into "these are the tools I was shown".
|
|
93
|
+
const catalog = allTools.map((t) => ({
|
|
94
|
+
name: t.name,
|
|
95
|
+
summary: firstSentence(t.description),
|
|
96
|
+
}));
|
|
89
97
|
return Promise.resolve({
|
|
90
|
-
|
|
98
|
+
total: catalog.length,
|
|
99
|
+
tools: catalog,
|
|
91
100
|
...profileBlock,
|
|
92
|
-
next:
|
|
101
|
+
next: `All ${catalog.length} tools above are callable, advertised or not. Load full params with reticle_tools { names:[…] }, then call reticle_run { tool, args }.`,
|
|
93
102
|
});
|
|
94
103
|
}
|
|
95
104
|
return Promise.resolve({
|
|
@@ -56,10 +56,12 @@ function isSelfRecovering(message) {
|
|
|
56
56
|
}
|
|
57
57
|
/** The recovery hints, named so they are not free strings and can be asserted in tests. */
|
|
58
58
|
export const RECOVERY = {
|
|
59
|
-
NO_SESSION:
|
|
60
|
-
'
|
|
61
|
-
'
|
|
62
|
-
'
|
|
59
|
+
NO_SESSION: "No app is connected to Reticle. If nothing is serving the app, start the project's own dev " +
|
|
60
|
+
'script (from its package.json) in the BACKGROUND yourself and tell the human in one line that ' +
|
|
61
|
+
'it is running and how to stop it — never a second server, never a guessed command, never kill ' +
|
|
62
|
+
'anything. Then run `reticle status` to confirm a session appears. If the app IS running and no ' +
|
|
63
|
+
'session shows, the SDK is not reaching the bridge — check @reticlehq/browser is loaded in the ' +
|
|
64
|
+
'page and that both sides use the configured Reticle port.',
|
|
63
65
|
MULTIPLE_SESSIONS: 'Several tabs are connected. Call reticle_sessions to list them, then pass an explicit sessionId to ' +
|
|
64
66
|
'target the one you mean.',
|
|
65
67
|
UNKNOWN_SESSION: 'That sessionId is not connected. Call reticle_sessions for the current ids and retry with a valid one.',
|
|
@@ -121,7 +123,9 @@ export const RECOVERY = {
|
|
|
121
123
|
COMMAND_TIMEOUT: 'The page did not answer within the command window. That is a fact about the page, not a Reticle ' +
|
|
122
124
|
'failure: check reticle_sessions for `throttled`/`stale` on this session — a backgrounded tab is ' +
|
|
123
125
|
'throttled and may never answer, so ask the human to bring it to the front or drive your own ' +
|
|
124
|
-
'browser with
|
|
126
|
+
'browser with reticle_run { tool: "reticle_lease", action: "acquire", url } — reticle_lease is ' +
|
|
127
|
+
'not advertised under the default profile, so it is reached through reticle_run, not called ' +
|
|
128
|
+
'directly. If the tab is in front, the page is busy or blocked (a long ' +
|
|
125
129
|
'synchronous task, an alert/confirm dialog); reticle_console usually shows what it hit. Retry ' +
|
|
126
130
|
'once the cause is addressed.',
|
|
127
131
|
FLOW_STEP_MISSING: 'A recorded step points at an element that is not on the page now. Either the app changed under ' +
|