@reticlehq/server 2.12.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 +67 -109
- 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 -1
- package/dist/bridge/bridge.js +247 -164
- 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 +6 -6
- package/dist/capsule/causal-summary.js +7 -2
- package/dist/cli/cli-doctor.js +31 -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 +15 -1
- package/dist/cli/cli-parse.js +212 -8
- package/dist/cli/cli-verify.d.ts +1 -0
- package/dist/cli/cli-verify.js +3 -2
- package/dist/cli/cloud-cli.js +428 -259
- 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.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 +9 -0
- package/dist/cli/doctor-rows.js +9 -0
- 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/mcp-command.js +51 -20
- package/dist/cli/project-name.d.ts +30 -0
- package/dist/cli/project-name.js +44 -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 +134 -80
- package/dist/cloud/cloud-config.d.ts +22 -1
- package/dist/cloud/cloud-config.js +64 -5
- package/dist/cloud/cloud-sync.d.ts +5 -5
- package/dist/cloud/cloud-sync.js +33 -2
- 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 +3 -0
- package/dist/crawl/crawl.js +5 -0
- package/dist/daemon/daemon-resolve.d.ts +43 -0
- package/dist/daemon/daemon-resolve.js +69 -0
- package/dist/daemon/dev-servers.d.ts +15 -0
- package/dist/daemon/dev-servers.js +68 -0
- package/dist/daemon/wake-decision.d.ts +32 -0
- package/dist/daemon/wake-decision.js +38 -0
- package/dist/domain/domain-tools.js +2 -2
- package/dist/events/contradictions.d.ts +18 -0
- package/dist/events/contradictions.js +134 -21
- package/dist/events/declared.d.ts +7 -4
- package/dist/events/declared.js +15 -6
- package/dist/events/echo-mismatch.js +64 -14
- package/dist/events/event-filters.d.ts +8 -0
- package/dist/events/event-filters.js +19 -2
- package/dist/events/predicate-eval.js +53 -5
- 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.d.ts +5 -0
- package/dist/events/predicate.js +37 -5
- package/dist/events/split-text-miss.d.ts +30 -0
- package/dist/events/split-text-miss.js +44 -0
- package/dist/flows/flake.d.ts +6 -6
- 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 +239 -22
- 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-tools.js +58 -32
- package/dist/flows/flows.d.ts +8 -0
- package/dist/flows/flows.js +468 -2
- 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 +30 -12
- 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/verify-change-tools.js +5 -2
- package/dist/honesty/blind-spots.d.ts +55 -2
- package/dist/honesty/blind-spots.js +63 -3
- package/dist/honesty/honesty.d.ts +24 -0
- package/dist/honesty/honesty.js +25 -1
- package/dist/honesty/instrumentation-gaps.d.ts +9 -0
- package/dist/honesty/instrumentation-gaps.js +29 -2
- package/dist/honesty/verified.d.ts +2 -1
- package/dist/honesty/verified.js +16 -1
- package/dist/http-server.js +12 -0
- package/dist/impact/impact-recorder.d.ts +31 -8
- package/dist/impact/impact-recorder.js +104 -21
- package/dist/impact/impact-store.d.ts +15 -1
- package/dist/impact/impact-store.js +43 -2
- package/dist/index.js +113 -7
- package/dist/init/agent-rules.d.ts +13 -6
- package/dist/init/agent-rules.js +23 -12
- package/dist/init/astro-patch.d.ts +16 -0
- package/dist/init/astro-patch.js +28 -0
- package/dist/init/capabilities.d.ts +4 -1
- package/dist/init/capabilities.js +22 -2
- package/dist/init/confirm.d.ts +31 -7
- package/dist/init/confirm.js +48 -8
- package/dist/init/cra.d.ts +20 -4
- package/dist/init/cra.js +57 -17
- 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 +3 -1
- package/dist/init/dev-script.js +8 -5
- 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 +1 -1
- package/dist/init/mcp.js +1 -1
- package/dist/init/node-io.js +11 -1
- package/dist/init/plan-framework.js +81 -45
- package/dist/init/plan.d.ts +20 -7
- package/dist/init/plan.js +20 -45
- package/dist/init/preflight.d.ts +28 -0
- package/dist/init/preflight.js +38 -0
- package/dist/init/run.d.ts +32 -9
- package/dist/init/run.js +141 -119
- package/dist/init/slash-command.d.ts +1 -1
- package/dist/init/snippets.js +41 -25
- 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.js +25 -5
- package/dist/input/scroll-tools.js +4 -6
- package/dist/input/viewport-tools.js +5 -4
- package/dist/intent/inline-intent.d.ts +10 -19
- package/dist/intent/inline-intent.js +91 -1
- 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 +12 -0
- package/dist/intent/intent-store.js +21 -0
- package/dist/intent/intent-subject.d.ts +45 -0
- package/dist/intent/intent-subject.js +177 -0
- package/dist/intent/intent-tools.js +65 -2
- package/dist/intent/open-intents.d.ts +21 -0
- package/dist/intent/open-intents.js +15 -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 +18 -2
- package/dist/mcp/attach-memory.js +50 -7
- 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 -1
- package/dist/mcp/mcp-proxy.js +52 -62
- package/dist/mcp/mcp.js +7 -1
- 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/tool-catalog-cache.d.ts +8 -0
- package/dist/mcp/tool-catalog-cache.js +12 -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/propose-consequences.js +12 -3
- package/dist/pool/browser-pool.d.ts +63 -4
- package/dist/pool/browser-pool.js +113 -4
- package/dist/pool/playwright-launcher.js +9 -0
- package/dist/project/project-tools.js +3 -3
- package/dist/project/reticle-dir.d.ts +15 -2
- package/dist/project/reticle-dir.js +21 -4
- 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 +22 -1
- 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/lease-visibility.d.ts +67 -0
- package/dist/session/lease-visibility.js +76 -0
- package/dist/session/no-session-diagnosis.d.ts +23 -0
- package/dist/session/no-session-diagnosis.js +58 -17
- package/dist/session/no-session-next-action.d.ts +7 -0
- package/dist/session/no-session-next-action.js +42 -3
- package/dist/session/no-session-watch.d.ts +10 -6
- package/dist/session/no-session-watch.js +51 -19
- 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-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 +61 -3
- 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.js +36 -1
- package/dist/session/session-tools.js +2 -2
- package/dist/session/session.d.ts +22 -2
- package/dist/session/session.js +87 -7
- 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/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.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 -1
- package/dist/telemetry/project-profile.js +11 -6
- package/dist/telemetry/proxy-telemetry.d.ts +3 -0
- package/dist/telemetry/proxy-telemetry.js +21 -0
- 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/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 +100 -213
- package/dist/tools/assert-verdict.js +22 -16
- package/dist/tools/browser-tools.js +6 -2
- package/dist/tools/dynamic-tools.js +24 -5
- package/dist/tools/error-recovery.d.ts +1 -0
- package/dist/tools/error-recovery.js +8 -0
- package/dist/tools/invoke-tool.js +59 -9
- package/dist/tools/lease-tools.d.ts +30 -4
- package/dist/tools/lease-tools.js +169 -5
- package/dist/tools/merged-name-redirect.d.ts +13 -0
- package/dist/tools/merged-name-redirect.js +22 -0
- package/dist/tools/numeric-bounds.d.ts +100 -0
- package/dist/tools/numeric-bounds.js +125 -0
- package/dist/tools/observe-tools.js +47 -40
- package/dist/tools/read-tools.js +28 -7
- package/dist/tools/real-input-attempt.d.ts +10 -0
- package/dist/tools/real-input-attempt.js +59 -1
- package/dist/tools/reconcile-tools.js +4 -4
- package/dist/tools/tool-kit.d.ts +12 -0
- package/dist/tools/tool-kit.js +6 -1
- package/dist/tools/tool-names.d.ts +2 -0
- package/dist/tools/tool-names.js +2 -0
- 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 +56 -8
- 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 +18 -0
- package/dist/update/update-nudge.js +20 -0
- 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/cli/doctor.mdx +1 -0
- package/docs/cli/serve.mdx +4 -2
- package/docs/cli.mdx +7 -3
- package/docs/desktop.mdx +1 -1
- package/docs/docs.json +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.mdx +1 -1
- package/docs/skill-file.mdx +3 -3
- package/docs/telemetry-contract.md +108 -4
- package/docs/tools-act-sequence.mdx +16 -0
- package/docs/usage.md +1 -1
- package/package.json +4 -3
- package/dist/init/cursor.d.ts +0 -24
- package/dist/init/cursor.js +0 -80
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The contract between the agent and the script.
|
|
3
|
+
*
|
|
4
|
+
* Everything setup does is deterministic except for the handful of things that require reading a
|
|
5
|
+
* repository and a human's request and understanding both. Those are exactly what an agent is good
|
|
6
|
+
* at and code is not, so they arrive here as ARGUMENTS rather than as steps somebody walks through:
|
|
7
|
+
* the agent decides, the script executes, and neither does the other's job.
|
|
8
|
+
*
|
|
9
|
+
* What only an agent can know, and therefore what belongs in this type:
|
|
10
|
+
*
|
|
11
|
+
* --flow which journey proves the thing the user cares about. Code can enumerate buttons; it
|
|
12
|
+
* cannot know that checkout matters and the theme toggle does not. Measured, naming
|
|
13
|
+
* it took one app's drive from a ten-minute timeout to 138 seconds, because the turns
|
|
14
|
+
* go into FINDING a flow and naming one removes that search.
|
|
15
|
+
* --app which app in a monorepo the user meant. init can find the ones that are servable;
|
|
16
|
+
* only the request says which of them is being worked on.
|
|
17
|
+
* --env what the app needs to get past its own front door. A key in .env.example, a mock
|
|
18
|
+
* backend named in the README, the variable that skips an auth wall: all readable by
|
|
19
|
+
* an agent, none inferable by a script. Without this a real app sits on a login
|
|
20
|
+
* screen and there is no flow to drive at all.
|
|
21
|
+
* --dev-cmd the command, when the project's own scripts do not say it plainly.
|
|
22
|
+
* --url the app is already running here, so do not start anything.
|
|
23
|
+
*
|
|
24
|
+
* Everything else on this type is an operator's dial, not an agent's judgement.
|
|
25
|
+
*/
|
|
26
|
+
/** The bridge port. NOT the dev server's, and conflating them is a documented setup failure. */
|
|
27
|
+
export const DEFAULT_BRIDGE_PORT = 4400;
|
|
28
|
+
/** Per-phase budget. The drive gets its own, much larger one. */
|
|
29
|
+
export const DEFAULT_PHASE_TIMEOUT_MS = 120_000;
|
|
30
|
+
/** What the drive may spend before it is stopped. */
|
|
31
|
+
export const DEFAULT_DRIVE_BUDGET_USD = 3;
|
|
32
|
+
export const DEFAULT_SETUP_OPTIONS = {
|
|
33
|
+
bridgePort: DEFAULT_BRIDGE_PORT,
|
|
34
|
+
phaseTimeoutMs: DEFAULT_PHASE_TIMEOUT_MS,
|
|
35
|
+
driveBudgetUsd: DEFAULT_DRIVE_BUDGET_USD,
|
|
36
|
+
registerAgents: true,
|
|
37
|
+
openBrowser: true,
|
|
38
|
+
drive: true,
|
|
39
|
+
escalateWeakFlow: true,
|
|
40
|
+
restartStaleDevServer: true,
|
|
41
|
+
filesOnly: false,
|
|
42
|
+
};
|
|
43
|
+
/** One `KEY=VALUE`, as an agent would pass it. Everything after the first `=` is the value. */
|
|
44
|
+
export function parseEnvAssignment(pair) {
|
|
45
|
+
const at = pair.indexOf('=');
|
|
46
|
+
if (0 >= at)
|
|
47
|
+
return null;
|
|
48
|
+
const key = pair.slice(0, at).trim();
|
|
49
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key))
|
|
50
|
+
return null;
|
|
51
|
+
return { key, value: pair.slice(at + 1) };
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Fold repeated `--env KEY=VALUE` into one map, ignoring what cannot be a variable.
|
|
55
|
+
*
|
|
56
|
+
* A malformed pair is dropped rather than guessed at: an agent that meant `--env` and typed
|
|
57
|
+
* something else should see its variable missing, not see setup invent one.
|
|
58
|
+
*/
|
|
59
|
+
export function collectEnv(pairs) {
|
|
60
|
+
const env = {};
|
|
61
|
+
for (const pair of pairs) {
|
|
62
|
+
const parsed = parseEnvAssignment(pair);
|
|
63
|
+
if (null !== parsed)
|
|
64
|
+
env[parsed.key] = parsed.value;
|
|
65
|
+
}
|
|
66
|
+
return env;
|
|
67
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Does a transcript exist for this Claude Code session.
|
|
3
|
+
*
|
|
4
|
+
* Two roots, because a shared-config install writes elsewhere and a restart that checks only one of
|
|
5
|
+
* them refuses a session that is perfectly real.
|
|
6
|
+
*/
|
|
7
|
+
export declare function claudeTranscriptExists(sessionId: string): boolean;
|
|
8
|
+
/**
|
|
9
|
+
* The codex conversation running in `cwd`, when one is.
|
|
10
|
+
*
|
|
11
|
+
* Codex names no session in the environment — unlike Claude Code — so it is recognised from the
|
|
12
|
+
* rollout file it is writing. Not yet wired to a reader here: returning undefined means "no codex
|
|
13
|
+
* session identified", which is the honest answer until one exists, and the decision it feeds says
|
|
14
|
+
* plainly that the client identified no conversation.
|
|
15
|
+
*/
|
|
16
|
+
export declare function codexSessionFor(_cwd: string): string | undefined;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which conversation is asking, and does it really exist.
|
|
3
|
+
*
|
|
4
|
+
* Both answers come off the filesystem, so they are kept away from the pure decision in relaunch.ts
|
|
5
|
+
* — that is the part worth testing, and it is the part that can be wrong without anybody noticing.
|
|
6
|
+
*/
|
|
7
|
+
import { existsSync, readdirSync } from 'node:fs';
|
|
8
|
+
import { homedir } from 'node:os';
|
|
9
|
+
import { join } from 'node:path';
|
|
10
|
+
/**
|
|
11
|
+
* Does a transcript exist for this Claude Code session.
|
|
12
|
+
*
|
|
13
|
+
* Two roots, because a shared-config install writes elsewhere and a restart that checks only one of
|
|
14
|
+
* them refuses a session that is perfectly real.
|
|
15
|
+
*/
|
|
16
|
+
export function claudeTranscriptExists(sessionId) {
|
|
17
|
+
for (const base of [
|
|
18
|
+
join(homedir(), '.claude-shared', 'projects'),
|
|
19
|
+
join(homedir(), '.claude', 'projects'),
|
|
20
|
+
]) {
|
|
21
|
+
try {
|
|
22
|
+
for (const project of readdirSync(base)) {
|
|
23
|
+
if (existsSync(join(base, project, `${sessionId}.jsonl`)))
|
|
24
|
+
return true;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
/* that history directory does not exist on this machine */
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* The codex conversation running in `cwd`, when one is.
|
|
35
|
+
*
|
|
36
|
+
* Codex names no session in the environment — unlike Claude Code — so it is recognised from the
|
|
37
|
+
* rollout file it is writing. Not yet wired to a reader here: returning undefined means "no codex
|
|
38
|
+
* session identified", which is the honest answer until one exists, and the decision it feeds says
|
|
39
|
+
* plainly that the client identified no conversation.
|
|
40
|
+
*/
|
|
41
|
+
export function codexSessionFor(_cwd) {
|
|
42
|
+
return undefined;
|
|
43
|
+
}
|
package/dist/status-payload.d.ts
CHANGED
|
@@ -24,8 +24,21 @@ interface StatusPayload {
|
|
|
24
24
|
* running the command we tell them to run in `init`'s closing line deserves the same sentence.
|
|
25
25
|
*/
|
|
26
26
|
why?: string;
|
|
27
|
+
/** Port of the verify HTTP endpoint this daemon serves — present only when started with `--http`. */
|
|
28
|
+
verifyPort?: number;
|
|
27
29
|
}
|
|
28
30
|
export declare function statusPayload(sessionCount: number, sessions: SessionInfo[],
|
|
29
31
|
/** The no-session diagnosis, injected so this stays pure and the port probe stays off this path. */
|
|
30
|
-
why?: string): StatusPayload;
|
|
32
|
+
why?: string, verifyPort?: number): StatusPayload;
|
|
33
|
+
/**
|
|
34
|
+
* Why `serve --http` cannot claim success against an already-running daemon — or undefined when
|
|
35
|
+
* that daemon already serves the verify HTTP endpoint on the wanted port.
|
|
36
|
+
*
|
|
37
|
+
* `serve` never hands flags to a daemon that is already up, so `--http`/`--http-port` against one
|
|
38
|
+
* used to be accepted, dropped, and reported as success (#687). This is the read side of the
|
|
39
|
+
* `verifyPort` field above: the daemon says which port it serves, and `serve` compares. A daemon
|
|
40
|
+
* too old to report the field — or one that did not answer /status — reads as "not honoured",
|
|
41
|
+
* which errs loud rather than green.
|
|
42
|
+
*/
|
|
43
|
+
export declare function verifyEndpointMismatch(status: unknown, wantedPort: number): string | undefined;
|
|
31
44
|
export {};
|
package/dist/status-payload.js
CHANGED
|
@@ -2,7 +2,7 @@ import { CONTRACT_FINGERPRINT } from '@reticlehq/core';
|
|
|
2
2
|
import { SERVER_VERSION } from './version/server-version.js';
|
|
3
3
|
export function statusPayload(sessionCount, sessions,
|
|
4
4
|
/** The no-session diagnosis, injected so this stays pure and the port probe stays off this path. */
|
|
5
|
-
why) {
|
|
5
|
+
why, verifyPort) {
|
|
6
6
|
return {
|
|
7
7
|
running: true,
|
|
8
8
|
version: SERVER_VERSION,
|
|
@@ -12,5 +12,27 @@ why) {
|
|
|
12
12
|
// Only when there is nothing to explain away: a diagnosis printed beside a live session would
|
|
13
13
|
// contradict it.
|
|
14
14
|
...(0 === sessionCount && why !== undefined ? { why } : {}),
|
|
15
|
+
...(verifyPort === undefined ? {} : { verifyPort }),
|
|
15
16
|
};
|
|
16
17
|
}
|
|
18
|
+
/**
|
|
19
|
+
* Why `serve --http` cannot claim success against an already-running daemon — or undefined when
|
|
20
|
+
* that daemon already serves the verify HTTP endpoint on the wanted port.
|
|
21
|
+
*
|
|
22
|
+
* `serve` never hands flags to a daemon that is already up, so `--http`/`--http-port` against one
|
|
23
|
+
* used to be accepted, dropped, and reported as success (#687). This is the read side of the
|
|
24
|
+
* `verifyPort` field above: the daemon says which port it serves, and `serve` compares. A daemon
|
|
25
|
+
* too old to report the field — or one that did not answer /status — reads as "not honoured",
|
|
26
|
+
* which errs loud rather than green.
|
|
27
|
+
*/
|
|
28
|
+
export function verifyEndpointMismatch(status, wantedPort) {
|
|
29
|
+
const served = 'object' === typeof status && status !== null
|
|
30
|
+
? status['verifyPort']
|
|
31
|
+
: undefined;
|
|
32
|
+
if (served === wantedPort)
|
|
33
|
+
return undefined;
|
|
34
|
+
const fix = 'stop it (`reticle stop`) and run `reticle serve --http` again';
|
|
35
|
+
return 'number' === typeof served
|
|
36
|
+
? `the daemon already running serves the verify HTTP endpoint on :${String(served)}, not :${String(wantedPort)} — ${fix}`
|
|
37
|
+
: `a daemon is already running without the verify HTTP endpoint — \`--http\` cannot be applied to it; ${fix}`;
|
|
38
|
+
}
|
|
@@ -53,4 +53,11 @@ export interface DaemonTelemetry {
|
|
|
53
53
|
* Report the daemon as started, profile the project once, and keep the session counters flushing.
|
|
54
54
|
* Every timer is `unref`'d so telemetry can never be the reason a daemon refuses to exit.
|
|
55
55
|
*/
|
|
56
|
-
export declare function installDaemonTelemetry(cwd: string, now?: () => number
|
|
56
|
+
export declare function installDaemonTelemetry(cwd: string, now?: () => number,
|
|
57
|
+
/**
|
|
58
|
+
* The bridge port this daemon bound, so the profile can say whether an app for this project has
|
|
59
|
+
* EVER connected. Optional: a caller that does not know the port gets a profile without that bit
|
|
60
|
+
* rather than one with a guessed `false`, which is the value the whole field exists to make
|
|
61
|
+
* trustworthy.
|
|
62
|
+
*/
|
|
63
|
+
port?: number): DaemonTelemetry;
|
|
@@ -10,10 +10,34 @@ import { TelemetryEventKind } from '@reticlehq/core';
|
|
|
10
10
|
import { getTelemetry } from './telemetry.js';
|
|
11
11
|
import { getSessionMetrics } from './session-metrics.js';
|
|
12
12
|
import { profileProject } from './project-profile.js';
|
|
13
|
-
import {
|
|
13
|
+
import { hasProjectConnectedBefore } from '../session/connection-memory.js';
|
|
14
|
+
import { readProjectId } from '../cli/cli-port.js';
|
|
15
|
+
import { reticleStateHome } from '../daemon/daemon.js';
|
|
16
|
+
import { startUpdateCheck, updateNudgeState } from '../update/update-nudge.js';
|
|
14
17
|
import { markDaemonStart } from './mcp-connection.js';
|
|
15
18
|
import { markInstrumentationClock } from './app-instrumented.js';
|
|
16
19
|
import { markStallClock, STALL_AFTER_MS, stallUptime } from '../session/stall-clock.js';
|
|
20
|
+
/**
|
|
21
|
+
* Whether an app for this project has ever reached Reticle, read from the same durable memory the
|
|
22
|
+
* no-session diagnosis and the server instructions read.
|
|
23
|
+
*
|
|
24
|
+
* Best-effort: an unreadable state home must never be the reason a daemon start emits nothing, and
|
|
25
|
+
* `undefined` here means "not measured" rather than "no". A `false` invented from a read error is
|
|
26
|
+
* exactly the lie this field exists to avoid — it would put the working installs into the cohort we
|
|
27
|
+
* are trying to size.
|
|
28
|
+
*/
|
|
29
|
+
function installFacts(cwd, port) {
|
|
30
|
+
if (port === undefined)
|
|
31
|
+
return undefined;
|
|
32
|
+
try {
|
|
33
|
+
return {
|
|
34
|
+
appConnectedBefore: hasProjectConnectedBefore(reticleStateHome(), port, readProjectId(cwd)),
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return undefined;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
17
41
|
/** Let the daemon finish coming up before walking the source tree for a profile. */
|
|
18
42
|
const PROJECT_PROFILE_DELAY_MS = 5_000;
|
|
19
43
|
/**
|
|
@@ -44,11 +68,34 @@ export const SESSION_FLUSH_MS = 5 * 60 * 1000;
|
|
|
44
68
|
* and 74% of daemons never serve a tool at all.
|
|
45
69
|
*/
|
|
46
70
|
export const FIRST_FLUSH_MS = 90 * 1000;
|
|
71
|
+
/**
|
|
72
|
+
* Attach what the update nudge did to a summary that is about to be sent.
|
|
73
|
+
*
|
|
74
|
+
* Merged HERE rather than inside `SessionMetrics` deliberately: the nudge's state lives in the
|
|
75
|
+
* update module, and a second copy of it in the metrics object would be one more thing that can
|
|
76
|
+
* drift from the flag the delivery path actually reads. `summarize()` stays a pure roll-up of
|
|
77
|
+
* counters.
|
|
78
|
+
*/
|
|
79
|
+
function withNudgeState(summary) {
|
|
80
|
+
const nudge = updateNudgeState();
|
|
81
|
+
return {
|
|
82
|
+
...summary,
|
|
83
|
+
updateNudged: nudge.shown,
|
|
84
|
+
...(nudge.offered === undefined ? {} : { updateOffered: nudge.offered }),
|
|
85
|
+
};
|
|
86
|
+
}
|
|
47
87
|
/**
|
|
48
88
|
* Report the daemon as started, profile the project once, and keep the session counters flushing.
|
|
49
89
|
* Every timer is `unref`'d so telemetry can never be the reason a daemon refuses to exit.
|
|
50
90
|
*/
|
|
51
|
-
export function installDaemonTelemetry(cwd, now = () => Date.now()
|
|
91
|
+
export function installDaemonTelemetry(cwd, now = () => Date.now(),
|
|
92
|
+
/**
|
|
93
|
+
* The bridge port this daemon bound, so the profile can say whether an app for this project has
|
|
94
|
+
* EVER connected. Optional: a caller that does not know the port gets a profile without that bit
|
|
95
|
+
* rather than one with a guessed `false`, which is the value the whole field exists to make
|
|
96
|
+
* trustworthy.
|
|
97
|
+
*/
|
|
98
|
+
port) {
|
|
52
99
|
const metrics = getSessionMetrics(now);
|
|
53
100
|
void getTelemetry().emit(TelemetryEventKind.DAEMON_STARTED);
|
|
54
101
|
// Ask npm whether a newer Reticle exists, so the agent can tell the human. Nothing checked before
|
|
@@ -66,7 +113,7 @@ export function installDaemonTelemetry(cwd, now = () => Date.now()) {
|
|
|
66
113
|
setTimeout(() => {
|
|
67
114
|
try {
|
|
68
115
|
void getTelemetry().emit(TelemetryEventKind.PROJECT_PROFILED, {
|
|
69
|
-
project: profileProject(cwd, now()),
|
|
116
|
+
project: profileProject(cwd, now(), installFacts(cwd, port)),
|
|
70
117
|
});
|
|
71
118
|
}
|
|
72
119
|
catch {
|
|
@@ -93,7 +140,7 @@ export function installDaemonTelemetry(cwd, now = () => Date.now()) {
|
|
|
93
140
|
return; // an idle window is not worth an event
|
|
94
141
|
// NOT daemon_stopped: the daemon is still running. See SESSION_PROGRESS.
|
|
95
142
|
void getTelemetry().emit(TelemetryEventKind.SESSION_PROGRESS, {
|
|
96
|
-
session: metrics.summarize(false),
|
|
143
|
+
session: withNudgeState(metrics.summarize(false)),
|
|
97
144
|
});
|
|
98
145
|
metrics.reset();
|
|
99
146
|
};
|
|
@@ -114,7 +161,7 @@ export function installDaemonTelemetry(cwd, now = () => Date.now()) {
|
|
|
114
161
|
// The rich one: the whole session in a single event — duration, the tool histogram, error
|
|
115
162
|
// shapes, verifications, browser launches. This is what replaced the per-tool-call event.
|
|
116
163
|
await getTelemetry().emit(TelemetryEventKind.DAEMON_STOPPED, {
|
|
117
|
-
session: metrics.summarize(true, exit),
|
|
164
|
+
session: withNudgeState(metrics.summarize(true, exit)),
|
|
118
165
|
});
|
|
119
166
|
})();
|
|
120
167
|
return stopped;
|
|
@@ -30,7 +30,7 @@ export const ErrorClass = {
|
|
|
30
30
|
const PATTERNS = [
|
|
31
31
|
[/confirmdangerous|destructive action blocked/i, ErrorClass.REFUSAL],
|
|
32
32
|
[
|
|
33
|
-
/\brefus(e|ed|es|ing)\b|not supported|unsupported|cannot .* a <|contenteditable/i,
|
|
33
|
+
/\brefus(e|ed|es|ing)\b|not supported|unsupported|cannot .* a <|contenteditable|cannot hover/i,
|
|
34
34
|
ErrorClass.REFUSAL,
|
|
35
35
|
],
|
|
36
36
|
[
|
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import { McpScope, type Feedback } from '@reticlehq/core';
|
|
1
|
+
import { McpScope, StackUnknownReason, type Feedback } from '@reticlehq/core';
|
|
2
2
|
/** The context fields — the whole `Feedback` shape minus what the author supplies. */
|
|
3
3
|
export type FeedbackContext = Pick<Feedback, 'stack' | 'stackMajor' | 'runtime' | 'engine' | 'driver' | 'client' | 'clientVersion' | 'mcpScope'>;
|
|
4
4
|
export declare function detectStack(cwd: string, read?: (path: string) => string): {
|
|
5
5
|
stack?: string;
|
|
6
6
|
stackMajor?: number;
|
|
7
7
|
stackSource?: 'cwd' | 'workspace';
|
|
8
|
+
stackUnknownReason?: StackUnknownReason;
|
|
8
9
|
};
|
|
9
10
|
/**
|
|
10
11
|
* User scope unless the project checks in its own MCP registration. `reticle init` deliberately
|
|
@@ -12,6 +13,13 @@ export declare function detectStack(cwd: string, read?: (path: string) => string
|
|
|
12
13
|
* itself the interesting signal.
|
|
13
14
|
*/
|
|
14
15
|
export declare function detectMcpScope(cwd: string, exists?: (path: string) => boolean): McpScope;
|
|
16
|
+
/**
|
|
17
|
+
* The MCP client on the other end, self-reported in its `initialize` handshake (`claude-code`,
|
|
18
|
+
* `cursor-vscode`, …). Read through a HOOK rather than sniffing env vars: the handshake is the
|
|
19
|
+
* client's own claim about itself, while an env-var table is a guess that silently rots every time a
|
|
20
|
+
* vendor renames a variable. Unset (a CLI run with no MCP peer) simply reports no client.
|
|
21
|
+
*/
|
|
22
|
+
/** Kept as the telemetry-facing name; the mechanism itself lives in mcp/client-identity.ts. */
|
|
15
23
|
export declare function setMcpClientNameHook(hook: () => {
|
|
16
24
|
name?: string;
|
|
17
25
|
version?: string;
|
|
@@ -9,11 +9,12 @@
|
|
|
9
9
|
* holds to. Detection is entirely best-effort: an unreadable package.json or an SDK too old to report
|
|
10
10
|
* its runtime yields `undefined`, never a throw and never a guess.
|
|
11
11
|
*/
|
|
12
|
+
import { mcpClientIdentity, setMcpClientIdentityHook } from '../mcp/client-identity.js';
|
|
12
13
|
import { existsSync, readFileSync, readdirSync } from 'node:fs';
|
|
13
14
|
import { isAbsolute, join } from 'node:path';
|
|
14
|
-
import { AppRuntime, McpScope, PageDriver } from '@reticlehq/core';
|
|
15
|
+
import { AppRuntime, McpScope, PageDriver, StackUnknownReason, } from '@reticlehq/core';
|
|
15
16
|
import { parseMajor } from '../init/detect.js';
|
|
16
|
-
import { findWorkspaceApps } from '../init/
|
|
17
|
+
import { findWorkspaceApps } from '../init/workspace-apps.js';
|
|
17
18
|
/**
|
|
18
19
|
* Dependency → reported stack, in resolution order. Meta-frameworks come FIRST: a Next app also
|
|
19
20
|
* depends on react, and reporting `react` for it would collapse the single most important segment
|
|
@@ -40,10 +41,14 @@ const PACKAGE_JSON = 'package.json';
|
|
|
40
41
|
/** A project-scoped MCP registration lives in one of these; `reticle init` writes user scope instead. */
|
|
41
42
|
const PROJECT_MCP_FILES = ['.mcp.json', join('.cursor', 'mcp.json'), join('.vscode', 'mcp.json')];
|
|
42
43
|
/**
|
|
43
|
-
* The project's framework and its MAJOR version
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
44
|
+
* The project's framework and its MAJOR version. The major comes from `parseMajor` — the same range
|
|
45
|
+
* parser `reticle init` uses to decide React 19 source mapping — so a range like `^15.0.0-canary.3`
|
|
46
|
+
* narrows the same way in both places instead of via a second, subtly different reader.
|
|
47
|
+
*
|
|
48
|
+
* `no-manifest` covers absent AND unreadable: both mean this directory could not answer, which is
|
|
49
|
+
* the distinction that matters to the caller. Unreadable is not separated because a manifest we
|
|
50
|
+
* cannot parse is not evidence about the app, and a reason nobody can act on is noise in a closed
|
|
51
|
+
* vocabulary.
|
|
47
52
|
*/
|
|
48
53
|
function stackOfManifest(dir, read) {
|
|
49
54
|
let pkg;
|
|
@@ -51,7 +56,7 @@ function stackOfManifest(dir, read) {
|
|
|
51
56
|
pkg = JSON.parse(read(join(dir, PACKAGE_JSON)));
|
|
52
57
|
}
|
|
53
58
|
catch {
|
|
54
|
-
return {};
|
|
59
|
+
return { kind: 'no-manifest' };
|
|
55
60
|
}
|
|
56
61
|
const deps = { ...pkg.devDependencies, ...pkg.dependencies };
|
|
57
62
|
for (const [dep, stack] of STACK_BY_DEP) {
|
|
@@ -59,15 +64,17 @@ function stackOfManifest(dir, read) {
|
|
|
59
64
|
if (range === undefined)
|
|
60
65
|
continue;
|
|
61
66
|
const stackMajor = parseMajor(range);
|
|
62
|
-
return { stack, ...(stackMajor !== undefined ? { stackMajor } : {}) };
|
|
67
|
+
return { kind: 'found', stack, ...(stackMajor !== undefined ? { stackMajor } : {}) };
|
|
63
68
|
}
|
|
64
|
-
return {};
|
|
69
|
+
return { kind: 'unrecognised', declaresWorkspaces: pkg.workspaces !== undefined };
|
|
65
70
|
}
|
|
66
71
|
export function detectStack(cwd, read = readFile) {
|
|
67
72
|
// The cwd wins when it IS an app — never redirect away from the real answer.
|
|
68
73
|
const here = stackOfManifest(cwd, read);
|
|
69
|
-
if (here.
|
|
70
|
-
|
|
74
|
+
if ('found' === here.kind) {
|
|
75
|
+
const { stack, stackMajor } = here;
|
|
76
|
+
return { stack, ...(stackMajor !== undefined ? { stackMajor } : {}), stackSource: 'cwd' };
|
|
77
|
+
}
|
|
71
78
|
// Otherwise look for the app. The daemon's cwd is wherever the agent's client happened to launch,
|
|
72
79
|
// which for a real repo is the ROOT while the app sits in `frontend/`, `web/`, or a declared
|
|
73
80
|
// workspace. Reading one directory and giving up is why this returned nothing for every project
|
|
@@ -78,19 +85,53 @@ export function detectStack(cwd, read = readFile) {
|
|
|
78
85
|
// declared workspaces (pnpm-workspace.yaml, package.json `workspaces`) AND scans top-level
|
|
79
86
|
// directories, and it was widened once already for a repo shape a user hit twice. A second
|
|
80
87
|
// detector here would drift from it the first time either changed.
|
|
88
|
+
//
|
|
89
|
+
// Every exit below carries WHY, because an empty result is not a cause. Four different facts used
|
|
90
|
+
// to arrive as one `{}`: no app anywhere, an app we read and did not recognise, workspace apps
|
|
91
|
+
// that were all unrecognised, and discovery erroring. They have different fixes, and the bucket
|
|
92
|
+
// was unusable while they were indistinguishable (#617).
|
|
93
|
+
let sawWorkspaceApp = false;
|
|
81
94
|
try {
|
|
82
95
|
for (const app of findWorkspaceApps(profileIo(cwd))) {
|
|
83
96
|
const found = stackOfManifest(join(cwd, app), read);
|
|
84
97
|
// Reported as `workspace` so the share of projects needing discovery is measurable — that
|
|
85
98
|
// number is what says whether our inference needs an agent to correct it.
|
|
86
|
-
if (found
|
|
87
|
-
|
|
99
|
+
if ('found' === found.kind) {
|
|
100
|
+
const { stack, stackMajor } = found;
|
|
101
|
+
return {
|
|
102
|
+
stack,
|
|
103
|
+
...(stackMajor !== undefined ? { stackMajor } : {}),
|
|
104
|
+
stackSource: 'workspace',
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
// A manifest we READ and did not recognise is evidence about the app; a directory with no
|
|
108
|
+
// manifest is not an app at all and says nothing either way.
|
|
109
|
+
if ('unrecognised' === found.kind)
|
|
110
|
+
sawWorkspaceApp = true;
|
|
88
111
|
}
|
|
89
112
|
}
|
|
90
113
|
catch {
|
|
91
114
|
// Discovery touches the filesystem; a permission error must never cost us the profile event.
|
|
115
|
+
// Named rather than folded into "found nothing": an error is not an absence, and a detector
|
|
116
|
+
// that reports one as the other sends the reader looking for a project that was there.
|
|
117
|
+
return { stackUnknownReason: StackUnknownReason.DISCOVERY_FAILED };
|
|
118
|
+
}
|
|
119
|
+
if (sawWorkspaceApp) {
|
|
120
|
+
return { stackUnknownReason: StackUnknownReason.WORKSPACE_APPS_UNRECOGNISED };
|
|
92
121
|
}
|
|
93
|
-
|
|
122
|
+
// The cwd's own manifest is the stronger statement when it exists: we read this app and did not
|
|
123
|
+
// know it. Ranked below the workspace answer only because discovery may have found the REAL app.
|
|
124
|
+
if ('unrecognised' === here.kind) {
|
|
125
|
+
// A monorepo root naming no framework is not an unrecognised APP — the app is a directory down
|
|
126
|
+
// and discovery did not surface it. Reporting MANIFEST_UNRECOGNISED here would read as "add
|
|
127
|
+
// this framework to the table" about a manifest that describes no framework at all.
|
|
128
|
+
return {
|
|
129
|
+
stackUnknownReason: here.declaresWorkspaces
|
|
130
|
+
? StackUnknownReason.WORKSPACE_ROOT_NO_APPS
|
|
131
|
+
: StackUnknownReason.MANIFEST_UNRECOGNISED,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
return { stackUnknownReason: StackUnknownReason.NO_APP_FOUND };
|
|
94
135
|
}
|
|
95
136
|
/**
|
|
96
137
|
* The three filesystem methods `findWorkspaceApps` needs, rooted at `cwd`.
|
|
@@ -150,26 +191,16 @@ function fileExists(path) {
|
|
|
150
191
|
* client's own claim about itself, while an env-var table is a guess that silently rots every time a
|
|
151
192
|
* vendor renames a variable. Unset (a CLI run with no MCP peer) simply reports no client.
|
|
152
193
|
*/
|
|
153
|
-
|
|
194
|
+
/** Kept as the telemetry-facing name; the mechanism itself lives in mcp/client-identity.ts. */
|
|
154
195
|
export function setMcpClientNameHook(hook) {
|
|
155
|
-
|
|
196
|
+
setMcpClientIdentityHook(hook);
|
|
156
197
|
}
|
|
157
|
-
/**
|
|
158
|
-
* The client's own name and version from its handshake.
|
|
159
|
-
*
|
|
160
|
-
* Note what is NOT here and cannot be: the MODEL. MCP's `clientInfo` carries a name, a title and a
|
|
161
|
-
* version and has no concept of a model, so the transport genuinely cannot tell us. The agent
|
|
162
|
-
* self-reports it on the feedback itself, which is the only mechanism available and a reliable one
|
|
163
|
-
* given the report is already something the agent authored.
|
|
164
|
-
*/
|
|
165
198
|
function detectClient() {
|
|
166
199
|
try {
|
|
167
|
-
const info =
|
|
168
|
-
const name = info?.name;
|
|
169
|
-
const version = info?.version;
|
|
200
|
+
const info = mcpClientIdentity();
|
|
170
201
|
return {
|
|
171
|
-
...(name
|
|
172
|
-
...(version
|
|
202
|
+
...(info.name === undefined ? {} : { client: info.name }),
|
|
203
|
+
...(info.version === undefined ? {} : { clientVersion: info.version }),
|
|
173
204
|
};
|
|
174
205
|
}
|
|
175
206
|
catch {
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { Feedback } from '@reticlehq/core';
|
|
2
|
+
export interface SavedFeedback {
|
|
3
|
+
path: string;
|
|
4
|
+
/** A ready-to-run command that turns the file into an issue. */
|
|
5
|
+
command: string;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Write the report under `<cwd>/.reticle/feedback/`. Returns undefined if it cannot be written —
|
|
9
|
+
* a feedback save that throws would take down the tool call that was reporting a problem, which is
|
|
10
|
+
* the one outcome worse than losing the report.
|
|
11
|
+
*/
|
|
12
|
+
export declare function saveFeedbackLocally(cwd: string, input: {
|
|
13
|
+
kind?: string;
|
|
14
|
+
source?: string;
|
|
15
|
+
}, text: string, context: Partial<Feedback>, now?: number): SavedFeedback | undefined;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where a feedback report goes when it cannot be sent.
|
|
3
|
+
*
|
|
4
|
+
* The report is fully written and redacted by the time delivery is refused. Throwing it away
|
|
5
|
+
* punishes exactly the behaviour the channel exists to encourage — and it hits hardest where the
|
|
6
|
+
* reports are best, because a Reticle source checkout disables telemetry by cwd, so the contributor
|
|
7
|
+
* who can name the file and the line is the one person who cannot file at all.
|
|
8
|
+
*
|
|
9
|
+
* Writing it down costs nothing and loses nothing. The receipt then carries a path and a command,
|
|
10
|
+
* so the human is handed one thing to run instead of being asked to retype what an agent already
|
|
11
|
+
* wrote.
|
|
12
|
+
*/
|
|
13
|
+
import { mkdirSync, writeFileSync } from 'node:fs';
|
|
14
|
+
import { join } from 'node:path';
|
|
15
|
+
import { ReticleDir } from '@reticlehq/core';
|
|
16
|
+
/** Reports live beside the rest of the workspace, not in a temp dir somebody has to be told about. */
|
|
17
|
+
const FEEDBACK_SUBDIR = 'feedback';
|
|
18
|
+
/** Filesystem-safe, sortable, and unique enough for one report per second per kind. */
|
|
19
|
+
const stamp = (now) => new Date(now).toISOString().replace(/[:.]/g, '-').replace('Z', '');
|
|
20
|
+
const asMarkdown = (input, text, context) => {
|
|
21
|
+
const title = `Reticle feedback: ${input.kind ?? 'note'}`;
|
|
22
|
+
return [
|
|
23
|
+
`# ${title}`,
|
|
24
|
+
'',
|
|
25
|
+
text.trim(),
|
|
26
|
+
'',
|
|
27
|
+
'## Context',
|
|
28
|
+
'',
|
|
29
|
+
'```json',
|
|
30
|
+
JSON.stringify(context, null, 2),
|
|
31
|
+
'```',
|
|
32
|
+
'',
|
|
33
|
+
].join('\n');
|
|
34
|
+
};
|
|
35
|
+
/**
|
|
36
|
+
* Write the report under `<cwd>/.reticle/feedback/`. Returns undefined if it cannot be written —
|
|
37
|
+
* a feedback save that throws would take down the tool call that was reporting a problem, which is
|
|
38
|
+
* the one outcome worse than losing the report.
|
|
39
|
+
*/
|
|
40
|
+
export function saveFeedbackLocally(cwd, input, text, context, now = Date.now()) {
|
|
41
|
+
try {
|
|
42
|
+
const dir = join(cwd, ReticleDir.ROOT, FEEDBACK_SUBDIR);
|
|
43
|
+
mkdirSync(dir, { recursive: true });
|
|
44
|
+
const path = join(dir, `${stamp(now)}-${input.kind ?? 'note'}.md`);
|
|
45
|
+
writeFileSync(path, asMarkdown(input, text, context), 'utf8');
|
|
46
|
+
return {
|
|
47
|
+
path,
|
|
48
|
+
// `--body-file` rather than an inlined body: the report contains newlines, quotes and JSON,
|
|
49
|
+
// and a command somebody has to repair before running is a command they do not run.
|
|
50
|
+
command: `gh issue create --repo reticlehq/reticle --title "Reticle feedback: ${input.kind ?? 'note'}" --body-file "${path}"`,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
return undefined;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -55,6 +55,17 @@ export interface FeedbackReceipt {
|
|
|
55
55
|
*/
|
|
56
56
|
accepted: boolean;
|
|
57
57
|
reason?: string;
|
|
58
|
+
/**
|
|
59
|
+
* Where the report was written when it could not be SENT.
|
|
60
|
+
*
|
|
61
|
+
* A refusal used to be the end of the road: the report was already written by then, and throwing
|
|
62
|
+
* it away punished exactly the behaviour this channel exists to encourage. Worse, the commonest
|
|
63
|
+
* refusal is a Reticle SOURCE CHECKOUT — so the reporter best placed to name a file and a line was
|
|
64
|
+
* the only one who could not file at all.
|
|
65
|
+
*/
|
|
66
|
+
savedTo?: string;
|
|
67
|
+
/** A ready-to-run command that turns the saved file into an issue. */
|
|
68
|
+
fileWith?: string;
|
|
58
69
|
/** Which redaction rules fired. Empty when nothing needed removing. */
|
|
59
70
|
redacted: string[];
|
|
60
71
|
/** The exact context that went with it, echoed back so the send is never a black box. */
|
|
@@ -26,6 +26,7 @@ import { platform } from 'node:os';
|
|
|
26
26
|
import { getTelemetry } from './telemetry.js';
|
|
27
27
|
import { noteFeedbackUndelivered } from './feedback-delivery.js';
|
|
28
28
|
import { isReticleSourceCheckout } from './dev-repo.js';
|
|
29
|
+
import { saveFeedbackLocally } from './feedback-local.js';
|
|
29
30
|
import { SERVER_VERSION } from '../version/server-version.js';
|
|
30
31
|
import { feedbackContext } from './feedback-context.js';
|
|
31
32
|
import { markDelivered, outboxPath, queueFeedback } from './feedback-outbox.js';
|
|
@@ -140,15 +141,27 @@ export async function submitFeedback(input, opts = {}) {
|
|
|
140
141
|
}
|
|
141
142
|
const telemetry = getTelemetry();
|
|
142
143
|
if (!telemetry.enabled) {
|
|
144
|
+
/*
|
|
145
|
+
* Cannot SEND is not the same as cannot FILE.
|
|
146
|
+
*
|
|
147
|
+
* The report is fully written and redacted by this point. Discarding it punishes the exact
|
|
148
|
+
* behaviour this channel exists to encourage, and it hits hardest where the reports are best: a
|
|
149
|
+
* Reticle source checkout disables telemetry by cwd, so a contributor who can name the file and
|
|
150
|
+
* the line was the one person who could not file at all.
|
|
151
|
+
*
|
|
152
|
+
* So it goes to disk and the receipt says where, plus a command that turns it into an issue.
|
|
153
|
+
* Nothing is lost, and the human is handed one thing to run rather than asked to retype it.
|
|
154
|
+
*/
|
|
155
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
156
|
+
const saved = saveFeedbackLocally(cwd, input, body.text, context);
|
|
143
157
|
return {
|
|
144
158
|
sent: false,
|
|
145
159
|
accepted: false,
|
|
146
|
-
// Named precisely, because the commonest cause is not what the old wording suggested
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
: `telemetry is disabled on this machine, so feedback has nowhere to go. Re-enable with \`reticle telemetry enable\`, or open an issue at https://github.com/reticlehq/reticle/issues.`,
|
|
160
|
+
// Named precisely, because the commonest cause is not what the old wording suggested.
|
|
161
|
+
reason: isReticleSourceCheckout(cwd)
|
|
162
|
+
? 'this is a Reticle source checkout, where telemetry is disabled by design, so this could not be sent — it has been written to disk instead, and nothing was lost.'
|
|
163
|
+
: `telemetry is disabled on this machine, so this could not be sent — it has been written to disk instead. Re-enable with \`reticle telemetry enable\` to send directly.`,
|
|
164
|
+
...(saved === undefined ? {} : { savedTo: saved.path, fileWith: saved.command }),
|
|
152
165
|
redacted,
|
|
153
166
|
context,
|
|
154
167
|
};
|
|
@@ -15,6 +15,8 @@ import { type InitOutcome } from '@reticlehq/core';
|
|
|
15
15
|
export declare const InitFailure: {
|
|
16
16
|
/** Run outside a project root. The single most common first-run mistake. */
|
|
17
17
|
readonly NO_PACKAGE_JSON: "no_package_json";
|
|
18
|
+
/** The manifest exists and is not valid JSON — a trailing comma, usually. Never a stack trace. */
|
|
19
|
+
readonly MALFORMED_PACKAGE_JSON: "malformed_package_json";
|
|
18
20
|
/** The package manager failed: offline, a locked registry, a broken install. */
|
|
19
21
|
readonly DEPENDENCY_INSTALL: "dependency_install";
|
|
20
22
|
/** The `claude mcp add` step failed — the CLI is missing or refused. Reticle installs but is unreachable. */
|
|
@@ -17,6 +17,8 @@ import { resolveInstallSource } from './install-source.js';
|
|
|
17
17
|
export const InitFailure = {
|
|
18
18
|
/** Run outside a project root. The single most common first-run mistake. */
|
|
19
19
|
NO_PACKAGE_JSON: 'no_package_json',
|
|
20
|
+
/** The manifest exists and is not valid JSON — a trailing comma, usually. Never a stack trace. */
|
|
21
|
+
MALFORMED_PACKAGE_JSON: 'malformed_package_json',
|
|
20
22
|
/** The package manager failed: offline, a locked registry, a broken install. */
|
|
21
23
|
DEPENDENCY_INSTALL: 'dependency_install',
|
|
22
24
|
/** The `claude mcp add` step failed — the CLI is missing or refused. Reticle installs but is unreachable. */
|