@reticlehq/server 2.9.0 → 2.11.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 +64 -26
- package/dist/capsule/capsule-store.d.ts +2 -2
- package/dist/cli/cli-parse.d.ts +1 -1
- package/dist/cli/cli-parse.js +1 -1
- package/dist/cli/cli-verify.js +1 -1
- package/dist/crawl/crawl.d.ts +7 -2
- package/dist/crawl/crawl.js +52 -9
- package/dist/dev/stale-issue-guard.d.ts +43 -0
- package/dist/dev/stale-issue-guard.js +75 -0
- package/dist/domain/domain-tools.js +3 -2
- package/dist/ee/audit-log.js +2 -2
- package/dist/events/body-failures.d.ts +2 -2
- package/dist/events/contradiction-folds.d.ts +45 -0
- package/dist/events/contradiction-folds.js +42 -0
- package/dist/events/contradictions.d.ts +19 -1
- package/dist/events/contradictions.js +17 -2
- package/dist/events/echo-mismatch.d.ts +2 -2
- package/dist/events/predicate-asks.d.ts +17 -0
- package/dist/events/predicate-asks.js +26 -0
- package/dist/events/predicate-eval.js +211 -24
- package/dist/events/predicate-parse.js +9 -3
- package/dist/events/predicate-schema.d.ts +18 -0
- package/dist/events/predicate-schema.js +55 -0
- package/dist/events/stale-response.d.ts +2 -3
- package/dist/events/stale-response.js +49 -2
- package/dist/events/unit-mismatch.d.ts +2 -2
- package/dist/flows/flow-tools.js +34 -5
- package/dist/flows/predicate-to-expect.js +8 -0
- package/dist/flows/verify-change-tools.js +6 -2
- package/dist/honesty/blind-spots.d.ts +9 -0
- package/dist/honesty/blind-spots.js +17 -4
- package/dist/honesty/gap-ledger.d.ts +52 -0
- package/dist/honesty/gap-ledger.js +63 -0
- package/dist/honesty/instrumentation-gaps.d.ts +55 -0
- package/dist/honesty/instrumentation-gaps.js +21 -0
- package/dist/honesty/observability.d.ts +37 -0
- package/dist/honesty/observability.js +40 -0
- package/dist/honesty/verified.js +16 -0
- package/dist/index.d.ts +18 -1
- package/dist/index.js +57 -0
- package/dist/init/mcp.d.ts +1 -2
- package/dist/init/mcp.js +4 -1
- package/dist/init/node-io.js +3 -0
- package/dist/init/run.d.ts +18 -0
- package/dist/init/run.js +34 -3
- package/dist/init/slash-command.d.ts +1 -1
- package/dist/init/slash-command.js +33 -10
- package/dist/journal/envelope.d.ts +24 -24
- package/dist/license/license.d.ts +76 -5
- package/dist/license/license.js +93 -29
- package/dist/mcp/mcp-proxy.d.ts +2 -27
- package/dist/mcp/mcp-proxy.js +4 -88
- package/dist/mcp/mcp.d.ts +18 -2
- package/dist/mcp/mcp.js +31 -10
- package/dist/mcp/proxy-daemon-probe.d.ts +8 -0
- package/dist/mcp/proxy-daemon-probe.js +48 -0
- package/dist/mcp/proxy-handshake.js +1 -1
- package/dist/mcp/server-instructions.js +2 -2
- package/dist/mcp/sse-frame-parser.d.ts +19 -0
- package/dist/mcp/sse-frame-parser.js +41 -0
- package/dist/project/artifact-root.d.ts +83 -0
- package/dist/project/artifact-root.js +112 -0
- package/dist/project/memory-fs.d.ts +9 -0
- package/dist/project/memory-fs.js +88 -0
- package/dist/project/project-store.d.ts +17 -0
- package/dist/project/project-store.js +33 -0
- package/dist/project/remember-project.d.ts +24 -0
- package/dist/project/remember-project.js +40 -0
- package/dist/project/session-root.d.ts +15 -0
- package/dist/project/session-root.js +31 -0
- package/dist/runs/run-tools.js +2 -1
- package/dist/session/lease-availability.d.ts +40 -0
- package/dist/session/lease-availability.js +43 -0
- package/dist/session/no-session-diagnosis.d.ts +17 -0
- package/dist/session/no-session-diagnosis.js +41 -8
- package/dist/session/no-session-watch.js +7 -0
- package/dist/session/session-readiness.js +1 -1
- package/dist/session/session.d.ts +9 -0
- package/dist/session/session.js +9 -0
- package/dist/session/stall-clock.d.ts +18 -0
- package/dist/session/stall-clock.js +48 -0
- package/dist/telemetry/daemon-telemetry.js +16 -22
- package/dist/telemetry/license-activation.d.ts +48 -0
- package/dist/telemetry/license-activation.js +57 -0
- 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 +34 -5
- package/dist/tools/act-tools.js +34 -4
- package/dist/tools/assert-verdict.d.ts +2 -0
- package/dist/tools/assert-verdict.js +24 -1
- package/dist/tools/contract-tools.js +5 -3
- package/dist/tools/coverage-tools.js +33 -0
- package/dist/tools/dynamic-tools.js +1 -1
- package/dist/tools/error-recovery.js +1 -1
- package/dist/tools/feedback-tools.js +31 -6
- package/dist/tools/invoke-tool.d.ts +1 -1
- package/dist/tools/invoke-tool.js +10 -5
- package/dist/tools/observe-tools.js +8 -2
- package/dist/tools/tool-kit.d.ts +46 -4
- package/dist/tools/tool-names.d.ts +2 -0
- package/dist/tools/tool-names.js +2 -0
- package/dist/tools/tool-surface.js +2 -3
- package/dist/tools/tools.js +16 -1
- package/docs/README.md +1 -0
- package/docs/agent-cheatsheet.md +2 -2
- package/docs/capabilities.mdx +4 -4
- package/docs/cli/hunt.mdx +2 -2
- package/docs/deploy-checks.md +2 -2
- package/docs/docs.json +2 -2
- package/docs/enterprise.md +9 -3
- package/docs/flows.md +4 -4
- package/docs/gates.md +1 -1
- package/docs/license-activation.mdx +145 -0
- package/docs/recipes.mdx +2 -2
- package/docs/telemetry-contract.md +18 -12
- package/docs/telemetry-events.mdx +1 -34
- package/docs/telemetry.md +0 -1
- package/docs/tools-overview.mdx +1 -1
- package/docs/tools-session-and-feedback.mdx +5 -0
- package/docs/tools-tools-and-run.mdx +15 -15
- package/docs/usage.md +3 -3
- package/package.json +2 -2
- package/dist/telemetry/instrumentation-stall.d.ts +0 -50
- package/dist/telemetry/instrumentation-stall.js +0 -90
package/dist/tools/act-tools.js
CHANGED
|
@@ -8,7 +8,7 @@ import { resolveTargetRef } from './resolve-target.js';
|
|
|
8
8
|
import { z } from 'zod';
|
|
9
9
|
import { aliasParam } from './alias-args.js';
|
|
10
10
|
import { captureAct, compileSequenceStep } from '../flows/replay.js';
|
|
11
|
-
import { ActionType, ActionWarning, CaptureLoss, DEFAULT_ASSERT_TIMEOUT_MS, InputMode, ReticleCommand, Verified, PredicateKind, } from '@reticlehq/core';
|
|
11
|
+
import { ActionType, ActionWarning, CaptureLoss, DEFAULT_ASSERT_TIMEOUT_MS, InputMode, ReticleCommand, Verified, VerifiedReason, PredicateKind, } from '@reticlehq/core';
|
|
12
12
|
import { assertNativeInputSupported } from './act-danger.js';
|
|
13
13
|
import { leanActResult, mutatedWithin } from './act-view.js';
|
|
14
14
|
import { ReticleTool } from './tool-names.js';
|
|
@@ -16,6 +16,10 @@ import { buildReactionReport, summarizeReaction } from '../events/reaction.js';
|
|
|
16
16
|
import { parsePredicate } from '../events/predicate-parse.js';
|
|
17
17
|
import { causalSummary } from '../capsule/causal-summary.js';
|
|
18
18
|
import { findContradictions } from '../events/contradictions.js';
|
|
19
|
+
import { gapsForAction } from '../honesty/instrumentation-gaps.js';
|
|
20
|
+
import { noteSessionGaps } from '../honesty/gap-ledger.js';
|
|
21
|
+
import { declaresState } from '../events/predicate-asks.js';
|
|
22
|
+
import { isStateUnwatched } from '../honesty/blind-spots.js';
|
|
19
23
|
import { inFlightRequestLabels, repeatedRequestLabels, waitForInFlight, } from './settle-in-flight.js';
|
|
20
24
|
import { waitForReaction } from './react-grace.js';
|
|
21
25
|
import { decideVerified } from '../honesty/verified.js';
|
|
@@ -26,7 +30,7 @@ import { saveFailedAssertCapsule } from './act-capsule.js';
|
|
|
26
30
|
import { buildDivergenceCapsule } from '../capsule/capsule.js';
|
|
27
31
|
import { predicateToExpectedLinks } from '../capsule/predicate-to-links.js';
|
|
28
32
|
import { buildHonestyBlock } from '../honesty/honesty.js';
|
|
29
|
-
import {
|
|
33
|
+
import { buildCoverageStatement, blindSpotsFromState, transportGapNote, Coverage, impeachesCapture, } from '../honesty/blind-spots.js';
|
|
30
34
|
import { hasAcceptedWrite } from '../honesty/accepted-write.js';
|
|
31
35
|
import { unreadWriteLabels } from '../honesty/unread-outcome.js';
|
|
32
36
|
import { evaluatePredicate, waitForPredicate, provenExpectedLinks, PredicateSchema, } from '../events/predicate.js';
|
|
@@ -432,6 +436,10 @@ export const ACT_TOOLS = [
|
|
|
432
436
|
.array(z.unknown())
|
|
433
437
|
.optional()
|
|
434
438
|
.describe('Channels that DISAGREE about this action — the UI advanced while its write failed, a success signal fired over a failed request, a response changed nothing, a duplicate fired, a request never settled. OMITTED when clean. Treat any entry as a finding even when the verdict is green: a passing assertion and a contradicted channel is exactly the false green this exists to catch.'),
|
|
439
|
+
instrumentationGaps: z
|
|
440
|
+
.array(z.unknown())
|
|
441
|
+
.optional()
|
|
442
|
+
.describe('What the APP did not tell Reticle, and the one change that would fix it — each entry is { kind, missing, cost, fix, source?, ref? }. Reported ONLY where an absence made THIS verdict weaker (a red that cannot name a file:line, a state assertion with no registered store, a DOM change no signal announced, a route change nothing signalled); never a survey of the page, so an entry is always work worth doing now. OMITTED when the app told Reticle everything it needed. Applying `fix` makes every later verdict on this app stronger, not just this one.'),
|
|
435
443
|
honesty: z
|
|
436
444
|
.unknown()
|
|
437
445
|
.describe('The verdict trust block { grade, attribution, coverage, integrity, envelope? } — a green never looks stronger than this. Gate on grade ≥ net AND integrity.clean. Fields that were not measured are OMITTED rather than reported as zero, so treat an absent `envelope` as "not sampled", never as a failure.'),
|
|
@@ -561,7 +569,7 @@ export const ACT_TOOLS = [
|
|
|
561
569
|
const coverage = buildCoverageStatement(spots);
|
|
562
570
|
// Nothing subscribed ⇒ the state channel is dark, and the summary must say so rather than
|
|
563
571
|
// report an empty diff list that reads like a fact about the app. See CausalSummary.
|
|
564
|
-
const stateUnwatched = spots
|
|
572
|
+
const stateUnwatched = isStateUnwatched(spots);
|
|
565
573
|
// Only a spot that IMPEACHES the capture belongs in integrity — see impeachesCapture. A
|
|
566
574
|
// structural boundary (virtualized rows, a cross-origin frame) is reported as coverage and
|
|
567
575
|
// must not downgrade a verdict about what WAS observed.
|
|
@@ -672,6 +680,25 @@ export const ACT_TOOLS = [
|
|
|
672
680
|
repeated: repeatedRequestLabels(windowEvents),
|
|
673
681
|
},
|
|
674
682
|
});
|
|
683
|
+
// Computed once: the verdict block reports it, and the instrumentation gaps are a second
|
|
684
|
+
// reading of the same evidence rather than a new observation.
|
|
685
|
+
const actionSummary = causalSummary(windowEvents, { stateUnwatched });
|
|
686
|
+
const gaps = gapsForAction({
|
|
687
|
+
pass: verdict.pass,
|
|
688
|
+
proved: decision.verifiedReason === VerifiedReason.PROVED,
|
|
689
|
+
sourceKnown: actedSource !== undefined,
|
|
690
|
+
ref: asString(args['ref']),
|
|
691
|
+
stateAsked: declaresState(until),
|
|
692
|
+
stateUnwatched,
|
|
693
|
+
domMutated: (session.lastAct.effect().mutatedWithin ?? 0) > 0,
|
|
694
|
+
signalsFired: actionSummary.signals.length,
|
|
695
|
+
routeChanged: actionSummary.route !== undefined,
|
|
696
|
+
routeSignalFired: actionSummary.signals.some((name) => name.startsWith('route')),
|
|
697
|
+
});
|
|
698
|
+
// Recorded on the session, so a later "am I done?" can answer with what is STILL missing
|
|
699
|
+
// rather than with everything that was ever missing. An empty list closes a gap, which is
|
|
700
|
+
// why it is noted rather than skipped.
|
|
701
|
+
noteSessionGaps(session, gaps);
|
|
675
702
|
return withControl(session, {
|
|
676
703
|
...decision,
|
|
677
704
|
effect: leanActResult(actResult.result),
|
|
@@ -686,7 +713,10 @@ export const ACT_TOOLS = [
|
|
|
686
713
|
...(capsuleSaved === undefined ? {} : { capsuleSaved }),
|
|
687
714
|
// The window cannot say whether anything was WATCHING state — that is a level fact the
|
|
688
715
|
// session holds. Without it an empty `stateDiffs` reads as "the app changed nothing".
|
|
689
|
-
summary:
|
|
716
|
+
summary: actionSummary,
|
|
717
|
+
// What the APP did not tell Reticle, and the one change that would fix it. Reported only
|
|
718
|
+
// where an absence made this very verdict weaker — never as a survey of the page.
|
|
719
|
+
...(gaps.length > 0 ? { instrumentationGaps: gaps } : {}),
|
|
690
720
|
// Cross-channel disagreement, reported WITH the action that caused it.
|
|
691
721
|
//
|
|
692
722
|
// This is the one finding here a human structurally cannot make — they watch one channel,
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { InstrumentationGap } from '@reticlehq/core';
|
|
1
2
|
import type { Predicate } from '../events/predicate.js';
|
|
2
3
|
import type { Session } from '../session/session.js';
|
|
3
4
|
import { type Contradiction } from '../events/contradictions.js';
|
|
@@ -26,4 +27,5 @@ observationLost?: boolean): Promise<{
|
|
|
26
27
|
decision: Record<string, unknown>;
|
|
27
28
|
contradictions: Contradiction[];
|
|
28
29
|
coverage: Record<string, unknown>;
|
|
30
|
+
gaps: InstrumentationGap[];
|
|
29
31
|
}>;
|
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
import { BlindSpotKind, CaptureLoss, PredicateKind } from '@reticlehq/core';
|
|
2
|
+
import { gapsForAction } from '../honesty/instrumentation-gaps.js';
|
|
3
|
+
import { noteSessionGaps } from '../honesty/gap-ledger.js';
|
|
4
|
+
import { declaresState } from '../events/predicate-asks.js';
|
|
5
|
+
import { isStateUnwatched } from '../honesty/blind-spots.js';
|
|
2
6
|
import { findContradictions } from '../events/contradictions.js';
|
|
3
7
|
import { declaredExpectations } from '../events/declared.js';
|
|
4
8
|
import { blindSpotsFromState, buildCoverageStatement, Coverage, impeachesCapture, transportGapNote, } from '../honesty/blind-spots.js';
|
|
@@ -122,5 +126,24 @@ observationLost) {
|
|
|
122
126
|
repeated: repeatedRequestLabels(windowEvents),
|
|
123
127
|
},
|
|
124
128
|
});
|
|
125
|
-
|
|
129
|
+
// The same rule the act path uses, fed by what THIS path knows. An assertion drives nothing, so
|
|
130
|
+
// only two of the four gates can fire here: a red with no remembered source to point at, and a
|
|
131
|
+
// state assertion against an app that registers no store.
|
|
132
|
+
const gaps = gapsForAction({
|
|
133
|
+
pass,
|
|
134
|
+
sourceKnown: session.lastAct.source() !== undefined,
|
|
135
|
+
stateAsked: declaresState(predicate),
|
|
136
|
+
stateUnwatched: isStateUnwatched(spots),
|
|
137
|
+
domMutated: false,
|
|
138
|
+
signalsFired: 0,
|
|
139
|
+
routeChanged: false,
|
|
140
|
+
routeSignalFired: false,
|
|
141
|
+
});
|
|
142
|
+
noteSessionGaps(session, gaps);
|
|
143
|
+
return {
|
|
144
|
+
decision: decision,
|
|
145
|
+
contradictions,
|
|
146
|
+
coverage,
|
|
147
|
+
gaps,
|
|
148
|
+
};
|
|
126
149
|
}
|
|
@@ -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
|
};
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
+
import { coverageRegressed, observabilityOf } from '../honesty/observability.js';
|
|
2
3
|
import { ReticleCommand, SnapshotMode } from '@reticlehq/core';
|
|
3
4
|
import { ReticleTool } from './tool-names.js';
|
|
4
5
|
import { asString } from './tools-helpers.js';
|
|
@@ -62,6 +63,22 @@ export const COVERAGE_TOOLS = [
|
|
|
62
63
|
.number()
|
|
63
64
|
.optional()
|
|
64
65
|
.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.'),
|
|
66
|
+
instrumentationGaps: z
|
|
67
|
+
.array(z.unknown())
|
|
68
|
+
.optional()
|
|
69
|
+
.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.'),
|
|
70
|
+
unproven: z
|
|
71
|
+
.boolean()
|
|
72
|
+
.optional()
|
|
73
|
+
.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.'),
|
|
74
|
+
observability: z
|
|
75
|
+
.object({ driven: z.number(), observable: z.number(), percent: z.number().optional() })
|
|
76
|
+
.optional()
|
|
77
|
+
.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%.'),
|
|
78
|
+
observabilityRegressed: z
|
|
79
|
+
.object({ was: z.number(), now: z.number() })
|
|
80
|
+
.optional()
|
|
81
|
+
.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.'),
|
|
65
82
|
},
|
|
66
83
|
handler: async (deps, args) => {
|
|
67
84
|
const sessionId = asString(args['sessionId']);
|
|
@@ -77,11 +94,27 @@ export const COVERAGE_TOOLS = [
|
|
|
77
94
|
// `droveGone` keeps the other honesty: archive/delete/submit remove their own control, so a
|
|
78
95
|
// drive that WORKED must not read as no coverage at all.
|
|
79
96
|
const { exercised, droveGone, untouched } = exercisedCount(parseControls(tree), session.actedRefs(), session.actedLabels());
|
|
97
|
+
// The other half of "am I done?". `untouched` answers what you did not DRIVE; this answers
|
|
98
|
+
// what this app cannot ANSWER, which no amount of further driving will change. Reporting the
|
|
99
|
+
// first without the second is how an agent finishes a pass believing it verified something the
|
|
100
|
+
// app was never able to confirm.
|
|
101
|
+
const gaps = session.gaps?.open() ?? [];
|
|
102
|
+
// The number, and the floor under it, together. A coverage figure that can only ever be
|
|
103
|
+
// reported and never contradicted is one an agent learns to satisfy rather than to earn.
|
|
104
|
+
const observability = observabilityOf(session.actedRefs(), gaps);
|
|
105
|
+
const best = await deps.project.bestObservability();
|
|
106
|
+
const regressed = coverageRegressed(best, observability);
|
|
107
|
+
if (observability.percent !== undefined) {
|
|
108
|
+
await deps.project.raiseObservability(observability.percent);
|
|
109
|
+
}
|
|
80
110
|
return {
|
|
81
111
|
total: parseControls(tree).length,
|
|
82
112
|
exercised,
|
|
83
113
|
untouched,
|
|
84
114
|
...(droveGone > 0 ? { alsoDroveGone: droveGone } : {}),
|
|
115
|
+
...(gaps.length > 0 ? { instrumentationGaps: gaps, unproven: true } : {}),
|
|
116
|
+
observability,
|
|
117
|
+
...(regressed === undefined ? {} : { observabilityRegressed: regressed }),
|
|
85
118
|
};
|
|
86
119
|
},
|
|
87
120
|
},
|
|
@@ -75,7 +75,7 @@ export function buildDynamicTools(allTools, profile) {
|
|
|
75
75
|
};
|
|
76
76
|
const reticleTools = {
|
|
77
77
|
name: ReticleTool.TOOLS,
|
|
78
|
-
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 →
|
|
78
|
+
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
79
|
inputSchema: {
|
|
80
80
|
names: z
|
|
81
81
|
.array(z.string())
|
|
@@ -129,7 +129,7 @@ export const RECOVERY = {
|
|
|
129
129
|
'synchronous task, an alert/confirm dialog); reticle_console usually shows what it hit. Retry ' +
|
|
130
130
|
'once the cause is addressed.',
|
|
131
131
|
FLOW_STEP_MISSING: 'A recorded step points at an element that is not on the page now. Either the app changed under ' +
|
|
132
|
-
'the flow — run reticle_flow_heal to re-anchor it, then
|
|
132
|
+
'the flow — run reticle_flow_heal to re-anchor it, then reticle_verify{action:"flows"} — or an earlier step ' +
|
|
133
133
|
'did not do what it used to, leaving the run on the wrong page: check reticle_snapshot to see ' +
|
|
134
134
|
'where it actually is. This is app/flow drift, which is exactly what the flow exists to catch.',
|
|
135
135
|
NO_POOL: 'Reticle cannot launch its own browser here: the daemon-managed pool is absent, or Playwright ' +
|
|
@@ -12,9 +12,9 @@
|
|
|
12
12
|
* tokens on that and not on politeness.
|
|
13
13
|
*/
|
|
14
14
|
import { z } from 'zod';
|
|
15
|
-
import { FeedbackKind, FeedbackSource } from '@reticlehq/core';
|
|
15
|
+
import { FeedbackKind, FeedbackSource, FEEDBACK_RATING_MAX, FEEDBACK_RATING_MIN, } from '@reticlehq/core';
|
|
16
16
|
import { ReticleTool } from './tool-names.js';
|
|
17
|
-
import { asString } from './tools-helpers.js';
|
|
17
|
+
import { asNumber, asString } from './tools-helpers.js';
|
|
18
18
|
import { submitFeedback, feedbackDisabled } from '../telemetry/feedback.js';
|
|
19
19
|
import { getTelemetry } from '../telemetry/telemetry.js';
|
|
20
20
|
/** One optional string arg, present only when the agent actually supplied it. */
|
|
@@ -22,6 +22,19 @@ function optionalText(args, key) {
|
|
|
22
22
|
const value = asString(args[key]);
|
|
23
23
|
return value !== undefined && value !== '' ? { [key]: value } : {};
|
|
24
24
|
}
|
|
25
|
+
/**
|
|
26
|
+
* The star rating, present only when the agent actually supplied one, and only when it is on the
|
|
27
|
+
* scale core defines. Bounds-checked HERE as well as in the schema because a number that survives
|
|
28
|
+
* to the wire off-scale is worse than an absent one: it still averages.
|
|
29
|
+
*/
|
|
30
|
+
function optionalRating(args) {
|
|
31
|
+
const value = asNumber(args['rating']);
|
|
32
|
+
if (value === undefined || !Number.isInteger(value))
|
|
33
|
+
return {};
|
|
34
|
+
if (value < FEEDBACK_RATING_MIN || value > FEEDBACK_RATING_MAX)
|
|
35
|
+
return {};
|
|
36
|
+
return { rating: value };
|
|
37
|
+
}
|
|
25
38
|
/** Read the connected session's self-reported facts, or nothing when no app is attached. */
|
|
26
39
|
function sessionFacts(deps, sessionId) {
|
|
27
40
|
const driven = deps.realInput !== undefined;
|
|
@@ -45,7 +58,7 @@ function sessionFacts(deps, sessionId) {
|
|
|
45
58
|
export const FEEDBACK_TOOLS = [
|
|
46
59
|
{
|
|
47
60
|
name: ReticleTool.FEEDBACK,
|
|
48
|
-
description: "Tell the Reticle maintainers something — a failure, OR a feature you wish existed. Reticle is built FOR agents, so your experience is the one that decides what gets built, and this is the only channel that reaches us. File a `bug` when a tool returned something wrong, a `gap` when Reticle could not observe what you needed, an `ambiguity` when a verdict was not decidable, a `feature_request` when you thought 'I wish Reticle could do X',
|
|
61
|
+
description: "Tell the Reticle maintainers something — a failure, OR a feature you wish existed. Reticle is built FOR agents, so your experience is the one that decides what gets built, and this is the only channel that reaches us. File a `bug` when a tool returned something wrong, a `gap` when Reticle could not observe what you needed, an `ambiguity` when a verdict was not decidable, a `feature_request` when you thought 'I wish Reticle could do X', an `improvement` when something works but is awkward (too many calls, a confusing shape, a slow path), or an `experience` when something worked notably WELL (carry `rating` 1-5). File that one unprompted, when you actually notice it, and say which call and what it saved you — every other kind here is a complaint, so it is the only evidence for what is worth protecting when we change things. Do not save these up or judge them as too minor — file it the moment you notice, then carry on with your task. For a failure, write `text` as a root-cause analysis (what you called, expected, got). For a request, write what you want and use `need` for the GOAL behind it, `impact` for what gets better, and `currentApproach` for how you are working around it today — that workaround is usually the most useful thing in the report. Set `model` to the model you are running. Never include app source, secrets, user data, or customer records. Do NOT use this for bugs you found in the app under test — that is a successful verification, which is Reticle working.",
|
|
49
62
|
inputSchema: {
|
|
50
63
|
kind: z
|
|
51
64
|
.enum([
|
|
@@ -54,8 +67,9 @@ export const FEEDBACK_TOOLS = [
|
|
|
54
67
|
FeedbackKind.AMBIGUITY,
|
|
55
68
|
FeedbackKind.FEATURE_REQUEST,
|
|
56
69
|
FeedbackKind.IMPROVEMENT,
|
|
70
|
+
FeedbackKind.EXPERIENCE,
|
|
57
71
|
])
|
|
58
|
-
.describe('bug = a Reticle tool returned a wrong or broken result; gap = Reticle could not observe something you needed at all; ambiguity = the verification ran but you could not tell pass from fail; feature_request = something that does not exist and would have helped; improvement = something that exists but is awkward or inefficient.'),
|
|
72
|
+
.describe('bug = a Reticle tool returned a wrong or broken result; gap = Reticle could not observe something you needed at all; ambiguity = the verification ran but you could not tell pass from fail; feature_request = something that does not exist and would have helped; improvement = something that exists but is awkward or inefficient; experience = something worked notably well and is worth keeping (pair it with `rating`).'),
|
|
59
73
|
text: z
|
|
60
74
|
.string()
|
|
61
75
|
.min(1)
|
|
@@ -72,6 +86,13 @@ export const FEEDBACK_TOOLS = [
|
|
|
72
86
|
.string()
|
|
73
87
|
.optional()
|
|
74
88
|
.describe('How you work around it TODAY. Usually the most useful field in the report: it proves the need is real, and often shows a cheaper fix than the feature being asked for.'),
|
|
89
|
+
rating: z
|
|
90
|
+
.number()
|
|
91
|
+
.int()
|
|
92
|
+
.min(FEEDBACK_RATING_MIN)
|
|
93
|
+
.max(FEEDBACK_RATING_MAX)
|
|
94
|
+
.optional()
|
|
95
|
+
.describe(`How well it worked, ${FEEDBACK_RATING_MIN}-${FEEDBACK_RATING_MAX}, with an \`experience\`. Only when you formed the judgement while working, never because you were asked: an agreeable number and an earned one are indistinguishable once both sit in the same column. The \`text\` is the part we can act on.`),
|
|
75
96
|
model: z
|
|
76
97
|
.string()
|
|
77
98
|
.optional()
|
|
@@ -112,6 +133,7 @@ export const FEEDBACK_TOOLS = [
|
|
|
112
133
|
...optionalText(args, 'impact'),
|
|
113
134
|
...optionalText(args, 'currentApproach'),
|
|
114
135
|
...optionalText(args, 'model'),
|
|
136
|
+
...optionalRating(args),
|
|
115
137
|
}, {
|
|
116
138
|
session: sessionFacts(deps, asString(args['sessionId'])),
|
|
117
139
|
// The AGENT does not wait for the network. `reticle feedback` (a human, at a terminal,
|
|
@@ -154,8 +176,11 @@ export const FEEDBACK_PROMPT = {
|
|
|
154
176
|
export const VERIFICATION_TOOLS = new Set([
|
|
155
177
|
ReticleTool.ACT_AND_WAIT,
|
|
156
178
|
ReticleTool.ASSERT,
|
|
157
|
-
|
|
158
|
-
|
|
179
|
+
// Merged: `flow_verify` and `verify_change` are actions on this tool now. Naming the merged tool
|
|
180
|
+
// does NOT make every action a verification — `verificationOf` still requires the result to carry
|
|
181
|
+
// a `verified` or a pass/fail `status`, so `coverage` and `crawl` contribute nothing and cannot
|
|
182
|
+
// inflate the count.
|
|
183
|
+
ReticleTool.VERIFY,
|
|
159
184
|
ReticleTool.RECONCILE,
|
|
160
185
|
]);
|
|
161
186
|
/**
|
|
@@ -7,4 +7,4 @@ export declare const SESSION_BOUND_TOOLS: ReadonlySet<string>;
|
|
|
7
7
|
* `sessionId`-bearing tool to be classified into exactly one set (bound XOR exempt).
|
|
8
8
|
*/
|
|
9
9
|
export declare const SESSION_EXEMPT_TOOLS: ReadonlySet<string>;
|
|
10
|
-
export declare function runTool(tool: ToolDef
|
|
10
|
+
export declare function runTool<Ext>(tool: ToolDef<Ext>, deps: ToolDeps<Ext>, args: Record<string, unknown>): Promise<unknown>;
|
|
@@ -47,14 +47,21 @@ const REF_MINTING_TOOLS = new Set([
|
|
|
47
47
|
ReticleTool.QUERY,
|
|
48
48
|
ReticleTool.INSPECT,
|
|
49
49
|
ReticleTool.EXPLORE,
|
|
50
|
-
|
|
50
|
+
// `crawl` mints refs for the controls it drives and `coverage` for the ones it reports untouched.
|
|
51
|
+
// Both are actions on the merged verify tool now.
|
|
52
|
+
ReticleTool.VERIFY,
|
|
51
53
|
]);
|
|
52
54
|
export const SESSION_BOUND_TOOLS = new Set([
|
|
53
55
|
ReticleTool.SNAPSHOT,
|
|
54
56
|
ReticleTool.QUERY,
|
|
55
57
|
ReticleTool.INSPECT,
|
|
56
|
-
|
|
57
|
-
|
|
58
|
+
// Merged change/flows/affected/coverage/crawl. Its members were SPLIT across bound and exempt —
|
|
59
|
+
// coverage/change/crawl bound, flow_verify exempt because it returns its own suite contract — and
|
|
60
|
+
// a merged tool has to be one or the other. Bound is the right side: `crawl` drives the app
|
|
61
|
+
// itself, so a throttled or event-dropping tab is exactly the condition its verdict depends on,
|
|
62
|
+
// and dropping the health disclosure there to protect a sibling's output shape would trade an
|
|
63
|
+
// honesty signal for a formatting preference.
|
|
64
|
+
ReticleTool.VERIFY,
|
|
58
65
|
ReticleTool.ACT,
|
|
59
66
|
ReticleTool.ACT_SEQUENCE,
|
|
60
67
|
ReticleTool.ACT_AND_WAIT,
|
|
@@ -74,7 +81,6 @@ export const SESSION_BOUND_TOOLS = new Set([
|
|
|
74
81
|
ReticleTool.STATE,
|
|
75
82
|
ReticleTool.STORAGE,
|
|
76
83
|
ReticleTool.EXPLORE,
|
|
77
|
-
ReticleTool.CRAWL,
|
|
78
84
|
ReticleTool.SCROLL_TO,
|
|
79
85
|
ReticleTool.NAVIGATE,
|
|
80
86
|
]);
|
|
@@ -90,7 +96,6 @@ export const SESSION_EXEMPT_TOOLS = new Set([
|
|
|
90
96
|
ReticleTool.FLOW_SAVE, // sessionId only scopes the write to the app's flow subdir; disk-side
|
|
91
97
|
ReticleTool.FLOW, // merged list/load/delete — sessionId only scopes the project; all disk-side
|
|
92
98
|
ReticleTool.FLOW_REPLAY, // returns its own FlowReplayResult contract (+ auto-records a run)
|
|
93
|
-
ReticleTool.FLOW_VERIFY, // returns its own SuiteVerdict contract (replays the whole suite)
|
|
94
99
|
ReticleTool.FLOW_SAVE_RECORDED, // reads the recording buffer, writes disk
|
|
95
100
|
ReticleTool.FLOW_HEAL, // returns its own FlowHealResult contract
|
|
96
101
|
ReticleTool.PROJECT, // reads .reticle/project.json
|
|
@@ -264,7 +264,7 @@ export const OBSERVE_TOOLS = [
|
|
|
264
264
|
// that guessed `urlContains` on route (net's spelling) and got unrecognized_keys.
|
|
265
265
|
'Predicate to evaluate. Kinds: { signal, name } { net, urlContains|method|status|count|bodyContains } ' +
|
|
266
266
|
'{ state, path|equals } { route, pathname (exact) | contains (path+query+hash) } ' +
|
|
267
|
-
'{ element, testid|role|text } { text } { console, level|absent } { animation, name } ' +
|
|
267
|
+
'{ element, testid|role|text } { text } { console, level|contains|absent } { animation, name } ' +
|
|
268
268
|
'{ settled } — combine with { allOf | anyOf | not }. Prefer a signal/net/state consequence ' +
|
|
269
269
|
'over element/text presence.'),
|
|
270
270
|
until: PredicateSchema.optional().describe("Alias for `predicate` (act_and_wait's name)."),
|
|
@@ -318,6 +318,10 @@ export const OBSERVE_TOOLS = [
|
|
|
318
318
|
.array(z.object({ kind: z.string(), count: z.number() }))
|
|
319
319
|
.optional()
|
|
320
320
|
.describe('Which regions were unobservable, when coverage is partial.'),
|
|
321
|
+
instrumentationGaps: z
|
|
322
|
+
.array(z.unknown())
|
|
323
|
+
.optional()
|
|
324
|
+
.describe('What the APP did not tell Reticle, and the one change that would fix it — each entry is { kind, missing, cost, fix, source?, ref? }. Reported ONLY where an absence made THIS verdict weaker: a failing assertion with no source to point at, or a state assertion against an app that registers no store. Never a survey of the page, so an entry is always work worth doing now. OMITTED when the app told Reticle everything it needed.'),
|
|
321
325
|
},
|
|
322
326
|
handler: async (deps, args) => {
|
|
323
327
|
const session = deps.sessions.resolve(asString(args['sessionId']));
|
|
@@ -341,11 +345,13 @@ export const OBSERVE_TOOLS = [
|
|
|
341
345
|
: assertsDerivedIpcStatus(predicate)
|
|
342
346
|
? { advice: DERIVED_IPC_STATUS_ADVICE }
|
|
343
347
|
: {};
|
|
344
|
-
const { decision, contradictions, coverage } = await assertVerdict(session, predicate, verdict.pass, since, verdict.inconclusive, verdict.observationLost);
|
|
348
|
+
const { decision, contradictions, coverage, gaps } = await assertVerdict(session, predicate, verdict.pass, since, verdict.inconclusive, verdict.observationLost);
|
|
345
349
|
return withControl(session, {
|
|
346
350
|
...decision,
|
|
347
351
|
...verdict,
|
|
348
352
|
...(contradictions.length > 0 ? { contradictions } : {}),
|
|
353
|
+
// What the app did not tell Reticle, on the same rule the act path uses.
|
|
354
|
+
...(gaps.length > 0 ? { instrumentationGaps: gaps } : {}),
|
|
349
355
|
...advice,
|
|
350
356
|
...coverage,
|
|
351
357
|
...lastActSourceOnFailure(session, verdict.pass),
|
package/dist/tools/tool-kit.d.ts
CHANGED
|
@@ -11,12 +11,13 @@ import type { RealInputProvider } from '../input/real-input.js';
|
|
|
11
11
|
import type { BaselineStore } from '../project/baselines.js';
|
|
12
12
|
import type { RecordingStore } from '../flows/recordings.js';
|
|
13
13
|
import type { FileSystemPort } from '../project/fs-port.js';
|
|
14
|
+
import type { ArtifactRoot } from '../project/artifact-root.js';
|
|
14
15
|
import type { FlowStore } from '../flows/flows.js';
|
|
15
16
|
import type { ProjectStore } from '../project/project-store.js';
|
|
16
17
|
import type { AnnotationStore } from '../flows/annotation-store.js';
|
|
17
18
|
import type { BrowserPool } from '../pool/browser-pool.js';
|
|
18
19
|
import type { ChromiumProbe } from '../cli/chromium-hint.js';
|
|
19
|
-
export interface ToolDeps {
|
|
20
|
+
export interface ToolDeps<Ext = unknown> {
|
|
20
21
|
sessions: SessionManager;
|
|
21
22
|
/** shared one-browser/N-context pool for headless leases. undefined ⇒ lease tools report unavailable. */
|
|
22
23
|
pool?: BrowserPool;
|
|
@@ -39,8 +40,22 @@ export interface ToolDeps {
|
|
|
39
40
|
realInput?: RealInputProvider;
|
|
40
41
|
/** injected filesystem seam (tests pass a fake/temp-dir adapter). */
|
|
41
42
|
fs: FileSystemPort;
|
|
42
|
-
/** absolute.reticle path (index.ts computes cwd/.reticle). */
|
|
43
|
+
/** absolute.reticle path (index.ts computes cwd/.reticle). The FALLBACK, not the answer. */
|
|
43
44
|
reticleRoot: string;
|
|
45
|
+
/**
|
|
46
|
+
* Which `.reticle` a SESSION's artifacts belong in, resolved per call.
|
|
47
|
+
*
|
|
48
|
+
* `reticleRoot` above is fixed at construction from the daemon's own `process.cwd()`, which is a
|
|
49
|
+
* statement about where the daemon was launched and was being read as a statement about which
|
|
50
|
+
* project is being verified. Those are usually not the same tree: an editor starts a user-scoped
|
|
51
|
+
* MCP server wherever it likes, so flows landed outside the repo they verify, or died on
|
|
52
|
+
* `mkdir '/.reticle'`.
|
|
53
|
+
*
|
|
54
|
+
* Optional because every existing construction of ToolDeps predates it and because a consumer
|
|
55
|
+
* embedding this engine may not have a filesystem to search. Absent ⇒ `reticleRoot`, which is
|
|
56
|
+
* exactly today's behaviour.
|
|
57
|
+
*/
|
|
58
|
+
artifactRootFor?: (projectId: string | undefined) => ArtifactRoot;
|
|
44
59
|
/**
|
|
45
60
|
* This daemon's OWN project id, derived from the directory it was started in.
|
|
46
61
|
*
|
|
@@ -59,8 +74,23 @@ export interface ToolDeps {
|
|
|
59
74
|
* ToolDeps keeps working; absent falls back to the default port.
|
|
60
75
|
*/
|
|
61
76
|
bridgePort?: number;
|
|
77
|
+
/**
|
|
78
|
+
* A consumer's OWN dependencies, carried through untouched.
|
|
79
|
+
*
|
|
80
|
+
* Nothing in this package reads it. It exists so a consumer embedding the engine can add tools of
|
|
81
|
+
* its own — answering out of a store this package has never heard of — without either widening the
|
|
82
|
+
* fields above with a stranger's vocabulary or forking to get a handle through.
|
|
83
|
+
*
|
|
84
|
+
* The type parameter, rather than `unknown`, is what keeps the seam typed: a consumer writes
|
|
85
|
+
* `ToolDef<TheirExt>` once and their handlers see their own type. Casting out of `unknown` in every
|
|
86
|
+
* handler is how a typed seam quietly becomes an untyped one.
|
|
87
|
+
*
|
|
88
|
+
* Optional, and defaulted to `unknown`, so every existing construction of this bag — and every test
|
|
89
|
+
* that builds one — is unchanged.
|
|
90
|
+
*/
|
|
91
|
+
ext?: Ext;
|
|
62
92
|
}
|
|
63
|
-
export interface ToolDef {
|
|
93
|
+
export interface ToolDef<Ext = unknown> {
|
|
64
94
|
name: string;
|
|
65
95
|
description: string;
|
|
66
96
|
inputSchema: z.ZodRawShape;
|
|
@@ -86,7 +116,19 @@ export interface ToolDef {
|
|
|
86
116
|
* example that does not validate is worse than none at all.
|
|
87
117
|
*/
|
|
88
118
|
example?: Record<string, unknown>;
|
|
89
|
-
|
|
119
|
+
/**
|
|
120
|
+
* Declared method-style, not as a property, and that is load-bearing rather than stylistic.
|
|
121
|
+
*
|
|
122
|
+
* Under `strictFunctionTypes` a property holding a function has CONTRAVARIANT parameters, so
|
|
123
|
+
* `ToolDef<TheirExt>` and `ToolDef<unknown>` become mutually unassignable and a consumer composing
|
|
124
|
+
* `[...TOOLS, ...theirTools]` needs a cast — as does every internal helper that then touches the
|
|
125
|
+
* array. Method-style parameters are checked bivariantly, which collapses all of that.
|
|
126
|
+
*
|
|
127
|
+
* The unsoundness bivariance admits is that a handler written against `ToolDeps<TheirExt>` could be
|
|
128
|
+
* handed a bag carrying no `ext` at all. That costs nothing here, because `ext` is optional by
|
|
129
|
+
* design: any handler reading it already has to survive `undefined`.
|
|
130
|
+
*/
|
|
131
|
+
handler(deps: ToolDeps<Ext>, args: Record<string, unknown>): Promise<unknown>;
|
|
90
132
|
}
|
|
91
133
|
export declare const sessionIdShape: {
|
|
92
134
|
sessionId: z.ZodOptional<z.ZodString>;
|
|
@@ -46,6 +46,8 @@ export declare const ReticleTool: {
|
|
|
46
46
|
readonly AFFECTED: "reticle_affected";
|
|
47
47
|
readonly COVERAGE: "reticle_coverage";
|
|
48
48
|
readonly VERIFY_CHANGE: "reticle_verify_change";
|
|
49
|
+
/** Merged: change/flows/affected/coverage/crawl — "what is proved, and what is not". */
|
|
50
|
+
readonly VERIFY: "reticle_verify";
|
|
49
51
|
/** read cross-run history (.reticle/project.json) + diff-vs-last for a name. */
|
|
50
52
|
readonly PROJECT: "reticle_project";
|
|
51
53
|
/** explicitly record a run outcome (the manual companion to auto-recording). */
|
package/dist/tools/tool-names.js
CHANGED
|
@@ -47,6 +47,8 @@ export const ReticleTool = {
|
|
|
47
47
|
AFFECTED: 'reticle_affected',
|
|
48
48
|
COVERAGE: 'reticle_coverage',
|
|
49
49
|
VERIFY_CHANGE: 'reticle_verify_change',
|
|
50
|
+
/** Merged: change/flows/affected/coverage/crawl — "what is proved, and what is not". */
|
|
51
|
+
VERIFY: 'reticle_verify',
|
|
50
52
|
/** read cross-run history (.reticle/project.json) + diff-vs-last for a name. */
|
|
51
53
|
PROJECT: 'reticle_project',
|
|
52
54
|
/** explicitly record a run outcome (the manual companion to auto-recording). */
|
|
@@ -206,15 +206,14 @@ export const EXTENDED_TOOL_NAMES = new Set([
|
|
|
206
206
|
ReticleTool.CAPABILITIES,
|
|
207
207
|
ReticleTool.FLOW_SAVE,
|
|
208
208
|
ReticleTool.FLOW_REPLAY,
|
|
209
|
-
ReticleTool.FLOW_VERIFY,
|
|
210
209
|
ReticleTool.RECORD,
|
|
211
210
|
ReticleTool.SCREENSHOT,
|
|
212
211
|
ReticleTool.VISUAL_DIFF,
|
|
213
212
|
ReticleTool.CLOCK,
|
|
214
213
|
ReticleTool.NETWORK_MOCK,
|
|
215
214
|
ReticleTool.STORAGE,
|
|
216
|
-
|
|
217
|
-
ReticleTool.
|
|
215
|
+
// Merged change/flows/affected/coverage/crawl — one name where three of these used to sit.
|
|
216
|
+
ReticleTool.VERIFY,
|
|
218
217
|
]);
|
|
219
218
|
/**
|
|
220
219
|
* The verify surface: one acting tool that returns a verdict, plus the two meta-tools that reach
|
package/dist/tools/tools.js
CHANGED
|
@@ -508,7 +508,7 @@ export const MERGE_PLANS = [
|
|
|
508
508
|
},
|
|
509
509
|
{
|
|
510
510
|
name: ReticleTool.FLOW,
|
|
511
|
-
description: 'Saved-flow management: { action: "list" } names every saved flow, "load" reads+validates one by flowName, "delete" removes one. The hot verbs (flow_save, flow_replay,
|
|
511
|
+
description: 'Saved-flow management: { action: "list" } names every saved flow, "load" reads+validates one by flowName, "delete" removes one. The hot verbs (flow_save, flow_replay, flow_heal) stay separate; whole-suite replay is reticle_verify { action: "flows" }.',
|
|
512
512
|
members: {
|
|
513
513
|
list: ReticleTool.FLOW_LIST,
|
|
514
514
|
load: ReticleTool.FLOW_LOAD,
|
|
@@ -531,6 +531,21 @@ export const MERGE_PLANS = [
|
|
|
531
531
|
// is the one that must not need a schema lookup first.
|
|
532
532
|
example: { action: 'yield', mode: 'waiting' },
|
|
533
533
|
},
|
|
534
|
+
{
|
|
535
|
+
name: ReticleTool.VERIFY,
|
|
536
|
+
description: 'What is proved, and what is not — by action. Start from a change: { action: "affected" } names which saved flows must re-verify for the files you edited (pass `files`, and/or `since` for a git ref), and "change" goes further and actually replays them, answering with one `verified` plus `because`. `unknown` there is the honest answer when NO saved flow covers the change: nothing ran, so nothing was proved, and it is never reported as green. Without a change to start from: "flows" replays every saved flow for one consolidated suite verdict (deterministic, no LLM per flow), and "coverage" lists the interactive controls you have and have NOT driven this session — an untouched list still holding the controls your change affects means you are not done. "crawl" is the no-script option: it drives every reachable control itself and reports single-channel faults (console errors, failed requests, dead controls) and CONTRADICTIONS, two channels disagreeing about the same click — the false greens a human cannot see, because a human watches the screen and the screen looks correct. DESTRUCTIVE: crawl really clicks, and may navigate or mutate state.',
|
|
537
|
+
members: {
|
|
538
|
+
change: ReticleTool.VERIFY_CHANGE,
|
|
539
|
+
flows: ReticleTool.FLOW_VERIFY,
|
|
540
|
+
affected: ReticleTool.AFFECTED,
|
|
541
|
+
coverage: ReticleTool.COVERAGE,
|
|
542
|
+
crawl: ReticleTool.CRAWL,
|
|
543
|
+
},
|
|
544
|
+
// No default action ON PURPOSE. The name implies no single member, and one of them really
|
|
545
|
+
// clicks: a fallthrough that guessed wrong would drive the app rather than read it. Every action
|
|
546
|
+
// is explicit, which for `crawl` is the whole safety story.
|
|
547
|
+
example: { action: 'change', files: ['src/App.tsx'] },
|
|
548
|
+
},
|
|
534
549
|
{
|
|
535
550
|
name: ReticleTool.LEASE,
|
|
536
551
|
description: 'Isolated headless contexts from the shared pool: { action: "acquire" } leases one and navigates it to the app URL, "release" closes it and frees the slot.',
|
package/docs/README.md
CHANGED
|
@@ -69,6 +69,7 @@ Everything here is published to [docs.reticle.sh](https://docs.reticle.sh) by `d
|
|
|
69
69
|
| [vs-screenshots.mdx](vs-screenshots.mdx) | why a better vision model does not fix a non-visual bug |
|
|
70
70
|
| [telemetry.md](telemetry.md) | what is collected, and how to turn it off |
|
|
71
71
|
| [local-registry.md](local-registry.md) | installing an unpublished build |
|
|
72
|
+
| [license-activation.mdx](license-activation.mdx) | you have a key: where to set it, how to confirm it, what to do when it fails |
|
|
72
73
|
| [enterprise.md](enterprise.md) | the licensed surface |
|
|
73
74
|
|
|
74
75
|
## For people working on Reticle
|
package/docs/agent-cheatsheet.md
CHANGED
|
@@ -115,7 +115,7 @@ The tools advertised DIRECTLY, i.e. what you'll use 90% of the time. Everything
|
|
|
115
115
|
|
|
116
116
|
Frequently useful but NOT core, so reach them through `reticle_run({ tool, args })`: `reticle_capabilities` (the app's whole testable surface in one call; `{fromDisk:true}` needs no browser), `reticle_domain` (learn the app + gaps), `reticle_baseline {action:"diff"}`, `reticle_project` (run history).
|
|
117
117
|
|
|
118
|
-
**Reach past core when…** you need to record/replay a journey (`reticle_record {action:"start"}/stop`, `reticle_replay`), persist a self-healing golden flow (`reticle_flow_save*` / `reticle_flow_replay` / `reticle_flow_heal`), compile annotations (`reticle_annotate`), explore autonomously (`reticle_explore` lists controls; `
|
|
118
|
+
**Reach past core when…** you need to record/replay a journey (`reticle_record {action:"start"}/stop`, `reticle_replay`), persist a self-healing golden flow (`reticle_flow_save*` / `reticle_flow_replay` / `reticle_flow_heal`), compile annotations (`reticle_annotate`), explore autonomously (`reticle_explore` lists controls; `reticle_verify {action:"crawl"}` clicks them all and reports anomalies, and is **destructive**), reveal a virtualized off-screen row (`reticle_scroll_to`, for when `reticle_query` finds nothing because a windowed list hasn't rendered it yet), visual-check (`reticle_screenshot` / `reticle_visual_diff`, pinned with `reticle_viewport` for reproducible baselines), test error/edge states by stubbing the network (`reticle_network_mock`: 500 / offline / delay, driven mode), control time for toasts/debounces/auto-dismiss (`reticle_clock { freeze | advanceMs | reset }`), or work with a human (`reticle_session {action:"end"}` / `reticle_session {action:"resume"}` / `reticle_session {action:"messages"}`, and **`reticle_session {action:"review"}`** to drain + fix the bugs the human flagged from the panel).
|
|
119
119
|
|
|
120
120
|
## flows vs baselines vs project.json (the persistence layers)
|
|
121
121
|
|
|
@@ -160,4 +160,4 @@ Recent additions, each of which answers a question agents were previously asking
|
|
|
160
160
|
- **Cap broad reads.** `reticle_query` takes `limit` (caps descriptors; reports `total`/`truncated`) and `count_only` (just the match count). `reticle_network` / `reticle_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.
|
|
161
161
|
- **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.
|
|
162
162
|
- **Predicate schema is not bloated.** The recursive predicate DSL used by `reticle_assert` / `reticle_wait_for` / `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.
|
|
163
|
-
- **One tool surface.** Reticle advertises the core set directly (sessions/navigate/snapshot/query/act/act_and_wait/observe/network/console/wait_for/assert/state/inspect/feedback/session: the whole detect loop, plus the file-pointer, the feedback channel and the handback) PLUS two meta-tools that keep every other tool one call away: `reticle_tools` (discover; no args lists every tool name + summary, `names:[…]` loads full params on demand) and `reticle_run({ tool, args })` (invoke any tool by name; a top-level `sessionId` is forwarded to the target, so you need not nest it in `args`). So to record/replay/verify a flow, call `reticle_run({ tool:"
|
|
163
|
+
- **One tool surface.** Reticle advertises the core set directly (sessions/navigate/snapshot/query/act/act_and_wait/observe/network/console/wait_for/assert/state/inspect/feedback/session: the whole detect loop, plus the file-pointer, the feedback channel and the handback) PLUS two meta-tools that keep every other tool one call away: `reticle_tools` (discover; no args lists every tool name + summary, `names:[…]` loads full params on demand) and `reticle_run({ tool, args })` (invoke any tool by name; a top-level `sessionId` is forwarded to the target, so you need not nest it in `args`). So to record/replay/verify a flow, call `reticle_run({ tool:"reticle_verify", args:{ action:"flows" }, sessionId })` (or `reticle_tools` first to see params). There is nothing to pick. `RETICLE_ADVERTISE_ALL_TOOLS=1` (read by the daemon at startup, so restart it) advertises everything with output schemas: a verification switch for suites, not a way to run agents. **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.
|