@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
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
* Lives in its own module so `cli.ts` stays a dispatcher, and so the `_daemon` exclusion below sits
|
|
5
5
|
* next to the explanation of why it exists rather than buried in a startup function.
|
|
6
6
|
*/
|
|
7
|
-
import {
|
|
8
|
-
import { DAEMON_INNER_COMMAND, knownCommand } from '../cli-parse.js';
|
|
7
|
+
import { TelemetryEventKind } from '@reticlehq/core';
|
|
8
|
+
import { DAEMON_INNER_COMMAND, knownCommand } from '../cli/cli-parse.js';
|
|
9
9
|
import { getTelemetry } from './telemetry.js';
|
|
10
10
|
import { describeCliFlags } from './argument-shape.js';
|
|
11
11
|
/**
|
|
@@ -20,22 +20,45 @@ import { describeCliFlags } from './argument-shape.js';
|
|
|
20
20
|
* The daemon\'s own lifecycle is already reported by `daemon_started` / `daemon_stopped`; a spawned
|
|
21
21
|
* daemon is not a CLI run.
|
|
22
22
|
*/
|
|
23
|
+
/**
|
|
24
|
+
* The agent's MCP transport, not a person. `reticle mcp` is what an MCP client runs to open a stdio
|
|
25
|
+
* connection — nobody types it. Measured over one real day it was 475 of 561 `cli_command_run`
|
|
26
|
+
* events (85%), on an event whose whole purpose is human intent, and the agent attaching is already
|
|
27
|
+
* reported by `mcp_client_connected` with strictly more detail (reconnect, daemon age, client).
|
|
28
|
+
*
|
|
29
|
+
* `serve` is NOT excluded: nothing spawns it on a person's behalf, so typing it is a real act.
|
|
30
|
+
*/
|
|
31
|
+
const AGENT_TRANSPORT_COMMAND = 'mcp';
|
|
32
|
+
/** True when this command represents a person typing something. Exported for the gate. */
|
|
33
|
+
export function isHumanCliCommand(command) {
|
|
34
|
+
return command !== AGENT_TRANSPORT_COMMAND && command !== DAEMON_INNER_COMMAND;
|
|
35
|
+
}
|
|
23
36
|
export function reportCliRun(argv) {
|
|
24
37
|
const firstArg = argv[0];
|
|
25
38
|
if (firstArg === DAEMON_INNER_COMMAND)
|
|
26
39
|
return;
|
|
40
|
+
const command = knownCommand(firstArg);
|
|
27
41
|
const telemetry = getTelemetry();
|
|
42
|
+
// FIRST — before the human-command filter. The install happened whichever command ran, and on most
|
|
43
|
+
// machines the very first contact is the agent spawning `reticle mcp`, which that filter excludes.
|
|
44
|
+
// Measured over one sweep: 15 `init_completed` and ZERO `reticle_installed`, in a run that
|
|
45
|
+
// installed the SDK into nine apps — the top of every funnel, missing.
|
|
28
46
|
// `detach` so a quick command (`version`/`gate`) exits immediately instead of waiting out the POST.
|
|
29
47
|
if (telemetry.firstRun)
|
|
30
48
|
void telemetry.emit(TelemetryEventKind.RETICLE_INSTALLED, { detach: true });
|
|
49
|
+
if (!isHumanCliCommand(command))
|
|
50
|
+
return;
|
|
31
51
|
void telemetry.emit(TelemetryEventKind.CLI_COMMAND_RUN, {
|
|
32
52
|
detach: true,
|
|
33
|
-
actor
|
|
53
|
+
// No `actor`: with the agent's transport excluded, this event is human BY DEFINITION. It was
|
|
54
|
+
// constant `human` on all 561 events in a real day — zero information, and wrong for the 475
|
|
55
|
+
// that were the agent. `actor` stays where it earns its place: verification_completed and
|
|
56
|
+
// bug_found, where it splits the agent's own loop from a human/CI-triggered run.
|
|
34
57
|
// A fixed, low-cardinality vocabulary WE define, so it is safe to send whole and it is the closest
|
|
35
58
|
// honest read we have on intent: `verify` and `gate` mean something very different from `status`.
|
|
36
59
|
// An unrecognized first arg reports as `unknown` rather than being echoed — an echo would put
|
|
37
60
|
// whatever someone mistyped, including a path or a URL, straight onto the wire.
|
|
38
|
-
command
|
|
61
|
+
command,
|
|
39
62
|
// Which flags were PRESENT, by name only — never their values. `--http-token` alone makes that
|
|
40
63
|
// rule absolute rather than case-by-case; `--drive` and `--storage-state` carry a URL and a path.
|
|
41
64
|
flags: describeCliFlags(argv),
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a crash was, when the stack cannot say where it was.
|
|
3
|
+
*
|
|
4
|
+
* `reticleFrames` keeps only frames inside our own published packages, on the rule that a crash
|
|
5
|
+
* stack is mostly the user's application and node internals and neither is ours to collect. That
|
|
6
|
+
* rule is right and stays. But a refused socket has a stack that is ENTIRELY node internals:
|
|
7
|
+
*
|
|
8
|
+
* Error: connect ECONNREFUSED 127.0.0.1:4400
|
|
9
|
+
* at TCPConnectWrap.afterConnect [as oncomplete] (node:net:1637:16)
|
|
10
|
+
*
|
|
11
|
+
* so every crash of that shape arrives with an error type, a redacted message, and nothing else —
|
|
12
|
+
* 65 of them in one measured day, one fingerprint, zero location on all 65, and 63 of those from
|
|
13
|
+
* two people who emitted no other event of any kind. A loopback-heavy tool produces plenty of
|
|
14
|
+
* these, and the next one is just as blind.
|
|
15
|
+
*
|
|
16
|
+
* Everything here is derived from a Node SYSTEM ERROR's own structured properties, and every field
|
|
17
|
+
* is chosen so that it carries nothing of the reporter's: the syscall and errno are our own
|
|
18
|
+
* vocabulary, `loopback` is one bit, the port is compared against values we already know and
|
|
19
|
+
* reported as an enum, and the frame names a line in NODE's source. The address and the port number
|
|
20
|
+
* are read and deliberately thrown away.
|
|
21
|
+
*/
|
|
22
|
+
/** The structured cause of a crash, when the error carries one. Every field is optional. */
|
|
23
|
+
export interface CrashCause {
|
|
24
|
+
syscall?: string;
|
|
25
|
+
errno?: string;
|
|
26
|
+
loopback?: boolean;
|
|
27
|
+
port?: CrashPort;
|
|
28
|
+
internalFrame?: string;
|
|
29
|
+
}
|
|
30
|
+
import { CrashPort } from '@reticlehq/core';
|
|
31
|
+
/**
|
|
32
|
+
* The innermost frame that names NODE's own source.
|
|
33
|
+
*
|
|
34
|
+
* Innermost because that is where the failure actually happened, and it distinguishes a connect
|
|
35
|
+
* from a DNS lookup from a TLS handshake — the thing that, in the report this came from, had to be
|
|
36
|
+
* worked out by hand from a proxy's reject path. `node:net:1637` is a line in Node's published
|
|
37
|
+
* source: it carries nothing about the machine, the app, or anyone's directory layout, which is
|
|
38
|
+
* why it can be kept when the user's own frames cannot.
|
|
39
|
+
*/
|
|
40
|
+
export declare function innermostInternalFrame(stack: string): string | undefined;
|
|
41
|
+
/**
|
|
42
|
+
* Read a Node system error's structured cause, keeping only what is ours to keep.
|
|
43
|
+
*
|
|
44
|
+
* `knownPorts` is passed IN rather than read from the environment so this stays pure and so the
|
|
45
|
+
* comparison is testable — the port itself is used to answer one enum and is then discarded.
|
|
46
|
+
*/
|
|
47
|
+
export declare function crashCause(value: unknown, knownPorts: readonly number[]): CrashCause;
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a crash was, when the stack cannot say where it was.
|
|
3
|
+
*
|
|
4
|
+
* `reticleFrames` keeps only frames inside our own published packages, on the rule that a crash
|
|
5
|
+
* stack is mostly the user's application and node internals and neither is ours to collect. That
|
|
6
|
+
* rule is right and stays. But a refused socket has a stack that is ENTIRELY node internals:
|
|
7
|
+
*
|
|
8
|
+
* Error: connect ECONNREFUSED 127.0.0.1:4400
|
|
9
|
+
* at TCPConnectWrap.afterConnect [as oncomplete] (node:net:1637:16)
|
|
10
|
+
*
|
|
11
|
+
* so every crash of that shape arrives with an error type, a redacted message, and nothing else —
|
|
12
|
+
* 65 of them in one measured day, one fingerprint, zero location on all 65, and 63 of those from
|
|
13
|
+
* two people who emitted no other event of any kind. A loopback-heavy tool produces plenty of
|
|
14
|
+
* these, and the next one is just as blind.
|
|
15
|
+
*
|
|
16
|
+
* Everything here is derived from a Node SYSTEM ERROR's own structured properties, and every field
|
|
17
|
+
* is chosen so that it carries nothing of the reporter's: the syscall and errno are our own
|
|
18
|
+
* vocabulary, `loopback` is one bit, the port is compared against values we already know and
|
|
19
|
+
* reported as an enum, and the frame names a line in NODE's source. The address and the port number
|
|
20
|
+
* are read and deliberately thrown away.
|
|
21
|
+
*/
|
|
22
|
+
import { CrashPort } from '@reticlehq/core';
|
|
23
|
+
import { reticleFrames } from './error-fingerprint.js';
|
|
24
|
+
/** IPv4 loopback is the whole 127/8 block, not just 127.0.0.1. */
|
|
25
|
+
const IPV4_LOOPBACK = /^127\./;
|
|
26
|
+
/** `node:net:1637` out of `at TCPConnectWrap.afterConnect [as oncomplete] (node:net:1637:16)`. */
|
|
27
|
+
const NODE_INTERNAL_FRAME = /\((node:[\w/.-]+):(\d+):\d+\)/;
|
|
28
|
+
/** V8 renders an IPv4-mapped IPv6 address this way; the mapped half is what decides loopback. */
|
|
29
|
+
const IPV6_MAPPED_PREFIX = '::ffff:';
|
|
30
|
+
/**
|
|
31
|
+
* Was this aimed at the machine it was running on?
|
|
32
|
+
*
|
|
33
|
+
* One bit, and it splits two populations with different owners: refused ON loopback is a Reticle
|
|
34
|
+
* lifecycle problem (a daemon that is not up), refused off-box is a network problem that is not
|
|
35
|
+
* ours. Today they are indistinguishable in the data.
|
|
36
|
+
*/
|
|
37
|
+
function isLoopback(address) {
|
|
38
|
+
const host = address.toLowerCase();
|
|
39
|
+
if ('::1' === host || 'localhost' === host)
|
|
40
|
+
return true;
|
|
41
|
+
const unmapped = host.startsWith(IPV6_MAPPED_PREFIX)
|
|
42
|
+
? host.slice(IPV6_MAPPED_PREFIX.length)
|
|
43
|
+
: host;
|
|
44
|
+
return IPV4_LOOPBACK.test(unmapped);
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* The innermost frame that names NODE's own source.
|
|
48
|
+
*
|
|
49
|
+
* Innermost because that is where the failure actually happened, and it distinguishes a connect
|
|
50
|
+
* from a DNS lookup from a TLS handshake — the thing that, in the report this came from, had to be
|
|
51
|
+
* worked out by hand from a proxy's reject path. `node:net:1637` is a line in Node's published
|
|
52
|
+
* source: it carries nothing about the machine, the app, or anyone's directory layout, which is
|
|
53
|
+
* why it can be kept when the user's own frames cannot.
|
|
54
|
+
*/
|
|
55
|
+
export function innermostInternalFrame(stack) {
|
|
56
|
+
for (const line of stack.split('\n')) {
|
|
57
|
+
const match = line.match(NODE_INTERNAL_FRAME);
|
|
58
|
+
if (null === match)
|
|
59
|
+
continue;
|
|
60
|
+
const [, module, lineNumber] = match;
|
|
61
|
+
if (module === undefined || lineNumber === undefined)
|
|
62
|
+
continue;
|
|
63
|
+
return `${module}:${lineNumber}`;
|
|
64
|
+
}
|
|
65
|
+
return undefined;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Read a Node system error's structured cause, keeping only what is ours to keep.
|
|
69
|
+
*
|
|
70
|
+
* `knownPorts` is passed IN rather than read from the environment so this stays pure and so the
|
|
71
|
+
* comparison is testable — the port itself is used to answer one enum and is then discarded.
|
|
72
|
+
*/
|
|
73
|
+
export function crashCause(value, knownPorts) {
|
|
74
|
+
if (!(value instanceof Error))
|
|
75
|
+
return {};
|
|
76
|
+
const error = value;
|
|
77
|
+
const cause = {};
|
|
78
|
+
if ('string' === typeof error.syscall && error.syscall.length > 0)
|
|
79
|
+
cause.syscall = error.syscall;
|
|
80
|
+
// `code` (`ECONNREFUSED`), not the numeric `errno` (-61): the symbolic name is stable across
|
|
81
|
+
// platforms, where the number is not.
|
|
82
|
+
if ('string' === typeof error.code && error.code.length > 0)
|
|
83
|
+
cause.errno = error.code;
|
|
84
|
+
if ('string' === typeof error.address)
|
|
85
|
+
cause.loopback = isLoopback(error.address);
|
|
86
|
+
if ('number' === typeof error.port) {
|
|
87
|
+
cause.port = knownPorts.includes(error.port) ? CrashPort.RETICLE : CrashPort.OTHER;
|
|
88
|
+
}
|
|
89
|
+
// Only for a SYSTEM error, and only when nothing of ours is on the stack.
|
|
90
|
+
//
|
|
91
|
+
// Both halves are about collecting the minimum that answers the question. Any error raised
|
|
92
|
+
// anywhere in Node carries `node:internal/*` frames from whatever called it, so an ungated rule
|
|
93
|
+
// would attach one to every crash — noise on the reports that already have a location, and more
|
|
94
|
+
// than we need. The blind reports are the ones with a syscall and no frames of our own, and they
|
|
95
|
+
// are the only ones this exists for.
|
|
96
|
+
if (cause.syscall === undefined && cause.errno === undefined)
|
|
97
|
+
return cause;
|
|
98
|
+
if (reticleFrames(error.stack ?? '').length > 0)
|
|
99
|
+
return cause;
|
|
100
|
+
const frame = innermostInternalFrame(error.stack ?? '');
|
|
101
|
+
if (frame !== undefined)
|
|
102
|
+
cause.internalFrame = frame;
|
|
103
|
+
return cause;
|
|
104
|
+
}
|
|
@@ -1,3 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The daemon's telemetry lifecycle, in one place: start, the periodic flush that survives a kill, the
|
|
3
|
+
* one-shot project profile, and the rich summary on shutdown.
|
|
4
|
+
*
|
|
5
|
+
* Extracted from `cli.ts` because it is a cohesive unit with its own timers and its own failure rule
|
|
6
|
+
* (nothing here may ever be able to fail a daemon start), and because `cli.ts` is a dispatcher — the
|
|
7
|
+
* more of this that lives there, the harder it is to see what a command actually does.
|
|
8
|
+
*/
|
|
9
|
+
import { type SessionSummary } from '@reticlehq/core';
|
|
10
|
+
/**
|
|
11
|
+
* How often a running daemon rolls up its window.
|
|
12
|
+
*
|
|
13
|
+
* Exported because it is the BOUND ON WHAT IS LOST: a daemon that has served a tool never
|
|
14
|
+
* idle-exits, so nothing calls shutdown, so its last partial window dies with the process. At 30
|
|
15
|
+
* minutes against a median 28-minute session that meant the median session reported nothing at all.
|
|
16
|
+
* Only non-empty windows emit, so a shorter interval costs nothing on the 74% of daemons that never
|
|
17
|
+
* serve a tool.
|
|
18
|
+
*/
|
|
19
|
+
export declare const SESSION_FLUSH_MS: number;
|
|
20
|
+
/**
|
|
21
|
+
* When a session first rolls up, independent of the periodic interval.
|
|
22
|
+
*
|
|
23
|
+
* The interval alone leaves a hole exactly the width of itself: a daemon SIGKILLed before its first
|
|
24
|
+
* tick — a closed laptop, OOM, `kill -9`, a force-quit editor — reaches no shutdown handler and has
|
|
25
|
+
* emitted nothing, so the whole session is invisible.
|
|
26
|
+
*
|
|
27
|
+
* Measured 2026-08-10/11 (non-CI): **614 `daemon_started` against 499 `daemon_stopped` — 19% of
|
|
28
|
+
* sessions never reported a summary at all.** Every "did anyone use Reticle" number is computed on
|
|
29
|
+
* the 81% that did, and undercounts by an unknown amount, which is the one thing a funnel metric
|
|
30
|
+
* must not do.
|
|
31
|
+
*
|
|
32
|
+
* Ninety seconds is chosen against what it protects: a session that did real work usually does it
|
|
33
|
+
* early (snapshot -> act -> assert is seconds), so one early tick captures nearly all of it. It
|
|
34
|
+
* costs nothing on the daemons that dominate the population, because only a NON-EMPTY window emits
|
|
35
|
+
* and 74% of daemons never serve a tool at all.
|
|
36
|
+
*/
|
|
37
|
+
export declare const FIRST_FLUSH_MS: number;
|
|
1
38
|
/** Stops the timers this installed. Called from the daemon's shutdown path. */
|
|
2
39
|
export interface DaemonTelemetry {
|
|
3
40
|
/**
|
|
@@ -10,7 +47,7 @@ export interface DaemonTelemetry {
|
|
|
10
47
|
* fails or hangs still cannot block the exit, because `emit` swallows its own errors and is bounded
|
|
11
48
|
* by its own request timeout.
|
|
12
49
|
*/
|
|
13
|
-
shutdown(): Promise<void>;
|
|
50
|
+
shutdown(exit?: SessionSummary['exit']): Promise<void>;
|
|
14
51
|
}
|
|
15
52
|
/**
|
|
16
53
|
* Report the daemon as started, profile the project once, and keep the session counters flushing.
|
|
@@ -15,15 +15,33 @@ import { markDaemonStart } from './mcp-connection.js';
|
|
|
15
15
|
/** Let the daemon finish coming up before walking the source tree for a profile. */
|
|
16
16
|
const PROJECT_PROFILE_DELAY_MS = 5_000;
|
|
17
17
|
/**
|
|
18
|
-
* How often a
|
|
18
|
+
* How often a running daemon rolls up its window.
|
|
19
19
|
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
* minutes
|
|
23
|
-
*
|
|
24
|
-
*
|
|
20
|
+
* Exported because it is the BOUND ON WHAT IS LOST: a daemon that has served a tool never
|
|
21
|
+
* idle-exits, so nothing calls shutdown, so its last partial window dies with the process. At 30
|
|
22
|
+
* minutes against a median 28-minute session that meant the median session reported nothing at all.
|
|
23
|
+
* Only non-empty windows emit, so a shorter interval costs nothing on the 74% of daemons that never
|
|
24
|
+
* serve a tool.
|
|
25
25
|
*/
|
|
26
|
-
const SESSION_FLUSH_MS =
|
|
26
|
+
export const SESSION_FLUSH_MS = 5 * 60 * 1000;
|
|
27
|
+
/**
|
|
28
|
+
* When a session first rolls up, independent of the periodic interval.
|
|
29
|
+
*
|
|
30
|
+
* The interval alone leaves a hole exactly the width of itself: a daemon SIGKILLed before its first
|
|
31
|
+
* tick — a closed laptop, OOM, `kill -9`, a force-quit editor — reaches no shutdown handler and has
|
|
32
|
+
* emitted nothing, so the whole session is invisible.
|
|
33
|
+
*
|
|
34
|
+
* Measured 2026-08-10/11 (non-CI): **614 `daemon_started` against 499 `daemon_stopped` — 19% of
|
|
35
|
+
* sessions never reported a summary at all.** Every "did anyone use Reticle" number is computed on
|
|
36
|
+
* the 81% that did, and undercounts by an unknown amount, which is the one thing a funnel metric
|
|
37
|
+
* must not do.
|
|
38
|
+
*
|
|
39
|
+
* Ninety seconds is chosen against what it protects: a session that did real work usually does it
|
|
40
|
+
* early (snapshot -> act -> assert is seconds), so one early tick captures nearly all of it. It
|
|
41
|
+
* costs nothing on the daemons that dominate the population, because only a NON-EMPTY window emits
|
|
42
|
+
* and 74% of daemons never serve a tool at all.
|
|
43
|
+
*/
|
|
44
|
+
export const FIRST_FLUSH_MS = 90 * 1000;
|
|
27
45
|
/**
|
|
28
46
|
* Report the daemon as started, profile the project once, and keep the session counters flushing.
|
|
29
47
|
* Every timer is `unref`'d so telemetry can never be the reason a daemon refuses to exit.
|
|
@@ -48,26 +66,33 @@ export function installDaemonTelemetry(cwd, now = () => Date.now()) {
|
|
|
48
66
|
/* a profile is a nice-to-have; it must never touch the daemon */
|
|
49
67
|
}
|
|
50
68
|
}, PROJECT_PROFILE_DELAY_MS).unref();
|
|
51
|
-
const
|
|
69
|
+
const flushWindow = () => {
|
|
52
70
|
if (metrics.empty)
|
|
53
71
|
return; // an idle window is not worth an event
|
|
54
|
-
|
|
72
|
+
// NOT daemon_stopped: the daemon is still running. See SESSION_PROGRESS.
|
|
73
|
+
void getTelemetry().emit(TelemetryEventKind.SESSION_PROGRESS, {
|
|
55
74
|
session: metrics.summarize(false),
|
|
56
75
|
});
|
|
57
76
|
metrics.reset();
|
|
58
|
-
}
|
|
77
|
+
};
|
|
78
|
+
// The early one closes the hole the interval cannot: a session killed before its first tick had
|
|
79
|
+
// emitted nothing at all. See FIRST_FLUSH_MS.
|
|
80
|
+
const firstFlush = setTimeout(flushWindow, FIRST_FLUSH_MS);
|
|
81
|
+
firstFlush.unref();
|
|
82
|
+
const flush = setInterval(flushWindow, SESSION_FLUSH_MS);
|
|
59
83
|
flush.unref();
|
|
60
84
|
let stopped;
|
|
61
85
|
return {
|
|
62
|
-
shutdown() {
|
|
86
|
+
shutdown(exit) {
|
|
63
87
|
// Both SIGTERM and the idle-shutdown path can call this; the second caller awaits the first
|
|
64
88
|
// send rather than emitting a duplicate summary.
|
|
65
89
|
stopped ??= (async () => {
|
|
66
90
|
clearInterval(flush);
|
|
91
|
+
clearTimeout(firstFlush);
|
|
67
92
|
// The rich one: the whole session in a single event — duration, the tool histogram, error
|
|
68
93
|
// shapes, verifications, browser launches. This is what replaced the per-tool-call event.
|
|
69
94
|
await getTelemetry().emit(TelemetryEventKind.DAEMON_STOPPED, {
|
|
70
|
-
session: metrics.summarize(true),
|
|
95
|
+
session: metrics.summarize(true, exit),
|
|
71
96
|
});
|
|
72
97
|
})();
|
|
73
98
|
return stopped;
|
|
@@ -21,7 +21,7 @@ export function isReticleSourceCheckout(cwd) {
|
|
|
21
21
|
if (existsSync(pkg)) {
|
|
22
22
|
try {
|
|
23
23
|
const parsed = JSON.parse(readFileSync(pkg, 'utf8'));
|
|
24
|
-
const name = typeof parsed
|
|
24
|
+
const name = 'object' === typeof parsed && parsed !== null
|
|
25
25
|
? parsed.name
|
|
26
26
|
: undefined;
|
|
27
27
|
if (name === MONOREPO_NAME)
|
|
@@ -42,7 +42,7 @@ export function isReticleSourceCheckout(cwd) {
|
|
|
42
42
|
/** Strip one layer of matching quotes from a .env value. */
|
|
43
43
|
function unquote(value) {
|
|
44
44
|
const first = value[0];
|
|
45
|
-
if ((
|
|
45
|
+
if (('"' === first || "'" === first) && value.endsWith(first) && value.length >= 2) {
|
|
46
46
|
return value.slice(1, -1);
|
|
47
47
|
}
|
|
48
48
|
return value;
|
|
@@ -67,13 +67,13 @@ export function loadDotEnv(dir, env = process.env) {
|
|
|
67
67
|
}
|
|
68
68
|
for (const raw of text.split('\n')) {
|
|
69
69
|
const line = raw.trim();
|
|
70
|
-
if (line.length
|
|
70
|
+
if (0 === line.length || line.startsWith('#'))
|
|
71
71
|
continue;
|
|
72
72
|
const eq = line.indexOf('=');
|
|
73
73
|
if (eq <= 0)
|
|
74
74
|
continue; // not a KEY=value pair — skip rather than fail the whole file
|
|
75
75
|
const key = line.slice(0, eq).trim();
|
|
76
|
-
if (key.length
|
|
76
|
+
if (0 === key.length || env[key] !== undefined)
|
|
77
77
|
continue;
|
|
78
78
|
env[key] = unquote(line.slice(eq + 1).trim());
|
|
79
79
|
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Whose defect was that tool error?
|
|
3
|
+
*
|
|
4
|
+
* `toolErrors` was one number covering three failures with three different fixes, so the top-line
|
|
5
|
+
* count could be acted on only by unpacking `errors[]` by hand and reading the prose. Measured over
|
|
6
|
+
* 2026-08-10/11: 60 `state`, 22 `refusal`, 20 `schema` (serialized zod arrays) and 20 stale refs.
|
|
7
|
+
* Read as "126 tool errors" that hides the sixth of them where OUR schema failed to explain itself —
|
|
8
|
+
* which is the only bucket we can fix by writing better descriptions.
|
|
9
|
+
*
|
|
10
|
+
* Classified from the message SHAPE, never stored raw: this returns a bucket, and the caller keeps
|
|
11
|
+
* counts. The full message is already fingerprinted and skeletonised elsewhere.
|
|
12
|
+
*/
|
|
13
|
+
/** Whose defect. Ordered by which fix it implies. */
|
|
14
|
+
export declare const ErrorClass: {
|
|
15
|
+
/** Missing/unknown parameter, wrong type, unparseable predicate. **Our schema is unclear.** */
|
|
16
|
+
readonly SCHEMA: "schema";
|
|
17
|
+
/** No session, stale ref, disconnected. The world moved under the agent; not a schema problem. */
|
|
18
|
+
readonly STATE: "state";
|
|
19
|
+
/** We said no on purpose — a destructive block, an unsupported capability. Working as intended. */
|
|
20
|
+
readonly REFUSAL: "refusal";
|
|
21
|
+
/** Unrecognised. The bucket to watch: a large one is a blind spot, not a long tail. */
|
|
22
|
+
readonly OTHER: "other";
|
|
23
|
+
};
|
|
24
|
+
export type ErrorClass = (typeof ErrorClass)[keyof typeof ErrorClass];
|
|
25
|
+
/** Which bucket this message belongs to. Never throws — a classifier must not break a tool path. */
|
|
26
|
+
export declare function classifyError(message: string): ErrorClass;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Whose defect was that tool error?
|
|
3
|
+
*
|
|
4
|
+
* `toolErrors` was one number covering three failures with three different fixes, so the top-line
|
|
5
|
+
* count could be acted on only by unpacking `errors[]` by hand and reading the prose. Measured over
|
|
6
|
+
* 2026-08-10/11: 60 `state`, 22 `refusal`, 20 `schema` (serialized zod arrays) and 20 stale refs.
|
|
7
|
+
* Read as "126 tool errors" that hides the sixth of them where OUR schema failed to explain itself —
|
|
8
|
+
* which is the only bucket we can fix by writing better descriptions.
|
|
9
|
+
*
|
|
10
|
+
* Classified from the message SHAPE, never stored raw: this returns a bucket, and the caller keeps
|
|
11
|
+
* counts. The full message is already fingerprinted and skeletonised elsewhere.
|
|
12
|
+
*/
|
|
13
|
+
/** Whose defect. Ordered by which fix it implies. */
|
|
14
|
+
export const ErrorClass = {
|
|
15
|
+
/** Missing/unknown parameter, wrong type, unparseable predicate. **Our schema is unclear.** */
|
|
16
|
+
SCHEMA: 'schema',
|
|
17
|
+
/** No session, stale ref, disconnected. The world moved under the agent; not a schema problem. */
|
|
18
|
+
STATE: 'state',
|
|
19
|
+
/** We said no on purpose — a destructive block, an unsupported capability. Working as intended. */
|
|
20
|
+
REFUSAL: 'refusal',
|
|
21
|
+
/** Unrecognised. The bucket to watch: a large one is a blind spot, not a long tail. */
|
|
22
|
+
OTHER: 'other',
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* Patterns, most specific first.
|
|
26
|
+
*
|
|
27
|
+
* REFUSAL is tested before STATE because a destructive block often names a session too, and
|
|
28
|
+
* "we refused on purpose" is the more useful reading of that event.
|
|
29
|
+
*/
|
|
30
|
+
const PATTERNS = [
|
|
31
|
+
[/confirmdangerous|destructive action blocked/i, ErrorClass.REFUSAL],
|
|
32
|
+
[
|
|
33
|
+
/\brefus(e|ed|es|ing)\b|not supported|unsupported|cannot .* a <|contenteditable/i,
|
|
34
|
+
ErrorClass.REFUSAL,
|
|
35
|
+
],
|
|
36
|
+
[
|
|
37
|
+
/did not parse|unknown field|unrecognized_keys|missing required|requires a (string|number)|invalid parameter|expected .* received|is larger than/i,
|
|
38
|
+
ErrorClass.SCHEMA,
|
|
39
|
+
],
|
|
40
|
+
[
|
|
41
|
+
/no browser session connected|no connected session|multiple sessions connected|session disconnected|no longer resolves|no such session|not connected/i,
|
|
42
|
+
ErrorClass.STATE,
|
|
43
|
+
],
|
|
44
|
+
];
|
|
45
|
+
/** Which bucket this message belongs to. Never throws — a classifier must not break a tool path. */
|
|
46
|
+
export function classifyError(message) {
|
|
47
|
+
for (const [pattern, cls] of PATTERNS) {
|
|
48
|
+
if (pattern.test(message))
|
|
49
|
+
return cls;
|
|
50
|
+
}
|
|
51
|
+
return ErrorClass.OTHER;
|
|
52
|
+
}
|
|
@@ -19,6 +19,25 @@ import { createHash } from 'node:crypto';
|
|
|
19
19
|
* quoted value often contains the very numbers and paths the later rules would only partly catch.
|
|
20
20
|
*/
|
|
21
21
|
const VARIABLE_PARTS = [
|
|
22
|
+
// ── Identifiers and credentials FIRST ────────────────────────────────────────────────────────
|
|
23
|
+
//
|
|
24
|
+
// The rules below this block were written to make messages GROUPABLE — blank the variable parts so
|
|
25
|
+
// the same defect hashes the same everywhere. Redaction was a side effect of that, and a side
|
|
26
|
+
// effect is not a guarantee. A telemetry audit caught `bob@acme.com` arriving verbatim in
|
|
27
|
+
// `crash_message`; probing further, API-key-shaped tokens survived intact and a JWT was only
|
|
28
|
+
// half-masked. This function also feeds `session.errors[]` on EVERY session summary, so the
|
|
29
|
+
// exposure was every session that logged an error naming a user, not a rare crash path.
|
|
30
|
+
//
|
|
31
|
+
// Order matters: these run before the generic patterns, because `\d+` and the hex rule would chew
|
|
32
|
+
// a token into pieces that no longer match a credential shape while still leaking most of it —
|
|
33
|
+
// which is exactly what happened to the JWT.
|
|
34
|
+
/[\w.+-]+@[\w-]+\.[\w.-]+/g, // email addresses
|
|
35
|
+
/\beyJ[\w-]*\.[\w-]*\.?[\w-]*/g, // JWTs (header segment is always base64 `eyJ`)
|
|
36
|
+
/\b(?:sk|pk|rk|ghp|gho|ghs|ghu|ghr|github_pat|xox[abposr]|AKIA|ASIA|glpat)[_-][A-Za-z0-9_-]{8,}/gi,
|
|
37
|
+
// Anything else long enough and dense enough to be a secret rather than a word. The catch-all for
|
|
38
|
+
// formats nobody here has seen: on a privacy boundary, unrecognised is not the same as safe.
|
|
39
|
+
/\b[A-Za-z0-9_-]{24,}\b/g,
|
|
40
|
+
// ── Then the grouping rules ──────────────────────────────────────────────────────────────────
|
|
22
41
|
/"[^"]*"|'[^']*'|`[^`]*`/g, // quoted values — names, selectors, snippets
|
|
23
42
|
/\b[a-z][a-z0-9+.-]*:\/\/\S+/gi, // URLs
|
|
24
43
|
/(?:\/[\w.-]+){2,}\/?/g, // POSIX-ish paths
|
|
@@ -53,7 +72,7 @@ export function reticleFrames(stack) {
|
|
|
53
72
|
if (!/@reticlehq[/\\]|[/\\]reticle[/\\]dist[/\\]/.test(line))
|
|
54
73
|
continue;
|
|
55
74
|
const location = line.match(/([\w.-]+\.(?:js|mjs|cjs|ts)):(\d+)/);
|
|
56
|
-
if (
|
|
75
|
+
if (null === location)
|
|
57
76
|
continue;
|
|
58
77
|
// `at Object.runTool (/path/to/invoke-tool.js:88:3)` → `runTool`. The function name is half the
|
|
59
78
|
// value of a frame in an RCA — "it died in act-tools.js" narrows to a file, "it died in
|
|
@@ -62,7 +81,7 @@ export function reticleFrames(stack) {
|
|
|
62
81
|
const where = `${location[1] ?? ''}:${location[2] ?? ''}`;
|
|
63
82
|
// `Object.` / `Module.` prefixes are V8 noise that make the same function look like two.
|
|
64
83
|
const name = fn?.replace(/^(?:Object|Module|Function)\./, '');
|
|
65
|
-
frames.push(name === undefined ||
|
|
84
|
+
frames.push(name === undefined || '' === name ? where : `${name}@${where}`);
|
|
66
85
|
}
|
|
67
86
|
return frames;
|
|
68
87
|
}
|
|
@@ -1,15 +1,20 @@
|
|
|
1
|
-
import { TelemetryAction } from '../cli-parse.js';
|
|
1
|
+
import { TelemetryAction } from '../cli/cli-parse.js';
|
|
2
2
|
/**
|
|
3
|
-
* `reticle feedback [--rating 1-5] [--bug] "what happened"` — the
|
|
3
|
+
* `reticle feedback [--rating 1-5] [--bug] "what happened"` — the feedback channel as a command.
|
|
4
4
|
*
|
|
5
5
|
* Kept to a single command with no prompts, no editor, and no account: the cost of telling us
|
|
6
6
|
* something has to be lower than the cost of shrugging and moving on, or we only ever hear from people
|
|
7
7
|
* angry enough to open an issue. A bare rating is a valid report; the words are optional garnish.
|
|
8
8
|
*
|
|
9
|
+
* `--agent --kind <…>` is the same channel for an AGENT that cannot reach `reticle_feedback` — during
|
|
10
|
+
* `init`, while instrumentation is half-wired, or when the daemon will not start. Those are the
|
|
11
|
+
* reports we are least likely to hear and most need: every other path we have requires the tool
|
|
12
|
+
* surface to already work, so a Reticle that fails before it is running fails silently by design.
|
|
13
|
+
*
|
|
9
14
|
* What is sent is PRINTED before it goes, every time. A channel that carries free text and does not
|
|
10
15
|
* show you the payload is asking for trust it has not earned.
|
|
11
16
|
*/
|
|
12
|
-
export declare function handleFeedback(text: string, rating: number | undefined, bug: boolean): Promise<void>;
|
|
17
|
+
export declare function handleFeedback(text: string, rating: number | undefined, bug: boolean, feedbackKind?: string, agent?: boolean): Promise<void>;
|
|
13
18
|
/**
|
|
14
19
|
* `reticle identify` — opt-in, and it shows the consent notice before it sends anything.
|
|
15
20
|
*
|
|
@@ -1,34 +1,45 @@
|
|
|
1
1
|
import { FeedbackKind, FeedbackSource } from '@reticlehq/core';
|
|
2
|
-
import { BUG_FLAG, RATING_FLAG } from '../cli-parse.js';
|
|
2
|
+
import { AGENT_FLAG, BUG_FLAG, FEEDBACK_KINDS, KIND_FLAG, RATING_FLAG } from '../cli/cli-parse.js';
|
|
3
3
|
import { describeFeedbackPayload, submitFeedback } from './feedback.js';
|
|
4
4
|
import { describeTelemetry, setTelemetryEnabled } from './telemetry.js';
|
|
5
|
-
import { TelemetryAction } from '../cli-parse.js';
|
|
5
|
+
import { TelemetryAction } from '../cli/cli-parse.js';
|
|
6
6
|
import { clearIdentity, IDENTIFY_NOTICE, readIdentity, saveIdentity, submitIdentity, UsageContextKind, } from './identify.js';
|
|
7
7
|
/**
|
|
8
|
-
* `reticle feedback [--rating 1-5] [--bug] "what happened"` — the
|
|
8
|
+
* `reticle feedback [--rating 1-5] [--bug] "what happened"` — the feedback channel as a command.
|
|
9
9
|
*
|
|
10
10
|
* Kept to a single command with no prompts, no editor, and no account: the cost of telling us
|
|
11
11
|
* something has to be lower than the cost of shrugging and moving on, or we only ever hear from people
|
|
12
12
|
* angry enough to open an issue. A bare rating is a valid report; the words are optional garnish.
|
|
13
13
|
*
|
|
14
|
+
* `--agent --kind <…>` is the same channel for an AGENT that cannot reach `reticle_feedback` — during
|
|
15
|
+
* `init`, while instrumentation is half-wired, or when the daemon will not start. Those are the
|
|
16
|
+
* reports we are least likely to hear and most need: every other path we have requires the tool
|
|
17
|
+
* surface to already work, so a Reticle that fails before it is running fails silently by design.
|
|
18
|
+
*
|
|
14
19
|
* What is sent is PRINTED before it goes, every time. A channel that carries free text and does not
|
|
15
20
|
* show you the payload is asking for trust it has not earned.
|
|
16
21
|
*/
|
|
17
|
-
export async function handleFeedback(text, rating, bug) {
|
|
22
|
+
export async function handleFeedback(text, rating, bug, feedbackKind, agent = false) {
|
|
18
23
|
const line = (s) => {
|
|
19
24
|
process.stdout.write(`${s}\n`);
|
|
20
25
|
};
|
|
21
|
-
if (
|
|
26
|
+
if ('' === text && rating === undefined) {
|
|
22
27
|
line(`usage: reticle feedback [${RATING_FLAG} 1-5] [${BUG_FLAG}] "what worked, what didn't"`);
|
|
28
|
+
line(` agents: reticle feedback ${AGENT_FLAG} ${KIND_FLAG} <${FEEDBACK_KINDS.join('|')}> "what happened"`);
|
|
23
29
|
line(' your words go to the maintainers; nothing from your app is ever included.');
|
|
24
30
|
process.exitCode = 1;
|
|
25
31
|
return;
|
|
26
32
|
}
|
|
27
33
|
const receipt = await submitFeedback({
|
|
28
|
-
|
|
29
|
-
|
|
34
|
+
// An agent filing through the CLI must not land in the HUMAN bucket: that bucket carries the
|
|
35
|
+
// ratings and the experience reports, and a few hundred agent reports mixed into it would make
|
|
36
|
+
// the one number that says whether people like Reticle mean nothing.
|
|
37
|
+
source: agent ? FeedbackSource.AGENT : FeedbackSource.HUMAN,
|
|
38
|
+
// `--kind` wins when given; `--bug` stays the shorthand it always was.
|
|
39
|
+
kind: feedbackKind ??
|
|
40
|
+
(bug ? FeedbackKind.BUG : FeedbackKind.EXPERIENCE),
|
|
30
41
|
// A rating with no words is a real report, but the schema wants text — say what the rating means.
|
|
31
|
-
text:
|
|
42
|
+
text: '' === text ? `rated ${String(rating)}/5 with no comment` : text,
|
|
32
43
|
...(rating !== undefined ? { rating } : {}),
|
|
33
44
|
});
|
|
34
45
|
line(`sending ${JSON.stringify(describeFeedbackPayload(receipt.context))}`);
|
|
@@ -38,6 +49,39 @@ export async function handleFeedback(text, rating, bug) {
|
|
|
38
49
|
line(receipt.sent
|
|
39
50
|
? 'thanks your feedback is in. it genuinely changes what gets built next.'
|
|
40
51
|
: `not sent ${receipt.reason ?? 'unknown reason'}`);
|
|
52
|
+
for (const extra of identifyInvite(agent))
|
|
53
|
+
line(extra);
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Offer a HUMAN a way to be replied to — printed after the receipt, never as a prompt.
|
|
57
|
+
*
|
|
58
|
+
* Feedback is anonymous by design and stays that way: the report has already been sent by the time
|
|
59
|
+
* this prints, so nothing here gates or delays it. This is an offer, not a question.
|
|
60
|
+
*
|
|
61
|
+
* It routes through `reticle identify`, which already does consented identity properly — it prints
|
|
62
|
+
* what it will send, states that identifying links this machine's prior anonymous history to that
|
|
63
|
+
* identity, and has `--forget`. Capturing an address inline on `feedback_submitted` instead would
|
|
64
|
+
* put PII on the anonymous event stream and break what `docs/telemetry.md` promises in public:
|
|
65
|
+
* "no domain sniffing, no email inference" and "no personal data is collected".
|
|
66
|
+
*
|
|
67
|
+
* Only for humans (an agent has no email), and only when nobody has identified yet — so somebody
|
|
68
|
+
* who declined is never asked twice.
|
|
69
|
+
*/
|
|
70
|
+
function identifyInvite(agent) {
|
|
71
|
+
if (agent)
|
|
72
|
+
return [];
|
|
73
|
+
try {
|
|
74
|
+
if (readIdentity() !== undefined)
|
|
75
|
+
return [];
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
return []; // unreadable identity file: say nothing rather than risk asking twice
|
|
79
|
+
}
|
|
80
|
+
return [
|
|
81
|
+
'',
|
|
82
|
+
'want a reply? `reticle identify --context <company|side_project|oss|learning> --email you@example.com`',
|
|
83
|
+
' optional, and it tells you exactly what it sends before sending it.',
|
|
84
|
+
];
|
|
41
85
|
}
|
|
42
86
|
/**
|
|
43
87
|
* `reticle identify` — opt-in, and it shows the consent notice before it sends anything.
|