@reticlehq/server 2.3.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} +105 -15
- package/dist/{bridge.test-harness.d.ts → bridge/bridge.test-harness.d.ts} +6 -11
- package/dist/{bridge.test-harness.js → bridge/bridge.test-harness.js} +21 -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/cli-parse.d.ts +143 -0
- package/dist/{cli-parse.js → cli/cli-parse.js} +146 -65
- package/dist/{cli-port.d.ts → cli/cli-port.d.ts} +15 -0
- package/dist/{cli-port.js → cli/cli-port.js} +29 -8
- 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 +182 -93
- 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.d.ts +0 -6
- package/dist/flows/decision.js +64 -8
- 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 +67 -13
- 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.d.ts +17 -0
- package/dist/init/capabilities.js +54 -0
- 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 +26 -0
- package/dist/init/detect.js +78 -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 +39 -0
- package/dist/init/next-patch.js +196 -0
- package/dist/init/node-io.js +48 -6
- package/dist/init/patch-kind.d.ts +20 -0
- package/dist/init/patch-kind.js +10 -0
- package/dist/init/plan-framework.d.ts +50 -0
- package/dist/init/plan-framework.js +341 -0
- package/dist/init/plan.d.ts +119 -6
- package/dist/init/plan.js +230 -131
- package/dist/init/project-id.d.ts +5 -9
- package/dist/init/project-id.js +10 -22
- package/dist/init/run.d.ts +22 -0
- package/dist/init/run.js +393 -28
- package/dist/init/slash-command.d.ts +27 -0
- package/dist/init/slash-command.js +61 -0
- package/dist/init/snippets.d.ts +41 -3
- package/dist/init/snippets.js +185 -10
- package/dist/init/vite-config.d.ts +4 -10
- package/dist/init/vite-config.js +15 -7
- package/dist/init/workspace-apps.d.ts +23 -0
- package/dist/init/workspace-apps.js +68 -0
- package/dist/input/driven-redaction.d.ts +15 -0
- package/dist/input/driven-redaction.js +76 -0
- package/dist/input/network-detail.d.ts +9 -3
- package/dist/input/network-detail.js +43 -23
- 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 +58 -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 +20 -2
- package/dist/session/session.js +76 -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 +85 -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/cli-parse.d.ts +0 -140
- package/dist/daemon-resilience.d.ts +0 -22
- package/dist/daemon-resilience.js +0 -85
- package/dist/mcp-proxy.d.ts +0 -35
- package/dist/mcp-proxy.js +0 -190
- package/dist/tools/profiles.d.ts +0 -37
- package/dist/tools/profiles.js +0 -139
- /package/dist/{pairing-token.d.ts → bridge/pairing-token.d.ts} +0 -0
- /package/dist/{token-auth.d.ts → bridge/token-auth.d.ts} +0 -0
- /package/dist/{token-auth.js → bridge/token-auth.js} +0 -0
- /package/dist/{cli-doctor.d.ts → cli/cli-doctor.d.ts} +0 -0
- /package/dist/{cli-verify.d.ts → cli/cli-verify.d.ts} +0 -0
- /package/dist/{cloud-cli.d.ts → cli/cloud-cli.d.ts} +0 -0
- /package/dist/{server-version.d.ts → version/server-version.d.ts} +0 -0
package/dist/init/run.js
CHANGED
|
@@ -4,11 +4,51 @@
|
|
|
4
4
|
* access goes through `InitIo` so the orchestration is unit-testable with an in-memory IO.
|
|
5
5
|
*/
|
|
6
6
|
import { dirname, join } from 'node:path';
|
|
7
|
+
import { spanSync } from '../trace.js';
|
|
8
|
+
import { readFileSync } from 'node:fs';
|
|
9
|
+
import { homedir } from 'node:os';
|
|
7
10
|
import { detect, Framework } from './detect.js';
|
|
11
|
+
import { wasMcpRegistered } from './mcp-registered.js';
|
|
12
|
+
import { pickAstroHost } from './astro-host.js';
|
|
13
|
+
import { workspaceParents } from './workspace-apps.js';
|
|
14
|
+
import { chooseWorkspaceApp } from './app-choice.js';
|
|
15
|
+
import { isConnectStep } from './plan.js';
|
|
16
|
+
import { CURSOR_RULE_PATH } from './agent-rules.js';
|
|
17
|
+
import { CRA_ENV_PATH } from './cra.js';
|
|
18
|
+
/** CRA's bundled entry, in the order create-react-app itself generates them. */
|
|
19
|
+
const CRA_ENTRY_CANDIDATES = ['src/index.tsx', 'src/index.jsx', 'src/index.ts', 'src/index.js'];
|
|
20
|
+
function craEntryOf(io) {
|
|
21
|
+
for (const path of CRA_ENTRY_CANDIDATES) {
|
|
22
|
+
const source = io.readFile(path);
|
|
23
|
+
if (source !== null)
|
|
24
|
+
return { path, source };
|
|
25
|
+
}
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* The daemon's pairing token, for the one stack that cannot read it at build time.
|
|
30
|
+
*
|
|
31
|
+
* Every other framework's snippet reads this file in Node when the dev server starts, so the token
|
|
32
|
+
* is never written into the project. CRA bundles browser code and inlines only REACT_APP_*, so it
|
|
33
|
+
* has to be materialised into .env.development.local — which CRA's own template gitignores.
|
|
34
|
+
*/
|
|
35
|
+
function readPairingToken() {
|
|
36
|
+
try {
|
|
37
|
+
return readFileSync(join(homedir(), '.reticle', 'pairing-token'), 'utf8').trim();
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
return '';
|
|
41
|
+
}
|
|
42
|
+
}
|
|
8
43
|
import { DEPS_TARGET, MCP_TARGET, buildPlan, StepStatus, } from './plan.js';
|
|
9
44
|
import { claudeAvailableProbe, claudeExistsProbe } from './mcp.js';
|
|
45
|
+
import { reticleDevLocation } from './next-patch.js';
|
|
46
|
+
import { scanTestids, storeHints } from './capabilities.js';
|
|
10
47
|
import { CURSOR_DIR_RELPATH, CURSOR_MCP_RELPATH } from './cursor.js';
|
|
11
48
|
import { deriveProjectId, packageName } from './project-id.js';
|
|
49
|
+
import { VITE_DEV_MODULE_PATH } from './snippets.js';
|
|
50
|
+
import { CLAUDE_COMMAND_PATH, CURSOR_COMMAND_PATH } from './slash-command.js';
|
|
51
|
+
import { SERVER_VERSION } from '../version/server-version.js';
|
|
12
52
|
import { InitFailure, reportInitOutcome } from '../telemetry/init-telemetry.js';
|
|
13
53
|
/** Lockfile basenames, in package-manager preference order (mirrors detect.ts). */
|
|
14
54
|
const LOCKFILE_NAMES = [
|
|
@@ -43,25 +83,108 @@ export function resolveLockfiles(rootFiles, cwd, io) {
|
|
|
43
83
|
return set;
|
|
44
84
|
}
|
|
45
85
|
const PACKAGE_JSON = 'package.json';
|
|
46
|
-
|
|
86
|
+
/**
|
|
87
|
+
* Root-layout candidates, App Router only. `--src-dir` apps keep theirs under `src/app`, and the
|
|
88
|
+
* ReticleDev component has to land NEXT TO the layout or the relative import it generates is dead.
|
|
89
|
+
*/
|
|
90
|
+
const NEXT_LAYOUT_CANDIDATES = [
|
|
91
|
+
'app/layout.tsx',
|
|
92
|
+
'app/layout.jsx',
|
|
93
|
+
'app/layout.js',
|
|
94
|
+
'src/app/layout.tsx',
|
|
95
|
+
'src/app/layout.jsx',
|
|
96
|
+
'src/app/layout.js',
|
|
97
|
+
];
|
|
98
|
+
/**
|
|
99
|
+
* Pages Router mount points, checked only when there is no App Router layout. A Pages app has no
|
|
100
|
+
* `app/` directory at all, so writing the component there produced a file nothing imported.
|
|
101
|
+
*/
|
|
102
|
+
const NEXT_PAGES_APP_CANDIDATES = [
|
|
103
|
+
'pages/_app.tsx',
|
|
104
|
+
'pages/_app.jsx',
|
|
105
|
+
'pages/_app.js',
|
|
106
|
+
'src/pages/_app.tsx',
|
|
107
|
+
'src/pages/_app.jsx',
|
|
108
|
+
'src/pages/_app.js',
|
|
109
|
+
];
|
|
47
110
|
const SVELTEKIT_HOOKS = 'src/hooks.client.ts';
|
|
111
|
+
/** Where an app's components live. Bounded on purpose — this is a hint, not an index of the repo. */
|
|
112
|
+
const SOURCE_DIRS = ['src', 'src/components', 'src/pages', 'app', 'components'];
|
|
113
|
+
const SOURCE_FILE = /\.(tsx|jsx|ts|js|svelte|vue|astro)$/;
|
|
114
|
+
/** Files read for the testid scan. A capabilities block is a hint; reading a whole repo for it is not. */
|
|
115
|
+
const MAX_SCANNED_FILES = 200;
|
|
116
|
+
/** Read a bounded set of the app's source files, for the `data-testid` scan. */
|
|
117
|
+
function readSourceFiles(io) {
|
|
118
|
+
const out = [];
|
|
119
|
+
for (const dir of SOURCE_DIRS) {
|
|
120
|
+
for (const name of io.listFiles(dir)) {
|
|
121
|
+
if (!SOURCE_FILE.test(name))
|
|
122
|
+
continue;
|
|
123
|
+
const content = io.readFile(`${dir}/${name}`);
|
|
124
|
+
if (content !== null)
|
|
125
|
+
out.push(content);
|
|
126
|
+
if (out.length >= MAX_SCANNED_FILES)
|
|
127
|
+
return out;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return out;
|
|
131
|
+
}
|
|
132
|
+
/** Direct dependency names, for naming the state libraries an app actually has. */
|
|
133
|
+
function dependencyNames(pkg) {
|
|
134
|
+
const p = (pkg ?? {});
|
|
135
|
+
return new Set([
|
|
136
|
+
...Object.keys(p['dependencies'] ?? {}),
|
|
137
|
+
...Object.keys(p['devDependencies'] ?? {}),
|
|
138
|
+
]);
|
|
139
|
+
}
|
|
48
140
|
const VITE_CONFIG_CANDIDATES = [
|
|
49
141
|
'vite.config.ts',
|
|
50
142
|
'vite.config.js',
|
|
51
143
|
'vite.config.mjs',
|
|
52
144
|
'vite.config.mts',
|
|
53
145
|
];
|
|
146
|
+
const ASTRO_CONFIG_CANDIDATES = [
|
|
147
|
+
'astro.config.mjs',
|
|
148
|
+
'astro.config.js',
|
|
149
|
+
'astro.config.ts',
|
|
150
|
+
'astro.config.cjs',
|
|
151
|
+
];
|
|
152
|
+
/** Where a conventional Astro app keeps the layout every page wraps itself in. */
|
|
153
|
+
const ASTRO_LAYOUTS_DIR = 'src/layouts';
|
|
154
|
+
/** Also searched: an app with no layouts directory renders the document straight from a page. */
|
|
155
|
+
const ASTRO_PAGES_DIR = 'src/pages';
|
|
54
156
|
const NEXT_CONFIG_CANDIDATES = [
|
|
55
157
|
'next.config.mjs',
|
|
56
158
|
'next.config.js',
|
|
57
159
|
'next.config.ts',
|
|
58
160
|
'next.config.cjs',
|
|
59
161
|
];
|
|
162
|
+
/**
|
|
163
|
+
* `⚠` means WORK LEFT TO DO and nothing else — it is what an agent (and the release gate) counts to
|
|
164
|
+
* decide whether the install finished. A notice gets its own mark so "steps remaining" can reach zero
|
|
165
|
+
* on a working install that happens to be on an ungated stack.
|
|
166
|
+
*/
|
|
167
|
+
/**
|
|
168
|
+
* A step's status AFTER the run, which is what actually happened to it.
|
|
169
|
+
*
|
|
170
|
+
* `report` applies the same downgrade when printing: a step that failed or was skipped is shown as
|
|
171
|
+
* MANUAL whatever it planned to be. Reading the planned status alone would say a step applied when
|
|
172
|
+
* the run had already given up on it.
|
|
173
|
+
*/
|
|
174
|
+
function resolvedStatus(plan, target, failed, skipped) {
|
|
175
|
+
const step = plan.steps.find((s) => s.target === target);
|
|
176
|
+
if (step === undefined)
|
|
177
|
+
return undefined;
|
|
178
|
+
if (failed.has(target) || skipped.has(target))
|
|
179
|
+
return StepStatus.MANUAL;
|
|
180
|
+
return step.status;
|
|
181
|
+
}
|
|
60
182
|
const STATUS_SYMBOL = {
|
|
61
183
|
[StepStatus.APPLY]: '✓',
|
|
62
184
|
[StepStatus.MANUAL]: '⚠',
|
|
63
185
|
[StepStatus.ALREADY]: '·',
|
|
64
186
|
[StepStatus.SKIP]: '–',
|
|
187
|
+
[StepStatus.NOTICE]: 'ℹ',
|
|
65
188
|
};
|
|
66
189
|
function firstPresent(files, candidates) {
|
|
67
190
|
for (const c of candidates)
|
|
@@ -75,14 +198,16 @@ function gatherPlanInput(options, io, pkgRaw) {
|
|
|
75
198
|
const projectId = deriveProjectId(packageName(pkg), options.cwd);
|
|
76
199
|
const rootFiles = new Set(io.rootFiles());
|
|
77
200
|
const detectInput = {
|
|
78
|
-
pkg: typeof pkg
|
|
201
|
+
pkg: 'object' === typeof pkg && pkg !== null ? pkg : {},
|
|
79
202
|
configFiles: rootFiles,
|
|
80
203
|
// Walk up for the lockfile so a monorepo sub-package picks the workspace's package manager.
|
|
81
204
|
lockfiles: resolveLockfiles(rootFiles, options.cwd, io),
|
|
205
|
+
// An already-installed tree names its own manager, which matters when no lockfile is committed.
|
|
206
|
+
nodeModulesMarkers: new Set(io.listFiles('node_modules')),
|
|
82
207
|
};
|
|
83
208
|
const detection = detect(detectInput);
|
|
84
209
|
const vitePath = firstPresent(rootFiles, VITE_CONFIG_CANDIDATES);
|
|
85
|
-
const viteSource =
|
|
210
|
+
const viteSource = null === vitePath ? null : io.readFile(vitePath);
|
|
86
211
|
const viteConfig = vitePath !== null && viteSource !== null ? { path: vitePath, source: viteSource } : null;
|
|
87
212
|
// Global MCP registration targets each agent that's present: Claude via its CLI, Cursor via its
|
|
88
213
|
// global config file. Only probe when the MCP step is in play.
|
|
@@ -90,55 +215,195 @@ function gatherPlanInput(options, io, pkgRaw) {
|
|
|
90
215
|
const claudeCli = options.mcp ? io.probe(availableProbe.command, availableProbe.args) : false;
|
|
91
216
|
const existsProbe = claudeExistsProbe();
|
|
92
217
|
const mcpExists = claudeCli ? io.probe(existsProbe.command, existsProbe.args) : false;
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
const
|
|
218
|
+
// join(), not a literal '/': this path is checked and WRITTEN on the user's machine, and a
|
|
219
|
+
// backslash platform silently misses the config it is meant to find.
|
|
220
|
+
const cursorDir = join(io.homeDir(), CURSOR_DIR_RELPATH);
|
|
221
|
+
const cursorConfigPath = join(io.homeDir(), CURSOR_MCP_RELPATH);
|
|
222
|
+
// Cursor is "present" if its global config directory exists OR the project carries a `.cursor/`.
|
|
223
|
+
// The directory alone missed a real case: Cursor installed with a FRESH profile has not written
|
|
224
|
+
// ~/.cursor yet, so a user who had it open got no Cursor step and no message about one. A
|
|
225
|
+
// project-level .cursor/ is unambiguous evidence somebody uses it here.
|
|
226
|
+
const cursorPresent = options.mcp && (io.exists(cursorDir) || io.exists(CURSOR_DIR_RELPATH));
|
|
96
227
|
const cursorConfig = cursorPresent ? io.readFile(cursorConfigPath) : null;
|
|
228
|
+
const astroPath = firstPresent(rootFiles, ASTRO_CONFIG_CANDIDATES);
|
|
229
|
+
const astroSource = null === astroPath ? null : io.readFile(astroPath);
|
|
230
|
+
// Which file owns the DOCUMENT, not how many files sit in a directory. The old rule ("exactly one
|
|
231
|
+
// .astro in src/layouts") fired on neither real Astro app: one has no layouts directory and
|
|
232
|
+
// renders from src/pages/index.astro, the other has three files there of which two are partials.
|
|
233
|
+
// `</body>` is the discriminator — see astro-host.
|
|
234
|
+
const astroCandidates = [ASTRO_LAYOUTS_DIR, ASTRO_PAGES_DIR].flatMap((dir) => io
|
|
235
|
+
.listFiles(dir)
|
|
236
|
+
.filter((f) => f.endsWith('.astro'))
|
|
237
|
+
.map((f) => ({ path: `${dir}/${f}`, source: io.readFile(`${dir}/${f}`) }))
|
|
238
|
+
.filter((c) => c.source !== null));
|
|
239
|
+
const astroHost = pickAstroHost(astroCandidates);
|
|
240
|
+
const layoutRelPath = astroHost?.path ?? null;
|
|
241
|
+
const astroLayoutSource = astroHost?.source ?? null;
|
|
242
|
+
const nextConfigFile = firstPresent(rootFiles, NEXT_CONFIG_CANDIDATES);
|
|
243
|
+
// App Router first; a Pages Router app has no layout, and its mount point is pages/_app.
|
|
244
|
+
const layoutPath = NEXT_LAYOUT_CANDIDATES.find((p) => io.exists(p)) ??
|
|
245
|
+
NEXT_PAGES_APP_CANDIDATES.find((p) => io.exists(p)) ??
|
|
246
|
+
null;
|
|
247
|
+
const layoutSource = null === layoutPath ? null : io.readFile(layoutPath);
|
|
248
|
+
// Where the component goes depends on WHICH router mounts it: `pages/` routes on presence, so a
|
|
249
|
+
// component there becomes a broken route; `app/` routes on filename, so a sibling is inert.
|
|
250
|
+
const devLocation = reticleDevLocation(layoutPath ?? 'app/layout.tsx', detection.typescript);
|
|
97
251
|
return {
|
|
98
252
|
detection,
|
|
99
253
|
claudeCli,
|
|
100
254
|
mcpExists,
|
|
101
255
|
cursorPresent,
|
|
256
|
+
cursorProjectPresent: io.exists(CURSOR_DIR_RELPATH),
|
|
102
257
|
cursorConfig,
|
|
103
258
|
cursorConfigPath,
|
|
104
259
|
viteConfig,
|
|
105
|
-
|
|
106
|
-
|
|
260
|
+
astroConfig: astroPath !== null && astroSource !== null ? { path: astroPath, source: astroSource } : null,
|
|
261
|
+
astroLayout: layoutRelPath !== null && astroLayoutSource !== null
|
|
262
|
+
? { path: layoutRelPath, source: astroLayoutSource }
|
|
263
|
+
: null,
|
|
264
|
+
nextConfigFile,
|
|
265
|
+
nextConfigSource: null === nextConfigFile ? null : io.readFile(nextConfigFile),
|
|
266
|
+
nextLayout: layoutPath !== null && layoutSource !== null
|
|
267
|
+
? { path: layoutPath, source: layoutSource }
|
|
268
|
+
: null,
|
|
269
|
+
// Capabilities: scanned, never asked for. Bounded — a hint for the agent, not a repo index.
|
|
270
|
+
testids: scanTestids(readSourceFiles(io)),
|
|
271
|
+
storeHints: storeHints(dependencyNames(pkg)),
|
|
272
|
+
viteDevModuleExists: io.exists(VITE_DEV_MODULE_PATH),
|
|
273
|
+
nextReticleDevPath: devLocation.path,
|
|
274
|
+
nextReticleDevImport: devLocation.importSpecifier,
|
|
275
|
+
nextReticleDevExists: io.exists(devLocation.path),
|
|
107
276
|
svelteKitHooksExists: io.exists(SVELTEKIT_HOOKS),
|
|
277
|
+
craEntry: craEntryOf(io),
|
|
278
|
+
craEnv: io.readFile(CRA_ENV_PATH),
|
|
279
|
+
pairingToken: readPairingToken(),
|
|
108
280
|
reticleConfigExists: io.exists('.reticle.json'),
|
|
109
281
|
// Read the agent instruction files so the rule merge stays idempotent across re-runs.
|
|
110
282
|
claudeMdContent: io.readFile('CLAUDE.md'),
|
|
111
283
|
agentsMdContent: io.readFile('AGENTS.md'),
|
|
112
|
-
|
|
113
|
-
|
|
284
|
+
cursorRuleContent: io.readFile(CURSOR_RULE_PATH),
|
|
285
|
+
claudeCommandContent: io.readFile(CLAUDE_COMMAND_PATH),
|
|
286
|
+
cursorCommandContent: io.readFile(CURSOR_COMMAND_PATH),
|
|
287
|
+
options: {
|
|
288
|
+
port: options.port,
|
|
289
|
+
mcp: options.mcp,
|
|
290
|
+
install: options.install,
|
|
291
|
+
projectId,
|
|
292
|
+
// The SDK must match the CLI asking for it — see pinnedPackages.
|
|
293
|
+
sdkVersion: SERVER_VERSION,
|
|
294
|
+
},
|
|
114
295
|
};
|
|
115
296
|
}
|
|
297
|
+
/** Where workspace tooling conventionally puts packages. */
|
|
298
|
+
/** pnpm's workspace declaration, read when present — it is authoritative about where packages live. */
|
|
299
|
+
const PNPM_WORKSPACE = 'pnpm-workspace.yaml';
|
|
300
|
+
/** Deps that mark a directory as a runnable web app even when it has no bundler config file. */
|
|
301
|
+
const APP_DEPS = ['next', 'vite'];
|
|
302
|
+
function looksLikeApp(dir, io) {
|
|
303
|
+
const pkgRaw = io.readFile(`${dir}/${PACKAGE_JSON}`);
|
|
304
|
+
if (null === pkgRaw)
|
|
305
|
+
return false;
|
|
306
|
+
const configs = [...VITE_CONFIG_CANDIDATES, ...NEXT_CONFIG_CANDIDATES];
|
|
307
|
+
if (configs.some((c) => io.exists(`${dir}/${c}`)))
|
|
308
|
+
return true;
|
|
309
|
+
// `next.config` is optional in Next, so the dependency list is the other half of the signal.
|
|
310
|
+
return APP_DEPS.some((d) => pkgRaw.includes(`"${d}"`));
|
|
311
|
+
}
|
|
312
|
+
/**
|
|
313
|
+
* App directories under a workspace root.
|
|
314
|
+
*
|
|
315
|
+
* Running `reticle init` at the repo root is what people actually do, and in a monorepo the app is a
|
|
316
|
+
* directory down — so init detected "no framework", printed a wall of manual HTML instructions, and
|
|
317
|
+
* would have installed the SDK into the ROOT package.json. It already walks UP for the lockfile, so
|
|
318
|
+
* it knows it is in a workspace; this is the matching walk DOWN.
|
|
319
|
+
*/
|
|
320
|
+
export function findWorkspaceApps(io) {
|
|
321
|
+
const found = [];
|
|
322
|
+
// A workspace DECLARES its packages; `['apps','packages']` was a guess that missed a real repo
|
|
323
|
+
// with three Next apps at web/, admin/ and space/ — and missing them meant init ran against the
|
|
324
|
+
// root and reported ✓ for a file Next never compiles. See workspace-apps.
|
|
325
|
+
const pkgRaw = io.readFile(PACKAGE_JSON);
|
|
326
|
+
let pkgWorkspaces;
|
|
327
|
+
try {
|
|
328
|
+
const parsed = null === pkgRaw ? undefined : JSON.parse(pkgRaw);
|
|
329
|
+
pkgWorkspaces =
|
|
330
|
+
'object' === typeof parsed && parsed !== null
|
|
331
|
+
? parsed.workspaces
|
|
332
|
+
: undefined;
|
|
333
|
+
}
|
|
334
|
+
catch {
|
|
335
|
+
pkgWorkspaces = undefined;
|
|
336
|
+
}
|
|
337
|
+
const parents = workspaceParents({
|
|
338
|
+
...(null === io.readFile(PNPM_WORKSPACE)
|
|
339
|
+
? {}
|
|
340
|
+
: { pnpmWorkspace: io.readFile(PNPM_WORKSPACE) ?? '' }),
|
|
341
|
+
...(pkgWorkspaces === undefined ? {} : { pkgWorkspaces }),
|
|
342
|
+
topLevelDirs: io.listDirs('.'),
|
|
343
|
+
});
|
|
344
|
+
// A declared parent can itself BE the app (`workspaces: ["web"]`), so check both the directory and
|
|
345
|
+
// its children rather than assuming one level of nesting.
|
|
346
|
+
for (const parent of parents) {
|
|
347
|
+
if (looksLikeApp(parent, io))
|
|
348
|
+
found.push(parent);
|
|
349
|
+
for (const name of io.listDirs(parent)) {
|
|
350
|
+
const dir = `${parent}/${name}`;
|
|
351
|
+
if (looksLikeApp(dir, io))
|
|
352
|
+
found.push(dir);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
return [...new Set(found)];
|
|
356
|
+
}
|
|
116
357
|
function restartHint(framework) {
|
|
117
358
|
if (framework === Framework.NEXT)
|
|
118
359
|
return 'Restart `next dev`, then ask your agent: "List Reticle sessions".';
|
|
119
360
|
if (framework === Framework.VITE)
|
|
120
361
|
return 'Restart `vite`, then ask your agent: "List Reticle sessions".';
|
|
362
|
+
if (framework === Framework.ASTRO)
|
|
363
|
+
return 'Restart `astro dev`, then ask your agent: "List Reticle sessions".';
|
|
121
364
|
if (framework === Framework.SVELTEKIT)
|
|
122
365
|
return 'Restart your dev server (`npm run dev`), then ask your agent: "List Reticle sessions".';
|
|
123
366
|
return 'Reload your app on localhost, then ask your agent: "List Reticle sessions".';
|
|
124
367
|
}
|
|
125
|
-
|
|
368
|
+
const SKIPPED_DETAIL = 'skipped — the dependency install above failed, and wiring the app to a package that is not ' +
|
|
369
|
+
'installed stops it booting. Run that install, then re-run `reticle init`.';
|
|
370
|
+
function report(plan, dryRun, failed, skipped, degraded, io) {
|
|
126
371
|
io.print(dryRun ? 'reticle init (dry run — no files written)' : 'reticle init');
|
|
127
372
|
io.print('');
|
|
128
373
|
let applied = 0;
|
|
129
374
|
let manual = 0;
|
|
375
|
+
// A ⚠ on a CONNECT step is a guaranteed failure, not a caveat: nothing performs the manual step, so
|
|
376
|
+
// the app never dials the daemon and every tool answers "no browser session connected". `ok` was
|
|
377
|
+
// hardcoded true, so a run that could not possibly work reported success.
|
|
378
|
+
let connectPending = false;
|
|
130
379
|
for (const s of plan.steps) {
|
|
131
380
|
// A side effect that failed to apply is reported as a manual step with its fallback command.
|
|
132
|
-
const
|
|
381
|
+
const note = degraded.get(s.target);
|
|
382
|
+
if (note !== undefined) {
|
|
383
|
+
// Applied, but not the way it was asked for. A NOTICE, not work — the install did happen.
|
|
384
|
+
io.print(` [${STATUS_SYMBOL[StepStatus.NOTICE]}] ${s.title} → ${s.target}`);
|
|
385
|
+
for (const line of note.split('\n'))
|
|
386
|
+
io.print(` ${line}`);
|
|
387
|
+
applied++;
|
|
388
|
+
continue;
|
|
389
|
+
}
|
|
390
|
+
const downgraded = failed.has(s.target) || skipped.has(s.target);
|
|
133
391
|
const status = downgraded ? StepStatus.MANUAL : s.status;
|
|
134
|
-
const detail =
|
|
135
|
-
?
|
|
136
|
-
: s.
|
|
392
|
+
const detail = skipped.has(s.target)
|
|
393
|
+
? SKIPPED_DETAIL
|
|
394
|
+
: downgraded && s.exec !== undefined
|
|
395
|
+
? `step failed — run manually: ${s.exec.fallback}`
|
|
396
|
+
: s.detail;
|
|
137
397
|
io.print(` [${STATUS_SYMBOL[status]}] ${s.title} → ${s.target}`);
|
|
138
398
|
if (status === StepStatus.APPLY)
|
|
139
399
|
applied++;
|
|
140
|
-
if (status === StepStatus.MANUAL) {
|
|
141
|
-
manual
|
|
400
|
+
if (status === StepStatus.MANUAL || status === StepStatus.NOTICE) {
|
|
401
|
+
// A notice prints in full like a manual step — it is worth reading — but is NOT counted as work.
|
|
402
|
+
if (status === StepStatus.MANUAL) {
|
|
403
|
+
manual++;
|
|
404
|
+
if (isConnectStep(s.title))
|
|
405
|
+
connectPending = true;
|
|
406
|
+
}
|
|
142
407
|
for (const line of detail.split('\n'))
|
|
143
408
|
io.print(` ${line}`);
|
|
144
409
|
}
|
|
@@ -147,21 +412,68 @@ function report(plan, dryRun, failed, io) {
|
|
|
147
412
|
}
|
|
148
413
|
}
|
|
149
414
|
io.print('');
|
|
415
|
+
if (connectPending) {
|
|
416
|
+
io.print('This app will NOT connect until the ⚠ step above is done by hand — Reticle tools will report ' +
|
|
417
|
+
'"no browser session connected" until then. Everything else is already in place.');
|
|
418
|
+
io.print('');
|
|
419
|
+
}
|
|
150
420
|
io.print(restartHint(plan.framework));
|
|
151
|
-
return { ok:
|
|
421
|
+
return { ok: !connectPending, applied, manual };
|
|
152
422
|
}
|
|
153
|
-
/**
|
|
423
|
+
/**
|
|
424
|
+
* Perform the apply-step side effects; return the targets whose side effect failed.
|
|
425
|
+
*
|
|
426
|
+
* Steps run in plan order, which puts the dependency install BEFORE everything that imports what it
|
|
427
|
+
* installs. If it fails, the wiring is skipped rather than applied: patching `next.config.ts` to
|
|
428
|
+
* import a `@reticlehq/next` that was never installed takes the dev server down with
|
|
429
|
+
* MODULE_NOT_FOUND, so the app stops booting *because* Reticle was installed. A skipped step is a
|
|
430
|
+
* message; a half-wired app is a broken project.
|
|
431
|
+
*/
|
|
154
432
|
function applyEffects(plan, io) {
|
|
155
433
|
const failed = new Set();
|
|
434
|
+
const skipped = new Set();
|
|
435
|
+
const degraded = new Map();
|
|
436
|
+
let installFailed = false;
|
|
156
437
|
for (const s of plan.steps) {
|
|
157
438
|
if (s.status !== StepStatus.APPLY)
|
|
158
439
|
continue;
|
|
159
|
-
if (s.
|
|
160
|
-
|
|
161
|
-
|
|
440
|
+
if (installFailed && true === s.dependsOnInstall) {
|
|
441
|
+
skipped.add(s.target);
|
|
442
|
+
continue;
|
|
443
|
+
}
|
|
444
|
+
const write = s.write;
|
|
445
|
+
if (write !== undefined) {
|
|
446
|
+
spanSync('init.write', { target: s.target, path: write.path }, () => {
|
|
447
|
+
io.writeFile(write.path, write.content);
|
|
448
|
+
});
|
|
449
|
+
}
|
|
450
|
+
// Bound once so the traced call cannot need a `?? ''` fallback — a default there would turn a
|
|
451
|
+
// narrowing mistake into an empty command that silently "succeeds".
|
|
452
|
+
const exec = s.exec;
|
|
453
|
+
// Per-step, because the interesting part of init's wall-clock is WHICH step spent it: a
|
|
454
|
+
// package-manager install and a `claude mcp add` are both subprocesses, and one of them being
|
|
455
|
+
// slow is a completely different problem from the other.
|
|
456
|
+
if (exec !== undefined &&
|
|
457
|
+
!spanSync('init.exec', { target: s.target, command: exec.command }, () => io.exec(exec.command, exec.args))) {
|
|
458
|
+
// A weaker second attempt beats no install at all — but only when it is REPORTED, because the
|
|
459
|
+
// thing it gives up is the version pin that keeps SDK and daemon in step.
|
|
460
|
+
//
|
|
461
|
+
// Traced separately, and it is the reason init can take twice as long as it looks like it
|
|
462
|
+
// should: this is a SECOND full package-manager run, and until it had its own span 1.6 of
|
|
463
|
+
// init's 2.3 seconds simply vanished — the span above accounted for the first attempt and
|
|
464
|
+
// nothing accounted for this one.
|
|
465
|
+
const retry = s.retry;
|
|
466
|
+
if (retry !== undefined &&
|
|
467
|
+
spanSync('init.exec.retry', { target: s.target, command: retry.command }, () => io.exec(retry.command, retry.args))) {
|
|
468
|
+
degraded.set(s.target, retry.note);
|
|
469
|
+
continue;
|
|
470
|
+
}
|
|
162
471
|
failed.add(s.target);
|
|
472
|
+
if (s.target === DEPS_TARGET)
|
|
473
|
+
installFailed = true;
|
|
474
|
+
}
|
|
163
475
|
}
|
|
164
|
-
return failed;
|
|
476
|
+
return { failed, skipped, degraded };
|
|
165
477
|
}
|
|
166
478
|
/**
|
|
167
479
|
* Which STEP failed, from the step targets — not from an error string, which would carry paths.
|
|
@@ -178,25 +490,78 @@ function classifyInitFailure(failed) {
|
|
|
178
490
|
return InitFailure.MCP_REGISTRATION;
|
|
179
491
|
return InitFailure.OTHER;
|
|
180
492
|
}
|
|
493
|
+
const AMBIGUOUS_HEADER = 'Several apps found in this workspace. Re-run `reticle init` inside the one you want:';
|
|
494
|
+
/**
|
|
495
|
+
* When the current directory is a workspace root with no app of its own, wire the app instead of the
|
|
496
|
+
* root. One candidate is wired silently (there is nothing to ask about); several are listed, because
|
|
497
|
+
* guessing which app someone meant is worse than one line of output.
|
|
498
|
+
* Returns null when there is nothing to redirect to — the caller then proceeds here as before.
|
|
499
|
+
*/
|
|
500
|
+
function redirectToWorkspaceApp(options, io, pkgRaw) {
|
|
501
|
+
if (true === options.redirected)
|
|
502
|
+
return null;
|
|
503
|
+
const pkg = JSON.parse(pkgRaw);
|
|
504
|
+
const rootFiles = new Set(io.rootFiles());
|
|
505
|
+
const here = detect({
|
|
506
|
+
pkg: 'object' === typeof pkg && pkg !== null ? pkg : {},
|
|
507
|
+
configFiles: rootFiles,
|
|
508
|
+
lockfiles: new Set(),
|
|
509
|
+
});
|
|
510
|
+
if (here.framework !== Framework.HTML)
|
|
511
|
+
return null; // this directory IS the app
|
|
512
|
+
const apps = findWorkspaceApps(io);
|
|
513
|
+
// An explicitly named app answers the ambiguity. Refusing to guess is right, but "re-run inside the
|
|
514
|
+
// one you want" is not something a script, a CI step, or an agent that cannot change directory can
|
|
515
|
+
// act on — so the refusal was a dead end for exactly the callers most likely to hit it.
|
|
516
|
+
const chosen = chooseWorkspaceApp(options.app, apps);
|
|
517
|
+
if (!chosen.ok) {
|
|
518
|
+
io.print(chosen.message);
|
|
519
|
+
return { ok: false, applied: 0, manual: 1 };
|
|
520
|
+
}
|
|
521
|
+
const target = chosen.app ?? (1 === apps.length ? apps[0] : undefined);
|
|
522
|
+
if (target === undefined) {
|
|
523
|
+
if (0 === apps.length)
|
|
524
|
+
return null; // not a workspace — fall through to the normal HTML plan
|
|
525
|
+
io.print(AMBIGUOUS_HEADER);
|
|
526
|
+
for (const a of apps)
|
|
527
|
+
io.print(` ${a}`);
|
|
528
|
+
io.print('');
|
|
529
|
+
io.print(`Or name one without changing directory: reticle init --app ${apps[0] ?? '<dir>'}`);
|
|
530
|
+
return { ok: false, applied: 0, manual: apps.length };
|
|
531
|
+
}
|
|
532
|
+
io.print(`No app in this directory — wiring ${target} instead.`);
|
|
533
|
+
io.print('');
|
|
534
|
+
return runInit({ ...options, cwd: join(options.cwd, target), redirected: true }, io.scoped(target));
|
|
535
|
+
}
|
|
181
536
|
export function runInit(options, io) {
|
|
182
537
|
const pkgRaw = io.readFile(PACKAGE_JSON);
|
|
183
|
-
if (
|
|
538
|
+
if (null === pkgRaw) {
|
|
184
539
|
io.print('No package.json found. Run `reticle init` from your project root.');
|
|
185
540
|
// The onboarding funnel had NO instrumentation, so a setup that died here was indistinguishable
|
|
186
541
|
// from someone who never ran the command — the two failure modes with the most different fixes.
|
|
187
542
|
reportInitOutcome({ ok: false, reason: InitFailure.NO_PACKAGE_JSON });
|
|
188
543
|
return { ok: false, applied: 0, manual: 0 };
|
|
189
544
|
}
|
|
190
|
-
const
|
|
191
|
-
|
|
192
|
-
|
|
545
|
+
const redirected = redirectToWorkspaceApp(options, io, pkgRaw);
|
|
546
|
+
if (redirected !== null)
|
|
547
|
+
return redirected;
|
|
548
|
+
// Init is the flow a user experiences the wait of personally, and the fixture gate measures it at
|
|
549
|
+
// 1–6s per app with no explanation of the spread. These three spans split that number into detect
|
|
550
|
+
// (filesystem probing), plan (pure), and apply (writes + package-manager and CLI subprocesses).
|
|
551
|
+
const plan = spanSync('init.plan', {}, () => buildPlan(gatherPlanInput(options, io, pkgRaw)));
|
|
552
|
+
const effects = options.dryRun
|
|
553
|
+
? { failed: new Set(), skipped: new Set(), degraded: new Map() }
|
|
554
|
+
: spanSync('init.apply', { steps: plan.steps.length }, () => applyEffects(plan, io));
|
|
555
|
+
const { failed, skipped, degraded } = effects;
|
|
556
|
+
const result = report(plan, options.dryRun, failed, skipped, degraded, io);
|
|
193
557
|
// A dry run is a preview, not an outcome — reporting it would inflate both success and failure.
|
|
194
558
|
if (!options.dryRun) {
|
|
195
559
|
reportInitOutcome({
|
|
196
560
|
ok: result.ok,
|
|
197
561
|
...(result.ok ? {} : { reason: classifyInitFailure(failed) }),
|
|
198
562
|
stack: plan.framework,
|
|
199
|
-
|
|
563
|
+
// The step's REAL final status, not the absence of a failure — see mcp-registered.
|
|
564
|
+
mcpRegistered: wasMcpRegistered(resolvedStatus(plan, MCP_TARGET, failed, skipped)),
|
|
200
565
|
});
|
|
201
566
|
}
|
|
202
567
|
return result;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `/reticle` slash command — the entry point the docs have been promising and nothing created.
|
|
3
|
+
*
|
|
4
|
+
* SKILL.md tells the user "Type `/reticle` anytime to verify the app" in three places, and `init`
|
|
5
|
+
* never wrote the file that makes it exist. So the one obvious way in was a command that silently did
|
|
6
|
+
* nothing in every tool. Claude Code and Cursor both read custom commands from a markdown file per
|
|
7
|
+
* command; the body is the prompt the agent receives.
|
|
8
|
+
*
|
|
9
|
+
* Deliberately SCOPED to one flow. A user installing Reticle has an existing app with many flows, and
|
|
10
|
+
* an agent told to "verify the app" will try to instrument all of them — slow, noisy, and nothing to
|
|
11
|
+
* see for minutes. One flow, driven where the human can watch it, is the whole difference between a
|
|
12
|
+
* two-minute first impression and an afternoon.
|
|
13
|
+
*/
|
|
14
|
+
/** Claude Code reads project commands from `.claude/commands/<name>.md`. */
|
|
15
|
+
export declare const CLAUDE_COMMAND_PATH = ".claude/commands/reticle.md";
|
|
16
|
+
/** Cursor reads them from `.cursor/commands/<name>.md`. */
|
|
17
|
+
export declare const CURSOR_COMMAND_PATH = ".cursor/commands/reticle.md";
|
|
18
|
+
/**
|
|
19
|
+
* The frontmatter line that marks a command file as OURS.
|
|
20
|
+
*
|
|
21
|
+
* A stale command has to be refreshable — a file frozen at whatever release created it can never be
|
|
22
|
+
* improved for anyone who already ran init — but the path is one a human may have claimed for their
|
|
23
|
+
* own `/reticle`, and overwriting that would be destroying somebody's work. This line is how the two
|
|
24
|
+
* are told apart.
|
|
25
|
+
*/
|
|
26
|
+
export declare const SLASH_COMMAND_SIGNATURE = "description: Verify this app in the browser with Reticle";
|
|
27
|
+
export declare const SLASH_COMMAND_BODY = "---\ndescription: Verify this app in the browser with Reticle \u2014 drive one real flow and report what happened.\n---\n\nVerify this running app with Reticle. Drive it; do not read the code and guess.\n\n## Pick ONE flow\n\nNot the whole app. Pick the single most important flow you can complete in a handful of steps \u2014 the\none a user would do first (sign in, search, add to cart, submit the form). If the user named a flow or\njust changed some code, use that one instead.\n\nState which flow you picked in one line before you start.\n\n## Drive it where the human can see\n\n1. `reticle_sessions` \u2014 find the connected tab. None? Tell the user to run their dev server and open\n the app, then stop. Reticle never starts a dev server.\n2. `reticle_snapshot` \u2014 read the accessibility tree and locate the elements the flow needs. Elements\n are addressable by role and name; you do **not** need `data-testid` to drive them.\n3. Walk the flow with `reticle_act_and_wait`, one step at a time. Narrate each step first \u2014 the human\n is watching the page, and the HUD shows what you say.\n4. After each step, check the effect with `reticle_assert` \u2014 not just that the click dispatched, but\n that the thing it was supposed to do actually happened.\n5. Finish with `reticle_console` and `reticle_network` to catch errors the DOM does not show.\n\n## Report\n\n- What you drove, step by step, and what each step actually produced.\n- Anything broken, with the `file:line` Reticle gave you.\n- If a step could not be verified, say so plainly. \"Unknown\" is a real answer; a green verdict you\n cannot back is not.\n\nDo not weaken or skip an assertion to make the run pass \u2014 that is a finding, not a fix.\n";
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `/reticle` slash command — the entry point the docs have been promising and nothing created.
|
|
3
|
+
*
|
|
4
|
+
* SKILL.md tells the user "Type `/reticle` anytime to verify the app" in three places, and `init`
|
|
5
|
+
* never wrote the file that makes it exist. So the one obvious way in was a command that silently did
|
|
6
|
+
* nothing in every tool. Claude Code and Cursor both read custom commands from a markdown file per
|
|
7
|
+
* command; the body is the prompt the agent receives.
|
|
8
|
+
*
|
|
9
|
+
* Deliberately SCOPED to one flow. A user installing Reticle has an existing app with many flows, and
|
|
10
|
+
* an agent told to "verify the app" will try to instrument all of them — slow, noisy, and nothing to
|
|
11
|
+
* see for minutes. One flow, driven where the human can watch it, is the whole difference between a
|
|
12
|
+
* two-minute first impression and an afternoon.
|
|
13
|
+
*/
|
|
14
|
+
/** Claude Code reads project commands from `.claude/commands/<name>.md`. */
|
|
15
|
+
export const CLAUDE_COMMAND_PATH = '.claude/commands/reticle.md';
|
|
16
|
+
/** Cursor reads them from `.cursor/commands/<name>.md`. */
|
|
17
|
+
export const CURSOR_COMMAND_PATH = '.cursor/commands/reticle.md';
|
|
18
|
+
/**
|
|
19
|
+
* The frontmatter line that marks a command file as OURS.
|
|
20
|
+
*
|
|
21
|
+
* A stale command has to be refreshable — a file frozen at whatever release created it can never be
|
|
22
|
+
* improved for anyone who already ran init — but the path is one a human may have claimed for their
|
|
23
|
+
* own `/reticle`, and overwriting that would be destroying somebody's work. This line is how the two
|
|
24
|
+
* are told apart.
|
|
25
|
+
*/
|
|
26
|
+
export const SLASH_COMMAND_SIGNATURE = 'description: Verify this app in the browser with Reticle';
|
|
27
|
+
export const SLASH_COMMAND_BODY = `---
|
|
28
|
+
${SLASH_COMMAND_SIGNATURE} — drive one real flow and report what happened.
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
Verify this running app with Reticle. Drive it; do not read the code and guess.
|
|
32
|
+
|
|
33
|
+
## Pick ONE flow
|
|
34
|
+
|
|
35
|
+
Not the whole app. Pick the single most important flow you can complete in a handful of steps — the
|
|
36
|
+
one a user would do first (sign in, search, add to cart, submit the form). If the user named a flow or
|
|
37
|
+
just changed some code, use that one instead.
|
|
38
|
+
|
|
39
|
+
State which flow you picked in one line before you start.
|
|
40
|
+
|
|
41
|
+
## Drive it where the human can see
|
|
42
|
+
|
|
43
|
+
1. \`reticle_sessions\` — find the connected tab. None? Tell the user to run their dev server and open
|
|
44
|
+
the app, then stop. Reticle never starts a dev server.
|
|
45
|
+
2. \`reticle_snapshot\` — read the accessibility tree and locate the elements the flow needs. Elements
|
|
46
|
+
are addressable by role and name; you do **not** need \`data-testid\` to drive them.
|
|
47
|
+
3. Walk the flow with \`reticle_act_and_wait\`, one step at a time. Narrate each step first — the human
|
|
48
|
+
is watching the page, and the HUD shows what you say.
|
|
49
|
+
4. After each step, check the effect with \`reticle_assert\` — not just that the click dispatched, but
|
|
50
|
+
that the thing it was supposed to do actually happened.
|
|
51
|
+
5. Finish with \`reticle_console\` and \`reticle_network\` to catch errors the DOM does not show.
|
|
52
|
+
|
|
53
|
+
## Report
|
|
54
|
+
|
|
55
|
+
- What you drove, step by step, and what each step actually produced.
|
|
56
|
+
- Anything broken, with the \`file:line\` Reticle gave you.
|
|
57
|
+
- If a step could not be verified, say so plainly. "Unknown" is a real answer; a green verdict you
|
|
58
|
+
cannot back is not.
|
|
59
|
+
|
|
60
|
+
Do not weaken or skip an assertion to make the run pass — that is a finding, not a fix.
|
|
61
|
+
`;
|