@reticlehq/server 2.8.0 → 2.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/SKILL.md +130 -813
- package/dist/bridge/bridge.d.ts +23 -0
- package/dist/bridge/bridge.js +80 -2
- package/dist/bridge/bridge.test-harness.d.ts +5 -0
- package/dist/bridge/bridge.test-harness.js +6 -4
- package/dist/capsule/capsule-store.d.ts +2 -2
- package/dist/capsule/causal-summary.d.ts +16 -1
- package/dist/capsule/causal-summary.js +2 -1
- package/dist/cli/cli-doctor.js +41 -13
- package/dist/cli/cli-flow-commands.js +8 -8
- package/dist/cli/cli-launch.d.ts +0 -17
- package/dist/cli/cli-launch.js +0 -28
- package/dist/cli/cli-parse.d.ts +1 -1
- package/dist/cli/cli-parse.js +1 -1
- package/dist/cli/cli-port.d.ts +24 -0
- package/dist/cli/cli-port.js +66 -35
- package/dist/cli/cli-verify.d.ts +21 -0
- package/dist/cli/cli-verify.js +45 -1
- package/dist/cli/config-discovery.d.ts +35 -0
- package/dist/cli/config-discovery.js +163 -0
- package/dist/cli/doctor-daemon-line.js +4 -1
- package/dist/cli/doctor-project-line.d.ts +37 -0
- package/dist/cli/doctor-project-line.js +35 -0
- package/dist/cli/doctor-rows.d.ts +51 -0
- package/dist/cli/doctor-rows.js +54 -0
- package/dist/cli/doctor-sessions-line.js +6 -3
- package/dist/cli/drive-attach.d.ts +99 -0
- package/dist/cli/drive-attach.js +203 -0
- package/dist/cli/drive-command.d.ts +13 -0
- package/dist/cli/drive-command.js +103 -0
- package/dist/cli/open-note.d.ts +20 -0
- package/dist/cli/open-note.js +37 -0
- package/dist/cli/status-next-action.d.ts +37 -0
- package/dist/cli/status-next-action.js +61 -0
- package/dist/cli.js +65 -70
- package/dist/crawl/crawl.d.ts +7 -2
- package/dist/crawl/crawl.js +52 -9
- package/dist/ee/audit-log.js +2 -2
- package/dist/events/body-failures.d.ts +2 -2
- package/dist/events/contradiction-folds.d.ts +45 -0
- package/dist/events/contradiction-folds.js +42 -0
- package/dist/events/contradictions.d.ts +51 -1
- package/dist/events/contradictions.js +26 -7
- package/dist/events/declared.d.ts +45 -0
- package/dist/events/declared.js +73 -0
- package/dist/events/echo-mismatch.d.ts +9 -3
- package/dist/events/echo-mismatch.js +33 -5
- package/dist/events/event-filters.d.ts +1 -1
- package/dist/events/event-filters.js +7 -4
- package/dist/events/predicate-eval.d.ts +17 -91
- package/dist/events/predicate-eval.js +85 -326
- package/dist/events/predicate-parse.js +31 -3
- package/dist/events/predicate-route.d.ts +32 -0
- package/dist/events/predicate-route.js +102 -0
- package/dist/events/predicate-schema.d.ts +99 -0
- package/dist/events/predicate-schema.js +309 -0
- package/dist/events/predicate.d.ts +8 -0
- package/dist/events/predicate.js +127 -5
- package/dist/events/stale-response.d.ts +2 -3
- package/dist/events/stale-response.js +49 -2
- package/dist/events/unit-mismatch.d.ts +2 -2
- package/dist/flows/annotate-tools.js +3 -0
- package/dist/flows/annotate.js +18 -4
- package/dist/flows/coverage.d.ts +38 -5
- package/dist/flows/coverage.js +30 -4
- package/dist/flows/decision.d.ts +15 -0
- package/dist/flows/decision.js +1 -1
- package/dist/flows/flow-replay-run.js +8 -1
- package/dist/flows/flow-tools.js +9 -1
- package/dist/flows/replay.js +3 -0
- package/dist/flows/verify-change-tools.js +1 -1
- package/dist/honesty/accepted-write.d.ts +3 -0
- package/dist/honesty/accepted-write.js +8 -3
- package/dist/honesty/already-true.js +15 -0
- package/dist/honesty/blind-spots.js +4 -0
- package/dist/honesty/honesty.d.ts +11 -0
- package/dist/honesty/honesty.js +1 -0
- package/dist/honesty/uncaptured-bodies.d.ts +4 -2
- package/dist/honesty/uncaptured-bodies.js +9 -2
- package/dist/honesty/unread-outcome.d.ts +8 -0
- package/dist/honesty/unread-outcome.js +36 -9
- package/dist/honesty/unsettled.d.ts +9 -0
- package/dist/honesty/unsettled.js +7 -3
- package/dist/honesty/verified.d.ts +29 -3
- package/dist/honesty/verified.js +67 -8
- package/dist/http-server.d.ts +19 -0
- package/dist/http-server.js +202 -4
- package/dist/impact/impact-recorder.d.ts +23 -0
- package/dist/impact/impact-recorder.js +76 -0
- package/dist/impact/impact-store.d.ts +47 -0
- package/dist/impact/impact-store.js +171 -0
- package/dist/index.d.ts +25 -0
- package/dist/index.js +60 -10
- package/dist/init/agent-rules.d.ts +20 -0
- package/dist/init/agent-rules.js +36 -0
- package/dist/init/astro-patch.js +27 -1
- package/dist/init/confirm.d.ts +21 -3
- package/dist/init/confirm.js +32 -5
- package/dist/init/connect-steps.d.ts +2 -0
- package/dist/init/connect-steps.js +50 -0
- package/dist/init/csp-check.d.ts +38 -0
- package/dist/init/csp-check.js +92 -0
- package/dist/init/csp-doctor.d.ts +25 -0
- package/dist/init/csp-doctor.js +49 -0
- package/dist/init/cursor.js +6 -9
- package/dist/init/detect.d.ts +8 -0
- package/dist/init/detect.js +33 -3
- package/dist/init/mcp-clients.d.ts +1 -0
- package/dist/init/mcp-clients.js +51 -7
- package/dist/init/mcp.d.ts +16 -2
- package/dist/init/mcp.js +23 -1
- package/dist/init/plan-framework.d.ts +10 -0
- package/dist/init/plan-framework.js +25 -1
- package/dist/init/plan.d.ts +7 -2
- package/dist/init/plan.js +66 -30
- package/dist/init/run.js +11 -5
- package/dist/init/slash-command.d.ts +1 -1
- package/dist/init/slash-command.js +12 -4
- package/dist/init/snippets.d.ts +1 -1
- package/dist/init/snippets.js +35 -7
- package/dist/init/vite-config.js +24 -2
- package/dist/input/network-mock-tools.js +8 -1
- package/dist/input/scroll-find.d.ts +6 -0
- package/dist/input/scroll-find.js +17 -2
- package/dist/input/scroll-tools.js +4 -0
- package/dist/journal/attach-journal.d.ts +9 -1
- package/dist/journal/attach-journal.js +13 -2
- package/dist/journal/journal-query.d.ts +13 -0
- package/dist/journal/journal-query.js +28 -3
- package/dist/journal/workspace-gitignore.d.ts +10 -0
- package/dist/journal/workspace-gitignore.js +59 -0
- package/dist/license/license.d.ts +69 -4
- package/dist/license/license.js +84 -15
- package/dist/mcp/mcp-proxy.d.ts +7 -93
- package/dist/mcp/mcp-proxy.js +183 -250
- package/dist/mcp/mcp.d.ts +27 -2
- package/dist/mcp/mcp.js +77 -36
- package/dist/mcp/proxy-daemon-probe.d.ts +8 -0
- package/dist/mcp/proxy-daemon-probe.js +48 -0
- package/dist/mcp/proxy-handshake.d.ts +82 -1
- package/dist/mcp/proxy-handshake.js +110 -3
- package/dist/mcp/proxy-lifecycle.d.ts +10 -1
- package/dist/mcp/proxy-lifecycle.js +11 -2
- package/dist/mcp/proxy-log.d.ts +63 -0
- package/dist/mcp/proxy-log.js +132 -0
- package/dist/mcp/server-instructions.d.ts +17 -0
- package/dist/mcp/server-instructions.js +56 -0
- package/dist/mcp/shared-params.d.ts +27 -0
- package/dist/mcp/shared-params.js +36 -0
- package/dist/mcp/sse-frame-parser.d.ts +19 -0
- package/dist/mcp/sse-frame-parser.js +41 -0
- package/dist/project/reticle-dir.d.ts +2 -0
- package/dist/project/reticle-dir.js +1 -0
- package/dist/session/connection-memory.d.ts +59 -0
- package/dist/session/connection-memory.js +108 -0
- package/dist/session/dev-command.d.ts +26 -0
- package/dist/session/dev-command.js +89 -0
- package/dist/session/dev-server-probe.d.ts +68 -0
- package/dist/session/dev-server-probe.js +102 -7
- package/dist/session/last-act.d.ts +26 -0
- package/dist/session/last-act.js +35 -1
- package/dist/session/live-control-tools.js +42 -5
- package/dist/session/no-session-diagnosis.d.ts +55 -2
- package/dist/session/no-session-diagnosis.js +199 -22
- package/dist/session/no-session-next-action.d.ts +46 -0
- package/dist/session/no-session-next-action.js +85 -0
- package/dist/session/no-session-watch.d.ts +40 -1
- package/dist/session/no-session-watch.js +172 -11
- package/dist/session/pending-commands.d.ts +12 -0
- package/dist/session/pending-commands.js +13 -1
- package/dist/session/session-manager.d.ts +4 -0
- package/dist/session/session-manager.js +35 -5
- package/dist/session/session-readiness.js +1 -1
- package/dist/session/session.d.ts +10 -1
- package/dist/session/session.js +146 -5
- package/dist/telemetry/app-instrumented.d.ts +8 -0
- package/dist/telemetry/app-instrumented.js +10 -0
- package/dist/telemetry/bug-found.d.ts +18 -0
- package/dist/telemetry/bug-found.js +14 -0
- package/dist/telemetry/daemon-telemetry.js +29 -0
- package/dist/telemetry/install-source.d.ts +20 -0
- package/dist/telemetry/install-source.js +34 -0
- package/dist/telemetry/instrumentation-stall.d.ts +57 -0
- package/dist/telemetry/instrumentation-stall.js +103 -0
- package/dist/telemetry/license-activation.d.ts +48 -0
- package/dist/telemetry/license-activation.js +57 -0
- package/dist/telemetry/session-metrics.d.ts +8 -0
- package/dist/telemetry/session-metrics.js +10 -0
- package/dist/telemetry/telemetry.d.ts +3 -1
- package/dist/telemetry/telemetry.js +23 -3
- package/dist/tools/act-tools.js +166 -29
- package/dist/tools/assert-verdict.js +32 -7
- package/dist/tools/browser-tools.js +10 -0
- package/dist/tools/dynamic-tools.js +17 -8
- package/dist/tools/error-recovery.js +10 -6
- package/dist/tools/feedback-tools.js +31 -6
- package/dist/tools/invoke-tool.d.ts +1 -1
- package/dist/tools/invoke-tool.js +30 -8
- package/dist/tools/lease-hint.d.ts +30 -8
- package/dist/tools/lease-hint.js +65 -14
- package/dist/tools/lease-tools.d.ts +11 -0
- package/dist/tools/lease-tools.js +141 -96
- package/dist/tools/observe-tools.js +9 -3
- package/dist/tools/react-grace.d.ts +4 -0
- package/dist/tools/react-grace.js +6 -3
- package/dist/tools/read-tools.js +47 -1
- package/dist/tools/resolve-target.d.ts +33 -0
- package/dist/tools/resolve-target.js +53 -0
- package/dist/tools/settle-in-flight.d.ts +15 -0
- package/dist/tools/settle-in-flight.js +42 -2
- package/dist/tools/snapshot-delta.d.ts +3 -0
- package/dist/tools/snapshot-delta.js +67 -6
- package/dist/tools/tool-kit.d.ts +38 -3
- package/dist/tools/tool-names.d.ts +2 -0
- package/dist/tools/tool-names.js +2 -0
- package/dist/tools/tool-surface.d.ts +60 -0
- package/dist/tools/tool-surface.js +84 -19
- package/dist/tools/tools.js +61 -5
- package/dist/tools/unadvertised-help.js +9 -6
- package/docs/README.md +1 -2
- package/docs/actions.mdx +41 -8
- package/docs/agent-cheatsheet.md +6 -4
- package/docs/architecture.md +6 -0
- package/docs/benchmarks.md +60 -12
- package/docs/best-practices.mdx +11 -6
- package/docs/capabilities.mdx +204 -57
- package/docs/cli/affected.mdx +6 -4
- package/docs/cli/capsules.mdx +9 -8
- package/docs/cli/cloud.mdx +30 -4
- package/docs/cli/doctor.mdx +41 -8
- package/docs/cli/drive.mdx +34 -15
- package/docs/cli/gate.mdx +6 -6
- package/docs/cli/help.mdx +8 -6
- package/docs/cli/hunt.mdx +20 -4
- package/docs/cli/identify.mdx +16 -2
- package/docs/cli/init.mdx +35 -8
- package/docs/cli/kill.mdx +5 -5
- package/docs/cli/license.mdx +1 -1
- package/docs/cli/open.mdx +6 -0
- package/docs/cli/restart.mdx +6 -6
- package/docs/cli/serve.mdx +10 -3
- package/docs/cli/status.mdx +29 -5
- package/docs/cli/stop.mdx +8 -0
- package/docs/cli/verify.mdx +20 -1
- package/docs/cli/version.mdx +1 -1
- package/docs/cli/watch.mdx +9 -2
- package/docs/cli.mdx +44 -22
- package/docs/debugging.md +3 -1
- package/docs/deploy-checks.md +31 -2
- package/docs/desktop.mdx +276 -80
- package/docs/docs.json +13 -12
- package/docs/enterprise.md +9 -3
- package/docs/faq.mdx +49 -35
- package/docs/fixtures.md +2 -0
- package/docs/flows.md +7 -5
- package/docs/for-agents.md +3 -3
- package/docs/frameworks.mdx +46 -7
- package/docs/gate-plan.md +4 -2
- package/docs/gates.md +8 -6
- package/docs/getting-started.md +53 -12
- package/docs/human-control.md +2 -0
- package/docs/index.mdx +43 -1
- package/docs/install-agentic.mdx +13 -4
- package/docs/install-manual.mdx +6 -4
- package/docs/instrumentation.mdx +7 -1
- package/docs/integration-patterns.md +3 -1
- package/docs/local-registry.md +4 -2
- package/docs/multi-agent-testing.md +3 -1
- package/docs/packages/babel-plugin.mdx +15 -6
- package/docs/packages/browser.mdx +2 -2
- package/docs/packages/core.mdx +26 -16
- package/docs/packages/electron.mdx +5 -7
- package/docs/packages/eslint-plugin.mdx +5 -3
- package/docs/packages/next.mdx +3 -5
- package/docs/packages/react.mdx +3 -3
- package/docs/packages/server.mdx +2 -2
- package/docs/packages/tauri.mdx +6 -6
- package/docs/packages/test.mdx +18 -7
- package/docs/packages/vite-plugin.mdx +4 -6
- package/docs/packages.mdx +14 -13
- package/docs/platform-integration.md +2 -0
- package/docs/predicates.mdx +76 -7
- package/docs/quickstart.mdx +51 -5
- package/docs/recipes.mdx +63 -23
- package/docs/skill-file.mdx +3 -1
- package/docs/state-management.mdx +2 -0
- package/docs/system-map.md +3 -1
- package/docs/telemetry-contract.md +33 -5
- package/docs/telemetry-events.mdx +63 -18
- package/docs/telemetry.md +8 -5
- package/docs/testing.md +35 -1
- package/docs/token-efficiency.md +8 -2
- package/docs/tools-act-and-wait.mdx +64 -14
- package/docs/tools-act-sequence.mdx +34 -17
- package/docs/tools-act.mdx +24 -11
- package/docs/tools-assert.mdx +69 -10
- package/docs/tools-console.mdx +11 -7
- package/docs/tools-inspect.mdx +11 -8
- package/docs/tools-navigate.mdx +41 -5
- package/docs/tools-network.mdx +42 -9
- package/docs/tools-observe.mdx +66 -10
- package/docs/tools-overview.mdx +26 -23
- package/docs/tools-query.mdx +52 -8
- package/docs/tools-session-and-feedback.mdx +35 -9
- package/docs/tools-sessions.mdx +39 -10
- package/docs/tools-snapshot.mdx +57 -3
- package/docs/tools-state.mdx +24 -1
- package/docs/tools-tools-and-run.mdx +55 -24
- package/docs/tools-wait-for.mdx +34 -15
- package/docs/troubleshooting.mdx +80 -10
- package/docs/usage.md +46 -24
- package/docs/vs-chrome-devtools-mcp.mdx +41 -0
- package/docs/vs-playwright-mcp.mdx +42 -2
- package/docs/vs-screenshots.mdx +42 -1
- package/docs/what-is-reticle.mdx +1 -1
- package/docs/why-reticle.mdx +3 -1
- package/package.json +5 -5
- package/docs/desktop-apps.md +0 -265
package/dist/bridge/bridge.d.ts
CHANGED
|
@@ -15,7 +15,30 @@ type SessionReadyHandler = (session: Session) => void;
|
|
|
15
15
|
export declare const WS_CLOSE_REASON: {
|
|
16
16
|
readonly PROTOCOL_MISMATCH: "protocol version mismatch — upgrade @reticlehq/browser";
|
|
17
17
|
readonly AUTH_FAILED: "authentication failed — reload the page to pick up the current pairing token";
|
|
18
|
+
/**
|
|
19
|
+
* The pool of half-open handshakes was full, so this dial was turned away before it could say
|
|
20
|
+
* anything. Every other refusal here records why and this one did not — it closed the socket and
|
|
21
|
+
* returned — so an app that was running, instrumented and actively dialling looked exactly like an
|
|
22
|
+
* app nobody had started, and the diagnosis went hunting a stopped dev server.
|
|
23
|
+
*
|
|
24
|
+
* Names the cause and the fact that it is transient, because the remedy differs from every other
|
|
25
|
+
* refusal on this path: nothing about the app is wrong and retrying is the correct response.
|
|
26
|
+
*/
|
|
27
|
+
readonly HANDSHAKE_POOL_FULL: string;
|
|
18
28
|
};
|
|
29
|
+
/**
|
|
30
|
+
* A dial turned away at the ORIGIN gate, in words the reader can act on.
|
|
31
|
+
*
|
|
32
|
+
* This is the refusal that used to leave no trace anywhere. It happens before any HELLO, so it
|
|
33
|
+
* produced no `noteClosure`, no `session_disconnected`, nothing but an `origin_rejected` log line —
|
|
34
|
+
* and the daemon log is the file nobody opens until they have already lost the afternoon. Reported
|
|
35
|
+
* from a multi-tenant app resolving its tenant from the `Host` header: on a hosts-file alias every
|
|
36
|
+
* dial was refused here, `doctor` reported every check green, and the lease hint blamed the port.
|
|
37
|
+
*
|
|
38
|
+
* The origin is IN the message because it is the fix: it is either the alias to allow-list, or the
|
|
39
|
+
* proof that the page is not being served from where the developer thinks it is.
|
|
40
|
+
*/
|
|
41
|
+
export declare function originRejectedReason(origin: string | undefined): string;
|
|
19
42
|
interface BridgeOptions {
|
|
20
43
|
port: number;
|
|
21
44
|
host?: string;
|
package/dist/bridge/bridge.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import * as http from 'node:http';
|
|
2
2
|
import { authFailureReason } from './auth-failure-reason.js';
|
|
3
|
+
import { impactSnapshot, recordImpact } from '../impact/impact-recorder.js';
|
|
3
4
|
import { WebSocketServer } from 'ws';
|
|
4
5
|
import { EventType, HumanControlKind, RETICLE_WS_PATH, ReticleMessageSchema, LOOPBACK_HOST, MessageKind, ReticleEnv, RETICLE_PROTOCOL_VERSION, TRANSPORT_LIMITS, isLocalPage, isLoopbackHostname, isOpaqueOrigin, OPAQUE_ORIGIN, isHighValueEvent, RATE_CAP_HIGH_VALUE_RESERVE_RATIO, CONTRACT_FINGERPRINT, } from '@reticlehq/core';
|
|
5
6
|
import { Session, SessionManager } from '../session/session.js';
|
|
@@ -38,7 +39,39 @@ function refreshIdentity(session, hello) {
|
|
|
38
39
|
export const WS_CLOSE_REASON = {
|
|
39
40
|
PROTOCOL_MISMATCH: 'protocol version mismatch — upgrade @reticlehq/browser',
|
|
40
41
|
AUTH_FAILED: 'authentication failed — reload the page to pick up the current pairing token',
|
|
42
|
+
/**
|
|
43
|
+
* The pool of half-open handshakes was full, so this dial was turned away before it could say
|
|
44
|
+
* anything. Every other refusal here records why and this one did not — it closed the socket and
|
|
45
|
+
* returned — so an app that was running, instrumented and actively dialling looked exactly like an
|
|
46
|
+
* app nobody had started, and the diagnosis went hunting a stopped dev server.
|
|
47
|
+
*
|
|
48
|
+
* Names the cause and the fact that it is transient, because the remedy differs from every other
|
|
49
|
+
* refusal on this path: nothing about the app is wrong and retrying is the correct response.
|
|
50
|
+
*/
|
|
51
|
+
HANDSHAKE_POOL_FULL: 'refused at the handshake pool — too many connections were mid-handshake on this daemon, so ' +
|
|
52
|
+
'this dial never got to identify itself. Nothing is wrong with the app; the pool drains on its ' +
|
|
53
|
+
'own and reloading the page reconnects. If it persists, something else on this machine is ' +
|
|
54
|
+
'opening sockets to this port.',
|
|
41
55
|
};
|
|
56
|
+
/**
|
|
57
|
+
* A dial turned away at the ORIGIN gate, in words the reader can act on.
|
|
58
|
+
*
|
|
59
|
+
* This is the refusal that used to leave no trace anywhere. It happens before any HELLO, so it
|
|
60
|
+
* produced no `noteClosure`, no `session_disconnected`, nothing but an `origin_rejected` log line —
|
|
61
|
+
* and the daemon log is the file nobody opens until they have already lost the afternoon. Reported
|
|
62
|
+
* from a multi-tenant app resolving its tenant from the `Host` header: on a hosts-file alias every
|
|
63
|
+
* dial was refused here, `doctor` reported every check green, and the lease hint blamed the port.
|
|
64
|
+
*
|
|
65
|
+
* The origin is IN the message because it is the fix: it is either the alias to allow-list, or the
|
|
66
|
+
* proof that the page is not being served from where the developer thinks it is.
|
|
67
|
+
*/
|
|
68
|
+
export function originRejectedReason(origin) {
|
|
69
|
+
const named = origin === undefined || 0 === origin.length ? 'a page sending no Origin' : origin;
|
|
70
|
+
return (`a browser dialled this daemon and was REFUSED at the origin gate: ${named} is not allowed. ` +
|
|
71
|
+
'Off localhost the SDK needs BOTH `allowNonLocalhost: true` AND a pairing token, and the ' +
|
|
72
|
+
'daemon needs that origin allow-listed (RETICLE_ALLOWED_ORIGINS). The app is running and ' +
|
|
73
|
+
'instrumented — it was turned away, so do not go looking for a stopped dev server.');
|
|
74
|
+
}
|
|
42
75
|
/** WS close codes + reasons the bridge sends to the SDK (1008 = policy violation, 1013 = try again later). */
|
|
43
76
|
const WS_CLOSE = {
|
|
44
77
|
TOO_MANY_HANDSHAKES: [1013, 'too many pending handshakes'],
|
|
@@ -135,6 +168,8 @@ export class Bridge {
|
|
|
135
168
|
* authFailureReason.
|
|
136
169
|
*/
|
|
137
170
|
#servedProjects = new Set();
|
|
171
|
+
/** Session ids already counted in the impact record - a reconnect is not a new session. */
|
|
172
|
+
#countedSessions = new Set();
|
|
138
173
|
#allowedOrigins;
|
|
139
174
|
#maxMessagesPerSecond;
|
|
140
175
|
#maxSessions;
|
|
@@ -198,7 +233,7 @@ export class Bridge {
|
|
|
198
233
|
verifyClient: ({ origin }, done) => {
|
|
199
234
|
const allowed = this.#originAllowed(origin);
|
|
200
235
|
if (!allowed)
|
|
201
|
-
|
|
236
|
+
this.#noteOriginRejected(origin);
|
|
202
237
|
done(allowed, 403, 'Forbidden');
|
|
203
238
|
},
|
|
204
239
|
});
|
|
@@ -229,7 +264,7 @@ export class Bridge {
|
|
|
229
264
|
verifyClient: ({ origin }, done) => {
|
|
230
265
|
const allowed = this.#originAllowed(origin);
|
|
231
266
|
if (!allowed)
|
|
232
|
-
|
|
267
|
+
this.#noteOriginRejected(origin);
|
|
233
268
|
done(allowed, 403, 'Forbidden');
|
|
234
269
|
},
|
|
235
270
|
});
|
|
@@ -244,12 +279,27 @@ export class Bridge {
|
|
|
244
279
|
});
|
|
245
280
|
});
|
|
246
281
|
}
|
|
282
|
+
// A PERMANENT 'error' listener, on top of the one `ready` installs and then removes.
|
|
283
|
+
//
|
|
284
|
+
// `ws` re-emits every 'error' from the server it is attached to, and both `ready` branches above
|
|
285
|
+
// drop their listener the moment the port is bound. From then on the emitter is bare, and an
|
|
286
|
+
// EventEmitter that emits 'error' with no listener THROWS — which in the daemon is an
|
|
287
|
+
// uncaughtException, which is an exit, which takes every agent and every session on this port
|
|
288
|
+
// with it. Logging one is all it takes; the transport itself needs no other reaction.
|
|
289
|
+
this.#wss.on('error', (err) => {
|
|
290
|
+
log('bridge_ws_error', { error: err.message });
|
|
291
|
+
});
|
|
247
292
|
this.#wss.on('connection', (socket) => {
|
|
248
293
|
this.#onConnection(socket);
|
|
249
294
|
});
|
|
250
295
|
}
|
|
251
296
|
#onConnection(socket) {
|
|
252
297
|
if (this.#pendingConnections >= this.#maxPendingConnections) {
|
|
298
|
+
// On the channel the no-session diagnosis reads, not only into the log. This refusal happens
|
|
299
|
+
// before any HELLO, so without it the dial leaves no trace at all — the same hole the origin
|
|
300
|
+
// gate had, and the same fix.
|
|
301
|
+
this.sessions.noteClosure(WS_CLOSE_REASON.HANDSHAKE_POOL_FULL, this.#clock());
|
|
302
|
+
log('bridge_handshake_pool_full', { pending: this.#pendingConnections });
|
|
253
303
|
socket.close(...WS_CLOSE.TOO_MANY_HANDSHAKES);
|
|
254
304
|
return;
|
|
255
305
|
}
|
|
@@ -362,6 +412,16 @@ export class Bridge {
|
|
|
362
412
|
if (parsed.projectId !== undefined)
|
|
363
413
|
this.#servedProjects.add(parsed.projectId);
|
|
364
414
|
this.#onSessionCreate?.(session); // attach the durable journal before any events stream in
|
|
415
|
+
// A tab that has just connected has no impact record yet, so the report would read "nothing
|
|
416
|
+
// recorded" over a file with a month of history in it. Push what is already on disk as soon
|
|
417
|
+
// as there is somewhere to push it to; a session is also a thing that HAPPENED, so it counts.
|
|
418
|
+
// A reconnecting tab keeps its id, so counting every connect made a page reload look like
|
|
419
|
+
// a fresh session - the number climbed while nothing new happened.
|
|
420
|
+
if (!this.#countedSessions.has(session.id)) {
|
|
421
|
+
this.#countedSessions.add(session.id);
|
|
422
|
+
recordImpact({ sessions: 1 });
|
|
423
|
+
}
|
|
424
|
+
session.pushImpact(impactSnapshot, true);
|
|
365
425
|
const replaced = this.sessions.add(session);
|
|
366
426
|
if (replaced !== undefined) {
|
|
367
427
|
// Name the newcomer. A field report had a live session vanish during `reticle_lease` and the
|
|
@@ -374,6 +434,10 @@ export class Bridge {
|
|
|
374
434
|
byUrl: session.url,
|
|
375
435
|
previousUrl: replaced.url,
|
|
376
436
|
});
|
|
437
|
+
// Hand the displaced session its replacement BEFORE ending it, so a tool call still holding
|
|
438
|
+
// the old handle can finish against the live connection instead of returning an error whose
|
|
439
|
+
// only answer is to go and rediscover an id that has not changed. See Session.succeededBy.
|
|
440
|
+
replaced.succeededBy(session);
|
|
377
441
|
replaced.disconnect(`session replaced by a newer connection claiming the same id (${session.id}) from ${session.url}`);
|
|
378
442
|
}
|
|
379
443
|
// The daemon is the single judge of skew, and HELLO is where the page announces itself.
|
|
@@ -448,6 +512,20 @@ export class Bridge {
|
|
|
448
512
|
log('socket_error', { error: err.message });
|
|
449
513
|
});
|
|
450
514
|
}
|
|
515
|
+
/**
|
|
516
|
+
* Record a refused dial where something OTHER than the daemon log can read it.
|
|
517
|
+
*
|
|
518
|
+
* The log line stays — it is the forensic record — but a line in `~/.reticle/daemon-<port>.log` is
|
|
519
|
+
* invisible to the agent that is, at that exact moment, being told "no browser session connected"
|
|
520
|
+
* with no reason attached. `noteClosure` is the channel the no-session diagnosis already reads, so
|
|
521
|
+
* the refusal now travels the same route as an auth failure and a protocol mismatch, and the loop
|
|
522
|
+
* (the reason is only in the page console; the console needs a session; the refusal prevents one)
|
|
523
|
+
* is broken from the daemon side.
|
|
524
|
+
*/
|
|
525
|
+
#noteOriginRejected(origin) {
|
|
526
|
+
log('origin_rejected', { origin: origin ?? 'missing' });
|
|
527
|
+
this.sessions.noteClosure(originRejectedReason(origin), this.#clock());
|
|
528
|
+
}
|
|
451
529
|
#originAllowed(origin) {
|
|
452
530
|
// A real browser SDK connection always carries an Origin. A missing Origin means a non-browser
|
|
453
531
|
// local process (another app, a malicious npm postinstall, an extension's native host) — untrusted
|
|
@@ -25,6 +25,11 @@ export declare class FakeBrowser {
|
|
|
25
25
|
name: string;
|
|
26
26
|
args: Record<string, unknown>;
|
|
27
27
|
}[];
|
|
28
|
+
/** The snapshot tree to return. Mutable so tests can vary the page between calls. */
|
|
29
|
+
snapshotResult: {
|
|
30
|
+
tree: string;
|
|
31
|
+
status: Record<string, unknown>;
|
|
32
|
+
};
|
|
28
33
|
constructor(port: number, sessionId: string, hasCapabilities?: boolean);
|
|
29
34
|
open(): Promise<void>;
|
|
30
35
|
/** Re-send the HELLO on the live socket, the way the SDK does when capabilities register late. */
|
|
@@ -32,6 +32,11 @@ export class FakeBrowser {
|
|
|
32
32
|
queryResolves = true;
|
|
33
33
|
/** Records every command the bridge sent (for replay assertions). */
|
|
34
34
|
received = [];
|
|
35
|
+
/** The snapshot tree to return. Mutable so tests can vary the page between calls. */
|
|
36
|
+
snapshotResult = {
|
|
37
|
+
tree: '- button "Pay" (ref=e7)\n- dialog "Order confirmed" (ref=e12)',
|
|
38
|
+
status: { route: '/checkout' },
|
|
39
|
+
};
|
|
35
40
|
constructor(port, sessionId, hasCapabilities = false) {
|
|
36
41
|
this.sessionId = sessionId;
|
|
37
42
|
this.hasCapabilities = hasCapabilities;
|
|
@@ -147,10 +152,7 @@ export class FakeBrowser {
|
|
|
147
152
|
};
|
|
148
153
|
}
|
|
149
154
|
else if (name === ReticleCommand.SNAPSHOT) {
|
|
150
|
-
result =
|
|
151
|
-
tree: '- button "Pay" (ref=e7)\n- dialog "Order confirmed" (ref=e12)',
|
|
152
|
-
status: { route: '/checkout' },
|
|
153
|
-
};
|
|
155
|
+
result = this.snapshotResult;
|
|
154
156
|
}
|
|
155
157
|
else if (name === ReticleCommand.CAPABILITIES) {
|
|
156
158
|
if (!this.handlesCapabilities) {
|
|
@@ -31,19 +31,19 @@ export declare const CapsuleSchema: z.ZodObject<{
|
|
|
31
31
|
/** Minimal steps to reproduce (prefix-trimmed). */
|
|
32
32
|
steps: z.ZodArray<z.ZodType<FlowStep, z.ZodTypeDef, FlowStep>, "many">;
|
|
33
33
|
}, "strip", z.ZodTypeAny, {
|
|
34
|
+
expected: string;
|
|
34
35
|
origin: string;
|
|
35
36
|
version: 1;
|
|
36
37
|
id: string;
|
|
37
|
-
expected: string;
|
|
38
38
|
observed: string;
|
|
39
39
|
steps: FlowStep[];
|
|
40
40
|
createdAt: number;
|
|
41
41
|
flow?: string | undefined;
|
|
42
42
|
}, {
|
|
43
|
+
expected: string;
|
|
43
44
|
origin: string;
|
|
44
45
|
version: 1;
|
|
45
46
|
id: string;
|
|
46
|
-
expected: string;
|
|
47
47
|
observed: string;
|
|
48
48
|
steps: FlowStep[];
|
|
49
49
|
createdAt: number;
|
|
@@ -65,6 +65,21 @@ export interface CausalSummary {
|
|
|
65
65
|
signals: string[];
|
|
66
66
|
layoutShift?: number;
|
|
67
67
|
longTasks: number;
|
|
68
|
+
/**
|
|
69
|
+
* TRUE when no store was subscribed, so `statePathsChanged`/`stateDiffs` are empty because nothing
|
|
70
|
+
* was watching — not because the app changed nothing.
|
|
71
|
+
*
|
|
72
|
+
* The two readings are the same empty list, and the wrong one is the one an agent takes: "state
|
|
73
|
+
* did not change" is a claim about the app, and this codebase would rather say "I could not see"
|
|
74
|
+
* than say something confident and wrong. Present only when the channel is dark, so a wired app
|
|
75
|
+
* pays nothing and the field's PRESENCE is what carries the meaning — the same rule
|
|
76
|
+
* `stateSettleMs` and `elided` follow.
|
|
77
|
+
*/
|
|
78
|
+
stateUnwatched?: true;
|
|
79
|
+
}
|
|
80
|
+
/** What only the SESSION knows — level facts the event window cannot contain. See `stateUnwatched`. */
|
|
81
|
+
export interface SummaryContext {
|
|
82
|
+
stateUnwatched?: boolean;
|
|
68
83
|
}
|
|
69
84
|
/**
|
|
70
85
|
* THE INVARIANT: every field of this summary has a bounded size that does not depend on how much the
|
|
@@ -80,5 +95,5 @@ export interface CausalSummary {
|
|
|
80
95
|
export declare const MAX_SUMMARY_ENTRIES = 20;
|
|
81
96
|
/** The lists that are capped, named so `elided` can say which one lost entries. */
|
|
82
97
|
type CappedList = 'statePathsChanged' | 'storageKeysChanged' | 'stateDiffs' | 'storageDiffs' | 'signals';
|
|
83
|
-
export declare function causalSummary(events: readonly ReticleEvent[]): CausalSummary;
|
|
98
|
+
export declare function causalSummary(events: readonly ReticleEvent[], context?: SummaryContext): CausalSummary;
|
|
84
99
|
export {};
|
|
@@ -56,7 +56,7 @@ function stateSettle(diffs) {
|
|
|
56
56
|
const spread = Math.max(...times) - Math.min(...times);
|
|
57
57
|
return spread > 0 ? { stateSettleMs: spread } : {};
|
|
58
58
|
}
|
|
59
|
-
export function causalSummary(events) {
|
|
59
|
+
export function causalSummary(events, context = {}) {
|
|
60
60
|
let netTotal = 0;
|
|
61
61
|
let netErrors = 0;
|
|
62
62
|
let ignoredDevTooling = 0;
|
|
@@ -160,5 +160,6 @@ export function causalSummary(events) {
|
|
|
160
160
|
...(0 === Object.keys(elided).length ? {} : { elided }),
|
|
161
161
|
...(layoutShift === undefined ? {} : { layoutShift }),
|
|
162
162
|
longTasks,
|
|
163
|
+
...(true === context.stateUnwatched ? { stateUnwatched: true } : {}),
|
|
163
164
|
};
|
|
164
165
|
}
|
package/dist/cli/cli-doctor.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { readFileSync } from 'node:fs';
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import { ReticleEnv } from '@reticlehq/core';
|
|
4
4
|
import { readPid, reticleStateHome } from '../daemon/daemon.js';
|
|
@@ -6,13 +6,17 @@ import { PortPresence, probePresence } from '../daemon/port-presence.js';
|
|
|
6
6
|
import { probeDaemon } from '../mcp/mcp-proxy.js';
|
|
7
7
|
import { fetchStatus } from './cli-launch.js';
|
|
8
8
|
import { daemonLine } from './doctor-daemon-line.js';
|
|
9
|
+
import { projectWiringLine } from './doctor-project-line.js';
|
|
10
|
+
import { hasProjectConnectedBefore } from '../session/connection-memory.js';
|
|
9
11
|
import { sessionsLine } from './doctor-sessions-line.js';
|
|
10
12
|
import { captureLookup, describeForeignHolder, findPortHolder } from './port-holder.js';
|
|
11
13
|
import { chromiumHint, probeChromium } from './chromium-hint.js';
|
|
12
14
|
import { SERVER_VERSION } from '../version/server-version.js';
|
|
13
15
|
import { CONTRACT_FINGERPRINT } from '@reticlehq/core';
|
|
14
16
|
import { diagnoseDesktop, isDesktopProject } from '../init/desktop-doctor.js';
|
|
15
|
-
import {
|
|
17
|
+
import { diagnoseWebCsp } from '../init/csp-doctor.js';
|
|
18
|
+
import { RETICLE_CONFIG_BASENAME, diagnosePortMismatch, readProjectId, readProjectPort, } from './cli-port.js';
|
|
19
|
+
import { DoctorRow, doctorRow } from './doctor-rows.js';
|
|
16
20
|
/**
|
|
17
21
|
* `reticle doctor` — collapse the ~6 independent first-run failure modes into one command. Checks the
|
|
18
22
|
* Chromium install (the #1 silent failure), whether a daemon is up on the resolved bridge port, and
|
|
@@ -41,11 +45,11 @@ export async function handleDoctor(port) {
|
|
|
41
45
|
process.stdout.write(`${s}\n`);
|
|
42
46
|
};
|
|
43
47
|
line('reticle doctor');
|
|
44
|
-
line(
|
|
48
|
+
line(doctorRow(DoctorRow.NODE, process.version));
|
|
45
49
|
// Filled in only on the daemon branch — there is nothing to ask when no daemon is answering, and
|
|
46
50
|
// the branches below already say so in their own terms.
|
|
47
51
|
let sessions;
|
|
48
|
-
line(
|
|
52
|
+
line(doctorRow(DoctorRow.CHROMIUM, chromiumHint(await probeChromium())));
|
|
49
53
|
// Ask the PORT, not just the pid file. "not running on :4400" has been printed about a port that
|
|
50
54
|
// was demonstrably occupied, which sends the reader to start a daemon that cannot bind. The three
|
|
51
55
|
// states are genuinely different problems with different fixes, so doctor names which one it is.
|
|
@@ -63,7 +67,7 @@ export async function handleDoctor(port) {
|
|
|
63
67
|
});
|
|
64
68
|
line(built.text);
|
|
65
69
|
if (built.skew !== undefined)
|
|
66
|
-
line(
|
|
70
|
+
line(doctorRow(DoctorRow.VERSION, `✗ ${built.skew}`));
|
|
67
71
|
// The same payload already carries whether anything has CONNECTED, and doctor was reading
|
|
68
72
|
// `version` off it and dropping the rest. Without this the command is silent on the one step the
|
|
69
73
|
// funnel stalls on: wired correctly, daemon up, and no page has ever dialled in.
|
|
@@ -75,24 +79,35 @@ export async function handleDoctor(port) {
|
|
|
75
79
|
// pipeline that also kills the agent's own MCP proxy.
|
|
76
80
|
// `pid` is what our pid file recorded for this port. When it matches the process actually
|
|
77
81
|
// holding it, this is our OWN daemon wedged, not a stranger — and the fix is different.
|
|
78
|
-
line(
|
|
82
|
+
line(doctorRow(DoctorRow.DAEMON, `✗ ${describeForeignHolder(port, findPortHolder(port, captureLookup), pid)}`));
|
|
79
83
|
}
|
|
80
84
|
else {
|
|
81
|
-
line(
|
|
85
|
+
line(doctorRow(DoctorRow.DAEMON, `✗ not running on :${port} — your agent runs \`reticle mcp\` (or \`reticle serve\`)`));
|
|
82
86
|
}
|
|
83
87
|
if (sessions !== undefined)
|
|
84
88
|
line(sessions.text);
|
|
85
|
-
line(
|
|
89
|
+
line(doctorRow(DoctorRow.BRIDGE_PORT, `${port} (your app must dial THIS port, not your dev-server port)`));
|
|
86
90
|
const projectPort = readProjectPort(process.cwd());
|
|
87
91
|
const mismatch = diagnosePortMismatch(port, projectPort);
|
|
88
92
|
if (mismatch !== undefined)
|
|
89
|
-
line(
|
|
93
|
+
line(doctorRow(DoctorRow.PORT_CHECK, `✗ ${mismatch}`));
|
|
94
|
+
// The check this checklist was missing: is the APP wired, not just the tools. Everything above can
|
|
95
|
+
// be green in a project that has never been through `init`, and that combination is precisely the
|
|
96
|
+
// one `doctor` gets run to explain.
|
|
97
|
+
const projectId = readProjectId(process.cwd());
|
|
98
|
+
line(projectWiringLine({
|
|
99
|
+
projectId,
|
|
100
|
+
previouslyConnected: hasProjectConnectedBefore(reticleStateHome(), port, projectId),
|
|
101
|
+
// An absent config and a corrupt one both read back as "no id"; only this tells them apart,
|
|
102
|
+
// and telling somebody a file is missing while it sits in front of them is its own dead end.
|
|
103
|
+
configPresent: existsSync(join(process.cwd(), RETICLE_CONFIG_BASENAME)),
|
|
104
|
+
}));
|
|
90
105
|
// Where to LOOK when something is wrong. The daemon has always written a structured log here and
|
|
91
106
|
// nothing ever said so, so the first move in every investigation was reading source instead of
|
|
92
107
|
// reading the log. `RETICLE_TRACE=1` turns the same stream into a per-stage trace — see
|
|
93
108
|
// docs/debugging.md.
|
|
94
|
-
line(
|
|
95
|
-
line(
|
|
109
|
+
line(doctorRow(DoctorRow.DAEMON_LOG, join(reticleStateHome(), `daemon-${String(port)}.log`)));
|
|
110
|
+
line(doctorRow(DoctorRow.TRACING, `${ReticleEnv.TRACE}=1 on the daemon for per-stage timings in that log`));
|
|
96
111
|
// Below the checklist rather than inline: it is a paragraph, and a paragraph in the middle of a
|
|
97
112
|
// column of one-line checks buries the checks under it. This is the daemon's own no-session
|
|
98
113
|
// diagnosis, the same one an agent gets from an empty reticle_sessions.
|
|
@@ -112,10 +127,23 @@ export async function handleDoctor(port) {
|
|
|
112
127
|
return undefined;
|
|
113
128
|
}
|
|
114
129
|
};
|
|
130
|
+
// The web sibling of the desktop findings below: a `connect-src` that excludes the bridge makes
|
|
131
|
+
// the browser refuse the WebSocket and report it in ITS console only, so every check above passes
|
|
132
|
+
// at an app that can never connect. Named, with the exact text to paste.
|
|
133
|
+
const csp = diagnoseWebCsp(readProjectFile, port);
|
|
134
|
+
if (csp.length > 0) {
|
|
135
|
+
line('');
|
|
136
|
+
line(` csp ✗ a Content-Security-Policy is blocking the Reticle bridge:`);
|
|
137
|
+
for (const finding of csp) {
|
|
138
|
+
line(` ${finding.file}`);
|
|
139
|
+
line(` ${finding.problem}`);
|
|
140
|
+
line(` fix: ${finding.fix}`);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
115
143
|
const desktop = diagnoseDesktop(readProjectFile, port);
|
|
116
144
|
if (desktop.length > 0) {
|
|
117
145
|
line('');
|
|
118
|
-
line(
|
|
146
|
+
line(doctorRow(DoctorRow.DESKTOP, `✗ ${String(desktop.length)} issue(s) — the app will look fine and not work:`));
|
|
119
147
|
for (const finding of desktop) {
|
|
120
148
|
line(` ${finding.file}`);
|
|
121
149
|
line(` ${finding.problem}`);
|
|
@@ -125,6 +153,6 @@ export async function handleDoctor(port) {
|
|
|
125
153
|
else if (isDesktopProject(readProjectFile)) {
|
|
126
154
|
// Say so explicitly. Silence would read as "not checked", which is the same ambiguity the
|
|
127
155
|
// findings above exist to remove.
|
|
128
|
-
line('
|
|
156
|
+
line(doctorRow(DoctorRow.DESKTOP, '✓ desktop wiring looks right'));
|
|
129
157
|
}
|
|
130
158
|
}
|
|
@@ -19,7 +19,7 @@ import { formatBuddyStatus } from '../flows/buddy-status.js';
|
|
|
19
19
|
import { CapsuleStore } from '../capsule/capsule-store.js';
|
|
20
20
|
import { AssertionTiersStore } from '../flows/assertion-tiers-store.js';
|
|
21
21
|
import { detectDowngrades } from '../flows/assertion-integrity.js';
|
|
22
|
-
import { computeCoverage } from '../flows/coverage.js';
|
|
22
|
+
import { computeCoverage, flowCoverageReport } from '../flows/coverage.js';
|
|
23
23
|
import { createWatchBatcher } from '../flows/watch-batcher.js';
|
|
24
24
|
import { watch } from 'node:fs';
|
|
25
25
|
import { log } from '../log.js';
|
|
@@ -184,19 +184,19 @@ export async function handleGate(files, since) {
|
|
|
184
184
|
const result = gateDecision({ affected, passing, flaky, downgraded, deleted });
|
|
185
185
|
// Verified-surface coverage over flows: how much of the saved suite this run actually exercised.
|
|
186
186
|
const coverage = computeCoverage({ testids: [], signals: [], flows: allFlows.map((f) => f.name) }, { testids: [], signals: [], flows: passing });
|
|
187
|
+
// A gate over an empty suite has not passed — it had nothing to check. Same rule the suite
|
|
188
|
+
// verdict already applies to `reticle verify`; see flowCoverageReport.
|
|
189
|
+
const flowCoverage = flowCoverageReport(coverage.flows);
|
|
190
|
+
const pass = result.pass && flowCoverage.outcome === undefined;
|
|
187
191
|
log('reticle_gate', {
|
|
188
|
-
pass
|
|
192
|
+
pass,
|
|
189
193
|
uncovered: result.uncovered,
|
|
190
194
|
quarantined: result.quarantined,
|
|
191
195
|
...(result.downgraded.length > 0 ? { downgraded: result.downgraded } : {}),
|
|
192
196
|
...(result.deleted.length > 0 ? { deletedCoverage: result.deleted } : {}),
|
|
193
|
-
coverage:
|
|
194
|
-
pct: coverage.flows.pct,
|
|
195
|
-
covered: coverage.flows.covered,
|
|
196
|
-
total: coverage.flows.total,
|
|
197
|
-
},
|
|
197
|
+
coverage: flowCoverage,
|
|
198
198
|
});
|
|
199
|
-
if (!
|
|
199
|
+
if (!pass)
|
|
200
200
|
process.exitCode = 1;
|
|
201
201
|
}
|
|
202
202
|
catch (error) {
|
package/dist/cli/cli-launch.d.ts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { PortPresence } from '../daemon/port-presence.js';
|
|
2
1
|
/**
|
|
3
2
|
* CLI launch + status helpers — the daemon-introspection (`reticle status`) and the one-command
|
|
4
3
|
* "show me the app" flow (`reticle open`). Split out of cli.ts so that file stays under the size cap.
|
|
@@ -30,22 +29,6 @@ export declare function summarizeStatus(payload: unknown): {
|
|
|
30
29
|
* another agent's session on a version bump, which is worse than a loud line.
|
|
31
30
|
*/
|
|
32
31
|
export declare function warnOnDaemonSkew(port: number): Promise<void>;
|
|
33
|
-
/**
|
|
34
|
-
* Why `reticle drive` cannot have the port, or undefined when it can. Pure.
|
|
35
|
-
*
|
|
36
|
-
* `drive` bound the port unconditionally, and a daemon already listening on it is the NORMAL state
|
|
37
|
-
* once any MCP client has started one. The bind died with a raw `node:net` stack trace: the one
|
|
38
|
-
* failure in this CLI that answered a user with a Node core dump rather than a structured Reticle
|
|
39
|
-
* line naming the port. It is also the command Reticle itself recommends — `reticle_sessions`
|
|
40
|
-
* returns "run `reticle drive <url>`" whenever a tab is throttled, and a throttled tab nearly always
|
|
41
|
-
* coexists with the running daemon that makes the bind impossible.
|
|
42
|
-
*
|
|
43
|
-
* The `reticle_lease` suggestion is deliberately first: it needs no free port, it is what the
|
|
44
|
-
* daemon's own no-session diagnostic already points at, and it works while the collision stands.
|
|
45
|
-
*/
|
|
46
|
-
export declare function drivePortConflict(presence: PortPresence, port: number, holder?: {
|
|
47
|
-
ourPid: number | null;
|
|
48
|
-
}): string | undefined;
|
|
49
32
|
/** GET the daemon's /status JSON. Resolves to the parsed body, or undefined on any failure. */
|
|
50
33
|
export declare function fetchStatus(port: number): Promise<unknown>;
|
|
51
34
|
/** What `reticle open` should do: reuse an already-connected tab, open a new one, or ask for a url. */
|
package/dist/cli/cli-launch.js
CHANGED
|
@@ -6,7 +6,6 @@ import { describeSkew, DAEMON_FIX } from '../version/version-skew.js';
|
|
|
6
6
|
import { CONTRACT_FINGERPRINT } from '@reticlehq/core';
|
|
7
7
|
import { SERVER_VERSION } from '../version/server-version.js';
|
|
8
8
|
import { log } from '../log.js';
|
|
9
|
-
import { PortPresence, describePresence } from '../daemon/port-presence.js';
|
|
10
9
|
/**
|
|
11
10
|
* Reduce the daemon's /status JSON to the compact view `reticle status` prints. Pure: narrows the
|
|
12
11
|
* untrusted wire payload (never `any`) and tolerates a missing/partial body so a malformed response
|
|
@@ -65,33 +64,6 @@ export async function warnOnDaemonSkew(port) {
|
|
|
65
64
|
if (skew !== undefined)
|
|
66
65
|
log('reticle_daemon_skew', { port, warning: skew });
|
|
67
66
|
}
|
|
68
|
-
/**
|
|
69
|
-
* Why `reticle drive` cannot have the port, or undefined when it can. Pure.
|
|
70
|
-
*
|
|
71
|
-
* `drive` bound the port unconditionally, and a daemon already listening on it is the NORMAL state
|
|
72
|
-
* once any MCP client has started one. The bind died with a raw `node:net` stack trace: the one
|
|
73
|
-
* failure in this CLI that answered a user with a Node core dump rather than a structured Reticle
|
|
74
|
-
* line naming the port. It is also the command Reticle itself recommends — `reticle_sessions`
|
|
75
|
-
* returns "run `reticle drive <url>`" whenever a tab is throttled, and a throttled tab nearly always
|
|
76
|
-
* coexists with the running daemon that makes the bind impossible.
|
|
77
|
-
*
|
|
78
|
-
* The `reticle_lease` suggestion is deliberately first: it needs no free port, it is what the
|
|
79
|
-
* daemon's own no-session diagnostic already points at, and it works while the collision stands.
|
|
80
|
-
*/
|
|
81
|
-
export function drivePortConflict(presence, port, holder) {
|
|
82
|
-
if (presence !== PortPresence.DAEMON && presence !== PortPresence.FOREIGN)
|
|
83
|
-
return undefined;
|
|
84
|
-
if (PortPresence.FOREIGN === presence)
|
|
85
|
-
return describePresence(presence, port);
|
|
86
|
-
const pid = holder?.ourPid;
|
|
87
|
-
const who = pid !== null && pid !== undefined
|
|
88
|
-
? `a Reticle daemon (pid ${String(pid)}) is already serving :${String(port)}`
|
|
89
|
-
: `a Reticle daemon is already serving :${String(port)}`;
|
|
90
|
-
return (`${who}, so \`drive\` cannot bind that port. Use the daemon that is already there — its ` +
|
|
91
|
-
'`reticle_lease` tool opens a browser Reticle drives itself and needs no free port — or stop ' +
|
|
92
|
-
'it with `reticle stop` and retry, or drive on another port with `RETICLE_PORT=N` (`drive` ' +
|
|
93
|
-
'takes the port from the environment or .reticle.json, not from a flag).');
|
|
94
|
-
}
|
|
95
67
|
/** How long the daemon /status probe waits before giving up — a local loopback call is near-instant. */
|
|
96
68
|
const STATUS_PROBE_TIMEOUT_MS = 1000;
|
|
97
69
|
/** GET the daemon's /status JSON. Resolves to the parsed body, or undefined on any failure. */
|
package/dist/cli/cli-parse.d.ts
CHANGED
|
@@ -13,7 +13,7 @@ export { AGENT_FLAG, BUG_FLAG, FEEDBACK_KINDS, KIND_FLAG, RATING_FLAG, } from '.
|
|
|
13
13
|
* telling readers to do exactly that on 110 lines before it was caught, so the invocation now leads
|
|
14
14
|
* the usage block rather than being a footnote somewhere else.
|
|
15
15
|
*/
|
|
16
|
-
export declare const CLI_USAGE = "usage: npx @reticlehq/server <command> (or `reticle <command>` once the bin is on your PATH)\n\n reticle init [--dry-run] [--port N] [--no-mcp] [--no-install] [--app <dir>] (wire Reticle into the project in this directory)\n --app picks WHICH app in a monorepo, when several are found\n --no-mcp skips MORE than the server registration: also the agent rule files\n (CLAUDE.md / AGENTS.md / .cursor) and the /reticle command, because all three\n only make sense once the tools are reachable.\n reticle serve [--port N] [--drive <url>] [--headless] [--http] [--http-port N] [--http-token T]\n reticle stop [--port N] [--quiet] (stop the daemon we started, by its recorded pid)\n reticle kill [--port N] [--force] (free the port by its LISTENER, never the agent's mcp proxy)\n reticle restart [--port N] [--force] (kill, then start a daemon and wait for a real bind)\n reticle status [--port N]\n reticle doctor [--port N] (one command to diagnose setup: Chromium, daemon, port)\n reticle open [url] [--port N] (show the app: reuse the connected tab, else open one)\n reticle verify <url> [--headed] [--timeout N] [--storage-state <file>] (one-shot: drive the URL, verify saved flows, exit 0=pass)\n reticle affected [--since <ref>] [file...] (which saved flows must re-verify for the changed files)\n reticle gate [--since <ref>] [file...] (exit non-zero unless passing artifacts cover the affected flows)\n reticle watch [url] (on save, report which saved flows must re-verify)\n reticle drive <url> [--headless] (foreground mode, for debugging)\n reticle mcp [--port N] [--drive <url>] [--headless] (MCP stdio proxy; auto-starts daemon if needed)\n reticle update (install the latest server version and restart)\n reticle rollback (restore the previous server version and restart)\n reticle license (show enterprise license status: active | eval | missing)\n reticle telemetry [status|enable|disable] (anonymous usage metrics; status shows what's sent + the policy)\n reticle feedback [--rating 1-5] [--bug] \"message\" (tell us what worked and what didn't; prints exactly what it sends)\n reticle feedback --agent --kind <bug|gap|ambiguity|feature_request|improvement> \"message\"\n (agents: file from anywhere, including a setup that never finished)\n reticle identify --context company|side_project|open_source|learning [--company N] [--email E] [--forget]\n (OPT-IN: tell us who you are, e.g. for support or an enterprise trial)\n\nCloud (link this project to Reticle Cloud; runs/flows recorded on the dashboard):\n reticle login --email <e> [--code <c>] [--org <n>] (sign in: mails a code, then exchanges it)\n reticle link [--project <name|id>] (bind this repo: mints a scoped key, writes .reticle/cloud.json)\n reticle whoami (who am I signed in as, and is this repo attached?)\n reticle project <ls|create <name>> (list or create cloud projects)\n reticle config [--runs on|off] [--memory on|off] [--flows on|off] [--verify local|server]\n reticle push (send local run artifacts to the dashboard)\n reticle runs | regression | share <runId> (read cloud state; regression exits 3 if any flow broke)\n\n'drive' shows the browser; everything else is hidden (serve/mcp own the pool behind leases, replay\nand the spec runner: batch work). --headed opts any of them in, --headless hides drive, CI hides it.";
|
|
16
|
+
export declare const CLI_USAGE = "usage: npx @reticlehq/server <command> (or `reticle <command>` once the bin is on your PATH)\n\n reticle init [--dry-run] [--port N] [--no-mcp] [--no-install] [--app <dir>] (wire Reticle into the project in this directory)\n --app picks WHICH app in a monorepo, when several are found\n --no-mcp skips MORE than the server registration: also the agent rule files\n (CLAUDE.md / AGENTS.md / .cursor) and the /reticle command, because all three\n only make sense once the tools are reachable.\n reticle serve [--port N] [--drive <url>] [--headless] [--http] [--http-port N] [--http-token T]\n reticle stop [--port N] [--quiet] (stop the daemon we started, by its recorded pid)\n reticle kill [--port N] [--force] (free the port by its LISTENER, never the agent's mcp proxy)\n reticle restart [--port N] [--force] (kill, then start a daemon and wait for a real bind)\n reticle status [--port N]\n reticle doctor [--port N] (one command to diagnose setup: Chromium, daemon, port)\n reticle open [url] [--port N] (show the app: reuse the connected tab, else open one)\n reticle verify <url> [--headed] [--timeout N] [--storage-state <file>] (one-shot: drive the URL, verify saved flows, exit 0=pass)\n reticle affected [--since <ref>] [file...] (which saved flows must re-verify for the changed files)\n reticle gate [--since <ref>] [file...] (exit non-zero unless passing artifacts cover the affected flows)\n reticle watch [url] (on save, report which saved flows must re-verify)\n reticle drive <url> [--headless] (foreground mode, for debugging)\n reticle mcp [--port N] [--drive <url>] [--headless] (MCP stdio proxy; auto-starts daemon if needed)\n reticle update (install the latest server version and restart)\n reticle rollback (restore the previous server version and restart)\n reticle license (show enterprise license status: active | eval | missing)\n reticle telemetry [status|enable|disable] (anonymous usage metrics; status shows what's sent + the policy)\n reticle feedback [--rating 1-5] [--bug] \"message\" (tell us what worked and what didn't; prints exactly what it sends)\n reticle feedback --agent --kind <bug|gap|ambiguity|feature_request|improvement|experience> \"message\"\n (agents: file from anywhere, including a setup that never finished)\n reticle identify --context company|side_project|open_source|learning [--company N] [--email E] [--forget]\n (OPT-IN: tell us who you are, e.g. for support or an enterprise trial)\n\nCloud (link this project to Reticle Cloud; runs/flows recorded on the dashboard):\n reticle login --email <e> [--code <c>] [--org <n>] (sign in: mails a code, then exchanges it)\n reticle link [--project <name|id>] (bind this repo: mints a scoped key, writes .reticle/cloud.json)\n reticle whoami (who am I signed in as, and is this repo attached?)\n reticle project <ls|create <name>> (list or create cloud projects)\n reticle config [--runs on|off] [--memory on|off] [--flows on|off] [--verify local|server]\n reticle push (send local run artifacts to the dashboard)\n reticle runs | regression | share <runId> (read cloud state; regression exits 3 if any flow broke)\n\n'drive' shows the browser; everything else is hidden (serve/mcp own the pool behind leases, replay\nand the spec runner: batch work). --headed opts any of them in, --headless hides drive, CI hides it.";
|
|
17
17
|
/** The `reticle telemetry` sub-actions. Bare `reticle telemetry` means `status`. */
|
|
18
18
|
export declare const TelemetryAction: {
|
|
19
19
|
readonly STATUS: "status";
|
package/dist/cli/cli-parse.js
CHANGED
|
@@ -40,7 +40,7 @@ export const CLI_USAGE = `usage: npx @reticlehq/server <command> (or \`reticl
|
|
|
40
40
|
reticle license (show enterprise license status: active | eval | missing)
|
|
41
41
|
reticle telemetry [status|enable|disable] (anonymous usage metrics; status shows what's sent + the policy)
|
|
42
42
|
reticle feedback [--rating 1-5] [--bug] "message" (tell us what worked and what didn't; prints exactly what it sends)
|
|
43
|
-
reticle feedback --agent --kind <bug|gap|ambiguity|feature_request|improvement> "message"
|
|
43
|
+
reticle feedback --agent --kind <bug|gap|ambiguity|feature_request|improvement|experience> "message"
|
|
44
44
|
(agents: file from anywhere, including a setup that never finished)
|
|
45
45
|
reticle identify --context company|side_project|open_source|learning [--company N] [--email E] [--forget]
|
|
46
46
|
(OPT-IN: tell us who you are, e.g. for support or an enterprise trial)
|
package/dist/cli/cli-port.d.ts
CHANGED
|
@@ -7,6 +7,21 @@
|
|
|
7
7
|
* 3..reticle.json "port" field in the cwd ← per-project isolation
|
|
8
8
|
* 4. RETICLE_DEFAULT_PORT (4400)
|
|
9
9
|
*/
|
|
10
|
+
/** The project config `reticle init` writes. */
|
|
11
|
+
export declare const RETICLE_CONFIG_BASENAME = ".reticle.json";
|
|
12
|
+
/**
|
|
13
|
+
* Find the nearest `.reticle.json` at or above `cwd`, and return its parsed contents.
|
|
14
|
+
*
|
|
15
|
+
* Every reader below used to join the basename onto `cwd` and stop there, so a process whose
|
|
16
|
+
* working directory was one level away from the config behaved exactly like a project that had
|
|
17
|
+
* never been through `init`: no port, no projectId, no journal setting, and a diagnostic that told
|
|
18
|
+
* the user to run an install they had already run. That is the shape of several field reports at
|
|
19
|
+
* once — an app wired in a subdirectory, an agent running from a repo root, a worktree beside its
|
|
20
|
+
* main checkout — and they are all the same missing walk.
|
|
21
|
+
*
|
|
22
|
+
* Returns undefined when nothing is found, which keeps every caller's existing default intact.
|
|
23
|
+
*/
|
|
24
|
+
export declare function findProjectConfig(cwd: string): Record<string, unknown> | undefined;
|
|
10
25
|
/**
|
|
11
26
|
* Ports that belong to a DEV SERVER, not to Reticle's bridge. Next defaults to 3000, Vite to 5173,
|
|
12
27
|
* Astro to 4321, and so on.
|
|
@@ -33,6 +48,15 @@ export declare function readProjectPort(cwd: string): number | undefined;
|
|
|
33
48
|
* undefined if the file is absent/unreadable or has no non-empty string projectId.
|
|
34
49
|
*/
|
|
35
50
|
export declare function readProjectId(cwd: string): string | undefined;
|
|
51
|
+
/**
|
|
52
|
+
* The framework `reticle init` stamped into `.reticle.json`, when there is one.
|
|
53
|
+
*
|
|
54
|
+
* Read so the no-session diagnosis can RANK its causes instead of printing one static differential.
|
|
55
|
+
* Nuxt is the case that pays for this: it does not register a newly added plugin on HMR, so a dev
|
|
56
|
+
* server older than the wiring is its single most likely cause — `init` warns about it at install
|
|
57
|
+
* time and the hint the agent reads hours later never mentioned it.
|
|
58
|
+
*/
|
|
59
|
+
export declare function readProjectFramework(cwd: string): string | undefined;
|
|
36
60
|
/**
|
|
37
61
|
* Whether the durable causal journal is enabled. On by default (the journal IS the loop); off only via
|
|
38
62
|
* explicit opt-out — `.reticle.json` `"journal": false`, or `RETICLE_JOURNAL` set to `0`/`false`. The env
|