@reticlehq/server 3.3.0 → 3.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/LICENSE-ENTERPRISE +1 -1
- package/README.md +3 -3
- package/SKILL.md +12 -14
- package/dist/chromium-launch-options.d.ts +4 -3
- package/dist/chromium-launch-options.js +6 -2
- package/dist/command/cli/adhoc-suite.d.ts +22 -0
- package/dist/command/cli/adhoc-suite.js +90 -19
- package/dist/command/cli/adhoc-verdict.d.ts +35 -0
- package/dist/command/cli/adhoc-verdict.js +90 -3
- package/dist/command/cli/answers/gate-hook-message.d.ts +6 -0
- package/dist/command/cli/answers/gate-hook-message.js +6 -0
- package/dist/command/cli/auth/cloud-keystore.d.ts +0 -2
- package/dist/command/cli/auth/cloud-keystore.js +0 -3
- package/dist/command/cli/auth/cloud-session.d.ts +6 -2
- package/dist/command/cli/auth/cloud-session.js +2 -0
- package/dist/command/cli/auth/device-flow.d.ts +2 -2
- package/dist/command/cli/cli-doctor.js +7 -5
- package/dist/command/cli/cli-flow-commands.d.ts +3 -6
- package/dist/command/cli/cli-flow-commands.js +67 -4
- package/dist/command/cli/cli-parse.d.ts +8 -1
- package/dist/command/cli/cli-parse.js +24 -4
- package/dist/command/cli/cli-update-commands.js +8 -1
- package/dist/command/cli/cli-verify.d.ts +24 -3
- package/dist/command/cli/cli-verify.js +54 -11
- package/dist/command/cli/cloud-cli.js +64 -2
- package/dist/command/cli/cloud-kit.js +3 -5
- package/dist/command/cli/cloud-login.d.ts +1 -1
- package/dist/command/cli/cloud-login.js +27 -8
- package/dist/command/cli/demo/demo-page.js +6 -3
- package/dist/command/cli/doctor/browser/chromium-hint.d.ts +5 -0
- package/dist/command/cli/doctor/browser/chromium-hint.js +5 -0
- package/dist/command/cli/launch/cli-launch.d.ts +8 -0
- package/dist/command/cli/launch/cli-launch.js +21 -9
- package/dist/command/cli/launch/sdk-version-match.d.ts +84 -0
- package/dist/command/cli/launch/sdk-version-match.js +206 -0
- package/dist/command/cli/lifecycle/daemon-lifecycle.js +2 -16
- package/dist/command/cli/ports/resolve/cli-port.d.ts +25 -0
- package/dist/command/cli/ports/resolve/cli-port.js +125 -15
- package/dist/command/cli/setup-mcp-cli.js +21 -13
- package/dist/command/cli/tutorial.d.ts +2 -2
- package/dist/command/cli/tutorial.js +21 -7
- package/dist/command/cli.js +79 -11
- package/dist/command/daemon/binding/daemon-status-probe.d.ts +14 -2
- package/dist/command/daemon/binding/daemon-status-probe.js +34 -6
- package/dist/command/daemon/daemon-resolve.d.ts +9 -0
- package/dist/command/daemon/daemon-resolve.js +1 -1
- package/dist/command/daemon/lifetime/daemon-usefulness.d.ts +7 -1
- package/dist/command/daemon/lifetime/daemon-usefulness.js +30 -7
- package/dist/command/dev/stale-issue-guard.d.ts +3 -4
- package/dist/command/dev/stale-issue-guard.js +8 -8
- package/dist/command/setup/bringup/connect-lease.d.ts +17 -0
- package/dist/command/setup/bringup/connect-lease.js +38 -0
- package/dist/command/setup/bringup/dev-server-files.d.ts +68 -0
- package/dist/command/setup/bringup/dev-server-files.js +131 -0
- package/dist/command/setup/bringup/dev-server-log-cap.d.ts +35 -0
- package/dist/command/setup/bringup/dev-server-log-cap.js +121 -0
- package/dist/command/setup/bringup/dev-server-wait.d.ts +2 -0
- package/dist/command/setup/bringup/dev-server-wait.js +1 -1
- package/dist/command/setup/bringup/ensure-daemon.d.ts +65 -4
- package/dist/command/setup/bringup/ensure-daemon.js +135 -16
- package/dist/command/setup/bringup/static-page-server.d.ts +35 -0
- package/dist/command/setup/bringup/static-page-server.js +151 -0
- package/dist/command/setup/desktop-shape.d.ts +4 -0
- package/dist/command/setup/desktop-shape.js +18 -2
- package/dist/command/setup/init/init-port.d.ts +15 -0
- package/dist/command/setup/init/init-port.js +44 -0
- package/dist/command/setup/init/init-runtime.d.ts +1 -1
- package/dist/command/setup/init/init-runtime.js +91 -9
- package/dist/command/setup/node-effects.d.ts +27 -14
- package/dist/command/setup/node-effects.js +86 -45
- package/dist/command/setup/probe/loopback-probe-urls.d.ts +7 -0
- package/dist/command/setup/probe/loopback-probe-urls.js +40 -0
- package/dist/command/setup/probe/page-probe.d.ts +13 -2
- package/dist/command/setup/probe/page-probe.js +18 -3
- package/dist/command/setup/run-setup.d.ts +28 -1
- package/dist/command/setup/run-setup.js +103 -15
- package/dist/command/setup/session-pick.d.ts +13 -0
- package/dist/command/setup/session-pick.js +20 -0
- package/dist/command/setup/setup-command.d.ts +2 -14
- package/dist/command/setup/setup-command.js +105 -31
- package/dist/command/setup/setup-mcp.d.ts +2 -0
- package/dist/command/setup/setup-mcp.js +17 -8
- package/dist/command/setup/terminal/wait-progress.d.ts +8 -0
- package/dist/command/setup/terminal/wait-progress.js +17 -0
- package/dist/command/version/peer-announce.d.ts +6 -2
- package/dist/command/version/peer-announce.js +8 -6
- package/dist/command/version/version-nudge.d.ts +17 -3
- package/dist/command/version/version-nudge.js +25 -4
- package/dist/features/crawl/crawl-empty.js +1 -1
- package/dist/features/crawl/crawl-tools.d.ts +6 -0
- package/dist/features/crawl/crawl-tools.js +57 -0
- package/dist/features/crawl/crawl.js +171 -157
- package/dist/features/exhaust/app-coverage.d.ts +33 -0
- package/dist/features/exhaust/app-coverage.js +31 -0
- package/dist/features/exhaust/code-coverage.d.ts +47 -0
- package/dist/features/exhaust/code-coverage.js +77 -0
- package/dist/features/exhaust/exhaust-run.d.ts +33 -0
- package/dist/features/exhaust/exhaust-run.js +82 -0
- package/dist/features/exhaust/explorer.d.ts +103 -0
- package/dist/features/exhaust/explorer.js +195 -0
- package/dist/features/exhaust/failure-branches.d.ts +33 -0
- package/dist/features/exhaust/failure-branches.js +92 -0
- package/dist/features/exhaust/ledger.d.ts +202 -0
- package/dist/features/exhaust/ledger.js +252 -0
- package/dist/features/exhaust/session-fold.d.ts +31 -0
- package/dist/features/exhaust/session-fold.js +79 -0
- package/dist/features/exhaust/session-port.d.ts +36 -0
- package/dist/features/exhaust/session-port.js +102 -0
- package/dist/features/harness/drive-report.d.ts +0 -9
- package/dist/features/harness/drive-report.js +18 -0
- package/dist/features/harness/driver.js +4 -7
- package/dist/features/harness/fill-values.d.ts +10 -0
- package/dist/features/harness/fill-values.js +50 -11
- package/dist/features/harness/jev-driver.js +30 -13
- package/dist/features/harness/openai-driver.d.ts +12 -0
- package/dist/features/harness/openai-driver.js +21 -12
- package/dist/features/harness/platform-config.js +5 -6
- package/dist/features/license/license.d.ts +2 -2
- package/dist/index.d.ts +9 -0
- package/dist/index.js +56 -19
- package/dist/judgement/capsule/capsule-store.d.ts +4 -4
- package/dist/judgement/domain/domain-model.js +3 -1
- package/dist/judgement/oracles/self-instrument.js +4 -2
- package/dist/judgement/runs/artifact/replay-mapping.d.ts +2 -2
- package/dist/judgement/runs/artifact/replay-mapping.js +44 -9
- package/dist/judgement/runs/verification-sync.d.ts +3 -1
- package/dist/judgement/runs/verification-sync.js +1 -1
- package/dist/language/flows/change/affected.d.ts +9 -0
- package/dist/language/flows/change/affected.js +35 -0
- package/dist/language/flows/change/gate.d.ts +20 -0
- package/dist/language/flows/change/gate.js +12 -1
- package/dist/language/flows/flow-author.d.ts +7 -0
- package/dist/language/flows/flow-author.js +19 -0
- package/dist/language/flows/flow-expect-grammar.js +1 -1
- package/dist/language/flows/flow-journey.d.ts +54 -0
- package/dist/language/flows/flow-journey.js +119 -0
- package/dist/language/flows/flow-replay-run.d.ts +2 -3
- package/dist/language/flows/flow-replay-run.js +13 -47
- package/dist/language/flows/flow-replay.d.ts +22 -8
- package/dist/language/flows/flow-replay.js +55 -9
- package/dist/language/flows/flow-result.d.ts +2 -0
- package/dist/language/flows/flow-result.js +25 -0
- package/dist/language/flows/flow-tools.js +17 -5
- package/dist/language/flows/flows.d.ts +10 -1
- package/dist/language/flows/flows.js +90 -14
- package/dist/language/flows/recording/replay-halt.js +3 -1
- package/dist/language/flows/recording/tape/recordings.d.ts +30 -1
- package/dist/language/flows/recording/tape/recordings.js +55 -1
- package/dist/language/flows/replay.d.ts +19 -0
- package/dist/language/flows/replay.js +42 -6
- package/dist/language/flows/verify-change-tools.js +5 -2
- package/dist/launch-chromium.d.ts +80 -0
- package/dist/launch-chromium.js +218 -0
- package/dist/memory/cloud/cloud-config.d.ts +11 -2
- package/dist/memory/cloud/cloud-config.js +47 -7
- package/dist/memory/cloud/cloud-sync.d.ts +4 -1
- package/dist/memory/cloud/cloud-sync.js +6 -9
- package/dist/memory/cloud/harness-offer.js +5 -6
- package/dist/memory/cloud/harness-switch.js +5 -6
- package/dist/memory/cloud/sync-cycle.d.ts +77 -3
- package/dist/memory/cloud/sync-cycle.js +325 -55
- package/dist/memory/cloud/sync-daemon.js +24 -20
- package/dist/memory/cloud/sync-disk.js +16 -2
- package/dist/memory/intent/intent-shard.d.ts +10 -32
- package/dist/memory/intent/intent-shard.js +0 -10
- package/dist/memory/intent/intent-tools.js +8 -3
- package/dist/memory/journal/drive-flow.d.ts +18 -13
- package/dist/memory/journal/drive-flow.js +45 -37
- package/dist/memory/journal/envelope.d.ts +8 -8
- package/dist/memory/journal/journal-query.d.ts +14 -0
- package/dist/memory/journal/journal-query.js +15 -0
- package/dist/memory/journal/on-disk/workspace-tiers.js +2 -0
- package/dist/memory/journal/session-end.d.ts +9 -1
- package/dist/memory/journal/session-end.js +24 -12
- package/dist/memory/project/artifact-root-resolver.d.ts +10 -0
- package/dist/memory/project/artifact-root-resolver.js +46 -19
- package/dist/memory/project/dir/reticle-dir.d.ts +2 -0
- package/dist/memory/project/dir/reticle-dir.js +1 -0
- package/dist/memory/project/project-tools.js +3 -1
- package/dist/portal/bridge/auth-failure-reason.d.ts +1 -1
- package/dist/portal/bridge/auth-failure-reason.js +4 -2
- package/dist/portal/bridge/bridge.d.ts +12 -3
- package/dist/portal/bridge/bridge.js +49 -3
- package/dist/portal/input/js-coverage.d.ts +25 -0
- package/dist/portal/input/js-coverage.js +30 -0
- package/dist/portal/input/real-input.d.ts +11 -0
- package/dist/portal/input/real-input.js +29 -4
- package/dist/portal/input/viewport-tools.js +16 -6
- package/dist/portal/pool/playwright-launcher.d.ts +6 -0
- package/dist/portal/pool/playwright-launcher.js +20 -13
- package/dist/portal/session/dev-server/dev-command.d.ts +2 -2
- package/dist/portal/session/dev-server/dev-command.js +30 -12
- package/dist/portal/session/dev-server/dev-server-probe.js +17 -1
- package/dist/portal/session/dev-server/route-status-probe.d.ts +24 -0
- package/dist/portal/session/dev-server/route-status-probe.js +57 -0
- package/dist/portal/session/facts/observed-state.d.ts +11 -0
- package/dist/portal/session/facts/observed-state.js +34 -0
- package/dist/portal/session/no-session-diagnosis.d.ts +29 -0
- package/dist/portal/session/no-session-diagnosis.js +62 -5
- package/dist/portal/session/no-session-next-action.d.ts +13 -0
- package/dist/portal/session/no-session-next-action.js +48 -9
- package/dist/portal/session/no-session-watch.d.ts +18 -0
- package/dist/portal/session/no-session-watch.js +88 -2
- package/dist/portal/session/session-health.js +3 -2
- package/dist/portal/session/session-manager.d.ts +20 -6
- package/dist/portal/session/session-manager.js +19 -3
- package/dist/portal/session/session-reaper.d.ts +28 -0
- package/dist/portal/session/session-reaper.js +49 -1
- package/dist/portal/session/session.d.ts +10 -8
- package/dist/portal/session/session.js +24 -32
- package/dist/surface/http-server.d.ts +3 -2
- package/dist/surface/http-server.js +8 -5
- package/dist/surface/mcp/mcp-proxy.d.ts +2 -2
- package/dist/surface/mcp/mcp-proxy.js +2 -2
- package/dist/surface/mcp/mcp.js +5 -3
- package/dist/surface/mcp/proxy/proxy-daemon-probe.d.ts +13 -1
- package/dist/surface/mcp/proxy/proxy-daemon-probe.js +27 -2
- package/dist/surface/mcp/proxy/proxy-log.js +3 -5
- package/dist/surface/mcp/server-instructions.js +9 -7
- package/dist/surface/tools/act/after-match.d.ts +17 -0
- package/dist/surface/tools/act/after-match.js +30 -0
- package/dist/surface/tools/act/durable.d.ts +69 -0
- package/dist/surface/tools/act/durable.js +139 -0
- package/dist/surface/tools/act/navigation/navigate-arrival.d.ts +13 -6
- package/dist/surface/tools/act/navigation/navigate-arrival.js +47 -10
- package/dist/surface/tools/act/navigation/navigate-result.d.ts +1 -12
- package/dist/surface/tools/act/navigation/navigate-result.js +21 -3
- package/dist/surface/tools/act/proved-controls.d.ts +9 -0
- package/dist/surface/tools/act/proved-controls.js +11 -0
- package/dist/surface/tools/act/sequence-grade.d.ts +5 -0
- package/dist/surface/tools/act/sequence-grade.js +32 -1
- package/dist/surface/tools/act-capsule.d.ts +7 -0
- package/dist/surface/tools/act-capsule.js +46 -1
- package/dist/surface/tools/act-sequence-tool.js +25 -8
- package/dist/surface/tools/act-tools.js +28 -52
- package/dist/surface/tools/assert/assert-grade.js +12 -1
- package/dist/surface/tools/assert/assert-verdict.js +2 -2
- package/dist/surface/tools/browser-tools.js +14 -2
- package/dist/surface/tools/coverage-tools.js +27 -1
- package/dist/surface/tools/dynamic-tools.js +29 -2
- package/dist/surface/tools/error-recovery.js +5 -6
- package/dist/surface/tools/feedback-invite.js +6 -4
- package/dist/surface/tools/harness-explore.d.ts +3 -2
- package/dist/surface/tools/harness-explore.js +12 -7
- package/dist/surface/tools/invoke-tool.js +12 -8
- package/dist/surface/tools/lease-tools.js +15 -6
- package/dist/surface/tools/live-call-text.js +4 -1
- package/dist/surface/tools/observe-tools.js +9 -1
- package/dist/surface/tools/tool-kit.d.ts +10 -0
- package/dist/surface/tools/tools.js +4 -0
- package/dist/telemetry/argument-shape.d.ts +0 -2
- package/dist/telemetry/argument-shape.js +1 -5
- package/dist/telemetry/error-class.d.ts +2 -3
- package/dist/telemetry/error-class.js +2 -3
- package/dist/telemetry/harness-drive.d.ts +0 -2
- package/dist/telemetry/harness-drive.js +0 -4
- package/dist/telemetry/onboarding-firsts.d.ts +2 -2
- package/dist/telemetry/onboarding-firsts.js +2 -2
- package/dist/wire-journal.d.ts +3 -0
- package/dist/wire-journal.js +4 -0
- package/dist/workspace-packages.d.ts +0 -2
- package/dist/workspace-packages.js +1 -9
- package/docs/README.md +4 -2
- package/docs/actions.mdx +3 -4
- package/docs/agent-cheatsheet.md +4 -4
- package/docs/architecture.md +6 -6
- package/docs/best-practices.mdx +1 -1
- package/docs/capabilities.mdx +32 -1
- package/docs/cli/cloud.mdx +20 -8
- package/docs/cli/doctor.mdx +4 -0
- package/docs/cli/feedback.mdx +2 -2
- package/docs/cli/gate.mdx +9 -4
- package/docs/cli/update.mdx +10 -0
- package/docs/cli.mdx +22 -7
- package/docs/desktop.mdx +14 -10
- package/docs/environment.mdx +87 -0
- package/docs/faq.mdx +18 -16
- package/docs/first-drive.md +5 -13
- package/docs/flows.md +21 -2
- package/docs/frameworks.mdx +29 -10
- package/docs/getting-started.md +15 -5
- package/docs/http-transport.md +6 -3
- package/docs/human-control.md +2 -2
- package/docs/index.mdx +16 -6
- package/docs/install-agentic.mdx +2 -2
- package/docs/install-manual.mdx +1 -1
- package/docs/integration-patterns.md +3 -3
- package/docs/multi-agent-testing.md +1 -1
- package/docs/packages/babel-plugin.mdx +1 -1
- package/docs/packages/browser.mdx +2 -2
- package/docs/packages/next.mdx +3 -0
- package/docs/packages/react.mdx +2 -2
- package/docs/packages/tauri.mdx +1 -1
- package/docs/packages/vite-plugin.mdx +4 -2
- package/docs/packages.mdx +9 -9
- package/docs/predicates.mdx +50 -5
- package/docs/quickstart.mdx +12 -10
- package/docs/recipes.mdx +2 -2
- package/docs/state-management.mdx +2 -2
- package/docs/system-map.md +1 -1
- package/docs/telemetry.md +1 -1
- package/docs/token-efficiency.md +3 -3
- package/docs/tools/act-and-wait.mdx +1 -0
- package/docs/tools/act-sequence.mdx +1 -1
- package/docs/tools/act.mdx +1 -1
- package/docs/tools/console.mdx +1 -1
- package/docs/tools/inspect.mdx +1 -1
- package/docs/tools/network.mdx +1 -1
- package/docs/tools/query.mdx +1 -1
- package/docs/tools/sessions.mdx +1 -1
- package/docs/tools/snapshot.mdx +1 -1
- package/docs/tools/state.mdx +1 -1
- package/docs/tools/wait-for.mdx +1 -1
- package/docs/usage.md +7 -7
- package/docs/use-cases.mdx +157 -0
- package/docs/vs-screenshots.mdx +3 -3
- package/docs/what-is-recorded.md +2 -2
- package/docs/what-is-reticle.mdx +17 -20
- package/docs/why-reticle.mdx +14 -21
- package/package.json +10 -9
- package/dist/judgement/capsule/minimize.d.ts +0 -12
- package/dist/judgement/capsule/minimize.js +0 -22
- package/dist/judgement/phenomena/phenomena.d.ts +0 -27
- package/dist/judgement/phenomena/phenomena.js +0 -101
- package/dist/language/flows/flow-report.d.ts +0 -22
- package/dist/language/flows/flow-report.js +0 -106
|
@@ -14,7 +14,9 @@ import { ReticleDir, RunFlowStatus } from '@reticlehq/core';
|
|
|
14
14
|
import { FlowStore } from '../../language/flows/flows.js';
|
|
15
15
|
import { RunStore } from '../../judgement/runs/artifact/run-store.js';
|
|
16
16
|
import { createNodeFileSystem } from '../../memory/project/fs/fs-port.js';
|
|
17
|
-
import { affectedSavedFlows } from '../../language/flows/change/flow-sources.js';
|
|
17
|
+
import { affectedSavedFlows, toFlowSources, } from '../../language/flows/change/flow-sources.js';
|
|
18
|
+
import { isInteractiveSource, unflowedFiles } from '../../language/flows/change/affected.js';
|
|
19
|
+
import { LedgerStore, regressions, staleChanged, unexecutedChanged, } from '../../features/exhaust/ledger.js';
|
|
18
20
|
import { gateDecision } from '../../language/flows/change/gate.js';
|
|
19
21
|
import { FlakeStore } from '../../language/flows/stores/flake-store.js';
|
|
20
22
|
import { formatBuddyStatus } from '../../language/flows/buddy-status.js';
|
|
@@ -23,7 +25,7 @@ import { AssertionTiersStore } from '../../language/flows/stores/assertion-tiers
|
|
|
23
25
|
import { detectDowngrades } from '../../judgement/outcome/assertion-integrity.js';
|
|
24
26
|
import { computeCoverage, flowCoverageReport } from '../../language/flows/suite/coverage.js';
|
|
25
27
|
import { createWatchBatcher } from '../../language/flows/change/watch-batcher.js';
|
|
26
|
-
import { watch } from 'node:fs';
|
|
28
|
+
import { readFileSync, statSync, watch } from 'node:fs';
|
|
27
29
|
import { log } from '../../log.js';
|
|
28
30
|
/** Load the {name, steps} of every saved flow for the active project. */
|
|
29
31
|
/** Explicit files plus, when --since is given, the git-changed files since that ref. */
|
|
@@ -163,9 +165,35 @@ export async function handleCapsules() {
|
|
|
163
165
|
* changed files. Flaky flows are quarantined (surfaced, not blocking). The environment-side enforcement
|
|
164
166
|
* that makes verification unavoidable. Never throws; a fault fails closed (exit 1).
|
|
165
167
|
*/
|
|
168
|
+
/**
|
|
169
|
+
* A changed file's text, or '' when it is gone. `git diff --name-only` answers from the repository
|
|
170
|
+
* root while the gate may run in a package below it, so leading segments are dropped until the path
|
|
171
|
+
* resolves from `cwd`.
|
|
172
|
+
*/
|
|
173
|
+
function atChangedPath(cwd, file, read) {
|
|
174
|
+
const parts = file.split('/');
|
|
175
|
+
for (let i = 0; i < parts.length; i += 1) {
|
|
176
|
+
try {
|
|
177
|
+
return read(join(cwd, ...parts.slice(i)));
|
|
178
|
+
}
|
|
179
|
+
catch {
|
|
180
|
+
// not at this depth; try the path one segment shorter
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
return undefined;
|
|
184
|
+
}
|
|
185
|
+
function readChangedFile(cwd, file) {
|
|
186
|
+
return atChangedPath(cwd, file, (path) => readFileSync(path, 'utf8')) ?? '';
|
|
187
|
+
}
|
|
188
|
+
/** When a changed file was last modified, or undefined when it is gone. Same lookup as its text. */
|
|
189
|
+
function changedFileModifiedAt(cwd, file) {
|
|
190
|
+
return atChangedPath(cwd, file, (path) => statSync(path).mtimeMs);
|
|
191
|
+
}
|
|
166
192
|
export async function handleGate(files, since,
|
|
167
193
|
/** Hook mode: prose for a human, and silence when there was simply nothing to check. */
|
|
168
|
-
hook = false
|
|
194
|
+
hook = false,
|
|
195
|
+
/** Accept today's coverage levels as the best before judging, for a drop that was intended. */
|
|
196
|
+
acceptCoverage = false) {
|
|
169
197
|
try {
|
|
170
198
|
const fs = createNodeFileSystem();
|
|
171
199
|
const reticleRoot = join(process.cwd(), ReticleDir.ROOT);
|
|
@@ -204,7 +232,32 @@ hook = false) {
|
|
|
204
232
|
const deleted = Object.entries(baseline)
|
|
205
233
|
.filter(([name, entry]) => !byName.has(name) && entry.sources.some((f) => changedSet.has(f)))
|
|
206
234
|
.map(([name]) => name);
|
|
207
|
-
|
|
235
|
+
// Only against a suite that exists: a project with no flows yet is NOTHING_TO_CHECK, and
|
|
236
|
+
// demanding a flow per edited component on its first day would block every stop.
|
|
237
|
+
const unflowed = 0 === allFlows.length
|
|
238
|
+
? []
|
|
239
|
+
: unflowedFiles(toFlowSources(allFlows), changed, (file) => isInteractiveSource(readChangedFile(process.cwd(), file)));
|
|
240
|
+
// The coverage ledger, when one exists: a level that fell below its best, and changed code the
|
|
241
|
+
// browser loaded and never ran. A project that never folded coverage has an empty ledger and
|
|
242
|
+
// neither can fire.
|
|
243
|
+
const ledgerStore = new LedgerStore(fs, reticleRoot);
|
|
244
|
+
const coverageAccepted = acceptCoverage ? await ledgerStore.acceptCurrent() : [];
|
|
245
|
+
const ledger = await ledgerStore.load();
|
|
246
|
+
const unexecuted = unexecutedChanged(ledger.code, changed);
|
|
247
|
+
// Changed files whose coverage predates the edit: their counts describe the old code, so
|
|
248
|
+
// `unexecuted` cannot judge them either way. Named rather than silently read as covered; not
|
|
249
|
+
// blocking, because a destructive exhaustive crawl after every edit is not a price a gate may set.
|
|
250
|
+
const coverageStale = staleChanged(ledger, changed, (file) => changedFileModifiedAt(process.cwd(), file)).filter((file) => !unexecuted.includes(file));
|
|
251
|
+
const result = gateDecision({
|
|
252
|
+
affected,
|
|
253
|
+
passing,
|
|
254
|
+
flaky,
|
|
255
|
+
downgraded,
|
|
256
|
+
deleted,
|
|
257
|
+
unflowed,
|
|
258
|
+
coverageRegressed: regressions(ledger),
|
|
259
|
+
unexecuted,
|
|
260
|
+
});
|
|
208
261
|
// Verified-surface coverage over flows: how much of the saved suite this run actually exercised.
|
|
209
262
|
const coverage = computeCoverage({ testids: [], signals: [], flows: allFlows.map((f) => f.name) }, { testids: [], signals: [], flows: passing });
|
|
210
263
|
// A gate over an empty suite has not passed — it had nothing to check. Same rule the suite
|
|
@@ -217,6 +270,13 @@ hook = false) {
|
|
|
217
270
|
quarantined: result.quarantined,
|
|
218
271
|
...(result.downgraded.length > 0 ? { downgraded: result.downgraded } : {}),
|
|
219
272
|
...(result.deleted.length > 0 ? { deletedCoverage: result.deleted } : {}),
|
|
273
|
+
...(result.unflowed.length > 0 ? { unflowed: result.unflowed } : {}),
|
|
274
|
+
...(result.coverageRegressed.length > 0
|
|
275
|
+
? { coverageRegressed: result.coverageRegressed }
|
|
276
|
+
: {}),
|
|
277
|
+
...(result.unexecuted.length > 0 ? { unexecuted: result.unexecuted } : {}),
|
|
278
|
+
...(coverageStale.length > 0 ? { coverageStale } : {}),
|
|
279
|
+
...(coverageAccepted.length > 0 ? { coverageAccepted } : {}),
|
|
220
280
|
coverage: flowCoverage,
|
|
221
281
|
});
|
|
222
282
|
// Two non-zero codes, because two callers want opposite things from the same run. CI wants any
|
|
@@ -242,6 +302,9 @@ hook = false) {
|
|
|
242
302
|
// A downgrade is reported per flow with its step indices; the hook names the flow.
|
|
243
303
|
downgraded: result.downgraded.map((d) => d.flow),
|
|
244
304
|
deleted: result.deleted,
|
|
305
|
+
unflowed: result.unflowed,
|
|
306
|
+
coverageRegressed: result.coverageRegressed.map((r) => `${r.level} ${String(r.was)}% -> ${String(r.now)}%`),
|
|
307
|
+
unexecuted: result.unexecuted,
|
|
245
308
|
});
|
|
246
309
|
if (message !== undefined)
|
|
247
310
|
process.stderr.write(`${message}\n`);
|
|
@@ -15,7 +15,7 @@ export { AGENT_FLAG, BUG_FLAG, FEEDBACK_KINDS, KIND_FLAG, RATING_FLAG, } from '.
|
|
|
15
15
|
* telling readers to do exactly that on 110 lines before it was caught, so the invocation now leads
|
|
16
16
|
* the usage block rather than being a footnote somewhere else.
|
|
17
17
|
*/
|
|
18
|
-
export declare const CLI_USAGE = "usage: npx @reticlehq/server <command> (or `reticle <command>` once the bin is on your PATH)\n\n reticle init [--dry-run] [--port N] [--no-mcp] [--no-install] [--app <dir>]\n [--env KEY=VALUE]... [--files-only] [--hooks] (wire Reticle into the project in this directory)\n init is ONBOARDING: it wires the project, boots the app and proves a session\n connected. It does not drive. The FIRST RUN is the stage that proves a flow:\n reticle verify <url> --explore --persona \"<who does what>\", or the same thing\n from an agent as reticle_verify { action: \"explore\", persona: \"\u2026\" }\n --app picks WHICH app in a monorepo, when several are found\n --env is what the app needs to reach a usable state: the key from\n .env.example, the mock backend, the variable that skips an auth wall.\n Repeatable, and a value may contain spaces and equals signs\n --files-only writes the files and stops, which is what init did before it\n learned to boot the app and prove the install works\n --relaunch prints the exact command that restarts THIS conversation with\n the tools loaded, so the restart is not a chore handed to a human. It\n refuses when the session id has no transcript behind it, because\n --resume on an empty id opens a blank conversation that looks like\n success. Works with --files-only\n --license writes the key to .env and keeps .env out of git\n --json puts the result on stdout, so an agent reads one object\n --no-open / --no-agents / --url / --timeout are the runtime dials: CI, a\n headless box, or an app you already run\n --no-mcp skips MORE than the server registration: also the agent rule files\n (CLAUDE.md / AGENTS.md / .cursor) and the /reticle command, because all three\n only make sense once the tools are reachable.\n reticle serve [--port N] [--drive <url>] [--headless] [--http] [--http-port N] [--http-token T]\n reticle stop [--port N] [--quiet] (stop the daemon we started, by its recorded pid)\n reticle kill [--port N] [--force] (free the port by its LISTENER, never the agent's mcp proxy)\n reticle restart [--port N] [--force] (kill, then start a daemon and wait for a real bind)\n reticle status [--port N] [--json] (a readable block; --json for the event)\n reticle doctor [--port N] (one command to diagnose setup: Chromium, daemon, port)\n reticle open [url] [--port N] (show the app: reuse the connected tab, else open one)\n reticle verify <url> [--port N] [--headed] [--timeout N] [--storage-state <file>] [--session-id <id>] (one-shot: drive the URL, verify saved flows, exit 0=pass)\n [--explore] [--persona <who>] (no saved flows? let Reticle drive the app itself and record them)\n [--select <label>] (repeatable: verify only flows carrying these labels \u2014 no model, exit 0=pass)\n [--expect '<json predicate>' | --expect-file <path>] (one verdict, no saved\n flows needed. --expect-file is the form no shell can mangle: cmd.exe keeps the\n single quotes the docs show, and PowerShell strips the inner double quotes, and\n a predicate that does not parse produces no verdict at all \u2014 asks the daemon that\n is already running, so nothing is bound and nothing is stopped. It NEEDS that\n daemon: with none on the port it refuses and names the flag, rather than reporting\n on your saved flows instead. Cannot be combined with --storage-state, which this\n path has nowhere to load. This is the path when your client never loaded the\n reticle_* tools. exit 0 ONLY on verified:\"yes\" \u2014 \"unknown\" is not a pass)\n reticle affected [--since <ref>] [file...] (which saved flows must re-verify for the changed files)\n reticle gate [--since <ref>] [file...]
|
|
18
|
+
export declare const CLI_USAGE = "usage: npx @reticlehq/server <command> (or `reticle <command>` once the bin is on your PATH)\n\n reticle init [--dry-run] [--port N] [--no-mcp] [--no-install] [--app <dir>]\n [--env KEY=VALUE]... [--files-only] [--hooks] (wire Reticle into the project in this directory)\n init is ONBOARDING: it wires the project, boots the app and proves a session\n connected. It does not drive. The FIRST RUN is the stage that proves a flow:\n reticle verify <url> --explore --persona \"<who does what>\", or the same thing\n from an agent as reticle_verify { action: \"explore\", persona: \"\u2026\" }\n --app picks WHICH app in a monorepo, when several are found\n --env is what the app needs to reach a usable state: the key from\n .env.example, the mock backend, the variable that skips an auth wall.\n Repeatable, and a value may contain spaces and equals signs\n --files-only writes the files and stops, which is what init did before it\n learned to boot the app and prove the install works\n --relaunch prints the exact command that restarts THIS conversation with\n the tools loaded, so the restart is not a chore handed to a human. It\n refuses when the session id has no transcript behind it, because\n --resume on an empty id opens a blank conversation that looks like\n success. Works with --files-only\n --license writes the key to .env and keeps .env out of git\n --json puts the result on stdout, so an agent reads one object\n --no-open / --no-agents / --url / --timeout are the runtime dials: CI, a\n headless box, or an app you already run\n --no-mcp skips MORE than the server registration: also the agent rule files\n (CLAUDE.md / AGENTS.md / .cursor) and the /reticle command, because all three\n only make sense once the tools are reachable.\n reticle serve [--port N] [--drive <url>] [--headless] [--http] [--http-port N] [--http-token T]\n reticle stop [--port N] [--quiet] (stop the daemon we started, by its recorded pid)\n reticle kill [--port N] [--force] (free the port by its LISTENER, never the agent's mcp proxy)\n reticle restart [--port N] [--force] (kill, then start a daemon and wait for a real bind)\n reticle status [--port N] [--json] (a readable block; --json for the event)\n reticle doctor [--port N] (one command to diagnose setup: Chromium, daemon, port)\n reticle open [url] [--port N] (show the app: reuse the connected tab, else open one)\n reticle verify <url> [--port N] [--headed] [--timeout N] [--storage-state <file>] [--session-id <id>] (one-shot: drive the URL, verify saved flows, exit 0=pass)\n [--explore] [--persona <who>] (no saved flows? let Reticle drive the app itself and record them)\n [--select <label>] (repeatable: verify only flows carrying these labels \u2014 no model, exit 0=pass)\n [--expect '<json predicate>' | --expect-file <path>] (one verdict, no saved\n flows needed. --expect-file is the form no shell can mangle: cmd.exe keeps the\n single quotes the docs show, and PowerShell strips the inner double quotes, and\n a predicate that does not parse produces no verdict at all \u2014 asks the daemon that\n is already running, so nothing is bound and nothing is stopped. It NEEDS that\n daemon: with none on the port it refuses and names the flag, rather than reporting\n on your saved flows instead. Cannot be combined with --storage-state, which this\n path has nowhere to load. This is the path when your client never loaded the\n reticle_* tools. exit 0 ONLY on verified:\"yes\" \u2014 \"unknown\" is not a pass)\n reticle affected [--since <ref>] [file...] (which saved flows must re-verify for the changed files)\n reticle gate [--since <ref>] [--accept-coverage] [file...] (exit non-zero unless passing artifacts cover the affected flows)\n reticle report [--session <id>] [--hook] (what the latest session claimed, and what held)\n reticle setup mcp (register the MCP server with your agents; the installer's registration half)\n reticle capsules (list the saved fail-to-pass bug capsules in .reticle/capsules)\n reticle hunt <dir> (aggregate a directory of crawl reports into one false-green rate)\n reticle watch [url] (on save, report which saved flows must re-verify)\n reticle drive <url> [--headless] (foreground mode, for debugging)\n reticle mcp [--port N] [--drive <url>] [--headless] (MCP stdio proxy; auto-starts daemon if needed)\n reticle update (install the latest server version and restart)\n reticle rollback (restore the previous server version and restart)\n reticle license (show enterprise license status: active | eval | missing)\n reticle telemetry [status|enable|disable] (anonymous usage metrics; status shows what's sent + the policy)\n reticle feedback [--rating 1-5] [--bug] \"message\" (tell us what worked and what didn't; prints exactly what it sends)\n reticle feedback --agent --kind <bug|gap|ambiguity|feature_request|improvement|experience> \"message\"\n (agents: file from anywhere, including a setup that never finished)\n reticle identify --context company|side_project|open_source|learning [--company N] [--email E] [--forget]\n (OPT-IN: tell us who you are, e.g. for support or an enterprise trial)\n\nCloud (link this project to Reticle; runs/flows recorded on the dashboard):\n reticle connect [--project <name|id>] [--url <cloud origin>]\n (wire this app if needed, sign in, link, send local history)\n reticle login [--url <u>] [--email <e>] [--code <c>] [--org <n>]\n (browser device flow by default; --email mails a code)\n reticle logout [--url <u>] (sign out of ONE host; others stay signed in)\n reticle link [--project <name|id>] (bind this repo: mints a scoped key, writes .reticle/cloud.json)\n reticle whoami (who am I signed in as, and is this repo attached?)\n reticle project <ls|create <name>> (list or create cloud projects)\n reticle config [--runs on|off] [--memory on|off] [--flows on|off] [--verify local|server]\n reticle push | sync [--watch] (one sync cycle: send the difference, collect decisions)\n reticle runs | regression | share <runId> (read cloud state; regression exits 3 if any flow broke)\n\n'drive' shows the browser; everything else is hidden (serve/mcp own the pool behind leases, replay\nand the spec runner: batch work). --headed opts any of them in, --headless hides drive, CI hides it.";
|
|
19
19
|
/** The `reticle telemetry` sub-actions. Bare `reticle telemetry` means `status`. */
|
|
20
20
|
export declare const TelemetryAction: {
|
|
21
21
|
readonly STATUS: "status";
|
|
@@ -167,6 +167,7 @@ export type CliResult = {
|
|
|
167
167
|
files: string[];
|
|
168
168
|
since?: string;
|
|
169
169
|
hook?: boolean;
|
|
170
|
+
acceptCoverage?: boolean;
|
|
170
171
|
} | {
|
|
171
172
|
kind: 'report';
|
|
172
173
|
session?: string;
|
|
@@ -190,6 +191,12 @@ export type CliResult = {
|
|
|
190
191
|
kind: 'error';
|
|
191
192
|
message: string;
|
|
192
193
|
};
|
|
194
|
+
/**
|
|
195
|
+
* Whether this command talks to a daemon on the resolved port, and so must not run when
|
|
196
|
+
* the workspace port conflict (cli-port) has said the port is a guess. `init` carries a port but resolves the
|
|
197
|
+
* workspace itself, and a tutorial only dials one when asked to run.
|
|
198
|
+
*/
|
|
199
|
+
export declare function dialsTheDaemon(parsed: CliResult): boolean;
|
|
193
200
|
/**
|
|
194
201
|
* @param defaultHeadless Whether a browser Reticle launches should be hidden when no flag says
|
|
195
202
|
* otherwise. Injected rather than read from the environment here, because this module is pure —
|
|
@@ -66,8 +66,11 @@ export const CLI_USAGE = `usage: npx @reticlehq/server <command> (or \`reticl
|
|
|
66
66
|
path has nowhere to load. This is the path when your client never loaded the
|
|
67
67
|
reticle_* tools. exit 0 ONLY on verified:"yes" — "unknown" is not a pass)
|
|
68
68
|
reticle affected [--since <ref>] [file...] (which saved flows must re-verify for the changed files)
|
|
69
|
-
reticle gate [--since <ref>] [file...]
|
|
69
|
+
reticle gate [--since <ref>] [--accept-coverage] [file...] (exit non-zero unless passing artifacts cover the affected flows)
|
|
70
70
|
reticle report [--session <id>] [--hook] (what the latest session claimed, and what held)
|
|
71
|
+
reticle setup mcp (register the MCP server with your agents; the installer's registration half)
|
|
72
|
+
reticle capsules (list the saved fail-to-pass bug capsules in .reticle/capsules)
|
|
73
|
+
reticle hunt <dir> (aggregate a directory of crawl reports into one false-green rate)
|
|
71
74
|
reticle watch [url] (on save, report which saved flows must re-verify)
|
|
72
75
|
reticle drive <url> [--headless] (foreground mode, for debugging)
|
|
73
76
|
reticle mcp [--port N] [--drive <url>] [--headless] (MCP stdio proxy; auto-starts daemon if needed)
|
|
@@ -82,6 +85,8 @@ export const CLI_USAGE = `usage: npx @reticlehq/server <command> (or \`reticl
|
|
|
82
85
|
(OPT-IN: tell us who you are, e.g. for support or an enterprise trial)
|
|
83
86
|
|
|
84
87
|
Cloud (link this project to Reticle; runs/flows recorded on the dashboard):
|
|
88
|
+
reticle connect [--project <name|id>] [--url <cloud origin>]
|
|
89
|
+
(wire this app if needed, sign in, link, send local history)
|
|
85
90
|
reticle login [--url <u>] [--email <e>] [--code <c>] [--org <n>]
|
|
86
91
|
(browser device flow by default; --email mails a code)
|
|
87
92
|
reticle logout [--url <u>] (sign out of ONE host; others stay signed in)
|
|
@@ -108,6 +113,7 @@ const CAPSULES_COMMAND = 'capsules';
|
|
|
108
113
|
const GATE_COMMAND = 'gate';
|
|
109
114
|
/** Hook mode: prose a human can read, and silence when there was simply nothing to check. */
|
|
110
115
|
const HOOK_FLAG = '--hook';
|
|
116
|
+
const ACCEPT_COVERAGE_FLAG = '--accept-coverage';
|
|
111
117
|
/** `reticle report [--session <id>] [--hook]` — what the latest session claimed, and what held. */
|
|
112
118
|
const REPORT_COMMAND = 'report';
|
|
113
119
|
const SESSION_FLAG = '--session';
|
|
@@ -166,7 +172,7 @@ const KNOWN_COMMANDS = new Set([
|
|
|
166
172
|
FEEDBACK_COMMAND,
|
|
167
173
|
IDENTIFY_COMMAND,
|
|
168
174
|
DAEMON_INNER_COMMAND,
|
|
169
|
-
|
|
175
|
+
'connect',
|
|
170
176
|
'login',
|
|
171
177
|
'logout',
|
|
172
178
|
'whoami',
|
|
@@ -578,6 +584,18 @@ function parseTargetArgs(rest) {
|
|
|
578
584
|
}
|
|
579
585
|
return since === undefined ? { files } : { files, since };
|
|
580
586
|
}
|
|
587
|
+
/**
|
|
588
|
+
* Whether this command talks to a daemon on the resolved port, and so must not run when
|
|
589
|
+
* the workspace port conflict (cli-port) has said the port is a guess. `init` carries a port but resolves the
|
|
590
|
+
* workspace itself, and a tutorial only dials one when asked to run.
|
|
591
|
+
*/
|
|
592
|
+
export function dialsTheDaemon(parsed) {
|
|
593
|
+
if ('init' === parsed.kind)
|
|
594
|
+
return false;
|
|
595
|
+
if ('tutorial' === parsed.kind)
|
|
596
|
+
return parsed.run;
|
|
597
|
+
return 'port' in parsed;
|
|
598
|
+
}
|
|
581
599
|
/**
|
|
582
600
|
* @param defaultHeadless Whether a browser Reticle launches should be hidden when no flag says
|
|
583
601
|
* otherwise. Injected rather than read from the environment here, because this module is pure —
|
|
@@ -810,15 +828,17 @@ export function parseCliArgs(argv, defaultPort, defaultHeadless = false) {
|
|
|
810
828
|
return { kind: 'affected', files: t.files, ...(since === undefined ? {} : { since }) };
|
|
811
829
|
}
|
|
812
830
|
case GATE_COMMAND: {
|
|
813
|
-
//
|
|
831
|
+
// Flags are stripped before target parsing, which would reject them as unknown.
|
|
814
832
|
const hook = rest.includes(HOOK_FLAG);
|
|
815
|
-
const
|
|
833
|
+
const acceptCoverage = rest.includes(ACCEPT_COVERAGE_FLAG);
|
|
834
|
+
const t = parseTargetArgs(rest.filter((a) => a !== HOOK_FLAG && a !== ACCEPT_COVERAGE_FLAG));
|
|
816
835
|
const since = t.since ?? implicitSince(t.files);
|
|
817
836
|
return {
|
|
818
837
|
kind: 'gate',
|
|
819
838
|
files: t.files,
|
|
820
839
|
...(since === undefined ? {} : { since }),
|
|
821
840
|
...(hook ? { hook: true } : {}),
|
|
841
|
+
...(acceptCoverage ? { acceptCoverage: true } : {}),
|
|
822
842
|
};
|
|
823
843
|
}
|
|
824
844
|
case REPORT_COMMAND: {
|
|
@@ -14,6 +14,7 @@ import { log } from '../../log.js';
|
|
|
14
14
|
import { readFileSync, writeFileSync, existsSync } from 'node:fs';
|
|
15
15
|
import { join } from 'node:path';
|
|
16
16
|
import { reticleDepsOf, sdkSyncCommand } from '../update/sdk-sync.js';
|
|
17
|
+
const WINDOWS_MANUAL_UPDATE = 'irm https://raw.githubusercontent.com/reticlehq/reticle/main/install/install.ps1 | iex';
|
|
17
18
|
/**
|
|
18
19
|
* Bring the SDK in the CURRENT project to the version being installed.
|
|
19
20
|
*
|
|
@@ -94,7 +95,13 @@ export async function handleUpdate() {
|
|
|
94
95
|
await applyUpdate(target); // calls process.exit; Claude Code restarts
|
|
95
96
|
}
|
|
96
97
|
catch (error) {
|
|
97
|
-
|
|
98
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
99
|
+
log('reticle_update_failed', {
|
|
100
|
+
error: message,
|
|
101
|
+
...('win32' === process.platform && message.includes('spawn EINVAL')
|
|
102
|
+
? { hint: `run \`${WINDOWS_MANUAL_UPDATE}\` in PowerShell to update manually` }
|
|
103
|
+
: {}),
|
|
104
|
+
});
|
|
98
105
|
process.exitCode = 1;
|
|
99
106
|
}
|
|
100
107
|
}
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
import { type ReticleVerificationRun } from '@reticlehq/core';
|
|
13
13
|
import { type RunningServer } from '../../index.js';
|
|
14
14
|
import { PortPresence } from '../daemon/binding/port-presence.js';
|
|
15
|
+
import { type CloudConfig } from '../../memory/cloud/cloud-sync.js';
|
|
15
16
|
import { type VerifyProgressListener } from '../../judgement/runs/reticle-runner.js';
|
|
16
17
|
import type { ToolDeps } from '../../surface/tools/tool-kit.js';
|
|
17
18
|
/** The live capabilities runVerify needs — faked in tests so the logic runs without a browser. */
|
|
@@ -47,6 +48,11 @@ export interface VerifyPorts {
|
|
|
47
48
|
out: (line: string) => void;
|
|
48
49
|
fail: (line: string) => void;
|
|
49
50
|
exit: (code: number) => void;
|
|
51
|
+
/**
|
|
52
|
+
* The platform credential for this repo: the stored key for a linked repo, else the exported
|
|
53
|
+
* one. A port so the push is testable, and so this reads what every other caller reads.
|
|
54
|
+
*/
|
|
55
|
+
cloud: () => Promise<CloudConfig | null>;
|
|
50
56
|
}
|
|
51
57
|
interface VerifyArgs {
|
|
52
58
|
url: string;
|
|
@@ -118,12 +124,19 @@ export declare function portBusyMessage(port: number): string;
|
|
|
118
124
|
* diagnose: the refusal they got was true.
|
|
119
125
|
*/
|
|
120
126
|
export declare function expectNeedsDaemonMessage(port: number, presence: PortPresence): string;
|
|
127
|
+
/**
|
|
128
|
+
* The model key is set here but not in the daemon, which is where the drive runs. The daemon is
|
|
129
|
+
* usually the one `init` or the agent's MCP client started, from an environment without the key.
|
|
130
|
+
*/
|
|
131
|
+
export declare function daemonLacksKeyMessage(port: number): string;
|
|
121
132
|
/** What `verify` does next, once the options and the state of the port are both known. */
|
|
122
133
|
export declare const VerifyRoute: {
|
|
123
134
|
/** Ask the daemon that already owns the port for a one-shot verdict on the predicate. */
|
|
124
135
|
readonly ADHOC: "adhoc";
|
|
125
136
|
/** Ask the daemon that already owns the port to replay the saved flows. See runAdhocSuite. */
|
|
126
137
|
readonly ADHOC_SUITE: "adhoc-suite";
|
|
138
|
+
/** Ask the daemon that already owns the port to explore the url and record flows. */
|
|
139
|
+
readonly ADHOC_EXPLORE: "adhoc-explore";
|
|
127
140
|
/** Boot our own daemon, drive the url, replay the flows saved on disk. */
|
|
128
141
|
readonly FLOWS: "flows";
|
|
129
142
|
/** Answer the caller, and run nothing. */
|
|
@@ -134,6 +147,8 @@ export type VerifyPlan = {
|
|
|
134
147
|
route: typeof VerifyRoute.ADHOC;
|
|
135
148
|
} | {
|
|
136
149
|
route: typeof VerifyRoute.ADHOC_SUITE;
|
|
150
|
+
} | {
|
|
151
|
+
route: typeof VerifyRoute.ADHOC_EXPLORE;
|
|
137
152
|
} | {
|
|
138
153
|
route: typeof VerifyRoute.FLOWS;
|
|
139
154
|
} | {
|
|
@@ -153,11 +168,17 @@ export declare function routeVerify(args: {
|
|
|
153
168
|
/** True when the caller supplied a predicate, by `--expect` or `--expect-file`. */
|
|
154
169
|
hasPredicate: boolean;
|
|
155
170
|
/**
|
|
156
|
-
* True when the caller asked for something only a browser of OUR OWN can do: `--
|
|
157
|
-
*
|
|
158
|
-
*
|
|
171
|
+
* True when the caller asked for something only a browser of OUR OWN can do: `--headed` or
|
|
172
|
+
* `--storage-state`. The running daemon's tab can honour neither, so replaying there would
|
|
173
|
+
* silently ignore the request.
|
|
159
174
|
*/
|
|
160
175
|
wantsOwnBrowser?: boolean;
|
|
176
|
+
/**
|
|
177
|
+
* `--explore`: a model drives the url and records flows. The daemon on the port runs the same
|
|
178
|
+
* drive (`reticle_verify { action: "explore" }`), so this is NOT a reason to need our own browser —
|
|
179
|
+
* it used to be, and `init` leaves exactly that daemon behind before recommending this command.
|
|
180
|
+
*/
|
|
181
|
+
explore?: boolean;
|
|
161
182
|
presence: PortPresence;
|
|
162
183
|
port: number;
|
|
163
184
|
}): VerifyPlan;
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
import { join, basename } from 'node:path';
|
|
13
13
|
import { readFileSync } from 'node:fs';
|
|
14
14
|
import { runAdhocVerdict } from './adhoc-verdict.js';
|
|
15
|
-
import { runAdhocSuite } from './adhoc-suite.js';
|
|
15
|
+
import { runAdhocExplore, runAdhocSuite } from './adhoc-suite.js';
|
|
16
16
|
import { exploreApp, harnessAvailable, MSG_NO_HARNESS_KEY, } from '../../surface/tools/harness-explore.js';
|
|
17
17
|
import { resolveSuiteSelection } from '../../language/flows/suite-selection.js';
|
|
18
18
|
import { readOrCreatePairingTokenSync, defaultPairingTokenDir, } from '../../portal/bridge/pairing-token.js';
|
|
@@ -22,8 +22,10 @@ import { start } from '../../index.js';
|
|
|
22
22
|
import { probePresence, describePresence, PortPresence, } from '../daemon/binding/port-presence.js';
|
|
23
23
|
import { EXPECT_FLAG } from './cli-parse-grammar.js';
|
|
24
24
|
import { probeDaemon } from '../../surface/mcp/mcp-proxy.js';
|
|
25
|
-
import { fetchStatus } from '../daemon/binding/daemon-status-probe.js';
|
|
26
|
-
import { cloudFetch, createProgressReporter,
|
|
25
|
+
import { fetchStatus, summarizeStatus } from '../daemon/binding/daemon-status-probe.js';
|
|
26
|
+
import { cloudFetch, createProgressReporter, syncProgressToCloud, syncRunToCloud, SyncOutcome, } from '../../memory/cloud/cloud-sync.js';
|
|
27
|
+
import { linkedCloudPort } from '../../memory/cloud/cloud-config.js';
|
|
28
|
+
import { homedir } from 'node:os';
|
|
27
29
|
import { ReticleRunner } from '../../judgement/runs/reticle-runner.js';
|
|
28
30
|
import { createRunnerPort } from '../../judgement/runs/runner-port.js';
|
|
29
31
|
import { RunStore } from '../../judgement/runs/artifact/run-store.js';
|
|
@@ -128,7 +130,7 @@ export async function runVerify(args, ports) {
|
|
|
128
130
|
* live progress by it until the finished run replaces the whole picture.
|
|
129
131
|
*/
|
|
130
132
|
const streamId = randomUUID();
|
|
131
|
-
const cloud =
|
|
133
|
+
const cloud = await ports.cloud().catch(() => null);
|
|
132
134
|
const progress = createProgressReporter(streamId, cloud, cloudFetch);
|
|
133
135
|
let run;
|
|
134
136
|
try {
|
|
@@ -140,7 +142,7 @@ export async function runVerify(args, ports) {
|
|
|
140
142
|
progress.stop();
|
|
141
143
|
await progress.flush().catch(() => undefined);
|
|
142
144
|
}
|
|
143
|
-
await pushRunToCloud(run, ports); // best-effort; opt-in; never changes the verdict or exit code
|
|
145
|
+
await pushRunToCloud(run, cloud, ports); // best-effort; opt-in; never changes the verdict or exit code
|
|
144
146
|
/*
|
|
145
147
|
* The last event anybody watching is waiting for, sent after the artifact has actually landed —
|
|
146
148
|
* so "pushed" on a dashboard means the run is really there, not that we were about to try.
|
|
@@ -184,13 +186,12 @@ async function explore(conn, args, ports) {
|
|
|
184
186
|
return true;
|
|
185
187
|
}
|
|
186
188
|
/**
|
|
187
|
-
* Best-effort push of a finished run to the cloud dashboard. Opt-in: only fires when
|
|
188
|
-
*
|
|
189
|
-
*
|
|
189
|
+
* Best-effort push of a finished run to the cloud dashboard. Opt-in: only fires when this repo is
|
|
190
|
+
* linked or RETICLE_API_KEY is set. Absent → no-op, nothing leaves the machine (the no-phone-home
|
|
191
|
+
* default). A push failure NEVER changes the verdict or exit code — the run is
|
|
190
192
|
* already reported locally; the cloud copy is an enhancement.
|
|
191
193
|
*/
|
|
192
|
-
async function pushRunToCloud(run, ports) {
|
|
193
|
-
const config = resolveCloudConfig(process.env);
|
|
194
|
+
async function pushRunToCloud(run, config, ports) {
|
|
194
195
|
if (null === config)
|
|
195
196
|
return;
|
|
196
197
|
const result = await syncRunToCloud(run, config, cloudFetch);
|
|
@@ -390,12 +391,23 @@ export function expectNeedsDaemonMessage(port, presence) {
|
|
|
390
391
|
` • Or drop ${EXPECT_FLAG}, and verify drives the url itself and replays your saved flows — ` +
|
|
391
392
|
'a different question, and it proves nothing about your predicate.');
|
|
392
393
|
}
|
|
394
|
+
/**
|
|
395
|
+
* The model key is set here but not in the daemon, which is where the drive runs. The daemon is
|
|
396
|
+
* usually the one `init` or the agent's MCP client started, from an environment without the key.
|
|
397
|
+
*/
|
|
398
|
+
export function daemonLacksKeyMessage(port) {
|
|
399
|
+
return (`${ReticleEnv.HARNESS_KEY} is set in this shell, but the daemon on port ${String(port)} was ` +
|
|
400
|
+
'started without it, and the drive runs inside the daemon. Restart it from this shell so it ' +
|
|
401
|
+
`inherits the key: npx @reticlehq/server restart --port ${String(port)}`);
|
|
402
|
+
}
|
|
393
403
|
/** What `verify` does next, once the options and the state of the port are both known. */
|
|
394
404
|
export const VerifyRoute = {
|
|
395
405
|
/** Ask the daemon that already owns the port for a one-shot verdict on the predicate. */
|
|
396
406
|
ADHOC: 'adhoc',
|
|
397
407
|
/** Ask the daemon that already owns the port to replay the saved flows. See runAdhocSuite. */
|
|
398
408
|
ADHOC_SUITE: 'adhoc-suite',
|
|
409
|
+
/** Ask the daemon that already owns the port to explore the url and record flows. */
|
|
410
|
+
ADHOC_EXPLORE: 'adhoc-explore',
|
|
399
411
|
/** Boot our own daemon, drive the url, replay the flows saved on disk. */
|
|
400
412
|
FLOWS: 'flows',
|
|
401
413
|
/** Answer the caller, and run nothing. */
|
|
@@ -418,6 +430,9 @@ export function routeVerify(args) {
|
|
|
418
430
|
}
|
|
419
431
|
if (args.presence !== PortPresence.DAEMON)
|
|
420
432
|
return { route: VerifyRoute.FLOWS };
|
|
433
|
+
if (true === args.explore && true !== args.wantsOwnBrowser) {
|
|
434
|
+
return { route: VerifyRoute.ADHOC_EXPLORE };
|
|
435
|
+
}
|
|
421
436
|
// No predicate, but there may be SAVED FLOWS, and a daemon already owns the port: replay them
|
|
422
437
|
// there rather than refusing, unless the request needs a browser that path never opens.
|
|
423
438
|
return true === args.wantsOwnBrowser
|
|
@@ -470,6 +485,7 @@ export function handleVerify(parsed) {
|
|
|
470
485
|
out: (line) => process.stdout.write(`${line}\n`),
|
|
471
486
|
fail: (line) => process.stderr.write(`${line}\n`),
|
|
472
487
|
exit: (code) => process.exit(code),
|
|
488
|
+
cloud: linkedCloudPort(createNodeFileSystem(), reticleRoot, homedir(), process.env),
|
|
473
489
|
};
|
|
474
490
|
// Asked BEFORE anything binds. The listen failure arrives asynchronously on the server object,
|
|
475
491
|
// long after `start` has resolved, so no `.catch` on that promise can ever see it — which is why
|
|
@@ -479,7 +495,8 @@ export function handleVerify(parsed) {
|
|
|
479
495
|
const presence = await probePresence(port, { tcpOpen: probeDaemon, status: fetchStatus });
|
|
480
496
|
const plan = routeVerify({
|
|
481
497
|
hasPredicate: expectation !== undefined,
|
|
482
|
-
wantsOwnBrowser:
|
|
498
|
+
wantsOwnBrowser: !parsed.headless || parsed.storageState !== undefined,
|
|
499
|
+
explore: true === parsed.explore || parsed.persona !== undefined,
|
|
483
500
|
presence,
|
|
484
501
|
port,
|
|
485
502
|
});
|
|
@@ -502,6 +519,9 @@ export function handleVerify(parsed) {
|
|
|
502
519
|
port,
|
|
503
520
|
...(parsed.url !== undefined && '' !== parsed.url ? { url: parsed.url } : {}),
|
|
504
521
|
predicate: expectation,
|
|
522
|
+
// Read fresh, not from `presence`: that probe asks whether a daemon is on the port, and
|
|
523
|
+
// this asks whether a TAB is — the difference between asserting and opening the url first.
|
|
524
|
+
sessions: async () => summarizeStatus(await fetchStatus(port)).sessions,
|
|
505
525
|
...(parsed.sessionId === undefined ? {} : { sessionId: parsed.sessionId }),
|
|
506
526
|
...((t) => (t === undefined || 0 === t.length ? {} : { token: t }))(readOrCreatePairingTokenSync(defaultPairingTokenDir())),
|
|
507
527
|
});
|
|
@@ -524,6 +544,29 @@ export function handleVerify(parsed) {
|
|
|
524
544
|
ports.exit(suite.code);
|
|
525
545
|
return;
|
|
526
546
|
}
|
|
547
|
+
// `--explore` with a daemon on the port — the state `init` leaves behind right before it names
|
|
548
|
+
// this command as the first run. The daemon drives and records; this process only asks.
|
|
549
|
+
case VerifyRoute.ADHOC_EXPLORE: {
|
|
550
|
+
const explored = await runAdhocExplore({
|
|
551
|
+
port,
|
|
552
|
+
url: parsed.url,
|
|
553
|
+
...(parsed.persona === undefined ? {} : { persona: parsed.persona }),
|
|
554
|
+
sessions: async () => summarizeStatus(await fetchStatus(port)).sessions,
|
|
555
|
+
...((t) => (t === undefined || 0 === t.length ? {} : { token: t }))(readOrCreatePairingTokenSync(defaultPairingTokenDir())),
|
|
556
|
+
});
|
|
557
|
+
for (const line of explored.lines)
|
|
558
|
+
ports.out(line);
|
|
559
|
+
// The drive runs INSIDE the daemon, so the key has to be in ITS environment. A reader who
|
|
560
|
+
// exported it in this shell and got "set ANTHROPIC_API_KEY" back has been told to do the
|
|
561
|
+
// thing they just did; say where the key is actually missing.
|
|
562
|
+
if (0 !== explored.code && harnessAvailable(process.env)) {
|
|
563
|
+
const refusedForKey = explored.lines.some((line) => line.includes(MSG_NO_HARNESS_KEY));
|
|
564
|
+
if (refusedForKey)
|
|
565
|
+
ports.fail(daemonLacksKeyMessage(port));
|
|
566
|
+
}
|
|
567
|
+
ports.exit(explored.code);
|
|
568
|
+
return;
|
|
569
|
+
}
|
|
527
570
|
case VerifyRoute.FLOWS:
|
|
528
571
|
await runVerify({
|
|
529
572
|
url: parsed.url,
|
|
@@ -42,6 +42,7 @@ import { diskSink, diskSource, readCloudIssues, readCloudState } from '../../mem
|
|
|
42
42
|
*/
|
|
43
43
|
const CLOUD_COMMANDS = new Set([
|
|
44
44
|
'login',
|
|
45
|
+
'connect',
|
|
45
46
|
'logout',
|
|
46
47
|
'whoami',
|
|
47
48
|
'link',
|
|
@@ -438,6 +439,62 @@ const cmdLink = async (argv) => {
|
|
|
438
439
|
}
|
|
439
440
|
return 0;
|
|
440
441
|
};
|
|
442
|
+
/**
|
|
443
|
+
* `reticle connect` finishes the cloud half of first-run setup in one invocation. The CLI entry
|
|
444
|
+
* initializes an unwired app before calling this verb. A saved login is checked against this exact
|
|
445
|
+
* host before reuse; an expired one opens the normal browser approval flow. Login does not auto-link
|
|
446
|
+
* here because this command links once, to the project the caller named, then sends local history.
|
|
447
|
+
*/
|
|
448
|
+
const cmdConnect = async (argv) => {
|
|
449
|
+
for (let i = 0; i < argv.length; i += 2) {
|
|
450
|
+
const flag = argv[i];
|
|
451
|
+
const value = argv[i + 1];
|
|
452
|
+
if ((flag !== '--project' && flag !== '--url') ||
|
|
453
|
+
value === undefined ||
|
|
454
|
+
0 === value.length ||
|
|
455
|
+
value.startsWith('--')) {
|
|
456
|
+
err('usage: reticle connect [--project <name|id>] [--url <cloud origin>]');
|
|
457
|
+
return 2;
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
if (!(await createNodeFileSystem().exists(join(process.cwd(), RETICLE_CONFIG_BASENAME)))) {
|
|
461
|
+
err('this app is not wired yet — run `reticle init` in its directory, then `reticle connect`');
|
|
462
|
+
return 2;
|
|
463
|
+
}
|
|
464
|
+
const f = flags(argv);
|
|
465
|
+
const url = baseUrl(null, f['url']);
|
|
466
|
+
if (undefined === apiKeyFrom(process.env)) {
|
|
467
|
+
const session = await readSessionFor(url);
|
|
468
|
+
let signedIn = false;
|
|
469
|
+
if (session !== null) {
|
|
470
|
+
try {
|
|
471
|
+
await api('GET', `${url}/v1/me`, session.token);
|
|
472
|
+
signedIn = true;
|
|
473
|
+
}
|
|
474
|
+
catch {
|
|
475
|
+
hint('saved sign-in needs renewal — opening browser approval');
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
if (!signedIn) {
|
|
479
|
+
const code = await cmdLogin(['--url', url], cmdLink, false);
|
|
480
|
+
if (code !== 0)
|
|
481
|
+
return code;
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
const linkArgs = [
|
|
485
|
+
'--url',
|
|
486
|
+
url,
|
|
487
|
+
...(f['project'] === undefined ? [] : ['--project', f['project']]),
|
|
488
|
+
];
|
|
489
|
+
const linked = await cmdLink(linkArgs);
|
|
490
|
+
if (linked !== 0)
|
|
491
|
+
return linked;
|
|
492
|
+
const pushed = await cmdPush();
|
|
493
|
+
if (pushed !== 0)
|
|
494
|
+
return pushed;
|
|
495
|
+
hint('Connected. Open your coding agent and ask it to verify one flow in your app.');
|
|
496
|
+
return 0;
|
|
497
|
+
};
|
|
441
498
|
/** `reticle config [--runs on|off] [--memory on|off] [--flows on|off] [--verify local|server]`. */
|
|
442
499
|
const cmdConfig = async (argv) => {
|
|
443
500
|
const f = flags(argv);
|
|
@@ -489,7 +546,7 @@ const cmdSync = async (argv) => {
|
|
|
489
546
|
const reticleRoot = join(process.cwd(), RETICLE_DIR);
|
|
490
547
|
const cloud = await resolveProjectCloud(fs, reticleRoot, homedir(), process.env);
|
|
491
548
|
if (null === cloud.config) {
|
|
492
|
-
err('cloud not attached here
|
|
549
|
+
err(cloud.reason ?? 'cloud not attached here: run `reticle link`, or set RETICLE_API_KEY');
|
|
493
550
|
return 1;
|
|
494
551
|
}
|
|
495
552
|
const config = cloud.config;
|
|
@@ -521,7 +578,10 @@ const cmdSync = async (argv) => {
|
|
|
521
578
|
flows: report.flowsSent,
|
|
522
579
|
records: report.derivedSent,
|
|
523
580
|
...(report.runsRejected.length > 0 ? { rejected: report.runsRejected } : {}),
|
|
581
|
+
...(report.refused.length > 0 ? { refused: report.refused } : {}),
|
|
524
582
|
},
|
|
583
|
+
...(report.held.length > 0 ? { notSent: report.held } : {}),
|
|
584
|
+
...(report.notRetried.length > 0 ? { notRetried: report.notRetried } : {}),
|
|
525
585
|
pulled: report.pulled,
|
|
526
586
|
...(report.morePending ? { morePending: true } : {}),
|
|
527
587
|
...(report.error === undefined ? {} : { error: report.error }),
|
|
@@ -546,7 +606,7 @@ const repoCloud = async () => {
|
|
|
546
606
|
const fs = createNodeFileSystem();
|
|
547
607
|
const cloud = await resolveProjectCloud(fs, join(process.cwd(), RETICLE_DIR), homedir(), process.env);
|
|
548
608
|
if (null === cloud.config)
|
|
549
|
-
throw new Error('cloud not attached here
|
|
609
|
+
throw new Error(cloud.reason ?? 'cloud not attached here: run `reticle link`, or set RETICLE_API_KEY');
|
|
550
610
|
return cloud.config;
|
|
551
611
|
};
|
|
552
612
|
/** `reticle runs` — the linked project's recent run artifacts (the key scopes it server-side). */
|
|
@@ -658,6 +718,8 @@ export const runCloudCommand = async (argv) => {
|
|
|
658
718
|
// The linker is passed in rather than imported by cloud-login: login ends by linking and
|
|
659
719
|
// link needs a session, so the cycle is broken at this one call site.
|
|
660
720
|
return await cmdLogin(rest, cmdLink);
|
|
721
|
+
case 'connect':
|
|
722
|
+
return await cmdConnect(rest);
|
|
661
723
|
case 'logout':
|
|
662
724
|
return await cmdLogout(rest);
|
|
663
725
|
case 'whoami':
|
|
@@ -8,15 +8,13 @@
|
|
|
8
8
|
* `cloud-login` is the first to move — and it is a real seam rather than a file-size dodge: nothing
|
|
9
9
|
* here knows what a project, a credential or a link is.
|
|
10
10
|
*/
|
|
11
|
-
import { apiKeyFrom } from '@reticlehq/core';
|
|
11
|
+
import { DEFAULT_PLATFORM_URL, apiKeyFrom, cloudUrlFrom } from '@reticlehq/core';
|
|
12
12
|
import { readFile } from 'node:fs/promises';
|
|
13
13
|
import { homedir } from 'node:os';
|
|
14
14
|
import { join } from 'node:path';
|
|
15
15
|
import { z } from 'zod';
|
|
16
16
|
import { cloudFetch } from '../../memory/cloud/cloud-sync.js';
|
|
17
17
|
import { normalizeUrl, readSessionFrom, resolveSessionFor, sessionPath, } from './auth/cloud-session.js';
|
|
18
|
-
/** Where `reticle login` dials when nothing says otherwise: the hosted service. */
|
|
19
|
-
const DEFAULT_URL = 'https://app.reticle.sh';
|
|
20
18
|
export const RETICLE_DIR = '.reticle';
|
|
21
19
|
export const SESSION_FILE = 'session.json';
|
|
22
20
|
export const CREDENTIALS_FILE = 'credentials.json';
|
|
@@ -72,7 +70,7 @@ export const baseUrl = (session, explicit) => {
|
|
|
72
70
|
// and it cannot leak into a sibling process the way an exported variable does.
|
|
73
71
|
if (explicit !== undefined && explicit.length > 0)
|
|
74
72
|
return normalizeUrl(explicit);
|
|
75
|
-
const env = process.env
|
|
73
|
+
const env = cloudUrlFrom(process.env);
|
|
76
74
|
if (env !== undefined && env.length > 0)
|
|
77
75
|
return env.replace(/\/+$/, '');
|
|
78
76
|
if (null !== session && session.url.length > 0)
|
|
@@ -81,7 +79,7 @@ export const baseUrl = (session, explicit) => {
|
|
|
81
79
|
// saying because that default was wrong for everyone except us. The default is now the hosted
|
|
82
80
|
// service, so the fallback IS the intended path — and a warning printed on the correct path is
|
|
83
81
|
// how people learn to ignore stderr, which is where the real problems are written.
|
|
84
|
-
return
|
|
82
|
+
return DEFAULT_PLATFORM_URL;
|
|
85
83
|
};
|
|
86
84
|
/** Bearer for a command: an explicit api key (agent) wins, else the human login token. */
|
|
87
85
|
export const bearer = (session) => apiKeyFrom(process.env) ?? session?.token ?? null;
|
|
@@ -4,7 +4,7 @@ type Linker = (argv: readonly string[]) => Promise<number>;
|
|
|
4
4
|
* `reticle login` — browser device flow by default; `--email <e>` (or a positional email) keeps the
|
|
5
5
|
* headless two-step code path for CI/servers where opening a browser makes no sense.
|
|
6
6
|
*/
|
|
7
|
-
export declare const cmdLogin: (argv: readonly string[], link: Linker) => Promise<number>;
|
|
7
|
+
export declare const cmdLogin: (argv: readonly string[], link: Linker, autoLink?: boolean) => Promise<number>;
|
|
8
8
|
/** `reticle logout` — forget the cached session token (per-project keys under credentials.json stay). */
|
|
9
9
|
/**
|
|
10
10
|
* `reticle logout` — sign out of ONE host, not of everywhere.
|