@reticlehq/server 2.11.0 → 2.13.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 +93 -111
- package/dist/bridge/auth-failure-reason.d.ts +6 -2
- package/dist/bridge/auth-failure-reason.js +15 -8
- package/dist/bridge/bridge.d.ts +18 -14
- package/dist/bridge/bridge.js +248 -165
- package/dist/bridge/bridge.test-harness.d.ts +2 -0
- package/dist/bridge/bridge.test-harness.js +8 -1
- package/dist/bridge/pairing-token.d.ts +5 -0
- package/dist/bridge/pairing-token.js +28 -0
- package/dist/bridge/protocol-skew.d.ts +37 -0
- package/dist/bridge/protocol-skew.js +45 -0
- package/dist/capsule/capsule-store.d.ts +8 -7
- package/dist/capsule/capsule-store.js +1 -1
- package/dist/capsule/causal-summary.d.ts +2 -2
- package/dist/capsule/causal-summary.js +7 -2
- package/dist/chromium-launch-options.d.ts +28 -0
- package/dist/chromium-launch-options.js +29 -0
- package/dist/cli/cli-doctor.js +39 -1
- package/dist/cli/cli-flow-commands.d.ts +3 -1
- package/dist/cli/cli-flow-commands.js +34 -4
- package/dist/cli/cli-kill.d.ts +2 -1
- package/dist/cli/cli-launch.d.ts +11 -1
- package/dist/cli/cli-launch.js +59 -10
- package/dist/cli/cli-parse.d.ts +17 -1
- package/dist/cli/cli-parse.js +238 -12
- package/dist/cli/cli-update-commands.js +20 -1
- package/dist/cli/cli-verify.d.ts +2 -1
- package/dist/cli/cli-verify.js +5 -4
- package/dist/cli/cloud-cli.js +428 -253
- package/dist/cli/cloud-keystore.d.ts +37 -0
- package/dist/cli/cloud-keystore.js +97 -0
- package/dist/cli/cloud-kit.d.ts +37 -0
- package/dist/cli/cloud-kit.js +121 -0
- package/dist/cli/cloud-login.d.ts +17 -0
- package/dist/cli/cloud-login.js +183 -0
- package/dist/cli/cloud-session.d.ts +48 -0
- package/dist/cli/cloud-session.js +53 -0
- package/dist/cli/config-discovery.d.ts +2 -1
- package/dist/cli/config-discovery.js +1 -0
- package/dist/cli/daemon-start-options.d.ts +32 -0
- package/dist/cli/daemon-start-options.js +38 -0
- package/dist/cli/device-flow.d.ts +76 -0
- package/dist/cli/device-flow.js +56 -0
- package/dist/cli/doctor-rows.d.ts +12 -1
- package/dist/cli/doctor-rows.js +12 -1
- package/dist/cli/drive-attach.d.ts +13 -0
- package/dist/cli/drive-attach.js +18 -0
- package/dist/cli/drive-command.js +7 -3
- package/dist/cli/gate-exit.d.ts +21 -0
- package/dist/cli/gate-exit.js +21 -0
- package/dist/cli/gate-hook-message.d.ts +33 -0
- package/dist/cli/gate-hook-message.js +46 -0
- package/dist/cli/mcp-command.d.ts +25 -0
- package/dist/cli/mcp-command.js +174 -0
- package/dist/cli/project-name.d.ts +30 -0
- package/dist/cli/project-name.js +44 -0
- package/dist/cli/sibling-ports.d.ts +37 -0
- package/dist/cli/sibling-ports.js +56 -0
- package/dist/cli/status-next-action.d.ts +14 -0
- package/dist/cli/status-next-action.js +4 -4
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +167 -186
- package/dist/cloud/cloud-config.d.ts +24 -2
- package/dist/cloud/cloud-config.js +64 -5
- package/dist/cloud/cloud-sync.d.ts +16 -5
- package/dist/cloud/cloud-sync.js +64 -1
- package/dist/cloud/sync-cycle.d.ts +110 -0
- package/dist/cloud/sync-cycle.js +270 -0
- package/dist/cloud/sync-daemon.d.ts +69 -0
- package/dist/cloud/sync-daemon.js +260 -0
- package/dist/cloud/sync-disk.d.ts +9 -0
- package/dist/cloud/sync-disk.js +191 -0
- package/dist/cloud/sync-hash.d.ts +2 -0
- package/dist/cloud/sync-hash.js +35 -0
- package/dist/crawl/crawl-tools.js +5 -3
- package/dist/crawl/crawl.d.ts +12 -13
- package/dist/crawl/crawl.js +25 -3
- package/dist/daemon/daemon-resolve.d.ts +122 -0
- package/dist/daemon/daemon-resolve.js +187 -0
- package/dist/daemon/dev-servers.d.ts +15 -0
- package/dist/daemon/dev-servers.js +68 -0
- package/dist/daemon/free-port.d.ts +29 -0
- package/dist/daemon/free-port.js +55 -0
- package/dist/daemon/heartbeat.d.ts +0 -4
- package/dist/daemon/heartbeat.js +2 -2
- package/dist/daemon/wake-decision.d.ts +32 -0
- package/dist/daemon/wake-decision.js +38 -0
- package/dist/dev/stale-issue-guard.d.ts +0 -2
- package/dist/dev/stale-issue-guard.js +1 -1
- package/dist/domain/domain-tools.js +2 -2
- package/dist/events/contradictions.d.ts +59 -0
- package/dist/events/contradictions.js +253 -27
- package/dist/events/declared.d.ts +17 -0
- package/dist/events/declared.js +43 -1
- package/dist/events/echo-mismatch.js +65 -1
- package/dist/events/event-filters.d.ts +8 -0
- package/dist/events/event-filters.js +19 -2
- package/dist/events/observed-in-window.d.ts +12 -0
- package/dist/events/observed-in-window.js +17 -0
- package/dist/events/predicate-asks.d.ts +13 -0
- package/dist/events/predicate-asks.js +22 -0
- package/dist/events/predicate-eval.js +117 -27
- package/dist/events/predicate-parse.d.ts +0 -6
- package/dist/events/predicate-parse.js +43 -3
- package/dist/events/predicate-precheck.d.ts +8 -0
- package/dist/events/predicate-precheck.js +41 -0
- package/dist/events/predicate-route.d.ts +14 -0
- package/dist/events/predicate-route.js +36 -8
- package/dist/events/predicate-schema.d.ts +44 -0
- package/dist/events/predicate-schema.js +126 -23
- package/dist/events/predicate.d.ts +20 -0
- package/dist/events/predicate.js +77 -6
- package/dist/events/split-text-miss.d.ts +30 -0
- package/dist/events/split-text-miss.js +44 -0
- package/dist/flows/coverage.d.ts +0 -8
- package/dist/flows/coverage.js +1 -1
- package/dist/flows/decision.d.ts +5 -1
- package/dist/flows/decision.js +33 -7
- package/dist/flows/flake.d.ts +6 -8
- package/dist/flows/flake.js +1 -1
- package/dist/flows/flow-intent.d.ts +52 -0
- package/dist/flows/flow-intent.js +123 -0
- package/dist/flows/flow-memory-consult.d.ts +55 -0
- package/dist/flows/flow-memory-consult.js +38 -0
- package/dist/flows/flow-replay-run.d.ts +18 -7
- package/dist/flows/flow-replay-run.js +263 -23
- package/dist/flows/flow-replay.js +11 -2
- package/dist/flows/flow-step-runners.d.ts +8 -0
- package/dist/flows/flow-step-runners.js +8 -2
- package/dist/flows/flow-store-for-session.d.ts +31 -0
- package/dist/flows/flow-store-for-session.js +33 -0
- package/dist/flows/flow-success.js +15 -3
- package/dist/flows/flow-tools.js +87 -33
- package/dist/flows/flows.d.ts +14 -1
- package/dist/flows/flows.js +502 -24
- package/dist/flows/heal-run.js +5 -2
- package/dist/flows/predicate-to-expect.d.ts +17 -12
- package/dist/flows/predicate-to-expect.js +36 -15
- package/dist/flows/recordings.d.ts +13 -1
- package/dist/flows/recordings.js +17 -2
- package/dist/flows/replay.d.ts +22 -2
- package/dist/flows/replay.js +28 -2
- package/dist/flows/server-verify.js +16 -2
- package/dist/flows/verify-change-tools.js +9 -3
- package/dist/honesty/blind-spots.d.ts +51 -15
- package/dist/honesty/blind-spots.js +64 -4
- package/dist/honesty/feature-capture.d.ts +150 -0
- package/dist/honesty/feature-capture.js +233 -0
- package/dist/honesty/honesty.d.ts +24 -0
- package/dist/honesty/honesty.js +25 -1
- package/dist/honesty/instrumentation-gaps.d.ts +39 -6
- package/dist/honesty/instrumentation-gaps.js +88 -5
- package/dist/honesty/tool-hit-rate.d.ts +86 -0
- package/dist/honesty/tool-hit-rate.js +90 -0
- package/dist/honesty/undeclared-change.d.ts +51 -0
- package/dist/honesty/undeclared-change.js +56 -0
- package/dist/honesty/verified.d.ts +16 -0
- package/dist/honesty/verified.js +84 -5
- package/dist/http-server.d.ts +0 -1
- package/dist/http-server.js +13 -1
- package/dist/hunt/hunt-report.d.ts +0 -2
- package/dist/hunt/hunt-report.js +1 -1
- package/dist/impact/impact-recorder.d.ts +31 -8
- package/dist/impact/impact-recorder.js +104 -21
- package/dist/impact/impact-store.d.ts +13 -4
- package/dist/impact/impact-store.js +44 -3
- package/dist/index.d.ts +4 -1
- package/dist/index.js +113 -7
- package/dist/init/agent-rules.d.ts +30 -6
- package/dist/init/agent-rules.js +54 -13
- package/dist/init/astro-patch.d.ts +16 -0
- package/dist/init/astro-patch.js +28 -0
- package/dist/init/capabilities.d.ts +4 -3
- package/dist/init/capabilities.js +23 -3
- package/dist/init/closing-hint.d.ts +36 -0
- package/dist/init/closing-hint.js +120 -0
- package/dist/init/confirm.d.ts +27 -28
- package/dist/init/confirm.js +51 -11
- package/dist/init/cra.d.ts +34 -5
- package/dist/init/cra.js +96 -25
- package/dist/init/csp-check.js +17 -2
- package/dist/init/csp-doctor.d.ts +8 -1
- package/dist/init/csp-doctor.js +8 -3
- package/dist/init/dev-script.d.ts +50 -0
- package/dist/init/dev-script.js +66 -0
- package/dist/init/format-generated.d.ts +18 -0
- package/dist/init/format-generated.js +39 -0
- package/dist/init/install-hint.d.ts +9 -0
- package/dist/init/install-hint.js +29 -0
- package/dist/init/mcp-clients.d.ts +7 -0
- package/dist/init/mcp-clients.js +7 -0
- package/dist/init/mcp.d.ts +17 -6
- package/dist/init/mcp.js +27 -11
- package/dist/init/node-io.js +11 -1
- package/dist/init/plan-framework.d.ts +0 -5
- package/dist/init/plan-framework.js +147 -37
- package/dist/init/plan.d.ts +30 -37
- package/dist/init/plan.js +50 -60
- package/dist/init/preflight.d.ts +28 -0
- package/dist/init/preflight.js +38 -0
- package/dist/init/refresh-rules.d.ts +39 -0
- package/dist/init/refresh-rules.js +51 -0
- package/dist/init/run.d.ts +32 -35
- package/dist/init/run.js +151 -202
- package/dist/init/slash-command.d.ts +1 -1
- package/dist/init/snippets.js +90 -31
- package/dist/init/vite-config.d.ts +1 -1
- package/dist/init/vite-config.js +39 -25
- package/dist/init/workspace-apps.d.ts +17 -10
- package/dist/init/workspace-apps.js +110 -0
- package/dist/input/network-mock-tools.js +3 -6
- package/dist/input/real-input.d.ts +2 -0
- package/dist/input/real-input.js +28 -7
- package/dist/input/scroll-find.d.ts +7 -4
- package/dist/input/scroll-find.js +63 -25
- package/dist/input/scroll-tools.js +4 -6
- package/dist/input/viewport-tools.js +5 -4
- package/dist/intent/inline-intent.d.ts +37 -0
- package/dist/intent/inline-intent.js +193 -0
- package/dist/intent/intent-shard-store.d.ts +72 -0
- package/dist/intent/intent-shard-store.js +203 -0
- package/dist/intent/intent-shard.d.ts +411 -0
- package/dist/intent/intent-shard.js +176 -0
- package/dist/intent/intent-store.d.ts +49 -0
- package/dist/intent/intent-store.js +133 -0
- package/dist/intent/intent-subject.d.ts +45 -0
- package/dist/intent/intent-subject.js +177 -0
- package/dist/intent/intent-tools.d.ts +2 -0
- package/dist/intent/intent-tools.js +143 -0
- package/dist/intent/open-intents.d.ts +42 -0
- package/dist/intent/open-intents.js +40 -0
- package/dist/journal/envelope.d.ts +26 -26
- package/dist/journal/journal-recorder.d.ts +23 -0
- package/dist/journal/journal-recorder.js +35 -0
- package/dist/license/license-env.d.ts +29 -0
- package/dist/license/license-env.js +148 -0
- package/dist/loopback-agent.d.ts +31 -0
- package/dist/loopback-agent.js +57 -0
- package/dist/mcp/attach-memory.d.ts +83 -0
- package/dist/mcp/attach-memory.js +185 -0
- package/dist/mcp/client-identity.d.ts +31 -0
- package/dist/mcp/client-identity.js +51 -0
- package/dist/mcp/mcp-post-transport.d.ts +26 -0
- package/dist/mcp/mcp-post-transport.js +109 -0
- package/dist/mcp/mcp-proxy.d.ts +2 -8
- package/dist/mcp/mcp-proxy.js +64 -62
- package/dist/mcp/mcp.d.ts +2 -0
- package/dist/mcp/mcp.js +52 -24
- package/dist/mcp/proxy-backoff.d.ts +11 -0
- package/dist/mcp/proxy-backoff.js +13 -0
- package/dist/mcp/proxy-daemon-probe.d.ts +1 -0
- package/dist/mcp/proxy-daemon-probe.js +7 -1
- package/dist/mcp/proxy-handshake.d.ts +23 -0
- package/dist/mcp/proxy-handshake.js +31 -0
- package/dist/mcp/server-instructions.js +45 -3
- package/dist/mcp/tool-catalog-cache.d.ts +16 -0
- package/dist/mcp/tool-catalog-cache.js +22 -0
- package/dist/memory/memory-tools.d.ts +2 -0
- package/dist/memory/memory-tools.js +82 -0
- package/dist/memory/project-memory.d.ts +34 -0
- package/dist/memory/project-memory.js +96 -0
- package/dist/oracles/flow-instrument-gaps.d.ts +4 -4
- package/dist/oracles/flow-instrument-gaps.js +4 -5
- package/dist/oracles/propose-consequences.js +12 -3
- package/dist/oracles/self-instrument.d.ts +9 -9
- package/dist/oracles/self-instrument.js +19 -13
- package/dist/pool/browser-pool.d.ts +75 -5
- package/dist/pool/browser-pool.js +144 -5
- package/dist/pool/playwright-launcher.js +12 -1
- package/dist/project/fs-port.d.ts +3 -0
- package/dist/project/fs-port.js +3 -2
- package/dist/project/memory-fs.js +6 -1
- package/dist/project/project-tools.js +6 -35
- package/dist/project/reticle-dir.d.ts +17 -2
- package/dist/project/reticle-dir.js +22 -4
- package/dist/runs/context-tools.d.ts +2 -0
- package/dist/runs/context-tools.js +89 -0
- package/dist/runs/run-context.d.ts +40 -0
- package/dist/runs/run-context.js +158 -0
- package/dist/runs/run-diff.d.ts +2 -1
- package/dist/runs/run-store.d.ts +10 -0
- package/dist/runs/run-store.js +10 -0
- package/dist/runs/verification-sync.d.ts +0 -11
- package/dist/runs/verification-sync.js +24 -3
- package/dist/runs/verify-server.d.ts +8 -1
- package/dist/runs/verify-server.js +14 -2
- package/dist/session/command-timeout.d.ts +5 -2
- package/dist/session/command-timeout.js +5 -4
- package/dist/session/dev-server-probe.d.ts +0 -8
- package/dist/session/dev-server-probe.js +1 -1
- package/dist/session/last-act.d.ts +7 -1
- package/dist/session/last-act.js +2 -2
- package/dist/session/lease-visibility.d.ts +67 -0
- package/dist/session/lease-visibility.js +76 -0
- package/dist/session/live-control-tools.js +1 -42
- package/dist/session/no-session-diagnosis.d.ts +33 -0
- package/dist/session/no-session-diagnosis.js +82 -24
- package/dist/session/no-session-next-action.d.ts +12 -0
- package/dist/session/no-session-next-action.js +77 -6
- package/dist/session/no-session-watch.d.ts +18 -6
- package/dist/session/no-session-watch.js +125 -75
- package/dist/session/output-budget.js +4 -1
- package/dist/session/pending-commands.d.ts +11 -1
- package/dist/session/pending-commands.js +12 -2
- package/dist/session/resolve-within.d.ts +34 -0
- package/dist/session/resolve-within.js +53 -0
- package/dist/session/session-health.d.ts +9 -0
- package/dist/session/session-health.js +23 -0
- package/dist/session/session-info.d.ts +18 -0
- package/dist/session/session-info.js +11 -0
- package/dist/session/session-manager.d.ts +14 -0
- package/dist/session/session-manager.js +111 -4
- package/dist/session/session-mirror.d.ts +16 -0
- package/dist/session/session-mirror.js +22 -0
- package/dist/session/session-recommendation.d.ts +19 -5
- package/dist/session/session-recommendation.js +25 -9
- package/dist/session/session-successor.d.ts +37 -0
- package/dist/session/session-successor.js +113 -0
- package/dist/session/session-tools.js +2 -2
- package/dist/session/session.d.ts +53 -3
- package/dist/session/session.js +162 -9
- package/dist/session/wall-clock.d.ts +12 -0
- package/dist/session/wall-clock.js +14 -0
- package/dist/setup/agent-configs.d.ts +96 -0
- package/dist/setup/agent-configs.js +333 -0
- package/dist/setup/agent-io.d.ts +9 -0
- package/dist/setup/agent-io.js +16 -0
- package/dist/setup/agent-writer.d.ts +51 -0
- package/dist/setup/agent-writer.js +115 -0
- package/dist/setup/approval-migration.d.ts +47 -0
- package/dist/setup/approval-migration.js +68 -0
- package/dist/setup/auto-approve.d.ts +85 -0
- package/dist/setup/auto-approve.js +150 -0
- package/dist/setup/bridge-port.d.ts +15 -0
- package/dist/setup/bridge-port.js +24 -0
- package/dist/setup/desktop-shape.d.ts +57 -0
- package/dist/setup/desktop-shape.js +63 -0
- package/dist/setup/dev-server-wait.d.ts +60 -0
- package/dist/setup/dev-server-wait.js +65 -0
- package/dist/setup/drive-agent.d.ts +71 -0
- package/dist/setup/drive-agent.js +171 -0
- package/dist/setup/drive-plan.d.ts +68 -0
- package/dist/setup/drive-plan.js +130 -0
- package/dist/setup/ensure-daemon.d.ts +29 -0
- package/dist/setup/ensure-daemon.js +58 -0
- package/dist/setup/init-runtime.d.ts +47 -0
- package/dist/setup/init-runtime.js +160 -0
- package/dist/setup/interrupt.d.ts +25 -0
- package/dist/setup/interrupt.js +40 -0
- package/dist/setup/license-key.d.ts +39 -0
- package/dist/setup/license-key.js +72 -0
- package/dist/setup/listeners.d.ts +37 -0
- package/dist/setup/listeners.js +85 -0
- package/dist/setup/node-effects.d.ts +60 -0
- package/dist/setup/node-effects.js +210 -0
- package/dist/setup/page-probe.d.ts +39 -0
- package/dist/setup/page-probe.js +49 -0
- package/dist/setup/relaunch.d.ts +36 -0
- package/dist/setup/relaunch.js +57 -0
- package/dist/setup/remaining-steps.d.ts +19 -0
- package/dist/setup/remaining-steps.js +31 -0
- package/dist/setup/run-setup.d.ts +99 -0
- package/dist/setup/run-setup.js +187 -0
- package/dist/setup/session-pick.d.ts +47 -0
- package/dist/setup/session-pick.js +51 -0
- package/dist/setup/setup-command.d.ts +45 -0
- package/dist/setup/setup-command.js +199 -0
- package/dist/setup/setup-options.d.ts +74 -0
- package/dist/setup/setup-options.js +67 -0
- package/dist/setup/transcripts.d.ts +16 -0
- package/dist/setup/transcripts.js +43 -0
- package/dist/status-payload.d.ts +14 -1
- package/dist/status-payload.js +23 -1
- package/dist/telemetry/automation-hint.d.ts +2 -1
- package/dist/telemetry/daemon-telemetry.d.ts +8 -1
- package/dist/telemetry/daemon-telemetry.js +52 -5
- package/dist/telemetry/error-class.js +1 -1
- package/dist/telemetry/feedback-context.d.ts +9 -1
- package/dist/telemetry/feedback-context.js +60 -29
- package/dist/telemetry/feedback-local.d.ts +15 -0
- package/dist/telemetry/feedback-local.js +56 -0
- package/dist/telemetry/feedback-outbox.d.ts +2 -1
- package/dist/telemetry/feedback.d.ts +11 -0
- package/dist/telemetry/feedback.js +19 -6
- package/dist/telemetry/init-telemetry.d.ts +2 -0
- package/dist/telemetry/init-telemetry.js +2 -0
- package/dist/telemetry/install-source.d.ts +14 -0
- package/dist/telemetry/install-source.js +30 -0
- package/dist/telemetry/license-activation.d.ts +7 -1
- package/dist/telemetry/license-activation.js +9 -5
- package/dist/telemetry/mcp-connection.js +7 -0
- package/dist/telemetry/project-profile.d.ts +12 -9
- package/dist/telemetry/project-profile.js +12 -7
- package/dist/telemetry/proxy-telemetry.d.ts +3 -0
- package/dist/telemetry/proxy-telemetry.js +21 -0
- package/dist/telemetry/run-telemetry.d.ts +0 -2
- package/dist/telemetry/run-telemetry.js +1 -1
- package/dist/telemetry/session-metrics.d.ts +12 -0
- package/dist/telemetry/session-metrics.js +35 -1
- package/dist/telemetry/telemetry.js +17 -2
- package/dist/telemetry/tool-refused.d.ts +3 -1
- package/dist/telemetry/tool-refused.js +34 -2
- package/dist/telemetry/verification-of.js +2 -2
- package/dist/tools/act-capsule.d.ts +10 -0
- package/dist/tools/act-capsule.js +8 -2
- package/dist/tools/act-danger.d.ts +2 -1
- package/dist/tools/act-danger.js +11 -3
- package/dist/tools/act-observation.d.ts +41 -0
- package/dist/tools/act-observation.js +36 -0
- package/dist/tools/act-preflight.d.ts +20 -0
- package/dist/tools/act-preflight.js +98 -0
- package/dist/tools/act-sequence-retry.d.ts +45 -0
- package/dist/tools/act-sequence-retry.js +58 -0
- package/dist/tools/act-sequence-tool.d.ts +13 -0
- package/dist/tools/act-sequence-tool.js +135 -0
- package/dist/tools/act-target.d.ts +14 -0
- package/dist/tools/act-target.js +47 -0
- package/dist/tools/act-tools.d.ts +21 -1
- package/dist/tools/act-tools.js +196 -204
- package/dist/tools/assert-source.d.ts +22 -0
- package/dist/tools/assert-source.js +53 -0
- package/dist/tools/assert-verdict.d.ts +20 -3
- package/dist/tools/assert-verdict.js +58 -20
- package/dist/tools/browser-tools.js +6 -22
- package/dist/tools/complete-snapshot.d.ts +48 -0
- package/dist/tools/complete-snapshot.js +75 -0
- package/dist/tools/coverage-tools.d.ts +10 -1
- package/dist/tools/coverage-tools.js +123 -69
- package/dist/tools/dynamic-tools.js +53 -13
- package/dist/tools/error-recovery.d.ts +25 -14
- package/dist/tools/error-recovery.js +78 -9
- package/dist/tools/feedback-tools.d.ts +1 -1
- package/dist/tools/feedback-tools.js +2 -2
- package/dist/tools/invoke-tool.js +121 -11
- package/dist/tools/lease-hint.d.ts +10 -0
- package/dist/tools/lease-hint.js +53 -4
- package/dist/tools/lease-tools.d.ts +35 -5
- package/dist/tools/lease-tools.js +191 -13
- package/dist/tools/merged-name-redirect.d.ts +13 -0
- package/dist/tools/merged-name-redirect.js +22 -0
- package/dist/tools/navigate-arrival.d.ts +0 -2
- package/dist/tools/navigate-arrival.js +1 -1
- package/dist/tools/numeric-bounds.d.ts +100 -0
- package/dist/tools/numeric-bounds.js +125 -0
- package/dist/tools/observe-tools.js +109 -65
- package/dist/tools/read-tools.js +31 -8
- package/dist/tools/real-input-attempt.d.ts +32 -0
- package/dist/tools/real-input-attempt.js +212 -2
- package/dist/tools/reconcile-tools.js +22 -8
- package/dist/tools/resolve-target.d.ts +0 -7
- package/dist/tools/sdk-marker-probe.d.ts +29 -0
- package/dist/tools/sdk-marker-probe.js +57 -0
- package/dist/tools/snapshot-delta.d.ts +1 -1
- package/dist/tools/tool-kit.d.ts +24 -0
- package/dist/tools/tool-kit.js +21 -0
- package/dist/tools/tool-names.d.ts +5 -0
- package/dist/tools/tool-names.js +5 -0
- package/dist/tools/tool-surface.d.ts +85 -6
- package/dist/tools/tool-surface.js +122 -2
- package/dist/tools/tools-helpers.d.ts +19 -0
- package/dist/tools/tools-helpers.js +46 -0
- package/dist/tools/tools.d.ts +2 -0
- package/dist/tools/tools.js +78 -12
- package/dist/tools/unadvertised-help.js +4 -4
- package/dist/tools/verify-next-baton.d.ts +53 -0
- package/dist/tools/verify-next-baton.js +52 -0
- package/dist/update/reticle-deps.d.ts +9 -0
- package/dist/update/reticle-deps.js +27 -0
- package/dist/update/sdk-sync.d.ts +1 -2
- package/dist/update/sdk-sync.js +1 -21
- package/dist/update/update-nudge.d.ts +25 -10
- package/dist/update/update-nudge.js +32 -3
- package/dist/update/updater.d.ts +1 -1
- package/dist/version/peer-announce.js +4 -2
- package/dist/version/sdk-fix.d.ts +47 -0
- package/dist/version/sdk-fix.js +127 -0
- package/dist/version/version-nudge.d.ts +14 -0
- package/dist/version/version-nudge.js +21 -0
- package/dist/version/version-skew.d.ts +38 -3
- package/dist/version/version-skew.js +83 -7
- package/dist/visual/visual-store.d.ts +6 -6
- package/dist/visual/visual-store.js +18 -16
- package/dist/visual/visual-tools.js +57 -12
- package/docs/README.md +2 -1
- package/docs/cli/cloud.mdx +1 -1
- package/docs/cli/doctor.mdx +2 -0
- package/docs/cli/serve.mdx +4 -2
- package/docs/cli/verify.mdx +4 -5
- package/docs/cli.mdx +7 -3
- package/docs/desktop.mdx +1 -1
- package/docs/docs.json +2 -2
- package/docs/gates.md +1 -1
- package/docs/getting-started.md +19 -3
- package/docs/index.mdx +9 -6
- package/docs/install-agentic.mdx +38 -12
- package/docs/install-manual.mdx +1 -1
- package/docs/packages/browser.mdx +2 -2
- package/docs/packages/server.mdx +1 -1
- package/docs/packages.mdx +8 -6
- package/docs/platform-integration.md +17 -0
- package/docs/predicates.mdx +8 -0
- package/docs/reticle-manual.md +106 -0
- package/docs/skill-file.mdx +3 -3
- package/docs/telemetry-contract.md +108 -4
- package/docs/tools-act-sequence.mdx +16 -0
- package/docs/tools-tools-and-run.mdx +2 -4
- package/docs/troubleshooting.mdx +9 -0
- package/docs/usage.md +4 -3
- package/package.json +4 -3
- package/dist/init/cursor.d.ts +0 -24
- package/dist/init/cursor.js +0 -80
- package/dist/session/session-readiness.d.ts +0 -34
- package/dist/session/session-readiness.js +0 -36
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which daemon does THIS project belong to?
|
|
3
|
+
*
|
|
4
|
+
* The system answered that two incompatible ways. Build plugins asked the registry by projectId, the
|
|
5
|
+
* way `pickDaemonPort` was designed to be asked. The CLI and the MCP proxy asked a NUMBER —
|
|
6
|
+
* `envPort ?? projectPort ?? 4400` — and then attached to whatever owned it, whoever it belonged to.
|
|
7
|
+
*
|
|
8
|
+
* So every project on a machine funnelled into one daemon. Its recorded identity was whichever
|
|
9
|
+
* project won the race to the port, which made the registry entry wrong the moment a second project
|
|
10
|
+
* attached, and made one `kill` on one well-known port a machine-wide outage for every agent at once.
|
|
11
|
+
*
|
|
12
|
+
* It worked anyway, by luck: everyone defaulted to the same number, so everyone found each other.
|
|
13
|
+
* The luck ends the moment a daemon is anywhere but the default — which is exactly when discovery was
|
|
14
|
+
* supposed to help, and exactly when it returned nothing.
|
|
15
|
+
*
|
|
16
|
+
* This module is the one answer both sides now use. It is pure and injectable: no process spawning,
|
|
17
|
+
* no port binding, nothing that needs a real `~/.reticle` to test.
|
|
18
|
+
*/
|
|
19
|
+
import { type DaemonRegistryEntry } from '@reticlehq/core';
|
|
20
|
+
/**
|
|
21
|
+
* Every well-formed registry entry in `home`.
|
|
22
|
+
*
|
|
23
|
+
* A corrupt or half-written entry is skipped rather than thrown on: the registry is written by other
|
|
24
|
+
* processes that can be killed mid-write, and one bad file must not make a machine's daemons
|
|
25
|
+
* undiscoverable. Validated through core's schema so a shape change cannot silently pass here.
|
|
26
|
+
*/
|
|
27
|
+
export declare function readDaemonRegistry(home: string): DaemonRegistryEntry[];
|
|
28
|
+
/**
|
|
29
|
+
* The port of the live daemon serving `projectId`, or undefined when this project has none.
|
|
30
|
+
*
|
|
31
|
+
* Undefined means "start one", never "use the default and hope". That distinction is the fix: the old
|
|
32
|
+
* path had no way to express "no daemon of mine exists", so it fell through to a number and attached
|
|
33
|
+
* to a stranger.
|
|
34
|
+
*
|
|
35
|
+
* The selection rule is core's `pickDaemonPort`, shared with every build plugin, so the daemon side
|
|
36
|
+
* and the app side cannot disagree about which daemon is ours.
|
|
37
|
+
*/
|
|
38
|
+
export declare function resolveDaemonForProject(projectId: string | undefined, home: string, alive: (pid: number) => boolean): number | undefined;
|
|
39
|
+
/**
|
|
40
|
+
* May `caller` take over a daemon already sitting on the port it wanted?
|
|
41
|
+
*
|
|
42
|
+
* Two deliberate escape hatches, both of which keep an existing install working:
|
|
43
|
+
*
|
|
44
|
+
* A daemon that claims NO project was started somewhere without a `.reticle.json` — the global MCP
|
|
45
|
+
* registration in a directory that is not an app, which is the majority of daemons in the field. It
|
|
46
|
+
* belongs to nobody, so it belongs to whoever asks; refusing would break every user running Reticle
|
|
47
|
+
* globally, which is how it is documented to be installed.
|
|
48
|
+
*
|
|
49
|
+
* A caller with no project of its own has no identity to defend and nothing to be confused about, so
|
|
50
|
+
* it keeps the old permissive behaviour rather than being locked out of a daemon it can use.
|
|
51
|
+
*
|
|
52
|
+
* Everything else — a named daemon and a differently-named caller — is the cross-project bleed, and
|
|
53
|
+
* is the one case this refuses.
|
|
54
|
+
*/
|
|
55
|
+
export declare function adoptable(daemonProjectId: string | undefined, callerProjectId: string | undefined): boolean;
|
|
56
|
+
/** The project a daemon on `port` claims, or undefined when it claims none / is not registered. */
|
|
57
|
+
export declare function daemonProjectAt(port: number, home: string): string | undefined;
|
|
58
|
+
/**
|
|
59
|
+
* Live daemons OTHER than `myPort` that an app for this project has connected to.
|
|
60
|
+
*
|
|
61
|
+
* The split brain this whole module was written to prevent has a second half nobody was reporting.
|
|
62
|
+
* `resolveMcpPort` correctly refuses to adopt a stranger's daemon and relocates to an OS-assigned
|
|
63
|
+
* port — and the APP, which resolves its own port independently (core's `pickDaemonPort` in the
|
|
64
|
+
* build plugin, falling back to the documented default when nothing matches), can land on the
|
|
65
|
+
* stranger. The pairing token is per USER, not per project, so that connect SUCCEEDS: the app is
|
|
66
|
+
* live on one daemon and the agent is attached to a different, empty one.
|
|
67
|
+
*
|
|
68
|
+
* Every individual check is then green. The browser shows an instrumented page, `reticle status`
|
|
69
|
+
* reports a session, the agent reports Reticle registered and enabled — and the agent's tools see
|
|
70
|
+
* nothing, with a no-session diagnosis that tells it to start a dev server that is already running.
|
|
71
|
+
*
|
|
72
|
+
* `connectedOn` is injected rather than imported so this stays free of the session layer (which
|
|
73
|
+
* imports from here) and so the rule is testable without a real `~/.reticle`. Callers pass
|
|
74
|
+
* `hasProjectConnectedBefore` bound to the same projectId.
|
|
75
|
+
*
|
|
76
|
+
* Only LIVE daemons are named: a port a session used yesterday is history, not a split.
|
|
77
|
+
*/
|
|
78
|
+
export declare function daemonsServingProjectElsewhere(projectId: string | undefined, myPort: number, home: string, alive: (pid: number) => boolean, connectedOn: (port: number) => boolean): readonly number[];
|
|
79
|
+
/**
|
|
80
|
+
* The sentence for a split brain, or `undefined` when there is none.
|
|
81
|
+
*
|
|
82
|
+
* Stated as a fact about two ports rather than a hedge, because unlike the sibling-port observation
|
|
83
|
+
* next door this is not "something is listening somewhere" — it is this project's own connection
|
|
84
|
+
* record naming a daemon that is not the one being asked. The wording says which side is empty,
|
|
85
|
+
* since the failure is invisible precisely because both sides look healthy from where they stand.
|
|
86
|
+
*/
|
|
87
|
+
export declare function splitBrainNote(myPort: number, elsewhere: readonly number[]): string | undefined;
|
|
88
|
+
/**
|
|
89
|
+
* The other stance on the same split, for a command standing on the port that HAS the app.
|
|
90
|
+
*
|
|
91
|
+
* `splitBrainNote` is asked from the empty daemon and reads the connection record. From the daemon
|
|
92
|
+
* the app is connected to there is nothing missing to notice — sessions are live, every check is
|
|
93
|
+
* green — and the only visible fact is that this project ALSO owns a live daemon somewhere else.
|
|
94
|
+
* One project, two daemons, is a split whichever end you are standing on, and saying it from both
|
|
95
|
+
* ends is the difference between a user inferring it from two green checkmarks and being told.
|
|
96
|
+
*
|
|
97
|
+
* Silent for a caller with no project (it owns no daemon to be split from) and silent when the port
|
|
98
|
+
* being inspected is the project's own.
|
|
99
|
+
*/
|
|
100
|
+
export declare function wrongDaemonNote(inspectedPort: number, ownPort: number | undefined): string | undefined;
|
|
101
|
+
/**
|
|
102
|
+
* The port this agent's MCP proxy should serve, decided BEFORE the proxy starts.
|
|
103
|
+
*
|
|
104
|
+
* Resolved once rather than per reconnect because the proxy's transport is built around a fixed
|
|
105
|
+
* number, and making it chase a moving port would mean reworking the reconnect path — the one piece
|
|
106
|
+
* of this system whose failure mode is "the MCP server disappeared mid-session". The port it settles
|
|
107
|
+
* on is this project's own, so the thing it would be chasing is a daemon it alone is responsible for.
|
|
108
|
+
*
|
|
109
|
+
* The order is the whole fix:
|
|
110
|
+
* 1. our project's live daemon, wherever it is listening — a daemon that moved is still ours;
|
|
111
|
+
* 2. the preferred port, but only if free or holding a daemon we may adopt;
|
|
112
|
+
* 3. a port the OS gives us, when the preferred one belongs to somebody else.
|
|
113
|
+
*
|
|
114
|
+
* Step 3 is what stops a second project from attaching to the first project's daemon. That single
|
|
115
|
+
* silent adoption is what made every agent on a machine share one process, one identity and one
|
|
116
|
+
* blast radius.
|
|
117
|
+
*/
|
|
118
|
+
export declare function resolveMcpPort(preferred: number, projectId: string | undefined, home: string, deps: {
|
|
119
|
+
alive: (pid: number) => boolean;
|
|
120
|
+
daemonPresent: (port: number) => Promise<boolean>;
|
|
121
|
+
pickPort: (preferred: number) => Promise<number>;
|
|
122
|
+
}): Promise<number>;
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which daemon does THIS project belong to?
|
|
3
|
+
*
|
|
4
|
+
* The system answered that two incompatible ways. Build plugins asked the registry by projectId, the
|
|
5
|
+
* way `pickDaemonPort` was designed to be asked. The CLI and the MCP proxy asked a NUMBER —
|
|
6
|
+
* `envPort ?? projectPort ?? 4400` — and then attached to whatever owned it, whoever it belonged to.
|
|
7
|
+
*
|
|
8
|
+
* So every project on a machine funnelled into one daemon. Its recorded identity was whichever
|
|
9
|
+
* project won the race to the port, which made the registry entry wrong the moment a second project
|
|
10
|
+
* attached, and made one `kill` on one well-known port a machine-wide outage for every agent at once.
|
|
11
|
+
*
|
|
12
|
+
* It worked anyway, by luck: everyone defaulted to the same number, so everyone found each other.
|
|
13
|
+
* The luck ends the moment a daemon is anywhere but the default — which is exactly when discovery was
|
|
14
|
+
* supposed to help, and exactly when it returned nothing.
|
|
15
|
+
*
|
|
16
|
+
* This module is the one answer both sides now use. It is pure and injectable: no process spawning,
|
|
17
|
+
* no port binding, nothing that needs a real `~/.reticle` to test.
|
|
18
|
+
*/
|
|
19
|
+
import { readdirSync, readFileSync } from 'node:fs';
|
|
20
|
+
import { join } from 'node:path';
|
|
21
|
+
import { DaemonRegistryEntrySchema, daemonRegistryPort, pickDaemonPort, } from '@reticlehq/core';
|
|
22
|
+
/**
|
|
23
|
+
* Every well-formed registry entry in `home`.
|
|
24
|
+
*
|
|
25
|
+
* A corrupt or half-written entry is skipped rather than thrown on: the registry is written by other
|
|
26
|
+
* processes that can be killed mid-write, and one bad file must not make a machine's daemons
|
|
27
|
+
* undiscoverable. Validated through core's schema so a shape change cannot silently pass here.
|
|
28
|
+
*/
|
|
29
|
+
export function readDaemonRegistry(home) {
|
|
30
|
+
let files;
|
|
31
|
+
try {
|
|
32
|
+
files = readdirSync(home);
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
return []; // no ~/.reticle yet: no daemon has ever run here
|
|
36
|
+
}
|
|
37
|
+
const entries = [];
|
|
38
|
+
for (const file of files) {
|
|
39
|
+
if (null === daemonRegistryPort(file))
|
|
40
|
+
continue;
|
|
41
|
+
try {
|
|
42
|
+
const parsed = DaemonRegistryEntrySchema.safeParse(JSON.parse(readFileSync(join(home, file), 'utf8')));
|
|
43
|
+
if (parsed.success)
|
|
44
|
+
entries.push(parsed.data);
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return entries;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* The port of the live daemon serving `projectId`, or undefined when this project has none.
|
|
54
|
+
*
|
|
55
|
+
* Undefined means "start one", never "use the default and hope". That distinction is the fix: the old
|
|
56
|
+
* path had no way to express "no daemon of mine exists", so it fell through to a number and attached
|
|
57
|
+
* to a stranger.
|
|
58
|
+
*
|
|
59
|
+
* The selection rule is core's `pickDaemonPort`, shared with every build plugin, so the daemon side
|
|
60
|
+
* and the app side cannot disagree about which daemon is ours.
|
|
61
|
+
*/
|
|
62
|
+
export function resolveDaemonForProject(projectId, home, alive) {
|
|
63
|
+
return pickDaemonPort(readDaemonRegistry(home), projectId, alive) ?? undefined;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* May `caller` take over a daemon already sitting on the port it wanted?
|
|
67
|
+
*
|
|
68
|
+
* Two deliberate escape hatches, both of which keep an existing install working:
|
|
69
|
+
*
|
|
70
|
+
* A daemon that claims NO project was started somewhere without a `.reticle.json` — the global MCP
|
|
71
|
+
* registration in a directory that is not an app, which is the majority of daemons in the field. It
|
|
72
|
+
* belongs to nobody, so it belongs to whoever asks; refusing would break every user running Reticle
|
|
73
|
+
* globally, which is how it is documented to be installed.
|
|
74
|
+
*
|
|
75
|
+
* A caller with no project of its own has no identity to defend and nothing to be confused about, so
|
|
76
|
+
* it keeps the old permissive behaviour rather than being locked out of a daemon it can use.
|
|
77
|
+
*
|
|
78
|
+
* Everything else — a named daemon and a differently-named caller — is the cross-project bleed, and
|
|
79
|
+
* is the one case this refuses.
|
|
80
|
+
*/
|
|
81
|
+
export function adoptable(daemonProjectId, callerProjectId) {
|
|
82
|
+
if (daemonProjectId === undefined || 0 === daemonProjectId.length)
|
|
83
|
+
return true;
|
|
84
|
+
if (callerProjectId === undefined || 0 === callerProjectId.length)
|
|
85
|
+
return true;
|
|
86
|
+
return daemonProjectId === callerProjectId;
|
|
87
|
+
}
|
|
88
|
+
/** The project a daemon on `port` claims, or undefined when it claims none / is not registered. */
|
|
89
|
+
export function daemonProjectAt(port, home) {
|
|
90
|
+
return readDaemonRegistry(home).find((e) => e.port === port)?.projectId;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Live daemons OTHER than `myPort` that an app for this project has connected to.
|
|
94
|
+
*
|
|
95
|
+
* The split brain this whole module was written to prevent has a second half nobody was reporting.
|
|
96
|
+
* `resolveMcpPort` correctly refuses to adopt a stranger's daemon and relocates to an OS-assigned
|
|
97
|
+
* port — and the APP, which resolves its own port independently (core's `pickDaemonPort` in the
|
|
98
|
+
* build plugin, falling back to the documented default when nothing matches), can land on the
|
|
99
|
+
* stranger. The pairing token is per USER, not per project, so that connect SUCCEEDS: the app is
|
|
100
|
+
* live on one daemon and the agent is attached to a different, empty one.
|
|
101
|
+
*
|
|
102
|
+
* Every individual check is then green. The browser shows an instrumented page, `reticle status`
|
|
103
|
+
* reports a session, the agent reports Reticle registered and enabled — and the agent's tools see
|
|
104
|
+
* nothing, with a no-session diagnosis that tells it to start a dev server that is already running.
|
|
105
|
+
*
|
|
106
|
+
* `connectedOn` is injected rather than imported so this stays free of the session layer (which
|
|
107
|
+
* imports from here) and so the rule is testable without a real `~/.reticle`. Callers pass
|
|
108
|
+
* `hasProjectConnectedBefore` bound to the same projectId.
|
|
109
|
+
*
|
|
110
|
+
* Only LIVE daemons are named: a port a session used yesterday is history, not a split.
|
|
111
|
+
*/
|
|
112
|
+
export function daemonsServingProjectElsewhere(projectId, myPort, home, alive, connectedOn) {
|
|
113
|
+
if (projectId === undefined || 0 === projectId.length)
|
|
114
|
+
return [];
|
|
115
|
+
return readDaemonRegistry(home)
|
|
116
|
+
.filter((entry) => entry.port !== myPort && alive(entry.pid) && connectedOn(entry.port))
|
|
117
|
+
.map((entry) => entry.port)
|
|
118
|
+
.sort((a, b) => a - b);
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* The sentence for a split brain, or `undefined` when there is none.
|
|
122
|
+
*
|
|
123
|
+
* Stated as a fact about two ports rather than a hedge, because unlike the sibling-port observation
|
|
124
|
+
* next door this is not "something is listening somewhere" — it is this project's own connection
|
|
125
|
+
* record naming a daemon that is not the one being asked. The wording says which side is empty,
|
|
126
|
+
* since the failure is invisible precisely because both sides look healthy from where they stand.
|
|
127
|
+
*/
|
|
128
|
+
export function splitBrainNote(myPort, elsewhere) {
|
|
129
|
+
if (0 === elsewhere.length)
|
|
130
|
+
return undefined;
|
|
131
|
+
const listed = elsewhere.map((port) => `:${String(port)}`).join(', ');
|
|
132
|
+
const first = elsewhere[0] ?? myPort;
|
|
133
|
+
return (`this project's app is connected to a DIFFERENT Reticle daemon (${listed}), while this one is ` +
|
|
134
|
+
`on :${String(myPort)} — two daemons for one project, and the agent is attached to the empty ` +
|
|
135
|
+
'half. Nothing is wrong with the app or the wiring. Stop the other daemon with ' +
|
|
136
|
+
`\`reticle stop --port ${String(first)}\` and restart the dev server so the app rediscovers ` +
|
|
137
|
+
'this one, or start the agent and the dev server from the same project directory so both ' +
|
|
138
|
+
'resolve the same daemon.');
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* The other stance on the same split, for a command standing on the port that HAS the app.
|
|
142
|
+
*
|
|
143
|
+
* `splitBrainNote` is asked from the empty daemon and reads the connection record. From the daemon
|
|
144
|
+
* the app is connected to there is nothing missing to notice — sessions are live, every check is
|
|
145
|
+
* green — and the only visible fact is that this project ALSO owns a live daemon somewhere else.
|
|
146
|
+
* One project, two daemons, is a split whichever end you are standing on, and saying it from both
|
|
147
|
+
* ends is the difference between a user inferring it from two green checkmarks and being told.
|
|
148
|
+
*
|
|
149
|
+
* Silent for a caller with no project (it owns no daemon to be split from) and silent when the port
|
|
150
|
+
* being inspected is the project's own.
|
|
151
|
+
*/
|
|
152
|
+
export function wrongDaemonNote(inspectedPort, ownPort) {
|
|
153
|
+
if (ownPort === undefined || ownPort === inspectedPort)
|
|
154
|
+
return undefined;
|
|
155
|
+
return (`this is not this project's own daemon: the registry says it owns a live daemon on ` +
|
|
156
|
+
`:${String(ownPort)} and this command is looking at :${String(inspectedPort)}. Two daemons for ` +
|
|
157
|
+
'one project means the half your agent is attached to and the half your app is connected to can ' +
|
|
158
|
+
'be different processes, with both reporting healthy. Stop one of them and restart the dev ' +
|
|
159
|
+
'server so everything lands on the same port.');
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* The port this agent's MCP proxy should serve, decided BEFORE the proxy starts.
|
|
163
|
+
*
|
|
164
|
+
* Resolved once rather than per reconnect because the proxy's transport is built around a fixed
|
|
165
|
+
* number, and making it chase a moving port would mean reworking the reconnect path — the one piece
|
|
166
|
+
* of this system whose failure mode is "the MCP server disappeared mid-session". The port it settles
|
|
167
|
+
* on is this project's own, so the thing it would be chasing is a daemon it alone is responsible for.
|
|
168
|
+
*
|
|
169
|
+
* The order is the whole fix:
|
|
170
|
+
* 1. our project's live daemon, wherever it is listening — a daemon that moved is still ours;
|
|
171
|
+
* 2. the preferred port, but only if free or holding a daemon we may adopt;
|
|
172
|
+
* 3. a port the OS gives us, when the preferred one belongs to somebody else.
|
|
173
|
+
*
|
|
174
|
+
* Step 3 is what stops a second project from attaching to the first project's daemon. That single
|
|
175
|
+
* silent adoption is what made every agent on a machine share one process, one identity and one
|
|
176
|
+
* blast radius.
|
|
177
|
+
*/
|
|
178
|
+
export async function resolveMcpPort(preferred, projectId, home, deps) {
|
|
179
|
+
const mine = resolveDaemonForProject(projectId, home, deps.alive);
|
|
180
|
+
if (mine !== undefined)
|
|
181
|
+
return mine;
|
|
182
|
+
if (!(await deps.daemonPresent(preferred)))
|
|
183
|
+
return preferred;
|
|
184
|
+
return adoptable(daemonProjectAt(preferred, home), projectId)
|
|
185
|
+
? preferred
|
|
186
|
+
: deps.pickPort(preferred);
|
|
187
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { type DevServerEntry } from '@reticlehq/core';
|
|
2
|
+
/** Injected so the reading is unit-testable without a real home or real processes. */
|
|
3
|
+
export interface DevServerIo {
|
|
4
|
+
readdir: (dir: string) => string[];
|
|
5
|
+
readFile: (path: string) => string;
|
|
6
|
+
isAlive: (pid: number) => boolean;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Every dev server that announced itself and is still running, lowest port first.
|
|
10
|
+
*
|
|
11
|
+
* Fails soft at every step. This runs on the path that explains to a user why setup has not worked,
|
|
12
|
+
* so a half-written file, a hand-edit, or a missing directory must degrade the answer rather than
|
|
13
|
+
* replace the diagnosis with a stack trace.
|
|
14
|
+
*/
|
|
15
|
+
export declare function readDevServers(home: string, io?: DevServerIo): DevServerEntry[];
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reading what dev servers have announced about themselves.
|
|
3
|
+
*
|
|
4
|
+
* The consumer half of `devserver-<port>.json`. A build plugin writes one when its dev server is
|
|
5
|
+
* actually listening; this reads them back, so `init` and `status` can tell three states apart that
|
|
6
|
+
* used to be one:
|
|
7
|
+
*
|
|
8
|
+
* nothing announced no dev server with Reticle loaded is running
|
|
9
|
+
* announced, no page the bundle is wired and no browser dialled — and the entry names the URL
|
|
10
|
+
* announced + session working
|
|
11
|
+
*
|
|
12
|
+
* The middle one is the case that had no message. It was folded into "restart your dev server and
|
|
13
|
+
* load the app", which is two instructions where only the second one applies, addressed to someone
|
|
14
|
+
* who has already done the first.
|
|
15
|
+
*
|
|
16
|
+
* Fs plumbing only — the selection rule is `liveDevServers` in core, shared with anything else that
|
|
17
|
+
* reads this registry, for the same reason `pickDaemonPort` lives there.
|
|
18
|
+
*/
|
|
19
|
+
import { readdirSync, readFileSync } from 'node:fs';
|
|
20
|
+
import { join } from 'node:path';
|
|
21
|
+
import { DevServerEntrySchema, devServerRegistryPort, liveDevServers, } from '@reticlehq/core';
|
|
22
|
+
/** process.kill(pid, 0) throws iff the process is gone — the same probe the daemon registry uses. */
|
|
23
|
+
function isAliveNow(pid) {
|
|
24
|
+
try {
|
|
25
|
+
process.kill(pid, 0);
|
|
26
|
+
return true;
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
const nodeIo = {
|
|
33
|
+
readdir: (dir) => readdirSync(dir),
|
|
34
|
+
readFile: (path) => readFileSync(path, 'utf8'),
|
|
35
|
+
isAlive: isAliveNow,
|
|
36
|
+
};
|
|
37
|
+
/**
|
|
38
|
+
* Every dev server that announced itself and is still running, lowest port first.
|
|
39
|
+
*
|
|
40
|
+
* Fails soft at every step. This runs on the path that explains to a user why setup has not worked,
|
|
41
|
+
* so a half-written file, a hand-edit, or a missing directory must degrade the answer rather than
|
|
42
|
+
* replace the diagnosis with a stack trace.
|
|
43
|
+
*/
|
|
44
|
+
export function readDevServers(home, io = nodeIo) {
|
|
45
|
+
let files;
|
|
46
|
+
try {
|
|
47
|
+
files = io.readdir(home);
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
return []; // No state directory yet — nothing has ever run here.
|
|
51
|
+
}
|
|
52
|
+
const entries = [];
|
|
53
|
+
for (const file of files) {
|
|
54
|
+
// Strict on the filename BEFORE reading: the daemon's own `daemon-<port>.json` is a sibling, and
|
|
55
|
+
// parsing one as a dev server would report the daemon itself as an instrumented app.
|
|
56
|
+
if (null === devServerRegistryPort(file))
|
|
57
|
+
continue;
|
|
58
|
+
try {
|
|
59
|
+
const parsed = DevServerEntrySchema.safeParse(JSON.parse(io.readFile(join(home, file))));
|
|
60
|
+
if (parsed.success)
|
|
61
|
+
entries.push(parsed.data);
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
// Truncated, mid-write, or hand-edited. One bad file is not a reason to report none.
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return liveDevServers(entries, io.isAlive);
|
|
68
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A port this machine will actually let us bind.
|
|
3
|
+
*
|
|
4
|
+
* Once daemons are per-project, "which port" stops being a constant and becomes a question. The
|
|
5
|
+
* answer must not be "the next number up": probing 4401, 4402, 4403 races every other agent doing
|
|
6
|
+
* the same arithmetic at the same moment, and the loser finds out at bind time, which is after it has
|
|
7
|
+
* already told the caller which port to use.
|
|
8
|
+
*
|
|
9
|
+
* So the OS assigns it. Binding port 0 asks the kernel for a free one and hands back the number it
|
|
10
|
+
* chose, and nothing else can be holding it at that instant. There is still a window between closing
|
|
11
|
+
* the probe socket and the daemon binding for real — unavoidable without passing the listener itself
|
|
12
|
+
* to the child — but it is microseconds wide and loses to a retry, where a guessed number loses every
|
|
13
|
+
* time on a busy machine.
|
|
14
|
+
*
|
|
15
|
+
* The preferred port is tried first so the ordinary single-project install keeps landing on the
|
|
16
|
+
* documented default, and every doc, log line and troubleshooting page that says 4400 stays true.
|
|
17
|
+
*/
|
|
18
|
+
/** Can we bind `port` on the loopback interface right now? */
|
|
19
|
+
export declare function portIsFree(port: number, host?: string): Promise<boolean>;
|
|
20
|
+
/** Ask the kernel for a free port. Rejects only if the machine cannot bind loopback at all. */
|
|
21
|
+
export declare function osAssignedPort(host?: string): Promise<number>;
|
|
22
|
+
/**
|
|
23
|
+
* `preferred` when it is free, otherwise whatever the OS gives us.
|
|
24
|
+
*
|
|
25
|
+
* Injectable so the decision is unit-testable without binding anything: the interesting case is the
|
|
26
|
+
* one where the preferred port is taken, and a test must be able to say so without racing a real
|
|
27
|
+
* socket.
|
|
28
|
+
*/
|
|
29
|
+
export declare function pickDaemonPortToBind(preferred: number, isFree?: (port: number) => Promise<boolean>, assign?: () => Promise<number>): Promise<number>;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A port this machine will actually let us bind.
|
|
3
|
+
*
|
|
4
|
+
* Once daemons are per-project, "which port" stops being a constant and becomes a question. The
|
|
5
|
+
* answer must not be "the next number up": probing 4401, 4402, 4403 races every other agent doing
|
|
6
|
+
* the same arithmetic at the same moment, and the loser finds out at bind time, which is after it has
|
|
7
|
+
* already told the caller which port to use.
|
|
8
|
+
*
|
|
9
|
+
* So the OS assigns it. Binding port 0 asks the kernel for a free one and hands back the number it
|
|
10
|
+
* chose, and nothing else can be holding it at that instant. There is still a window between closing
|
|
11
|
+
* the probe socket and the daemon binding for real — unavoidable without passing the listener itself
|
|
12
|
+
* to the child — but it is microseconds wide and loses to a retry, where a guessed number loses every
|
|
13
|
+
* time on a busy machine.
|
|
14
|
+
*
|
|
15
|
+
* The preferred port is tried first so the ordinary single-project install keeps landing on the
|
|
16
|
+
* documented default, and every doc, log line and troubleshooting page that says 4400 stays true.
|
|
17
|
+
*/
|
|
18
|
+
import { createServer } from 'node:net';
|
|
19
|
+
import { LOOPBACK_HOST } from '@reticlehq/core';
|
|
20
|
+
/** Can we bind `port` on the loopback interface right now? */
|
|
21
|
+
export function portIsFree(port, host = LOOPBACK_HOST) {
|
|
22
|
+
return new Promise((resolve) => {
|
|
23
|
+
const probe = createServer();
|
|
24
|
+
probe.once('error', () => resolve(false));
|
|
25
|
+
probe.listen(port, host, () => {
|
|
26
|
+
probe.close(() => resolve(true));
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
/** Ask the kernel for a free port. Rejects only if the machine cannot bind loopback at all. */
|
|
31
|
+
export function osAssignedPort(host = LOOPBACK_HOST) {
|
|
32
|
+
return new Promise((resolve, reject) => {
|
|
33
|
+
const probe = createServer();
|
|
34
|
+
probe.once('error', reject);
|
|
35
|
+
probe.listen(0, host, () => {
|
|
36
|
+
const address = probe.address();
|
|
37
|
+
if (null === address || 'string' === typeof address) {
|
|
38
|
+
probe.close(() => reject(new Error('could not read an OS-assigned port')));
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
const { port } = address;
|
|
42
|
+
probe.close(() => resolve(port));
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* `preferred` when it is free, otherwise whatever the OS gives us.
|
|
48
|
+
*
|
|
49
|
+
* Injectable so the decision is unit-testable without binding anything: the interesting case is the
|
|
50
|
+
* one where the preferred port is taken, and a test must be able to say so without racing a real
|
|
51
|
+
* socket.
|
|
52
|
+
*/
|
|
53
|
+
export async function pickDaemonPortToBind(preferred, isFree = portIsFree, assign = osAssignedPort) {
|
|
54
|
+
return (await isFree(preferred)) ? preferred : assign();
|
|
55
|
+
}
|
|
@@ -19,10 +19,6 @@
|
|
|
19
19
|
*/
|
|
20
20
|
/** The event name a beat carries. One `grep` separates liveness from everything else in the file. */
|
|
21
21
|
export declare const DAEMON_HEARTBEAT_EVENT = "reticle_daemon_alive";
|
|
22
|
-
/** Written by installExitTrace when the process exits through Node. */
|
|
23
|
-
export declare const DAEMON_EXIT_EVENT = "reticle_daemon_exiting";
|
|
24
|
-
/** Written by installExitTrace when a signal arrives, which is a CAUSE the exit line does not carry. */
|
|
25
|
-
export declare const DAEMON_SIGNAL_EVENT = "reticle_daemon_signalled";
|
|
26
22
|
/**
|
|
27
23
|
* How often a live daemon says so.
|
|
28
24
|
*
|
package/dist/daemon/heartbeat.js
CHANGED
|
@@ -20,9 +20,9 @@
|
|
|
20
20
|
/** The event name a beat carries. One `grep` separates liveness from everything else in the file. */
|
|
21
21
|
export const DAEMON_HEARTBEAT_EVENT = 'reticle_daemon_alive';
|
|
22
22
|
/** Written by installExitTrace when the process exits through Node. */
|
|
23
|
-
|
|
23
|
+
const DAEMON_EXIT_EVENT = 'reticle_daemon_exiting';
|
|
24
24
|
/** Written by installExitTrace when a signal arrives, which is a CAUSE the exit line does not carry. */
|
|
25
|
-
|
|
25
|
+
const DAEMON_SIGNAL_EVENT = 'reticle_daemon_signalled';
|
|
26
26
|
/**
|
|
27
27
|
* How often a live daemon says so.
|
|
28
28
|
*
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a woken MCP proxy may do with whatever is sitting on its port.
|
|
3
|
+
*
|
|
4
|
+
* Pulled out as a pure function because the wake path lives inside a long side-effecting closure in
|
|
5
|
+
* `mcp-command.ts`, and the decision it was getting wrong is three lines of reasoning that deserve a
|
|
6
|
+
* test of their own.
|
|
7
|
+
*
|
|
8
|
+
* `resolveMcpPort` asks the identity question at BOOT and relocates rather than adopting a stranger.
|
|
9
|
+
* The wake path asked only whether a daemon answered, so after an idle exit freed the port and
|
|
10
|
+
* another project's daemon bound it, a dormant proxy could reattach to a daemon that was not its
|
|
11
|
+
* own — and then serve an agent verdicts about a different application. `adoptable` is the same rule
|
|
12
|
+
* boot uses; this is just the wake path finally asking it.
|
|
13
|
+
*/
|
|
14
|
+
import { PortPresence } from './port-presence.js';
|
|
15
|
+
export declare const WakeAction: {
|
|
16
|
+
/** A daemon we may use is already here. */
|
|
17
|
+
readonly USE: "use";
|
|
18
|
+
/** Nothing is listening: start one. */
|
|
19
|
+
readonly SPAWN: "spawn";
|
|
20
|
+
/** Somebody else's. Go dormant with a reason rather than pretend the wake succeeded. */
|
|
21
|
+
readonly REFUSE: "refuse";
|
|
22
|
+
};
|
|
23
|
+
export type WakeAction = (typeof WakeAction)[keyof typeof WakeAction];
|
|
24
|
+
/**
|
|
25
|
+
* Identity is asked only of a port that actually holds a daemon.
|
|
26
|
+
*
|
|
27
|
+
* A FREE port must stay spawnable whatever the registry says about it, because a stale entry is the
|
|
28
|
+
* registry's ordinary state for the moment after any daemon exits — and refusing there would turn
|
|
29
|
+
* every idle retirement into a permanent lockout, which is the failure this whole path exists to
|
|
30
|
+
* prevent.
|
|
31
|
+
*/
|
|
32
|
+
export declare function decideWake(presence: PortPresence, daemonProjectId: string | undefined, callerProjectId: string | undefined): WakeAction;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a woken MCP proxy may do with whatever is sitting on its port.
|
|
3
|
+
*
|
|
4
|
+
* Pulled out as a pure function because the wake path lives inside a long side-effecting closure in
|
|
5
|
+
* `mcp-command.ts`, and the decision it was getting wrong is three lines of reasoning that deserve a
|
|
6
|
+
* test of their own.
|
|
7
|
+
*
|
|
8
|
+
* `resolveMcpPort` asks the identity question at BOOT and relocates rather than adopting a stranger.
|
|
9
|
+
* The wake path asked only whether a daemon answered, so after an idle exit freed the port and
|
|
10
|
+
* another project's daemon bound it, a dormant proxy could reattach to a daemon that was not its
|
|
11
|
+
* own — and then serve an agent verdicts about a different application. `adoptable` is the same rule
|
|
12
|
+
* boot uses; this is just the wake path finally asking it.
|
|
13
|
+
*/
|
|
14
|
+
import { PortPresence } from './port-presence.js';
|
|
15
|
+
import { adoptable } from './daemon-resolve.js';
|
|
16
|
+
export const WakeAction = {
|
|
17
|
+
/** A daemon we may use is already here. */
|
|
18
|
+
USE: 'use',
|
|
19
|
+
/** Nothing is listening: start one. */
|
|
20
|
+
SPAWN: 'spawn',
|
|
21
|
+
/** Somebody else's. Go dormant with a reason rather than pretend the wake succeeded. */
|
|
22
|
+
REFUSE: 'refuse',
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* Identity is asked only of a port that actually holds a daemon.
|
|
26
|
+
*
|
|
27
|
+
* A FREE port must stay spawnable whatever the registry says about it, because a stale entry is the
|
|
28
|
+
* registry's ordinary state for the moment after any daemon exits — and refusing there would turn
|
|
29
|
+
* every idle retirement into a permanent lockout, which is the failure this whole path exists to
|
|
30
|
+
* prevent.
|
|
31
|
+
*/
|
|
32
|
+
export function decideWake(presence, daemonProjectId, callerProjectId) {
|
|
33
|
+
if (PortPresence.FREE === presence)
|
|
34
|
+
return WakeAction.SPAWN;
|
|
35
|
+
if (PortPresence.DAEMON !== presence)
|
|
36
|
+
return WakeAction.REFUSE;
|
|
37
|
+
return adoptable(daemonProjectId, callerProjectId) ? WakeAction.USE : WakeAction.REFUSE;
|
|
38
|
+
}
|
|
@@ -27,8 +27,6 @@ export interface IssueState {
|
|
|
27
27
|
state: 'open' | 'closed';
|
|
28
28
|
labels: readonly string[];
|
|
29
29
|
}
|
|
30
|
-
/** The label that means "fixed, waiting for a release" — an honest reason to still be open. */
|
|
31
|
-
export declare const PENDING_RELEASE_LABEL = "fixed-pending-release";
|
|
32
30
|
export declare function closedRefsIn(commitMessage: string): number[];
|
|
33
31
|
/**
|
|
34
32
|
* Of the issues these commits claimed to close, the ones still reading as available work.
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
* below testable without a network or a repo.
|
|
24
24
|
*/
|
|
25
25
|
/** The label that means "fixed, waiting for a release" — an honest reason to still be open. */
|
|
26
|
-
|
|
26
|
+
const PENDING_RELEASE_LABEL = 'fixed-pending-release';
|
|
27
27
|
/**
|
|
28
28
|
* Issue numbers a commit message CLAIMS to have closed.
|
|
29
29
|
*
|
|
@@ -2,6 +2,7 @@ import { z } from 'zod';
|
|
|
2
2
|
import { sessionRoot } from '../project/session-root.js';
|
|
3
3
|
import { asNumber, asString } from '../tools/tools-helpers.js';
|
|
4
4
|
import { ReticleTool } from '../tools/tool-names.js';
|
|
5
|
+
import { countSchema } from '../tools/numeric-bounds.js';
|
|
5
6
|
import { readContract } from '../project/reticle-dir.js';
|
|
6
7
|
import { buildDomainModel } from './domain-model.js';
|
|
7
8
|
import { proposeInstrumentation } from '../oracles/self-instrument.js';
|
|
@@ -19,8 +20,7 @@ export const DOMAIN_TOOLS = [
|
|
|
19
20
|
name: ReticleTool.DOMAIN,
|
|
20
21
|
description: 'Read the app domain model BEFORE testing: every saved flow with its assertion grade, the consequence that MUST hold for it (mustHold = what it actually tests), the anchors/signals it exercises, plus GAPS — declared signals/testids that NO flow asserts (untested intent), and flows that assert no observable consequence. Use this to decide what to test and where the real risk is, instead of crawling the whole app. Reads .reticle/flows/ + .reticle/contract.json (no browser needed).',
|
|
21
22
|
inputSchema: {
|
|
22
|
-
limit:
|
|
23
|
-
.number()
|
|
23
|
+
limit: countSchema
|
|
24
24
|
.optional()
|
|
25
25
|
.describe('Most-recent N flows to detail. Defaults to 25 — the per-flow list grows with every saved flow (measured at ~11KB / ~2,700 tokens at 22 flows) and a large project would spend most of an agent context on flows it is not asking about. `flowCount` is always the true total, and the gaps/coverage summary is computed over ALL flows regardless, so capping the detail never changes the analysis.'),
|
|
26
26
|
},
|