@reticlehq/server 2.7.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/README.md +14 -5
- package/SKILL.md +130 -778
- 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 +36 -1
- package/dist/capsule/causal-summary.js +33 -7
- package/dist/capsule/divergence.js +7 -1
- package/dist/cli/chromium-hint.d.ts +33 -5
- package/dist/cli/chromium-hint.js +82 -12
- package/dist/cli/cli-doctor.js +65 -34
- package/dist/cli/cli-flow-commands.js +8 -8
- package/dist/cli/cli-kill.d.ts +58 -0
- package/dist/cli/cli-kill.js +162 -0
- package/dist/cli/cli-parse.d.ts +16 -1
- package/dist/cli/cli-parse.js +32 -9
- package/dist/cli/cli-port.d.ts +32 -0
- package/dist/cli/cli-port.js +82 -35
- package/dist/cli/cli-verify.d.ts +21 -0
- package/dist/cli/cli-verify.js +47 -3
- 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.d.ts +12 -0
- package/dist/cli/doctor-sessions-line.js +69 -0
- 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/port-holder.d.ts +10 -0
- package/dist/cli/port-holder.js +26 -2
- package/dist/cli/status-next-action.d.ts +37 -0
- package/dist/cli/status-next-action.js +61 -0
- package/dist/cli.js +119 -39
- package/dist/crawl/crawl-tools.js +12 -2
- package/dist/crawl/crawl.d.ts +13 -0
- package/dist/crawl/crawl.js +50 -2
- package/dist/events/contradictions.d.ts +46 -0
- package/dist/events/contradictions.js +35 -22
- 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 -72
- package/dist/events/predicate-eval.js +117 -236
- 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 +296 -22
- 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-classify.js +1 -1
- package/dist/flows/flow-replay-run.js +8 -1
- package/dist/flows/flow-tools.js +21 -5
- package/dist/flows/heal-run.js +1 -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 +10 -2
- 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 +49 -0
- package/dist/honesty/unsettled.js +82 -0
- package/dist/honesty/verified.d.ts +36 -3
- package/dist/honesty/verified.js +54 -11
- 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 +47 -10
- package/dist/init/agent-rules.d.ts +36 -0
- package/dist/init/agent-rules.js +132 -22
- package/dist/init/astro-patch.d.ts +2 -1
- package/dist/init/astro-patch.js +38 -6
- package/dist/init/capabilities.d.ts +22 -0
- package/dist/init/capabilities.js +76 -0
- package/dist/init/confirm.d.ts +114 -0
- package/dist/init/confirm.js +165 -0
- package/dist/init/connect-steps.d.ts +2 -0
- package/dist/init/connect-steps.js +50 -0
- package/dist/init/cra.js +3 -2
- 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/non-js-project.js +29 -5
- package/dist/init/plan-framework.d.ts +10 -0
- package/dist/init/plan-framework.js +78 -16
- package/dist/init/plan.d.ts +47 -6
- package/dist/init/plan.js +228 -69
- package/dist/init/run.d.ts +24 -2
- package/dist/init/run.js +157 -45
- package/dist/init/slash-command.d.ts +1 -1
- package/dist/init/slash-command.js +54 -6
- package/dist/init/snippets.d.ts +45 -5
- package/dist/init/snippets.js +194 -42
- package/dist/init/vite-config.js +53 -5
- package/dist/input/network-mock-tools.d.ts +19 -0
- package/dist/input/network-mock-tools.js +37 -5
- 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.js +1 -1
- package/dist/mcp/mcp-proxy.d.ts +35 -30
- package/dist/mcp/mcp-proxy.js +306 -104
- package/dist/mcp/mcp.d.ts +10 -1
- package/dist/mcp/mcp.js +70 -34
- package/dist/mcp/proxy-handshake.d.ts +82 -1
- package/dist/mcp/proxy-handshake.js +109 -2
- package/dist/mcp/proxy-lifecycle.d.ts +11 -2
- package/dist/mcp/proxy-lifecycle.js +12 -3
- 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/pool/browser-pool.d.ts +22 -2
- package/dist/pool/browser-pool.js +44 -4
- package/dist/project/learned-routes.d.ts +24 -0
- package/dist/project/learned-routes.js +63 -0
- package/dist/project/project-store.d.ts +6 -0
- package/dist/project/project-store.js +25 -1
- package/dist/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 +67 -2
- package/dist/session/no-session-diagnosis.js +236 -27
- 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 +174 -8
- package/dist/session/pending-commands.d.ts +12 -0
- package/dist/session/pending-commands.js +13 -1
- package/dist/session/ref-provenance.d.ts +54 -0
- package/dist/session/ref-provenance.js +61 -0
- package/dist/session/session-lease.js +1 -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/automation-hint.d.ts +17 -0
- package/dist/telemetry/automation-hint.js +71 -0
- package/dist/telemetry/bug-found.d.ts +15 -17
- package/dist/telemetry/bug-found.js +52 -21
- package/dist/telemetry/cli-telemetry.js +7 -1
- package/dist/telemetry/daemon-telemetry.js +24 -0
- package/dist/telemetry/feedback.js +1 -1
- package/dist/telemetry/init-telemetry.js +9 -1
- package/dist/telemetry/install-source.d.ts +56 -0
- package/dist/telemetry/install-source.js +74 -0
- package/dist/telemetry/instrumentation-stall.d.ts +50 -0
- package/dist/telemetry/instrumentation-stall.js +90 -0
- package/dist/telemetry/run-telemetry.js +4 -1
- package/dist/telemetry/session-metrics.d.ts +8 -0
- package/dist/telemetry/session-metrics.js +15 -0
- package/dist/telemetry/telemetry.d.ts +7 -1
- package/dist/telemetry/telemetry.js +26 -2
- package/dist/telemetry/tool-refused.d.ts +19 -0
- package/dist/telemetry/tool-refused.js +55 -0
- package/dist/telemetry/verification-of.js +46 -7
- package/dist/temp-dir.d.ts +27 -0
- package/dist/temp-dir.js +46 -0
- package/dist/tools/act-tools.js +209 -28
- package/dist/tools/assert-grade.d.ts +1 -1
- package/dist/tools/assert-grade.js +27 -5
- package/dist/tools/assert-verdict.js +49 -7
- package/dist/tools/browser-tools.js +10 -0
- package/dist/tools/contract-tools.js +4 -0
- package/dist/tools/dynamic-tools.js +16 -7
- package/dist/tools/error-recovery.d.ts +10 -0
- package/dist/tools/error-recovery.js +73 -9
- package/dist/tools/feedback-invite.js +1 -1
- package/dist/tools/invoke-tool.js +159 -26
- package/dist/tools/lease-hint.d.ts +30 -8
- package/dist/tools/lease-hint.js +65 -14
- package/dist/tools/lease-tools.d.ts +40 -1
- package/dist/tools/lease-tools.js +181 -83
- package/dist/tools/navigate-result.js +1 -1
- package/dist/tools/observe-tools.js +10 -4
- package/dist/tools/observed-nothing.js +1 -1
- package/dist/tools/react-grace.d.ts +52 -0
- package/dist/tools/react-grace.js +90 -0
- package/dist/tools/read-tools.js +67 -5
- 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 +94 -0
- package/dist/tools/settle-in-flight.js +146 -0
- package/dist/tools/snapshot-delta.d.ts +10 -0
- package/dist/tools/snapshot-delta.js +194 -9
- package/dist/tools/tool-kit.d.ts +8 -0
- package/dist/tools/tool-surface.d.ts +60 -0
- package/dist/tools/tool-surface.js +86 -1
- package/dist/tools/tools.js +61 -8
- package/dist/tools/unadvertised-help.js +9 -6
- package/docs/README.md +55 -11
- package/docs/actions.mdx +120 -0
- package/docs/agent-cheatsheet.md +95 -29
- package/docs/architecture.md +52 -35
- package/docs/benchmarks.md +127 -75
- package/docs/best-practices.mdx +216 -0
- package/docs/capabilities.mdx +534 -0
- package/docs/cli/affected.mdx +53 -0
- package/docs/cli/capsules.mdx +70 -0
- package/docs/cli/cloud.mdx +153 -0
- package/docs/cli/doctor.mdx +109 -0
- package/docs/cli/drive.mdx +74 -0
- package/docs/cli/feedback.mdx +67 -0
- package/docs/cli/gate.mdx +64 -0
- package/docs/cli/help.mdx +94 -0
- package/docs/cli/hunt.mdx +73 -0
- package/docs/cli/identify.mdx +76 -0
- package/docs/cli/init.mdx +121 -0
- package/docs/cli/kill.mdx +91 -0
- package/docs/cli/license.mdx +48 -0
- package/docs/cli/mcp.mdx +61 -0
- package/docs/cli/open.mdx +74 -0
- package/docs/cli/restart.mdx +60 -0
- package/docs/cli/rollback.mdx +35 -0
- package/docs/cli/serve.mdx +69 -0
- package/docs/cli/status.mdx +88 -0
- package/docs/cli/stop.mdx +64 -0
- package/docs/cli/telemetry.mdx +57 -0
- package/docs/cli/update.mdx +52 -0
- package/docs/cli/verify.mdx +76 -0
- package/docs/cli/version.mdx +39 -0
- package/docs/cli/watch.mdx +62 -0
- package/docs/cli.mdx +241 -0
- package/docs/debugging.md +16 -10
- package/docs/deploy-checks.md +45 -12
- package/docs/desktop.mdx +384 -0
- package/docs/docs.json +364 -0
- package/docs/enterprise.md +22 -16
- package/docs/faq.mdx +102 -0
- package/docs/fixtures.md +13 -7
- package/docs/flows.md +29 -23
- package/docs/for-agents.md +71 -0
- package/docs/frameworks.mdx +238 -0
- package/docs/gate-plan.md +48 -42
- package/docs/gates.md +28 -22
- package/docs/getting-started.md +129 -77
- package/docs/human-control.md +24 -18
- package/docs/index.mdx +143 -0
- package/docs/install-agentic.mdx +224 -0
- package/docs/install-manual.mdx +196 -0
- package/docs/instrumentation.mdx +238 -0
- package/docs/integration-patterns.md +41 -35
- package/docs/local-registry.md +19 -13
- package/docs/multi-agent-testing.md +19 -13
- package/docs/packages/babel-plugin.mdx +79 -0
- package/docs/packages/browser.mdx +111 -0
- package/docs/packages/core.mdx +116 -0
- package/docs/packages/electron.mdx +85 -0
- package/docs/packages/eslint-plugin.mdx +86 -0
- package/docs/packages/next.mdx +89 -0
- package/docs/packages/react.mdx +91 -0
- package/docs/packages/server.mdx +98 -0
- package/docs/packages/tauri.mdx +63 -0
- package/docs/packages/test.mdx +119 -0
- package/docs/packages/vite-plugin.mdx +94 -0
- package/docs/packages.mdx +218 -0
- package/docs/platform-integration.md +35 -26
- package/docs/predicates.mdx +248 -0
- package/docs/quickstart.mdx +257 -0
- package/docs/recipes.mdx +282 -0
- package/docs/skill-file.mdx +107 -0
- package/docs/state-management.mdx +199 -0
- package/docs/style.css +14 -0
- package/docs/system-map.md +50 -33
- package/docs/telemetry-contract.md +106 -52
- package/docs/telemetry-events.mdx +475 -0
- package/docs/telemetry.md +63 -55
- package/docs/testing.md +44 -6
- package/docs/token-efficiency.md +37 -27
- package/docs/tools-act-and-wait.mdx +173 -0
- package/docs/tools-act-sequence.mdx +137 -0
- package/docs/tools-act.mdx +95 -0
- package/docs/tools-assert.mdx +129 -0
- package/docs/tools-console.mdx +63 -0
- package/docs/tools-inspect.mdx +88 -0
- package/docs/tools-navigate.mdx +99 -0
- package/docs/tools-network.mdx +119 -0
- package/docs/tools-observe.mdx +145 -0
- package/docs/tools-overview.mdx +160 -0
- package/docs/tools-query.mdx +126 -0
- package/docs/tools-session-and-feedback.mdx +112 -0
- package/docs/tools-sessions.mdx +94 -0
- package/docs/tools-snapshot.mdx +129 -0
- package/docs/tools-state.mdx +104 -0
- package/docs/tools-tools-and-run.mdx +156 -0
- package/docs/tools-wait-for.mdx +85 -0
- package/docs/troubleshooting.mdx +263 -0
- package/docs/usage.md +209 -165
- package/docs/vs-chrome-devtools-mcp.mdx +123 -0
- package/docs/vs-playwright-mcp.mdx +125 -0
- package/docs/vs-screenshots.mdx +137 -0
- package/docs/what-is-reticle.mdx +63 -0
- package/docs/why-reticle.mdx +109 -0
- package/package.json +5 -5
- package/docs/desktop-apps.md +0 -256
- package/docs/matrix/2.5.0/claude-code-machine.json +0 -19
- package/docs/matrix/2.5.0/codex-machine.json +0 -20
- package/docs/matrix/2.5.0/cursor-machine.json +0 -22
- package/docs/matrix/2.5.0/gemini-machine.json +0 -22
- package/docs/matrix/2.5.0/opencode-machine.json +0 -22
- package/docs/matrix/2.5.0/vscode-machine.json +0 -22
- package/docs/matrix/2.5.0/windsurf-machine.json +0 -22
- package/docs/matrix/MATRIX.md +0 -26
- package/docs/matrix/README.md +0 -59
- package/docs/mint.json +0 -61
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The sentence `status` owes the user, because `init` promised it.
|
|
3
|
+
*
|
|
4
|
+
* `init` closes with "run `npx @reticlehq/server status` — it confirms the app connected, or says
|
|
5
|
+
* exactly why it has not". On a real install, following that instruction printed `running: false,
|
|
6
|
+
* presence: "free"` and nothing else: no reason, no next step, no mention of the app. This is the last
|
|
7
|
+
* checkpoint in the setup path, so a user who arrives here and learns nothing has nowhere left to go.
|
|
8
|
+
*
|
|
9
|
+
* Deliberately thin. The differential already exists in `nextActionFor`, which ranks these causes from
|
|
10
|
+
* what the daemon knows and was rewritten this week to stop leading with a port mismatch; a second
|
|
11
|
+
* differential here could disagree with the first, and two confident answers pointing different ways
|
|
12
|
+
* is worse than one. This only supplies the facts the CLI has and renders the answer.
|
|
13
|
+
*/
|
|
14
|
+
import { nextActionFor } from '../session/no-session-next-action.js';
|
|
15
|
+
/**
|
|
16
|
+
* What to do next, or `undefined` when a session is connected and there is nothing to fix.
|
|
17
|
+
*
|
|
18
|
+
* The success case has to stay silent: advice printed beside a working session reads as though
|
|
19
|
+
* something is still wrong, which is its own kind of lie.
|
|
20
|
+
*/
|
|
21
|
+
export function statusNextAction(facts) {
|
|
22
|
+
if (facts.sessionCount > 0)
|
|
23
|
+
return undefined;
|
|
24
|
+
// Ahead of everything else, because it dominates: an app that was never wired cannot connect, and
|
|
25
|
+
// no advice about dev servers or ports applies until it is. `previouslyConnected` overrides it —
|
|
26
|
+
// an app CAN be wired by the Vite or Babel plugin with no `.reticle.json` at all, so a project
|
|
27
|
+
// that has connected here before is wired whatever this file says, and sending it back to `init`
|
|
28
|
+
// would be the same wrong answer in the other direction.
|
|
29
|
+
if (!facts.initialized && !facts.previouslyConnected) {
|
|
30
|
+
return ('no app has ever connected for this project, and there is no Reticle config here — so the ' +
|
|
31
|
+
'tools are registered and the app itself is not instrumented. Those are two different halves ' +
|
|
32
|
+
'of the install. Run `npx @reticlehq/server init` in the app directory, then start the dev ' +
|
|
33
|
+
'server and load the page.');
|
|
34
|
+
}
|
|
35
|
+
if (!facts.running) {
|
|
36
|
+
// `running: false` reads as "Reticle is broken", and it usually means the opposite: the daemon is
|
|
37
|
+
// started by an agent, on demand, and idles out when nobody is driving. A user who has not
|
|
38
|
+
// attached an agent yet is exactly on track, and saying so is the whole job of this branch.
|
|
39
|
+
const wiring = facts.previouslyConnected
|
|
40
|
+
? 'This project has connected before, so the wiring is correct.'
|
|
41
|
+
: '';
|
|
42
|
+
return (`no daemon is running on this port, which is normal — an agent starts it when it first calls a ` +
|
|
43
|
+
`Reticle tool, and it exits again when idle. ${wiring} Ask your agent to verify something, then ` +
|
|
44
|
+
`run status again.`).replace(/\s+/g, ' ');
|
|
45
|
+
}
|
|
46
|
+
// The daemon is up and no page has connected. `nextActionFor` owns the ranking; the CLI cannot see
|
|
47
|
+
// listening ports or a dev command from here, so it passes what it has and lets that function say
|
|
48
|
+
// what is missing rather than guessing at it.
|
|
49
|
+
const next = nextActionFor({
|
|
50
|
+
// The durable bit IS this side's `everConnected`. In-process the flag means "this daemon has served
|
|
51
|
+
// a session", which a daemon seconds old cannot know; from the CLI the honest equivalent is "an app
|
|
52
|
+
// for this project has connected on this port", which is the fact that decides whether the wiring
|
|
53
|
+
// is in question at all.
|
|
54
|
+
everConnected: facts.previouslyConnected,
|
|
55
|
+
initialized: facts.initialized,
|
|
56
|
+
listening: [],
|
|
57
|
+
dev: undefined,
|
|
58
|
+
previouslyConnected: facts.previouslyConnected,
|
|
59
|
+
});
|
|
60
|
+
return next.reason;
|
|
61
|
+
}
|
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,10 +25,13 @@ 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, openInBrowser, } from './cli/cli-launch.js';
|
|
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';
|
|
31
|
+
import { runKill } from './cli/cli-kill.js';
|
|
27
32
|
import { summarizeHunt } from './hunt/hunt-report.js';
|
|
28
33
|
import { runInit } from './init/run.js';
|
|
34
|
+
import { confirmInstall, nodeConfirmDeps } from './init/confirm.js';
|
|
29
35
|
import { handleDoctor } from './cli/cli-doctor.js';
|
|
30
36
|
import { buildNodeIo } from './init/node-io.js';
|
|
31
37
|
import { describeLicense } from './license/license.js';
|
|
@@ -38,6 +44,7 @@ import { reportCliRun } from './telemetry/cli-telemetry.js';
|
|
|
38
44
|
export { parseCliArgs, CLI_USAGE };
|
|
39
45
|
function handleInit(parsed) {
|
|
40
46
|
const cwd = process.cwd();
|
|
47
|
+
const io = buildNodeIo(cwd);
|
|
41
48
|
const result = runInit({
|
|
42
49
|
cwd,
|
|
43
50
|
port: parsed.port,
|
|
@@ -45,9 +52,14 @@ function handleInit(parsed) {
|
|
|
45
52
|
dryRun: parsed.dryRun,
|
|
46
53
|
install: parsed.install,
|
|
47
54
|
...(parsed.app === undefined ? {} : { app: parsed.app }),
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
55
|
+
// The outcome is reported by confirmInstall instead, once it knows whether an app connected —
|
|
56
|
+
// `init` writing files was never the same thing as `init` working (#269).
|
|
57
|
+
deferOutcome: true,
|
|
58
|
+
}, io);
|
|
59
|
+
void confirmInstall(result, io, nodeConfirmDeps(parsed.port ?? RETICLE_DEFAULT_PORT)).then(() => {
|
|
60
|
+
if (!result.ok)
|
|
61
|
+
process.exit(1);
|
|
62
|
+
});
|
|
51
63
|
}
|
|
52
64
|
function handleServe(parsed) {
|
|
53
65
|
void serveWithHonestExit(parsed);
|
|
@@ -190,17 +202,56 @@ function handleStop(port, quiet) {
|
|
|
190
202
|
}
|
|
191
203
|
}, 100);
|
|
192
204
|
}
|
|
205
|
+
/**
|
|
206
|
+
* `reticle restart` — free the port, then put a daemon back on it and prove it bound.
|
|
207
|
+
*
|
|
208
|
+
* The two halves are reported separately on purpose. A restart that killed the old daemon and then
|
|
209
|
+
* could not start a new one leaves the user in a different place from one that could not kill
|
|
210
|
+
* anything at all, and a single `ok: false` cannot tell them which happened.
|
|
211
|
+
*/
|
|
212
|
+
async function handleRestart(port, force) {
|
|
213
|
+
const freed = await runKill(port, force);
|
|
214
|
+
if (!freed) {
|
|
215
|
+
log('reticle_restart_aborted', {
|
|
216
|
+
port,
|
|
217
|
+
reason: 'the port was not freed, so nothing was started — the old holder is still there',
|
|
218
|
+
});
|
|
219
|
+
process.exit(1);
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
// Reuses `serve`'s path, which waits for the daemon to ANSWER before claiming it started and exits
|
|
223
|
+
// non-zero when it does not. A restart that reports success for a daemon that never bound is the
|
|
224
|
+
// same lie `serve` already stopped telling.
|
|
225
|
+
await serveWithHonestExit({ port, headless: true, http: false });
|
|
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
|
+
}
|
|
193
232
|
function handleStatus(port) {
|
|
194
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;
|
|
195
241
|
if (null === pid || !isAlive(pid)) {
|
|
196
242
|
// `running: false` on its own has been reported about a port that was demonstrably occupied,
|
|
197
243
|
// because the pid file is not the port. Ask the port before answering.
|
|
198
244
|
void probePresence(port, { tcpOpen: probeDaemon, status: fetchStatus }).then((presence) => {
|
|
245
|
+
const running = presenceIsUsable(presence);
|
|
199
246
|
log('reticle_status', {
|
|
200
247
|
port,
|
|
201
|
-
running
|
|
248
|
+
running,
|
|
202
249
|
presence,
|
|
203
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 }),
|
|
204
255
|
});
|
|
205
256
|
});
|
|
206
257
|
return;
|
|
@@ -213,10 +264,23 @@ function handleStatus(port) {
|
|
|
213
264
|
const update = availableUpdate();
|
|
214
265
|
const nudge = update === undefined ? {} : { updateAvailable: update };
|
|
215
266
|
if (payload === undefined) {
|
|
216
|
-
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
|
+
});
|
|
217
274
|
return;
|
|
218
275
|
}
|
|
219
|
-
|
|
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 });
|
|
220
284
|
});
|
|
221
285
|
}
|
|
222
286
|
/** Print the running package version (resolved once in server-version.ts). */
|
|
@@ -288,13 +352,20 @@ async function handleHunt(dir) {
|
|
|
288
352
|
}
|
|
289
353
|
/** Ensure a daemon is reachable on `port` (probe the real port; spawn + wait only if nothing's there). */
|
|
290
354
|
function ensureDaemon(port) {
|
|
291
|
-
return
|
|
355
|
+
return probePresence(port, { tcpOpen: probeDaemon, status: fetchStatus }).then(async (presence) => {
|
|
292
356
|
// Attaching to whatever already owns the port is the whole point of a daemon — but it means an
|
|
293
357
|
// upgrade does NOT take effect until that daemon dies, and nothing used to say so. Say it here,
|
|
294
358
|
// where both versions are in hand, and keep attaching: killing another agent's daemon on a
|
|
295
359
|
// version bump is worse than a loud warning.
|
|
296
|
-
if (
|
|
360
|
+
if (presenceIsUsable(presence))
|
|
297
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));
|
|
298
369
|
const scriptPath = process.argv[1];
|
|
299
370
|
if (scriptPath === undefined)
|
|
300
371
|
throw new Error('cannot locate the reticle daemon script');
|
|
@@ -364,8 +435,12 @@ function handleOpen(requestedPort, url) {
|
|
|
364
435
|
log('reticle_open', {
|
|
365
436
|
port,
|
|
366
437
|
error: `could not launch a browser: ${launchError}`,
|
|
367
|
-
|
|
368
|
-
|
|
438
|
+
// Points at the OS handler, not at `reticle doctor`. Doctor's browser check is about
|
|
439
|
+
// Reticle's own Chromium, which this command never touches — the same misdirect the
|
|
440
|
+
// no-session note below spells out at length, left standing here for one more release.
|
|
441
|
+
recovery: 'Nothing was opened. This command asks the OS to open a url in your default browser ' +
|
|
442
|
+
`(\`${openCommand(decision.url, process.platform).cmd}\` on this platform) and that ` +
|
|
443
|
+
'failed, so the fix is on the OS side: open the url yourself, or set a default browser.',
|
|
369
444
|
});
|
|
370
445
|
process.exit(1);
|
|
371
446
|
return;
|
|
@@ -376,6 +451,7 @@ function handleOpen(requestedPort, url) {
|
|
|
376
451
|
const connected = await waitForNewSession(port, sessions.length);
|
|
377
452
|
log('reticle_open', {
|
|
378
453
|
port,
|
|
454
|
+
...(port === requestedPort ? {} : { requestedPort }),
|
|
379
455
|
opened: decision.url,
|
|
380
456
|
connected,
|
|
381
457
|
...(connected
|
|
@@ -388,14 +464,7 @@ function handleOpen(requestedPort, url) {
|
|
|
388
464
|
// about a browser THIS command never uses, so a missing Chromium reads as the
|
|
389
465
|
// explanation for a session that is missing for an unrelated reason. Both misdirects
|
|
390
466
|
// were reported from the field, each costing several calls of app-side wiring hunt.
|
|
391
|
-
note:
|
|
392
|
-
`Reticle's own Chromium, so a chromium warning from \`reticle doctor\` is unrelated ` +
|
|
393
|
-
`to this). No Reticle session appeared. By far the likeliest cause is that the app ` +
|
|
394
|
-
`carries no Reticle SDK, or dials a port other than ${String(port)} — run ` +
|
|
395
|
-
`\`reticle init\` in the app's directory and restart its dev server. If the app IS ` +
|
|
396
|
-
`wired, give the page a moment and check the browser console: the SDK announces its ` +
|
|
397
|
-
`own connect failures there, including the one it refuses to make from a ` +
|
|
398
|
-
`non-localhost host without allowNonLocalhost.`,
|
|
467
|
+
note: openFailureNote(port, requestedPort),
|
|
399
468
|
}),
|
|
400
469
|
});
|
|
401
470
|
})
|
|
@@ -444,6 +513,12 @@ function handleDaemonInner(parsed) {
|
|
|
444
513
|
// which is after everything below has run. Without it the exit line says `code: 0` and a
|
|
445
514
|
// reader cannot tell a tidy stop from the bridge disappearing — see #123.
|
|
446
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?.();
|
|
447
522
|
// Awaited before the close/exit chain: `process.exit(0)` kills an in-flight POST, and this is
|
|
448
523
|
// the one event carrying the whole session. A failed send resolves anyway (emit swallows its
|
|
449
524
|
// own errors), so this can delay the exit by at most the send timeout, never prevent it.
|
|
@@ -538,8 +613,15 @@ function handleMcp(opts) {
|
|
|
538
613
|
* disappeared mid-session with nothing said. Respawning here makes the reconnect self-healing.
|
|
539
614
|
*/
|
|
540
615
|
const ensure = async () => {
|
|
541
|
-
|
|
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))
|
|
542
622
|
return;
|
|
623
|
+
if (PortPresence.FREE !== presence)
|
|
624
|
+
throw new Error(describePresence(presence, port));
|
|
543
625
|
const scriptPath = process.argv[1];
|
|
544
626
|
if (scriptPath === undefined) {
|
|
545
627
|
log('reticle_mcp_no_script', {});
|
|
@@ -560,8 +642,13 @@ function handleMcp(opts) {
|
|
|
560
642
|
daemonArgs.push(HTTP_TOKEN_FLAG, httpToken);
|
|
561
643
|
}
|
|
562
644
|
spawnDaemon(process.execPath, scriptPath, daemonArgs, port);
|
|
563
|
-
|
|
645
|
+
// Announce the daemon only once the PORT ACCEPTS. This line used to be written the instant the
|
|
646
|
+
// child was spawned, which on a Windows first bootstrap meant `reticle_mcp_daemon_started`
|
|
647
|
+
// followed about ten seconds later by `reticle_mcp_daemon_unavailable` and a first
|
|
648
|
+
// `reticle_sessions` that expired. A readiness signal that precedes readiness is worse than
|
|
649
|
+
// none: a client that believes it stops waiting for the thing that has not happened.
|
|
564
650
|
await waitForDaemon(port);
|
|
651
|
+
log('reticle_mcp_daemon_started', { port, ...(driveUrl !== undefined ? { driveUrl } : {}) });
|
|
565
652
|
};
|
|
566
653
|
// Start the proxy WHATEVER happened to the daemon.
|
|
567
654
|
//
|
|
@@ -601,22 +688,6 @@ function handleMcp(opts) {
|
|
|
601
688
|
});
|
|
602
689
|
});
|
|
603
690
|
}
|
|
604
|
-
function handleLegacyDrive(parsed) {
|
|
605
|
-
const options = {
|
|
606
|
-
port: parsed.port,
|
|
607
|
-
driveUrl: parsed.driveUrl,
|
|
608
|
-
headless: parsed.headless,
|
|
609
|
-
};
|
|
610
|
-
start(options)
|
|
611
|
-
.then(() => {
|
|
612
|
-
log('reticle_started', { port: parsed.port });
|
|
613
|
-
})
|
|
614
|
-
.catch((error) => {
|
|
615
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
616
|
-
log('reticle_start_failed', { error: message });
|
|
617
|
-
process.exit(1);
|
|
618
|
-
});
|
|
619
|
-
}
|
|
620
691
|
function main() {
|
|
621
692
|
// Before anything reads process.env — notably the telemetry gate and the bridge's security
|
|
622
693
|
// options — fold in a project-local `.env`. Values already in the environment always win.
|
|
@@ -669,6 +740,15 @@ function main() {
|
|
|
669
740
|
case 'stop':
|
|
670
741
|
handleStop(parsed.port, parsed.quiet);
|
|
671
742
|
break;
|
|
743
|
+
case 'kill':
|
|
744
|
+
void runKill(parsed.port, parsed.force).then((freed) => {
|
|
745
|
+
if (!freed)
|
|
746
|
+
process.exit(1);
|
|
747
|
+
});
|
|
748
|
+
break;
|
|
749
|
+
case 'restart':
|
|
750
|
+
void handleRestart(parsed.port, parsed.force);
|
|
751
|
+
break;
|
|
672
752
|
case 'status':
|
|
673
753
|
handleStatus(parsed.port);
|
|
674
754
|
break;
|
|
@@ -697,7 +777,7 @@ function main() {
|
|
|
697
777
|
handleOpen(parsed.port, parsed.url);
|
|
698
778
|
break;
|
|
699
779
|
case 'drive':
|
|
700
|
-
|
|
780
|
+
handleDrive(parsed);
|
|
701
781
|
break;
|
|
702
782
|
case 'verify':
|
|
703
783
|
handleVerify(parsed);
|
|
@@ -2,6 +2,7 @@ import { z } from 'zod';
|
|
|
2
2
|
import { ReticleTool } from '../tools/tool-names.js';
|
|
3
3
|
import { asNumber, asString } from '../tools/tools-helpers.js';
|
|
4
4
|
import { crawl } from './crawl.js';
|
|
5
|
+
import { routeFromUrl, routesFromEvents } from '../project/learned-routes.js';
|
|
5
6
|
const nodeSleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
6
7
|
/**
|
|
7
8
|
* The autonomous "smart monkey" tool. Builds on reticle_explore (which only LISTS) by
|
|
@@ -58,8 +59,10 @@ export const CRAWL_TOOLS = [
|
|
|
58
59
|
.describe('Present only when the page exceeded one snapshot, so controls past the cap were never listed. When present, a zero-anomaly result does NOT mean the app is clean.'),
|
|
59
60
|
truncated: z.boolean(),
|
|
60
61
|
},
|
|
61
|
-
handler: (deps, args) => {
|
|
62
|
+
handler: async (deps, args) => {
|
|
62
63
|
const session = deps.sessions.resolve(asString(args['sessionId']));
|
|
64
|
+
const since = session.elapsed();
|
|
65
|
+
const initialRoute = routeFromUrl(session.url);
|
|
63
66
|
const maxSteps = asNumber(args['maxSteps']);
|
|
64
67
|
const settleMs = asNumber(args['settleMs']);
|
|
65
68
|
const scope = asString(args['scope']);
|
|
@@ -69,7 +72,14 @@ export const CRAWL_TOOLS = [
|
|
|
69
72
|
...(scope !== undefined ? { scope } : {}),
|
|
70
73
|
...(true === args['confirmDangerous'] ? { confirmDangerous: true } : {}),
|
|
71
74
|
};
|
|
72
|
-
|
|
75
|
+
const report = await crawl(session, opts, nodeSleep);
|
|
76
|
+
const routes = [
|
|
77
|
+
...(initialRoute === undefined ? [] : [initialRoute]),
|
|
78
|
+
...routesFromEvents(session.eventsSince(since)),
|
|
79
|
+
];
|
|
80
|
+
if (routes.length > 0)
|
|
81
|
+
await deps.project.recordRoutes(routes);
|
|
82
|
+
return report;
|
|
73
83
|
},
|
|
74
84
|
},
|
|
75
85
|
];
|
package/dist/crawl/crawl.d.ts
CHANGED
|
@@ -39,6 +39,19 @@ export interface CrawlAnomaly {
|
|
|
39
39
|
}
|
|
40
40
|
/** Said out loud when the page was larger than one snapshot, so a clean sweep cannot mean "all clear". */
|
|
41
41
|
export declare const CAPPED_SNAPSHOT_NOTE = "the page exceeded one snapshot, so controls past the cap were never seen \u2014 interactiveFound is a floor, not a total; re-run with a narrower `scope` to cover the rest";
|
|
42
|
+
/**
|
|
43
|
+
* Said out loud when the crawl's OWN clicks put controls on the page that it then never visited.
|
|
44
|
+
*
|
|
45
|
+
* The enumeration happens once, before the first click, so a control that only exists after a login,
|
|
46
|
+
* a modal or a tab switch is invisible to this run. Measured on the bench app: a crawl of the login
|
|
47
|
+
* screen visited three controls, its click on "Sign in" produced seven more, and it returned
|
|
48
|
+
* `truncated: false` with an empty anomaly list and nine of twelve steps unused. Read as a sweep,
|
|
49
|
+
* that is a clean bill of health for an app the crawl never entered.
|
|
50
|
+
*
|
|
51
|
+
* The same words as the capped-snapshot case on purpose — it is the same idea (interactiveFound is a
|
|
52
|
+
* floor) reached by a different route, and a second vocabulary for it would just be more to learn.
|
|
53
|
+
*/
|
|
54
|
+
export declare function revealedControlsNote(count: number): string;
|
|
42
55
|
export interface CrawlReport {
|
|
43
56
|
interactiveFound: number;
|
|
44
57
|
stepsRun: number;
|
package/dist/crawl/crawl.js
CHANGED
|
@@ -6,6 +6,23 @@ import { ReticleTool } from '../tools/tool-names.js';
|
|
|
6
6
|
import { findContradictions } from '../events/contradictions.js';
|
|
7
7
|
/** Said out loud when the page was larger than one snapshot, so a clean sweep cannot mean "all clear". */
|
|
8
8
|
export const CAPPED_SNAPSHOT_NOTE = 'the page exceeded one snapshot, so controls past the cap were never seen — interactiveFound is a floor, not a total; re-run with a narrower `scope` to cover the rest';
|
|
9
|
+
/**
|
|
10
|
+
* Said out loud when the crawl's OWN clicks put controls on the page that it then never visited.
|
|
11
|
+
*
|
|
12
|
+
* The enumeration happens once, before the first click, so a control that only exists after a login,
|
|
13
|
+
* a modal or a tab switch is invisible to this run. Measured on the bench app: a crawl of the login
|
|
14
|
+
* screen visited three controls, its click on "Sign in" produced seven more, and it returned
|
|
15
|
+
* `truncated: false` with an empty anomaly list and nine of twelve steps unused. Read as a sweep,
|
|
16
|
+
* that is a clean bill of health for an app the crawl never entered.
|
|
17
|
+
*
|
|
18
|
+
* The same words as the capped-snapshot case on purpose — it is the same idea (interactiveFound is a
|
|
19
|
+
* floor) reached by a different route, and a second vocabulary for it would just be more to learn.
|
|
20
|
+
*/
|
|
21
|
+
export function revealedControlsNote(count) {
|
|
22
|
+
return (`the page changed as you clicked: ${String(count)} control(s) that did not exist when this crawl ` +
|
|
23
|
+
'started were never visited — interactiveFound is a floor, not a total; re-run the crawl from ' +
|
|
24
|
+
'the state you are now in to cover them');
|
|
25
|
+
}
|
|
9
26
|
/** Any buffer event that proves the app reacted to a click (vs a dead/no-op control). */
|
|
10
27
|
function isActivity(e) {
|
|
11
28
|
return (e.type === EventType.NET_REQUEST ||
|
|
@@ -50,6 +67,25 @@ export function legitimatelyInert(desc) {
|
|
|
50
67
|
return true;
|
|
51
68
|
return /\b(textbox|searchbox|combobox|spinbutton)\b/.test(desc);
|
|
52
69
|
}
|
|
70
|
+
/**
|
|
71
|
+
* How many interactive controls exist now that did not exist when the crawl enumerated.
|
|
72
|
+
*
|
|
73
|
+
* Keyed on ref, which is the element's identity: a control that re-rendered keeps its ref and is not
|
|
74
|
+
* new, and one that vanished simply does not appear. Returns 0 rather than throwing if the snapshot
|
|
75
|
+
* fails — this is a disclosure, and failing the whole crawl to report on coverage would be worse
|
|
76
|
+
* than the silence it is fixing.
|
|
77
|
+
*/
|
|
78
|
+
async function countRevealed(session, opts, before) {
|
|
79
|
+
const snap = await session.command(ReticleCommand.SNAPSHOT, {
|
|
80
|
+
mode: 'interactive',
|
|
81
|
+
...(opts.scope !== undefined ? { scope: opts.scope } : {}),
|
|
82
|
+
});
|
|
83
|
+
if (!snap.ok)
|
|
84
|
+
return 0;
|
|
85
|
+
const tree = (snap.result ?? {}).tree ?? '';
|
|
86
|
+
const known = new Set(before.map((i) => (i.ref !== '' ? i.ref : i.desc)));
|
|
87
|
+
return parseInteractive(tree).filter((i) => !known.has(i.ref !== '' ? i.ref : i.desc)).length;
|
|
88
|
+
}
|
|
53
89
|
export async function crawl(session, opts, sleep) {
|
|
54
90
|
const maxSteps = opts.maxSteps ?? CRAWL_DEFAULTS.MAX_STEPS;
|
|
55
91
|
const settleMs = opts.settleMs ?? CRAWL_DEFAULTS.SETTLE_MS;
|
|
@@ -168,6 +204,14 @@ export async function crawl(session, opts, sleep) {
|
|
|
168
204
|
});
|
|
169
205
|
}
|
|
170
206
|
}
|
|
207
|
+
// One more look, AFTER the clicks. The enumeration above happened before any of them, so anything
|
|
208
|
+
// the crawl itself unlocked — a dashboard behind a login, a modal's contents, a tab's panel — was
|
|
209
|
+
// never in `items` and never had a chance to be visited. Counted by ref against what was
|
|
210
|
+
// enumerated, so a control that merely moved or re-rendered under the same ref is not miscounted
|
|
211
|
+
// as new, and a surface that SHRANK (a modal closed) reports nothing, because nothing went
|
|
212
|
+
// unvisited. Skipped when the snapshot was already capped: that case has its own note and a second
|
|
213
|
+
// one would only muddy it.
|
|
214
|
+
const revealed = coverageCapped ? 0 : await countRevealed(session, opts, items);
|
|
171
215
|
return {
|
|
172
216
|
interactiveFound: items.length,
|
|
173
217
|
stepsRun,
|
|
@@ -177,8 +221,12 @@ export async function crawl(session, opts, sleep) {
|
|
|
177
221
|
// True when coverage was bounded for EITHER reason: the step budget ran out, or the page was
|
|
178
222
|
// bigger than one snapshot. Conflating "I stopped early" with "I saw everything" is what turns a
|
|
179
223
|
// partial sweep into "all controls healthy".
|
|
180
|
-
truncated: items.length > stepsRun || coverageCapped,
|
|
181
|
-
...(coverageCapped
|
|
224
|
+
truncated: items.length > stepsRun || coverageCapped || revealed > 0,
|
|
225
|
+
...(coverageCapped
|
|
226
|
+
? { coverageNote: CAPPED_SNAPSHOT_NOTE }
|
|
227
|
+
: revealed > 0
|
|
228
|
+
? { coverageNote: revealedControlsNote(revealed) }
|
|
229
|
+
: {}),
|
|
182
230
|
// A bare `stepsRun: 0` made "the page had no controls" and "it had 34 and none were clicked" the
|
|
183
231
|
// same answer. See crawl-empty.
|
|
184
232
|
...(() => {
|
|
@@ -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
|
*
|
|
@@ -35,6 +36,20 @@ export interface ContradictionOptions {
|
|
|
35
36
|
* contain. See `findUnitMismatches` for the measured case this exists for.
|
|
36
37
|
*/
|
|
37
38
|
prior?: readonly ReticleEvent[] | undefined;
|
|
39
|
+
/**
|
|
40
|
+
* Event time at which the action that opened this window was dispatched — the attribution floor.
|
|
41
|
+
*
|
|
42
|
+
* `duplicate-request` claims "one user action was performed", and that claim is only sound over ONE
|
|
43
|
+
* action's window. `reticle_observe` takes a caller-supplied window that can be arbitrarily wide, so
|
|
44
|
+
* two legitimate separate saves to the same endpoint read as a double submit — and the finding then
|
|
45
|
+
* sat on every verdict for that tab. Undefined means nothing attributed the window to an action, and
|
|
46
|
+
* the rule stays silent rather than accusing an app of something nobody can show it did.
|
|
47
|
+
*
|
|
48
|
+
* ponytail: a NET_REQUEST is stamped when it COMPLETED, so a write dispatched before the action and
|
|
49
|
+
* landing after it counts as inside. Keying on the matching NET_PENDING would fix that; it has not
|
|
50
|
+
* been worth the second index.
|
|
51
|
+
*/
|
|
52
|
+
actionSince?: number | undefined;
|
|
38
53
|
/**
|
|
39
54
|
* DOM mutations observed INSIDE the target's own subtree, as the act tool measured them.
|
|
40
55
|
*
|
|
@@ -49,5 +64,36 @@ export interface ContradictionOptions {
|
|
|
49
64
|
* test, which is weaker but never wrong in the direction of a false accusation.
|
|
50
65
|
*/
|
|
51
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;
|
|
52
98
|
}
|
|
53
99
|
export declare function findContradictions(allEvents: readonly ReticleEvent[], options?: ContradictionOptions): Contradiction[];
|