@reticlehq/server 3.2.0 → 3.3.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 +16 -13
- package/dist/command/cli/adhoc-suite.d.ts +19 -0
- package/dist/command/cli/adhoc-suite.js +88 -0
- package/dist/command/cli/adhoc-verdict.d.ts +23 -2
- package/dist/command/cli/adhoc-verdict.js +18 -10
- package/dist/command/cli/cli-doctor.js +25 -5
- package/dist/command/cli/cli-parse-grammar.d.ts +7 -0
- package/dist/command/cli/cli-parse-grammar.js +7 -0
- package/dist/command/cli/cli-parse-verify.d.ts +2 -5
- package/dist/command/cli/cli-parse-verify.js +86 -13
- package/dist/command/cli/cli-parse.d.ts +7 -1
- package/dist/command/cli/cli-parse.js +33 -5
- package/dist/command/cli/cli-verify.d.ts +59 -0
- package/dist/command/cli/cli-verify.js +129 -17
- package/dist/command/cli/cloud-kit.d.ts +0 -2
- package/dist/command/cli/cloud-kit.js +1 -1
- package/dist/command/cli/doctor/doctor-sessions-line.d.ts +2 -1
- package/dist/command/cli/launch/cli-launch.d.ts +1 -29
- package/dist/command/cli/launch/cli-launch.js +6 -40
- package/dist/command/cli/ports/resolve/cli-port.d.ts +13 -0
- package/dist/command/cli/ports/resolve/cli-port.js +45 -1
- package/dist/command/cli/report-command.d.ts +16 -0
- package/dist/command/cli/report-command.js +97 -0
- package/dist/command/cli.js +5 -0
- package/dist/command/daemon/binding/daemon-status-probe.d.ts +30 -0
- package/dist/command/daemon/binding/daemon-status-probe.js +45 -0
- package/dist/command/daemon/binding/port-presence.d.ts +1 -1
- package/dist/command/daemon/idle-shutdown.d.ts +2 -1
- package/dist/command/setup/init/init-host.js +8 -0
- package/dist/features/crawl/crawl.d.ts +2 -0
- package/dist/features/crawl/crawl.js +1 -0
- package/dist/features/harness/jev-driver.js +6 -9
- package/dist/features/harness/platform-config.d.ts +10 -0
- package/dist/features/harness/platform-config.js +8 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +40 -95
- package/dist/judgement/capsule/capsule-store.d.ts +31 -2
- package/dist/judgement/capsule/capsule-store.js +58 -6
- package/dist/judgement/capsule/capsule.js +5 -0
- package/dist/judgement/capsule/causal-summary.d.ts +13 -1
- package/dist/judgement/capsule/causal-summary.js +7 -0
- package/dist/judgement/domain/domain-model.js +38 -9
- package/dist/judgement/outcome/assertion-integrity.d.ts +3 -3
- package/dist/judgement/phenomena/phenomena.d.ts +2 -1
- package/dist/judgement/runs/artifact/gap-report.d.ts +17 -0
- package/dist/judgement/runs/artifact/gap-report.js +46 -0
- package/dist/judgement/runs/artifact/gap-summary.d.ts +58 -0
- package/dist/judgement/runs/artifact/gap-summary.js +85 -0
- package/dist/judgement/runs/artifact/render-report.js +4 -1
- package/dist/judgement/runs/artifact/to-artifact.d.ts +7 -2
- package/dist/judgement/runs/artifact/to-artifact.js +1 -1
- package/dist/judgement/runs/context-tools.js +21 -3
- package/dist/judgement/runs/drive-run.js +7 -2
- package/dist/language/flows/annotate-notes/annotate.d.ts +0 -15
- package/dist/language/flows/annotate-notes/annotate.js +66 -18
- package/dist/language/flows/flow-anchor.d.ts +12 -0
- package/dist/language/flows/flow-anchor.js +20 -0
- package/dist/language/flows/flow-expect-grammar.d.ts +2 -2
- package/dist/language/flows/flow-expect-grammar.js +92 -11
- package/dist/language/flows/flow-intent.js +1 -1
- package/dist/language/flows/flow-learning.d.ts +3 -0
- package/dist/language/flows/flow-learning.js +39 -8
- package/dist/language/flows/flow-replay-run.d.ts +31 -9
- package/dist/language/flows/flow-replay-run.js +146 -33
- package/dist/language/flows/flow-replay-types.d.ts +9 -0
- package/dist/language/flows/flow-replay.d.ts +12 -11
- package/dist/language/flows/flow-replay.js +79 -109
- package/dist/language/flows/flow-step-runners.d.ts +26 -10
- package/dist/language/flows/flow-step-runners.js +20 -4
- package/dist/language/flows/flow-success.d.ts +21 -6
- package/dist/language/flows/flow-success.js +50 -138
- package/dist/language/flows/flows.d.ts +8 -4
- package/dist/language/flows/flows.js +11 -2
- package/dist/language/flows/heal.d.ts +9 -1
- package/dist/language/flows/heal.js +24 -2
- package/dist/language/flows/learned-sources.d.ts +21 -0
- package/dist/language/flows/learned-sources.js +84 -0
- package/dist/language/flows/recording/recording-backtrack.d.ts +0 -7
- package/dist/language/flows/recording/recording-backtrack.js +1 -1
- package/dist/language/flows/recording/replay-halt.d.ts +2 -1
- package/dist/language/flows/recording/tape/recordings.d.ts +2 -2
- package/dist/language/flows/replay.js +12 -8
- package/dist/language/flows/stores/annotation-store.d.ts +5 -5
- package/dist/language/flows/stores/assertion-tiers-store.js +2 -3
- package/dist/language/flows/stores/flake-store.js +2 -3
- package/dist/language/flows/suite/coverage.d.ts +1 -1
- package/dist/language/flows/suite/parallel-suite.d.ts +2 -1
- package/dist/language/flows/verify-change-tools.js +14 -0
- package/dist/memory/cloud/cloud-sync.d.ts +0 -2
- package/dist/memory/cloud/cloud-sync.js +1 -1
- package/dist/memory/cloud/harness-config.d.ts +2 -0
- package/dist/memory/cloud/sync-cycle.d.ts +12 -0
- package/dist/memory/cloud/sync-cycle.js +29 -4
- package/dist/memory/cloud/sync-daemon.d.ts +13 -0
- package/dist/memory/cloud/sync-daemon.js +21 -0
- package/dist/memory/cloud/sync-disk.js +11 -6
- package/dist/memory/impact/impact-store.js +6 -0
- package/dist/memory/intent/inline-intent.js +8 -3
- package/dist/memory/intent/intent-shard-store.d.ts +33 -47
- package/dist/memory/intent/intent-shard-store.js +252 -140
- package/dist/memory/intent/intent-shard.d.ts +10 -11
- package/dist/memory/intent/intent-shard.js +1 -1
- package/dist/memory/intent/intent-store.d.ts +13 -8
- package/dist/memory/intent/intent-store.js +46 -90
- package/dist/memory/intent/intent-tools.js +34 -23
- package/dist/memory/journal/ambient-store.js +2 -3
- package/dist/memory/journal/deviation-report.js +4 -1
- package/dist/memory/journal/deviation-service.js +10 -2
- package/dist/memory/journal/drive-flow.d.ts +2 -2
- package/dist/memory/journal/drive-flow.js +21 -12
- package/dist/memory/journal/envelope-store.js +2 -3
- package/dist/memory/journal/journal-recorder.d.ts +21 -0
- package/dist/memory/journal/on-disk/retain-policy.d.ts +34 -0
- package/dist/memory/journal/on-disk/retain-policy.js +44 -0
- package/dist/memory/journal/on-disk/retention.d.ts +14 -10
- package/dist/memory/journal/on-disk/retention.js +13 -12
- package/dist/memory/journal/on-disk/startup-maintenance.d.ts +42 -0
- package/dist/memory/journal/on-disk/startup-maintenance.js +14 -0
- package/dist/memory/journal/on-disk/workspace-gitignore.d.ts +0 -11
- package/dist/memory/journal/on-disk/workspace-gitignore.js +6 -45
- package/dist/memory/journal/on-disk/workspace-tiers.d.ts +52 -0
- package/dist/memory/journal/on-disk/workspace-tiers.js +88 -0
- package/dist/memory/journal/route-template.d.ts +22 -0
- package/dist/memory/journal/route-template.js +44 -0
- package/dist/memory/journal/session-end.d.ts +4 -1
- package/dist/memory/journal/session-end.js +64 -6
- package/dist/memory/journal/session-journal.d.ts +8 -1
- package/dist/memory/journal/session-journal.js +10 -0
- package/dist/memory/project/baselines.d.ts +1 -1
- package/dist/memory/project/dir/reticle-dir.d.ts +1 -1
- package/dist/memory/project/fs/fs-port.js +44 -5
- package/dist/memory/project/fs/write-atomic.d.ts +26 -0
- package/dist/memory/project/fs/write-atomic.js +48 -0
- package/dist/memory/recall/prior/connection-memory.d.ts +6 -0
- package/dist/memory/recall/prior/connection-memory.js +8 -0
- package/dist/memory/recall/registered-projects.d.ts +2 -1
- package/dist/portal/bridge/auth-failure-reason.js +5 -2
- package/dist/portal/input/real-input.js +2 -1
- package/dist/portal/pool/browser-pool.d.ts +10 -0
- package/dist/portal/pool/browser-pool.js +10 -0
- package/dist/portal/pool/playwright-launcher.js +1 -0
- package/dist/portal/pool/pool-contract.d.ts +2 -0
- package/dist/portal/pool/storage-seed.d.ts +2 -1
- package/dist/portal/pool/zero-install.d.ts +27 -0
- package/dist/portal/pool/zero-install.js +54 -0
- package/dist/portal/realm/web-realm.js +6 -0
- package/dist/portal/session/dev-server/dev-server-probe.d.ts +2 -1
- package/dist/portal/session/dev-server/served-document.d.ts +21 -0
- package/dist/portal/session/dev-server/served-document.js +57 -0
- package/dist/portal/session/facts/background-traffic.d.ts +9 -0
- package/dist/portal/session/facts/background-traffic.js +25 -0
- package/dist/portal/session/live-control-tools.js +48 -3
- package/dist/portal/session/presence/attachment-history.d.ts +13 -2
- package/dist/portal/session/session-health.d.ts +61 -0
- package/dist/portal/session/session-health.js +40 -1
- package/dist/portal/session/session-info.d.ts +6 -0
- package/dist/portal/session/session-manager.d.ts +9 -0
- package/dist/portal/session/session-manager.js +23 -2
- package/dist/portal/session/session-successor.d.ts +11 -0
- package/dist/portal/session/session-successor.js +14 -1
- package/dist/portal/session/session-verdict-facts.d.ts +28 -0
- package/dist/portal/session/session-verdict-facts.js +15 -0
- package/dist/portal/session/session.d.ts +5 -0
- package/dist/portal/session/session.js +19 -7
- package/dist/surface/mcp/mcp-proxy.js +3 -3
- package/dist/surface/mcp/proxy-idle-exit.d.ts +2 -1
- package/dist/surface/mcp/server-instructions.js +12 -1
- package/dist/surface/tools/act/already-true.d.ts +40 -0
- package/dist/surface/tools/act/already-true.js +36 -0
- package/dist/surface/tools/act/navigation/navigate-arrival.d.ts +6 -0
- package/dist/surface/tools/act/navigation/navigate-arrival.js +13 -0
- package/dist/surface/tools/act/settle-in-flight.d.ts +6 -2
- package/dist/surface/tools/act/settle-in-flight.js +29 -6
- package/dist/surface/tools/act-capsule.js +22 -7
- package/dist/surface/tools/act-tools.js +25 -24
- package/dist/surface/tools/args/numeric-bounds.d.ts +15 -2
- package/dist/surface/tools/args/numeric-bounds.js +30 -3
- package/dist/surface/tools/assert/assert-grade.js +14 -2
- package/dist/surface/tools/assert/assert-verdict.js +30 -4
- package/dist/surface/tools/browser-tools.js +4 -3
- package/dist/surface/tools/dynamic-tools.js +6 -0
- package/dist/surface/tools/feedback-tools.d.ts +2 -1
- package/dist/surface/tools/feedback-tools.js +1 -1
- package/dist/surface/tools/invoke-tool.js +17 -4
- package/dist/surface/tools/lease-hint.d.ts +8 -0
- package/dist/surface/tools/lease-hint.js +10 -0
- package/dist/surface/tools/lease-tools.d.ts +16 -28
- package/dist/surface/tools/lease-tools.js +53 -5
- package/dist/surface/tools/lineage-tools.js +1 -1
- package/dist/surface/tools/observe-tools.js +14 -6
- package/dist/surface/tools/real-input-attempt.d.ts +0 -5
- package/dist/surface/tools/real-input-attempt.js +1 -1
- package/dist/surface/tools/tool-kit.d.ts +5 -0
- package/dist/surface/tools/tool-kit.js +6 -1
- package/dist/surface/tools/tool-surface.js +17 -5
- package/dist/surface/tools/tools.js +64 -29
- package/dist/telemetry/feedback-local.d.ts +1 -0
- package/dist/telemetry/feedback-local.js +1 -3
- package/dist/telemetry/feedback.d.ts +1 -1
- package/dist/telemetry/hud-metrics.d.ts +32 -0
- package/dist/telemetry/hud-metrics.js +95 -0
- package/dist/telemetry/session-metrics.d.ts +5 -0
- package/dist/telemetry/session-metrics.js +15 -1
- package/dist/wire-journal.d.ts +31 -0
- package/dist/wire-journal.js +100 -0
- package/docs/README.md +1 -0
- package/docs/cli/cloud.mdx +9 -7
- package/docs/cli/init.mdx +1 -0
- package/docs/cli/report.mdx +44 -0
- package/docs/desktop.mdx +1 -1
- package/docs/flows.md +11 -0
- package/docs/frameworks.mdx +113 -1
- package/docs/install-agentic.mdx +36 -0
- package/docs/install-manual.mdx +1 -1
- package/docs/packages/tauri.mdx +1 -1
- package/docs/packages.mdx +1 -1
- package/docs/predicates.mdx +128 -4
- package/docs/tools/query.mdx +10 -10
- package/docs/usage.md +1 -1
- package/docs/what-is-recorded.md +62 -0
- package/package.json +6 -6
- package/dist/judgement/outcome/predicate-to-expect.d.ts +0 -44
- package/dist/judgement/outcome/predicate-to-expect.js +0 -148
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { isHudControl } from '@reticlehq/core/hud';
|
|
2
|
+
import { HudPanel, HudView } from '@reticlehq/core';
|
|
3
|
+
/**
|
|
4
|
+
* How people use the in-page HUD, rolled into the session summary.
|
|
5
|
+
*
|
|
6
|
+
* Windowed like the tool histograms beside it: a periodic flush reports what happened since the
|
|
7
|
+
* last one, so a dashboard sums across a session's events. Names only — every key is a control id
|
|
8
|
+
* the wire schema already checked against core's closed list.
|
|
9
|
+
*/
|
|
10
|
+
/** How many steps of the journey one window keeps. A journey longer than this is cut, and says so. */
|
|
11
|
+
export const HUD_JOURNEY_MAX = 60;
|
|
12
|
+
export class HudMetrics {
|
|
13
|
+
#now;
|
|
14
|
+
#controls = new Map();
|
|
15
|
+
#viewMs = new Map();
|
|
16
|
+
#panelMs = new Map();
|
|
17
|
+
#journey = [];
|
|
18
|
+
#cut = false;
|
|
19
|
+
/** Did anything happen this window. A HUD merely sitting open is not worth a flush of its own. */
|
|
20
|
+
#changed = false;
|
|
21
|
+
#view;
|
|
22
|
+
#panel;
|
|
23
|
+
constructor(now) {
|
|
24
|
+
this.#now = now;
|
|
25
|
+
}
|
|
26
|
+
record(use) {
|
|
27
|
+
// A control the HUD never rendered is dropped here, the one place HUD use is counted, so free
|
|
28
|
+
// text cannot ride in on the id.
|
|
29
|
+
const control = use.control !== undefined && isHudControl(use.control) ? use.control : undefined;
|
|
30
|
+
if (control === undefined && use.view === undefined && use.panel === undefined)
|
|
31
|
+
return;
|
|
32
|
+
this.#changed = true;
|
|
33
|
+
if (control !== undefined) {
|
|
34
|
+
const key = use.toggle === undefined ? control : `${control}:${use.toggle}`;
|
|
35
|
+
this.#controls.set(key, (this.#controls.get(key) ?? 0) + 1);
|
|
36
|
+
this.#step(key);
|
|
37
|
+
}
|
|
38
|
+
if (use.view !== undefined && use.view !== this.#view?.value) {
|
|
39
|
+
this.#close(this.#view, this.#viewMs);
|
|
40
|
+
this.#view = { value: use.view, since: this.#now() };
|
|
41
|
+
this.#step(`view:${use.view}`);
|
|
42
|
+
}
|
|
43
|
+
if (use.panel !== undefined && use.panel !== this.#panel?.value) {
|
|
44
|
+
this.#close(this.#panel, this.#panelMs);
|
|
45
|
+
this.#panel = { value: use.panel, since: this.#now() };
|
|
46
|
+
if (use.panel !== HudPanel.NONE)
|
|
47
|
+
this.#step(`panel:${use.panel}`);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
get empty() {
|
|
51
|
+
return !this.#changed;
|
|
52
|
+
}
|
|
53
|
+
/** The window so far. Open view and panel are counted up to now, and keep running after. */
|
|
54
|
+
summarize() {
|
|
55
|
+
const viewMs = this.#withOpen(this.#view, this.#viewMs);
|
|
56
|
+
const panelMs = this.#withOpen(this.#panel, this.#panelMs);
|
|
57
|
+
return {
|
|
58
|
+
...(this.#controls.size > 0 ? { hudControls: Object.fromEntries(this.#controls) } : {}),
|
|
59
|
+
...(viewMs.size > 0 ? { hudViewMs: Object.fromEntries(viewMs) } : {}),
|
|
60
|
+
...(panelMs.size > 0 ? { hudPanelMs: Object.fromEntries(panelMs) } : {}),
|
|
61
|
+
...(this.#journey.length > 0 ? { hudJourney: [...this.#journey] } : {}),
|
|
62
|
+
...(this.#cut ? { hudJourneyCut: true } : {}),
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
/** Start the next window. The current view and panel carry over, timed from now. */
|
|
66
|
+
reset() {
|
|
67
|
+
const at = this.#now();
|
|
68
|
+
this.#controls.clear();
|
|
69
|
+
this.#viewMs.clear();
|
|
70
|
+
this.#panelMs.clear();
|
|
71
|
+
this.#journey.length = 0;
|
|
72
|
+
this.#cut = false;
|
|
73
|
+
this.#changed = false;
|
|
74
|
+
if (this.#view !== undefined)
|
|
75
|
+
this.#view = { ...this.#view, since: at };
|
|
76
|
+
if (this.#panel !== undefined)
|
|
77
|
+
this.#panel = { ...this.#panel, since: at };
|
|
78
|
+
}
|
|
79
|
+
#step(entry) {
|
|
80
|
+
if (this.#journey.length < HUD_JOURNEY_MAX)
|
|
81
|
+
this.#journey.push(entry);
|
|
82
|
+
else
|
|
83
|
+
this.#cut = true;
|
|
84
|
+
}
|
|
85
|
+
#close(open, into) {
|
|
86
|
+
if (open === undefined)
|
|
87
|
+
return;
|
|
88
|
+
into.set(open.value, (into.get(open.value) ?? 0) + Math.max(0, this.#now() - open.since));
|
|
89
|
+
}
|
|
90
|
+
#withOpen(open, closed) {
|
|
91
|
+
const out = new Map(closed);
|
|
92
|
+
this.#close(open, out);
|
|
93
|
+
return out;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
* than a rule saying it shouldn't.
|
|
17
17
|
*/
|
|
18
18
|
import { BrowserLaunchKind, type ConnectFailure, type SessionSummary } from '@reticlehq/core/telemetry';
|
|
19
|
+
import type { HudUseData } from '@reticlehq/core';
|
|
19
20
|
/**
|
|
20
21
|
* Mutable counters for one daemon lifetime. A plain class, not a store: it is process-local, it never
|
|
21
22
|
* persists, and it dies with the daemon.
|
|
@@ -24,6 +25,8 @@ export declare class SessionMetrics {
|
|
|
24
25
|
#private;
|
|
25
26
|
/** Clock injected — this file must stay testable without a real one, per the repo's clock rule. */
|
|
26
27
|
constructor(now: () => number);
|
|
28
|
+
/** A person used the HUD. Names only; the wire schema has already narrowed it. */
|
|
29
|
+
recordHudUse(use: HudUseData): void;
|
|
27
30
|
/**
|
|
28
31
|
* Record a call starting. Returns a function to call when it finishes — that closure is what makes
|
|
29
32
|
* timing correct under CONCURRENCY: several agents can be inside `runTool` at once, so a single
|
|
@@ -162,5 +165,7 @@ export declare const recordSdkFailure: (failure: {
|
|
|
162
165
|
site: string;
|
|
163
166
|
message: string;
|
|
164
167
|
}) => void;
|
|
168
|
+
/** Narrow-and-record one HUD use. Keeps the session hot path to a single call. */
|
|
169
|
+
export declare const recordHudUse: (use: HudUseData) => void;
|
|
165
170
|
/** Tests only — drop the singleton so each case starts from zero. */
|
|
166
171
|
export declare const resetSessionMetrics: () => void;
|
|
@@ -20,6 +20,7 @@ import { errorSkeleton, fingerprintError } from './error-fingerprint.js';
|
|
|
20
20
|
import { describeToolParams } from './argument-shape.js';
|
|
21
21
|
import { machineSnapshot } from './machine-snapshot.js';
|
|
22
22
|
import { classifyError } from './error-class.js';
|
|
23
|
+
import { HudMetrics } from './hud-metrics.js';
|
|
23
24
|
/** Cap the distinct error shapes held in memory — a pathological loop must not grow unbounded. */
|
|
24
25
|
const MAX_ERROR_KINDS = 40;
|
|
25
26
|
/** Distinct tool names whose repeat-runs are tracked. Its own cap: the tool surface, not error shapes. */
|
|
@@ -165,10 +166,16 @@ export class SessionMetrics {
|
|
|
165
166
|
#surface;
|
|
166
167
|
#startedAt;
|
|
167
168
|
#now;
|
|
169
|
+
#hud;
|
|
168
170
|
/** Clock injected — this file must stay testable without a real one, per the repo's clock rule. */
|
|
169
171
|
constructor(now) {
|
|
170
172
|
this.#now = now;
|
|
171
173
|
this.#startedAt = now();
|
|
174
|
+
this.#hud = new HudMetrics(now);
|
|
175
|
+
}
|
|
176
|
+
/** A person used the HUD. Names only; the wire schema has already narrowed it. */
|
|
177
|
+
recordHudUse(use) {
|
|
178
|
+
this.#hud.record(use);
|
|
172
179
|
}
|
|
173
180
|
/**
|
|
174
181
|
* Record a call starting. Returns a function to call when it finishes — that closure is what makes
|
|
@@ -555,6 +562,7 @@ export class SessionMetrics {
|
|
|
555
562
|
...(this.#firstAppAt === undefined
|
|
556
563
|
? {}
|
|
557
564
|
: { msToFirstApp: Math.max(0, this.#firstAppAt - this.#startedAt) }),
|
|
565
|
+
...this.#hud.summarize(),
|
|
558
566
|
final,
|
|
559
567
|
// Only on a real exit. A periodic flush carrying `exit: "unknown"` would read as a daemon that
|
|
560
568
|
// died without a shutdown path, which is the one thing this field exists to make visible.
|
|
@@ -613,7 +621,8 @@ export class SessionMetrics {
|
|
|
613
621
|
0 === this.#bugsFound &&
|
|
614
622
|
0 === this.#sdkFailures &&
|
|
615
623
|
0 === this.#postSocketFailures &&
|
|
616
|
-
0 === this.#postRetriesSaved
|
|
624
|
+
0 === this.#postRetriesSaved &&
|
|
625
|
+
this.#hud.empty);
|
|
617
626
|
}
|
|
618
627
|
/** Zero the counters after a non-final flush so the next flush reports the NEXT window, not a total. */
|
|
619
628
|
reset() {
|
|
@@ -641,6 +650,7 @@ export class SessionMetrics {
|
|
|
641
650
|
this.#unknownToolCalls = 0;
|
|
642
651
|
this.#bugsFound = 0;
|
|
643
652
|
this.#bugKinds.clear();
|
|
653
|
+
this.#hud.reset();
|
|
644
654
|
// #seenBugKinds is deliberately NOT cleared. It is not a window counter — it is the
|
|
645
655
|
// session-lifetime memory behind `repeat` on bug_found, and zeroing it made the same defect,
|
|
646
656
|
// found again after a flush, report as a newly distinct one. Sessions run to 11.5 hours in the
|
|
@@ -665,6 +675,10 @@ export const recordBrowserLatency = (ms) => {
|
|
|
665
675
|
export const recordSdkFailure = (failure) => {
|
|
666
676
|
getSessionMetrics().recordSdkFailure(failure.site, failure.message);
|
|
667
677
|
};
|
|
678
|
+
/** Narrow-and-record one HUD use. Keeps the session hot path to a single call. */
|
|
679
|
+
export const recordHudUse = (use) => {
|
|
680
|
+
getSessionMetrics().recordHudUse(use);
|
|
681
|
+
};
|
|
668
682
|
/** Tests only — drop the singleton so each case starts from zero. */
|
|
669
683
|
export const resetSessionMetrics = () => {
|
|
670
684
|
current = undefined;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { Bridge } from './portal/bridge/bridge.js';
|
|
2
|
+
import { FlowStore } from './language/flows/flows.js';
|
|
3
|
+
import type { FileSystemPort } from './memory/project/fs/fs-port.js';
|
|
4
|
+
import type { TapeStep } from './memory/journal/drive-flow.js';
|
|
5
|
+
import type { OnboardingStep } from '@reticlehq/core/telemetry';
|
|
6
|
+
import { type PruneWorkspaceOptions } from './memory/journal/on-disk/startup-maintenance.js';
|
|
7
|
+
/**
|
|
8
|
+
* Wire journal capture, ambient seeding and the journal-tail flush onto a bridge.
|
|
9
|
+
*
|
|
10
|
+
* Both entry points need all three, and both used to hand-roll them. `startDaemon` only ever wired the
|
|
11
|
+
* first, so on the path every user actually takes (`reticle serve` / `reticle mcp`) the journal tail was
|
|
12
|
+
* dropped at session end and the learned ambient map was never persisted OR seeded — meaning ambient
|
|
13
|
+
* learning could not converge across sessions and the last events of every session were lost. The two
|
|
14
|
+
* call sites had already drifted once before, which is why this is one function rather than a
|
|
15
|
+
* copy-paste both are asked to keep in step.
|
|
16
|
+
*/
|
|
17
|
+
export declare function attachJournal(bridge: Bridge, deps: {
|
|
18
|
+
fs: FileSystemPort;
|
|
19
|
+
reticleRoot: string;
|
|
20
|
+
enabled: boolean;
|
|
21
|
+
takeAmbientTape?: () => {
|
|
22
|
+
steps: readonly TapeStep[];
|
|
23
|
+
startPath?: string;
|
|
24
|
+
} | undefined;
|
|
25
|
+
reportStep?: (step: OnboardingStep) => Promise<boolean>;
|
|
26
|
+
/** Tell cloud sync a run landed, so it cycles instead of waiting for its timer. */
|
|
27
|
+
onRunPersisted?: () => void;
|
|
28
|
+
flows?: FlowStore;
|
|
29
|
+
/** What this project is willing to keep — the `retain` block of its `.reticle.json`. */
|
|
30
|
+
retain?: PruneWorkspaceOptions;
|
|
31
|
+
}): void;
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Journal wiring for the composition root.
|
|
3
|
+
*
|
|
4
|
+
* A sibling of `index.ts` rather than a move into `memory/journal/`: this is COMPOSITION, not
|
|
5
|
+
* journalling. It reaches the bridge, the flow store, the telemetry funnel and the recording tape to
|
|
6
|
+
* join them together, and putting those reaches inside the journal domain would make that domain
|
|
7
|
+
* depend on four areas it has no business knowing. `index.ts` already holds every one of them, and
|
|
8
|
+
* the 1000-line backstop is what asked for the split.
|
|
9
|
+
*/
|
|
10
|
+
import { artifactRootResolver } from './memory/project/artifact-root-resolver.js';
|
|
11
|
+
import { originOf } from './portal/session/session-manager.js';
|
|
12
|
+
import { Bridge } from './portal/bridge/bridge.js';
|
|
13
|
+
import { FlowStore } from './language/flows/flows.js';
|
|
14
|
+
import { makeJournalAttach } from './memory/journal/attach-journal.js';
|
|
15
|
+
import { makeSessionEnd, recordDriveRun } from './memory/journal/session-end.js';
|
|
16
|
+
import { attachDriveRunFlush } from './memory/journal/drive-run-flush.js';
|
|
17
|
+
import { AmbientStore } from './memory/journal/ambient-store.js';
|
|
18
|
+
import { ensureWorkspaceGitignore } from './memory/journal/on-disk/workspace-gitignore.js';
|
|
19
|
+
import { pruneWorkspace, } from './memory/journal/on-disk/startup-maintenance.js';
|
|
20
|
+
import { dirname } from 'node:path';
|
|
21
|
+
import { findProjectConfig } from './command/cli/ports/resolve/cli-port.js';
|
|
22
|
+
import { readBackgroundTraffic } from './portal/session/facts/background-traffic.js';
|
|
23
|
+
/**
|
|
24
|
+
* Wire journal capture, ambient seeding and the journal-tail flush onto a bridge.
|
|
25
|
+
*
|
|
26
|
+
* Both entry points need all three, and both used to hand-roll them. `startDaemon` only ever wired the
|
|
27
|
+
* first, so on the path every user actually takes (`reticle serve` / `reticle mcp`) the journal tail was
|
|
28
|
+
* dropped at session end and the learned ambient map was never persisted OR seeded — meaning ambient
|
|
29
|
+
* learning could not converge across sessions and the last events of every session were lost. The two
|
|
30
|
+
* call sites had already drifted once before, which is why this is one function rather than a
|
|
31
|
+
* copy-paste both are asked to keep in step.
|
|
32
|
+
*/
|
|
33
|
+
export function attachJournal(bridge, deps) {
|
|
34
|
+
const journalAttach = makeJournalAttach(deps);
|
|
35
|
+
const ambientStore = new AmbientStore(deps.fs, deps.reticleRoot);
|
|
36
|
+
// Built once, not per session: the resolver walks config discovery and the user-level registry,
|
|
37
|
+
// and neither changes between two tabs connecting a second apart.
|
|
38
|
+
const resolveArtifactRoot = artifactRootResolver(deps.reticleRoot);
|
|
39
|
+
bridge.attachSessionCreate((session) => {
|
|
40
|
+
// Stamp the project's own `.reticle` before ANY counter fires for this session. Without it every
|
|
41
|
+
// verdict is recorded against wherever the daemon was started, which is how one app's evidence
|
|
42
|
+
// reached a different account's production dashboard.
|
|
43
|
+
// The origin is passed for the case where the page never stamped a project id: it is the only
|
|
44
|
+
// distinguishing fact left, and without it every such app shares one bucket.
|
|
45
|
+
session.artifactRoot = resolveArtifactRoot(session.projectId, originOf(session.url)).root;
|
|
46
|
+
// The project's config sits beside its `.reticle/`, so the declaration is read from the tab's
|
|
47
|
+
// own project rather than from wherever the daemon was started.
|
|
48
|
+
session.background = readBackgroundTraffic(findProjectConfig(dirname(session.artifactRoot)));
|
|
49
|
+
// Here rather than on the start path, which was neither the moment we were about to write into a
|
|
50
|
+
// repository nor the root we were about to write into: it created `.reticle/` — holding nothing
|
|
51
|
+
// but the ignore file — wherever the daemon was launched, coming back every boot after the user
|
|
52
|
+
// deleted it, while the journals this ignore protects landed in another tree, uncovered.
|
|
53
|
+
if (deps.enabled)
|
|
54
|
+
void ensureWorkspaceGitignore(deps.fs, session.artifactRoot);
|
|
55
|
+
journalAttach(session);
|
|
56
|
+
// Seed the learned ambient map so a fresh session starts knowing which regions churn, instead of
|
|
57
|
+
// re-learning from zero. Best-effort + async: a late seed still helps, a failure is silent.
|
|
58
|
+
if (deps.enabled) {
|
|
59
|
+
void ambientStore
|
|
60
|
+
.load()
|
|
61
|
+
.then((counts) => session.seedAmbient(counts))
|
|
62
|
+
.catch(() => undefined);
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
// Teardown: flush the journal tail to disk + persist what this session learned.
|
|
66
|
+
bridge.attachSessionEnd(makeSessionEnd({
|
|
67
|
+
...deps,
|
|
68
|
+
...(deps.retain === undefined ? {} : { retain: deps.retain }),
|
|
69
|
+
// Retention runs from teardown, and it must not delete the journal of a session that is still
|
|
70
|
+
// being written. The registry is the only thing that knows which those are.
|
|
71
|
+
liveSessionIds: () => new Set(bridge.sessions.all().map((s) => s.id)),
|
|
72
|
+
}));
|
|
73
|
+
/*
|
|
74
|
+
* And the same write, DURING the session rather than only at the end of it.
|
|
75
|
+
*
|
|
76
|
+
* Teardown was the only writer of a run artifact, so every verdict produced while a tab stayed
|
|
77
|
+
* open was invisible to cloud sync — which can only push artifacts that exist. Reported as "sync
|
|
78
|
+
* is not happening" and diagnosed as the agent forgetting to sync; there is no sync command to
|
|
79
|
+
* forget. The evidence simply was not on disk yet.
|
|
80
|
+
*
|
|
81
|
+
* Safe to call repeatedly because the run id is derived from the session, so this rewrites one
|
|
82
|
+
* artifact rather than accumulating them — a property `recordDriveRun` already had, for the
|
|
83
|
+
* unrelated reason that a reconnecting tab must not publish two overlapping rows.
|
|
84
|
+
*/
|
|
85
|
+
if (deps.enabled) {
|
|
86
|
+
attachDriveRunFlush({
|
|
87
|
+
resolve: (sessionId) => bridge.sessions.get(sessionId),
|
|
88
|
+
write: (session) => recordDriveRun(deps, session),
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
// One call, because "what maintenance runs at startup" needs one answer. Inlined here, the byte
|
|
92
|
+
// budget was simply missing — written, tested, and called by nothing, while the three count
|
|
93
|
+
// bounds beside it ran every time. See startup-maintenance.ts.
|
|
94
|
+
//
|
|
95
|
+
// Deliberately NOT behind `deps.enabled`. Switching journalling off is what somebody does BECAUSE
|
|
96
|
+
// the directory got too big, and both sweep sites were gated on it, so that setting was the one
|
|
97
|
+
// under which nothing ever deleted what was already there. Visual diffs, feedback copies and run
|
|
98
|
+
// artifacts do not need the journal to be written at all and kept accumulating regardless.
|
|
99
|
+
void pruneWorkspace(deps.fs, deps.reticleRoot, new Set(bridge.sessions.all().map((s) => s.id)), deps.retain ?? {});
|
|
100
|
+
}
|
package/docs/README.md
CHANGED
|
@@ -72,6 +72,7 @@ Everything here is published to [docs.reticle.sh](https://docs.reticle.sh) by `d
|
|
|
72
72
|
| [vs-playwright-mcp.mdx](vs-playwright-mcp.mdx) | outside-in vs inside-out, and when Playwright is right |
|
|
73
73
|
| [vs-chrome-devtools-mcp.mdx](vs-chrome-devtools-mcp.mdx) | cheaper per look, catches less: the trade, measured |
|
|
74
74
|
| [vs-screenshots.mdx](vs-screenshots.mdx) | why a better vision model does not fix a non-visual bug |
|
|
75
|
+
| [what-is-recorded.md](what-is-recorded.md) | what Reticle keeps, what is redacted, and what it takes for any of it to leave your machine |
|
|
75
76
|
| [telemetry.md](telemetry.md) | what is collected, and how to turn it off |
|
|
76
77
|
| [local-registry.md](local-registry.md) | installing an unpublished build |
|
|
77
78
|
| [license-activation.mdx](license-activation.mdx) | you have a key: where to set it, how to confirm it, what to do when it fails |
|
package/docs/cli/cloud.mdx
CHANGED
|
@@ -101,7 +101,9 @@ reticle: no .reticle/cloud.json here ... run `reticle link` first
|
|
|
101
101
|
|
|
102
102
|
### `reticle push`
|
|
103
103
|
|
|
104
|
-
Sends local run artifacts from `.reticle` to the linked project, honoring the sync policy. With `sync.runs` off it does nothing and says so. Prints `{
|
|
104
|
+
Sends local run artifacts from `.reticle` to the linked project, honoring the sync policy. With `sync.runs` off it does nothing and says so. Prints `{ ok, project, sent, pulled }`, where `sent` carries the accepted counts and, when the server refused anything, a `rejected` array naming each artifact's index and reason.
|
|
105
|
+
|
|
106
|
+
**A rejection is a failed push.** If the server refuses any artifact, `ok` is `false` and the command exits `1`, even when other artifacts in the same bundle landed. A refused artifact never reaches the dashboard and is re-offered on every cycle, so partial loss is still loss. `ok: true` and exit `0` mean everything offered was accepted, which includes the common case of a repo that was already up to date. The accepted counts still report what did land, so a partial push is visible as one.
|
|
105
107
|
|
|
106
108
|
### `reticle runs`
|
|
107
109
|
|
|
@@ -133,12 +135,12 @@ reticle: cloud not attached here ... run `reticle link` (or set RETICLE_CLOUD_UR
|
|
|
133
135
|
|
|
134
136
|
## Exit codes
|
|
135
137
|
|
|
136
|
-
| Code | Meaning
|
|
137
|
-
|
|
|
138
|
-
| `0`
|
|
139
|
-
| `1`
|
|
140
|
-
| `2`
|
|
141
|
-
| `3`
|
|
138
|
+
| Code | Meaning |
|
|
139
|
+
| --- | --- |
|
|
140
|
+
| `0` | Success |
|
|
141
|
+
| `1` | The API call failed, the repository is not attached where attachment is required, or `push` had an artifact refused |
|
|
142
|
+
| `2` | Usage error, or not signed in and no `RETICLE_API_KEY` |
|
|
143
|
+
| `3` | `regression` only: at least one flow broke |
|
|
142
144
|
|
|
143
145
|
Output is pretty-printed JSON on stdout. Errors and next-step nudges go to stderr, so an agent parsing stdout is unaffected.
|
|
144
146
|
|
package/docs/cli/init.mdx
CHANGED
|
@@ -19,6 +19,7 @@ npx @reticlehq/server init [--dry-run] [--port N] [--no-mcp] [--no-install] [--a
|
|
|
19
19
|
| `--no-mcp` | boolean | `false` (MCP is registered) | Skip the MCP registration **and** the agent rule files (`CLAUDE.md`, `AGENTS.md`, `.cursor`) and the `/reticle` command |
|
|
20
20
|
| `--no-install` | boolean | `false` (deps are installed) | Do not run the install. The step becomes `[⚠]` and prints the `add -D` command for you to run by hand, rather than vanishing from the plan |
|
|
21
21
|
| `--app <dir>` | string | unset | Pick which app to wire in a monorepo, without changing directory |
|
|
22
|
+
| `--hooks` | boolean | `false` | Also add a print-only Stop hook to `.claude/settings.json`, merged into what is there. When the agent stops without a claim that held, it prints one line; it never blocks. See [`reticle report`](/cli/report) |
|
|
22
23
|
| `--yes` | boolean | `false` | Accepted for scripting. `init` has no interactive prompts today, so it is a no-op |
|
|
23
24
|
|
|
24
25
|
<Warning>
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: reticle report
|
|
3
|
+
description: What the latest session claimed, and what held. Also a print-only Stop hook for Claude Code.
|
|
4
|
+
icon: clipboard-check
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
`reticle report` reads this project's most recent session from `.reticle/` and says how many of its claims held, how many failed, how many came back `unknown` and who can act on each, and how many proved nothing. It reads the journal on disk, so no daemon has to be running.
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npx @reticlehq/server report [--session <id>] [--hook]
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Arguments and flags
|
|
14
|
+
|
|
15
|
+
| Flag | Default | What it does |
|
|
16
|
+
| --- | --- | --- |
|
|
17
|
+
| `--session <id>` | the newest session | Report a specific session |
|
|
18
|
+
| `--hook` | off | Print one line, and only when the working tree changed and no claim held |
|
|
19
|
+
|
|
20
|
+
## What it prints
|
|
21
|
+
|
|
22
|
+
```text
|
|
23
|
+
session lease-85f8191b-3874-457b-973b-8ee365e5acfb
|
|
24
|
+
2 of 4 claims held
|
|
25
|
+
1 failed, 1 of them a green a channel contradicted
|
|
26
|
+
1 unknown (environment: 1)
|
|
27
|
+
✗ signal order:placed
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
An `unknown` is never counted as held. It is Reticle saying it could not tell, and the owner beside it says what to do next.
|
|
31
|
+
|
|
32
|
+
## As a Claude Code Stop hook
|
|
33
|
+
|
|
34
|
+
With `--hook` it prints nothing when a claim held or nothing in the tree changed, and otherwise one line, such as `Reticle: not verified. 3 claims: 1 failed, 2 unknown`. It prints Claude Code's `systemMessage` shape and always exits `0`: you see the line when the agent stops, and the agent is never blocked by it. `reticle init --hooks` adds it for you, merged into whatever `.claude/settings.json` already holds. By hand:
|
|
35
|
+
|
|
36
|
+
```json
|
|
37
|
+
{
|
|
38
|
+
"hooks": {
|
|
39
|
+
"Stop": [{ "hooks": [{ "type": "command", "command": "npx @reticlehq/server report --hook" }] }]
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
A session last written more than an hour ago does not count as this turn, so a morning drive cannot answer for an afternoon edit. If you want the stop blocked until the change is verified, use [`reticle gate --hook`](/cli/gate) instead.
|
package/docs/desktop.mdx
CHANGED
package/docs/flows.md
CHANGED
|
@@ -87,6 +87,16 @@ reticle_flow {action:"load"}({ flowName: "create-task" }) // → the flow JSON
|
|
|
87
87
|
reticle_flow_replay({ flowName: "create-task" }) // re-resolve each anchor against the LIVE DOM, run it
|
|
88
88
|
```
|
|
89
89
|
|
|
90
|
+
## Delete a flow
|
|
91
|
+
|
|
92
|
+
```jsonc
|
|
93
|
+
reticle_flow {action:"delete"}({ flowName: "create-task" }) // → { deleted: true }
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
A renamed or obsolete flow otherwise lingers in `reticle_flow {action:"list"}` and in every `reticle_verify {action:"flows"}` suite run, where it fails forever against a screen nobody intends to keep.
|
|
97
|
+
|
|
98
|
+
**Deleting a flow that is not there is an error, not a no-op.** It answers `{ error, code: "not_found" }` rather than `{ deleted: true }`, so a mistyped name cannot read as a completed cleanup while the real flow stays in the suite. Check the spelling against `reticle_flow {action:"list"}` and try again.
|
|
99
|
+
|
|
90
100
|
**Watch it replay on the page.** When the presenter is on (`present: true`), a replay isn't silent. Each step drives the real page, so the synthetic cursor flies to the element, the focus ring lands, and the activity log streams the journey live. You (or a teammate) literally watch the saved journey re-walk itself on your app, then see the verdict land. It's the fastest way to _see_ that a flow still works, not just read a green checkmark.
|
|
91
101
|
|
|
92
102
|
`reticle_flow_replay` returns a status:
|
|
@@ -164,6 +174,7 @@ With `apply: false` the flow file is **never modified**; you get the proposed di
|
|
|
164
174
|
| `reticle_flow_save_recorded` | `{ flowName? }` | persist a human-recorded (toolbar) flow |
|
|
165
175
|
| `reticle_flow {action:"list"}` | `{}` | flows on disk |
|
|
166
176
|
| `reticle_flow {action:"load"}` | `{ flowName }` | the flow JSON |
|
|
177
|
+
| `reticle_flow {action:"delete"}` | `{ flowName }` | `{ deleted: true }`, or `{ error, code }` (`not_found` when no such flow) |
|
|
167
178
|
| `reticle_flow_replay` | `{ flowName }` | `{ status, steps, decision? }` (decision on drift/fail) |
|
|
168
179
|
| `reticle_verify {action:"flows"}` | `{ names?, sessionId? }` | suite verdict `{ status, passed, failed, failures[] }` |
|
|
169
180
|
| `reticle_verify { action: "heal" }` | `{ flowName, apply? }` | propose / apply nearest-match rebind |
|
package/docs/frameworks.mdx
CHANGED
|
@@ -35,6 +35,7 @@ Two columns say different things and it is worth keeping them apart. **CI gate**
|
|
|
35
35
|
| **Plain HTML** | yes | no | no | no | Wired, unverified |
|
|
36
36
|
| **Vue (non-Nuxt)** | as Vite | no | install only | no | Install gated, drive unverified |
|
|
37
37
|
| **Preact** | as Vite | no | no | unproven | Sensor, React adapter via `preact/compat` |
|
|
38
|
+
| **Server-rendered (Django, Flask, FastAPI, Streamlit, Rails)** | yes | no | no | no | Wired, unverified |
|
|
38
39
|
|
|
39
40
|
<Warning>
|
|
40
41
|
"Wired, unverified" means exactly that. The recipe exists and was written from a real integration,
|
|
@@ -85,7 +86,41 @@ module.exports = withReticle({/* your config */});
|
|
|
85
86
|
|
|
86
87
|
## Remix and Astro
|
|
87
88
|
|
|
88
|
-
|
|
89
|
+
Remix uses the Vite plugin with `inject: false`: Remix SSRs its own HTML, so the plugin's connect-injection does not fire, but the stamping half still puts `file:line` on a verdict.
|
|
90
|
+
|
|
91
|
+
Astro is the same on the plugin (wire it with `inject: false` if you want source pointers on JSX islands) and uses a different channel for the pairing token. `vite.define` does not reach Astro's client pipeline on 7.2+, so the identifier stays literal and the bridge refuses the dial. `init` reads the token in frontmatter and puts it on a `<meta name="reticle-pairing-token">`. Reading it per request also means a token written after the dev server started is picked up on the next load, with no restart.
|
|
92
|
+
|
|
93
|
+
Use a processed page `<script>` with a static SDK import. Vite owns that module graph from the first transform:
|
|
94
|
+
|
|
95
|
+
```astro
|
|
96
|
+
---
|
|
97
|
+
import { readFileSync } from 'node:fs';
|
|
98
|
+
import { homedir } from 'node:os';
|
|
99
|
+
import { join } from 'node:path';
|
|
100
|
+
const pairingToken = (() => {
|
|
101
|
+
if (!import.meta.env.DEV) return '';
|
|
102
|
+
const dir = process.env['RETICLE_PAIRING_TOKEN_DIR'] || join(homedir(), '.reticle');
|
|
103
|
+
try { return readFileSync(join(dir, 'pairing-token'), 'utf8').trim(); } catch { return ''; }
|
|
104
|
+
})();
|
|
105
|
+
const pairingRoot = import.meta.env.DEV ? process.cwd() : '';
|
|
106
|
+
---
|
|
107
|
+
<meta name="reticle-pairing-token" content={pairingToken} />
|
|
108
|
+
<meta name="reticle-pairing-root" content={pairingRoot} />
|
|
109
|
+
<script>
|
|
110
|
+
import { reticle, install } from '@reticlehq/react';
|
|
111
|
+
if (import.meta.env.DEV) {
|
|
112
|
+
const token = document.querySelector('meta[name="reticle-pairing-token"]')?.getAttribute('content') ?? '';
|
|
113
|
+
const root = document.querySelector('meta[name="reticle-pairing-root"]')?.getAttribute('content') ?? '';
|
|
114
|
+
install();
|
|
115
|
+
reticle.connect({
|
|
116
|
+
...(0 < token.length ? { token } : {}),
|
|
117
|
+
...(0 < root.length ? { root } : {}),
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
</script>
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
The `<script>` tag must not sit inside a conditional. Astro hoists them statically, so `{isDev && (<script />)}` is never hoisted. `init` writes the same `<meta>` tags and a local `src/components/ReticleDev.ts` that the script statically imports. It also raises `vite.build.target` to `es2022` (Astro's default down-levels the SDK), declares the SDK in `optimizeDeps.include`, and keeps `.reticle/` out of the watcher. Static `.astro` markup still gets DOM, network and routing without the React integration.
|
|
89
124
|
|
|
90
125
|
## SvelteKit
|
|
91
126
|
|
|
@@ -205,6 +240,83 @@ The same shape works for webpack, Parcel, and the Vue and Svelte CLIs.
|
|
|
205
240
|
|
|
206
241
|
Keep it out of your production template. Reticle self-disables when the build reports `NODE_ENV=production`, but a second lock on a door leading to your users is a reasonable number.
|
|
207
242
|
|
|
243
|
+
## Server-rendered apps (Django, Flask, FastAPI, Rails)
|
|
244
|
+
|
|
245
|
+
There is no JS build to import the SDK from, and no single template to paste a script tag into: an app has many templates and may not have a base one. The SDK loads from a URL instead, so a page with no build step is not a blocker.
|
|
246
|
+
|
|
247
|
+
`reticle init` detects these and prints the right thing for the stack it found, writing `.reticle.json` as it goes. What follows is the same wiring, written down so you can read it before running anything, or wire it by hand.
|
|
248
|
+
|
|
249
|
+
### Django
|
|
250
|
+
|
|
251
|
+
Middleware is the Django-shaped answer: one place, runs for every page, no template to find.
|
|
252
|
+
|
|
253
|
+
```python
|
|
254
|
+
# reticle_dev.py: add to MIDDLEWARE while DEBUG is on:
|
|
255
|
+
# MIDDLEWARE = [..., "reticle_dev.ReticleDevMiddleware"]
|
|
256
|
+
from django.conf import settings
|
|
257
|
+
|
|
258
|
+
_SNIPPET = """<script type="module">
|
|
259
|
+
import { reticle } from 'https://cdn.jsdelivr.net/npm/@reticlehq/browser@<version>/+esm';
|
|
260
|
+
reticle.connect({ projectId: '<your-project-id>', token: '<pairing token>' });
|
|
261
|
+
</script>"""
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
class ReticleDevMiddleware:
|
|
265
|
+
"""Inject the Reticle SDK into HTML responses during development only."""
|
|
266
|
+
|
|
267
|
+
def __init__(self, get_response):
|
|
268
|
+
self.get_response = get_response
|
|
269
|
+
|
|
270
|
+
def __call__(self, request):
|
|
271
|
+
response = self.get_response(request)
|
|
272
|
+
if not settings.DEBUG:
|
|
273
|
+
return response
|
|
274
|
+
if "text/html" not in response.get("Content-Type", ""):
|
|
275
|
+
return response
|
|
276
|
+
# Streaming responses have no .content to rewrite; leave them alone.
|
|
277
|
+
if getattr(response, "streaming", False):
|
|
278
|
+
return response
|
|
279
|
+
body = response.content.decode(response.charset)
|
|
280
|
+
if "</body>" not in body:
|
|
281
|
+
return response
|
|
282
|
+
response.content = body.replace("</body>", _SNIPPET + "</body>", 1).encode(
|
|
283
|
+
response.charset
|
|
284
|
+
)
|
|
285
|
+
if response.has_header("Content-Length"):
|
|
286
|
+
response["Content-Length"] = str(len(response.content))
|
|
287
|
+
return response
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
`reticle init` emits this with your real version, project id and pairing token filled in. Copy it from there rather than from this page: the placeholders above are the three things that have to be right.
|
|
291
|
+
|
|
292
|
+
Four details in that middleware are load-bearing, and each is there because the obvious version is wrong:
|
|
293
|
+
|
|
294
|
+
- **`settings.DEBUG`** is the guard. Reticle is dev-only, and this is the lock that cannot be forgotten at deploy time.
|
|
295
|
+
- **the `text/html` check** keeps it from corrupting a JSON API response or a file download.
|
|
296
|
+
- **the `streaming` check** exists because a `StreamingHttpResponse` has no `.content` to rewrite, and touching it raises.
|
|
297
|
+
- **`</body>`, not `<head>`** so the SDK sees a parsed document.
|
|
298
|
+
|
|
299
|
+
Re-setting `Content-Length` matters for the same reason: the body grew, and a stale length truncates the page in the browser rather than failing loudly.
|
|
300
|
+
|
|
301
|
+
### Flask and FastAPI with Jinja
|
|
302
|
+
|
|
303
|
+
Same shape, one hook instead of a middleware class: an `@app.after_request` in Flask, or a middleware in FastAPI, with the same four guards: a debug flag, `text/html` only, not streaming, and injected before `</body>`.
|
|
304
|
+
|
|
305
|
+
### Streamlit
|
|
306
|
+
|
|
307
|
+
Different again: Streamlit owns the top-level document, so there is no served template, and scripts passed to `st.markdown` do not run. Use `st.html` on Streamlit 1.63 or newer. `reticle init` prints the helper, which also guards against connecting twice across reruns.
|
|
308
|
+
|
|
309
|
+
### What you get, and what you do not
|
|
310
|
+
|
|
311
|
+
Everything that comes from reading the live page works: queries, snapshots, actions, network and console channels, verdicts.
|
|
312
|
+
|
|
313
|
+
What is missing is the **source pointer**. `file:line` on a verdict comes from the build plugin stamping `data-reticle-source`, and there is no build step here to stamp it. You get every other signal and no `source` field, the same way a Vue single-file component does.
|
|
314
|
+
|
|
315
|
+
<Warning>
|
|
316
|
+
Wired, unverified. No app in CI drives one of these to a verdict, so nothing will tell us if it
|
|
317
|
+
breaks. If a page does not register a session, that is worth reporting.
|
|
318
|
+
</Warning>
|
|
319
|
+
|
|
208
320
|
## Desktop is a different shape
|
|
209
321
|
|
|
210
322
|
Electron and Tauri invert the direction: your app dials the daemon, and there is no URL for an agent to open. Reticle also reaches the main-process and Rust IPC boundary, which the renderer cannot see.
|
package/docs/install-agentic.mdx
CHANGED
|
@@ -150,6 +150,42 @@ Four files in your project, none of them mysterious.
|
|
|
150
150
|
```
|
|
151
151
|
The `projectId` is how Reticle scopes sessions to this project, so two apps running at once
|
|
152
152
|
don't get confused for each other.
|
|
153
|
+
|
|
154
|
+
**Keeping `.reticle/` small.** Reticle bounds the directory for you, by count and by total size.
|
|
155
|
+
Add a `retain` block to change what it keeps:
|
|
156
|
+
|
|
157
|
+
```json
|
|
158
|
+
{
|
|
159
|
+
"retain": { "sessions": 20, "visual": 10, "feedback": 20, "budgetMb": 512 }
|
|
160
|
+
}
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
Those are the defaults, and every field is optional. `sessions` is how many session journals
|
|
164
|
+
survive, `visual` how many overlay diffs (pixel baselines are never deleted, because the next
|
|
165
|
+
comparison needs them), `feedback` how many local copies of reports already sent, and `budgetMb`
|
|
166
|
+
the total the whole evidence tier may occupy. `0` means keep none, and `"sessions": 0` also stops
|
|
167
|
+
journals being written in the first place.
|
|
168
|
+
|
|
169
|
+
Saved flows, capsules, baselines, the contract and the intent ledger are not touched by any of
|
|
170
|
+
this. They are the part meant to be committed, and a bound that deleted them would be deleting
|
|
171
|
+
the regression checks your team shares.
|
|
172
|
+
|
|
173
|
+
**Your app's own background calls.** An analytics or heartbeat endpoint on your app's own host
|
|
174
|
+
looks exactly like the app's work, so Reticle never guesses. If one of them keeps a verdict
|
|
175
|
+
waiting, or fails and gets blamed on an unrelated action, name it:
|
|
176
|
+
|
|
177
|
+
```json
|
|
178
|
+
{
|
|
179
|
+
"background": ["/api/analytics/events", "/api/heartbeat"]
|
|
180
|
+
}
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
Each entry is matched the way `urlContains` is. A listed request stops holding the wait open and
|
|
184
|
+
stops contradicting a claim; when a contradiction is still reported, it lists what was set aside.
|
|
185
|
+
It is never hidden from an assertion that names it: `net { urlContains: "/api/heartbeat" }` still
|
|
186
|
+
sees it. Keep entries specific: `/api` would set aside your whole backend.
|
|
187
|
+
Third-party calls (another site's beacon or SDK) need no entry; they are already set aside.
|
|
188
|
+
|
|
153
189
|
</Accordion>
|
|
154
190
|
|
|
155
191
|
<Accordion title="vite.config.ts (the plugin)">
|
package/docs/install-manual.mdx
CHANGED
package/docs/packages/tauri.mdx
CHANGED
|
@@ -21,7 +21,7 @@ Tauri IPC observation needs nothing on the Rust side. An `invoke('load_todos')`
|
|
|
21
21
|
|
|
22
22
|
```toml
|
|
23
23
|
[dependencies]
|
|
24
|
-
reticle-tauri = "3.
|
|
24
|
+
reticle-tauri = "3.3"
|
|
25
25
|
```
|
|
26
26
|
|
|
27
27
|
Its own dependencies are `tauri = "2"` plus the per-platform capture backends: `objc2` and friends on macOS, `webview2-com` and `windows` on Windows, `webkit2gtk` and `cairo-rs` on Linux and the BSDs.
|