@reticlehq/server 2.11.0 → 2.12.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 +26 -2
- package/dist/bridge/bridge.d.ts +0 -13
- package/dist/bridge/bridge.js +1 -1
- package/dist/bridge/bridge.test-harness.d.ts +2 -0
- package/dist/bridge/bridge.test-harness.js +8 -1
- package/dist/capsule/capsule-store.d.ts +2 -1
- package/dist/capsule/capsule-store.js +1 -1
- package/dist/capsule/causal-summary.d.ts +2 -2
- package/dist/chromium-launch-options.d.ts +28 -0
- package/dist/chromium-launch-options.js +29 -0
- package/dist/cli/cli-doctor.js +8 -0
- package/dist/cli/cli-flow-commands.d.ts +3 -1
- package/dist/cli/cli-flow-commands.js +34 -4
- package/dist/cli/cli-kill.d.ts +2 -1
- package/dist/cli/cli-parse.d.ts +3 -1
- package/dist/cli/cli-parse.js +29 -7
- package/dist/cli/cli-update-commands.js +20 -1
- package/dist/cli/cli-verify.d.ts +1 -1
- package/dist/cli/cli-verify.js +2 -2
- package/dist/cli/cloud-cli.js +12 -6
- package/dist/cli/config-discovery.d.ts +2 -1
- package/dist/cli/doctor-rows.d.ts +3 -1
- package/dist/cli/doctor-rows.js +3 -1
- package/dist/cli/gate-exit.d.ts +21 -0
- package/dist/cli/gate-exit.js +21 -0
- package/dist/cli/gate-hook-message.d.ts +33 -0
- package/dist/cli/gate-hook-message.js +46 -0
- package/dist/cli/mcp-command.d.ts +25 -0
- package/dist/cli/mcp-command.js +143 -0
- package/dist/cli/sibling-ports.d.ts +37 -0
- package/dist/cli/sibling-ports.js +56 -0
- package/dist/cli.js +37 -110
- package/dist/cloud/cloud-config.d.ts +2 -1
- package/dist/cloud/cloud-sync.d.ts +11 -0
- package/dist/cloud/cloud-sync.js +32 -0
- package/dist/crawl/crawl.d.ts +9 -13
- package/dist/crawl/crawl.js +20 -3
- package/dist/daemon/daemon-resolve.d.ts +79 -0
- package/dist/daemon/daemon-resolve.js +118 -0
- package/dist/daemon/free-port.d.ts +29 -0
- package/dist/daemon/free-port.js +55 -0
- package/dist/daemon/heartbeat.d.ts +0 -4
- package/dist/daemon/heartbeat.js +2 -2
- package/dist/dev/stale-issue-guard.d.ts +0 -2
- package/dist/dev/stale-issue-guard.js +1 -1
- package/dist/events/contradictions.d.ts +41 -0
- package/dist/events/contradictions.js +125 -12
- package/dist/events/declared.d.ts +14 -0
- package/dist/events/declared.js +33 -0
- package/dist/events/echo-mismatch.js +14 -0
- package/dist/events/observed-in-window.d.ts +12 -0
- package/dist/events/observed-in-window.js +17 -0
- package/dist/events/predicate-asks.d.ts +13 -0
- package/dist/events/predicate-asks.js +22 -0
- package/dist/events/predicate-eval.js +64 -22
- package/dist/events/predicate-parse.d.ts +0 -6
- package/dist/events/predicate-parse.js +43 -3
- package/dist/events/predicate-schema.d.ts +44 -0
- package/dist/events/predicate-schema.js +126 -23
- package/dist/events/predicate.d.ts +15 -0
- package/dist/events/predicate.js +41 -2
- package/dist/flows/coverage.d.ts +0 -8
- package/dist/flows/coverage.js +1 -1
- package/dist/flows/decision.d.ts +5 -1
- package/dist/flows/decision.js +33 -7
- package/dist/flows/flake.d.ts +0 -2
- package/dist/flows/flake.js +1 -1
- package/dist/flows/flow-intent.d.ts +52 -0
- package/dist/flows/flow-intent.js +123 -0
- package/dist/flows/flow-replay-run.js +27 -4
- package/dist/flows/flow-success.js +15 -3
- package/dist/flows/flow-tools.js +32 -4
- package/dist/flows/flows.d.ts +6 -1
- package/dist/flows/flows.js +34 -22
- package/dist/flows/predicate-to-expect.js +6 -3
- package/dist/flows/server-verify.js +16 -2
- package/dist/flows/verify-change-tools.js +4 -1
- package/dist/honesty/blind-spots.d.ts +0 -17
- package/dist/honesty/blind-spots.js +1 -1
- package/dist/honesty/feature-capture.d.ts +150 -0
- package/dist/honesty/feature-capture.js +233 -0
- package/dist/honesty/instrumentation-gaps.d.ts +30 -6
- package/dist/honesty/instrumentation-gaps.js +61 -5
- package/dist/honesty/tool-hit-rate.d.ts +86 -0
- package/dist/honesty/tool-hit-rate.js +90 -0
- package/dist/honesty/undeclared-change.d.ts +51 -0
- package/dist/honesty/undeclared-change.js +56 -0
- package/dist/honesty/verified.d.ts +15 -0
- package/dist/honesty/verified.js +68 -4
- package/dist/http-server.d.ts +0 -1
- package/dist/http-server.js +1 -1
- package/dist/hunt/hunt-report.d.ts +0 -2
- package/dist/hunt/hunt-report.js +1 -1
- package/dist/impact/impact-store.d.ts +0 -5
- package/dist/impact/impact-store.js +1 -1
- package/dist/index.d.ts +4 -1
- package/dist/init/agent-rules.d.ts +17 -0
- package/dist/init/agent-rules.js +31 -1
- package/dist/init/capabilities.d.ts +0 -2
- package/dist/init/capabilities.js +1 -1
- package/dist/init/closing-hint.d.ts +36 -0
- package/dist/init/closing-hint.js +120 -0
- package/dist/init/confirm.d.ts +0 -25
- package/dist/init/confirm.js +3 -3
- package/dist/init/cra.d.ts +14 -1
- package/dist/init/cra.js +43 -12
- package/dist/init/dev-script.d.ts +48 -0
- package/dist/init/dev-script.js +63 -0
- package/dist/init/mcp.d.ts +16 -5
- package/dist/init/mcp.js +26 -10
- package/dist/init/plan-framework.d.ts +0 -5
- package/dist/init/plan-framework.js +83 -9
- package/dist/init/plan.d.ts +10 -30
- package/dist/init/plan.js +30 -15
- package/dist/init/refresh-rules.d.ts +39 -0
- package/dist/init/refresh-rules.js +51 -0
- package/dist/init/run.d.ts +0 -26
- package/dist/init/run.js +14 -87
- package/dist/init/snippets.js +53 -10
- package/dist/input/real-input.d.ts +2 -0
- package/dist/input/real-input.js +3 -2
- package/dist/input/scroll-find.d.ts +7 -4
- package/dist/input/scroll-find.js +63 -25
- package/dist/intent/inline-intent.d.ts +46 -0
- package/dist/intent/inline-intent.js +103 -0
- package/dist/intent/intent-store.d.ts +37 -0
- package/dist/intent/intent-store.js +112 -0
- package/dist/intent/intent-tools.d.ts +2 -0
- package/dist/intent/intent-tools.js +80 -0
- package/dist/intent/open-intents.d.ts +21 -0
- package/dist/intent/open-intents.js +25 -0
- package/dist/journal/envelope.d.ts +26 -26
- package/dist/journal/journal-recorder.d.ts +23 -0
- package/dist/journal/journal-recorder.js +35 -0
- package/dist/mcp/attach-memory.d.ts +67 -0
- package/dist/mcp/attach-memory.js +142 -0
- package/dist/mcp/mcp-proxy.d.ts +0 -7
- package/dist/mcp/mcp-proxy.js +14 -2
- package/dist/mcp/mcp.d.ts +2 -0
- package/dist/mcp/mcp.js +45 -23
- package/dist/mcp/server-instructions.js +45 -3
- package/dist/mcp/tool-catalog-cache.d.ts +8 -0
- package/dist/mcp/tool-catalog-cache.js +10 -0
- package/dist/oracles/flow-instrument-gaps.d.ts +4 -4
- package/dist/oracles/flow-instrument-gaps.js +4 -5
- package/dist/oracles/self-instrument.d.ts +9 -9
- package/dist/oracles/self-instrument.js +19 -13
- package/dist/pool/browser-pool.d.ts +12 -1
- package/dist/pool/browser-pool.js +31 -1
- package/dist/pool/playwright-launcher.js +3 -1
- package/dist/project/fs-port.d.ts +3 -0
- package/dist/project/fs-port.js +3 -2
- package/dist/project/memory-fs.js +6 -1
- package/dist/project/project-tools.js +3 -32
- package/dist/project/reticle-dir.d.ts +2 -0
- package/dist/project/reticle-dir.js +1 -0
- package/dist/runs/context-tools.d.ts +2 -0
- package/dist/runs/context-tools.js +89 -0
- package/dist/runs/run-context.d.ts +40 -0
- package/dist/runs/run-context.js +158 -0
- package/dist/runs/run-diff.d.ts +2 -1
- package/dist/runs/verification-sync.js +2 -2
- package/dist/session/dev-server-probe.d.ts +0 -8
- package/dist/session/dev-server-probe.js +1 -1
- package/dist/session/last-act.d.ts +7 -1
- package/dist/session/last-act.js +2 -2
- package/dist/session/live-control-tools.js +1 -42
- package/dist/session/no-session-diagnosis.d.ts +10 -0
- package/dist/session/no-session-diagnosis.js +24 -7
- package/dist/session/no-session-next-action.d.ts +5 -0
- package/dist/session/no-session-next-action.js +35 -3
- package/dist/session/no-session-watch.d.ts +8 -0
- package/dist/session/no-session-watch.js +82 -64
- package/dist/session/output-budget.js +4 -1
- package/dist/session/session-health.d.ts +9 -0
- package/dist/session/session-health.js +23 -0
- package/dist/session/session-manager.js +50 -1
- package/dist/session/session-successor.d.ts +37 -0
- package/dist/session/session-successor.js +78 -0
- package/dist/session/session.d.ts +31 -1
- package/dist/session/session.js +75 -2
- package/dist/telemetry/automation-hint.d.ts +2 -1
- package/dist/telemetry/feedback-outbox.d.ts +2 -1
- package/dist/telemetry/project-profile.d.ts +0 -8
- package/dist/telemetry/project-profile.js +1 -1
- package/dist/telemetry/run-telemetry.d.ts +0 -2
- package/dist/telemetry/run-telemetry.js +1 -1
- package/dist/telemetry/verification-of.js +2 -2
- package/dist/tools/act-tools.js +126 -21
- package/dist/tools/assert-source.d.ts +22 -0
- package/dist/tools/assert-source.js +53 -0
- package/dist/tools/assert-verdict.d.ts +20 -3
- package/dist/tools/assert-verdict.js +36 -4
- package/dist/tools/browser-tools.js +0 -20
- package/dist/tools/complete-snapshot.d.ts +48 -0
- package/dist/tools/complete-snapshot.js +75 -0
- package/dist/tools/coverage-tools.d.ts +10 -1
- package/dist/tools/coverage-tools.js +123 -69
- package/dist/tools/dynamic-tools.js +29 -8
- package/dist/tools/error-recovery.d.ts +24 -14
- package/dist/tools/error-recovery.js +70 -9
- package/dist/tools/feedback-tools.d.ts +1 -1
- package/dist/tools/feedback-tools.js +2 -2
- package/dist/tools/invoke-tool.js +62 -2
- package/dist/tools/lease-hint.d.ts +10 -0
- package/dist/tools/lease-hint.js +53 -4
- package/dist/tools/lease-tools.d.ts +5 -1
- package/dist/tools/lease-tools.js +22 -8
- package/dist/tools/navigate-arrival.d.ts +0 -2
- package/dist/tools/navigate-arrival.js +1 -1
- package/dist/tools/observe-tools.js +63 -26
- package/dist/tools/read-tools.js +5 -3
- package/dist/tools/real-input-attempt.d.ts +22 -0
- package/dist/tools/real-input-attempt.js +153 -1
- package/dist/tools/reconcile-tools.js +18 -4
- package/dist/tools/resolve-target.d.ts +0 -7
- package/dist/tools/sdk-marker-probe.d.ts +29 -0
- package/dist/tools/sdk-marker-probe.js +57 -0
- package/dist/tools/snapshot-delta.d.ts +1 -1
- package/dist/tools/tool-kit.d.ts +12 -0
- package/dist/tools/tool-kit.js +16 -0
- package/dist/tools/tool-names.d.ts +3 -0
- package/dist/tools/tool-names.js +3 -0
- package/dist/tools/tool-surface.d.ts +85 -6
- package/dist/tools/tool-surface.js +122 -2
- package/dist/tools/tools.js +22 -4
- package/dist/tools/unadvertised-help.js +4 -4
- package/dist/tools/verify-next-baton.d.ts +53 -0
- package/dist/tools/verify-next-baton.js +52 -0
- package/dist/update/update-nudge.d.ts +7 -10
- package/dist/update/update-nudge.js +12 -3
- package/dist/update/updater.d.ts +1 -1
- package/docs/README.md +2 -1
- package/docs/cli/cloud.mdx +1 -1
- package/docs/cli/doctor.mdx +1 -0
- package/docs/cli/verify.mdx +4 -5
- package/docs/desktop.mdx +1 -1
- package/docs/docs.json +2 -2
- package/docs/gates.md +1 -1
- package/docs/install-manual.mdx +1 -1
- package/docs/packages/browser.mdx +2 -2
- package/docs/packages/server.mdx +1 -1
- package/docs/packages.mdx +8 -6
- package/docs/platform-integration.md +17 -0
- package/docs/predicates.mdx +8 -0
- package/docs/reticle-manual.md +106 -0
- package/docs/tools-tools-and-run.mdx +2 -4
- package/docs/troubleshooting.mdx +9 -0
- package/docs/usage.md +3 -2
- package/package.json +2 -2
- package/dist/session/session-readiness.d.ts +0 -34
- package/dist/session/session-readiness.js +0 -36
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bring an already-installed project's agent rules up to the version that is running.
|
|
3
|
+
*
|
|
4
|
+
* `mergeMarkedInstruction` has always been able to do this: the managed block is marker-delimited,
|
|
5
|
+
* idempotence is decided by comparing CONTENT rather than by seeing a marker, and everything
|
|
6
|
+
* outside the markers is left exactly as the human wrote it. What was missing is anything that
|
|
7
|
+
* CALLS it after the first install.
|
|
8
|
+
*
|
|
9
|
+
* It is reachable only from `buildPlan`, so the rules refresh when somebody re-runs `init` and
|
|
10
|
+
* never otherwise. A project set up on an older release keeps that release's instructions forever,
|
|
11
|
+
* which means every improvement to them — what to do when a capabilities file registers nothing,
|
|
12
|
+
* that an install is not finished until a verdict exists, that intent is captured while building —
|
|
13
|
+
* reaches new projects only.
|
|
14
|
+
*
|
|
15
|
+
* That is the wrong way round. The people who most need better instructions are the ones already
|
|
16
|
+
* installed and not getting value, and `reticle update` is the moment they are already touching the
|
|
17
|
+
* install.
|
|
18
|
+
*/
|
|
19
|
+
import { join } from 'node:path';
|
|
20
|
+
import { mergeMarkedInstruction, AgentRuleStatus, hasManagedBlock } from './agent-rules.js';
|
|
21
|
+
/** The instruction files the managed block lives in. Cursor's rule file is regenerated whole. */
|
|
22
|
+
export const RULE_FILES = ['CLAUDE.md', 'AGENTS.md'];
|
|
23
|
+
/**
|
|
24
|
+
* Refresh every instruction file that already carries a Reticle block.
|
|
25
|
+
*
|
|
26
|
+
* Deliberately does NOT create one where none exists. A project with no Reticle block in its
|
|
27
|
+
* CLAUDE.md either never ran `init` here or had the block removed on purpose, and `update` is not
|
|
28
|
+
* the place to make that decision for somebody — it is a version command, and quietly writing
|
|
29
|
+
* instructions into a file a user curates would be a surprise.
|
|
30
|
+
*/
|
|
31
|
+
export function refreshAgentRules(cwd, io) {
|
|
32
|
+
const updated = [];
|
|
33
|
+
const unchanged = [];
|
|
34
|
+
for (const name of RULE_FILES) {
|
|
35
|
+
const path = join(cwd, name);
|
|
36
|
+
const existing = io.read(path);
|
|
37
|
+
if (null === existing || !hasManagedBlock(existing)) {
|
|
38
|
+
unchanged.push(name);
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
const result = mergeMarkedInstruction(existing);
|
|
42
|
+
if (AgentRuleStatus.APPLY === result.status && result.content !== existing) {
|
|
43
|
+
io.write(path, result.content);
|
|
44
|
+
updated.push(name);
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
unchanged.push(name);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return { updated, unchanged };
|
|
51
|
+
}
|
package/dist/init/run.d.ts
CHANGED
|
@@ -3,8 +3,6 @@
|
|
|
3
3
|
* plan (pure), optionally write the apply-steps, and print a human-readable report. All filesystem
|
|
4
4
|
* access goes through `InitIo` so the orchestration is unit-testable with an in-memory IO.
|
|
5
5
|
*/
|
|
6
|
-
import { Framework } from './detect.js';
|
|
7
|
-
import { StepStatus } from './plan.js';
|
|
8
6
|
import type { InitOutcome } from '@reticlehq/core';
|
|
9
7
|
/**
|
|
10
8
|
* Resolve the lockfiles set used to pick the package manager. A lockfile in the project root wins;
|
|
@@ -93,28 +91,4 @@ export interface InitResult {
|
|
|
93
91
|
* it knows it is in a workspace; this is the matching walk DOWN.
|
|
94
92
|
*/
|
|
95
93
|
export declare function findWorkspaceApps(io: Pick<InitIo, 'exists' | 'readFile' | 'listDirs'>): string[];
|
|
96
|
-
/**
|
|
97
|
-
* The standing ask, printed at the end of every `init`.
|
|
98
|
-
*
|
|
99
|
-
* Addressed to the agent as much as the human: whichever of them just ran this command is the one
|
|
100
|
-
* holding the experience, and neither has a Reticle tool surface to file through at this point.
|
|
101
|
-
*
|
|
102
|
-
* Printed by the `runInit` wrapper, NOT by `report()` — see the comment there for why, and for how
|
|
103
|
-
* the workspace redirect is kept from asking twice.
|
|
104
|
-
*/
|
|
105
|
-
export declare const FEEDBACK_HINT: string;
|
|
106
|
-
/**
|
|
107
|
-
* The last two lines a user reads, and the order matters.
|
|
108
|
-
*
|
|
109
|
-
* This used to say only "Restart <dev server>, then ask your agent: List Reticle sessions" — and
|
|
110
|
-
* asking the agent was the one thing that could not work yet. `init` registers the MCP server, but
|
|
111
|
-
* an agent client reads its tool list when it STARTS and never re-reads it, so the session that
|
|
112
|
-
* just ran `init` has no `reticle_*` tools however clean the install was. The user follows the
|
|
113
|
-
* instruction, the agent answers "unknown tool", and the obvious conclusion is that the install
|
|
114
|
-
* failed.
|
|
115
|
-
*
|
|
116
|
-
* So the reload is named FIRST, before the sentence that depends on it. Omitted entirely when this
|
|
117
|
-
* run did not register MCP (`--no-mcp`), where it would be advice about something we did not do.
|
|
118
|
-
*/
|
|
119
|
-
export declare function restartHint(framework: Framework, mcpStatus: StepStatus | undefined): string;
|
|
120
94
|
export declare function runInit(options: InitOptions, io: InitIo): InitResult;
|
package/dist/init/run.js
CHANGED
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import { dirname, join } from 'node:path';
|
|
7
7
|
import { noPackageJsonMessage } from './non-js-project.js';
|
|
8
|
+
import { devCommandFrom } from './dev-script.js';
|
|
9
|
+
import { restartHint, FEEDBACK_HINT } from './closing-hint.js';
|
|
8
10
|
import { spanSync } from '../trace.js';
|
|
9
11
|
import { readFileSync } from 'node:fs';
|
|
10
12
|
import { homedir } from 'node:os';
|
|
@@ -15,7 +17,7 @@ import { pickAstroHost } from './astro-host.js';
|
|
|
15
17
|
import { workspaceParents } from './workspace-apps.js';
|
|
16
18
|
import { chooseWorkspaceApp } from './app-choice.js';
|
|
17
19
|
import { isConnectStep } from './connect-steps.js';
|
|
18
|
-
import {
|
|
20
|
+
import { CURSOR_RULE_PATH, RETICLE_MD_PATH } from './agent-rules.js';
|
|
19
21
|
import { CRA_ENV_PATH } from './cra.js';
|
|
20
22
|
/** CRA's bundled entry, in the order create-react-app itself generates them. */
|
|
21
23
|
const CRA_ENTRY_CANDIDATES = ['src/index.tsx', 'src/index.jsx', 'src/index.ts', 'src/index.js'];
|
|
@@ -352,6 +354,7 @@ function gatherPlanInput(options, io, pkgRaw) {
|
|
|
352
354
|
claudeCli,
|
|
353
355
|
mcpExists,
|
|
354
356
|
cursorPresent,
|
|
357
|
+
platform: process.platform,
|
|
355
358
|
detectedClients,
|
|
356
359
|
cursorProjectPresent: io.exists(CURSOR_DIR_RELPATH),
|
|
357
360
|
cursorConfig,
|
|
@@ -375,6 +378,7 @@ function gatherPlanInput(options, io, pkgRaw) {
|
|
|
375
378
|
nextReticleDevPath: devLocation.path,
|
|
376
379
|
nextReticleDevImport: devLocation.importSpecifier,
|
|
377
380
|
nextReticleDevExists: io.exists(devLocation.path),
|
|
381
|
+
nextReticleDevSource: io.readFile(devLocation.path),
|
|
378
382
|
svelteKitHooksExists: io.exists(SVELTEKIT_HOOKS),
|
|
379
383
|
craEntry: craEntryOf(io),
|
|
380
384
|
craEnv: io.readFile(CRA_ENV_PATH),
|
|
@@ -468,91 +472,11 @@ export function findWorkspaceApps(io) {
|
|
|
468
472
|
}
|
|
469
473
|
return [...new Set(found)];
|
|
470
474
|
}
|
|
471
|
-
/**
|
|
472
|
-
* The standing ask, printed at the end of every `init`.
|
|
473
|
-
*
|
|
474
|
-
* Addressed to the agent as much as the human: whichever of them just ran this command is the one
|
|
475
|
-
* holding the experience, and neither has a Reticle tool surface to file through at this point.
|
|
476
|
-
*
|
|
477
|
-
* Printed by the `runInit` wrapper, NOT by `report()` — see the comment there for why, and for how
|
|
478
|
-
* the workspace redirect is kept from asking twice.
|
|
479
|
-
*/
|
|
480
|
-
export const FEEDBACK_HINT = 'Anything wrong, missing, or awkward — in this setup or in Reticle itself? Tell us; it is the ' +
|
|
481
|
-
'one thing that decides what gets fixed:\n' +
|
|
482
|
-
` ${CLI} feedback --agent --kind <bug|gap|ambiguity|feature_request|improvement> "what happened" (agents)\n` +
|
|
483
|
-
` ${CLI} feedback "what worked, what didn't" (humans)`;
|
|
484
|
-
function devServerRestart(framework) {
|
|
485
|
-
if (framework === Framework.NEXT)
|
|
486
|
-
return 'Restart `next dev`';
|
|
487
|
-
if (framework === Framework.VITE)
|
|
488
|
-
return 'Restart `vite`';
|
|
489
|
-
if (framework === Framework.ASTRO)
|
|
490
|
-
return 'Restart `astro dev`';
|
|
491
|
-
if (framework === Framework.SVELTEKIT)
|
|
492
|
-
return 'Restart your dev server (`npm run dev`)';
|
|
493
|
-
return 'Reload your app on localhost';
|
|
494
|
-
}
|
|
495
|
-
/**
|
|
496
|
-
* The last two lines a user reads, and the order matters.
|
|
497
|
-
*
|
|
498
|
-
* This used to say only "Restart <dev server>, then ask your agent: List Reticle sessions" — and
|
|
499
|
-
* asking the agent was the one thing that could not work yet. `init` registers the MCP server, but
|
|
500
|
-
* an agent client reads its tool list when it STARTS and never re-reads it, so the session that
|
|
501
|
-
* just ran `init` has no `reticle_*` tools however clean the install was. The user follows the
|
|
502
|
-
* instruction, the agent answers "unknown tool", and the obvious conclusion is that the install
|
|
503
|
-
* failed.
|
|
504
|
-
*
|
|
505
|
-
* So the reload is named FIRST, before the sentence that depends on it. Omitted entirely when this
|
|
506
|
-
* run did not register MCP (`--no-mcp`), where it would be advice about something we did not do.
|
|
507
|
-
*/
|
|
508
|
-
export function restartHint(framework, mcpStatus) {
|
|
509
|
-
const dev = `${devServerRestart(framework)}.`;
|
|
510
|
-
// NAME THE COMMAND THAT PROVES IT, not one that merely asks.
|
|
511
|
-
//
|
|
512
|
-
// `init` writes files and stops; the install is not finished until an app carrying the SDK has
|
|
513
|
-
// actually dialled the daemon, and nothing here confirmed that. The field shape is unambiguous:
|
|
514
|
-
// people complete the agent half, never complete the app half, and keep a daemon running for
|
|
515
|
-
// weeks with nothing to drive — so this is the last instruction most of them read.
|
|
516
|
-
//
|
|
517
|
-
// "Ask your agent: List Reticle sessions" asks a question whose failure is a dead end. `reticle
|
|
518
|
-
// status` ANSWERS it: as of 2.7.0 it reports the session, or says why there is none — no app
|
|
519
|
-
// running, an app running that never dialled us, a tab that closed — with the fix for each.
|
|
520
|
-
const prove = 'Then run `npx @reticlehq/server status` — it confirms the app connected, or says exactly why ' +
|
|
521
|
-
'it has not.';
|
|
522
|
-
// Nothing registered this run (`--no-mcp`, or a client that needs a manual edit): a restart picks
|
|
523
|
-
// up nothing, so advice about one would be advice about something we did not do.
|
|
524
|
-
if (!wasMcpRegistered(mcpStatus))
|
|
525
|
-
return `${dev} ${prove}`;
|
|
526
|
-
// ALREADY: the tools are reachable RIGHT NOW, and this is the branch that used to lie.
|
|
527
|
-
//
|
|
528
|
-
// `wasMcpRegistered` is true for both APPLY and ALREADY, which is correct for the funnel field it
|
|
529
|
-
// was written for and wrong here, because the two cases have opposite answers to "must anybody
|
|
530
|
-
// restart". Every second project on a machine, and every Claude Code plugin install, took the
|
|
531
|
-
// APPLY text: "restart your agent — the tools only appear after that". An agent reads this AFTER
|
|
532
|
-
// whatever its skill file said, and it describes the output in front of it, so it wins. The agent
|
|
533
|
-
// stops with the project wired and nothing driven, which is the exact failure the install path
|
|
534
|
-
// exists to prevent, caused by the install path itself.
|
|
535
|
-
if (mcpStatus === StepStatus.ALREADY) {
|
|
536
|
-
return (`${dev}\n` +
|
|
537
|
-
'The Reticle MCP server was already registered on this machine, so there is no restart to do ' +
|
|
538
|
-
'and the tools are available now.\n' +
|
|
539
|
-
`${prove}\n` +
|
|
540
|
-
'Once it shows a session, drive a flow — that is the install finished. Carry straight on; ' +
|
|
541
|
-
'nothing here is waiting on a human.');
|
|
542
|
-
}
|
|
543
|
-
return (`${dev}\n` +
|
|
544
|
-
'Then restart your agent so it picks up the new MCP server — restart Claude Code, reload the ' +
|
|
545
|
-
'window in Cursor, or hit Start in `.vscode/mcp.json` in VS Code.\n' +
|
|
546
|
-
'The tools only appear after that: your agent read its server list before Reticle existed, ' +
|
|
547
|
-
'and no slash command re-reads it (`/mcp` manages servers already loaded, so it cannot pick ' +
|
|
548
|
-
'up a new one). This is once per machine — Reticle is registered globally, so every later ' +
|
|
549
|
-
'project starts with the tools already there.\n' +
|
|
550
|
-
`${prove}\n` +
|
|
551
|
-
'Once it shows a session, ask your agent to drive a flow — that is the install finished.');
|
|
552
|
-
}
|
|
553
475
|
const SKIPPED_DETAIL = 'skipped — the dependency install above failed, and wiring the app to a package that is not ' +
|
|
554
476
|
'installed stops it booting. Run that install, then re-run `reticle init`.';
|
|
555
|
-
function report(plan, dryRun, failed, skipped, degraded, io, projectDir, agentRoot
|
|
477
|
+
function report(plan, dryRun, failed, skipped, degraded, io, projectDir, agentRoot,
|
|
478
|
+
/** The project's own dev command, so the closing line names what a human would type. */
|
|
479
|
+
devCommand) {
|
|
556
480
|
io.print(dryRun ? 'reticle init (dry run, no files written)' : 'reticle init');
|
|
557
481
|
// Every path below is printed RELATIVE, and until now nothing said what to. Reported from the
|
|
558
482
|
// field as "[✓] Reticle config → .reticle.json" followed by the file not being there: the app was
|
|
@@ -612,7 +536,7 @@ function report(plan, dryRun, failed, skipped, degraded, io, projectDir, agentRo
|
|
|
612
536
|
'"no browser session connected" until then. Everything else is already in place.');
|
|
613
537
|
io.print('');
|
|
614
538
|
}
|
|
615
|
-
io.print(restartHint(plan.framework, resolvedStatus(plan, MCP_TARGET, failed, skipped)));
|
|
539
|
+
io.print(restartHint(plan.framework, resolvedStatus(plan, MCP_TARGET, failed, skipped), devCommand));
|
|
616
540
|
return { ok: !connectPending, applied, manual };
|
|
617
541
|
}
|
|
618
542
|
/**
|
|
@@ -834,12 +758,15 @@ function runInitSteps(options, io) {
|
|
|
834
758
|
// Init is the flow a user experiences the wait of personally, and the fixture gate measures it at
|
|
835
759
|
// 1–6s per app with no explanation of the spread. These three spans split that number into detect
|
|
836
760
|
// (filesystem probing), plan (pure), and apply (writes + package-manager and CLI subprocesses).
|
|
837
|
-
const
|
|
761
|
+
const planInput = gatherPlanInput(options, io, pkgRaw);
|
|
762
|
+
const plan = spanSync('init.plan', {}, () => buildPlan(planInput));
|
|
838
763
|
const effects = options.dryRun
|
|
839
764
|
? { failed: new Set(), skipped: new Set(), degraded: new Map() }
|
|
840
765
|
: spanSync('init.apply', { steps: plan.steps.length }, () => applyEffects(plan, io));
|
|
841
766
|
const { failed, skipped, degraded } = effects;
|
|
842
|
-
|
|
767
|
+
// The project's own dev command, so the closing line names what a human would actually type.
|
|
768
|
+
const devCommand = devCommandFrom(pkgRaw, planInput.detection.packageManager);
|
|
769
|
+
const result = report(plan, options.dryRun, failed, skipped, degraded, io, options.cwd, agentRootOf(options), devCommand);
|
|
843
770
|
// A dry run is a preview, not an outcome — reporting it would inflate both success and failure.
|
|
844
771
|
if (options.dryRun)
|
|
845
772
|
return result;
|
package/dist/init/snippets.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Generated file contents and copy-paste snippets for `reticle init`. Kept as named constants/builders
|
|
3
3
|
* so the runner never inlines free strings.
|
|
4
4
|
*/
|
|
5
|
-
import { RETICLE_DEFAULT_PORT, ReticleDir, bridgeWsUrl } from '@reticlehq/core';
|
|
5
|
+
import { RETICLE_DEFAULT_PORT, ReticleDir, bridgeWsUrl, RETICLE_CLIENT_HOST, RETICLE_WS_PATH, } from '@reticlehq/core';
|
|
6
6
|
import { UiLibrary } from './detect.js';
|
|
7
7
|
import { SERVER_VERSION } from '../version/server-version.js';
|
|
8
8
|
/**
|
|
@@ -17,9 +17,9 @@ import { SERVER_VERSION } from '../version/server-version.js';
|
|
|
17
17
|
*
|
|
18
18
|
* jsDelivr rather than esm.sh, measured: a third of the requests and a third of the bytes for the
|
|
19
19
|
* same result. `/+esm` is what makes it work, and the bare package URL does NOT: every file in
|
|
20
|
-
* `dist` carries bare specifiers
|
|
21
|
-
*
|
|
22
|
-
*
|
|
20
|
+
* `dist` still carries bare workspace specifiers such as `@reticlehq/core`, so an unbundled entry
|
|
21
|
+
* point dies on the first import. That is also why adding `unpkg`/`jsdelivr` fields to package.json
|
|
22
|
+
* would not help.
|
|
23
23
|
*
|
|
24
24
|
* PINNED to this server's version on purpose. A floating import upgrades the page SDK underneath a
|
|
25
25
|
* daemon that did not move, which is `version_skew` arriving by a route nothing checks.
|
|
@@ -151,7 +151,10 @@ export function ReticleDev() {
|
|
|
151
151
|
// the root makes source paths repo-relative instead of absolute.
|
|
152
152
|
const token = process.env.NEXT_PUBLIC_RETICLE_TOKEN;
|
|
153
153
|
const root = process.env.NEXT_PUBLIC_RETICLE_ROOT;
|
|
154
|
-
|
|
154
|
+
// withReticle() finds the daemon serving this project on every dev-server start. It wins over
|
|
155
|
+
// any port written into this file at install time, so moving the daemon needs no edit here.
|
|
156
|
+
const url = process.env.NEXT_PUBLIC_RETICLE_URL;
|
|
157
|
+
reticle.connect({ ${fields}...(url ? { url } : {}), ...(token ? { token } : {}), ...(root ? { root } : {}) });
|
|
155
158
|
|
|
156
159
|
// ── Start with ONE flow. ──────────────────────────────────────────────────────────────────
|
|
157
160
|
// Registering a store is the highest-value line here: it lets the agent check what the app
|
|
@@ -204,11 +207,51 @@ layoutPath) {
|
|
|
204
207
|
? `\n url: '${bridgeWsUrl(port)}',`
|
|
205
208
|
: '';
|
|
206
209
|
const id = projectId !== undefined && projectId.length > 0 ? `\n projectId: '${projectId}',` : '';
|
|
210
|
+
// Astro owns its Vite instance, so it has a build-time channel of its own: the same `define` that
|
|
211
|
+
// already inlines the token can inline the daemon URL, resolved every time the config is read —
|
|
212
|
+
// that is, on every `astro dev`. The port above is written once at install time and goes stale the
|
|
213
|
+
// moment the daemon moves; this does not.
|
|
214
|
+
//
|
|
215
|
+
// Needs the projectId to know WHICH daemon is ours. Without one there is nothing to match on, and
|
|
216
|
+
// adopting a daemon serving another project would report its state as this app's, so the whole
|
|
217
|
+
// block is omitted rather than made to guess.
|
|
218
|
+
const canDiscover = projectId !== undefined && projectId.length > 0;
|
|
219
|
+
// Interpolated from core rather than typed into the template: this is the same wire string
|
|
220
|
+
// `bridgeWsUrl` builds, and a generator that spells it out by hand is exactly how the four call
|
|
221
|
+
// sites that constant exists to unify drifted apart in the first place.
|
|
222
|
+
const RETICLE_CLIENT_HOST_LITERAL = `ws://${RETICLE_CLIENT_HOST}:`;
|
|
223
|
+
const urlHelper = canDiscover
|
|
224
|
+
? `
|
|
225
|
+
// The live daemon serving THIS project, re-read on every \`astro dev\`. Same rule as the Vite and
|
|
226
|
+
// Next plugins: skip dead daemons, match on projectId, lowest port wins, '' when none matches.
|
|
227
|
+
function reticleUrl() {
|
|
228
|
+
const dir = process.env['RETICLE_PAIRING_TOKEN_DIR'] || join(homedir(), '.reticle');
|
|
229
|
+
let best;
|
|
230
|
+
try {
|
|
231
|
+
for (const file of readdirSync(dir)) {
|
|
232
|
+
if (!file.startsWith('daemon-') || !file.endsWith('.json')) continue;
|
|
233
|
+
let entry;
|
|
234
|
+
try { entry = JSON.parse(readFileSync(join(dir, file), 'utf8')); } catch { continue; }
|
|
235
|
+
if (entry.projectId !== '${projectId}') continue;
|
|
236
|
+
try { process.kill(entry.pid, 0); } catch { continue; }
|
|
237
|
+
if (best === undefined || entry.port < best) best = entry.port;
|
|
238
|
+
}
|
|
239
|
+
} catch { return ''; }
|
|
240
|
+
return best === undefined ? '' : '${RETICLE_CLIENT_HOST_LITERAL}' + best + '${RETICLE_WS_PATH}';
|
|
241
|
+
}
|
|
242
|
+
`
|
|
243
|
+
: '';
|
|
244
|
+
const urlDefine = canDiscover ? `\n __RETICLE_URL__: JSON.stringify(reticleUrl()),` : '';
|
|
245
|
+
const urlRead = canDiscover
|
|
246
|
+
? `\n const url = typeof __RETICLE_URL__ !== 'undefined' ? __RETICLE_URL__ : '';`
|
|
247
|
+
: '';
|
|
248
|
+
// After the baked port, so the discovered one wins: later key in an object literal.
|
|
249
|
+
const urlSpread = canDiscover ? `\n ...(url.length > 0 ? { url } : {}),` : '';
|
|
207
250
|
return `Astro renders its own HTML, so the connect goes in a page <script> and the pairing token is inlined by the config.
|
|
208
251
|
|
|
209
252
|
1. In astro.config.mjs — inline the daemon's token and raise the build target:
|
|
210
253
|
|
|
211
|
-
import { readFileSync } from 'node:fs';
|
|
254
|
+
import { readFileSync, readdirSync } from 'node:fs';
|
|
212
255
|
import { homedir } from 'node:os';
|
|
213
256
|
import { join } from 'node:path';
|
|
214
257
|
|
|
@@ -216,14 +259,14 @@ layoutPath) {
|
|
|
216
259
|
const dir = process.env['RETICLE_PAIRING_TOKEN_DIR'] || join(homedir(), '.reticle');
|
|
217
260
|
try { return readFileSync(join(dir, 'pairing-token'), 'utf8').trim(); } catch { return ''; }
|
|
218
261
|
}
|
|
219
|
-
|
|
262
|
+
${urlHelper}
|
|
220
263
|
export default defineConfig({
|
|
221
264
|
vite: {
|
|
222
265
|
// Astro's default target down-levels the modern SDK bundle and fails on a destructuring transform.
|
|
223
266
|
build: { target: 'es2022' },
|
|
224
267
|
optimizeDeps: { esbuildOptions: { target: 'es2022' } },
|
|
225
268
|
define: {
|
|
226
|
-
__RETICLE_TOKEN__: JSON.stringify(reticleToken())
|
|
269
|
+
__RETICLE_TOKEN__: JSON.stringify(reticleToken()),${urlDefine}
|
|
227
270
|
// Without this, source pointers come back as absolute paths from YOUR machine — useless in a
|
|
228
271
|
// report. Every other framework gets it from its build plugin; Astro owns its Vite instance.
|
|
229
272
|
__RETICLE_ROOT__: JSON.stringify(process.cwd()),
|
|
@@ -236,10 +279,10 @@ ${layoutHost(layoutPath)}
|
|
|
236
279
|
<script>
|
|
237
280
|
if (import.meta.env.DEV) {
|
|
238
281
|
const token = typeof __RETICLE_TOKEN__ !== 'undefined' ? __RETICLE_TOKEN__ : '';
|
|
239
|
-
const root = typeof __RETICLE_ROOT__ !== 'undefined' ? __RETICLE_ROOT__ : ''
|
|
282
|
+
const root = typeof __RETICLE_ROOT__ !== 'undefined' ? __RETICLE_ROOT__ : '';${urlRead}
|
|
240
283
|
const { reticle, install } = await import('@reticlehq/react');
|
|
241
284
|
install();
|
|
242
|
-
reticle.connect({${id}${extra}
|
|
285
|
+
reticle.connect({${id}${extra}${urlSpread}
|
|
243
286
|
...(token.length > 0 ? { token } : {}),
|
|
244
287
|
...(root.length > 0 ? { root } : {}),
|
|
245
288
|
});
|
|
@@ -171,6 +171,8 @@ export interface LaunchedProviderOptions {
|
|
|
171
171
|
*/
|
|
172
172
|
onNetworkDetail?: (detail: NetworkDetail) => void;
|
|
173
173
|
}
|
|
174
|
+
/** The only place the dynamic value import of Playwright lives for the launched (drive) path. */
|
|
175
|
+
export declare const launchedChromium: LaunchFn;
|
|
174
176
|
/**
|
|
175
177
|
* Launches and OWNS a Playwright Chromium, navigates it to `driveUrl`, then drives native
|
|
176
178
|
* input on that page. Headless-capable so @reticlehq/test / CI can run hover/drag unattended.
|
package/dist/input/real-input.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { chromiumLaunchOptions } from '../chromium-launch-options.js';
|
|
1
2
|
import { gotoOptions } from '../pool/playwright-launcher.js';
|
|
2
3
|
import { BrowserLaunchKind } from '@reticlehq/core';
|
|
3
4
|
import { getSessionMetrics } from '../telemetry/session-metrics.js';
|
|
@@ -220,7 +221,7 @@ export class CdpRealInputProvider {
|
|
|
220
221
|
}
|
|
221
222
|
const INJECT_CONNECT_WAIT_MS = 8_000;
|
|
222
223
|
/** The only place the dynamic value import of Playwright lives for the launched (drive) path. */
|
|
223
|
-
const launchedChromium = async (headless) => {
|
|
224
|
+
export const launchedChromium = async (headless) => {
|
|
224
225
|
let mod;
|
|
225
226
|
try {
|
|
226
227
|
mod = await import('playwright');
|
|
@@ -230,7 +231,7 @@ const launchedChromium = async (headless) => {
|
|
|
230
231
|
}
|
|
231
232
|
const settle = getSessionMetrics().recordConnectAttempt(BrowserLaunchKind.LAUNCHED);
|
|
232
233
|
try {
|
|
233
|
-
const browser = await mod.chromium.launch(
|
|
234
|
+
const browser = await mod.chromium.launch(chromiumLaunchOptions(headless));
|
|
234
235
|
settle();
|
|
235
236
|
return browser;
|
|
236
237
|
}
|
|
@@ -36,10 +36,13 @@ export interface ScrollFindResult {
|
|
|
36
36
|
note?: string;
|
|
37
37
|
}
|
|
38
38
|
/**
|
|
39
|
-
* Reveal an element that a windowed/virtualized list has not mounted yet. Queries
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
39
|
+
* Reveal an element that a windowed/virtualized list has not mounted yet. Queries once (it may
|
|
40
|
+
* already be visible), optionally jumps straight to a bisection estimate, then walks the container
|
|
41
|
+
* a viewport at a time re-querying after each step. Downward first; when the bottom is reached the
|
|
42
|
+
* walk TURNS AROUND and continues upward, because a list that is already scrolled to its end keeps
|
|
43
|
+
* its earlier rows mounted above the viewport and a downward-only search answered "exhausted" on
|
|
44
|
+
* its first step (#505). `exhausted` therefore means BOTH directions are spent, not "the bottom is
|
|
45
|
+
* here". Pure orchestration over the session command seam — fully unit-testable with a fake.
|
|
43
46
|
*/
|
|
44
47
|
export declare function scrollToFind(session: ScrollFindSession, q: ScrollFindQuery, opts?: {
|
|
45
48
|
maxScrolls?: number;
|
|
@@ -13,61 +13,99 @@ async function queryFirst(session, q) {
|
|
|
13
13
|
return undefined;
|
|
14
14
|
}
|
|
15
15
|
/**
|
|
16
|
-
* A note attached to `{ found: false, exhausted: true }` only when the failure is
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
16
|
+
* A note attached to `{ found: false, exhausted: true }` only when the failure is actually
|
|
17
|
+
* informative: nothing in the container ever moved (so the "list" being searched was never
|
|
18
|
+
* scrollable and the target is likely inside one with its own scroll container). Reaching the
|
|
19
|
+
* genuine ends of a container that DID scroll stays quiet — that is exhaustion, not confusion.
|
|
20
20
|
*/
|
|
21
|
-
function exhaustNote(q, data) {
|
|
21
|
+
function exhaustNote(q, data, nothingEverScrolled) {
|
|
22
22
|
if (q.container !== undefined)
|
|
23
23
|
return {};
|
|
24
|
-
if (
|
|
24
|
+
if (true === data['scrolled'] || !nothingEverScrolled)
|
|
25
25
|
return {};
|
|
26
26
|
return {
|
|
27
27
|
note: "The document did not scroll — the target may be inside a list with its own scroll container. Pass that container's ref as `container`.",
|
|
28
28
|
};
|
|
29
29
|
}
|
|
30
|
+
/** One viewport-height step upward when the last result carried no clientHeight to size it by. */
|
|
31
|
+
const UP_STEP_FALLBACK_PX = 400;
|
|
30
32
|
/**
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
|
|
34
|
-
|
|
33
|
+
* Size the upward step off the container the SCROLL result just described, falling back to a
|
|
34
|
+
* plain viewport height when the result did not carry one.
|
|
35
|
+
*/
|
|
36
|
+
function upStepPx(last) {
|
|
37
|
+
const h = last['clientHeight'];
|
|
38
|
+
return 'number' === typeof h && h > 0 ? h : UP_STEP_FALLBACK_PX;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Reveal an element that a windowed/virtualized list has not mounted yet. Queries once (it may
|
|
42
|
+
* already be visible), optionally jumps straight to a bisection estimate, then walks the container
|
|
43
|
+
* a viewport at a time re-querying after each step. Downward first; when the bottom is reached the
|
|
44
|
+
* walk TURNS AROUND and continues upward, because a list that is already scrolled to its end keeps
|
|
45
|
+
* its earlier rows mounted above the viewport and a downward-only search answered "exhausted" on
|
|
46
|
+
* its first step (#505). `exhausted` therefore means BOTH directions are spent, not "the bottom is
|
|
47
|
+
* here". Pure orchestration over the session command seam — fully unit-testable with a fake.
|
|
35
48
|
*/
|
|
36
49
|
export async function scrollToFind(session, q, opts = {}) {
|
|
37
50
|
const max = opts.maxScrolls ?? SCROLL_FIND_DEFAULTS.MAX_SCROLLS;
|
|
38
51
|
const first = await queryFirst(session, q);
|
|
39
52
|
if (first !== undefined)
|
|
40
53
|
return { found: true, element: first, scrolls: 0, exhausted: false };
|
|
41
|
-
|
|
42
|
-
// in one scroll command rather than stepping a viewport at a time. Then refine linearly.
|
|
54
|
+
const baseArgs = () => q.container !== undefined ? { ref: q.container } : {};
|
|
43
55
|
let scrolls = 0;
|
|
56
|
+
let last = {};
|
|
57
|
+
// Bisection: if the caller knows the target index and list size, jump to the estimated offset in
|
|
58
|
+
// one scroll command rather than stepping a viewport at a time. The estimate can land on either
|
|
59
|
+
// side of the target, so refinement below still searches both directions from wherever this put us.
|
|
44
60
|
if (q.targetIndex !== undefined && q.totalCount !== undefined && q.totalCount > 1) {
|
|
45
61
|
const fraction = Math.min(1, Math.max(0, q.targetIndex / q.totalCount));
|
|
46
|
-
const sr = await session.command(ReticleCommand.SCROLL, {
|
|
47
|
-
...(q.container !== undefined ? { ref: q.container } : {}),
|
|
48
|
-
fraction,
|
|
49
|
-
});
|
|
62
|
+
const sr = await session.command(ReticleCommand.SCROLL, { ...baseArgs(), fraction });
|
|
50
63
|
scrolls += 1;
|
|
64
|
+
last = asRecord(sr.result);
|
|
51
65
|
const hit = await queryFirst(session, q);
|
|
52
66
|
if (hit !== undefined)
|
|
53
67
|
return { found: true, element: hit, scrolls, exhausted: false };
|
|
54
|
-
|
|
68
|
+
}
|
|
69
|
+
let downwardSpent = false;
|
|
70
|
+
let everScrolled = false;
|
|
71
|
+
// The downward pass. Spending its whole budget without reaching the end means rows may remain
|
|
72
|
+
// further down, so that answer is `exhausted:false` and the upward pass never runs.
|
|
73
|
+
for (let i = 0; i < max && !downwardSpent; i += 1) {
|
|
74
|
+
const sr = await session.command(ReticleCommand.SCROLL, baseArgs());
|
|
75
|
+
scrolls += 1;
|
|
55
76
|
const data = asRecord(sr.result);
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
77
|
+
last = data;
|
|
78
|
+
if (true === data['scrolled'])
|
|
79
|
+
everScrolled = true;
|
|
80
|
+
const hit = await queryFirst(session, q);
|
|
81
|
+
if (hit !== undefined)
|
|
82
|
+
return { found: true, element: hit, scrolls, exhausted: false };
|
|
83
|
+
// Reached the bottom, or the container would not move downward — turn around before giving up.
|
|
84
|
+
if (true === data['atEnd'] || true !== data['scrolled'])
|
|
85
|
+
downwardSpent = true;
|
|
86
|
+
}
|
|
87
|
+
// If the downward pass spent its budget it never saw the end, so there is nothing to turn around
|
|
88
|
+
// for; report honestly rather than doubling a call whose caller asked for `max` steps.
|
|
89
|
+
if (!downwardSpent) {
|
|
90
|
+
return { found: false, scrolls, exhausted: false };
|
|
59
91
|
}
|
|
92
|
+
// The upward pass gets the caller's budget again, not what is left of it: a list at the bottom of
|
|
93
|
+
// 14k px needs ~90 scrolls to reach its end and ~90 more to get back, so sharing one budget would
|
|
94
|
+
// strand the search halfway and answer `exhausted:false` about a list both of whose ends are known.
|
|
60
95
|
for (let i = 0; i < max; i += 1) {
|
|
61
|
-
const sr = await session.command(ReticleCommand.SCROLL,
|
|
96
|
+
const sr = await session.command(ReticleCommand.SCROLL, { ...baseArgs(), dy: -upStepPx(last) });
|
|
62
97
|
scrolls += 1;
|
|
63
98
|
const data = asRecord(sr.result);
|
|
99
|
+
last = data;
|
|
100
|
+
if (true === data['scrolled'])
|
|
101
|
+
everScrolled = true;
|
|
64
102
|
const hit = await queryFirst(session, q);
|
|
65
103
|
if (hit !== undefined)
|
|
66
104
|
return { found: true, element: hit, scrolls, exhausted: false };
|
|
67
|
-
//
|
|
68
|
-
if (true
|
|
69
|
-
return { found: false, scrolls, exhausted: true, ...exhaustNote(q, data) };
|
|
105
|
+
// The top refusing to move means both directions are spent.
|
|
106
|
+
if (true !== data['scrolled']) {
|
|
107
|
+
return { found: false, scrolls, exhausted: true, ...exhaustNote(q, data, !everScrolled) };
|
|
70
108
|
}
|
|
71
109
|
}
|
|
72
|
-
return { found: false, scrolls, exhausted: false }; // spent the budget; more may lie
|
|
110
|
+
return { found: false, scrolls, exhausted: false }; // spent the upward budget; more may lie above
|
|
73
111
|
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { ToolDeps } from '../tools/tool-kit.js';
|
|
2
|
+
/**
|
|
3
|
+
* The ledger id a piece of inline prose is declared under.
|
|
4
|
+
*
|
|
5
|
+
* Derived from the statement rather than minted, so the same sentence declared on five verdicts is
|
|
6
|
+
* one row rather than five, and a DIFFERENT sentence is a different row rather than a false
|
|
7
|
+
* amendment of the first. The slug is there because the id is what an agent later types to reference
|
|
8
|
+
* the intent, and a bare digest is unreadable; the digest is there because the slug is truncated and
|
|
9
|
+
* two long statements sharing a prefix must not share a row.
|
|
10
|
+
*/
|
|
11
|
+
export declare function inlineIntentId(statement: string): string;
|
|
12
|
+
/** What `provenBy` records: which tool drew the verdict, and when. */
|
|
13
|
+
export declare function inlineVerdictId(tool: string, at: number): string;
|
|
14
|
+
/**
|
|
15
|
+
* Put an inline intent in the ledger and attach the predicate about to be evaluated. Returns the id
|
|
16
|
+
* the verdict may later discharge, or undefined when there was no intent to record.
|
|
17
|
+
*
|
|
18
|
+
* `intent` carries EITHER prose OR the id of an intent already in the ledger. One field rather than
|
|
19
|
+
* two because the surface is the scarce thing here and the two cases are told apart by the ledger
|
|
20
|
+
* itself: a string that names an existing row is a reference, anything else is prose. A statement
|
|
21
|
+
* that happens to equal an existing id is the same intent said twice, so pointing at the row is the
|
|
22
|
+
* right answer there too, not a collision.
|
|
23
|
+
*
|
|
24
|
+
* A referenced intent is never re-declared, which would overwrite the agent's own words with an id,
|
|
25
|
+
* and its binding is left alone when it already has one — a predicate bound deliberately through
|
|
26
|
+
* `reticle_intent { action: "bind" }` is a stronger statement than whatever this one call asserts,
|
|
27
|
+
* and quietly replacing it is exactly the narrowing the ledger exists to make visible.
|
|
28
|
+
*
|
|
29
|
+
* Best-effort by construction: a ledger that cannot be written is a small problem, and a verdict
|
|
30
|
+
* that fails to return because of one is a large problem.
|
|
31
|
+
*/
|
|
32
|
+
export declare function linkInlineIntent(deps: ToolDeps, sessionId: string | undefined, intent: string | undefined,
|
|
33
|
+
/** The predicate this verdict will evaluate, or undefined when it proves nothing (a bare settle). */
|
|
34
|
+
binding: unknown): Promise<string | undefined>;
|
|
35
|
+
/**
|
|
36
|
+
* Record that this verdict proved the intent. Call only for a verdict that actually proved something.
|
|
37
|
+
*
|
|
38
|
+
* The ledger's own rule does the rest: `dischargeIntent` refuses an intent with no binding, so an
|
|
39
|
+
* intent declared inline on a wait that asserted no consequence stays open and honest instead of
|
|
40
|
+
* collecting a proof nothing earned.
|
|
41
|
+
*/
|
|
42
|
+
export declare function dischargeInlineIntent(deps: ToolDeps, sessionId: string | undefined, id: string | undefined, proof: {
|
|
43
|
+
verdictId: string;
|
|
44
|
+
grade: string;
|
|
45
|
+
at: number;
|
|
46
|
+
}): Promise<void>;
|