@reticlehq/server 2.11.0 → 2.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/SKILL.md +93 -111
- package/dist/bridge/auth-failure-reason.d.ts +6 -2
- package/dist/bridge/auth-failure-reason.js +15 -8
- package/dist/bridge/bridge.d.ts +18 -14
- package/dist/bridge/bridge.js +248 -165
- package/dist/bridge/bridge.test-harness.d.ts +2 -0
- package/dist/bridge/bridge.test-harness.js +8 -1
- package/dist/bridge/pairing-token.d.ts +5 -0
- package/dist/bridge/pairing-token.js +28 -0
- package/dist/bridge/protocol-skew.d.ts +37 -0
- package/dist/bridge/protocol-skew.js +45 -0
- package/dist/capsule/capsule-store.d.ts +8 -7
- package/dist/capsule/capsule-store.js +1 -1
- package/dist/capsule/causal-summary.d.ts +2 -2
- package/dist/capsule/causal-summary.js +7 -2
- package/dist/chromium-launch-options.d.ts +28 -0
- package/dist/chromium-launch-options.js +29 -0
- package/dist/cli/cli-doctor.js +39 -1
- package/dist/cli/cli-flow-commands.d.ts +3 -1
- package/dist/cli/cli-flow-commands.js +34 -4
- package/dist/cli/cli-kill.d.ts +2 -1
- package/dist/cli/cli-launch.d.ts +11 -1
- package/dist/cli/cli-launch.js +59 -10
- package/dist/cli/cli-parse.d.ts +17 -1
- package/dist/cli/cli-parse.js +238 -12
- package/dist/cli/cli-update-commands.js +20 -1
- package/dist/cli/cli-verify.d.ts +2 -1
- package/dist/cli/cli-verify.js +5 -4
- package/dist/cli/cloud-cli.js +428 -253
- package/dist/cli/cloud-keystore.d.ts +37 -0
- package/dist/cli/cloud-keystore.js +97 -0
- package/dist/cli/cloud-kit.d.ts +37 -0
- package/dist/cli/cloud-kit.js +121 -0
- package/dist/cli/cloud-login.d.ts +17 -0
- package/dist/cli/cloud-login.js +183 -0
- package/dist/cli/cloud-session.d.ts +48 -0
- package/dist/cli/cloud-session.js +53 -0
- package/dist/cli/config-discovery.d.ts +2 -1
- package/dist/cli/config-discovery.js +1 -0
- package/dist/cli/daemon-start-options.d.ts +32 -0
- package/dist/cli/daemon-start-options.js +38 -0
- package/dist/cli/device-flow.d.ts +76 -0
- package/dist/cli/device-flow.js +56 -0
- package/dist/cli/doctor-rows.d.ts +12 -1
- package/dist/cli/doctor-rows.js +12 -1
- package/dist/cli/drive-attach.d.ts +13 -0
- package/dist/cli/drive-attach.js +18 -0
- package/dist/cli/drive-command.js +7 -3
- package/dist/cli/gate-exit.d.ts +21 -0
- package/dist/cli/gate-exit.js +21 -0
- package/dist/cli/gate-hook-message.d.ts +33 -0
- package/dist/cli/gate-hook-message.js +46 -0
- package/dist/cli/mcp-command.d.ts +25 -0
- package/dist/cli/mcp-command.js +174 -0
- package/dist/cli/project-name.d.ts +30 -0
- package/dist/cli/project-name.js +44 -0
- package/dist/cli/sibling-ports.d.ts +37 -0
- package/dist/cli/sibling-ports.js +56 -0
- package/dist/cli/status-next-action.d.ts +14 -0
- package/dist/cli/status-next-action.js +4 -4
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +167 -186
- package/dist/cloud/cloud-config.d.ts +24 -2
- package/dist/cloud/cloud-config.js +64 -5
- package/dist/cloud/cloud-sync.d.ts +16 -5
- package/dist/cloud/cloud-sync.js +64 -1
- package/dist/cloud/sync-cycle.d.ts +110 -0
- package/dist/cloud/sync-cycle.js +270 -0
- package/dist/cloud/sync-daemon.d.ts +69 -0
- package/dist/cloud/sync-daemon.js +260 -0
- package/dist/cloud/sync-disk.d.ts +9 -0
- package/dist/cloud/sync-disk.js +191 -0
- package/dist/cloud/sync-hash.d.ts +2 -0
- package/dist/cloud/sync-hash.js +35 -0
- package/dist/crawl/crawl-tools.js +5 -3
- package/dist/crawl/crawl.d.ts +12 -13
- package/dist/crawl/crawl.js +25 -3
- package/dist/daemon/daemon-resolve.d.ts +122 -0
- package/dist/daemon/daemon-resolve.js +187 -0
- package/dist/daemon/dev-servers.d.ts +15 -0
- package/dist/daemon/dev-servers.js +68 -0
- package/dist/daemon/free-port.d.ts +29 -0
- package/dist/daemon/free-port.js +55 -0
- package/dist/daemon/heartbeat.d.ts +0 -4
- package/dist/daemon/heartbeat.js +2 -2
- package/dist/daemon/wake-decision.d.ts +32 -0
- package/dist/daemon/wake-decision.js +38 -0
- package/dist/dev/stale-issue-guard.d.ts +0 -2
- package/dist/dev/stale-issue-guard.js +1 -1
- package/dist/domain/domain-tools.js +2 -2
- package/dist/events/contradictions.d.ts +59 -0
- package/dist/events/contradictions.js +253 -27
- package/dist/events/declared.d.ts +17 -0
- package/dist/events/declared.js +43 -1
- package/dist/events/echo-mismatch.js +65 -1
- package/dist/events/event-filters.d.ts +8 -0
- package/dist/events/event-filters.js +19 -2
- package/dist/events/observed-in-window.d.ts +12 -0
- package/dist/events/observed-in-window.js +17 -0
- package/dist/events/predicate-asks.d.ts +13 -0
- package/dist/events/predicate-asks.js +22 -0
- package/dist/events/predicate-eval.js +117 -27
- package/dist/events/predicate-parse.d.ts +0 -6
- package/dist/events/predicate-parse.js +43 -3
- package/dist/events/predicate-precheck.d.ts +8 -0
- package/dist/events/predicate-precheck.js +41 -0
- package/dist/events/predicate-route.d.ts +14 -0
- package/dist/events/predicate-route.js +36 -8
- package/dist/events/predicate-schema.d.ts +44 -0
- package/dist/events/predicate-schema.js +126 -23
- package/dist/events/predicate.d.ts +20 -0
- package/dist/events/predicate.js +77 -6
- package/dist/events/split-text-miss.d.ts +30 -0
- package/dist/events/split-text-miss.js +44 -0
- package/dist/flows/coverage.d.ts +0 -8
- package/dist/flows/coverage.js +1 -1
- package/dist/flows/decision.d.ts +5 -1
- package/dist/flows/decision.js +33 -7
- package/dist/flows/flake.d.ts +6 -8
- package/dist/flows/flake.js +1 -1
- package/dist/flows/flow-intent.d.ts +52 -0
- package/dist/flows/flow-intent.js +123 -0
- package/dist/flows/flow-memory-consult.d.ts +55 -0
- package/dist/flows/flow-memory-consult.js +38 -0
- package/dist/flows/flow-replay-run.d.ts +18 -7
- package/dist/flows/flow-replay-run.js +263 -23
- package/dist/flows/flow-replay.js +11 -2
- package/dist/flows/flow-step-runners.d.ts +8 -0
- package/dist/flows/flow-step-runners.js +8 -2
- package/dist/flows/flow-store-for-session.d.ts +31 -0
- package/dist/flows/flow-store-for-session.js +33 -0
- package/dist/flows/flow-success.js +15 -3
- package/dist/flows/flow-tools.js +87 -33
- package/dist/flows/flows.d.ts +14 -1
- package/dist/flows/flows.js +502 -24
- package/dist/flows/heal-run.js +5 -2
- package/dist/flows/predicate-to-expect.d.ts +17 -12
- package/dist/flows/predicate-to-expect.js +36 -15
- package/dist/flows/recordings.d.ts +13 -1
- package/dist/flows/recordings.js +17 -2
- package/dist/flows/replay.d.ts +22 -2
- package/dist/flows/replay.js +28 -2
- package/dist/flows/server-verify.js +16 -2
- package/dist/flows/verify-change-tools.js +9 -3
- package/dist/honesty/blind-spots.d.ts +51 -15
- package/dist/honesty/blind-spots.js +64 -4
- package/dist/honesty/feature-capture.d.ts +150 -0
- package/dist/honesty/feature-capture.js +233 -0
- package/dist/honesty/honesty.d.ts +24 -0
- package/dist/honesty/honesty.js +25 -1
- package/dist/honesty/instrumentation-gaps.d.ts +39 -6
- package/dist/honesty/instrumentation-gaps.js +88 -5
- package/dist/honesty/tool-hit-rate.d.ts +86 -0
- package/dist/honesty/tool-hit-rate.js +90 -0
- package/dist/honesty/undeclared-change.d.ts +51 -0
- package/dist/honesty/undeclared-change.js +56 -0
- package/dist/honesty/verified.d.ts +16 -0
- package/dist/honesty/verified.js +84 -5
- package/dist/http-server.d.ts +0 -1
- package/dist/http-server.js +13 -1
- package/dist/hunt/hunt-report.d.ts +0 -2
- package/dist/hunt/hunt-report.js +1 -1
- package/dist/impact/impact-recorder.d.ts +31 -8
- package/dist/impact/impact-recorder.js +104 -21
- package/dist/impact/impact-store.d.ts +13 -4
- package/dist/impact/impact-store.js +44 -3
- package/dist/index.d.ts +4 -1
- package/dist/index.js +113 -7
- package/dist/init/agent-rules.d.ts +30 -6
- package/dist/init/agent-rules.js +54 -13
- package/dist/init/astro-patch.d.ts +16 -0
- package/dist/init/astro-patch.js +28 -0
- package/dist/init/capabilities.d.ts +4 -3
- package/dist/init/capabilities.js +23 -3
- package/dist/init/closing-hint.d.ts +36 -0
- package/dist/init/closing-hint.js +120 -0
- package/dist/init/confirm.d.ts +27 -28
- package/dist/init/confirm.js +51 -11
- package/dist/init/cra.d.ts +34 -5
- package/dist/init/cra.js +96 -25
- package/dist/init/csp-check.js +17 -2
- package/dist/init/csp-doctor.d.ts +8 -1
- package/dist/init/csp-doctor.js +8 -3
- package/dist/init/dev-script.d.ts +50 -0
- package/dist/init/dev-script.js +66 -0
- package/dist/init/format-generated.d.ts +18 -0
- package/dist/init/format-generated.js +39 -0
- package/dist/init/install-hint.d.ts +9 -0
- package/dist/init/install-hint.js +29 -0
- package/dist/init/mcp-clients.d.ts +7 -0
- package/dist/init/mcp-clients.js +7 -0
- package/dist/init/mcp.d.ts +17 -6
- package/dist/init/mcp.js +27 -11
- package/dist/init/node-io.js +11 -1
- package/dist/init/plan-framework.d.ts +0 -5
- package/dist/init/plan-framework.js +147 -37
- package/dist/init/plan.d.ts +30 -37
- package/dist/init/plan.js +50 -60
- package/dist/init/preflight.d.ts +28 -0
- package/dist/init/preflight.js +38 -0
- package/dist/init/refresh-rules.d.ts +39 -0
- package/dist/init/refresh-rules.js +51 -0
- package/dist/init/run.d.ts +32 -35
- package/dist/init/run.js +151 -202
- package/dist/init/slash-command.d.ts +1 -1
- package/dist/init/snippets.js +90 -31
- package/dist/init/vite-config.d.ts +1 -1
- package/dist/init/vite-config.js +39 -25
- package/dist/init/workspace-apps.d.ts +17 -10
- package/dist/init/workspace-apps.js +110 -0
- package/dist/input/network-mock-tools.js +3 -6
- package/dist/input/real-input.d.ts +2 -0
- package/dist/input/real-input.js +28 -7
- package/dist/input/scroll-find.d.ts +7 -4
- package/dist/input/scroll-find.js +63 -25
- package/dist/input/scroll-tools.js +4 -6
- package/dist/input/viewport-tools.js +5 -4
- package/dist/intent/inline-intent.d.ts +37 -0
- package/dist/intent/inline-intent.js +193 -0
- package/dist/intent/intent-shard-store.d.ts +72 -0
- package/dist/intent/intent-shard-store.js +203 -0
- package/dist/intent/intent-shard.d.ts +411 -0
- package/dist/intent/intent-shard.js +176 -0
- package/dist/intent/intent-store.d.ts +49 -0
- package/dist/intent/intent-store.js +133 -0
- package/dist/intent/intent-subject.d.ts +45 -0
- package/dist/intent/intent-subject.js +177 -0
- package/dist/intent/intent-tools.d.ts +2 -0
- package/dist/intent/intent-tools.js +143 -0
- package/dist/intent/open-intents.d.ts +42 -0
- package/dist/intent/open-intents.js +40 -0
- package/dist/journal/envelope.d.ts +26 -26
- package/dist/journal/journal-recorder.d.ts +23 -0
- package/dist/journal/journal-recorder.js +35 -0
- package/dist/license/license-env.d.ts +29 -0
- package/dist/license/license-env.js +148 -0
- package/dist/loopback-agent.d.ts +31 -0
- package/dist/loopback-agent.js +57 -0
- package/dist/mcp/attach-memory.d.ts +83 -0
- package/dist/mcp/attach-memory.js +185 -0
- package/dist/mcp/client-identity.d.ts +31 -0
- package/dist/mcp/client-identity.js +51 -0
- package/dist/mcp/mcp-post-transport.d.ts +26 -0
- package/dist/mcp/mcp-post-transport.js +109 -0
- package/dist/mcp/mcp-proxy.d.ts +2 -8
- package/dist/mcp/mcp-proxy.js +64 -62
- package/dist/mcp/mcp.d.ts +2 -0
- package/dist/mcp/mcp.js +52 -24
- package/dist/mcp/proxy-backoff.d.ts +11 -0
- package/dist/mcp/proxy-backoff.js +13 -0
- package/dist/mcp/proxy-daemon-probe.d.ts +1 -0
- package/dist/mcp/proxy-daemon-probe.js +7 -1
- package/dist/mcp/proxy-handshake.d.ts +23 -0
- package/dist/mcp/proxy-handshake.js +31 -0
- package/dist/mcp/server-instructions.js +45 -3
- package/dist/mcp/tool-catalog-cache.d.ts +16 -0
- package/dist/mcp/tool-catalog-cache.js +22 -0
- package/dist/memory/memory-tools.d.ts +2 -0
- package/dist/memory/memory-tools.js +82 -0
- package/dist/memory/project-memory.d.ts +34 -0
- package/dist/memory/project-memory.js +96 -0
- package/dist/oracles/flow-instrument-gaps.d.ts +4 -4
- package/dist/oracles/flow-instrument-gaps.js +4 -5
- package/dist/oracles/propose-consequences.js +12 -3
- package/dist/oracles/self-instrument.d.ts +9 -9
- package/dist/oracles/self-instrument.js +19 -13
- package/dist/pool/browser-pool.d.ts +75 -5
- package/dist/pool/browser-pool.js +144 -5
- package/dist/pool/playwright-launcher.js +12 -1
- package/dist/project/fs-port.d.ts +3 -0
- package/dist/project/fs-port.js +3 -2
- package/dist/project/memory-fs.js +6 -1
- package/dist/project/project-tools.js +6 -35
- package/dist/project/reticle-dir.d.ts +17 -2
- package/dist/project/reticle-dir.js +22 -4
- package/dist/runs/context-tools.d.ts +2 -0
- package/dist/runs/context-tools.js +89 -0
- package/dist/runs/run-context.d.ts +40 -0
- package/dist/runs/run-context.js +158 -0
- package/dist/runs/run-diff.d.ts +2 -1
- package/dist/runs/run-store.d.ts +10 -0
- package/dist/runs/run-store.js +10 -0
- package/dist/runs/verification-sync.d.ts +0 -11
- package/dist/runs/verification-sync.js +24 -3
- package/dist/runs/verify-server.d.ts +8 -1
- package/dist/runs/verify-server.js +14 -2
- package/dist/session/command-timeout.d.ts +5 -2
- package/dist/session/command-timeout.js +5 -4
- package/dist/session/dev-server-probe.d.ts +0 -8
- package/dist/session/dev-server-probe.js +1 -1
- package/dist/session/last-act.d.ts +7 -1
- package/dist/session/last-act.js +2 -2
- package/dist/session/lease-visibility.d.ts +67 -0
- package/dist/session/lease-visibility.js +76 -0
- package/dist/session/live-control-tools.js +1 -42
- package/dist/session/no-session-diagnosis.d.ts +33 -0
- package/dist/session/no-session-diagnosis.js +82 -24
- package/dist/session/no-session-next-action.d.ts +12 -0
- package/dist/session/no-session-next-action.js +77 -6
- package/dist/session/no-session-watch.d.ts +18 -6
- package/dist/session/no-session-watch.js +125 -75
- package/dist/session/output-budget.js +4 -1
- package/dist/session/pending-commands.d.ts +11 -1
- package/dist/session/pending-commands.js +12 -2
- package/dist/session/resolve-within.d.ts +34 -0
- package/dist/session/resolve-within.js +53 -0
- package/dist/session/session-health.d.ts +9 -0
- package/dist/session/session-health.js +23 -0
- package/dist/session/session-info.d.ts +18 -0
- package/dist/session/session-info.js +11 -0
- package/dist/session/session-manager.d.ts +14 -0
- package/dist/session/session-manager.js +111 -4
- package/dist/session/session-mirror.d.ts +16 -0
- package/dist/session/session-mirror.js +22 -0
- package/dist/session/session-recommendation.d.ts +19 -5
- package/dist/session/session-recommendation.js +25 -9
- package/dist/session/session-successor.d.ts +37 -0
- package/dist/session/session-successor.js +113 -0
- package/dist/session/session-tools.js +2 -2
- package/dist/session/session.d.ts +53 -3
- package/dist/session/session.js +162 -9
- package/dist/session/wall-clock.d.ts +12 -0
- package/dist/session/wall-clock.js +14 -0
- package/dist/setup/agent-configs.d.ts +96 -0
- package/dist/setup/agent-configs.js +333 -0
- package/dist/setup/agent-io.d.ts +9 -0
- package/dist/setup/agent-io.js +16 -0
- package/dist/setup/agent-writer.d.ts +51 -0
- package/dist/setup/agent-writer.js +115 -0
- package/dist/setup/approval-migration.d.ts +47 -0
- package/dist/setup/approval-migration.js +68 -0
- package/dist/setup/auto-approve.d.ts +85 -0
- package/dist/setup/auto-approve.js +150 -0
- package/dist/setup/bridge-port.d.ts +15 -0
- package/dist/setup/bridge-port.js +24 -0
- package/dist/setup/desktop-shape.d.ts +57 -0
- package/dist/setup/desktop-shape.js +63 -0
- package/dist/setup/dev-server-wait.d.ts +60 -0
- package/dist/setup/dev-server-wait.js +65 -0
- package/dist/setup/drive-agent.d.ts +71 -0
- package/dist/setup/drive-agent.js +171 -0
- package/dist/setup/drive-plan.d.ts +68 -0
- package/dist/setup/drive-plan.js +130 -0
- package/dist/setup/ensure-daemon.d.ts +29 -0
- package/dist/setup/ensure-daemon.js +58 -0
- package/dist/setup/init-runtime.d.ts +47 -0
- package/dist/setup/init-runtime.js +160 -0
- package/dist/setup/interrupt.d.ts +25 -0
- package/dist/setup/interrupt.js +40 -0
- package/dist/setup/license-key.d.ts +39 -0
- package/dist/setup/license-key.js +72 -0
- package/dist/setup/listeners.d.ts +37 -0
- package/dist/setup/listeners.js +85 -0
- package/dist/setup/node-effects.d.ts +60 -0
- package/dist/setup/node-effects.js +210 -0
- package/dist/setup/page-probe.d.ts +39 -0
- package/dist/setup/page-probe.js +49 -0
- package/dist/setup/relaunch.d.ts +36 -0
- package/dist/setup/relaunch.js +57 -0
- package/dist/setup/remaining-steps.d.ts +19 -0
- package/dist/setup/remaining-steps.js +31 -0
- package/dist/setup/run-setup.d.ts +99 -0
- package/dist/setup/run-setup.js +187 -0
- package/dist/setup/session-pick.d.ts +47 -0
- package/dist/setup/session-pick.js +51 -0
- package/dist/setup/setup-command.d.ts +45 -0
- package/dist/setup/setup-command.js +199 -0
- package/dist/setup/setup-options.d.ts +74 -0
- package/dist/setup/setup-options.js +67 -0
- package/dist/setup/transcripts.d.ts +16 -0
- package/dist/setup/transcripts.js +43 -0
- package/dist/status-payload.d.ts +14 -1
- package/dist/status-payload.js +23 -1
- package/dist/telemetry/automation-hint.d.ts +2 -1
- package/dist/telemetry/daemon-telemetry.d.ts +8 -1
- package/dist/telemetry/daemon-telemetry.js +52 -5
- package/dist/telemetry/error-class.js +1 -1
- package/dist/telemetry/feedback-context.d.ts +9 -1
- package/dist/telemetry/feedback-context.js +60 -29
- package/dist/telemetry/feedback-local.d.ts +15 -0
- package/dist/telemetry/feedback-local.js +56 -0
- package/dist/telemetry/feedback-outbox.d.ts +2 -1
- package/dist/telemetry/feedback.d.ts +11 -0
- package/dist/telemetry/feedback.js +19 -6
- package/dist/telemetry/init-telemetry.d.ts +2 -0
- package/dist/telemetry/init-telemetry.js +2 -0
- package/dist/telemetry/install-source.d.ts +14 -0
- package/dist/telemetry/install-source.js +30 -0
- package/dist/telemetry/license-activation.d.ts +7 -1
- package/dist/telemetry/license-activation.js +9 -5
- package/dist/telemetry/mcp-connection.js +7 -0
- package/dist/telemetry/project-profile.d.ts +12 -9
- package/dist/telemetry/project-profile.js +12 -7
- package/dist/telemetry/proxy-telemetry.d.ts +3 -0
- package/dist/telemetry/proxy-telemetry.js +21 -0
- package/dist/telemetry/run-telemetry.d.ts +0 -2
- package/dist/telemetry/run-telemetry.js +1 -1
- package/dist/telemetry/session-metrics.d.ts +12 -0
- package/dist/telemetry/session-metrics.js +35 -1
- package/dist/telemetry/telemetry.js +17 -2
- package/dist/telemetry/tool-refused.d.ts +3 -1
- package/dist/telemetry/tool-refused.js +34 -2
- package/dist/telemetry/verification-of.js +2 -2
- package/dist/tools/act-capsule.d.ts +10 -0
- package/dist/tools/act-capsule.js +8 -2
- package/dist/tools/act-danger.d.ts +2 -1
- package/dist/tools/act-danger.js +11 -3
- package/dist/tools/act-observation.d.ts +41 -0
- package/dist/tools/act-observation.js +36 -0
- package/dist/tools/act-preflight.d.ts +20 -0
- package/dist/tools/act-preflight.js +98 -0
- package/dist/tools/act-sequence-retry.d.ts +45 -0
- package/dist/tools/act-sequence-retry.js +58 -0
- package/dist/tools/act-sequence-tool.d.ts +13 -0
- package/dist/tools/act-sequence-tool.js +135 -0
- package/dist/tools/act-target.d.ts +14 -0
- package/dist/tools/act-target.js +47 -0
- package/dist/tools/act-tools.d.ts +21 -1
- package/dist/tools/act-tools.js +196 -204
- package/dist/tools/assert-source.d.ts +22 -0
- package/dist/tools/assert-source.js +53 -0
- package/dist/tools/assert-verdict.d.ts +20 -3
- package/dist/tools/assert-verdict.js +58 -20
- package/dist/tools/browser-tools.js +6 -22
- package/dist/tools/complete-snapshot.d.ts +48 -0
- package/dist/tools/complete-snapshot.js +75 -0
- package/dist/tools/coverage-tools.d.ts +10 -1
- package/dist/tools/coverage-tools.js +123 -69
- package/dist/tools/dynamic-tools.js +53 -13
- package/dist/tools/error-recovery.d.ts +25 -14
- package/dist/tools/error-recovery.js +78 -9
- package/dist/tools/feedback-tools.d.ts +1 -1
- package/dist/tools/feedback-tools.js +2 -2
- package/dist/tools/invoke-tool.js +121 -11
- package/dist/tools/lease-hint.d.ts +10 -0
- package/dist/tools/lease-hint.js +53 -4
- package/dist/tools/lease-tools.d.ts +35 -5
- package/dist/tools/lease-tools.js +191 -13
- package/dist/tools/merged-name-redirect.d.ts +13 -0
- package/dist/tools/merged-name-redirect.js +22 -0
- package/dist/tools/navigate-arrival.d.ts +0 -2
- package/dist/tools/navigate-arrival.js +1 -1
- package/dist/tools/numeric-bounds.d.ts +100 -0
- package/dist/tools/numeric-bounds.js +125 -0
- package/dist/tools/observe-tools.js +109 -65
- package/dist/tools/read-tools.js +31 -8
- package/dist/tools/real-input-attempt.d.ts +32 -0
- package/dist/tools/real-input-attempt.js +212 -2
- package/dist/tools/reconcile-tools.js +22 -8
- package/dist/tools/resolve-target.d.ts +0 -7
- package/dist/tools/sdk-marker-probe.d.ts +29 -0
- package/dist/tools/sdk-marker-probe.js +57 -0
- package/dist/tools/snapshot-delta.d.ts +1 -1
- package/dist/tools/tool-kit.d.ts +24 -0
- package/dist/tools/tool-kit.js +21 -0
- package/dist/tools/tool-names.d.ts +5 -0
- package/dist/tools/tool-names.js +5 -0
- package/dist/tools/tool-surface.d.ts +85 -6
- package/dist/tools/tool-surface.js +122 -2
- package/dist/tools/tools-helpers.d.ts +19 -0
- package/dist/tools/tools-helpers.js +46 -0
- package/dist/tools/tools.d.ts +2 -0
- package/dist/tools/tools.js +78 -12
- package/dist/tools/unadvertised-help.js +4 -4
- package/dist/tools/verify-next-baton.d.ts +53 -0
- package/dist/tools/verify-next-baton.js +52 -0
- package/dist/update/reticle-deps.d.ts +9 -0
- package/dist/update/reticle-deps.js +27 -0
- package/dist/update/sdk-sync.d.ts +1 -2
- package/dist/update/sdk-sync.js +1 -21
- package/dist/update/update-nudge.d.ts +25 -10
- package/dist/update/update-nudge.js +32 -3
- package/dist/update/updater.d.ts +1 -1
- package/dist/version/peer-announce.js +4 -2
- package/dist/version/sdk-fix.d.ts +47 -0
- package/dist/version/sdk-fix.js +127 -0
- package/dist/version/version-nudge.d.ts +14 -0
- package/dist/version/version-nudge.js +21 -0
- package/dist/version/version-skew.d.ts +38 -3
- package/dist/version/version-skew.js +83 -7
- package/dist/visual/visual-store.d.ts +6 -6
- package/dist/visual/visual-store.js +18 -16
- package/dist/visual/visual-tools.js +57 -12
- package/docs/README.md +2 -1
- package/docs/cli/cloud.mdx +1 -1
- package/docs/cli/doctor.mdx +2 -0
- package/docs/cli/serve.mdx +4 -2
- package/docs/cli/verify.mdx +4 -5
- package/docs/cli.mdx +7 -3
- package/docs/desktop.mdx +1 -1
- package/docs/docs.json +2 -2
- package/docs/gates.md +1 -1
- package/docs/getting-started.md +19 -3
- package/docs/index.mdx +9 -6
- package/docs/install-agentic.mdx +38 -12
- package/docs/install-manual.mdx +1 -1
- package/docs/packages/browser.mdx +2 -2
- package/docs/packages/server.mdx +1 -1
- package/docs/packages.mdx +8 -6
- package/docs/platform-integration.md +17 -0
- package/docs/predicates.mdx +8 -0
- package/docs/reticle-manual.md +106 -0
- package/docs/skill-file.mdx +3 -3
- package/docs/telemetry-contract.md +108 -4
- package/docs/tools-act-sequence.mdx +16 -0
- package/docs/tools-tools-and-run.mdx +2 -4
- package/docs/troubleshooting.mdx +9 -0
- package/docs/usage.md +4 -3
- package/package.json +4 -3
- package/dist/init/cursor.d.ts +0 -24
- package/dist/init/cursor.js +0 -80
- package/dist/session/session-readiness.d.ts +0 -34
- package/dist/session/session-readiness.js +0 -36
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Automatic sync, for the session nobody thinks about.
|
|
3
|
+
*
|
|
4
|
+
* Somebody runs Reticle all morning. Every tool call rewrites the impact record; every verification
|
|
5
|
+
* writes a run. If sync is a command a human types, the dashboard is stale for exactly as long as
|
|
6
|
+
* they are busy — which is precisely when the numbers on it would have been worth looking at.
|
|
7
|
+
*
|
|
8
|
+
* So the daemon does it. Not a new process, not a thing to remember: the daemon is already running
|
|
9
|
+
* for the whole session (it is what the agent talks to), so it is the only place where "sync while
|
|
10
|
+
* work is happening" costs nobody anything.
|
|
11
|
+
*
|
|
12
|
+
* ── THE THREE RULES THAT KEEP IT INVISIBLE ────────────────────────────────────────────────────
|
|
13
|
+
* QUIET WHEN IDLE. A cycle on an unchanged session is one small GET and nothing else. That is what
|
|
14
|
+
* makes a one-minute timer affordable; it is also why the timer is not adaptive — a scheme that
|
|
15
|
+
* "backs off when idle" is complexity paid for a cost that is already near zero.
|
|
16
|
+
*
|
|
17
|
+
* NEVER IN THE WAY. Nothing here is awaited by a tool call. A sync that can slow down or fail a
|
|
18
|
+
* verification would be a sync worth switching off, and the local record is authoritative anyway.
|
|
19
|
+
*
|
|
20
|
+
* SILENT UNLESS IT MATTERS. A failed cycle is logged once per NEW error, not once per minute: a
|
|
21
|
+
* laptop on a train would otherwise fill the log with the same line four hundred times and teach
|
|
22
|
+
* everyone to ignore it.
|
|
23
|
+
*/
|
|
24
|
+
import { log } from '../log.js';
|
|
25
|
+
import { describeSync, runSyncCycle } from './sync-cycle.js';
|
|
26
|
+
import { diskSink, diskSource, readCloudState } from './sync-disk.js';
|
|
27
|
+
/** How often the daemon cycles. See the note above on why this is a constant and not a curve. */
|
|
28
|
+
export const DAEMON_SYNC_INTERVAL_MS = 60_000;
|
|
29
|
+
/**
|
|
30
|
+
* The cadence while something is actually happening.
|
|
31
|
+
*
|
|
32
|
+
* A fixed minute is the wrong shape for both states it has to cover. During a drive the ledger
|
|
33
|
+
* changes on every tool call and a minute of lag is what makes a dashboard feel dead; idle, a minute
|
|
34
|
+
* is already more often than nothing has changed deserves.
|
|
35
|
+
*
|
|
36
|
+
* So the interval follows the work: a cycle that MOVED something earns the fast rate, a cycle that
|
|
37
|
+
* moved nothing backs off to the slow one. No session plumbing and no new configuration — activity
|
|
38
|
+
* is inferred from what the last cycle actually sent, which is the only honest evidence available.
|
|
39
|
+
*
|
|
40
|
+
* Deliberately not per-tool-call. A push in the tool path would put the network in the agent's inner
|
|
41
|
+
* loop, and verification working with the network down is a promise this product makes.
|
|
42
|
+
*/
|
|
43
|
+
export const DAEMON_SYNC_ACTIVE_INTERVAL_MS = 5_000;
|
|
44
|
+
/** Given to the first cycle so a freshly-started daemon does not race the session that woke it. */
|
|
45
|
+
const FIRST_CYCLE_DELAY_MS = 5_000;
|
|
46
|
+
/**
|
|
47
|
+
* How long a nudge waits before cycling.
|
|
48
|
+
*
|
|
49
|
+
* Long enough to COALESCE — a battery that writes six runs in a second must cause one cycle, not
|
|
50
|
+
* six — and short enough that finishing a verification and looking at the dashboard feels like one
|
|
51
|
+
* action. Not zero: a run artifact is renamed into place and the sibling records it implies are
|
|
52
|
+
* written just after, so cycling on the very first byte would ship a bundle that is missing them.
|
|
53
|
+
*/
|
|
54
|
+
const NUDGE_DELAY_MS = 1_500;
|
|
55
|
+
const defaultRequest = async (url, init) => {
|
|
56
|
+
const res = await fetch(url, init);
|
|
57
|
+
return { status: res.status, text: await res.text() };
|
|
58
|
+
};
|
|
59
|
+
/**
|
|
60
|
+
* Start the loop. Safe to call for an UNLINKED project: it resolves the link on every tick and does
|
|
61
|
+
* nothing until one appears, which is what lets `reticle link` take effect without a restart.
|
|
62
|
+
*/
|
|
63
|
+
export function startSyncDaemon(deps) {
|
|
64
|
+
const intervalMs = deps.intervalMs ?? DAEMON_SYNC_INTERVAL_MS;
|
|
65
|
+
// Never slower than the idle rate: a deployment that lengthens the interval means "sync less", and
|
|
66
|
+
// an active burst must not quietly re-introduce the cost it was lowering.
|
|
67
|
+
const activeIntervalMs = Math.min(DAEMON_SYNC_ACTIVE_INTERVAL_MS, intervalMs);
|
|
68
|
+
const now = deps.now ?? (() => Date.now());
|
|
69
|
+
const request = deps.request ?? defaultRequest;
|
|
70
|
+
let timer;
|
|
71
|
+
let stopped = false;
|
|
72
|
+
let running = false;
|
|
73
|
+
/** The last error already reported. Repeats are swallowed so an offline laptop stays quiet. */
|
|
74
|
+
let reportedError;
|
|
75
|
+
/**
|
|
76
|
+
* Whether the last cycle found a link — undefined until the first one has looked.
|
|
77
|
+
*
|
|
78
|
+
* An unlinked project used to sync nothing and say nothing, which is indistinguishable from a
|
|
79
|
+
* working sync until somebody notices the dashboard is hours behind. Observed for real: the
|
|
80
|
+
* daemon was started from a sibling repo that had never been linked, so every call for an entire
|
|
81
|
+
* session was recorded into THAT repo's ledger and pushed nowhere, and the first evidence was a
|
|
82
|
+
* stale number on a dashboard nobody had reason to distrust.
|
|
83
|
+
*
|
|
84
|
+
* Announced on the FIRST cycle and on every change after it, so `reticle link` mid-session says
|
|
85
|
+
* so too. Once per transition, never per tick — a line every minute is a line people stop reading.
|
|
86
|
+
*/
|
|
87
|
+
let wasLinked;
|
|
88
|
+
/** One root's bundle. The state, source and sink are already per-root; only the caller was not. */
|
|
89
|
+
const pushRoot = async (root, cloud) => {
|
|
90
|
+
if (null === cloud.config)
|
|
91
|
+
return undefined;
|
|
92
|
+
const full = diskSource(root);
|
|
93
|
+
return runSyncCycle({
|
|
94
|
+
config: cloud.config,
|
|
95
|
+
source: {
|
|
96
|
+
runs: () => (cloud.policy.runs ? full.runs() : []),
|
|
97
|
+
flows: () => (cloud.policy.flows ? full.flows() : []),
|
|
98
|
+
derived: (kind) => (cloud.policy.memory ? full.derived(kind) : undefined),
|
|
99
|
+
},
|
|
100
|
+
sink: diskSink(root),
|
|
101
|
+
state: readCloudState(root),
|
|
102
|
+
now,
|
|
103
|
+
request,
|
|
104
|
+
});
|
|
105
|
+
};
|
|
106
|
+
/**
|
|
107
|
+
* Push every other linked root this machine knows about.
|
|
108
|
+
*
|
|
109
|
+
* Isolated per root on purpose. A revoked credential, a deleted directory or an unreachable
|
|
110
|
+
* self-hosted server in ONE repo is a fact about that repo; letting it throw would take the
|
|
111
|
+
* daemon's own push down with it and turn one broken link into a machine-wide outage.
|
|
112
|
+
*/
|
|
113
|
+
const syncOtherRoots = async () => {
|
|
114
|
+
const roots = deps.otherRoots;
|
|
115
|
+
const cloudFor = deps.cloudFor;
|
|
116
|
+
if (roots === undefined || cloudFor === undefined)
|
|
117
|
+
return;
|
|
118
|
+
let list = [];
|
|
119
|
+
try {
|
|
120
|
+
list = await roots();
|
|
121
|
+
}
|
|
122
|
+
catch {
|
|
123
|
+
// Enumeration is best-effort: a registry that cannot be read must not stop this daemon
|
|
124
|
+
// syncing the root it is standing in.
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
for (const root of list) {
|
|
128
|
+
if (root === deps.reticleRoot)
|
|
129
|
+
continue;
|
|
130
|
+
try {
|
|
131
|
+
const cloud = await cloudFor(root);
|
|
132
|
+
if (null === cloud.config)
|
|
133
|
+
continue;
|
|
134
|
+
const report = await pushRoot(root, cloud);
|
|
135
|
+
if (report === undefined)
|
|
136
|
+
continue;
|
|
137
|
+
if (report.error !== undefined) {
|
|
138
|
+
log('reticle_cloud_sync_failed', { root, error: report.error });
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
const moved = report.runsSent > 0 ||
|
|
142
|
+
report.flowsSent > 0 ||
|
|
143
|
+
report.derivedSent.length > 0 ||
|
|
144
|
+
report.pulled > 0;
|
|
145
|
+
// The root is NAMED here and not in the single-root log below, because with several repos
|
|
146
|
+
// reporting, "synced 3 runs" without a directory is not an answer to "synced from where".
|
|
147
|
+
if (moved)
|
|
148
|
+
log('reticle_cloud_synced', { root, summary: describeSync(report) });
|
|
149
|
+
}
|
|
150
|
+
catch (error) {
|
|
151
|
+
log('reticle_cloud_sync_failed', {
|
|
152
|
+
root,
|
|
153
|
+
error: error instanceof Error ? error.message : String(error),
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
};
|
|
158
|
+
const cycle = async () => {
|
|
159
|
+
// Overlap guard: a slow cycle must not have a second one started on top of it, or two bundles
|
|
160
|
+
// race and the cursor written by the loser silently rewinds the winner's progress.
|
|
161
|
+
if (running)
|
|
162
|
+
return undefined;
|
|
163
|
+
running = true;
|
|
164
|
+
try {
|
|
165
|
+
const cloud = await deps.cloud();
|
|
166
|
+
const linked = null !== cloud.config;
|
|
167
|
+
if (linked !== wasLinked) {
|
|
168
|
+
wasLinked = linked;
|
|
169
|
+
if (linked)
|
|
170
|
+
log('reticle_cloud_linked', {
|
|
171
|
+
projectId: cloud.projectId,
|
|
172
|
+
root: deps.reticleRoot,
|
|
173
|
+
});
|
|
174
|
+
// Names the ROOT, because the answer is almost always "the daemon is not where you think it
|
|
175
|
+
// is" — and a message that omits the directory sends people to check their key instead.
|
|
176
|
+
else
|
|
177
|
+
log('reticle_cloud_unlinked', {
|
|
178
|
+
root: deps.reticleRoot,
|
|
179
|
+
fix: 'run `reticle link` in this directory, or start the daemon in the linked one — nothing is being synced from here',
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
// Every OTHER linked root, first. Their failures are logged and never abort this daemon's own
|
|
183
|
+
// push: one repo whose credential was revoked must not silence the rest of the machine.
|
|
184
|
+
await syncOtherRoots();
|
|
185
|
+
if (null === cloud.config)
|
|
186
|
+
return undefined;
|
|
187
|
+
const report = await pushRoot(deps.reticleRoot, cloud);
|
|
188
|
+
// `pushRoot` returns undefined only for an unlinked root, which the guard above has ruled out.
|
|
189
|
+
if (report === undefined)
|
|
190
|
+
return undefined;
|
|
191
|
+
if (report.error !== undefined) {
|
|
192
|
+
if (report.error !== reportedError) {
|
|
193
|
+
reportedError = report.error;
|
|
194
|
+
log('reticle_cloud_sync_failed', { error: report.error });
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
else {
|
|
198
|
+
reportedError = undefined;
|
|
199
|
+
// Only when something actually moved. A per-minute "nothing to send" is noise that trains
|
|
200
|
+
// people to stop reading the log.
|
|
201
|
+
const moved = report.runsSent > 0 ||
|
|
202
|
+
report.flowsSent > 0 ||
|
|
203
|
+
report.derivedSent.length > 0 ||
|
|
204
|
+
report.pulled > 0;
|
|
205
|
+
if (moved)
|
|
206
|
+
log('reticle_cloud_synced', { summary: describeSync(report) });
|
|
207
|
+
/*
|
|
208
|
+
* A cycle that moved something means a drive is in progress, so the next one comes sooner;
|
|
209
|
+
* one that moved nothing means the machine is idle, so it backs straight off. The rate
|
|
210
|
+
* follows the work without needing to be told about sessions, and an idle laptop settles at
|
|
211
|
+
* the same cost it had before this existed.
|
|
212
|
+
*/
|
|
213
|
+
nextDelay = moved ? activeIntervalMs : intervalMs;
|
|
214
|
+
}
|
|
215
|
+
return report;
|
|
216
|
+
}
|
|
217
|
+
catch (error) {
|
|
218
|
+
// Belt and braces: runSyncCycle already swallows, and a throw here would kill the timer.
|
|
219
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
220
|
+
if (message !== reportedError) {
|
|
221
|
+
reportedError = message;
|
|
222
|
+
log('reticle_cloud_sync_failed', { error: message });
|
|
223
|
+
}
|
|
224
|
+
return undefined;
|
|
225
|
+
}
|
|
226
|
+
finally {
|
|
227
|
+
running = false;
|
|
228
|
+
}
|
|
229
|
+
};
|
|
230
|
+
/** The delay for the NEXT cycle: fast while the last one moved something, slow once it stops. */
|
|
231
|
+
let nextDelay = intervalMs;
|
|
232
|
+
const schedule = (delay) => {
|
|
233
|
+
if (stopped)
|
|
234
|
+
return;
|
|
235
|
+
// Replace, never stack. `nudge` and the interval both schedule, and leaving the old timer armed
|
|
236
|
+
// would let every nudge add a permanent extra cycle per minute for the life of the process.
|
|
237
|
+
if (timer !== undefined)
|
|
238
|
+
clearTimeout(timer);
|
|
239
|
+
timer = setTimeout(() => {
|
|
240
|
+
void cycle().finally(() => schedule(nextDelay));
|
|
241
|
+
}, delay);
|
|
242
|
+
// Unref'd: a pending sync must never be the reason a process refuses to exit.
|
|
243
|
+
timer.unref?.();
|
|
244
|
+
};
|
|
245
|
+
schedule(FIRST_CYCLE_DELAY_MS);
|
|
246
|
+
return {
|
|
247
|
+
syncNow: cycle,
|
|
248
|
+
nudge: () => {
|
|
249
|
+
// A cycle already in flight will not pick this up, so the nudge is still scheduled behind it
|
|
250
|
+
// rather than dropped — otherwise the run that arrived during a slow cycle waits a full tick.
|
|
251
|
+
schedule(NUDGE_DELAY_MS);
|
|
252
|
+
},
|
|
253
|
+
stop: () => {
|
|
254
|
+
stopped = true;
|
|
255
|
+
if (timer !== undefined)
|
|
256
|
+
clearTimeout(timer);
|
|
257
|
+
timer = undefined;
|
|
258
|
+
},
|
|
259
|
+
};
|
|
260
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { CloudSyncState, PulledIssues, SyncSink, SyncSource } from './sync-cycle.js';
|
|
2
|
+
/** What the cycle reads, backed by a real `.reticle` directory. */
|
|
3
|
+
export declare function diskSource(reticleRoot: string): SyncSource;
|
|
4
|
+
/** What the cycle writes back into `.reticle/`. */
|
|
5
|
+
export declare function diskSink(reticleRoot: string): SyncSink;
|
|
6
|
+
/** This machine's side of the conversation, or a blank one on first run. */
|
|
7
|
+
export declare function readCloudState(reticleRoot: string): CloudSyncState;
|
|
8
|
+
/** The decisions pulled back from the dashboard, for the HUD and for the next run to read. */
|
|
9
|
+
export declare function readCloudIssues(reticleRoot: string): PulledIssues;
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The filesystem half of a sync cycle: what `.reticle/` looks like to the protocol.
|
|
3
|
+
*
|
|
4
|
+
* Kept apart from `sync-cycle.ts` on purpose. The cycle is the interesting part — what to send, what
|
|
5
|
+
* to skip, what to do when the network is down — and it stays provable only because it never touches
|
|
6
|
+
* a disk. This is the boring part, and boring is what it should be: read a file, tolerate its
|
|
7
|
+
* absence, never throw.
|
|
8
|
+
*
|
|
9
|
+
* EVERY READ IS FORGIVING. A half-written impact.json from a process that died mid-flush must cost
|
|
10
|
+
* one unsynced record, not a crashed sync. There is no state here worth defending against a parse
|
|
11
|
+
* error — the file will be rewritten on the next tool call anyway.
|
|
12
|
+
*/
|
|
13
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, renameSync, writeFileSync, } from 'node:fs';
|
|
14
|
+
import { dirname, join } from 'node:path';
|
|
15
|
+
import { ReticleDir } from '@reticlehq/core';
|
|
16
|
+
import { subjectFor } from '../intent/intent-subject.js';
|
|
17
|
+
const JSON_SUFFIX = '.json';
|
|
18
|
+
/** Read + parse, or undefined for absent/unreadable/malformed. Never throws. */
|
|
19
|
+
function readJson(path) {
|
|
20
|
+
try {
|
|
21
|
+
return existsSync(path) ? JSON.parse(readFileSync(path, 'utf8')) : undefined;
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
return undefined;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Write atomically. tmp + rename, because two daemons can serve one repo at once and a half-written
|
|
29
|
+
* cursor file is one the next read discards — which would silently re-pull the whole decision
|
|
30
|
+
* history, or worse, skip it.
|
|
31
|
+
*/
|
|
32
|
+
function writeJson(path, value) {
|
|
33
|
+
try {
|
|
34
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
35
|
+
const tmp = `${path}.${String(process.pid)}.tmp`;
|
|
36
|
+
writeFileSync(tmp, `${JSON.stringify(value, null, 2)}\n`, 'utf8');
|
|
37
|
+
renameSync(tmp, path);
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
// Bookkeeping that cannot be written must never break a sync. The next cycle re-derives it; the
|
|
41
|
+
// worst case is one redundant upload, which the server dedupes anyway.
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
/** Every `*.json` directly inside a directory, parsed. Absent directory reads as empty. */
|
|
45
|
+
function readJsonDir(dir) {
|
|
46
|
+
try {
|
|
47
|
+
if (!existsSync(dir))
|
|
48
|
+
return [];
|
|
49
|
+
return readdirSync(dir)
|
|
50
|
+
.filter((f) => f.endsWith(JSON_SUFFIX))
|
|
51
|
+
.map((f) => readJson(join(dir, f)))
|
|
52
|
+
.filter((v) => v !== undefined);
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
return [];
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
/** Flows live one directory deeper, under the app's own project id. */
|
|
59
|
+
function readFlows(root) {
|
|
60
|
+
const dir = join(root, ReticleDir.FLOWS_SUBDIR);
|
|
61
|
+
try {
|
|
62
|
+
if (!existsSync(dir))
|
|
63
|
+
return [];
|
|
64
|
+
return readdirSync(dir).flatMap((scope) => readJsonDir(join(dir, scope)));
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
return [];
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
const DERIVED_FILE = {
|
|
71
|
+
impact: ReticleDir.IMPACT_FILE,
|
|
72
|
+
flake: ReticleDir.FLAKE_FILE,
|
|
73
|
+
intent: ReticleDir.INTENT_FILE,
|
|
74
|
+
};
|
|
75
|
+
/** The directory the sharded intent store writes into, beside the legacy flat file. */
|
|
76
|
+
const INTENT_SUBDIR = 'intent';
|
|
77
|
+
const INTENT_INDEX = 'index.json';
|
|
78
|
+
/**
|
|
79
|
+
* Every intent this project holds, flat file and shards merged.
|
|
80
|
+
*
|
|
81
|
+
* The store was split into one file per subject so that writing one record stops rewriting all of
|
|
82
|
+
* them. Sync was not told, and kept reading `intent.json` alone — which the migration deliberately
|
|
83
|
+
* does not delete. The result was the worst shape a sync bug can take: not an error, but a corpus
|
|
84
|
+
* that looked healthy on the dashboard and had silently stopped moving, because every intent
|
|
85
|
+
* captured after the migration stayed on the engineer's laptop.
|
|
86
|
+
*
|
|
87
|
+
* Shards WIN on a collision, matching the store's own rule: an id present in both has been migrated
|
|
88
|
+
* and possibly edited since, and letting the flat copy overwrite that would revert the edit on every
|
|
89
|
+
* cycle. `index.json` is derived from the shards and is skipped — sending it would duplicate every
|
|
90
|
+
* record as a summary of itself.
|
|
91
|
+
*
|
|
92
|
+
* Returns undefined when there is no memory at all, so a project with nothing to say sends nothing
|
|
93
|
+
* rather than an empty envelope the server has to store.
|
|
94
|
+
*/
|
|
95
|
+
function readIntent(reticleRoot) {
|
|
96
|
+
const legacy = readJson(join(reticleRoot, ReticleDir.INTENT_FILE));
|
|
97
|
+
const merged = {
|
|
98
|
+
...(legacy?.intents ?? {}),
|
|
99
|
+
};
|
|
100
|
+
let shards = 0;
|
|
101
|
+
for (const file of safeReaddir(join(reticleRoot, INTENT_SUBDIR))) {
|
|
102
|
+
if (!file.endsWith(JSON_SUFFIX) || INTENT_INDEX === file)
|
|
103
|
+
continue;
|
|
104
|
+
const shard = readJson(join(reticleRoot, INTENT_SUBDIR, file));
|
|
105
|
+
// A hand-edited or half-written shard costs one subject, never the whole sync.
|
|
106
|
+
if (shard?.intents === undefined)
|
|
107
|
+
continue;
|
|
108
|
+
shards++;
|
|
109
|
+
for (const [id, record] of Object.entries(shard.intents))
|
|
110
|
+
merged[id] = record;
|
|
111
|
+
}
|
|
112
|
+
if (legacy === undefined && 0 === shards)
|
|
113
|
+
return undefined;
|
|
114
|
+
/*
|
|
115
|
+
* Stamp the SUBJECT before sending, on anything that does not already carry one.
|
|
116
|
+
*
|
|
117
|
+
* The subject ladder lives here, in the engine, and knows how to read a flow, a route and a
|
|
118
|
+
* binding predicate. The server had no access to it and had reimplemented a far weaker version —
|
|
119
|
+
* flow-or-nothing — so records this ladder files correctly arrived at the dashboard as
|
|
120
|
+
* `unsorted`. Measured on the real corpus: 105 of 163 records the engine can place were landing
|
|
121
|
+
* in the bucket of last resort, which is what made a coverage map read as one pile with six
|
|
122
|
+
* labels.
|
|
123
|
+
*
|
|
124
|
+
* Computed HERE rather than duplicated there, because two rules for one question is how the
|
|
125
|
+
* answers start disagreeing — and the one with the evidence should be the one that decides.
|
|
126
|
+
* Records that already name a subject keep it: an agent's explicit choice outranks inference.
|
|
127
|
+
*/
|
|
128
|
+
const placed = Object.fromEntries(Object.entries(merged).map(([id, record]) => {
|
|
129
|
+
const held = record;
|
|
130
|
+
if ('string' === typeof held.subject && held.subject.length > 0)
|
|
131
|
+
return [id, record];
|
|
132
|
+
const subject = subjectFor({
|
|
133
|
+
surface: held.surface,
|
|
134
|
+
binding: held.binding,
|
|
135
|
+
});
|
|
136
|
+
return [id, { ...held, subject }];
|
|
137
|
+
}));
|
|
138
|
+
return { version: 1, intents: placed };
|
|
139
|
+
}
|
|
140
|
+
/** Directory listing that tolerates absence — an unmigrated project has no `intent/`. */
|
|
141
|
+
function safeReaddir(dir) {
|
|
142
|
+
try {
|
|
143
|
+
return readdirSync(dir);
|
|
144
|
+
}
|
|
145
|
+
catch {
|
|
146
|
+
return [];
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
/** What the cycle reads, backed by a real `.reticle` directory. */
|
|
150
|
+
export function diskSource(reticleRoot) {
|
|
151
|
+
return {
|
|
152
|
+
runs: () => readJsonDir(join(reticleRoot, ReticleDir.RUNS_SUBDIR))
|
|
153
|
+
.map((payload) => {
|
|
154
|
+
const id = payload?.runId;
|
|
155
|
+
return 'string' === typeof id ? { runId: id, payload } : undefined;
|
|
156
|
+
})
|
|
157
|
+
// A run artifact with no id cannot be diffed against the server's list, so sending it would
|
|
158
|
+
// mean re-sending it every cycle forever. Dropped rather than uploaded repeatedly.
|
|
159
|
+
.filter((r) => r !== undefined),
|
|
160
|
+
flows: () => readFlows(reticleRoot),
|
|
161
|
+
// Intent is the only derived record that is no longer one file. See readIntent.
|
|
162
|
+
derived: (kind) => 'intent' === kind ? readIntent(reticleRoot) : readJson(join(reticleRoot, DERIVED_FILE[kind])),
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
/** What the cycle writes back into `.reticle/`. */
|
|
166
|
+
export function diskSink(reticleRoot) {
|
|
167
|
+
return {
|
|
168
|
+
writeIssues: (issues) => {
|
|
169
|
+
// MERGED, not replaced. A pull returns only what changed since the cursor, so overwriting
|
|
170
|
+
// would drop every earlier decision the moment one new decision arrived.
|
|
171
|
+
const held = readCloudIssues(reticleRoot);
|
|
172
|
+
writeJson(join(reticleRoot, ReticleDir.ISSUES_FILE), {
|
|
173
|
+
triage: { ...held.triage, ...issues.triage },
|
|
174
|
+
});
|
|
175
|
+
},
|
|
176
|
+
writeState: (state) => {
|
|
177
|
+
writeJson(join(reticleRoot, ReticleDir.CLOUD_STATE_FILE), state);
|
|
178
|
+
},
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
/** This machine's side of the conversation, or a blank one on first run. */
|
|
182
|
+
export function readCloudState(reticleRoot) {
|
|
183
|
+
const raw = readJson(join(reticleRoot, ReticleDir.CLOUD_STATE_FILE));
|
|
184
|
+
return 'object' === typeof raw && null !== raw ? raw : {};
|
|
185
|
+
}
|
|
186
|
+
/** The decisions pulled back from the dashboard, for the HUD and for the next run to read. */
|
|
187
|
+
export function readCloudIssues(reticleRoot) {
|
|
188
|
+
const raw = readJson(join(reticleRoot, ReticleDir.ISSUES_FILE));
|
|
189
|
+
const triage = 'object' === typeof raw && null !== raw ? raw.triage : undefined;
|
|
190
|
+
return { triage: 'object' === typeof triage && null !== triage ? triage : {} };
|
|
191
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The content hash both halves of sync compare on.
|
|
3
|
+
*
|
|
4
|
+
* The server answers "here is a hash per record you have already sent me"; the machine hashes what
|
|
5
|
+
* it holds and sends only what differs. That only works if the two sides compute the SAME number
|
|
6
|
+
* from the same record, so this function is a CONTRACT, not a utility — it has to stay byte-for-byte
|
|
7
|
+
* equivalent to the server's, and it is separated out and tested against fixed vectors for exactly
|
|
8
|
+
* that reason.
|
|
9
|
+
*
|
|
10
|
+
* Keys are sorted before serialising because two structurally identical records must hash the same
|
|
11
|
+
* however they happened to be written out. Without that, a machine whose JSON key order differed
|
|
12
|
+
* from the server's would re-upload the same unchanged record on every cycle, forever, and the whole
|
|
13
|
+
* point of hashing — not paying for what has not moved — would be lost silently.
|
|
14
|
+
*/
|
|
15
|
+
import { createHash } from 'node:crypto';
|
|
16
|
+
/** How much of the digest is kept. Long enough that a collision is not a practical concern here. */
|
|
17
|
+
const HASH_LENGTH = 32;
|
|
18
|
+
/** Recursively sort object keys so serialisation is canonical. Arrays keep their order — it is data. */
|
|
19
|
+
function canonical(value) {
|
|
20
|
+
if (Array.isArray(value))
|
|
21
|
+
return value.map(canonical);
|
|
22
|
+
if (value !== null && 'object' === typeof value) {
|
|
23
|
+
return Object.fromEntries(Object.entries(value)
|
|
24
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
25
|
+
.map(([k, v]) => [k, canonical(v)]));
|
|
26
|
+
}
|
|
27
|
+
return value;
|
|
28
|
+
}
|
|
29
|
+
/** A stable content hash of one derived record. Must equal the server's for the same input. */
|
|
30
|
+
export function hashPayload(payload) {
|
|
31
|
+
return createHash('sha256')
|
|
32
|
+
.update(JSON.stringify(canonical(payload)))
|
|
33
|
+
.digest('hex')
|
|
34
|
+
.slice(0, HASH_LENGTH);
|
|
35
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import { ReticleTool } from '../tools/tool-names.js';
|
|
3
3
|
import { asNumber, asString } from '../tools/tools-helpers.js';
|
|
4
|
+
import { stepCountSchema, timeoutMsSchema } from '../tools/numeric-bounds.js';
|
|
4
5
|
import { crawl } from './crawl.js';
|
|
5
6
|
import { routeFromUrl, routesFromEvents } from '../project/learned-routes.js';
|
|
6
7
|
const nodeSleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
@@ -14,9 +15,10 @@ export const CRAWL_TOOLS = [
|
|
|
14
15
|
name: ReticleTool.CRAWL,
|
|
15
16
|
description: 'Autonomously click every reachable interactive control (bounded by maxSteps, default 25) and report anomalies WITHOUT a script. Two classes: single-channel faults (console errors, failed requests ≥400, DEAD controls that dispatched but did nothing) and CONTRADICTIONS — two channels disagreeing about the same click, e.g. the UI advanced while its write failed, a success signal fired over a failed request, a write succeeded and nothing changed, the same write fired twice, or the UI moved on over an in-flight request. Contradictions are the false greens a human cannot see, because a human watches one channel (the screen) and it looks correct. DESTRUCTIVE — it really clicks (may navigate/mutate state); use reticle_explore first for a non-destructive list. Returns { interactiveFound, stepsRun, anomalies[{kind,ref,desc,detail}], counts, visited, truncated }.',
|
|
16
17
|
inputSchema: {
|
|
17
|
-
maxSteps:
|
|
18
|
-
|
|
19
|
-
.number
|
|
18
|
+
maxSteps: stepCountSchema
|
|
19
|
+
.optional()
|
|
20
|
+
.describe('Maximum number of controls to click. Default: 25.'),
|
|
21
|
+
settleMs: timeoutMsSchema
|
|
20
22
|
.optional()
|
|
21
23
|
.describe('Milliseconds to wait after each click for the app to react. Default: 500.'),
|
|
22
24
|
scope: z
|
package/dist/crawl/crawl.d.ts
CHANGED
|
@@ -4,6 +4,18 @@ export interface CrawlSession {
|
|
|
4
4
|
command(name: string, args?: Record<string, unknown>): Promise<CommandResult>;
|
|
5
5
|
elapsed(): number;
|
|
6
6
|
eventsSince(cursor: number): ReticleEvent[];
|
|
7
|
+
/**
|
|
8
|
+
* Which document is on screen now, so a finding is never drawn from a page the crawl has already
|
|
9
|
+
* navigated away from. Optional for the same reason `beginAction` is: a test fake supplies a
|
|
10
|
+
* minimal session, and its absence leaves the scoping inert rather than broken.
|
|
11
|
+
*/
|
|
12
|
+
readonly currentDocumentId?: string | undefined;
|
|
13
|
+
/** Which round of source edits is in force, so a finding drawn entirely from pre-edit evidence
|
|
14
|
+
* says so. Optional for the same reason `currentDocumentId` is. */
|
|
15
|
+
readonly currentEditEpoch?: number | undefined;
|
|
16
|
+
/** The page under test, so a third-party beacon cannot be reported against a control. Optional
|
|
17
|
+
* for the same reason `currentDocumentId` is. */
|
|
18
|
+
readonly url?: string | undefined;
|
|
7
19
|
/**
|
|
8
20
|
* Attribution window around each click. Optional so a caller can supply a minimal session, but a real
|
|
9
21
|
* session MUST provide it: without a window the click's own effects carry no actionId, and an
|
|
@@ -44,19 +56,6 @@ export interface CrawlAnomaly {
|
|
|
44
56
|
}
|
|
45
57
|
/** Said out loud when the page was larger than one snapshot, so a clean sweep cannot mean "all clear". */
|
|
46
58
|
export declare const CAPPED_SNAPSHOT_NOTE = "the page exceeded one snapshot, so controls past the cap were never seen \u2014 interactiveFound is a floor, not a total; re-run with a narrower `scope` to cover the rest";
|
|
47
|
-
/**
|
|
48
|
-
* Said out loud when the crawl's OWN clicks put controls on the page that it then never visited.
|
|
49
|
-
*
|
|
50
|
-
* The enumeration happens once, before the first click, so a control that only exists after a login,
|
|
51
|
-
* a modal or a tab switch is invisible to this run. Measured on the bench app: a crawl of the login
|
|
52
|
-
* screen visited three controls, its click on "Sign in" produced seven more, and it returned
|
|
53
|
-
* `truncated: false` with an empty anomaly list and nine of twelve steps unused. Read as a sweep,
|
|
54
|
-
* that is a clean bill of health for an app the crawl never entered.
|
|
55
|
-
*
|
|
56
|
-
* The same words as the capped-snapshot case on purpose — it is the same idea (interactiveFound is a
|
|
57
|
-
* floor) reached by a different route, and a second vocabulary for it would just be more to learn.
|
|
58
|
-
*/
|
|
59
|
-
export declare function revealedControlsNote(count: number): string;
|
|
60
59
|
export interface CrawlReport {
|
|
61
60
|
interactiveFound: number;
|
|
62
61
|
stepsRun: number;
|
package/dist/crawl/crawl.js
CHANGED
|
@@ -18,7 +18,7 @@ export const CAPPED_SNAPSHOT_NOTE = 'the page exceeded one snapshot, so controls
|
|
|
18
18
|
* The same words as the capped-snapshot case on purpose — it is the same idea (interactiveFound is a
|
|
19
19
|
* floor) reached by a different route, and a second vocabulary for it would just be more to learn.
|
|
20
20
|
*/
|
|
21
|
-
|
|
21
|
+
function revealedControlsNote(count) {
|
|
22
22
|
return (`the page changed as you clicked: ${String(count)} control(s) that did not exist when this crawl ` +
|
|
23
23
|
'started were never visited — interactiveFound is a floor, not a total; re-run the crawl from ' +
|
|
24
24
|
'the state you are now in to cover them');
|
|
@@ -65,7 +65,19 @@ export function legitimatelyInert(desc) {
|
|
|
65
65
|
return true;
|
|
66
66
|
if (desc.includes('[checked]'))
|
|
67
67
|
return true;
|
|
68
|
-
|
|
68
|
+
// Inputs, and the ARIA LIVE-REGION roles.
|
|
69
|
+
//
|
|
70
|
+
// `alert`, `status`, `log`, `timer` and `marquee` exist to be READ — a screen reader announces
|
|
71
|
+
// them and nothing is meant to happen when one is clicked. Clicking twice and seeing nothing is
|
|
72
|
+
// therefore correct behaviour, and reporting it as `dead-control` accuses a working app.
|
|
73
|
+
//
|
|
74
|
+
// Found by driving a real app: the demo reported `dead-control — alert "Invalid email or
|
|
75
|
+
// password"`, which is a login form correctly telling somebody their password was wrong. That is
|
|
76
|
+
// the most expensive kind of false positive there is — the strongest claim the crawler makes,
|
|
77
|
+
// about an app that was working, and on the path a new user meets first.
|
|
78
|
+
//
|
|
79
|
+
// `alertdialog` is deliberately NOT here: it is interactive, and a word boundary keeps it out.
|
|
80
|
+
return /\b(textbox|searchbox|combobox|spinbutton|alert|status|log|timer|marquee)\b/.test(desc);
|
|
69
81
|
}
|
|
70
82
|
/**
|
|
71
83
|
* How many interactive controls exist now that did not exist when the crawl enumerated.
|
|
@@ -199,7 +211,17 @@ export async function crawl(session, opts, sleep) {
|
|
|
199
211
|
}
|
|
200
212
|
// CONTRADICTIONS: the channels disagree about what this click did. Reported per control, so a
|
|
201
213
|
// crawl of an unknown app surfaces its false greens without anyone knowing where to look.
|
|
202
|
-
|
|
214
|
+
// A crawl clicks controls that navigate, so its windows are the ones most likely to straddle a
|
|
215
|
+
// document boundary — and a finding here is reported against a specific control by name.
|
|
216
|
+
for (const c of findContradictions(events, {
|
|
217
|
+
currentDocumentId: session.currentDocumentId,
|
|
218
|
+
currentEditEpoch: session.currentEditEpoch,
|
|
219
|
+
appOrigin: session.url,
|
|
220
|
+
// The crawl's window IS one control's click, so it can name the floor the consequence rules
|
|
221
|
+
// need — without it a crawl would report nothing about the UI moving, which is most of what a
|
|
222
|
+
// crawl is for.
|
|
223
|
+
actionSince: since,
|
|
224
|
+
})) {
|
|
203
225
|
counts.contradictions += 1;
|
|
204
226
|
anomalies.push({
|
|
205
227
|
kind: c.kind,
|