@reticlehq/server 3.1.0 → 3.2.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 +17 -18
- package/dist/command/cli/auth/device-flow.d.ts +2 -2
- package/dist/command/cli/cli-doctor.js +13 -1
- package/dist/command/cli/cli-flow-commands.d.ts +2 -1
- package/dist/command/cli/cli-kill.js +1 -1
- package/dist/command/cli/cli-parse.d.ts +2 -1
- package/dist/command/cli/cli-parse.js +5 -2
- package/dist/command/cli/cli-verify.js +2 -2
- package/dist/command/cli/cloud-cli.js +7 -6
- package/dist/command/cli/cloud-kit.js +2 -6
- package/dist/command/cli/config/config-discovery.d.ts +12 -0
- package/dist/command/cli/config/config-discovery.js +19 -0
- package/dist/command/cli/doctor/doctor-rows.d.ts +12 -0
- package/dist/command/cli/doctor/doctor-rows.js +12 -0
- package/dist/command/cli/drive/drive-command.js +1 -1
- package/dist/command/cli/launch/cli-launch.d.ts +10 -2
- package/dist/command/cli/launch/cli-launch.js +32 -39
- package/dist/command/cli/lifecycle/daemon-lifecycle.d.ts +33 -0
- package/dist/command/cli/lifecycle/daemon-lifecycle.js +198 -0
- package/dist/command/cli/mcp-command.js +5 -3
- package/dist/command/cli/ports/resolve/cli-port.d.ts +2 -1
- package/dist/command/cli/ports/resolve/cli-port.js +5 -1
- package/dist/command/cli/status/status-lines.d.ts +28 -0
- package/dist/command/cli/status/status-lines.js +117 -0
- package/dist/command/cli.d.ts +1 -1
- package/dist/command/cli.js +24 -177
- package/dist/command/daemon/binding/daemon-status-probe.d.ts +2 -0
- package/dist/command/daemon/binding/daemon-status-probe.js +48 -0
- package/dist/command/daemon/binding/loopback-reach.d.ts +42 -0
- package/dist/command/daemon/binding/loopback-reach.js +63 -0
- package/dist/command/daemon/daemon-resilience.d.ts +13 -1
- package/dist/command/daemon/daemon-resilience.js +12 -1
- package/dist/command/daemon/daemon.d.ts +2 -11
- package/dist/command/daemon/daemon.js +8 -11
- package/dist/command/daemon/lifetime/heartbeat.d.ts +8 -0
- package/dist/command/daemon/lifetime/heartbeat.js +10 -0
- package/dist/command/daemon/lifetime/log-cap.d.ts +68 -0
- package/dist/command/daemon/lifetime/log-cap.js +115 -0
- package/dist/command/setup/agent-notice-memory.d.ts +16 -0
- package/dist/command/setup/agent-notice-memory.js +65 -0
- package/dist/command/setup/bringup/dev-server-wait.d.ts +1 -0
- package/dist/command/setup/bringup/dev-server-wait.js +16 -0
- package/dist/command/setup/bringup/ensure-daemon.js +1 -1
- package/dist/command/setup/init/init-runtime.js +13 -1
- package/dist/command/setup/node-effects.js +1 -1
- package/dist/command/setup/probe/page-probe.js +9 -2
- package/dist/command/setup/run-setup.d.ts +25 -7
- package/dist/command/setup/run-setup.js +51 -9
- package/dist/command/setup/setup-command.js +31 -3
- package/dist/command/setup/terminal/confirm.js +2 -1
- package/dist/command/update/updater.d.ts +27 -0
- package/dist/command/update/updater.js +35 -2
- package/dist/features/crawl/crawl-tools.js +3 -1
- package/dist/features/crawl/crawl.js +2 -1
- package/dist/features/harness/drive-report.d.ts +73 -0
- package/dist/features/harness/drive-report.js +196 -0
- package/dist/features/harness/driver.js +29 -8
- package/dist/features/harness/drivers.d.ts +19 -0
- package/dist/features/harness/drivers.js +19 -0
- package/dist/features/harness/fill-values.d.ts +67 -0
- package/dist/features/harness/fill-values.js +153 -0
- package/dist/features/harness/jev-driver.d.ts +188 -0
- package/dist/features/harness/jev-driver.js +664 -0
- package/dist/features/harness/openai-driver.d.ts +46 -0
- package/dist/features/harness/openai-driver.js +197 -0
- package/dist/features/harness/platform-config.d.ts +52 -0
- package/dist/features/harness/platform-config.js +89 -0
- package/dist/features/visual/visual-tools.js +26 -2
- package/dist/index.js +60 -55
- package/dist/judgement/domain/domain-tools.js +4 -2
- package/dist/judgement/runs/verification-sync.d.ts +2 -2
- package/dist/judgement/runs/verification-sync.js +9 -2
- package/dist/language/flows/affected-tools.js +6 -3
- package/dist/language/flows/flow-replay-run.d.ts +25 -2
- package/dist/language/flows/flow-replay-run.js +73 -27
- package/dist/language/flows/flow-replay.d.ts +17 -0
- package/dist/language/flows/flow-replay.js +148 -100
- package/dist/language/flows/flow-result.d.ts +18 -1
- package/dist/language/flows/flow-result.js +18 -2
- package/dist/language/flows/flow-store-for-session.d.ts +2 -1
- package/dist/language/flows/flow-tools.js +8 -4
- package/dist/language/flows/flows.d.ts +8 -7
- package/dist/language/flows/flows.js +7 -3
- package/dist/language/flows/narrow-write.d.ts +4 -4
- package/dist/language/flows/replay.js +1 -1
- package/dist/language/flows/suite-selection.d.ts +15 -1
- package/dist/language/flows/suite-selection.js +1 -1
- package/dist/language/flows/verify-change-tools.js +7 -4
- package/dist/memory/cloud/account-state.d.ts +1 -1
- package/dist/memory/cloud/account-state.js +5 -4
- package/dist/memory/cloud/cloud-config.d.ts +9 -0
- package/dist/memory/cloud/cloud-config.js +9 -0
- package/dist/memory/cloud/cloud-sync.d.ts +7 -2
- package/dist/memory/cloud/cloud-sync.js +10 -5
- package/dist/memory/cloud/harness-config.d.ts +10 -0
- package/dist/memory/cloud/harness-config.js +58 -0
- package/dist/memory/cloud/harness-offer.d.ts +44 -0
- package/dist/memory/cloud/harness-offer.js +146 -0
- package/dist/memory/cloud/harness-switch.d.ts +11 -0
- package/dist/memory/cloud/harness-switch.js +53 -0
- package/dist/memory/cloud/sync-daemon.d.ts +22 -2
- package/dist/memory/cloud/sync-daemon.js +24 -0
- package/dist/memory/impact/impact-recorder.d.ts +8 -0
- package/dist/memory/impact/impact-recorder.js +2 -1
- package/dist/memory/impact/impact-store.d.ts +5 -0
- package/dist/memory/impact/impact-store.js +19 -0
- package/dist/memory/intent/intent-shard-store.js +2 -2
- package/dist/memory/intent/intent-shard.d.ts +12 -12
- package/dist/memory/journal/attach-journal.d.ts +11 -0
- package/dist/memory/journal/attach-journal.js +1 -1
- package/dist/memory/journal/journal-query.d.ts +35 -1
- package/dist/memory/journal/journal-query.js +46 -0
- package/dist/memory/journal/journal-recorder.d.ts +50 -2
- package/dist/memory/journal/journal-recorder.js +45 -7
- package/dist/memory/journal/on-disk/retention.d.ts +87 -2
- package/dist/memory/journal/on-disk/retention.js +177 -8
- package/dist/memory/journal/on-disk/workspace-gitignore.d.ts +9 -0
- package/dist/memory/journal/on-disk/workspace-gitignore.js +9 -2
- package/dist/memory/journal/self-observation.d.ts +3 -0
- package/dist/memory/journal/self-observation.js +67 -0
- package/dist/memory/journal/session-end.d.ts +13 -3
- package/dist/memory/journal/session-end.js +24 -4
- package/dist/memory/journal/session-journal.d.ts +115 -5
- package/dist/memory/journal/session-journal.js +479 -32
- package/dist/memory/project/artifact-root-resolver.d.ts +3 -0
- package/dist/memory/project/artifact-root-resolver.js +89 -0
- package/dist/memory/project/artifact-root.d.ts +39 -2
- package/dist/memory/project/artifact-root.js +58 -1
- package/dist/memory/project/dir/fill-value-store.d.ts +32 -0
- package/dist/memory/project/dir/fill-value-store.js +88 -0
- package/dist/memory/project/dir/reticle-dir.d.ts +14 -4
- package/dist/memory/project/dir/reticle-dir.js +11 -0
- package/dist/memory/project/fs/fs-port.d.ts +22 -1
- package/dist/memory/project/fs/fs-port.js +33 -5
- package/dist/memory/project/learned-routes.d.ts +16 -2
- package/dist/memory/project/learned-routes.js +26 -10
- package/dist/memory/project/project-for-root.d.ts +30 -0
- package/dist/memory/project/project-for-root.js +43 -0
- package/dist/memory/project/project-tools.js +10 -5
- package/dist/memory/project/session-root.d.ts +35 -6
- package/dist/memory/project/session-root.js +70 -8
- package/dist/portal/bridge/auth-failure-reason.d.ts +9 -0
- package/dist/portal/bridge/auth-failure-reason.js +15 -1
- package/dist/portal/bridge/bridge.d.ts +6 -1
- package/dist/portal/bridge/bridge.js +36 -2
- package/dist/portal/session/facts/session-replaced.d.ts +19 -0
- package/dist/portal/session/facts/session-replaced.js +22 -0
- package/dist/portal/session/no-session-diagnosis.d.ts +1 -0
- package/dist/portal/session/no-session-diagnosis.js +67 -60
- package/dist/portal/session/no-session-next-action.js +3 -2
- package/dist/portal/session/no-session-watch.js +14 -10
- package/dist/portal/session/rebind.d.ts +18 -0
- package/dist/portal/session/rebind.js +38 -0
- package/dist/portal/session/session-manager.d.ts +4 -0
- package/dist/portal/session/session-manager.js +19 -1
- package/dist/portal/session/session.d.ts +21 -10
- package/dist/portal/session/session.js +29 -42
- package/dist/status-payload.d.ts +12 -1
- package/dist/status-payload.js +4 -1
- package/dist/surface/http-server.js +3 -5
- package/dist/surface/mcp/faults/mcp-outage.d.ts +2 -1
- package/dist/surface/mcp/faults/mcp-outage.js +20 -4
- package/dist/surface/mcp/mcp-proxy.d.ts +10 -1
- package/dist/surface/mcp/mcp-proxy.js +20 -9
- package/dist/surface/mcp/mcp.d.ts +10 -1
- package/dist/surface/mcp/mcp.js +59 -29
- package/dist/surface/mcp/proxy/proxy-daemon-probe.d.ts +15 -2
- package/dist/surface/mcp/proxy/proxy-daemon-probe.js +22 -5
- package/dist/surface/mcp/proxy/proxy-log.d.ts +5 -11
- package/dist/surface/mcp/proxy/proxy-log.js +7 -22
- package/dist/surface/tools/act/act-preflight.js +6 -1
- package/dist/surface/tools/act/sequence-grade.js +13 -0
- package/dist/surface/tools/act-capsule.js +27 -1
- package/dist/surface/tools/assert/assert-verdict.js +44 -17
- package/dist/surface/tools/coverage-tools.js +5 -2
- package/dist/surface/tools/dynamic-tools.js +37 -46
- package/dist/surface/tools/error-recovery.d.ts +6 -0
- package/dist/surface/tools/error-recovery.js +15 -0
- package/dist/surface/tools/explore-tools.js +80 -7
- package/dist/surface/tools/feedback-tools.js +49 -2
- package/dist/surface/tools/first-sentence.d.ts +24 -0
- package/dist/surface/tools/first-sentence.js +0 -0
- package/dist/surface/tools/harness-explore.d.ts +125 -1
- package/dist/surface/tools/harness-explore.js +401 -15
- package/dist/surface/tools/harness-plan.d.ts +76 -0
- package/dist/surface/tools/harness-plan.js +156 -0
- package/dist/surface/tools/harness-toolset.js +12 -1
- package/dist/surface/tools/invoke-tool.js +65 -5
- package/dist/surface/tools/live-call-text.d.ts +6 -0
- package/dist/surface/tools/live-call-text.js +36 -2
- package/dist/surface/tools/observe-tools.js +55 -9
- package/dist/surface/tools/read/query-windowed.d.ts +1 -1
- package/dist/surface/tools/read/query-windowed.js +1 -1
- package/dist/surface/tools/read-tools.js +1 -2
- package/dist/surface/tools/tool-kit.d.ts +18 -3
- package/dist/surface/tools/tools-helpers.d.ts +8 -7
- package/dist/surface/tools/tools-helpers.js +11 -13
- package/dist/surface/tools/tools.js +6 -1
- package/dist/telemetry/bug-found.js +32 -2
- package/dist/telemetry/feedback-context.js +3 -3
- package/dist/telemetry/harness-drive.d.ts +6 -0
- package/dist/telemetry/harness-drive.js +39 -0
- package/dist/telemetry/mcp-connection.d.ts +14 -1
- package/dist/telemetry/mcp-connection.js +17 -2
- package/dist/telemetry/onboarding-firsts.d.ts +30 -1
- package/dist/telemetry/onboarding-firsts.js +54 -12
- package/dist/telemetry/verification-of.js +31 -3
- package/docs/agent-cheatsheet.md +7 -3
- package/docs/autodrive.md +42 -2
- package/docs/benchmarks.md +1 -1
- package/docs/capabilities.mdx +1 -1
- package/docs/cli/cloud.mdx +3 -3
- package/docs/cli/doctor.mdx +1 -0
- package/docs/cli/status.mdx +35 -20
- package/docs/cli.mdx +8 -18
- package/docs/desktop.mdx +1 -1
- package/docs/faq.mdx +1 -1
- package/docs/install-manual.mdx +1 -1
- package/docs/packages/tauri.mdx +1 -1
- package/docs/packages.mdx +1 -1
- package/docs/predicates.mdx +16 -0
- package/docs/system-map.md +6 -4
- package/docs/telemetry.md +2 -1
- package/docs/token-efficiency.md +2 -2
- package/docs/tools/overview.mdx +1 -1
- package/docs/troubleshooting.mdx +8 -2
- package/docs/usage.md +1 -1
- package/package.json +6 -6
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a drive is FOR, decided before it starts and read out of `.reticle` rather than the app.
|
|
3
|
+
*
|
|
4
|
+
* The driver used to open its eyes on a page and pick whatever looked interesting. That makes every
|
|
5
|
+
* run start from zero: it re-drives journeys that are already saved, and it has no idea which
|
|
6
|
+
* declared intent nobody has ever tested. Both are the same missing input — the project's own
|
|
7
|
+
* record of what it claims to do and what has been proved about it.
|
|
8
|
+
*
|
|
9
|
+
* `.reticle` already holds that. `buildDomainModel` reads `.reticle/flows/` and
|
|
10
|
+
* `.reticle/contract.json` and answers with every saved flow, the consequence that MUST hold for
|
|
11
|
+
* each one, and the GAPS: signals and testids the app DECLARED that no flow asserts. So the plan is
|
|
12
|
+
* derivable with no browser, no model and no source code — which is the order this product argues
|
|
13
|
+
* for everywhere else and did not follow here.
|
|
14
|
+
*
|
|
15
|
+
* Two kinds of step, and the split is the whole economics:
|
|
16
|
+
*
|
|
17
|
+
* REPLAY a journey that is already recorded. Deterministic, no model in the loop, a few hundred
|
|
18
|
+
* tokens. Anything already covered belongs here and must never be re-driven.
|
|
19
|
+
* DRIVE a gap. A model is worth paying for exactly where nothing is proved yet.
|
|
20
|
+
*
|
|
21
|
+
* Nothing here calls a model. The plan is the deterministic half, and the model's only job is to
|
|
22
|
+
* choose among steps somebody already decided were worth taking.
|
|
23
|
+
*/
|
|
24
|
+
import type { DomainModel } from '../../judgement/domain/domain-model.js';
|
|
25
|
+
export declare const PlanStepKind: {
|
|
26
|
+
/** Already recorded: replay it deterministically instead of paying a model to rediscover it. */
|
|
27
|
+
readonly REPLAY: "replay";
|
|
28
|
+
/** Declared but never proved: this is what a drive is actually for. */
|
|
29
|
+
readonly DRIVE: "drive";
|
|
30
|
+
};
|
|
31
|
+
export type PlanStepKind = (typeof PlanStepKind)[keyof typeof PlanStepKind];
|
|
32
|
+
export interface PlanStep {
|
|
33
|
+
kind: PlanStepKind;
|
|
34
|
+
/** The flow to replay, or a short label for the gap to cover. */
|
|
35
|
+
target: string;
|
|
36
|
+
/**
|
|
37
|
+
* WHY this step is in the plan, in the project's own words.
|
|
38
|
+
*
|
|
39
|
+
* For a replay it is the consequence that must hold — the flow's `mustHold`, which is the thing
|
|
40
|
+
* that stops being true when the feature breaks. For a drive it is the declared intent nobody has
|
|
41
|
+
* tested. Carried because the model choosing between steps should be reading intent, not a list
|
|
42
|
+
* of names it has no way to rank.
|
|
43
|
+
*/
|
|
44
|
+
why: string;
|
|
45
|
+
}
|
|
46
|
+
export interface HarnessPlan {
|
|
47
|
+
steps: readonly PlanStep[];
|
|
48
|
+
/** One line an agent or a human can read, derived rather than narrated. */
|
|
49
|
+
summary: string;
|
|
50
|
+
/**
|
|
51
|
+
* The app's OWN vocabulary: the signals it declared in its capabilities, untested ones first.
|
|
52
|
+
*
|
|
53
|
+
* Carried out of `.reticle` for the same reason the steps are, and then used for something the
|
|
54
|
+
* plan itself never does. A drive declares what it expects BEFORE it acts, and a declaration can
|
|
55
|
+
* only be saved into the flow if it names something: `signal "order:placed"` records, a bare
|
|
56
|
+
* "some signal fires" does not, and a flow that records nothing replays green whatever the
|
|
57
|
+
* feature does. These are the names that make the difference.
|
|
58
|
+
*/
|
|
59
|
+
vocabulary: readonly string[];
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Build the plan.
|
|
63
|
+
*
|
|
64
|
+
* Replays come first, always. They are the cheap half and they establish what is still true before
|
|
65
|
+
* any model is paid to explore what is not yet known — and a regression found by a replay is found
|
|
66
|
+
* for a few hundred tokens rather than a few hundred thousand.
|
|
67
|
+
*/
|
|
68
|
+
export declare function buildHarnessPlan(domain: DomainModel): HarnessPlan;
|
|
69
|
+
/**
|
|
70
|
+
* The plan as the model reads it.
|
|
71
|
+
*
|
|
72
|
+
* Intent first and names second, because a model ranking "what next" against a list of identifiers
|
|
73
|
+
* is guessing, and ranking against what the project says each one is FOR is not. The kind is stated
|
|
74
|
+
* so it can prefer finishing the cheap deterministic half before spending itself on the rest.
|
|
75
|
+
*/
|
|
76
|
+
export declare function planAsText(plan: HarnessPlan): string;
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a drive is FOR, decided before it starts and read out of `.reticle` rather than the app.
|
|
3
|
+
*
|
|
4
|
+
* The driver used to open its eyes on a page and pick whatever looked interesting. That makes every
|
|
5
|
+
* run start from zero: it re-drives journeys that are already saved, and it has no idea which
|
|
6
|
+
* declared intent nobody has ever tested. Both are the same missing input — the project's own
|
|
7
|
+
* record of what it claims to do and what has been proved about it.
|
|
8
|
+
*
|
|
9
|
+
* `.reticle` already holds that. `buildDomainModel` reads `.reticle/flows/` and
|
|
10
|
+
* `.reticle/contract.json` and answers with every saved flow, the consequence that MUST hold for
|
|
11
|
+
* each one, and the GAPS: signals and testids the app DECLARED that no flow asserts. So the plan is
|
|
12
|
+
* derivable with no browser, no model and no source code — which is the order this product argues
|
|
13
|
+
* for everywhere else and did not follow here.
|
|
14
|
+
*
|
|
15
|
+
* Two kinds of step, and the split is the whole economics:
|
|
16
|
+
*
|
|
17
|
+
* REPLAY a journey that is already recorded. Deterministic, no model in the loop, a few hundred
|
|
18
|
+
* tokens. Anything already covered belongs here and must never be re-driven.
|
|
19
|
+
* DRIVE a gap. A model is worth paying for exactly where nothing is proved yet.
|
|
20
|
+
*
|
|
21
|
+
* Nothing here calls a model. The plan is the deterministic half, and the model's only job is to
|
|
22
|
+
* choose among steps somebody already decided were worth taking.
|
|
23
|
+
*/
|
|
24
|
+
export const PlanStepKind = {
|
|
25
|
+
/** Already recorded: replay it deterministically instead of paying a model to rediscover it. */
|
|
26
|
+
REPLAY: 'replay',
|
|
27
|
+
/** Declared but never proved: this is what a drive is actually for. */
|
|
28
|
+
DRIVE: 'drive',
|
|
29
|
+
};
|
|
30
|
+
/**
|
|
31
|
+
* How many untested declarations become drive steps.
|
|
32
|
+
*
|
|
33
|
+
* A contract can declare hundreds of testids, and a plan longer than the step budget is not a plan,
|
|
34
|
+
* it is a list. Capped so the drive spends its budget on the front of a ranked queue rather than
|
|
35
|
+
* spreading itself across everything and finishing none of it.
|
|
36
|
+
*/
|
|
37
|
+
const MAX_DRIVE_STEPS = 12;
|
|
38
|
+
/**
|
|
39
|
+
* How many recorded journeys become replay steps.
|
|
40
|
+
*
|
|
41
|
+
* A project with seventy-four flows produced a seventy-four-step plan, which went into the tool
|
|
42
|
+
* result AND the standing instruction — thousands of tokens spent, every turn, listing work the
|
|
43
|
+
* drive had no budget to do. The whole argument for reading `.reticle` first is spending less, and
|
|
44
|
+
* an uncapped plan spends more.
|
|
45
|
+
*
|
|
46
|
+
* `reticle_domain` caps its own per-flow detail at 25 for the same measured reason. Ranked before
|
|
47
|
+
* cutting, so what survives is what is worth replaying first rather than whatever was listed first.
|
|
48
|
+
*/
|
|
49
|
+
const MAX_REPLAY_STEPS = 15;
|
|
50
|
+
/** Signals first: a declared signal is a stronger statement of intent than a testid. */
|
|
51
|
+
function driveSteps(domain) {
|
|
52
|
+
const steps = [];
|
|
53
|
+
for (const signal of domain.gaps.declaredUntestedSignals) {
|
|
54
|
+
steps.push({
|
|
55
|
+
kind: PlanStepKind.DRIVE,
|
|
56
|
+
target: signal,
|
|
57
|
+
why: `the app declares the signal "${signal}" and no saved flow asserts it`,
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
for (const testid of domain.gaps.declaredUntestedTestids) {
|
|
61
|
+
steps.push({
|
|
62
|
+
kind: PlanStepKind.DRIVE,
|
|
63
|
+
target: testid,
|
|
64
|
+
why: `the app declares the control "${testid}" and no saved flow exercises it`,
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
return steps.slice(0, MAX_DRIVE_STEPS);
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Replay steps, worst-risk first when there is run history to rank by.
|
|
71
|
+
*
|
|
72
|
+
* A flow that asserts nothing is listed too, and says so: replaying it proves very little, and the
|
|
73
|
+
* honest plan admits that rather than counting it as coverage.
|
|
74
|
+
*/
|
|
75
|
+
function replaySteps(domain) {
|
|
76
|
+
const byName = new Map(domain.flows.map((flow) => [flow.name, flow]));
|
|
77
|
+
const ordered = 0 === domain.riskRanked.length
|
|
78
|
+
? domain.flows.map((flow) => flow.name)
|
|
79
|
+
: [
|
|
80
|
+
...domain.riskRanked,
|
|
81
|
+
...domain.flows.map((f) => f.name).filter((name) => !domain.riskRanked.includes(name)),
|
|
82
|
+
];
|
|
83
|
+
/*
|
|
84
|
+
* Flows that assert a real consequence come first, whatever the run history says.
|
|
85
|
+
*
|
|
86
|
+
* A flow with no `mustHold` passes as long as its steps still run, so replaying one proves close
|
|
87
|
+
* to nothing — and when the plan has to be cut, proving nothing is the first thing to give up.
|
|
88
|
+
*/
|
|
89
|
+
const ranked = [
|
|
90
|
+
...ordered.filter((name) => byName.get(name)?.mustHold !== undefined),
|
|
91
|
+
...ordered.filter((name) => byName.get(name)?.mustHold === undefined),
|
|
92
|
+
];
|
|
93
|
+
const steps = [];
|
|
94
|
+
for (const name of ranked) {
|
|
95
|
+
const flow = byName.get(name);
|
|
96
|
+
if (flow === undefined)
|
|
97
|
+
continue;
|
|
98
|
+
steps.push({
|
|
99
|
+
kind: PlanStepKind.REPLAY,
|
|
100
|
+
target: name,
|
|
101
|
+
why: flow.mustHold === undefined
|
|
102
|
+
? `already recorded, but it asserts no consequence — replaying it proves only that the steps still run`
|
|
103
|
+
: `already recorded; it must still hold: ${flow.mustHold}`,
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
return steps.slice(0, MAX_REPLAY_STEPS);
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Build the plan.
|
|
110
|
+
*
|
|
111
|
+
* Replays come first, always. They are the cheap half and they establish what is still true before
|
|
112
|
+
* any model is paid to explore what is not yet known — and a regression found by a replay is found
|
|
113
|
+
* for a few hundred tokens rather than a few hundred thousand.
|
|
114
|
+
*/
|
|
115
|
+
export function buildHarnessPlan(domain) {
|
|
116
|
+
const replays = replaySteps(domain);
|
|
117
|
+
const drives = driveSteps(domain);
|
|
118
|
+
const steps = [...replays, ...drives];
|
|
119
|
+
// Counted over EVERYTHING, then listed up to the cap — the same split `reticle_domain` uses, so a
|
|
120
|
+
// capped listing never quietly becomes a smaller analysis.
|
|
121
|
+
const totalRecorded = domain.flows.length;
|
|
122
|
+
const totalGaps = domain.gaps.declaredUntestedSignals.length + domain.gaps.declaredUntestedTestids.length;
|
|
123
|
+
const omitted = totalRecorded - replays.length + (totalGaps - drives.length);
|
|
124
|
+
/*
|
|
125
|
+
* Untested declarations first: a signal no flow asserts is exactly the one a drive should be
|
|
126
|
+
* claiming, and it is the same ranking the drive steps above already use.
|
|
127
|
+
*
|
|
128
|
+
* Then the signals saved flows ALREADY assert, which is not redundant and not a nicety. Most apps
|
|
129
|
+
* declare no capabilities at all -- the production dashboard this was measured on has no
|
|
130
|
+
* `contract.json` -- so a vocabulary built only from declarations is empty for exactly the
|
|
131
|
+
* projects that most need one. A signal a saved flow asserts is better evidence than a
|
|
132
|
+
* declaration anyway: it is a name the app has actually been observed to fire.
|
|
133
|
+
*/
|
|
134
|
+
const vocabulary = [
|
|
135
|
+
...domain.gaps.declaredUntestedSignals,
|
|
136
|
+
...domain.declared.signals,
|
|
137
|
+
...domain.flows.flatMap((flow) => flow.signals),
|
|
138
|
+
].filter((name, index, all) => 0 < name.length && all.indexOf(name) === index);
|
|
139
|
+
const summary = 0 === steps.length
|
|
140
|
+
? 'Nothing is recorded and nothing is declared: there is no plan to follow, so drive the app to find out what it does.'
|
|
141
|
+
: `${String(totalRecorded)} recorded journey(s) and ${String(totalGaps)} untested declaration(s); the plan lists the ${String(steps.length)} worth doing first${0 === omitted ? '' : `, omitting ${String(omitted)}`}.`;
|
|
142
|
+
return { steps, summary, vocabulary };
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* The plan as the model reads it.
|
|
146
|
+
*
|
|
147
|
+
* Intent first and names second, because a model ranking "what next" against a list of identifiers
|
|
148
|
+
* is guessing, and ranking against what the project says each one is FOR is not. The kind is stated
|
|
149
|
+
* so it can prefer finishing the cheap deterministic half before spending itself on the rest.
|
|
150
|
+
*/
|
|
151
|
+
export function planAsText(plan) {
|
|
152
|
+
if (0 === plan.steps.length)
|
|
153
|
+
return 'PLAN: none — nothing recorded, nothing declared.';
|
|
154
|
+
const lines = plan.steps.map((step, index) => ` ${String(index + 1)}. [${step.kind}] ${step.target} — ${step.why}`);
|
|
155
|
+
return `PLAN (from .reticle, not from the source):\n${lines.join('\n')}`;
|
|
156
|
+
}
|
|
@@ -23,7 +23,18 @@ import { withTimeout } from '../../features/harness/with-timeout.js';
|
|
|
23
23
|
* only through a discovery hop — and a hop the model has to take on every journey is a hop it will
|
|
24
24
|
* sometimes skip, which silently turns the cheap path off.
|
|
25
25
|
*/
|
|
26
|
-
|
|
26
|
+
/**
|
|
27
|
+
* Beyond the default surface, the three tools a drive cannot do its job without.
|
|
28
|
+
*
|
|
29
|
+
* RECORD and FLOW_SAVE are how a drive keeps what it drove. FLOW_REPLAY is how it avoids driving
|
|
30
|
+
* the same thing twice — it was absent, so a driver handed a plan full of already-recorded journeys
|
|
31
|
+
* had no way to run one, and every run paid a model to rediscover what was on disk.
|
|
32
|
+
*/
|
|
33
|
+
const HARNESS_EXTRA_TOOLS = [
|
|
34
|
+
ReticleTool.RECORD,
|
|
35
|
+
ReticleTool.FLOW_SAVE,
|
|
36
|
+
ReticleTool.FLOW_REPLAY,
|
|
37
|
+
];
|
|
27
38
|
/**
|
|
28
39
|
* The one deliberate subtraction.
|
|
29
40
|
*
|
|
@@ -14,10 +14,11 @@ import { resultIsError } from '../mcp/faults/mcp-is-error.js';
|
|
|
14
14
|
import { verificationOf } from '../../telemetry/verification-of.js';
|
|
15
15
|
import { emitBugFoundHook, emitVerdictHook } from '../../hooks/hook-emit.js';
|
|
16
16
|
import { reportOnboardingStep } from '../../telemetry/onboarding-funnel.js';
|
|
17
|
-
import { noteFirstVerdict, noteOnboardingFirst } from '../../telemetry/onboarding-firsts.js';
|
|
17
|
+
import { noteActed, noteFirstVerdict, noteOnboardingFirst } from '../../telemetry/onboarding-firsts.js';
|
|
18
|
+
import { withHarnessDrive } from '../../telemetry/harness-drive.js';
|
|
18
19
|
import { OnboardingPhase, OnboardingStepStatus } from '@reticlehq/core/telemetry';
|
|
19
20
|
import { asString } from '@reticlehq/core';
|
|
20
|
-
import { sessionIdFromArgs, spentRefFromArgs } from './tools-helpers.js';
|
|
21
|
+
import { SESSION_ID_ARG, sessionIdFromArgs, spentRefFromArgs } from './tools-helpers.js';
|
|
21
22
|
import { EnvelopeKey } from './tool-kit.js';
|
|
22
23
|
import { ReticleTool } from '@reticlehq/core';
|
|
23
24
|
import { takeFeedbackPrompt } from './feedback-tools.js';
|
|
@@ -152,6 +153,17 @@ export const SESSION_EXEMPT_TOOLS = new Set([
|
|
|
152
153
|
// when NO session exists — "nothing ever connected" is feedback we especially want.
|
|
153
154
|
ReticleTool.FEEDBACK,
|
|
154
155
|
]);
|
|
156
|
+
/**
|
|
157
|
+
* The merged action that hands the drive to a model inside the daemon.
|
|
158
|
+
*
|
|
159
|
+
* Derived from the member tool's own name rather than spelled again: the merge dispatches on
|
|
160
|
+
* `action`, and `reticle_verify` plus `explore` IS `reticle_verify_explore`. A literal here would be
|
|
161
|
+
* a third spelling of one name, and the one that silently stops matching when the action is renamed.
|
|
162
|
+
*/
|
|
163
|
+
const HARNESS_DRIVE_ACTION = ReticleTool.VERIFY_EXPLORE.slice(`${ReticleTool.VERIFY}_`.length);
|
|
164
|
+
function isHarnessDrive(toolName, args) {
|
|
165
|
+
return ReticleTool.VERIFY === toolName && HARNESS_DRIVE_ACTION === args['action'];
|
|
166
|
+
}
|
|
155
167
|
function isPlainObject(value) {
|
|
156
168
|
return 'object' === typeof value && value !== null && !Array.isArray(value);
|
|
157
169
|
}
|
|
@@ -299,6 +311,34 @@ function firstOversizedArg(args) {
|
|
|
299
311
|
}
|
|
300
312
|
return undefined;
|
|
301
313
|
}
|
|
314
|
+
/**
|
|
315
|
+
* The args the handler is dispatched with, pinned to the session THIS call resolved.
|
|
316
|
+
*
|
|
317
|
+
* `runTool` resolves the session once and then attributes the whole call to it: the pool lease it
|
|
318
|
+
* refreshes, the health envelope it splices, the tab a ref is recorded as minted in, the project
|
|
319
|
+
* ledger the call lands in, the session a verdict is reported under. The handler was then handed
|
|
320
|
+
* the caller's args untouched, so a call that named no `sessionId` resolved a SECOND time — and a
|
|
321
|
+
* tab that connects in between can win auto-selection on its fresher heartbeat. The action goes to
|
|
322
|
+
* one tab and the answer describes another, which is a verdict about a page that was never driven
|
|
323
|
+
* and reads exactly like a correct one (#983).
|
|
324
|
+
*
|
|
325
|
+
* The wrong-tab ref guard is the sharpest case: it is asked whether the ref belongs to the tab
|
|
326
|
+
* resolution picked, so its permission is only sound if that is also the tab the handler drives.
|
|
327
|
+
*
|
|
328
|
+
* A COPY, never a write into the caller's object. `reticle_run`, the merged families and the
|
|
329
|
+
* programmatic invoker all pass the same record on, and a dispatch decision leaking back into it
|
|
330
|
+
* would outlive this call.
|
|
331
|
+
*
|
|
332
|
+
* Only a GUESSED session is pinned. An explicit id — including one lifted off a sequence step — is
|
|
333
|
+
* passed through exactly as written, so whether a stale id refuses or rebinds stays the resolver's
|
|
334
|
+
* decision; and a tool no session was resolved for (disk-only tools, or nothing connected) is left
|
|
335
|
+
* unscoped, so it answers as it always did.
|
|
336
|
+
*/
|
|
337
|
+
function pinnedArgs(args, explicitSessionId, session) {
|
|
338
|
+
if (explicitSessionId !== undefined || session === undefined)
|
|
339
|
+
return args;
|
|
340
|
+
return { ...args, [SESSION_ID_ARG]: session.id };
|
|
341
|
+
}
|
|
302
342
|
export async function runTool(tool, deps, args) {
|
|
303
343
|
// Both dispatch paths (MCP + programmatic) pass through here — the one place "which tool is mostly
|
|
304
344
|
// used" can be counted. This used to EMIT an event per call; it now increments an in-process counter
|
|
@@ -313,7 +353,7 @@ export async function runTool(tool, deps, args) {
|
|
|
313
353
|
noteToolCall();
|
|
314
354
|
// The ONBOARD firsts, at the chokepoint every call already passes through — so a second dispatch
|
|
315
355
|
// path cannot quietly stop reporting them the way an extra listener would.
|
|
316
|
-
noteOnboardingFirst(tool.name
|
|
356
|
+
noteOnboardingFirst(tool.name);
|
|
317
357
|
// The impact record needs the project's own `.reticle` root, and this is the first place every
|
|
318
358
|
// call knows it. Idempotent: the first root wins for the daemon's lifetime.
|
|
319
359
|
initImpact({ reticleRoot: deps.reticleRoot });
|
|
@@ -343,8 +383,13 @@ export async function runTool(tool, deps, args) {
|
|
|
343
383
|
}
|
|
344
384
|
// An ACTION is what gets abandoned. Counted here, against verifications, so "the agent drove the
|
|
345
385
|
// page and then wandered off" becomes a number instead of an impression.
|
|
346
|
-
if (ACTION_TOOLS.has(tool.name))
|
|
386
|
+
if (ACTION_TOOLS.has(tool.name)) {
|
|
347
387
|
getSessionMetrics().recordAction();
|
|
388
|
+
// The two funnel rungs this branch is the authority on: the first action of the run, and the
|
|
389
|
+
// fact that this install has been driven at all. Reported from the set that already decides
|
|
390
|
+
// what "drove the page" means, so a fourth driving tool inherits both.
|
|
391
|
+
noteActed(args);
|
|
392
|
+
}
|
|
348
393
|
const settleTiming = getSessionMetrics().startToolCall(tool.name, args);
|
|
349
394
|
const startedAt = Date.now();
|
|
350
395
|
const rawSessionId = sessionIdFromArgs(args);
|
|
@@ -430,12 +475,27 @@ export async function runTool(tool, deps, args) {
|
|
|
430
475
|
const leaseId = session?.id ?? rawSessionId;
|
|
431
476
|
if (leaseId !== undefined)
|
|
432
477
|
deps.pool?.touch(leaseId);
|
|
478
|
+
// One identity for the whole call: the handler drives the session everything above is attributed
|
|
479
|
+
// to, rather than resolving again against a registry that can have changed underneath it.
|
|
480
|
+
const dispatchArgs = pinnedArgs(args, rawSessionId, session);
|
|
433
481
|
let raw;
|
|
434
482
|
try {
|
|
435
483
|
// The one dispatch point every tool call passes through, so it is where the trace's root span
|
|
436
484
|
// belongs: with RETICLE_TRACE on, every stage that runs underneath inherits this call's id and
|
|
437
485
|
// nests under it. Free when off — see trace.ts.
|
|
438
|
-
|
|
486
|
+
// `dispatchArgs`, not `args`: an inferred session is pinned into the arguments before the
|
|
487
|
+
// handler sees them, so a tool that resolves the session a second time cannot land on a
|
|
488
|
+
// different one than the dispatcher chose.
|
|
489
|
+
const call = () => span('tool.handler', { tool: tool.name, session: session?.id }, () => tool.handler(deps, dispatchArgs));
|
|
490
|
+
// A drive we run on the user's behalf, marked for its whole span so the verdicts it produces
|
|
491
|
+
// underneath are distinguishable from the ones the user's own agent earned.
|
|
492
|
+
//
|
|
493
|
+
// The outer call emits no verdict of its own, and that is the honest answer rather than a gap:
|
|
494
|
+
// it drives and records, it adjudicates nothing, and its result carries no verified/pass field
|
|
495
|
+
// to read. Deriving one from how far the drive got would be inventing a verdict nobody reached.
|
|
496
|
+
// What was missing was never an event here — it was that a session driven BY US read exactly
|
|
497
|
+
// like one the agent earned, which is what the span fixes.
|
|
498
|
+
raw = await (isHarnessDrive(tool.name, args) ? withHarnessDrive(call) : call());
|
|
439
499
|
}
|
|
440
500
|
catch (error) {
|
|
441
501
|
// The commonest refusal shape by far, and the one nothing could see: the message is built, handed
|
|
@@ -15,5 +15,11 @@ export declare function liveCallText(text: string, advertised: ReadonlySet<strin
|
|
|
15
15
|
* was corrected and the result became unreadable, which is a worse trade than the advice was worth.
|
|
16
16
|
*
|
|
17
17
|
* Values, not keys: a key is a contract with the caller and nothing in a key is advice.
|
|
18
|
+
*
|
|
19
|
+
* And not every value is advice either. A field that carries a tool's IDENTITY is an answer to
|
|
20
|
+
* "which one", not a suggestion about what to call — rewriting it destroys the thing the caller
|
|
21
|
+
* asked for. `reticle_tools { names: ["reticle_act_sequence"] }` sets `name` to the name it was
|
|
22
|
+
* asked about, and this rewrote it to `reticle_act`, so a batch query came back with entries the
|
|
23
|
+
* caller could no longer match to their questions. See `IDENTITY_KEYS`.
|
|
18
24
|
*/
|
|
19
25
|
export declare function liveCallValues(value: unknown, advertised: ReadonlySet<string>): unknown;
|
|
@@ -123,12 +123,37 @@ export function liveCallText(text, advertised) {
|
|
|
123
123
|
continue;
|
|
124
124
|
out = out.replace(pattern, instead);
|
|
125
125
|
}
|
|
126
|
-
|
|
126
|
+
/**
|
|
127
|
+
* A sentence explaining where a name WENT keeps that name, even though it is unreachable.
|
|
128
|
+
*
|
|
129
|
+
* These messages open with their own subject — "reticle_act_sequence was merged into reticle_act",
|
|
130
|
+
* "reticle_refresh no longer exists" — and the rewrite is precisely a rule for replacing
|
|
131
|
+
* unreachable names, so it replaced the subject too. Asking about `reticle_act_sequence` answered
|
|
132
|
+
* "reticle_act no longer exists", which is false: `reticle_act` is a tool, and the one thing the
|
|
133
|
+
* reader needed to know was which name had moved.
|
|
134
|
+
*
|
|
135
|
+
* Only the leading token, and only in front of those two phrases. Every other mention in the same
|
|
136
|
+
* sentence — including the tool to call INSTEAD — is still rewritten, which is the whole job.
|
|
137
|
+
*/
|
|
138
|
+
const explainsItsSubject = /^(reticle_[a-z_]+)(?= (?:was merged into|no longer exists))/.exec(out);
|
|
139
|
+
const subject = explainsItsSubject?.[1];
|
|
140
|
+
return out.replace(TOOL_TOKEN, (name, offset) => {
|
|
141
|
+
if (name === subject && 0 === offset)
|
|
142
|
+
return name;
|
|
127
143
|
if (advertised.has(name))
|
|
128
144
|
return name;
|
|
129
145
|
return replacementFor(name, advertised) ?? name;
|
|
130
146
|
});
|
|
131
147
|
}
|
|
148
|
+
/**
|
|
149
|
+
* Keys whose STRING value names a thing rather than advising a call.
|
|
150
|
+
*
|
|
151
|
+
* `name` is what the caller asked about and has to match their question against; `tool` is the
|
|
152
|
+
* already-resolved answer to "call this instead", so redirecting it a second time can only move it
|
|
153
|
+
* away from the answer. Deliberately two names and not a pattern: every other string in a result is
|
|
154
|
+
* prose, and prose is exactly what the rewrite is for.
|
|
155
|
+
*/
|
|
156
|
+
const IDENTITY_KEYS = new Set(['name', 'tool']);
|
|
132
157
|
/**
|
|
133
158
|
* The same rewrite, applied to a result's STRING VALUES instead of to its encoded form.
|
|
134
159
|
*
|
|
@@ -140,6 +165,12 @@ export function liveCallText(text, advertised) {
|
|
|
140
165
|
* was corrected and the result became unreadable, which is a worse trade than the advice was worth.
|
|
141
166
|
*
|
|
142
167
|
* Values, not keys: a key is a contract with the caller and nothing in a key is advice.
|
|
168
|
+
*
|
|
169
|
+
* And not every value is advice either. A field that carries a tool's IDENTITY is an answer to
|
|
170
|
+
* "which one", not a suggestion about what to call — rewriting it destroys the thing the caller
|
|
171
|
+
* asked for. `reticle_tools { names: ["reticle_act_sequence"] }` sets `name` to the name it was
|
|
172
|
+
* asked about, and this rewrote it to `reticle_act`, so a batch query came back with entries the
|
|
173
|
+
* caller could no longer match to their questions. See `IDENTITY_KEYS`.
|
|
143
174
|
*/
|
|
144
175
|
export function liveCallValues(value, advertised) {
|
|
145
176
|
if ('string' === typeof value)
|
|
@@ -149,7 +180,10 @@ export function liveCallValues(value, advertised) {
|
|
|
149
180
|
if (null !== value && 'object' === typeof value) {
|
|
150
181
|
const out = {};
|
|
151
182
|
for (const [key, each] of Object.entries(value)) {
|
|
152
|
-
out[key] =
|
|
183
|
+
out[key] =
|
|
184
|
+
IDENTITY_KEYS.has(key) && 'string' === typeof each
|
|
185
|
+
? each
|
|
186
|
+
: liveCallValues(each, advertised);
|
|
153
187
|
}
|
|
154
188
|
return out;
|
|
155
189
|
}
|
|
@@ -19,6 +19,7 @@ import { applyEventBudget, costHint, withSizeCost, DEFAULT_OBSERVE_EVENT_LIMIT,
|
|
|
19
19
|
import { annotateStarvedFailure, healthEnvelope, bufferEnvelope, } from '../../portal/session/session-health.js';
|
|
20
20
|
import { assertsDerivedIpcStatus, DERIVED_IPC_STATUS_ADVICE, isPresenceOnlyAssertion, PRESENCE_ONLY_ADVICE, } from './assert/assert-grade.js';
|
|
21
21
|
import { assertVerdict } from './assert/assert-verdict.js';
|
|
22
|
+
import { followLostObservation } from './act/act-observation.js';
|
|
22
23
|
import { withGapNovelty } from './gap-novelty.js';
|
|
23
24
|
import { assertionSource } from './assert/assert-source.js';
|
|
24
25
|
import { isChangeUndeclared } from '@reticlehq/engine/evidence/undeclared-change.js';
|
|
@@ -208,6 +209,10 @@ export const OBSERVE_TOOLS = [
|
|
|
208
209
|
.boolean()
|
|
209
210
|
.optional()
|
|
210
211
|
.describe('The tab disconnected mid-wait, so this was never observed — the verdict is UNKNOWN, not a failure of the app.'),
|
|
212
|
+
sessionId: z
|
|
213
|
+
.string()
|
|
214
|
+
.optional()
|
|
215
|
+
.describe('The session that answered, when a full-document navigation replaced the one this was asked of. Absent when the original session survived.'),
|
|
211
216
|
inconclusive: z
|
|
212
217
|
.string()
|
|
213
218
|
.optional()
|
|
@@ -236,7 +241,7 @@ export const OBSERVE_TOOLS = [
|
|
|
236
241
|
handler: async (deps, args) => {
|
|
237
242
|
const waitBudget = asNumber(args['timeout_ms']) ?? DEFAULT_ASSERT_TIMEOUT_MS;
|
|
238
243
|
// Spend the budget waiting for the APP as well as for the predicate. See resolve-within.
|
|
239
|
-
|
|
244
|
+
let session = await resolveSessionWithin(deps.sessions, asString(args['sessionId']), waitBudget, WALL_CLOCK);
|
|
240
245
|
// `until` is act_and_wait's name for this — see alias-args.ts.
|
|
241
246
|
const predicate = parsePredicate(aliasParam(args, 'predicate', ['until'])['predicate']);
|
|
242
247
|
// Refused up front rather than waited out: a body clause this session cannot answer would
|
|
@@ -245,8 +250,22 @@ export const OBSERVE_TOOLS = [
|
|
|
245
250
|
if (bodyRefusal !== undefined)
|
|
246
251
|
throw new Error(bodyRefusal);
|
|
247
252
|
// Honesty: explicit since wins; else default to the last act's cursor; else the whole buffer.
|
|
248
|
-
|
|
249
|
-
|
|
253
|
+
let since = asNumber(args['since']) ?? session.lastAct.cursor() ?? 0;
|
|
254
|
+
// See the note on the assert handler below: a wait cut off by a full-document navigation is
|
|
255
|
+
// followed to the document that took over, rather than graded as a lost observation there.
|
|
256
|
+
const predicateStarted = session.elapsed();
|
|
257
|
+
const followed = await followLostObservation({
|
|
258
|
+
sessions: deps.sessions,
|
|
259
|
+
session,
|
|
260
|
+
verdict: await waitForPredicate(session, predicate, waitBudget, since),
|
|
261
|
+
timeout: waitBudget,
|
|
262
|
+
predicateStarted,
|
|
263
|
+
reevaluate: (next, budget) => waitForPredicate(next, predicate, budget, 0),
|
|
264
|
+
});
|
|
265
|
+
if (followed.followed)
|
|
266
|
+
since = 0;
|
|
267
|
+
session = followed.session;
|
|
268
|
+
const verdict = followed.verdict;
|
|
250
269
|
// match reticle_assert — wrap with control + session health (throttle matters most while blocking)
|
|
251
270
|
// and the buffer envelope, so a verdict reached over an evicted window says so.
|
|
252
271
|
return withControl(session, {
|
|
@@ -255,6 +274,7 @@ export const OBSERVE_TOOLS = [
|
|
|
255
274
|
// `lastActSourceOnFailure` — an assert used to be blamed on the previous act's file:line.
|
|
256
275
|
...annotateStarvedFailure(session, verdict),
|
|
257
276
|
...assertionSource(session, predicate, verdict),
|
|
277
|
+
...(followed.followed ? { sessionId: session.id } : {}),
|
|
258
278
|
...healthEnvelope(session),
|
|
259
279
|
...bufferEnvelope(session),
|
|
260
280
|
});
|
|
@@ -271,7 +291,7 @@ export const OBSERVE_TOOLS = [
|
|
|
271
291
|
// is the most common thing an agent wants to assert. A field report reached us from an agent
|
|
272
292
|
// that guessed `urlContains` on route (net's spelling) and got unrecognized_keys.
|
|
273
293
|
'Predicate to evaluate. Kinds: { signal, name|dataMatches|count } ' +
|
|
274
|
-
'{ net, urlContains|method|status|count|bodyContains|requestBodyContains|requestBodyMatches } ' +
|
|
294
|
+
'{ net, urlContains|method|status|count|bodyMatches|bodyContains|requestBodyContains|requestBodyMatches } ' +
|
|
275
295
|
'{ state, path|equals } { route, pathname (exact) | contains (path+query+hash) } ' +
|
|
276
296
|
'{ element, testid|role|text } { text } { console, level|contains|absent } { animation, name } ' +
|
|
277
297
|
'{ settled } — combine with { allOf | anyOf | not }. Prefer a signal/net/state consequence ' +
|
|
@@ -294,6 +314,10 @@ export const OBSERVE_TOOLS = [
|
|
|
294
314
|
.boolean()
|
|
295
315
|
.optional()
|
|
296
316
|
.describe('The tab disconnected mid-wait, so this was never observed — the verdict is UNKNOWN, not a failure of the app.'),
|
|
317
|
+
sessionId: z
|
|
318
|
+
.string()
|
|
319
|
+
.optional()
|
|
320
|
+
.describe('The session that answered, when a full-document navigation replaced the one this was asked of. Absent when the original session survived.'),
|
|
297
321
|
inconclusive: z
|
|
298
322
|
.string()
|
|
299
323
|
.optional()
|
|
@@ -338,7 +362,7 @@ export const OBSERVE_TOOLS = [
|
|
|
338
362
|
handler: async (deps, args) => {
|
|
339
363
|
const timeout = asNumber(args['timeout_ms']) ?? 0;
|
|
340
364
|
// Spend the budget waiting for the APP as well as for the predicate. See resolve-within.
|
|
341
|
-
|
|
365
|
+
let session = await resolveSessionWithin(deps.sessions, asString(args['sessionId']), timeout, WALL_CLOCK);
|
|
342
366
|
// `until` is act_and_wait's name for this — see alias-args.ts.
|
|
343
367
|
const predicate = parsePredicate(aliasParam(args, 'predicate', ['until'])['predicate']);
|
|
344
368
|
// Refused up front rather than waited out: a body clause this session cannot answer would
|
|
@@ -347,13 +371,34 @@ export const OBSERVE_TOOLS = [
|
|
|
347
371
|
if (bodyRefusal !== undefined)
|
|
348
372
|
throw new Error(bodyRefusal);
|
|
349
373
|
// Honesty: explicit since wins; else default to the last act's cursor; else the whole buffer.
|
|
350
|
-
|
|
374
|
+
let since = asNumber(args['since']) ?? session.lastAct.cursor() ?? 0;
|
|
351
375
|
// Declared BEFORE the verdict, so the undeclared-change read below finds it open and stays
|
|
352
376
|
// quiet on THIS verdict rather than on the next one. Discharged after that read.
|
|
353
377
|
const intentId = await linkInlineIntent(deps, asString(args['sessionId']), asString(args['intent']), PredicateKind.SETTLED === predicate.kind ? undefined : predicate);
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
378
|
+
// A full-document navigation tears the in-page SDK down mid-wait, and the predicate finishes
|
|
379
|
+
// `observationLost` against a channel that no longer exists. `act_and_wait` has followed the
|
|
380
|
+
// document that took over since the MPA drive was fixed; this path did not, so an assertion
|
|
381
|
+
// straddling a navigation came back "Reticle could not tell" while the answer was sitting on
|
|
382
|
+
// the successor — reported four times from the field, every one of them proving `yes` on the
|
|
383
|
+
// UNCHANGED predicate after a manual reconnect. Re-asking is not fabricating: the consequence
|
|
384
|
+
// is evaluated on a live document, and where there is no unique successor to follow,
|
|
385
|
+
// `followLostObservation` refuses to guess and the honest lost verdict stands.
|
|
386
|
+
const predicateStarted = session.elapsed();
|
|
387
|
+
const followed = await followLostObservation({
|
|
388
|
+
sessions: deps.sessions,
|
|
389
|
+
session,
|
|
390
|
+
verdict: timeout > 0
|
|
391
|
+
? await waitForPredicate(session, predicate, timeout, since)
|
|
392
|
+
: await evaluatePredicate(session, predicate, since),
|
|
393
|
+
timeout,
|
|
394
|
+
predicateStarted,
|
|
395
|
+
reevaluate: (next, budget) => waitForPredicate(next, predicate, budget, 0),
|
|
396
|
+
});
|
|
397
|
+
// The successor's buffer is its own: the departed session's cursor means nothing there.
|
|
398
|
+
if (followed.followed)
|
|
399
|
+
since = 0;
|
|
400
|
+
session = followed.session;
|
|
401
|
+
const verdict = followed.verdict;
|
|
357
402
|
// A GREEN presence-only assertion is the dangerous case (a wrong element can fake it) — nudge
|
|
358
403
|
// toward a consequence. Never on a failing verdict (moot) or when a signal/net is asserted.
|
|
359
404
|
// Two nudges, both only on a GREEN verdict (a failing one is moot): a presence-only assertion
|
|
@@ -396,6 +441,7 @@ export const OBSERVE_TOOLS = [
|
|
|
396
441
|
return withControl(session, {
|
|
397
442
|
...decision,
|
|
398
443
|
...annotateStarvedFailure(session, verdict),
|
|
444
|
+
...(followed.followed ? { sessionId: session.id } : {}),
|
|
399
445
|
...(contradictions.length > 0 ? { contradictions } : {}),
|
|
400
446
|
// What the app did not tell Reticle, on the same rule the act path uses.
|
|
401
447
|
// Remedy once per session, facts every time. Applied HERE rather than inside
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* is not advertised on the default surface, so the agent that needed it could not see it and had no
|
|
12
12
|
* way to learn it existed. A capability reachable only by already knowing about it is not reachable.
|
|
13
13
|
*
|
|
14
|
-
* The SCROLLING half lives in `portal/input/
|
|
14
|
+
* The SCROLLING half lives in `portal/input/scroll-find.ts`, because it drives the page and this
|
|
15
15
|
* directory must not reach into `input/` — doing so completed a mutual pair, and two directories
|
|
16
16
|
* that need each other cannot be read, moved or tested apart. Shaping an answer and scrolling a list
|
|
17
17
|
* are different jobs anyway; the guard made that visible before it was a habit.
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* is not advertised on the default surface, so the agent that needed it could not see it and had no
|
|
12
12
|
* way to learn it existed. A capability reachable only by already knowing about it is not reachable.
|
|
13
13
|
*
|
|
14
|
-
* The SCROLLING half lives in `portal/input/
|
|
14
|
+
* The SCROLLING half lives in `portal/input/scroll-find.ts`, because it drives the page and this
|
|
15
15
|
* directory must not reach into `input/` — doing so completed a mutual pair, and two directories
|
|
16
16
|
* that need each other cannot be read, moved or tested apart. Shaping an answer and scrolling a list
|
|
17
17
|
* are different jobs anyway; the guard made that visible before it was a habit.
|
|
@@ -14,8 +14,7 @@ import { diffLines } from '../../memory/project/baselines.js';
|
|
|
14
14
|
import { selectPath, capDepth, projectComponentState } from '../../portal/session/state-select.js';
|
|
15
15
|
import { costHint } from '../../portal/session/output-budget.js';
|
|
16
16
|
import { buildReactionReport, summarizeReaction } from '@reticlehq/engine/question/reaction.js';
|
|
17
|
-
import { asNumber, asString } from '@reticlehq/core';
|
|
18
|
-
import { parseInteractive } from './tools-helpers.js';
|
|
17
|
+
import { asNumber, asString, parseInteractive } from '@reticlehq/core';
|
|
19
18
|
import { sessionIdShape, commandOrThrow, snapshotTree } from './tool-kit.js';
|
|
20
19
|
import { bufferEnvelope } from '../../portal/session/session-health.js';
|
|
21
20
|
import { routeOfUrl } from '@reticlehq/engine/question/predicate/predicate-route.js';
|