@reticlehq/server 2.10.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 +73 -11
- 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 +41 -0
- package/dist/dev/stale-issue-guard.js +75 -0
- package/dist/domain/domain-tools.js +3 -2
- 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 +30 -0
- package/dist/events/predicate-asks.js +48 -0
- package/dist/events/predicate-eval.js +254 -25
- package/dist/events/predicate-parse.d.ts +0 -6
- package/dist/events/predicate-parse.js +51 -5
- package/dist/events/predicate-schema.d.ts +62 -0
- package/dist/events/predicate-schema.js +162 -4
- 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 +65 -8
- package/dist/flows/flows.d.ts +6 -1
- package/dist/flows/flows.js +34 -22
- package/dist/flows/predicate-to-expect.js +14 -3
- package/dist/flows/server-verify.js +16 -2
- package/dist/flows/verify-change-tools.js +9 -2
- package/dist/honesty/blind-spots.d.ts +8 -16
- package/dist/honesty/blind-spots.js +18 -5
- package/dist/honesty/feature-capture.d.ts +150 -0
- package/dist/honesty/feature-capture.js +233 -0
- package/dist/honesty/gap-ledger.d.ts +52 -0
- package/dist/honesty/gap-ledger.js +63 -0
- package/dist/honesty/instrumentation-gaps.d.ts +79 -0
- package/dist/honesty/instrumentation-gaps.js +77 -0
- package/dist/honesty/observability.d.ts +37 -0
- package/dist/honesty/observability.js +40 -0
- 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 -2
- package/dist/index.js +41 -0
- 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/node-io.js +3 -0
- 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 +2 -10
- package/dist/init/run.js +26 -68
- package/dist/init/slash-command.d.ts +1 -1
- package/dist/init/slash-command.js +33 -10
- 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 +2 -2
- package/dist/journal/journal-recorder.d.ts +23 -0
- package/dist/journal/journal-recorder.js +35 -0
- package/dist/license/license.d.ts +8 -2
- package/dist/license/license.js +10 -15
- 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/artifact-root.d.ts +83 -0
- package/dist/project/artifact-root.js +112 -0
- package/dist/project/fs-port.d.ts +3 -0
- package/dist/project/fs-port.js +3 -2
- package/dist/project/memory-fs.d.ts +9 -0
- package/dist/project/memory-fs.js +93 -0
- package/dist/project/project-store.d.ts +17 -0
- package/dist/project/project-store.js +33 -0
- package/dist/project/project-tools.js +3 -32
- package/dist/project/remember-project.d.ts +24 -0
- package/dist/project/remember-project.js +40 -0
- package/dist/project/reticle-dir.d.ts +2 -0
- package/dist/project/reticle-dir.js +1 -0
- package/dist/project/session-root.d.ts +15 -0
- package/dist/project/session-root.js +31 -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/run-tools.js +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/lease-availability.d.ts +40 -0
- package/dist/session/lease-availability.js +43 -0
- package/dist/session/live-control-tools.js +1 -42
- package/dist/session/no-session-diagnosis.d.ts +20 -0
- package/dist/session/no-session-diagnosis.js +41 -12
- 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 +83 -65
- 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 +40 -1
- package/dist/session/session.js +84 -2
- package/dist/session/stall-clock.d.ts +18 -0
- package/dist/session/stall-clock.js +48 -0
- package/dist/telemetry/automation-hint.d.ts +2 -1
- package/dist/telemetry/daemon-telemetry.js +12 -23
- 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/session-metrics.d.ts +1 -1
- package/dist/telemetry/session-metrics.js +1 -1
- package/dist/telemetry/telemetry.d.ts +12 -3
- package/dist/telemetry/telemetry.js +25 -5
- package/dist/telemetry/verification-of.js +2 -2
- package/dist/tools/act-tools.js +159 -24
- package/dist/tools/assert-source.d.ts +22 -0
- package/dist/tools/assert-source.js +53 -0
- package/dist/tools/assert-verdict.d.ts +21 -2
- package/dist/tools/assert-verdict.js +59 -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/contract-tools.js +5 -3
- package/dist/tools/coverage-tools.d.ts +10 -1
- package/dist/tools/coverage-tools.js +124 -37
- 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 +70 -27
- 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 +28 -1
- 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 +3 -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 +3 -3
- package/docs/enterprise.md +3 -1
- package/docs/gates.md +1 -1
- package/docs/install-manual.mdx +1 -1
- package/docs/license-activation.mdx +145 -0
- 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/telemetry-contract.md +1 -12
- package/docs/telemetry-events.mdx +1 -34
- package/docs/telemetry.md +0 -1
- 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
- package/dist/telemetry/instrumentation-stall.d.ts +0 -57
- package/dist/telemetry/instrumentation-stall.js +0 -103
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Finish a truncated snapshot, or say it could not be finished.
|
|
3
|
+
*
|
|
4
|
+
* The snapshot walk stops at its node cap and returns a document-order PREFIX with `truncated:true`.
|
|
5
|
+
* That flag has always been honest and has never been actionable: it says the read stopped, not
|
|
6
|
+
* where, so a caller could receive half a page and draw a NEGATIVE conclusion from it — "no error is
|
|
7
|
+
* shown", "the row is gone" — when the thing was simply past the cut. A read that under-reports is a
|
|
8
|
+
* verification result that is quietly wrong.
|
|
9
|
+
*
|
|
10
|
+
* The walk now names its own frontier (`unread`: refs of the subtrees it never entered), so the cut
|
|
11
|
+
* is recoverable: re-read each branch at its own path and the union is the whole tree. This is that
|
|
12
|
+
* loop, plus the half that matters — when the union CANNOT be assembled it says so, and the caller
|
|
13
|
+
* must not turn an empty result over it into an absence.
|
|
14
|
+
*
|
|
15
|
+
* The assembled tree is a UNION of subtrees, not a re-render: each appended chunk is indented from
|
|
16
|
+
* its own root. Callers scan it line by line (that is what every consumer of `tree` does), so the
|
|
17
|
+
* indentation restart costs nothing and re-walking the whole page from scratch would cost a lot.
|
|
18
|
+
*/
|
|
19
|
+
/**
|
|
20
|
+
* How many snapshot round-trips one completion may spend.
|
|
21
|
+
*
|
|
22
|
+
* Each is a real command to the page, so this is the cost ceiling of a completed read, not a safety
|
|
23
|
+
* net. Twelve covers a page whose cut frontier is a handful of branches a couple of levels deep —
|
|
24
|
+
* the shape a node cap actually produces — while keeping the worst case near a dozen round-trips
|
|
25
|
+
* rather than a walk of the DOM one subtree at a time.
|
|
26
|
+
*/
|
|
27
|
+
export declare const MAX_COMPLETION_READS = 12;
|
|
28
|
+
/**
|
|
29
|
+
* How many times the completion may follow a frontier into a new one.
|
|
30
|
+
*
|
|
31
|
+
* A branch re-read can itself truncate and name further branches, so this recursion needs a floor
|
|
32
|
+
* under it independent of the read budget: a hostile or pathologically deep DOM can hand back a new
|
|
33
|
+
* frontier every round forever. Four rounds (the first read plus three levels of re-reads) is deeper
|
|
34
|
+
* than a node cap reaches on a real page, and terminates on one that is not real.
|
|
35
|
+
*/
|
|
36
|
+
export declare const MAX_COMPLETION_DEPTH = 4;
|
|
37
|
+
/** Reads one snapshot — whole page when `scope` is undefined, that branch when it is not. */
|
|
38
|
+
export type SnapshotReader = (scope: string | undefined) => Promise<unknown>;
|
|
39
|
+
export interface CompletedRead {
|
|
40
|
+
/** The union of every subtree that was read. Never a lie about what it contains — only about how much. */
|
|
41
|
+
tree: string;
|
|
42
|
+
/** True only when every branch the page cut was read back. A negative conclusion needs this. */
|
|
43
|
+
complete: boolean;
|
|
44
|
+
reads: number;
|
|
45
|
+
/** Why completion stopped short, for the caller to quote. Present ONLY when `complete` is false. */
|
|
46
|
+
incompleteBecause?: string;
|
|
47
|
+
}
|
|
48
|
+
export declare function readCompleteTree(read: SnapshotReader): Promise<CompletedRead>;
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
/**
|
|
3
|
+
* Finish a truncated snapshot, or say it could not be finished.
|
|
4
|
+
*
|
|
5
|
+
* The snapshot walk stops at its node cap and returns a document-order PREFIX with `truncated:true`.
|
|
6
|
+
* That flag has always been honest and has never been actionable: it says the read stopped, not
|
|
7
|
+
* where, so a caller could receive half a page and draw a NEGATIVE conclusion from it — "no error is
|
|
8
|
+
* shown", "the row is gone" — when the thing was simply past the cut. A read that under-reports is a
|
|
9
|
+
* verification result that is quietly wrong.
|
|
10
|
+
*
|
|
11
|
+
* The walk now names its own frontier (`unread`: refs of the subtrees it never entered), so the cut
|
|
12
|
+
* is recoverable: re-read each branch at its own path and the union is the whole tree. This is that
|
|
13
|
+
* loop, plus the half that matters — when the union CANNOT be assembled it says so, and the caller
|
|
14
|
+
* must not turn an empty result over it into an absence.
|
|
15
|
+
*
|
|
16
|
+
* The assembled tree is a UNION of subtrees, not a re-render: each appended chunk is indented from
|
|
17
|
+
* its own root. Callers scan it line by line (that is what every consumer of `tree` does), so the
|
|
18
|
+
* indentation restart costs nothing and re-walking the whole page from scratch would cost a lot.
|
|
19
|
+
*/
|
|
20
|
+
/**
|
|
21
|
+
* How many snapshot round-trips one completion may spend.
|
|
22
|
+
*
|
|
23
|
+
* Each is a real command to the page, so this is the cost ceiling of a completed read, not a safety
|
|
24
|
+
* net. Twelve covers a page whose cut frontier is a handful of branches a couple of levels deep —
|
|
25
|
+
* the shape a node cap actually produces — while keeping the worst case near a dozen round-trips
|
|
26
|
+
* rather than a walk of the DOM one subtree at a time.
|
|
27
|
+
*/
|
|
28
|
+
export const MAX_COMPLETION_READS = 12;
|
|
29
|
+
/**
|
|
30
|
+
* How many times the completion may follow a frontier into a new one.
|
|
31
|
+
*
|
|
32
|
+
* A branch re-read can itself truncate and name further branches, so this recursion needs a floor
|
|
33
|
+
* under it independent of the read budget: a hostile or pathologically deep DOM can hand back a new
|
|
34
|
+
* frontier every round forever. Four rounds (the first read plus three levels of re-reads) is deeper
|
|
35
|
+
* than a node cap reaches on a real page, and terminates on one that is not real.
|
|
36
|
+
*/
|
|
37
|
+
export const MAX_COMPLETION_DEPTH = 4;
|
|
38
|
+
const SnapshotShape = z.object({
|
|
39
|
+
tree: z.string().optional(),
|
|
40
|
+
truncated: z.boolean().optional(),
|
|
41
|
+
unread: z.array(z.string()).optional(),
|
|
42
|
+
unreadOverflow: z.literal(true).optional(),
|
|
43
|
+
});
|
|
44
|
+
export async function readCompleteTree(read) {
|
|
45
|
+
const parts = [];
|
|
46
|
+
let frontier = [undefined];
|
|
47
|
+
let reads = 0;
|
|
48
|
+
const stop = (because) => ({
|
|
49
|
+
tree: parts.join('\n'),
|
|
50
|
+
complete: false,
|
|
51
|
+
reads,
|
|
52
|
+
incompleteBecause: because,
|
|
53
|
+
});
|
|
54
|
+
for (let round = 0; round < MAX_COMPLETION_DEPTH; round += 1) {
|
|
55
|
+
const next = [];
|
|
56
|
+
for (const scope of frontier) {
|
|
57
|
+
if (reads >= MAX_COMPLETION_READS) {
|
|
58
|
+
return stop(`the page kept naming unread branches past ${String(MAX_COMPLETION_READS)} reads, so part of it was never seen`);
|
|
59
|
+
}
|
|
60
|
+
const parsed = SnapshotShape.safeParse(await read(scope));
|
|
61
|
+
reads += 1;
|
|
62
|
+
if (!parsed.success)
|
|
63
|
+
return stop('a snapshot came back in a shape this read cannot trust');
|
|
64
|
+
parts.push(parsed.data.tree ?? '');
|
|
65
|
+
if (true === parsed.data.unreadOverflow) {
|
|
66
|
+
return stop('the cut named more branches than one snapshot can carry, so the frontier itself is incomplete and no sequence of re-reads can finish this page');
|
|
67
|
+
}
|
|
68
|
+
next.push(...(parsed.data.unread ?? []));
|
|
69
|
+
}
|
|
70
|
+
if (0 === next.length)
|
|
71
|
+
return { tree: parts.join('\n'), complete: true, reads };
|
|
72
|
+
frontier = next;
|
|
73
|
+
}
|
|
74
|
+
return stop(`re-reads kept cutting after ${String(MAX_COMPLETION_DEPTH)} rounds, so part of the page was never seen`);
|
|
75
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
+
import { sessionRoot } from '../project/session-root.js';
|
|
2
3
|
import { CapabilitiesSchema, ContractReadError, FROM_DISK_ARG, ReticleCommand, } from '@reticlehq/core';
|
|
3
4
|
import { ReticleTool } from './tool-names.js';
|
|
4
5
|
import { asString } from './tools-helpers.js';
|
|
@@ -59,7 +60,7 @@ export const CONTRACT_TOOLS = [
|
|
|
59
60
|
},
|
|
60
61
|
handler: async (deps, args) => {
|
|
61
62
|
if (true === args[FROM_DISK_ARG]) {
|
|
62
|
-
const r = await readContract(deps.fs, deps
|
|
63
|
+
const r = await readContract(deps.fs, sessionRoot(deps, asString(args['sessionId'])));
|
|
63
64
|
if (!r.ok)
|
|
64
65
|
throw new Error(r.reason === ContractReadError.MISSING
|
|
65
66
|
? 'no .reticle/contract.json on disk — run reticle_contract_save first (or omit fromDisk to read the live session)'
|
|
@@ -84,10 +85,11 @@ export const CONTRACT_TOOLS = [
|
|
|
84
85
|
const res = await commandOrThrow(deps, asString(args['sessionId']), ReticleCommand.CAPABILITIES, {});
|
|
85
86
|
const caps = CapabilitiesSchema.parse(res);
|
|
86
87
|
assertSameProject(deps, asString(args['sessionId']));
|
|
87
|
-
|
|
88
|
+
const root = sessionRoot(deps, asString(args['sessionId']));
|
|
89
|
+
await writeContract(deps.fs, root, caps, deps.now);
|
|
88
90
|
return {
|
|
89
91
|
saved: true,
|
|
90
|
-
path: reticleDirPaths(
|
|
92
|
+
path: reticleDirPaths(root).contract,
|
|
91
93
|
testidCount: caps.testids.length,
|
|
92
94
|
signalCount: caps.signals.length,
|
|
93
95
|
};
|
|
@@ -5,5 +5,14 @@ interface Control {
|
|
|
5
5
|
}
|
|
6
6
|
/** Parse the interactive snapshot into {ref,label} controls, preserving document order. */
|
|
7
7
|
export declare function parseControls(tree: string): Control[];
|
|
8
|
-
|
|
8
|
+
/**
|
|
9
|
+
* Build the coverage tool over the dispatchable tool table.
|
|
10
|
+
*
|
|
11
|
+
* The table arrives as a thunk rather than an import: `tools.ts` assembles it FROM this module, so
|
|
12
|
+
* importing it back would close a cycle whose failure mode is a load-order-dependent crash rather
|
|
13
|
+
* than an error anyone sees while writing it. The thunk is called inside the handler, long after the
|
|
14
|
+
* table exists. It carries the merged names — the ones `reticle_run` will actually accept — because
|
|
15
|
+
* a tool that merged into a parent is not something an agent can call or a maintainer can cut.
|
|
16
|
+
*/
|
|
17
|
+
export declare function buildCoverageTools(toolNames: () => readonly string[]): ToolDef[];
|
|
9
18
|
export {};
|
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
+
import { coverageRegressed, observabilityOf } from '../honesty/observability.js';
|
|
3
|
+
import { foldFeatureCapture } from '../honesty/feature-capture.js';
|
|
4
|
+
import { foldToolHitRate } from '../honesty/tool-hit-rate.js';
|
|
5
|
+
import { allSessionIntents } from '../intent/open-intents.js';
|
|
2
6
|
import { ReticleCommand, SnapshotMode } from '@reticlehq/core';
|
|
3
7
|
import { ReticleTool } from './tool-names.js';
|
|
4
8
|
import { asString } from './tools-helpers.js';
|
|
@@ -48,41 +52,124 @@ export function parseControls(tree) {
|
|
|
48
52
|
}
|
|
49
53
|
return controls;
|
|
50
54
|
}
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
55
|
+
/**
|
|
56
|
+
* Build the coverage tool over the dispatchable tool table.
|
|
57
|
+
*
|
|
58
|
+
* The table arrives as a thunk rather than an import: `tools.ts` assembles it FROM this module, so
|
|
59
|
+
* importing it back would close a cycle whose failure mode is a load-order-dependent crash rather
|
|
60
|
+
* than an error anyone sees while writing it. The thunk is called inside the handler, long after the
|
|
61
|
+
* table exists. It carries the merged names — the ones `reticle_run` will actually accept — because
|
|
62
|
+
* a tool that merged into a parent is not something an agent can call or a maintainer can cut.
|
|
63
|
+
*/
|
|
64
|
+
export function buildCoverageTools(toolNames) {
|
|
65
|
+
return [
|
|
66
|
+
{
|
|
67
|
+
name: ReticleTool.COVERAGE,
|
|
68
|
+
description: 'Which interactive controls you have driven this session, and which you have NOT. Returns { total, exercised, untouched:[{ref,label}], alsoDroveGone? } over the controls currently on the page. Use it to decide whether verification is finished: an untouched list that still holds the controls your change affects means you are not done. This is about what you did not TOUCH — distinct from the `coverage` field on an action result, which reports what the layer could not SEE.',
|
|
69
|
+
example: {},
|
|
70
|
+
inputSchema: { ...sessionIdShape },
|
|
71
|
+
outputSchema: {
|
|
72
|
+
total: z.number(),
|
|
73
|
+
exercised: z.number(),
|
|
74
|
+
untouched: z.array(z.object({ ref: z.string(), label: z.string() })),
|
|
75
|
+
alsoDroveGone: z
|
|
76
|
+
.number()
|
|
77
|
+
.optional()
|
|
78
|
+
.describe('Controls you drove that are no longer on the page — usually because the action SUCCEEDED and removed them (archive/delete/submit/navigate). Counted separately so `exercised: 0` never appears immediately after real work.'),
|
|
79
|
+
instrumentationGaps: z
|
|
80
|
+
.array(z.unknown())
|
|
81
|
+
.optional()
|
|
82
|
+
.describe('What this app still cannot tell Reticle, as of your most recent verdict — each entry is { kind, missing, cost, fix, source?, ref? }. These are not controls you skipped; they are checks this app CANNOT answer until it is instrumented, so driving the untouched list will not close them. Apply each `fix` and re-verify: the gap disappears from this list when the app can answer, and every later verdict on this app gets stronger. OMITTED when nothing is missing.'),
|
|
83
|
+
unproven: z
|
|
84
|
+
.boolean()
|
|
85
|
+
.optional()
|
|
86
|
+
.describe('True when verification is NOT finished for a reason driving more controls cannot fix — instrumentationGaps is non-empty. Present only when true, so its absence is not a claim.'),
|
|
87
|
+
observability: z
|
|
88
|
+
.object({ driven: z.number(), observable: z.number(), percent: z.number().optional() })
|
|
89
|
+
.optional()
|
|
90
|
+
.describe('Of the controls you DROVE, how many Reticle could fully observe. `untouched` above is work left for you; this is work left in the APP, and driving more controls does not move it. `percent` is OMITTED when nothing was driven, because 0/0 is not 100%.'),
|
|
91
|
+
featureUse: z
|
|
92
|
+
.object({
|
|
93
|
+
observed: z.boolean(),
|
|
94
|
+
truncated: z.boolean().optional(),
|
|
95
|
+
context: z.unknown().optional(),
|
|
96
|
+
intents: z.unknown().optional(),
|
|
97
|
+
missed: z.unknown().optional(),
|
|
98
|
+
})
|
|
99
|
+
.optional()
|
|
100
|
+
.describe('Whether this session used `reticle_context` and `reticle_intent` at all, and what it cost when it did not: `context` counts the calls and what followed each one, `intents` the ledger, `missed` the verdicts drawn against an empty ledger and the reads that re-fetched a fact already established. `observed:false` means nothing was recorded for this session — NOT that nothing was used. An instrument for deciding whether those two features earn their place; nothing here is a fault of yours.'),
|
|
101
|
+
toolHitRate: z
|
|
102
|
+
.object({
|
|
103
|
+
observed: z.boolean(),
|
|
104
|
+
cells: z.unknown().optional(),
|
|
105
|
+
called: z.unknown().optional(),
|
|
106
|
+
neverCalled: z.unknown().optional(),
|
|
107
|
+
})
|
|
108
|
+
.optional()
|
|
109
|
+
.describe('Which Reticle tools THIS session called, and how reachable each one was: `called` lists every tool with its call count, `neverCalled` the rest of the table, and both carry a `tier` — `default` (advertised to every agent), `extended` (advertised only on the full surface) or `run-only` (reachable solely by naming it to reticle_run). `cells` counts each tier on both sides. `observed:false` means nothing was recorded for this session, NOT that nothing was called. THREE LIMITS, all load-bearing: this is ONE session, so it is evidence about this run and is not a hit rate — a hit rate is a distribution over many sessions. A never-called tool is not a useless tool; one used once a month in an emergency looks identical here to one nobody has ever called, so frequency alone cannot decide a cut. And the counts describe how this run went, never how it should have gone — nothing here is a fault of yours.'),
|
|
110
|
+
observabilityRegressed: z
|
|
111
|
+
.object({ was: z.number(), now: z.number() })
|
|
112
|
+
.optional()
|
|
113
|
+
.describe('This project has previously reached a HIGHER observability than this run did. Usually means an assertion or an instrumented path was removed — the cheapest way to stop a gap firing is to stop asserting the thing that revealed it. Present only when a drop is real and the run was large enough to compare.'),
|
|
114
|
+
},
|
|
115
|
+
handler: async (deps, args) => {
|
|
116
|
+
const sessionId = asString(args['sessionId']);
|
|
117
|
+
const session = deps.sessions.resolve(sessionId);
|
|
118
|
+
// INTERACTIVE mode is already the "controls only" view, so the denominator is the page's own
|
|
119
|
+
// notion of what can be driven rather than a second, drifting definition maintained here.
|
|
120
|
+
const result = await commandOrThrow(deps, sessionId, ReticleCommand.SNAPSHOT, {
|
|
121
|
+
mode: SnapshotMode.INTERACTIVE,
|
|
122
|
+
});
|
|
123
|
+
const tree = asString(result['tree']) ?? '';
|
|
124
|
+
// Matched by ref AND by label — see coverage-identity. A ref dies with the next re-render, so
|
|
125
|
+
// on a framework that replaces nodes this reported `exercised: 0` however much work was done.
|
|
126
|
+
// `droveGone` keeps the other honesty: archive/delete/submit remove their own control, so a
|
|
127
|
+
// drive that WORKED must not read as no coverage at all.
|
|
128
|
+
const { exercised, droveGone, untouched } = exercisedCount(parseControls(tree), session.actedRefs(), session.actedLabels());
|
|
129
|
+
// The other half of "am I done?". `untouched` answers what you did not DRIVE; this answers
|
|
130
|
+
// what this app cannot ANSWER, which no amount of further driving will change. Reporting the
|
|
131
|
+
// first without the second is how an agent finishes a pass believing it verified something the
|
|
132
|
+
// app was never able to confirm.
|
|
133
|
+
const gaps = session.gaps?.open() ?? [];
|
|
134
|
+
// Folded here rather than given a tool of its own: this is already the "am I done, and what is
|
|
135
|
+
// this verdict worth" read, and a number nobody has a reason to ask for separately is one
|
|
136
|
+
// nobody ever sees. It is a fold over the journal and the intent ledger plus the one thing
|
|
137
|
+
// neither records — see honesty/feature-capture.ts.
|
|
138
|
+
const featureUse = foldFeatureCapture({
|
|
139
|
+
calls: session.capture.calls(),
|
|
140
|
+
dropped: session.capture.dropped,
|
|
141
|
+
actions: await session.readJournalActions(),
|
|
142
|
+
intents: await allSessionIntents(deps, sessionId),
|
|
143
|
+
finalActions: session.actionCount,
|
|
144
|
+
});
|
|
145
|
+
// Same fold, same reason, one level out: which of the whole tool table this session reached
|
|
146
|
+
// for, crossed with how reachable each name was. See honesty/tool-hit-rate.ts for what it can
|
|
147
|
+
// and cannot establish — the limits matter more than usual, because this is the number a
|
|
148
|
+
// decision to DELETE tools would be made on.
|
|
149
|
+
const toolHitRate = foldToolHitRate({
|
|
150
|
+
calls: session.capture?.toolCalls(),
|
|
151
|
+
allTools: toolNames(),
|
|
152
|
+
});
|
|
153
|
+
// The number, and the floor under it, together. A coverage figure that can only ever be
|
|
154
|
+
// reported and never contradicted is one an agent learns to satisfy rather than to earn.
|
|
155
|
+
const observability = observabilityOf(session.actedRefs(), gaps);
|
|
156
|
+
const best = await deps.project.bestObservability();
|
|
157
|
+
const regressed = coverageRegressed(best, observability);
|
|
158
|
+
if (observability.percent !== undefined) {
|
|
159
|
+
await deps.project.raiseObservability(observability.percent);
|
|
160
|
+
}
|
|
161
|
+
return {
|
|
162
|
+
total: parseControls(tree).length,
|
|
163
|
+
exercised,
|
|
164
|
+
untouched,
|
|
165
|
+
...(droveGone > 0 ? { alsoDroveGone: droveGone } : {}),
|
|
166
|
+
...(gaps.length > 0 ? { instrumentationGaps: gaps, unproven: true } : {}),
|
|
167
|
+
observability,
|
|
168
|
+
...(regressed === undefined ? {} : { observabilityRegressed: regressed }),
|
|
169
|
+
featureUse,
|
|
170
|
+
toolHitRate,
|
|
171
|
+
};
|
|
172
|
+
},
|
|
86
173
|
},
|
|
87
|
-
|
|
88
|
-
|
|
174
|
+
];
|
|
175
|
+
}
|
|
@@ -3,8 +3,9 @@ import { runTool } from './invoke-tool.js';
|
|
|
3
3
|
import { buildErrorPayload } from './error-recovery.js';
|
|
4
4
|
import { mergedNameRedirect, mergedNameMessage } from './merged-name-redirect.js';
|
|
5
5
|
import { ReticleTool } from './tool-names.js';
|
|
6
|
-
import {
|
|
6
|
+
import { ADVERTISE_ALL_ENV } from './tool-surface.js';
|
|
7
7
|
import { getSessionMetrics } from '../telemetry/session-metrics.js';
|
|
8
|
+
import { isPredicateParam, predicateGrammar } from '../events/predicate-eval.js';
|
|
8
9
|
/**
|
|
9
10
|
* On-demand tool loading for MCP — the answer to the per-turn tool-definition tax.
|
|
10
11
|
*
|
|
@@ -47,6 +48,8 @@ function paramInfo(shape) {
|
|
|
47
48
|
function unknownParamsError(toolName, unknown) {
|
|
48
49
|
return `unknown ${1 === unknown.length ? 'parameter' : 'parameters'} for ${toolName}: ${unknown.join(', ')} — NOT applied, so any result would be an answer to a different question`;
|
|
49
50
|
}
|
|
51
|
+
/** What to write instead — the one parameter reticle_tools declares, and the no-argument form. */
|
|
52
|
+
const TOOLS_ARG_HINT = 'name the tools you want in `names`: reticle_tools { names: ["reticle_act_and_wait"] } — or call it with no arguments for the full catalog';
|
|
50
53
|
/** The keys not declared by `shape`, in call order. */
|
|
51
54
|
function unknownKeys(args, shape) {
|
|
52
55
|
const declared = new Set(Object.keys(shape));
|
|
@@ -70,19 +73,32 @@ export function buildDynamicTools(allTools, profile) {
|
|
|
70
73
|
// advertises the whole registry any more, so BOTH meta-tools are on every surface and
|
|
71
74
|
// reticle_run is always the way to the tail. The old wording would now send an agent
|
|
72
75
|
// away from the only tool that can reach half the registry.
|
|
73
|
-
note: `The surface is read once at daemon startup:
|
|
76
|
+
note: `The surface is read once at daemon startup: set ${ADVERTISE_ALL_ENV}=1 and restart the daemon, or it has no effect. No surface advertises every tool: the advertised count is capped because editors budget tools across all connected MCP servers. Every tool listed here is callable through reticle_run { tool, args } whether or not it is advertised.`,
|
|
74
77
|
},
|
|
75
78
|
};
|
|
79
|
+
const toolsShape = {
|
|
80
|
+
names: z
|
|
81
|
+
.array(z.string())
|
|
82
|
+
.optional()
|
|
83
|
+
.describe('Tool names to load full params for. Omit to list all tools with summaries.'),
|
|
84
|
+
};
|
|
76
85
|
const reticleTools = {
|
|
77
86
|
name: ReticleTool.TOOLS,
|
|
78
87
|
description: 'Discover Reticle tools on demand. Call with no arguments to list every tool (name + one-line summary); call with names:["reticle_network", …] to load full descriptions and parameters for specific tools. Then invoke them with reticle_run. This avoids paying for every tool definition on every turn. To make a verification REUSABLE (record once, replay free forever), the flow workflow lives here: reticle_record{action:"start"} → act → reticle_flow_save → reticle_verify{action:"flows"} (and reticle_flow_heal on drift). Load those names when you want to save or re-run a flow.',
|
|
79
|
-
inputSchema:
|
|
80
|
-
names: z
|
|
81
|
-
.array(z.string())
|
|
82
|
-
.optional()
|
|
83
|
-
.describe('Tool names to load full params for. Omit to list all tools with summaries.'),
|
|
84
|
-
},
|
|
88
|
+
inputSchema: toolsShape,
|
|
85
89
|
handler: (_deps, args) => {
|
|
90
|
+
// The discovery tool declared one parameter and checked none, so a call that misnamed it had
|
|
91
|
+
// the key dropped and came back with the WHOLE catalogue — a well-formed answer to a question
|
|
92
|
+
// nobody asked, and one the caller cannot tell from the answer it wanted. Same check and same
|
|
93
|
+
// wording as reticle_run's, because two spellings of one refusal is how one of them rots.
|
|
94
|
+
const stray = unknownKeys(args, toolsShape);
|
|
95
|
+
if (stray.length > 0) {
|
|
96
|
+
return Promise.resolve({
|
|
97
|
+
error: unknownParamsError(ReticleTool.TOOLS, stray),
|
|
98
|
+
params: paramInfo(toolsShape),
|
|
99
|
+
hint: TOOLS_ARG_HINT,
|
|
100
|
+
});
|
|
101
|
+
}
|
|
86
102
|
const names = Array.isArray(args['names'])
|
|
87
103
|
? args['names'].filter((n) => 'string' === typeof n)
|
|
88
104
|
: undefined;
|
|
@@ -101,6 +117,10 @@ export function buildDynamicTools(allTools, profile) {
|
|
|
101
117
|
next: `All ${catalog.length} tools above are callable, advertised or not. Load full params with reticle_tools { names:[…] }, then call reticle_run { tool, args }.`,
|
|
102
118
|
});
|
|
103
119
|
}
|
|
120
|
+
// The grammar rides ONLY on a `names:[…]` reply that asked for a tool taking a predicate, so
|
|
121
|
+
// no turn pays for it in the catalog or in the advertised schema. The lean tool surface says
|
|
122
|
+
// "call reticle_tools for the full field grammar of a kind"; this is where that lands.
|
|
123
|
+
const carriesPredicate = names.some((n) => Object.values(byName.get(n)?.inputSchema ?? {}).some((schema) => isPredicateParam(schema)));
|
|
104
124
|
return Promise.resolve({
|
|
105
125
|
tools: names.map((n) => {
|
|
106
126
|
const t = byName.get(n);
|
|
@@ -108,6 +128,7 @@ export function buildDynamicTools(allTools, profile) {
|
|
|
108
128
|
? { name: n, error: 'unknown tool' }
|
|
109
129
|
: { name: n, description: t.description, params: paramInfo(t.inputSchema) };
|
|
110
130
|
}),
|
|
131
|
+
...(carriesPredicate ? { predicateGrammar: predicateGrammar() } : {}),
|
|
111
132
|
});
|
|
112
133
|
},
|
|
113
134
|
};
|
|
@@ -18,11 +18,35 @@ export declare const RECOVERY: {
|
|
|
18
18
|
readonly MISSING_BASELINE: string;
|
|
19
19
|
readonly MISSING_RECORDING: string;
|
|
20
20
|
readonly STALE_REF: string;
|
|
21
|
+
readonly STALE_REF_AFTER_EDIT: string;
|
|
21
22
|
readonly INVALID_NAME: string;
|
|
23
|
+
/**
|
|
24
|
+
* The call was well-formed and the SELECTOR missed. Nothing about the arguments was wrong, so the
|
|
25
|
+
* BAD_ARGUMENTS answer ("re-read that tool's parameters") sends the reader to the one place that
|
|
26
|
+
* holds no answer — measured live on a `target: { testid }` that simply was not on the page yet.
|
|
27
|
+
*
|
|
28
|
+
* The error text already names the two moves that work, so this adds the fact the text cannot
|
|
29
|
+
* carry: which of them to reach for, and that nothing was acted on.
|
|
30
|
+
*/
|
|
31
|
+
readonly TARGET_MISSED: string;
|
|
22
32
|
readonly NO_SUCH_OPTION: string;
|
|
23
33
|
readonly BAD_ARGUMENTS: string;
|
|
24
34
|
readonly WRONG_TARGET: string;
|
|
25
35
|
readonly NOT_EDITABLE: string;
|
|
36
|
+
/**
|
|
37
|
+
* Written as the ARGUMENT OBJECT the caller has to send, not as a dotted path.
|
|
38
|
+
*
|
|
39
|
+
* Measured on a benchmark run that spent its entire budget here: the refusal said "retry with
|
|
40
|
+
* args.confirmDangerous=true", and the agent went looking for a parameter by that name, could not
|
|
41
|
+
* find one at the top level of the schema, and re-read the tool list rather than retrying. The
|
|
42
|
+
* flag IS documented — inside the `args` object's own description — so a dotted path pointing at
|
|
43
|
+
* something that is not a top-level parameter is the whole distance between a one-line retry and
|
|
44
|
+
* a lost budget.
|
|
45
|
+
*
|
|
46
|
+
* The classifier is also wider than "destructive": it matches `deploy` and `publish`, so an
|
|
47
|
+
* ordinary "New deploy" button trips it. Naming a real trigger word here keeps that from reading
|
|
48
|
+
* as a malfunction on an app where nothing is being deleted.
|
|
49
|
+
*/
|
|
26
50
|
readonly CONFIRM_DANGEROUS: string;
|
|
27
51
|
readonly UNSUPPORTED_SURFACE: string;
|
|
28
52
|
readonly TOKEN_REQUIRED: string;
|
|
@@ -50,20 +74,6 @@ export declare function recoveryFor(message: string): string | undefined;
|
|
|
50
74
|
* this table rather than about anybody's app.
|
|
51
75
|
*/
|
|
52
76
|
export declare function refusalReasonFor(rawMessage: string): RefusalReason;
|
|
53
|
-
/**
|
|
54
|
-
* A serialized zod issue array, rendered as a sentence. Anything else is returned untouched.
|
|
55
|
-
*
|
|
56
|
-
* This is the one boundary every agent-visible tool failure crosses, which is why the rewrite lives
|
|
57
|
-
* here rather than in each tool. `parsePredicate` solved this for the three predicate tools by never
|
|
58
|
-
* producing the array; it cannot help anywhere else, because the MCP SDK validates a tool's schema
|
|
59
|
-
* BEFORE our handler runs. So `reticle_session` with no action — the commonest mistake there is —
|
|
60
|
-
* never reaches the friendly "unknown action … expected: [tune, yield, …]" its handler already
|
|
61
|
-
* builds, and the agent gets the raw array instead. Driven live against the shipped daemon.
|
|
62
|
-
*
|
|
63
|
-
* Conservative by construction: it only rewrites a JSON ARRAY whose every element carries both
|
|
64
|
-
* `code` and `message`. A tool that legitimately returns JSON is left alone.
|
|
65
|
-
*/
|
|
66
|
-
export declare function zodArrayAsSentence(message: string): string;
|
|
67
77
|
/**
|
|
68
78
|
* The ask attached to errors we do NOT recognize. An unrecognized error is, by definition, the case
|
|
69
79
|
* where Reticle failed in a way nobody anticipated — the single highest-value moment to hear from the
|
|
@@ -66,7 +66,9 @@ export const RECOVERY = {
|
|
|
66
66
|
'target the one you mean.',
|
|
67
67
|
UNKNOWN_SESSION: 'That sessionId is not connected. Call reticle_sessions for the current ids and retry with a valid one.',
|
|
68
68
|
THROTTLED: 'The target tab is backgrounded/throttled, so actions may silently no-op. Ask the human to bring ' +
|
|
69
|
-
'the tab to the front, or
|
|
69
|
+
'the tab to the front, or acquire a guaranteed scriptable context yourself with ' +
|
|
70
|
+
'`reticle_run { tool: "reticle_lease", action: "acquire", url }` (a human can equivalently run ' +
|
|
71
|
+
'`reticle drive <url>`).',
|
|
70
72
|
MISSING_BASELINE: 'That baseline does not exist yet. Call reticle_baseline { action: "list" } to see saved names, or ' +
|
|
71
73
|
'reticle_baseline { action: "save", name } to capture one before diffing against it.',
|
|
72
74
|
MISSING_RECORDING:
|
|
@@ -79,11 +81,28 @@ export const RECOVERY = {
|
|
|
79
81
|
'before it. Reticle refuses here rather than clicking whatever now occupies that slot. Call ' +
|
|
80
82
|
'reticle_query again for a fresh ref and retry the action — and prefer reticle_act_and_wait ' +
|
|
81
83
|
'{ until } when an action changes the page, so the next ref is taken after it settles.',
|
|
84
|
+
STALE_REF_AFTER_EDIT: 'That ref went stale because YOUR OWN EDIT landed: the dev server hot-updated the module named ' +
|
|
85
|
+
'in the message above and the framework re-rendered, so the node the ref pointed at was ' +
|
|
86
|
+
'replaced. Nothing is wrong with the app and nothing is wrong with Reticle — this is the ' +
|
|
87
|
+
'build-and-verify loop working. Call reticle_query for a fresh ref and drive again, and treat ' +
|
|
88
|
+
'anything you observed before the update as describing the previous version of the code.',
|
|
82
89
|
INVALID_NAME: 'That name is not a usable one. A flow, baseline or recording name must be a single safe path ' +
|
|
83
90
|
'segment — letters, digits, dash and underscore only, starting with a letter or digit, at most ' +
|
|
84
91
|
'64 characters. No slashes, no "..", no leading dot: the name becomes a filename under ' +
|
|
85
92
|
'.reticle/, so anything that could escape that directory is refused. Retry with a slug like ' +
|
|
86
93
|
'"checkout-happy-path". This is an invalid call, not a Reticle defect: there is nothing to report.',
|
|
94
|
+
/**
|
|
95
|
+
* The call was well-formed and the SELECTOR missed. Nothing about the arguments was wrong, so the
|
|
96
|
+
* BAD_ARGUMENTS answer ("re-read that tool's parameters") sends the reader to the one place that
|
|
97
|
+
* holds no answer — measured live on a `target: { testid }` that simply was not on the page yet.
|
|
98
|
+
*
|
|
99
|
+
* The error text already names the two moves that work, so this adds the fact the text cannot
|
|
100
|
+
* carry: which of them to reach for, and that nothing was acted on.
|
|
101
|
+
*/
|
|
102
|
+
TARGET_MISSED: 'The call was valid — the selector matched nothing on the page RIGHT NOW, and nothing was ' +
|
|
103
|
+
'acted on. Take a reticle_snapshot to see what is actually there (a view may still be ' +
|
|
104
|
+
'rendering, or the control may be named differently), then retry with what it shows. This is ' +
|
|
105
|
+
'a miss, not a Reticle defect: there is nothing to report.',
|
|
87
106
|
NO_SUCH_OPTION: 'That <select> has no option with the value you asked for, and the message above lists the ones ' +
|
|
88
107
|
'it does have. Reticle refuses rather than assigning it: an unmatched value deselects everything, ' +
|
|
89
108
|
'and the change event that follows has been observed persisting an empty setting into the app. ' +
|
|
@@ -102,9 +121,24 @@ export const RECOVERY = {
|
|
|
102
121
|
'telling you something: drive whatever ENABLES the field (open the editor, satisfy the ' +
|
|
103
122
|
'precondition) and act again. If the field should have been editable, that is a bug in the app, ' +
|
|
104
123
|
'not in Reticle.',
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
124
|
+
/**
|
|
125
|
+
* Written as the ARGUMENT OBJECT the caller has to send, not as a dotted path.
|
|
126
|
+
*
|
|
127
|
+
* Measured on a benchmark run that spent its entire budget here: the refusal said "retry with
|
|
128
|
+
* args.confirmDangerous=true", and the agent went looking for a parameter by that name, could not
|
|
129
|
+
* find one at the top level of the schema, and re-read the tool list rather than retrying. The
|
|
130
|
+
* flag IS documented — inside the `args` object's own description — so a dotted path pointing at
|
|
131
|
+
* something that is not a top-level parameter is the whole distance between a one-line retry and
|
|
132
|
+
* a lost budget.
|
|
133
|
+
*
|
|
134
|
+
* The classifier is also wider than "destructive": it matches `deploy` and `publish`, so an
|
|
135
|
+
* ordinary "New deploy" button trips it. Naming a real trigger word here keeps that from reading
|
|
136
|
+
* as a malfunction on an app where nothing is being deleted.
|
|
137
|
+
*/
|
|
138
|
+
CONFIRM_DANGEROUS: 'Reticle blocked a control whose label reads as consequential (delete, remove, revoke, deploy, ' +
|
|
139
|
+
'publish, pay…) on purpose. If you mean it, send the SAME call again with ' +
|
|
140
|
+
'`args: { confirmDangerous: true }` — it goes inside the `args` object, not beside it. This is ' +
|
|
141
|
+
'a deliberate refusal, not a defect: there is nothing to report.',
|
|
108
142
|
UNSUPPORTED_SURFACE: 'That surface is not supported yet, and Reticle refuses rather than pretending — a rich-text ' +
|
|
109
143
|
'editor keeps its own document model, so writing to the DOM would look right and submit the old ' +
|
|
110
144
|
'content. Drive the same outcome another way (a plain input, a command, a keyboard action) or ' +
|
|
@@ -163,7 +197,9 @@ const REASON_OF = {
|
|
|
163
197
|
MISSING_BASELINE: RefusalReason.NO_MATCH,
|
|
164
198
|
MISSING_RECORDING: RefusalReason.NO_MATCH,
|
|
165
199
|
STALE_REF: RefusalReason.NO_MATCH,
|
|
200
|
+
STALE_REF_AFTER_EDIT: RefusalReason.NO_MATCH,
|
|
166
201
|
NO_SUCH_OPTION: RefusalReason.NO_MATCH,
|
|
202
|
+
TARGET_MISSED: RefusalReason.NO_MATCH,
|
|
167
203
|
FLOW_STEP_MISSING: RefusalReason.NO_MATCH,
|
|
168
204
|
UNSUPPORTED_SURFACE: RefusalReason.UNSUPPORTED,
|
|
169
205
|
NOT_EDITABLE: RefusalReason.UNSUPPORTED,
|
|
@@ -200,6 +236,9 @@ const RULES = [
|
|
|
200
236
|
},
|
|
201
237
|
// The commonest post-action condition there is. Unmatched, it fell through to FEEDBACK_ASK and
|
|
202
238
|
// told the agent a successful click's aftermath might be a bug in Reticle.
|
|
239
|
+
// Before the general rule: the edit-aware refusal contains the general one's substring too, and
|
|
240
|
+
// the first hit wins. "Your edit landed" is a next step; "the ref is stale" is a dead end.
|
|
241
|
+
{ match: /the code changed underneath it/i, hint: RECOVERY.STALE_REF_AFTER_EDIT },
|
|
203
242
|
{ match: /no longer resolves to an element/i, hint: RECOVERY.STALE_REF },
|
|
204
243
|
{ match: /no <option> with value/i, hint: RECOVERY.NO_SUCH_OPTION },
|
|
205
244
|
// Every guard in the browser's executeAction. These are Reticle-authored refusals of an invalid
|
|
@@ -224,15 +263,37 @@ const RULES = [
|
|
|
224
263
|
match: /no form to submit|upload target must be|is not an HTMLElement/i,
|
|
225
264
|
hint: RECOVERY.WRONG_TARGET,
|
|
226
265
|
},
|
|
266
|
+
// BEFORE the catch-all, which would otherwise claim this one.
|
|
267
|
+
//
|
|
268
|
+
// "target matched no element ... take a reticle_snapshot" names a `reticle_*` tool in its own
|
|
269
|
+
// advice, so the catch-all below matched it and answered "That call did not match the tool's
|
|
270
|
+
// schema". The call matched the schema perfectly; the element was not on the page. Sending an
|
|
271
|
+
// agent to re-read arguments that were already correct costs it a turn, and this is the commonest
|
|
272
|
+
// refusal there is.
|
|
273
|
+
{ match: /target matched no element/i, hint: RECOVERY.TARGET_MISSED },
|
|
227
274
|
// Authored by Reticle, about the caller's arguments: the message already names the valid answers.
|
|
228
275
|
{ match: /^unknown action '/i, hint: RECOVERY.BAD_ARGUMENTS },
|
|
229
276
|
{ match: /^unsupported query strategy '/i, hint: RECOVERY.BAD_ARGUMENTS },
|
|
230
277
|
{ match: /^\w+ requires a string `\w+`/i, hint: RECOVERY.BAD_ARGUMENTS },
|
|
231
|
-
// A message naming a `reticle_*` tool is one WE authored about our own
|
|
232
|
-
// an unanticipated failure. Left unmatched it collected the feedback
|
|
233
|
-
// file reports about their own bad arguments. Kept LAST so a specific
|
|
278
|
+
// A message naming a `reticle_*` tool or an `args.*` parameter is one WE authored about our own
|
|
279
|
+
// API — an invalid call, not an unanticipated failure. Left unmatched it collected the feedback
|
|
280
|
+
// ask, which pushed agents to file reports about their own bad arguments. Kept LAST so a specific
|
|
281
|
+
// rule above always wins.
|
|
282
|
+
//
|
|
283
|
+
// Keyed on the tool/parameter NAME rather than on a sentence shape. The previous form wanted
|
|
284
|
+
// `reticle_x { ... }` with braces AND requires/must/expected, which is one phrasing out of many;
|
|
285
|
+
// measured live, a drag refusal reading "pass a ref from reticle_snapshot or reticle_query as
|
|
286
|
+
// args.toRef" fell straight through and told the agent its own malformed call might be a Reticle
|
|
287
|
+
// defect worth a root-cause report. That is the FOURTH patch to this same default, after the
|
|
288
|
+
// executeAction guards, the destructive-action block, and two of the three spellings of
|
|
289
|
+
// "potentially destructive ... blocked" — each one added a phrasing instead of changing the key.
|
|
290
|
+
//
|
|
291
|
+
// A path is deliberately excluded: `@reticlehq/server/dist/...` in a stack trace is a crash we did
|
|
292
|
+
// NOT anticipate, and silencing the feedback ask there would lose the reports worth having. Hence
|
|
293
|
+
// a word boundary before `reticle_` and a required space-or-punctuation after the name, so a tool
|
|
294
|
+
// named in prose matches and a module path does not.
|
|
234
295
|
{
|
|
235
|
-
match: /reticle_[a-z_]
|
|
296
|
+
match: /(?:^|[\s'"`(])reticle_[a-z_]+(?![\w/.-])|(?:^|[\s'"`(])args\.[a-z][\w]*/i,
|
|
236
297
|
hint: RECOVERY.BAD_ARGUMENTS,
|
|
237
298
|
},
|
|
238
299
|
];
|
|
@@ -321,7 +382,7 @@ function issueClause(issue) {
|
|
|
321
382
|
* Conservative by construction: it only rewrites a JSON ARRAY whose every element carries both
|
|
322
383
|
* `code` and `message`. A tool that legitimately returns JSON is left alone.
|
|
323
384
|
*/
|
|
324
|
-
|
|
385
|
+
function zodArrayAsSentence(message) {
|
|
325
386
|
const text = message.trim();
|
|
326
387
|
if (!text.startsWith('['))
|
|
327
388
|
return message;
|
|
@@ -24,7 +24,7 @@ export declare const FEEDBACK_PROMPT: {
|
|
|
24
24
|
* calls, assert ZERO, verification_completed 2. Agents were verifying the entire time and the metric
|
|
25
25
|
* could not see it. The contract test now checks the SHAPE instead.
|
|
26
26
|
*/
|
|
27
|
-
export declare const
|
|
27
|
+
export declare const VERDICT_TOOLS: ReadonlySet<string>;
|
|
28
28
|
/**
|
|
29
29
|
* The human prompt, when one is due. Resettable for tests.
|
|
30
30
|
*
|