@reticlehq/server 2.4.0 → 2.5.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 +702 -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} +1 -1
- package/dist/{bridge.js → bridge/bridge.js} +63 -13
- package/dist/{bridge.test-harness.d.ts → bridge/bridge.test-harness.d.ts} +1 -11
- package/dist/{bridge.test-harness.js → bridge/bridge.test-harness.js} +9 -9
- 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-doctor.js → cli/cli-doctor.js} +9 -2
- 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-parse.d.ts → cli/cli-parse.d.ts} +2 -11
- package/dist/{cli-parse.js → cli/cli-parse.js} +113 -60
- 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.d.ts +2 -2
- package/dist/cli.js +174 -92
- 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 +49 -0
- package/dist/daemon/daemon-resilience.js +191 -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} +80 -48
- 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} +13 -0
- package/dist/{idle-shutdown.js → daemon/idle-shutdown.js} +27 -4
- 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 +43 -20
- package/dist/events/predicate-eval.js +159 -55
- package/dist/events/predicate.d.ts +2 -2
- package/dist/events/predicate.js +112 -35
- package/dist/events/reconcile.js +5 -5
- package/dist/events/ring-buffer.js +1 -1
- 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 +5 -7
- package/dist/flows/flow-replay.js +114 -47
- 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 +157 -6
- package/dist/flows/flow-success.js +20 -15
- package/dist/flows/flow-tools.js +53 -7
- 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/server-verify.d.ts +1 -1
- package/dist/flows/server-verify.js +5 -5
- 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 +16 -3
- package/dist/honesty/blind-spots.js +24 -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 +22 -3
- package/dist/honesty/verified.js +48 -8
- package/dist/http-server.js +14 -7
- package/dist/hunt/hunt-report.d.ts +2 -1
- package/dist/hunt/hunt-report.js +2 -2
- package/dist/index.d.ts +8 -6
- package/dist/index.js +32 -25
- package/dist/init/agent-rules.d.ts +2 -1
- package/dist/init/agent-rules.js +45 -7
- 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 +37 -0
- package/dist/init/cra.js +90 -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 +3 -2
- package/dist/init/detect.js +16 -5
- 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 +23 -6
- package/dist/init/plan-framework.d.ts +16 -1
- package/dist/init/plan-framework.js +132 -8
- package/dist/init/plan.d.ts +73 -8
- package/dist/init/plan.js +133 -34
- package/dist/init/project-id.d.ts +5 -9
- package/dist/init/project-id.js +10 -22
- package/dist/init/run.d.ts +5 -0
- package/dist/init/run.js +210 -36
- 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/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 +31 -0
- package/dist/mcp/mcp-outage.js +48 -0
- package/dist/mcp/mcp-proxy.d.ts +124 -0
- package/dist/mcp/mcp-proxy.js +697 -0
- package/dist/{mcp.d.ts → mcp/mcp.d.ts} +13 -10
- package/dist/{mcp.js → mcp/mcp.js} +110 -35
- 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/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/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 +38 -0
- package/dist/session/no-session-diagnosis.js +70 -0
- package/dist/session/no-session-watch.d.ts +19 -0
- package/dist/session/no-session-watch.js +61 -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/session-health.d.ts +8 -1
- package/dist/session/session-health.js +8 -7
- package/dist/session/session-info.d.ts +26 -7
- package/dist/session/session-info.js +42 -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 +3 -0
- package/dist/session/session-manager.js +52 -9
- 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 +14 -2
- package/dist/session/session.js +69 -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/daemon-telemetry.d.ts +19 -0
- package/dist/telemetry/daemon-telemetry.js +12 -2
- package/dist/telemetry/dev-repo.js +4 -4
- package/dist/telemetry/error-fingerprint.js +21 -2
- package/dist/telemetry/feedback-cli.d.ts +1 -1
- package/dist/telemetry/feedback-cli.js +4 -4
- package/dist/telemetry/feedback-context.d.ts +22 -0
- package/dist/telemetry/feedback-context.js +41 -1
- 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.js +1 -1
- package/dist/telemetry/session-metrics.d.ts +2 -0
- package/dist/telemetry/session-metrics.js +68 -6
- package/dist/telemetry/telemetry.d.ts +27 -2
- package/dist/telemetry/telemetry.js +119 -29
- 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 +84 -115
- 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 +3 -1
- package/dist/tools/assert-verdict.js +8 -5
- package/dist/tools/browser-tools.js +30 -11
- package/dist/tools/contract-tools.js +1 -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 +90 -18
- package/dist/tools/error-recovery.d.ts +20 -2
- package/dist/tools/error-recovery.js +179 -4
- 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 -6
- package/dist/tools/invoke-tool.js +57 -32
- 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-result.d.ts +17 -0
- package/dist/tools/navigate-result.js +31 -0
- package/dist/tools/observe-tools.js +54 -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 +88 -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 +8 -0
- package/dist/tools/tool-surface.d.ts +83 -0
- package/dist/tools/tool-surface.js +204 -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/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.md +28 -0
- package/docs/flows.md +166 -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/mint.json +61 -0
- package/docs/multi-agent-testing.md +55 -0
- package/docs/platform-integration.md +122 -0
- package/docs/telemetry-contract.md +214 -0
- package/docs/telemetry.md +184 -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-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-doctor.d.ts → cli/cli-doctor.d.ts} +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
package/dist/trace.js
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Verbose internal flow tracing, for people working ON Reticle.
|
|
3
|
+
*
|
|
4
|
+
* The gap it fills: the journal records what the AGENT did to the app, and telemetry records
|
|
5
|
+
* aggregates. Neither says which of OUR code a tool call went through, in what order, or where the
|
|
6
|
+
* time went — so every "why is this flow slow" and "which path produced this verdict" was
|
|
7
|
+
* reconstructed by hand from reading source. One line per stage answers both.
|
|
8
|
+
*
|
|
9
|
+
* Design, in order of what it cost to get wrong elsewhere in this repo:
|
|
10
|
+
*
|
|
11
|
+
* - OFF by default, and ~126ns per site when off (measured) — `span` calls the function and
|
|
12
|
+
* returns. A trace on every tool call is a real cost on the hot path, and the hot path is a
|
|
13
|
+
* verification loop of 50–200 calls.
|
|
14
|
+
* - ONE line per stage, emitted when it ENDS, carrying its own duration. A start line as well
|
|
15
|
+
* would double the volume to say something the end line already implies.
|
|
16
|
+
* - Correlated by a call id and a depth, carried in AsyncLocalStorage rather than threaded through
|
|
17
|
+
* every signature. Several agents are inside `runTool` at once — interleaving is the normal case
|
|
18
|
+
* — so without an id the output is a pile of unrelated lines, and instrumenting anything would
|
|
19
|
+
* mean changing its parameters, which is how instrumentation stops being added.
|
|
20
|
+
* - Through the existing `log()`, so it lands in the same stderr stream the daemon already
|
|
21
|
+
* redirects to `~/.reticle/daemon-<port>.log`. No new sink, no new file, nothing else to tail.
|
|
22
|
+
*/
|
|
23
|
+
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
24
|
+
import { ReticleEnv } from '@reticlehq/core';
|
|
25
|
+
import { log } from './log.js';
|
|
26
|
+
/** The `event` field every trace line carries, so one `grep` separates it from milestone logs. */
|
|
27
|
+
const TRACE_EVENT = 'trace';
|
|
28
|
+
const ON_VALUES = new Set(['1', 'true', 'on', 'yes']);
|
|
29
|
+
const context = new AsyncLocalStorage();
|
|
30
|
+
/**
|
|
31
|
+
* Monotonic within a process, and PID-qualified so it stays unique across them.
|
|
32
|
+
*
|
|
33
|
+
* A bare counter was not enough: the e2e battery runs many daemons, and a restarted daemon starts
|
|
34
|
+
* over at 1, so `c7` named a different call in every process. Aggregating that trace silently
|
|
35
|
+
* merged unrelated calls into one — measured on the first real trace collected, before it had a
|
|
36
|
+
* chance to mislead anything.
|
|
37
|
+
*/
|
|
38
|
+
let nextCallId = 0;
|
|
39
|
+
const CALL_ID_PREFIX = `p${String(process.pid)}-c`;
|
|
40
|
+
/**
|
|
41
|
+
* Whether tracing is on.
|
|
42
|
+
*
|
|
43
|
+
* Read per call, not cached at import, and the reason is `loadDotEnv`: the CLI populates
|
|
44
|
+
* `process.env` from the project's .env AFTER every module has been imported, so a value captured at
|
|
45
|
+
* import time would miss a `RETICLE_TRACE` set there — silently, which is the worst way for a
|
|
46
|
+
* diagnostic switch to fail. (The reason originally given here — "so it can be turned on while the
|
|
47
|
+
* daemon runs" — was wrong: nothing outside a process can change its environment.)
|
|
48
|
+
*
|
|
49
|
+
* Measured cost of reading it per call: 126ns per traced site when tracing is OFF, against a ~9ns
|
|
50
|
+
* bare call. With a handful of spans per tool call that is under a microsecond on a call that takes
|
|
51
|
+
* milliseconds — cheap enough to be worth the correctness, and not the literal zero this comment
|
|
52
|
+
* used to imply.
|
|
53
|
+
*/
|
|
54
|
+
export function traceEnabled() {
|
|
55
|
+
const raw = process.env[ReticleEnv.TRACE];
|
|
56
|
+
return raw !== undefined && ON_VALUES.has(raw.trim().toLowerCase());
|
|
57
|
+
}
|
|
58
|
+
/** The scope a stage runs in, plus how to report it. Shared by the async and sync forms. */
|
|
59
|
+
function openSpan(name, fields) {
|
|
60
|
+
const parent = context.getStore();
|
|
61
|
+
const scope = parent === undefined
|
|
62
|
+
? { callId: `${CALL_ID_PREFIX}${String((nextCallId += 1))}`, depth: 0 }
|
|
63
|
+
: parent;
|
|
64
|
+
const depth = parent === undefined ? 0 : parent.depth;
|
|
65
|
+
const startedAt = Date.now();
|
|
66
|
+
return {
|
|
67
|
+
// The child scope is what nested spans see: same call, one level deeper.
|
|
68
|
+
child: { callId: scope.callId, depth: depth + 1 },
|
|
69
|
+
emit: (ok, extra) => {
|
|
70
|
+
log(TRACE_EVENT, {
|
|
71
|
+
span: name,
|
|
72
|
+
ms: Date.now() - startedAt,
|
|
73
|
+
depth,
|
|
74
|
+
callId: scope.callId,
|
|
75
|
+
ok,
|
|
76
|
+
...fields,
|
|
77
|
+
...extra,
|
|
78
|
+
});
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Bind a callback to the call that is running NOW, so it reports inside that call when it fires later.
|
|
84
|
+
*
|
|
85
|
+
* `waitForPredicate` re-checks its predicate from a WebSocket event listener and an interval. Neither
|
|
86
|
+
* is in the awaited chain, so AsyncLocalStorage does not reach them and every re-check used to open a
|
|
87
|
+
* brand new call at depth 0. Measured on one healthy run: 165 spans across 60 callIds, 25 of them a
|
|
88
|
+
* `browser.command` with no matching `tool.handler`.
|
|
89
|
+
*
|
|
90
|
+
* That is not cosmetic. The documented signature of a HUNG tool call is exactly "a browser.command
|
|
91
|
+
* with no tool.handler for that callId, ever" — the invariant that located a real hang. A clean run
|
|
92
|
+
* emitting 23 of them makes the invariant unusable and hides the next hang in its own noise.
|
|
93
|
+
*
|
|
94
|
+
* Returns `fn` unchanged when there is no call to bind to, so a caller outside a span is not given a
|
|
95
|
+
* fabricated parent.
|
|
96
|
+
*/
|
|
97
|
+
export function bindSpanContext(fn) {
|
|
98
|
+
const captured = context.getStore();
|
|
99
|
+
if (captured === undefined)
|
|
100
|
+
return fn;
|
|
101
|
+
return (...args) => {
|
|
102
|
+
context.run(captured, () => fn(...args));
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
/** How much of a thrown value to keep. Enough to identify it, not enough to paste a stack into a log. */
|
|
106
|
+
const ERROR_CHARS = 300;
|
|
107
|
+
/**
|
|
108
|
+
* Run `fn` as a named stage, recording how long it took and how it sat inside the call around it.
|
|
109
|
+
*
|
|
110
|
+
* Wrap a stage worth a line in a bottleneck hunt — a session resolve, a browser round-trip, a
|
|
111
|
+
* predicate evaluation — not every function. The value of the trace is that reading it is faster
|
|
112
|
+
* than reading the code, and that stops being true somewhere around one line per statement.
|
|
113
|
+
*/
|
|
114
|
+
export async function span(name, fields, fn) {
|
|
115
|
+
if (!traceEnabled())
|
|
116
|
+
return fn();
|
|
117
|
+
const { child, emit } = openSpan(name, fields);
|
|
118
|
+
try {
|
|
119
|
+
const value = await context.run(child, async () => fn());
|
|
120
|
+
emit(true, {});
|
|
121
|
+
return value;
|
|
122
|
+
}
|
|
123
|
+
catch (err) {
|
|
124
|
+
// A stage that THREW is the one most worth having in the trace: without this line the trace
|
|
125
|
+
// shows a call that entered a stage and never left it, which reads as a hang.
|
|
126
|
+
emit(false, { error: String(err).slice(0, ERROR_CHARS) });
|
|
127
|
+
throw err;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* The synchronous form. Same line, same tree, same cost when off.
|
|
132
|
+
*
|
|
133
|
+
* It exists because `reticle init` — the flow every user hits before anything else, and the one
|
|
134
|
+
* whose slowness they experience personally — is synchronous end to end. An async-only span left
|
|
135
|
+
* exactly that flow untraceable.
|
|
136
|
+
*/
|
|
137
|
+
export function spanSync(name, fields, fn) {
|
|
138
|
+
if (!traceEnabled())
|
|
139
|
+
return fn();
|
|
140
|
+
const { child, emit } = openSpan(name, fields);
|
|
141
|
+
try {
|
|
142
|
+
const value = context.run(child, fn);
|
|
143
|
+
emit(true, {});
|
|
144
|
+
return value;
|
|
145
|
+
}
|
|
146
|
+
catch (err) {
|
|
147
|
+
emit(false, { error: String(err).slice(0, ERROR_CHARS) });
|
|
148
|
+
throw err;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
/** Record that an agent was told about `version`. Called when the nudge is actually delivered. */
|
|
2
|
+
export declare function creditNudge(version: string, dir?: string, now?: number): void;
|
|
3
|
+
/** True when this exact version was offered to an agent recently enough to have caused the update. */
|
|
4
|
+
export declare function wasNudged(version: string, dir?: string, now?: number): boolean;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Did the update nudge cause the update?
|
|
3
|
+
*
|
|
4
|
+
* `version_changed` says a version moved. It has never said why, so the only question worth asking
|
|
5
|
+
* about the nudge — does telling agents about a release make releases get installed — was
|
|
6
|
+
* unanswerable. That is not academic: 2.4.0 carried a fix for a connect defect affecting every Vite
|
|
7
|
+
* app and reached zero users, and nothing in the data could distinguish "the nudge never fired" from
|
|
8
|
+
* "the nudge fired and nobody acted on it". Those need opposite responses.
|
|
9
|
+
*
|
|
10
|
+
* The nudge is delivered by a DAEMON; `reticle update` runs in a different process. They cannot see
|
|
11
|
+
* each other in memory, so a marker file is the join. It holds the version that was offered and
|
|
12
|
+
* when — no identity, nothing about the machine, and it is overwritten rather than appended.
|
|
13
|
+
*
|
|
14
|
+
* Every function here swallows its own errors. This runs inside `reticle update`, which must finish
|
|
15
|
+
* whatever the telemetry believes.
|
|
16
|
+
*/
|
|
17
|
+
import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
18
|
+
import { homedir } from 'node:os';
|
|
19
|
+
import { join } from 'node:path';
|
|
20
|
+
const DEFAULT_DIR = join(homedir(), '.reticle');
|
|
21
|
+
const FILE = 'update-nudge.json';
|
|
22
|
+
/**
|
|
23
|
+
* How long a nudge can plausibly have caused an update. A week is generous for "the agent mentioned
|
|
24
|
+
* it and the human got round to it"; beyond that the two are unrelated events that happen to share a
|
|
25
|
+
* version number.
|
|
26
|
+
*/
|
|
27
|
+
const CREDIT_WINDOW_MS = 7 * 24 * 60 * 60 * 1000;
|
|
28
|
+
/** Record that an agent was told about `version`. Called when the nudge is actually delivered. */
|
|
29
|
+
export function creditNudge(version, dir = DEFAULT_DIR, now = Date.now()) {
|
|
30
|
+
try {
|
|
31
|
+
mkdirSync(dir, { recursive: true });
|
|
32
|
+
const record = { offered: version, at: now };
|
|
33
|
+
writeFileSync(join(dir, FILE), JSON.stringify(record), 'utf8');
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
/* a metric must never break the nudge */
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
/** True when this exact version was offered to an agent recently enough to have caused the update. */
|
|
40
|
+
export function wasNudged(version, dir = DEFAULT_DIR, now = Date.now()) {
|
|
41
|
+
try {
|
|
42
|
+
const parsed = JSON.parse(readFileSync(join(dir, FILE), 'utf8'));
|
|
43
|
+
if (typeof parsed !== 'object' || null === parsed)
|
|
44
|
+
return false;
|
|
45
|
+
const record = parsed;
|
|
46
|
+
if (record.offered !== version || typeof record.at !== 'number')
|
|
47
|
+
return false;
|
|
48
|
+
return now - record.at <= CREDIT_WINDOW_MS;
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The half of `reticle update` that was missing: bringing the APP's SDK to the same version as the
|
|
3
|
+
* daemon being installed.
|
|
4
|
+
*
|
|
5
|
+
* `reticle update` swapped the CLI and stopped. The app kept whatever `@reticlehq/react` it had, so
|
|
6
|
+
* the command whose job is to keep an install current was itself a way to create a version-skewed
|
|
7
|
+
* pair — the failure the HELLO skew check, the nudge and the pin all exist to prevent. The skew
|
|
8
|
+
* message even told people to run `reticle update` to fix an outdated SDK, which could not work.
|
|
9
|
+
*
|
|
10
|
+
* Pinned to an exact version for the same reason `init` pins: an unpinned `add` can resolve out of a
|
|
11
|
+
* stale registry cache and reinstall the skew being fixed. Measured once as pnpm taking 2.2.1 while
|
|
12
|
+
* npm took 2.3.0 in the next project over.
|
|
13
|
+
*/
|
|
14
|
+
import { type PackageManager } from '../init/detect.js';
|
|
15
|
+
/** The Reticle packages this project declares, from either dependency section. */
|
|
16
|
+
export declare function reticleDepsOf(pkgJson: unknown): string[];
|
|
17
|
+
/** The install to run, or null when this project has nothing of ours to sync. */
|
|
18
|
+
export declare function sdkSyncCommand(pm: PackageManager, packages: readonly string[], version: string): {
|
|
19
|
+
command: string;
|
|
20
|
+
args: string[];
|
|
21
|
+
} | null;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The half of `reticle update` that was missing: bringing the APP's SDK to the same version as the
|
|
3
|
+
* daemon being installed.
|
|
4
|
+
*
|
|
5
|
+
* `reticle update` swapped the CLI and stopped. The app kept whatever `@reticlehq/react` it had, so
|
|
6
|
+
* the command whose job is to keep an install current was itself a way to create a version-skewed
|
|
7
|
+
* pair — the failure the HELLO skew check, the nudge and the pin all exist to prevent. The skew
|
|
8
|
+
* message even told people to run `reticle update` to fix an outdated SDK, which could not work.
|
|
9
|
+
*
|
|
10
|
+
* Pinned to an exact version for the same reason `init` pins: an unpinned `add` can resolve out of a
|
|
11
|
+
* stale registry cache and reinstall the skew being fixed. Measured once as pnpm taking 2.2.1 while
|
|
12
|
+
* npm took 2.3.0 in the next project over.
|
|
13
|
+
*/
|
|
14
|
+
import { installCommandParts } from '../init/detect.js';
|
|
15
|
+
/** Everything we publish is scoped here. */
|
|
16
|
+
const RETICLE_SCOPE = '@reticlehq/';
|
|
17
|
+
/**
|
|
18
|
+
* The CLI. Never an app dependency: it carries the Node MCP server and `ws`, and putting it in a
|
|
19
|
+
* browser app's tree is what the retired `@reticlehq/core` umbrella got wrong. The other half of
|
|
20
|
+
* `reticle update` owns this one.
|
|
21
|
+
*/
|
|
22
|
+
const SERVER_PACKAGE = '@reticlehq/server';
|
|
23
|
+
function namesIn(section) {
|
|
24
|
+
if ('object' !== typeof section || null === section)
|
|
25
|
+
return [];
|
|
26
|
+
return Object.keys(section);
|
|
27
|
+
}
|
|
28
|
+
/** The Reticle packages this project declares, from either dependency section. */
|
|
29
|
+
export function reticleDepsOf(pkgJson) {
|
|
30
|
+
if ('object' !== typeof pkgJson || null === pkgJson)
|
|
31
|
+
return [];
|
|
32
|
+
const manifest = pkgJson;
|
|
33
|
+
const declared = [...namesIn(manifest['dependencies']), ...namesIn(manifest['devDependencies'])];
|
|
34
|
+
return [...new Set(declared)].filter((name) => name.startsWith(RETICLE_SCOPE) && name !== SERVER_PACKAGE);
|
|
35
|
+
}
|
|
36
|
+
/** The install to run, or null when this project has nothing of ours to sync. */
|
|
37
|
+
export function sdkSyncCommand(pm, packages, version) {
|
|
38
|
+
if (0 === packages.length)
|
|
39
|
+
return null;
|
|
40
|
+
const { command, args } = installCommandParts(pm, packages.map((p) => `${p}@${version}`));
|
|
41
|
+
return { command, args: [...args] };
|
|
42
|
+
}
|
|
@@ -3,7 +3,7 @@ import * as https from 'node:https';
|
|
|
3
3
|
import { join } from 'node:path';
|
|
4
4
|
import { homedir } from 'node:os';
|
|
5
5
|
import { UpdateCheckIntervalMs } from '@reticlehq/core';
|
|
6
|
-
import { RETICLE_NPM_PACKAGE } from '../server-version.js';
|
|
6
|
+
import { RETICLE_NPM_PACKAGE } from '../version/server-version.js';
|
|
7
7
|
import { log } from '../log.js';
|
|
8
8
|
const RETICLE_HOME = join(homedir(), '.reticle');
|
|
9
9
|
const MANIFEST_PATH = join(RETICLE_HOME, 'update-manifest.json');
|
|
@@ -20,12 +20,12 @@ function validateNpmInfo(info) {
|
|
|
20
20
|
if (typeof info.version !== 'string' || !VERSION_RE.test(info.version)) {
|
|
21
21
|
throw new Error('npm registry returned an implausible version');
|
|
22
22
|
}
|
|
23
|
-
const changelog = typeof info.reticle?.changelog
|
|
23
|
+
const changelog = 'string' === typeof info.reticle?.changelog
|
|
24
24
|
? info.reticle.changelog.slice(0, MAX_MANIFEST_TEXT)
|
|
25
25
|
: undefined;
|
|
26
26
|
const breakingChanges = Array.isArray(info.reticle?.breakingChanges)
|
|
27
27
|
? info.reticle.breakingChanges
|
|
28
|
-
.filter((x) => typeof x
|
|
28
|
+
.filter((x) => 'string' === typeof x)
|
|
29
29
|
.slice(0, 100)
|
|
30
30
|
.map((s) => s.slice(0, MAX_MANIFEST_TEXT))
|
|
31
31
|
: undefined;
|
|
@@ -1,4 +1,14 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Is `candidate` a strictly NEWER release than `current`?
|
|
3
|
+
*
|
|
4
|
+
* Replaces a plain `!==`, which told anyone on a version newer than the published one — a
|
|
5
|
+
* prerelease, a local build, a rollback in progress — to "update" to something older. A nudge that
|
|
6
|
+
* prompts a downgrade is one people learn to ignore, and this is the mechanism the entire adoption
|
|
7
|
+
* story rests on. Numeric per segment, so 2.10.0 correctly beats 2.9.0 (string order does not), and
|
|
8
|
+
* a bare release beats its own prerelease.
|
|
9
|
+
*/
|
|
10
|
+
export declare function isNewerVersion(candidate: string, current: string): boolean;
|
|
11
|
+
interface UpdateNudge {
|
|
2
12
|
/** The version available, so the agent can tell the human what they would be moving to. */
|
|
3
13
|
latestVersion: string;
|
|
4
14
|
currentVersion: string;
|
|
@@ -7,14 +17,61 @@ export interface UpdateNudge {
|
|
|
7
17
|
action: string;
|
|
8
18
|
}
|
|
9
19
|
/**
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
20
|
+
* Describe the release, not just its number.
|
|
21
|
+
*
|
|
22
|
+
* `packages/server/package.json` carries `reticle.changelog` and `reticle.breakingChanges`, and
|
|
23
|
+
* `update-checker` parses both into the manifest — where they stopped. This function named two
|
|
24
|
+
* versions and nothing else, so both fields were written, shipped, parsed and discarded. Latent
|
|
25
|
+
* metadata nobody ever printed.
|
|
26
|
+
*
|
|
27
|
+
* Survivable while releases are additive; actively harmful for one that is not. An agent told only
|
|
28
|
+
* "2.4.1 → 2.5.0" runs `reticle update` mid-task and discovers a retired environment variable and
|
|
29
|
+
* six newly-strict parameters by breaking on them.
|
|
30
|
+
*
|
|
31
|
+
* Bounded on purpose: this rides on a tool result every turn until it is delivered, so a long
|
|
32
|
+
* changelog is a per-turn tax. The count of anything elided is stated rather than silently dropped.
|
|
13
33
|
*/
|
|
34
|
+
export declare function buildNudge(latestVersion: string, currentVersion: string, release?: {
|
|
35
|
+
changelog?: string;
|
|
36
|
+
breakingChanges?: string[];
|
|
37
|
+
}): UpdateNudge;
|
|
14
38
|
export declare function startUpdateCheck(now?: () => number): void;
|
|
39
|
+
/**
|
|
40
|
+
* What `reticle update` should install, or undefined when it should do nothing.
|
|
41
|
+
*
|
|
42
|
+
* `handleUpdate` gated on `manifest.updateAvailable`, a plain `latest !== current`, so whenever the
|
|
43
|
+
* registry's latest was OLDER than the running build — a prerelease, a local build, a rollback in
|
|
44
|
+
* progress, a stale npx cache — it announced the move and then installed the downgrade. Reported by
|
|
45
|
+
* a real user on 2026-08-06: "both the `update_available` banner and `reticle update` report the
|
|
46
|
+
* current and target versions swapped, so an upgrade is described as a downgrade."
|
|
47
|
+
*/
|
|
48
|
+
export declare function updateTarget(manifest: {
|
|
49
|
+
updateAvailable?: boolean;
|
|
50
|
+
latestVersion?: string;
|
|
51
|
+
}, currentVersion?: string): string | undefined;
|
|
52
|
+
/**
|
|
53
|
+
* Arm from an already-cached answer, synchronously, at daemon boot.
|
|
54
|
+
*
|
|
55
|
+
* The network check fires 8s after boot and the nudge is delivered by riding a tool result — but
|
|
56
|
+
* half the sessions that use Reticle at all make exactly ONE tool call, usually in the first
|
|
57
|
+
* seconds. The check had not finished, so the single chance to tell them was gone, and the
|
|
58
|
+
* population most in need of the update is precisely the one that never heard about it. Yesterday's
|
|
59
|
+
* answer is already on disk; use it now and let the network refresh behind it.
|
|
60
|
+
*/
|
|
61
|
+
export declare function armUpdateNudgeFrom(cached: {
|
|
62
|
+
latestVersion?: string;
|
|
63
|
+
updateAvailable?: boolean;
|
|
64
|
+
changelog?: string;
|
|
65
|
+
breakingChanges?: string[];
|
|
66
|
+
} | null, currentVersion?: string): void;
|
|
15
67
|
/** The nudge, once. Returns undefined afterwards so a long session is told exactly one time. */
|
|
16
68
|
export declare function takeUpdateNudge(): UpdateNudge | undefined;
|
|
69
|
+
/**
|
|
70
|
+
* The newer version, if the cached answer says there is one — WITHOUT consuming the agent's
|
|
71
|
+
* one-shot nudge. `reticle status` is a human reading a terminal; the two channels are separate
|
|
72
|
+
* audiences and must not steal each other's message.
|
|
73
|
+
*/
|
|
74
|
+
export declare function availableUpdate(currentVersion?: string): string | undefined;
|
|
17
75
|
/** Tests only — drop the module state so each case starts clean. */
|
|
18
76
|
export declare function resetUpdateNudge(): void;
|
|
19
|
-
|
|
20
|
-
export declare function setPendingUpdate(nudge: UpdateNudge | undefined): void;
|
|
77
|
+
export {};
|
|
@@ -16,9 +16,35 @@
|
|
|
16
16
|
* human never agreed to interrupt. So the envelope carries the fact and the exact command, and the
|
|
17
17
|
* decision stays with the people whose machine it is.
|
|
18
18
|
*/
|
|
19
|
-
import { checkForUpdate } from './update-checker.js';
|
|
20
|
-
import { SERVER_VERSION } from '../server-version.js';
|
|
19
|
+
import { checkForUpdate, loadManifest } from './update-checker.js';
|
|
20
|
+
import { SERVER_VERSION } from '../version/server-version.js';
|
|
21
21
|
import { log } from '../log.js';
|
|
22
|
+
import { creditNudge } from './nudge-credit.js';
|
|
23
|
+
/**
|
|
24
|
+
* Is `candidate` a strictly NEWER release than `current`?
|
|
25
|
+
*
|
|
26
|
+
* Replaces a plain `!==`, which told anyone on a version newer than the published one — a
|
|
27
|
+
* prerelease, a local build, a rollback in progress — to "update" to something older. A nudge that
|
|
28
|
+
* prompts a downgrade is one people learn to ignore, and this is the mechanism the entire adoption
|
|
29
|
+
* story rests on. Numeric per segment, so 2.10.0 correctly beats 2.9.0 (string order does not), and
|
|
30
|
+
* a bare release beats its own prerelease.
|
|
31
|
+
*/
|
|
32
|
+
export function isNewerVersion(candidate, current) {
|
|
33
|
+
const parse = (v) => {
|
|
34
|
+
const [core = '', ...rest] = v.split(/[-+]/);
|
|
35
|
+
return { parts: core.split('.').map((n) => Number.parseInt(n, 10) || 0), pre: rest.length > 0 };
|
|
36
|
+
};
|
|
37
|
+
const a = parse(candidate);
|
|
38
|
+
const b = parse(current);
|
|
39
|
+
for (let i = 0; i < 3; i++) {
|
|
40
|
+
const left = a.parts[i] ?? 0;
|
|
41
|
+
const right = b.parts[i] ?? 0;
|
|
42
|
+
if (left !== right)
|
|
43
|
+
return left > right;
|
|
44
|
+
}
|
|
45
|
+
// Same numbers: a final release is newer than its own prerelease, and nothing else counts.
|
|
46
|
+
return !a.pre && b.pre;
|
|
47
|
+
}
|
|
22
48
|
/** Let the daemon finish coming up before touching the network. */
|
|
23
49
|
const CHECK_DELAY_MS = 8_000;
|
|
24
50
|
const UPDATE_COMMAND = 'reticle update';
|
|
@@ -29,20 +55,63 @@ let delivered = false;
|
|
|
29
55
|
* for 24h on disk, and falls back to the cached manifest when the registry is unreachable — so this
|
|
30
56
|
* costs one npm registry request per day per machine, and nothing at all when offline.
|
|
31
57
|
*/
|
|
58
|
+
/** How much of the release description may ride on a tool result. See buildNudge. */
|
|
59
|
+
const MAX_CHANGELOG_CHARS = 240;
|
|
60
|
+
const MAX_LISTED_BREAKING = 4;
|
|
61
|
+
/**
|
|
62
|
+
* Describe the release, not just its number.
|
|
63
|
+
*
|
|
64
|
+
* `packages/server/package.json` carries `reticle.changelog` and `reticle.breakingChanges`, and
|
|
65
|
+
* `update-checker` parses both into the manifest — where they stopped. This function named two
|
|
66
|
+
* versions and nothing else, so both fields were written, shipped, parsed and discarded. Latent
|
|
67
|
+
* metadata nobody ever printed.
|
|
68
|
+
*
|
|
69
|
+
* Survivable while releases are additive; actively harmful for one that is not. An agent told only
|
|
70
|
+
* "2.4.1 → 2.5.0" runs `reticle update` mid-task and discovers a retired environment variable and
|
|
71
|
+
* six newly-strict parameters by breaking on them.
|
|
72
|
+
*
|
|
73
|
+
* Bounded on purpose: this rides on a tool result every turn until it is delivered, so a long
|
|
74
|
+
* changelog is a per-turn tax. The count of anything elided is stated rather than silently dropped.
|
|
75
|
+
*/
|
|
76
|
+
export function buildNudge(latestVersion, currentVersion, release) {
|
|
77
|
+
const changelog = (release?.changelog ?? '').trim();
|
|
78
|
+
const breaking = (release?.breakingChanges ?? []).filter((line) => 0 < line.trim().length);
|
|
79
|
+
const listed = breaking.slice(0, MAX_LISTED_BREAKING);
|
|
80
|
+
const rest = breaking.length - listed.length;
|
|
81
|
+
return {
|
|
82
|
+
currentVersion,
|
|
83
|
+
latestVersion,
|
|
84
|
+
command: UPDATE_COMMAND,
|
|
85
|
+
action: `A newer Reticle is available (${currentVersion} → ${latestVersion}). ` +
|
|
86
|
+
('' === changelog ? '' : `${changelog.slice(0, MAX_CHANGELOG_CHARS)} `) +
|
|
87
|
+
(0 === breaking.length
|
|
88
|
+
? ''
|
|
89
|
+
: `This release has BREAKING changes: ${listed.join('; ')}` +
|
|
90
|
+
(0 < rest ? ` (+${String(rest)} more — see the changelog)` : '') +
|
|
91
|
+
'. Read them before updating. ') +
|
|
92
|
+
`Tell the human, and run \`${UPDATE_COMMAND}\` if they agree — it restarts the daemon, ` +
|
|
93
|
+
'so do it between tasks rather than mid-verification. Continue your current task first.',
|
|
94
|
+
};
|
|
95
|
+
}
|
|
32
96
|
export function startUpdateCheck(now = () => Date.now()) {
|
|
97
|
+
// The cached answer first, so the very first tool call can carry it — see armUpdateNudgeFrom.
|
|
98
|
+
armUpdateNudgeFrom(loadManifest());
|
|
33
99
|
setTimeout(() => {
|
|
34
100
|
void checkForUpdate(SERVER_VERSION, now)
|
|
35
101
|
.then((manifest) => {
|
|
36
102
|
if (!manifest.updateAvailable || manifest.latestVersion === undefined)
|
|
37
103
|
return;
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
104
|
+
if (!isNewerVersion(manifest.latestVersion, SERVER_VERSION))
|
|
105
|
+
return;
|
|
106
|
+
// WITH the release description. The manifest has carried `changelog` and `breakingChanges`
|
|
107
|
+
// all along; nothing read them.
|
|
108
|
+
pending = buildNudge(manifest.latestVersion, SERVER_VERSION, {
|
|
109
|
+
...(manifest.changelog === undefined ? {} : { changelog: manifest.changelog }),
|
|
110
|
+
...(manifest.breakingChanges === undefined
|
|
111
|
+
? {}
|
|
112
|
+
: { breakingChanges: manifest.breakingChanges }),
|
|
113
|
+
});
|
|
114
|
+
delivered = false;
|
|
46
115
|
log('reticle_update_available', {
|
|
47
116
|
from: SERVER_VERSION,
|
|
48
117
|
to: manifest.latestVersion,
|
|
@@ -53,20 +122,64 @@ export function startUpdateCheck(now = () => Date.now()) {
|
|
|
53
122
|
});
|
|
54
123
|
}, CHECK_DELAY_MS).unref();
|
|
55
124
|
}
|
|
125
|
+
/**
|
|
126
|
+
* What `reticle update` should install, or undefined when it should do nothing.
|
|
127
|
+
*
|
|
128
|
+
* `handleUpdate` gated on `manifest.updateAvailable`, a plain `latest !== current`, so whenever the
|
|
129
|
+
* registry's latest was OLDER than the running build — a prerelease, a local build, a rollback in
|
|
130
|
+
* progress, a stale npx cache — it announced the move and then installed the downgrade. Reported by
|
|
131
|
+
* a real user on 2026-08-06: "both the `update_available` banner and `reticle update` report the
|
|
132
|
+
* current and target versions swapped, so an upgrade is described as a downgrade."
|
|
133
|
+
*/
|
|
134
|
+
export function updateTarget(manifest, currentVersion = SERVER_VERSION) {
|
|
135
|
+
const latest = manifest.latestVersion;
|
|
136
|
+
if (latest === undefined || !isNewerVersion(latest, currentVersion))
|
|
137
|
+
return undefined;
|
|
138
|
+
return latest;
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Arm from an already-cached answer, synchronously, at daemon boot.
|
|
142
|
+
*
|
|
143
|
+
* The network check fires 8s after boot and the nudge is delivered by riding a tool result — but
|
|
144
|
+
* half the sessions that use Reticle at all make exactly ONE tool call, usually in the first
|
|
145
|
+
* seconds. The check had not finished, so the single chance to tell them was gone, and the
|
|
146
|
+
* population most in need of the update is precisely the one that never heard about it. Yesterday's
|
|
147
|
+
* answer is already on disk; use it now and let the network refresh behind it.
|
|
148
|
+
*/
|
|
149
|
+
export function armUpdateNudgeFrom(cached, currentVersion = SERVER_VERSION) {
|
|
150
|
+
const latest = cached?.latestVersion;
|
|
151
|
+
if (latest === undefined || !isNewerVersion(latest, currentVersion))
|
|
152
|
+
return;
|
|
153
|
+
// The CACHED path matters more than the live one: it is what the very first tool call carries, and
|
|
154
|
+
// it is the only nudge an offline machine ever sees. Dropping the release description here would
|
|
155
|
+
// have left the fields unread on the commonest path while looking fixed.
|
|
156
|
+
pending = buildNudge(latest, currentVersion, {
|
|
157
|
+
...(cached?.changelog === undefined ? {} : { changelog: cached.changelog }),
|
|
158
|
+
...(cached?.breakingChanges === undefined ? {} : { breakingChanges: cached.breakingChanges }),
|
|
159
|
+
});
|
|
160
|
+
delivered = false;
|
|
161
|
+
}
|
|
56
162
|
/** The nudge, once. Returns undefined afterwards so a long session is told exactly one time. */
|
|
57
163
|
export function takeUpdateNudge() {
|
|
58
164
|
if (delivered || pending === undefined)
|
|
59
165
|
return undefined;
|
|
60
166
|
delivered = true;
|
|
167
|
+
// Leave a mark an `reticle update` in another process can read, so version_changed can say whether
|
|
168
|
+
// this nudge is what caused the upgrade. Best-effort by construction — see nudge-credit.
|
|
169
|
+
creditNudge(pending.latestVersion);
|
|
61
170
|
return pending;
|
|
62
171
|
}
|
|
172
|
+
/**
|
|
173
|
+
* The newer version, if the cached answer says there is one — WITHOUT consuming the agent's
|
|
174
|
+
* one-shot nudge. `reticle status` is a human reading a terminal; the two channels are separate
|
|
175
|
+
* audiences and must not steal each other's message.
|
|
176
|
+
*/
|
|
177
|
+
export function availableUpdate(currentVersion = SERVER_VERSION) {
|
|
178
|
+
const latest = loadManifest()?.latestVersion;
|
|
179
|
+
return latest !== undefined && isNewerVersion(latest, currentVersion) ? latest : undefined;
|
|
180
|
+
}
|
|
63
181
|
/** Tests only — drop the module state so each case starts clean. */
|
|
64
182
|
export function resetUpdateNudge() {
|
|
65
183
|
pending = undefined;
|
|
66
184
|
delivered = false;
|
|
67
185
|
}
|
|
68
|
-
/** Tests only — seed a pending nudge without going near the network. */
|
|
69
|
-
export function setPendingUpdate(nudge) {
|
|
70
|
-
pending = nudge;
|
|
71
|
-
delivered = false;
|
|
72
|
-
}
|
package/dist/update/updater.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/** Which way the installed version moved. A rollback means a release hurt someone enough to retreat. */
|
|
2
|
-
|
|
2
|
+
declare const VersionChangeDirection: {
|
|
3
3
|
readonly UPDATE: "update";
|
|
4
4
|
readonly ROLLBACK: "rollback";
|
|
5
5
|
};
|
|
@@ -22,14 +22,6 @@ declare const ExecutionKind: {
|
|
|
22
22
|
readonly LOCAL: "local";
|
|
23
23
|
};
|
|
24
24
|
type ExecutionKind = (typeof ExecutionKind)[keyof typeof ExecutionKind];
|
|
25
|
-
/**
|
|
26
|
-
* Infer how reticle was launched from process.argv[1].
|
|
27
|
-
*
|
|
28
|
-
* npm's npx cache stores packages under a path containing `_npx`, which is the
|
|
29
|
-
* most reliable cross-platform signal. Local installs always live inside a
|
|
30
|
-
* `node_modules` directory. Everything else is treated as a global install.
|
|
31
|
-
*/
|
|
32
|
-
export declare function detectExecutionKind(): ExecutionKind;
|
|
33
25
|
/**
|
|
34
26
|
* Classify a launch from the entry script path. Pure so it's testable. Global installs ALSO live under
|
|
35
27
|
* a `node_modules`, but under the npm global prefix — a `lib/node_modules` (unix: /usr/local, Homebrew,
|
package/dist/update/updater.js
CHANGED
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
import { execFile } from 'node:child_process';
|
|
2
|
+
import { NodePlatform } from '../platform.js';
|
|
2
3
|
import { existsSync } from 'node:fs';
|
|
3
4
|
import { platform } from 'node:os';
|
|
4
5
|
import { dirname, join } from 'node:path';
|
|
5
6
|
import { loadManifest, saveManifest } from './update-checker.js';
|
|
6
|
-
import { RETICLE_NPM_PACKAGE, SERVER_VERSION } from '../server-version.js';
|
|
7
|
+
import { RETICLE_NPM_PACKAGE, SERVER_VERSION } from '../version/server-version.js';
|
|
7
8
|
import { log } from '../log.js';
|
|
8
9
|
import { TelemetryEventKind } from '@reticlehq/core';
|
|
9
10
|
import { getTelemetry } from '../telemetry/telemetry.js';
|
|
11
|
+
import { wasNudged } from './nudge-credit.js';
|
|
10
12
|
/** Which way the installed version moved. A rollback means a release hurt someone enough to retreat. */
|
|
11
|
-
|
|
13
|
+
const VersionChangeDirection = {
|
|
12
14
|
UPDATE: 'update',
|
|
13
15
|
ROLLBACK: 'rollback',
|
|
14
16
|
};
|
|
@@ -21,15 +23,18 @@ export const VersionChangeDirection = {
|
|
|
21
23
|
*/
|
|
22
24
|
export async function reportVersionChange(from, to, direction) {
|
|
23
25
|
try {
|
|
26
|
+
const nudged = wasNudged(to);
|
|
24
27
|
await getTelemetry().emit(TelemetryEventKind.VERSION_CHANGED, {
|
|
25
|
-
|
|
28
|
+
// Only when true: an absent flag reads as "we do not know", which is honest for a machine that
|
|
29
|
+
// updated from a shell script or a package bump with no agent involved at all.
|
|
30
|
+
versionChange: { from, to, direction, ...(nudged ? { nudged } : {}) },
|
|
26
31
|
});
|
|
27
32
|
}
|
|
28
33
|
catch {
|
|
29
34
|
/* best-effort, like every other send */
|
|
30
35
|
}
|
|
31
36
|
}
|
|
32
|
-
const NPM_BIN = platform()
|
|
37
|
+
const NPM_BIN = NodePlatform.WINDOWS === platform() ? 'npm.cmd' : 'npm';
|
|
33
38
|
const NPM_TIMEOUT_MS = 120_000;
|
|
34
39
|
/** How this reticle process was launched — determines which npm strategy to use for updates. */
|
|
35
40
|
const ExecutionKind = {
|
|
@@ -47,7 +52,7 @@ const ExecutionKind = {
|
|
|
47
52
|
* most reliable cross-platform signal. Local installs always live inside a
|
|
48
53
|
* `node_modules` directory. Everything else is treated as a global install.
|
|
49
54
|
*/
|
|
50
|
-
|
|
55
|
+
function detectExecutionKind() {
|
|
51
56
|
return classifyExecutionKind(process.argv[1] ?? '');
|
|
52
57
|
}
|
|
53
58
|
/**
|
|
@@ -116,7 +121,7 @@ export function installArgs(version, kind, localRoot) {
|
|
|
116
121
|
async function installVersion(version, kind) {
|
|
117
122
|
const localRoot = kind === ExecutionKind.LOCAL ? findLocalProjectRoot() : null;
|
|
118
123
|
const plan = installArgs(version, kind, localRoot);
|
|
119
|
-
if (
|
|
124
|
+
if (null === plan) {
|
|
120
125
|
// npx re-resolves the package from npm on the next Claude Code restart — no npm
|
|
121
126
|
// install needed. The restart itself is what triggers the update.
|
|
122
127
|
log('reticle_update_npx_strategy', {
|
|
@@ -124,7 +129,7 @@ async function installVersion(version, kind) {
|
|
|
124
129
|
});
|
|
125
130
|
return;
|
|
126
131
|
}
|
|
127
|
-
if (kind === ExecutionKind.LOCAL &&
|
|
132
|
+
if (kind === ExecutionKind.LOCAL && null === localRoot) {
|
|
128
133
|
// Could not find a project root — fell back to a global install as a safe default.
|
|
129
134
|
log('reticle_update_local_no_root', { fallback: 'global' });
|
|
130
135
|
}
|
|
@@ -166,7 +171,7 @@ export async function applyUpdate(targetVersion) {
|
|
|
166
171
|
*/
|
|
167
172
|
export async function rollback() {
|
|
168
173
|
const manifest = loadManifest();
|
|
169
|
-
if (
|
|
174
|
+
if (null === manifest || manifest.previousVersion === undefined) {
|
|
170
175
|
throw new Error('No previous version available for rollback');
|
|
171
176
|
}
|
|
172
177
|
const prev = manifest.previousVersion;
|