@reticlehq/server 2.5.0 → 2.6.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 +27 -10
- package/dist/bridge/bridge.d.ts +12 -0
- package/dist/bridge/bridge.js +32 -2
- package/dist/bridge/bridge.test-harness.d.ts +8 -0
- package/dist/bridge/bridge.test-harness.js +9 -1
- package/dist/cli/cli-doctor.d.ts +0 -5
- package/dist/cli/cli-doctor.js +71 -3
- package/dist/cli/cli-parse-feedback.d.ts +31 -0
- package/dist/cli/cli-parse-feedback.js +56 -0
- package/dist/cli/cli-parse.d.ts +5 -9
- package/dist/cli/cli-parse.js +7 -23
- package/dist/cli/doctor-daemon-line.d.ts +37 -0
- package/dist/cli/doctor-daemon-line.js +35 -0
- package/dist/cli/port-holder.d.ts +43 -0
- package/dist/cli/port-holder.js +99 -0
- package/dist/cli.js +131 -11
- package/dist/daemon/daemon-resilience.d.ts +24 -0
- package/dist/daemon/daemon-resilience.js +105 -17
- package/dist/daemon/daemon.js +26 -1
- package/dist/daemon/heartbeat.d.ts +115 -0
- package/dist/daemon/heartbeat.js +141 -0
- package/dist/daemon/idle-shutdown.d.ts +13 -5
- package/dist/daemon/idle-shutdown.js +14 -6
- package/dist/daemon/port-presence.d.ts +76 -0
- package/dist/daemon/port-presence.js +93 -0
- package/dist/daemon/state-dir.d.ts +7 -0
- package/dist/daemon/state-dir.js +59 -0
- package/dist/daemon/unclean-predecessor.d.ts +23 -0
- package/dist/daemon/unclean-predecessor.js +27 -0
- package/dist/events/predicate-eval.d.ts +10 -0
- package/dist/events/predicate-eval.js +24 -4
- package/dist/events/predicate-parse.d.ts +23 -0
- package/dist/events/predicate-parse.js +54 -0
- package/dist/events/predicate.d.ts +5 -0
- package/dist/events/predicate.js +17 -3
- package/dist/events/ring-buffer.js +1 -16
- package/dist/flows/flow-replay.d.ts +1 -1
- package/dist/flows/flow-replay.js +19 -2
- package/dist/flows/flow-step-runners.js +2 -3
- package/dist/flows/flow-tools.js +10 -14
- package/dist/flows/role-drift-reason.d.ts +20 -0
- package/dist/flows/role-drift-reason.js +39 -0
- package/dist/flows/suite-flakes.d.ts +33 -0
- package/dist/flows/suite-flakes.js +32 -0
- package/dist/honesty/blind-spots.d.ts +19 -0
- package/dist/honesty/blind-spots.js +33 -0
- package/dist/honesty/verified.d.ts +10 -1
- package/dist/honesty/verified.js +16 -0
- package/dist/http-server.js +9 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +10 -1
- package/dist/init/agent-rules.d.ts +18 -0
- package/dist/init/agent-rules.js +22 -4
- package/dist/init/cra.d.ts +13 -0
- package/dist/init/cra.js +16 -0
- package/dist/init/detect.d.ts +6 -0
- package/dist/init/detect.js +8 -0
- package/dist/init/mcp-clients.d.ts +81 -0
- package/dist/init/mcp-clients.js +248 -0
- package/dist/init/node-io.js +8 -2
- package/dist/init/plan-framework.js +36 -2
- package/dist/init/plan.d.ts +14 -0
- package/dist/init/plan.js +73 -9
- package/dist/init/run.d.ts +18 -1
- package/dist/init/run.js +133 -16
- package/dist/init/windows-quote.d.ts +38 -0
- package/dist/init/windows-quote.js +70 -0
- package/dist/mcp/mcp-outage.d.ts +1 -0
- package/dist/mcp/mcp-outage.js +8 -1
- package/dist/mcp/mcp-proxy.d.ts +11 -1
- package/dist/mcp/mcp-proxy.js +30 -7
- package/dist/mcp/mcp.js +29 -2
- package/dist/pool/browser-pool.d.ts +9 -0
- package/dist/pool/browser-pool.js +38 -9
- package/dist/session/attachment-history.d.ts +46 -0
- package/dist/session/attachment-history.js +65 -0
- package/dist/session/no-session-diagnosis.d.ts +7 -0
- package/dist/session/no-session-diagnosis.js +53 -3
- package/dist/session/no-session-watch.d.ts +3 -1
- package/dist/session/no-session-watch.js +10 -2
- package/dist/session/pending-commands.js +1 -0
- package/dist/session/session-info.d.ts +17 -1
- package/dist/session/session-info.js +5 -1
- package/dist/session/session-manager.d.ts +3 -0
- package/dist/session/session-manager.js +72 -3
- package/dist/session/session.d.ts +1 -0
- package/dist/session/session.js +8 -0
- package/dist/telemetry/crash-cause.d.ts +47 -0
- package/dist/telemetry/crash-cause.js +104 -0
- package/dist/telemetry/daemon-telemetry.d.ts +25 -7
- package/dist/telemetry/daemon-telemetry.js +28 -13
- package/dist/telemetry/error-class.d.ts +26 -0
- package/dist/telemetry/error-class.js +52 -0
- package/dist/telemetry/feedback-cli.d.ts +7 -2
- package/dist/telemetry/feedback-cli.js +49 -5
- package/dist/telemetry/feedback-context.d.ts +1 -6
- package/dist/telemetry/feedback-context.js +64 -4
- package/dist/telemetry/git-facts.d.ts +7 -0
- package/dist/telemetry/git-facts.js +4 -4
- package/dist/telemetry/project-profile.js +2 -1
- package/dist/telemetry/session-metrics.d.ts +45 -3
- package/dist/telemetry/session-metrics.js +247 -9
- package/dist/telemetry/telemetry.d.ts +19 -1
- package/dist/telemetry/telemetry.js +31 -8
- package/dist/tools/act-tools.js +33 -6
- package/dist/tools/assert-verdict.d.ts +7 -1
- package/dist/tools/assert-verdict.js +15 -3
- package/dist/tools/browser-tools.js +8 -2
- package/dist/tools/contract-tools.d.ts +0 -5
- package/dist/tools/contract-tools.js +26 -0
- package/dist/tools/dynamic-tools.js +6 -1
- package/dist/tools/error-recovery.d.ts +14 -0
- package/dist/tools/error-recovery.js +83 -7
- package/dist/tools/feedback-invite.d.ts +48 -0
- package/dist/tools/feedback-invite.js +71 -0
- package/dist/tools/invoke-tool.d.ts +0 -6
- package/dist/tools/invoke-tool.js +69 -1
- package/dist/tools/navigate-arrival.d.ts +26 -0
- package/dist/tools/navigate-arrival.js +60 -0
- package/dist/tools/navigate-result.d.ts +15 -1
- package/dist/tools/navigate-result.js +17 -10
- package/dist/tools/observe-tools.js +22 -4
- package/dist/tools/real-input-attempt.js +10 -5
- package/dist/tools/tool-kit.d.ts +8 -0
- package/dist/tools/tool-surface.js +13 -0
- package/docs/README.md +55 -0
- package/docs/agent-cheatsheet.md +2 -2
- package/docs/benchmarks.md +1 -1
- package/docs/fixtures-dispatch-receiver.yml +76 -0
- package/docs/fixtures.md +17 -0
- package/docs/gate-plan.md +98 -0
- package/docs/gates.md +117 -0
- package/docs/local-registry.md +1 -1
- package/docs/matrix/2.5.0/claude-code-machine.json +19 -0
- package/docs/matrix/2.5.0/codex-machine.json +20 -0
- package/docs/matrix/2.5.0/cursor-machine.json +22 -0
- package/docs/matrix/2.5.0/gemini-machine.json +22 -0
- package/docs/matrix/2.5.0/opencode-machine.json +22 -0
- package/docs/matrix/2.5.0/vscode-machine.json +22 -0
- package/docs/matrix/2.5.0/windsurf-machine.json +22 -0
- package/docs/matrix/MATRIX.md +26 -0
- package/docs/matrix/README.md +59 -0
- package/docs/mint.json +1 -1
- package/docs/system-map.md +152 -0
- package/docs/telemetry-contract.md +2 -2
- package/docs/telemetry.md +7 -0
- package/package.json +2 -2
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { healthEnvelope } from '../session/session-health.js';
|
|
2
|
-
import { TelemetryActor, TelemetryEventKind } from '@reticlehq/core';
|
|
2
|
+
import { TelemetryActor, TelemetryEventKind, TRANSPORT_LIMITS, } from '@reticlehq/core';
|
|
3
3
|
import { getSessionMetrics } from '../telemetry/session-metrics.js';
|
|
4
4
|
import { getTelemetry } from '../telemetry/telemetry.js';
|
|
5
5
|
import { takeUpdateNudge } from '../update/update-nudge.js';
|
|
@@ -12,6 +12,7 @@ import { ReticleTool } from './tool-names.js';
|
|
|
12
12
|
import { takeFeedbackPrompt } from './feedback-tools.js';
|
|
13
13
|
import { takeFeedbackUndelivered } from '../telemetry/feedback-delivery.js';
|
|
14
14
|
import { span } from '../trace.js';
|
|
15
|
+
import { frictionOf, inviteFor } from './feedback-invite.js';
|
|
15
16
|
/**
|
|
16
17
|
* The live-session tools whose result MUST carry the
|
|
17
18
|
* session-health envelope. Owned in ONE place — not retrofitted per handler — so a throttled tab
|
|
@@ -138,6 +139,27 @@ function reportBugsFound(toolName, result) {
|
|
|
138
139
|
* object that did not already include `session` — splices the health envelope on. Idempotent
|
|
139
140
|
* (handlers that already add health are left untouched) and never alters non-object results.
|
|
140
141
|
*/
|
|
142
|
+
/**
|
|
143
|
+
* The name of the first argument whose string value exceeds the transport bound, if any.
|
|
144
|
+
*
|
|
145
|
+
* One level of nesting is searched because that is where callers put payloads — `reticle_act`'s
|
|
146
|
+
* `args.value`, and the fuzz's own `huge-string` shape. Deeper recursion would cost more than it
|
|
147
|
+
* buys: the failure is a single huge string, not a deeply nested structure of small ones.
|
|
148
|
+
*/
|
|
149
|
+
function firstOversizedArg(args) {
|
|
150
|
+
const tooLong = (value) => 'string' === typeof value && value.length > TRANSPORT_LIMITS.MAX_STRING_LENGTH;
|
|
151
|
+
for (const [key, value] of Object.entries(args)) {
|
|
152
|
+
if (tooLong(value))
|
|
153
|
+
return key;
|
|
154
|
+
if ('object' === typeof value && null !== value && !Array.isArray(value)) {
|
|
155
|
+
for (const [inner, nested] of Object.entries(value)) {
|
|
156
|
+
if (tooLong(nested))
|
|
157
|
+
return `${key}.${inner}`;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
return undefined;
|
|
162
|
+
}
|
|
141
163
|
export async function runTool(tool, deps, args) {
|
|
142
164
|
// Both dispatch paths (MCP + programmatic) pass through here — the one place "which tool is mostly
|
|
143
165
|
// used" can be counted. This used to EMIT an event per call; it now increments an in-process counter
|
|
@@ -150,6 +172,28 @@ export async function runTool(tool, deps, args) {
|
|
|
150
172
|
// closure carries this call's own identity, which is also what makes peak-concurrency measurable.
|
|
151
173
|
// A daemon that has served even one tool call is doing a job for somebody; see daemon-usefulness.
|
|
152
174
|
noteToolCall();
|
|
175
|
+
// An oversized argument is refused BEFORE the handler deserialises it.
|
|
176
|
+
//
|
|
177
|
+
// tool-fuzz failed CI on the invariant that matters most — `every tool answers every hostile call`
|
|
178
|
+
// — for a ~100KB string. Seen twice with different tools; the earlier one came back
|
|
179
|
+
// `-32001 sse_aborted` with the tool surface dropping 48→17, which is a RESTARTED daemon. The
|
|
180
|
+
// shape is: oversized argument → the daemon dies → the proxy answers the in-flight call with
|
|
181
|
+
// transport loss, and the next assertion is talking to a different daemon.
|
|
182
|
+
//
|
|
183
|
+
// `TRANSPORT_LIMITS.MAX_STRING_LENGTH` already bounds what crosses the bridge; a tool argument
|
|
184
|
+
// arrives over MCP stdio and never met it. Here, because this is the one place every invocation
|
|
185
|
+
// routes through, so a tool added later inherits the bound instead of having to remember it.
|
|
186
|
+
//
|
|
187
|
+
// A refusal is a normal answer. An UNANSWERED call is a hung agent, and that is the failure this
|
|
188
|
+
// prevents: the agent learns its argument was too big, which is something it can fix.
|
|
189
|
+
const oversized = firstOversizedArg(args);
|
|
190
|
+
if (oversized !== undefined) {
|
|
191
|
+
return {
|
|
192
|
+
error: `argument '${oversized}' is larger than ${String(TRANSPORT_LIMITS.MAX_STRING_LENGTH)} ` +
|
|
193
|
+
'characters, which is the most Reticle moves in one call. Nothing ran. Send a smaller ' +
|
|
194
|
+
'value — a selector or an id rather than a document, or a slice of the text you need.',
|
|
195
|
+
};
|
|
196
|
+
}
|
|
153
197
|
// An ACTION is what gets abandoned. Counted here, against verifications, so "the agent drove the
|
|
154
198
|
// page and then wandered off" becomes a number instead of an impression.
|
|
155
199
|
if (ACTION_TOOLS.has(tool.name))
|
|
@@ -246,5 +290,29 @@ export async function runTool(tool, deps, args) {
|
|
|
246
290
|
const warning = resolved.ageWarning();
|
|
247
291
|
if (warning !== undefined)
|
|
248
292
|
envelope['session_age_warning'] = warning;
|
|
293
|
+
// Ask for a verdict when the agent has driven the page and not asked for one. 137 of the 140
|
|
294
|
+
// verdict-less sessions measured over 2026-08-10/11 never called a verdict-producing tool ONCE;
|
|
295
|
+
// the counter behind this already existed and was reported only to us. One-shot per abandoned
|
|
296
|
+
// run, same discipline as the pool lease — a hint on every call is noise that gets tuned out.
|
|
297
|
+
const unverified = getSessionMetrics().takeUnverifiedNudge();
|
|
298
|
+
if (unverified !== undefined)
|
|
299
|
+
envelope['verify_next'] = unverified;
|
|
300
|
+
// Invite feedback at the moment of friction, worded for what just happened. Feedback produced
|
|
301
|
+
// ~11 of this release's fixes from 14 reports; the handshake instruction is what got those 14,
|
|
302
|
+
// and this closes the gap it leaves — the agent mid-problem, not thinking about feedback tooling.
|
|
303
|
+
// Counted so `feedback_submitted / feedbackPrompted` can tell us whether it works at all.
|
|
304
|
+
const friction = frictionOf({
|
|
305
|
+
unknownTool: false,
|
|
306
|
+
verifiedUnknown: 'unknown' === result.verified,
|
|
307
|
+
repeatRun: getSessionMetrics().currentRun,
|
|
308
|
+
errored: 'error' in result,
|
|
309
|
+
// A verdict is progress. Without this, three successful act_and_wait calls in a row — the exact
|
|
310
|
+
// loop this release exists to encourage — get told they are stuck.
|
|
311
|
+
producedVerdict: 'verified' in result,
|
|
312
|
+
});
|
|
313
|
+
if (friction !== undefined) {
|
|
314
|
+
envelope['feedback_invite'] = inviteFor(friction);
|
|
315
|
+
getSessionMetrics().recordFeedbackPrompt();
|
|
316
|
+
}
|
|
249
317
|
return Object.keys(envelope).length > 0 ? { ...result, ...envelope } : result;
|
|
250
318
|
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wait, briefly, for the SDK to reconnect after a navigation — so `confirmed` can mean something.
|
|
3
|
+
*
|
|
4
|
+
* `window.location.assign(url)` destroys the SDK that would report on the new document, so the
|
|
5
|
+
* navigation itself cannot be confirmed from inside the page. But the SDK reconnects TO THE DAEMON,
|
|
6
|
+
* which makes the daemon the only party that can see arrival happen. Before this, it did not look,
|
|
7
|
+
* and told the agent to poll `reticle_sessions` instead — a tool call on every navigation, on the
|
|
8
|
+
* least reliable tool we ship.
|
|
9
|
+
*
|
|
10
|
+
* Bounded and best-effort by construction: a navigation to a page that is not instrumented, or not
|
|
11
|
+
* there at all, must still return promptly with `confirmed:false` rather than hanging. The clock and
|
|
12
|
+
* the sleep are injected so this is testable without waiting on a real one.
|
|
13
|
+
*/
|
|
14
|
+
import type { SessionManager } from '../session/session-manager.js';
|
|
15
|
+
import type { NavigateArrival } from './navigate-result.js';
|
|
16
|
+
/** Long enough for a dev server to serve a page and the SDK to dial back; short enough to not hang. */
|
|
17
|
+
export declare const ARRIVAL_TIMEOUT_MS = 5000;
|
|
18
|
+
export interface ArrivalClock {
|
|
19
|
+
now: () => number;
|
|
20
|
+
sleep: (ms: number) => Promise<void>;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Poll until a session is present at `target`, or the budget runs out. Returns `null` on timeout —
|
|
24
|
+
* never throws, because a failure to confirm is a legitimate answer, not an error.
|
|
25
|
+
*/
|
|
26
|
+
export declare function awaitArrival(sessions: SessionManager, target: string, timeoutMs?: number, clock?: ArrivalClock): Promise<NavigateArrival | null>;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wait, briefly, for the SDK to reconnect after a navigation — so `confirmed` can mean something.
|
|
3
|
+
*
|
|
4
|
+
* `window.location.assign(url)` destroys the SDK that would report on the new document, so the
|
|
5
|
+
* navigation itself cannot be confirmed from inside the page. But the SDK reconnects TO THE DAEMON,
|
|
6
|
+
* which makes the daemon the only party that can see arrival happen. Before this, it did not look,
|
|
7
|
+
* and told the agent to poll `reticle_sessions` instead — a tool call on every navigation, on the
|
|
8
|
+
* least reliable tool we ship.
|
|
9
|
+
*
|
|
10
|
+
* Bounded and best-effort by construction: a navigation to a page that is not instrumented, or not
|
|
11
|
+
* there at all, must still return promptly with `confirmed:false` rather than hanging. The clock and
|
|
12
|
+
* the sleep are injected so this is testable without waiting on a real one.
|
|
13
|
+
*/
|
|
14
|
+
/** Long enough for a dev server to serve a page and the SDK to dial back; short enough to not hang. */
|
|
15
|
+
export const ARRIVAL_TIMEOUT_MS = 5_000;
|
|
16
|
+
const POLL_MS = 100;
|
|
17
|
+
/**
|
|
18
|
+
* Compare only origin + pathname.
|
|
19
|
+
*
|
|
20
|
+
* The app is entitled to add or rewrite the query and hash on arrival — a redirect to
|
|
21
|
+
* `?redirect=%2F`, a router normalising a trailing slash, an auth guard appending a reason. Matching
|
|
22
|
+
* the whole URL string would report `confirmed:false` for a navigation that plainly succeeded, which
|
|
23
|
+
* is the same lie in the opposite direction.
|
|
24
|
+
*/
|
|
25
|
+
function samePage(a, b) {
|
|
26
|
+
try {
|
|
27
|
+
const x = new URL(a);
|
|
28
|
+
const y = new URL(b);
|
|
29
|
+
return x.origin === y.origin && x.pathname.replace(/\/$/, '') === y.pathname.replace(/\/$/, '');
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
function findArrival(sessions, target) {
|
|
36
|
+
for (const s of sessions.all()) {
|
|
37
|
+
if (samePage(s.url, target))
|
|
38
|
+
return { sessionId: s.id };
|
|
39
|
+
}
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
const REAL_CLOCK = {
|
|
43
|
+
now: () => Date.now(),
|
|
44
|
+
sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
|
|
45
|
+
};
|
|
46
|
+
/**
|
|
47
|
+
* Poll until a session is present at `target`, or the budget runs out. Returns `null` on timeout —
|
|
48
|
+
* never throws, because a failure to confirm is a legitimate answer, not an error.
|
|
49
|
+
*/
|
|
50
|
+
export async function awaitArrival(sessions, target, timeoutMs = ARRIVAL_TIMEOUT_MS, clock = REAL_CLOCK) {
|
|
51
|
+
const deadline = clock.now() + timeoutMs;
|
|
52
|
+
for (;;) {
|
|
53
|
+
const found = findArrival(sessions, target);
|
|
54
|
+
if (found !== null)
|
|
55
|
+
return found;
|
|
56
|
+
if (clock.now() >= deadline)
|
|
57
|
+
return null;
|
|
58
|
+
await clock.sleep(POLL_MS);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -10,8 +10,22 @@
|
|
|
10
10
|
* `reticle_act` already draws this line — `dispatched` (sent) versus `settled` (a frame flushed).
|
|
11
11
|
* This gives navigate the same honesty without changing what it does.
|
|
12
12
|
*/
|
|
13
|
+
/** The session the SDK reconnected as after the page moved, when one was observed in the window. */
|
|
14
|
+
export interface NavigateArrival {
|
|
15
|
+
sessionId: string;
|
|
16
|
+
}
|
|
13
17
|
export declare function navigateResult(result: {
|
|
14
18
|
ok?: unknown;
|
|
15
19
|
url?: unknown;
|
|
16
20
|
reason?: unknown;
|
|
17
|
-
}
|
|
21
|
+
},
|
|
22
|
+
/**
|
|
23
|
+
* Arrival, if the daemon saw the SDK come back. `null` keeps the original honest envelope.
|
|
24
|
+
*
|
|
25
|
+
* Measured on three fixtures: `confirmed` was `false` on every navigation on every app, because
|
|
26
|
+
* nothing ever set it. A boolean that never varies teaches an agent nothing, and an agent that
|
|
27
|
+
* learns to ignore it will keep ignoring it when it starts meaning something. The daemon is the
|
|
28
|
+
* one place that CAN answer this — the SDK reconnects to it — so making it answer removes a
|
|
29
|
+
* `reticle_sessions` poll from every single navigation.
|
|
30
|
+
*/
|
|
31
|
+
arrival?: NavigateArrival | null): Record<string, unknown>;
|
|
@@ -1,16 +1,14 @@
|
|
|
1
|
+
export function navigateResult(result,
|
|
1
2
|
/**
|
|
2
|
-
*
|
|
3
|
+
* Arrival, if the daemon saw the SDK come back. `null` keeps the original honest envelope.
|
|
3
4
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* `reticle_act` already draws this line — `dispatched` (sent) versus `settled` (a frame flushed).
|
|
11
|
-
* This gives navigate the same honesty without changing what it does.
|
|
5
|
+
* Measured on three fixtures: `confirmed` was `false` on every navigation on every app, because
|
|
6
|
+
* nothing ever set it. A boolean that never varies teaches an agent nothing, and an agent that
|
|
7
|
+
* learns to ignore it will keep ignoring it when it starts meaning something. The daemon is the
|
|
8
|
+
* one place that CAN answer this — the SDK reconnects to it — so making it answer removes a
|
|
9
|
+
* `reticle_sessions` poll from every single navigation.
|
|
12
10
|
*/
|
|
13
|
-
|
|
11
|
+
arrival = null) {
|
|
14
12
|
const ok = true === result.ok;
|
|
15
13
|
const base = {
|
|
16
14
|
ok,
|
|
@@ -20,6 +18,15 @@ export function navigateResult(result) {
|
|
|
20
18
|
// A refusal is conclusive: the page never moved, so there is nothing unconfirmed to report.
|
|
21
19
|
if (!ok)
|
|
22
20
|
return base;
|
|
21
|
+
if (arrival !== null) {
|
|
22
|
+
return {
|
|
23
|
+
...base,
|
|
24
|
+
confirmed: true,
|
|
25
|
+
// The SDK that reconnects after a navigation is a NEW session with a new id. Returning it here
|
|
26
|
+
// is the difference between a confirmation and a confirmation the agent can act on.
|
|
27
|
+
sessionId: arrival.sessionId,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
23
30
|
return {
|
|
24
31
|
...base,
|
|
25
32
|
confirmed: false,
|
|
@@ -10,6 +10,7 @@ import { ReticleTool } from './tool-names.js';
|
|
|
10
10
|
import { buildReactionReport } from '../events/reaction.js';
|
|
11
11
|
import { findContradictions } from '../events/contradictions.js';
|
|
12
12
|
import { evaluatePredicate, waitForPredicate, PredicateSchema } from '../events/predicate.js';
|
|
13
|
+
import { parsePredicate } from '../events/predicate-parse.js';
|
|
13
14
|
import { matchNet, matchConsole, isConsoleEvent, eventMatchesFilters, netEmptyHint, consoleEmptyHint, reconcileNet, projectNetCall, projectConsoleLog, } from '../events/event-filters.js';
|
|
14
15
|
import { applyEventBudget, costHint, withSizeCost, DEFAULT_QUERY_LIMIT, } from '../session/output-budget.js';
|
|
15
16
|
import { healthEnvelope, bufferEnvelope } from '../session/session-health.js';
|
|
@@ -209,6 +210,10 @@ export const OBSERVE_TOOLS = [
|
|
|
209
210
|
pass: z.boolean(),
|
|
210
211
|
evidence: z.unknown().optional(),
|
|
211
212
|
failureReason: z.string().optional(),
|
|
213
|
+
observationLost: z
|
|
214
|
+
.boolean()
|
|
215
|
+
.optional()
|
|
216
|
+
.describe('The tab disconnected mid-wait, so this was never observed — the verdict is UNKNOWN, not a failure of the app.'),
|
|
212
217
|
session: z
|
|
213
218
|
.object({ lastSeenMs: z.number(), throttled: z.boolean(), focused: z.boolean() })
|
|
214
219
|
.optional(),
|
|
@@ -233,7 +238,7 @@ export const OBSERVE_TOOLS = [
|
|
|
233
238
|
handler: async (deps, args) => {
|
|
234
239
|
const session = deps.sessions.resolve(asString(args['sessionId']));
|
|
235
240
|
// `until` is act_and_wait's name for this — see alias-args.ts.
|
|
236
|
-
const predicate =
|
|
241
|
+
const predicate = parsePredicate(aliasParam(args, 'predicate', ['until'])['predicate']);
|
|
237
242
|
// Honesty: explicit since wins; else default to the last act's cursor; else the whole buffer.
|
|
238
243
|
const since = asNumber(args['since']) ?? session.lastAct.cursor() ?? 0;
|
|
239
244
|
const verdict = await waitForPredicate(session, predicate, asNumber(args['timeout_ms']) ?? DEFAULT_ASSERT_TIMEOUT_MS, since);
|
|
@@ -252,7 +257,16 @@ export const OBSERVE_TOOLS = [
|
|
|
252
257
|
example: { predicate: { kind: PredicateKind.SIGNAL, name: 'todos:loaded' } },
|
|
253
258
|
description: 'Evaluate a predicate (optionally waiting up to timeout_ms). Returns { pass, evidence, failureReason? }. The end of every verify loop. Prefer a { signal } or { net } consequence over { element }/{ text } presence — a passing presence-only assertion returns `advice` because a wrong/healed element can fake it. By default it only counts events since your last act, so a stale buffered signal can never fake a pass; pass `since` (an observe/act cursor) to set the window explicitly.',
|
|
254
259
|
inputSchema: {
|
|
255
|
-
predicate: PredicateSchema.optional().describe(
|
|
260
|
+
predicate: PredicateSchema.optional().describe(
|
|
261
|
+
// Every kind, each with the field that carries its argument. Five of the nine were
|
|
262
|
+
// undocumented here, including `route` — and "did submitting the login form navigate away"
|
|
263
|
+
// is the most common thing an agent wants to assert. A field report reached us from an agent
|
|
264
|
+
// that guessed `urlContains` on route (net's spelling) and got unrecognized_keys.
|
|
265
|
+
'Predicate to evaluate. Kinds: { signal, name } { net, urlContains|method|status|count } ' +
|
|
266
|
+
'{ state, path|equals } { route, pathname (exact) | contains (path+query+hash) } ' +
|
|
267
|
+
'{ element, testid|role|text } { text } { console, level|absent } { animation, name } ' +
|
|
268
|
+
'{ settled } — combine with { allOf | anyOf | not }. Prefer a signal/net/state consequence ' +
|
|
269
|
+
'over element/text presence.'),
|
|
256
270
|
until: PredicateSchema.optional().describe("Alias for `predicate` (act_and_wait's name)."),
|
|
257
271
|
timeout_ms: z
|
|
258
272
|
.number()
|
|
@@ -268,6 +282,10 @@ export const OBSERVE_TOOLS = [
|
|
|
268
282
|
pass: z.boolean(),
|
|
269
283
|
evidence: z.unknown().optional(),
|
|
270
284
|
failureReason: z.string().optional(),
|
|
285
|
+
observationLost: z
|
|
286
|
+
.boolean()
|
|
287
|
+
.optional()
|
|
288
|
+
.describe('The tab disconnected mid-wait, so this was never observed — the verdict is UNKNOWN, not a failure of the app.'),
|
|
271
289
|
advice: z
|
|
272
290
|
.string()
|
|
273
291
|
.optional()
|
|
@@ -304,7 +322,7 @@ export const OBSERVE_TOOLS = [
|
|
|
304
322
|
handler: async (deps, args) => {
|
|
305
323
|
const session = deps.sessions.resolve(asString(args['sessionId']));
|
|
306
324
|
// `until` is act_and_wait's name for this — see alias-args.ts.
|
|
307
|
-
const predicate =
|
|
325
|
+
const predicate = parsePredicate(aliasParam(args, 'predicate', ['until'])['predicate']);
|
|
308
326
|
const timeout = asNumber(args['timeout_ms']) ?? 0;
|
|
309
327
|
// Honesty: explicit since wins; else default to the last act's cursor; else the whole buffer.
|
|
310
328
|
const since = asNumber(args['since']) ?? session.lastAct.cursor() ?? 0;
|
|
@@ -323,7 +341,7 @@ export const OBSERVE_TOOLS = [
|
|
|
323
341
|
: assertsDerivedIpcStatus(predicate)
|
|
324
342
|
? { advice: DERIVED_IPC_STATUS_ADVICE }
|
|
325
343
|
: {};
|
|
326
|
-
const { decision, contradictions, coverage } = await assertVerdict(session, predicate, verdict.pass, since, verdict.inconclusive);
|
|
344
|
+
const { decision, contradictions, coverage } = await assertVerdict(session, predicate, verdict.pass, since, verdict.inconclusive, verdict.observationLost);
|
|
327
345
|
return withControl(session, {
|
|
328
346
|
...decision,
|
|
329
347
|
...verdict,
|
|
@@ -27,17 +27,22 @@ function synthetic(reason) {
|
|
|
27
27
|
*/
|
|
28
28
|
export async function tryRealInput(deps, session, ref, action, args) {
|
|
29
29
|
const provider = deps.realInput;
|
|
30
|
-
|
|
31
|
-
|
|
30
|
+
const inner = asRecord(args['args']);
|
|
31
|
+
const askedForNative = true === inner[NATIVE_INPUT_ARG];
|
|
32
|
+
if (provider === undefined) {
|
|
33
|
+
// Silent by default: with no provider EVERY action is synthetic, and a reason on all of them is
|
|
34
|
+
// noise on the most-used tool in the product. But an agent that passed native:true asked a
|
|
35
|
+
// question and got the opposite answer — reported from the field as a silent downgrade that cost
|
|
36
|
+
// real debugging time, because the tool description promises a reason is "never silent".
|
|
37
|
+
return askedForNative ? synthetic(InputModeReason.NOT_CONFIGURED) : synthetic();
|
|
38
|
+
}
|
|
32
39
|
if (!isPointerAction(action))
|
|
33
40
|
return synthetic(InputModeReason.NOT_POINTER); // fill/type stay synthetic
|
|
34
|
-
const inner = asRecord(args['args']);
|
|
35
41
|
// "Don't click, run the code": a click/dblclick runs the occlusion-honest SYNTHETIC path by default
|
|
36
42
|
// even with a provider configured — no coordinate gesture to be intercepted by the HUD or missed
|
|
37
43
|
// off-screen. Opt into a trusted native click with args.native:true (file pickers, clipboard,
|
|
38
44
|
// isTrusted-gated handlers). hover/drag genuinely need native pointer state, so they stay real.
|
|
39
|
-
if ((action === ActionType.CLICK || action === ActionType.DBLCLICK) &&
|
|
40
|
-
inner[NATIVE_INPUT_ARG] !== true) {
|
|
45
|
+
if ((action === ActionType.CLICK || action === ActionType.DBLCLICK) && !askedForNative) {
|
|
41
46
|
return synthetic(InputModeReason.SYNTHETIC_CLICK_PREFERRED);
|
|
42
47
|
}
|
|
43
48
|
if (!(await provider.isAvailableFor(session.url)))
|
package/dist/tools/tool-kit.d.ts
CHANGED
|
@@ -33,6 +33,14 @@ export interface ToolDeps {
|
|
|
33
33
|
fs: FileSystemPort;
|
|
34
34
|
/** absolute.reticle path (index.ts computes cwd/.reticle). */
|
|
35
35
|
reticleRoot: string;
|
|
36
|
+
/**
|
|
37
|
+
* This daemon's OWN project id, derived from the directory it was started in.
|
|
38
|
+
*
|
|
39
|
+
* Optional because a daemon above an uninitialised directory has none, and because every existing
|
|
40
|
+
* test construction of ToolDeps predates it. Absence means "cannot tell", which must never be
|
|
41
|
+
* treated as a mismatch.
|
|
42
|
+
*/
|
|
43
|
+
projectId?: string;
|
|
36
44
|
/** injected clock for the contract's generatedAt stamp. */
|
|
37
45
|
now: () => number;
|
|
38
46
|
/**
|
|
@@ -98,6 +98,19 @@ export const CORE_TOOL_NAMES = new Set([
|
|
|
98
98
|
ReticleTool.QUERY,
|
|
99
99
|
ReticleTool.ACT,
|
|
100
100
|
ReticleTool.ACT_AND_WAIT,
|
|
101
|
+
// ACT_SEQUENCE is here because its absence was measurably CAUSING the biggest loop in the data.
|
|
102
|
+
//
|
|
103
|
+
// Measured 2026-08-10/11 (non-CI): `reticle_act` 319 calls, `reticle_act_sequence` **7**. `act`
|
|
104
|
+
// also leads the repeat table at 110 consecutive-repeat runs, and inside those sessions the
|
|
105
|
+
// repeated calls are 98 clicks and 21 fills — a login form driven one round trip at a time, which
|
|
106
|
+
// is exactly the antipattern SKILL.md warns about and exactly what this tool exists to collapse.
|
|
107
|
+
//
|
|
108
|
+
// The repeats are NOT retries: looping sessions have a lower error rate (0.051) than non-looping
|
|
109
|
+
// ones (0.078). The calls succeed and get repeated, because the batching tool was reachable only
|
|
110
|
+
// through `reticle_run` — so an agent had to already know it existed to use it, and 7-vs-319 says
|
|
111
|
+
// essentially nobody did. A tool an agent must already know about is a tool that never gets
|
|
112
|
+
// called; the same argument that put INSPECT and FEEDBACK here.
|
|
113
|
+
ReticleTool.ACT_SEQUENCE,
|
|
101
114
|
ReticleTool.OBSERVE,
|
|
102
115
|
ReticleTool.NETWORK,
|
|
103
116
|
ReticleTool.CONSOLE,
|
package/docs/README.md
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# Docs index
|
|
2
|
+
|
|
3
|
+
Two audiences share this directory, which is why it looked like twenty-five unsorted files. Every page states its own audience in the first blockquote; this index is the shortcut.
|
|
4
|
+
|
|
5
|
+
**User docs are published** to [reticle.sh](https://reticle.sh) via `mint.json`. **Contributor docs are not** — they are for people working _on_ Reticle, and they live here because they belong next to what they describe.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## For people using Reticle
|
|
10
|
+
|
|
11
|
+
**Start:** [getting-started.md](getting-started.md) → [usage.md](usage.md) (the full tool reference).
|
|
12
|
+
|
|
13
|
+
| Page | What it answers |
|
|
14
|
+
| --- | --- |
|
|
15
|
+
| [getting-started.md](getting-started.md) | install it, connect an agent, verify something |
|
|
16
|
+
| [usage.md](usage.md) | every tool, every argument — the reference |
|
|
17
|
+
| [agent-cheatsheet.md](agent-cheatsheet.md) | the condensed version an agent keeps in context |
|
|
18
|
+
| [architecture.md](architecture.md) | how it works, and why it is built this way |
|
|
19
|
+
| [platform-integration.md](platform-integration.md) | Vite, Next, Remix, Astro, plain HTML |
|
|
20
|
+
| [desktop-apps.md](desktop-apps.md) | Electron and Tauri |
|
|
21
|
+
| [integration-patterns.md](integration-patterns.md) | wiring it into a real codebase |
|
|
22
|
+
| [flows.md](flows.md) | record → save → replay → heal |
|
|
23
|
+
| [testing.md](testing.md) | `@reticlehq/test` — turning a session into a CI suite |
|
|
24
|
+
| [multi-agent-testing.md](multi-agent-testing.md) | more than one agent on one app |
|
|
25
|
+
| [human-control.md](human-control.md) | taking the wheel back from the agent |
|
|
26
|
+
| [deploy-checks.md](deploy-checks.md) | running Reticle against a deployed build |
|
|
27
|
+
| [token-efficiency.md](token-efficiency.md) | why it costs less than a screenshot loop |
|
|
28
|
+
| [benchmarks.md](benchmarks.md) | how the numbers were measured, including where Reticle loses |
|
|
29
|
+
| [telemetry.md](telemetry.md) | what is collected, and how to turn it off |
|
|
30
|
+
| [local-registry.md](local-registry.md) | installing an unpublished build |
|
|
31
|
+
| [enterprise.md](enterprise.md) | the licensed surface |
|
|
32
|
+
|
|
33
|
+
## For people working on Reticle
|
|
34
|
+
|
|
35
|
+
**Start:** [`CONTRIBUTING.md`](../CONTRIBUTING.md) → [gates.md](gates.md).
|
|
36
|
+
|
|
37
|
+
| Page | What it answers |
|
|
38
|
+
| --- | --- |
|
|
39
|
+
| [gates.md](gates.md) | **I changed some files — which gate do I run?** |
|
|
40
|
+
| [gate-plan.md](gate-plan.md) | why the gates are shaped this way, and what is still unbuilt |
|
|
41
|
+
| [system-map.md](system-map.md) | how a tool call reaches the app, and which failures are silent |
|
|
42
|
+
| [telemetry-contract.md](telemetry-contract.md) | required reading before touching anything that emits |
|
|
43
|
+
| [debugging.md](debugging.md) | debugging Reticle itself — the four signals and what each answers |
|
|
44
|
+
| [fixtures.md](fixtures.md) | the sibling `reticle-fixtures` repo: install complexity, not regressions |
|
|
45
|
+
| [matrix/README.md](matrix/README.md) | submitting an MCP-client compatibility record — the best first contribution |
|
|
46
|
+
| [matrix/MATRIX.md](matrix/MATRIX.md) | which clients are known to work (generated; do not hand-edit) |
|
|
47
|
+
| [`../apps/README.md`](../apps/README.md) | what belongs in `apps/`, and what does not |
|
|
48
|
+
| [`../apps/e2e/harness-rules.md`](../apps/e2e/harness-rules.md) | the four rules every gate obeys, and the incident behind each |
|
|
49
|
+
| [`../bench/README.md`](../bench/README.md) | what in `bench/` is live and what is a kept one-off study |
|
|
50
|
+
|
|
51
|
+
`fixtures-dispatch-receiver.yml` is not a doc — it is the workflow template `reticle-fixtures` needs, kept in this repo so the two ends of the contract cannot drift apart.
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
**Adding a page?** Put it in the right table above. If it is user-facing, add it to `mint.json` too — a page that is published but unlisted is a page nobody finds, and a page listed here but absent from `mint.json` is one users cannot reach at all.
|
package/docs/agent-cheatsheet.md
CHANGED
|
@@ -7,7 +7,7 @@ One screen to get fluent. Reticle is the **proof layer for AI agents** — no sc
|
|
|
7
7
|
| Verb | Tool | One-liner |
|
|
8
8
|
| --- | --- | --- |
|
|
9
9
|
| **look** | `reticle_snapshot` / `reticle_query` | See the page (semantic tree) / find one specific element. |
|
|
10
|
-
| **act** | `reticle_act` / `reticle_act_and_wait` | Click/fill a `ref` / act + wait for a predicate in one hop. |
|
|
10
|
+
| **act** | `reticle_act` / `reticle_act_sequence` / `reticle_act_and_wait` | Click/fill a `ref` / **batch a whole journey in one hop** / act + wait for a predicate in one hop. |
|
|
11
11
|
| **observe** | `reticle_observe` / `reticle_wait_for` | Everything the app did after `since` / block until true. |
|
|
12
12
|
| **assert** | `reticle_assert` | Evaluate a predicate → `{ pass, evidence, failureReason? }`. The end of every loop. |
|
|
13
13
|
|
|
@@ -50,7 +50,7 @@ A claim is real only when the layers agree. Check more than the UI:
|
|
|
50
50
|
|
|
51
51
|
The tools advertised DIRECTLY — what you'll use 90% of the time. Everything else is one `reticle_run` away (see Token note below).
|
|
52
52
|
|
|
53
|
-
`reticle_sessions` · `reticle_navigate` · `reticle_snapshot` · `reticle_query` · `reticle_act` · `reticle_act_and_wait` · `reticle_observe` · `reticle_network` · `reticle_console` · `reticle_wait_for` · `reticle_assert` · `reticle_state` · `reticle_inspect` (DOM node → `src/App.tsx:104`) · `reticle_feedback` (tell the maintainers what is missing) · `reticle_session` (hand back: `{action:"yield"}` the moment you stop driving, `{action:"resume"}` after a human pause).
|
|
53
|
+
`reticle_sessions` · `reticle_navigate` · `reticle_snapshot` · `reticle_query` · `reticle_act` · `reticle_act_sequence` · `reticle_act_and_wait` · `reticle_observe` · `reticle_network` · `reticle_console` · `reticle_wait_for` · `reticle_assert` · `reticle_state` · `reticle_inspect` (DOM node → `src/App.tsx:104`) · `reticle_feedback` (tell the maintainers what is missing) · `reticle_session` (hand back: `{action:"yield"}` the moment you stop driving, `{action:"resume"}` after a human pause).
|
|
54
54
|
|
|
55
55
|
Frequently useful but NOT core, so reach them through `reticle_run({ tool, args })`: `reticle_domain` (learn the app + gaps), `reticle_capabilities` (the app's declared testable surface), `reticle_baseline {action:"diff"}`, `reticle_project` (run history).
|
|
56
56
|
|
package/docs/benchmarks.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
> This page assumes **zero** testing background. By the end you'll understand what software testing is, why AI coding agents made it urgent, how to tell a good verification tool from a bad one, and exactly how Reticle measures up against the main alternatives — including the places Reticle loses. If a term looks like jargon, it's defined the first time it appears.
|
|
4
4
|
>
|
|
5
|
-
>
|
|
5
|
+
> _Where the pictures come from, stated accurately: the **SVG charts** (`bench/artifacts/`) are generated from measured raws by `bench/harness/charts.mjs`. The **headline PNG cards** (`assets/readme/`) are screenshots of hand-authored HTML in `assets/benchmarks/src/`, rendered by `assets/benchmarks/render.mjs` — their figures are typed in by a human and are **not** read from `history.jsonl`, which this line previously claimed. Treat a card as a design asset that has to be updated by hand when a number moves, and `bench/SCORECARD.md` (plus its freshness banner) as the source of truth._
|
|
6
6
|
|
|
7
7
|
---
|
|
8
8
|
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# Copy this into `reticle-fixtures` as `.github/workflows/verify-on-dispatch.yml`.
|
|
2
|
+
#
|
|
3
|
+
# It is kept HERE, in the repo that sends the dispatch, on purpose: the two halves have to agree on
|
|
4
|
+
# an event name and a payload shape, and a contract whose two ends live in different repositories
|
|
5
|
+
# drifts the first time somebody edits one of them. This file is the sending end's statement of what
|
|
6
|
+
# the receiving end must look like.
|
|
7
|
+
#
|
|
8
|
+
# It has never run. Nothing in this repository can verify it — the dispatch needs a token that does
|
|
9
|
+
# not exist yet, and the receiver needs to be committed to another repository. Treat every line as
|
|
10
|
+
# unverified until somebody has watched one green run, and do not let its presence here be mistaken
|
|
11
|
+
# for Tier 2 being live.
|
|
12
|
+
name: Verify a Reticle commit against the fixtures
|
|
13
|
+
|
|
14
|
+
on:
|
|
15
|
+
repository_dispatch:
|
|
16
|
+
types: [reticle-commit]
|
|
17
|
+
workflow_dispatch:
|
|
18
|
+
inputs:
|
|
19
|
+
sha:
|
|
20
|
+
description: Reticle commit to verify
|
|
21
|
+
required: true
|
|
22
|
+
|
|
23
|
+
jobs:
|
|
24
|
+
verify:
|
|
25
|
+
runs-on: ubuntu-latest
|
|
26
|
+
# Nine real apps, each installed and booted. This is why Tier 2 cannot block a merge.
|
|
27
|
+
timeout-minutes: 90
|
|
28
|
+
steps:
|
|
29
|
+
- name: Check out the fixtures (the pristine surface)
|
|
30
|
+
uses: actions/checkout@v4
|
|
31
|
+
with:
|
|
32
|
+
# `clean` is the branch that has NEVER been instrumented. Running init over an already
|
|
33
|
+
# wired app reports "already wired" for every step and proves nothing, which is exactly how
|
|
34
|
+
# an install regression ships unnoticed.
|
|
35
|
+
ref: clean
|
|
36
|
+
|
|
37
|
+
- name: Check out the Reticle commit under test
|
|
38
|
+
uses: actions/checkout@v4
|
|
39
|
+
with:
|
|
40
|
+
repository: reticlehq/reticle
|
|
41
|
+
ref: ${{ github.event.client_payload.sha || inputs.sha }}
|
|
42
|
+
path: reticle
|
|
43
|
+
|
|
44
|
+
- uses: pnpm/action-setup@v4
|
|
45
|
+
- uses: actions/setup-node@v4
|
|
46
|
+
with:
|
|
47
|
+
node-version: 22
|
|
48
|
+
|
|
49
|
+
- name: Build the release candidate
|
|
50
|
+
working-directory: reticle
|
|
51
|
+
run: |
|
|
52
|
+
pnpm install --frozen-lockfile
|
|
53
|
+
pnpm build
|
|
54
|
+
|
|
55
|
+
- run: npx playwright install --with-deps chromium
|
|
56
|
+
|
|
57
|
+
- name: Materialise the fixture apps
|
|
58
|
+
run: node scripts/materialise.mjs
|
|
59
|
+
|
|
60
|
+
- name: Install into every fixture and open each one in a browser
|
|
61
|
+
# `--local` is the whole point: without it the gate can only confirm what already shipped,
|
|
62
|
+
# which is the wrong half of the question before a release.
|
|
63
|
+
run: node scripts/verify.mjs --local ./reticle
|
|
64
|
+
env:
|
|
65
|
+
# The fixtures gate's own idle shutdown once fired mid-install and scored three fixtures as
|
|
66
|
+
# install failures. A gate owns its daemon for the whole run — see the reticle repo's
|
|
67
|
+
# apps/e2e/harness-rules.md, rule 2.
|
|
68
|
+
RETICLE_IDLE_SHUTDOWN_MS: '0'
|
|
69
|
+
|
|
70
|
+
- name: Upload the run
|
|
71
|
+
if: always()
|
|
72
|
+
uses: actions/upload-artifact@v4
|
|
73
|
+
with:
|
|
74
|
+
name: fixtures-run
|
|
75
|
+
path: runs/
|
|
76
|
+
if-no-files-found: ignore
|
package/docs/fixtures.md
CHANGED
|
@@ -26,3 +26,20 @@ The last step is the one that earns its keep. Every install bug found so far has
|
|
|
26
26
|
`apps/` — bench-app, next-smoke, the Remix and Astro examples, the Electron and Tauri smoke apps, atlas, and the API. These are the CI gates (`pnpm test:e2e`, `pnpm test:e2e:desktop`), and `integration-coverage.test.ts` fails if one of them goes missing while `SKILL.md` still offers its framework.
|
|
27
27
|
|
|
28
28
|
The split is: **`apps/` proves the tools work. `reticle-fixtures` proves the install works.**
|
|
29
|
+
|
|
30
|
+
## Wiring Tier 2 (not yet live)
|
|
31
|
+
|
|
32
|
+
CI can ask this repo to verify a Reticle commit automatically — the [`vite-ecosystem-ci`](https://github.com/vitejs/vite-ecosystem-ci) shape, where a separate repo holds the downstream apps and the main repo asks it to run. The sending half is built (`fixtures-dispatch` in `.github/workflows/ci.yml`); the receiving half is a template at [`docs/fixtures-dispatch-receiver.yml`](./fixtures-dispatch-receiver.yml).
|
|
33
|
+
|
|
34
|
+
**It is inert until two things happen, and neither can be done from inside this repository:**
|
|
35
|
+
|
|
36
|
+
1. A fine-grained PAT with **Actions: write** on `reticlehq/reticle-fixtures`, added to `reticlehq/reticle` as the secret `FIXTURES_DISPATCH_TOKEN`. Until it exists the job logs that Tier 2 is unwired and exits 0 — it never reddens CI, because a dispatch failure has nothing to do with the change being pushed.
|
|
37
|
+
2. The template copied into `reticle-fixtures` as `.github/workflows/verify-on-dispatch.yml`.
|
|
38
|
+
|
|
39
|
+
The template lives in the _sending_ repo deliberately: the two halves must agree on an event name and payload shape, and a contract whose ends live in different repositories drifts the first time somebody edits one of them.
|
|
40
|
+
|
|
41
|
+
**Nothing here has ever run.** Treat every line as unverified until somebody has watched one green run. Tier 2 being present in the plan is not Tier 2 being live.
|
|
42
|
+
|
|
43
|
+
### Why Tier 2 exists when Tier 1 already passes
|
|
44
|
+
|
|
45
|
+
`pnpm gate:install` (Tier 1) scaffolds pristine apps and catches install **regressions**. It cannot catch install **complexity**: `rowy` is 70 dependencies of somebody else's product, `phanpy` has a 130-line vite config holding ten plugins inside a `defineConfig` callback. That is where install bugs actually hide, and a scaffold deliberately has none of it. The two tiers answer different questions and neither substitutes for the other.
|