@reticlehq/server 2.12.0 → 2.13.1
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 +68 -0
- package/dist/setup/dev-server-wait.js +67 -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 +203 -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,203 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The half of setup that happens after the files are written.
|
|
3
|
+
*
|
|
4
|
+
* `init` wires a project; this gets the app running with the SDK inside it, proves a session
|
|
5
|
+
* connected, and drives one flow to a verdict. Measured against an agent doing the same steps by
|
|
6
|
+
* hand from SKILL.md across five real applications: 176 model turns, $9.92, forty minutes, and a
|
|
7
|
+
* verdict in two runs out of five — three ended by asking a human to restart their client, having
|
|
8
|
+
* shown them nothing.
|
|
9
|
+
*
|
|
10
|
+
* Every effect is injected. That is not ceremony: the sequence has five phases, each with its own
|
|
11
|
+
* way of going wrong, and the alternative to injecting them is a test that boots a real dev server
|
|
12
|
+
* and a real browser to find out what happens when neither works.
|
|
13
|
+
*/
|
|
14
|
+
import { judgeWait, QUIET_MEANS_HUNG_MS, urlToWatch, WaitVerdict } from './dev-server-wait.js';
|
|
15
|
+
/**
|
|
16
|
+
* Windows gets longer to say something before silence counts as a wedge.
|
|
17
|
+
*
|
|
18
|
+
* A first `npm run dev` on a cold Windows runner optimises dependencies before the bundler prints a
|
|
19
|
+
* line, and 45s of quiet is inside that window. Measured on the install gate: the Vue scaffold was
|
|
20
|
+
* declared hung, `init` exited 1, and the gate then started the very same app and connected to it
|
|
21
|
+
* on the next line. Nothing was wrong with the app; the wait was too impatient for the platform.
|
|
22
|
+
*
|
|
23
|
+
* Only the QUIET budget moves. The ceiling is unchanged, and a launcher that exits is still dead
|
|
24
|
+
* immediately, so this buys patience for a starting server and never for a broken one.
|
|
25
|
+
*/
|
|
26
|
+
const WINDOWS_QUIET_MEANS_HUNG_MS = 3 * 60_000;
|
|
27
|
+
const WINDOWS_QUIET_MEANS_HUNG_MS_APPLIES = 'win32' === process.platform;
|
|
28
|
+
import { pickSession } from './session-pick.js';
|
|
29
|
+
import { readPage, describePage } from './page-probe.js';
|
|
30
|
+
import { remainingSteps } from './remaining-steps.js';
|
|
31
|
+
import { AppShape, isDesktop, policyFor } from './desktop-shape.js';
|
|
32
|
+
/** Where a run got to, and why it stopped if it did. */
|
|
33
|
+
export const SetupPhase = {
|
|
34
|
+
DEV_SERVER: 'dev-server',
|
|
35
|
+
CONNECT: 'connect',
|
|
36
|
+
DRIVE: 'drive',
|
|
37
|
+
DONE: 'done',
|
|
38
|
+
};
|
|
39
|
+
const asProgress = (input, o) => ({
|
|
40
|
+
initDone: true,
|
|
41
|
+
devServerUp: undefined !== o.url,
|
|
42
|
+
sessionConnected: undefined !== o.sessionId,
|
|
43
|
+
flowSaved: true === o.flowSaved,
|
|
44
|
+
urlSuppliedByCaller: undefined !== input.suppliedUrl,
|
|
45
|
+
...(undefined === o.url ? {} : { url: o.url }),
|
|
46
|
+
...(undefined === input.devCommand ? {} : { devCommand: input.devCommand }),
|
|
47
|
+
});
|
|
48
|
+
const stop = (input, phase, partial, notes) => ({
|
|
49
|
+
ok: false,
|
|
50
|
+
reachedPhase: phase,
|
|
51
|
+
flowSaved: false,
|
|
52
|
+
...partial,
|
|
53
|
+
notes,
|
|
54
|
+
fallback: remainingSteps(asProgress(input, partial)),
|
|
55
|
+
});
|
|
56
|
+
/**
|
|
57
|
+
* Run the phases. Stops at the first one that cannot continue, and always says what is left.
|
|
58
|
+
*
|
|
59
|
+
* A phase that fails is not an error to throw: the caller is usually an agent, and a thrown
|
|
60
|
+
* exception loses the four things it needs — how far this got, the url, the session, and what to do
|
|
61
|
+
* about it.
|
|
62
|
+
*/
|
|
63
|
+
export async function runSetupPhases(input, fx) {
|
|
64
|
+
const notes = [];
|
|
65
|
+
const note = (line) => {
|
|
66
|
+
notes.push(line);
|
|
67
|
+
fx.note(line);
|
|
68
|
+
};
|
|
69
|
+
// ── the app has to be running ────────────────────────────────────────────────────────────────
|
|
70
|
+
let url = input.suppliedUrl;
|
|
71
|
+
if (undefined === url) {
|
|
72
|
+
if (undefined === input.devCommand) {
|
|
73
|
+
note(
|
|
74
|
+
// "stop here rather than invent one" is the SKILL.md rule, and the words are the contract —
|
|
75
|
+
// inventing a dev command is how a setup script runs the wrong thing and reports success.
|
|
76
|
+
'No dev command: the project names no dev, start or serve script, and there is no --url. ' +
|
|
77
|
+
'Stopping here rather than invent one — pass --dev-cmd or --url to say what serves this app.');
|
|
78
|
+
return stop(input, SetupPhase.DEV_SERVER, {}, notes);
|
|
79
|
+
}
|
|
80
|
+
await fx.startDevServer(input.devCommand, input.appDir);
|
|
81
|
+
const startedAt = fx.now();
|
|
82
|
+
for (;;) {
|
|
83
|
+
const watching = urlToWatch(fx.devServerOutput(), fx.observedPorts());
|
|
84
|
+
const serving = undefined === watching ? false : (await fx.probePage(watching)).served;
|
|
85
|
+
const verdict = judgeWait({
|
|
86
|
+
output: fx.devServerOutput(),
|
|
87
|
+
launcherExited: fx.devServerExited(),
|
|
88
|
+
serving,
|
|
89
|
+
quietForMs: fx.devServerQuietForMs(),
|
|
90
|
+
elapsedMs: fx.now() - startedAt,
|
|
91
|
+
quietMeansHungMs: WINDOWS_QUIET_MEANS_HUNG_MS_APPLIES
|
|
92
|
+
? WINDOWS_QUIET_MEANS_HUNG_MS
|
|
93
|
+
: QUIET_MEANS_HUNG_MS,
|
|
94
|
+
});
|
|
95
|
+
if (WaitVerdict.READY === verdict && undefined !== watching) {
|
|
96
|
+
url = watching;
|
|
97
|
+
break;
|
|
98
|
+
}
|
|
99
|
+
// A desktop shell serves its webview from inside the app, so there is no port to answer and
|
|
100
|
+
// nothing to be READY. Once it has announced a url, or bound one we can see, that is as far
|
|
101
|
+
// as this phase can get: the session it dials from its own window is the real signal.
|
|
102
|
+
if (isDesktop(input.shape) && undefined !== watching) {
|
|
103
|
+
url = watching;
|
|
104
|
+
break;
|
|
105
|
+
}
|
|
106
|
+
if (WaitVerdict.DEAD === verdict) {
|
|
107
|
+
note('The dev server exited without serving anything.');
|
|
108
|
+
return stop(input, SetupPhase.DEV_SERVER, {}, notes);
|
|
109
|
+
}
|
|
110
|
+
if (WaitVerdict.HUNG === verdict) {
|
|
111
|
+
// Says BOTH were checked. A server that prints nothing but IS listening is the CRA case and
|
|
112
|
+
// must not be failed, so a reader has to be able to tell "we looked at the log" from "we
|
|
113
|
+
// looked at the log AND the ports".
|
|
114
|
+
note('The dev server neither printed a URL nor bound a port, so setup has nothing to open. ' +
|
|
115
|
+
'Check its log, or pass --url with the address it serves.');
|
|
116
|
+
return stop(input, SetupPhase.DEV_SERVER, {}, notes);
|
|
117
|
+
}
|
|
118
|
+
await fx.sleep(input.pollMs);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
// ── and something has to connect from inside it ──────────────────────────────────────────────
|
|
122
|
+
const policy = policyFor(input.shape);
|
|
123
|
+
// Through `note`, not `fx.note`: a caller reading the result should see it too.
|
|
124
|
+
if (undefined !== policy.note)
|
|
125
|
+
note(policy.note);
|
|
126
|
+
const before = new Set((await fx.listSessions()).map((s) => s.sessionId));
|
|
127
|
+
// Never for a desktop app: its own window is the client, and a browser tab would be a SECOND
|
|
128
|
+
// session that is not the app.
|
|
129
|
+
if (input.openBrowser && policy.openBrowser)
|
|
130
|
+
await fx.openBrowser(url);
|
|
131
|
+
const deadline = fx.now() + (input.connectBudgetMs ?? Math.max(input.phaseTimeoutMs, policy.connectBudgetMs));
|
|
132
|
+
let session = null;
|
|
133
|
+
for (;;) {
|
|
134
|
+
// On a desktop app, only the desktop window counts. AppShape and the runtime a page reports use
|
|
135
|
+
// the same three names, so the shape IS the requirement — see session-pick.
|
|
136
|
+
session = pickSession(await fx.listSessions(), url, before, isDesktop(input.shape) ? input.shape : undefined);
|
|
137
|
+
if (null !== session)
|
|
138
|
+
break;
|
|
139
|
+
if (deadline <= fx.now())
|
|
140
|
+
break;
|
|
141
|
+
await fx.sleep(input.pollMs);
|
|
142
|
+
}
|
|
143
|
+
if (null === session) {
|
|
144
|
+
if (isDesktop(input.shape)) {
|
|
145
|
+
// Nothing outside the app can fetch its webview, so there is no page to describe. What is
|
|
146
|
+
// wrong here is desktop-shaped: the preload, the capture helper, or a CSP that blocks the
|
|
147
|
+
// bridge — all of which `reticle doctor` checks.
|
|
148
|
+
note('The app never dialled in. For a desktop shell that is usually the preload not being required, ' +
|
|
149
|
+
'the capture helper not installed, or a CSP that blocks the bridge: run `npx @reticlehq/server doctor`.');
|
|
150
|
+
}
|
|
151
|
+
else {
|
|
152
|
+
// What the page contains is the one thing the daemon cannot know, so it is worth one fetch.
|
|
153
|
+
note(describePage(readPage(await fx.probePage(url)), url));
|
|
154
|
+
}
|
|
155
|
+
return stop(input, SetupPhase.CONNECT, { url }, notes);
|
|
156
|
+
}
|
|
157
|
+
// ── and it has to be driven, or nothing has been proved ──────────────────────────────────────
|
|
158
|
+
if (!input.drive) {
|
|
159
|
+
return {
|
|
160
|
+
ok: true,
|
|
161
|
+
reachedPhase: SetupPhase.CONNECT,
|
|
162
|
+
url,
|
|
163
|
+
sessionId: session.sessionId,
|
|
164
|
+
flowSaved: false,
|
|
165
|
+
notes,
|
|
166
|
+
fallback: [],
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
// Nothing to drive WITH is not the same as driving and proving nothing, and only the second is a
|
|
170
|
+
// failure of this command. The app is installed, instrumented, booted and connected; the one step
|
|
171
|
+
// left needs a tool this machine does not have, so it is reported and handed to the caller.
|
|
172
|
+
if (!fx.driverAvailable()) {
|
|
173
|
+
note('Installed and connected, but no agent CLI is on this machine (claude, codex, opencode, ' +
|
|
174
|
+
'cursor-agent or gemini), so nothing drove the app and no verdict was produced.');
|
|
175
|
+
return {
|
|
176
|
+
ok: true,
|
|
177
|
+
reachedPhase: SetupPhase.CONNECT,
|
|
178
|
+
url,
|
|
179
|
+
sessionId: session.sessionId,
|
|
180
|
+
flowSaved: false,
|
|
181
|
+
notes,
|
|
182
|
+
fallback: remainingSteps(asProgress(input, { url, sessionId: session.sessionId, flowSaved: false })),
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
const verdict = await fx.drive(url, session);
|
|
186
|
+
const flowSaved = fx.flowsSaved();
|
|
187
|
+
if (null === verdict) {
|
|
188
|
+
note('No agent CLI could drive the app, so nothing was proved.');
|
|
189
|
+
}
|
|
190
|
+
if (!flowSaved) {
|
|
191
|
+
return stop(input, SetupPhase.DRIVE, { url, sessionId: session.sessionId, ...(null === verdict ? {} : { verdict }) }, notes);
|
|
192
|
+
}
|
|
193
|
+
return {
|
|
194
|
+
ok: true,
|
|
195
|
+
reachedPhase: SetupPhase.DONE,
|
|
196
|
+
url,
|
|
197
|
+
sessionId: session.sessionId,
|
|
198
|
+
...(null === verdict ? {} : { verdict }),
|
|
199
|
+
flowSaved: true,
|
|
200
|
+
notes,
|
|
201
|
+
fallback: [],
|
|
202
|
+
};
|
|
203
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which connected session to drive, out of everything the daemon is holding.
|
|
3
|
+
*
|
|
4
|
+
* This is a false-green guard, which is why it is its own module. Two ways to get it wrong, and
|
|
5
|
+
* both report a successful install for an app that was never verified:
|
|
6
|
+
*
|
|
7
|
+
* - Match "any session" and you pass on somebody else's tab. A daemon usually holds several, and
|
|
8
|
+
* one of them being alive says nothing about whether THIS app connected.
|
|
9
|
+
* - Match "the first session on this url" and you drive whichever the daemon listed first, usually
|
|
10
|
+
* the oldest: a tab whose dev server died yesterday. The HUD then plays to a window nobody is
|
|
11
|
+
* watching, and the verdict describes a page the user cannot see.
|
|
12
|
+
*/
|
|
13
|
+
/** The fields of a daemon session this decision actually reads. */
|
|
14
|
+
export interface CandidateSession {
|
|
15
|
+
readonly sessionId: string;
|
|
16
|
+
readonly url?: string;
|
|
17
|
+
readonly hidden?: boolean;
|
|
18
|
+
readonly throttled?: boolean;
|
|
19
|
+
readonly lastSeenMs?: number;
|
|
20
|
+
/** False when the capabilities file init scaffolded was never completed. */
|
|
21
|
+
readonly hasCapabilities?: boolean;
|
|
22
|
+
/** Which shell answered. Absent on an SDK too old to report one — see requiredRuntime below. */
|
|
23
|
+
readonly runtime?: string;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* The session to drive, or null when nothing on this url qualifies.
|
|
27
|
+
*
|
|
28
|
+
* Preference order, strongest evidence first:
|
|
29
|
+
* 1. NEW since we opened the tab — definitely this run's, definitely alive
|
|
30
|
+
* 2. visible and not throttled — the tab a human is actually looking at
|
|
31
|
+
* 3. least stale — the best of a bad set
|
|
32
|
+
*
|
|
33
|
+
* `before` is the set of session ids that already existed when this run started; anything outside
|
|
34
|
+
* it belongs to us.
|
|
35
|
+
*/
|
|
36
|
+
export declare function pickSession(sessions: readonly CandidateSession[], url: string, before?: ReadonlySet<string>,
|
|
37
|
+
/**
|
|
38
|
+
* The runtime this project's app RUNS as, when it is a desktop one.
|
|
39
|
+
*
|
|
40
|
+
* A desktop shell serves its renderer from an ordinary dev server, so its window and a browser tab
|
|
41
|
+
* open on the same origin are indistinguishable by url. Driving the tab passes every check here —
|
|
42
|
+
* live, on the url, SDK present — while having none of the app's IPC and none of its commands, so
|
|
43
|
+
* the verdict describes a different program that happens to share an address.
|
|
44
|
+
*
|
|
45
|
+
* Undefined for a web project, where the runtime is not a distinction worth making.
|
|
46
|
+
*/
|
|
47
|
+
requiredRuntime?: string): CandidateSession | null;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which connected session to drive, out of everything the daemon is holding.
|
|
3
|
+
*
|
|
4
|
+
* This is a false-green guard, which is why it is its own module. Two ways to get it wrong, and
|
|
5
|
+
* both report a successful install for an app that was never verified:
|
|
6
|
+
*
|
|
7
|
+
* - Match "any session" and you pass on somebody else's tab. A daemon usually holds several, and
|
|
8
|
+
* one of them being alive says nothing about whether THIS app connected.
|
|
9
|
+
* - Match "the first session on this url" and you drive whichever the daemon listed first, usually
|
|
10
|
+
* the oldest: a tab whose dev server died yesterday. The HUD then plays to a window nobody is
|
|
11
|
+
* watching, and the verdict describes a page the user cannot see.
|
|
12
|
+
*/
|
|
13
|
+
/** Sorts a hidden tab after a visible one; among equals, the least stale first. */
|
|
14
|
+
const isLive = (s) => true !== s.hidden && true !== s.throttled;
|
|
15
|
+
const staleness = (s) => s.lastSeenMs ?? Number.POSITIVE_INFINITY;
|
|
16
|
+
/**
|
|
17
|
+
* The session to drive, or null when nothing on this url qualifies.
|
|
18
|
+
*
|
|
19
|
+
* Preference order, strongest evidence first:
|
|
20
|
+
* 1. NEW since we opened the tab — definitely this run's, definitely alive
|
|
21
|
+
* 2. visible and not throttled — the tab a human is actually looking at
|
|
22
|
+
* 3. least stale — the best of a bad set
|
|
23
|
+
*
|
|
24
|
+
* `before` is the set of session ids that already existed when this run started; anything outside
|
|
25
|
+
* it belongs to us.
|
|
26
|
+
*/
|
|
27
|
+
export function pickSession(sessions, url, before = new Set(),
|
|
28
|
+
/**
|
|
29
|
+
* The runtime this project's app RUNS as, when it is a desktop one.
|
|
30
|
+
*
|
|
31
|
+
* A desktop shell serves its renderer from an ordinary dev server, so its window and a browser tab
|
|
32
|
+
* open on the same origin are indistinguishable by url. Driving the tab passes every check here —
|
|
33
|
+
* live, on the url, SDK present — while having none of the app's IPC and none of its commands, so
|
|
34
|
+
* the verdict describes a different program that happens to share an address.
|
|
35
|
+
*
|
|
36
|
+
* Undefined for a web project, where the runtime is not a distinction worth making.
|
|
37
|
+
*/
|
|
38
|
+
requiredRuntime) {
|
|
39
|
+
const wanted = String(url).replace(/\/$/, '');
|
|
40
|
+
const onUrl = sessions
|
|
41
|
+
.filter((s) => (s?.url ?? '').startsWith(wanted))
|
|
42
|
+
// An SDK too old to report a runtime is ACCEPTED rather than excluded: re-running init is the
|
|
43
|
+
// only upgrade path an existing user has, and refusing them their own install to enforce a
|
|
44
|
+
// field their SDK cannot send would break the one route out.
|
|
45
|
+
.filter((s) => undefined === requiredRuntime || undefined === s.runtime || requiredRuntime === s.runtime);
|
|
46
|
+
if (0 === onUrl.length)
|
|
47
|
+
return null;
|
|
48
|
+
const fresh = onUrl.filter((s) => !before.has(s.sessionId));
|
|
49
|
+
const pool = 0 < fresh.length ? fresh : onUrl;
|
|
50
|
+
return pool.find(isLive) ?? [...pool].sort((a, b) => staleness(a) - staleness(b))[0] ?? null;
|
|
51
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The wire: everything init established, plus the world, plus the phases.
|
|
3
|
+
*
|
|
4
|
+
* Kept apart from cli.ts so the assembly is readable in one place and so `handleInit` stays the
|
|
5
|
+
* three lines it was. The only decision here is which effects to hand over; the sequencing lives in
|
|
6
|
+
* run-setup.ts and the pieces it calls.
|
|
7
|
+
*/
|
|
8
|
+
import { type SetupInput, type SetupOutcome } from './run-setup.js';
|
|
9
|
+
export interface SetupCommandInput extends Omit<SetupInput, 'shape'> {
|
|
10
|
+
/** Where setup was invoked, which is not the app directory in a monorepo. */
|
|
11
|
+
readonly invokedAt: string;
|
|
12
|
+
readonly bridgePort: number;
|
|
13
|
+
readonly env: Readonly<Record<string, string>>;
|
|
14
|
+
readonly flow?: string | undefined;
|
|
15
|
+
readonly driveBudgetUsd: number;
|
|
16
|
+
readonly driveModel?: string | undefined;
|
|
17
|
+
readonly escalateWeakFlow: boolean;
|
|
18
|
+
/** Register the MCP server with the coding agents init does not itself reach. */
|
|
19
|
+
readonly registerAgents: boolean;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Register with the agents `init` does not reach, and leave the skill where one loads skills from.
|
|
23
|
+
*
|
|
24
|
+
* init covers eight clients and only where it finds them installed, which leaves VS Code's USER
|
|
25
|
+
* scope unwritten — it exists on machines today, and init only ever writes the project-scope file,
|
|
26
|
+
* so a VS Code user has no tools outside the directory they ran init in.
|
|
27
|
+
*/
|
|
28
|
+
export declare function registerOtherAgents(print: (line: string) => void): void;
|
|
29
|
+
export interface SetupCommandResult extends SetupOutcome {
|
|
30
|
+
/** Set when a weak flow was re-recorded with the stronger model. */
|
|
31
|
+
readonly escalated?: {
|
|
32
|
+
readonly from: string;
|
|
33
|
+
readonly to: string;
|
|
34
|
+
} | undefined;
|
|
35
|
+
readonly driveTurns?: number | undefined;
|
|
36
|
+
readonly driveCostUsd?: number | undefined;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Run the phases against the real world.
|
|
40
|
+
*
|
|
41
|
+
* The dev server is stopped on every ending except success, where it is handed to the user: an
|
|
42
|
+
* instrumented app they can watch is the deliverable, and killing it would leave them with config
|
|
43
|
+
* files and a dead tab.
|
|
44
|
+
*/
|
|
45
|
+
export declare function runSetupCommand(input: SetupCommandInput, print: (line: string) => void): Promise<SetupCommandResult>;
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The wire: everything init established, plus the world, plus the phases.
|
|
3
|
+
*
|
|
4
|
+
* Kept apart from cli.ts so the assembly is readable in one place and so `handleInit` stays the
|
|
5
|
+
* three lines it was. The only decision here is which effects to hand over; the sequencing lives in
|
|
6
|
+
* run-setup.ts and the pieces it calls.
|
|
7
|
+
*/
|
|
8
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
9
|
+
import { homedir } from 'node:os';
|
|
10
|
+
import { join } from 'node:path';
|
|
11
|
+
import { planAgentConfigs } from './agent-configs.js';
|
|
12
|
+
import { AppShape, readShape } from './desktop-shape.js';
|
|
13
|
+
import { stopOnInterrupt } from './interrupt.js';
|
|
14
|
+
import { applyAgentPlan, applyAgentSkills } from './agent-writer.js';
|
|
15
|
+
import { ApprovalOutcome, grantAutoApproval } from './auto-approve.js';
|
|
16
|
+
import { agentIo } from './agent-io.js';
|
|
17
|
+
import { EnsureDaemon, ensureDaemon, nodeEnsureDaemonDeps } from './ensure-daemon.js';
|
|
18
|
+
import { openInBrowser } from '../cli/cli-launch.js';
|
|
19
|
+
import { chooseDriver, DRIVERS, shouldEscalate } from './drive-plan.js';
|
|
20
|
+
import { driveWith } from './drive-agent.js';
|
|
21
|
+
import { binaryExists, flowsSaved, listSessions, OwnedDevServer, probePage, } from './node-effects.js';
|
|
22
|
+
import { runSetupPhases, } from './run-setup.js';
|
|
23
|
+
/** The capabilities file init scaffolds, in the order a project is likely to have it. */
|
|
24
|
+
const CAPABILITY_FILES = ['reticle-dev.tsx', 'reticle-dev.ts', 'reticle-dev.jsx', 'reticle-dev.js'];
|
|
25
|
+
/** `electron` in either dependency list, read the same way init's desktop doctor reads it. */
|
|
26
|
+
function hasElectronDependency(dir) {
|
|
27
|
+
try {
|
|
28
|
+
const pkg = JSON.parse(readFileSync(join(dir, 'package.json'), 'utf8'));
|
|
29
|
+
if ('object' !== typeof pkg || null === pkg)
|
|
30
|
+
return false;
|
|
31
|
+
const p = pkg;
|
|
32
|
+
return undefined !== { ...p.dependencies, ...p.devDependencies }['electron'];
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Register with the agents `init` does not reach, and leave the skill where one loads skills from.
|
|
40
|
+
*
|
|
41
|
+
* init covers eight clients and only where it finds them installed, which leaves VS Code's USER
|
|
42
|
+
* scope unwritten — it exists on machines today, and init only ever writes the project-scope file,
|
|
43
|
+
* so a VS Code user has no tools outside the directory they ran init in.
|
|
44
|
+
*/
|
|
45
|
+
export function registerOtherAgents(print) {
|
|
46
|
+
const platform = process.platform;
|
|
47
|
+
const home = homedir();
|
|
48
|
+
const results = applyAgentPlan(planAgentConfigs({ home, platform, exists: agentIo.exists, readFile: agentIo.readFile }), agentIo);
|
|
49
|
+
const wrote = results.filter((r) => 'created' === r.action || 'merged' === r.action);
|
|
50
|
+
if (0 < wrote.length) {
|
|
51
|
+
print(`registered the MCP server with ${wrote.length} more agent(s): ${wrote.map((r) => r.name).join(', ')}`);
|
|
52
|
+
}
|
|
53
|
+
// A format we will not rewrite is somebody's to edit, so it has to be said rather than skipped.
|
|
54
|
+
for (const manual of results.filter((r) => 'manual' === r.action)) {
|
|
55
|
+
print(`${manual.name}: ${manual.why} — add the reticle entry to ${manual.file} by hand.`);
|
|
56
|
+
}
|
|
57
|
+
const skills = applyAgentSkills(agentIo, { home, platform });
|
|
58
|
+
if (0 < skills.length)
|
|
59
|
+
print(`wrote the /reticle skill for ${skills.length} agent(s)`);
|
|
60
|
+
// Registration alone still leaves an Accept dialog in front of every call, and a verification run
|
|
61
|
+
// makes dozens of them: the loop is only autonomous once the tools are pre-approved.
|
|
62
|
+
const approvals = grantAutoApproval(agentIo, { home, platform });
|
|
63
|
+
const granted = approvals.filter((a) => ApprovalOutcome.GRANTED === a.outcome);
|
|
64
|
+
if (0 < granted.length) {
|
|
65
|
+
print(`pre-approved the reticle tools in ${granted.map((a) => a.name).join(', ')} — no Accept prompt per call`);
|
|
66
|
+
}
|
|
67
|
+
for (const noted of approvals.filter((a) => undefined !== a.warn)) {
|
|
68
|
+
print(`${noted.name}: ${String(noted.warn)}`);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Run the phases against the real world.
|
|
73
|
+
*
|
|
74
|
+
* The dev server is stopped on every ending except success, where it is handed to the user: an
|
|
75
|
+
* instrumented app they can watch is the deliverable, and killing it would leave them with config
|
|
76
|
+
* files and a dead tab.
|
|
77
|
+
*/
|
|
78
|
+
export async function runSetupCommand(input, print) {
|
|
79
|
+
if (input.registerAgents)
|
|
80
|
+
registerOtherAgents(print);
|
|
81
|
+
// Before the app is booted, because the app's whole job from here is to dial this port. Without
|
|
82
|
+
// it the phases wait out their budget and then report the SDK as the thing that failed.
|
|
83
|
+
const daemon = await ensureDaemon(input.bridgePort, nodeEnsureDaemonDeps());
|
|
84
|
+
if (EnsureDaemon.UNAVAILABLE === daemon) {
|
|
85
|
+
print(`could not start the Reticle daemon on port ${String(input.bridgePort)}, so nothing is listening for the app to connect to. Run \`npx @reticlehq/server serve --port ${String(input.bridgePort)}\` and try again.`);
|
|
86
|
+
}
|
|
87
|
+
// Which shell this is decides three things the phases would otherwise get wrong: opening a
|
|
88
|
+
// browser (harmful for desktop, where the app's own window is the client), waiting for an HTTP
|
|
89
|
+
// port (a Tauri webview has none), and how long the app gets to appear (a cold `tauri dev` builds
|
|
90
|
+
// Rust first). The same evidence init's desktop doctor reads.
|
|
91
|
+
const shape = readShape({
|
|
92
|
+
hasTauriConf: existsSync(join(input.appDir, 'src-tauri', 'tauri.conf.json')),
|
|
93
|
+
hasElectronDep: hasElectronDependency(input.appDir),
|
|
94
|
+
});
|
|
95
|
+
if (AppShape.WEB !== shape)
|
|
96
|
+
print(`detected a ${shape} app`);
|
|
97
|
+
const server = new OwnedDevServer();
|
|
98
|
+
// Both roots, because in a monorepo `.reticle/` sits at the app root rather than where setup ran.
|
|
99
|
+
const flowRoots = [input.invokedAt, input.appDir];
|
|
100
|
+
let lastDrive;
|
|
101
|
+
let escalated;
|
|
102
|
+
const effects = {
|
|
103
|
+
startDevServer: (command, cwd) => {
|
|
104
|
+
print(`starting: ${command}`);
|
|
105
|
+
server.start(command, cwd, input.env);
|
|
106
|
+
return Promise.resolve();
|
|
107
|
+
},
|
|
108
|
+
devServerOutput: () => server.output(),
|
|
109
|
+
devServerExited: () => server.exited(),
|
|
110
|
+
devServerQuietForMs: () => server.quietForMs(),
|
|
111
|
+
observedPorts: () => server.listeningPorts(),
|
|
112
|
+
probePage,
|
|
113
|
+
openBrowser: async (url) => {
|
|
114
|
+
const failure = await openInBrowser(url);
|
|
115
|
+
if (null !== failure) {
|
|
116
|
+
print(`could not open a browser (${failure}). On a machine with none — CI, a container, an SSH ` +
|
|
117
|
+
'session — take a tab Reticle owns instead: reticle_run({ tool: "reticle_lease", args: ' +
|
|
118
|
+
`{ action: "acquire", url: "${url}" } }).`);
|
|
119
|
+
}
|
|
120
|
+
},
|
|
121
|
+
listSessions: () => listSessions(input.bridgePort),
|
|
122
|
+
// Asked before the drive rather than inferred from its result: "nothing to drive with" and
|
|
123
|
+
// "drove and proved nothing" are different answers and only the second is a failure.
|
|
124
|
+
driverAvailable: () => null !==
|
|
125
|
+
chooseDriver(DRIVERS, (bin) => ({ present: binaryExists(bin), runs: binaryExists(bin) })),
|
|
126
|
+
drive: (url, session) => {
|
|
127
|
+
const driver = chooseDriver(DRIVERS, (bin) => ({
|
|
128
|
+
present: binaryExists(bin),
|
|
129
|
+
// Present is not enough: a CLI that does not run produces an empty session that looks
|
|
130
|
+
// exactly like success.
|
|
131
|
+
runs: binaryExists(bin),
|
|
132
|
+
}));
|
|
133
|
+
if (null === driver)
|
|
134
|
+
return Promise.resolve(null);
|
|
135
|
+
// Only when the SESSION says they were never finished. init fills this file for a
|
|
136
|
+
// conventional app — it detects a state library and the testids — so opening it otherwise
|
|
137
|
+
// spends a turn on work already done, and grants write access nobody needed.
|
|
138
|
+
const capabilitiesFile = false === session.hasCapabilities
|
|
139
|
+
? CAPABILITY_FILES.map((f) => join(input.appDir, 'src', f)).find((p) => existsSync(p))
|
|
140
|
+
: undefined;
|
|
141
|
+
print('');
|
|
142
|
+
print(` ▸ WATCH ${url} NOW — the HUD is on, and you are about to see Reticle drive your app.`);
|
|
143
|
+
print('');
|
|
144
|
+
const request = {
|
|
145
|
+
url,
|
|
146
|
+
sessionId: session.sessionId,
|
|
147
|
+
tabThrottled: false,
|
|
148
|
+
budgetUsd: input.driveBudgetUsd,
|
|
149
|
+
...(undefined === input.flow ? {} : { flow: input.flow }),
|
|
150
|
+
...(undefined === input.driveModel ? {} : { model: input.driveModel }),
|
|
151
|
+
...(undefined === capabilitiesFile ? {} : { unfinishedCapabilitiesFile: capabilitiesFile }),
|
|
152
|
+
};
|
|
153
|
+
lastDrive = driveWith(driver, request, input.appDir);
|
|
154
|
+
// A weak flow only ACTS, so it passes when the feature is broken — and setup replays saved
|
|
155
|
+
// flows, which turns one weak recording into a permanent green. Re-record it rather than
|
|
156
|
+
// hand the trade to the user.
|
|
157
|
+
if (shouldEscalate({
|
|
158
|
+
escalationEnabled: input.escalateWeakFlow,
|
|
159
|
+
fasterModel: input.driveModel,
|
|
160
|
+
flowSaved: flowsSaved(flowRoots),
|
|
161
|
+
...(undefined === lastDrive.grade ? { grade: undefined } : { grade: lastDrive.grade }),
|
|
162
|
+
})) {
|
|
163
|
+
print(`the saved flow graded \`${lastDrive.grade ?? 'unknown'}\` — re-recording with the default model`);
|
|
164
|
+
const stronger = driveWith(driver, { ...request, model: undefined }, input.appDir);
|
|
165
|
+
escalated = { from: lastDrive.grade ?? 'unknown', to: stronger.grade ?? 'unknown' };
|
|
166
|
+
if (undefined !== stronger.grade)
|
|
167
|
+
lastDrive = stronger;
|
|
168
|
+
}
|
|
169
|
+
if (undefined !== lastDrive.incomplete) {
|
|
170
|
+
print(`the drive did not finish, and ${lastDrive.incomplete}`);
|
|
171
|
+
}
|
|
172
|
+
return Promise.resolve('' === lastDrive.text ? null : lastDrive.text);
|
|
173
|
+
},
|
|
174
|
+
flowsSaved: () => flowsSaved(flowRoots),
|
|
175
|
+
now: () => Date.now(),
|
|
176
|
+
sleep: (ms) => new Promise((r) => setTimeout(r, ms)),
|
|
177
|
+
note: print,
|
|
178
|
+
};
|
|
179
|
+
// A `finally` does not run on SIGINT, and this phase owns a detached dev server — see interrupt.ts.
|
|
180
|
+
const releaseSignals = stopOnInterrupt(() => {
|
|
181
|
+
server.stop();
|
|
182
|
+
}, process);
|
|
183
|
+
try {
|
|
184
|
+
const outcome = await runSetupPhases({ ...input, shape }, effects);
|
|
185
|
+
// The app stays up only when there is something worth watching.
|
|
186
|
+
if (outcome.ok)
|
|
187
|
+
server.handOver();
|
|
188
|
+
return {
|
|
189
|
+
...outcome,
|
|
190
|
+
...(undefined === escalated ? {} : { escalated }),
|
|
191
|
+
...(undefined === lastDrive?.turns ? {} : { driveTurns: lastDrive.turns }),
|
|
192
|
+
...(undefined === lastDrive?.costUsd ? {} : { driveCostUsd: lastDrive.costUsd }),
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
finally {
|
|
196
|
+
releaseSignals();
|
|
197
|
+
server.stop();
|
|
198
|
+
}
|
|
199
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
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 declare const DEFAULT_BRIDGE_PORT = 4400;
|
|
28
|
+
/** Per-phase budget. The drive gets its own, much larger one. */
|
|
29
|
+
export declare const DEFAULT_PHASE_TIMEOUT_MS = 120000;
|
|
30
|
+
/** What the drive may spend before it is stopped. */
|
|
31
|
+
export declare const DEFAULT_DRIVE_BUDGET_USD = 3;
|
|
32
|
+
export interface SetupOptions {
|
|
33
|
+
/** The journey to drive, in the caller's own words. */
|
|
34
|
+
readonly flow?: string | undefined;
|
|
35
|
+
/** Which app in a monorepo. */
|
|
36
|
+
readonly app?: string | undefined;
|
|
37
|
+
/** Environment the app needs in order to reach a usable state. */
|
|
38
|
+
readonly env?: Readonly<Record<string, string>> | undefined;
|
|
39
|
+
/** The dev command, when the project's scripts do not say it plainly. */
|
|
40
|
+
readonly devCommand?: string | undefined;
|
|
41
|
+
/** The app is already served here. */
|
|
42
|
+
readonly url?: string | undefined;
|
|
43
|
+
readonly bridgePort: number;
|
|
44
|
+
readonly phaseTimeoutMs: number;
|
|
45
|
+
readonly driveBudgetUsd: number;
|
|
46
|
+
readonly driveModel?: string | undefined;
|
|
47
|
+
/** Register the MCP with the other coding agents on this machine. */
|
|
48
|
+
readonly registerAgents: boolean;
|
|
49
|
+
/** Open a browser. False for CI, a headless box, or a tab the user already has. */
|
|
50
|
+
readonly openBrowser: boolean;
|
|
51
|
+
/** Drive a flow. False leaves step five to the caller, and is the only honest way to skip it. */
|
|
52
|
+
readonly drive: boolean;
|
|
53
|
+
/** Re-record with the stronger model when the saved flow is graded weaker than `asserted`. */
|
|
54
|
+
readonly escalateWeakFlow: boolean;
|
|
55
|
+
/** Restart a dev server whose bundle predates the build-config edit. */
|
|
56
|
+
readonly restartStaleDevServer: boolean;
|
|
57
|
+
/** Write files and stop, which is what `init` did before it grew the runtime phases. */
|
|
58
|
+
readonly filesOnly: boolean;
|
|
59
|
+
/** A license key, written to .env and never echoed. */
|
|
60
|
+
readonly licenseKey?: string | undefined;
|
|
61
|
+
}
|
|
62
|
+
export declare const DEFAULT_SETUP_OPTIONS: SetupOptions;
|
|
63
|
+
/** One `KEY=VALUE`, as an agent would pass it. Everything after the first `=` is the value. */
|
|
64
|
+
export declare function parseEnvAssignment(pair: string): {
|
|
65
|
+
key: string;
|
|
66
|
+
value: string;
|
|
67
|
+
} | null;
|
|
68
|
+
/**
|
|
69
|
+
* Fold repeated `--env KEY=VALUE` into one map, ignoring what cannot be a variable.
|
|
70
|
+
*
|
|
71
|
+
* A malformed pair is dropped rather than guessed at: an agent that meant `--env` and typed
|
|
72
|
+
* something else should see its variable missing, not see setup invent one.
|
|
73
|
+
*/
|
|
74
|
+
export declare function collectEnv(pairs: readonly string[]): Record<string, string>;
|