@reticlehq/server 3.9.0 → 3.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/SKILL.md +13 -12
- package/dist/command/cli/adhoc-suite.js +2 -0
- package/dist/command/cli/adhoc-verdict.d.ts +2 -43
- package/dist/command/cli/adhoc-verdict.js +50 -187
- package/dist/command/cli/answers/mcp-connection-hint.d.ts +2 -0
- package/dist/command/cli/answers/mcp-connection-hint.js +6 -0
- package/dist/command/cli/cli-doctor.js +1 -1
- package/dist/command/cli/cli-flow-commands.js +2 -9
- package/dist/command/cli/cli-kill.js +1 -1
- package/dist/command/cli/cli-parse-compare.d.ts +22 -0
- package/dist/command/cli/cli-parse-compare.js +75 -0
- package/dist/command/cli/cli-parse-grade.d.ts +36 -0
- package/dist/command/cli/cli-parse-grade.js +75 -0
- package/dist/command/cli/cli-parse-grammar.d.ts +2 -0
- package/dist/command/cli/cli-parse-grammar.js +18 -20
- package/dist/command/cli/cli-parse-verify.d.ts +10 -0
- package/dist/command/cli/cli-parse-verify.js +63 -14
- package/dist/command/cli/cli-parse.d.ts +17 -15
- package/dist/command/cli/cli-parse.js +44 -27
- package/dist/command/cli/cli-update-commands.d.ts +1 -1
- package/dist/command/cli/cli-update-commands.js +29 -20
- package/dist/command/cli/cli-usage.js +58 -29
- package/dist/command/cli/cli-verify.d.ts +11 -2
- package/dist/command/cli/cli-verify.js +27 -8
- package/dist/command/cli/cloud-cli.js +2 -119
- package/dist/command/cli/daemon-link/daemon-client.d.ts +45 -0
- package/dist/command/cli/daemon-link/daemon-client.js +205 -0
- package/dist/command/cli/doctor/doctor-daemon-line.js +6 -3
- package/dist/command/cli/doctor-repairs.js +1 -1
- package/dist/command/cli/ensure-daemon.d.ts +26 -0
- package/dist/command/cli/ensure-daemon.js +57 -0
- package/dist/command/cli/finished-run/grade-command.d.ts +23 -0
- package/dist/command/cli/finished-run/grade-command.js +180 -0
- package/dist/command/cli/lifecycle/daemon-lifecycle.d.ts +1 -1
- package/dist/command/cli/lifecycle/daemon-lifecycle.js +1 -1
- package/dist/command/cli/ports/port-holder.d.ts +9 -0
- package/dist/command/cli/ports/port-holder.js +27 -3
- package/dist/command/cli/status/status-lines.d.ts +1 -0
- package/dist/command/cli/status/status-lines.js +22 -0
- package/dist/command/cli/status-fields.d.ts +5 -1
- package/dist/command/cli/status-fields.js +7 -3
- package/dist/command/cli/two-builds/compare-command.d.ts +48 -0
- package/dist/command/cli/two-builds/compare-command.js +314 -0
- package/dist/command/cli/verify-against.d.ts +61 -0
- package/dist/command/cli/verify-against.js +187 -0
- package/dist/command/cli/verify-no-session.d.ts +2 -0
- package/dist/command/cli/verify-no-session.js +13 -0
- package/dist/command/cli/verify-results.d.ts +35 -2
- package/dist/command/cli/verify-results.js +76 -10
- package/dist/command/cli.js +46 -41
- package/dist/command/daemon/daemon-resolve.d.ts +8 -1
- package/dist/command/daemon/daemon-resolve.js +27 -1
- package/dist/command/daemon/daemon.d.ts +12 -1
- package/dist/command/daemon/daemon.js +35 -4
- package/dist/command/daemon/lifetime/death-cause.js +21 -2
- package/dist/command/daemon/lifetime/heartbeat.js +7 -2
- package/dist/command/daemon/lifetime/startup-failure.d.ts +29 -0
- package/dist/command/daemon/lifetime/startup-failure.js +62 -0
- package/dist/command/setup/bringup/dev-server-files.d.ts +9 -0
- package/dist/command/setup/bringup/dev-server-files.js +17 -0
- package/dist/command/setup/bringup/static-page-server.d.ts +14 -2
- package/dist/command/setup/bringup/static-page-server.js +43 -9
- package/dist/command/setup/init/init-runtime.js +6 -0
- package/dist/command/setup/node-effects.d.ts +3 -2
- package/dist/command/setup/node-effects.js +9 -3
- package/dist/command/setup/remaining-steps.d.ts +8 -0
- package/dist/command/setup/remaining-steps.js +10 -0
- package/dist/command/setup/run-setup.d.ts +2 -0
- package/dist/command/setup/run-setup.js +9 -1
- package/dist/command/setup/setup-command.js +6 -3
- package/dist/command/update/updater.js +6 -0
- package/dist/command/version/version-skew.js +1 -1
- package/dist/features/compare/action-plan.d.ts +260 -0
- package/dist/features/compare/action-plan.js +543 -0
- package/dist/features/compare/behaviour-compare.d.ts +24 -0
- package/dist/features/compare/behaviour-compare.js +85 -0
- package/dist/features/compare/behaviour-diff.d.ts +97 -0
- package/dist/features/compare/behaviour-diff.js +202 -0
- package/dist/features/compare/behaviour-report.d.ts +40 -0
- package/dist/features/compare/behaviour-report.js +128 -0
- package/dist/features/compare/build-readings.d.ts +106 -0
- package/dist/features/compare/build-readings.js +250 -0
- package/dist/features/compare/compare-diff.d.ts +62 -0
- package/dist/features/compare/compare-diff.js +177 -0
- package/dist/features/compare/compare-report-html.d.ts +6 -0
- package/dist/features/compare/compare-report-html.js +102 -0
- package/dist/features/compare/compare-report.d.ts +15 -0
- package/dist/features/compare/compare-report.js +73 -0
- package/dist/features/compare/compare-run.d.ts +60 -0
- package/dist/features/compare/compare-run.js +123 -0
- package/dist/features/compare/outside/effect-lines.d.ts +26 -0
- package/dist/features/compare/outside/effect-lines.js +103 -0
- package/dist/features/compare/outside/state-sources.d.ts +76 -0
- package/dist/features/compare/outside/state-sources.js +164 -0
- package/dist/features/compare/outside/value-dictionary.d.ts +51 -0
- package/dist/features/compare/outside/value-dictionary.js +227 -0
- package/dist/features/compare/visible-content.d.ts +20 -0
- package/dist/features/compare/visible-content.js +125 -0
- package/dist/features/crawl/crawl-empty.js +2 -1
- package/dist/features/crawl/crawl-exhaustive.d.ts +55 -0
- package/dist/features/crawl/crawl-exhaustive.js +138 -0
- package/dist/features/crawl/crawl-tools.d.ts +9 -2
- package/dist/features/crawl/crawl-tools.js +28 -12
- package/dist/features/crawl/crawl.d.ts +8 -9
- package/dist/features/crawl/crawl.js +61 -65
- package/dist/features/exhaust/app-coverage.d.ts +5 -1
- package/dist/features/exhaust/app-coverage.js +3 -1
- package/dist/features/exhaust/code-coverage.js +8 -4
- package/dist/features/exhaust/edge-cases.d.ts +133 -0
- package/dist/features/exhaust/edge-cases.js +310 -0
- package/dist/features/exhaust/edges/edge-drive.d.ts +42 -0
- package/dist/features/exhaust/edges/edge-drive.js +364 -0
- package/dist/features/exhaust/edges/edge-run.d.ts +59 -0
- package/dist/features/exhaust/edges/edge-run.js +169 -0
- package/dist/features/exhaust/explorer.d.ts +87 -14
- package/dist/features/exhaust/explorer.js +159 -25
- package/dist/features/exhaust/failure-branches.d.ts +5 -0
- package/dist/features/exhaust/failure-branches.js +29 -11
- package/dist/features/exhaust/invariants.d.ts +112 -0
- package/dist/features/exhaust/invariants.js +188 -0
- package/dist/features/exhaust/ledger.d.ts +68 -0
- package/dist/features/exhaust/ledger.js +121 -13
- package/dist/features/exhaust/session-fold.d.ts +10 -0
- package/dist/features/exhaust/session-fold.js +46 -3
- package/dist/features/exhaust/session-port.d.ts +3 -1
- package/dist/features/exhaust/session-port.js +97 -28
- package/dist/features/grade/grade-answer.d.ts +99 -0
- package/dist/features/grade/grade-answer.js +112 -0
- package/dist/features/grade/grade-noise.d.ts +27 -0
- package/dist/features/grade/grade-noise.js +55 -0
- package/dist/features/grade/grade-renumber.d.ts +12 -0
- package/dist/features/grade/grade-renumber.js +86 -0
- package/dist/features/grade/grade-state.d.ts +50 -0
- package/dist/features/grade/grade-state.js +93 -0
- package/dist/features/grade/grade-suggest.d.ts +33 -0
- package/dist/features/grade/grade-suggest.js +98 -0
- package/dist/features/visual/visual-tools.js +66 -19
- package/dist/index.js +7 -4
- package/dist/judgement/capsule/causal-summary.js +9 -2
- package/dist/judgement/runs/artifact/replay-mapping.d.ts +13 -1
- package/dist/judgement/runs/artifact/replay-mapping.js +1 -1
- package/dist/judgement/runs/reticle-runner.js +16 -2
- package/dist/judgement/runs/runner-port.d.ts +2 -0
- package/dist/judgement/runs/runner-port.js +23 -5
- package/dist/language/flows/affected-tools.d.ts +1 -1
- package/dist/language/flows/affected-tools.js +49 -30
- package/dist/language/flows/change/git-changed.d.ts +2 -1
- package/dist/language/flows/change/git-changed.js +19 -2
- package/dist/language/flows/fields/flow-secret-field.d.ts +1 -13
- package/dist/language/flows/fields/flow-secret-field.js +77 -21
- package/dist/language/flows/flow-anchor.d.ts +60 -2
- package/dist/language/flows/flow-anchor.js +125 -20
- package/dist/language/flows/flow-classify.d.ts +20 -0
- package/dist/language/flows/flow-classify.js +90 -12
- package/dist/language/flows/flow-cross-step.d.ts +7 -1
- package/dist/language/flows/flow-cross-step.js +25 -1
- package/dist/language/flows/flow-journey.d.ts +38 -4
- package/dist/language/flows/flow-journey.js +138 -8
- package/dist/language/flows/flow-replay-run.d.ts +0 -16
- package/dist/language/flows/flow-replay-run.js +76 -14
- package/dist/language/flows/flow-replay-types.d.ts +4 -2
- package/dist/language/flows/flow-replay.d.ts +19 -1
- package/dist/language/flows/flow-replay.js +169 -52
- package/dist/language/flows/flow-replayable.js +7 -1
- package/dist/language/flows/flow-start-path.d.ts +34 -5
- package/dist/language/flows/flow-start-path.js +202 -15
- package/dist/language/flows/flow-step-runners.d.ts +14 -13
- package/dist/language/flows/flow-step-runners.js +184 -109
- package/dist/language/flows/flow-store-for-session.d.ts +26 -25
- package/dist/language/flows/flow-store-for-session.js +44 -0
- package/dist/language/flows/flow-tools.js +46 -33
- package/dist/language/flows/flows.js +176 -76
- package/dist/language/flows/recording/tape/recordings.d.ts +19 -3
- package/dist/language/flows/recording/tape/recordings.js +71 -5
- package/dist/language/flows/replay.d.ts +19 -5
- package/dist/language/flows/replay.js +99 -7
- package/dist/language/flows/steps/page-steps.d.ts +68 -0
- package/dist/language/flows/steps/page-steps.js +198 -0
- package/dist/language/flows/suite/sequential-run.d.ts +68 -0
- package/dist/language/flows/suite/sequential-run.js +96 -0
- package/dist/language/flows/verify-change-tools.js +26 -15
- package/dist/memory/cloud/background-sync.d.ts +13 -0
- package/dist/memory/cloud/background-sync.js +24 -3
- package/dist/memory/cloud/sync-daemon.js +26 -27
- package/dist/memory/journal/attach-journal.js +2 -1
- package/dist/memory/journal/drive-flow.d.ts +7 -0
- package/dist/memory/journal/drive-flow.js +44 -13
- package/dist/memory/journal/journal-recorder.d.ts +4 -2
- package/dist/memory/journal/session-journal.d.ts +7 -0
- package/dist/memory/journal/session-journal.js +58 -10
- package/dist/memory/project/root-claims.d.ts +9 -0
- package/dist/memory/project/root-claims.js +9 -4
- package/dist/portal/input/download-capture.d.ts +7 -0
- package/dist/portal/input/download-capture.js +92 -0
- package/dist/portal/input/network-detail.d.ts +6 -0
- package/dist/portal/input/network-detail.js +12 -1
- package/dist/portal/input/network-mock-tools.js +49 -13
- package/dist/portal/input/network-mock.d.ts +8 -0
- package/dist/portal/input/network-mock.js +28 -9
- package/dist/portal/input/page-capture.d.ts +24 -0
- package/dist/portal/input/page-capture.js +33 -0
- package/dist/portal/input/real-input.d.ts +22 -20
- package/dist/portal/input/real-input.js +41 -31
- package/dist/portal/input/viewport-tools.d.ts +8 -0
- package/dist/portal/input/viewport-tools.js +75 -18
- package/dist/portal/pool/browser-pool.d.ts +11 -29
- package/dist/portal/pool/browser-pool.js +35 -17
- package/dist/portal/pool/context-permissions.d.ts +17 -3
- package/dist/portal/pool/context-permissions.js +36 -5
- package/dist/portal/pool/determinism.d.ts +52 -0
- package/dist/portal/pool/determinism.js +259 -0
- package/dist/portal/pool/lease-emulation.d.ts +34 -1
- package/dist/portal/pool/lease-emulation.js +18 -0
- package/dist/portal/pool/lease-marker-stamp.d.ts +6 -0
- package/dist/portal/pool/lease-marker-stamp.js +15 -1
- package/dist/portal/pool/lease-page-error.d.ts +44 -17
- package/dist/portal/pool/lease-page-error.js +147 -5
- package/dist/portal/pool/page-error-journal.d.ts +7 -1
- package/dist/portal/pool/page-error-journal.js +27 -0
- package/dist/portal/pool/playwright-launcher.js +51 -8
- package/dist/portal/pool/pool-contract.d.ts +27 -1
- package/dist/portal/pool/pool-lease.d.ts +36 -1
- package/dist/portal/pool/zero-install.d.ts +8 -0
- package/dist/portal/pool/zero-install.js +23 -1
- package/dist/portal/session/dev-server/dev-command.d.ts +7 -6
- package/dist/portal/session/dev-server/dev-command.js +61 -15
- package/dist/portal/session/dev-server/dev-server-probe.d.ts +7 -0
- package/dist/portal/session/dev-server/dev-server-probe.js +9 -0
- package/dist/portal/session/dev-server/project-ports.d.ts +21 -0
- package/dist/portal/session/dev-server/project-ports.js +62 -0
- package/dist/portal/session/facts/departed-network.d.ts +66 -0
- package/dist/portal/session/facts/departed-network.js +142 -0
- package/dist/portal/session/no-session-diagnosis.d.ts +9 -0
- package/dist/portal/session/no-session-diagnosis.js +67 -25
- package/dist/portal/session/no-session-next-action.d.ts +2 -0
- package/dist/portal/session/no-session-next-action.js +12 -3
- package/dist/portal/session/no-session-watch.d.ts +6 -0
- package/dist/portal/session/no-session-watch.js +38 -3
- package/dist/portal/session/serving-directory.d.ts +10 -0
- package/dist/portal/session/serving-directory.js +39 -8
- package/dist/portal/session/session-manager.d.ts +10 -0
- package/dist/portal/session/session-manager.js +174 -63
- package/dist/start-wiring.d.ts +2 -1
- package/dist/start-wiring.js +2 -1
- package/dist/surface/mcp/mcp.js +3 -2
- package/dist/surface/mcp/shared-params.d.ts +2 -0
- package/dist/surface/mcp/shared-params.js +31 -0
- package/dist/surface/tools/act/act-observation.d.ts +40 -2
- package/dist/surface/tools/act/act-observation.js +118 -12
- package/dist/surface/tools/act/act-preflight.js +1 -1
- package/dist/surface/tools/act/act-sequence-retry.d.ts +23 -2
- package/dist/surface/tools/act/act-sequence-retry.js +61 -5
- package/dist/surface/tools/act/after-match.d.ts +4 -1
- package/dist/surface/tools/act/after-match.js +8 -1
- package/dist/surface/tools/act/already-true.js +1 -1
- package/dist/surface/tools/act/navigation/reload-result.d.ts +12 -1
- package/dist/surface/tools/act/navigation/reload-result.js +25 -2
- package/dist/surface/tools/act/resolve-target.d.ts +15 -1
- package/dist/surface/tools/act/resolve-target.js +24 -1
- package/dist/surface/tools/act/sequence-grade.d.ts +12 -0
- package/dist/surface/tools/act/sequence-grade.js +37 -26
- package/dist/surface/tools/act/settle-in-flight.js +27 -15
- package/dist/surface/tools/act-command.d.ts +23 -0
- package/dist/surface/tools/act-command.js +50 -0
- package/dist/surface/tools/act-sequence-tool.js +78 -50
- package/dist/surface/tools/act-tools.d.ts +1 -13
- package/dist/surface/tools/act-tools.js +74 -86
- package/dist/surface/tools/agent/build-staleness.d.ts +1 -1
- package/dist/surface/tools/agent/build-staleness.js +1 -1
- package/dist/surface/tools/assert/assert-verdict.js +16 -37
- package/dist/surface/tools/browser-tools.js +12 -6
- package/dist/surface/tools/compare-tools.d.ts +11 -0
- package/dist/surface/tools/compare-tools.js +94 -0
- package/dist/surface/tools/coverage-tools.js +1 -1
- package/dist/surface/tools/dynamic-tools.js +9 -2
- package/dist/surface/tools/error-recovery.d.ts +12 -1
- package/dist/surface/tools/error-recovery.js +91 -21
- package/dist/surface/tools/explore-tools.js +1 -1
- package/dist/surface/tools/invoke-tool.js +3 -2
- package/dist/surface/tools/lease-coverage-tools.d.ts +49 -0
- package/dist/surface/tools/lease-coverage-tools.js +128 -0
- package/dist/surface/tools/lease-hint.d.ts +35 -1
- package/dist/surface/tools/lease-hint.js +42 -0
- package/dist/surface/tools/lease-readiness.d.ts +26 -0
- package/dist/surface/tools/lease-readiness.js +79 -0
- package/dist/surface/tools/lease-seed.d.ts +14 -1
- package/dist/surface/tools/lease-seed.js +35 -3
- package/dist/surface/tools/lease-tools.d.ts +3 -41
- package/dist/surface/tools/lease-tools.js +62 -110
- package/dist/surface/tools/lease-url-identity.d.ts +46 -0
- package/dist/surface/tools/lease-url-identity.js +97 -0
- package/dist/surface/tools/merge-tools.js +99 -2
- package/dist/surface/tools/merged-name-redirect.js +5 -1
- package/dist/surface/tools/observe-tools.js +15 -9
- package/dist/surface/tools/read/console-pre-attach.d.ts +19 -0
- package/dist/surface/tools/read/console-pre-attach.js +23 -0
- package/dist/surface/tools/read-tools.js +33 -30
- package/dist/surface/tools/real-input-attempt.d.ts +0 -8
- package/dist/surface/tools/real-input-attempt.js +143 -28
- package/dist/surface/tools/session-ports.d.ts +14 -0
- package/dist/surface/tools/session-ports.js +53 -0
- package/dist/surface/tools/tool-kit.d.ts +6 -0
- package/dist/surface/tools/tool-kit.js +2 -3
- package/dist/surface/tools/tool-surface.js +0 -1
- package/dist/surface/tools/tools.js +28 -5
- package/dist/surface/tools/type-spelling.d.ts +13 -0
- package/dist/surface/tools/type-spelling.js +36 -0
- package/dist/telemetry/agent-client.d.ts +6 -0
- package/dist/telemetry/agent-client.js +10 -0
- package/dist/telemetry/argument-shape.js +1 -0
- package/dist/telemetry/tool-refused.d.ts +6 -3
- package/dist/telemetry/tool-refused.js +8 -3
- package/docs/actions.mdx +3 -3
- package/docs/agent-cheatsheet.md +1 -1
- package/docs/capabilities.mdx +61 -1
- package/docs/cli/affected.mdx +9 -7
- package/docs/cli/cloud.mdx +3 -3
- package/docs/cli/compare.mdx +94 -0
- package/docs/cli/doctor.mdx +1 -1
- package/docs/cli/grade.mdx +97 -0
- package/docs/cli/license.mdx +9 -7
- package/docs/cli/restart.mdx +7 -5
- package/docs/cli/rollback.mdx +5 -5
- package/docs/cli/stop.mdx +3 -3
- package/docs/cli/update.mdx +2 -2
- package/docs/cli/verify.mdx +23 -1
- package/docs/cli.mdx +33 -23
- package/docs/deploy-checks.md +1 -1
- package/docs/desktop.mdx +1 -1
- package/docs/enterprise.md +2 -2
- package/docs/environment.mdx +1 -1
- package/docs/flows.md +11 -1
- package/docs/frameworks.mdx +9 -9
- package/docs/getting-started.md +1 -1
- package/docs/install-manual.mdx +1 -1
- package/docs/license-activation.mdx +3 -3
- package/docs/packages/tauri.mdx +1 -1
- package/docs/packages.mdx +1 -1
- package/docs/predicates.mdx +1 -1
- package/docs/quickstart.mdx +68 -49
- package/docs/skill-setup.md +1 -1
- package/docs/skill.md +14 -13
- package/docs/telemetry.md +2 -2
- package/docs/tools/navigate.mdx +1 -1
- package/docs/tools/verify.mdx +4 -1
- package/docs/troubleshooting.mdx +1 -1
- package/docs/usage.md +17 -14
- package/package.json +13 -13
- package/dist/features/exhaust/exhaust-run.d.ts +0 -33
- package/dist/features/exhaust/exhaust-run.js +0 -82
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The CLI's link to a running daemon: an MCP client over the daemon's HTTP/SSE transport, the lease
|
|
3
|
+
* calls that open and close a tab, and the readers of a tool result.
|
|
4
|
+
*
|
|
5
|
+
* Split out of `adhoc-verdict.ts` so a caller that needs only the link (`reticle compare`, and the
|
|
6
|
+
* `reticle_verify { action: "compare" }` tool that runs the same compare) does not reach the
|
|
7
|
+
* browser launcher, and through it the installer.
|
|
8
|
+
*/
|
|
9
|
+
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
|
10
|
+
import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js';
|
|
11
|
+
import { LOOPBACK_HOST, MCP_SSE_PATH, ReticleTool, asString as asText } from '@reticlehq/core';
|
|
12
|
+
import { CLI_CLIENT_NAME } from '../../../judgement/runs/verification-sync.js';
|
|
13
|
+
import { TOKEN_QUERY_PARAM } from '../../../portal/bridge/token-auth.js';
|
|
14
|
+
import { LEASE_KEPT_SENTENCE, LEASE_RELEASED_SENTENCE } from '../../../surface/tools/lease-hint.js';
|
|
15
|
+
/** The lease tool is reached through `reticle_run`, the route that works on every tool profile. */
|
|
16
|
+
const LeaseAction = { ACQUIRE: 'acquire', RELEASE: 'release' };
|
|
17
|
+
/**
|
|
18
|
+
* Generous on purpose: on a machine with no Chrome, Edge or Playwright Chromium the FIRST lease
|
|
19
|
+
* downloads Chromium before it can open anything (see launch-chromium), which outlives the MCP
|
|
20
|
+
* client's 60s default and would report a timeout for a lease that was about to succeed.
|
|
21
|
+
*/
|
|
22
|
+
const LEASE_TIMEOUT_MS = 300_000;
|
|
23
|
+
export const leaseFailedLine = (url) => `could not open ${url} in a Reticle browser, so nothing was checked:`;
|
|
24
|
+
/**
|
|
25
|
+
* Why a lease's tab is not ready: its `notReadyReason`, then its `hint`, when it gave them. The CLI
|
|
26
|
+
* released the lease, so the hint's kept-lease sentence becomes the released-lease sentence.
|
|
27
|
+
*/
|
|
28
|
+
function notReadyLine(report) {
|
|
29
|
+
const reason = asText(report?.['notReadyReason']);
|
|
30
|
+
const hint = asText(report?.['hint'])?.replace(LEASE_KEPT_SENTENCE, LEASE_RELEASED_SENTENCE);
|
|
31
|
+
const head = reason === undefined ? 'the tab did not connect.' : `the tab did not connect (${reason}).`;
|
|
32
|
+
return hint === undefined ? head : `${head} ${hint}`;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Open `url` in a Reticle-leased browser unconditionally: `{ leased }` names the lease, which the
|
|
36
|
+
* caller hands to `releaseLease`; `{ failed }` is the lease's own refusal, as lines to print.
|
|
37
|
+
* `init`'s connect proof on a machine with no system browser takes this path directly.
|
|
38
|
+
*/
|
|
39
|
+
export async function acquireLease(caller, url,
|
|
40
|
+
/** Open it in a window somebody can watch, whatever the daemon was started as. */
|
|
41
|
+
headed,
|
|
42
|
+
/** The `.reticle` the lease's runs belong in: the caller's project, not wherever the page resolves. */
|
|
43
|
+
root) {
|
|
44
|
+
const ask = (withRoot) => caller.call(ReticleTool.RUN, {
|
|
45
|
+
tool: ReticleTool.LEASE,
|
|
46
|
+
args: {
|
|
47
|
+
action: LeaseAction.ACQUIRE,
|
|
48
|
+
url,
|
|
49
|
+
...(true === headed ? { headed } : {}),
|
|
50
|
+
...(withRoot && root !== undefined ? { root } : {}),
|
|
51
|
+
},
|
|
52
|
+
}, LEASE_TIMEOUT_MS);
|
|
53
|
+
let acquired = await ask(true);
|
|
54
|
+
// A daemon from an older release does not know `root` and refuses the whole call, so the page
|
|
55
|
+
// was never opened. Ask once more without it: the lease works, only its runs land where that
|
|
56
|
+
// daemon puts them.
|
|
57
|
+
if (root !== undefined && REFUSES_ROOT.test(refusalOf(acquired) ?? '')) {
|
|
58
|
+
acquired = await ask(false);
|
|
59
|
+
}
|
|
60
|
+
const report = verdictOf(acquired, 'sessionId');
|
|
61
|
+
const sessionId = report?.['sessionId'];
|
|
62
|
+
// `ready: false` still carries a sessionId, but no session is registered under it. Driving it
|
|
63
|
+
// answered "names no session" and hid the cause the lease gave. Only an explicit false counts.
|
|
64
|
+
if ('string' === typeof sessionId && false === report?.['ready']) {
|
|
65
|
+
await releaseLease(caller, sessionId);
|
|
66
|
+
return { failed: [leaseFailedLine(url), notReadyLine(report)] };
|
|
67
|
+
}
|
|
68
|
+
// `zeroInstall`: the page never dialled in, so the lease supplied Reticle's own reader.
|
|
69
|
+
if ('string' === typeof sessionId) {
|
|
70
|
+
return { leased: sessionId, zeroInstall: true === report?.['zeroInstall'] };
|
|
71
|
+
}
|
|
72
|
+
return {
|
|
73
|
+
failed: [leaseFailedLine(url), refusalOf(acquired) ?? JSON.stringify(acquired)],
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
/** The lease refusal an older daemon answers when it does not know the `root` argument. */
|
|
77
|
+
const REFUSES_ROOT = /unknown parameter[^\n]*\broot\b/i;
|
|
78
|
+
/** What the lease said no with: the `error` of its payload, else the refusal text. */
|
|
79
|
+
function refusalOf(acquired) {
|
|
80
|
+
const why = verdictOf(acquired, 'error')?.['error'];
|
|
81
|
+
return 'string' === typeof why ? why : refusalText(acquired);
|
|
82
|
+
}
|
|
83
|
+
/** Hand back a lease `leaseIfNoTab` opened. Best-effort: an expired lease is already gone. */
|
|
84
|
+
export async function releaseLease(caller, sessionId) {
|
|
85
|
+
await caller
|
|
86
|
+
.call(ReticleTool.RUN, {
|
|
87
|
+
tool: ReticleTool.LEASE,
|
|
88
|
+
args: { action: LeaseAction.RELEASE, sessionId },
|
|
89
|
+
})
|
|
90
|
+
.catch(() => undefined);
|
|
91
|
+
}
|
|
92
|
+
export function endpointFor(port, token) {
|
|
93
|
+
const url = new URL(`http://${LOOPBACK_HOST}:${String(port)}${MCP_SSE_PATH}`);
|
|
94
|
+
if (token !== undefined && token.length > 0) {
|
|
95
|
+
url.searchParams.set(TOKEN_QUERY_PARAM, token);
|
|
96
|
+
}
|
|
97
|
+
return url;
|
|
98
|
+
}
|
|
99
|
+
export async function connectOverSse(endpoint) {
|
|
100
|
+
const client = new Client({ name: CLI_CLIENT_NAME, version: '1' }, { capabilities: {} });
|
|
101
|
+
await client.connect(new SSEClientTransport(endpoint));
|
|
102
|
+
return {
|
|
103
|
+
call: async (name, args, timeoutMs) => await client.callTool({ name, arguments: args }, undefined, timeoutMs === undefined ? undefined : { timeout: timeoutMs }),
|
|
104
|
+
close: async () => {
|
|
105
|
+
await client.close();
|
|
106
|
+
},
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* The report object an MCP tool result carries, or undefined when the shape is not one.
|
|
111
|
+
*
|
|
112
|
+
* `field` is the key that makes a payload the report this caller wants: `verified` for a one-shot
|
|
113
|
+
* verdict, `status` for a suite. It is a parameter rather than a fixed name because the suite path
|
|
114
|
+
* reproduced this function's own incident the moment it reused it — the headline printed
|
|
115
|
+
* `unverifiable` while the JSON under it said `"status":"fail"` with 74 failing flows, because a
|
|
116
|
+
* suite report has no `verified` key and the text reader would only accept one that did.
|
|
117
|
+
*/
|
|
118
|
+
export function verdictOf(result, field = 'verified') {
|
|
119
|
+
const structured = result?.structuredContent;
|
|
120
|
+
if ('object' === typeof structured && null !== structured) {
|
|
121
|
+
return structured;
|
|
122
|
+
}
|
|
123
|
+
// A daemon that answers with the verdict as TEXT left this undefined, so the headline read
|
|
124
|
+
// `verified: unknown` while the JSON printed underneath it said `"verified":"no"` — one response
|
|
125
|
+
// giving two answers to the same question. The text is the same object, so read it.
|
|
126
|
+
return verdictFromText(result, field);
|
|
127
|
+
}
|
|
128
|
+
/** The report object inside an MCP text part, when the result carried it there instead. */
|
|
129
|
+
function verdictFromText(result, field) {
|
|
130
|
+
const content = result?.content;
|
|
131
|
+
if (!Array.isArray(content)) {
|
|
132
|
+
return undefined;
|
|
133
|
+
}
|
|
134
|
+
for (const part of content) {
|
|
135
|
+
const text = part.text;
|
|
136
|
+
if ('string' !== typeof text) {
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
try {
|
|
140
|
+
const parsed = JSON.parse(text);
|
|
141
|
+
// Only a shape that actually carries a verdict — anything else stays `unknown`, which is the
|
|
142
|
+
// honest answer for a result this function could not read.
|
|
143
|
+
if ('object' === typeof parsed && null !== parsed && field in parsed) {
|
|
144
|
+
return parsed;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
catch {
|
|
148
|
+
/* Not JSON; the next part may still be. */
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
return undefined;
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* The tool's own words when it refused, rather than the envelope carrying them.
|
|
155
|
+
*
|
|
156
|
+
* A refusal from these tools is the most useful text they produce — it names the cause and the next
|
|
157
|
+
* action — and printing the MCP result object around it buries that in JSON escaping. The reader is
|
|
158
|
+
* at a terminal; give them the sentence.
|
|
159
|
+
*/
|
|
160
|
+
export function refusalText(result) {
|
|
161
|
+
const r = result;
|
|
162
|
+
if (true !== r?.isError || !Array.isArray(r.content)) {
|
|
163
|
+
return undefined;
|
|
164
|
+
}
|
|
165
|
+
const text = r.content
|
|
166
|
+
.map((part) => part.text)
|
|
167
|
+
.filter((t) => 'string' === typeof t)
|
|
168
|
+
.join('\n');
|
|
169
|
+
if (0 === text.length) {
|
|
170
|
+
return undefined;
|
|
171
|
+
}
|
|
172
|
+
// The tools answer with a JSON envelope whose `error` is the sentence; unwrap when it is one.
|
|
173
|
+
try {
|
|
174
|
+
const parsed = JSON.parse(text);
|
|
175
|
+
const inner = parsed?.error;
|
|
176
|
+
return 'string' === typeof inner ? inner : text;
|
|
177
|
+
}
|
|
178
|
+
catch {
|
|
179
|
+
return text;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
/** The payload of a tool result: its structured content, or the JSON object in its text. */
|
|
183
|
+
export function payloadOf(result) {
|
|
184
|
+
const structured = result?.structuredContent;
|
|
185
|
+
if ('object' === typeof structured && null !== structured) {
|
|
186
|
+
return structured;
|
|
187
|
+
}
|
|
188
|
+
const content = result?.content;
|
|
189
|
+
for (const part of Array.isArray(content) ? content : []) {
|
|
190
|
+
const text = part.text;
|
|
191
|
+
if ('string' !== typeof text) {
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
try {
|
|
195
|
+
const parsed = JSON.parse(text);
|
|
196
|
+
if ('object' === typeof parsed && null !== parsed) {
|
|
197
|
+
return parsed;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
catch {
|
|
201
|
+
// Not JSON; the next part may be.
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
return {};
|
|
205
|
+
}
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
* at the call site.
|
|
15
15
|
*/
|
|
16
16
|
import { describeSkew } from '../../version/version-skew.js';
|
|
17
|
+
import { mcpConnectionHint } from '../answers/mcp-connection-hint.js';
|
|
17
18
|
import { DoctorRow, doctorRow } from './doctor-rows.js';
|
|
18
19
|
/**
|
|
19
20
|
* `reticle stop`, not `reticle kill`.
|
|
@@ -34,14 +35,16 @@ const FIX = 'Restart it so both ends are the same build: `reticle stop`, then le
|
|
|
34
35
|
*/
|
|
35
36
|
export function daemonLine(port, pid, peer, self) {
|
|
36
37
|
const parts = [];
|
|
37
|
-
if (null !== pid)
|
|
38
|
+
if (null !== pid) {
|
|
38
39
|
parts.push(`pid ${String(pid)}`);
|
|
39
|
-
|
|
40
|
+
}
|
|
41
|
+
if (peer.version !== undefined) {
|
|
40
42
|
parts.push(`v${peer.version}`);
|
|
43
|
+
}
|
|
41
44
|
const detail = 0 === parts.length ? '' : ` (${parts.join(', ')})`;
|
|
42
45
|
const skew = describeSkew({ what: 'the daemon on this port', version: peer.version, contract: peer.contract, fix: FIX }, self);
|
|
43
46
|
const line = {
|
|
44
|
-
text: doctorRow(DoctorRow.DAEMON, `✓ running on :${String(port)}${detail}`),
|
|
47
|
+
text: doctorRow(DoctorRow.DAEMON, `✓ running on :${String(port)}${detail}. ${mcpConnectionHint(port)}`),
|
|
45
48
|
};
|
|
46
49
|
return skew === undefined ? line : { ...line, skew };
|
|
47
50
|
}
|
|
@@ -50,7 +50,7 @@ export function wedgedDaemonRepair(port, deps) {
|
|
|
50
50
|
return {
|
|
51
51
|
row: DoctorRow.DAEMON,
|
|
52
52
|
what: 'a Reticle daemon that does not answer',
|
|
53
|
-
step: `reticle restart --port ${String(port)}`,
|
|
53
|
+
step: `reticle serve --restart --port ${String(port)}`,
|
|
54
54
|
needed: async () => PortPresence.FOREIGN === (await deps.presence()) && true === deps.holder()?.reticleDaemon,
|
|
55
55
|
apply: async () => {
|
|
56
56
|
await deps.stop();
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `reticle open` and every other flow that needs a daemon on `port`.
|
|
3
|
+
*
|
|
4
|
+
* The retry: a FOREIGN reading seconds after `reticle restart` is usually OUR OWN daemon still
|
|
5
|
+
* answering its first `/status` — the 1 s `/status` window and the 500 ms TCP window do not
|
|
6
|
+
* survive a cold Windows start, and the refusal then names a holder it could not identify, while
|
|
7
|
+
* `reticle status` run moments later shows the daemon fine. The pid file names what `restart`
|
|
8
|
+
* started; while that process is alive the port is probed once more before refusing. A stranger's
|
|
9
|
+
* pid is never in the file, so the refusal keeps its teeth for the stranger case it was written
|
|
10
|
+
* for.
|
|
11
|
+
*
|
|
12
|
+
* Probes, the pid file and the spawn are injected so the rule is testable without a socket — the
|
|
13
|
+
* same shape `probePresence`'s own callers use.
|
|
14
|
+
*/
|
|
15
|
+
import { PortPresence } from '../daemon/binding/port-presence.js';
|
|
16
|
+
export interface EnsureDaemonDeps {
|
|
17
|
+
/** The classified presence probe. Defaults to the shared TCP + `/status` pair. */
|
|
18
|
+
probe?: (port: number) => Promise<PortPresence>;
|
|
19
|
+
readPid?: (port: number) => number | null;
|
|
20
|
+
isAlive?: (pid: number) => boolean;
|
|
21
|
+
/** How long to wait before the second probe. */
|
|
22
|
+
retryDelayMs?: number;
|
|
23
|
+
spawnDaemon?: (port: number) => Promise<void>;
|
|
24
|
+
onUsable?: (port: number) => Promise<void> | void;
|
|
25
|
+
}
|
|
26
|
+
export declare function ensureDaemon(port: number, deps?: EnsureDaemonDeps): Promise<void>;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `reticle open` and every other flow that needs a daemon on `port`.
|
|
3
|
+
*
|
|
4
|
+
* The retry: a FOREIGN reading seconds after `reticle restart` is usually OUR OWN daemon still
|
|
5
|
+
* answering its first `/status` — the 1 s `/status` window and the 500 ms TCP window do not
|
|
6
|
+
* survive a cold Windows start, and the refusal then names a holder it could not identify, while
|
|
7
|
+
* `reticle status` run moments later shows the daemon fine. The pid file names what `restart`
|
|
8
|
+
* started; while that process is alive the port is probed once more before refusing. A stranger's
|
|
9
|
+
* pid is never in the file, so the refusal keeps its teeth for the stranger case it was written
|
|
10
|
+
* for.
|
|
11
|
+
*
|
|
12
|
+
* Probes, the pid file and the spawn are injected so the rule is testable without a socket — the
|
|
13
|
+
* same shape `probePresence`'s own callers use.
|
|
14
|
+
*/
|
|
15
|
+
import { PortPresence, probePresence, presenceIsUsable, } from '../daemon/binding/port-presence.js';
|
|
16
|
+
import { describePortState } from '../daemon/port-state.js';
|
|
17
|
+
import { probeDaemon, waitForDaemon } from '../../surface/mcp/mcp-proxy.js';
|
|
18
|
+
import { fetchStatus } from '../daemon/binding/daemon-status-probe.js';
|
|
19
|
+
import { isAlive, readPid, spawnDaemon } from '../daemon/daemon.js';
|
|
20
|
+
import { warnOnDaemonSkew } from './launch/cli-launch.js';
|
|
21
|
+
import { DAEMON_INNER_COMMAND, PORT_FLAG } from './cli-parse.js';
|
|
22
|
+
/** The probe pair every daemon-touching command shares. */
|
|
23
|
+
const REAL_PROBES = {
|
|
24
|
+
tcpOpen: (port) => probeDaemon(port),
|
|
25
|
+
status: (port) => fetchStatus(port),
|
|
26
|
+
};
|
|
27
|
+
export async function ensureDaemon(port, deps = {}) {
|
|
28
|
+
const probe = deps.probe ?? (() => probePresence(port, REAL_PROBES));
|
|
29
|
+
const readRecordedPid = deps.readPid ?? readPid;
|
|
30
|
+
const pidIsAlive = deps.isAlive ?? isAlive;
|
|
31
|
+
const spawn = deps.spawnDaemon ?? spawnRealDaemon;
|
|
32
|
+
const onUsable = deps.onUsable ?? warnOnDaemonSkew;
|
|
33
|
+
const retryDelayMs = deps.retryDelayMs ?? 1_500;
|
|
34
|
+
let presence = await probe(port);
|
|
35
|
+
if (PortPresence.FOREIGN === presence) {
|
|
36
|
+
const recorded = readRecordedPid(port);
|
|
37
|
+
if (recorded !== null && pidIsAlive(recorded)) {
|
|
38
|
+
await new Promise((resolve) => setTimeout(resolve, retryDelayMs));
|
|
39
|
+
presence = await probe(port);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
if (presenceIsUsable(presence)) {
|
|
43
|
+
return void (await onUsable(port));
|
|
44
|
+
}
|
|
45
|
+
if (PortPresence.FREE !== presence) {
|
|
46
|
+
throw new Error(describePortState(presence, port));
|
|
47
|
+
}
|
|
48
|
+
await spawn(port);
|
|
49
|
+
}
|
|
50
|
+
async function spawnRealDaemon(port) {
|
|
51
|
+
const scriptPath = process.argv[1];
|
|
52
|
+
if (scriptPath === undefined) {
|
|
53
|
+
throw new Error('cannot locate the reticle daemon script');
|
|
54
|
+
}
|
|
55
|
+
spawnDaemon(process.execPath, scriptPath, [DAEMON_INNER_COMMAND, PORT_FLAG, String(port)], port);
|
|
56
|
+
await waitForDaemon(port);
|
|
57
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { type StateIo } from '../../../features/compare/outside/state-sources.js';
|
|
2
|
+
import { type GradeArgs } from '../cli-parse-grade.js';
|
|
3
|
+
export declare const GradeExit: {
|
|
4
|
+
readonly PASS: 0;
|
|
5
|
+
readonly FAIL: 1;
|
|
6
|
+
readonly UNVERIFIED: 2;
|
|
7
|
+
};
|
|
8
|
+
/** The most difference lines printed as text. `--json` gives every line. */
|
|
9
|
+
export declare const MAX_TEXT_LINES = 10;
|
|
10
|
+
type Ok = Extract<GradeArgs, {
|
|
11
|
+
kind: 'ok';
|
|
12
|
+
}>;
|
|
13
|
+
export interface GradeOut {
|
|
14
|
+
line: (text: string) => void;
|
|
15
|
+
exit: (code: number) => void;
|
|
16
|
+
}
|
|
17
|
+
export interface GradeIo {
|
|
18
|
+
state: StateIo;
|
|
19
|
+
readText: (path: string) => Promise<string>;
|
|
20
|
+
writeText: (path: string, text: string) => void;
|
|
21
|
+
}
|
|
22
|
+
export declare function handleGrade(args: Ok, out: GradeOut, io?: GradeIo): Promise<void>;
|
|
23
|
+
export {};
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `reticle grade`: grade a finished run of an app from snapshots of its stored data.
|
|
3
|
+
*
|
|
4
|
+
* - `snapshot` reads the declared state sources (and the page the run ended on) into a file.
|
|
5
|
+
* - `judge` compares a candidate snapshot with two reference snapshots, and checks the final answer
|
|
6
|
+
* against a source read at grade time.
|
|
7
|
+
* - `tables` writes a state declaration with one source for each table of a database.
|
|
8
|
+
* - `suggest` removes the sources that change between two runs of the same correct steps.
|
|
9
|
+
*
|
|
10
|
+
* It needs no daemon and no browser. The rules are in `@/features/grade/`.
|
|
11
|
+
*/
|
|
12
|
+
import { mkdirSync, writeFileSync } from 'node:fs';
|
|
13
|
+
import { readFile } from 'node:fs/promises';
|
|
14
|
+
import { dirname } from 'node:path';
|
|
15
|
+
import { MSG_READ_FAILED, readStateSources, sourceRows, StateDeclarationSchema, } from '../../../features/compare/outside/state-sources.js';
|
|
16
|
+
import { checkAnswer, ExpectSchema } from '../../../features/grade/grade-answer.js';
|
|
17
|
+
import { GradeVerdict, gradeState, SnapshotSchema, urlLine, } from '../../../features/grade/grade-state.js';
|
|
18
|
+
import { quietSources, tableSources } from '../../../features/grade/grade-suggest.js';
|
|
19
|
+
import { GradeStep } from '../cli-parse-grade.js';
|
|
20
|
+
import { hostStateIo } from '../two-builds/compare-command.js';
|
|
21
|
+
export const GradeExit = { PASS: 0, FAIL: 1, UNVERIFIED: 2 };
|
|
22
|
+
const EXIT = {
|
|
23
|
+
[GradeVerdict.PASS]: GradeExit.PASS,
|
|
24
|
+
[GradeVerdict.FAIL]: GradeExit.FAIL,
|
|
25
|
+
[GradeVerdict.UNVERIFIED]: GradeExit.UNVERIFIED,
|
|
26
|
+
};
|
|
27
|
+
/** The most difference lines printed as text. `--json` gives every line. */
|
|
28
|
+
export const MAX_TEXT_LINES = 10;
|
|
29
|
+
const UTF8 = 'utf8';
|
|
30
|
+
const COLUMN = '\t';
|
|
31
|
+
const LINE_BREAK = /\r?\n/;
|
|
32
|
+
const SKIP_SEPARATOR = ',';
|
|
33
|
+
const reasonOf = (error) => error instanceof Error ? error.message : String(error);
|
|
34
|
+
function writeFile(path, text) {
|
|
35
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
36
|
+
writeFileSync(path, text);
|
|
37
|
+
}
|
|
38
|
+
const defaultIo = {
|
|
39
|
+
state: hostStateIo,
|
|
40
|
+
readText: (path) => readFile(path, UTF8),
|
|
41
|
+
writeText: writeFile,
|
|
42
|
+
};
|
|
43
|
+
const json = (value) => `${JSON.stringify(value, null, 2)}\n`;
|
|
44
|
+
async function readSnapshot(io, path) {
|
|
45
|
+
const parsed = SnapshotSchema.safeParse(JSON.parse(await io.readText(path)));
|
|
46
|
+
if (!parsed.success) {
|
|
47
|
+
throw new Error(`${path} is not a grade snapshot: ${parsed.error.message}`);
|
|
48
|
+
}
|
|
49
|
+
return parsed.data;
|
|
50
|
+
}
|
|
51
|
+
/** The sources of a state declaration file, or why they could not be read. */
|
|
52
|
+
async function declarationOf(io, path) {
|
|
53
|
+
try {
|
|
54
|
+
const parsed = StateDeclarationSchema.safeParse(JSON.parse(await io.readText(path)));
|
|
55
|
+
return parsed.success ? parsed.data.sources : { error: parsed.error.message };
|
|
56
|
+
}
|
|
57
|
+
catch (error) {
|
|
58
|
+
return { error: reasonOf(error) };
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
/** At most `MAX_TEXT_LINES` lines, then a line that says how many were left out. */
|
|
62
|
+
function capped(sign, lines) {
|
|
63
|
+
const shown = lines.slice(0, MAX_TEXT_LINES).map((line) => ` ${sign} ${line}`);
|
|
64
|
+
const rest = lines.length - shown.length;
|
|
65
|
+
return rest > 0 ? [...shown, ` ... and ${String(rest)} more (--json lists every line)`] : shown;
|
|
66
|
+
}
|
|
67
|
+
async function snapshot(args, io, out) {
|
|
68
|
+
const sources = await declarationOf(io, args.state ?? '');
|
|
69
|
+
if ('error' in sources) {
|
|
70
|
+
out.line(`Could not read the state sources in ${args.state ?? ''}: ${sources.error}`);
|
|
71
|
+
out.exit(GradeExit.UNVERIFIED);
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
// No row cap: the grade diff is linear, and a cut row would not name its record.
|
|
75
|
+
const lines = await readStateSources(sources, io.state, Number.POSITIVE_INFINITY);
|
|
76
|
+
if (args.url !== undefined) {
|
|
77
|
+
lines.push(urlLine(args.url));
|
|
78
|
+
}
|
|
79
|
+
io.writeText(args.out ?? '', json({ lines }));
|
|
80
|
+
const failed = lines.filter((line) => line.startsWith(MSG_READ_FAILED));
|
|
81
|
+
out.line(`Snapshot: ${args.out ?? ''}: ${String(lines.length)} lines`);
|
|
82
|
+
failed.forEach((line) => out.line(` ${line}`));
|
|
83
|
+
out.exit(failed.length > 0 ? GradeExit.UNVERIFIED : GradeExit.PASS);
|
|
84
|
+
}
|
|
85
|
+
async function answerOf(args, io) {
|
|
86
|
+
if (args.expect === undefined) {
|
|
87
|
+
return {};
|
|
88
|
+
}
|
|
89
|
+
const parsed = ExpectSchema.safeParse(JSON.parse(await io.readText(args.expect)));
|
|
90
|
+
if (!parsed.success) {
|
|
91
|
+
return { unread: `${args.expect}: ${parsed.error.message}` };
|
|
92
|
+
}
|
|
93
|
+
const spec = parsed.data.answer;
|
|
94
|
+
let rows;
|
|
95
|
+
try {
|
|
96
|
+
rows = await sourceRows(spec.source, io.state);
|
|
97
|
+
}
|
|
98
|
+
catch (error) {
|
|
99
|
+
return { unread: `answer source ${spec.source.label}: ${reasonOf(error)}` };
|
|
100
|
+
}
|
|
101
|
+
return { check: checkAnswer(rows, args.answer, spec) };
|
|
102
|
+
}
|
|
103
|
+
async function judge(args, io, out) {
|
|
104
|
+
const [reference, again, candidate] = await Promise.all([args.reference, args.again, args.candidate].map((p) => readSnapshot(io, p ?? '')));
|
|
105
|
+
if (reference === undefined || again === undefined || candidate === undefined) {
|
|
106
|
+
throw new Error('a snapshot is missing');
|
|
107
|
+
}
|
|
108
|
+
const state = gradeState(reference, again, candidate);
|
|
109
|
+
const answer = await answerOf(args, io);
|
|
110
|
+
const unread = [...state.unread, ...(answer.unread === undefined ? [] : [answer.unread])];
|
|
111
|
+
let verdict = GradeVerdict.PASS;
|
|
112
|
+
if (unread.length > 0) {
|
|
113
|
+
verdict = GradeVerdict.UNVERIFIED;
|
|
114
|
+
}
|
|
115
|
+
else if (state.verdict === GradeVerdict.FAIL || false === answer.check?.ok) {
|
|
116
|
+
verdict = GradeVerdict.FAIL;
|
|
117
|
+
}
|
|
118
|
+
const result = {
|
|
119
|
+
verdict,
|
|
120
|
+
state: { removed: state.removed, added: state.added, ignored: state.ignored },
|
|
121
|
+
...(answer.check === undefined ? {} : { answer: answer.check }),
|
|
122
|
+
unread,
|
|
123
|
+
};
|
|
124
|
+
if (args.json) {
|
|
125
|
+
out.line(JSON.stringify(result));
|
|
126
|
+
}
|
|
127
|
+
else {
|
|
128
|
+
out.line(verdict.toUpperCase());
|
|
129
|
+
unread.forEach((line) => out.line(` unread: ${line}`));
|
|
130
|
+
if (answer.check !== undefined) {
|
|
131
|
+
out.line(answer.check.ok
|
|
132
|
+
? ` answer: correct (${answer.check.expected})`
|
|
133
|
+
: ` answer: wrong: ${answer.check.reason ?? ''}`);
|
|
134
|
+
}
|
|
135
|
+
out.line(` state: ${String(state.removed.length + state.added.length)} lines differ, ${String(state.ignored)} ignored as noise`);
|
|
136
|
+
capped('-', state.removed).forEach(out.line);
|
|
137
|
+
capped('+', state.added).forEach(out.line);
|
|
138
|
+
}
|
|
139
|
+
out.exit(EXIT[verdict]);
|
|
140
|
+
}
|
|
141
|
+
async function tables(args, io, out) {
|
|
142
|
+
const listed = await io.state.run(args.list ?? '', args.container);
|
|
143
|
+
const names = listed.split(LINE_BREAK).map((line) => line.split(COLUMN)[0] ?? '');
|
|
144
|
+
const skip = (args.skip ?? '').split(SKIP_SEPARATOR).filter((prefix) => '' !== prefix);
|
|
145
|
+
const result = tableSources(names, args.rows ?? '', args.container, skip);
|
|
146
|
+
io.writeText(args.out ?? '', json({ sources: result.sources }));
|
|
147
|
+
out.line(`Declaration: ${args.out ?? ''}: ${String(result.sources.length)} sources`);
|
|
148
|
+
result.skipped.forEach((s) => out.line(` skipped ${s.label}: ${s.reason}`));
|
|
149
|
+
out.exit(GradeExit.PASS);
|
|
150
|
+
}
|
|
151
|
+
async function suggest(args, io, out) {
|
|
152
|
+
const sources = await declarationOf(io, args.state ?? '');
|
|
153
|
+
if ('error' in sources) {
|
|
154
|
+
out.line(`Could not read the state sources in ${args.state ?? ''}: ${sources.error}`);
|
|
155
|
+
out.exit(GradeExit.UNVERIFIED);
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
const reference = await readSnapshot(io, args.reference ?? '');
|
|
159
|
+
const again = await readSnapshot(io, args.again ?? '');
|
|
160
|
+
const result = quietSources(sources, reference.lines, again.lines);
|
|
161
|
+
io.writeText(args.out ?? '', json({ sources: result.sources }));
|
|
162
|
+
out.line(`Declaration: ${args.out ?? ''}: kept ${String(result.sources.length)} of ${String(sources.length)} sources`);
|
|
163
|
+
result.dropped.forEach((d) => out.line(` dropped ${d.label}: ${d.reason}`));
|
|
164
|
+
out.exit(GradeExit.PASS);
|
|
165
|
+
}
|
|
166
|
+
const STEPS = {
|
|
167
|
+
[GradeStep.SNAPSHOT]: snapshot,
|
|
168
|
+
[GradeStep.JUDGE]: judge,
|
|
169
|
+
[GradeStep.TABLES]: tables,
|
|
170
|
+
[GradeStep.SUGGEST]: suggest,
|
|
171
|
+
};
|
|
172
|
+
export async function handleGrade(args, out, io = defaultIo) {
|
|
173
|
+
try {
|
|
174
|
+
await STEPS[args.step](args, io, out);
|
|
175
|
+
}
|
|
176
|
+
catch (error) {
|
|
177
|
+
out.line(`UNVERIFIED\n ${reasonOf(error)}`);
|
|
178
|
+
out.exit(GradeExit.UNVERIFIED);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
@@ -24,7 +24,7 @@ export declare function handleServe(parsed: {
|
|
|
24
24
|
}): void;
|
|
25
25
|
export declare function handleStop(port: number, quiet: boolean): void;
|
|
26
26
|
/**
|
|
27
|
-
* `reticle restart` — free the port, then put a daemon back on it and prove it bound.
|
|
27
|
+
* `reticle serve --restart` — free the port, then put a daemon back on it and prove it bound.
|
|
28
28
|
*
|
|
29
29
|
* The two halves are reported separately on purpose. A restart that killed the old daemon and then
|
|
30
30
|
* could not start a new one leaves the user in a different place from one that could not kill
|
|
@@ -197,7 +197,7 @@ function waitForStop(port, pid, quiet) {
|
|
|
197
197
|
}, 100);
|
|
198
198
|
}
|
|
199
199
|
/**
|
|
200
|
-
* `reticle restart` — free the port, then put a daemon back on it and prove it bound.
|
|
200
|
+
* `reticle serve --restart` — free the port, then put a daemon back on it and prove it bound.
|
|
201
201
|
*
|
|
202
202
|
* The two halves are reported separately on purpose. A restart that killed the old daemon and then
|
|
203
203
|
* could not start a new one leaves the user in a different place from one that could not kill
|
|
@@ -55,6 +55,15 @@ ourPid?: number | null): string;
|
|
|
55
55
|
export declare function findPortHolder(port: number, exec: (command: string, args: readonly string[]) => string | null): PortHolder | null;
|
|
56
56
|
/** The path in `lsof -a -p <pid> -d cwd -Fn` output, or null when it printed none. */
|
|
57
57
|
export declare function parseProcessCwd(stdout: string): string | null;
|
|
58
|
+
/** Each port in `lsof -nP -iTCP -sTCP:LISTEN -Fn` output, once. A dual-stack bind prints two lines. */
|
|
59
|
+
export declare function parseListeningPorts(stdout: string): number[];
|
|
60
|
+
/**
|
|
61
|
+
* Every TCP port on this machine with a listener, or nothing when the lookup cannot run.
|
|
62
|
+
*
|
|
63
|
+
* Off on Windows. A process there shows no working directory, so `findListenerCwd` starts PowerShell
|
|
64
|
+
* for each port, and the daemon blocks for each start.
|
|
65
|
+
*/
|
|
66
|
+
export declare function findListeningPorts(exec: (command: string, args: readonly string[]) => string | null, platform?: NodeJS.Platform): number[];
|
|
58
67
|
/**
|
|
59
68
|
* The folder that holds the `node_modules` a command line runs from, or null.
|
|
60
69
|
*
|
|
@@ -93,7 +93,7 @@ ourPid) {
|
|
|
93
93
|
// Naming the command matters more than naming the process. "Stop that process" describes an
|
|
94
94
|
// outcome and leaves the reader to invent a route to it, and the route they invent is the `-ti`
|
|
95
95
|
// pipeline that takes their agent's proxy down with the holder.
|
|
96
|
-
const tail = 'a daemon cannot bind it. Free it with `reticle restart --force` (it signals the listener only, ' +
|
|
96
|
+
const tail = 'a daemon cannot bind it. Free it with `reticle serve --restart --force` (it signals the listener only, ' +
|
|
97
97
|
"never the agent's mcp proxy), or run Reticle on a different port (`--port`), then retry.";
|
|
98
98
|
if (null === holder) {
|
|
99
99
|
/**
|
|
@@ -117,11 +117,11 @@ ourPid) {
|
|
|
117
117
|
return `port ${String(port)} is held by a process Reticle could not identify — ${tail}${ours}`;
|
|
118
118
|
}
|
|
119
119
|
if (true === holder.reticleDaemon && ourPid !== holder.pid) {
|
|
120
|
-
// Not the pid we recorded, so `reticle stop` cannot find it. `restart` finds the listener.
|
|
120
|
+
// Not the pid we recorded, so `reticle stop` cannot find it. `serve --restart` finds the listener.
|
|
121
121
|
// It did not answer a short probe. A busy daemon does that too, and other agents can use it.
|
|
122
122
|
return (`port ${String(port)} is held by a Reticle daemon (pid ${String(holder.pid)}) that did not ` +
|
|
123
123
|
'answer in time. It may be busy. Run `reticle status` again in a few seconds. If it stays ' +
|
|
124
|
-
'silent, run `reticle restart`: it stops that process and starts a new daemon on this port.');
|
|
124
|
+
'silent, run `reticle serve --restart`: it stops that process and starts a new daemon on this port.');
|
|
125
125
|
}
|
|
126
126
|
if (ourPid !== null && ourPid !== undefined && ourPid === holder.pid) {
|
|
127
127
|
return (`port ${String(port)} is held by YOUR Reticle daemon (pid ${String(holder.pid)}), and it is ` +
|
|
@@ -150,6 +150,30 @@ export function parseProcessCwd(stdout) {
|
|
|
150
150
|
}
|
|
151
151
|
return null;
|
|
152
152
|
}
|
|
153
|
+
/** Each port in `lsof -nP -iTCP -sTCP:LISTEN -Fn` output, once. A dual-stack bind prints two lines. */
|
|
154
|
+
export function parseListeningPorts(stdout) {
|
|
155
|
+
const ports = new Set();
|
|
156
|
+
for (const line of stdout.split('\n')) {
|
|
157
|
+
const port = line.startsWith(NAME_FIELD) ? /:(\d+)$/.exec(line)?.[1] : undefined;
|
|
158
|
+
if (port !== undefined) {
|
|
159
|
+
ports.add(Number(port));
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
return [...ports];
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Every TCP port on this machine with a listener, or nothing when the lookup cannot run.
|
|
166
|
+
*
|
|
167
|
+
* Off on Windows. A process there shows no working directory, so `findListenerCwd` starts PowerShell
|
|
168
|
+
* for each port, and the daemon blocks for each start.
|
|
169
|
+
*/
|
|
170
|
+
export function findListeningPorts(exec, platform = process.platform) {
|
|
171
|
+
if ('win32' === platform) {
|
|
172
|
+
return [];
|
|
173
|
+
}
|
|
174
|
+
const out = exec('lsof', ['-nP', '-iTCP', '-sTCP:LISTEN', '-Fn']);
|
|
175
|
+
return null === out ? [] : parseListeningPorts(out);
|
|
176
|
+
}
|
|
153
177
|
/** A Windows PowerShell cold start alone can take more than the lsof bound. */
|
|
154
178
|
const WINDOWS_LOOKUP_TIMEOUT_MS = 8_000;
|
|
155
179
|
/** The listener's command line: Windows does not show another process's working directory. */
|
|
@@ -24,6 +24,7 @@ export declare const StatusLabel: {
|
|
|
24
24
|
readonly NEXT: "next";
|
|
25
25
|
readonly SPLIT_BRAIN: "split brain";
|
|
26
26
|
readonly UPDATE: "update";
|
|
27
|
+
readonly LICENSE: "license";
|
|
27
28
|
};
|
|
28
29
|
export type StatusLabel = (typeof StatusLabel)[keyof typeof StatusLabel];
|
|
29
30
|
/** The block to print on stdout, one string per line, heading first. */
|