@reticlehq/server 3.1.0 → 3.2.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 +17 -18
- package/dist/command/cli/auth/device-flow.d.ts +2 -2
- package/dist/command/cli/cli-doctor.js +13 -1
- package/dist/command/cli/cli-flow-commands.d.ts +2 -1
- package/dist/command/cli/cli-kill.js +1 -1
- package/dist/command/cli/cli-parse.d.ts +2 -1
- package/dist/command/cli/cli-parse.js +5 -2
- package/dist/command/cli/cli-verify.js +2 -2
- package/dist/command/cli/cloud-cli.js +7 -6
- package/dist/command/cli/cloud-kit.js +2 -6
- package/dist/command/cli/config/config-discovery.d.ts +12 -0
- package/dist/command/cli/config/config-discovery.js +19 -0
- package/dist/command/cli/doctor/doctor-rows.d.ts +12 -0
- package/dist/command/cli/doctor/doctor-rows.js +12 -0
- package/dist/command/cli/drive/drive-command.js +1 -1
- package/dist/command/cli/launch/cli-launch.d.ts +10 -2
- package/dist/command/cli/launch/cli-launch.js +32 -39
- package/dist/command/cli/lifecycle/daemon-lifecycle.d.ts +33 -0
- package/dist/command/cli/lifecycle/daemon-lifecycle.js +198 -0
- package/dist/command/cli/mcp-command.js +5 -3
- package/dist/command/cli/ports/resolve/cli-port.d.ts +2 -1
- package/dist/command/cli/ports/resolve/cli-port.js +5 -1
- package/dist/command/cli/status/status-lines.d.ts +28 -0
- package/dist/command/cli/status/status-lines.js +117 -0
- package/dist/command/cli.d.ts +1 -1
- package/dist/command/cli.js +24 -177
- package/dist/command/daemon/binding/daemon-status-probe.d.ts +2 -0
- package/dist/command/daemon/binding/daemon-status-probe.js +48 -0
- package/dist/command/daemon/binding/loopback-reach.d.ts +42 -0
- package/dist/command/daemon/binding/loopback-reach.js +63 -0
- package/dist/command/daemon/daemon-resilience.d.ts +13 -1
- package/dist/command/daemon/daemon-resilience.js +12 -1
- package/dist/command/daemon/daemon.d.ts +2 -11
- package/dist/command/daemon/daemon.js +8 -11
- package/dist/command/daemon/lifetime/heartbeat.d.ts +8 -0
- package/dist/command/daemon/lifetime/heartbeat.js +10 -0
- package/dist/command/daemon/lifetime/log-cap.d.ts +68 -0
- package/dist/command/daemon/lifetime/log-cap.js +115 -0
- package/dist/command/setup/agent-notice-memory.d.ts +16 -0
- package/dist/command/setup/agent-notice-memory.js +65 -0
- package/dist/command/setup/bringup/dev-server-wait.d.ts +1 -0
- package/dist/command/setup/bringup/dev-server-wait.js +16 -0
- package/dist/command/setup/bringup/ensure-daemon.js +1 -1
- package/dist/command/setup/init/init-runtime.js +13 -1
- package/dist/command/setup/node-effects.js +1 -1
- package/dist/command/setup/probe/page-probe.js +9 -2
- package/dist/command/setup/run-setup.d.ts +25 -7
- package/dist/command/setup/run-setup.js +51 -9
- package/dist/command/setup/setup-command.js +31 -3
- package/dist/command/setup/terminal/confirm.js +2 -1
- package/dist/command/update/updater.d.ts +27 -0
- package/dist/command/update/updater.js +35 -2
- package/dist/features/crawl/crawl-tools.js +3 -1
- package/dist/features/crawl/crawl.js +2 -1
- package/dist/features/harness/drive-report.d.ts +73 -0
- package/dist/features/harness/drive-report.js +196 -0
- package/dist/features/harness/driver.js +29 -8
- package/dist/features/harness/drivers.d.ts +19 -0
- package/dist/features/harness/drivers.js +19 -0
- package/dist/features/harness/fill-values.d.ts +67 -0
- package/dist/features/harness/fill-values.js +153 -0
- package/dist/features/harness/jev-driver.d.ts +188 -0
- package/dist/features/harness/jev-driver.js +664 -0
- package/dist/features/harness/openai-driver.d.ts +46 -0
- package/dist/features/harness/openai-driver.js +197 -0
- package/dist/features/harness/platform-config.d.ts +52 -0
- package/dist/features/harness/platform-config.js +89 -0
- package/dist/features/visual/visual-tools.js +26 -2
- package/dist/index.js +60 -55
- package/dist/judgement/domain/domain-tools.js +4 -2
- package/dist/judgement/runs/verification-sync.d.ts +2 -2
- package/dist/judgement/runs/verification-sync.js +9 -2
- package/dist/language/flows/affected-tools.js +6 -3
- package/dist/language/flows/flow-replay-run.d.ts +25 -2
- package/dist/language/flows/flow-replay-run.js +73 -27
- package/dist/language/flows/flow-replay.d.ts +17 -0
- package/dist/language/flows/flow-replay.js +148 -100
- package/dist/language/flows/flow-result.d.ts +18 -1
- package/dist/language/flows/flow-result.js +18 -2
- package/dist/language/flows/flow-store-for-session.d.ts +2 -1
- package/dist/language/flows/flow-tools.js +8 -4
- package/dist/language/flows/flows.d.ts +8 -7
- package/dist/language/flows/flows.js +7 -3
- package/dist/language/flows/narrow-write.d.ts +4 -4
- package/dist/language/flows/replay.js +1 -1
- package/dist/language/flows/suite-selection.d.ts +15 -1
- package/dist/language/flows/suite-selection.js +1 -1
- package/dist/language/flows/verify-change-tools.js +7 -4
- package/dist/memory/cloud/account-state.d.ts +1 -1
- package/dist/memory/cloud/account-state.js +5 -4
- package/dist/memory/cloud/cloud-config.d.ts +9 -0
- package/dist/memory/cloud/cloud-config.js +9 -0
- package/dist/memory/cloud/cloud-sync.d.ts +7 -2
- package/dist/memory/cloud/cloud-sync.js +10 -5
- package/dist/memory/cloud/harness-config.d.ts +10 -0
- package/dist/memory/cloud/harness-config.js +58 -0
- package/dist/memory/cloud/harness-offer.d.ts +44 -0
- package/dist/memory/cloud/harness-offer.js +146 -0
- package/dist/memory/cloud/harness-switch.d.ts +11 -0
- package/dist/memory/cloud/harness-switch.js +53 -0
- package/dist/memory/cloud/sync-daemon.d.ts +22 -2
- package/dist/memory/cloud/sync-daemon.js +24 -0
- package/dist/memory/impact/impact-recorder.d.ts +8 -0
- package/dist/memory/impact/impact-recorder.js +2 -1
- package/dist/memory/impact/impact-store.d.ts +5 -0
- package/dist/memory/impact/impact-store.js +19 -0
- package/dist/memory/intent/intent-shard-store.js +2 -2
- package/dist/memory/intent/intent-shard.d.ts +12 -12
- package/dist/memory/journal/attach-journal.d.ts +11 -0
- package/dist/memory/journal/attach-journal.js +1 -1
- package/dist/memory/journal/journal-query.d.ts +35 -1
- package/dist/memory/journal/journal-query.js +46 -0
- package/dist/memory/journal/journal-recorder.d.ts +50 -2
- package/dist/memory/journal/journal-recorder.js +45 -7
- package/dist/memory/journal/on-disk/retention.d.ts +87 -2
- package/dist/memory/journal/on-disk/retention.js +177 -8
- package/dist/memory/journal/on-disk/workspace-gitignore.d.ts +9 -0
- package/dist/memory/journal/on-disk/workspace-gitignore.js +9 -2
- package/dist/memory/journal/self-observation.d.ts +3 -0
- package/dist/memory/journal/self-observation.js +67 -0
- package/dist/memory/journal/session-end.d.ts +13 -3
- package/dist/memory/journal/session-end.js +24 -4
- package/dist/memory/journal/session-journal.d.ts +115 -5
- package/dist/memory/journal/session-journal.js +479 -32
- package/dist/memory/project/artifact-root-resolver.d.ts +3 -0
- package/dist/memory/project/artifact-root-resolver.js +89 -0
- package/dist/memory/project/artifact-root.d.ts +39 -2
- package/dist/memory/project/artifact-root.js +58 -1
- package/dist/memory/project/dir/fill-value-store.d.ts +32 -0
- package/dist/memory/project/dir/fill-value-store.js +88 -0
- package/dist/memory/project/dir/reticle-dir.d.ts +14 -4
- package/dist/memory/project/dir/reticle-dir.js +11 -0
- package/dist/memory/project/fs/fs-port.d.ts +22 -1
- package/dist/memory/project/fs/fs-port.js +33 -5
- package/dist/memory/project/learned-routes.d.ts +16 -2
- package/dist/memory/project/learned-routes.js +26 -10
- package/dist/memory/project/project-for-root.d.ts +30 -0
- package/dist/memory/project/project-for-root.js +43 -0
- package/dist/memory/project/project-tools.js +10 -5
- package/dist/memory/project/session-root.d.ts +35 -6
- package/dist/memory/project/session-root.js +70 -8
- package/dist/portal/bridge/auth-failure-reason.d.ts +9 -0
- package/dist/portal/bridge/auth-failure-reason.js +15 -1
- package/dist/portal/bridge/bridge.d.ts +6 -1
- package/dist/portal/bridge/bridge.js +36 -2
- package/dist/portal/session/facts/session-replaced.d.ts +19 -0
- package/dist/portal/session/facts/session-replaced.js +22 -0
- package/dist/portal/session/no-session-diagnosis.d.ts +1 -0
- package/dist/portal/session/no-session-diagnosis.js +67 -60
- package/dist/portal/session/no-session-next-action.js +3 -2
- package/dist/portal/session/no-session-watch.js +14 -10
- package/dist/portal/session/rebind.d.ts +18 -0
- package/dist/portal/session/rebind.js +38 -0
- package/dist/portal/session/session-manager.d.ts +4 -0
- package/dist/portal/session/session-manager.js +19 -1
- package/dist/portal/session/session.d.ts +21 -10
- package/dist/portal/session/session.js +29 -42
- package/dist/status-payload.d.ts +12 -1
- package/dist/status-payload.js +4 -1
- package/dist/surface/http-server.js +3 -5
- package/dist/surface/mcp/faults/mcp-outage.d.ts +2 -1
- package/dist/surface/mcp/faults/mcp-outage.js +20 -4
- package/dist/surface/mcp/mcp-proxy.d.ts +10 -1
- package/dist/surface/mcp/mcp-proxy.js +20 -9
- package/dist/surface/mcp/mcp.d.ts +10 -1
- package/dist/surface/mcp/mcp.js +59 -29
- package/dist/surface/mcp/proxy/proxy-daemon-probe.d.ts +15 -2
- package/dist/surface/mcp/proxy/proxy-daemon-probe.js +22 -5
- package/dist/surface/mcp/proxy/proxy-log.d.ts +5 -11
- package/dist/surface/mcp/proxy/proxy-log.js +7 -22
- package/dist/surface/tools/act/act-preflight.js +6 -1
- package/dist/surface/tools/act/sequence-grade.js +13 -0
- package/dist/surface/tools/act-capsule.js +27 -1
- package/dist/surface/tools/assert/assert-verdict.js +44 -17
- package/dist/surface/tools/coverage-tools.js +5 -2
- package/dist/surface/tools/dynamic-tools.js +37 -46
- package/dist/surface/tools/error-recovery.d.ts +6 -0
- package/dist/surface/tools/error-recovery.js +15 -0
- package/dist/surface/tools/explore-tools.js +80 -7
- package/dist/surface/tools/feedback-tools.js +49 -2
- package/dist/surface/tools/first-sentence.d.ts +24 -0
- package/dist/surface/tools/first-sentence.js +0 -0
- package/dist/surface/tools/harness-explore.d.ts +125 -1
- package/dist/surface/tools/harness-explore.js +401 -15
- package/dist/surface/tools/harness-plan.d.ts +76 -0
- package/dist/surface/tools/harness-plan.js +156 -0
- package/dist/surface/tools/harness-toolset.js +12 -1
- package/dist/surface/tools/invoke-tool.js +65 -5
- package/dist/surface/tools/live-call-text.d.ts +6 -0
- package/dist/surface/tools/live-call-text.js +36 -2
- package/dist/surface/tools/observe-tools.js +55 -9
- package/dist/surface/tools/read/query-windowed.d.ts +1 -1
- package/dist/surface/tools/read/query-windowed.js +1 -1
- package/dist/surface/tools/read-tools.js +1 -2
- package/dist/surface/tools/tool-kit.d.ts +18 -3
- package/dist/surface/tools/tools-helpers.d.ts +8 -7
- package/dist/surface/tools/tools-helpers.js +11 -13
- package/dist/surface/tools/tools.js +6 -1
- package/dist/telemetry/bug-found.js +32 -2
- package/dist/telemetry/feedback-context.js +3 -3
- package/dist/telemetry/harness-drive.d.ts +6 -0
- package/dist/telemetry/harness-drive.js +39 -0
- package/dist/telemetry/mcp-connection.d.ts +14 -1
- package/dist/telemetry/mcp-connection.js +17 -2
- package/dist/telemetry/onboarding-firsts.d.ts +30 -1
- package/dist/telemetry/onboarding-firsts.js +54 -12
- package/dist/telemetry/verification-of.js +31 -3
- package/docs/agent-cheatsheet.md +7 -3
- package/docs/autodrive.md +42 -2
- package/docs/benchmarks.md +1 -1
- package/docs/capabilities.mdx +1 -1
- package/docs/cli/cloud.mdx +3 -3
- package/docs/cli/doctor.mdx +1 -0
- package/docs/cli/status.mdx +35 -20
- package/docs/cli.mdx +8 -18
- package/docs/desktop.mdx +1 -1
- package/docs/faq.mdx +1 -1
- package/docs/install-manual.mdx +1 -1
- package/docs/packages/tauri.mdx +1 -1
- package/docs/packages.mdx +1 -1
- package/docs/predicates.mdx +16 -0
- package/docs/system-map.md +6 -4
- package/docs/telemetry.md +2 -1
- package/docs/token-efficiency.md +2 -2
- package/docs/tools/overview.mdx +1 -1
- package/docs/troubleshooting.mdx +8 -2
- package/docs/usage.md +1 -1
- package/package.json +6 -6
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* without a circular import — `tools.ts` assembles the groups and re-exports `ToolDef`/`ToolDeps`.
|
|
6
6
|
*/
|
|
7
7
|
import { z } from 'zod';
|
|
8
|
-
import { ReticleCommand } from '@reticlehq/core';
|
|
8
|
+
import { type ProjectId, ReticleCommand } from '@reticlehq/core';
|
|
9
9
|
import type { SessionManager } from '../../portal/session/session-manager.js';
|
|
10
10
|
import type { RealInputProvider } from '../../portal/input/real-input.js';
|
|
11
11
|
import type { BaselineStore } from '../../memory/project/baselines.js';
|
|
@@ -33,6 +33,21 @@ export interface ToolDeps<Ext = unknown> {
|
|
|
33
33
|
recordings: RecordingStore;
|
|
34
34
|
/** on-disk anchored-flow store (.reticle/flows/). */
|
|
35
35
|
flows: FlowStore;
|
|
36
|
+
/**
|
|
37
|
+
* The credential this machine already holds for the linked project, if it is linked.
|
|
38
|
+
*
|
|
39
|
+
* INJECTED rather than read here. `reticle link` files a project-scoped key in
|
|
40
|
+
* `~/.reticle/credentials.json`, and the harness needs it so that somebody who has signed in and
|
|
41
|
+
* linked is not also asked to export a key by hand. Resolving it inside the tool surface would
|
|
42
|
+
* make `tools` reach into `memory/cloud`, which the directory guard refused — and it was right
|
|
43
|
+
* to: this is a port, and the daemon already resolves exactly this for cloud sync.
|
|
44
|
+
*
|
|
45
|
+
* Absent ⇒ nothing is linked, or the caller is an embedder with no filesystem. Never an error.
|
|
46
|
+
*/
|
|
47
|
+
linkedCloud?: () => Promise<{
|
|
48
|
+
url: string;
|
|
49
|
+
apiKey: string;
|
|
50
|
+
} | null>;
|
|
36
51
|
/** structured annotations accumulating for the live recording. */
|
|
37
52
|
annotations: AnnotationStore;
|
|
38
53
|
/**
|
|
@@ -65,7 +80,7 @@ export interface ToolDeps<Ext = unknown> {
|
|
|
65
80
|
* embedding this engine may not have a filesystem to search. Absent ⇒ `reticleRoot`, which is
|
|
66
81
|
* exactly today's behaviour.
|
|
67
82
|
*/
|
|
68
|
-
artifactRootFor?: (projectId:
|
|
83
|
+
artifactRootFor?: (projectId: ProjectId | undefined) => ArtifactRoot;
|
|
69
84
|
/**
|
|
70
85
|
* A verification run just landed — wake cloud sync instead of waiting for its next tick.
|
|
71
86
|
*
|
|
@@ -85,7 +100,7 @@ export interface ToolDeps<Ext = unknown> {
|
|
|
85
100
|
* test construction of ToolDeps predates it. Absence means "cannot tell", which must never be
|
|
86
101
|
* treated as a mismatch.
|
|
87
102
|
*/
|
|
88
|
-
projectId?:
|
|
103
|
+
projectId?: ProjectId;
|
|
89
104
|
/** injected clock for the contract's generatedAt stamp. */
|
|
90
105
|
now: () => number;
|
|
91
106
|
/**
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
/** Small pure helpers shared by the MCP tool handlers. */
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
2
|
+
/**
|
|
3
|
+
* The argument a call names a tab with.
|
|
4
|
+
*
|
|
5
|
+
* A named constant because dispatch now WRITES this key as well as reading it — see `runTool`,
|
|
6
|
+
* which pins the session it resolved into the args the handler is given — and a key spelled at both
|
|
7
|
+
* ends of that can drift without anything saying so.
|
|
8
|
+
*/
|
|
9
|
+
export declare const SESSION_ID_ARG = "sessionId";
|
|
8
10
|
/**
|
|
9
11
|
* The session this call is aimed at: a top-level `sessionId`, or the same key on sequence steps.
|
|
10
12
|
*
|
|
@@ -36,4 +38,3 @@ export declare function sourceOf(value: unknown): {
|
|
|
36
38
|
line: number;
|
|
37
39
|
column?: number;
|
|
38
40
|
} | undefined;
|
|
39
|
-
export {};
|
|
@@ -1,15 +1,13 @@
|
|
|
1
1
|
import { asRecord, asString } from '@reticlehq/core';
|
|
2
|
-
/**
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
return items;
|
|
12
|
-
}
|
|
2
|
+
/** Small pure helpers shared by the MCP tool handlers. */
|
|
3
|
+
/**
|
|
4
|
+
* The argument a call names a tab with.
|
|
5
|
+
*
|
|
6
|
+
* A named constant because dispatch now WRITES this key as well as reading it — see `runTool`,
|
|
7
|
+
* which pins the session it resolved into the args the handler is given — and a key spelled at both
|
|
8
|
+
* ends of that can drift without anything saying so.
|
|
9
|
+
*/
|
|
10
|
+
export const SESSION_ID_ARG = 'sessionId';
|
|
13
11
|
/**
|
|
14
12
|
* The session this call is aimed at: a top-level `sessionId`, or the same key on sequence steps.
|
|
15
13
|
*
|
|
@@ -21,7 +19,7 @@ export function parseInteractive(tree) {
|
|
|
21
19
|
* Mixed step ids refuse rather than pick: two named tabs is a guess we will not make.
|
|
22
20
|
*/
|
|
23
21
|
export function sessionIdFromArgs(args) {
|
|
24
|
-
const top = asString(args[
|
|
22
|
+
const top = asString(args[SESSION_ID_ARG]);
|
|
25
23
|
if (top !== undefined)
|
|
26
24
|
return top;
|
|
27
25
|
const steps = args['steps'];
|
|
@@ -29,7 +27,7 @@ export function sessionIdFromArgs(args) {
|
|
|
29
27
|
return undefined;
|
|
30
28
|
let found;
|
|
31
29
|
for (const raw of steps) {
|
|
32
|
-
const id = asString(asRecord(raw)[
|
|
30
|
+
const id = asString(asRecord(raw)[SESSION_ID_ARG]);
|
|
33
31
|
if (id === undefined)
|
|
34
32
|
continue;
|
|
35
33
|
if (found !== undefined && found !== id) {
|
|
@@ -504,7 +504,12 @@ export const MERGE_PLANS = [
|
|
|
504
504
|
},
|
|
505
505
|
{
|
|
506
506
|
name: ReticleTool.SESSION,
|
|
507
|
-
description:
|
|
507
|
+
description:
|
|
508
|
+
// `yield` leads, and that is not a style choice. A catalogue prints ONE line per tool, so
|
|
509
|
+
// whatever comes first is what an agent discovers; when this sentence opened with `tune` the
|
|
510
|
+
// MANDATORY handback sat past the cut and no agent reading the catalogue ever learned it
|
|
511
|
+
// existed. The rule is: if a tool has an obligation, the obligation goes first.
|
|
512
|
+
'Session lifecycle and the human channel, by action: "yield" hands control back to the human and is MANDATORY before you stop driving (mode: waiting|ask, between turns); "tune" adjusts the presenter session (e.g. idle-end window); "end" terminates the session for good; "resume" clears a human pause; "messages" drains the human→agent inbox; "review" lists/resolves the mistakes a human pinned to elements; "narrate" states your intent on the presenter HUD.',
|
|
508
513
|
members: {
|
|
509
514
|
tune: ReticleTool.SESSION_TUNE,
|
|
510
515
|
yield: ReticleTool.YIELD,
|
|
@@ -18,6 +18,12 @@ import { ReticleTool } from '@reticlehq/core';
|
|
|
18
18
|
import { BugAttribution, BugSource, ContradictionKind, isAbsenceDerived, } from '@reticlehq/core';
|
|
19
19
|
/** Cap per tool call — a pathological crawl must not turn one call into a thousand events. */
|
|
20
20
|
const MAX_BUGS_PER_CALL = 25;
|
|
21
|
+
/** The kinds in a replay's `regressed` list — bare strings, not the `{ kind }` objects above. */
|
|
22
|
+
function regressedKinds(value) {
|
|
23
|
+
if (!Array.isArray(value))
|
|
24
|
+
return [];
|
|
25
|
+
return value.filter((kind) => 'string' === typeof kind && kind.length > 0);
|
|
26
|
+
}
|
|
21
27
|
function kindsOf(value) {
|
|
22
28
|
if (!Array.isArray(value))
|
|
23
29
|
return [];
|
|
@@ -127,7 +133,12 @@ export function bugsInResult(toolName, result) {
|
|
|
127
133
|
const verdict = verdictOf(result);
|
|
128
134
|
const passed = true === verdict;
|
|
129
135
|
// 1. Contradictions — channels disagreeing. Invisible to a human watching the screen.
|
|
130
|
-
|
|
136
|
+
//
|
|
137
|
+
// `crossStep` is read alongside `contradictions` because a flow replay spells them that way: a
|
|
138
|
+
// step's window closes when the step ends, so a request fired at step 2 and still unanswered at
|
|
139
|
+
// step 5 sits outside every per-step window and is exactly the shape a long journey produces.
|
|
140
|
+
// Every whole-span contradiction a replay found was unreportable for want of this one name.
|
|
141
|
+
for (const kind of [...kindsOf(result['contradictions']), ...kindsOf(result['crossStep'])]) {
|
|
131
142
|
// A contradiction is two of the APP's own channels disagreeing — never the agent's phrasing.
|
|
132
143
|
// Omitted for the one kind Reticle can produce on its own: see contradictionAttribution.
|
|
133
144
|
bugs.push({
|
|
@@ -176,7 +187,26 @@ export function bugsInResult(toolName, result) {
|
|
|
176
187
|
attribution: BugAttribution.UNCLASSIFIED,
|
|
177
188
|
});
|
|
178
189
|
}
|
|
179
|
-
// 4.
|
|
190
|
+
// 4. A guarded defect that came BACK. The flow observed it, then observed it gone, and the app
|
|
191
|
+
// has now reacquired it — a claim no other detector in this file can make, because only a saved
|
|
192
|
+
// flow carries what it has already seen. It is the replay feature's stated payoff and it was
|
|
193
|
+
// counted nowhere: `regressed` is a list of KINDS, not of anomaly objects, so neither of the
|
|
194
|
+
// array readers above could see it.
|
|
195
|
+
for (const kind of regressedKinds(result['regressed'])) {
|
|
196
|
+
bugs.push({
|
|
197
|
+
source: BugSource.REPLAY,
|
|
198
|
+
kind,
|
|
199
|
+
// The replay carries no assertion of its own, so there is no green for this to have hidden
|
|
200
|
+
// behind. Whether it was a false green is a question about the run that reacquired the defect,
|
|
201
|
+
// not about the run that noticed.
|
|
202
|
+
falseGreen: passed,
|
|
203
|
+
tool: toolName,
|
|
204
|
+
// Same kind, same owner, wherever it arrives — the line `attributionOf` draws is the one the
|
|
205
|
+
// verdict already uses, and a defect does not change whose fault it was by coming back.
|
|
206
|
+
attribution: attributionOf(kind),
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
// 5. Replay failures — a saved flow that used to pass no longer does. That is a regression caught,
|
|
180
210
|
// which is a different and stronger claim than "a check failed".
|
|
181
211
|
if ('fail' === result['status'] && Array.isArray(result['failures'])) {
|
|
182
212
|
for (const failure of result['failures'].slice(0, MAX_BUGS_PER_CALL)) {
|
|
@@ -78,9 +78,9 @@ export function detectStack(cwd, read = readFile) {
|
|
|
78
78
|
}
|
|
79
79
|
// Otherwise look for the app. The daemon's cwd is wherever the agent's client happened to launch,
|
|
80
80
|
// which for a real repo is the ROOT while the app sits in `frontend/`, `web/`, or a declared
|
|
81
|
-
// workspace. Reading one directory and giving up is why this returned nothing for
|
|
82
|
-
// that actually had Reticle set up
|
|
83
|
-
// The
|
|
81
|
+
// workspace. Reading one directory and giving up is why this returned nothing for essentially
|
|
82
|
+
// every project that actually had Reticle set up, and why the bigger the repo, the more certainly
|
|
83
|
+
// we failed. (The measurement that established that came from the field and stays in `plan/`.)
|
|
84
84
|
//
|
|
85
85
|
// `findWorkspaceApps` is init's discovery, reused rather than reimplemented: it already reads
|
|
86
86
|
// declared workspaces (pnpm-workspace.yaml, package.json `workspaces`) AND scans top-level
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/** Run `drive` with every verdict underneath it attributed to the harness. */
|
|
2
|
+
export declare function withHarnessDrive<T>(drive: () => Promise<T>): Promise<T>;
|
|
3
|
+
/** True while a harness drive is in progress. */
|
|
4
|
+
export declare function harnessDriving(): boolean;
|
|
5
|
+
/** Tests only — a leaked span would mis-attribute every verdict after it. */
|
|
6
|
+
export declare function resetHarnessDrive(): void;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Whether the tool call running right now belongs to one of OUR drives.
|
|
3
|
+
*
|
|
4
|
+
* `reticle_verify { action: "explore" }` hands the app to a model inside the daemon, which drives
|
|
5
|
+
* through the same dispatch chokepoint every agent uses. That is the right design — the inner
|
|
6
|
+
* `act_and_wait` and `assert` calls are real verifications and must be counted — but it left every
|
|
7
|
+
* verdict a drive produced indistinguishable from one the user's own agent earned, under the same
|
|
8
|
+
* session and the same `actor: agent`. So "did this install ever verify anything by itself" had no
|
|
9
|
+
* answer, and any per-session rate read high by however much we drove.
|
|
10
|
+
*
|
|
11
|
+
* Module state, one flag, set for the span of the drive. The same one-hop pattern `tool-refused.ts`
|
|
12
|
+
* already uses for its pending reason, and for the same reason: the fact is produced in one place
|
|
13
|
+
* and needed in another, with a chokepoint in between that has no business taking an extra argument
|
|
14
|
+
* for a metric.
|
|
15
|
+
*
|
|
16
|
+
* A COUNTER rather than a boolean, so a drive nested inside a drive cannot clear the flag on its way
|
|
17
|
+
* out and leave the outer one unmarked.
|
|
18
|
+
*/
|
|
19
|
+
let depth = 0;
|
|
20
|
+
/** Run `drive` with every verdict underneath it attributed to the harness. */
|
|
21
|
+
export async function withHarnessDrive(drive) {
|
|
22
|
+
depth += 1;
|
|
23
|
+
try {
|
|
24
|
+
return await drive();
|
|
25
|
+
}
|
|
26
|
+
finally {
|
|
27
|
+
// In a `finally` because a drive that THREW still drove: leaving the flag raised would attribute
|
|
28
|
+
// the user's own next verdict to a harness that is no longer running.
|
|
29
|
+
depth -= 1;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
/** True while a harness drive is in progress. */
|
|
33
|
+
export function harnessDriving() {
|
|
34
|
+
return depth > 0;
|
|
35
|
+
}
|
|
36
|
+
/** Tests only — a leaked span would mis-attribute every verdict after it. */
|
|
37
|
+
export function resetHarnessDrive() {
|
|
38
|
+
depth = 0;
|
|
39
|
+
}
|
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
export declare function markDaemonStart(now: number): void;
|
|
2
|
-
/**
|
|
2
|
+
/**
|
|
3
|
+
* Report one client attaching, BY NAME. Best-effort; a metric must never affect whether a client
|
|
4
|
+
* connects.
|
|
5
|
+
*
|
|
6
|
+
* The name is required in practice, and that is the fix rather than a restriction. `connection.client`
|
|
7
|
+
* was defined and documented and never once populated: the only caller reported from
|
|
8
|
+
* `transport.connect().then(...)`, which resolves when the SSE stream opens — before the client has
|
|
9
|
+
* said who it is. `clientInfo` first exists at `oninitialized`, so the event carried every fact about
|
|
10
|
+
* the connection except the one that makes "which agent client converts best" answerable at all.
|
|
11
|
+
*
|
|
12
|
+
* An unnamed call reports NOTHING. A stream that opens and never completes a handshake is a probe,
|
|
13
|
+
* not a client attaching, and counting it was what put an unattributable row exactly where the name
|
|
14
|
+
* belonged.
|
|
15
|
+
*/
|
|
3
16
|
export declare function reportMcpConnected(client?: string, now?: () => number): void;
|
|
4
17
|
/** Tests only. */
|
|
5
18
|
export declare function resetMcpConnections(): void;
|
|
@@ -19,8 +19,23 @@ export function markDaemonStart(now) {
|
|
|
19
19
|
daemonStartedAt = now;
|
|
20
20
|
connectsSeen = 0;
|
|
21
21
|
}
|
|
22
|
-
/**
|
|
22
|
+
/**
|
|
23
|
+
* Report one client attaching, BY NAME. Best-effort; a metric must never affect whether a client
|
|
24
|
+
* connects.
|
|
25
|
+
*
|
|
26
|
+
* The name is required in practice, and that is the fix rather than a restriction. `connection.client`
|
|
27
|
+
* was defined and documented and never once populated: the only caller reported from
|
|
28
|
+
* `transport.connect().then(...)`, which resolves when the SSE stream opens — before the client has
|
|
29
|
+
* said who it is. `clientInfo` first exists at `oninitialized`, so the event carried every fact about
|
|
30
|
+
* the connection except the one that makes "which agent client converts best" answerable at all.
|
|
31
|
+
*
|
|
32
|
+
* An unnamed call reports NOTHING. A stream that opens and never completes a handshake is a probe,
|
|
33
|
+
* not a client attaching, and counting it was what put an unattributable row exactly where the name
|
|
34
|
+
* belonged.
|
|
35
|
+
*/
|
|
23
36
|
export function reportMcpConnected(client, now = () => Date.now()) {
|
|
37
|
+
if (client === undefined)
|
|
38
|
+
return;
|
|
24
39
|
try {
|
|
25
40
|
const reconnect = connectsSeen > 0;
|
|
26
41
|
connectsSeen += 1;
|
|
@@ -30,7 +45,7 @@ export function reportMcpConnected(client, now = () => Date.now()) {
|
|
|
30
45
|
// A large value on the FIRST connect is the interesting case: Reticle was started and then
|
|
31
46
|
// sat there. On a reconnect it is just how long the daemon has been up.
|
|
32
47
|
daemonAgeMs: daemonStartedAt === undefined ? 0 : Math.max(0, now() - daemonStartedAt),
|
|
33
|
-
|
|
48
|
+
client,
|
|
34
49
|
// WHAT THE AGENT HAD TO LOOK AT. Most clients that attach never call a tool, and that cohort
|
|
35
50
|
// emits nothing at all — an agent that reads the instructions, learns nothing is wired and
|
|
36
51
|
// stops has refused nothing, so `tool_refused` cannot see it either. Read off the same flag
|
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
/** A tool call the daemon just ran. Called from the dispatch chokepoint, which sees every one. */
|
|
2
|
-
export declare function noteOnboardingFirst(toolName: string
|
|
2
|
+
export declare function noteOnboardingFirst(toolName: string): void;
|
|
3
|
+
/**
|
|
4
|
+
* The first ACTION of this run, and the fact that this install has been driven at all.
|
|
5
|
+
*
|
|
6
|
+
* Called from the dispatch chokepoint's own action branch rather than matched on a tool name here.
|
|
7
|
+
* The name test was `toolName === ReticleTool.ACT`, on the worry that `act_and_wait` would report
|
|
8
|
+
* one call as two different firsts — and the worry was backwards: this server's instructions tell
|
|
9
|
+
* every agent to PREFER `act_and_wait`, so the step measured the tool we ask agents not to use and
|
|
10
|
+
* the ONBOARD phase read as "looked, never acted, then produced a verdict from nowhere". A call that
|
|
11
|
+
* both acts and proves genuinely did both, and `first_act` and `first_verdict` are different steps
|
|
12
|
+
* in different positions of the same funnel.
|
|
13
|
+
*
|
|
14
|
+
* Taking the decision from the caller is what keeps it from happening again: the dispatcher already
|
|
15
|
+
* owns the set of tools that drive the page, and a second copy of that set here is a second thing to
|
|
16
|
+
* update when a fourth driving tool arrives.
|
|
17
|
+
*/
|
|
18
|
+
export declare function noteActed(args: Record<string, unknown>): void;
|
|
3
19
|
/**
|
|
4
20
|
* The first verdict of this run.
|
|
5
21
|
*
|
|
@@ -7,5 +23,18 @@ export declare function noteOnboardingFirst(toolName: string, args: Record<strin
|
|
|
7
23
|
* and only the site that already decided "this was a verification" can say so. Reported from there.
|
|
8
24
|
*/
|
|
9
25
|
export declare function noteFirstVerdict(): void;
|
|
26
|
+
/**
|
|
27
|
+
* Something was driven on the user's own project — the FIRST_RUN rung between "the install worked"
|
|
28
|
+
* and "they used it".
|
|
29
|
+
*
|
|
30
|
+
* Declared in the closed step list from the day the funnel was written and emitted by no code at
|
|
31
|
+
* all, so any chart rendered over the declared order showed a 100% cliff in the middle of the
|
|
32
|
+
* conversion story. It reads as catastrophe and was an unwired emit.
|
|
33
|
+
*
|
|
34
|
+
* Reported from `noteActed`, so the one place that knows a tool drove the page is the one place that
|
|
35
|
+
* says so. That includes a drive we run on the user's behalf, whose own actions cross the same
|
|
36
|
+
* chokepoint — a drive that is refused before its first action drove nothing and says nothing.
|
|
37
|
+
*/
|
|
38
|
+
export declare function noteDriven(): void;
|
|
10
39
|
/** Arm again for the next daemon run — the tests' seam, mirroring resetAppInstrumented. */
|
|
11
40
|
export declare function resetOnboardingFirsts(): void;
|
|
@@ -14,9 +14,9 @@ import { reportOnboardingStep } from './onboarding-funnel.js';
|
|
|
14
14
|
* The flags are module state rather than per-session, because a person who tours in one tab and
|
|
15
15
|
* drives in another has still only crossed each of these once.
|
|
16
16
|
*/
|
|
17
|
-
const seen = { look: false, act: false, verdict: false };
|
|
17
|
+
const seen = { look: false, act: false, verdict: false, driven: false };
|
|
18
18
|
/** A tool call the daemon just ran. Called from the dispatch chokepoint, which sees every one. */
|
|
19
|
-
export function noteOnboardingFirst(toolName
|
|
19
|
+
export function noteOnboardingFirst(toolName) {
|
|
20
20
|
if (!seen.look && LOOK_TOOLS.has(toolName)) {
|
|
21
21
|
seen.look = true;
|
|
22
22
|
void reportOnboardingStep({
|
|
@@ -25,16 +25,35 @@ export function noteOnboardingFirst(toolName, args) {
|
|
|
25
25
|
status: OnboardingStepStatus.COMPLETED,
|
|
26
26
|
});
|
|
27
27
|
}
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* The first ACTION of this run, and the fact that this install has been driven at all.
|
|
31
|
+
*
|
|
32
|
+
* Called from the dispatch chokepoint's own action branch rather than matched on a tool name here.
|
|
33
|
+
* The name test was `toolName === ReticleTool.ACT`, on the worry that `act_and_wait` would report
|
|
34
|
+
* one call as two different firsts — and the worry was backwards: this server's instructions tell
|
|
35
|
+
* every agent to PREFER `act_and_wait`, so the step measured the tool we ask agents not to use and
|
|
36
|
+
* the ONBOARD phase read as "looked, never acted, then produced a verdict from nowhere". A call that
|
|
37
|
+
* both acts and proves genuinely did both, and `first_act` and `first_verdict` are different steps
|
|
38
|
+
* in different positions of the same funnel.
|
|
39
|
+
*
|
|
40
|
+
* Taking the decision from the caller is what keeps it from happening again: the dispatcher already
|
|
41
|
+
* owns the set of tools that drive the page, and a second copy of that set here is a second thing to
|
|
42
|
+
* update when a fourth driving tool arrives.
|
|
43
|
+
*/
|
|
44
|
+
export function noteActed(args) {
|
|
45
|
+
// A hover changes nothing and is not an action anybody drove.
|
|
46
|
+
if (args['action'] === ActionType.HOVER)
|
|
47
|
+
return;
|
|
48
|
+
noteDriven();
|
|
49
|
+
if (seen.act)
|
|
50
|
+
return;
|
|
51
|
+
seen.act = true;
|
|
52
|
+
void reportOnboardingStep({
|
|
53
|
+
phase: OnboardingPhase.ONBOARD,
|
|
54
|
+
step: 'first_act',
|
|
55
|
+
status: OnboardingStepStatus.COMPLETED,
|
|
56
|
+
});
|
|
38
57
|
}
|
|
39
58
|
/**
|
|
40
59
|
* The first verdict of this run.
|
|
@@ -52,6 +71,28 @@ export function noteFirstVerdict() {
|
|
|
52
71
|
status: OnboardingStepStatus.COMPLETED,
|
|
53
72
|
});
|
|
54
73
|
}
|
|
74
|
+
/**
|
|
75
|
+
* Something was driven on the user's own project — the FIRST_RUN rung between "the install worked"
|
|
76
|
+
* and "they used it".
|
|
77
|
+
*
|
|
78
|
+
* Declared in the closed step list from the day the funnel was written and emitted by no code at
|
|
79
|
+
* all, so any chart rendered over the declared order showed a 100% cliff in the middle of the
|
|
80
|
+
* conversion story. It reads as catastrophe and was an unwired emit.
|
|
81
|
+
*
|
|
82
|
+
* Reported from `noteActed`, so the one place that knows a tool drove the page is the one place that
|
|
83
|
+
* says so. That includes a drive we run on the user's behalf, whose own actions cross the same
|
|
84
|
+
* chokepoint — a drive that is refused before its first action drove nothing and says nothing.
|
|
85
|
+
*/
|
|
86
|
+
export function noteDriven() {
|
|
87
|
+
if (seen.driven)
|
|
88
|
+
return;
|
|
89
|
+
seen.driven = true;
|
|
90
|
+
void reportOnboardingStep({
|
|
91
|
+
phase: OnboardingPhase.FIRST_RUN,
|
|
92
|
+
step: 'driven',
|
|
93
|
+
status: OnboardingStepStatus.COMPLETED,
|
|
94
|
+
});
|
|
95
|
+
}
|
|
55
96
|
/** Looking: the tools that read the page without changing it. */
|
|
56
97
|
const LOOK_TOOLS = new Set([
|
|
57
98
|
ReticleTool.SNAPSHOT,
|
|
@@ -64,4 +105,5 @@ export function resetOnboardingFirsts() {
|
|
|
64
105
|
seen.look = false;
|
|
65
106
|
seen.act = false;
|
|
66
107
|
seen.verdict = false;
|
|
108
|
+
seen.driven = false;
|
|
67
109
|
}
|
|
@@ -10,9 +10,10 @@
|
|
|
10
10
|
* Kept in one place, out of `runTool`, because a rule this easy to get wrong should be readable and
|
|
11
11
|
* testable on its own rather than inferred from a ternary inside a dispatcher.
|
|
12
12
|
*/
|
|
13
|
-
import { CaptureLoss, VerifiedReason } from '@reticlehq/core';
|
|
13
|
+
import { CaptureLoss, ReplayStatus, ReticleTool, VerificationDriver, Verified, VerifiedReason, } from '@reticlehq/core';
|
|
14
14
|
import { VERDICT_TOOLS } from '../surface/tools/feedback-tools.js';
|
|
15
15
|
import { getBrowserMode } from './browser-mode.js';
|
|
16
|
+
import { harnessDriving } from './harness-drive.js';
|
|
16
17
|
/**
|
|
17
18
|
* The deciding clause, read off the result `decideVerified` wrote it onto.
|
|
18
19
|
*
|
|
@@ -51,6 +52,30 @@ const SuiteStatus = {
|
|
|
51
52
|
PASS: 'pass',
|
|
52
53
|
FAIL: 'fail',
|
|
53
54
|
};
|
|
55
|
+
/**
|
|
56
|
+
* A flow replay's verdict, in the vocabulary the rest of this file speaks.
|
|
57
|
+
*
|
|
58
|
+
* `ReplayStatus` spells itself `ok | drift | error`, so a reader looking for `pass`/`fail` found
|
|
59
|
+
* neither and every replay emitted nothing at all — on the tool documented as the single-flow
|
|
60
|
+
* companion to the whole-suite replay, i.e. a verdict in no numerator and no denominator.
|
|
61
|
+
*
|
|
62
|
+
* `error` maps to UNKNOWN rather than to a fail. It is the flow failing to LOAD or an action failing
|
|
63
|
+
* to run, which is Reticle not being able to tell — publishing that as the app's defect is exactly
|
|
64
|
+
* the mistake the suite reader made the first time.
|
|
65
|
+
*
|
|
66
|
+
* Read for the REPLAY TOOL ONLY. `ok` is not a verdict word anywhere else in this surface, and a
|
|
67
|
+
* merged action that happens to answer with one must not be counted as a verification it never made.
|
|
68
|
+
*/
|
|
69
|
+
const REPLAY_VERDICTS = new Map([
|
|
70
|
+
[ReplayStatus.OK, { verified: Verified.YES, passed: true }],
|
|
71
|
+
[ReplayStatus.DRIFT, { verified: Verified.NO, passed: false }],
|
|
72
|
+
[ReplayStatus.ERROR, { verified: Verified.UNKNOWN, passed: false }],
|
|
73
|
+
]);
|
|
74
|
+
function replayVerdict(toolName, status) {
|
|
75
|
+
if (ReticleTool.FLOW_REPLAY !== toolName || 'string' !== typeof status)
|
|
76
|
+
return undefined;
|
|
77
|
+
return REPLAY_VERDICTS.get(status);
|
|
78
|
+
}
|
|
54
79
|
/**
|
|
55
80
|
* Did the assertion itself pass, across three tool shapes that spell it three ways.
|
|
56
81
|
*
|
|
@@ -97,13 +122,14 @@ export function verificationOf(toolName, result, durationMs, brand) {
|
|
|
97
122
|
// here for work that happened.
|
|
98
123
|
if (true === result['paused'])
|
|
99
124
|
return undefined;
|
|
100
|
-
const
|
|
125
|
+
const replay = replayVerdict(toolName, result['status']);
|
|
126
|
+
const verified = 'string' === typeof result['verified'] ? result['verified'] : (replay?.verified ?? undefined);
|
|
101
127
|
// flow_verify reports `status: pass|fail|unverifiable`; assert reports a boolean `pass`. Accept
|
|
102
128
|
// either shape so the whole family is covered without normalizing four tools' contracts for a
|
|
103
129
|
// metric's convenience — but a status that is neither pass nor fail is NOT a verdict, and an empty
|
|
104
130
|
// suite reports exactly that.
|
|
105
131
|
const status = result['status'];
|
|
106
|
-
const passed = passedOf(result, status);
|
|
132
|
+
const passed = passedOf(result, status) ?? replay?.passed;
|
|
107
133
|
if (verified === undefined && passed === undefined)
|
|
108
134
|
return undefined;
|
|
109
135
|
const reason = reasonOf(result);
|
|
@@ -142,5 +168,7 @@ export function verificationOf(toolName, result, durationMs, brand) {
|
|
|
142
168
|
// WHAT was lost, when the reason was that something was. Three owners, three fixes, one bar
|
|
143
169
|
// until now.
|
|
144
170
|
...(uncleanLoss === undefined ? {} : { uncleanLoss }),
|
|
171
|
+
// WHOSE drive this was. Absent is the ordinary case — the agent called the tool itself.
|
|
172
|
+
...(harnessDriving() ? { driven: VerificationDriver.HARNESS } : {}),
|
|
145
173
|
};
|
|
146
174
|
}
|
package/docs/agent-cheatsheet.md
CHANGED
|
@@ -97,7 +97,11 @@ A claim is real only when the layers agree. Check more than the UI:
|
|
|
97
97
|
|
|
98
98
|
> **Rule:** a passing UI assert that the store, network, or signal contradicts is a **false green**.
|
|
99
99
|
|
|
100
|
-
**Session health is universal.** Every live-session tool result carries a `session` block (`throttled`, `focused`, `lastSeenMs`); when `throttled:true` it also adds a `warning` + `recommendation
|
|
100
|
+
**Session health is universal.** Every live-session tool result carries a `session` block (`throttled`, `focused`, `lastSeenMs`); when `throttled:true` it also adds a `warning` + `recommendation`. Read the `recommendation`: it differs by cause, and the two causes have different fixes.
|
|
101
|
+
|
|
102
|
+
`throttled` is `hidden || stale`, and it is not a verdict that the tab cannot be driven. A tab that is **hidden** does clamp timers and rAF, so an action can land on a page that never advances. A tab that is merely **stale**, meaning no health heartbeat arrived inside the window, is usually just a quiet page, and is very often still driveable; a session flagged this way has taken a sign-in and two clean net-grade verdicts with no retries. Try the drive before you conclude anything, and reach for `refuseWhenThrottled: true` when the work is timing-sensitive (animations, debounces, gestures), which is what actually degrades.
|
|
103
|
+
|
|
104
|
+
**Leasing is not a free upgrade.** `reticle_lease` gives a guaranteed scriptable context, and it is a _separate_ one: the HUD lives in the human's tab, so everything you do in a lease is invisible to the person watching. Taking that advice with someone sitting in front of the app makes the product look broken while it works correctly. Working alone, lease and do not think about it; with an audience, spend a slower tab to stay visible.
|
|
101
105
|
|
|
102
106
|
> Store reads (`reticle_look { action: "state" }`) are the reliable path; the DOM can lie (optimistic UI, stale render).
|
|
103
107
|
|
|
@@ -109,7 +113,7 @@ A claim is real only when the layers agree. Check more than the UI:
|
|
|
109
113
|
|
|
110
114
|
## Core tool set
|
|
111
115
|
|
|
112
|
-
The
|
|
116
|
+
The tools you are shown, plus `reticle_run`, which calls any registered tool by name whether or not it is advertised.
|
|
113
117
|
|
|
114
118
|
`reticle_navigate` · `reticle_act` · `reticle_act_and_wait` · `reticle_assert` · `reticle_look` · `reticle_observe` · `reticle_session` · `reticle_verify` · `reticle_tools`
|
|
115
119
|
|
|
@@ -168,4 +172,4 @@ Recent additions, each of which answers a question agents were previously asking
|
|
|
168
172
|
- **Cap broad reads.** `reticle_look { action: "find" }` takes `limit` (caps descriptors; reports `total`/`truncated`) and `count_only` (just the match count). `reticle_observe { action: "network" }` / `reticle_observe { action: "console" }` take `limit` (most-recent-N, reports `droppedOldest`) and carry the same `cost` hint, so a busy page or wide window never floods your context unnoticed.
|
|
169
173
|
- **A saved flow tells you if it's a real test.** `reticle_flow_save` returns `assertions.grade` (`asserted` / `presence-only` / `assertion-free`); if it's not `asserted`, add a consequence (`reticle_annotate` assert-signal/assert-net or a success-state) so it can't pass while broken. On replay, an ambiguous heal (two testids tie) is surfaced, never auto-applied. And an `apply` heal re-replays the rebound flow and **refuses to write** if the success consequence no longer fires (`status:consequence_broken`): it heals the locator, never the intent.
|
|
170
174
|
- **Predicate schema is not bloated.** The recursive predicate DSL used by `reticle_assert` / `reticle_assert { action: "wait" }` / `reticle_act_and_wait` is **factored, not inlined**: when converted to the JSON Schema MCP sends, the predicate body is emitted **once** (~2.7k chars ≈ **~685 tokens** per tool) and recursion is handled by self-`$ref` (`#/properties/predicate`), with no per-recursion duplication. No action needed.
|
|
171
|
-
- **One tool surface, and
|
|
175
|
+
- **One tool surface, and a hatch behind it.** Reticle advertises navigate/act/act_and_wait/assert/look/observe/session/verify, plus `reticle_tools` and `reticle_run`. That is the whole detect loop, the file-pointer, the feedback channel, the handback, and the one tool that CONCLUDES, reached by an `action` argument rather than by a name each. `reticle_tools` discovers: no args lists every registered tool name and summary, `names:[…]` loads full params on demand. `reticle_run { tool, args }` then calls any of them, advertised or not, which is what keeps recording a flow, screenshots, visual diff, storage, network mocking and the clock reachable without spending a slot on each. A daemon started with `RETICLE_ADVERTISE_ALL_TOOLS=1` (read at startup, so restart it) advertises the full table outright, with output schemas. `reticle_verify` is advertised: `{ action:"flows" }` replays every saved flow with no model in the loop, and `{ action:"crawl" }` drives every reachable control itself and reports the whole fault set in one call. **Sizes are deliberately not quoted here.** A count in prose goes stale, and has three times already. `reticle_tools` reports the live surface, and SKILL.md carries the one gated table.
|
package/docs/autodrive.md
CHANGED
|
@@ -28,16 +28,56 @@ A drive costs a model. A **saved flow costs nothing to run again**: `reticle_ver
|
|
|
28
28
|
|
|
29
29
|
So the drive is a one-off: pay for it once, and the journeys it recorded become the regression suite every later run replays. That is why the harness is told to record as it goes, and why a drive that saved nothing is reported as having proved nothing, however long it ran.
|
|
30
30
|
|
|
31
|
+
## It replays before it drives
|
|
32
|
+
|
|
33
|
+
A drive reads `.reticle` first: every saved flow with the consequence that must still hold, and every signal and testid your app declares that no flow has ever asserted. It builds a plan from that before it opens its eyes on a page.
|
|
34
|
+
|
|
35
|
+
Anything already recorded is **replayed**, deterministically, with no model call at all. Only the gaps are driven. So a second run over the same app is mostly free, and the model budget is spent on the part nobody has proved yet rather than on rediscovering what is already on disk. A replay that goes red is reported as a regression; one that DRIFTS is reported separately, because the app moved under the recording and that needs re-anchoring rather than a code change.
|
|
36
|
+
|
|
37
|
+
Every journey a drive walks is saved, whatever happens to the drive, including one that broke, ran out of budget, or whose model simply stopped asking for tools.
|
|
38
|
+
|
|
31
39
|
## What it needs
|
|
32
40
|
|
|
33
|
-
|
|
41
|
+
**A model in the daemon's environment.** Any one of three, and the choice is reported back so a comparison can never mislabel its own arms:
|
|
42
|
+
|
|
43
|
+
| Variable | Driver |
|
|
44
|
+
| --- | --- |
|
|
45
|
+
| `RETICLE_API_KEY` | `jev`, against the platform's own proxy, using the key `reticle link` already wrote. No model API key of your own, and the cheap path: it answers typed questions about the page rather than generating tool calls as text. |
|
|
46
|
+
| `ANTHROPIC_API_KEY` | `anthropic`, which generates its calls as text. |
|
|
47
|
+
| `OPENAI_API_KEY` | `openai`, the same, on OpenAI. |
|
|
48
|
+
| `JEV_API_KEY` | `jev`, against TypeSafe directly, for anyone who holds their own key. |
|
|
49
|
+
|
|
50
|
+
With several configured, `anthropic` is the default and `RETICLE_HARNESS_DRIVER` moves off it; with only a platform key, `jev` is not the cheaper option, it is the only one. Naming a driver that is not configured is an error, never a silent substitution.
|
|
51
|
+
|
|
52
|
+
A driver can also be chosen per call, as `reticle_verify { action: "explore", driver: "jev" }`. The question people actually have is comparative, and answering it with an environment variable means restarting the daemon between arms.
|
|
53
|
+
|
|
54
|
+
Three other dials, all optional:
|
|
34
55
|
|
|
35
56
|
| Variable | What it does |
|
|
36
57
|
| --- | --- |
|
|
37
|
-
| `RETICLE_HARNESS_MODEL` | The model that drives. Defaults to a mid-tier one on purpose, for the reason below. |
|
|
58
|
+
| `RETICLE_HARNESS_MODEL` | The Anthropic model that drives. Defaults to a mid-tier one on purpose, for the reason below. |
|
|
38
59
|
| `RETICLE_HARNESS_MAX_STEPS` | Ceiling on model turns in one drive. Bounds cost, not value. |
|
|
39
60
|
| `RETICLE_HARNESS_BASE_URL` | A proxy or gateway instead of the default API host. |
|
|
40
61
|
|
|
62
|
+
## Two models, and what each one is for
|
|
63
|
+
|
|
64
|
+
The `jev` driver decides; it cannot write. Every choice a drive makes is a selection from candidates Reticle enumerated off the page: which element, which tool, what consequence to claim. That is why it costs a fraction of a generating model and answers in a few hundred milliseconds.
|
|
65
|
+
|
|
66
|
+
One thing in a drive is not a selection. A text field is a composition: "a business name", "a statement descriptor", "a search term that returns results" cannot be enumerated from the page. Where the field's label is enough to guess, a small table answers it. Where it is not, and a generating model is configured, that model writes the value, and nothing else.
|
|
67
|
+
|
|
68
|
+
Typing is a small minority of what a drive does: clicks and navigations are the overwhelming majority. So this is escalation rather than a second model in the loop, and a drive with no text fields never calls it at all.
|
|
69
|
+
|
|
70
|
+
What it writes is a FIXTURE, not an opinion. Generated values are saved to `.reticle/fill-values.json` and reused forever: the second drive pays nothing, a replay sends exactly what the recording sent, and a value you dislike is a line in a git-checked file you can edit rather than an argument with a model.
|
|
71
|
+
|
|
72
|
+
## Turning it off, and who pays for it
|
|
73
|
+
|
|
74
|
+
A project linked to a Reticle workspace reads two things from it before a drive starts, and honours both:
|
|
75
|
+
|
|
76
|
+
- **The switch.** Autonomous driving can be turned off per project in the dashboard, under Settings → Verification model. A drive then refuses and says where to turn it back on. Everything else is unaffected, including the tools your own agent drives with.
|
|
77
|
+
- **Who is paying.** Driving through the platform spends Reticle's model budget, which is free for three months once claimed and included on a paid plan. Outside both, a drive is refused with the claim link rather than run on somebody else's money.
|
|
78
|
+
|
|
79
|
+
Neither applies to a drive on a model key of your own: that costs us nothing, so it is not ours to gate. A daemon that cannot reach the platform at all drives normally: an unreachable settings endpoint is not a reason to lose a feature you were never told to stop using.
|
|
80
|
+
|
|
41
81
|
## What it will not do
|
|
42
82
|
|
|
43
83
|
**It does not decide whether your app is correct.** The model chooses what to _try_; the engine decides what _happened_, from what it recorded. A model that graded its own driving would be scoring its own homework, and its verdict would be unfalsifiable. So the summary it writes is an account of what it drove, and nothing downstream grades from it.
|
package/docs/benchmarks.md
CHANGED
|
@@ -71,7 +71,7 @@ Why not compare to traditional test frameworks (Playwright the library, Cypress,
|
|
|
71
71
|
|
|
72
72
|
There **is** a comparison against a Playwright script, and it answers a different question: not "what does it cost an agent to look" but "what can each tool structurally SEE, at all." That one is in Part 5a. It runs no model, so nothing about it depends on how well an agent drives, which is what makes it the more durable of the two results.
|
|
73
73
|
|
|
74
|
-
Tool versions are
|
|
74
|
+
Tool versions are recorded by hand in `bench/README.md`, not generated: the file that used to pin them is no longer produced. Treat that table as the expiry date on every cross-tool number here, and re-derive before quoting rather than assuming a run from months ago still describes today's tools.
|
|
75
75
|
|
|
76
76
|
---
|
|
77
77
|
|
package/docs/capabilities.mdx
CHANGED
|
@@ -4,7 +4,7 @@ description: Coverage, storage, fake clocks, autonomous crawling, visual diffing
|
|
|
4
4
|
icon: sparkles
|
|
5
5
|
---
|
|
6
6
|
|
|
7
|
-
These are the Reticle tools that do something other than look, act, observe and assert: control coverage, client storage, a fake clock, autonomous crawling, semantic and visual baselines, network mocking, flow recording and replay, and the CI artifact. Reach for them when the verify loop is not the shape of the question. They are **not on the default tool surface,
|
|
7
|
+
These are the Reticle tools that do something other than look, act, observe and assert: control coverage, client storage, a fake clock, autonomous crawling, semantic and visual baselines, network mocking, flow recording and replay, and the CI artifact. Reach for them when the verify loop is not the shape of the question. They are **not advertised on the default tool surface, but they are reachable from it**: `reticle_run({ tool, args })` dispatches to any registered tool by name, advertised or not, which is the spelling used on this page. To have them advertised outright instead, with their output schemas, start the daemon with `RETICLE_ADVERTISE_ALL_TOOLS=1` (read once at startup, so restart it) and call them by name.
|
|
8
8
|
|
|
9
9
|
Most of the time you want the verify loop. Look, act, observe, assert. These are the tools for the times you want something else, and they are the ones people are most surprised exist.
|
|
10
10
|
|