@reticlehq/server 2.8.0 → 2.10.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/capsule-store.d.ts +2 -2
- 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-parse.d.ts +1 -1
- package/dist/cli/cli-parse.js +1 -1
- 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/crawl/crawl.d.ts +7 -2
- package/dist/crawl/crawl.js +52 -9
- package/dist/ee/audit-log.js +2 -2
- package/dist/events/body-failures.d.ts +2 -2
- package/dist/events/contradiction-folds.d.ts +45 -0
- package/dist/events/contradiction-folds.js +42 -0
- package/dist/events/contradictions.d.ts +51 -1
- package/dist/events/contradictions.js +26 -7
- package/dist/events/declared.d.ts +45 -0
- package/dist/events/declared.js +73 -0
- package/dist/events/echo-mismatch.d.ts +9 -3
- 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/events/stale-response.d.ts +2 -3
- package/dist/events/stale-response.js +49 -2
- package/dist/events/unit-mismatch.d.ts +2 -2
- 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/flows/verify-change-tools.js +1 -1
- 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 +67 -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 +25 -0
- package/dist/index.js +60 -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 +16 -2
- package/dist/init/mcp.js +23 -1
- 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/license/license.d.ts +69 -4
- package/dist/license/license.js +84 -15
- package/dist/mcp/mcp-proxy.d.ts +7 -93
- package/dist/mcp/mcp-proxy.js +183 -250
- package/dist/mcp/mcp.d.ts +27 -2
- package/dist/mcp/mcp.js +77 -36
- package/dist/mcp/proxy-daemon-probe.d.ts +8 -0
- package/dist/mcp/proxy-daemon-probe.js +48 -0
- package/dist/mcp/proxy-handshake.d.ts +82 -1
- package/dist/mcp/proxy-handshake.js +110 -3
- 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/mcp/sse-frame-parser.d.ts +19 -0
- package/dist/mcp/sse-frame-parser.js +41 -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 +55 -2
- package/dist/session/no-session-diagnosis.js +199 -22
- 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 +172 -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-readiness.js +1 -1
- 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 +29 -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 +57 -0
- package/dist/telemetry/instrumentation-stall.js +103 -0
- package/dist/telemetry/license-activation.d.ts +48 -0
- package/dist/telemetry/license-activation.js +57 -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 +23 -3
- 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 +17 -8
- package/dist/tools/error-recovery.js +10 -6
- package/dist/tools/feedback-tools.js +31 -6
- package/dist/tools/invoke-tool.d.ts +1 -1
- package/dist/tools/invoke-tool.js +30 -8
- 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 +38 -3
- package/dist/tools/tool-names.d.ts +2 -0
- package/dist/tools/tool-names.js +2 -0
- package/dist/tools/tool-surface.d.ts +60 -0
- package/dist/tools/tool-surface.js +84 -19
- package/dist/tools/tools.js +61 -5
- 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 +6 -4
- package/docs/architecture.md +6 -0
- package/docs/benchmarks.md +60 -12
- package/docs/best-practices.mdx +11 -6
- package/docs/capabilities.mdx +204 -57
- 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 +20 -4
- 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 +31 -2
- package/docs/desktop.mdx +276 -80
- package/docs/docs.json +13 -12
- package/docs/enterprise.md +9 -3
- package/docs/faq.mdx +49 -35
- package/docs/fixtures.md +2 -0
- package/docs/flows.md +7 -5
- package/docs/for-agents.md +3 -3
- package/docs/frameworks.mdx +46 -7
- package/docs/gate-plan.md +4 -2
- package/docs/gates.md +8 -6
- 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 +63 -23
- 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 +33 -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 +35 -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 +55 -24
- package/docs/tools-wait-for.mdx +34 -15
- package/docs/troubleshooting.mdx +80 -10
- package/docs/usage.md +46 -24
- 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
|
@@ -262,7 +262,7 @@ export const OBSERVE_TOOLS = [
|
|
|
262
262
|
// undocumented here, including `route` — and "did submitting the login form navigate away"
|
|
263
263
|
// is the most common thing an agent wants to assert. A field report reached us from an agent
|
|
264
264
|
// that guessed `urlContains` on route (net's spelling) and got unrecognized_keys.
|
|
265
|
-
'Predicate to evaluate. Kinds: { signal, name } { net, urlContains|method|status|count } ' +
|
|
265
|
+
'Predicate to evaluate. Kinds: { signal, name } { net, urlContains|method|status|count|bodyContains } ' +
|
|
266
266
|
'{ state, path|equals } { route, pathname (exact) | contains (path+query+hash) } ' +
|
|
267
267
|
'{ element, testid|role|text } { text } { console, level|absent } { animation, name } ' +
|
|
268
268
|
'{ settled } — combine with { allOf | anyOf | not }. Prefer a signal/net/state consequence ' +
|
|
@@ -386,6 +386,10 @@ export const OBSERVE_TOOLS = [
|
|
|
386
386
|
.nonnegative()
|
|
387
387
|
.optional()
|
|
388
388
|
.describe('Keep only the most recent N matching calls (older are dropped and counted in droppedOldest) — cuts tokens on a wide window. Defaults to 200 when omitted; pass a higher number for more, or scope with since/until.'),
|
|
389
|
+
bodies: z
|
|
390
|
+
.boolean()
|
|
391
|
+
.optional()
|
|
392
|
+
.describe('Include request/response bodies (default true). Pass false for a body-free listing — method, url, status, timing only — for the common "did POST /x return 200?" read. Bodies dominate the payload, so this cuts the cheap case by a large factor.'),
|
|
389
393
|
...sessionIdShape,
|
|
390
394
|
},
|
|
391
395
|
outputSchema: {
|
|
@@ -411,6 +415,8 @@ export const OBSERVE_TOOLS = [
|
|
|
411
415
|
const status = asNumber(args['status']);
|
|
412
416
|
const ok = 'boolean' === typeof args['ok'] ? args['ok'] : undefined;
|
|
413
417
|
const limit = asNumber(args['limit']);
|
|
418
|
+
// Default true keeps the current shape; `bodies: false` returns the body-free listing (#401).
|
|
419
|
+
const bodies = 'boolean' === typeof args['bodies'] ? args['bodies'] : true;
|
|
414
420
|
const buffer = bufferEnvelope(session);
|
|
415
421
|
// Completed calls + unresolved in-flight requests (a hung request shows as pending).
|
|
416
422
|
const allNet = reconcileNet(await session.queryEvents({
|
|
@@ -425,14 +431,14 @@ export const OBSERVE_TOOLS = [
|
|
|
425
431
|
}
|
|
426
432
|
// Default the cap so an omitted `limit` can't dump a whole flooded session (since defaults to 0).
|
|
427
433
|
const { events: budgeted, droppedOldest } = applyEventBudget(matched, limit ?? DEFAULT_QUERY_LIMIT);
|
|
428
|
-
const calls = budgeted.map(projectNetCall);
|
|
434
|
+
const calls = budgeted.map((e) => projectNetCall(e, bodies));
|
|
429
435
|
// A zero-match FILTER already reports what did fire (netEmptyHint above). Zero calls at all
|
|
430
436
|
// fell through as a bare `[]`, which is indistinguishable from an observer that is not
|
|
431
437
|
// recording — and those need opposite responses. Say the look happened.
|
|
432
438
|
return withSizeCost(noteEmptyRead({
|
|
433
439
|
calls,
|
|
434
440
|
...(droppedOldest > 0 ? { total: matched.length, droppedOldest } : {}),
|
|
435
|
-
...bodiesNotCaptured(calls),
|
|
441
|
+
...(bodies ? bodiesNotCaptured(calls) : {}),
|
|
436
442
|
...buffer,
|
|
437
443
|
}, 'calls', { noun: 'network calls' }));
|
|
438
444
|
},
|
|
@@ -30,6 +30,10 @@ import type { SettleSource } from './settle-in-flight.js';
|
|
|
30
30
|
* True only for the accusable shape: a SUCCESSFUL MUTATING request settled, and nothing has moved. A
|
|
31
31
|
* read that changed nothing is a prefetch, a failed write is a different finding entirely, and an
|
|
32
32
|
* action that already moved the UI has nothing to wait for — none of them buy a grace period.
|
|
33
|
+
*
|
|
34
|
+
* "Mutating" is read from the same constant the accuser uses rather than restated here. Restating it
|
|
35
|
+
* is how `IPC` came to be absent from this list and present in that one, so on a desktop app every
|
|
36
|
+
* successful write skipped the grace and a correct app was told it had ignored its own response.
|
|
33
37
|
*/
|
|
34
38
|
export declare function awaitsReaction(events: readonly {
|
|
35
39
|
type: string;
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
* Deliberately short. This covers a task-boundary re-render, not a slow app: if nothing has moved
|
|
24
24
|
* within the grace, "ignored" has become an honest description and the finding should stand.
|
|
25
25
|
*/
|
|
26
|
-
import { EventType } from '@reticlehq/core';
|
|
26
|
+
import { EventType, MUTATING_METHODS } from '@reticlehq/core';
|
|
27
27
|
/**
|
|
28
28
|
* How long to let the app react. A React commit after a resolved fetch is one or two task hops; this
|
|
29
29
|
* is generous against that and still far too short to paper over an app that genuinely dropped the
|
|
@@ -32,7 +32,6 @@ import { EventType } from '@reticlehq/core';
|
|
|
32
32
|
const REACTION_GRACE_MS = 300;
|
|
33
33
|
/** Poll cadence — the same as the settle wait, for the same reason. */
|
|
34
34
|
const POLL_MS = 50;
|
|
35
|
-
const MUTATING = ['POST', 'PUT', 'PATCH', 'DELETE'];
|
|
36
35
|
/** Movement that would clear `response-ignored`; mirrors `uiAdvanced` in the detector. */
|
|
37
36
|
const MOVEMENT = new Set([
|
|
38
37
|
EventType.DOM_ADDED,
|
|
@@ -51,6 +50,10 @@ function moved(events) {
|
|
|
51
50
|
* True only for the accusable shape: a SUCCESSFUL MUTATING request settled, and nothing has moved. A
|
|
52
51
|
* read that changed nothing is a prefetch, a failed write is a different finding entirely, and an
|
|
53
52
|
* action that already moved the UI has nothing to wait for — none of them buy a grace period.
|
|
53
|
+
*
|
|
54
|
+
* "Mutating" is read from the same constant the accuser uses rather than restated here. Restating it
|
|
55
|
+
* is how `IPC` came to be absent from this list and present in that one, so on a desktop app every
|
|
56
|
+
* successful write skipped the grace and a correct app was told it had ignored its own response.
|
|
54
57
|
*/
|
|
55
58
|
export function awaitsReaction(events) {
|
|
56
59
|
if (moved(events))
|
|
@@ -60,7 +63,7 @@ export function awaitsReaction(events) {
|
|
|
60
63
|
return false;
|
|
61
64
|
const method = 'string' === typeof e.data['method'] ? e.data['method'].toUpperCase() : '';
|
|
62
65
|
const ok = e.data['ok'];
|
|
63
|
-
return
|
|
66
|
+
return MUTATING_METHODS.includes(method) && true === ok;
|
|
64
67
|
});
|
|
65
68
|
}
|
|
66
69
|
/**
|
package/dist/tools/read-tools.js
CHANGED
|
@@ -39,6 +39,15 @@ function unanchoredWarning(count) {
|
|
|
39
39
|
const HUD_LEVELS = ['info', 'warn', 'error'];
|
|
40
40
|
const HUD_LEVEL_LIST = HUD_LEVELS.join(' | ');
|
|
41
41
|
const hudLevelEnum = z.enum(HUD_LEVELS);
|
|
42
|
+
/**
|
|
43
|
+
* How deep an unscoped `reticle_state` read goes before collapsing to size markers.
|
|
44
|
+
*
|
|
45
|
+
* 3 keeps every store name and every top-level key visible — enough to see what exists and to name a
|
|
46
|
+
* `path` — while collapsing the collections that make an unscoped read enormous. Measured on the
|
|
47
|
+
* bench fixture: unbounded 10,119 B, depth 4 1,323 B, depth 3 596 B, depth 2 322 B. Two loses the
|
|
48
|
+
* per-store keys, which is the orientation the next call needs.
|
|
49
|
+
*/
|
|
50
|
+
const DEFAULT_STATE_DEPTH = 1;
|
|
42
51
|
export const READ_TOOLS = [
|
|
43
52
|
{
|
|
44
53
|
name: ReticleTool.BASELINE_SAVE,
|
|
@@ -395,7 +404,44 @@ export const READ_TOOLS = [
|
|
|
395
404
|
return { ...root, storeNames: names };
|
|
396
405
|
}
|
|
397
406
|
if (path === undefined && depth === undefined) {
|
|
398
|
-
|
|
407
|
+
// An unscoped read used to hand back the WHOLE store tree: 10,119 bytes on this repo's own
|
|
408
|
+
// fixture. A tool result is not paid once — it stays in the conversation and is re-sent on
|
|
409
|
+
// every later turn, so one such read cost roughly 34,000 tokens across a 16-turn run.
|
|
410
|
+
//
|
|
411
|
+
// The VALUES are bounded; the ENVELOPE is not touched. `{ stores, storeNames, component }`
|
|
412
|
+
// is the documented shape and callers depend on it — a first attempt applied the bound by
|
|
413
|
+
// routing through the path selector, which silently changed the reply to `{ found, value }`
|
|
414
|
+
// and took the component projection and the bridge round-trip with it. Capping in place
|
|
415
|
+
// keeps every store name and the component read exactly where they were; only collections
|
|
416
|
+
// deeper than the bound collapse to a marker like "[Array(40)]", which still says what is
|
|
417
|
+
// there and how much of it.
|
|
418
|
+
//
|
|
419
|
+
// A default, not a cap: any explicit `depth` is honoured at any value, and `path` reads a
|
|
420
|
+
// sub-tree at full fidelity. Nothing became unreachable, only cheaper by default.
|
|
421
|
+
const stores = root.stores;
|
|
422
|
+
const bounded = 'object' === typeof stores && null !== stores
|
|
423
|
+
? Object.fromEntries(Object.entries(stores).map(([k, v]) => [k, capDepth(v, DEFAULT_STATE_DEPTH)]))
|
|
424
|
+
: undefined;
|
|
425
|
+
// Say that it was bounded, and only when it actually was. A read that quietly hands back
|
|
426
|
+
// less than it was asked for is the shape of a false green: the agent sees
|
|
427
|
+
// `deployments: "[Array(40)]"`, concludes it has read the store, and asserts over a summary.
|
|
428
|
+
// The marker alone is suggestive; this makes it explicit and names the way to the real value.
|
|
429
|
+
const trimmed = bounded !== undefined && JSON.stringify(bounded) !== JSON.stringify(stores);
|
|
430
|
+
return {
|
|
431
|
+
...root,
|
|
432
|
+
...(bounded === undefined ? {} : { stores: bounded }),
|
|
433
|
+
storeNames: names,
|
|
434
|
+
...(trimmed
|
|
435
|
+
? {
|
|
436
|
+
truncation: {
|
|
437
|
+
note: `bounded to depth ${String(DEFAULT_STATE_DEPTH)} per store — this is NOT the ` +
|
|
438
|
+
'whole store. Values shown as "[Array(n)]" or "{…n keys}" were collapsed; read ' +
|
|
439
|
+
'one with { store, path } (e.g. { store:"app", path:"deployments.0" }) or pass ' +
|
|
440
|
+
'an explicit `depth` for more.',
|
|
441
|
+
},
|
|
442
|
+
}
|
|
443
|
+
: {}),
|
|
444
|
+
};
|
|
399
445
|
}
|
|
400
446
|
// Back-compat: an older browser returned the whole store; scope it here (may already be
|
|
401
447
|
// size-truncated in transit for a very large store — that is the limitation this fix removes
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/** One candidate the browser returned for a target query. */
|
|
2
|
+
export interface TargetCandidate {
|
|
3
|
+
ref?: unknown;
|
|
4
|
+
role?: unknown;
|
|
5
|
+
name?: unknown;
|
|
6
|
+
visible?: unknown;
|
|
7
|
+
}
|
|
8
|
+
/** What the resolution produced: a ref to act on, or the reason there isn't one. */
|
|
9
|
+
export type TargetResolution = {
|
|
10
|
+
readonly kind: 'ref';
|
|
11
|
+
readonly ref: string;
|
|
12
|
+
} | {
|
|
13
|
+
readonly kind: 'error';
|
|
14
|
+
readonly message: string;
|
|
15
|
+
};
|
|
16
|
+
/**
|
|
17
|
+
* Turn a target QUERY into the single ref an action can be performed on.
|
|
18
|
+
*
|
|
19
|
+
* `act_and_wait` took only a `ref`, so every verification cost a `reticle_query` turn first just to
|
|
20
|
+
* learn one string. The advertised tool surface is re-sent on every turn, so that extra turn was not
|
|
21
|
+
* a small cost: measured on the wire, a two-turn verification spent 10,756 of its 11,235 tokens on
|
|
22
|
+
* schema and 479 on the actual answers. Removing the turn removes half the schema bill.
|
|
23
|
+
*
|
|
24
|
+
* Ambiguity is an ERROR, never a pick. Choosing "the first match" would let a verification act on a
|
|
25
|
+
* different element than the author meant and still report success, which is the false green this
|
|
26
|
+
* product exists to prevent — and the failure is invisible, because the verdict describes the
|
|
27
|
+
* element it DID act on. The message names the candidates so the caller can narrow rather than guess.
|
|
28
|
+
*
|
|
29
|
+
* Invisible matches are filtered before counting: a hidden duplicate is a common way for a
|
|
30
|
+
* legitimately unambiguous query to look ambiguous, and refusing on it would push callers back to
|
|
31
|
+
* the two-turn path for no gain.
|
|
32
|
+
*/
|
|
33
|
+
export declare function resolveTargetRef(candidates: readonly unknown[]): TargetResolution;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { asRecord } from './tools-helpers.js';
|
|
2
|
+
/**
|
|
3
|
+
* Turn a target QUERY into the single ref an action can be performed on.
|
|
4
|
+
*
|
|
5
|
+
* `act_and_wait` took only a `ref`, so every verification cost a `reticle_query` turn first just to
|
|
6
|
+
* learn one string. The advertised tool surface is re-sent on every turn, so that extra turn was not
|
|
7
|
+
* a small cost: measured on the wire, a two-turn verification spent 10,756 of its 11,235 tokens on
|
|
8
|
+
* schema and 479 on the actual answers. Removing the turn removes half the schema bill.
|
|
9
|
+
*
|
|
10
|
+
* Ambiguity is an ERROR, never a pick. Choosing "the first match" would let a verification act on a
|
|
11
|
+
* different element than the author meant and still report success, which is the false green this
|
|
12
|
+
* product exists to prevent — and the failure is invisible, because the verdict describes the
|
|
13
|
+
* element it DID act on. The message names the candidates so the caller can narrow rather than guess.
|
|
14
|
+
*
|
|
15
|
+
* Invisible matches are filtered before counting: a hidden duplicate is a common way for a
|
|
16
|
+
* legitimately unambiguous query to look ambiguous, and refusing on it would push callers back to
|
|
17
|
+
* the two-turn path for no gain.
|
|
18
|
+
*/
|
|
19
|
+
export function resolveTargetRef(candidates) {
|
|
20
|
+
const all = candidates.map((c) => asRecord(c));
|
|
21
|
+
const visible = all.filter((c) => c.visible !== false);
|
|
22
|
+
const usable = visible.length > 0 ? visible : all;
|
|
23
|
+
if (0 === usable.length) {
|
|
24
|
+
return {
|
|
25
|
+
kind: 'error',
|
|
26
|
+
message: 'target matched no element. Nothing was acted on and no verdict is possible — widen the ' +
|
|
27
|
+
'query, or take a reticle_snapshot to see what is actually on the page.',
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
if (usable.length > 1) {
|
|
31
|
+
const named = usable
|
|
32
|
+
.slice(0, 5)
|
|
33
|
+
.map((c) => {
|
|
34
|
+
const role = 'string' === typeof c.role ? c.role : '?';
|
|
35
|
+
const name = 'string' === typeof c.name ? c.name : '';
|
|
36
|
+
const ref = 'string' === typeof c.ref ? c.ref : '?';
|
|
37
|
+
return name.length > 0 ? `${ref} (${role} "${name}")` : `${ref} (${role})`;
|
|
38
|
+
})
|
|
39
|
+
.join(', ');
|
|
40
|
+
return {
|
|
41
|
+
kind: 'error',
|
|
42
|
+
message: `target matched ${String(usable.length)} elements and an action must not guess between ` +
|
|
43
|
+
`them: ${named}. Narrow the query (add role/name/testid or a scope), or pass an explicit ` +
|
|
44
|
+
'`ref` from reticle_query.',
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
const only = usable[0];
|
|
48
|
+
const ref = only === undefined ? undefined : only.ref;
|
|
49
|
+
if ('string' !== typeof ref || 0 === ref.length) {
|
|
50
|
+
return { kind: 'error', message: 'target matched an element with no usable ref.' };
|
|
51
|
+
}
|
|
52
|
+
return { kind: 'ref', ref };
|
|
53
|
+
}
|
|
@@ -62,6 +62,21 @@ export declare function inFlightRequestLabels(events: readonly {
|
|
|
62
62
|
type: string;
|
|
63
63
|
data: Record<string, unknown>;
|
|
64
64
|
}[]): string[];
|
|
65
|
+
/**
|
|
66
|
+
* Endpoints called MORE THAN ONCE in this window, busiest first, as "METHOD url ×N".
|
|
67
|
+
*
|
|
68
|
+
* The other half of an honest `unsettled`. A retrying query against a dead backend leaves nothing in
|
|
69
|
+
* flight — every attempt comes back, then the next one fires — so the verdict fell through to "what
|
|
70
|
+
* kept the page busy was its own churn (a poll, a timer, an animation)", which names three things
|
|
71
|
+
* and points at none of them. Measured in the field: on an app with a background retry loop
|
|
72
|
+
* `act_and_wait` could never return a pass, and nothing in the verdict said which endpoint to look
|
|
73
|
+
* at. This does not judge the repetition — a poll is legitimate — it reports it, which is all an
|
|
74
|
+
* agent needs to tell "my assertion was wrong" from "this app never quiets".
|
|
75
|
+
*/
|
|
76
|
+
export declare function repeatedRequestLabels(events: readonly {
|
|
77
|
+
type: string;
|
|
78
|
+
data: Record<string, unknown>;
|
|
79
|
+
}[]): string[];
|
|
65
80
|
export interface SettleResult {
|
|
66
81
|
/** True when nothing this window started is still outstanding. */
|
|
67
82
|
settled: boolean;
|
|
@@ -27,7 +27,16 @@
|
|
|
27
27
|
* - still in flight when the budget genuinely runs out → `unsettled`, as before, but now an honest
|
|
28
28
|
* limit instead of an early exit.
|
|
29
29
|
*/
|
|
30
|
-
import { EventType } from '@reticlehq/core';
|
|
30
|
+
import { EventType, isDevToolingUrl } from '@reticlehq/core';
|
|
31
|
+
/**
|
|
32
|
+
* The dev toolchain talking about ITSELF is not the app finishing its work.
|
|
33
|
+
*
|
|
34
|
+
* `evalSettled` and `findContradictions` both drop it already; this file read the raw window, so the
|
|
35
|
+
* exclusion was cosmetic exactly where it decides a verdict. Reported from a real drive: a
|
|
36
|
+
* deliberately disabled control — no state change, no storage change, zero application requests —
|
|
37
|
+
* was graded on the strength of a Next webpack hot-update the same payload printed as ignored.
|
|
38
|
+
*/
|
|
39
|
+
const isDevTooling = (data) => isDevToolingUrl('string' === typeof data['url'] ? data['url'] : undefined);
|
|
31
40
|
/** How often to re-read the buffer. Short enough to return promptly, long enough not to spin. */
|
|
32
41
|
const POLL_MS = 50;
|
|
33
42
|
const idOf = (data) => 'string' === typeof data['id'] ? data['id'] : undefined;
|
|
@@ -49,7 +58,7 @@ export function inFlightRequestIds(events) {
|
|
|
49
58
|
}
|
|
50
59
|
const open = [];
|
|
51
60
|
for (const e of events) {
|
|
52
|
-
if (e.type !== EventType.NET_PENDING)
|
|
61
|
+
if (e.type !== EventType.NET_PENDING || isDevTooling(e.data))
|
|
53
62
|
continue;
|
|
54
63
|
const id = idOf(e.data);
|
|
55
64
|
if (id !== undefined && !settled.has(id) && !open.includes(id))
|
|
@@ -81,6 +90,37 @@ export function inFlightRequestLabels(events) {
|
|
|
81
90
|
}
|
|
82
91
|
return labels;
|
|
83
92
|
}
|
|
93
|
+
/** Enough to recognise the loop; a busy dashboard must not spend the verdict on a URL list. */
|
|
94
|
+
const MAX_REPEATED = 3;
|
|
95
|
+
/** One call is traffic. Two to the same endpoint inside one window is a repeat. */
|
|
96
|
+
const MIN_REPEATS = 2;
|
|
97
|
+
/**
|
|
98
|
+
* Endpoints called MORE THAN ONCE in this window, busiest first, as "METHOD url ×N".
|
|
99
|
+
*
|
|
100
|
+
* The other half of an honest `unsettled`. A retrying query against a dead backend leaves nothing in
|
|
101
|
+
* flight — every attempt comes back, then the next one fires — so the verdict fell through to "what
|
|
102
|
+
* kept the page busy was its own churn (a poll, a timer, an animation)", which names three things
|
|
103
|
+
* and points at none of them. Measured in the field: on an app with a background retry loop
|
|
104
|
+
* `act_and_wait` could never return a pass, and nothing in the verdict said which endpoint to look
|
|
105
|
+
* at. This does not judge the repetition — a poll is legitimate — it reports it, which is all an
|
|
106
|
+
* agent needs to tell "my assertion was wrong" from "this app never quiets".
|
|
107
|
+
*/
|
|
108
|
+
export function repeatedRequestLabels(events) {
|
|
109
|
+
const counts = new Map();
|
|
110
|
+
for (const e of events) {
|
|
111
|
+
if (e.type !== EventType.NET_REQUEST || isDevTooling(e.data))
|
|
112
|
+
continue;
|
|
113
|
+
const method = 'string' === typeof e.data['method'] ? e.data['method'] : 'request';
|
|
114
|
+
const url = 'string' === typeof e.data['url'] ? e.data['url'] : 'an unreported url';
|
|
115
|
+
const key = `${method} ${url}`;
|
|
116
|
+
counts.set(key, (counts.get(key) ?? 0) + 1);
|
|
117
|
+
}
|
|
118
|
+
return [...counts.entries()]
|
|
119
|
+
.filter(([, count]) => count >= MIN_REPEATS)
|
|
120
|
+
.sort(([, a], [, b]) => b - a)
|
|
121
|
+
.slice(0, MAX_REPEATED)
|
|
122
|
+
.map(([key, count]) => `${key} ×${String(count)}`);
|
|
123
|
+
}
|
|
84
124
|
/**
|
|
85
125
|
* Wait, bounded by `budgetMs`, for the requests this window started to come back.
|
|
86
126
|
*
|
|
@@ -21,6 +21,7 @@ export type SnapshotDeltaMode = (typeof SnapshotDeltaMode)[keyof typeof Snapshot
|
|
|
21
21
|
interface SnapshotDelta {
|
|
22
22
|
added: string[];
|
|
23
23
|
removed: string[];
|
|
24
|
+
changed: string[];
|
|
24
25
|
addedCount: number;
|
|
25
26
|
removedCount: number;
|
|
26
27
|
}
|
|
@@ -45,6 +46,8 @@ export declare function snapshotDelta(prevTree: string | undefined, nextTree: st
|
|
|
45
46
|
export declare class SnapshotCache {
|
|
46
47
|
#private;
|
|
47
48
|
constructor(max?: number);
|
|
49
|
+
/** True when an entry exists for this key (regardless of route match). */
|
|
50
|
+
has(key: string): boolean;
|
|
48
51
|
/** Last tree for this key IF the route still matches; undefined when absent or route changed. */
|
|
49
52
|
recall(key: string, route: string): string | undefined;
|
|
50
53
|
remember(key: string, route: string, tree: string): void;
|
|
@@ -63,6 +63,17 @@ function deltaLines(tree) {
|
|
|
63
63
|
* buttons — and the count is the only thing that says how many left. Emitting the ORIGINAL text is
|
|
64
64
|
* what finally tells them apart: with refs, "which twelve" has an answer.
|
|
65
65
|
*/
|
|
66
|
+
/** Extract the ref marker value from a text line, or undefined if absent. */
|
|
67
|
+
function refOf(text) {
|
|
68
|
+
return REF_MARKER.exec(text)?.[0]?.trim();
|
|
69
|
+
}
|
|
70
|
+
/** Extract the role (first word after the bullet) from a snapshot line — the structural type that must match. */
|
|
71
|
+
function roleOf(text) {
|
|
72
|
+
return (text
|
|
73
|
+
.trimStart()
|
|
74
|
+
.replace(/^-\s*/, '')
|
|
75
|
+
.split(/[\s"(]/)[0] ?? '');
|
|
76
|
+
}
|
|
66
77
|
function diffKeyed(prev, next) {
|
|
67
78
|
const bucket = (lines) => {
|
|
68
79
|
const map = new Map();
|
|
@@ -96,6 +107,39 @@ function diffKeyed(prev, next) {
|
|
|
96
107
|
}
|
|
97
108
|
return { removed, added };
|
|
98
109
|
}
|
|
110
|
+
/**
|
|
111
|
+
* Same ref AND same role in both added and removed = value change, not a structural
|
|
112
|
+
* arrival/departure. Refs alone are not stable identities — they are re-minted on re-render — so a
|
|
113
|
+
* removed button and an added textbox sharing a ref must stay in their respective lists.
|
|
114
|
+
*/
|
|
115
|
+
function extractChanged(added, removed) {
|
|
116
|
+
const addedByRef = new Map();
|
|
117
|
+
for (const [i, text] of added.entries()) {
|
|
118
|
+
const ref = refOf(text);
|
|
119
|
+
if (ref !== undefined)
|
|
120
|
+
addedByRef.set(ref, { idx: i, text, role: roleOf(text) });
|
|
121
|
+
}
|
|
122
|
+
const changedIndicesInAdded = new Set();
|
|
123
|
+
const changedIndicesInRemoved = new Set();
|
|
124
|
+
const changed = [];
|
|
125
|
+
for (const [i, text] of removed.entries()) {
|
|
126
|
+
const ref = refOf(text);
|
|
127
|
+
if (ref === undefined)
|
|
128
|
+
continue;
|
|
129
|
+
const match = addedByRef.get(ref);
|
|
130
|
+
if (match !== undefined && match.role === roleOf(text)) {
|
|
131
|
+
changed.push(match.text);
|
|
132
|
+
changedIndicesInAdded.add(match.idx);
|
|
133
|
+
changedIndicesInRemoved.add(i);
|
|
134
|
+
addedByRef.delete(ref);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return {
|
|
138
|
+
added: added.filter((_, i) => !changedIndicesInAdded.has(i)),
|
|
139
|
+
removed: removed.filter((_, i) => !changedIndicesInRemoved.has(i)),
|
|
140
|
+
changed,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
99
143
|
/**
|
|
100
144
|
* Where focus sits now: the line to show, plus the identity to compare it BY.
|
|
101
145
|
*
|
|
@@ -123,7 +167,8 @@ export const SnapshotDeltaMode = {
|
|
|
123
167
|
export function snapshotDelta(prevTree, nextTree) {
|
|
124
168
|
if (prevTree === undefined)
|
|
125
169
|
return { mode: SnapshotDeltaMode.FULL };
|
|
126
|
-
const
|
|
170
|
+
const raw = diffKeyed(deltaLines(prevTree), deltaLines(nextTree));
|
|
171
|
+
const { added, removed, changed } = extractChanged(raw.added, raw.removed);
|
|
127
172
|
const before = focusedLine(prevTree);
|
|
128
173
|
const now = focusedLine(nextTree);
|
|
129
174
|
// Reported only on a MOVE. Repeating "focus is still here" every turn is the noise the delta exists
|
|
@@ -135,12 +180,18 @@ export function snapshotDelta(prevTree, nextTree) {
|
|
|
135
180
|
...(now === undefined ? {} : { to: now.text }),
|
|
136
181
|
};
|
|
137
182
|
const moved = focusChanged === undefined ? {} : { focusChanged };
|
|
138
|
-
if (0 === added.length && 0 === removed.length) {
|
|
183
|
+
if (0 === added.length && 0 === removed.length && 0 === changed.length) {
|
|
139
184
|
return { mode: SnapshotDeltaMode.UNCHANGED, ...moved };
|
|
140
185
|
}
|
|
141
186
|
return {
|
|
142
187
|
mode: SnapshotDeltaMode.DELTA,
|
|
143
|
-
delta: {
|
|
188
|
+
delta: {
|
|
189
|
+
added,
|
|
190
|
+
removed,
|
|
191
|
+
changed,
|
|
192
|
+
addedCount: added.length,
|
|
193
|
+
removedCount: removed.length,
|
|
194
|
+
},
|
|
144
195
|
...moved,
|
|
145
196
|
};
|
|
146
197
|
}
|
|
@@ -152,6 +203,10 @@ export class SnapshotCache {
|
|
|
152
203
|
constructor(max = DEFAULT_MAX_ENTRIES) {
|
|
153
204
|
this.#max = max;
|
|
154
205
|
}
|
|
206
|
+
/** True when an entry exists for this key (regardless of route match). */
|
|
207
|
+
has(key) {
|
|
208
|
+
return this.#map.has(key);
|
|
209
|
+
}
|
|
155
210
|
/** Last tree for this key IF the route still matches; undefined when absent or route changed. */
|
|
156
211
|
recall(key, route) {
|
|
157
212
|
const entry = this.#map.get(key);
|
|
@@ -210,10 +265,13 @@ export function applySnapshotDelta(raw, opts, cache) {
|
|
|
210
265
|
return raw;
|
|
211
266
|
}
|
|
212
267
|
const prev = cache.recall(key, route);
|
|
268
|
+
const hadEntry = cache.has(key);
|
|
213
269
|
cache.remember(key, route, tree);
|
|
214
270
|
const decision = snapshotDelta(prev, tree);
|
|
215
|
-
if (decision.mode === SnapshotDeltaMode.FULL)
|
|
216
|
-
|
|
271
|
+
if (decision.mode === SnapshotDeltaMode.FULL) {
|
|
272
|
+
const reason = hadEntry ? 'route changed' : 'first snapshot for this route';
|
|
273
|
+
return { ...r, mode: SnapshotDeltaMode.FULL, reason };
|
|
274
|
+
}
|
|
217
275
|
// The walk stops at a node cap and returns a DOCUMENT-ORDER PREFIX, so two capped snapshots of a
|
|
218
276
|
// large page are identical whenever the change happened past the cap — and "unchanged" is then a
|
|
219
277
|
// statement about the cap, not about the page. Carried through on both branches: a delta computed
|
|
@@ -226,9 +284,12 @@ export function applySnapshotDelta(raw, opts, cache) {
|
|
|
226
284
|
if (decision.mode === SnapshotDeltaMode.UNCHANGED) {
|
|
227
285
|
return { mode: SnapshotDeltaMode.UNCHANGED, status: r['status'], ...moved, ...capped };
|
|
228
286
|
}
|
|
287
|
+
const { changed, ...structuralDelta } = decision.delta;
|
|
288
|
+
const changedField = changed.length > 0 ? { changed, changedCount: changed.length } : {};
|
|
229
289
|
return {
|
|
230
290
|
mode: SnapshotDeltaMode.DELTA,
|
|
231
|
-
delta:
|
|
291
|
+
delta: structuralDelta,
|
|
292
|
+
...changedField,
|
|
232
293
|
status: r['status'],
|
|
233
294
|
...moved,
|
|
234
295
|
...capped,
|
package/dist/tools/tool-kit.d.ts
CHANGED
|
@@ -15,10 +15,18 @@ import type { FlowStore } from '../flows/flows.js';
|
|
|
15
15
|
import type { ProjectStore } from '../project/project-store.js';
|
|
16
16
|
import type { AnnotationStore } from '../flows/annotation-store.js';
|
|
17
17
|
import type { BrowserPool } from '../pool/browser-pool.js';
|
|
18
|
-
|
|
18
|
+
import type { ChromiumProbe } from '../cli/chromium-hint.js';
|
|
19
|
+
export interface ToolDeps<Ext = unknown> {
|
|
19
20
|
sessions: SessionManager;
|
|
20
21
|
/** shared one-browser/N-context pool for headless leases. undefined ⇒ lease tools report unavailable. */
|
|
21
22
|
pool?: BrowserPool;
|
|
23
|
+
/**
|
|
24
|
+
* Probe for a launchable Chromium, so the lease path can refuse at the FIRST call with the install
|
|
25
|
+
* fix rather than failing several calls in with a message that reads as "the app is not running".
|
|
26
|
+
* Optional: absent ⇒ no preflight — every existing test construction of ToolDeps keeps working, and
|
|
27
|
+
* the fake-pool lease tests are deliberately runnable without a real Chromium.
|
|
28
|
+
*/
|
|
29
|
+
browserProbe?: () => Promise<ChromiumProbe>;
|
|
22
30
|
baselines: BaselineStore;
|
|
23
31
|
recordings: RecordingStore;
|
|
24
32
|
/** on-disk anchored-flow store (.reticle/flows/). */
|
|
@@ -51,8 +59,23 @@ export interface ToolDeps {
|
|
|
51
59
|
* ToolDeps keeps working; absent falls back to the default port.
|
|
52
60
|
*/
|
|
53
61
|
bridgePort?: number;
|
|
62
|
+
/**
|
|
63
|
+
* A consumer's OWN dependencies, carried through untouched.
|
|
64
|
+
*
|
|
65
|
+
* Nothing in this package reads it. It exists so a consumer embedding the engine can add tools of
|
|
66
|
+
* its own — answering out of a store this package has never heard of — without either widening the
|
|
67
|
+
* fields above with a stranger's vocabulary or forking to get a handle through.
|
|
68
|
+
*
|
|
69
|
+
* The type parameter, rather than `unknown`, is what keeps the seam typed: a consumer writes
|
|
70
|
+
* `ToolDef<TheirExt>` once and their handlers see their own type. Casting out of `unknown` in every
|
|
71
|
+
* handler is how a typed seam quietly becomes an untyped one.
|
|
72
|
+
*
|
|
73
|
+
* Optional, and defaulted to `unknown`, so every existing construction of this bag — and every test
|
|
74
|
+
* that builds one — is unchanged.
|
|
75
|
+
*/
|
|
76
|
+
ext?: Ext;
|
|
54
77
|
}
|
|
55
|
-
export interface ToolDef {
|
|
78
|
+
export interface ToolDef<Ext = unknown> {
|
|
56
79
|
name: string;
|
|
57
80
|
description: string;
|
|
58
81
|
inputSchema: z.ZodRawShape;
|
|
@@ -78,7 +101,19 @@ export interface ToolDef {
|
|
|
78
101
|
* example that does not validate is worse than none at all.
|
|
79
102
|
*/
|
|
80
103
|
example?: Record<string, unknown>;
|
|
81
|
-
|
|
104
|
+
/**
|
|
105
|
+
* Declared method-style, not as a property, and that is load-bearing rather than stylistic.
|
|
106
|
+
*
|
|
107
|
+
* Under `strictFunctionTypes` a property holding a function has CONTRAVARIANT parameters, so
|
|
108
|
+
* `ToolDef<TheirExt>` and `ToolDef<unknown>` become mutually unassignable and a consumer composing
|
|
109
|
+
* `[...TOOLS, ...theirTools]` needs a cast — as does every internal helper that then touches the
|
|
110
|
+
* array. Method-style parameters are checked bivariantly, which collapses all of that.
|
|
111
|
+
*
|
|
112
|
+
* The unsoundness bivariance admits is that a handler written against `ToolDeps<TheirExt>` could be
|
|
113
|
+
* handed a bag carrying no `ext` at all. That costs nothing here, because `ext` is optional by
|
|
114
|
+
* design: any handler reading it already has to survive `undefined`.
|
|
115
|
+
*/
|
|
116
|
+
handler(deps: ToolDeps<Ext>, args: Record<string, unknown>): Promise<unknown>;
|
|
82
117
|
}
|
|
83
118
|
export declare const sessionIdShape: {
|
|
84
119
|
sessionId: z.ZodOptional<z.ZodString>;
|
|
@@ -46,6 +46,8 @@ export declare const ReticleTool: {
|
|
|
46
46
|
readonly AFFECTED: "reticle_affected";
|
|
47
47
|
readonly COVERAGE: "reticle_coverage";
|
|
48
48
|
readonly VERIFY_CHANGE: "reticle_verify_change";
|
|
49
|
+
/** Merged: change/flows/affected/coverage/crawl — "what is proved, and what is not". */
|
|
50
|
+
readonly VERIFY: "reticle_verify";
|
|
49
51
|
/** read cross-run history (.reticle/project.json) + diff-vs-last for a name. */
|
|
50
52
|
readonly PROJECT: "reticle_project";
|
|
51
53
|
/** explicitly record a run outcome (the manual companion to auto-recording). */
|
package/dist/tools/tool-names.js
CHANGED
|
@@ -47,6 +47,8 @@ export const ReticleTool = {
|
|
|
47
47
|
AFFECTED: 'reticle_affected',
|
|
48
48
|
COVERAGE: 'reticle_coverage',
|
|
49
49
|
VERIFY_CHANGE: 'reticle_verify_change',
|
|
50
|
+
/** Merged: change/flows/affected/coverage/crawl — "what is proved, and what is not". */
|
|
51
|
+
VERIFY: 'reticle_verify',
|
|
50
52
|
/** read cross-run history (.reticle/project.json) + diff-vs-last for a name. */
|
|
51
53
|
PROJECT: 'reticle_project',
|
|
52
54
|
/** explicitly record a run outcome (the manual companion to auto-recording). */
|
|
@@ -40,6 +40,36 @@ export declare const TOOL_SURFACE: {
|
|
|
40
40
|
readonly DEFAULT: "default";
|
|
41
41
|
/** Every tool advertised directly, WITH output schemas. A verification switch — see above. */
|
|
42
42
|
readonly ALL: "all";
|
|
43
|
+
/**
|
|
44
|
+
* The smallest surface that can still produce a VERDICT. Not a profile — a cost switch.
|
|
45
|
+
*
|
|
46
|
+
* Measured on the wire: a verification that names its own target costs one `act_and_wait` call,
|
|
47
|
+
* and 5,480 of its 5,909 tokens are the advertised surface re-sent for that single turn. The
|
|
48
|
+
* answers cost 430. So on the path where the caller already knows what to assert, almost the
|
|
49
|
+
* entire bill is the menu, and the menu is the thing to cut.
|
|
50
|
+
*
|
|
51
|
+
* Deliberately NOT the default, and now MEASURED rather than inherited. Run over the same 30-bug
|
|
52
|
+
* set that the default surface scores 23/27 detection and 2/29 false alarms on:
|
|
53
|
+
*
|
|
54
|
+
* detection 24/28 (held)
|
|
55
|
+
* FALSE ALARMS 7/30 (was 2/29 — TRIPLED)
|
|
56
|
+
* tokens 113,599 per run (was 179,959 — 37% cheaper)
|
|
57
|
+
*
|
|
58
|
+
* The five new false alarms are not scattered. They are `mutation-leak` and
|
|
59
|
+
* `generate-blast-filter` (state), `kpi-deploys-tamper` (business-logic), `debounce-broken`
|
|
60
|
+
* (timing) and `route-stuck-deployments` (routing) — precisely the classes whose evidence lives in
|
|
61
|
+
* `reticle_state`, `reticle_network` and `reticle_observe`, which this surface does not advertise.
|
|
62
|
+
* Strip the observation tools and the model stops observing: it reaches for the verdict without
|
|
63
|
+
* the evidence and calls a healthy build broken.
|
|
64
|
+
*
|
|
65
|
+
* So the retired `dynamic` finding is confirmed, with a sharper mechanism than "accuracy drops".
|
|
66
|
+
* 37% of the tokens is not worth trading for the one metric this product actually wins on, and
|
|
67
|
+
* this must not become the default on the strength of the token number alone.
|
|
68
|
+
*
|
|
69
|
+
* It remains correct for a caller who has ALREADY decided what to assert — there the assertion
|
|
70
|
+
* supplies the evidence the surface would otherwise have to go and find.
|
|
71
|
+
*/
|
|
72
|
+
readonly VERIFY: "verify";
|
|
43
73
|
};
|
|
44
74
|
export type ToolSurface = (typeof TOOL_SURFACE)[keyof typeof TOOL_SURFACE];
|
|
45
75
|
/**
|
|
@@ -49,6 +79,8 @@ export type ToolSurface = (typeof TOOL_SURFACE)[keyof typeof TOOL_SURFACE];
|
|
|
49
79
|
* users to shop among alternatives that did not meaningfully differ.
|
|
50
80
|
*/
|
|
51
81
|
export declare const ADVERTISE_ALL_ENV = "RETICLE_ADVERTISE_ALL_TOOLS";
|
|
82
|
+
/** Opt into the smallest verdict-capable surface. Read by the DAEMON at startup, like the others. */
|
|
83
|
+
export declare const VERIFY_SURFACE_ENV = "RETICLE_VERIFY_SURFACE";
|
|
52
84
|
/**
|
|
53
85
|
* The retired setting, still read so nobody's shell profile breaks.
|
|
54
86
|
*
|
|
@@ -58,6 +90,34 @@ export declare const ADVERTISE_ALL_ENV = "RETICLE_ADVERTISE_ALL_TOOLS";
|
|
|
58
90
|
*/
|
|
59
91
|
export declare const TOOL_PROFILE_ENV = "RETICLE_TOOL_PROFILE";
|
|
60
92
|
export declare const CORE_TOOL_NAMES: ReadonlySet<string>;
|
|
93
|
+
/**
|
|
94
|
+
* The extended surface: what `all` advertises BEYOND the default set.
|
|
95
|
+
*
|
|
96
|
+
* `all` used to mean "every tool in the registry", and that is no longer allowed to be true. Cursor
|
|
97
|
+
* enforces a limit of 40 tools across every connected MCP server COMBINED, so a server advertising
|
|
98
|
+
* 48 by itself can push a user's other servers out or be dropped wholesale. The budget is a count,
|
|
99
|
+
* so no amount of trimming parameter prose buys anything back — only advertising fewer names does.
|
|
100
|
+
*
|
|
101
|
+
* Everything omitted stays in the registry, stays catalogued by `reticle_tools`, and stays callable
|
|
102
|
+
* by name through `reticle_run { tool, args }`. The cost is one discovery hop on the cold tail, which
|
|
103
|
+
* is the same trade the default surface has always made, applied one level further out.
|
|
104
|
+
*
|
|
105
|
+
* What it costs that is NOT free: `all` is the only surface that carries `outputSchema`, which is
|
|
106
|
+
* what makes the MCP layer validate tool OUTPUT — the check that once caught `reticle_verify_change`
|
|
107
|
+
* returning a payload its own schema rejected. The tools left off this list no longer get that
|
|
108
|
+
* validation from the wire. That defect class is now uncovered for them, and it is written down here
|
|
109
|
+
* rather than discovered later.
|
|
110
|
+
*
|
|
111
|
+
* Chosen as the capabilities an agent plausibly reaches for once the verify loop is not enough:
|
|
112
|
+
* orientation, the record/replay flow loop, visual evidence, and the three fault-injection controls.
|
|
113
|
+
*/
|
|
114
|
+
export declare const EXTENDED_TOOL_NAMES: ReadonlySet<string>;
|
|
115
|
+
/**
|
|
116
|
+
* The verify surface: one acting tool that returns a verdict, plus the two meta-tools that reach
|
|
117
|
+
* everything else. `act_and_wait` can resolve its own target, so no query tool is needed to name an
|
|
118
|
+
* element — that round trip was half the token cost of a verification.
|
|
119
|
+
*/
|
|
120
|
+
export declare const VERIFY_TOOL_NAMES: ReadonlySet<string>;
|
|
61
121
|
/**
|
|
62
122
|
* Which surface is live.
|
|
63
123
|
*
|