@reticlehq/server 2.4.0 → 2.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/SKILL.md +702 -0
- package/dist/bridge/auth-failure-reason.d.ts +19 -0
- package/dist/bridge/auth-failure-reason.js +29 -0
- package/dist/{bridge-security.d.ts → bridge/bridge-security.d.ts} +1 -1
- package/dist/{bridge-security.js → bridge/bridge-security.js} +1 -1
- package/dist/{bridge.d.ts → bridge/bridge.d.ts} +1 -1
- package/dist/{bridge.js → bridge/bridge.js} +63 -13
- package/dist/{bridge.test-harness.d.ts → bridge/bridge.test-harness.d.ts} +1 -11
- package/dist/{bridge.test-harness.js → bridge/bridge.test-harness.js} +9 -9
- package/dist/{pairing-token.js → bridge/pairing-token.js} +1 -1
- package/dist/capsule/capsule-store.d.ts +8 -3
- package/dist/capsule/capsule-store.js +7 -2
- package/dist/capsule/capsule.js +3 -3
- package/dist/capsule/causal-summary.d.ts +8 -0
- package/dist/capsule/causal-summary.js +22 -12
- package/dist/capsule/divergence.d.ts +9 -4
- package/dist/capsule/divergence.js +8 -8
- package/dist/capsule/predicate-to-links.js +11 -8
- package/dist/{cli-doctor.js → cli/cli-doctor.js} +9 -2
- package/dist/{cli-flow-commands.d.ts → cli/cli-flow-commands.d.ts} +2 -2
- package/dist/{cli-flow-commands.js → cli/cli-flow-commands.js} +15 -15
- package/dist/{cli-launch.d.ts → cli/cli-launch.d.ts} +32 -3
- package/dist/cli/cli-launch.js +171 -0
- package/dist/{cli-parse.d.ts → cli/cli-parse.d.ts} +2 -11
- package/dist/{cli-parse.js → cli/cli-parse.js} +113 -60
- package/dist/{cli-port.d.ts → cli/cli-port.d.ts} +12 -0
- package/dist/{cli-port.js → cli/cli-port.js} +9 -9
- package/dist/cli/cli-update-commands.d.ts +4 -0
- package/dist/cli/cli-update-commands.js +93 -0
- package/dist/{cli-verify.js → cli/cli-verify.js} +14 -14
- package/dist/{cloud-cli.js → cli/cloud-cli.js} +29 -24
- package/dist/cli.d.ts +2 -2
- package/dist/cli.js +174 -92
- package/dist/cloud/cloud-config.d.ts +0 -1
- package/dist/cloud/cloud-config.js +9 -9
- package/dist/cloud/cloud-sync.d.ts +1 -7
- package/dist/cloud/cloud-sync.js +12 -12
- package/dist/crawl/crawl-empty.d.ts +16 -0
- package/dist/crawl/crawl-empty.js +24 -0
- package/dist/crawl/crawl-tools.js +4 -1
- package/dist/crawl/crawl.js +26 -7
- package/dist/daemon/daemon-resilience.d.ts +49 -0
- package/dist/daemon/daemon-resilience.js +191 -0
- package/dist/daemon/daemon-usefulness.d.ts +61 -0
- package/dist/daemon/daemon-usefulness.js +62 -0
- package/dist/{daemon.d.ts → daemon/daemon.d.ts} +33 -8
- package/dist/{daemon.js → daemon/daemon.js} +80 -48
- package/dist/daemon/idle-grace.d.ts +32 -0
- package/dist/daemon/idle-grace.js +41 -0
- package/dist/{idle-shutdown.d.ts → daemon/idle-shutdown.d.ts} +13 -0
- package/dist/{idle-shutdown.js → daemon/idle-shutdown.js} +27 -4
- package/dist/domain/domain-model.js +2 -2
- package/dist/events/body-failures.js +8 -8
- package/dist/events/contradictions.d.ts +1 -1
- package/dist/events/contradictions.js +60 -10
- package/dist/events/echo-mismatch.js +10 -10
- package/dist/events/event-filters.js +6 -6
- package/dist/events/json-salvage.d.ts +2 -1
- package/dist/events/json-salvage.js +6 -6
- package/dist/events/observed-in-window.d.ts +12 -0
- package/dist/events/observed-in-window.js +21 -0
- package/dist/events/predicate-eval.d.ts +43 -20
- package/dist/events/predicate-eval.js +159 -55
- package/dist/events/predicate.d.ts +2 -2
- package/dist/events/predicate.js +112 -35
- package/dist/events/reconcile.js +5 -5
- package/dist/events/ring-buffer.js +1 -1
- package/dist/events/stale-response.js +6 -6
- package/dist/events/testid-near-miss.d.ts +22 -0
- package/dist/events/testid-near-miss.js +33 -0
- package/dist/events/unit-mismatch.js +8 -8
- package/dist/flows/affected-tools.js +19 -4
- package/dist/flows/affected.js +1 -1
- package/dist/flows/annotate-target.d.ts +1 -0
- package/dist/flows/annotate-target.js +20 -0
- package/dist/flows/annotate-tools.js +25 -3
- package/dist/flows/annotate.js +17 -4
- package/dist/flows/assertion-integrity.d.ts +2 -1
- package/dist/flows/assertion-tiers-store.d.ts +3 -66
- package/dist/flows/assertion-tiers-store.js +2 -2
- package/dist/flows/attributed-failure.d.ts +17 -0
- package/dist/flows/attributed-failure.js +20 -0
- package/dist/flows/buddy-status.d.ts +2 -1
- package/dist/flows/buddy-status.js +2 -2
- package/dist/flows/coverage.d.ts +3 -2
- package/dist/flows/coverage.js +2 -2
- package/dist/flows/decision.js +24 -9
- package/dist/flows/empty-flow.d.ts +6 -0
- package/dist/flows/empty-flow.js +24 -0
- package/dist/flows/flake-store.d.ts +0 -1
- package/dist/flows/flake-store.js +1 -1
- package/dist/flows/flake.d.ts +2 -1
- package/dist/flows/flake.js +2 -2
- package/dist/flows/flow-classify.js +12 -0
- package/dist/flows/flow-replay-run.js +4 -4
- package/dist/flows/flow-replay.d.ts +5 -7
- package/dist/flows/flow-replay.js +114 -47
- package/dist/flows/flow-report.js +1 -1
- package/dist/flows/flow-scope.d.ts +1 -6
- package/dist/flows/flow-step-runners.d.ts +26 -6
- package/dist/flows/flow-step-runners.js +157 -6
- package/dist/flows/flow-success.js +20 -15
- package/dist/flows/flow-tools.js +53 -7
- package/dist/flows/flows.js +15 -11
- package/dist/flows/gate.d.ts +4 -3
- package/dist/flows/gate.js +1 -1
- package/dist/flows/heal-run.d.ts +0 -8
- package/dist/flows/heal-run.js +5 -5
- package/dist/flows/heal.js +3 -3
- package/dist/flows/predicate-to-expect.d.ts +38 -0
- package/dist/flows/predicate-to-expect.js +95 -0
- package/dist/flows/replay.d.ts +16 -1
- package/dist/flows/replay.js +58 -22
- package/dist/flows/role-anchor-nearest.d.ts +6 -0
- package/dist/flows/role-anchor-nearest.js +45 -0
- package/dist/flows/server-verify.d.ts +1 -1
- package/dist/flows/server-verify.js +5 -5
- package/dist/flows/verify-change-tools.js +32 -10
- package/dist/flows/verify-change-verdict.d.ts +15 -0
- package/dist/flows/verify-change-verdict.js +25 -0
- package/dist/flows/watch-batcher.d.ts +3 -2
- package/dist/honesty/already-true.d.ts +2 -0
- package/dist/honesty/already-true.js +31 -0
- package/dist/honesty/blind-spots.d.ts +16 -3
- package/dist/honesty/blind-spots.js +24 -12
- package/dist/honesty/honesty.d.ts +3 -2
- package/dist/honesty/honesty.js +4 -4
- package/dist/honesty/uncaptured-bodies.js +1 -1
- package/dist/honesty/unread-outcome.js +2 -2
- package/dist/honesty/verified.d.ts +22 -3
- package/dist/honesty/verified.js +48 -8
- package/dist/http-server.js +14 -7
- package/dist/hunt/hunt-report.d.ts +2 -1
- package/dist/hunt/hunt-report.js +2 -2
- package/dist/index.d.ts +8 -6
- package/dist/index.js +32 -25
- package/dist/init/agent-rules.d.ts +2 -1
- package/dist/init/agent-rules.js +45 -7
- package/dist/init/app-choice.d.ts +20 -0
- package/dist/init/app-choice.js +25 -0
- package/dist/init/astro-host.d.ts +21 -0
- package/dist/init/astro-host.js +27 -0
- package/dist/init/astro-patch.d.ts +16 -0
- package/dist/init/astro-patch.js +240 -0
- package/dist/init/capabilities.js +4 -1
- package/dist/init/cra.d.ts +37 -0
- package/dist/init/cra.js +90 -0
- package/dist/init/cursor.js +39 -4
- package/dist/init/desktop-doctor.d.ts +3 -2
- package/dist/init/desktop-doctor.js +4 -4
- package/dist/init/detect.d.ts +3 -2
- package/dist/init/detect.js +16 -5
- package/dist/init/mcp-registered.d.ts +12 -0
- package/dist/init/mcp-registered.js +14 -0
- package/dist/init/mcp.d.ts +27 -2
- package/dist/init/mcp.js +40 -4
- package/dist/init/next-patch.d.ts +2 -4
- package/dist/init/next-patch.js +109 -19
- package/dist/init/node-io.js +23 -6
- package/dist/init/plan-framework.d.ts +16 -1
- package/dist/init/plan-framework.js +132 -8
- package/dist/init/plan.d.ts +73 -8
- package/dist/init/plan.js +133 -34
- package/dist/init/project-id.d.ts +5 -9
- package/dist/init/project-id.js +10 -22
- package/dist/init/run.d.ts +5 -0
- package/dist/init/run.js +210 -36
- package/dist/init/slash-command.d.ts +9 -0
- package/dist/init/slash-command.js +10 -1
- package/dist/init/snippets.d.ts +7 -10
- package/dist/init/snippets.js +44 -15
- package/dist/init/vite-config.js +1 -1
- package/dist/init/workspace-apps.d.ts +23 -0
- package/dist/init/workspace-apps.js +68 -0
- package/dist/input/driven-redaction.js +1 -1
- package/dist/input/network-detail.js +8 -8
- package/dist/input/network-mock-tools.js +2 -2
- package/dist/input/network-mock.js +4 -4
- package/dist/input/real-input.js +8 -3
- package/dist/input/scroll-find.js +2 -2
- package/dist/input/scroll-tools.d.ts +0 -5
- package/dist/input/scroll-tools.js +11 -4
- package/dist/input/viewport-tools.js +2 -2
- package/dist/journal/ambient-store.d.ts +0 -2
- package/dist/journal/ambient-store.js +1 -1
- package/dist/journal/ambient.js +1 -1
- package/dist/journal/attach-journal.d.ts +2 -1
- package/dist/journal/deviation-report.js +6 -6
- package/dist/journal/deviation-service.js +1 -1
- package/dist/journal/envelope-store.d.ts +0 -2
- package/dist/journal/envelope-store.js +1 -1
- package/dist/journal/envelope.d.ts +2 -3
- package/dist/journal/envelope.js +3 -3
- package/dist/journal/journal-query.js +1 -1
- package/dist/journal/journal-recorder.d.ts +2 -1
- package/dist/journal/journal-recorder.js +2 -2
- package/dist/journal/rollups.js +9 -9
- package/dist/journal/session-end.d.ts +2 -1
- package/dist/journal/session-journal.js +4 -4
- package/dist/license/license.d.ts +0 -14
- package/dist/license/license.js +8 -8
- package/dist/log.d.ts +10 -0
- package/dist/log.js +11 -1
- package/dist/mcp/mcp-is-error.d.ts +13 -0
- package/dist/mcp/mcp-is-error.js +18 -0
- package/dist/mcp/mcp-outage.d.ts +31 -0
- package/dist/mcp/mcp-outage.js +48 -0
- package/dist/mcp/mcp-proxy.d.ts +124 -0
- package/dist/mcp/mcp-proxy.js +697 -0
- package/dist/{mcp.d.ts → mcp/mcp.d.ts} +13 -10
- package/dist/{mcp.js → mcp/mcp.js} +110 -35
- package/dist/mcp/proxy-handshake.d.ts +3 -0
- package/dist/mcp/proxy-handshake.js +54 -0
- package/dist/mcp/proxy-lifecycle.d.ts +64 -0
- package/dist/mcp/proxy-lifecycle.js +70 -0
- package/dist/mcp/tool-catalog-cache.d.ts +29 -0
- package/dist/mcp/tool-catalog-cache.js +55 -0
- package/dist/oracles/propose-consequences.d.ts +2 -1
- package/dist/oracles/propose-consequences.js +16 -11
- package/dist/oracles/self-instrument.d.ts +2 -1
- package/dist/oracles/self-instrument.js +1 -1
- package/dist/phenomena/phenomena.js +3 -3
- package/dist/platform.d.ts +16 -0
- package/dist/platform.js +16 -0
- package/dist/pool/playwright-launcher.d.ts +15 -2
- package/dist/pool/playwright-launcher.js +21 -2
- package/dist/project/fs-port.js +1 -1
- package/dist/project/project-tools.js +2 -2
- package/dist/runs/build-verification-run.js +1 -1
- package/dist/runs/repair-prompt.d.ts +0 -6
- package/dist/runs/repair-prompt.js +3 -3
- package/dist/runs/risk-classify.js +1 -1
- package/dist/runs/run-diff.d.ts +0 -2
- package/dist/runs/run-diff.js +3 -3
- package/dist/runs/run-tools.js +2 -2
- package/dist/runs/verification-sync.js +2 -2
- package/dist/runs/verify-http.js +1 -1
- package/dist/runs/verify-server.js +1 -1
- package/dist/session/command-timeout.d.ts +11 -3
- package/dist/session/command-timeout.js +2 -2
- package/dist/session/control-envelope.d.ts +6 -0
- package/dist/session/control-envelope.js +7 -1
- package/dist/session/dev-server-probe.d.ts +13 -0
- package/dist/session/dev-server-probe.js +52 -0
- package/dist/session/last-act.d.ts +17 -3
- package/dist/session/last-act.js +16 -4
- package/dist/session/live-control-tools.js +7 -2
- package/dist/session/live-control.js +1 -1
- package/dist/session/looks-like-dev-server.d.ts +14 -0
- package/dist/session/looks-like-dev-server.js +25 -0
- package/dist/session/no-session-diagnosis.d.ts +38 -0
- package/dist/session/no-session-diagnosis.js +70 -0
- package/dist/session/no-session-watch.d.ts +19 -0
- package/dist/session/no-session-watch.js +61 -0
- package/dist/session/observed-state.d.ts +4 -0
- package/dist/session/observed-state.js +19 -1
- package/dist/session/output-budget.js +1 -1
- package/dist/session/session-health.d.ts +8 -1
- package/dist/session/session-health.js +8 -7
- package/dist/session/session-info.d.ts +26 -7
- package/dist/session/session-info.js +42 -1
- package/dist/session/session-lease.d.ts +0 -11
- package/dist/session/session-lease.js +1 -1
- package/dist/session/session-manager.d.ts +3 -0
- package/dist/session/session-manager.js +52 -9
- package/dist/session/session-readiness.d.ts +1 -1
- package/dist/session/session-readiness.js +6 -1
- package/dist/session/session-reaper.js +1 -1
- package/dist/session/session-reconnect.d.ts +32 -0
- package/dist/session/session-reconnect.js +42 -0
- package/dist/session/session.d.ts +14 -2
- package/dist/session/session.js +69 -26
- package/dist/session/state-select.d.ts +1 -1
- package/dist/session/state-select.js +1 -1
- package/dist/status-payload.d.ts +19 -0
- package/dist/status-payload.js +11 -0
- package/dist/telemetry/argument-shape.js +10 -1
- package/dist/telemetry/browser-mode.d.ts +27 -0
- package/dist/telemetry/browser-mode.js +33 -0
- package/dist/telemetry/bug-found.d.ts +17 -21
- package/dist/telemetry/bug-found.js +120 -8
- package/dist/telemetry/cli-telemetry.d.ts +2 -12
- package/dist/telemetry/cli-telemetry.js +27 -4
- package/dist/telemetry/daemon-telemetry.d.ts +19 -0
- package/dist/telemetry/daemon-telemetry.js +12 -2
- package/dist/telemetry/dev-repo.js +4 -4
- package/dist/telemetry/error-fingerprint.js +21 -2
- package/dist/telemetry/feedback-cli.d.ts +1 -1
- package/dist/telemetry/feedback-cli.js +4 -4
- package/dist/telemetry/feedback-context.d.ts +22 -0
- package/dist/telemetry/feedback-context.js +41 -1
- package/dist/telemetry/feedback-delivery.d.ts +20 -0
- package/dist/telemetry/feedback-delivery.js +30 -0
- package/dist/telemetry/feedback-outbox.d.ts +22 -0
- package/dist/telemetry/feedback-outbox.js +87 -0
- package/dist/telemetry/feedback.d.ts +22 -1
- package/dist/telemetry/feedback.js +67 -8
- package/dist/telemetry/git-facts.js +1 -1
- package/dist/telemetry/session-metrics.d.ts +2 -0
- package/dist/telemetry/session-metrics.js +68 -6
- package/dist/telemetry/telemetry.d.ts +27 -2
- package/dist/telemetry/telemetry.js +119 -29
- package/dist/telemetry/verification-of.d.ts +22 -0
- package/dist/telemetry/verification-of.js +84 -0
- package/dist/tools/act-capsule.js +2 -2
- package/dist/tools/act-danger.d.ts +12 -0
- package/dist/tools/act-danger.js +20 -3
- package/dist/tools/act-helpers.js +4 -4
- package/dist/tools/act-tools.d.ts +0 -8
- package/dist/tools/act-tools.js +84 -115
- package/dist/tools/act-view.js +7 -7
- package/dist/tools/alias-args.d.ts +17 -0
- package/dist/tools/alias-args.js +25 -0
- package/dist/tools/assert-grade.js +9 -9
- package/dist/tools/assert-verdict.d.ts +3 -1
- package/dist/tools/assert-verdict.js +8 -5
- package/dist/tools/browser-tools.js +30 -11
- package/dist/tools/contract-tools.js +1 -1
- package/dist/tools/coverage-identity.d.ts +34 -0
- package/dist/tools/coverage-identity.js +46 -0
- package/dist/tools/coverage-tools.js +10 -14
- package/dist/tools/dynamic-tools.d.ts +2 -1
- package/dist/tools/dynamic-tools.js +90 -18
- package/dist/tools/error-recovery.d.ts +20 -2
- package/dist/tools/error-recovery.js +179 -4
- package/dist/tools/feedback-tools.d.ts +12 -1
- package/dist/tools/feedback-tools.js +28 -5
- package/dist/tools/invoke-tool.d.ts +0 -6
- package/dist/tools/invoke-tool.js +57 -32
- package/dist/tools/lease-hint.d.ts +17 -0
- package/dist/tools/lease-hint.js +24 -0
- package/dist/tools/lease-tools.js +6 -5
- package/dist/tools/merge-tools.d.ts +10 -1
- package/dist/tools/merge-tools.js +9 -3
- package/dist/tools/merged-name-redirect.d.ts +13 -0
- package/dist/tools/merged-name-redirect.js +55 -0
- package/dist/tools/navigate-result.d.ts +17 -0
- package/dist/tools/navigate-result.js +31 -0
- package/dist/tools/observe-tools.js +54 -20
- package/dist/tools/observed-nothing.d.ts +25 -0
- package/dist/tools/observed-nothing.js +33 -0
- package/dist/tools/query-paginate.js +2 -2
- package/dist/tools/query-shape.d.ts +18 -0
- package/dist/tools/query-shape.js +39 -0
- package/dist/tools/read-tools.js +81 -20
- package/dist/tools/real-input-attempt.d.ts +28 -0
- package/dist/tools/real-input-attempt.js +88 -0
- package/dist/tools/reconcile-tools.js +7 -7
- package/dist/tools/reload-result.d.ts +14 -0
- package/dist/tools/reload-result.js +29 -0
- package/dist/tools/snapshot-delta.d.ts +4 -5
- package/dist/tools/snapshot-delta.js +0 -0
- package/dist/tools/tool-kit.d.ts +8 -0
- package/dist/tools/tool-surface.d.ts +83 -0
- package/dist/tools/tool-surface.js +204 -0
- package/dist/tools/tools-helpers.js +6 -6
- package/dist/tools/tools.d.ts +15 -0
- package/dist/tools/tools.js +53 -17
- package/dist/tools/unadvertised-help.d.ts +5 -0
- package/dist/tools/unadvertised-help.js +39 -0
- package/dist/trace.d.ts +69 -0
- package/dist/trace.js +150 -0
- package/dist/update/nudge-credit.d.ts +4 -0
- package/dist/update/nudge-credit.js +53 -0
- package/dist/update/sdk-sync.d.ts +21 -0
- package/dist/update/sdk-sync.js +42 -0
- package/dist/update/update-checker.js +3 -3
- package/dist/update/update-nudge.d.ts +63 -6
- package/dist/update/update-nudge.js +128 -15
- package/dist/update/updater.d.ts +1 -9
- package/dist/update/updater.js +13 -8
- package/dist/version/peer-announce.d.ts +4 -0
- package/dist/version/peer-announce.js +30 -0
- package/dist/{server-version.js → version/server-version.js} +5 -1
- package/dist/version/version-nudge.d.ts +28 -0
- package/dist/version/version-nudge.js +40 -0
- package/dist/version/version-skew.d.ts +66 -0
- package/dist/version/version-skew.js +71 -0
- package/dist/visual/visual-diff.js +2 -2
- package/dist/visual/visual-tools.js +4 -4
- package/docs/agent-cheatsheet.md +85 -0
- package/docs/architecture.md +113 -0
- package/docs/benchmarks.md +255 -0
- package/docs/debugging.md +102 -0
- package/docs/deploy-checks.md +69 -0
- package/docs/desktop-apps.md +256 -0
- package/docs/enterprise.md +62 -0
- package/docs/fixtures.md +28 -0
- package/docs/flows.md +166 -0
- package/docs/getting-started.md +469 -0
- package/docs/human-control.md +63 -0
- package/docs/integration-patterns.md +171 -0
- package/docs/local-registry.md +126 -0
- package/docs/mint.json +61 -0
- package/docs/multi-agent-testing.md +55 -0
- package/docs/platform-integration.md +122 -0
- package/docs/telemetry-contract.md +214 -0
- package/docs/telemetry.md +184 -0
- package/docs/testing.md +82 -0
- package/docs/token-efficiency.md +80 -0
- package/docs/usage.md +1046 -0
- package/package.json +8 -5
- package/dist/cli-launch.js +0 -112
- package/dist/daemon-resilience.d.ts +0 -22
- package/dist/daemon-resilience.js +0 -85
- package/dist/mcp-proxy.d.ts +0 -67
- package/dist/mcp-proxy.js +0 -337
- package/dist/tools/profiles.d.ts +0 -37
- package/dist/tools/profiles.js +0 -139
- /package/dist/{pairing-token.d.ts → bridge/pairing-token.d.ts} +0 -0
- /package/dist/{token-auth.d.ts → bridge/token-auth.d.ts} +0 -0
- /package/dist/{token-auth.js → bridge/token-auth.js} +0 -0
- /package/dist/{cli-doctor.d.ts → cli/cli-doctor.d.ts} +0 -0
- /package/dist/{cli-verify.d.ts → cli/cli-verify.d.ts} +0 -0
- /package/dist/{cloud-cli.d.ts → cli/cloud-cli.d.ts} +0 -0
- /package/dist/{server-version.d.ts → version/server-version.d.ts} +0 -0
|
@@ -1,13 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
|
|
3
|
-
*
|
|
4
|
-
* `_daemon` is EXCLUDED, and that exclusion is the whole point of this function. `reticle mcp` and
|
|
5
|
-
* `reticle serve` start the daemon by re-running this very binary, so the child re-entered the CLI
|
|
6
|
-
* entry point and emitted a second event for what a person experienced as ONE action. The old
|
|
7
|
-
* `invoke` metric was therefore inflated ~2x — and inflated worst on the agent-driven sessions that
|
|
8
|
-
* matter most, while one-shot commands like `version` counted once. That skewed the RATIO between
|
|
9
|
-
* commands, not merely the scale, which is the kind of error you cannot correct for after the fact.
|
|
10
|
-
* The daemon\'s own lifecycle is already reported by `daemon_started` / `daemon_stopped`; a spawned
|
|
11
|
-
* daemon is not a CLI run.
|
|
12
|
-
*/
|
|
1
|
+
/** True when this command represents a person typing something. Exported for the gate. */
|
|
2
|
+
export declare function isHumanCliCommand(command: string): boolean;
|
|
13
3
|
export declare function reportCliRun(argv: readonly string[]): void;
|
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
* Lives in its own module so `cli.ts` stays a dispatcher, and so the `_daemon` exclusion below sits
|
|
5
5
|
* next to the explanation of why it exists rather than buried in a startup function.
|
|
6
6
|
*/
|
|
7
|
-
import {
|
|
8
|
-
import { DAEMON_INNER_COMMAND, knownCommand } from '../cli-parse.js';
|
|
7
|
+
import { TelemetryEventKind } from '@reticlehq/core';
|
|
8
|
+
import { DAEMON_INNER_COMMAND, knownCommand } from '../cli/cli-parse.js';
|
|
9
9
|
import { getTelemetry } from './telemetry.js';
|
|
10
10
|
import { describeCliFlags } from './argument-shape.js';
|
|
11
11
|
/**
|
|
@@ -20,22 +20,45 @@ import { describeCliFlags } from './argument-shape.js';
|
|
|
20
20
|
* The daemon\'s own lifecycle is already reported by `daemon_started` / `daemon_stopped`; a spawned
|
|
21
21
|
* daemon is not a CLI run.
|
|
22
22
|
*/
|
|
23
|
+
/**
|
|
24
|
+
* The agent's MCP transport, not a person. `reticle mcp` is what an MCP client runs to open a stdio
|
|
25
|
+
* connection — nobody types it. Measured over one real day it was 475 of 561 `cli_command_run`
|
|
26
|
+
* events (85%), on an event whose whole purpose is human intent, and the agent attaching is already
|
|
27
|
+
* reported by `mcp_client_connected` with strictly more detail (reconnect, daemon age, client).
|
|
28
|
+
*
|
|
29
|
+
* `serve` is NOT excluded: nothing spawns it on a person's behalf, so typing it is a real act.
|
|
30
|
+
*/
|
|
31
|
+
const AGENT_TRANSPORT_COMMAND = 'mcp';
|
|
32
|
+
/** True when this command represents a person typing something. Exported for the gate. */
|
|
33
|
+
export function isHumanCliCommand(command) {
|
|
34
|
+
return command !== AGENT_TRANSPORT_COMMAND && command !== DAEMON_INNER_COMMAND;
|
|
35
|
+
}
|
|
23
36
|
export function reportCliRun(argv) {
|
|
24
37
|
const firstArg = argv[0];
|
|
25
38
|
if (firstArg === DAEMON_INNER_COMMAND)
|
|
26
39
|
return;
|
|
40
|
+
const command = knownCommand(firstArg);
|
|
27
41
|
const telemetry = getTelemetry();
|
|
42
|
+
// FIRST — before the human-command filter. The install happened whichever command ran, and on most
|
|
43
|
+
// machines the very first contact is the agent spawning `reticle mcp`, which that filter excludes.
|
|
44
|
+
// Measured over one sweep: 15 `init_completed` and ZERO `reticle_installed`, in a run that
|
|
45
|
+
// installed the SDK into nine apps — the top of every funnel, missing.
|
|
28
46
|
// `detach` so a quick command (`version`/`gate`) exits immediately instead of waiting out the POST.
|
|
29
47
|
if (telemetry.firstRun)
|
|
30
48
|
void telemetry.emit(TelemetryEventKind.RETICLE_INSTALLED, { detach: true });
|
|
49
|
+
if (!isHumanCliCommand(command))
|
|
50
|
+
return;
|
|
31
51
|
void telemetry.emit(TelemetryEventKind.CLI_COMMAND_RUN, {
|
|
32
52
|
detach: true,
|
|
33
|
-
actor
|
|
53
|
+
// No `actor`: with the agent's transport excluded, this event is human BY DEFINITION. It was
|
|
54
|
+
// constant `human` on all 561 events in a real day — zero information, and wrong for the 475
|
|
55
|
+
// that were the agent. `actor` stays where it earns its place: verification_completed and
|
|
56
|
+
// bug_found, where it splits the agent's own loop from a human/CI-triggered run.
|
|
34
57
|
// A fixed, low-cardinality vocabulary WE define, so it is safe to send whole and it is the closest
|
|
35
58
|
// honest read we have on intent: `verify` and `gate` mean something very different from `status`.
|
|
36
59
|
// An unrecognized first arg reports as `unknown` rather than being echoed — an echo would put
|
|
37
60
|
// whatever someone mistyped, including a path or a URL, straight onto the wire.
|
|
38
|
-
command
|
|
61
|
+
command,
|
|
39
62
|
// Which flags were PRESENT, by name only — never their values. `--http-token` alone makes that
|
|
40
63
|
// rule absolute rather than case-by-case; `--drive` and `--storage-state` carry a URL and a path.
|
|
41
64
|
flags: describeCliFlags(argv),
|
|
@@ -1,3 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How often a long-lived daemon flushes its counters.
|
|
3
|
+
*
|
|
4
|
+
* ponytail: coarse on purpose. A daemon that is SIGKILLed — OOM, a closed laptop, `kill -9` — never
|
|
5
|
+
* reaches its shutdown handler, so without this the whole session's counters die with it. Thirty
|
|
6
|
+
* minutes bounds that loss to one window while keeping the event volume near zero for the common case
|
|
7
|
+
* (most daemons stop cleanly and send exactly one summary). If the lost windows ever show up as a
|
|
8
|
+
* real gap, the upgrade is a small on-disk journal replayed at the next start, not a shorter timer.
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* How often a running daemon rolls up its window.
|
|
12
|
+
*
|
|
13
|
+
* Exported because it is the BOUND ON WHAT IS LOST: a daemon that has served a tool never
|
|
14
|
+
* idle-exits, so nothing calls shutdown, so its last partial window dies with the process. At 30
|
|
15
|
+
* minutes against a median 28-minute session that meant the median session reported nothing at all.
|
|
16
|
+
* Only non-empty windows emit, so a shorter interval costs nothing on the 74% of daemons that never
|
|
17
|
+
* serve a tool.
|
|
18
|
+
*/
|
|
19
|
+
export declare const SESSION_FLUSH_MS: number;
|
|
1
20
|
/** Stops the timers this installed. Called from the daemon's shutdown path. */
|
|
2
21
|
export interface DaemonTelemetry {
|
|
3
22
|
/**
|
|
@@ -23,7 +23,16 @@ const PROJECT_PROFILE_DELAY_MS = 5_000;
|
|
|
23
23
|
* (most daemons stop cleanly and send exactly one summary). If the lost windows ever show up as a
|
|
24
24
|
* real gap, the upgrade is a small on-disk journal replayed at the next start, not a shorter timer.
|
|
25
25
|
*/
|
|
26
|
-
|
|
26
|
+
/**
|
|
27
|
+
* How often a running daemon rolls up its window.
|
|
28
|
+
*
|
|
29
|
+
* Exported because it is the BOUND ON WHAT IS LOST: a daemon that has served a tool never
|
|
30
|
+
* idle-exits, so nothing calls shutdown, so its last partial window dies with the process. At 30
|
|
31
|
+
* minutes against a median 28-minute session that meant the median session reported nothing at all.
|
|
32
|
+
* Only non-empty windows emit, so a shorter interval costs nothing on the 74% of daemons that never
|
|
33
|
+
* serve a tool.
|
|
34
|
+
*/
|
|
35
|
+
export const SESSION_FLUSH_MS = 5 * 60 * 1000;
|
|
27
36
|
/**
|
|
28
37
|
* Report the daemon as started, profile the project once, and keep the session counters flushing.
|
|
29
38
|
* Every timer is `unref`'d so telemetry can never be the reason a daemon refuses to exit.
|
|
@@ -51,7 +60,8 @@ export function installDaemonTelemetry(cwd, now = () => Date.now()) {
|
|
|
51
60
|
const flush = setInterval(() => {
|
|
52
61
|
if (metrics.empty)
|
|
53
62
|
return; // an idle window is not worth an event
|
|
54
|
-
|
|
63
|
+
// NOT daemon_stopped: the daemon is still running. See SESSION_PROGRESS.
|
|
64
|
+
void getTelemetry().emit(TelemetryEventKind.SESSION_PROGRESS, {
|
|
55
65
|
session: metrics.summarize(false),
|
|
56
66
|
});
|
|
57
67
|
metrics.reset();
|
|
@@ -21,7 +21,7 @@ export function isReticleSourceCheckout(cwd) {
|
|
|
21
21
|
if (existsSync(pkg)) {
|
|
22
22
|
try {
|
|
23
23
|
const parsed = JSON.parse(readFileSync(pkg, 'utf8'));
|
|
24
|
-
const name = typeof parsed
|
|
24
|
+
const name = 'object' === typeof parsed && parsed !== null
|
|
25
25
|
? parsed.name
|
|
26
26
|
: undefined;
|
|
27
27
|
if (name === MONOREPO_NAME)
|
|
@@ -42,7 +42,7 @@ export function isReticleSourceCheckout(cwd) {
|
|
|
42
42
|
/** Strip one layer of matching quotes from a .env value. */
|
|
43
43
|
function unquote(value) {
|
|
44
44
|
const first = value[0];
|
|
45
|
-
if ((
|
|
45
|
+
if (('"' === first || "'" === first) && value.endsWith(first) && value.length >= 2) {
|
|
46
46
|
return value.slice(1, -1);
|
|
47
47
|
}
|
|
48
48
|
return value;
|
|
@@ -67,13 +67,13 @@ export function loadDotEnv(dir, env = process.env) {
|
|
|
67
67
|
}
|
|
68
68
|
for (const raw of text.split('\n')) {
|
|
69
69
|
const line = raw.trim();
|
|
70
|
-
if (line.length
|
|
70
|
+
if (0 === line.length || line.startsWith('#'))
|
|
71
71
|
continue;
|
|
72
72
|
const eq = line.indexOf('=');
|
|
73
73
|
if (eq <= 0)
|
|
74
74
|
continue; // not a KEY=value pair — skip rather than fail the whole file
|
|
75
75
|
const key = line.slice(0, eq).trim();
|
|
76
|
-
if (key.length
|
|
76
|
+
if (0 === key.length || env[key] !== undefined)
|
|
77
77
|
continue;
|
|
78
78
|
env[key] = unquote(line.slice(eq + 1).trim());
|
|
79
79
|
}
|
|
@@ -19,6 +19,25 @@ import { createHash } from 'node:crypto';
|
|
|
19
19
|
* quoted value often contains the very numbers and paths the later rules would only partly catch.
|
|
20
20
|
*/
|
|
21
21
|
const VARIABLE_PARTS = [
|
|
22
|
+
// ── Identifiers and credentials FIRST ────────────────────────────────────────────────────────
|
|
23
|
+
//
|
|
24
|
+
// The rules below this block were written to make messages GROUPABLE — blank the variable parts so
|
|
25
|
+
// the same defect hashes the same everywhere. Redaction was a side effect of that, and a side
|
|
26
|
+
// effect is not a guarantee. A telemetry audit caught `bob@acme.com` arriving verbatim in
|
|
27
|
+
// `crash_message`; probing further, API-key-shaped tokens survived intact and a JWT was only
|
|
28
|
+
// half-masked. This function also feeds `session.errors[]` on EVERY session summary, so the
|
|
29
|
+
// exposure was every session that logged an error naming a user, not a rare crash path.
|
|
30
|
+
//
|
|
31
|
+
// Order matters: these run before the generic patterns, because `\d+` and the hex rule would chew
|
|
32
|
+
// a token into pieces that no longer match a credential shape while still leaking most of it —
|
|
33
|
+
// which is exactly what happened to the JWT.
|
|
34
|
+
/[\w.+-]+@[\w-]+\.[\w.-]+/g, // email addresses
|
|
35
|
+
/\beyJ[\w-]*\.[\w-]*\.?[\w-]*/g, // JWTs (header segment is always base64 `eyJ`)
|
|
36
|
+
/\b(?:sk|pk|rk|ghp|gho|ghs|ghu|ghr|github_pat|xox[abposr]|AKIA|ASIA|glpat)[_-][A-Za-z0-9_-]{8,}/gi,
|
|
37
|
+
// Anything else long enough and dense enough to be a secret rather than a word. The catch-all for
|
|
38
|
+
// formats nobody here has seen: on a privacy boundary, unrecognised is not the same as safe.
|
|
39
|
+
/\b[A-Za-z0-9_-]{24,}\b/g,
|
|
40
|
+
// ── Then the grouping rules ──────────────────────────────────────────────────────────────────
|
|
22
41
|
/"[^"]*"|'[^']*'|`[^`]*`/g, // quoted values — names, selectors, snippets
|
|
23
42
|
/\b[a-z][a-z0-9+.-]*:\/\/\S+/gi, // URLs
|
|
24
43
|
/(?:\/[\w.-]+){2,}\/?/g, // POSIX-ish paths
|
|
@@ -53,7 +72,7 @@ export function reticleFrames(stack) {
|
|
|
53
72
|
if (!/@reticlehq[/\\]|[/\\]reticle[/\\]dist[/\\]/.test(line))
|
|
54
73
|
continue;
|
|
55
74
|
const location = line.match(/([\w.-]+\.(?:js|mjs|cjs|ts)):(\d+)/);
|
|
56
|
-
if (
|
|
75
|
+
if (null === location)
|
|
57
76
|
continue;
|
|
58
77
|
// `at Object.runTool (/path/to/invoke-tool.js:88:3)` → `runTool`. The function name is half the
|
|
59
78
|
// value of a frame in an RCA — "it died in act-tools.js" narrows to a file, "it died in
|
|
@@ -62,7 +81,7 @@ export function reticleFrames(stack) {
|
|
|
62
81
|
const where = `${location[1] ?? ''}:${location[2] ?? ''}`;
|
|
63
82
|
// `Object.` / `Module.` prefixes are V8 noise that make the same function look like two.
|
|
64
83
|
const name = fn?.replace(/^(?:Object|Module|Function)\./, '');
|
|
65
|
-
frames.push(name === undefined ||
|
|
84
|
+
frames.push(name === undefined || '' === name ? where : `${name}@${where}`);
|
|
66
85
|
}
|
|
67
86
|
return frames;
|
|
68
87
|
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { FeedbackKind, FeedbackSource } from '@reticlehq/core';
|
|
2
|
-
import { BUG_FLAG, RATING_FLAG } from '../cli-parse.js';
|
|
2
|
+
import { BUG_FLAG, RATING_FLAG } from '../cli/cli-parse.js';
|
|
3
3
|
import { describeFeedbackPayload, submitFeedback } from './feedback.js';
|
|
4
4
|
import { describeTelemetry, setTelemetryEnabled } from './telemetry.js';
|
|
5
|
-
import { TelemetryAction } from '../cli-parse.js';
|
|
5
|
+
import { TelemetryAction } from '../cli/cli-parse.js';
|
|
6
6
|
import { clearIdentity, IDENTIFY_NOTICE, readIdentity, saveIdentity, submitIdentity, UsageContextKind, } from './identify.js';
|
|
7
7
|
/**
|
|
8
8
|
* `reticle feedback [--rating 1-5] [--bug] "what happened"` — the human half of the feedback channel.
|
|
@@ -18,7 +18,7 @@ export async function handleFeedback(text, rating, bug) {
|
|
|
18
18
|
const line = (s) => {
|
|
19
19
|
process.stdout.write(`${s}\n`);
|
|
20
20
|
};
|
|
21
|
-
if (
|
|
21
|
+
if ('' === text && rating === undefined) {
|
|
22
22
|
line(`usage: reticle feedback [${RATING_FLAG} 1-5] [${BUG_FLAG}] "what worked, what didn't"`);
|
|
23
23
|
line(' your words go to the maintainers; nothing from your app is ever included.');
|
|
24
24
|
process.exitCode = 1;
|
|
@@ -28,7 +28,7 @@ export async function handleFeedback(text, rating, bug) {
|
|
|
28
28
|
source: FeedbackSource.HUMAN,
|
|
29
29
|
kind: bug ? FeedbackKind.BUG : FeedbackKind.EXPERIENCE,
|
|
30
30
|
// A rating with no words is a real report, but the schema wants text — say what the rating means.
|
|
31
|
-
text:
|
|
31
|
+
text: '' === text ? `rated ${String(rating)}/5 with no comment` : text,
|
|
32
32
|
...(rating !== undefined ? { rating } : {}),
|
|
33
33
|
});
|
|
34
34
|
line(`sending ${JSON.stringify(describeFeedbackPayload(receipt.context))}`);
|
|
@@ -27,6 +27,28 @@ export interface SessionFacts {
|
|
|
27
27
|
engine?: string;
|
|
28
28
|
/** True when Reticle drives the page over CDP rather than observing the human's own browser. */
|
|
29
29
|
driven?: boolean;
|
|
30
|
+
/** What the APP said it is running, from the SDK's hello. The only session-scoped stack evidence. */
|
|
31
|
+
adapters?: string[];
|
|
30
32
|
}
|
|
33
|
+
/**
|
|
34
|
+
* Reconcile what the DIRECTORY says with what the APP says.
|
|
35
|
+
*
|
|
36
|
+
* `detectStack` reads the daemon's cwd, which is not the session's project whenever one daemon
|
|
37
|
+
* serves several apps — the normal case for anyone with two dev servers, and the default in the
|
|
38
|
+
* fixtures. Reported from the field: feedback filed from an astro session arrived stamped
|
|
39
|
+
* `"stack":"sveltekit"`, because that was the directory the daemon happened to be started in. The
|
|
40
|
+
* sessionId was passed and ignored.
|
|
41
|
+
*
|
|
42
|
+
* The rule: keep the directory's answer only while the app agrees with it. An adapter that implies a
|
|
43
|
+
* different family means the directory is describing somebody else's project, so the app's own
|
|
44
|
+
* report wins — less specific, and true, which is the right trade for an attribution field.
|
|
45
|
+
*/
|
|
46
|
+
export declare function reconcileStack(fromCwd: {
|
|
47
|
+
stack?: string;
|
|
48
|
+
stackMajor?: number;
|
|
49
|
+
}, adapters: readonly string[] | undefined): {
|
|
50
|
+
stack?: string;
|
|
51
|
+
stackMajor?: number;
|
|
52
|
+
};
|
|
31
53
|
/** Assemble the full context. Every field independently optional — a partial context still ships. */
|
|
32
54
|
export declare function feedbackContext(cwd: string, session?: SessionFacts): FeedbackContext;
|
|
@@ -116,6 +116,43 @@ function detectClient() {
|
|
|
116
116
|
return {};
|
|
117
117
|
}
|
|
118
118
|
}
|
|
119
|
+
/**
|
|
120
|
+
* Which cwd-derived stacks each SDK adapter is consistent with.
|
|
121
|
+
*
|
|
122
|
+
* The adapter names a UI library; the directory may name a meta-framework built on it. `react` and
|
|
123
|
+
* `next` are the same app described at two levels, so the more specific one is kept. `react` and
|
|
124
|
+
* `sveltekit` are not — that pairing means the daemon's directory is not this session's project.
|
|
125
|
+
*/
|
|
126
|
+
const ADAPTER_IMPLIES = {
|
|
127
|
+
react: ['react', 'next', 'remix'],
|
|
128
|
+
svelte: ['svelte', 'sveltekit'],
|
|
129
|
+
vue: ['vue', 'nuxt'],
|
|
130
|
+
solid: ['solid'],
|
|
131
|
+
angular: ['angular'],
|
|
132
|
+
};
|
|
133
|
+
/**
|
|
134
|
+
* Reconcile what the DIRECTORY says with what the APP says.
|
|
135
|
+
*
|
|
136
|
+
* `detectStack` reads the daemon's cwd, which is not the session's project whenever one daemon
|
|
137
|
+
* serves several apps — the normal case for anyone with two dev servers, and the default in the
|
|
138
|
+
* fixtures. Reported from the field: feedback filed from an astro session arrived stamped
|
|
139
|
+
* `"stack":"sveltekit"`, because that was the directory the daemon happened to be started in. The
|
|
140
|
+
* sessionId was passed and ignored.
|
|
141
|
+
*
|
|
142
|
+
* The rule: keep the directory's answer only while the app agrees with it. An adapter that implies a
|
|
143
|
+
* different family means the directory is describing somebody else's project, so the app's own
|
|
144
|
+
* report wins — less specific, and true, which is the right trade for an attribution field.
|
|
145
|
+
*/
|
|
146
|
+
export function reconcileStack(fromCwd, adapters) {
|
|
147
|
+
const claimed = adapters?.find((adapter) => adapter in ADAPTER_IMPLIES);
|
|
148
|
+
if (claimed === undefined)
|
|
149
|
+
return fromCwd;
|
|
150
|
+
const consistent = ADAPTER_IMPLIES[claimed] ?? [];
|
|
151
|
+
if (fromCwd.stack !== undefined && consistent.includes(fromCwd.stack))
|
|
152
|
+
return fromCwd;
|
|
153
|
+
// The major belonged to the directory's framework, so it cannot travel with a different stack.
|
|
154
|
+
return { stack: claimed };
|
|
155
|
+
}
|
|
119
156
|
function asRuntime(value) {
|
|
120
157
|
return Object.values(AppRuntime).find((known) => known === value);
|
|
121
158
|
}
|
|
@@ -131,7 +168,10 @@ export function feedbackContext(cwd, session) {
|
|
|
131
168
|
const engine = asEngine(session?.engine);
|
|
132
169
|
const client = detectClient();
|
|
133
170
|
return {
|
|
134
|
-
|
|
171
|
+
// Session-scoped when the app reports something that contradicts the directory — see
|
|
172
|
+
// reconcileStack. A daemon serving two apps otherwise stamps both with whichever one it was
|
|
173
|
+
// started next to.
|
|
174
|
+
...reconcileStack(detectStack(cwd), session?.adapters),
|
|
135
175
|
...client,
|
|
136
176
|
...(runtime !== undefined ? { runtime } : {}),
|
|
137
177
|
...(engine !== undefined ? { engine } : {}),
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Telling the reporter that a BACKGROUND feedback delivery failed.
|
|
3
|
+
*
|
|
4
|
+
* The agent's `reticle_feedback` no longer waits for the POST — a ~340ms network round-trip on the
|
|
5
|
+
* one call an agent makes mid-task is the product blocking the user's work to talk about itself. But
|
|
6
|
+
* not waiting reopens the exact hole the awaited version was written to close: `sent` used to be
|
|
7
|
+
* unconditional, so a DNS miss and a 4xx both reported "filed", and this is the ONLY qualitative
|
|
8
|
+
* channel the product has. A lost report that was announced as filed is worse than no channel.
|
|
9
|
+
*
|
|
10
|
+
* So the wait goes and the honesty stays: the receipt says ACCEPTED (validated, redacted, queued),
|
|
11
|
+
* never "delivered", and if the send then fails the agent is told on its next tool result through
|
|
12
|
+
* the same one-shot envelope the update and skew nudges use. Told once — a reporter who cannot fix
|
|
13
|
+
* the network does not need it repeated, and a banner on every call is one agents learn to skip.
|
|
14
|
+
*/
|
|
15
|
+
/** Record that a queued report never made it. */
|
|
16
|
+
export declare function noteFeedbackUndelivered(reason: string): void;
|
|
17
|
+
/** The undelivered-feedback notice, once. */
|
|
18
|
+
export declare function takeFeedbackUndelivered(): string | undefined;
|
|
19
|
+
/** Tests only — drop the module state so each case starts clean. */
|
|
20
|
+
export declare function resetFeedbackDelivery(): void;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Telling the reporter that a BACKGROUND feedback delivery failed.
|
|
3
|
+
*
|
|
4
|
+
* The agent's `reticle_feedback` no longer waits for the POST — a ~340ms network round-trip on the
|
|
5
|
+
* one call an agent makes mid-task is the product blocking the user's work to talk about itself. But
|
|
6
|
+
* not waiting reopens the exact hole the awaited version was written to close: `sent` used to be
|
|
7
|
+
* unconditional, so a DNS miss and a 4xx both reported "filed", and this is the ONLY qualitative
|
|
8
|
+
* channel the product has. A lost report that was announced as filed is worse than no channel.
|
|
9
|
+
*
|
|
10
|
+
* So the wait goes and the honesty stays: the receipt says ACCEPTED (validated, redacted, queued),
|
|
11
|
+
* never "delivered", and if the send then fails the agent is told on its next tool result through
|
|
12
|
+
* the same one-shot envelope the update and skew nudges use. Told once — a reporter who cannot fix
|
|
13
|
+
* the network does not need it repeated, and a banner on every call is one agents learn to skip.
|
|
14
|
+
*/
|
|
15
|
+
/** Set when a background feedback send failed and the reporter has not been told yet. */
|
|
16
|
+
let pending;
|
|
17
|
+
/** Record that a queued report never made it. */
|
|
18
|
+
export function noteFeedbackUndelivered(reason) {
|
|
19
|
+
pending = reason;
|
|
20
|
+
}
|
|
21
|
+
/** The undelivered-feedback notice, once. */
|
|
22
|
+
export function takeFeedbackUndelivered() {
|
|
23
|
+
const notice = pending;
|
|
24
|
+
pending = undefined;
|
|
25
|
+
return notice;
|
|
26
|
+
}
|
|
27
|
+
/** Tests only — drop the module state so each case starts clean. */
|
|
28
|
+
export function resetFeedbackDelivery() {
|
|
29
|
+
pending = undefined;
|
|
30
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/** One queued report: the payload, plus what is needed to find and remove it again. */
|
|
2
|
+
export interface OutboxEntry {
|
|
3
|
+
id: string;
|
|
4
|
+
t: string;
|
|
5
|
+
payload: unknown;
|
|
6
|
+
}
|
|
7
|
+
export declare function outboxPath(home?: string): string;
|
|
8
|
+
/**
|
|
9
|
+
* Persist a report and return its id. Never throws: failing to queue must not also fail the send
|
|
10
|
+
* that was about to happen anyway.
|
|
11
|
+
*/
|
|
12
|
+
export declare function queueFeedback(payload: unknown, now?: () => Date, home?: string): string | null;
|
|
13
|
+
/** Read every queued report. A malformed line is skipped, never fatal — the rest still matter. */
|
|
14
|
+
export declare function readOutbox(home?: string): OutboxEntry[];
|
|
15
|
+
/**
|
|
16
|
+
* Drop a report that was delivered. Rewrites the file without it.
|
|
17
|
+
*
|
|
18
|
+
* A rewrite rather than a tombstone because the file's whole purpose is "what still needs sending" —
|
|
19
|
+
* a reader should be able to `cat` it and see exactly the backlog, with no filtering rule to learn.
|
|
20
|
+
* Volume is a handful of lines a week, so the cost is irrelevant.
|
|
21
|
+
*/
|
|
22
|
+
export declare function markDelivered(id: string | null, home?: string): void;
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A durable outbox for feedback, written BEFORE the network is touched.
|
|
3
|
+
*
|
|
4
|
+
* Feedback is the only qualitative channel this product has, and it carries an agent's whole
|
|
5
|
+
* root-cause analysis — often an hour of driving distilled into one payload. It was sent with the
|
|
6
|
+
* same 2-second fire-and-forget budget as a usage counter, with no retry and no persistence, so a
|
|
7
|
+
* 1.3-second network hiccup destroyed the report outright. Measured to the collector from one
|
|
8
|
+
* machine with a WARM DNS cache: dns 0.003s, connect 0.233s, tls 0.467s, total 0.694s — roughly a
|
|
9
|
+
* third of the entire budget spent before a byte of payload moves, and that is the good case. A
|
|
10
|
+
* fresh short-lived CLI process pays cold DNS and a cold route on top.
|
|
11
|
+
*
|
|
12
|
+
* So the order is inverted: write it down first, then try to send. A failed send is then a queued
|
|
13
|
+
* report rather than a lost one, and `sent: false` becomes a statement about the network instead of
|
|
14
|
+
* an epitaph.
|
|
15
|
+
*
|
|
16
|
+
* JSONL, appended, one payload per line. Append-only is the point — a crash mid-write costs at most
|
|
17
|
+
* the line being written, never the ones already there.
|
|
18
|
+
*/
|
|
19
|
+
import { appendFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
20
|
+
import { homedir } from 'node:os';
|
|
21
|
+
import { join } from 'node:path';
|
|
22
|
+
import { randomUUID } from 'node:crypto';
|
|
23
|
+
import { ReticleDir } from '@reticlehq/core';
|
|
24
|
+
export function outboxPath(home = homedir()) {
|
|
25
|
+
return join(home, ReticleDir.ROOT, 'feedback-outbox.jsonl');
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Persist a report and return its id. Never throws: failing to queue must not also fail the send
|
|
29
|
+
* that was about to happen anyway.
|
|
30
|
+
*/
|
|
31
|
+
export function queueFeedback(payload, now = () => new Date(), home = homedir()) {
|
|
32
|
+
const id = randomUUID();
|
|
33
|
+
try {
|
|
34
|
+
mkdirSync(join(home, ReticleDir.ROOT), { recursive: true });
|
|
35
|
+
const entry = { id, t: now().toISOString(), payload };
|
|
36
|
+
appendFileSync(outboxPath(home), `${JSON.stringify(entry)}\n`, 'utf8');
|
|
37
|
+
return id;
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
/** Read every queued report. A malformed line is skipped, never fatal — the rest still matter. */
|
|
44
|
+
export function readOutbox(home = homedir()) {
|
|
45
|
+
try {
|
|
46
|
+
const path = outboxPath(home);
|
|
47
|
+
if (!existsSync(path))
|
|
48
|
+
return [];
|
|
49
|
+
return readFileSync(path, 'utf8')
|
|
50
|
+
.split('\n')
|
|
51
|
+
.filter((line) => 0 < line.trim().length)
|
|
52
|
+
.flatMap((line) => {
|
|
53
|
+
try {
|
|
54
|
+
return [JSON.parse(line)];
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
return [];
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
return [];
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Drop a report that was delivered. Rewrites the file without it.
|
|
67
|
+
*
|
|
68
|
+
* A rewrite rather than a tombstone because the file's whole purpose is "what still needs sending" —
|
|
69
|
+
* a reader should be able to `cat` it and see exactly the backlog, with no filtering rule to learn.
|
|
70
|
+
* Volume is a handful of lines a week, so the cost is irrelevant.
|
|
71
|
+
*/
|
|
72
|
+
export function markDelivered(id, home = homedir()) {
|
|
73
|
+
if (null === id)
|
|
74
|
+
return;
|
|
75
|
+
try {
|
|
76
|
+
const remaining = readOutbox(home).filter((entry) => entry.id !== id);
|
|
77
|
+
const path = outboxPath(home);
|
|
78
|
+
if (0 === remaining.length) {
|
|
79
|
+
writeFileSync(path, '', 'utf8');
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
writeFileSync(path, `${remaining.map((entry) => JSON.stringify(entry)).join('\n')}\n`, 'utf8');
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
// A report that stays queued after a successful send is a duplicate at worst. Losing one is not.
|
|
86
|
+
}
|
|
87
|
+
}
|
|
@@ -39,8 +39,21 @@ export declare function feedbackDisabled(env?: NodeJS.ProcessEnv): boolean;
|
|
|
39
39
|
/** What the author supplies; the environment context is detected, never asked for. */
|
|
40
40
|
export type FeedbackInput = Pick<Feedback, 'source' | 'kind' | 'text' | 'trace' | 'rating' | 'need' | 'impact' | 'currentApproach' | 'model'>;
|
|
41
41
|
export interface FeedbackReceipt {
|
|
42
|
-
/**
|
|
42
|
+
/**
|
|
43
|
+
* DELIVERY confirmed. Only ever true when the send was awaited and the endpoint accepted it —
|
|
44
|
+
* never as an optimistic stand-in for "we handed it to the emitter". That distinction is the whole
|
|
45
|
+
* value of this field: it was once unconditional, so a DNS miss and a 4xx both reported "filed".
|
|
46
|
+
*
|
|
47
|
+
* A backgrounded send (the agent tool) therefore returns `sent: false` with `accepted: true` —
|
|
48
|
+
* see below. It is not a failure, and the note says so.
|
|
49
|
+
*/
|
|
43
50
|
sent: boolean;
|
|
51
|
+
/**
|
|
52
|
+
* Validated, redacted and handed to the emitter. TRUE says the report is well-formed and on its
|
|
53
|
+
* way; it does NOT say it arrived. If a backgrounded send then fails, the reporter is told on its
|
|
54
|
+
* next tool result — see feedback-delivery.ts.
|
|
55
|
+
*/
|
|
56
|
+
accepted: boolean;
|
|
44
57
|
reason?: string;
|
|
45
58
|
/** Which redaction rules fired. Empty when nothing needed removing. */
|
|
46
59
|
redacted: string[];
|
|
@@ -56,6 +69,14 @@ export declare function submitFeedback(input: FeedbackInput, opts?: {
|
|
|
56
69
|
cwd?: string;
|
|
57
70
|
session?: SessionFacts;
|
|
58
71
|
env?: NodeJS.ProcessEnv;
|
|
72
|
+
/**
|
|
73
|
+
* Return as soon as the report is validated and queued, instead of waiting out the POST.
|
|
74
|
+
*
|
|
75
|
+
* Used by the AGENT tool: ~340ms of network on a call made mid-task is the product blocking the
|
|
76
|
+
* user's work to talk about itself. NOT used by `reticle feedback`, where a human typed the
|
|
77
|
+
* command and is waiting for an answer — there, confirmed delivery is the answer.
|
|
78
|
+
*/
|
|
79
|
+
background?: boolean;
|
|
59
80
|
}): Promise<FeedbackReceipt>;
|
|
60
81
|
/**
|
|
61
82
|
* The receipt's context PLUS the fields the emitter stamps on every event regardless of kind — the
|