@reticlehq/server 2.6.0 → 2.8.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 +14 -5
- package/SKILL.md +251 -33
- package/dist/bridge/bridge.d.ts +6 -1
- package/dist/bridge/bridge.js +27 -4
- package/dist/capsule/causal-summary.d.ts +20 -0
- package/dist/capsule/causal-summary.js +31 -6
- package/dist/capsule/divergence.js +7 -1
- package/dist/cli/chromium-hint.d.ts +68 -0
- package/dist/cli/chromium-hint.js +145 -0
- package/dist/cli/cli-doctor.js +31 -36
- package/dist/cli/cli-kill.d.ts +58 -0
- package/dist/cli/cli-kill.js +162 -0
- package/dist/cli/cli-launch.d.ts +18 -0
- package/dist/cli/cli-launch.js +32 -1
- package/dist/cli/cli-parse.d.ts +16 -1
- package/dist/cli/cli-parse.js +32 -9
- package/dist/cli/cli-port.d.ts +8 -0
- package/dist/cli/cli-port.js +36 -1
- package/dist/cli/cli-verify.js +2 -2
- package/dist/cli/doctor-daemon-line.js +10 -1
- package/dist/cli/doctor-sessions-line.d.ts +12 -0
- package/dist/cli/doctor-sessions-line.js +66 -0
- package/dist/cli/port-holder.d.ts +10 -0
- package/dist/cli/port-holder.js +26 -2
- package/dist/cli.js +139 -23
- package/dist/crawl/crawl-tools.js +12 -2
- package/dist/crawl/crawl.d.ts +13 -0
- package/dist/crawl/crawl.js +50 -2
- package/dist/daemon/daemon-resilience.js +2 -2
- package/dist/daemon/loopback-alias.d.ts +32 -0
- package/dist/daemon/loopback-alias.js +80 -0
- package/dist/events/contradictions.d.ts +14 -0
- package/dist/events/contradictions.js +26 -17
- package/dist/events/predicate-eval.d.ts +26 -3
- package/dist/events/predicate-eval.js +284 -95
- package/dist/events/predicate-parse.d.ts +1 -1
- package/dist/events/predicate-parse.js +18 -4
- package/dist/events/predicate.js +170 -18
- package/dist/events/ring-buffer.d.ts +8 -0
- package/dist/events/ring-buffer.js +34 -1
- package/dist/flows/flow-classify.js +1 -1
- package/dist/flows/flow-tools.js +12 -4
- package/dist/flows/heal-run.js +1 -1
- package/dist/honesty/blind-spots.js +6 -2
- package/dist/honesty/honesty.d.ts +11 -0
- package/dist/honesty/honesty.js +9 -1
- package/dist/honesty/unsettled.d.ts +40 -0
- package/dist/honesty/unsettled.js +78 -0
- package/dist/honesty/verified.d.ts +7 -0
- package/dist/honesty/verified.js +27 -4
- package/dist/http-server.js +11 -1
- package/dist/index.js +29 -2
- package/dist/init/agent-rules.d.ts +16 -0
- package/dist/init/agent-rules.js +96 -17
- package/dist/init/astro-patch.d.ts +2 -1
- package/dist/init/astro-patch.js +11 -5
- package/dist/init/capabilities.d.ts +22 -0
- package/dist/init/capabilities.js +76 -0
- package/dist/init/confirm.d.ts +96 -0
- package/dist/init/confirm.js +138 -0
- package/dist/init/cra.js +3 -2
- package/dist/init/detect.d.ts +8 -0
- package/dist/init/detect.js +15 -0
- package/dist/init/non-js-project.d.ts +27 -0
- package/dist/init/non-js-project.js +84 -0
- package/dist/init/plan-framework.d.ts +9 -0
- package/dist/init/plan-framework.js +72 -16
- package/dist/init/plan.d.ts +40 -4
- package/dist/init/plan.js +183 -43
- package/dist/init/run.d.ts +24 -2
- package/dist/init/run.js +181 -46
- package/dist/init/slash-command.d.ts +1 -1
- package/dist/init/slash-command.js +46 -6
- package/dist/init/snippets.d.ts +64 -5
- package/dist/init/snippets.js +220 -31
- package/dist/init/vite-config.js +29 -3
- package/dist/input/network-mock-tools.d.ts +19 -0
- package/dist/input/network-mock-tools.js +29 -4
- package/dist/license/license.js +1 -1
- package/dist/mcp/mcp-proxy.d.ts +69 -3
- package/dist/mcp/mcp-proxy.js +215 -23
- package/dist/mcp/mcp.js +58 -7
- package/dist/mcp/proxy-lifecycle.d.ts +20 -1
- package/dist/mcp/proxy-lifecycle.js +22 -1
- package/dist/pool/browser-pool.d.ts +22 -2
- package/dist/pool/browser-pool.js +44 -4
- package/dist/pool/playwright-launcher.js +10 -2
- package/dist/project/learned-routes.d.ts +24 -0
- package/dist/project/learned-routes.js +63 -0
- package/dist/project/project-store.d.ts +6 -0
- package/dist/project/project-store.js +25 -1
- package/dist/session/live-control-tools.js +39 -2
- package/dist/session/live-control.d.ts +7 -9
- package/dist/session/live-control.js +34 -1
- package/dist/session/no-session-diagnosis.d.ts +23 -4
- package/dist/session/no-session-diagnosis.js +128 -44
- package/dist/session/no-session-watch.js +12 -0
- package/dist/session/ref-provenance.d.ts +54 -0
- package/dist/session/ref-provenance.js +61 -0
- package/dist/session/review-store.d.ts +19 -0
- package/dist/session/review-store.js +34 -5
- package/dist/session/session-lease.js +1 -1
- package/dist/session/session-manager.d.ts +9 -0
- package/dist/session/session-manager.js +11 -0
- package/dist/session/session.d.ts +8 -0
- package/dist/session/session.js +12 -0
- package/dist/status-payload.d.ts +13 -1
- package/dist/status-payload.js +6 -1
- package/dist/telemetry/app-instrumented.d.ts +37 -0
- package/dist/telemetry/app-instrumented.js +54 -0
- package/dist/telemetry/automation-hint.d.ts +17 -0
- package/dist/telemetry/automation-hint.js +71 -0
- package/dist/telemetry/bug-found.d.ts +0 -20
- package/dist/telemetry/bug-found.js +38 -21
- package/dist/telemetry/cli-telemetry.js +7 -1
- package/dist/telemetry/daemon-telemetry.d.ts +4 -4
- package/dist/telemetry/daemon-telemetry.js +8 -4
- package/dist/telemetry/feedback.js +1 -1
- package/dist/telemetry/init-telemetry.js +9 -1
- package/dist/telemetry/install-source.d.ts +36 -0
- package/dist/telemetry/install-source.js +40 -0
- package/dist/telemetry/run-telemetry.js +4 -1
- package/dist/telemetry/session-metrics.d.ts +3 -3
- package/dist/telemetry/session-metrics.js +9 -4
- package/dist/telemetry/telemetry.d.ts +7 -1
- package/dist/telemetry/telemetry.js +17 -2
- package/dist/telemetry/tool-refused.d.ts +19 -0
- package/dist/telemetry/tool-refused.js +55 -0
- package/dist/telemetry/verification-of.js +72 -8
- package/dist/temp-dir.d.ts +27 -0
- package/dist/temp-dir.js +46 -0
- package/dist/tools/act-tools.js +77 -11
- package/dist/tools/assert-grade.d.ts +1 -1
- package/dist/tools/assert-grade.js +27 -5
- package/dist/tools/assert-verdict.js +25 -1
- package/dist/tools/contract-tools.js +4 -0
- package/dist/tools/dynamic-tools.js +1 -1
- package/dist/tools/error-recovery.d.ts +10 -0
- package/dist/tools/error-recovery.js +70 -5
- package/dist/tools/feedback-invite.js +1 -1
- package/dist/tools/invoke-tool.js +149 -32
- package/dist/tools/lease-tools.d.ts +29 -1
- package/dist/tools/lease-tools.js +58 -5
- package/dist/tools/navigate-result.js +1 -1
- package/dist/tools/observe-tools.js +1 -1
- package/dist/tools/observed-nothing.js +1 -1
- package/dist/tools/react-grace.d.ts +48 -0
- package/dist/tools/react-grace.js +87 -0
- package/dist/tools/read-tools.js +20 -4
- package/dist/tools/settle-in-flight.d.ts +79 -0
- package/dist/tools/settle-in-flight.js +106 -0
- package/dist/tools/snapshot-delta.d.ts +7 -0
- package/dist/tools/snapshot-delta.js +130 -6
- package/dist/tools/tool-kit.d.ts +52 -5
- package/dist/tools/tool-kit.js +55 -24
- package/dist/tools/tool-surface.js +28 -9
- package/dist/tools/tools.js +38 -4
- package/dist/visual/capture-cleanup.d.ts +16 -0
- package/dist/visual/capture-cleanup.js +39 -0
- package/dist/visual/visual-tools.d.ts +18 -0
- package/dist/visual/visual-tools.js +24 -5
- package/docs/README.md +56 -11
- package/docs/actions.mdx +87 -0
- package/docs/agent-cheatsheet.md +102 -26
- package/docs/architecture.md +46 -35
- package/docs/benchmarks.md +76 -72
- package/docs/best-practices.mdx +211 -0
- package/docs/capabilities.mdx +387 -0
- package/docs/cli/affected.mdx +51 -0
- package/docs/cli/capsules.mdx +69 -0
- package/docs/cli/cloud.mdx +127 -0
- package/docs/cli/doctor.mdx +76 -0
- package/docs/cli/drive.mdx +55 -0
- package/docs/cli/feedback.mdx +67 -0
- package/docs/cli/gate.mdx +64 -0
- package/docs/cli/help.mdx +92 -0
- package/docs/cli/hunt.mdx +57 -0
- package/docs/cli/identify.mdx +62 -0
- package/docs/cli/init.mdx +94 -0
- package/docs/cli/kill.mdx +91 -0
- package/docs/cli/license.mdx +48 -0
- package/docs/cli/mcp.mdx +61 -0
- package/docs/cli/open.mdx +68 -0
- package/docs/cli/restart.mdx +60 -0
- package/docs/cli/rollback.mdx +35 -0
- package/docs/cli/serve.mdx +62 -0
- package/docs/cli/status.mdx +64 -0
- package/docs/cli/stop.mdx +56 -0
- package/docs/cli/telemetry.mdx +57 -0
- package/docs/cli/update.mdx +52 -0
- package/docs/cli/verify.mdx +57 -0
- package/docs/cli/version.mdx +39 -0
- package/docs/cli/watch.mdx +55 -0
- package/docs/cli.mdx +219 -0
- package/docs/debugging.md +13 -9
- package/docs/deploy-checks.md +15 -11
- package/docs/desktop-apps.md +49 -40
- package/docs/desktop.mdx +188 -0
- package/docs/docs.json +363 -0
- package/docs/enterprise.md +20 -16
- package/docs/faq.mdx +88 -0
- package/docs/fixtures.md +11 -7
- package/docs/flows.md +26 -22
- package/docs/for-agents.md +71 -0
- package/docs/frameworks.mdx +199 -0
- package/docs/gate-plan.md +45 -41
- package/docs/gates.md +23 -19
- package/docs/getting-started.md +76 -65
- package/docs/human-control.md +22 -18
- package/docs/index.mdx +101 -0
- package/docs/install-agentic.mdx +215 -0
- package/docs/install-manual.mdx +194 -0
- package/docs/instrumentation.mdx +232 -0
- package/docs/integration-patterns.md +38 -34
- package/docs/local-registry.md +16 -12
- package/docs/multi-agent-testing.md +16 -12
- package/docs/packages/babel-plugin.mdx +70 -0
- package/docs/packages/browser.mdx +111 -0
- package/docs/packages/core.mdx +106 -0
- package/docs/packages/electron.mdx +87 -0
- package/docs/packages/eslint-plugin.mdx +84 -0
- package/docs/packages/next.mdx +91 -0
- package/docs/packages/react.mdx +91 -0
- package/docs/packages/server.mdx +98 -0
- package/docs/packages/tauri.mdx +63 -0
- package/docs/packages/test.mdx +108 -0
- package/docs/packages/vite-plugin.mdx +96 -0
- package/docs/packages.mdx +217 -0
- package/docs/platform-integration.md +33 -26
- package/docs/predicates.mdx +179 -0
- package/docs/quickstart.mdx +211 -0
- package/docs/recipes.mdx +242 -0
- package/docs/skill-file.mdx +105 -0
- package/docs/state-management.mdx +197 -0
- package/docs/style.css +14 -0
- package/docs/system-map.md +47 -32
- package/docs/telemetry-contract.md +101 -46
- package/docs/telemetry-events.mdx +430 -0
- package/docs/telemetry.md +63 -50
- package/docs/testing.md +10 -6
- package/docs/token-efficiency.md +30 -26
- package/docs/tools-act-and-wait.mdx +123 -0
- package/docs/tools-act-sequence.mdx +120 -0
- package/docs/tools-act.mdx +82 -0
- package/docs/tools-assert.mdx +70 -0
- package/docs/tools-console.mdx +59 -0
- package/docs/tools-inspect.mdx +85 -0
- package/docs/tools-navigate.mdx +63 -0
- package/docs/tools-network.mdx +86 -0
- package/docs/tools-observe.mdx +89 -0
- package/docs/tools-overview.mdx +157 -0
- package/docs/tools-query.mdx +82 -0
- package/docs/tools-session-and-feedback.mdx +91 -0
- package/docs/tools-sessions.mdx +65 -0
- package/docs/tools-snapshot.mdx +75 -0
- package/docs/tools-state.mdx +81 -0
- package/docs/tools-tools-and-run.mdx +125 -0
- package/docs/tools-wait-for.mdx +66 -0
- package/docs/troubleshooting.mdx +193 -0
- package/docs/usage.md +175 -153
- package/docs/vs-chrome-devtools-mcp.mdx +82 -0
- package/docs/vs-playwright-mcp.mdx +85 -0
- package/docs/vs-screenshots.mdx +96 -0
- package/docs/what-is-reticle.mdx +63 -0
- package/docs/why-reticle.mdx +107 -0
- package/package.json +2 -2
- package/docs/matrix/2.5.0/claude-code-machine.json +0 -19
- package/docs/matrix/2.5.0/codex-machine.json +0 -20
- package/docs/matrix/2.5.0/cursor-machine.json +0 -22
- package/docs/matrix/2.5.0/gemini-machine.json +0 -22
- package/docs/matrix/2.5.0/opencode-machine.json +0 -22
- package/docs/matrix/2.5.0/vscode-machine.json +0 -22
- package/docs/matrix/2.5.0/windsurf-machine.json +0 -22
- package/docs/matrix/MATRIX.md +0 -26
- package/docs/matrix/README.md +0 -59
- package/docs/mint.json +0 -61
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* Extracted because a rule this easy to get wrong, feeding the one number shown to investors, should
|
|
13
13
|
* be readable and testable on its own rather than inferred from a ternary inside a dispatcher.
|
|
14
14
|
*/
|
|
15
|
-
import { VerifiedReason } from '@reticlehq/core';
|
|
15
|
+
import { CaptureLoss, VerifiedReason } from '@reticlehq/core';
|
|
16
16
|
import { VERIFICATION_TOOLS } from '../tools/feedback-tools.js';
|
|
17
17
|
import { getBrowserMode } from './browser-mode.js';
|
|
18
18
|
/**
|
|
@@ -27,11 +27,64 @@ function reasonOf(result) {
|
|
|
27
27
|
const raw = result['verifiedReason'];
|
|
28
28
|
return 'string' === typeof raw && REASONS.has(raw) ? raw : undefined;
|
|
29
29
|
}
|
|
30
|
+
/**
|
|
31
|
+
* WHICH loss made the capture unclean, read off the honesty block the verdict already carries.
|
|
32
|
+
*
|
|
33
|
+
* Narrowed against core's own list for the same reason `reasonOf` is: a value that is not a member
|
|
34
|
+
* is not an answer. Reported as `OTHER` rather than dropped when the block says the capture was
|
|
35
|
+
* dirty and names no kind — the loss is real either way, and a silent absence there would read as
|
|
36
|
+
* "no unclean verdicts happened", which is the opposite of the truth.
|
|
37
|
+
*/
|
|
38
|
+
const LOSSES = new Set(Object.values(CaptureLoss));
|
|
39
|
+
function uncleanLossOf(result, reason) {
|
|
40
|
+
if (VerifiedReason.UNCLEAN_CAPTURE !== reason)
|
|
41
|
+
return undefined;
|
|
42
|
+
const honesty = result['honesty'];
|
|
43
|
+
const integrity = null !== honesty && 'object' === typeof honesty
|
|
44
|
+
? honesty.integrity
|
|
45
|
+
: undefined;
|
|
46
|
+
const first = Array.isArray(integrity?.losses) ? integrity.losses[0] : undefined;
|
|
47
|
+
return 'string' === typeof first && LOSSES.has(first)
|
|
48
|
+
? first
|
|
49
|
+
: CaptureLoss.OTHER;
|
|
50
|
+
}
|
|
30
51
|
/** Suite statuses. `unverifiable` is a suite that ran but proved nothing — including an empty one. */
|
|
31
52
|
const SuiteStatus = {
|
|
32
53
|
PASS: 'pass',
|
|
33
54
|
FAIL: 'fail',
|
|
34
55
|
};
|
|
56
|
+
/**
|
|
57
|
+
* Did the assertion itself pass, across three tool shapes that spell it three ways.
|
|
58
|
+
*
|
|
59
|
+
* `reticle_assert` spreads its verdict at the top level, so `pass` is right there. `flow_verify`
|
|
60
|
+
* reports a suite `status`. **`reticle_act_and_wait` nests it under `verdict`**, and that omission is
|
|
61
|
+
* why `falseGreenCaught` had never once been true in the field: `passed` came back `undefined` for
|
|
62
|
+
* every act_and_wait, so `true === passed` was false whatever the app did, on the one tool the
|
|
63
|
+
* product tells agents to reach for first.
|
|
64
|
+
*
|
|
65
|
+
* The metric it feeds is the product's central claim reduced to a boolean, and it was structurally
|
|
66
|
+
* unable to fire on the flagship path. Worse, the test file pinned that: one case used the real
|
|
67
|
+
* nested shape and asserted `falseGreenCaught: false`, directly beneath a comment calling this case
|
|
68
|
+
* "the thesis", while the case that asserted `true` used a flat shape act_and_wait never emits.
|
|
69
|
+
*
|
|
70
|
+
* A permanently-zero metric is indistinguishable from a working one that has nothing to report, so
|
|
71
|
+
* nobody could have noticed this from the data. It took reading the emit path.
|
|
72
|
+
*/
|
|
73
|
+
function passedOf(result, status) {
|
|
74
|
+
if ('boolean' === typeof result['pass'])
|
|
75
|
+
return result['pass'];
|
|
76
|
+
const verdict = result['verdict'];
|
|
77
|
+
if (null !== verdict && 'object' === typeof verdict) {
|
|
78
|
+
const nested = verdict['pass'];
|
|
79
|
+
if ('boolean' === typeof nested)
|
|
80
|
+
return nested;
|
|
81
|
+
}
|
|
82
|
+
if (status === SuiteStatus.PASS)
|
|
83
|
+
return true;
|
|
84
|
+
if (status === SuiteStatus.FAIL)
|
|
85
|
+
return false;
|
|
86
|
+
return undefined;
|
|
87
|
+
}
|
|
35
88
|
/**
|
|
36
89
|
* The verification payload for a result, or undefined when nothing was verified.
|
|
37
90
|
*
|
|
@@ -52,20 +105,28 @@ export function verificationOf(toolName, result, durationMs, brand) {
|
|
|
52
105
|
// metric's convenience — but a status that is neither pass nor fail is NOT a verdict, and an empty
|
|
53
106
|
// suite reports exactly that.
|
|
54
107
|
const status = result['status'];
|
|
55
|
-
const passed =
|
|
56
|
-
? result['pass']
|
|
57
|
-
: status === SuiteStatus.PASS
|
|
58
|
-
? true
|
|
59
|
-
: status === SuiteStatus.FAIL
|
|
60
|
-
? false
|
|
61
|
-
: undefined;
|
|
108
|
+
const passed = passedOf(result, status);
|
|
62
109
|
if (verified === undefined && passed === undefined)
|
|
63
110
|
return undefined;
|
|
64
111
|
const reason = reasonOf(result);
|
|
112
|
+
const uncleanLoss = uncleanLossOf(result, reason);
|
|
65
113
|
return {
|
|
66
114
|
via: toolName,
|
|
67
115
|
verified: verified ?? (true === passed ? 'yes' : 'no'),
|
|
68
116
|
passed: passed ?? 'yes' === verified,
|
|
117
|
+
// The assertion passed and Reticle answered `no` anyway: a green actively refuted.
|
|
118
|
+
//
|
|
119
|
+
// Deliberately NOT widened to every refusal of a passing assertion, though several of them are
|
|
120
|
+
// arguably false greens caught too. `decideVerified` refuses a green seven ways and only
|
|
121
|
+
// `contradicted` answers `no`; the rest answer `unknown` on purpose, because UNKNOWN must never
|
|
122
|
+
// collapse into NO. `already_true` is the tempting one: a green that proved nothing because the
|
|
123
|
+
// condition already held before the action, found by a check built for exactly that.
|
|
124
|
+
//
|
|
125
|
+
// It stays out because `reason` is on this same payload. Anyone who wants the wider number can
|
|
126
|
+
// count it without a definition change, and a definition that shifts underneath a metric is
|
|
127
|
+
// worse than one that is narrow, because the old and new series look comparable and are not.
|
|
128
|
+
// Widening it would also have landed in the same release as the fix above, making it impossible
|
|
129
|
+
// to tell how much of any rise was the bug and how much was the redefinition.
|
|
69
130
|
falseGreenCaught: true === passed && 'no' === verified,
|
|
70
131
|
durationMs,
|
|
71
132
|
// Headless CI, a human watching, or somebody's own dev server — three different products behind
|
|
@@ -80,5 +141,8 @@ export function verificationOf(toolName, result, durationMs, brand) {
|
|
|
80
141
|
// belonging to three different owners — the agent, the app, and Reticle's own blind spots — and
|
|
81
142
|
// they need opposite responses. A suite verdict has no clause behind it and reports none.
|
|
82
143
|
...(reason === undefined ? {} : { reason }),
|
|
144
|
+
// WHAT was lost, when the reason was that something was. Three owners, three fixes, one bar
|
|
145
|
+
// until now.
|
|
146
|
+
...(uncleanLoss === undefined ? {} : { uncleanLoss }),
|
|
83
147
|
};
|
|
84
148
|
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Removing a temp directory on Windows, without inventing a product failure.
|
|
3
|
+
*
|
|
4
|
+
* `rm(dir, { recursive: true, force: true })` is the obvious teardown and it is not enough on
|
|
5
|
+
* Windows. Deletion there is not immediate: a handle the OS, the indexer or a virus scanner still
|
|
6
|
+
* holds keeps the entry alive for a moment after the last close, so the directory is observed
|
|
7
|
+
* non-empty at the instant `rmdir` runs and Node throws `ENOTEMPTY`. `force` does not cover it —
|
|
8
|
+
* that suppresses "missing", not "busy".
|
|
9
|
+
*
|
|
10
|
+
* The failure lands in `afterEach`, so it is reported against the test that just PASSED, and the
|
|
11
|
+
* leftover directory then bleeds state into what runs next. On CI this presented as five failures in
|
|
12
|
+
* one file, four of them consequential and one of them the wonderfully misleading "expected length 1
|
|
13
|
+
* but got 2" — a fake production bug manufactured entirely by teardown. This repo already has that
|
|
14
|
+
* exact story written up in `project-store.test.ts` about a timed-out test's stray writes; this is the
|
|
15
|
+
* same failure arriving through the filesystem instead of through the clock.
|
|
16
|
+
*
|
|
17
|
+
* `maxRetries`/`retryDelay` exist in Node for precisely this, and are documented as Windows-oriented.
|
|
18
|
+
* They are a no-op on POSIX, where the first attempt already succeeds.
|
|
19
|
+
*/
|
|
20
|
+
/**
|
|
21
|
+
* Delete a temp directory tree, tolerating Windows' delayed handle release.
|
|
22
|
+
*
|
|
23
|
+
* Never throws: a temp directory that outlives the test is the operating system's problem, not a
|
|
24
|
+
* reason to fail a run or to report a defect the product does not have. What must not happen is the
|
|
25
|
+
* opposite — a red that says nothing about the code under test.
|
|
26
|
+
*/
|
|
27
|
+
export declare function removeTempDir(dir: string): Promise<void>;
|
package/dist/temp-dir.js
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Removing a temp directory on Windows, without inventing a product failure.
|
|
3
|
+
*
|
|
4
|
+
* `rm(dir, { recursive: true, force: true })` is the obvious teardown and it is not enough on
|
|
5
|
+
* Windows. Deletion there is not immediate: a handle the OS, the indexer or a virus scanner still
|
|
6
|
+
* holds keeps the entry alive for a moment after the last close, so the directory is observed
|
|
7
|
+
* non-empty at the instant `rmdir` runs and Node throws `ENOTEMPTY`. `force` does not cover it —
|
|
8
|
+
* that suppresses "missing", not "busy".
|
|
9
|
+
*
|
|
10
|
+
* The failure lands in `afterEach`, so it is reported against the test that just PASSED, and the
|
|
11
|
+
* leftover directory then bleeds state into what runs next. On CI this presented as five failures in
|
|
12
|
+
* one file, four of them consequential and one of them the wonderfully misleading "expected length 1
|
|
13
|
+
* but got 2" — a fake production bug manufactured entirely by teardown. This repo already has that
|
|
14
|
+
* exact story written up in `project-store.test.ts` about a timed-out test's stray writes; this is the
|
|
15
|
+
* same failure arriving through the filesystem instead of through the clock.
|
|
16
|
+
*
|
|
17
|
+
* `maxRetries`/`retryDelay` exist in Node for precisely this, and are documented as Windows-oriented.
|
|
18
|
+
* They are a no-op on POSIX, where the first attempt already succeeds.
|
|
19
|
+
*/
|
|
20
|
+
import { rm } from 'node:fs/promises';
|
|
21
|
+
/**
|
|
22
|
+
* Retries and backoff. Small and bounded: this is covering a millisecond-scale handle release, so if
|
|
23
|
+
* it has not gone in half a second the cause is not a race and a slower retry would only hide it.
|
|
24
|
+
*/
|
|
25
|
+
const MAX_RETRIES = 10;
|
|
26
|
+
const RETRY_DELAY_MS = 50;
|
|
27
|
+
/**
|
|
28
|
+
* Delete a temp directory tree, tolerating Windows' delayed handle release.
|
|
29
|
+
*
|
|
30
|
+
* Never throws: a temp directory that outlives the test is the operating system's problem, not a
|
|
31
|
+
* reason to fail a run or to report a defect the product does not have. What must not happen is the
|
|
32
|
+
* opposite — a red that says nothing about the code under test.
|
|
33
|
+
*/
|
|
34
|
+
export async function removeTempDir(dir) {
|
|
35
|
+
try {
|
|
36
|
+
await rm(dir, {
|
|
37
|
+
recursive: true,
|
|
38
|
+
force: true,
|
|
39
|
+
maxRetries: MAX_RETRIES,
|
|
40
|
+
retryDelay: RETRY_DELAY_MS,
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
// Deliberately swallowed — see above. The OS reclaims the temp directory regardless.
|
|
45
|
+
}
|
|
46
|
+
}
|
package/dist/tools/act-tools.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
import { z } from 'zod';
|
|
7
7
|
import { aliasParam } from './alias-args.js';
|
|
8
8
|
import { captureAct, compileSequenceStep } from '../flows/replay.js';
|
|
9
|
-
import { ActionType, ActionWarning, DEFAULT_ASSERT_TIMEOUT_MS, InputMode, ReticleCommand, Verified, PredicateKind, } from '@reticlehq/core';
|
|
9
|
+
import { ActionType, ActionWarning, CaptureLoss, DEFAULT_ASSERT_TIMEOUT_MS, InputMode, ReticleCommand, Verified, PredicateKind, } from '@reticlehq/core';
|
|
10
10
|
import { assertNativeInputSupported } from './act-danger.js';
|
|
11
11
|
import { leanActResult, mutatedWithin } from './act-view.js';
|
|
12
12
|
import { ReticleTool } from './tool-names.js';
|
|
@@ -14,8 +14,11 @@ import { buildReactionReport, summarizeReaction } from '../events/reaction.js';
|
|
|
14
14
|
import { parsePredicate } from '../events/predicate-parse.js';
|
|
15
15
|
import { causalSummary } from '../capsule/causal-summary.js';
|
|
16
16
|
import { findContradictions } from '../events/contradictions.js';
|
|
17
|
+
import { inFlightRequestLabels, waitForInFlight } from './settle-in-flight.js';
|
|
18
|
+
import { waitForReaction } from './react-grace.js';
|
|
17
19
|
import { decideVerified } from '../honesty/verified.js';
|
|
18
20
|
import { readsDomState } from '../honesty/already-true.js';
|
|
21
|
+
import { describeWaitTarget } from '../honesty/unsettled.js';
|
|
19
22
|
import { saveFailedAssertCapsule } from './act-capsule.js';
|
|
20
23
|
import { buildDivergenceCapsule } from '../capsule/capsule.js';
|
|
21
24
|
import { predicateToExpectedLinks } from '../capsule/predicate-to-links.js';
|
|
@@ -55,7 +58,7 @@ export const ACT_TOOLS = [
|
|
|
55
58
|
{
|
|
56
59
|
name: ReticleTool.ACT,
|
|
57
60
|
example: { ref: 'e42', action: 'fill', args: { value: 'hello' } },
|
|
58
|
-
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`): docs/usage.md §18.',
|
|
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.',
|
|
59
62
|
inputSchema: {
|
|
60
63
|
ref: z
|
|
61
64
|
.string()
|
|
@@ -64,7 +67,7 @@ export const ACT_TOOLS = [
|
|
|
64
67
|
args: z
|
|
65
68
|
.record(z.unknown())
|
|
66
69
|
.optional()
|
|
67
|
-
.describe('Action-specific arguments: { value } for fill/select, { text } for type/press, { 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.'),
|
|
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.'),
|
|
68
71
|
refuseWhenThrottled: z
|
|
69
72
|
.boolean()
|
|
70
73
|
.optional()
|
|
@@ -255,7 +258,7 @@ export const ACT_TOOLS = [
|
|
|
255
258
|
args: z
|
|
256
259
|
.record(z.unknown())
|
|
257
260
|
.optional()
|
|
258
|
-
.describe('Action-specific arguments: { value } for fill/select, { text } for type/press, { confirmDangerous: true } for a potentially destructive control.'),
|
|
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.'),
|
|
259
262
|
predicate: PredicateSchema.optional().describe('Alias for `until` (the name reticle_assert / reticle_wait_for use).'),
|
|
260
263
|
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" }] }.'),
|
|
261
264
|
timeout_ms: z
|
|
@@ -295,7 +298,7 @@ export const ACT_TOOLS = [
|
|
|
295
298
|
.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.'),
|
|
296
299
|
summary: z
|
|
297
300
|
.unknown()
|
|
298
|
-
.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.'),
|
|
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.'),
|
|
299
302
|
// Promoted out of `effect` on RED only — the file:line the failure came from, the first thing a
|
|
300
303
|
// repair wants. Undeclared, it was stripped on the validating profile exactly like the structured
|
|
301
304
|
// cause above was, losing the highest-value pointer on the one path (a failed verdict) that has it.
|
|
@@ -355,11 +358,6 @@ export const ACT_TOOLS = [
|
|
|
355
358
|
// argument and ignoring it told the agent its trusted click had happened. See act-danger.
|
|
356
359
|
assertNativeInputSupported(asRecord(args['args']));
|
|
357
360
|
const since = session.elapsed();
|
|
358
|
-
// `dropped` is cumulative for the SESSION, so comparing against a pre-action reading is what
|
|
359
|
-
// asks "did the buffer lose anything while THIS action was observed?" — the question the
|
|
360
|
-
// honesty block means. Reading the raw total pinned every later verdict to `unknown` after a
|
|
361
|
-
// single early eviction (measured on the Next.js demo at dropped:51, windows intact).
|
|
362
|
-
const droppedBefore = session.bufferHealth().dropped;
|
|
363
361
|
// The cursor + effect are marked after the act dispatches (below) — a refused act leaves none.
|
|
364
362
|
// The attribution window stays open across the settle wait below, so post-dispatch async events
|
|
365
363
|
// (the whole point of act_and_wait) attribute to this action. finishAction fires after the wait.
|
|
@@ -385,9 +383,39 @@ export const ACT_TOOLS = [
|
|
|
385
383
|
// stops the spread below from inheriting an earlier reticle_act's action and mutation count.
|
|
386
384
|
session.lastAct.markActed(since, asString(args['action']), mutatedWithin(asRecord(actResult.result)));
|
|
387
385
|
// Honesty: floor the predicate at this act's cursor so a stale buffered event can't satisfy it.
|
|
386
|
+
const predicateStarted = session.elapsed();
|
|
388
387
|
const verdict = timeout > 0
|
|
389
388
|
? await waitForPredicate(session, until, timeout, since)
|
|
390
389
|
: await evaluatePredicate(session, until, since);
|
|
390
|
+
// The predicate resolves the INSTANT it holds, which on an optimistically-navigating app is
|
|
391
|
+
// while the write is still in flight — so the verdict was taken over a window the app had not
|
|
392
|
+
// finished, and came back `unknown / unsettled` asking the CALLER to re-check. A login that
|
|
393
|
+
// genuinely succeeded returned at 492ms of an 8000ms budget with every corroborating channel
|
|
394
|
+
// agreeing. Spend the rest of the budget the caller already granted.
|
|
395
|
+
//
|
|
396
|
+
// This decides nothing: it waits for the answer instead of guessing at it. A response that
|
|
397
|
+
// arrives as a FAILURE still contradicts — and now arrives INSIDE the window, where the
|
|
398
|
+
// detector can see it at all — while a request still open when the budget genuinely runs out
|
|
399
|
+
// reports unsettled exactly as before, an honest limit rather than an early exit. Costs
|
|
400
|
+
// nothing on the common path, where no request is in flight.
|
|
401
|
+
if (verdict.pass && timeout > 0) {
|
|
402
|
+
const sleep = {
|
|
403
|
+
sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
|
|
404
|
+
};
|
|
405
|
+
const spent = () => timeout - (session.elapsed() - predicateStarted);
|
|
406
|
+
await waitForInFlight(session, since, spent(), sleep);
|
|
407
|
+
// Waiting for the response moved the hazard rather than removing it: the response now lands
|
|
408
|
+
// inside the window BY DESIGN, and the app's re-render happens a task or two later. Close
|
|
409
|
+
// the window in that gap and every channel agrees the app took a successful write and did
|
|
410
|
+
// nothing — `response-ignored`, i.e. `verified:"no"` on a correct app, produced entirely by
|
|
411
|
+
// where we stopped looking. A false accusation is the more damaging direction of error for
|
|
412
|
+
// a verification tool: it sends someone to fix code that is not broken.
|
|
413
|
+
//
|
|
414
|
+
// So the response is not the end of the window; the app's REACTION to it is. Paid only in
|
|
415
|
+
// the shape that would otherwise be accused — a successful mutating write with nothing
|
|
416
|
+
// moved after it — and short enough that a genuinely dropped response is still reported.
|
|
417
|
+
await waitForReaction(session, since, spent(), sleep);
|
|
418
|
+
}
|
|
391
419
|
const r = asRecord(actResult.result);
|
|
392
420
|
if ('boolean' === typeof r['settled'])
|
|
393
421
|
settledOutcome = r['settled'];
|
|
@@ -427,12 +455,28 @@ export const ACT_TOOLS = [
|
|
|
427
455
|
// whose own words were "over a clean capture".
|
|
428
456
|
const gapNote = transportGapNote(windowEvents);
|
|
429
457
|
const impeachingNotes = [impeaching.note, gapNote].filter((n) => n !== undefined);
|
|
458
|
+
const bufferLost = session.lostSince(since);
|
|
459
|
+
// Which loss, as an enum, beside the prose that describes it. Classified here because this is
|
|
460
|
+
// the only place that knows the three apart: our buffer, our transport, and the page's own
|
|
461
|
+
// boundaries. See `CaptureLoss`.
|
|
462
|
+
const losses = [
|
|
463
|
+
...(bufferLost ? [CaptureLoss.BUFFER_LOSS] : []),
|
|
464
|
+
...(gapNote === undefined ? [] : [CaptureLoss.TRANSPORT_GAP]),
|
|
465
|
+
...(impeaching.note === undefined ? [] : [CaptureLoss.BLIND_SPOT]),
|
|
466
|
+
];
|
|
430
467
|
const honesty = buildHonestyBlock({
|
|
431
468
|
grade: gradeOf(gradedLinks),
|
|
432
469
|
attribution: 'window',
|
|
433
|
-
|
|
470
|
+
// Did the buffer lose scarce evidence FROM THIS WINDOW — not "did it evict anything while
|
|
471
|
+
// the action ran", which was the previous rule and which is true on essentially every live
|
|
472
|
+
// page. Age eviction retires everything past 60s on every push, and the churn floor is
|
|
473
|
+
// sacrificed on purpose; neither costs this verdict a single event it needed. In the
|
|
474
|
+
// field this made `unclean_capture` the dominant cause of `unknown`, reported for
|
|
475
|
+
// evictions that happened outside the window they impeached.
|
|
476
|
+
truncated: bufferLost,
|
|
434
477
|
coveragePartial: Coverage.PARTIAL === coverage.coverage,
|
|
435
478
|
...(0 === impeachingNotes.length ? {} : { blindSpots: impeachingNotes }),
|
|
479
|
+
losses,
|
|
436
480
|
});
|
|
437
481
|
const capsuleSaved = await saveFailedAssertCapsule({
|
|
438
482
|
deps,
|
|
@@ -446,8 +490,23 @@ export const ACT_TOOLS = [
|
|
|
446
490
|
// Pass the action: an EMPTY window then reads as "the target does not react" rather than as a
|
|
447
491
|
// clean run — `settled` is satisfied by a page that did nothing, i.e. by a click that missed.
|
|
448
492
|
const acted = asString(args['action']);
|
|
493
|
+
// `prior` matters here for the same reason it matters on the assert path: a SCALE error
|
|
494
|
+
// disagrees with a value the API stated EARLIER in the session, which an action-scoped
|
|
495
|
+
// window can never contain. Without it `unit-mismatch` — the money false green, where a
|
|
496
|
+
// total renders 100x off — is structurally unreachable from `act_and_wait`, which is the
|
|
497
|
+
// tool the product tells agents to reach for first. Our best detector could not fire on our
|
|
498
|
+
// flagship path, and nothing said so: the finding was simply never produced.
|
|
499
|
+
//
|
|
500
|
+
// Learning material only. Attribution is unchanged — findings still come exclusively from
|
|
501
|
+
// `windowEvents`, exactly as on the assert path.
|
|
502
|
+
const prior = since > 0 ? (await session.queryEvents({ since: 0 })).filter((e) => e.t < since) : [];
|
|
503
|
+
//
|
|
504
|
+
// `actionSince` is the window's own floor here, which is the tightest attribution there is —
|
|
505
|
+
// and it is what lets `duplicate-request` claim "one user action was performed" and mean it.
|
|
449
506
|
const contradictions = findContradictions(windowEvents, {
|
|
450
507
|
action: acted,
|
|
508
|
+
prior,
|
|
509
|
+
actionSince: since,
|
|
451
510
|
...session.lastAct.effect(),
|
|
452
511
|
});
|
|
453
512
|
// The single field an agent reads. Everything below it is the evidence it was derived from;
|
|
@@ -470,6 +529,13 @@ export const ACT_TOOLS = [
|
|
|
470
529
|
// `settled` is genuinely optional: a wait that declared no predicate never measured it, and
|
|
471
530
|
// passing `false` there would report "never settled" about something never asked to settle.
|
|
472
531
|
...(settledOutcome === undefined ? {} : { settled: settledOutcome }),
|
|
532
|
+
// What the wait was for and what was still outstanding when it ended. Read only by the
|
|
533
|
+
// UNSETTLED clauses; supplied always because both of them are reachable from here and the
|
|
534
|
+
// window is already in hand.
|
|
535
|
+
unsettled: {
|
|
536
|
+
waitedFor: describeWaitTarget(until),
|
|
537
|
+
stillInFlight: inFlightRequestLabels(windowEvents),
|
|
538
|
+
},
|
|
473
539
|
});
|
|
474
540
|
return withControl(session, {
|
|
475
541
|
...decision,
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
*/
|
|
13
13
|
import { HonestyGrade } from '../honesty/honesty.js';
|
|
14
14
|
import type { Predicate } from '../events/predicate.js';
|
|
15
|
-
export declare const PRESENCE_ONLY_ADVICE = "This predicate only checks element/text presence, not an observable consequence. A locator healed to the wrong element (or a stale render) can satisfy it while the feature is broken. Prefer a { signal } or { net } assertion
|
|
15
|
+
export declare const PRESENCE_ONLY_ADVICE = "This predicate only checks element/text presence, not an observable consequence. A locator healed to the wrong element (or a stale render) can satisfy it while the feature is broken. Prefer a { signal } or { net } assertion (or allOf it with one) so green means the feature actually worked.";
|
|
16
16
|
/**
|
|
17
17
|
* True when the predicate asserts ONLY element/text presence with no signal/net consequence anywhere
|
|
18
18
|
* — the weak pattern worth nudging. A predicate that mixes in a consequence, or that checks something
|
|
@@ -12,24 +12,46 @@
|
|
|
12
12
|
*/
|
|
13
13
|
import { isConsequenceKind, isPresenceKind, PredicateKind } from '@reticlehq/core';
|
|
14
14
|
import { HonestyGrade } from '../honesty/honesty.js';
|
|
15
|
-
export const PRESENCE_ONLY_ADVICE = 'This predicate only checks element/text presence, not an observable consequence. A locator healed to the wrong element (or a stale render) can satisfy it while the feature is broken. Prefer a { signal } or { net } assertion
|
|
16
|
-
function walk(predicate) {
|
|
15
|
+
export const PRESENCE_ONLY_ADVICE = 'This predicate only checks element/text presence, not an observable consequence. A locator healed to the wrong element (or a stale render) can satisfy it while the feature is broken. Prefer a { signal } or { net } assertion (or allOf it with one) so green means the feature actually worked.';
|
|
16
|
+
function walk(predicate, negated = false) {
|
|
17
17
|
if (PredicateKind.ALL_OF === predicate.kind || PredicateKind.ANY_OF === predicate.kind) {
|
|
18
|
-
const subs = predicate.predicates.map(walk);
|
|
18
|
+
const subs = predicate.predicates.map((p) => walk(p, negated));
|
|
19
19
|
return {
|
|
20
20
|
consequence: subs.some((s) => s.consequence),
|
|
21
21
|
presence: subs.some((s) => s.presence),
|
|
22
22
|
};
|
|
23
23
|
}
|
|
24
|
+
// Under a NOT, checking content strengthens nothing, so the exemption below must not apply. The
|
|
25
|
+
// nudge warns that a healed or wrong locator can satisfy the predicate, and for an ABSENCE claim a
|
|
26
|
+
// wrong locator satisfies it trivially: "the error message is gone" is true of a selector that
|
|
27
|
+
// never matched anything in the first place. Negation makes a presence check weaker, not stronger.
|
|
24
28
|
if (PredicateKind.NOT === predicate.kind)
|
|
25
|
-
return walk(predicate.predicate);
|
|
29
|
+
return walk(predicate.predicate, true);
|
|
26
30
|
// Leaf: classify against the single source of truth in core. Kinds that are neither consequence
|
|
27
31
|
// nor presence (route/console/settled/animation) correctly return false for both.
|
|
28
32
|
return {
|
|
29
33
|
consequence: isConsequenceKind(predicate.kind),
|
|
30
|
-
presence: isPresenceKind(predicate.kind),
|
|
34
|
+
presence: isPresenceKind(predicate.kind) && (negated || !assertsElementContent(predicate)),
|
|
31
35
|
};
|
|
32
36
|
}
|
|
37
|
+
/**
|
|
38
|
+
* An element predicate that checks the element's CONTENT, not merely that it is there.
|
|
39
|
+
*
|
|
40
|
+
* The presence nudge exists because "a locator healed to the wrong element, or a stale render, can
|
|
41
|
+
* satisfy this while the feature is broken". That is exactly true of `{ element, role, name }` and
|
|
42
|
+
* exactly false of `{ element, role, name, value: "274.58" }`: a wrong element now has to carry the
|
|
43
|
+
* same value to pass, which is the thing the nudge is warning cannot be relied on.
|
|
44
|
+
*
|
|
45
|
+
* Kept as a nudge-level distinction rather than a new honesty grade. The grade still reads
|
|
46
|
+
* `presence`, which UNDERSTATES what was proved, and understating is the safe direction for a field
|
|
47
|
+
* whose whole job is to qualify a green. Telling an agent that a value check "only checks presence"
|
|
48
|
+
* is the direction that costs something: it argues for replacing an assertion that is already sound.
|
|
49
|
+
*/
|
|
50
|
+
function assertsElementContent(predicate) {
|
|
51
|
+
if (predicate.kind !== PredicateKind.ELEMENT)
|
|
52
|
+
return false;
|
|
53
|
+
return predicate.query?.value !== undefined || predicate.query?.text !== undefined;
|
|
54
|
+
}
|
|
33
55
|
/**
|
|
34
56
|
* True when the predicate asserts ONLY element/text presence with no signal/net consequence anywhere
|
|
35
57
|
* — the weak pattern worth nudging. A predicate that mixes in a consequence, or that checks something
|
|
@@ -1,9 +1,12 @@
|
|
|
1
|
+
import { CaptureLoss } from '@reticlehq/core';
|
|
1
2
|
import { findContradictions } from '../events/contradictions.js';
|
|
2
3
|
import { blindSpotsFromState, buildCoverageStatement, Coverage, impeachesCapture, transportGapNote, } from '../honesty/blind-spots.js';
|
|
3
4
|
import { buildHonestyBlock } from '../honesty/honesty.js';
|
|
4
5
|
import { hasAcceptedWrite } from '../honesty/accepted-write.js';
|
|
5
6
|
import { hasUnreadWriteOutcome } from '../honesty/unread-outcome.js';
|
|
6
7
|
import { decideVerified } from '../honesty/verified.js';
|
|
8
|
+
import { describeWaitTarget } from '../honesty/unsettled.js';
|
|
9
|
+
import { inFlightRequestLabels } from './settle-in-flight.js';
|
|
7
10
|
import { gradeOfPredicate } from './assert-grade.js';
|
|
8
11
|
/**
|
|
9
12
|
* The honesty verdict for a plain `reticle_assert`.
|
|
@@ -48,7 +51,16 @@ observationLost) {
|
|
|
48
51
|
// measured live, `observe` over a wide window reported `unit-mismatch` while `assert` on the same
|
|
49
52
|
// session reported none. Attribution is unchanged: findings still come only from windowEvents.
|
|
50
53
|
const prior = since > 0 ? (await session.queryEvents({ since: 0 })).filter((e) => e.t < since) : [];
|
|
51
|
-
|
|
54
|
+
// The last act, when it falls inside the window being judged. Without it `duplicate-request` counted
|
|
55
|
+
// `method + url` across whatever window the caller asked for, so two legitimate separate saves to one
|
|
56
|
+
// endpoint were reported as a double submit — the instrument accusing the app of something nobody
|
|
57
|
+
// could show it did. An assert taken over a window that predates the act attributes nothing, and the
|
|
58
|
+
// rule then stays quiet.
|
|
59
|
+
const actCursor = session.lastAct.cursor();
|
|
60
|
+
const contradictions = findContradictions(windowEvents, {
|
|
61
|
+
prior,
|
|
62
|
+
...(actCursor !== undefined && actCursor >= since ? { actionSince: actCursor } : {}),
|
|
63
|
+
});
|
|
52
64
|
// Only a spot that IMPEACHES the capture downgrades a verdict. A structural boundary (virtualized
|
|
53
65
|
// rows, a cross-origin frame) is reported as coverage and must not impugn what WAS observed.
|
|
54
66
|
const impeaching = buildCoverageStatement(spots.filter((sp) => impeachesCapture(sp.kind)));
|
|
@@ -68,10 +80,22 @@ observationLost) {
|
|
|
68
80
|
attribution: 'window',
|
|
69
81
|
coveragePartial: Coverage.PARTIAL === statement.coverage,
|
|
70
82
|
...(0 === impeachingNotes.length ? {} : { blindSpots: impeachingNotes }),
|
|
83
|
+
// Which loss, as an enum, beside the prose. `assert` observes an already-open window and never
|
|
84
|
+
// consults the ring buffer's health, so `buffer_loss` is not one of its answers.
|
|
85
|
+
losses: [
|
|
86
|
+
...(gap === undefined ? [] : [CaptureLoss.TRANSPORT_GAP]),
|
|
87
|
+
...(impeaching.note === undefined ? [] : [CaptureLoss.BLIND_SPOT]),
|
|
88
|
+
],
|
|
71
89
|
}),
|
|
72
90
|
contradictions,
|
|
73
91
|
...(outcomePending ? { outcomePending } : {}),
|
|
74
92
|
...(outcomeUnread ? { outcomeUnread } : {}),
|
|
93
|
+
// Same detail the act path supplies: this route reaches UNSETTLED through an absence-derived
|
|
94
|
+
// contradiction, and "the window closed before the app finished" is no more actionable here.
|
|
95
|
+
unsettled: {
|
|
96
|
+
waitedFor: describeWaitTarget(predicate),
|
|
97
|
+
stillInFlight: inFlightRequestLabels(windowEvents),
|
|
98
|
+
},
|
|
75
99
|
});
|
|
76
100
|
return { decision: decision, contradictions, coverage };
|
|
77
101
|
}
|
|
@@ -38,6 +38,10 @@ export const CONTRACT_TOOLS = [
|
|
|
38
38
|
{
|
|
39
39
|
name: ReticleTool.CAPABILITIES,
|
|
40
40
|
description: 'The app-advertised testable surface (reticle.describe): testids, signals, stores, and named flows. Call this first to learn what to assert on without reading source. Pass { fromDisk:true } to read the git-checked .reticle/contract.json instead of the live session (a fresh agent can learn the surface with no browser attached).',
|
|
41
|
+
// Every core tool carries one, so an agent reading the surface never has to guess the shape.
|
|
42
|
+
// `fromDisk` is the example deliberately: it is the call that works with nothing attached, which
|
|
43
|
+
// is the state a fresh agent is actually in when it first reads this list.
|
|
44
|
+
example: { [FROM_DISK_ARG]: true },
|
|
41
45
|
inputSchema: { [FROM_DISK_ARG]: z.boolean().optional(), ...sessionIdShape },
|
|
42
46
|
outputSchema: {
|
|
43
47
|
testids: z.array(z.string()),
|
|
@@ -69,7 +69,7 @@ export function buildDynamicTools(allTools, profile) {
|
|
|
69
69
|
// `full` advertises all 46 tools DIRECTLY and carries no meta-tools at all, so
|
|
70
70
|
// reticle_run does not exist there. Saying otherwise sends the agent to a tool the
|
|
71
71
|
// profile does not have.
|
|
72
|
-
note: `The profile is read once at daemon startup
|
|
72
|
+
note: `The profile is read once at daemon startup: change ${TOOL_PROFILE_ENV} and restart the daemon, or it has no effect. Under this profile every unadvertised tool is reachable through reticle_run; under \`full\` there are no meta-tools because every tool is advertised directly.`,
|
|
73
73
|
},
|
|
74
74
|
};
|
|
75
75
|
const reticleTools = {
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
* on stable, human-authored substrings of the thrown messages (see session-manager.ts, the flow/
|
|
9
9
|
* baseline stores). No clock, no IO — unit-testable in isolation.
|
|
10
10
|
*/
|
|
11
|
+
import { RefusalReason } from '@reticlehq/core';
|
|
11
12
|
/** The recovery hints, named so they are not free strings and can be asserted in tests. */
|
|
12
13
|
export declare const RECOVERY: {
|
|
13
14
|
readonly NO_SESSION: string;
|
|
@@ -40,6 +41,15 @@ export declare const RECOVERY: {
|
|
|
40
41
|
* saying a server is running and the recovery saying to go start one.
|
|
41
42
|
*/
|
|
42
43
|
export declare function recoveryFor(message: string): string | undefined;
|
|
44
|
+
/**
|
|
45
|
+
* WHY a tool refused, as one of the six buckets in core's `RefusalReason`.
|
|
46
|
+
*
|
|
47
|
+
* Reads the SAME rules `buildErrorPayload` reads, in the same order, so the reason we report and the
|
|
48
|
+
* advice the agent got can never describe different things. Anything unrecognised is `other`, which
|
|
49
|
+
* is the bucket that says the classifier has a blind spot — and a growing `other` is a finding about
|
|
50
|
+
* this table rather than about anybody's app.
|
|
51
|
+
*/
|
|
52
|
+
export declare function refusalReasonFor(rawMessage: string): RefusalReason;
|
|
43
53
|
/**
|
|
44
54
|
* A serialized zod issue array, rendered as a sentence. Anything else is returned untouched.
|
|
45
55
|
*
|