@reticlehq/server 2.11.0 → 2.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/SKILL.md +93 -111
- package/dist/bridge/auth-failure-reason.d.ts +6 -2
- package/dist/bridge/auth-failure-reason.js +15 -8
- package/dist/bridge/bridge.d.ts +18 -14
- package/dist/bridge/bridge.js +248 -165
- package/dist/bridge/bridge.test-harness.d.ts +2 -0
- package/dist/bridge/bridge.test-harness.js +8 -1
- package/dist/bridge/pairing-token.d.ts +5 -0
- package/dist/bridge/pairing-token.js +28 -0
- package/dist/bridge/protocol-skew.d.ts +37 -0
- package/dist/bridge/protocol-skew.js +45 -0
- package/dist/capsule/capsule-store.d.ts +8 -7
- package/dist/capsule/capsule-store.js +1 -1
- package/dist/capsule/causal-summary.d.ts +2 -2
- package/dist/capsule/causal-summary.js +7 -2
- package/dist/chromium-launch-options.d.ts +28 -0
- package/dist/chromium-launch-options.js +29 -0
- package/dist/cli/cli-doctor.js +39 -1
- package/dist/cli/cli-flow-commands.d.ts +3 -1
- package/dist/cli/cli-flow-commands.js +34 -4
- package/dist/cli/cli-kill.d.ts +2 -1
- package/dist/cli/cli-launch.d.ts +11 -1
- package/dist/cli/cli-launch.js +59 -10
- package/dist/cli/cli-parse.d.ts +17 -1
- package/dist/cli/cli-parse.js +238 -12
- package/dist/cli/cli-update-commands.js +20 -1
- package/dist/cli/cli-verify.d.ts +2 -1
- package/dist/cli/cli-verify.js +5 -4
- package/dist/cli/cloud-cli.js +428 -253
- package/dist/cli/cloud-keystore.d.ts +37 -0
- package/dist/cli/cloud-keystore.js +97 -0
- package/dist/cli/cloud-kit.d.ts +37 -0
- package/dist/cli/cloud-kit.js +121 -0
- package/dist/cli/cloud-login.d.ts +17 -0
- package/dist/cli/cloud-login.js +183 -0
- package/dist/cli/cloud-session.d.ts +48 -0
- package/dist/cli/cloud-session.js +53 -0
- package/dist/cli/config-discovery.d.ts +2 -1
- package/dist/cli/config-discovery.js +1 -0
- package/dist/cli/daemon-start-options.d.ts +32 -0
- package/dist/cli/daemon-start-options.js +38 -0
- package/dist/cli/device-flow.d.ts +76 -0
- package/dist/cli/device-flow.js +56 -0
- package/dist/cli/doctor-rows.d.ts +12 -1
- package/dist/cli/doctor-rows.js +12 -1
- package/dist/cli/drive-attach.d.ts +13 -0
- package/dist/cli/drive-attach.js +18 -0
- package/dist/cli/drive-command.js +7 -3
- package/dist/cli/gate-exit.d.ts +21 -0
- package/dist/cli/gate-exit.js +21 -0
- package/dist/cli/gate-hook-message.d.ts +33 -0
- package/dist/cli/gate-hook-message.js +46 -0
- package/dist/cli/mcp-command.d.ts +25 -0
- package/dist/cli/mcp-command.js +174 -0
- package/dist/cli/project-name.d.ts +30 -0
- package/dist/cli/project-name.js +44 -0
- package/dist/cli/sibling-ports.d.ts +37 -0
- package/dist/cli/sibling-ports.js +56 -0
- package/dist/cli/status-next-action.d.ts +14 -0
- package/dist/cli/status-next-action.js +4 -4
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +167 -186
- package/dist/cloud/cloud-config.d.ts +24 -2
- package/dist/cloud/cloud-config.js +64 -5
- package/dist/cloud/cloud-sync.d.ts +16 -5
- package/dist/cloud/cloud-sync.js +64 -1
- package/dist/cloud/sync-cycle.d.ts +110 -0
- package/dist/cloud/sync-cycle.js +270 -0
- package/dist/cloud/sync-daemon.d.ts +69 -0
- package/dist/cloud/sync-daemon.js +260 -0
- package/dist/cloud/sync-disk.d.ts +9 -0
- package/dist/cloud/sync-disk.js +191 -0
- package/dist/cloud/sync-hash.d.ts +2 -0
- package/dist/cloud/sync-hash.js +35 -0
- package/dist/crawl/crawl-tools.js +5 -3
- package/dist/crawl/crawl.d.ts +12 -13
- package/dist/crawl/crawl.js +25 -3
- package/dist/daemon/daemon-resolve.d.ts +122 -0
- package/dist/daemon/daemon-resolve.js +187 -0
- package/dist/daemon/dev-servers.d.ts +15 -0
- package/dist/daemon/dev-servers.js +68 -0
- package/dist/daemon/free-port.d.ts +29 -0
- package/dist/daemon/free-port.js +55 -0
- package/dist/daemon/heartbeat.d.ts +0 -4
- package/dist/daemon/heartbeat.js +2 -2
- package/dist/daemon/wake-decision.d.ts +32 -0
- package/dist/daemon/wake-decision.js +38 -0
- package/dist/dev/stale-issue-guard.d.ts +0 -2
- package/dist/dev/stale-issue-guard.js +1 -1
- package/dist/domain/domain-tools.js +2 -2
- package/dist/events/contradictions.d.ts +59 -0
- package/dist/events/contradictions.js +253 -27
- package/dist/events/declared.d.ts +17 -0
- package/dist/events/declared.js +43 -1
- package/dist/events/echo-mismatch.js +65 -1
- package/dist/events/event-filters.d.ts +8 -0
- package/dist/events/event-filters.js +19 -2
- package/dist/events/observed-in-window.d.ts +12 -0
- package/dist/events/observed-in-window.js +17 -0
- package/dist/events/predicate-asks.d.ts +13 -0
- package/dist/events/predicate-asks.js +22 -0
- package/dist/events/predicate-eval.js +117 -27
- package/dist/events/predicate-parse.d.ts +0 -6
- package/dist/events/predicate-parse.js +43 -3
- package/dist/events/predicate-precheck.d.ts +8 -0
- package/dist/events/predicate-precheck.js +41 -0
- package/dist/events/predicate-route.d.ts +14 -0
- package/dist/events/predicate-route.js +36 -8
- package/dist/events/predicate-schema.d.ts +44 -0
- package/dist/events/predicate-schema.js +126 -23
- package/dist/events/predicate.d.ts +20 -0
- package/dist/events/predicate.js +77 -6
- package/dist/events/split-text-miss.d.ts +30 -0
- package/dist/events/split-text-miss.js +44 -0
- package/dist/flows/coverage.d.ts +0 -8
- package/dist/flows/coverage.js +1 -1
- package/dist/flows/decision.d.ts +5 -1
- package/dist/flows/decision.js +33 -7
- package/dist/flows/flake.d.ts +6 -8
- package/dist/flows/flake.js +1 -1
- package/dist/flows/flow-intent.d.ts +52 -0
- package/dist/flows/flow-intent.js +123 -0
- package/dist/flows/flow-memory-consult.d.ts +55 -0
- package/dist/flows/flow-memory-consult.js +38 -0
- package/dist/flows/flow-replay-run.d.ts +18 -7
- package/dist/flows/flow-replay-run.js +263 -23
- package/dist/flows/flow-replay.js +11 -2
- package/dist/flows/flow-step-runners.d.ts +8 -0
- package/dist/flows/flow-step-runners.js +8 -2
- package/dist/flows/flow-store-for-session.d.ts +31 -0
- package/dist/flows/flow-store-for-session.js +33 -0
- package/dist/flows/flow-success.js +15 -3
- package/dist/flows/flow-tools.js +87 -33
- package/dist/flows/flows.d.ts +14 -1
- package/dist/flows/flows.js +502 -24
- package/dist/flows/heal-run.js +5 -2
- package/dist/flows/predicate-to-expect.d.ts +17 -12
- package/dist/flows/predicate-to-expect.js +36 -15
- package/dist/flows/recordings.d.ts +13 -1
- package/dist/flows/recordings.js +17 -2
- package/dist/flows/replay.d.ts +22 -2
- package/dist/flows/replay.js +28 -2
- package/dist/flows/server-verify.js +16 -2
- package/dist/flows/verify-change-tools.js +9 -3
- package/dist/honesty/blind-spots.d.ts +51 -15
- package/dist/honesty/blind-spots.js +64 -4
- package/dist/honesty/feature-capture.d.ts +150 -0
- package/dist/honesty/feature-capture.js +233 -0
- package/dist/honesty/honesty.d.ts +24 -0
- package/dist/honesty/honesty.js +25 -1
- package/dist/honesty/instrumentation-gaps.d.ts +39 -6
- package/dist/honesty/instrumentation-gaps.js +88 -5
- package/dist/honesty/tool-hit-rate.d.ts +86 -0
- package/dist/honesty/tool-hit-rate.js +90 -0
- package/dist/honesty/undeclared-change.d.ts +51 -0
- package/dist/honesty/undeclared-change.js +56 -0
- package/dist/honesty/verified.d.ts +16 -0
- package/dist/honesty/verified.js +84 -5
- package/dist/http-server.d.ts +0 -1
- package/dist/http-server.js +13 -1
- package/dist/hunt/hunt-report.d.ts +0 -2
- package/dist/hunt/hunt-report.js +1 -1
- package/dist/impact/impact-recorder.d.ts +31 -8
- package/dist/impact/impact-recorder.js +104 -21
- package/dist/impact/impact-store.d.ts +13 -4
- package/dist/impact/impact-store.js +44 -3
- package/dist/index.d.ts +4 -1
- package/dist/index.js +113 -7
- package/dist/init/agent-rules.d.ts +30 -6
- package/dist/init/agent-rules.js +54 -13
- package/dist/init/astro-patch.d.ts +16 -0
- package/dist/init/astro-patch.js +28 -0
- package/dist/init/capabilities.d.ts +4 -3
- package/dist/init/capabilities.js +23 -3
- package/dist/init/closing-hint.d.ts +36 -0
- package/dist/init/closing-hint.js +120 -0
- package/dist/init/confirm.d.ts +27 -28
- package/dist/init/confirm.js +51 -11
- package/dist/init/cra.d.ts +34 -5
- package/dist/init/cra.js +96 -25
- package/dist/init/csp-check.js +17 -2
- package/dist/init/csp-doctor.d.ts +8 -1
- package/dist/init/csp-doctor.js +8 -3
- package/dist/init/dev-script.d.ts +50 -0
- package/dist/init/dev-script.js +66 -0
- package/dist/init/format-generated.d.ts +18 -0
- package/dist/init/format-generated.js +39 -0
- package/dist/init/install-hint.d.ts +9 -0
- package/dist/init/install-hint.js +29 -0
- package/dist/init/mcp-clients.d.ts +7 -0
- package/dist/init/mcp-clients.js +7 -0
- package/dist/init/mcp.d.ts +17 -6
- package/dist/init/mcp.js +27 -11
- package/dist/init/node-io.js +11 -1
- package/dist/init/plan-framework.d.ts +0 -5
- package/dist/init/plan-framework.js +147 -37
- package/dist/init/plan.d.ts +30 -37
- package/dist/init/plan.js +50 -60
- package/dist/init/preflight.d.ts +28 -0
- package/dist/init/preflight.js +38 -0
- package/dist/init/refresh-rules.d.ts +39 -0
- package/dist/init/refresh-rules.js +51 -0
- package/dist/init/run.d.ts +32 -35
- package/dist/init/run.js +151 -202
- package/dist/init/slash-command.d.ts +1 -1
- package/dist/init/snippets.js +90 -31
- package/dist/init/vite-config.d.ts +1 -1
- package/dist/init/vite-config.js +39 -25
- package/dist/init/workspace-apps.d.ts +17 -10
- package/dist/init/workspace-apps.js +110 -0
- package/dist/input/network-mock-tools.js +3 -6
- package/dist/input/real-input.d.ts +2 -0
- package/dist/input/real-input.js +28 -7
- package/dist/input/scroll-find.d.ts +7 -4
- package/dist/input/scroll-find.js +63 -25
- package/dist/input/scroll-tools.js +4 -6
- package/dist/input/viewport-tools.js +5 -4
- package/dist/intent/inline-intent.d.ts +37 -0
- package/dist/intent/inline-intent.js +193 -0
- package/dist/intent/intent-shard-store.d.ts +72 -0
- package/dist/intent/intent-shard-store.js +203 -0
- package/dist/intent/intent-shard.d.ts +411 -0
- package/dist/intent/intent-shard.js +176 -0
- package/dist/intent/intent-store.d.ts +49 -0
- package/dist/intent/intent-store.js +133 -0
- package/dist/intent/intent-subject.d.ts +45 -0
- package/dist/intent/intent-subject.js +177 -0
- package/dist/intent/intent-tools.d.ts +2 -0
- package/dist/intent/intent-tools.js +143 -0
- package/dist/intent/open-intents.d.ts +42 -0
- package/dist/intent/open-intents.js +40 -0
- package/dist/journal/envelope.d.ts +26 -26
- package/dist/journal/journal-recorder.d.ts +23 -0
- package/dist/journal/journal-recorder.js +35 -0
- package/dist/license/license-env.d.ts +29 -0
- package/dist/license/license-env.js +148 -0
- package/dist/loopback-agent.d.ts +31 -0
- package/dist/loopback-agent.js +57 -0
- package/dist/mcp/attach-memory.d.ts +83 -0
- package/dist/mcp/attach-memory.js +185 -0
- package/dist/mcp/client-identity.d.ts +31 -0
- package/dist/mcp/client-identity.js +51 -0
- package/dist/mcp/mcp-post-transport.d.ts +26 -0
- package/dist/mcp/mcp-post-transport.js +109 -0
- package/dist/mcp/mcp-proxy.d.ts +2 -8
- package/dist/mcp/mcp-proxy.js +64 -62
- package/dist/mcp/mcp.d.ts +2 -0
- package/dist/mcp/mcp.js +52 -24
- package/dist/mcp/proxy-backoff.d.ts +11 -0
- package/dist/mcp/proxy-backoff.js +13 -0
- package/dist/mcp/proxy-daemon-probe.d.ts +1 -0
- package/dist/mcp/proxy-daemon-probe.js +7 -1
- package/dist/mcp/proxy-handshake.d.ts +23 -0
- package/dist/mcp/proxy-handshake.js +31 -0
- package/dist/mcp/server-instructions.js +45 -3
- package/dist/mcp/tool-catalog-cache.d.ts +16 -0
- package/dist/mcp/tool-catalog-cache.js +22 -0
- package/dist/memory/memory-tools.d.ts +2 -0
- package/dist/memory/memory-tools.js +82 -0
- package/dist/memory/project-memory.d.ts +34 -0
- package/dist/memory/project-memory.js +96 -0
- package/dist/oracles/flow-instrument-gaps.d.ts +4 -4
- package/dist/oracles/flow-instrument-gaps.js +4 -5
- package/dist/oracles/propose-consequences.js +12 -3
- package/dist/oracles/self-instrument.d.ts +9 -9
- package/dist/oracles/self-instrument.js +19 -13
- package/dist/pool/browser-pool.d.ts +75 -5
- package/dist/pool/browser-pool.js +144 -5
- package/dist/pool/playwright-launcher.js +12 -1
- package/dist/project/fs-port.d.ts +3 -0
- package/dist/project/fs-port.js +3 -2
- package/dist/project/memory-fs.js +6 -1
- package/dist/project/project-tools.js +6 -35
- package/dist/project/reticle-dir.d.ts +17 -2
- package/dist/project/reticle-dir.js +22 -4
- package/dist/runs/context-tools.d.ts +2 -0
- package/dist/runs/context-tools.js +89 -0
- package/dist/runs/run-context.d.ts +40 -0
- package/dist/runs/run-context.js +158 -0
- package/dist/runs/run-diff.d.ts +2 -1
- package/dist/runs/run-store.d.ts +10 -0
- package/dist/runs/run-store.js +10 -0
- package/dist/runs/verification-sync.d.ts +0 -11
- package/dist/runs/verification-sync.js +24 -3
- package/dist/runs/verify-server.d.ts +8 -1
- package/dist/runs/verify-server.js +14 -2
- package/dist/session/command-timeout.d.ts +5 -2
- package/dist/session/command-timeout.js +5 -4
- package/dist/session/dev-server-probe.d.ts +0 -8
- package/dist/session/dev-server-probe.js +1 -1
- package/dist/session/last-act.d.ts +7 -1
- package/dist/session/last-act.js +2 -2
- package/dist/session/lease-visibility.d.ts +67 -0
- package/dist/session/lease-visibility.js +76 -0
- package/dist/session/live-control-tools.js +1 -42
- package/dist/session/no-session-diagnosis.d.ts +33 -0
- package/dist/session/no-session-diagnosis.js +82 -24
- package/dist/session/no-session-next-action.d.ts +12 -0
- package/dist/session/no-session-next-action.js +77 -6
- package/dist/session/no-session-watch.d.ts +18 -6
- package/dist/session/no-session-watch.js +125 -75
- package/dist/session/output-budget.js +4 -1
- package/dist/session/pending-commands.d.ts +11 -1
- package/dist/session/pending-commands.js +12 -2
- package/dist/session/resolve-within.d.ts +34 -0
- package/dist/session/resolve-within.js +53 -0
- package/dist/session/session-health.d.ts +9 -0
- package/dist/session/session-health.js +23 -0
- package/dist/session/session-info.d.ts +18 -0
- package/dist/session/session-info.js +11 -0
- package/dist/session/session-manager.d.ts +14 -0
- package/dist/session/session-manager.js +111 -4
- package/dist/session/session-mirror.d.ts +16 -0
- package/dist/session/session-mirror.js +22 -0
- package/dist/session/session-recommendation.d.ts +19 -5
- package/dist/session/session-recommendation.js +25 -9
- package/dist/session/session-successor.d.ts +37 -0
- package/dist/session/session-successor.js +113 -0
- package/dist/session/session-tools.js +2 -2
- package/dist/session/session.d.ts +53 -3
- package/dist/session/session.js +162 -9
- package/dist/session/wall-clock.d.ts +12 -0
- package/dist/session/wall-clock.js +14 -0
- package/dist/setup/agent-configs.d.ts +96 -0
- package/dist/setup/agent-configs.js +333 -0
- package/dist/setup/agent-io.d.ts +9 -0
- package/dist/setup/agent-io.js +16 -0
- package/dist/setup/agent-writer.d.ts +51 -0
- package/dist/setup/agent-writer.js +115 -0
- package/dist/setup/approval-migration.d.ts +47 -0
- package/dist/setup/approval-migration.js +68 -0
- package/dist/setup/auto-approve.d.ts +85 -0
- package/dist/setup/auto-approve.js +150 -0
- package/dist/setup/bridge-port.d.ts +15 -0
- package/dist/setup/bridge-port.js +24 -0
- package/dist/setup/desktop-shape.d.ts +57 -0
- package/dist/setup/desktop-shape.js +63 -0
- package/dist/setup/dev-server-wait.d.ts +60 -0
- package/dist/setup/dev-server-wait.js +65 -0
- package/dist/setup/drive-agent.d.ts +71 -0
- package/dist/setup/drive-agent.js +171 -0
- package/dist/setup/drive-plan.d.ts +68 -0
- package/dist/setup/drive-plan.js +130 -0
- package/dist/setup/ensure-daemon.d.ts +29 -0
- package/dist/setup/ensure-daemon.js +58 -0
- package/dist/setup/init-runtime.d.ts +47 -0
- package/dist/setup/init-runtime.js +160 -0
- package/dist/setup/interrupt.d.ts +25 -0
- package/dist/setup/interrupt.js +40 -0
- package/dist/setup/license-key.d.ts +39 -0
- package/dist/setup/license-key.js +72 -0
- package/dist/setup/listeners.d.ts +37 -0
- package/dist/setup/listeners.js +85 -0
- package/dist/setup/node-effects.d.ts +60 -0
- package/dist/setup/node-effects.js +210 -0
- package/dist/setup/page-probe.d.ts +39 -0
- package/dist/setup/page-probe.js +49 -0
- package/dist/setup/relaunch.d.ts +36 -0
- package/dist/setup/relaunch.js +57 -0
- package/dist/setup/remaining-steps.d.ts +19 -0
- package/dist/setup/remaining-steps.js +31 -0
- package/dist/setup/run-setup.d.ts +99 -0
- package/dist/setup/run-setup.js +187 -0
- package/dist/setup/session-pick.d.ts +47 -0
- package/dist/setup/session-pick.js +51 -0
- package/dist/setup/setup-command.d.ts +45 -0
- package/dist/setup/setup-command.js +199 -0
- package/dist/setup/setup-options.d.ts +74 -0
- package/dist/setup/setup-options.js +67 -0
- package/dist/setup/transcripts.d.ts +16 -0
- package/dist/setup/transcripts.js +43 -0
- package/dist/status-payload.d.ts +14 -1
- package/dist/status-payload.js +23 -1
- package/dist/telemetry/automation-hint.d.ts +2 -1
- package/dist/telemetry/daemon-telemetry.d.ts +8 -1
- package/dist/telemetry/daemon-telemetry.js +52 -5
- package/dist/telemetry/error-class.js +1 -1
- package/dist/telemetry/feedback-context.d.ts +9 -1
- package/dist/telemetry/feedback-context.js +60 -29
- package/dist/telemetry/feedback-local.d.ts +15 -0
- package/dist/telemetry/feedback-local.js +56 -0
- package/dist/telemetry/feedback-outbox.d.ts +2 -1
- package/dist/telemetry/feedback.d.ts +11 -0
- package/dist/telemetry/feedback.js +19 -6
- package/dist/telemetry/init-telemetry.d.ts +2 -0
- package/dist/telemetry/init-telemetry.js +2 -0
- package/dist/telemetry/install-source.d.ts +14 -0
- package/dist/telemetry/install-source.js +30 -0
- package/dist/telemetry/license-activation.d.ts +7 -1
- package/dist/telemetry/license-activation.js +9 -5
- package/dist/telemetry/mcp-connection.js +7 -0
- package/dist/telemetry/project-profile.d.ts +12 -9
- package/dist/telemetry/project-profile.js +12 -7
- package/dist/telemetry/proxy-telemetry.d.ts +3 -0
- package/dist/telemetry/proxy-telemetry.js +21 -0
- package/dist/telemetry/run-telemetry.d.ts +0 -2
- package/dist/telemetry/run-telemetry.js +1 -1
- package/dist/telemetry/session-metrics.d.ts +12 -0
- package/dist/telemetry/session-metrics.js +35 -1
- package/dist/telemetry/telemetry.js +17 -2
- package/dist/telemetry/tool-refused.d.ts +3 -1
- package/dist/telemetry/tool-refused.js +34 -2
- package/dist/telemetry/verification-of.js +2 -2
- package/dist/tools/act-capsule.d.ts +10 -0
- package/dist/tools/act-capsule.js +8 -2
- package/dist/tools/act-danger.d.ts +2 -1
- package/dist/tools/act-danger.js +11 -3
- package/dist/tools/act-observation.d.ts +41 -0
- package/dist/tools/act-observation.js +36 -0
- package/dist/tools/act-preflight.d.ts +20 -0
- package/dist/tools/act-preflight.js +98 -0
- package/dist/tools/act-sequence-retry.d.ts +45 -0
- package/dist/tools/act-sequence-retry.js +58 -0
- package/dist/tools/act-sequence-tool.d.ts +13 -0
- package/dist/tools/act-sequence-tool.js +135 -0
- package/dist/tools/act-target.d.ts +14 -0
- package/dist/tools/act-target.js +47 -0
- package/dist/tools/act-tools.d.ts +21 -1
- package/dist/tools/act-tools.js +196 -204
- package/dist/tools/assert-source.d.ts +22 -0
- package/dist/tools/assert-source.js +53 -0
- package/dist/tools/assert-verdict.d.ts +20 -3
- package/dist/tools/assert-verdict.js +58 -20
- package/dist/tools/browser-tools.js +6 -22
- package/dist/tools/complete-snapshot.d.ts +48 -0
- package/dist/tools/complete-snapshot.js +75 -0
- package/dist/tools/coverage-tools.d.ts +10 -1
- package/dist/tools/coverage-tools.js +123 -69
- package/dist/tools/dynamic-tools.js +53 -13
- package/dist/tools/error-recovery.d.ts +25 -14
- package/dist/tools/error-recovery.js +78 -9
- package/dist/tools/feedback-tools.d.ts +1 -1
- package/dist/tools/feedback-tools.js +2 -2
- package/dist/tools/invoke-tool.js +121 -11
- package/dist/tools/lease-hint.d.ts +10 -0
- package/dist/tools/lease-hint.js +53 -4
- package/dist/tools/lease-tools.d.ts +35 -5
- package/dist/tools/lease-tools.js +191 -13
- package/dist/tools/merged-name-redirect.d.ts +13 -0
- package/dist/tools/merged-name-redirect.js +22 -0
- package/dist/tools/navigate-arrival.d.ts +0 -2
- package/dist/tools/navigate-arrival.js +1 -1
- package/dist/tools/numeric-bounds.d.ts +100 -0
- package/dist/tools/numeric-bounds.js +125 -0
- package/dist/tools/observe-tools.js +109 -65
- package/dist/tools/read-tools.js +31 -8
- package/dist/tools/real-input-attempt.d.ts +32 -0
- package/dist/tools/real-input-attempt.js +212 -2
- package/dist/tools/reconcile-tools.js +22 -8
- package/dist/tools/resolve-target.d.ts +0 -7
- package/dist/tools/sdk-marker-probe.d.ts +29 -0
- package/dist/tools/sdk-marker-probe.js +57 -0
- package/dist/tools/snapshot-delta.d.ts +1 -1
- package/dist/tools/tool-kit.d.ts +24 -0
- package/dist/tools/tool-kit.js +21 -0
- package/dist/tools/tool-names.d.ts +5 -0
- package/dist/tools/tool-names.js +5 -0
- package/dist/tools/tool-surface.d.ts +85 -6
- package/dist/tools/tool-surface.js +122 -2
- package/dist/tools/tools-helpers.d.ts +19 -0
- package/dist/tools/tools-helpers.js +46 -0
- package/dist/tools/tools.d.ts +2 -0
- package/dist/tools/tools.js +78 -12
- package/dist/tools/unadvertised-help.js +4 -4
- package/dist/tools/verify-next-baton.d.ts +53 -0
- package/dist/tools/verify-next-baton.js +52 -0
- package/dist/update/reticle-deps.d.ts +9 -0
- package/dist/update/reticle-deps.js +27 -0
- package/dist/update/sdk-sync.d.ts +1 -2
- package/dist/update/sdk-sync.js +1 -21
- package/dist/update/update-nudge.d.ts +25 -10
- package/dist/update/update-nudge.js +32 -3
- package/dist/update/updater.d.ts +1 -1
- package/dist/version/peer-announce.js +4 -2
- package/dist/version/sdk-fix.d.ts +47 -0
- package/dist/version/sdk-fix.js +127 -0
- package/dist/version/version-nudge.d.ts +14 -0
- package/dist/version/version-nudge.js +21 -0
- package/dist/version/version-skew.d.ts +38 -3
- package/dist/version/version-skew.js +83 -7
- package/dist/visual/visual-store.d.ts +6 -6
- package/dist/visual/visual-store.js +18 -16
- package/dist/visual/visual-tools.js +57 -12
- package/docs/README.md +2 -1
- package/docs/cli/cloud.mdx +1 -1
- package/docs/cli/doctor.mdx +2 -0
- package/docs/cli/serve.mdx +4 -2
- package/docs/cli/verify.mdx +4 -5
- package/docs/cli.mdx +7 -3
- package/docs/desktop.mdx +1 -1
- package/docs/docs.json +2 -2
- package/docs/gates.md +1 -1
- package/docs/getting-started.md +19 -3
- package/docs/index.mdx +9 -6
- package/docs/install-agentic.mdx +38 -12
- package/docs/install-manual.mdx +1 -1
- package/docs/packages/browser.mdx +2 -2
- package/docs/packages/server.mdx +1 -1
- package/docs/packages.mdx +8 -6
- package/docs/platform-integration.md +17 -0
- package/docs/predicates.mdx +8 -0
- package/docs/reticle-manual.md +106 -0
- package/docs/skill-file.mdx +3 -3
- package/docs/telemetry-contract.md +108 -4
- package/docs/tools-act-sequence.mdx +16 -0
- package/docs/tools-tools-and-run.mdx +2 -4
- package/docs/troubleshooting.mdx +9 -0
- package/docs/usage.md +4 -3
- package/package.json +4 -3
- package/dist/init/cursor.d.ts +0 -24
- package/dist/init/cursor.js +0 -80
- package/dist/session/session-readiness.d.ts +0 -34
- package/dist/session/session-readiness.js +0 -36
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Optional cloud sync for saved flows. "Logged in" here means the two cloud env vars are set (written by
|
|
3
|
-
* `reticle login` later; settable by hand today): the hosted URL and an API key from the Reticle
|
|
3
|
+
* `reticle login` later; settable by hand today): the hosted URL and an API key from the Reticle
|
|
4
4
|
* dashboard. When present, a freshly-saved flow is pushed to `POST /v1/flows` so the team's regression
|
|
5
5
|
* suite lives in the cloud — surviving refactors and runnable in CI. When absent, sync is a no-op and
|
|
6
6
|
* everything stays 100% local (the "no phone-home" default: nothing leaves the machine unless you opt in).
|
|
@@ -21,6 +21,17 @@ export interface CloudConfig {
|
|
|
21
21
|
}
|
|
22
22
|
/** Resolve cloud credentials from the environment, or null when not logged in (sync disabled). */
|
|
23
23
|
export declare function resolveCloudConfig(env: NodeJS.ProcessEnv): CloudConfig | null;
|
|
24
|
+
export declare const CLOUD_VERIFY_TIMEOUT_MS = 120000;
|
|
25
|
+
/**
|
|
26
|
+
* The fetch every cloud call goes through. Adds the abort signal, and turns the abort into a message
|
|
27
|
+
* that says what happened and what to do — a bare `AbortError` reaching a user or an agent is a riddle,
|
|
28
|
+
* and the agent-facing half of this product is judged on whether its errors are actionable.
|
|
29
|
+
*/
|
|
30
|
+
export declare function cloudFetch(url: string, init: {
|
|
31
|
+
method: string;
|
|
32
|
+
headers: Record<string, string>;
|
|
33
|
+
body?: string;
|
|
34
|
+
}, timeoutMs?: number): Promise<Response>;
|
|
24
35
|
export declare const SyncOutcome: {
|
|
25
36
|
readonly SYNCED: "synced";
|
|
26
37
|
readonly SKIPPED: "skipped";
|
|
@@ -104,21 +115,21 @@ declare const ServerVerificationSchema: z.ZodObject<{
|
|
|
104
115
|
}>, "many">;
|
|
105
116
|
summary: z.ZodString;
|
|
106
117
|
}, "strip", z.ZodTypeAny, {
|
|
118
|
+
verdict: string | null;
|
|
107
119
|
flows: {
|
|
108
120
|
status: string;
|
|
109
121
|
name: string;
|
|
110
122
|
}[];
|
|
111
|
-
verificationId: string;
|
|
112
|
-
verdict: string | null;
|
|
113
123
|
summary: string;
|
|
124
|
+
verificationId: string;
|
|
114
125
|
}, {
|
|
126
|
+
verdict: string | null;
|
|
115
127
|
flows: {
|
|
116
128
|
status: string;
|
|
117
129
|
name: string;
|
|
118
130
|
}[];
|
|
119
|
-
verificationId: string;
|
|
120
|
-
verdict: string | null;
|
|
121
131
|
summary: string;
|
|
132
|
+
verificationId: string;
|
|
122
133
|
}>;
|
|
123
134
|
export type ServerVerification = z.infer<typeof ServerVerificationSchema>;
|
|
124
135
|
/**
|
package/dist/cloud/cloud-sync.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Optional cloud sync for saved flows. "Logged in" here means the two cloud env vars are set (written by
|
|
3
|
-
* `reticle login` later; settable by hand today): the hosted URL and an API key from the Reticle
|
|
3
|
+
* `reticle login` later; settable by hand today): the hosted URL and an API key from the Reticle
|
|
4
4
|
* dashboard. When present, a freshly-saved flow is pushed to `POST /v1/flows` so the team's regression
|
|
5
5
|
* suite lives in the cloud — surviving refactors and runnable in CI. When absent, sync is a no-op and
|
|
6
6
|
* everything stays 100% local (the "no phone-home" default: nothing leaves the machine unless you opt in).
|
|
@@ -30,6 +30,69 @@ export function resolveCloudConfig(env) {
|
|
|
30
30
|
return null;
|
|
31
31
|
return { url: url.replace(/\/+$/, ''), apiKey };
|
|
32
32
|
}
|
|
33
|
+
/**
|
|
34
|
+
* Budget for a cloud call. Node's `fetch` has NO default timeout, so a connection that opens and then
|
|
35
|
+
* stalls never settles — and the caller `await`s it forever. In `reticle_flow_verify` that is an MCP
|
|
36
|
+
* tool call that never returns, and in `reticle cloud …` a terminal that prints nothing.
|
|
37
|
+
*
|
|
38
|
+
* One shared budget covers every call here: they are all small JSON request/response pairs against the
|
|
39
|
+
* same API, so there is nothing to tune per site. The ONE exception is the hosted verification submit,
|
|
40
|
+
* which blocks while a real browser runs the suite server-side — a normal response there is minutes,
|
|
41
|
+
* not seconds, so it gets its own (still bounded) budget rather than dragging the common one up.
|
|
42
|
+
*/
|
|
43
|
+
const CLOUD_FETCH_TIMEOUT_MS = 30_000;
|
|
44
|
+
export const CLOUD_VERIFY_TIMEOUT_MS = 120_000;
|
|
45
|
+
/** Names Node gives an abort: `AbortSignal.timeout` raises TimeoutError, an explicit abort AbortError. */
|
|
46
|
+
const TIMEOUT_ERROR_NAMES = new Set(['TimeoutError', 'AbortError']);
|
|
47
|
+
/**
|
|
48
|
+
* A header value `fetch` can actually send. Anything above U+00FF is not a valid ByteString.
|
|
49
|
+
*
|
|
50
|
+
* The one that happens in practice is an ellipsis. A dashboard masks a key as `rk_live_…` so it is
|
|
51
|
+
* never shown in full, somebody copies what they can SEE into `.env`, and the request dies deep
|
|
52
|
+
* inside undici with "Cannot convert argument to a ByteString because the character at index 15 has
|
|
53
|
+
* a value of 8230" — a byte offset into a header the user never wrote, naming no variable, no file
|
|
54
|
+
* and no fix.
|
|
55
|
+
*/
|
|
56
|
+
const UNSENDABLE_HEADER_CHAR = /[^ -ÿ]/;
|
|
57
|
+
/** Which knob holds the credential behind a header, so the error can name the thing to go and edit. */
|
|
58
|
+
const CREDENTIAL_SOURCE = {
|
|
59
|
+
authorization: `${CloudEnv.KEY} (or the session from \`reticle login\`)`,
|
|
60
|
+
};
|
|
61
|
+
/**
|
|
62
|
+
* Refuse a request whose headers cannot be encoded, before it is sent.
|
|
63
|
+
*
|
|
64
|
+
* Before dialling on purpose: a credential that cannot be encoded cannot succeed, so the round trip
|
|
65
|
+
* buys only a slower and less specific failure.
|
|
66
|
+
*/
|
|
67
|
+
function assertSendableHeaders(headers) {
|
|
68
|
+
for (const [name, value] of Object.entries(headers)) {
|
|
69
|
+
if (!UNSENDABLE_HEADER_CHAR.test(value))
|
|
70
|
+
continue;
|
|
71
|
+
const source = CREDENTIAL_SOURCE[name];
|
|
72
|
+
throw new Error(`the ${name} header carries a character that cannot be sent, which means the value is truncated — ` +
|
|
73
|
+
`a credential displayed masked as \`rk_live_…\` keeps that ellipsis when it is copied. ` +
|
|
74
|
+
`Paste the full value${source === undefined ? '' : ` into ${source}`}.`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* The fetch every cloud call goes through. Adds the abort signal, and turns the abort into a message
|
|
79
|
+
* that says what happened and what to do — a bare `AbortError` reaching a user or an agent is a riddle,
|
|
80
|
+
* and the agent-facing half of this product is judged on whether its errors are actionable.
|
|
81
|
+
*/
|
|
82
|
+
export async function cloudFetch(url, init, timeoutMs = CLOUD_FETCH_TIMEOUT_MS) {
|
|
83
|
+
assertSendableHeaders(init.headers);
|
|
84
|
+
try {
|
|
85
|
+
return await fetch(url, { ...init, signal: AbortSignal.timeout(timeoutMs) });
|
|
86
|
+
}
|
|
87
|
+
catch (err) {
|
|
88
|
+
if (err instanceof Error && TIMEOUT_ERROR_NAMES.has(err.name)) {
|
|
89
|
+
throw new Error(`Reticle request timed out after ${Math.round(timeoutMs / 1000)}s: ${init.method} ${url}. ` +
|
|
90
|
+
`The server accepted the connection but never answered. Check the network and ${CloudEnv.URL}, then retry — ` +
|
|
91
|
+
`verification works locally without cloud.`);
|
|
92
|
+
}
|
|
93
|
+
throw err;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
33
96
|
export const SyncOutcome = {
|
|
34
97
|
SYNCED: 'synced',
|
|
35
98
|
SKIPPED: 'skipped',
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The derived records that ride along with runs, and the file each one lives in.
|
|
3
|
+
*
|
|
4
|
+
* A list rather than three hand-written blocks so adding a fourth is one line and cannot be
|
|
5
|
+
* half-done — the bundle, the hashing and the reporting all walk this.
|
|
6
|
+
*/
|
|
7
|
+
export declare const DERIVED_RECORDS: readonly [{
|
|
8
|
+
readonly kind: "impact";
|
|
9
|
+
readonly file: "impact.json";
|
|
10
|
+
}, {
|
|
11
|
+
readonly kind: "flake";
|
|
12
|
+
readonly file: "flake.json";
|
|
13
|
+
}, {
|
|
14
|
+
readonly kind: "intent";
|
|
15
|
+
readonly file: "intent.json";
|
|
16
|
+
}];
|
|
17
|
+
export type DerivedKind = (typeof DERIVED_RECORDS)[number]['kind'];
|
|
18
|
+
/** What the machine reads from disk. Injected so a cycle is testable with no filesystem at all. */
|
|
19
|
+
export interface SyncSource {
|
|
20
|
+
/** Every run artifact currently on disk. */
|
|
21
|
+
runs: () => ReadonlyArray<{
|
|
22
|
+
runId: string;
|
|
23
|
+
payload: unknown;
|
|
24
|
+
}>;
|
|
25
|
+
/** Every saved flow. Small, upserted by name, so they ride along whenever anything else does. */
|
|
26
|
+
flows: () => readonly unknown[];
|
|
27
|
+
/** One derived record, or undefined when the file is absent. */
|
|
28
|
+
derived: (kind: DerivedKind) => unknown;
|
|
29
|
+
}
|
|
30
|
+
/** What the machine writes back. Separated from reads so a dry run is a source with no sink. */
|
|
31
|
+
export interface SyncSink {
|
|
32
|
+
/** Persist the triage decisions pulled from the dashboard. */
|
|
33
|
+
writeIssues: (issues: PulledIssues) => void;
|
|
34
|
+
/** Persist the cursor and the bookkeeping. */
|
|
35
|
+
writeState: (state: CloudSyncState) => void;
|
|
36
|
+
}
|
|
37
|
+
/** A decision a human made on the dashboard, as the machine stores it. */
|
|
38
|
+
export interface PulledIssue {
|
|
39
|
+
status: string;
|
|
40
|
+
flowName: string | null;
|
|
41
|
+
title: string;
|
|
42
|
+
at: number;
|
|
43
|
+
}
|
|
44
|
+
export interface PulledIssues {
|
|
45
|
+
/** Keyed by the server's fingerprint — stable across runs, which is the point of it. */
|
|
46
|
+
triage: Record<string, PulledIssue>;
|
|
47
|
+
}
|
|
48
|
+
/** `.reticle/cloud-state.json`. This machine's side of the conversation, never git-checked. */
|
|
49
|
+
export interface CloudSyncState {
|
|
50
|
+
/** Opaque. Handed back to the server verbatim; the machine must never parse or invent one. */
|
|
51
|
+
cursor?: string;
|
|
52
|
+
lastPushAt?: number;
|
|
53
|
+
lastPullAt?: number;
|
|
54
|
+
/** The last failure, kept so `reticle sync` can say why it is behind instead of just "0 sent". */
|
|
55
|
+
lastError?: string;
|
|
56
|
+
}
|
|
57
|
+
export interface SyncReport {
|
|
58
|
+
ok: boolean;
|
|
59
|
+
/** Runs the server accepted this cycle. */
|
|
60
|
+
runsSent: number;
|
|
61
|
+
/** Runs it refused, with the reason, so a bad artifact is visible rather than silently stuck. */
|
|
62
|
+
runsRejected: Array<{
|
|
63
|
+
index: number;
|
|
64
|
+
reason: string;
|
|
65
|
+
}>;
|
|
66
|
+
flowsSent: number;
|
|
67
|
+
/** Which derived records had actually moved. Empty on a quiet cycle, which is the normal case. */
|
|
68
|
+
derivedSent: DerivedKind[];
|
|
69
|
+
/** Decisions collected from the dashboard. */
|
|
70
|
+
pulled: number;
|
|
71
|
+
/** True when the pull page was full — call again now rather than waiting for the next tick. */
|
|
72
|
+
morePending: boolean;
|
|
73
|
+
/**
|
|
74
|
+
* The repo holds NO artifacts at all — no runs, no flows, no derived records.
|
|
75
|
+
*
|
|
76
|
+
* Distinct from "everything here is already pushed", which is the healthy steady state and looks
|
|
77
|
+
* identical from the outside. An empty repo usually means the app announces no projectId, so its
|
|
78
|
+
* runs are pooling into a different root and this binding will never report anything. Tracked so
|
|
79
|
+
* the summary can tell a user which of the two they are looking at.
|
|
80
|
+
*/
|
|
81
|
+
localIsEmpty?: boolean;
|
|
82
|
+
/** Set when the cycle could not complete. The local record is untouched either way. */
|
|
83
|
+
error?: string;
|
|
84
|
+
}
|
|
85
|
+
export interface SyncDeps {
|
|
86
|
+
config: {
|
|
87
|
+
url: string;
|
|
88
|
+
apiKey: string;
|
|
89
|
+
};
|
|
90
|
+
source: SyncSource;
|
|
91
|
+
sink: SyncSink;
|
|
92
|
+
state: CloudSyncState;
|
|
93
|
+
now: () => number;
|
|
94
|
+
/** Injected so a cycle can be driven against a scripted server with no network. */
|
|
95
|
+
request: (url: string, init: {
|
|
96
|
+
method: string;
|
|
97
|
+
headers: Record<string, string>;
|
|
98
|
+
body?: string;
|
|
99
|
+
}) => Promise<{
|
|
100
|
+
status: number;
|
|
101
|
+
text: string;
|
|
102
|
+
}>;
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Run one cycle. Never throws: the local record is already safe on disk, and a sync that can take
|
|
106
|
+
* down the thing it is backing up is worse than no sync.
|
|
107
|
+
*/
|
|
108
|
+
export declare function runSyncCycle(deps: SyncDeps): Promise<SyncReport>;
|
|
109
|
+
/** One line a human can read, for the CLI and the daemon log. */
|
|
110
|
+
export declare function describeSync(report: SyncReport): string;
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One full conversation with the dashboard: ask, send the difference, collect what came back.
|
|
3
|
+
*
|
|
4
|
+
* ── WHY IT LOOKS LIKE THIS ─────────────────────────────────────────────────────────────────────
|
|
5
|
+
* `.reticle/` is the source of truth and always will be. Reticle has to work with no account and no
|
|
6
|
+
* network; a verification that blocks on an HTTP round trip is one somebody switches off. So the
|
|
7
|
+
* dashboard is a REPLICA, and this is the replication protocol:
|
|
8
|
+
*
|
|
9
|
+
* 1. ASK GET /v1/sync/status → run ids it already holds, a content hash per derived record
|
|
10
|
+
* 2. SEND POST /v1/sync → only unseen runs, only records whose hash moved
|
|
11
|
+
* 3. COLLECT GET /v1/sync/pull → decisions a human made there, since our cursor
|
|
12
|
+
* 4. APPLY write them to .reticle/issues.json so the HUD and the next run can see them
|
|
13
|
+
*
|
|
14
|
+
* ASK-THEN-SEND, not send-everything. A three-hour session rewrites impact.json on every tool call;
|
|
15
|
+
* uploading it each time would re-send the same megabytes dozens of times, and a sync that costs
|
|
16
|
+
* that much is one people turn off — which is the real failure, because a dashboard nobody syncs to
|
|
17
|
+
* is a dashboard nobody opens. When nothing has moved, a cycle sends nothing at all, and that is
|
|
18
|
+
* what makes running it on a timer affordable.
|
|
19
|
+
*
|
|
20
|
+
* HASHES, NOT TIMESTAMPS. Two machines comparing their own clocks is a guess: they drift, and a
|
|
21
|
+
* laptop that was asleep will cheerfully decide it is current. A content hash is an answer.
|
|
22
|
+
*
|
|
23
|
+
* BOTH DIRECTIONS, ALWAYS. The pull runs even when there is nothing to push — a quiet machine is
|
|
24
|
+
* precisely the one whose dashboard somebody has been triaging on, so skipping the pull when the
|
|
25
|
+
* push is empty would starve the direction that matters most.
|
|
26
|
+
*
|
|
27
|
+
* NOTHING IS DELETED LOCALLY ON SUCCESS. A lost response costs one redundant upload rather than a
|
|
28
|
+
* hole in the record, and the server dedupes by run id anyway.
|
|
29
|
+
*
|
|
30
|
+
* Every dependency is injected — the clock, the fetch, the reads and writes — because this is the
|
|
31
|
+
* one piece of Reticle that talks to a network AND to a disk, and it must be provable without either.
|
|
32
|
+
*/
|
|
33
|
+
import { ReticleDir } from '@reticlehq/core';
|
|
34
|
+
import { hashPayload } from './sync-hash.js';
|
|
35
|
+
/** The server's own doors. Kept beside the code that calls them, like the other cloud paths. */
|
|
36
|
+
const SYNC_PATH = '/v1/sync';
|
|
37
|
+
const SYNC_STATUS_PATH = '/v1/sync/status';
|
|
38
|
+
const SYNC_PULL_PATH = '/v1/sync/pull';
|
|
39
|
+
/**
|
|
40
|
+
* The derived records that ride along with runs, and the file each one lives in.
|
|
41
|
+
*
|
|
42
|
+
* A list rather than three hand-written blocks so adding a fourth is one line and cannot be
|
|
43
|
+
* half-done — the bundle, the hashing and the reporting all walk this.
|
|
44
|
+
*/
|
|
45
|
+
export const DERIVED_RECORDS = [
|
|
46
|
+
{ kind: 'impact', file: ReticleDir.IMPACT_FILE },
|
|
47
|
+
{ kind: 'flake', file: ReticleDir.FLAKE_FILE },
|
|
48
|
+
{ kind: 'intent', file: ReticleDir.INTENT_FILE },
|
|
49
|
+
];
|
|
50
|
+
const asJson = (text) => {
|
|
51
|
+
try {
|
|
52
|
+
return JSON.parse(text);
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
return undefined;
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
const isRecord = (v) => 'object' === typeof v && null !== v;
|
|
59
|
+
/**
|
|
60
|
+
* Run one cycle. Never throws: the local record is already safe on disk, and a sync that can take
|
|
61
|
+
* down the thing it is backing up is worse than no sync.
|
|
62
|
+
*/
|
|
63
|
+
export async function runSyncCycle(deps) {
|
|
64
|
+
const empty = {
|
|
65
|
+
ok: false,
|
|
66
|
+
runsSent: 0,
|
|
67
|
+
runsRejected: [],
|
|
68
|
+
flowsSent: 0,
|
|
69
|
+
derivedSent: [],
|
|
70
|
+
pulled: 0,
|
|
71
|
+
morePending: false,
|
|
72
|
+
};
|
|
73
|
+
const call = async (path, init) => {
|
|
74
|
+
const res = await deps.request(`${deps.config.url}${path}`, {
|
|
75
|
+
method: init?.method ?? 'GET',
|
|
76
|
+
headers: {
|
|
77
|
+
'content-type': 'application/json',
|
|
78
|
+
authorization: `Bearer ${deps.config.apiKey}`,
|
|
79
|
+
},
|
|
80
|
+
...(init?.body === undefined ? {} : { body: init.body }),
|
|
81
|
+
});
|
|
82
|
+
return { status: res.status, json: asJson(res.text), text: res.text };
|
|
83
|
+
};
|
|
84
|
+
const nextState = { ...deps.state };
|
|
85
|
+
try {
|
|
86
|
+
// 1. ASK.
|
|
87
|
+
const status = await call(SYNC_STATUS_PATH);
|
|
88
|
+
if (200 !== status.status) {
|
|
89
|
+
const error = `status ${String(status.status)}: ${status.text.slice(0, 200)}`;
|
|
90
|
+
deps.sink.writeState({ ...nextState, lastError: error });
|
|
91
|
+
return { ...empty, error };
|
|
92
|
+
}
|
|
93
|
+
const held = isRecord(status.json) ? status.json : {};
|
|
94
|
+
/*
|
|
95
|
+
* Validated, not trusted. `new Set(someString)` builds a set of CHARACTERS, so a `knownRunIds`
|
|
96
|
+
* that arrived as a string made `known.has('a')` true and silently skipped any run whose id was
|
|
97
|
+
* one of those letters — the client deciding, on the server's malformed word, not to upload
|
|
98
|
+
* something the server does not have. Silent data loss is the one failure this protocol must
|
|
99
|
+
* not have, and an unreadable answer has to mean "it knows nothing", never "it knows this".
|
|
100
|
+
*/
|
|
101
|
+
const known = new Set(Array.isArray(held.knownRunIds)
|
|
102
|
+
? held.knownRunIds.filter((id) => 'string' === typeof id)
|
|
103
|
+
: []);
|
|
104
|
+
// Same rule for the hashes: only a string can equal a hash we computed, so anything else reads
|
|
105
|
+
// as "unknown" and the record is sent once. Re-sending costs a request; skipping costs the data.
|
|
106
|
+
const hashes = isRecord(held.stateHashes) ? held.stateHashes : {};
|
|
107
|
+
// 2. SEND — only what the server does not already have.
|
|
108
|
+
const allRuns = deps.source.runs();
|
|
109
|
+
const unsent = allRuns.filter((r) => !known.has(r.runId));
|
|
110
|
+
const derivedSent = [];
|
|
111
|
+
const bundle = {};
|
|
112
|
+
if (unsent.length > 0)
|
|
113
|
+
bundle['runs'] = unsent.map((r) => r.payload);
|
|
114
|
+
for (const { kind } of DERIVED_RECORDS) {
|
|
115
|
+
const payload = deps.source.derived(kind);
|
|
116
|
+
if (payload === undefined)
|
|
117
|
+
continue;
|
|
118
|
+
// The one comparison the whole protocol rests on. Same hash, do not send it.
|
|
119
|
+
if (hashPayload(payload) === hashes[kind])
|
|
120
|
+
continue;
|
|
121
|
+
bundle[kind] = payload;
|
|
122
|
+
derivedSent.push(kind);
|
|
123
|
+
}
|
|
124
|
+
// Flows are small and upserted by name, so they ride along whenever anything else does rather
|
|
125
|
+
// than earning a round trip of their own.
|
|
126
|
+
const flows = bundle['runs'] === undefined && 0 === derivedSent.length ? [] : deps.source.flows();
|
|
127
|
+
if (flows.length > 0)
|
|
128
|
+
bundle['flows'] = flows;
|
|
129
|
+
let runsSent = 0;
|
|
130
|
+
let flowsSent = 0;
|
|
131
|
+
let runsRejected = [];
|
|
132
|
+
if (Object.keys(bundle).length > 0) {
|
|
133
|
+
const pushed = await call(SYNC_PATH, { method: 'POST', body: JSON.stringify(bundle) });
|
|
134
|
+
if (200 !== pushed.status) {
|
|
135
|
+
const error = `sync ${String(pushed.status)}: ${pushed.text.slice(0, 200)}`;
|
|
136
|
+
deps.sink.writeState({ ...nextState, lastError: error });
|
|
137
|
+
return { ...empty, error };
|
|
138
|
+
}
|
|
139
|
+
const body = isRecord(pushed.json) ? pushed.json : {};
|
|
140
|
+
const runs = isRecord(body['runs']) ? body['runs'] : {};
|
|
141
|
+
const flowsPart = isRecord(body['flows']) ? body['flows'] : {};
|
|
142
|
+
runsSent = 'number' === typeof runs['accepted'] ? runs['accepted'] : 0;
|
|
143
|
+
flowsSent = 'number' === typeof flowsPart['accepted'] ? flowsPart['accepted'] : 0;
|
|
144
|
+
runsRejected = Array.isArray(runs['rejected'])
|
|
145
|
+
? runs['rejected']
|
|
146
|
+
: [];
|
|
147
|
+
nextState.lastPushAt = deps.now();
|
|
148
|
+
}
|
|
149
|
+
// 3. COLLECT — always, even when there was nothing to send.
|
|
150
|
+
const query = nextState.cursor === undefined ? '' : `?since=${encodeURIComponent(nextState.cursor)}`;
|
|
151
|
+
const pull = await call(`${SYNC_PULL_PATH}${query}`);
|
|
152
|
+
if (200 !== pull.status) {
|
|
153
|
+
// The push already landed; report it rather than throwing the whole cycle away.
|
|
154
|
+
const error = `pull ${String(pull.status)}: ${pull.text.slice(0, 200)}`;
|
|
155
|
+
deps.sink.writeState({ ...nextState, lastError: error });
|
|
156
|
+
return { ...empty, ok: false, runsSent, flowsSent, derivedSent, runsRejected, error };
|
|
157
|
+
}
|
|
158
|
+
const pulled = isRecord(pull.json) ? pull.json : {};
|
|
159
|
+
const decisions = pulled.triage ?? [];
|
|
160
|
+
// 4. APPLY.
|
|
161
|
+
if (decisions.length > 0) {
|
|
162
|
+
const triage = {};
|
|
163
|
+
for (const d of decisions) {
|
|
164
|
+
triage[d.fingerprint] = {
|
|
165
|
+
status: d.status,
|
|
166
|
+
flowName: d.flowName ?? null,
|
|
167
|
+
title: d.title ?? '',
|
|
168
|
+
at: d.at,
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
deps.sink.writeIssues({ triage });
|
|
172
|
+
}
|
|
173
|
+
// The cursor is written even on an empty page: it is the server's, and it never goes backwards.
|
|
174
|
+
if ('string' === typeof pulled.cursor)
|
|
175
|
+
nextState.cursor = pulled.cursor;
|
|
176
|
+
nextState.lastPullAt = deps.now();
|
|
177
|
+
delete nextState.lastError;
|
|
178
|
+
deps.sink.writeState(nextState);
|
|
179
|
+
return {
|
|
180
|
+
ok: true,
|
|
181
|
+
runsSent,
|
|
182
|
+
runsRejected,
|
|
183
|
+
flowsSent,
|
|
184
|
+
derivedSent,
|
|
185
|
+
pulled: decisions.length,
|
|
186
|
+
morePending: true === pulled.more,
|
|
187
|
+
/*
|
|
188
|
+
* Nothing on disk at all, as opposed to nothing NEW. Computed from what the source offered
|
|
189
|
+
* before any cursor filtering, because a repo whose runs were all already pushed is healthy
|
|
190
|
+
* and a repo that has never recorded one is usually misconfigured.
|
|
191
|
+
*/
|
|
192
|
+
localIsEmpty: 0 === allRuns.length &&
|
|
193
|
+
0 === deps.source.flows().length &&
|
|
194
|
+
DERIVED_RECORDS.every(({ kind }) => deps.source.derived(kind) === undefined),
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
catch (error) {
|
|
198
|
+
// A network that is down is not an error condition for a local-first tool; it is Tuesday.
|
|
199
|
+
const message = describeTransportError(error, deps.config.url);
|
|
200
|
+
deps.sink.writeState({ ...nextState, lastError: message });
|
|
201
|
+
return { ...empty, error: message };
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* Say WHICH host failed and WHY.
|
|
206
|
+
*
|
|
207
|
+
* Node's fetch rejects with the bare string "fetch failed" and hides the real reason — a refused
|
|
208
|
+
* connection, a bad hostname, an expired certificate — one level down in `cause`. A machine that
|
|
209
|
+
* has quietly stopped syncing shows that string in `reticle whoami`, and on its own it is
|
|
210
|
+
* indistinguishable from every other network problem, so the person reading it learns nothing and
|
|
211
|
+
* checks nothing. Naming the origin and the underlying code turns it into one thing to look at.
|
|
212
|
+
*/
|
|
213
|
+
function describeTransportError(error, url) {
|
|
214
|
+
if (!(error instanceof Error))
|
|
215
|
+
return String(error);
|
|
216
|
+
const cause = error.cause;
|
|
217
|
+
if (!(cause instanceof Error))
|
|
218
|
+
return `${error.message} (${url})`;
|
|
219
|
+
const code = cause.code;
|
|
220
|
+
// The code and the message are each sometimes empty — an aggregate DNS failure carries a code and
|
|
221
|
+
// no text, a TLS failure the reverse. Joined only when both are there, so the line never trails
|
|
222
|
+
// off into a dangling separator.
|
|
223
|
+
const parts = ['string' === typeof code && code.length > 0 ? code : '', cause.message].filter((part) => part.length > 0);
|
|
224
|
+
const detail = parts.join(': ');
|
|
225
|
+
return 0 === detail.length
|
|
226
|
+
? `${error.message} (${url})`
|
|
227
|
+
: `${error.message} — ${detail} (${url})`;
|
|
228
|
+
}
|
|
229
|
+
/** One line a human can read, for the CLI and the daemon log. */
|
|
230
|
+
export function describeSync(report) {
|
|
231
|
+
if (report.error !== undefined)
|
|
232
|
+
return `sync failed — ${report.error}`;
|
|
233
|
+
const sent = [];
|
|
234
|
+
if (report.runsSent > 0)
|
|
235
|
+
sent.push(`${String(report.runsSent)} run(s)`);
|
|
236
|
+
if (report.flowsSent > 0)
|
|
237
|
+
sent.push(`${String(report.flowsSent)} flow(s)`);
|
|
238
|
+
if (report.derivedSent.length > 0)
|
|
239
|
+
sent.push(report.derivedSent.join(', '));
|
|
240
|
+
/*
|
|
241
|
+
* "Nothing to send" is a statement about the QUEUE, and it is false the moment the queue was full
|
|
242
|
+
* and the server threw it away. Built from what was accepted, the sentence used to read "nothing
|
|
243
|
+
* to send, 3 rejected" — something was very much sent, and the reader is told both that it was
|
|
244
|
+
* not and nothing about why.
|
|
245
|
+
*/
|
|
246
|
+
const push = sent.length > 0
|
|
247
|
+
? `sent ${sent.join(' + ')}`
|
|
248
|
+
: report.runsRejected.length > 0
|
|
249
|
+
? 'nothing accepted'
|
|
250
|
+
: true === report.localIsEmpty
|
|
251
|
+
? // Not the same statement as "nothing to send", which describes a repo that is simply up
|
|
252
|
+
// to date. This one has never recorded anything, which for a LINKED repo usually means
|
|
253
|
+
// the app announces no projectId and its runs are landing under a different root.
|
|
254
|
+
'nothing recorded here yet — if this app has been driven, it is reporting somewhere else'
|
|
255
|
+
: 'nothing to send';
|
|
256
|
+
const pull = 0 === report.pulled
|
|
257
|
+
? ''
|
|
258
|
+
: `, pulled ${String(report.pulled)} decision(s)${report.morePending ? ' (more waiting)' : ''}`;
|
|
259
|
+
/*
|
|
260
|
+
* One reason, not a count. A rejection count tells somebody they have a problem and nothing about
|
|
261
|
+
* which problem — and these arrive from a BACKGROUND daemon, so the summary line is often the only
|
|
262
|
+
* place anybody ever sees it. Rejections in one cycle almost always share a cause (a version skew
|
|
263
|
+
* refuses every payload the same way), so the first reason plus the count is the whole story
|
|
264
|
+
* without printing a line per run; `reticle sync` still lists them all.
|
|
265
|
+
*/
|
|
266
|
+
const bad = 0 === report.runsRejected.length
|
|
267
|
+
? ''
|
|
268
|
+
: `, ${String(report.runsRejected.length)} rejected — ${report.runsRejected[0]?.reason ?? 'no reason given'}`;
|
|
269
|
+
return `${push}${pull}${bad}`;
|
|
270
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { type SyncReport } from './sync-cycle.js';
|
|
2
|
+
import type { ProjectCloud } from './cloud-config.js';
|
|
3
|
+
/** How often the daemon cycles. See the note above on why this is a constant and not a curve. */
|
|
4
|
+
export declare const DAEMON_SYNC_INTERVAL_MS = 60000;
|
|
5
|
+
/**
|
|
6
|
+
* The cadence while something is actually happening.
|
|
7
|
+
*
|
|
8
|
+
* A fixed minute is the wrong shape for both states it has to cover. During a drive the ledger
|
|
9
|
+
* changes on every tool call and a minute of lag is what makes a dashboard feel dead; idle, a minute
|
|
10
|
+
* is already more often than nothing has changed deserves.
|
|
11
|
+
*
|
|
12
|
+
* So the interval follows the work: a cycle that MOVED something earns the fast rate, a cycle that
|
|
13
|
+
* moved nothing backs off to the slow one. No session plumbing and no new configuration — activity
|
|
14
|
+
* is inferred from what the last cycle actually sent, which is the only honest evidence available.
|
|
15
|
+
*
|
|
16
|
+
* Deliberately not per-tool-call. A push in the tool path would put the network in the agent's inner
|
|
17
|
+
* loop, and verification working with the network down is a promise this product makes.
|
|
18
|
+
*/
|
|
19
|
+
export declare const DAEMON_SYNC_ACTIVE_INTERVAL_MS = 5000;
|
|
20
|
+
export interface SyncDaemonDeps {
|
|
21
|
+
reticleRoot: string;
|
|
22
|
+
/** Resolved per tick, not once: a repo linked while the daemon is alive starts syncing itself. */
|
|
23
|
+
cloud: () => Promise<ProjectCloud>;
|
|
24
|
+
/**
|
|
25
|
+
* Every OTHER `.reticle` root on this machine that might be linked.
|
|
26
|
+
*
|
|
27
|
+
* One daemon serves many projects — that is what `artifactRootFor` exists for — and this loop
|
|
28
|
+
* pushed exactly one of them: whichever directory the daemon happened to be started in. Every
|
|
29
|
+
* other linked repo went silent, and silent is indistinguishable from "nobody verified anything",
|
|
30
|
+
* which is the worst possible failure for a dashboard somebody is deciding budget on.
|
|
31
|
+
*
|
|
32
|
+
* Optional: a caller that does not supply it gets exactly the old single-root behaviour.
|
|
33
|
+
*/
|
|
34
|
+
otherRoots?: () => Promise<readonly string[]>;
|
|
35
|
+
/** Resolve the link for a root that is not this daemon's own. Required to use `otherRoots`. */
|
|
36
|
+
cloudFor?: (root: string) => Promise<ProjectCloud>;
|
|
37
|
+
now?: () => number;
|
|
38
|
+
intervalMs?: number;
|
|
39
|
+
/** Injected for the test; the real one is `fetch`. */
|
|
40
|
+
request?: (url: string, init: {
|
|
41
|
+
method: string;
|
|
42
|
+
headers: Record<string, string>;
|
|
43
|
+
body?: string;
|
|
44
|
+
}) => Promise<{
|
|
45
|
+
status: number;
|
|
46
|
+
text: string;
|
|
47
|
+
}>;
|
|
48
|
+
}
|
|
49
|
+
export interface SyncDaemon {
|
|
50
|
+
/** Run one cycle now, whatever the timer is doing. Returns undefined when not linked. */
|
|
51
|
+
syncNow: () => Promise<SyncReport | undefined>;
|
|
52
|
+
/**
|
|
53
|
+
* Something worth shipping just landed — cycle soon rather than at the next tick.
|
|
54
|
+
*
|
|
55
|
+
* A verification RUN is the artifact people wait on, and making them wait up to a full interval
|
|
56
|
+
* for it is the difference between a dashboard that reflects the work and one that lags it. The
|
|
57
|
+
* counters keep the timer: they are a rolling aggregate, and a write per tool call to move a
|
|
58
|
+
* number nobody is watching that second is exactly the cost the interval exists to avoid.
|
|
59
|
+
*
|
|
60
|
+
* Coalescing, never additive: many nudges in a burst collapse into one cycle.
|
|
61
|
+
*/
|
|
62
|
+
nudge: () => void;
|
|
63
|
+
stop: () => void;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Start the loop. Safe to call for an UNLINKED project: it resolves the link on every tick and does
|
|
67
|
+
* nothing until one appears, which is what lets `reticle link` take effect without a restart.
|
|
68
|
+
*/
|
|
69
|
+
export declare function startSyncDaemon(deps: SyncDaemonDeps): SyncDaemon;
|