@reticlehq/server 2.4.0 → 2.6.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 +719 -0
- package/dist/bridge/auth-failure-reason.d.ts +19 -0
- package/dist/bridge/auth-failure-reason.js +29 -0
- package/dist/{bridge-security.d.ts → bridge/bridge-security.d.ts} +1 -1
- package/dist/{bridge-security.js → bridge/bridge-security.js} +1 -1
- package/dist/{bridge.d.ts → bridge/bridge.d.ts} +13 -1
- package/dist/{bridge.js → bridge/bridge.js} +95 -15
- package/dist/{bridge.test-harness.d.ts → bridge/bridge.test-harness.d.ts} +9 -11
- package/dist/{bridge.test-harness.js → bridge/bridge.test-harness.js} +18 -10
- package/dist/{pairing-token.js → bridge/pairing-token.js} +1 -1
- package/dist/capsule/capsule-store.d.ts +8 -3
- package/dist/capsule/capsule-store.js +7 -2
- package/dist/capsule/capsule.js +3 -3
- package/dist/capsule/causal-summary.d.ts +8 -0
- package/dist/capsule/causal-summary.js +22 -12
- package/dist/capsule/divergence.d.ts +9 -4
- package/dist/capsule/divergence.js +8 -8
- package/dist/capsule/predicate-to-links.js +11 -8
- package/dist/cli/cli-doctor.d.ts +1 -0
- package/dist/cli/cli-doctor.js +135 -0
- package/dist/{cli-flow-commands.d.ts → cli/cli-flow-commands.d.ts} +2 -2
- package/dist/{cli-flow-commands.js → cli/cli-flow-commands.js} +15 -15
- package/dist/{cli-launch.d.ts → cli/cli-launch.d.ts} +32 -3
- package/dist/cli/cli-launch.js +171 -0
- package/dist/cli/cli-parse-feedback.d.ts +31 -0
- package/dist/cli/cli-parse-feedback.js +56 -0
- package/dist/{cli-parse.d.ts → cli/cli-parse.d.ts} +6 -19
- package/dist/{cli-parse.js → cli/cli-parse.js} +118 -81
- package/dist/{cli-port.d.ts → cli/cli-port.d.ts} +12 -0
- package/dist/{cli-port.js → cli/cli-port.js} +9 -9
- package/dist/cli/cli-update-commands.d.ts +4 -0
- package/dist/cli/cli-update-commands.js +93 -0
- package/dist/{cli-verify.js → cli/cli-verify.js} +14 -14
- package/dist/{cloud-cli.js → cli/cloud-cli.js} +29 -24
- package/dist/cli/doctor-daemon-line.d.ts +37 -0
- package/dist/cli/doctor-daemon-line.js +35 -0
- package/dist/cli/port-holder.d.ts +43 -0
- package/dist/cli/port-holder.js +99 -0
- package/dist/cli.d.ts +2 -2
- package/dist/cli.js +302 -100
- package/dist/cloud/cloud-config.d.ts +0 -1
- package/dist/cloud/cloud-config.js +9 -9
- package/dist/cloud/cloud-sync.d.ts +1 -7
- package/dist/cloud/cloud-sync.js +12 -12
- package/dist/crawl/crawl-empty.d.ts +16 -0
- package/dist/crawl/crawl-empty.js +24 -0
- package/dist/crawl/crawl-tools.js +4 -1
- package/dist/crawl/crawl.js +26 -7
- package/dist/daemon/daemon-resilience.d.ts +73 -0
- package/dist/daemon/daemon-resilience.js +279 -0
- package/dist/daemon/daemon-usefulness.d.ts +61 -0
- package/dist/daemon/daemon-usefulness.js +62 -0
- package/dist/{daemon.d.ts → daemon/daemon.d.ts} +33 -8
- package/dist/{daemon.js → daemon/daemon.js} +105 -48
- package/dist/daemon/heartbeat.d.ts +115 -0
- package/dist/daemon/heartbeat.js +141 -0
- package/dist/daemon/idle-grace.d.ts +32 -0
- package/dist/daemon/idle-grace.js +41 -0
- package/dist/{idle-shutdown.d.ts → daemon/idle-shutdown.d.ts} +23 -2
- package/dist/{idle-shutdown.js → daemon/idle-shutdown.js} +37 -6
- package/dist/daemon/port-presence.d.ts +76 -0
- package/dist/daemon/port-presence.js +93 -0
- package/dist/daemon/state-dir.d.ts +7 -0
- package/dist/daemon/state-dir.js +59 -0
- package/dist/daemon/unclean-predecessor.d.ts +23 -0
- package/dist/daemon/unclean-predecessor.js +27 -0
- package/dist/domain/domain-model.js +2 -2
- package/dist/events/body-failures.js +8 -8
- package/dist/events/contradictions.d.ts +1 -1
- package/dist/events/contradictions.js +60 -10
- package/dist/events/echo-mismatch.js +10 -10
- package/dist/events/event-filters.js +6 -6
- package/dist/events/json-salvage.d.ts +2 -1
- package/dist/events/json-salvage.js +6 -6
- package/dist/events/observed-in-window.d.ts +12 -0
- package/dist/events/observed-in-window.js +21 -0
- package/dist/events/predicate-eval.d.ts +53 -20
- package/dist/events/predicate-eval.js +179 -55
- package/dist/events/predicate-parse.d.ts +23 -0
- package/dist/events/predicate-parse.js +54 -0
- package/dist/events/predicate.d.ts +7 -2
- package/dist/events/predicate.js +126 -35
- package/dist/events/reconcile.js +5 -5
- package/dist/events/ring-buffer.js +2 -17
- package/dist/events/stale-response.js +6 -6
- package/dist/events/testid-near-miss.d.ts +22 -0
- package/dist/events/testid-near-miss.js +33 -0
- package/dist/events/unit-mismatch.js +8 -8
- package/dist/flows/affected-tools.js +19 -4
- package/dist/flows/affected.js +1 -1
- package/dist/flows/annotate-target.d.ts +1 -0
- package/dist/flows/annotate-target.js +20 -0
- package/dist/flows/annotate-tools.js +25 -3
- package/dist/flows/annotate.js +17 -4
- package/dist/flows/assertion-integrity.d.ts +2 -1
- package/dist/flows/assertion-tiers-store.d.ts +3 -66
- package/dist/flows/assertion-tiers-store.js +2 -2
- package/dist/flows/attributed-failure.d.ts +17 -0
- package/dist/flows/attributed-failure.js +20 -0
- package/dist/flows/buddy-status.d.ts +2 -1
- package/dist/flows/buddy-status.js +2 -2
- package/dist/flows/coverage.d.ts +3 -2
- package/dist/flows/coverage.js +2 -2
- package/dist/flows/decision.js +24 -9
- package/dist/flows/empty-flow.d.ts +6 -0
- package/dist/flows/empty-flow.js +24 -0
- package/dist/flows/flake-store.d.ts +0 -1
- package/dist/flows/flake-store.js +1 -1
- package/dist/flows/flake.d.ts +2 -1
- package/dist/flows/flake.js +2 -2
- package/dist/flows/flow-classify.js +12 -0
- package/dist/flows/flow-replay-run.js +4 -4
- package/dist/flows/flow-replay.d.ts +6 -8
- package/dist/flows/flow-replay.js +132 -48
- package/dist/flows/flow-report.js +1 -1
- package/dist/flows/flow-scope.d.ts +1 -6
- package/dist/flows/flow-step-runners.d.ts +26 -6
- package/dist/flows/flow-step-runners.js +156 -6
- package/dist/flows/flow-success.js +20 -15
- package/dist/flows/flow-tools.js +63 -21
- package/dist/flows/flows.js +15 -11
- package/dist/flows/gate.d.ts +4 -3
- package/dist/flows/gate.js +1 -1
- package/dist/flows/heal-run.d.ts +0 -8
- package/dist/flows/heal-run.js +5 -5
- package/dist/flows/heal.js +3 -3
- package/dist/flows/predicate-to-expect.d.ts +38 -0
- package/dist/flows/predicate-to-expect.js +95 -0
- package/dist/flows/replay.d.ts +16 -1
- package/dist/flows/replay.js +58 -22
- package/dist/flows/role-anchor-nearest.d.ts +6 -0
- package/dist/flows/role-anchor-nearest.js +45 -0
- package/dist/flows/role-drift-reason.d.ts +20 -0
- package/dist/flows/role-drift-reason.js +39 -0
- package/dist/flows/server-verify.d.ts +1 -1
- package/dist/flows/server-verify.js +5 -5
- package/dist/flows/suite-flakes.d.ts +33 -0
- package/dist/flows/suite-flakes.js +32 -0
- package/dist/flows/verify-change-tools.js +32 -10
- package/dist/flows/verify-change-verdict.d.ts +15 -0
- package/dist/flows/verify-change-verdict.js +25 -0
- package/dist/flows/watch-batcher.d.ts +3 -2
- package/dist/honesty/already-true.d.ts +2 -0
- package/dist/honesty/already-true.js +31 -0
- package/dist/honesty/blind-spots.d.ts +35 -3
- package/dist/honesty/blind-spots.js +57 -12
- package/dist/honesty/honesty.d.ts +3 -2
- package/dist/honesty/honesty.js +4 -4
- package/dist/honesty/uncaptured-bodies.js +1 -1
- package/dist/honesty/unread-outcome.js +2 -2
- package/dist/honesty/verified.d.ts +31 -3
- package/dist/honesty/verified.js +64 -8
- package/dist/http-server.js +23 -7
- package/dist/hunt/hunt-report.d.ts +2 -1
- package/dist/hunt/hunt-report.js +2 -2
- package/dist/index.d.ts +9 -6
- package/dist/index.js +41 -25
- package/dist/init/agent-rules.d.ts +20 -1
- package/dist/init/agent-rules.js +64 -8
- package/dist/init/app-choice.d.ts +20 -0
- package/dist/init/app-choice.js +25 -0
- package/dist/init/astro-host.d.ts +21 -0
- package/dist/init/astro-host.js +27 -0
- package/dist/init/astro-patch.d.ts +16 -0
- package/dist/init/astro-patch.js +240 -0
- package/dist/init/capabilities.js +4 -1
- package/dist/init/cra.d.ts +50 -0
- package/dist/init/cra.js +106 -0
- package/dist/init/cursor.js +39 -4
- package/dist/init/desktop-doctor.d.ts +3 -2
- package/dist/init/desktop-doctor.js +4 -4
- package/dist/init/detect.d.ts +9 -2
- package/dist/init/detect.js +24 -5
- package/dist/init/mcp-clients.d.ts +81 -0
- package/dist/init/mcp-clients.js +248 -0
- package/dist/init/mcp-registered.d.ts +12 -0
- package/dist/init/mcp-registered.js +14 -0
- package/dist/init/mcp.d.ts +27 -2
- package/dist/init/mcp.js +40 -4
- package/dist/init/next-patch.d.ts +2 -4
- package/dist/init/next-patch.js +109 -19
- package/dist/init/node-io.js +29 -6
- package/dist/init/plan-framework.d.ts +16 -1
- package/dist/init/plan-framework.js +166 -8
- package/dist/init/plan.d.ts +85 -6
- package/dist/init/plan.js +196 -33
- package/dist/init/project-id.d.ts +5 -9
- package/dist/init/project-id.js +10 -22
- package/dist/init/run.d.ts +23 -1
- package/dist/init/run.js +338 -47
- package/dist/init/slash-command.d.ts +9 -0
- package/dist/init/slash-command.js +10 -1
- package/dist/init/snippets.d.ts +7 -10
- package/dist/init/snippets.js +44 -15
- package/dist/init/vite-config.js +1 -1
- package/dist/init/windows-quote.d.ts +38 -0
- package/dist/init/windows-quote.js +70 -0
- package/dist/init/workspace-apps.d.ts +23 -0
- package/dist/init/workspace-apps.js +68 -0
- package/dist/input/driven-redaction.js +1 -1
- package/dist/input/network-detail.js +8 -8
- package/dist/input/network-mock-tools.js +2 -2
- package/dist/input/network-mock.js +4 -4
- package/dist/input/real-input.js +8 -3
- package/dist/input/scroll-find.js +2 -2
- package/dist/input/scroll-tools.d.ts +0 -5
- package/dist/input/scroll-tools.js +11 -4
- package/dist/input/viewport-tools.js +2 -2
- package/dist/journal/ambient-store.d.ts +0 -2
- package/dist/journal/ambient-store.js +1 -1
- package/dist/journal/ambient.js +1 -1
- package/dist/journal/attach-journal.d.ts +2 -1
- package/dist/journal/deviation-report.js +6 -6
- package/dist/journal/deviation-service.js +1 -1
- package/dist/journal/envelope-store.d.ts +0 -2
- package/dist/journal/envelope-store.js +1 -1
- package/dist/journal/envelope.d.ts +2 -3
- package/dist/journal/envelope.js +3 -3
- package/dist/journal/journal-query.js +1 -1
- package/dist/journal/journal-recorder.d.ts +2 -1
- package/dist/journal/journal-recorder.js +2 -2
- package/dist/journal/rollups.js +9 -9
- package/dist/journal/session-end.d.ts +2 -1
- package/dist/journal/session-journal.js +4 -4
- package/dist/license/license.d.ts +0 -14
- package/dist/license/license.js +8 -8
- package/dist/log.d.ts +10 -0
- package/dist/log.js +11 -1
- package/dist/mcp/mcp-is-error.d.ts +13 -0
- package/dist/mcp/mcp-is-error.js +18 -0
- package/dist/mcp/mcp-outage.d.ts +32 -0
- package/dist/mcp/mcp-outage.js +55 -0
- package/dist/mcp/mcp-proxy.d.ts +134 -0
- package/dist/mcp/mcp-proxy.js +720 -0
- package/dist/{mcp.d.ts → mcp/mcp.d.ts} +13 -10
- package/dist/{mcp.js → mcp/mcp.js} +139 -37
- package/dist/mcp/proxy-handshake.d.ts +3 -0
- package/dist/mcp/proxy-handshake.js +54 -0
- package/dist/mcp/proxy-lifecycle.d.ts +64 -0
- package/dist/mcp/proxy-lifecycle.js +70 -0
- package/dist/mcp/tool-catalog-cache.d.ts +29 -0
- package/dist/mcp/tool-catalog-cache.js +55 -0
- package/dist/oracles/propose-consequences.d.ts +2 -1
- package/dist/oracles/propose-consequences.js +16 -11
- package/dist/oracles/self-instrument.d.ts +2 -1
- package/dist/oracles/self-instrument.js +1 -1
- package/dist/phenomena/phenomena.js +3 -3
- package/dist/platform.d.ts +16 -0
- package/dist/platform.js +16 -0
- package/dist/pool/browser-pool.d.ts +9 -0
- package/dist/pool/browser-pool.js +38 -9
- package/dist/pool/playwright-launcher.d.ts +15 -2
- package/dist/pool/playwright-launcher.js +21 -2
- package/dist/project/fs-port.js +1 -1
- package/dist/project/project-tools.js +2 -2
- package/dist/runs/build-verification-run.js +1 -1
- package/dist/runs/repair-prompt.d.ts +0 -6
- package/dist/runs/repair-prompt.js +3 -3
- package/dist/runs/risk-classify.js +1 -1
- package/dist/runs/run-diff.d.ts +0 -2
- package/dist/runs/run-diff.js +3 -3
- package/dist/runs/run-tools.js +2 -2
- package/dist/runs/verification-sync.js +2 -2
- package/dist/runs/verify-http.js +1 -1
- package/dist/runs/verify-server.js +1 -1
- package/dist/session/attachment-history.d.ts +46 -0
- package/dist/session/attachment-history.js +65 -0
- package/dist/session/command-timeout.d.ts +11 -3
- package/dist/session/command-timeout.js +2 -2
- package/dist/session/control-envelope.d.ts +6 -0
- package/dist/session/control-envelope.js +7 -1
- package/dist/session/dev-server-probe.d.ts +13 -0
- package/dist/session/dev-server-probe.js +52 -0
- package/dist/session/last-act.d.ts +17 -3
- package/dist/session/last-act.js +16 -4
- package/dist/session/live-control-tools.js +7 -2
- package/dist/session/live-control.js +1 -1
- package/dist/session/looks-like-dev-server.d.ts +14 -0
- package/dist/session/looks-like-dev-server.js +25 -0
- package/dist/session/no-session-diagnosis.d.ts +45 -0
- package/dist/session/no-session-diagnosis.js +120 -0
- package/dist/session/no-session-watch.d.ts +21 -0
- package/dist/session/no-session-watch.js +69 -0
- package/dist/session/observed-state.d.ts +4 -0
- package/dist/session/observed-state.js +19 -1
- package/dist/session/output-budget.js +1 -1
- package/dist/session/pending-commands.js +1 -0
- package/dist/session/session-health.d.ts +8 -1
- package/dist/session/session-health.js +8 -7
- package/dist/session/session-info.d.ts +43 -8
- package/dist/session/session-info.js +46 -1
- package/dist/session/session-lease.d.ts +0 -11
- package/dist/session/session-lease.js +1 -1
- package/dist/session/session-manager.d.ts +6 -0
- package/dist/session/session-manager.js +124 -12
- package/dist/session/session-readiness.d.ts +1 -1
- package/dist/session/session-readiness.js +6 -1
- package/dist/session/session-reaper.js +1 -1
- package/dist/session/session-reconnect.d.ts +32 -0
- package/dist/session/session-reconnect.js +42 -0
- package/dist/session/session.d.ts +15 -2
- package/dist/session/session.js +77 -26
- package/dist/session/state-select.d.ts +1 -1
- package/dist/session/state-select.js +1 -1
- package/dist/status-payload.d.ts +19 -0
- package/dist/status-payload.js +11 -0
- package/dist/telemetry/argument-shape.js +10 -1
- package/dist/telemetry/browser-mode.d.ts +27 -0
- package/dist/telemetry/browser-mode.js +33 -0
- package/dist/telemetry/bug-found.d.ts +17 -21
- package/dist/telemetry/bug-found.js +120 -8
- package/dist/telemetry/cli-telemetry.d.ts +2 -12
- package/dist/telemetry/cli-telemetry.js +27 -4
- package/dist/telemetry/crash-cause.d.ts +47 -0
- package/dist/telemetry/crash-cause.js +104 -0
- package/dist/telemetry/daemon-telemetry.d.ts +38 -1
- package/dist/telemetry/daemon-telemetry.js +37 -12
- package/dist/telemetry/dev-repo.js +4 -4
- package/dist/telemetry/error-class.d.ts +26 -0
- package/dist/telemetry/error-class.js +52 -0
- package/dist/telemetry/error-fingerprint.js +21 -2
- package/dist/telemetry/feedback-cli.d.ts +8 -3
- package/dist/telemetry/feedback-cli.js +52 -8
- package/dist/telemetry/feedback-context.d.ts +23 -6
- package/dist/telemetry/feedback-context.js +105 -5
- package/dist/telemetry/feedback-delivery.d.ts +20 -0
- package/dist/telemetry/feedback-delivery.js +30 -0
- package/dist/telemetry/feedback-outbox.d.ts +22 -0
- package/dist/telemetry/feedback-outbox.js +87 -0
- package/dist/telemetry/feedback.d.ts +22 -1
- package/dist/telemetry/feedback.js +67 -8
- package/dist/telemetry/git-facts.d.ts +7 -0
- package/dist/telemetry/git-facts.js +5 -5
- package/dist/telemetry/project-profile.js +2 -1
- package/dist/telemetry/session-metrics.d.ts +47 -3
- package/dist/telemetry/session-metrics.js +315 -15
- package/dist/telemetry/telemetry.d.ts +45 -2
- package/dist/telemetry/telemetry.js +150 -37
- package/dist/telemetry/verification-of.d.ts +22 -0
- package/dist/telemetry/verification-of.js +84 -0
- package/dist/tools/act-capsule.js +2 -2
- package/dist/tools/act-danger.d.ts +12 -0
- package/dist/tools/act-danger.js +20 -3
- package/dist/tools/act-helpers.js +4 -4
- package/dist/tools/act-tools.d.ts +0 -8
- package/dist/tools/act-tools.js +114 -118
- package/dist/tools/act-view.js +7 -7
- package/dist/tools/alias-args.d.ts +17 -0
- package/dist/tools/alias-args.js +25 -0
- package/dist/tools/assert-grade.js +9 -9
- package/dist/tools/assert-verdict.d.ts +9 -1
- package/dist/tools/assert-verdict.js +21 -6
- package/dist/tools/browser-tools.js +36 -11
- package/dist/tools/contract-tools.d.ts +0 -5
- package/dist/tools/contract-tools.js +27 -1
- package/dist/tools/coverage-identity.d.ts +34 -0
- package/dist/tools/coverage-identity.js +46 -0
- package/dist/tools/coverage-tools.js +10 -14
- package/dist/tools/dynamic-tools.d.ts +2 -1
- package/dist/tools/dynamic-tools.js +96 -19
- package/dist/tools/error-recovery.d.ts +34 -2
- package/dist/tools/error-recovery.js +255 -4
- package/dist/tools/feedback-invite.d.ts +48 -0
- package/dist/tools/feedback-invite.js +71 -0
- package/dist/tools/feedback-tools.d.ts +12 -1
- package/dist/tools/feedback-tools.js +28 -5
- package/dist/tools/invoke-tool.d.ts +0 -12
- package/dist/tools/invoke-tool.js +126 -33
- package/dist/tools/lease-hint.d.ts +17 -0
- package/dist/tools/lease-hint.js +24 -0
- package/dist/tools/lease-tools.js +6 -5
- package/dist/tools/merge-tools.d.ts +10 -1
- package/dist/tools/merge-tools.js +9 -3
- package/dist/tools/merged-name-redirect.d.ts +13 -0
- package/dist/tools/merged-name-redirect.js +55 -0
- package/dist/tools/navigate-arrival.d.ts +26 -0
- package/dist/tools/navigate-arrival.js +60 -0
- package/dist/tools/navigate-result.d.ts +31 -0
- package/dist/tools/navigate-result.js +38 -0
- package/dist/tools/observe-tools.js +72 -20
- package/dist/tools/observed-nothing.d.ts +25 -0
- package/dist/tools/observed-nothing.js +33 -0
- package/dist/tools/query-paginate.js +2 -2
- package/dist/tools/query-shape.d.ts +18 -0
- package/dist/tools/query-shape.js +39 -0
- package/dist/tools/read-tools.js +81 -20
- package/dist/tools/real-input-attempt.d.ts +28 -0
- package/dist/tools/real-input-attempt.js +93 -0
- package/dist/tools/reconcile-tools.js +7 -7
- package/dist/tools/reload-result.d.ts +14 -0
- package/dist/tools/reload-result.js +29 -0
- package/dist/tools/snapshot-delta.d.ts +4 -5
- package/dist/tools/snapshot-delta.js +0 -0
- package/dist/tools/tool-kit.d.ts +16 -0
- package/dist/tools/tool-surface.d.ts +83 -0
- package/dist/tools/tool-surface.js +217 -0
- package/dist/tools/tools-helpers.js +6 -6
- package/dist/tools/tools.d.ts +15 -0
- package/dist/tools/tools.js +53 -17
- package/dist/tools/unadvertised-help.d.ts +5 -0
- package/dist/tools/unadvertised-help.js +39 -0
- package/dist/trace.d.ts +69 -0
- package/dist/trace.js +150 -0
- package/dist/update/nudge-credit.d.ts +4 -0
- package/dist/update/nudge-credit.js +53 -0
- package/dist/update/sdk-sync.d.ts +21 -0
- package/dist/update/sdk-sync.js +42 -0
- package/dist/update/update-checker.js +3 -3
- package/dist/update/update-nudge.d.ts +63 -6
- package/dist/update/update-nudge.js +128 -15
- package/dist/update/updater.d.ts +1 -9
- package/dist/update/updater.js +13 -8
- package/dist/version/peer-announce.d.ts +4 -0
- package/dist/version/peer-announce.js +30 -0
- package/dist/{server-version.js → version/server-version.js} +5 -1
- package/dist/version/version-nudge.d.ts +28 -0
- package/dist/version/version-nudge.js +40 -0
- package/dist/version/version-skew.d.ts +66 -0
- package/dist/version/version-skew.js +71 -0
- package/dist/visual/visual-diff.js +2 -2
- package/dist/visual/visual-tools.js +4 -4
- package/docs/README.md +55 -0
- package/docs/agent-cheatsheet.md +85 -0
- package/docs/architecture.md +113 -0
- package/docs/benchmarks.md +255 -0
- package/docs/debugging.md +102 -0
- package/docs/deploy-checks.md +69 -0
- package/docs/desktop-apps.md +256 -0
- package/docs/enterprise.md +62 -0
- package/docs/fixtures-dispatch-receiver.yml +76 -0
- package/docs/fixtures.md +45 -0
- package/docs/flows.md +166 -0
- package/docs/gate-plan.md +98 -0
- package/docs/gates.md +117 -0
- package/docs/getting-started.md +469 -0
- package/docs/human-control.md +63 -0
- package/docs/integration-patterns.md +171 -0
- package/docs/local-registry.md +126 -0
- package/docs/matrix/2.5.0/claude-code-machine.json +19 -0
- package/docs/matrix/2.5.0/codex-machine.json +20 -0
- package/docs/matrix/2.5.0/cursor-machine.json +22 -0
- package/docs/matrix/2.5.0/gemini-machine.json +22 -0
- package/docs/matrix/2.5.0/opencode-machine.json +22 -0
- package/docs/matrix/2.5.0/vscode-machine.json +22 -0
- package/docs/matrix/2.5.0/windsurf-machine.json +22 -0
- package/docs/matrix/MATRIX.md +26 -0
- package/docs/matrix/README.md +59 -0
- package/docs/mint.json +61 -0
- package/docs/multi-agent-testing.md +55 -0
- package/docs/platform-integration.md +122 -0
- package/docs/system-map.md +152 -0
- package/docs/telemetry-contract.md +214 -0
- package/docs/telemetry.md +191 -0
- package/docs/testing.md +82 -0
- package/docs/token-efficiency.md +80 -0
- package/docs/usage.md +1046 -0
- package/package.json +8 -5
- package/dist/cli-doctor.d.ts +0 -6
- package/dist/cli-doctor.js +0 -60
- package/dist/cli-launch.js +0 -112
- package/dist/daemon-resilience.d.ts +0 -22
- package/dist/daemon-resilience.js +0 -85
- package/dist/mcp-proxy.d.ts +0 -67
- package/dist/mcp-proxy.js +0 -337
- package/dist/tools/profiles.d.ts +0 -37
- package/dist/tools/profiles.js +0 -139
- /package/dist/{pairing-token.d.ts → bridge/pairing-token.d.ts} +0 -0
- /package/dist/{token-auth.d.ts → bridge/token-auth.d.ts} +0 -0
- /package/dist/{token-auth.js → bridge/token-auth.js} +0 -0
- /package/dist/{cli-verify.d.ts → cli/cli-verify.d.ts} +0 -0
- /package/dist/{cloud-cli.d.ts → cli/cloud-cli.d.ts} +0 -0
- /package/dist/{server-version.d.ts → version/server-version.d.ts} +0 -0
|
@@ -1,28 +1,28 @@
|
|
|
1
|
-
import { EventType } from '@reticlehq/core';
|
|
1
|
+
import { ConsequenceKind, EventType } from '@reticlehq/core';
|
|
2
2
|
function net(events) {
|
|
3
3
|
return events.filter((e) => e.type === EventType.NET_REQUEST);
|
|
4
4
|
}
|
|
5
5
|
function satisfies(link, events) {
|
|
6
6
|
switch (link.kind) {
|
|
7
|
-
case
|
|
7
|
+
case ConsequenceKind.SIGNAL:
|
|
8
8
|
return events.some((e) => e.type === EventType.SIGNAL && e.data['name'] === link.name);
|
|
9
|
-
case
|
|
9
|
+
case ConsequenceKind.STATE:
|
|
10
10
|
return events.some((e) => e.type === EventType.STATE_CHANGE && e.data['name'] === link.name);
|
|
11
|
-
case
|
|
12
|
-
return net(events).some((e) => typeof e.data['url']
|
|
11
|
+
case ConsequenceKind.NET:
|
|
12
|
+
return net(events).some((e) => 'string' === typeof e.data['url'] &&
|
|
13
13
|
e.data['url'].includes(link.urlContains) &&
|
|
14
14
|
(link.status === undefined || e.data['status'] === link.status));
|
|
15
15
|
}
|
|
16
16
|
}
|
|
17
17
|
/** Describe what WAS observed for a link that failed — the closest attempt, for a side-by-side. */
|
|
18
18
|
function observedFor(link, events) {
|
|
19
|
-
if (
|
|
20
|
-
const match = net(events).find((e) => typeof e.data['url']
|
|
19
|
+
if (ConsequenceKind.NET === link.kind) {
|
|
20
|
+
const match = net(events).find((e) => 'string' === typeof e.data['url'] && e.data['url'].includes(link.urlContains));
|
|
21
21
|
if (match === undefined)
|
|
22
22
|
return `no request to ${link.urlContains}`;
|
|
23
23
|
return `${link.urlContains} responded ${String(match.data['status'])} (expected ${String(link.status)})`;
|
|
24
24
|
}
|
|
25
|
-
if (
|
|
25
|
+
if (ConsequenceKind.SIGNAL === link.kind)
|
|
26
26
|
return `signal "${link.name}" never fired`;
|
|
27
27
|
return `state "${link.name}" never changed`;
|
|
28
28
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { ConsequenceKind, PredicateKind } from '@reticlehq/core';
|
|
1
2
|
/**
|
|
2
3
|
* Convert a mustHold predicate into the ordered ExpectedLinks the divergence capsule walks. Only the
|
|
3
4
|
* consequence kinds (signal/net/state) become links — a presence check (element/text) can't diverge in a
|
|
@@ -5,24 +6,26 @@
|
|
|
5
6
|
*/
|
|
6
7
|
export function predicateToExpectedLinks(predicate) {
|
|
7
8
|
switch (predicate.kind) {
|
|
8
|
-
case
|
|
9
|
-
return predicate.name === undefined
|
|
10
|
-
|
|
9
|
+
case PredicateKind.SIGNAL:
|
|
10
|
+
return predicate.name === undefined
|
|
11
|
+
? []
|
|
12
|
+
: [{ kind: ConsequenceKind.SIGNAL, name: predicate.name }];
|
|
13
|
+
case PredicateKind.NET:
|
|
11
14
|
return predicate.urlContains === undefined
|
|
12
15
|
? []
|
|
13
16
|
: [
|
|
14
17
|
{
|
|
15
|
-
kind:
|
|
18
|
+
kind: ConsequenceKind.NET,
|
|
16
19
|
urlContains: predicate.urlContains,
|
|
17
20
|
...(predicate.status === undefined ? {} : { status: predicate.status }),
|
|
18
21
|
},
|
|
19
22
|
];
|
|
20
|
-
case
|
|
23
|
+
case PredicateKind.STATE: {
|
|
21
24
|
const name = predicate.store ?? predicate.path;
|
|
22
|
-
return [{ kind:
|
|
25
|
+
return [{ kind: ConsequenceKind.STATE, name }];
|
|
23
26
|
}
|
|
24
|
-
case
|
|
25
|
-
case
|
|
27
|
+
case PredicateKind.ALL_OF:
|
|
28
|
+
case PredicateKind.ANY_OF:
|
|
26
29
|
return predicate.predicates.flatMap(predicateToExpectedLinks);
|
|
27
30
|
default:
|
|
28
31
|
return [];
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function handleDoctor(port: number): Promise<void>;
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { ReticleEnv } from '@reticlehq/core';
|
|
4
|
+
import { readPid, reticleStateHome } from '../daemon/daemon.js';
|
|
5
|
+
import { PortPresence, probePresence } from '../daemon/port-presence.js';
|
|
6
|
+
import { probeDaemon } from '../mcp/mcp-proxy.js';
|
|
7
|
+
import { fetchStatus } from './cli-launch.js';
|
|
8
|
+
import { daemonLine } from './doctor-daemon-line.js';
|
|
9
|
+
import { describeForeignHolder, findPortHolder } from './port-holder.js';
|
|
10
|
+
import { spawnSync } from 'node:child_process';
|
|
11
|
+
import { SERVER_VERSION } from '../version/server-version.js';
|
|
12
|
+
import { CONTRACT_FINGERPRINT } from '@reticlehq/core';
|
|
13
|
+
import { diagnoseDesktop, isDesktopProject } from '../init/desktop-doctor.js';
|
|
14
|
+
/**
|
|
15
|
+
* `reticle doctor` — collapse the ~6 independent first-run failure modes into one command. Checks the
|
|
16
|
+
* Chromium install (the #1 silent failure), whether a daemon is up on the resolved bridge port, and
|
|
17
|
+
* reminds the user which port the app must dial. Human-readable to stdout (not the JSON log).
|
|
18
|
+
*/
|
|
19
|
+
/**
|
|
20
|
+
* Run a lookup and return its stdout, or null if it could not run.
|
|
21
|
+
*
|
|
22
|
+
* Null on ANY failure — no lsof (every Windows user, a slim container), a non-zero exit, a throw.
|
|
23
|
+
* This is a diagnostic nicety inside the command people run when things are already broken; it must
|
|
24
|
+
* never be the reason `doctor` fails. `describeForeignHolder` handles the null by saying it could
|
|
25
|
+
* not identify the holder and naming our recorded pid, rather than asserting the holder is not ours
|
|
26
|
+
* — a claim it has no evidence for, and one that was wrong for every Windows user.
|
|
27
|
+
*/
|
|
28
|
+
function runCapture(command, args) {
|
|
29
|
+
try {
|
|
30
|
+
const result = spawnSync(command, [...args], { encoding: 'utf8', timeout: LOOKUP_TIMEOUT_MS });
|
|
31
|
+
return 0 === result.status && 'string' === typeof result.stdout ? result.stdout : null;
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
/** Bound on the holder lookup. A hung `lsof` must not hang the command that diagnoses hangs. */
|
|
38
|
+
const LOOKUP_TIMEOUT_MS = 2_000;
|
|
39
|
+
/** Narrow the `/status` payload to the two fields the daemon line reads. */
|
|
40
|
+
function asIdentity(payload) {
|
|
41
|
+
if (typeof payload !== 'object' || null === payload)
|
|
42
|
+
return {};
|
|
43
|
+
const record = payload;
|
|
44
|
+
const pick = (key) => {
|
|
45
|
+
const value = record[key];
|
|
46
|
+
return 'string' === typeof value && value.length > 0 ? value : undefined;
|
|
47
|
+
};
|
|
48
|
+
// Keys are OMITTED rather than set to undefined: `exactOptionalPropertyTypes` is on, and the
|
|
49
|
+
// distinction is the point — "this daemon did not say" is not the same as "this daemon said none".
|
|
50
|
+
const version = pick('version');
|
|
51
|
+
const contract = pick('contract');
|
|
52
|
+
return {
|
|
53
|
+
...(version === undefined ? {} : { version }),
|
|
54
|
+
...(contract === undefined ? {} : { contract }),
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
export async function handleDoctor(port) {
|
|
58
|
+
const line = (s) => {
|
|
59
|
+
process.stdout.write(`${s}\n`);
|
|
60
|
+
};
|
|
61
|
+
line('reticle doctor');
|
|
62
|
+
line(` node ${process.version}`);
|
|
63
|
+
try {
|
|
64
|
+
const { chromium } = await import('playwright');
|
|
65
|
+
const path = chromium.executablePath();
|
|
66
|
+
line(existsSync(path)
|
|
67
|
+
? ' chromium ✓ installed'
|
|
68
|
+
: ' chromium ✗ missing — run: npx playwright install chromium');
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
line(' chromium ✗ missing — run: npx playwright install chromium');
|
|
72
|
+
}
|
|
73
|
+
// Ask the PORT, not just the pid file. "not running on :4400" has been printed about a port that
|
|
74
|
+
// was demonstrably occupied, which sends the reader to start a daemon that cannot bind. The three
|
|
75
|
+
// states are genuinely different problems with different fixes, so doctor names which one it is.
|
|
76
|
+
const pid = readPid(port);
|
|
77
|
+
const presence = await probePresence(port, { tcpOpen: probeDaemon, status: fetchStatus });
|
|
78
|
+
if (presence === PortPresence.DAEMON) {
|
|
79
|
+
// Name WHICH daemon. The /status payload already carries version + contract, and doctor was
|
|
80
|
+
// throwing both away — while skew is invisible everywhere else, reaching the agent as a bare
|
|
81
|
+
// -32000 naming no version. This is the command a human runs at exactly that moment.
|
|
82
|
+
const status = asIdentity(await fetchStatus(port));
|
|
83
|
+
const built = daemonLine(port, pid, status, {
|
|
84
|
+
version: SERVER_VERSION,
|
|
85
|
+
contract: CONTRACT_FINGERPRINT,
|
|
86
|
+
});
|
|
87
|
+
line(built.text);
|
|
88
|
+
if (built.skew !== undefined)
|
|
89
|
+
line(` version ✗ ${built.skew}`);
|
|
90
|
+
}
|
|
91
|
+
else if (presence === PortPresence.FOREIGN) {
|
|
92
|
+
// Name the holder when we can. `doctor` exists for exactly this moment, and "another process"
|
|
93
|
+
// leaves the reader to find a shell command themselves — the obvious one being the `lsof -ti`
|
|
94
|
+
// pipeline that also kills the agent's own MCP proxy.
|
|
95
|
+
// `pid` is what our pid file recorded for this port. When it matches the process actually
|
|
96
|
+
// holding it, this is our OWN daemon wedged, not a stranger — and the fix is different.
|
|
97
|
+
line(` daemon ✗ ${describeForeignHolder(port, findPortHolder(port, runCapture), pid)}`);
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
line(` daemon ✗ not running on :${port} — your agent runs \`reticle mcp\` (or \`reticle serve\`)`);
|
|
101
|
+
}
|
|
102
|
+
line(` bridge port ${port} (your app must dial THIS port — not your dev-server port)`);
|
|
103
|
+
// Where to LOOK when something is wrong. The daemon has always written a structured log here and
|
|
104
|
+
// nothing ever said so, so the first move in every investigation was reading source instead of
|
|
105
|
+
// reading the log. `RETICLE_TRACE=1` turns the same stream into a per-stage trace — see
|
|
106
|
+
// docs/debugging.md.
|
|
107
|
+
line(` daemon log ${join(reticleStateHome(), `daemon-${String(port)}.log`)}`);
|
|
108
|
+
line(` tracing ${ReticleEnv.TRACE}=1 on the daemon for per-stage timings in that log`);
|
|
109
|
+
// Desktop setup RCA. Every one of these fails SILENTLY — a Tauri app with the default CSP runs
|
|
110
|
+
// perfectly and never connects; an Electron app without the preload line reports zero network
|
|
111
|
+
// activity forever, which reads as "makes no backend calls" rather than "you are blind to them".
|
|
112
|
+
const readProjectFile = (relative) => {
|
|
113
|
+
try {
|
|
114
|
+
return readFileSync(join(process.cwd(), relative), 'utf8');
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
return undefined;
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
const desktop = diagnoseDesktop(readProjectFile, port);
|
|
121
|
+
if (desktop.length > 0) {
|
|
122
|
+
line('');
|
|
123
|
+
line(` desktop ✗ ${String(desktop.length)} issue(s) — the app will look fine and not work:`);
|
|
124
|
+
for (const finding of desktop) {
|
|
125
|
+
line(` ${finding.file}`);
|
|
126
|
+
line(` ${finding.problem}`);
|
|
127
|
+
line(` fix: ${finding.fix}`);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
else if (isDesktopProject(readProjectFile)) {
|
|
131
|
+
// Say so explicitly. Silence would read as "not checked", which is the same ambiguity the
|
|
132
|
+
// findings above exist to remove.
|
|
133
|
+
line(' desktop ✓ desktop wiring looks right');
|
|
134
|
+
}
|
|
135
|
+
}
|
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
* opens first. These five functions form one cohesive group: they all read the flow store and report on
|
|
6
6
|
* verification state, and none of them touch daemon lifecycle, which is what the rest of cli.ts does.
|
|
7
7
|
*/
|
|
8
|
-
import { type FileSystemPort } from '
|
|
9
|
-
import { type NamedFlow } from '
|
|
8
|
+
import { type FileSystemPort } from '../project/fs-port.js';
|
|
9
|
+
import { type NamedFlow } from '../flows/flow-sources.js';
|
|
10
10
|
/** Load the {name, steps} of every saved flow for the active project. */
|
|
11
11
|
/** Explicit files plus, when --since is given, the git-changed files since that ref. */
|
|
12
12
|
export declare function resolveChangedFiles(files: string[], since: string | undefined): Promise<string[]>;
|
|
@@ -6,23 +6,23 @@
|
|
|
6
6
|
* verification state, and none of them touch daemon lifecycle, which is what the rest of cli.ts does.
|
|
7
7
|
*/
|
|
8
8
|
import { readProjectId } from './cli-port.js';
|
|
9
|
-
import { changedFilesSince } from '
|
|
9
|
+
import { changedFilesSince } from '../flows/git-changed.js';
|
|
10
10
|
import { join } from 'node:path';
|
|
11
11
|
import { ReticleDir, RunFlowStatus } from '@reticlehq/core';
|
|
12
|
-
import { FlowStore } from '
|
|
13
|
-
import { RunStore } from '
|
|
14
|
-
import { createNodeFileSystem } from '
|
|
15
|
-
import { affectedSavedFlows } from '
|
|
16
|
-
import { gateDecision } from '
|
|
17
|
-
import { FlakeStore } from '
|
|
18
|
-
import { formatBuddyStatus } from '
|
|
19
|
-
import { CapsuleStore } from '
|
|
20
|
-
import { AssertionTiersStore } from '
|
|
21
|
-
import { detectDowngrades } from '
|
|
22
|
-
import { computeCoverage } from '
|
|
23
|
-
import { createWatchBatcher } from '
|
|
12
|
+
import { FlowStore } from '../flows/flows.js';
|
|
13
|
+
import { RunStore } from '../runs/run-store.js';
|
|
14
|
+
import { createNodeFileSystem } from '../project/fs-port.js';
|
|
15
|
+
import { affectedSavedFlows } from '../flows/flow-sources.js';
|
|
16
|
+
import { gateDecision } from '../flows/gate.js';
|
|
17
|
+
import { FlakeStore } from '../flows/flake-store.js';
|
|
18
|
+
import { formatBuddyStatus } from '../flows/buddy-status.js';
|
|
19
|
+
import { CapsuleStore } from '../capsule/capsule-store.js';
|
|
20
|
+
import { AssertionTiersStore } from '../flows/assertion-tiers-store.js';
|
|
21
|
+
import { detectDowngrades } from '../flows/assertion-integrity.js';
|
|
22
|
+
import { computeCoverage } from '../flows/coverage.js';
|
|
23
|
+
import { createWatchBatcher } from '../flows/watch-batcher.js';
|
|
24
24
|
import { watch } from 'node:fs';
|
|
25
|
-
import { log } from '
|
|
25
|
+
import { log } from '../log.js';
|
|
26
26
|
/** Load the {name, steps} of every saved flow for the active project. */
|
|
27
27
|
/** Explicit files plus, when --since is given, the git-changed files since that ref. */
|
|
28
28
|
export async function resolveChangedFiles(files, since) {
|
|
@@ -107,7 +107,7 @@ export function handleWatch() {
|
|
|
107
107
|
.then((flows) => emitBuddyStatus(fs, reticleRoot, flows, []))
|
|
108
108
|
.catch(() => undefined);
|
|
109
109
|
watch(process.cwd(), { recursive: true }, (_event, filename) => {
|
|
110
|
-
if (typeof filename
|
|
110
|
+
if ('string' === typeof filename && WATCHED_EXTENSIONS.test(filename))
|
|
111
111
|
batcher.onChange(filename);
|
|
112
112
|
});
|
|
113
113
|
}
|
|
@@ -20,12 +20,26 @@ export declare function summarizeStatus(payload: unknown): {
|
|
|
20
20
|
sessionCount: number;
|
|
21
21
|
sessions: StatusSession[];
|
|
22
22
|
};
|
|
23
|
+
/**
|
|
24
|
+
* Warn when the daemon already on this port is a different version than this CLI, and attach anyway.
|
|
25
|
+
*
|
|
26
|
+
* Attaching to whatever owns the port is the point of a daemon — but it means an upgrade does not
|
|
27
|
+
* take effect until that daemon dies, and nothing used to say so. Killing it here would take out
|
|
28
|
+
* another agent's session on a version bump, which is worse than a loud line.
|
|
29
|
+
*/
|
|
30
|
+
export declare function warnOnDaemonSkew(port: number): Promise<void>;
|
|
23
31
|
/** GET the daemon's /status JSON. Resolves to the parsed body, or undefined on any failure. */
|
|
24
32
|
export declare function fetchStatus(port: number): Promise<unknown>;
|
|
25
33
|
/** What `reticle open` should do: reuse an already-connected tab, open a new one, or ask for a url. */
|
|
26
34
|
type OpenDecision = {
|
|
27
35
|
action: 'reuse';
|
|
28
36
|
url: string;
|
|
37
|
+
}
|
|
38
|
+
/** A tab on that origin exists, but on another page — kept, and NOT reported as done. */
|
|
39
|
+
| {
|
|
40
|
+
action: 'left-as-is';
|
|
41
|
+
url: string;
|
|
42
|
+
requested: string;
|
|
29
43
|
} | {
|
|
30
44
|
action: 'open';
|
|
31
45
|
url: string;
|
|
@@ -36,8 +50,16 @@ type OpenDecision = {
|
|
|
36
50
|
* Decide what `reticle open [url]` does, given the currently-connected tabs. Pure.
|
|
37
51
|
* - no url + a tab connected → reuse it (the app is already open; don't spawn a duplicate).
|
|
38
52
|
* - no url + nothing connected → ask for one.
|
|
39
|
-
* - url + a tab already
|
|
53
|
+
* - url + a tab already AT that url → reuse it (idempotent — re-running never piles up tabs).
|
|
54
|
+
* - url + a tab on that origin but another page → keep it, and say so (`left-as-is`).
|
|
40
55
|
* - url + no matching tab → open it.
|
|
56
|
+
*
|
|
57
|
+
* The origin match is why re-running this never piles up tabs, and it stays. What changed is the
|
|
58
|
+
* REPORT: matching by origin meant `reticle open http://localhost:3000/settings` answered `reusing`
|
|
59
|
+
* for a tab sitting on `http://localhost:3000/` and exited 0, so the caller read a success for a page
|
|
60
|
+
* that was never opened. Navigating the tab instead was the other option and is worse: `reticle open`
|
|
61
|
+
* is run by a human whose browser this is, and yanking the page they are looking at to somewhere else
|
|
62
|
+
* is a bigger surprise than being told where the tab actually is.
|
|
41
63
|
*/
|
|
42
64
|
export declare function decideOpen(sessions: {
|
|
43
65
|
url: string;
|
|
@@ -47,6 +69,13 @@ export declare function openCommand(url: string, platform: NodeJS.Platform): {
|
|
|
47
69
|
cmd: string;
|
|
48
70
|
args: string[];
|
|
49
71
|
};
|
|
50
|
-
/**
|
|
51
|
-
|
|
72
|
+
/**
|
|
73
|
+
* Launch the default browser at `url` (detached). The spawn is injected so tests stay hermetic.
|
|
74
|
+
*
|
|
75
|
+
* Resolves to whether the launch COMMAND started — not to whether a page appeared. It used to return
|
|
76
|
+
* nothing at all and the caller printed `{"opened": url}` unconditionally, so a launcher that failed
|
|
77
|
+
* outright still reported success. Reported from the field as twenty minutes lost chasing a phantom
|
|
78
|
+
* port problem while nothing had ever opened.
|
|
79
|
+
*/
|
|
80
|
+
export declare function openInBrowser(url: string, platform?: NodeJS.Platform, run?: (cmd: string, args: string[]) => Promise<string | null>): Promise<string | null>;
|
|
52
81
|
export {};
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import * as http from 'node:http';
|
|
2
|
+
import { NodePlatform } from '../platform.js';
|
|
3
|
+
import { spawn } from 'node:child_process';
|
|
4
|
+
import { isOpaqueOrigin, LOOPBACK_HOST, STATUS_PATH } from '@reticlehq/core';
|
|
5
|
+
import { describeSkew, DAEMON_FIX } from '../version/version-skew.js';
|
|
6
|
+
import { CONTRACT_FINGERPRINT } from '@reticlehq/core';
|
|
7
|
+
import { SERVER_VERSION } from '../version/server-version.js';
|
|
8
|
+
import { log } from '../log.js';
|
|
9
|
+
/**
|
|
10
|
+
* Reduce the daemon's /status JSON to the compact view `reticle status` prints. Pure: narrows the
|
|
11
|
+
* untrusted wire payload (never `any`) and tolerates a missing/partial body so a malformed response
|
|
12
|
+
* degrades to "running, 0 sessions" instead of throwing.
|
|
13
|
+
*/
|
|
14
|
+
export function summarizeStatus(payload) {
|
|
15
|
+
if (typeof payload !== 'object' || null === payload)
|
|
16
|
+
return { sessionCount: 0, sessions: [] };
|
|
17
|
+
const obj = payload;
|
|
18
|
+
const raw = Array.isArray(obj['sessions']) ? obj['sessions'] : [];
|
|
19
|
+
const sessions = raw
|
|
20
|
+
.map((s) => {
|
|
21
|
+
if (typeof s !== 'object' || null === s)
|
|
22
|
+
return null;
|
|
23
|
+
const r = s;
|
|
24
|
+
const sessionId = 'string' === typeof r['sessionId'] ? r['sessionId'] : '';
|
|
25
|
+
if ('' === sessionId)
|
|
26
|
+
return null;
|
|
27
|
+
return {
|
|
28
|
+
sessionId,
|
|
29
|
+
url: 'string' === typeof r['url'] ? r['url'] : '',
|
|
30
|
+
throttled: true === r['throttled'],
|
|
31
|
+
stale: true === r['stale'],
|
|
32
|
+
pendingMarks: 'number' === typeof r['pendingMarks'] ? r['pendingMarks'] : 0,
|
|
33
|
+
};
|
|
34
|
+
})
|
|
35
|
+
.filter((s) => s !== null);
|
|
36
|
+
const sessionCount = 'number' === typeof obj['sessionCount'] ? obj['sessionCount'] : sessions.length;
|
|
37
|
+
return { sessionCount, sessions };
|
|
38
|
+
}
|
|
39
|
+
/** A string field off the /status body, or undefined on a daemon too old to report it. */
|
|
40
|
+
function statusField(payload, key) {
|
|
41
|
+
if (typeof payload !== 'object' || null === payload)
|
|
42
|
+
return undefined;
|
|
43
|
+
const value = payload[key];
|
|
44
|
+
return 'string' === typeof value && value.length > 0 ? value : undefined;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Warn when the daemon already on this port is a different version than this CLI, and attach anyway.
|
|
48
|
+
*
|
|
49
|
+
* Attaching to whatever owns the port is the point of a daemon — but it means an upgrade does not
|
|
50
|
+
* take effect until that daemon dies, and nothing used to say so. Killing it here would take out
|
|
51
|
+
* another agent's session on a version bump, which is worse than a loud line.
|
|
52
|
+
*/
|
|
53
|
+
export async function warnOnDaemonSkew(port) {
|
|
54
|
+
const status = await fetchStatus(port);
|
|
55
|
+
const skew = describeSkew({
|
|
56
|
+
what: 'the daemon already running on this port',
|
|
57
|
+
version: statusField(status, 'version'),
|
|
58
|
+
contract: statusField(status, 'contract'),
|
|
59
|
+
fix: DAEMON_FIX,
|
|
60
|
+
}, { version: SERVER_VERSION, contract: CONTRACT_FINGERPRINT });
|
|
61
|
+
if (skew !== undefined)
|
|
62
|
+
log('reticle_daemon_skew', { port, warning: skew });
|
|
63
|
+
}
|
|
64
|
+
/** How long the daemon /status probe waits before giving up — a local loopback call is near-instant. */
|
|
65
|
+
const STATUS_PROBE_TIMEOUT_MS = 1000;
|
|
66
|
+
/** GET the daemon's /status JSON. Resolves to the parsed body, or undefined on any failure. */
|
|
67
|
+
export function fetchStatus(port) {
|
|
68
|
+
return new Promise((resolve) => {
|
|
69
|
+
const req = http.get({ host: LOOPBACK_HOST, port, path: STATUS_PATH, timeout: STATUS_PROBE_TIMEOUT_MS }, (res) => {
|
|
70
|
+
let body = '';
|
|
71
|
+
res.setEncoding('utf8');
|
|
72
|
+
res.on('data', (chunk) => (body += chunk));
|
|
73
|
+
res.on('end', () => {
|
|
74
|
+
try {
|
|
75
|
+
resolve(JSON.parse(body));
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
resolve(undefined);
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
req.on('error', () => resolve(undefined));
|
|
83
|
+
req.on('timeout', () => {
|
|
84
|
+
req.destroy();
|
|
85
|
+
resolve(undefined);
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
function sameOrigin(a, b) {
|
|
90
|
+
try {
|
|
91
|
+
const origin = new URL(a).origin;
|
|
92
|
+
// Two opaque origins (desktop webviews, file:// docs) BOTH stringify to "null", so comparing them
|
|
93
|
+
// would call every desktop app the same app and reuse the wrong session. Never match on opaque.
|
|
94
|
+
if (isOpaqueOrigin(origin))
|
|
95
|
+
return false;
|
|
96
|
+
return origin === new URL(b).origin;
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Decide what `reticle open [url]` does, given the currently-connected tabs. Pure.
|
|
104
|
+
* - no url + a tab connected → reuse it (the app is already open; don't spawn a duplicate).
|
|
105
|
+
* - no url + nothing connected → ask for one.
|
|
106
|
+
* - url + a tab already AT that url → reuse it (idempotent — re-running never piles up tabs).
|
|
107
|
+
* - url + a tab on that origin but another page → keep it, and say so (`left-as-is`).
|
|
108
|
+
* - url + no matching tab → open it.
|
|
109
|
+
*
|
|
110
|
+
* The origin match is why re-running this never piles up tabs, and it stays. What changed is the
|
|
111
|
+
* REPORT: matching by origin meant `reticle open http://localhost:3000/settings` answered `reusing`
|
|
112
|
+
* for a tab sitting on `http://localhost:3000/` and exited 0, so the caller read a success for a page
|
|
113
|
+
* that was never opened. Navigating the tab instead was the other option and is worse: `reticle open`
|
|
114
|
+
* is run by a human whose browser this is, and yanking the page they are looking at to somewhere else
|
|
115
|
+
* is a bigger surprise than being told where the tab actually is.
|
|
116
|
+
*/
|
|
117
|
+
export function decideOpen(sessions, url) {
|
|
118
|
+
if (url === undefined) {
|
|
119
|
+
const first = sessions[0];
|
|
120
|
+
return first !== undefined ? { action: 'reuse', url: first.url } : { action: 'need-url' };
|
|
121
|
+
}
|
|
122
|
+
const exact = sessions.find((s) => s.url === url);
|
|
123
|
+
if (exact !== undefined)
|
|
124
|
+
return { action: 'reuse', url: exact.url };
|
|
125
|
+
const onOrigin = sessions.find((s) => sameOrigin(s.url, url));
|
|
126
|
+
return onOrigin !== undefined
|
|
127
|
+
? { action: 'left-as-is', url: onOrigin.url, requested: url }
|
|
128
|
+
: { action: 'open', url };
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Percent-encode the cmd.exe metacharacters that `start` re-parses so a URL can't break out into
|
|
132
|
+
* command execution on Windows (`?next=x&calc` → command chaining). `%` is left untouched so an
|
|
133
|
+
* already-encoded URL isn't double-encoded; the browser decodes the escapes back to the real URL.
|
|
134
|
+
*/
|
|
135
|
+
function encodeForWindowsStart(url) {
|
|
136
|
+
return url.replace(/[&^|<>()"'!]/g, (ch) => `%${ch.charCodeAt(0).toString(16).toUpperCase()}`);
|
|
137
|
+
}
|
|
138
|
+
/** The OS command that opens a URL in the default browser, per platform. Pure — unit-tested. */
|
|
139
|
+
export function openCommand(url, platform) {
|
|
140
|
+
if (NodePlatform.MACOS === platform)
|
|
141
|
+
return { cmd: 'open', args: [url] };
|
|
142
|
+
if (NodePlatform.WINDOWS === platform) {
|
|
143
|
+
return { cmd: 'cmd', args: ['/c', 'start', '', encodeForWindowsStart(url)] };
|
|
144
|
+
}
|
|
145
|
+
return { cmd: 'xdg-open', args: [url] };
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Launch the default browser at `url` (detached). The spawn is injected so tests stay hermetic.
|
|
149
|
+
*
|
|
150
|
+
* Resolves to whether the launch COMMAND started — not to whether a page appeared. It used to return
|
|
151
|
+
* nothing at all and the caller printed `{"opened": url}` unconditionally, so a launcher that failed
|
|
152
|
+
* outright still reported success. Reported from the field as twenty minutes lost chasing a phantom
|
|
153
|
+
* port problem while nothing had ever opened.
|
|
154
|
+
*/
|
|
155
|
+
export async function openInBrowser(url, platform = process.platform, run = defaultRun) {
|
|
156
|
+
const { cmd, args } = openCommand(url, platform);
|
|
157
|
+
return run(cmd, args);
|
|
158
|
+
}
|
|
159
|
+
/** null when the launcher started; the failure message when it could not be run at all. */
|
|
160
|
+
function defaultRun(cmd, args) {
|
|
161
|
+
return new Promise((resolve) => {
|
|
162
|
+
const child = spawn(cmd, args, { detached: true, stdio: 'ignore' });
|
|
163
|
+
// An unhandled 'error' on a ChildProcess throws — and ENOENT on the launcher is exactly the case
|
|
164
|
+
// this function exists to report, so it must be listened for either way.
|
|
165
|
+
child.on('error', (err) => resolve(err.message));
|
|
166
|
+
child.on('spawn', () => {
|
|
167
|
+
child.unref();
|
|
168
|
+
resolve(null);
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/** `reticle feedback --rating 4 "the words"` — the human half of the channel. */
|
|
2
|
+
export declare const RATING_FLAG = "--rating";
|
|
3
|
+
export declare const BUG_FLAG = "--bug";
|
|
4
|
+
/**
|
|
5
|
+
* The AGENT half of the same channel, for the phase where `reticle_feedback` does not exist yet.
|
|
6
|
+
*
|
|
7
|
+
* An agent that is still installing Reticle — or that just watched `init` fail, or a daemon refuse to
|
|
8
|
+
* start — has no MCP tools to file with, and that is exactly the report we most need: the failures
|
|
9
|
+
* that happen before the tool surface is reachable are invisible to every other path we have. This
|
|
10
|
+
* gives that agent the same shape the tool takes, over a command that needs nothing running.
|
|
11
|
+
*/
|
|
12
|
+
export declare const KIND_FLAG = "--kind";
|
|
13
|
+
export declare const AGENT_FLAG = "--agent";
|
|
14
|
+
/** Accepted `--kind` values, from core's enum — never a second hand-written list to drift from it. */
|
|
15
|
+
export declare const FEEDBACK_KINDS: readonly string[];
|
|
16
|
+
/** What `parseFeedbackArgs` yields: the parsed command, or a usage error to print. */
|
|
17
|
+
export type ParsedFeedback = {
|
|
18
|
+
kind: 'feedback';
|
|
19
|
+
text: string;
|
|
20
|
+
rating?: number;
|
|
21
|
+
/** The FeedbackKind name from `--kind`; absent means the legacy bug/experience split. */
|
|
22
|
+
feedbackKind?: string;
|
|
23
|
+
bug: boolean;
|
|
24
|
+
/** `--agent`: file as the agent, so agent reports never dilute the human rating signal. */
|
|
25
|
+
agent: boolean;
|
|
26
|
+
} | {
|
|
27
|
+
kind: 'error';
|
|
28
|
+
message: string;
|
|
29
|
+
};
|
|
30
|
+
/** Parse everything after `reticle feedback`. */
|
|
31
|
+
export declare function parseFeedbackArgs(rest: string[]): ParsedFeedback;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `reticle feedback` grammar — flags, kind validation, and the message-vs-flag split.
|
|
3
|
+
*
|
|
4
|
+
* Split out of `cli-parse.ts` when that file crossed the line cap. Cohesive on its own terms: this
|
|
5
|
+
* is the one command whose arguments are free text, so it is the only place in the grammar where
|
|
6
|
+
* "everything that is not a flag" is a rule with edge cases rather than a one-liner.
|
|
7
|
+
*/
|
|
8
|
+
import { FeedbackKind } from '@reticlehq/core';
|
|
9
|
+
/** `reticle feedback --rating 4 "the words"` — the human half of the channel. */
|
|
10
|
+
export const RATING_FLAG = '--rating';
|
|
11
|
+
export const BUG_FLAG = '--bug';
|
|
12
|
+
/**
|
|
13
|
+
* The AGENT half of the same channel, for the phase where `reticle_feedback` does not exist yet.
|
|
14
|
+
*
|
|
15
|
+
* An agent that is still installing Reticle — or that just watched `init` fail, or a daemon refuse to
|
|
16
|
+
* start — has no MCP tools to file with, and that is exactly the report we most need: the failures
|
|
17
|
+
* that happen before the tool surface is reachable are invisible to every other path we have. This
|
|
18
|
+
* gives that agent the same shape the tool takes, over a command that needs nothing running.
|
|
19
|
+
*/
|
|
20
|
+
export const KIND_FLAG = '--kind';
|
|
21
|
+
export const AGENT_FLAG = '--agent';
|
|
22
|
+
/** Accepted `--kind` values, from core's enum — never a second hand-written list to drift from it. */
|
|
23
|
+
export const FEEDBACK_KINDS = Object.values(FeedbackKind);
|
|
24
|
+
/** Parse everything after `reticle feedback`. */
|
|
25
|
+
export function parseFeedbackArgs(rest) {
|
|
26
|
+
const ratingAt = rest.indexOf(RATING_FLAG);
|
|
27
|
+
const rating = -1 === ratingAt ? undefined : Number(rest[ratingAt + 1]);
|
|
28
|
+
if (rating !== undefined && (!Number.isInteger(rating) || rating < 1 || rating > 5)) {
|
|
29
|
+
return { kind: 'error', message: `${RATING_FLAG} takes a whole number from 1 to 5` };
|
|
30
|
+
}
|
|
31
|
+
const kindAt = rest.indexOf(KIND_FLAG);
|
|
32
|
+
const feedbackKind = -1 === kindAt ? undefined : rest[kindAt + 1];
|
|
33
|
+
if (feedbackKind !== undefined && !FEEDBACK_KINDS.includes(feedbackKind)) {
|
|
34
|
+
return { kind: 'error', message: `${KIND_FLAG} takes one of: ${FEEDBACK_KINDS.join(', ')}` };
|
|
35
|
+
}
|
|
36
|
+
// Everything that is not a flag or a flag's VALUE is the message. Quoting is the user's job for
|
|
37
|
+
// shell reasons, but joining the remainder means an unquoted sentence still works. Both value
|
|
38
|
+
// positions have to be consumed: without that, `--kind bug "..."` filed a report whose text began
|
|
39
|
+
// with the word "bug".
|
|
40
|
+
const consumed = new Set([
|
|
41
|
+
...(-1 === ratingAt ? [] : [ratingAt, ratingAt + 1]),
|
|
42
|
+
...(-1 === kindAt ? [] : [kindAt, kindAt + 1]),
|
|
43
|
+
]);
|
|
44
|
+
const text = rest
|
|
45
|
+
.filter((arg, i) => !consumed.has(i) && !arg.startsWith('--'))
|
|
46
|
+
.join(' ')
|
|
47
|
+
.trim();
|
|
48
|
+
return {
|
|
49
|
+
kind: 'feedback',
|
|
50
|
+
text,
|
|
51
|
+
...(rating !== undefined ? { rating } : {}),
|
|
52
|
+
...(feedbackKind !== undefined ? { feedbackKind } : {}),
|
|
53
|
+
bug: rest.includes(BUG_FLAG),
|
|
54
|
+
agent: rest.includes(AGENT_FLAG),
|
|
55
|
+
};
|
|
56
|
+
}
|