@reticlehq/server 2.8.0 → 2.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/SKILL.md +130 -813
- package/dist/bridge/bridge.d.ts +23 -0
- package/dist/bridge/bridge.js +80 -2
- package/dist/bridge/bridge.test-harness.d.ts +5 -0
- package/dist/bridge/bridge.test-harness.js +6 -4
- package/dist/capsule/causal-summary.d.ts +16 -1
- package/dist/capsule/causal-summary.js +2 -1
- package/dist/cli/cli-doctor.js +41 -13
- package/dist/cli/cli-flow-commands.js +8 -8
- package/dist/cli/cli-launch.d.ts +0 -17
- package/dist/cli/cli-launch.js +0 -28
- package/dist/cli/cli-port.d.ts +24 -0
- package/dist/cli/cli-port.js +66 -35
- package/dist/cli/cli-verify.d.ts +21 -0
- package/dist/cli/cli-verify.js +45 -1
- package/dist/cli/config-discovery.d.ts +35 -0
- package/dist/cli/config-discovery.js +163 -0
- package/dist/cli/doctor-daemon-line.js +4 -1
- package/dist/cli/doctor-project-line.d.ts +37 -0
- package/dist/cli/doctor-project-line.js +35 -0
- package/dist/cli/doctor-rows.d.ts +51 -0
- package/dist/cli/doctor-rows.js +54 -0
- package/dist/cli/doctor-sessions-line.js +6 -3
- package/dist/cli/drive-attach.d.ts +99 -0
- package/dist/cli/drive-attach.js +203 -0
- package/dist/cli/drive-command.d.ts +13 -0
- package/dist/cli/drive-command.js +103 -0
- package/dist/cli/open-note.d.ts +20 -0
- package/dist/cli/open-note.js +37 -0
- package/dist/cli/status-next-action.d.ts +37 -0
- package/dist/cli/status-next-action.js +61 -0
- package/dist/cli.js +65 -70
- package/dist/events/contradictions.d.ts +32 -0
- package/dist/events/contradictions.js +9 -5
- package/dist/events/declared.d.ts +45 -0
- package/dist/events/declared.js +73 -0
- package/dist/events/echo-mismatch.d.ts +8 -2
- package/dist/events/echo-mismatch.js +33 -5
- package/dist/events/event-filters.d.ts +1 -1
- package/dist/events/event-filters.js +7 -4
- package/dist/events/predicate-eval.d.ts +17 -91
- package/dist/events/predicate-eval.js +85 -326
- package/dist/events/predicate-parse.js +31 -3
- package/dist/events/predicate-route.d.ts +32 -0
- package/dist/events/predicate-route.js +102 -0
- package/dist/events/predicate-schema.d.ts +99 -0
- package/dist/events/predicate-schema.js +309 -0
- package/dist/events/predicate.d.ts +8 -0
- package/dist/events/predicate.js +127 -5
- package/dist/flows/annotate-tools.js +3 -0
- package/dist/flows/annotate.js +18 -4
- package/dist/flows/coverage.d.ts +38 -5
- package/dist/flows/coverage.js +30 -4
- package/dist/flows/decision.d.ts +15 -0
- package/dist/flows/decision.js +1 -1
- package/dist/flows/flow-replay-run.js +8 -1
- package/dist/flows/flow-tools.js +9 -1
- package/dist/flows/replay.js +3 -0
- package/dist/honesty/accepted-write.d.ts +3 -0
- package/dist/honesty/accepted-write.js +8 -3
- package/dist/honesty/already-true.js +15 -0
- package/dist/honesty/blind-spots.js +4 -0
- package/dist/honesty/honesty.d.ts +11 -0
- package/dist/honesty/honesty.js +1 -0
- package/dist/honesty/uncaptured-bodies.d.ts +4 -2
- package/dist/honesty/uncaptured-bodies.js +9 -2
- package/dist/honesty/unread-outcome.d.ts +8 -0
- package/dist/honesty/unread-outcome.js +36 -9
- package/dist/honesty/unsettled.d.ts +9 -0
- package/dist/honesty/unsettled.js +7 -3
- package/dist/honesty/verified.d.ts +29 -3
- package/dist/honesty/verified.js +51 -8
- package/dist/http-server.d.ts +19 -0
- package/dist/http-server.js +202 -4
- package/dist/impact/impact-recorder.d.ts +23 -0
- package/dist/impact/impact-recorder.js +76 -0
- package/dist/impact/impact-store.d.ts +47 -0
- package/dist/impact/impact-store.js +171 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +44 -10
- package/dist/init/agent-rules.d.ts +20 -0
- package/dist/init/agent-rules.js +36 -0
- package/dist/init/astro-patch.js +27 -1
- package/dist/init/confirm.d.ts +21 -3
- package/dist/init/confirm.js +32 -5
- package/dist/init/connect-steps.d.ts +2 -0
- package/dist/init/connect-steps.js +50 -0
- package/dist/init/csp-check.d.ts +38 -0
- package/dist/init/csp-check.js +92 -0
- package/dist/init/csp-doctor.d.ts +25 -0
- package/dist/init/csp-doctor.js +49 -0
- package/dist/init/cursor.js +6 -9
- package/dist/init/detect.d.ts +8 -0
- package/dist/init/detect.js +33 -3
- package/dist/init/mcp-clients.d.ts +1 -0
- package/dist/init/mcp-clients.js +51 -7
- package/dist/init/mcp.d.ts +15 -0
- package/dist/init/mcp.js +19 -0
- package/dist/init/plan-framework.d.ts +10 -0
- package/dist/init/plan-framework.js +25 -1
- package/dist/init/plan.d.ts +7 -2
- package/dist/init/plan.js +66 -30
- package/dist/init/run.js +11 -5
- package/dist/init/slash-command.d.ts +1 -1
- package/dist/init/slash-command.js +12 -4
- package/dist/init/snippets.d.ts +1 -1
- package/dist/init/snippets.js +35 -7
- package/dist/init/vite-config.js +24 -2
- package/dist/input/network-mock-tools.js +8 -1
- package/dist/input/scroll-find.d.ts +6 -0
- package/dist/input/scroll-find.js +17 -2
- package/dist/input/scroll-tools.js +4 -0
- package/dist/journal/attach-journal.d.ts +9 -1
- package/dist/journal/attach-journal.js +13 -2
- package/dist/journal/journal-query.d.ts +13 -0
- package/dist/journal/journal-query.js +28 -3
- package/dist/journal/workspace-gitignore.d.ts +10 -0
- package/dist/journal/workspace-gitignore.js +59 -0
- package/dist/mcp/mcp-proxy.d.ts +5 -66
- package/dist/mcp/mcp-proxy.js +180 -163
- package/dist/mcp/mcp.d.ts +10 -1
- package/dist/mcp/mcp.js +47 -27
- package/dist/mcp/proxy-handshake.d.ts +82 -1
- package/dist/mcp/proxy-handshake.js +109 -2
- package/dist/mcp/proxy-lifecycle.d.ts +10 -1
- package/dist/mcp/proxy-lifecycle.js +11 -2
- package/dist/mcp/proxy-log.d.ts +63 -0
- package/dist/mcp/proxy-log.js +132 -0
- package/dist/mcp/server-instructions.d.ts +17 -0
- package/dist/mcp/server-instructions.js +56 -0
- package/dist/mcp/shared-params.d.ts +27 -0
- package/dist/mcp/shared-params.js +36 -0
- package/dist/project/reticle-dir.d.ts +2 -0
- package/dist/project/reticle-dir.js +1 -0
- package/dist/session/connection-memory.d.ts +59 -0
- package/dist/session/connection-memory.js +108 -0
- package/dist/session/dev-command.d.ts +26 -0
- package/dist/session/dev-command.js +89 -0
- package/dist/session/dev-server-probe.d.ts +68 -0
- package/dist/session/dev-server-probe.js +102 -7
- package/dist/session/last-act.d.ts +26 -0
- package/dist/session/last-act.js +35 -1
- package/dist/session/live-control-tools.js +42 -5
- package/dist/session/no-session-diagnosis.d.ts +48 -2
- package/dist/session/no-session-diagnosis.js +176 -20
- package/dist/session/no-session-next-action.d.ts +46 -0
- package/dist/session/no-session-next-action.js +85 -0
- package/dist/session/no-session-watch.d.ts +40 -1
- package/dist/session/no-session-watch.js +165 -11
- package/dist/session/pending-commands.d.ts +12 -0
- package/dist/session/pending-commands.js +13 -1
- package/dist/session/session-manager.d.ts +4 -0
- package/dist/session/session-manager.js +35 -5
- package/dist/session/session.d.ts +10 -1
- package/dist/session/session.js +146 -5
- package/dist/telemetry/app-instrumented.d.ts +8 -0
- package/dist/telemetry/app-instrumented.js +10 -0
- package/dist/telemetry/bug-found.d.ts +18 -0
- package/dist/telemetry/bug-found.js +14 -0
- package/dist/telemetry/daemon-telemetry.js +24 -0
- package/dist/telemetry/install-source.d.ts +20 -0
- package/dist/telemetry/install-source.js +34 -0
- package/dist/telemetry/instrumentation-stall.d.ts +50 -0
- package/dist/telemetry/instrumentation-stall.js +90 -0
- package/dist/telemetry/session-metrics.d.ts +8 -0
- package/dist/telemetry/session-metrics.js +10 -0
- package/dist/telemetry/telemetry.d.ts +3 -1
- package/dist/telemetry/telemetry.js +13 -2
- package/dist/tools/act-tools.js +166 -29
- package/dist/tools/assert-verdict.js +32 -7
- package/dist/tools/browser-tools.js +10 -0
- package/dist/tools/dynamic-tools.js +16 -7
- package/dist/tools/error-recovery.js +9 -5
- package/dist/tools/invoke-tool.js +20 -3
- package/dist/tools/lease-hint.d.ts +30 -8
- package/dist/tools/lease-hint.js +65 -14
- package/dist/tools/lease-tools.d.ts +11 -0
- package/dist/tools/lease-tools.js +141 -96
- package/dist/tools/observe-tools.js +9 -3
- package/dist/tools/react-grace.d.ts +4 -0
- package/dist/tools/react-grace.js +6 -3
- package/dist/tools/read-tools.js +47 -1
- package/dist/tools/resolve-target.d.ts +33 -0
- package/dist/tools/resolve-target.js +53 -0
- package/dist/tools/settle-in-flight.d.ts +15 -0
- package/dist/tools/settle-in-flight.js +42 -2
- package/dist/tools/snapshot-delta.d.ts +3 -0
- package/dist/tools/snapshot-delta.js +67 -6
- package/dist/tools/tool-kit.d.ts +8 -0
- package/dist/tools/tool-surface.d.ts +60 -0
- package/dist/tools/tool-surface.js +85 -19
- package/dist/tools/tools.js +45 -4
- package/dist/tools/unadvertised-help.js +9 -6
- package/docs/README.md +1 -2
- package/docs/actions.mdx +41 -8
- package/docs/agent-cheatsheet.md +4 -2
- package/docs/architecture.md +6 -0
- package/docs/benchmarks.md +60 -12
- package/docs/best-practices.mdx +11 -6
- package/docs/capabilities.mdx +201 -54
- package/docs/cli/affected.mdx +6 -4
- package/docs/cli/capsules.mdx +9 -8
- package/docs/cli/cloud.mdx +30 -4
- package/docs/cli/doctor.mdx +41 -8
- package/docs/cli/drive.mdx +34 -15
- package/docs/cli/gate.mdx +6 -6
- package/docs/cli/help.mdx +8 -6
- package/docs/cli/hunt.mdx +18 -2
- package/docs/cli/identify.mdx +16 -2
- package/docs/cli/init.mdx +35 -8
- package/docs/cli/kill.mdx +5 -5
- package/docs/cli/license.mdx +1 -1
- package/docs/cli/open.mdx +6 -0
- package/docs/cli/restart.mdx +6 -6
- package/docs/cli/serve.mdx +10 -3
- package/docs/cli/status.mdx +29 -5
- package/docs/cli/stop.mdx +8 -0
- package/docs/cli/verify.mdx +20 -1
- package/docs/cli/version.mdx +1 -1
- package/docs/cli/watch.mdx +9 -2
- package/docs/cli.mdx +44 -22
- package/docs/debugging.md +3 -1
- package/docs/deploy-checks.md +30 -1
- package/docs/desktop.mdx +276 -80
- package/docs/docs.json +13 -12
- package/docs/enterprise.md +2 -0
- package/docs/faq.mdx +49 -35
- package/docs/fixtures.md +2 -0
- package/docs/flows.md +3 -1
- package/docs/for-agents.md +3 -3
- package/docs/frameworks.mdx +46 -7
- package/docs/gate-plan.md +4 -2
- package/docs/gates.md +7 -5
- package/docs/getting-started.md +53 -12
- package/docs/human-control.md +2 -0
- package/docs/index.mdx +43 -1
- package/docs/install-agentic.mdx +13 -4
- package/docs/install-manual.mdx +6 -4
- package/docs/instrumentation.mdx +7 -1
- package/docs/integration-patterns.md +3 -1
- package/docs/local-registry.md +4 -2
- package/docs/multi-agent-testing.md +3 -1
- package/docs/packages/babel-plugin.mdx +15 -6
- package/docs/packages/browser.mdx +2 -2
- package/docs/packages/core.mdx +26 -16
- package/docs/packages/electron.mdx +5 -7
- package/docs/packages/eslint-plugin.mdx +5 -3
- package/docs/packages/next.mdx +3 -5
- package/docs/packages/react.mdx +3 -3
- package/docs/packages/server.mdx +2 -2
- package/docs/packages/tauri.mdx +6 -6
- package/docs/packages/test.mdx +18 -7
- package/docs/packages/vite-plugin.mdx +4 -6
- package/docs/packages.mdx +14 -13
- package/docs/platform-integration.md +2 -0
- package/docs/predicates.mdx +76 -7
- package/docs/quickstart.mdx +51 -5
- package/docs/recipes.mdx +62 -22
- package/docs/skill-file.mdx +3 -1
- package/docs/state-management.mdx +2 -0
- package/docs/system-map.md +3 -1
- package/docs/telemetry-contract.md +16 -5
- package/docs/telemetry-events.mdx +63 -18
- package/docs/telemetry.md +8 -5
- package/docs/testing.md +35 -1
- package/docs/token-efficiency.md +8 -2
- package/docs/tools-act-and-wait.mdx +64 -14
- package/docs/tools-act-sequence.mdx +34 -17
- package/docs/tools-act.mdx +24 -11
- package/docs/tools-assert.mdx +69 -10
- package/docs/tools-console.mdx +11 -7
- package/docs/tools-inspect.mdx +11 -8
- package/docs/tools-navigate.mdx +41 -5
- package/docs/tools-network.mdx +42 -9
- package/docs/tools-observe.mdx +66 -10
- package/docs/tools-overview.mdx +26 -23
- package/docs/tools-query.mdx +52 -8
- package/docs/tools-session-and-feedback.mdx +30 -9
- package/docs/tools-sessions.mdx +39 -10
- package/docs/tools-snapshot.mdx +57 -3
- package/docs/tools-state.mdx +24 -1
- package/docs/tools-tools-and-run.mdx +40 -9
- package/docs/tools-wait-for.mdx +34 -15
- package/docs/troubleshooting.mdx +80 -10
- package/docs/usage.md +43 -21
- package/docs/vs-chrome-devtools-mcp.mdx +41 -0
- package/docs/vs-playwright-mcp.mdx +42 -2
- package/docs/vs-screenshots.mdx +42 -1
- package/docs/what-is-reticle.mdx +1 -1
- package/docs/why-reticle.mdx +3 -1
- package/package.json +5 -5
- package/docs/desktop-apps.md +0 -265
package/dist/cli.js
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { pathToFileURL } from 'node:url';
|
|
3
|
+
import { openFailureNote } from './cli/open-note.js';
|
|
3
4
|
import { realpathSync } from 'node:fs';
|
|
4
5
|
import { join } from 'node:path';
|
|
5
6
|
import { stateDirProblem } from './daemon/state-dir.js';
|
|
7
|
+
import { statusNextAction } from './cli/status-next-action.js';
|
|
8
|
+
import { hasConnectedBefore } from './session/connection-memory.js';
|
|
6
9
|
import { reticleStateHome } from './daemon/daemon.js';
|
|
7
10
|
import { handleWatch, handleCapsules, handleGate, loadNamedFlows, resolveChangedFiles, } from './cli/cli-flow-commands.js';
|
|
8
11
|
import { RETICLE_DEFAULT_PORT, ReticleDir, ReticleEnv } from '@reticlehq/core';
|
|
@@ -11,7 +14,7 @@ import { createNodeFileSystem } from './project/fs-port.js';
|
|
|
11
14
|
import { affectedSavedFlows } from './flows/flow-sources.js';
|
|
12
15
|
import { availableUpdate } from './update/update-nudge.js';
|
|
13
16
|
import { handleUpdate, handleRollback } from './cli/cli-update-commands.js';
|
|
14
|
-
import {
|
|
17
|
+
import { startDaemon } from './index.js';
|
|
15
18
|
import { isCloudCommand, runCloudCommand } from './cli/cloud-cli.js';
|
|
16
19
|
import { SERVER_VERSION } from './version/server-version.js';
|
|
17
20
|
import { log } from './log.js';
|
|
@@ -22,7 +25,8 @@ import { installDaemonResilience, installProxyResilience, recordExitReason, Daem
|
|
|
22
25
|
import { IdleShutdown, resolveIdleShutdownMs, resolveIdleCheckMs } from './daemon/idle-shutdown.js';
|
|
23
26
|
import { DaemonHeartbeat, resolveHeartbeatMs } from './daemon/heartbeat.js';
|
|
24
27
|
import { everServedToolCall } from './daemon/daemon-usefulness.js';
|
|
25
|
-
import { fetchStatus, summarizeStatus, warnOnDaemonSkew, decideOpen,
|
|
28
|
+
import { fetchStatus, summarizeStatus, warnOnDaemonSkew, decideOpen, openInBrowser, openCommand, } from './cli/cli-launch.js';
|
|
29
|
+
import { handleDrive } from './cli/drive-command.js';
|
|
26
30
|
import { handleVerify } from './cli/cli-verify.js';
|
|
27
31
|
import { runKill } from './cli/cli-kill.js';
|
|
28
32
|
import { summarizeHunt } from './hunt/hunt-report.js';
|
|
@@ -220,17 +224,34 @@ async function handleRestart(port, force) {
|
|
|
220
224
|
// same lie `serve` already stopped telling.
|
|
221
225
|
await serveWithHonestExit({ port, headless: true, http: false });
|
|
222
226
|
}
|
|
227
|
+
/** `{ nextAction }` when there is one, `{}` when a session is connected — so the success case is silent. */
|
|
228
|
+
function withNextAction(facts) {
|
|
229
|
+
const next = statusNextAction(facts);
|
|
230
|
+
return next === undefined ? {} : { nextAction: next };
|
|
231
|
+
}
|
|
223
232
|
function handleStatus(port) {
|
|
224
233
|
const pid = readPid(port);
|
|
234
|
+
// Durable, so it survives the daemon idling out — which is the state `status` is most often run in.
|
|
235
|
+
const projectId = readProjectId(process.cwd());
|
|
236
|
+
const previouslyConnected = hasConnectedBefore(reticleStateHome(), port, projectId);
|
|
237
|
+
// Whether `init` has run HERE. Registering the MCP server does not wire the app, and more than one
|
|
238
|
+
// path does the first without the second — so this is the commonest reason `status` has nothing to
|
|
239
|
+
// report, and it was not among the facts this command could state.
|
|
240
|
+
const initialized = projectId !== undefined;
|
|
225
241
|
if (null === pid || !isAlive(pid)) {
|
|
226
242
|
// `running: false` on its own has been reported about a port that was demonstrably occupied,
|
|
227
243
|
// because the pid file is not the port. Ask the port before answering.
|
|
228
244
|
void probePresence(port, { tcpOpen: probeDaemon, status: fetchStatus }).then((presence) => {
|
|
245
|
+
const running = presenceIsUsable(presence);
|
|
229
246
|
log('reticle_status', {
|
|
230
247
|
port,
|
|
231
|
-
running
|
|
248
|
+
running,
|
|
232
249
|
presence,
|
|
233
250
|
...(presence === PortPresence.FOREIGN ? { reason: describePresence(presence, port) } : {}),
|
|
251
|
+
// `init` promises this command says why the app has not connected. Without it the answer was
|
|
252
|
+
// `running: false` and nothing else, which reads as "Reticle is broken" for what is usually
|
|
253
|
+
// just a daemon that has not been asked to do anything yet.
|
|
254
|
+
...withNextAction({ running, sessionCount: 0, previouslyConnected, initialized }),
|
|
234
255
|
});
|
|
235
256
|
});
|
|
236
257
|
return;
|
|
@@ -243,10 +264,23 @@ function handleStatus(port) {
|
|
|
243
264
|
const update = availableUpdate();
|
|
244
265
|
const nudge = update === undefined ? {} : { updateAvailable: update };
|
|
245
266
|
if (payload === undefined) {
|
|
246
|
-
log('reticle_status', {
|
|
267
|
+
log('reticle_status', {
|
|
268
|
+
port,
|
|
269
|
+
running: true,
|
|
270
|
+
pid,
|
|
271
|
+
...nudge,
|
|
272
|
+
...withNextAction({ running: true, sessionCount: 0, previouslyConnected, initialized }),
|
|
273
|
+
});
|
|
247
274
|
return;
|
|
248
275
|
}
|
|
249
|
-
|
|
276
|
+
const summary = summarizeStatus(payload);
|
|
277
|
+
// Only when the daemon did NOT already explain itself. It has the whole diagnosis in-process and
|
|
278
|
+
// puts it on the wire as `why`; printing a second, thinner opinion beside it risks two confident
|
|
279
|
+
// answers pointing different ways, which is worse than one.
|
|
280
|
+
const next = summary.why === undefined
|
|
281
|
+
? withNextAction({ running: true, ...summary, previouslyConnected, initialized })
|
|
282
|
+
: {};
|
|
283
|
+
log('reticle_status', { port, running: true, pid, ...summary, ...next, ...nudge });
|
|
250
284
|
});
|
|
251
285
|
}
|
|
252
286
|
/** Print the running package version (resolved once in server-version.ts). */
|
|
@@ -318,13 +352,20 @@ async function handleHunt(dir) {
|
|
|
318
352
|
}
|
|
319
353
|
/** Ensure a daemon is reachable on `port` (probe the real port; spawn + wait only if nothing's there). */
|
|
320
354
|
function ensureDaemon(port) {
|
|
321
|
-
return
|
|
355
|
+
return probePresence(port, { tcpOpen: probeDaemon, status: fetchStatus }).then(async (presence) => {
|
|
322
356
|
// Attaching to whatever already owns the port is the whole point of a daemon — but it means an
|
|
323
357
|
// upgrade does NOT take effect until that daemon dies, and nothing used to say so. Say it here,
|
|
324
358
|
// where both versions are in hand, and keep attaching: killing another agent's daemon on a
|
|
325
359
|
// version bump is worse than a loud warning.
|
|
326
|
-
if (
|
|
360
|
+
if (presenceIsUsable(presence))
|
|
327
361
|
return warnOnDaemonSkew(port);
|
|
362
|
+
// Not free, not a daemon: a stranger holds the port. Spawning here is guaranteed to fail —
|
|
363
|
+
// the child cannot bind — and `waitForDaemon` would then report READY anyway, because its
|
|
364
|
+
// probe is the same bare TCP connect and the stranger accepts. That is the "serve reports
|
|
365
|
+
// success for a daemon that never bound" shape. Refuse instead, with the sentence `doctor`
|
|
366
|
+
// says.
|
|
367
|
+
if (PortPresence.FREE !== presence)
|
|
368
|
+
throw new Error(describePresence(presence, port));
|
|
328
369
|
const scriptPath = process.argv[1];
|
|
329
370
|
if (scriptPath === undefined)
|
|
330
371
|
throw new Error('cannot locate the reticle daemon script');
|
|
@@ -410,6 +451,7 @@ function handleOpen(requestedPort, url) {
|
|
|
410
451
|
const connected = await waitForNewSession(port, sessions.length);
|
|
411
452
|
log('reticle_open', {
|
|
412
453
|
port,
|
|
454
|
+
...(port === requestedPort ? {} : { requestedPort }),
|
|
413
455
|
opened: decision.url,
|
|
414
456
|
connected,
|
|
415
457
|
...(connected
|
|
@@ -422,14 +464,7 @@ function handleOpen(requestedPort, url) {
|
|
|
422
464
|
// about a browser THIS command never uses, so a missing Chromium reads as the
|
|
423
465
|
// explanation for a session that is missing for an unrelated reason. Both misdirects
|
|
424
466
|
// were reported from the field, each costing several calls of app-side wiring hunt.
|
|
425
|
-
note:
|
|
426
|
-
`Reticle's own Chromium, so a chromium warning from \`reticle doctor\` is unrelated ` +
|
|
427
|
-
`to this). No Reticle session appeared. By far the likeliest cause is that the app ` +
|
|
428
|
-
`carries no Reticle SDK, or dials a port other than ${String(port)} — run ` +
|
|
429
|
-
`\`reticle init\` in the app's directory and restart its dev server. If the app IS ` +
|
|
430
|
-
`wired, give the page a moment and check the browser console: the SDK announces its ` +
|
|
431
|
-
`own connect failures there, including the one it refuses to make from a ` +
|
|
432
|
-
`non-localhost host without allowNonLocalhost.`,
|
|
467
|
+
note: openFailureNote(port, requestedPort),
|
|
433
468
|
}),
|
|
434
469
|
});
|
|
435
470
|
})
|
|
@@ -478,6 +513,12 @@ function handleDaemonInner(parsed) {
|
|
|
478
513
|
// which is after everything below has run. Without it the exit line says `code: 0` and a
|
|
479
514
|
// reader cannot tell a tidy stop from the bridge disappearing — see #123.
|
|
480
515
|
recordExitReason(reason);
|
|
516
|
+
// Say so on the wire before anything closes. The proxy on the other end sees a clean stream
|
|
517
|
+
// end whether we retired on schedule or died under it, and it has no other way to tell —
|
|
518
|
+
// which is how a designed shutdown came to dominate the metric that means "the agent lost
|
|
519
|
+
// its tools". Told, the proxy classifies its own drop honestly. First, because the send has
|
|
520
|
+
// to reach a socket that is still open.
|
|
521
|
+
server.announceShutdown?.();
|
|
481
522
|
// Awaited before the close/exit chain: `process.exit(0)` kills an in-flight POST, and this is
|
|
482
523
|
// the one event carrying the whole session. A failed send resolves anyway (emit swallows its
|
|
483
524
|
// own errors), so this can delay the exit by at most the send timeout, never prevent it.
|
|
@@ -572,8 +613,15 @@ function handleMcp(opts) {
|
|
|
572
613
|
* disappeared mid-session with nothing said. Respawning here makes the reconnect self-healing.
|
|
573
614
|
*/
|
|
574
615
|
const ensure = async () => {
|
|
575
|
-
|
|
616
|
+
// The same question every other surface asks. It used to be a bare TCP connect, so a stranger on
|
|
617
|
+
// the bridge port answered "a daemon is here" — the proxy connected, the stream ended, and each
|
|
618
|
+
// client request woke into the identical non-answer. Rejecting here is what puts the proxy
|
|
619
|
+
// dormant with a reason, instead of pretending the wake succeeded.
|
|
620
|
+
const presence = await probePresence(port, { tcpOpen: probeDaemon, status: fetchStatus });
|
|
621
|
+
if (presenceIsUsable(presence))
|
|
576
622
|
return;
|
|
623
|
+
if (PortPresence.FREE !== presence)
|
|
624
|
+
throw new Error(describePresence(presence, port));
|
|
577
625
|
const scriptPath = process.argv[1];
|
|
578
626
|
if (scriptPath === undefined) {
|
|
579
627
|
log('reticle_mcp_no_script', {});
|
|
@@ -640,59 +688,6 @@ function handleMcp(opts) {
|
|
|
640
688
|
});
|
|
641
689
|
});
|
|
642
690
|
}
|
|
643
|
-
function handleLegacyDrive(parsed) {
|
|
644
|
-
void driveWithHonestConflict(parsed);
|
|
645
|
-
}
|
|
646
|
-
/**
|
|
647
|
-
* Refuse the port BEFORE binding it, and name what is holding it.
|
|
648
|
-
*
|
|
649
|
-
* `drive` went straight to `start`, which binds. The listen error surfaces asynchronously on the
|
|
650
|
-
* server object long after `start` has resolved, so the `.catch` on that promise could never see it
|
|
651
|
-
* and the process died with the raw `node:net` EADDRINUSE stack — in the one situation this command
|
|
652
|
-
* is most often run in, since `reticle_sessions` recommends `reticle drive` for a throttled tab and
|
|
653
|
-
* a throttled tab nearly always coexists with the daemon holding the port.
|
|
654
|
-
*/
|
|
655
|
-
async function driveWithHonestConflict(parsed) {
|
|
656
|
-
const presence = await probePresence(parsed.port, { tcpOpen: probeDaemon, status: fetchStatus });
|
|
657
|
-
const conflict = drivePortConflict(presence, parsed.port, { ourPid: readPid(parsed.port) });
|
|
658
|
-
if (conflict !== undefined) {
|
|
659
|
-
log('reticle_drive_port_conflict', { port: parsed.port, presence, reason: conflict });
|
|
660
|
-
process.stderr.write(`${conflict}\n`);
|
|
661
|
-
process.exit(1);
|
|
662
|
-
return;
|
|
663
|
-
}
|
|
664
|
-
// The probe cannot close the race: something can take the port between here and the bind, and the
|
|
665
|
-
// bind reports it on the server, outside every promise this function holds. Catching it at the
|
|
666
|
-
// process is the only place that sees it at all — and only EADDRINUSE, so a genuine crash still
|
|
667
|
-
// crashes rather than being dressed up as a port conflict. Who won the race is not knowable from
|
|
668
|
-
// in here, so the sentence used is the one that is true of any holder.
|
|
669
|
-
process.on('uncaughtException', (error) => {
|
|
670
|
-
const code = error?.code;
|
|
671
|
-
if ('EADDRINUSE' !== code)
|
|
672
|
-
throw error;
|
|
673
|
-
const reason = drivePortConflict(PortPresence.FOREIGN, parsed.port);
|
|
674
|
-
log('reticle_drive_port_conflict', {
|
|
675
|
-
port: parsed.port,
|
|
676
|
-
presence: PortPresence.FOREIGN,
|
|
677
|
-
reason,
|
|
678
|
-
});
|
|
679
|
-
process.stderr.write(`${String(reason)}\n`);
|
|
680
|
-
process.exit(1);
|
|
681
|
-
});
|
|
682
|
-
const options = {
|
|
683
|
-
port: parsed.port,
|
|
684
|
-
driveUrl: parsed.driveUrl,
|
|
685
|
-
headless: parsed.headless,
|
|
686
|
-
};
|
|
687
|
-
try {
|
|
688
|
-
await start(options);
|
|
689
|
-
log('reticle_started', { port: parsed.port });
|
|
690
|
-
}
|
|
691
|
-
catch (error) {
|
|
692
|
-
log('reticle_start_failed', { error: error instanceof Error ? error.message : String(error) });
|
|
693
|
-
process.exit(1);
|
|
694
|
-
}
|
|
695
|
-
}
|
|
696
691
|
function main() {
|
|
697
692
|
// Before anything reads process.env — notably the telemetry gate and the bridge's security
|
|
698
693
|
// options — fold in a project-local `.env`. Values already in the environment always win.
|
|
@@ -782,7 +777,7 @@ function main() {
|
|
|
782
777
|
handleOpen(parsed.port, parsed.url);
|
|
783
778
|
break;
|
|
784
779
|
case 'drive':
|
|
785
|
-
|
|
780
|
+
handleDrive(parsed);
|
|
786
781
|
break;
|
|
787
782
|
case 'verify':
|
|
788
783
|
handleVerify(parsed);
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { ContradictionKind, type ReticleEvent } from '@reticlehq/core';
|
|
2
|
+
import { type DeclaredNetFailure } from './declared.js';
|
|
2
3
|
/**
|
|
3
4
|
* The contradiction hunter.
|
|
4
5
|
*
|
|
@@ -63,5 +64,36 @@ export interface ContradictionOptions {
|
|
|
63
64
|
* test, which is weaker but never wrong in the direction of a false accusation.
|
|
64
65
|
*/
|
|
65
66
|
mutatedWithin?: number | undefined;
|
|
67
|
+
/**
|
|
68
|
+
* Requests the CALLER declared would fail, read off the oracle it wrote before acting.
|
|
69
|
+
*
|
|
70
|
+
* `ui-advanced-request-failed` cannot tell "the app swallowed the error and carried on" from "the
|
|
71
|
+
* app rendered the error, which is the behaviour under test" — both are a moved DOM beside a
|
|
72
|
+
* failed request. `failureAcknowledged` recovers the first from the app's own state, and an app
|
|
73
|
+
* that renders its error straight into the DOM without touching a store defeats it.
|
|
74
|
+
*
|
|
75
|
+
* The declaration is the missing evidence, and it costs no new API: an agent verifying an error
|
|
76
|
+
* path already writes `{ net, POST, /api/login, status: 500 }` into the predicate. Measured in the
|
|
77
|
+
* field: every branch of a login error path (500, 401, 503) passed every declared clause and every
|
|
78
|
+
* run still returned `verified: "no" / contradicted`, so error handling — empty states, offline
|
|
79
|
+
* banners, 4xx/5xx messaging, lockouts — was the code least able to reach a green verdict and the
|
|
80
|
+
* most worth verifying.
|
|
81
|
+
*
|
|
82
|
+
* Scoped as narrowly as it can be: it suppresses ONLY the heuristic rule that cannot see the
|
|
83
|
+
* difference. A success signal fired over the declared failure still contradicts, and a server
|
|
84
|
+
* fault blamed on the user is still misattributed — see the negative controls in
|
|
85
|
+
* `contradictions.declared.test.ts`.
|
|
86
|
+
*/
|
|
87
|
+
expectedFailures?: readonly DeclaredNetFailure[] | undefined;
|
|
88
|
+
/**
|
|
89
|
+
* The caller declared an on-screen consequence and it HELD in this window.
|
|
90
|
+
*
|
|
91
|
+
* `route-rendered-nothing` infers a blank destination from the absence of DOM events, and a
|
|
92
|
+
* verdict that names it beside an element match — heading found, with its source file and line —
|
|
93
|
+
* is a clause its own evidence disproves. Positive evidence outranks the absence it is inferred
|
|
94
|
+
* from. Undefined/false leaves the rule exactly as it was, which is the case that catches a route
|
|
95
|
+
* with no view.
|
|
96
|
+
*/
|
|
97
|
+
renderProved?: boolean | undefined;
|
|
66
98
|
}
|
|
67
99
|
export declare function findContradictions(allEvents: readonly ReticleEvent[], options?: ContradictionOptions): Contradiction[];
|
|
@@ -4,6 +4,7 @@ import { findBodyFailures } from './body-failures.js';
|
|
|
4
4
|
import { findEchoMismatches } from './echo-mismatch.js';
|
|
5
5
|
import { findUnitMismatches } from './unit-mismatch.js';
|
|
6
6
|
import { asNumber, asString } from '../tools/tools-helpers.js';
|
|
7
|
+
import { matchesDeclaredFailure } from './declared.js';
|
|
7
8
|
function netCall(e) {
|
|
8
9
|
const status = asNumber(e.data['status']);
|
|
9
10
|
return {
|
|
@@ -171,7 +172,7 @@ export function findContradictions(allEvents, options = {}) {
|
|
|
171
172
|
// Needs body capture; silent without it, which is why the assert path also declares when bodies
|
|
172
173
|
// were never recorded rather than letting an unread payload read as an empty one.
|
|
173
174
|
found.push(...findBodyFailures(events));
|
|
174
|
-
found.push(...findEchoMismatches(events));
|
|
175
|
+
found.push(...findEchoMismatches(events, options.actionSince));
|
|
175
176
|
// ── A money value written back at the wrong SCALE ───────────────────────────────────────────
|
|
176
177
|
found.push(...findUnitMismatches(events, options.prior ?? []));
|
|
177
178
|
// ── The action landed on something that does not react ──────────────────────────────────────
|
|
@@ -192,6 +193,9 @@ export function findContradictions(allEvents, options = {}) {
|
|
|
192
193
|
];
|
|
193
194
|
}
|
|
194
195
|
const failed = settled.filter((c) => false === c.ok);
|
|
196
|
+
// The failures NOBODY declared — see ContradictionOptions.expectedFailures. Only the heuristic
|
|
197
|
+
// "the UI moved while a request failed" rule reads this; the sharp rules still read `failed`.
|
|
198
|
+
const unexpected = failed.filter((c) => !matchesDeclaredFailure(c, options.expectedFailures ?? []));
|
|
195
199
|
const advanced = uiAdvanced(events);
|
|
196
200
|
const signals = events
|
|
197
201
|
.filter((e) => e.type === EventType.SIGNAL)
|
|
@@ -215,7 +219,7 @@ export function findContradictions(allEvents, options = {}) {
|
|
|
215
219
|
e.type === EventType.DOM_REMOVED ||
|
|
216
220
|
e.type === EventType.DOM_TEXT);
|
|
217
221
|
const fetched = events.some((e) => e.type === EventType.NET_REQUEST || e.type === EventType.NET_PENDING);
|
|
218
|
-
if (routed && !rendered && !fetched) {
|
|
222
|
+
if (routed && !rendered && !fetched && true !== options.renderProved) {
|
|
219
223
|
found.push({
|
|
220
224
|
kind: ContradictionKind.ROUTE_RENDERED_NOTHING,
|
|
221
225
|
claim: 'the app navigated to a new route',
|
|
@@ -259,12 +263,12 @@ export function findContradictions(allEvents, options = {}) {
|
|
|
259
263
|
detail: failed.map(describe).join('; '),
|
|
260
264
|
});
|
|
261
265
|
}
|
|
262
|
-
else if (
|
|
266
|
+
else if (unexpected.length > 0 && advanced && !misattributed && !failureAcknowledged(events)) {
|
|
263
267
|
found.push({
|
|
264
268
|
kind: ContradictionKind.UI_ADVANCED_REQUEST_FAILED,
|
|
265
269
|
claim: 'the UI moved forward (DOM/store/route changed)',
|
|
266
|
-
counter: `${String(
|
|
267
|
-
detail:
|
|
270
|
+
counter: `${String(unexpected.length)} request(s) in the same window failed`,
|
|
271
|
+
detail: unexpected.map(describe).join('; '),
|
|
268
272
|
});
|
|
269
273
|
}
|
|
270
274
|
// ── A write succeeded and nothing on the client moved ───────────────────────────────────────
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What the caller DECLARED before acting — the oracle, read back.
|
|
3
|
+
*
|
|
4
|
+
* `act_and_wait` asks the agent to name the expected consequence BEFORE the action, and that
|
|
5
|
+
* declaration was then thrown away: everything downstream judged the window as if nobody had said
|
|
6
|
+
* what they were expecting. Two measured consequences, one bug:
|
|
7
|
+
*
|
|
8
|
+
* - An error path can be declared exactly — `{ net, POST, /api/login, status: 500 }` — and the
|
|
9
|
+
* contradiction hunter still reported `ui-advanced-request-failed`, because a failed request plus
|
|
10
|
+
* a moved DOM is all it can see. The failure was EXPECTED and said so in the predicate, so a
|
|
11
|
+
* verdict of "channels disagree" is a statement about a disagreement nobody had.
|
|
12
|
+
* - A destination whose content was asserted and FOUND was still reported as
|
|
13
|
+
* `route-rendered-nothing`, a clause the element evidence beside it disproves.
|
|
14
|
+
*
|
|
15
|
+
* Deliberately conservative about what counts as declared. Only what the caller REQUIRED is read:
|
|
16
|
+
* the top level and `allOf` chains. An `anyOf` branch may never have held, and a `not` declares the
|
|
17
|
+
* opposite of a consequence — honouring either would suppress a real finding on the strength of
|
|
18
|
+
* something that never happened. Pure: a predicate in, a description out.
|
|
19
|
+
*/
|
|
20
|
+
import type { Predicate } from './predicate-eval.js';
|
|
21
|
+
/** A failing call the caller named in advance — matched against the window's real calls. */
|
|
22
|
+
export interface DeclaredNetFailure {
|
|
23
|
+
method?: string;
|
|
24
|
+
urlContains?: string;
|
|
25
|
+
status?: number;
|
|
26
|
+
}
|
|
27
|
+
export interface DeclaredExpectations {
|
|
28
|
+
/** Requests the caller declared would FAIL, so failing is the expected outcome, not a disagreement. */
|
|
29
|
+
netFailures: readonly DeclaredNetFailure[];
|
|
30
|
+
/** The caller required something to be ON SCREEN — an element or text, present rather than absent. */
|
|
31
|
+
rendersContent: boolean;
|
|
32
|
+
}
|
|
33
|
+
export declare function declaredExpectations(predicate: Predicate | undefined): DeclaredExpectations;
|
|
34
|
+
/**
|
|
35
|
+
* Does a call the window recorded match a failure the caller declared?
|
|
36
|
+
*
|
|
37
|
+
* Every field the caller named must agree — a declaration about `POST /api/login → 500` says nothing
|
|
38
|
+
* about `POST /api/orders`, and treating it as a blanket amnesty for failed requests would remove
|
|
39
|
+
* the check rather than inform it. Fields the caller left out are not constraints.
|
|
40
|
+
*/
|
|
41
|
+
export declare function matchesDeclaredFailure(call: {
|
|
42
|
+
method: string;
|
|
43
|
+
url: string;
|
|
44
|
+
status: number | undefined;
|
|
45
|
+
}, declared: readonly DeclaredNetFailure[]): boolean;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What the caller DECLARED before acting — the oracle, read back.
|
|
3
|
+
*
|
|
4
|
+
* `act_and_wait` asks the agent to name the expected consequence BEFORE the action, and that
|
|
5
|
+
* declaration was then thrown away: everything downstream judged the window as if nobody had said
|
|
6
|
+
* what they were expecting. Two measured consequences, one bug:
|
|
7
|
+
*
|
|
8
|
+
* - An error path can be declared exactly — `{ net, POST, /api/login, status: 500 }` — and the
|
|
9
|
+
* contradiction hunter still reported `ui-advanced-request-failed`, because a failed request plus
|
|
10
|
+
* a moved DOM is all it can see. The failure was EXPECTED and said so in the predicate, so a
|
|
11
|
+
* verdict of "channels disagree" is a statement about a disagreement nobody had.
|
|
12
|
+
* - A destination whose content was asserted and FOUND was still reported as
|
|
13
|
+
* `route-rendered-nothing`, a clause the element evidence beside it disproves.
|
|
14
|
+
*
|
|
15
|
+
* Deliberately conservative about what counts as declared. Only what the caller REQUIRED is read:
|
|
16
|
+
* the top level and `allOf` chains. An `anyOf` branch may never have held, and a `not` declares the
|
|
17
|
+
* opposite of a consequence — honouring either would suppress a real finding on the strength of
|
|
18
|
+
* something that never happened. Pure: a predicate in, a description out.
|
|
19
|
+
*/
|
|
20
|
+
import { PredicateKind } from '@reticlehq/core';
|
|
21
|
+
/** Below this, a status is a success or a redirect: not a declared failure. */
|
|
22
|
+
const FAILURE_STATUS_MIN = 400;
|
|
23
|
+
export function declaredExpectations(predicate) {
|
|
24
|
+
const netFailures = [];
|
|
25
|
+
let rendersContent = false;
|
|
26
|
+
const walk = (p) => {
|
|
27
|
+
switch (p.kind) {
|
|
28
|
+
case PredicateKind.ALL_OF:
|
|
29
|
+
for (const child of p.predicates)
|
|
30
|
+
walk(child);
|
|
31
|
+
return;
|
|
32
|
+
case PredicateKind.NET: {
|
|
33
|
+
const declaredFailure = false === p.ok || (p.status !== undefined && p.status >= FAILURE_STATUS_MIN);
|
|
34
|
+
if (!declaredFailure)
|
|
35
|
+
return;
|
|
36
|
+
netFailures.push({
|
|
37
|
+
...(p.method === undefined ? {} : { method: p.method }),
|
|
38
|
+
...(p.urlContains === undefined ? {} : { urlContains: p.urlContains }),
|
|
39
|
+
...(p.status === undefined ? {} : { status: p.status }),
|
|
40
|
+
});
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
case PredicateKind.ELEMENT:
|
|
44
|
+
case PredicateKind.TEXT:
|
|
45
|
+
if (true !== p.absent)
|
|
46
|
+
rendersContent = true;
|
|
47
|
+
return;
|
|
48
|
+
default:
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
if (predicate !== undefined)
|
|
53
|
+
walk(predicate);
|
|
54
|
+
return { netFailures, rendersContent };
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Does a call the window recorded match a failure the caller declared?
|
|
58
|
+
*
|
|
59
|
+
* Every field the caller named must agree — a declaration about `POST /api/login → 500` says nothing
|
|
60
|
+
* about `POST /api/orders`, and treating it as a blanket amnesty for failed requests would remove
|
|
61
|
+
* the check rather than inform it. Fields the caller left out are not constraints.
|
|
62
|
+
*/
|
|
63
|
+
export function matchesDeclaredFailure(call, declared) {
|
|
64
|
+
return declared.some((d) => {
|
|
65
|
+
if (d.method !== undefined && d.method.toUpperCase() !== call.method.toUpperCase())
|
|
66
|
+
return false;
|
|
67
|
+
if (d.urlContains !== undefined && !call.url.includes(d.urlContains))
|
|
68
|
+
return false;
|
|
69
|
+
if (d.status !== undefined && d.status !== call.status)
|
|
70
|
+
return false;
|
|
71
|
+
return true;
|
|
72
|
+
});
|
|
73
|
+
}
|
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
import { type ReticleEvent } from '@reticlehq/core';
|
|
2
2
|
import type { Contradiction } from './contradictions.js';
|
|
3
|
-
/**
|
|
4
|
-
|
|
3
|
+
/**
|
|
4
|
+
* Contradictions for writes whose response echoes a different value than the request asked for.
|
|
5
|
+
*
|
|
6
|
+
* `actionSince` is the attribution floor `findContradictions` already keeps — the event time the
|
|
7
|
+
* action that opened this window was dispatched. A finding about a request that had already settled
|
|
8
|
+
* before the agent acted otherwise reads as a verdict on the change it just made.
|
|
9
|
+
*/
|
|
10
|
+
export declare function findEchoMismatches(events: readonly ReticleEvent[], actionSince?: number): Contradiction[];
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ContradictionKind, EventType } from '@reticlehq/core';
|
|
1
|
+
import { ContradictionKind, EventType, MUTATING_METHODS } from '@reticlehq/core';
|
|
2
2
|
/**
|
|
3
3
|
* A write that returned OK and quietly did not apply part of what it was asked to.
|
|
4
4
|
*
|
|
@@ -18,6 +18,11 @@ import { ContradictionKind, EventType } from '@reticlehq/core';
|
|
|
18
18
|
* would fire on healthy APIs constantly, and a contradiction kind that cries wolf gets filtered out
|
|
19
19
|
* by the agents reading it, taking the true positives with it. So:
|
|
20
20
|
*
|
|
21
|
+
* - only requests that are WRITES are considered. Reported from the field: a lookup that sends its
|
|
22
|
+
* key in the body (`POST /get-branding` with `{workspace_id}`) and gets a record back was graded
|
|
23
|
+
* as a half-applied write. The gate was on the RESPONSE being a success and never on the REQUEST
|
|
24
|
+
* being a write, so every successful call carrying a body read as a save — and the same key name
|
|
25
|
+
* legitimately means different things on the two sides of a request/response pair;
|
|
21
26
|
* - only keys the request actually SENT are considered;
|
|
22
27
|
* - only scalars, since deep structural diffing is where the false positives live;
|
|
23
28
|
* - values are compared NORMALISED (trimmed, case-folded, numbers as numbers), so `FR` vs `fr` and
|
|
@@ -89,12 +94,33 @@ function scalarsByKey(body, into = new Map(), depth = 0) {
|
|
|
89
94
|
}
|
|
90
95
|
const OK_MIN = 200;
|
|
91
96
|
const OK_MAX = 300;
|
|
92
|
-
/**
|
|
93
|
-
|
|
97
|
+
/**
|
|
98
|
+
* Contradictions for writes whose response echoes a different value than the request asked for.
|
|
99
|
+
*
|
|
100
|
+
* `actionSince` is the attribution floor `findContradictions` already keeps — the event time the
|
|
101
|
+
* action that opened this window was dispatched. A finding about a request that had already settled
|
|
102
|
+
* before the agent acted otherwise reads as a verdict on the change it just made.
|
|
103
|
+
*/
|
|
104
|
+
export function findEchoMismatches(events, actionSince) {
|
|
94
105
|
const found = [];
|
|
95
106
|
for (const event of events) {
|
|
96
107
|
if (event.type !== EventType.NET_REQUEST)
|
|
97
108
|
continue;
|
|
109
|
+
// `MUTATING_METHODS` is the one definition of "is this a write" in the codebase — the same list
|
|
110
|
+
// `contradictions.ts`'s `isMutating` reads. A read has no fields to half-apply.
|
|
111
|
+
//
|
|
112
|
+
// KNOWN LIMIT, stated because it is easy to read this fix as bigger than it is: POST is in that
|
|
113
|
+
// list, so a lookup that POSTs its key and gets a record back — the shape actually reported —
|
|
114
|
+
// still reaches the comparison below. Method is the only signal available here, and a read-shaped
|
|
115
|
+
// POST is genuinely indistinguishable from a write by method alone.
|
|
116
|
+
//
|
|
117
|
+
// The tempting next step is to skip a pair whose JSON types disagree (a string key sent, a number
|
|
118
|
+
// echoed). Deliberately not taken: that also silences `{"qty":"5"}` answered with `3`, which is a
|
|
119
|
+
// real dropped write. A false negative here is a missed bug in someone's app, and that is not a
|
|
120
|
+
// trade this file makes to quieten a false positive.
|
|
121
|
+
const method = 'string' === typeof event.data['method'] ? event.data['method'] : '';
|
|
122
|
+
if (!MUTATING_METHODS.includes(method.toUpperCase()))
|
|
123
|
+
continue;
|
|
98
124
|
// IPC reports `ok` rather than a status; HTTP reports a status. Either must say success — a write
|
|
99
125
|
// that already failed is a different (and louder) finding than one that half-applied.
|
|
100
126
|
const status = event.data['status'];
|
|
@@ -126,12 +152,14 @@ export function findEchoMismatches(events) {
|
|
|
126
152
|
if (0 === dropped.length)
|
|
127
153
|
continue;
|
|
128
154
|
const url = 'string' === typeof event.data['url'] ? event.data['url'] : '';
|
|
129
|
-
const
|
|
155
|
+
const attribution = actionSince !== undefined && event.t < actionSince
|
|
156
|
+
? ' — this request completed before the action being verified, so the finding is not about that change'
|
|
157
|
+
: '';
|
|
130
158
|
found.push({
|
|
131
159
|
kind: ContradictionKind.WRITE_FIELD_IGNORED,
|
|
132
160
|
claim: 'the write returned success and the page treated it as saved',
|
|
133
161
|
counter: `its own echo shows ${String(dropped.length)} field(s) NOT applied — ${dropped.join('; ')}`,
|
|
134
|
-
detail: `${method} ${url} — the server answered OK and returned a different value than it was asked to set, so the write half-applied; the UI has no way to know and will show the value the user typed`,
|
|
162
|
+
detail: `${method} ${url} — the server answered OK and returned a different value than it was asked to set, so the write half-applied; the UI has no way to know and will show the value the user typed${attribution}`,
|
|
135
163
|
});
|
|
136
164
|
}
|
|
137
165
|
return found;
|
|
@@ -57,7 +57,7 @@ interface NetCallView {
|
|
|
57
57
|
*/
|
|
58
58
|
oneWay?: boolean;
|
|
59
59
|
}
|
|
60
|
-
export declare function projectNetCall(e: ReticleEvent): NetCallView;
|
|
60
|
+
export declare function projectNetCall(e: ReticleEvent, includeBodies?: boolean): NetCallView;
|
|
61
61
|
/**
|
|
62
62
|
* Compact console-log summary for reticle_console output.
|
|
63
63
|
*
|
|
@@ -86,7 +86,7 @@ export function reconcileNet(events) {
|
|
|
86
86
|
.map((e) => ({ ...e, data: { ...e.data, status: 'pending', pending: true } }));
|
|
87
87
|
return [...completed, ...unresolved].sort((a, b) => a.t - b.t);
|
|
88
88
|
}
|
|
89
|
-
export function projectNetCall(e) {
|
|
89
|
+
export function projectNetCall(e, includeBodies = true) {
|
|
90
90
|
const status = e.data['status'];
|
|
91
91
|
const ms = asNumber(e.data['durationMs']);
|
|
92
92
|
const statusText = asString(e.data['statusText']);
|
|
@@ -106,11 +106,14 @@ export function projectNetCall(e) {
|
|
|
106
106
|
view.contentType = contentType;
|
|
107
107
|
if (responseSize !== undefined)
|
|
108
108
|
view.responseSize = responseSize;
|
|
109
|
-
|
|
109
|
+
// Bodies dominate the payload of the most-called read tool, and are only occasionally the part
|
|
110
|
+
// anyone wanted — `bodies: false` on reticle_network drops them for a body-free listing (#401).
|
|
111
|
+
if (includeBodies && requestBody !== undefined)
|
|
110
112
|
view.requestBody = requestBody;
|
|
111
|
-
if (responseBody !== undefined)
|
|
113
|
+
if (includeBodies && responseBody !== undefined)
|
|
112
114
|
view.responseBody = responseBody;
|
|
113
|
-
if (
|
|
115
|
+
if (includeBodies &&
|
|
116
|
+
(true === e.data['requestBodyTruncated'] || true === e.data['responseBodyTruncated'])) {
|
|
114
117
|
view.bodyTruncated = true;
|
|
115
118
|
}
|
|
116
119
|
if (ms !== undefined)
|