@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
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { healthEnvelope } from '../session/session-health.js';
|
|
2
|
+
import { verifyNextBaton, SUPPRESS_VERIFY_NEXT_ENV } from './verify-next-baton.js';
|
|
2
3
|
import { TelemetryActor, TelemetryEventKind, TRANSPORT_LIMITS, fingerprintFinding, } from '@reticlehq/core';
|
|
3
4
|
import { getSessionMetrics } from '../telemetry/session-metrics.js';
|
|
4
5
|
import { getTelemetry } from '../telemetry/telemetry.js';
|
|
@@ -16,6 +17,7 @@ import { ReticleTool } from './tool-names.js';
|
|
|
16
17
|
import { takeFeedbackPrompt } from './feedback-tools.js';
|
|
17
18
|
import { takeFeedbackUndelivered } from '../telemetry/feedback-delivery.js';
|
|
18
19
|
import { noteRefsMinted, wrongTabRefusal } from '../session/ref-provenance.js';
|
|
20
|
+
import { CAPTURED_TOOLS, noteCapturedCall, noteToolDispatched, } from '../honesty/feature-capture.js';
|
|
19
21
|
import { span } from '../trace.js';
|
|
20
22
|
import { deltaForToolResult, impactSnapshot, initImpact, recordImpact, } from '../impact/impact-recorder.js';
|
|
21
23
|
import { frictionOf, inviteFor } from './feedback-invite.js';
|
|
@@ -98,6 +100,8 @@ export const SESSION_EXEMPT_TOOLS = new Set([
|
|
|
98
100
|
ReticleTool.FLOW_REPLAY, // returns its own FlowReplayResult contract (+ auto-records a run)
|
|
99
101
|
ReticleTool.FLOW_SAVE_RECORDED, // reads the recording buffer, writes disk
|
|
100
102
|
ReticleTool.FLOW_HEAL, // returns its own FlowHealResult contract
|
|
103
|
+
ReticleTool.INTENT, // reads/writes .reticle/intent.json; sessionId only picks the project
|
|
104
|
+
ReticleTool.CONTEXT, // folds the journal + intent ledger; must still answer when nothing is connected
|
|
101
105
|
ReticleTool.PROJECT, // reads .reticle/project.json
|
|
102
106
|
ReticleTool.RUN_EXPORT, // reads .reticle/runs/<id>.json (verification-run artifact)
|
|
103
107
|
ReticleTool.SESSION, // merged lifecycle/human-channel family (tune/yield/end/resume/messages/review/narrate)
|
|
@@ -300,6 +304,43 @@ export async function runTool(tool, deps, args) {
|
|
|
300
304
|
session = undefined;
|
|
301
305
|
}
|
|
302
306
|
}
|
|
307
|
+
// The read-only calls the journal never keeps, recorded HERE because this is the one dispatch
|
|
308
|
+
// point every call routes through — a second recording site is a second thing to forget, and this
|
|
309
|
+
// instrument exists precisely because nothing was recorded before. Recorded before the handler
|
|
310
|
+
// runs: a read dispatches no action, so the counter reads the same either side.
|
|
311
|
+
//
|
|
312
|
+
// `reticle_context` is session-EXEMPT, so `session` above is undefined for it and it needs its own
|
|
313
|
+
// lenient resolve. Failing that resolve is silent by design: a call made with nothing connected
|
|
314
|
+
// has no per-session ledger to land in, and an instrument must never be why a tool call fails.
|
|
315
|
+
//
|
|
316
|
+
// EVERY tool is counted here, not just the read set: which of the 70-odd names in the table an
|
|
317
|
+
// agent actually reaches for is the question a decision about cutting or consolidating the surface
|
|
318
|
+
// turns on, and it had no answer. `reticle_run` is the one exclusion, for the same reason
|
|
319
|
+
// `reportRefusal` and `bugsInResult` exclude it — its handler calls `runTool` on the real tool, so
|
|
320
|
+
// the inner call is already counted under the name that RAN, and counting the wrapper too would
|
|
321
|
+
// both double the total and hide the run-only tier behind a single wrapper name. That tier is
|
|
322
|
+
// precisely the one the decision hinges on. See honesty/tool-hit-rate.ts.
|
|
323
|
+
if (ReticleTool.RUN !== tool.name) {
|
|
324
|
+
let target = session;
|
|
325
|
+
if (target === undefined) {
|
|
326
|
+
try {
|
|
327
|
+
target = deps.sessions.resolve(rawSessionId);
|
|
328
|
+
}
|
|
329
|
+
catch {
|
|
330
|
+
target = undefined;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
if (target !== undefined) {
|
|
334
|
+
noteToolDispatched(target, tool.name);
|
|
335
|
+
if (CAPTURED_TOOLS.has(tool.name)) {
|
|
336
|
+
const subject = asString(args['ref']) ?? asString(args['target']);
|
|
337
|
+
noteCapturedCall(target, {
|
|
338
|
+
tool: tool.name,
|
|
339
|
+
...(subject === undefined ? {} : { subject }),
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
}
|
|
303
344
|
// A ref that was handed out by a DIFFERENT tab, on a call that named no session. Refused here, at
|
|
304
345
|
// the one point that knows both the ref and the tab resolution picked — the handler only learns
|
|
305
346
|
// that the ref missed, and answers that with "the DOM re-rendered", which is a confident diagnosis
|
|
@@ -450,8 +491,27 @@ export async function runTool(tool, deps, args) {
|
|
|
450
491
|
// verdict-less session in the field never called a verdict-producing tool ONCE;
|
|
451
492
|
// the counter behind this already existed and was reported only to us. One-shot per abandoned
|
|
452
493
|
// run, same discipline as the pool lease — a hint on every call is noise that gets tuned out.
|
|
494
|
+
// The A/B control, and the reason it exists as an env flag rather than a code edit.
|
|
495
|
+
//
|
|
496
|
+
// `verify_next` is described in this repo's own changelog as "the largest known lever on whether a
|
|
497
|
+
// session produces a verdict at all", and it has never been measured — it was once built, fired
|
|
498
|
+
// and silently dropped by schema-strict clients for a whole release, which is exactly what an
|
|
499
|
+
// unmeasured lever looks like from the inside. "The payload now arrives" and "the agent acts on
|
|
500
|
+
// it" are different claims and only the first was ever verified.
|
|
501
|
+
//
|
|
502
|
+
// Suppressing it for a control arm has to leave the ONLY difference being the baton: same build,
|
|
503
|
+
// same surface, same counters. So the nudge is still TAKEN (the counter still resets, so the
|
|
504
|
+
// one-shot cadence is identical) and only the envelope key is withheld.
|
|
505
|
+
const suppressBaton = '1' === process.env[SUPPRESS_VERIFY_NEXT_ENV];
|
|
453
506
|
const unverified = getSessionMetrics().takeUnverifiedNudge();
|
|
454
|
-
|
|
455
|
-
|
|
507
|
+
// Carry the CALL, not just the sentence. The prose has to be translated back into arguments, and
|
|
508
|
+
// that translation is where agents were already going wrong — `until` omitted, action arguments
|
|
509
|
+
// flat instead of nested under `args`. `ref` and `action` come from the act that actually
|
|
510
|
+
// dispatched, so the suggestion is about the element the agent touched. See verify-next-baton.
|
|
511
|
+
if (unverified !== undefined && !suppressBaton)
|
|
512
|
+
// `lastAct` read defensively: this envelope is built on EVERY tool call, so a session shape
|
|
513
|
+
// without it would turn a missing field into a crash on the whole surface rather than a missing
|
|
514
|
+
// suggestion. The baton degrades to prose, which is what it carried before.
|
|
515
|
+
envelope[EnvelopeKey.VERIFY_NEXT] = verifyNextBaton(unverified, resolved.lastAct?.effect() ?? {});
|
|
456
516
|
return Object.keys(envelope).length > 0 ? { ...result, ...envelope } : result;
|
|
457
517
|
}
|
|
@@ -35,5 +35,15 @@ export interface LeaseEvidence {
|
|
|
35
35
|
* reported as a marker that was not found.
|
|
36
36
|
*/
|
|
37
37
|
sdkMarker?: boolean;
|
|
38
|
+
/**
|
|
39
|
+
* The websocket URL the leased page actually tried, read from its own console.
|
|
40
|
+
*
|
|
41
|
+
* The only per-page proof there is about where the dial WENT. `refusal` proves a dial arrived
|
|
42
|
+
* HERE; this proves where one was aimed, which is the strictly more useful fact when it was aimed
|
|
43
|
+
* somewhere else — the case in which no refusal exists to be recorded, because nothing arrived.
|
|
44
|
+
*
|
|
45
|
+
* Absent means the page said nothing, never that it dialled correctly.
|
|
46
|
+
*/
|
|
47
|
+
dialledUrl?: string;
|
|
38
48
|
}
|
|
39
49
|
export declare function leaseNotConnectedHint(url: string, port: number, evidence?: LeaseEvidence): string;
|
package/dist/tools/lease-hint.js
CHANGED
|
@@ -45,9 +45,45 @@ function markerClause(sdkMarker) {
|
|
|
45
45
|
'document, so a lazily-imported SDK can still be missed — but combined with the rest it ' +
|
|
46
46
|
'points at a bundle that carries no Reticle at all, i.e. wiring that never took effect.';
|
|
47
47
|
}
|
|
48
|
+
/**
|
|
49
|
+
* The port a websocket URL names, or undefined when it names none we can read.
|
|
50
|
+
*
|
|
51
|
+
* `ws://host/path` with no explicit port is deliberately undefined rather than defaulted to 80: a
|
|
52
|
+
* guessed port compared against the daemon's would manufacture a mismatch out of nothing, and this
|
|
53
|
+
* branch outranks every other cause. No answer is correct here; a plausible one is not.
|
|
54
|
+
*/
|
|
55
|
+
function dialledPort(url) {
|
|
56
|
+
if (url === undefined)
|
|
57
|
+
return undefined;
|
|
58
|
+
try {
|
|
59
|
+
const port = new URL(url).port;
|
|
60
|
+
return '' === port ? undefined : Number(port);
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
return undefined;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
48
66
|
export function leaseNotConnectedHint(url, port, evidence = {}) {
|
|
49
67
|
const opening = `the leased tab loaded ${url} but never dialled this daemon (port ${String(port)}).`;
|
|
50
68
|
const marker = markerClause(evidence.sdkMarker);
|
|
69
|
+
// 0. The page told us where it dialled, and it was not here. Proof, and it outranks the refusal
|
|
70
|
+
// branch below: a refusal proves a dial reached THIS daemon, which is a fact about some page;
|
|
71
|
+
// this is a fact about the page in hand. When they disagree, the specific one wins.
|
|
72
|
+
//
|
|
73
|
+
// Neither side can reach this conclusion alone, which is why it went unsaid for so long. The
|
|
74
|
+
// page's own warning deliberately refuses to diagnose the daemon — from inside a browser a
|
|
75
|
+
// daemon that is absent and one that is unreachable are the same observation. The daemon cannot
|
|
76
|
+
// see a dial that never arrived. Put the address the page names beside the port the daemon is
|
|
77
|
+
// bound to and the ambiguity disappears.
|
|
78
|
+
const dialled = dialledPort(evidence.dialledUrl);
|
|
79
|
+
if (dialled !== undefined && dialled !== port) {
|
|
80
|
+
return (`${opening} The page dialled ${String(evidence.dialledUrl)} (port ${String(dialled)}) and ` +
|
|
81
|
+
`this daemon is on ${String(port)}, so the dial never reached it. Nothing about the app's ` +
|
|
82
|
+
`wiring is in question. Either start the daemon on ${String(dialled)}, or point the app at ` +
|
|
83
|
+
`${String(port)} — the app's port comes from its build config (\`.reticle.json\`, ` +
|
|
84
|
+
`RETICLE_PORT, or the build plugin's \`port\`), and \`VITE_RETICLE_WS_URL\` overrides it ` +
|
|
85
|
+
`outright.${marker} ${RELEASE}`);
|
|
86
|
+
}
|
|
51
87
|
// 1. A recorded refusal. The only cause with proof behind it, so nothing outranks it.
|
|
52
88
|
if (evidence.refusal !== undefined) {
|
|
53
89
|
return `${opening} A dial DID arrive and was refused: ${evidence.refusal}${marker} ${RELEASE}`;
|
|
@@ -55,11 +91,24 @@ export function leaseNotConnectedHint(url, port, evidence = {}) {
|
|
|
55
91
|
// 2. Nuxt, whose most likely cause differs from every other framework's and which `init` warns
|
|
56
92
|
// about at install time — while this hint never mentioned it.
|
|
57
93
|
const nuxt = evidence.framework?.toLowerCase() === NUXT ? ` ${NUXT_FIRST}` : '';
|
|
58
|
-
// 3. Proven
|
|
94
|
+
// 3. Proven PORT. Blaming the port here is the self-contradiction this rewrite exists to end —
|
|
95
|
+
// but "the wiring is correct" was a second claim, and it was not proven at all.
|
|
96
|
+
//
|
|
97
|
+
// `previouslyConnected` is scoped to project + port, never to the app. In a monorepo, or for
|
|
98
|
+
// anyone adding a second app, the thing that connected yesterday is a DIFFERENT app. Measured:
|
|
99
|
+
// an uninstrumented app driven in exactly that situation was told its wiring was correct, and
|
|
100
|
+
// handed four causes — a dev-mode guard, a stale dev server, a missing peer dependency, a
|
|
101
|
+
// non-localhost page — every one of which presupposes the SDK is already installed. It was
|
|
102
|
+
// not. That is the commonest failure there is, and `reticle init` appeared nowhere in the
|
|
103
|
+
// answer, because the only branch that mentions it is the one reached when nothing is known.
|
|
59
104
|
if (true === evidence.previouslyConnected) {
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
105
|
+
const notThisApp = true === evidence.sdkMarker
|
|
106
|
+
? ''
|
|
107
|
+
: ' That may have been a DIFFERENT app, though: this one may carry no Reticle SDK at all, ' +
|
|
108
|
+
'in which case run `reticle init` in ITS directory first — every cause below assumes the ' +
|
|
109
|
+
'SDK is already installed.';
|
|
110
|
+
return (`${opening} An app for this project HAS connected on this port before, so the port is ` +
|
|
111
|
+
`proven.${notThisApp}${nuxt}${marker} ${REAL_CAUSES} ${RELEASE}`);
|
|
63
112
|
}
|
|
64
113
|
// 4. Wired, but never seen connect. Rank the real causes; keep the port differential last, where
|
|
65
114
|
// the evidence for it actually sits.
|
|
@@ -58,6 +58,11 @@ export declare function acquireLeasedSession(pool: {
|
|
|
58
58
|
sessionId: string;
|
|
59
59
|
release: () => Promise<void>;
|
|
60
60
|
}>;
|
|
61
|
+
/**
|
|
62
|
+
* The address this lease's page said it could not reach. Optional so a test double need not
|
|
63
|
+
* implement it; the real pool always does.
|
|
64
|
+
*/
|
|
65
|
+
dialFailureUrl?: (sessionId: string) => string | undefined;
|
|
61
66
|
/** Optional so a test double need not implement it; the real pool always does. */
|
|
62
67
|
alias?: (registeredId: string, leaseId: string) => void;
|
|
63
68
|
}, sessions: {
|
|
@@ -81,5 +86,4 @@ export declare function waitForLeasedSession(isConnected: () => boolean, sleeper
|
|
|
81
86
|
* any other call instead of being a second, invisible dispatch path.
|
|
82
87
|
*/
|
|
83
88
|
export declare const LEASE_ACQUIRE_TOOL: ToolDef;
|
|
84
|
-
export declare const LEASE_RELEASE_TOOL: ToolDef;
|
|
85
89
|
export declare const LEASE_TOOLS: ToolDef[];
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
*/
|
|
11
11
|
import { z } from 'zod';
|
|
12
12
|
import { leaseNotConnectedHint } from './lease-hint.js';
|
|
13
|
+
import { probeSdkMarker } from './sdk-marker-probe.js';
|
|
13
14
|
import { readProjectFramework, readProjectId } from '../cli/cli-port.js';
|
|
14
15
|
import { hasConnectedBefore } from '../session/connection-memory.js';
|
|
15
16
|
import { reticleStateHome } from '../daemon/daemon.js';
|
|
@@ -24,14 +25,23 @@ import { chromiumHint } from '../cli/chromium-hint.js';
|
|
|
24
25
|
* printed a static differential over the top of it — telling one agent in `reticle_sessions` that
|
|
25
26
|
* the wiring was proven correct and telling it seconds later that the port was probably wrong.
|
|
26
27
|
*/
|
|
27
|
-
|
|
28
|
+
/**
|
|
29
|
+
* Async only for the marker probe, which is the piece that decides whether this app ships an SDK at
|
|
30
|
+
* all — the difference between the diagnosis a new user needs and four causes that all assume the
|
|
31
|
+
* install already happened. It is one localhost GET on a path that has already failed and is about
|
|
32
|
+
* to print a paragraph, and it can only ever add a sentence: the probe returns `undefined` on any
|
|
33
|
+
* doubt and the hint then says nothing about markers.
|
|
34
|
+
*/
|
|
35
|
+
async function leaseEvidence(deps, port, url) {
|
|
28
36
|
const cwd = process.cwd();
|
|
29
37
|
const projectId = readProjectId(cwd);
|
|
30
38
|
const refusal = deps.sessions.lastClosure()?.reason;
|
|
31
39
|
const framework = readProjectFramework(cwd);
|
|
40
|
+
const sdkMarker = await probeSdkMarker(url);
|
|
32
41
|
return {
|
|
33
42
|
...(refusal === undefined ? {} : { refusal }),
|
|
34
43
|
...(framework === undefined ? {} : { framework }),
|
|
44
|
+
...(sdkMarker === undefined ? {} : { sdkMarker }),
|
|
35
45
|
previouslyConnected: hasConnectedBefore(reticleStateHome(), port, projectId),
|
|
36
46
|
initialized: projectId !== undefined,
|
|
37
47
|
};
|
|
@@ -239,16 +249,20 @@ export const LEASE_ACQUIRE_TOOL = {
|
|
|
239
249
|
queued: pool.queuedCount(),
|
|
240
250
|
...(ready
|
|
241
251
|
? {}
|
|
242
|
-
: (()
|
|
243
|
-
const bridgePort = deps.bridgePort ?? RETICLE_DEFAULT_PORT;
|
|
244
|
-
return {
|
|
245
|
-
hint: leaseNotConnectedHint(url, bridgePort, leaseEvidence(deps, bridgePort)),
|
|
246
|
-
};
|
|
247
|
-
})()),
|
|
252
|
+
: { hint: await notConnectedHint(deps, url, pool.dialFailureUrl?.(lease.sessionId)) }),
|
|
248
253
|
};
|
|
249
254
|
},
|
|
250
255
|
};
|
|
251
|
-
|
|
256
|
+
/** The whole not-connected diagnosis, gathered and worded. Split out so the acquire path stays flat. */
|
|
257
|
+
async function notConnectedHint(deps, url, dialledUrl) {
|
|
258
|
+
const bridgePort = deps.bridgePort ?? RETICLE_DEFAULT_PORT;
|
|
259
|
+
const evidence = await leaseEvidence(deps, bridgePort, url);
|
|
260
|
+
return leaseNotConnectedHint(url, bridgePort, {
|
|
261
|
+
...evidence,
|
|
262
|
+
...(dialledUrl === undefined ? {} : { dialledUrl }),
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
const LEASE_RELEASE_TOOL = {
|
|
252
266
|
name: ReticleTool.LEASE_RELEASE,
|
|
253
267
|
description: 'Release a leased browser context by sessionId, closing it and freeing the pool slot for a queued acquire. Call this when a flow finishes so the pool stays within its concurrency cap.',
|
|
254
268
|
inputSchema: {
|
|
@@ -13,8 +13,6 @@
|
|
|
13
13
|
*/
|
|
14
14
|
import type { SessionManager } from '../session/session-manager.js';
|
|
15
15
|
import type { NavigateArrival } from './navigate-result.js';
|
|
16
|
-
/** Long enough for a dev server to serve a page and the SDK to dial back; short enough to not hang. */
|
|
17
|
-
export declare const ARRIVAL_TIMEOUT_MS = 5000;
|
|
18
16
|
export interface ArrivalClock {
|
|
19
17
|
now: () => number;
|
|
20
18
|
sleep: (ms: number) => Promise<void>;
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* the sleep are injected so this is testable without waiting on a real one.
|
|
13
13
|
*/
|
|
14
14
|
/** Long enough for a dev server to serve a page and the SDK to dial back; short enough to not hang. */
|
|
15
|
-
|
|
15
|
+
const ARRIVAL_TIMEOUT_MS = 5_000;
|
|
16
16
|
const POLL_MS = 100;
|
|
17
17
|
/**
|
|
18
18
|
* Compare only origin + pathname.
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
import { noteEmptyRead } from './observed-nothing.js';
|
|
6
6
|
import { z } from 'zod';
|
|
7
7
|
import { aliasParam } from './alias-args.js';
|
|
8
|
-
import { CONSOLE_LEVELS, ReticleCommand, DEFAULT_ASSERT_TIMEOUT_MS, PredicateKind, } from '@reticlehq/core';
|
|
8
|
+
import { CONSOLE_LEVELS, ReticleCommand, DEFAULT_ASSERT_TIMEOUT_MS, PredicateKind, Verified, } from '@reticlehq/core';
|
|
9
9
|
import { ReticleTool } from './tool-names.js';
|
|
10
10
|
import { buildReactionReport } from '../events/reaction.js';
|
|
11
11
|
import { findContradictions } from '../events/contradictions.js';
|
|
@@ -13,13 +13,18 @@ import { evaluatePredicate, waitForPredicate, PredicateSchema } from '../events/
|
|
|
13
13
|
import { parsePredicate } from '../events/predicate-parse.js';
|
|
14
14
|
import { matchNet, matchConsole, isConsoleEvent, eventMatchesFilters, netEmptyHint, consoleEmptyHint, reconcileNet, projectNetCall, projectConsoleLog, } from '../events/event-filters.js';
|
|
15
15
|
import { applyEventBudget, costHint, withSizeCost, DEFAULT_QUERY_LIMIT, } from '../session/output-budget.js';
|
|
16
|
-
import { healthEnvelope, bufferEnvelope } from '../session/session-health.js';
|
|
16
|
+
import { annotateStarvedFailure, healthEnvelope, bufferEnvelope, } from '../session/session-health.js';
|
|
17
17
|
import { assertsDerivedIpcStatus, DERIVED_IPC_STATUS_ADVICE, isPresenceOnlyAssertion, PRESENCE_ONLY_ADVICE, } from './assert-grade.js';
|
|
18
18
|
import { assertVerdict } from './assert-verdict.js';
|
|
19
|
+
import { assertSource } from './assert-source.js';
|
|
20
|
+
import { isChangeUndeclared } from '../honesty/undeclared-change.js';
|
|
21
|
+
import { openSessionIntents } from '../intent/open-intents.js';
|
|
22
|
+
import { dischargeInlineIntent, inlineVerdictId, linkInlineIntent, } from '../intent/inline-intent.js';
|
|
19
23
|
import { bodiesNotCaptured } from '../honesty/uncaptured-bodies.js';
|
|
20
24
|
import { withControl } from '../session/control-envelope.js';
|
|
21
|
-
import { asString, asNumber } from './tools-helpers.js';
|
|
22
|
-
import { sessionIdShape, commandOrThrow } from './tool-kit.js';
|
|
25
|
+
import { asString, asNumber, asRecord } from './tools-helpers.js';
|
|
26
|
+
import { intentArg, sessionIdShape, commandOrThrow } from './tool-kit.js';
|
|
27
|
+
import { gradeOfPredicate } from './assert-grade.js';
|
|
23
28
|
/**
|
|
24
29
|
* Evidence-completeness block: present on observe/network/console only when the ring buffer has
|
|
25
30
|
* evicted events, so a "no such event" answer is distinguishable from "I dropped it" (issue #27).
|
|
@@ -31,20 +36,20 @@ const bufferOutputShape = {
|
|
|
31
36
|
.describe('Present only when the event buffer evicted events — a negative result may then be a false negative.'),
|
|
32
37
|
};
|
|
33
38
|
/**
|
|
34
|
-
*
|
|
39
|
+
* The file:line an assertion may report — see `assertSource`.
|
|
35
40
|
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
* so a failing assertion can point there instead of at nothing.
|
|
41
|
-
*
|
|
42
|
-
* RED only: on a pass this is noise on the path the agent walks most.
|
|
41
|
+
* Neither `reticle_assert` nor `reticle_wait_for` drives anything, so the last act's source is about
|
|
42
|
+
* some earlier action and not about this verdict. The pointer comes from the assertion's own matched
|
|
43
|
+
* evidence; the last driven control is borrowed only for a RED whose predicate has no DOM clause at
|
|
44
|
+
* all, which is the failure that genuinely has no element to point at.
|
|
43
45
|
*/
|
|
44
|
-
function
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
46
|
+
function assertionSource(session, predicate, verdict) {
|
|
47
|
+
const source = assertSource({
|
|
48
|
+
predicate,
|
|
49
|
+
evidence: verdict.evidence,
|
|
50
|
+
pass: verdict.pass,
|
|
51
|
+
lastActSource: session.lastAct.source(),
|
|
52
|
+
});
|
|
48
53
|
return source === undefined ? {} : { source };
|
|
49
54
|
}
|
|
50
55
|
/**
|
|
@@ -179,7 +184,11 @@ export const OBSERVE_TOOLS = [
|
|
|
179
184
|
// never asked about.
|
|
180
185
|
const actCursor = session.lastAct.cursor();
|
|
181
186
|
const judgingTheAct = actCursor !== undefined && actCursor >= since;
|
|
182
|
-
const contradictions = findContradictions(filtered,
|
|
187
|
+
const contradictions = findContradictions(filtered, {
|
|
188
|
+
currentDocumentId: session.currentDocumentId,
|
|
189
|
+
currentEditEpoch: session.currentEditEpoch,
|
|
190
|
+
...(judgingTheAct ? { ...session.lastAct.effect(), actionSince: actCursor } : {}),
|
|
191
|
+
});
|
|
183
192
|
// carry session health — a throttled tab means the observed timeline may be incomplete.
|
|
184
193
|
return withControl(session, {
|
|
185
194
|
...report,
|
|
@@ -233,7 +242,7 @@ export const OBSERVE_TOOLS = [
|
|
|
233
242
|
source: z
|
|
234
243
|
.string()
|
|
235
244
|
.optional()
|
|
236
|
-
.describe('
|
|
245
|
+
.describe('Where the code behind this verdict lives, as `file:line`. For an element/text assertion it is the matched element itself; for a FAILING signal/net/state assertion — which has no element to point at — it is the control last acted on, where the handler that should have fired lives. OMITTED when neither is known: this tool never borrows an unrelated location.'),
|
|
237
246
|
},
|
|
238
247
|
handler: async (deps, args) => {
|
|
239
248
|
const session = deps.sessions.resolve(asString(args['sessionId']));
|
|
@@ -245,8 +254,11 @@ export const OBSERVE_TOOLS = [
|
|
|
245
254
|
// match reticle_assert — wrap with control + session health (throttle matters most while blocking)
|
|
246
255
|
// and the buffer envelope, so a verdict reached over an evicted window says so.
|
|
247
256
|
return withControl(session, {
|
|
248
|
-
|
|
249
|
-
|
|
257
|
+
// #537's starved-wait note wraps the verdict (it RETURNS the verdict), so it stands where
|
|
258
|
+
// `...verdict` did. The source line is #533's `assertionSource`, which superseded
|
|
259
|
+
// `lastActSourceOnFailure` — an assert used to be blamed on the previous act's file:line.
|
|
260
|
+
...annotateStarvedFailure(session, verdict),
|
|
261
|
+
...assertionSource(session, predicate, verdict),
|
|
250
262
|
...healthEnvelope(session),
|
|
251
263
|
...bufferEnvelope(session),
|
|
252
264
|
});
|
|
@@ -255,14 +267,15 @@ export const OBSERVE_TOOLS = [
|
|
|
255
267
|
{
|
|
256
268
|
name: ReticleTool.ASSERT,
|
|
257
269
|
example: { predicate: { kind: PredicateKind.SIGNAL, name: 'todos:loaded' } },
|
|
258
|
-
description: 'Evaluate a predicate (optionally waiting up to timeout_ms). Returns { pass, evidence, failureReason? }. The end of every verify loop. Prefer a { signal } or { net } consequence over { element }/{ text } presence — a passing presence-only assertion returns `advice` because a wrong/healed element can fake it. By default it only counts events since your last act, so a stale buffered signal can never fake a pass; pass `since` (an observe/act cursor) to set the window explicitly.',
|
|
270
|
+
description: 'Evaluate a predicate (optionally waiting up to timeout_ms). Returns { pass, evidence, failureReason? }. The end of every verify loop. CHECKING SEVERAL THINGS? Put them in ONE call with { kind: "allOf", predicates: [...] } — it returns one verdict naming whichever member failed, and one call costs one round trip where N calls cost N. Prefer a { signal } or { net } consequence over { element }/{ text } presence — a passing presence-only assertion returns `advice` because a wrong/healed element can fake it. By default it only counts events since your last act, so a stale buffered signal can never fake a pass; pass `since` (an observe/act cursor) to set the window explicitly.',
|
|
259
271
|
inputSchema: {
|
|
260
272
|
predicate: PredicateSchema.optional().describe(
|
|
261
273
|
// Every kind, each with the field that carries its argument. Five of the nine were
|
|
262
274
|
// undocumented here, including `route` — and "did submitting the login form navigate away"
|
|
263
275
|
// is the most common thing an agent wants to assert. A field report reached us from an agent
|
|
264
276
|
// that guessed `urlContains` on route (net's spelling) and got unrecognized_keys.
|
|
265
|
-
'Predicate to evaluate. Kinds: { signal, name
|
|
277
|
+
'Predicate to evaluate. Kinds: { signal, name|dataMatches|count } ' +
|
|
278
|
+
'{ net, urlContains|method|status|count|bodyContains } ' +
|
|
266
279
|
'{ state, path|equals } { route, pathname (exact) | contains (path+query+hash) } ' +
|
|
267
280
|
'{ element, testid|role|text } { text } { console, level|contains|absent } { animation, name } ' +
|
|
268
281
|
'{ settled } — combine with { allOf | anyOf | not }. Prefer a signal/net/state consequence ' +
|
|
@@ -276,6 +289,7 @@ export const OBSERVE_TOOLS = [
|
|
|
276
289
|
.number()
|
|
277
290
|
.optional()
|
|
278
291
|
.describe('Cursor from a prior reticle_act — scopes the assertion to events after that act.'),
|
|
292
|
+
intent: intentArg,
|
|
279
293
|
...sessionIdShape,
|
|
280
294
|
},
|
|
281
295
|
outputSchema: {
|
|
@@ -309,7 +323,7 @@ export const OBSERVE_TOOLS = [
|
|
|
309
323
|
source: z
|
|
310
324
|
.string()
|
|
311
325
|
.optional()
|
|
312
|
-
.describe('
|
|
326
|
+
.describe('Where the code behind this verdict lives, as `file:line`. For an element/text assertion it is the matched element itself; for a FAILING signal/net/state assertion — which has no element to point at — it is the control last acted on, where the handler that should have fired lives. OMITTED when neither is known: this tool never borrows an unrelated location.'),
|
|
313
327
|
coverage: z
|
|
314
328
|
.string()
|
|
315
329
|
.optional()
|
|
@@ -330,6 +344,9 @@ export const OBSERVE_TOOLS = [
|
|
|
330
344
|
const timeout = asNumber(args['timeout_ms']) ?? 0;
|
|
331
345
|
// Honesty: explicit since wins; else default to the last act's cursor; else the whole buffer.
|
|
332
346
|
const since = asNumber(args['since']) ?? session.lastAct.cursor() ?? 0;
|
|
347
|
+
// Declared BEFORE the verdict, so the undeclared-change read below finds it open and stays
|
|
348
|
+
// quiet on THIS verdict rather than on the next one. Discharged after that read.
|
|
349
|
+
const intentId = await linkInlineIntent(deps, asString(args['sessionId']), asString(args['intent']), PredicateKind.SETTLED === predicate.kind ? undefined : predicate);
|
|
333
350
|
const verdict = timeout > 0
|
|
334
351
|
? await waitForPredicate(session, predicate, timeout, since)
|
|
335
352
|
: await evaluatePredicate(session, predicate, since);
|
|
@@ -345,16 +362,36 @@ export const OBSERVE_TOOLS = [
|
|
|
345
362
|
: assertsDerivedIpcStatus(predicate)
|
|
346
363
|
? { advice: DERIVED_IPC_STATUS_ADVICE }
|
|
347
364
|
: {};
|
|
348
|
-
|
|
365
|
+
// Asked of every verdict drawn after an observed edit, not once per edit — see
|
|
366
|
+
// isChangeUndeclared for why repeating it is disclosure rather than nagging.
|
|
367
|
+
const changeUndeclared = await isChangeUndeclared(session.currentEditEpoch, () => openSessionIntents(deps, asString(args['sessionId'])));
|
|
368
|
+
const { decision, contradictions, coverage, gaps, verdictEffect } = await assertVerdict(session, predicate, verdict.pass, verdict.evidence, since, verdict.inconclusive, verdict.observationLost, changeUndeclared);
|
|
369
|
+
// The assertion IS the proof attempt, so a green one discharges the intent it was drawn for. A
|
|
370
|
+
// red proved nothing, and an unbound intent refuses discharge anyway — see inline-intent.ts.
|
|
371
|
+
// The id is checked HERE rather than only inside the helper so a caller that declared no intent
|
|
372
|
+
// touches nothing at all, not even the clock.
|
|
373
|
+
if (intentId !== undefined && Verified.YES === decision['verified']) {
|
|
374
|
+
await dischargeInlineIntent(deps, asString(args['sessionId']), intentId, {
|
|
375
|
+
verdictId: inlineVerdictId(ReticleTool.ASSERT, deps.now()),
|
|
376
|
+
grade: gradeOfPredicate(predicate),
|
|
377
|
+
at: deps.now(),
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
// Journal the verdict so a LATER turn can read what this one proved. A verdict that lives only
|
|
381
|
+
// in the response lives only in the agent's context window, which is the copy a compaction
|
|
382
|
+
// destroys — see runs/run-context.ts. Recorded WITHOUT an attribution window: this tool drives
|
|
383
|
+
// nothing, so no event it observed was caused by it.
|
|
384
|
+
session.recordAction(ReticleTool.ASSERT, asRecord(args), verdictEffect);
|
|
349
385
|
return withControl(session, {
|
|
350
386
|
...decision,
|
|
351
|
-
...verdict,
|
|
387
|
+
...annotateStarvedFailure(session, verdict),
|
|
352
388
|
...(contradictions.length > 0 ? { contradictions } : {}),
|
|
353
389
|
// What the app did not tell Reticle, on the same rule the act path uses.
|
|
354
390
|
...(gaps.length > 0 ? { instrumentationGaps: gaps } : {}),
|
|
355
391
|
...advice,
|
|
356
392
|
...coverage,
|
|
357
|
-
|
|
393
|
+
// The SAME pointer the journal keeps, not a second lookup — one verdict, one file:line.
|
|
394
|
+
...(verdictEffect.source === undefined ? {} : { source: verdictEffect.source }),
|
|
358
395
|
...healthEnvelope(session),
|
|
359
396
|
...bufferEnvelope(session),
|
|
360
397
|
});
|
package/dist/tools/read-tools.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import { resolveAnnotateTarget } from '../flows/annotate-target.js';
|
|
6
6
|
import { z } from 'zod';
|
|
7
|
-
import { EventType, ReticleCommand, REPLAY_PROGRAM_VERSION, SnapshotMode } from '@reticlehq/core';
|
|
7
|
+
import { EventType, ReticleCommand, REPLAY_PROGRAM_VERSION, SnapshotMode, StorageArea, } from '@reticlehq/core';
|
|
8
8
|
import { ReticleTool } from './tool-names.js';
|
|
9
9
|
import { proposeConsequences } from '../oracles/propose-consequences.js';
|
|
10
10
|
import { replayProgram } from '../flows/replay.js';
|
|
@@ -320,8 +320,10 @@ export const READ_TOOLS = [
|
|
|
320
320
|
.describe("Dot-path into the store (e.g. 'captionCache.v3'). Numeric array indices are supported."),
|
|
321
321
|
depth: z
|
|
322
322
|
.number()
|
|
323
|
+
.int()
|
|
324
|
+
.positive()
|
|
323
325
|
.optional()
|
|
324
|
-
.describe('Collapse anything deeper than N levels to a size marker — avoids huge outputs for large stores.'),
|
|
326
|
+
.describe('Collapse anything deeper than N levels to a size marker — avoids huge outputs for large stores. A whole number of levels, 1 or more.'),
|
|
325
327
|
...sessionIdShape,
|
|
326
328
|
},
|
|
327
329
|
outputSchema: {
|
|
@@ -513,7 +515,7 @@ export const READ_TOOLS = [
|
|
|
513
515
|
'diagnosable.',
|
|
514
516
|
inputSchema: {
|
|
515
517
|
area: z
|
|
516
|
-
.
|
|
518
|
+
.nativeEnum(StorageArea)
|
|
517
519
|
.optional()
|
|
518
520
|
.describe('Scope to one storage area. Omit to read all three.'),
|
|
519
521
|
key: z
|
|
@@ -5,10 +5,32 @@
|
|
|
5
5
|
* Split out of act-tools.ts along its natural seam: the act tools care only about the outcome
|
|
6
6
|
* (`result` defined = it went native), never about provider availability, box resolution,
|
|
7
7
|
* drag-target inspection or the reason taxonomy.
|
|
8
|
+
*
|
|
9
|
+
* Also hosts `rewriteUploadArgs` — the daemon-side path that lets an agent name a file on disk
|
|
10
|
+
* and have its REAL bytes reach the browser's `<input type="file">`. The browser SDK cannot read
|
|
11
|
+
* the filesystem; the daemon can. The rewrite happens here, before the ACT command crosses the
|
|
12
|
+
* bridge, so the browser side sees a normal `{ content, name, type }` call and `assertUploadArgs`
|
|
13
|
+
* keeps its invariant (no fabricated bytes, no silently-dropped keys).
|
|
14
|
+
*
|
|
15
|
+
* Trust boundary: scoped to the project root (one level above `deps.reticleRoot`), resolved
|
|
16
|
+
* through `realpath` so symlinks cannot escape it. Sensitive files (.env*, .git/, *.pem, id_*,
|
|
17
|
+
* .npmrc) are denied with a message that says why. The cap is derived from the bridge's own
|
|
18
|
+
* MAX_MESSAGE_BYTES with the base64 4/3 inflation factor applied, so the encoded payload always
|
|
19
|
+
* fits in one WebSocket frame.
|
|
8
20
|
*/
|
|
9
21
|
import { ActionType, InputModeReason } from '@reticlehq/core';
|
|
10
22
|
import type { Session } from '../session/session.js';
|
|
11
23
|
import { type ToolDeps } from './tool-kit.js';
|
|
24
|
+
/**
|
|
25
|
+
* If the action is `upload` AND the inner args carry `path`, read the file from disk and rewrite
|
|
26
|
+
* the args to `{ content, name, type, __base64: true }` before the ACT command reaches the browser.
|
|
27
|
+
*
|
|
28
|
+
* Returns the (possibly rewritten) args object. All other actions are returned unchanged.
|
|
29
|
+
*
|
|
30
|
+
* This is the SINGLE interception point — called from session.command() so flow replay, crawl,
|
|
31
|
+
* and every other ACT dispatch site are covered without per-site wiring.
|
|
32
|
+
*/
|
|
33
|
+
export declare function rewriteUploadArgs(deps: Pick<ToolDeps, 'fs' | 'reticleRoot'>, action: string, innerArgs: Record<string, unknown>): Promise<Record<string, unknown>>;
|
|
12
34
|
interface RealActResult {
|
|
13
35
|
/** Defined only on a successful native action; `undefined` means the synthetic path runs. */
|
|
14
36
|
result: unknown;
|