@reticlehq/server 2.11.0 → 2.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/SKILL.md +26 -2
- package/dist/bridge/bridge.d.ts +0 -13
- package/dist/bridge/bridge.js +1 -1
- package/dist/bridge/bridge.test-harness.d.ts +2 -0
- package/dist/bridge/bridge.test-harness.js +8 -1
- package/dist/capsule/capsule-store.d.ts +2 -1
- package/dist/capsule/capsule-store.js +1 -1
- package/dist/capsule/causal-summary.d.ts +2 -2
- package/dist/chromium-launch-options.d.ts +28 -0
- package/dist/chromium-launch-options.js +29 -0
- package/dist/cli/cli-doctor.js +8 -0
- package/dist/cli/cli-flow-commands.d.ts +3 -1
- package/dist/cli/cli-flow-commands.js +34 -4
- package/dist/cli/cli-kill.d.ts +2 -1
- package/dist/cli/cli-parse.d.ts +3 -1
- package/dist/cli/cli-parse.js +29 -7
- package/dist/cli/cli-update-commands.js +20 -1
- package/dist/cli/cli-verify.d.ts +1 -1
- package/dist/cli/cli-verify.js +2 -2
- package/dist/cli/cloud-cli.js +12 -6
- package/dist/cli/config-discovery.d.ts +2 -1
- package/dist/cli/doctor-rows.d.ts +3 -1
- package/dist/cli/doctor-rows.js +3 -1
- package/dist/cli/gate-exit.d.ts +21 -0
- package/dist/cli/gate-exit.js +21 -0
- package/dist/cli/gate-hook-message.d.ts +33 -0
- package/dist/cli/gate-hook-message.js +46 -0
- package/dist/cli/mcp-command.d.ts +25 -0
- package/dist/cli/mcp-command.js +143 -0
- package/dist/cli/sibling-ports.d.ts +37 -0
- package/dist/cli/sibling-ports.js +56 -0
- package/dist/cli.js +37 -110
- package/dist/cloud/cloud-config.d.ts +2 -1
- package/dist/cloud/cloud-sync.d.ts +11 -0
- package/dist/cloud/cloud-sync.js +32 -0
- package/dist/crawl/crawl.d.ts +9 -13
- package/dist/crawl/crawl.js +20 -3
- package/dist/daemon/daemon-resolve.d.ts +79 -0
- package/dist/daemon/daemon-resolve.js +118 -0
- package/dist/daemon/free-port.d.ts +29 -0
- package/dist/daemon/free-port.js +55 -0
- package/dist/daemon/heartbeat.d.ts +0 -4
- package/dist/daemon/heartbeat.js +2 -2
- package/dist/dev/stale-issue-guard.d.ts +0 -2
- package/dist/dev/stale-issue-guard.js +1 -1
- package/dist/events/contradictions.d.ts +41 -0
- package/dist/events/contradictions.js +125 -12
- package/dist/events/declared.d.ts +14 -0
- package/dist/events/declared.js +33 -0
- package/dist/events/echo-mismatch.js +14 -0
- package/dist/events/observed-in-window.d.ts +12 -0
- package/dist/events/observed-in-window.js +17 -0
- package/dist/events/predicate-asks.d.ts +13 -0
- package/dist/events/predicate-asks.js +22 -0
- package/dist/events/predicate-eval.js +64 -22
- package/dist/events/predicate-parse.d.ts +0 -6
- package/dist/events/predicate-parse.js +43 -3
- package/dist/events/predicate-schema.d.ts +44 -0
- package/dist/events/predicate-schema.js +126 -23
- package/dist/events/predicate.d.ts +15 -0
- package/dist/events/predicate.js +41 -2
- package/dist/flows/coverage.d.ts +0 -8
- package/dist/flows/coverage.js +1 -1
- package/dist/flows/decision.d.ts +5 -1
- package/dist/flows/decision.js +33 -7
- package/dist/flows/flake.d.ts +0 -2
- package/dist/flows/flake.js +1 -1
- package/dist/flows/flow-intent.d.ts +52 -0
- package/dist/flows/flow-intent.js +123 -0
- package/dist/flows/flow-replay-run.js +27 -4
- package/dist/flows/flow-success.js +15 -3
- package/dist/flows/flow-tools.js +32 -4
- package/dist/flows/flows.d.ts +6 -1
- package/dist/flows/flows.js +34 -22
- package/dist/flows/predicate-to-expect.js +6 -3
- package/dist/flows/server-verify.js +16 -2
- package/dist/flows/verify-change-tools.js +4 -1
- package/dist/honesty/blind-spots.d.ts +0 -17
- package/dist/honesty/blind-spots.js +1 -1
- package/dist/honesty/feature-capture.d.ts +150 -0
- package/dist/honesty/feature-capture.js +233 -0
- package/dist/honesty/instrumentation-gaps.d.ts +30 -6
- package/dist/honesty/instrumentation-gaps.js +61 -5
- package/dist/honesty/tool-hit-rate.d.ts +86 -0
- package/dist/honesty/tool-hit-rate.js +90 -0
- package/dist/honesty/undeclared-change.d.ts +51 -0
- package/dist/honesty/undeclared-change.js +56 -0
- package/dist/honesty/verified.d.ts +15 -0
- package/dist/honesty/verified.js +68 -4
- package/dist/http-server.d.ts +0 -1
- package/dist/http-server.js +1 -1
- package/dist/hunt/hunt-report.d.ts +0 -2
- package/dist/hunt/hunt-report.js +1 -1
- package/dist/impact/impact-store.d.ts +0 -5
- package/dist/impact/impact-store.js +1 -1
- package/dist/index.d.ts +4 -1
- package/dist/init/agent-rules.d.ts +17 -0
- package/dist/init/agent-rules.js +31 -1
- package/dist/init/capabilities.d.ts +0 -2
- package/dist/init/capabilities.js +1 -1
- package/dist/init/closing-hint.d.ts +36 -0
- package/dist/init/closing-hint.js +120 -0
- package/dist/init/confirm.d.ts +0 -25
- package/dist/init/confirm.js +3 -3
- package/dist/init/cra.d.ts +14 -1
- package/dist/init/cra.js +43 -12
- package/dist/init/dev-script.d.ts +48 -0
- package/dist/init/dev-script.js +63 -0
- package/dist/init/mcp.d.ts +16 -5
- package/dist/init/mcp.js +26 -10
- package/dist/init/plan-framework.d.ts +0 -5
- package/dist/init/plan-framework.js +83 -9
- package/dist/init/plan.d.ts +10 -30
- package/dist/init/plan.js +30 -15
- package/dist/init/refresh-rules.d.ts +39 -0
- package/dist/init/refresh-rules.js +51 -0
- package/dist/init/run.d.ts +0 -26
- package/dist/init/run.js +14 -87
- package/dist/init/snippets.js +53 -10
- package/dist/input/real-input.d.ts +2 -0
- package/dist/input/real-input.js +3 -2
- package/dist/input/scroll-find.d.ts +7 -4
- package/dist/input/scroll-find.js +63 -25
- package/dist/intent/inline-intent.d.ts +46 -0
- package/dist/intent/inline-intent.js +103 -0
- package/dist/intent/intent-store.d.ts +37 -0
- package/dist/intent/intent-store.js +112 -0
- package/dist/intent/intent-tools.d.ts +2 -0
- package/dist/intent/intent-tools.js +80 -0
- package/dist/intent/open-intents.d.ts +21 -0
- package/dist/intent/open-intents.js +25 -0
- package/dist/journal/envelope.d.ts +26 -26
- package/dist/journal/journal-recorder.d.ts +23 -0
- package/dist/journal/journal-recorder.js +35 -0
- package/dist/mcp/attach-memory.d.ts +67 -0
- package/dist/mcp/attach-memory.js +142 -0
- package/dist/mcp/mcp-proxy.d.ts +0 -7
- package/dist/mcp/mcp-proxy.js +14 -2
- package/dist/mcp/mcp.d.ts +2 -0
- package/dist/mcp/mcp.js +45 -23
- package/dist/mcp/server-instructions.js +45 -3
- package/dist/mcp/tool-catalog-cache.d.ts +8 -0
- package/dist/mcp/tool-catalog-cache.js +10 -0
- package/dist/oracles/flow-instrument-gaps.d.ts +4 -4
- package/dist/oracles/flow-instrument-gaps.js +4 -5
- package/dist/oracles/self-instrument.d.ts +9 -9
- package/dist/oracles/self-instrument.js +19 -13
- package/dist/pool/browser-pool.d.ts +12 -1
- package/dist/pool/browser-pool.js +31 -1
- package/dist/pool/playwright-launcher.js +3 -1
- package/dist/project/fs-port.d.ts +3 -0
- package/dist/project/fs-port.js +3 -2
- package/dist/project/memory-fs.js +6 -1
- package/dist/project/project-tools.js +3 -32
- package/dist/project/reticle-dir.d.ts +2 -0
- package/dist/project/reticle-dir.js +1 -0
- package/dist/runs/context-tools.d.ts +2 -0
- package/dist/runs/context-tools.js +89 -0
- package/dist/runs/run-context.d.ts +40 -0
- package/dist/runs/run-context.js +158 -0
- package/dist/runs/run-diff.d.ts +2 -1
- package/dist/runs/verification-sync.js +2 -2
- package/dist/session/dev-server-probe.d.ts +0 -8
- package/dist/session/dev-server-probe.js +1 -1
- package/dist/session/last-act.d.ts +7 -1
- package/dist/session/last-act.js +2 -2
- package/dist/session/live-control-tools.js +1 -42
- package/dist/session/no-session-diagnosis.d.ts +10 -0
- package/dist/session/no-session-diagnosis.js +24 -7
- package/dist/session/no-session-next-action.d.ts +5 -0
- package/dist/session/no-session-next-action.js +35 -3
- package/dist/session/no-session-watch.d.ts +8 -0
- package/dist/session/no-session-watch.js +82 -64
- package/dist/session/output-budget.js +4 -1
- package/dist/session/session-health.d.ts +9 -0
- package/dist/session/session-health.js +23 -0
- package/dist/session/session-manager.js +50 -1
- package/dist/session/session-successor.d.ts +37 -0
- package/dist/session/session-successor.js +78 -0
- package/dist/session/session.d.ts +31 -1
- package/dist/session/session.js +75 -2
- package/dist/telemetry/automation-hint.d.ts +2 -1
- package/dist/telemetry/feedback-outbox.d.ts +2 -1
- package/dist/telemetry/project-profile.d.ts +0 -8
- package/dist/telemetry/project-profile.js +1 -1
- package/dist/telemetry/run-telemetry.d.ts +0 -2
- package/dist/telemetry/run-telemetry.js +1 -1
- package/dist/telemetry/verification-of.js +2 -2
- package/dist/tools/act-tools.js +126 -21
- package/dist/tools/assert-source.d.ts +22 -0
- package/dist/tools/assert-source.js +53 -0
- package/dist/tools/assert-verdict.d.ts +20 -3
- package/dist/tools/assert-verdict.js +36 -4
- package/dist/tools/browser-tools.js +0 -20
- package/dist/tools/complete-snapshot.d.ts +48 -0
- package/dist/tools/complete-snapshot.js +75 -0
- package/dist/tools/coverage-tools.d.ts +10 -1
- package/dist/tools/coverage-tools.js +123 -69
- package/dist/tools/dynamic-tools.js +29 -8
- package/dist/tools/error-recovery.d.ts +24 -14
- package/dist/tools/error-recovery.js +70 -9
- package/dist/tools/feedback-tools.d.ts +1 -1
- package/dist/tools/feedback-tools.js +2 -2
- package/dist/tools/invoke-tool.js +62 -2
- package/dist/tools/lease-hint.d.ts +10 -0
- package/dist/tools/lease-hint.js +53 -4
- package/dist/tools/lease-tools.d.ts +5 -1
- package/dist/tools/lease-tools.js +22 -8
- package/dist/tools/navigate-arrival.d.ts +0 -2
- package/dist/tools/navigate-arrival.js +1 -1
- package/dist/tools/observe-tools.js +63 -26
- package/dist/tools/read-tools.js +5 -3
- package/dist/tools/real-input-attempt.d.ts +22 -0
- package/dist/tools/real-input-attempt.js +153 -1
- package/dist/tools/reconcile-tools.js +18 -4
- package/dist/tools/resolve-target.d.ts +0 -7
- package/dist/tools/sdk-marker-probe.d.ts +29 -0
- package/dist/tools/sdk-marker-probe.js +57 -0
- package/dist/tools/snapshot-delta.d.ts +1 -1
- package/dist/tools/tool-kit.d.ts +12 -0
- package/dist/tools/tool-kit.js +16 -0
- package/dist/tools/tool-names.d.ts +3 -0
- package/dist/tools/tool-names.js +3 -0
- package/dist/tools/tool-surface.d.ts +85 -6
- package/dist/tools/tool-surface.js +122 -2
- package/dist/tools/tools.js +22 -4
- package/dist/tools/unadvertised-help.js +4 -4
- package/dist/tools/verify-next-baton.d.ts +53 -0
- package/dist/tools/verify-next-baton.js +52 -0
- package/dist/update/update-nudge.d.ts +7 -10
- package/dist/update/update-nudge.js +12 -3
- package/dist/update/updater.d.ts +1 -1
- package/docs/README.md +2 -1
- package/docs/cli/cloud.mdx +1 -1
- package/docs/cli/doctor.mdx +1 -0
- package/docs/cli/verify.mdx +4 -5
- package/docs/desktop.mdx +1 -1
- package/docs/docs.json +2 -2
- package/docs/gates.md +1 -1
- package/docs/install-manual.mdx +1 -1
- package/docs/packages/browser.mdx +2 -2
- package/docs/packages/server.mdx +1 -1
- package/docs/packages.mdx +8 -6
- package/docs/platform-integration.md +17 -0
- package/docs/predicates.mdx +8 -0
- package/docs/reticle-manual.md +106 -0
- package/docs/tools-tools-and-run.mdx +2 -4
- package/docs/troubleshooting.mdx +9 -0
- package/docs/usage.md +3 -2
- package/package.json +2 -2
- package/dist/session/session-readiness.d.ts +0 -34
- package/dist/session/session-readiness.js +0 -36
|
@@ -4,33 +4,39 @@
|
|
|
4
4
|
* apply instrumentation proposals as concrete diffs, so the AGENT closes the gap and the app's
|
|
5
5
|
* observability compounds run over run — instead of a human being a prerequisite. Deterministic mapping
|
|
6
6
|
* from a located gap to the code to insert; pure and testable. Result-enrichment wiring is a later step.
|
|
7
|
+
*
|
|
8
|
+
* Kinds come from `@reticlehq/core`. A second enum here is how `reticle_domain` used to say
|
|
9
|
+
* `missing-signal` while honesty said `no-signal-on-mutation` for the same absence.
|
|
7
10
|
*/
|
|
8
|
-
|
|
9
|
-
MISSING_SIGNAL: 'missing-signal',
|
|
10
|
-
UNREGISTERED_STORE: 'unregistered-store',
|
|
11
|
-
MISSING_TESTID: 'missing-testid',
|
|
12
|
-
};
|
|
11
|
+
import { InstrumentationGapKind } from '@reticlehq/core';
|
|
13
12
|
/** PascalCase a store name for a `useX.getState` hint (cart → Cart). */
|
|
14
13
|
function pascal(name) {
|
|
15
14
|
return 0 === name.length ? name : name[0]?.toUpperCase() + name.slice(1);
|
|
16
15
|
}
|
|
17
16
|
function insertFor(gap) {
|
|
18
17
|
switch (gap.kind) {
|
|
19
|
-
case InstrumentationGapKind.
|
|
18
|
+
case InstrumentationGapKind.NO_SIGNAL_ON_MUTATION:
|
|
20
19
|
return `reticle.signal('${gap.name}');`;
|
|
21
|
-
case InstrumentationGapKind.
|
|
20
|
+
case InstrumentationGapKind.NO_STORE_REGISTERED:
|
|
22
21
|
return `registerStore('${gap.name}', () => use${pascal(gap.name)}.getState());`;
|
|
23
22
|
case InstrumentationGapKind.MISSING_TESTID:
|
|
24
23
|
return `data-testid="${gap.name}"`;
|
|
25
24
|
}
|
|
26
25
|
}
|
|
27
26
|
function rationaleFor(gap) {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
27
|
+
let base;
|
|
28
|
+
switch (gap.kind) {
|
|
29
|
+
case InstrumentationGapKind.NO_SIGNAL_ON_MUTATION:
|
|
30
|
+
base = `emit a consequence signal so verification proves the outcome, not just presence`;
|
|
31
|
+
break;
|
|
32
|
+
case InstrumentationGapKind.NO_STORE_REGISTERED:
|
|
33
|
+
base = `register the store so state assertions can read the source of truth`;
|
|
34
|
+
break;
|
|
35
|
+
case InstrumentationGapKind.MISSING_TESTID:
|
|
36
|
+
base = `add a stable testid so the anchor survives refactors`;
|
|
37
|
+
break;
|
|
38
|
+
}
|
|
39
|
+
return undefined === gap.context ? base : `${base} — ${gap.context}`;
|
|
34
40
|
}
|
|
35
41
|
/** Turn located capability gaps into ready-to-apply instrumentation proposals. Deterministic. */
|
|
36
42
|
export function proposeInstrumentation(gaps) {
|
|
@@ -9,7 +9,6 @@
|
|
|
9
9
|
* The browser is injected via a `Launcher` so all lifecycle logic is testable without real Chromium;
|
|
10
10
|
* the thin Playwright adapter that satisfies these interfaces lives separately.
|
|
11
11
|
*/
|
|
12
|
-
/** The minimal page surface the pool drives. Real Playwright `Page` satisfies this. */
|
|
13
12
|
export interface PooledPage {
|
|
14
13
|
goto(url: string, opts?: {
|
|
15
14
|
timeoutMs?: number;
|
|
@@ -17,6 +16,12 @@ export interface PooledPage {
|
|
|
17
16
|
close(): Promise<void>;
|
|
18
17
|
/** Fires when THIS page's renderer crashes — lets the pool reclaim just this lease, not the fleet. */
|
|
19
18
|
onCrash(handler: () => void): void;
|
|
19
|
+
/**
|
|
20
|
+
* Fires for each console message the page logs. OPTIONAL: a fake that does not implement it makes
|
|
21
|
+
* the pool record nothing, which is the correct degradation — an absent dial address must read as
|
|
22
|
+
* "the page said nothing", never as "the page dialled correctly".
|
|
23
|
+
*/
|
|
24
|
+
onConsole?(handler: (text: string) => void): void;
|
|
20
25
|
}
|
|
21
26
|
/** An isolated browsing context (cookies/storage). Real Playwright `BrowserContext` satisfies this. */
|
|
22
27
|
export interface PooledContext {
|
|
@@ -114,6 +119,12 @@ export declare class BrowserPool {
|
|
|
114
119
|
signal?: AbortSignal;
|
|
115
120
|
sessionId?: string;
|
|
116
121
|
}): Promise<Lease>;
|
|
122
|
+
/**
|
|
123
|
+
* The websocket address this lease's page reported it could not reach, if it reported one.
|
|
124
|
+
*
|
|
125
|
+
* Undefined means the page said nothing — never that it dialled the right place.
|
|
126
|
+
*/
|
|
127
|
+
dialFailureUrl(sessionId: string): string | undefined;
|
|
117
128
|
/** Close every context and the browser. Pending waiters are rejected (the pool is terminal now). */
|
|
118
129
|
shutdown(): Promise<void>;
|
|
119
130
|
}
|
|
@@ -9,6 +9,8 @@
|
|
|
9
9
|
* The browser is injected via a `Launcher` so all lifecycle logic is testable without real Chromium;
|
|
10
10
|
* the thin Playwright adapter that satisfies these interfaces lives separately.
|
|
11
11
|
*/
|
|
12
|
+
/** The minimal page surface the pool drives. Real Playwright `Page` satisfies this. */
|
|
13
|
+
import { unreachableUrlIn } from '@reticlehq/core';
|
|
12
14
|
/** Default lease time-to-live: a lease untouched for this long is presumed orphaned and reclaimed. */
|
|
13
15
|
export const DEFAULT_LEASE_TTL_MS = 5 * 60_000;
|
|
14
16
|
/** Default cap on how long a lease's initial navigation may take before it fails (frees the slot). */
|
|
@@ -147,6 +149,7 @@ export class BrowserPool {
|
|
|
147
149
|
await this.#waitForSlot(opts.signal);
|
|
148
150
|
const sessionId = opts.sessionId ?? this.#genId();
|
|
149
151
|
let context;
|
|
152
|
+
let pending;
|
|
150
153
|
try {
|
|
151
154
|
const browser = await this.#ensureBrowser();
|
|
152
155
|
context = await browser.newContext();
|
|
@@ -156,6 +159,19 @@ export class BrowserPool {
|
|
|
156
159
|
page.onCrash(() => {
|
|
157
160
|
void this.#release(sessionId);
|
|
158
161
|
});
|
|
162
|
+
// Listen BEFORE goto: the SDK's connect attempt races the navigation's own resolution, and a
|
|
163
|
+
// listener attached afterwards misses the first failures — the only ones logged before the
|
|
164
|
+
// retry backoff stretches out.
|
|
165
|
+
page.onConsole?.((text) => {
|
|
166
|
+
const dialled = unreachableUrlIn(text);
|
|
167
|
+
if (dialled === undefined)
|
|
168
|
+
return;
|
|
169
|
+
const active = this.#active.get(sessionId);
|
|
170
|
+
if (active !== undefined)
|
|
171
|
+
active.dialFailureUrl = dialled;
|
|
172
|
+
else
|
|
173
|
+
pending = dialled; // logged during goto, before the lease is registered below
|
|
174
|
+
});
|
|
159
175
|
await page.goto(url, { timeoutMs: this.#navTimeout });
|
|
160
176
|
// The browser can crash WHILE goto is resolving; #onCrash then clears #active and zeroes
|
|
161
177
|
// #occupied. Registering the lease now would resurrect a dead entry against a crashed browser with
|
|
@@ -163,7 +179,13 @@ export class BrowserPool {
|
|
|
163
179
|
// no longer the live browser, bail — the catch below closes the context and returns the slot.
|
|
164
180
|
if (this.#browser !== browser)
|
|
165
181
|
throw new Error('browser crashed during navigation');
|
|
166
|
-
this.#active.set(sessionId, {
|
|
182
|
+
this.#active.set(sessionId, {
|
|
183
|
+
context,
|
|
184
|
+
page,
|
|
185
|
+
url,
|
|
186
|
+
touchedAt: this.#now(),
|
|
187
|
+
...(pending === undefined ? {} : { dialFailureUrl: pending }),
|
|
188
|
+
});
|
|
167
189
|
return {
|
|
168
190
|
sessionId,
|
|
169
191
|
url,
|
|
@@ -179,6 +201,14 @@ export class BrowserPool {
|
|
|
179
201
|
throw err;
|
|
180
202
|
}
|
|
181
203
|
}
|
|
204
|
+
/**
|
|
205
|
+
* The websocket address this lease's page reported it could not reach, if it reported one.
|
|
206
|
+
*
|
|
207
|
+
* Undefined means the page said nothing — never that it dialled the right place.
|
|
208
|
+
*/
|
|
209
|
+
dialFailureUrl(sessionId) {
|
|
210
|
+
return this.#active.get(sessionId)?.dialFailureUrl;
|
|
211
|
+
}
|
|
182
212
|
/** Close every context and the browser. Pending waiters are rejected (the pool is terminal now). */
|
|
183
213
|
async shutdown() {
|
|
184
214
|
this.#closed = true; // set first so any woken waiter rejects instead of relaunching a browser
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* Headless by default — the pool exists for fault-tolerant headless multi-agent testing.
|
|
7
7
|
*/
|
|
8
8
|
import { BrowserLaunchKind } from '@reticlehq/core';
|
|
9
|
+
import { chromiumLaunchOptions } from '../chromium-launch-options.js';
|
|
9
10
|
import { getSessionMetrics } from '../telemetry/session-metrics.js';
|
|
10
11
|
import { classifyConnectFailure } from '../telemetry/connect-failure.js';
|
|
11
12
|
import { chromiumInstallCommand, bundledPlaywrightVersion } from '../cli/chromium-hint.js';
|
|
@@ -40,6 +41,7 @@ function wrapBrowser(browser) {
|
|
|
40
41
|
goto: (url, opts) => page.goto(url, gotoOptions(opts?.timeoutMs)),
|
|
41
42
|
close: () => page.close(),
|
|
42
43
|
onCrash: (handler) => page.on('crash', handler),
|
|
44
|
+
onConsole: (handler) => page.on('console', (msg) => handler(msg.text())),
|
|
43
45
|
};
|
|
44
46
|
},
|
|
45
47
|
close: () => context.close(),
|
|
@@ -72,7 +74,7 @@ export function playwrightLauncher(opts = {}) {
|
|
|
72
74
|
// number that actually costs memory and the one that explains a slow machine.
|
|
73
75
|
const settle = getSessionMetrics().recordConnectAttempt(BrowserLaunchKind.POOLED);
|
|
74
76
|
try {
|
|
75
|
-
const browser = wrapBrowser(await chromium.launch(
|
|
77
|
+
const browser = wrapBrowser(await chromium.launch(chromiumLaunchOptions(headless)));
|
|
76
78
|
settle();
|
|
77
79
|
return browser;
|
|
78
80
|
}
|
|
@@ -36,7 +36,10 @@ export interface FileSystemPort {
|
|
|
36
36
|
/** Modification time in epoch ms — for recency-based retention pruning. Rejects if absent. */
|
|
37
37
|
stat(path: string): Promise<{
|
|
38
38
|
mtimeMs: number;
|
|
39
|
+
size: number;
|
|
39
40
|
}>;
|
|
41
|
+
/** Resolve symlinks to their real path — for upload trust-boundary checks. Rejects if absent. */
|
|
42
|
+
realpath(path: string): Promise<string>;
|
|
40
43
|
/** ENOENT classifier — narrows unknown without `any`, so callers can distinguish missing-file. */
|
|
41
44
|
isNotFound(error: unknown): boolean;
|
|
42
45
|
}
|
package/dist/project/fs-port.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { access, appendFile, mkdir, open, readdir, readFile, rename, rm, stat, writeFile, } from 'node:fs/promises';
|
|
1
|
+
import { access, appendFile, mkdir, open, readdir, readFile, realpath, rename, rm, stat, writeFile, } from 'node:fs/promises';
|
|
2
2
|
/** Production adapter — the single import site of node:fs/promises in the server. */
|
|
3
3
|
export function createNodeFileSystem() {
|
|
4
4
|
return {
|
|
@@ -46,8 +46,9 @@ export function createNodeFileSystem() {
|
|
|
46
46
|
},
|
|
47
47
|
stat: async (path) => {
|
|
48
48
|
const s = await stat(path);
|
|
49
|
-
return { mtimeMs: s.mtimeMs };
|
|
49
|
+
return { mtimeMs: s.mtimeMs, size: s.size };
|
|
50
50
|
},
|
|
51
|
+
realpath: (path) => realpath(path),
|
|
51
52
|
isNotFound: (error) => 'ENOENT' === error?.code,
|
|
52
53
|
};
|
|
53
54
|
}
|
|
@@ -78,7 +78,12 @@ export function createMemoryFs() {
|
|
|
78
78
|
return Promise.resolve();
|
|
79
79
|
},
|
|
80
80
|
stat(path) {
|
|
81
|
-
return written.has(norm(path))
|
|
81
|
+
return written.has(norm(path))
|
|
82
|
+
? Promise.resolve({ mtimeMs: 0, size: 0 })
|
|
83
|
+
: Promise.reject(notFound());
|
|
84
|
+
},
|
|
85
|
+
realpath(path) {
|
|
86
|
+
return written.has(norm(path)) ? Promise.resolve(norm(path)) : Promise.reject(notFound());
|
|
82
87
|
},
|
|
83
88
|
isNotFound(error) {
|
|
84
89
|
return 'ENOENT' === error?.code;
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
-
import { ProjectReadError,
|
|
2
|
+
import { ProjectReadError, RunStatus } from '@reticlehq/core';
|
|
3
3
|
import { ReticleTool } from '../tools/tool-names.js';
|
|
4
4
|
import { sessionIdShape } from '../tools/tool-kit.js';
|
|
5
5
|
import { asNumber, asString } from '../tools/tools-helpers.js';
|
|
6
|
-
import { fetchProjectRegressionFromCloud, resolveCloudConfig } from '../cloud/cloud-sync.js';
|
|
6
|
+
import { cloudFetch, fetchProjectRegressionFromCloud, resolveCloudConfig, } from '../cloud/cloud-sync.js';
|
|
7
7
|
import { RunStore } from '../runs/run-store.js';
|
|
8
8
|
import { diffRuns as diffVerificationRuns, } from '../runs/run-diff.js';
|
|
9
9
|
const REGRESSION_STATUSES = new Set([
|
|
@@ -45,7 +45,7 @@ async function cloudRegression(deps, sessionId) {
|
|
|
45
45
|
catch {
|
|
46
46
|
projectId = undefined;
|
|
47
47
|
}
|
|
48
|
-
const report = await fetchProjectRegressionFromCloud(config, projectId,
|
|
48
|
+
const report = await fetchProjectRegressionFromCloud(config, projectId, cloudFetch);
|
|
49
49
|
return report ?? undefined;
|
|
50
50
|
}
|
|
51
51
|
/** The two most-recent runs for `name`, oldest-first, or undefined if there are fewer than two. */
|
|
@@ -152,33 +152,4 @@ export const PROJECT_TOOLS = [
|
|
|
152
152
|
});
|
|
153
153
|
},
|
|
154
154
|
},
|
|
155
|
-
{
|
|
156
|
-
name: ReticleTool.RUN_RECORD,
|
|
157
|
-
description: 'Explicitly record a run outcome into .reticle/project.json (the manual companion to the auto-record on reticle_flow_replay). Use it to log the result of an assertion sequence or a manual journey so future runs can diff against it. Returns { recorded:true, name, status }.',
|
|
158
|
-
inputSchema: {
|
|
159
|
-
name: z.string().describe('Run name for grouping in reticle_project history.'),
|
|
160
|
-
status: z.nativeEnum(RunStatus).describe('Outcome: pass | fail | drift | error'),
|
|
161
|
-
kind: z.nativeEnum(RunKind).optional(),
|
|
162
|
-
summary: z.string().optional().describe('One-line human summary of what this run covered.'),
|
|
163
|
-
...sessionIdShape,
|
|
164
|
-
},
|
|
165
|
-
outputSchema: {
|
|
166
|
-
recorded: z.boolean(),
|
|
167
|
-
runName: z.string(),
|
|
168
|
-
status: z.string(),
|
|
169
|
-
},
|
|
170
|
-
handler: async (deps, args) => {
|
|
171
|
-
const name = asString(args['name']) ?? '';
|
|
172
|
-
const status = args['status'];
|
|
173
|
-
const kindArg = args['kind'];
|
|
174
|
-
const summary = asString(args['summary']);
|
|
175
|
-
await deps.project.recordRun({
|
|
176
|
-
kind: 'string' === typeof kindArg ? kindArg : RunKind.MANUAL,
|
|
177
|
-
name,
|
|
178
|
-
status,
|
|
179
|
-
...(summary !== undefined ? { summary } : {}),
|
|
180
|
-
});
|
|
181
|
-
return { recorded: true, runName: name, status };
|
|
182
|
-
},
|
|
183
|
-
},
|
|
184
155
|
];
|
|
@@ -15,6 +15,8 @@ export interface ReticleDirPaths {
|
|
|
15
15
|
project: string;
|
|
16
16
|
/**.../.reticle/impact.json (what Reticle has done for this user, local only) */
|
|
17
17
|
impact: string;
|
|
18
|
+
/**.../.reticle/intent.json (what changes were supposed to make true — git-checked) */
|
|
19
|
+
intent: string;
|
|
18
20
|
/**.../.reticle/visual (PNG baselines + diffs) */
|
|
19
21
|
visual: string;
|
|
20
22
|
/**.../.reticle/runs (verification-run artifacts) */
|
|
@@ -8,6 +8,7 @@ export function reticleDirPaths(root) {
|
|
|
8
8
|
baselines: join(root, ReticleDir.BASELINES_SUBDIR),
|
|
9
9
|
project: join(root, ReticleDir.PROJECT_FILE),
|
|
10
10
|
impact: join(root, ReticleDir.IMPACT_FILE),
|
|
11
|
+
intent: join(root, ReticleDir.INTENT_FILE),
|
|
11
12
|
visual: join(root, ReticleDir.VISUAL_SUBDIR),
|
|
12
13
|
runs: join(root, ReticleDir.RUNS_SUBDIR),
|
|
13
14
|
sessions: join(root, ReticleDir.SESSIONS_SUBDIR),
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import {} from '@reticlehq/core';
|
|
3
|
+
import { runContextFor } from './run-context.js';
|
|
4
|
+
import { openSessionIntents } from '../intent/open-intents.js';
|
|
5
|
+
import { ReticleTool } from '../tools/tool-names.js';
|
|
6
|
+
import { sessionIdShape } from '../tools/tool-kit.js';
|
|
7
|
+
import { asString } from '../tools/tools-helpers.js';
|
|
8
|
+
/**
|
|
9
|
+
* What this run has established, asked for by the one party that knows when it is needed.
|
|
10
|
+
*
|
|
11
|
+
* Reticle holds the trajectory: the journal, the verdicts, the intent ledger. The agent holds a copy
|
|
12
|
+
* in its context window, and that copy is the one that disappears — at a compaction, at the end of a
|
|
13
|
+
* turn, at the handover to a sub-agent. Reticle's does not disappear at the same moment. So this is
|
|
14
|
+
* not a competing memory; it is the ground truth compaction destroyed, handed back on request.
|
|
15
|
+
*
|
|
16
|
+
* ## Why it is PULLED
|
|
17
|
+
*
|
|
18
|
+
* The same content was once pushed onto every session-bound tool response. It cost +136% on a
|
|
19
|
+
* verdict and was cut, because most of the time the agent still had the context and we were paying
|
|
20
|
+
* on every call to duplicate what it knew. Nobody but the agent can tell when that stops being true.
|
|
21
|
+
* So the agent asks, once, at the moment it knows its own memory is gone, and pays once.
|
|
22
|
+
*
|
|
23
|
+
* ## Why it is on the EXTENDED surface rather than the default
|
|
24
|
+
*
|
|
25
|
+
* The default surface is a hard COUNT — editors budget tools across every connected MCP server, so a
|
|
26
|
+
* slot taken here is taken from a tool that has already earned it, and nothing on that list has been
|
|
27
|
+
* demoted for this. The claim behind this tool is unmeasured, which is the same test `reticle_intent`
|
|
28
|
+
* and the `reticle_capabilities` demotion were held to, and it fails that test today.
|
|
29
|
+
*
|
|
30
|
+
* The reachability argument is also stronger here than for most of the cold tail. The caller is,
|
|
31
|
+
* by construction, an agent that has just lost its context and is re-reading the tool list it was
|
|
32
|
+
* handed this turn — so `reticle_tools` (advertised on every surface) is exactly the call it is
|
|
33
|
+
* already going to make, and this tool is one `reticle_run` hop behind it. A tool that is only useful
|
|
34
|
+
* after a discovery step, to a caller who is already performing a discovery step, is the cheapest
|
|
35
|
+
* possible thing to leave off the hot list. It moves to the default surface when a measurement says
|
|
36
|
+
* the pull happens often enough to be worth a permanent slot, and not before.
|
|
37
|
+
*/
|
|
38
|
+
/** What the run has established, and where each part of it came from. */
|
|
39
|
+
const CONTEXT_OUTPUT_SCHEMA = {
|
|
40
|
+
step: z
|
|
41
|
+
.number()
|
|
42
|
+
.describe('How many actions this run has dispatched. Counted off the journal, not a counter of its own.'),
|
|
43
|
+
established: z
|
|
44
|
+
.array(z.unknown())
|
|
45
|
+
.describe('What Reticle OBSERVED, folded and capped, each `{ key, fact, source?, doc?, epoch? }`. `key` is the subject, so re-observing it supersedes rather than appends. Anything seen under a replaced document or a pre-edit epoch is already gone from this list.'),
|
|
46
|
+
proven: z
|
|
47
|
+
.array(z.unknown())
|
|
48
|
+
.describe('Claims a verdict already settled, each `{ claim, verified, source?, doc?, epoch? }`. Read this before re-driving something: re-proving it is slow, and assuming it is a false green.'),
|
|
49
|
+
remaining: z
|
|
50
|
+
.array(z.string())
|
|
51
|
+
.describe('The intents from .reticle/intent.json that no verdict has discharged. Derived from the ledger, never a guess at what you meant to do next.'),
|
|
52
|
+
};
|
|
53
|
+
/** The ledger and the live page state this run is folded out of. `[]`/undefined when nothing is connected. */
|
|
54
|
+
async function evidenceFor(deps, sessionId) {
|
|
55
|
+
try {
|
|
56
|
+
const session = deps.sessions.resolve(sessionId);
|
|
57
|
+
return {
|
|
58
|
+
actions: await session.readJournalActions(),
|
|
59
|
+
events: await session.queryEvents({}),
|
|
60
|
+
currentDocumentId: session.currentDocumentId,
|
|
61
|
+
currentEditEpoch: session.currentEditEpoch,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
// `resolve` throws when nothing is connected, when the id names no session, and when several are
|
|
66
|
+
// connected and none was named. All three mean the same thing here: there is no run to report,
|
|
67
|
+
// and an empty context is the honest answer rather than a reason to fail the call.
|
|
68
|
+
return {
|
|
69
|
+
actions: [],
|
|
70
|
+
events: [],
|
|
71
|
+
currentDocumentId: undefined,
|
|
72
|
+
currentEditEpoch: undefined,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
export const CONTEXT_TOOLS = [
|
|
77
|
+
{
|
|
78
|
+
name: ReticleTool.CONTEXT,
|
|
79
|
+
description: 'What THIS run has already established, so you do not rediscover it. Call it when your own copy is gone: right after a compaction, at the top of a fresh sub-agent, or at the start of a turn you did not begin. Returns `established` (what Reticle OBSERVED, with the source file:line where one was reported), `proven` (claims a verdict already settled, so you neither re-prove them nor assume them) and `remaining` (the intents from .reticle/intent.json that nothing has discharged). It is a FOLD over the journal, never a second store, so it cannot disagree with the ledger. Bounded and superseding rather than accumulating, and anything observed under a replaced document or before your last source edit is already dropped rather than presented as current. Only what Reticle observed goes in: it never reports what you intended or believed.',
|
|
80
|
+
example: {},
|
|
81
|
+
inputSchema: { ...sessionIdShape },
|
|
82
|
+
outputSchema: CONTEXT_OUTPUT_SCHEMA,
|
|
83
|
+
handler: async (deps, args) => {
|
|
84
|
+
const sessionId = asString(args['sessionId']);
|
|
85
|
+
const evidence = await evidenceFor(deps, sessionId);
|
|
86
|
+
return runContextFor({ ...evidence, intents: await openSessionIntents(deps, sessionId) });
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
];
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { type EstablishedFact, type Intent, type JournalAction, type ProvenClaim, type ReticleEvent, type RunContext } from '@reticlehq/core';
|
|
2
|
+
/**
|
|
3
|
+
* The SUBJECT a journal action was about: the ref it spent, else the target it resolved from.
|
|
4
|
+
*
|
|
5
|
+
* Exported so the feature-use instrument files a re-fetch under the same key this fold establishes
|
|
6
|
+
* it under. A second copy of this rule there would be a second definition of "the same subject".
|
|
7
|
+
*/
|
|
8
|
+
export declare function subjectOf(action: JournalAction): string | undefined;
|
|
9
|
+
/**
|
|
10
|
+
* Fold the journal into candidate facts, newest last.
|
|
11
|
+
*
|
|
12
|
+
* Only the last `RUN_ESTABLISHED_CAP` actions are read: anything older cannot survive the fold, so
|
|
13
|
+
* parsing it would be work done to throw away. An action with no observed settle outcome states
|
|
14
|
+
* nothing and is skipped — "we drove it and cannot say what happened" is not an established fact,
|
|
15
|
+
* and writing it down as one is how a memory starts lying.
|
|
16
|
+
*/
|
|
17
|
+
export declare function establishedFromJournal(actions: readonly JournalAction[], events: readonly ReticleEvent[]): EstablishedFact[];
|
|
18
|
+
/**
|
|
19
|
+
* What a verdict already settled, newest last.
|
|
20
|
+
*
|
|
21
|
+
* Both verdict tools write here. `reticle_act_and_wait` closes its attribution window with one;
|
|
22
|
+
* `reticle_assert` drives nothing, so it appends its record WITHOUT opening a window — same effect
|
|
23
|
+
* shape, so this fold reads one kind of verdict rather than two.
|
|
24
|
+
*/
|
|
25
|
+
export declare function provenFromJournal(actions: readonly JournalAction[], events: readonly ReticleEvent[]): ProvenClaim[];
|
|
26
|
+
/**
|
|
27
|
+
* The context for one run.
|
|
28
|
+
*
|
|
29
|
+
* `step` is the journal's action count rather than a counter of its own, for the reason at the top
|
|
30
|
+
* of this file: a second counter is a second thing that can disagree with the ledger. The document
|
|
31
|
+
* and epoch are the session's own, so the staleness rule here is the same one every other verdict in
|
|
32
|
+
* this codebase is scoped by.
|
|
33
|
+
*/
|
|
34
|
+
export declare function runContextFor(input: {
|
|
35
|
+
actions: readonly JournalAction[];
|
|
36
|
+
events: readonly ReticleEvent[];
|
|
37
|
+
intents: readonly Intent[];
|
|
38
|
+
currentDocumentId: string | undefined;
|
|
39
|
+
currentEditEpoch: number | undefined;
|
|
40
|
+
}): RunContext;
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import { EventType, JournalVerdictEffectSchema, RUN_ESTABLISHED_CAP, buildRunContext, } from '@reticlehq/core';
|
|
2
|
+
/**
|
|
3
|
+
* The run, derived from the journal that is already there.
|
|
4
|
+
*
|
|
5
|
+
* `.reticle/sessions/<id>/` already holds an append-only causal ledger of every action dispatched
|
|
6
|
+
* and every event observed. The run is a FOLD over that, never a second store — the same argument
|
|
7
|
+
* that deleted `instrumentation_stalled` from this codebase applies without modification: two ways
|
|
8
|
+
* to compute one number is worse than one way, because the day they disagree the disagreement lands
|
|
9
|
+
* inside a verdict and a user finds it before we do. So nothing here writes: it reads the ledger and
|
|
10
|
+
* folds it, and if the fold is wrong the ledger is still the single authority.
|
|
11
|
+
*
|
|
12
|
+
* **One run per session, for now.** Nothing here assumes it: the fold is pure and takes its evidence
|
|
13
|
+
* as arguments, so a later multi-run or multi-agent model is a change of caller, not a rewrite. It
|
|
14
|
+
* is not built today because nobody has hit the case, and a coordination model invented ahead of its
|
|
15
|
+
* first user is a guess.
|
|
16
|
+
*
|
|
17
|
+
* Only what Reticle OBSERVED goes in. A settle outcome is an observation. A route change is an
|
|
18
|
+
* observation. A verdict a verification tool recorded is an observation. What the agent meant to do
|
|
19
|
+
* next is not, and never enters here.
|
|
20
|
+
*/
|
|
21
|
+
/**
|
|
22
|
+
* The one key the route is filed under.
|
|
23
|
+
*
|
|
24
|
+
* The subject of "the app is on /checkout" is the route itself, so a later route change SUPERSEDES
|
|
25
|
+
* rather than appends. Filing it per-path would accumulate every page ever visited, which is the
|
|
26
|
+
* token bomb the cap exists to prevent.
|
|
27
|
+
*/
|
|
28
|
+
const ROUTE_KEY = 'route';
|
|
29
|
+
/** The conclusions this fold is allowed to state, as prose rather than transcript. */
|
|
30
|
+
const Fact = {
|
|
31
|
+
route: (to) => `route is ${to}`,
|
|
32
|
+
settled: (verb) => `${verb} settled`,
|
|
33
|
+
unsettled: (verb) => `${verb} did not settle`,
|
|
34
|
+
};
|
|
35
|
+
/**
|
|
36
|
+
* The SUBJECT a journal action was about: the ref it spent, else the target it resolved from.
|
|
37
|
+
*
|
|
38
|
+
* Exported so the feature-use instrument files a re-fetch under the same key this fold establishes
|
|
39
|
+
* it under. A second copy of this rule there would be a second definition of "the same subject".
|
|
40
|
+
*/
|
|
41
|
+
export function subjectOf(action) {
|
|
42
|
+
const ref = action.args['ref'];
|
|
43
|
+
if ('string' === typeof ref && ref.length > 0)
|
|
44
|
+
return ref;
|
|
45
|
+
const target = action.args['target'];
|
|
46
|
+
if ('string' === typeof target && target.length > 0)
|
|
47
|
+
return target;
|
|
48
|
+
return undefined;
|
|
49
|
+
}
|
|
50
|
+
/** What the action did, in the caller's own vocabulary — 'click', 'fill', else the tool's name. */
|
|
51
|
+
function verbOf(action) {
|
|
52
|
+
const named = action.args['action'];
|
|
53
|
+
return 'string' === typeof named && named.length > 0 ? named : action.tool;
|
|
54
|
+
}
|
|
55
|
+
/** The verdict a verification tool recorded on this action, if it recorded one. */
|
|
56
|
+
function verdictOf(action) {
|
|
57
|
+
const parsed = JournalVerdictEffectSchema.safeParse(action.effect);
|
|
58
|
+
return parsed.success ? parsed.data : undefined;
|
|
59
|
+
}
|
|
60
|
+
/** Which document and edit epoch each action was observed under, read off its attributed events. */
|
|
61
|
+
function scopeByAction(events) {
|
|
62
|
+
const out = new Map();
|
|
63
|
+
for (const event of events) {
|
|
64
|
+
if (event.actionId === undefined)
|
|
65
|
+
continue;
|
|
66
|
+
if (event.documentId === undefined && event.editEpoch === undefined)
|
|
67
|
+
continue;
|
|
68
|
+
const existing = out.get(event.actionId) ?? {};
|
|
69
|
+
out.set(event.actionId, {
|
|
70
|
+
doc: event.documentId ?? existing.doc,
|
|
71
|
+
epoch: event.editEpoch ?? existing.epoch,
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
return out;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Fold the journal into candidate facts, newest last.
|
|
78
|
+
*
|
|
79
|
+
* Only the last `RUN_ESTABLISHED_CAP` actions are read: anything older cannot survive the fold, so
|
|
80
|
+
* parsing it would be work done to throw away. An action with no observed settle outcome states
|
|
81
|
+
* nothing and is skipped — "we drove it and cannot say what happened" is not an established fact,
|
|
82
|
+
* and writing it down as one is how a memory starts lying.
|
|
83
|
+
*/
|
|
84
|
+
export function establishedFromJournal(actions, events) {
|
|
85
|
+
const scope = scopeByAction(events);
|
|
86
|
+
let route;
|
|
87
|
+
for (const event of events) {
|
|
88
|
+
if (EventType.ROUTE_CHANGE !== event.type)
|
|
89
|
+
continue;
|
|
90
|
+
const pathname = event.data['pathname'];
|
|
91
|
+
const to = 'string' === typeof pathname ? pathname : event.data['to'];
|
|
92
|
+
if ('string' !== typeof to)
|
|
93
|
+
continue;
|
|
94
|
+
route = { key: ROUTE_KEY, fact: Fact.route(to), doc: event.documentId, epoch: event.editEpoch };
|
|
95
|
+
}
|
|
96
|
+
const out = [];
|
|
97
|
+
for (const action of actions.slice(-RUN_ESTABLISHED_CAP)) {
|
|
98
|
+
const settled = action.settled;
|
|
99
|
+
if (undefined === settled)
|
|
100
|
+
continue;
|
|
101
|
+
const key = subjectOf(action);
|
|
102
|
+
if (undefined === key)
|
|
103
|
+
continue;
|
|
104
|
+
const verb = verbOf(action);
|
|
105
|
+
out.push({
|
|
106
|
+
key,
|
|
107
|
+
fact: settled ? Fact.settled(verb) : Fact.unsettled(verb),
|
|
108
|
+
// The source pointer the verdict already reported. Read off the ledger rather than looked up
|
|
109
|
+
// again: a second lookup could disagree with the one the agent was shown.
|
|
110
|
+
source: verdictOf(action)?.source,
|
|
111
|
+
...(scope.get(action.actionId) ?? {}),
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
if (route !== undefined)
|
|
115
|
+
out.push(route);
|
|
116
|
+
return out;
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* What a verdict already settled, newest last.
|
|
120
|
+
*
|
|
121
|
+
* Both verdict tools write here. `reticle_act_and_wait` closes its attribution window with one;
|
|
122
|
+
* `reticle_assert` drives nothing, so it appends its record WITHOUT opening a window — same effect
|
|
123
|
+
* shape, so this fold reads one kind of verdict rather than two.
|
|
124
|
+
*/
|
|
125
|
+
export function provenFromJournal(actions, events) {
|
|
126
|
+
const scope = scopeByAction(events);
|
|
127
|
+
const out = [];
|
|
128
|
+
for (const action of actions.slice(-RUN_ESTABLISHED_CAP)) {
|
|
129
|
+
const verdict = verdictOf(action);
|
|
130
|
+
if (undefined === verdict)
|
|
131
|
+
continue;
|
|
132
|
+
out.push({
|
|
133
|
+
claim: verdict.claim,
|
|
134
|
+
verified: verdict.verified,
|
|
135
|
+
source: verdict.source,
|
|
136
|
+
...(scope.get(action.actionId) ?? {}),
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
return out;
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* The context for one run.
|
|
143
|
+
*
|
|
144
|
+
* `step` is the journal's action count rather than a counter of its own, for the reason at the top
|
|
145
|
+
* of this file: a second counter is a second thing that can disagree with the ledger. The document
|
|
146
|
+
* and epoch are the session's own, so the staleness rule here is the same one every other verdict in
|
|
147
|
+
* this codebase is scoped by.
|
|
148
|
+
*/
|
|
149
|
+
export function runContextFor(input) {
|
|
150
|
+
return buildRunContext({
|
|
151
|
+
step: input.actions.length,
|
|
152
|
+
intents: input.intents,
|
|
153
|
+
established: establishedFromJournal(input.actions, input.events),
|
|
154
|
+
proven: provenFromJournal(input.actions, input.events),
|
|
155
|
+
currentDocumentId: input.currentDocumentId,
|
|
156
|
+
currentEditEpoch: input.currentEditEpoch,
|
|
157
|
+
});
|
|
158
|
+
}
|
package/dist/runs/run-diff.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { ReticleVerificationRun } from '@reticlehq/core';
|
|
2
2
|
/** A per-flow change between two runs. Only flows with a meaningful change are reported. */
|
|
3
|
-
|
|
3
|
+
interface FlowDelta {
|
|
4
4
|
name: string;
|
|
5
5
|
beforeMs: number;
|
|
6
6
|
afterMs: number;
|
|
@@ -29,3 +29,4 @@ export interface RunDiff {
|
|
|
29
29
|
* console diffs land once runs persist per-step detail.
|
|
30
30
|
*/
|
|
31
31
|
export declare function diffRuns(before: ReticleVerificationRun, after: ReticleVerificationRun, noiseFloorPct?: number): RunDiff;
|
|
32
|
+
export {};
|
|
@@ -14,7 +14,7 @@ import { buildVerificationRun } from './build-verification-run.js';
|
|
|
14
14
|
import { mapReplayToFlowResult } from './replay-mapping.js';
|
|
15
15
|
import { defaultRunId } from './runner-port.js';
|
|
16
16
|
import { RunStore } from './run-store.js';
|
|
17
|
-
import { syncRunToCloud, SyncOutcome } from '../cloud/cloud-sync.js';
|
|
17
|
+
import { cloudFetch, syncRunToCloud, SyncOutcome } from '../cloud/cloud-sync.js';
|
|
18
18
|
import { resolveProjectCloud } from '../cloud/cloud-config.js';
|
|
19
19
|
import { log } from '../log.js';
|
|
20
20
|
/** Assemble the ReticleVerificationRun artifact for a verify suite (pure — no IO). */
|
|
@@ -58,7 +58,7 @@ export async function persistAndSyncVerificationRun(deps, timed, projectId) {
|
|
|
58
58
|
const cloud = await resolveProjectCloud(deps.fs, deps.reticleRoot, homedir(), process.env);
|
|
59
59
|
if (null === cloud.config || !cloud.policy.runs)
|
|
60
60
|
return run.runId; // not attached / runs disabled → local only
|
|
61
|
-
const result = await syncRunToCloud(run, cloud.config,
|
|
61
|
+
const result = await syncRunToCloud(run, cloud.config, cloudFetch);
|
|
62
62
|
if (result.outcome !== SyncOutcome.SYNCED) {
|
|
63
63
|
log('cloud-run-sync-failed', { runId: run.runId, status: result.status, error: result.error });
|
|
64
64
|
}
|
|
@@ -69,13 +69,5 @@ export declare function probeDevServerStates(ports?: readonly number[], probe?:
|
|
|
69
69
|
serving: number[];
|
|
70
70
|
slow: number[];
|
|
71
71
|
}>;
|
|
72
|
-
/**
|
|
73
|
-
* The strongest state either loopback family reports.
|
|
74
|
-
*
|
|
75
|
-
* Serving a document beats a bare connection beats nothing, so a server bound to only one family is
|
|
76
|
-
* still seen at its best — the same "either, not both" rule `anyFamilyServes` follows, extended to
|
|
77
|
-
* three states.
|
|
78
|
-
*/
|
|
79
|
-
export declare function anyFamilyState(port: number, probe?: (port: number, host: string) => Promise<PortState>): Promise<PortState>;
|
|
80
72
|
/** Every candidate port serving a document, in ascending order. Never rejects. */
|
|
81
73
|
export declare function probeDevServers(ports?: readonly number[], probe?: (port: number) => Promise<boolean>): Promise<number[]>;
|
|
@@ -132,7 +132,7 @@ export async function probeDevServerStates(ports = [...DEV_SERVER_PORTS], probe
|
|
|
132
132
|
* still seen at its best — the same "either, not both" rule `anyFamilyServes` follows, extended to
|
|
133
133
|
* three states.
|
|
134
134
|
*/
|
|
135
|
-
|
|
135
|
+
async function anyFamilyState(port, probe = probeState) {
|
|
136
136
|
const answers = await Promise.all(PROBE_HOSTS.map((host) => probe(port, host).catch(() => PortState.CLOSED)));
|
|
137
137
|
if (answers.includes(PortState.SERVES_DOCUMENT))
|
|
138
138
|
return PortState.SERVES_DOCUMENT;
|