@reticlehq/server 2.10.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 +73 -11
- 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 +41 -0
- package/dist/dev/stale-issue-guard.js +75 -0
- package/dist/domain/domain-tools.js +3 -2
- 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 +30 -0
- package/dist/events/predicate-asks.js +48 -0
- package/dist/events/predicate-eval.js +254 -25
- package/dist/events/predicate-parse.d.ts +0 -6
- package/dist/events/predicate-parse.js +51 -5
- package/dist/events/predicate-schema.d.ts +62 -0
- package/dist/events/predicate-schema.js +162 -4
- 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 +65 -8
- package/dist/flows/flows.d.ts +6 -1
- package/dist/flows/flows.js +34 -22
- package/dist/flows/predicate-to-expect.js +14 -3
- package/dist/flows/server-verify.js +16 -2
- package/dist/flows/verify-change-tools.js +9 -2
- package/dist/honesty/blind-spots.d.ts +8 -16
- package/dist/honesty/blind-spots.js +18 -5
- package/dist/honesty/feature-capture.d.ts +150 -0
- package/dist/honesty/feature-capture.js +233 -0
- package/dist/honesty/gap-ledger.d.ts +52 -0
- package/dist/honesty/gap-ledger.js +63 -0
- package/dist/honesty/instrumentation-gaps.d.ts +79 -0
- package/dist/honesty/instrumentation-gaps.js +77 -0
- package/dist/honesty/observability.d.ts +37 -0
- package/dist/honesty/observability.js +40 -0
- 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 -2
- package/dist/index.js +41 -0
- 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/node-io.js +3 -0
- 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 +2 -10
- package/dist/init/run.js +26 -68
- package/dist/init/slash-command.d.ts +1 -1
- package/dist/init/slash-command.js +33 -10
- 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 +2 -2
- package/dist/journal/journal-recorder.d.ts +23 -0
- package/dist/journal/journal-recorder.js +35 -0
- package/dist/license/license.d.ts +8 -2
- package/dist/license/license.js +10 -15
- 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/artifact-root.d.ts +83 -0
- package/dist/project/artifact-root.js +112 -0
- package/dist/project/fs-port.d.ts +3 -0
- package/dist/project/fs-port.js +3 -2
- package/dist/project/memory-fs.d.ts +9 -0
- package/dist/project/memory-fs.js +93 -0
- package/dist/project/project-store.d.ts +17 -0
- package/dist/project/project-store.js +33 -0
- package/dist/project/project-tools.js +3 -32
- package/dist/project/remember-project.d.ts +24 -0
- package/dist/project/remember-project.js +40 -0
- package/dist/project/reticle-dir.d.ts +2 -0
- package/dist/project/reticle-dir.js +1 -0
- package/dist/project/session-root.d.ts +15 -0
- package/dist/project/session-root.js +31 -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/run-tools.js +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/lease-availability.d.ts +40 -0
- package/dist/session/lease-availability.js +43 -0
- package/dist/session/live-control-tools.js +1 -42
- package/dist/session/no-session-diagnosis.d.ts +20 -0
- package/dist/session/no-session-diagnosis.js +41 -12
- 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 +83 -65
- 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 +40 -1
- package/dist/session/session.js +84 -2
- package/dist/session/stall-clock.d.ts +18 -0
- package/dist/session/stall-clock.js +48 -0
- package/dist/telemetry/automation-hint.d.ts +2 -1
- package/dist/telemetry/daemon-telemetry.js +12 -23
- 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/session-metrics.d.ts +1 -1
- package/dist/telemetry/session-metrics.js +1 -1
- package/dist/telemetry/telemetry.d.ts +12 -3
- package/dist/telemetry/telemetry.js +25 -5
- package/dist/telemetry/verification-of.js +2 -2
- package/dist/tools/act-tools.js +159 -24
- package/dist/tools/assert-source.d.ts +22 -0
- package/dist/tools/assert-source.js +53 -0
- package/dist/tools/assert-verdict.d.ts +21 -2
- package/dist/tools/assert-verdict.js +59 -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/contract-tools.js +5 -3
- package/dist/tools/coverage-tools.d.ts +10 -1
- package/dist/tools/coverage-tools.js +124 -37
- 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 +70 -27
- 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 +28 -1
- 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 +3 -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 +3 -3
- package/docs/enterprise.md +3 -1
- package/docs/gates.md +1 -1
- package/docs/install-manual.mdx +1 -1
- package/docs/license-activation.mdx +145 -0
- 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/telemetry-contract.md +1 -12
- package/docs/telemetry-events.mdx +1 -34
- package/docs/telemetry.md +0 -1
- 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
- package/dist/telemetry/instrumentation-stall.d.ts +0 -57
- package/dist/telemetry/instrumentation-stall.js +0 -103
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import { EventType, JournalVerdictEffectSchema, RUN_ESTABLISHED_CAP, buildRunContext, } from '@reticlehq/core';
|
|
2
|
+
/**
|
|
3
|
+
* The run, derived from the journal that is already there.
|
|
4
|
+
*
|
|
5
|
+
* `.reticle/sessions/<id>/` already holds an append-only causal ledger of every action dispatched
|
|
6
|
+
* and every event observed. The run is a FOLD over that, never a second store — the same argument
|
|
7
|
+
* that deleted `instrumentation_stalled` from this codebase applies without modification: two ways
|
|
8
|
+
* to compute one number is worse than one way, because the day they disagree the disagreement lands
|
|
9
|
+
* inside a verdict and a user finds it before we do. So nothing here writes: it reads the ledger and
|
|
10
|
+
* folds it, and if the fold is wrong the ledger is still the single authority.
|
|
11
|
+
*
|
|
12
|
+
* **One run per session, for now.** Nothing here assumes it: the fold is pure and takes its evidence
|
|
13
|
+
* as arguments, so a later multi-run or multi-agent model is a change of caller, not a rewrite. It
|
|
14
|
+
* is not built today because nobody has hit the case, and a coordination model invented ahead of its
|
|
15
|
+
* first user is a guess.
|
|
16
|
+
*
|
|
17
|
+
* Only what Reticle OBSERVED goes in. A settle outcome is an observation. A route change is an
|
|
18
|
+
* observation. A verdict a verification tool recorded is an observation. What the agent meant to do
|
|
19
|
+
* next is not, and never enters here.
|
|
20
|
+
*/
|
|
21
|
+
/**
|
|
22
|
+
* The one key the route is filed under.
|
|
23
|
+
*
|
|
24
|
+
* The subject of "the app is on /checkout" is the route itself, so a later route change SUPERSEDES
|
|
25
|
+
* rather than appends. Filing it per-path would accumulate every page ever visited, which is the
|
|
26
|
+
* token bomb the cap exists to prevent.
|
|
27
|
+
*/
|
|
28
|
+
const ROUTE_KEY = 'route';
|
|
29
|
+
/** The conclusions this fold is allowed to state, as prose rather than transcript. */
|
|
30
|
+
const Fact = {
|
|
31
|
+
route: (to) => `route is ${to}`,
|
|
32
|
+
settled: (verb) => `${verb} settled`,
|
|
33
|
+
unsettled: (verb) => `${verb} did not settle`,
|
|
34
|
+
};
|
|
35
|
+
/**
|
|
36
|
+
* The SUBJECT a journal action was about: the ref it spent, else the target it resolved from.
|
|
37
|
+
*
|
|
38
|
+
* Exported so the feature-use instrument files a re-fetch under the same key this fold establishes
|
|
39
|
+
* it under. A second copy of this rule there would be a second definition of "the same subject".
|
|
40
|
+
*/
|
|
41
|
+
export function subjectOf(action) {
|
|
42
|
+
const ref = action.args['ref'];
|
|
43
|
+
if ('string' === typeof ref && ref.length > 0)
|
|
44
|
+
return ref;
|
|
45
|
+
const target = action.args['target'];
|
|
46
|
+
if ('string' === typeof target && target.length > 0)
|
|
47
|
+
return target;
|
|
48
|
+
return undefined;
|
|
49
|
+
}
|
|
50
|
+
/** What the action did, in the caller's own vocabulary — 'click', 'fill', else the tool's name. */
|
|
51
|
+
function verbOf(action) {
|
|
52
|
+
const named = action.args['action'];
|
|
53
|
+
return 'string' === typeof named && named.length > 0 ? named : action.tool;
|
|
54
|
+
}
|
|
55
|
+
/** The verdict a verification tool recorded on this action, if it recorded one. */
|
|
56
|
+
function verdictOf(action) {
|
|
57
|
+
const parsed = JournalVerdictEffectSchema.safeParse(action.effect);
|
|
58
|
+
return parsed.success ? parsed.data : undefined;
|
|
59
|
+
}
|
|
60
|
+
/** Which document and edit epoch each action was observed under, read off its attributed events. */
|
|
61
|
+
function scopeByAction(events) {
|
|
62
|
+
const out = new Map();
|
|
63
|
+
for (const event of events) {
|
|
64
|
+
if (event.actionId === undefined)
|
|
65
|
+
continue;
|
|
66
|
+
if (event.documentId === undefined && event.editEpoch === undefined)
|
|
67
|
+
continue;
|
|
68
|
+
const existing = out.get(event.actionId) ?? {};
|
|
69
|
+
out.set(event.actionId, {
|
|
70
|
+
doc: event.documentId ?? existing.doc,
|
|
71
|
+
epoch: event.editEpoch ?? existing.epoch,
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
return out;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Fold the journal into candidate facts, newest last.
|
|
78
|
+
*
|
|
79
|
+
* Only the last `RUN_ESTABLISHED_CAP` actions are read: anything older cannot survive the fold, so
|
|
80
|
+
* parsing it would be work done to throw away. An action with no observed settle outcome states
|
|
81
|
+
* nothing and is skipped — "we drove it and cannot say what happened" is not an established fact,
|
|
82
|
+
* and writing it down as one is how a memory starts lying.
|
|
83
|
+
*/
|
|
84
|
+
export function establishedFromJournal(actions, events) {
|
|
85
|
+
const scope = scopeByAction(events);
|
|
86
|
+
let route;
|
|
87
|
+
for (const event of events) {
|
|
88
|
+
if (EventType.ROUTE_CHANGE !== event.type)
|
|
89
|
+
continue;
|
|
90
|
+
const pathname = event.data['pathname'];
|
|
91
|
+
const to = 'string' === typeof pathname ? pathname : event.data['to'];
|
|
92
|
+
if ('string' !== typeof to)
|
|
93
|
+
continue;
|
|
94
|
+
route = { key: ROUTE_KEY, fact: Fact.route(to), doc: event.documentId, epoch: event.editEpoch };
|
|
95
|
+
}
|
|
96
|
+
const out = [];
|
|
97
|
+
for (const action of actions.slice(-RUN_ESTABLISHED_CAP)) {
|
|
98
|
+
const settled = action.settled;
|
|
99
|
+
if (undefined === settled)
|
|
100
|
+
continue;
|
|
101
|
+
const key = subjectOf(action);
|
|
102
|
+
if (undefined === key)
|
|
103
|
+
continue;
|
|
104
|
+
const verb = verbOf(action);
|
|
105
|
+
out.push({
|
|
106
|
+
key,
|
|
107
|
+
fact: settled ? Fact.settled(verb) : Fact.unsettled(verb),
|
|
108
|
+
// The source pointer the verdict already reported. Read off the ledger rather than looked up
|
|
109
|
+
// again: a second lookup could disagree with the one the agent was shown.
|
|
110
|
+
source: verdictOf(action)?.source,
|
|
111
|
+
...(scope.get(action.actionId) ?? {}),
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
if (route !== undefined)
|
|
115
|
+
out.push(route);
|
|
116
|
+
return out;
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* What a verdict already settled, newest last.
|
|
120
|
+
*
|
|
121
|
+
* Both verdict tools write here. `reticle_act_and_wait` closes its attribution window with one;
|
|
122
|
+
* `reticle_assert` drives nothing, so it appends its record WITHOUT opening a window — same effect
|
|
123
|
+
* shape, so this fold reads one kind of verdict rather than two.
|
|
124
|
+
*/
|
|
125
|
+
export function provenFromJournal(actions, events) {
|
|
126
|
+
const scope = scopeByAction(events);
|
|
127
|
+
const out = [];
|
|
128
|
+
for (const action of actions.slice(-RUN_ESTABLISHED_CAP)) {
|
|
129
|
+
const verdict = verdictOf(action);
|
|
130
|
+
if (undefined === verdict)
|
|
131
|
+
continue;
|
|
132
|
+
out.push({
|
|
133
|
+
claim: verdict.claim,
|
|
134
|
+
verified: verdict.verified,
|
|
135
|
+
source: verdict.source,
|
|
136
|
+
...(scope.get(action.actionId) ?? {}),
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
return out;
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* The context for one run.
|
|
143
|
+
*
|
|
144
|
+
* `step` is the journal's action count rather than a counter of its own, for the reason at the top
|
|
145
|
+
* of this file: a second counter is a second thing that can disagree with the ledger. The document
|
|
146
|
+
* and epoch are the session's own, so the staleness rule here is the same one every other verdict in
|
|
147
|
+
* this codebase is scoped by.
|
|
148
|
+
*/
|
|
149
|
+
export function runContextFor(input) {
|
|
150
|
+
return buildRunContext({
|
|
151
|
+
step: input.actions.length,
|
|
152
|
+
intents: input.intents,
|
|
153
|
+
established: establishedFromJournal(input.actions, input.events),
|
|
154
|
+
proven: provenFromJournal(input.actions, input.events),
|
|
155
|
+
currentDocumentId: input.currentDocumentId,
|
|
156
|
+
currentEditEpoch: input.currentEditEpoch,
|
|
157
|
+
});
|
|
158
|
+
}
|
package/dist/runs/run-diff.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { ReticleVerificationRun } from '@reticlehq/core';
|
|
2
2
|
/** A per-flow change between two runs. Only flows with a meaningful change are reported. */
|
|
3
|
-
|
|
3
|
+
interface FlowDelta {
|
|
4
4
|
name: string;
|
|
5
5
|
beforeMs: number;
|
|
6
6
|
afterMs: number;
|
|
@@ -29,3 +29,4 @@ export interface RunDiff {
|
|
|
29
29
|
* console diffs land once runs persist per-step detail.
|
|
30
30
|
*/
|
|
31
31
|
export declare function diffRuns(before: ReticleVerificationRun, after: ReticleVerificationRun, noiseFloorPct?: number): RunDiff;
|
|
32
|
+
export {};
|
package/dist/runs/run-tools.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
+
import { sessionRoot } from '../project/session-root.js';
|
|
2
3
|
import { RunReadError } from '@reticlehq/core';
|
|
3
4
|
import { ReticleTool } from '../tools/tool-names.js';
|
|
4
5
|
import { asString } from '../tools/tools-helpers.js';
|
|
@@ -36,7 +37,7 @@ export const RUN_TOOLS = [
|
|
|
36
37
|
error: z.string().optional(),
|
|
37
38
|
},
|
|
38
39
|
handler: async (deps, args) => {
|
|
39
|
-
const store = new RunStore(deps.fs, deps
|
|
40
|
+
const store = new RunStore(deps.fs, sessionRoot(deps, asString(args['sessionId'])));
|
|
40
41
|
// format:"diff" is a whole-history operation (two most-recent runs), not a single-run read.
|
|
41
42
|
if ('diff' === asString(args['format'])) {
|
|
42
43
|
const pair = await store.latestTwo();
|
|
@@ -14,7 +14,7 @@ import { buildVerificationRun } from './build-verification-run.js';
|
|
|
14
14
|
import { mapReplayToFlowResult } from './replay-mapping.js';
|
|
15
15
|
import { defaultRunId } from './runner-port.js';
|
|
16
16
|
import { RunStore } from './run-store.js';
|
|
17
|
-
import { syncRunToCloud, SyncOutcome } from '../cloud/cloud-sync.js';
|
|
17
|
+
import { cloudFetch, syncRunToCloud, SyncOutcome } from '../cloud/cloud-sync.js';
|
|
18
18
|
import { resolveProjectCloud } from '../cloud/cloud-config.js';
|
|
19
19
|
import { log } from '../log.js';
|
|
20
20
|
/** Assemble the ReticleVerificationRun artifact for a verify suite (pure — no IO). */
|
|
@@ -58,7 +58,7 @@ export async function persistAndSyncVerificationRun(deps, timed, projectId) {
|
|
|
58
58
|
const cloud = await resolveProjectCloud(deps.fs, deps.reticleRoot, homedir(), process.env);
|
|
59
59
|
if (null === cloud.config || !cloud.policy.runs)
|
|
60
60
|
return run.runId; // not attached / runs disabled → local only
|
|
61
|
-
const result = await syncRunToCloud(run, cloud.config,
|
|
61
|
+
const result = await syncRunToCloud(run, cloud.config, cloudFetch);
|
|
62
62
|
if (result.outcome !== SyncOutcome.SYNCED) {
|
|
63
63
|
log('cloud-run-sync-failed', { runId: run.runId, status: result.status, error: result.error });
|
|
64
64
|
}
|
|
@@ -69,13 +69,5 @@ export declare function probeDevServerStates(ports?: readonly number[], probe?:
|
|
|
69
69
|
serving: number[];
|
|
70
70
|
slow: number[];
|
|
71
71
|
}>;
|
|
72
|
-
/**
|
|
73
|
-
* The strongest state either loopback family reports.
|
|
74
|
-
*
|
|
75
|
-
* Serving a document beats a bare connection beats nothing, so a server bound to only one family is
|
|
76
|
-
* still seen at its best — the same "either, not both" rule `anyFamilyServes` follows, extended to
|
|
77
|
-
* three states.
|
|
78
|
-
*/
|
|
79
|
-
export declare function anyFamilyState(port: number, probe?: (port: number, host: string) => Promise<PortState>): Promise<PortState>;
|
|
80
72
|
/** Every candidate port serving a document, in ascending order. Never rejects. */
|
|
81
73
|
export declare function probeDevServers(ports?: readonly number[], probe?: (port: number) => Promise<boolean>): Promise<number[]>;
|
|
@@ -132,7 +132,7 @@ export async function probeDevServerStates(ports = [...DEV_SERVER_PORTS], probe
|
|
|
132
132
|
* still seen at its best — the same "either, not both" rule `anyFamilyServes` follows, extended to
|
|
133
133
|
* three states.
|
|
134
134
|
*/
|
|
135
|
-
|
|
135
|
+
async function anyFamilyState(port, probe = probeState) {
|
|
136
136
|
const answers = await Promise.all(PROBE_HOSTS.map((host) => probe(port, host).catch(() => PortState.CLOSED)));
|
|
137
137
|
if (answers.includes(PortState.SERVES_DOCUMENT))
|
|
138
138
|
return PortState.SERVES_DOCUMENT;
|
|
@@ -16,6 +16,12 @@
|
|
|
16
16
|
*/
|
|
17
17
|
interface ActEffect {
|
|
18
18
|
action?: string | undefined;
|
|
19
|
+
/**
|
|
20
|
+
* The ref the action dispatched against. Recorded so the verdict nudge can suggest a call about
|
|
21
|
+
* the element the agent actually touched, rather than a worked example from a document. Written
|
|
22
|
+
* on the SAME success path as `action`, so a refused act leaves neither.
|
|
23
|
+
*/
|
|
24
|
+
ref?: string | undefined;
|
|
19
25
|
/** DOM mutations inside the acted element's own subtree. Undefined means nobody measured. */
|
|
20
26
|
mutatedWithin?: number | undefined;
|
|
21
27
|
}
|
|
@@ -35,7 +41,7 @@ export declare class LastAct {
|
|
|
35
41
|
* Marking only on the success path is what makes that unrepeatable: a failure mode added later
|
|
36
42
|
* cannot forget to clean up state that was never written.
|
|
37
43
|
*/
|
|
38
|
-
markActed(cursor: number, action: string | undefined, mutatedWithin: number | undefined): void;
|
|
44
|
+
markActed(cursor: number, action: string | undefined, mutatedWithin: number | undefined, ref?: string): void;
|
|
39
45
|
/**
|
|
40
46
|
* A navigation — by URL or by reload — moves the floor without being an act.
|
|
41
47
|
*
|
package/dist/session/last-act.js
CHANGED
|
@@ -18,9 +18,9 @@ export class LastAct {
|
|
|
18
18
|
* Marking only on the success path is what makes that unrepeatable: a failure mode added later
|
|
19
19
|
* cannot forget to clean up state that was never written.
|
|
20
20
|
*/
|
|
21
|
-
markActed(cursor, action, mutatedWithin) {
|
|
21
|
+
markActed(cursor, action, mutatedWithin, ref) {
|
|
22
22
|
this.#cursor = cursor;
|
|
23
|
-
this.#effect = { action, mutatedWithin };
|
|
23
|
+
this.#effect = { action, mutatedWithin, ...(ref === undefined ? {} : { ref }) };
|
|
24
24
|
}
|
|
25
25
|
/**
|
|
26
26
|
* A navigation — by URL or by reload — moves the floor without being an act.
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Whether the self-serve escape hatch can actually be taken, said in the same breath as the offer.
|
|
3
|
+
*
|
|
4
|
+
* `reticle_lease` is what `status`, `doctor` and the no-session diagnosis all recommend when no tab
|
|
5
|
+
* is connected: it opens a browser Reticle drives itself, so the agent does not have to wait for a
|
|
6
|
+
* human. It is the right advice and it is the one thing that cannot work when Chromium is missing or
|
|
7
|
+
* sitting at a revision this Playwright will not use — and the command giving the advice is usually
|
|
8
|
+
* the command that just diagnosed exactly that.
|
|
9
|
+
*
|
|
10
|
+
* Reported from the field, verbatim in shape: `doctor` printed `chromium ✗ wrong revision — the
|
|
11
|
+
* bundled playwright wants chromium-1234; the cache holds chromium-1228`, and then offered the lease
|
|
12
|
+
* as the way out. The reporter followed it, and could not have succeeded.
|
|
13
|
+
*
|
|
14
|
+
* Two things this refuses to do:
|
|
15
|
+
*
|
|
16
|
+
* **It does not fire on an absent probe.** A probe that could not run is not evidence the browser is
|
|
17
|
+
* missing, and a caveat asserting otherwise would be the same overclaiming this file exists to stop.
|
|
18
|
+
*
|
|
19
|
+
* **It does not tell the agent to install the browser.** A ~150MB download on somebody else's
|
|
20
|
+
* machine is not an agent's call to make unprompted, so the message names the command and hands it
|
|
21
|
+
* to the human rather than implying the agent should just run it.
|
|
22
|
+
*/
|
|
23
|
+
/** What the caller learned about the browser. Gathered by the caller so this stays pure. */
|
|
24
|
+
export interface LeaseBrowserState {
|
|
25
|
+
/** Does the executable this Playwright wants actually exist on disk? */
|
|
26
|
+
exists: boolean;
|
|
27
|
+
/** The install command, already pinned to the bundled Playwright by `chromium-hint`. */
|
|
28
|
+
installCommand?: string | undefined;
|
|
29
|
+
/** The revision this Playwright wants, when the path names one. */
|
|
30
|
+
wantedRevision?: string | undefined;
|
|
31
|
+
/** Chromium revisions actually present, wanted or not. */
|
|
32
|
+
installedRevisions?: readonly string[] | undefined;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* The sentence to append to a lease recommendation, or undefined when the lease is fine.
|
|
36
|
+
*
|
|
37
|
+
* Undefined rather than an empty string on the healthy path: a caller composing a message should not
|
|
38
|
+
* have to filter blanks, and a healthy install must pay nothing at all for this check.
|
|
39
|
+
*/
|
|
40
|
+
export declare function leaseCaveat(state: LeaseBrowserState | undefined): string | undefined;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Whether the self-serve escape hatch can actually be taken, said in the same breath as the offer.
|
|
3
|
+
*
|
|
4
|
+
* `reticle_lease` is what `status`, `doctor` and the no-session diagnosis all recommend when no tab
|
|
5
|
+
* is connected: it opens a browser Reticle drives itself, so the agent does not have to wait for a
|
|
6
|
+
* human. It is the right advice and it is the one thing that cannot work when Chromium is missing or
|
|
7
|
+
* sitting at a revision this Playwright will not use — and the command giving the advice is usually
|
|
8
|
+
* the command that just diagnosed exactly that.
|
|
9
|
+
*
|
|
10
|
+
* Reported from the field, verbatim in shape: `doctor` printed `chromium ✗ wrong revision — the
|
|
11
|
+
* bundled playwright wants chromium-1234; the cache holds chromium-1228`, and then offered the lease
|
|
12
|
+
* as the way out. The reporter followed it, and could not have succeeded.
|
|
13
|
+
*
|
|
14
|
+
* Two things this refuses to do:
|
|
15
|
+
*
|
|
16
|
+
* **It does not fire on an absent probe.** A probe that could not run is not evidence the browser is
|
|
17
|
+
* missing, and a caveat asserting otherwise would be the same overclaiming this file exists to stop.
|
|
18
|
+
*
|
|
19
|
+
* **It does not tell the agent to install the browser.** A ~150MB download on somebody else's
|
|
20
|
+
* machine is not an agent's call to make unprompted, so the message names the command and hands it
|
|
21
|
+
* to the human rather than implying the agent should just run it.
|
|
22
|
+
*/
|
|
23
|
+
/**
|
|
24
|
+
* The sentence to append to a lease recommendation, or undefined when the lease is fine.
|
|
25
|
+
*
|
|
26
|
+
* Undefined rather than an empty string on the healthy path: a caller composing a message should not
|
|
27
|
+
* have to filter blanks, and a healthy install must pay nothing at all for this check.
|
|
28
|
+
*/
|
|
29
|
+
export function leaseCaveat(state) {
|
|
30
|
+
if (state === undefined || state.exists)
|
|
31
|
+
return undefined;
|
|
32
|
+
// Present-at-the-wrong-revision and absent-entirely have different fixes and different readings,
|
|
33
|
+
// and collapsing them is what made the reported loop unbreakable — "the check is broken" and
|
|
34
|
+
// "none of these count" look identical from a flat `missing`.
|
|
35
|
+
const installed = state.installedRevisions ?? [];
|
|
36
|
+
const mismatch = state.wantedRevision !== undefined && installed.length > 0
|
|
37
|
+
? ` This Playwright wants ${state.wantedRevision} and the cache holds ${installed.join(', ')}, so the builds already on this machine will not be used.`
|
|
38
|
+
: '';
|
|
39
|
+
const command = state.installCommand === undefined ? '' : ` The fix is: ${state.installCommand}.`;
|
|
40
|
+
return ('NOTE — reticle_lease cannot run here: it drives a Reticle-owned Chromium and that browser is ' +
|
|
41
|
+
`not available.${mismatch}${command} That download is the human's call, not something to run ` +
|
|
42
|
+
'unprompted on their machine, so hand this back rather than treating the lease as a way forward.');
|
|
43
|
+
}
|
|
@@ -3,12 +3,7 @@ import { noteEmptyRead } from '../tools/observed-nothing.js';
|
|
|
3
3
|
import { AGENT_ASK_NOTICE, AGENT_WAITING_NOTICE, PresenterTone, SessionState, YIELD_WITHOUT_SESSION_NOTE, } from '@reticlehq/core';
|
|
4
4
|
import { ReticleTool } from '../tools/tool-names.js';
|
|
5
5
|
import { sessionIdShape } from '../tools/tool-kit.js';
|
|
6
|
-
import {
|
|
7
|
-
import { waitForReady, RETICLE_LOOP_GUIDE } from './session-readiness.js';
|
|
8
|
-
import { recoveryFor } from '../tools/error-recovery.js';
|
|
9
|
-
/** Default + ceiling for the readiness wait — keep it short so a truly-missing app fails fast. */
|
|
10
|
-
const WAIT_READY_DEFAULT_MS = 5000;
|
|
11
|
-
const WAIT_READY_MAX_MS = 30000;
|
|
6
|
+
import { asString } from '../tools/tools-helpers.js';
|
|
12
7
|
/**
|
|
13
8
|
* Is this a turn ending with nothing attached, rather than a call about a specific tab?
|
|
14
9
|
*
|
|
@@ -225,42 +220,6 @@ export const LIVE_CONTROL_TOOLS = [
|
|
|
225
220
|
return Promise.resolve(out);
|
|
226
221
|
},
|
|
227
222
|
},
|
|
228
|
-
{
|
|
229
|
-
name: ReticleTool.WAIT_READY,
|
|
230
|
-
description: 'Block until the app is connected, then continue — call this once right after init so your ' +
|
|
231
|
-
'first real tool call does not lose the race with the SDK connecting its WebSocket. Returns as ' +
|
|
232
|
-
'soon as a session exists (no latency if one already does), or after `timeoutMs` with a ' +
|
|
233
|
-
'recovery hint if none appears.',
|
|
234
|
-
inputSchema: {
|
|
235
|
-
timeoutMs: z
|
|
236
|
-
.number()
|
|
237
|
-
.optional()
|
|
238
|
-
.describe('How long to wait for a session (ms). Default 5000, max 30000.'),
|
|
239
|
-
},
|
|
240
|
-
outputSchema: {
|
|
241
|
-
ready: z.boolean(),
|
|
242
|
-
sessionCount: z.number(),
|
|
243
|
-
loop: z.string().optional(),
|
|
244
|
-
recovery: z.string().optional(),
|
|
245
|
-
},
|
|
246
|
-
handler: async (deps, args) => {
|
|
247
|
-
const requested = asNumber(args['timeoutMs']) ?? WAIT_READY_DEFAULT_MS;
|
|
248
|
-
const timeoutMs = Math.max(0, Math.min(requested, WAIT_READY_MAX_MS));
|
|
249
|
-
const ready = await waitForReady({
|
|
250
|
-
count: () => deps.sessions.count(),
|
|
251
|
-
timeoutMs,
|
|
252
|
-
now: deps.now,
|
|
253
|
-
sleep: (ms) => new Promise((res) => setTimeout(res, ms)),
|
|
254
|
-
});
|
|
255
|
-
// The first response a fresh agent gets — carry the loop guide so it learns how to drive Reticle.
|
|
256
|
-
if (ready)
|
|
257
|
-
return { ready: true, sessionCount: deps.sessions.count(), loop: RETICLE_LOOP_GUIDE };
|
|
258
|
-
const recovery = recoveryFor('no browser session connected');
|
|
259
|
-
return recovery !== undefined
|
|
260
|
-
? { ready: false, sessionCount: 0, recovery }
|
|
261
|
-
: { ready: false, sessionCount: 0 };
|
|
262
|
-
},
|
|
263
|
-
},
|
|
264
223
|
];
|
|
265
224
|
/**
|
|
266
225
|
* The single actionable next-move for a pending mark: point the agent at the source (or the element
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
* Pure: everything it needs is passed in, so the probe that discovers listening ports stays out of
|
|
19
19
|
* the hot resolve() path and this stays unit-testable.
|
|
20
20
|
*/
|
|
21
|
+
import { type LeaseBrowserState } from './lease-availability.js';
|
|
21
22
|
export interface NoSessionFacts {
|
|
22
23
|
/** Whether ANY session has connected to this daemon since it booted. */
|
|
23
24
|
everConnected: boolean;
|
|
@@ -36,6 +37,15 @@ export interface NoSessionFacts {
|
|
|
36
37
|
slowListeners?: readonly number[];
|
|
37
38
|
/** The port this daemon is on — half of the mismatch the old message asked about. */
|
|
38
39
|
port: number;
|
|
40
|
+
/**
|
|
41
|
+
* What the caller knows about the Chromium a lease would drive.
|
|
42
|
+
*
|
|
43
|
+
* Every branch below offers `reticle_lease` as the way not to wait for a human. When that browser
|
|
44
|
+
* is missing or at a revision this Playwright will not use, the offer is the one thing that cannot
|
|
45
|
+
* help — and it was being made by the same output that had just diagnosed exactly that. Optional:
|
|
46
|
+
* absent means the caller did not probe, which is not evidence the browser is missing.
|
|
47
|
+
*/
|
|
48
|
+
leaseBrowser?: LeaseBrowserState | undefined;
|
|
39
49
|
/**
|
|
40
50
|
* The directory `initialized` was decided in, named in the message when it is known.
|
|
41
51
|
*
|
|
@@ -107,6 +117,16 @@ export interface NoSessionFacts {
|
|
|
107
117
|
* install never finished" to the user-facing diagnosis rather than only to telemetry.
|
|
108
118
|
*/
|
|
109
119
|
daemonUpMs?: number;
|
|
120
|
+
/**
|
|
121
|
+
* Well-known Reticle ports other than `port` that currently accept a connection.
|
|
122
|
+
*
|
|
123
|
+
* Remaining half of #261. We cannot see an SDK dialling a port we are not on, but we can see that
|
|
124
|
+
* a well-known Reticle port has a listener. That is an observation: somebody else's daemon, another
|
|
125
|
+
* project, and an unrelated process all look the same from here. The sentence that reports it
|
|
126
|
+
* names the ports and refuses the conclusion. Absent or empty means nothing else was listening,
|
|
127
|
+
* which is the common case and must not gain a paragraph.
|
|
128
|
+
*/
|
|
129
|
+
siblingListeners?: readonly number[];
|
|
110
130
|
}
|
|
111
131
|
/**
|
|
112
132
|
* Every branch ends with this, and `recoveryFor` keys on it to suppress the generic no-session
|
|
@@ -18,8 +18,10 @@
|
|
|
18
18
|
* Pure: everything it needs is passed in, so the probe that discovers listening ports stays out of
|
|
19
19
|
* the hot resolve() path and this stays unit-testable.
|
|
20
20
|
*/
|
|
21
|
+
import { leaseCaveat } from './lease-availability.js';
|
|
21
22
|
import { DEV_SERVER_PORTS } from '../cli/cli-port.js';
|
|
22
|
-
import {
|
|
23
|
+
import { siblingListenerNote } from '../cli/sibling-ports.js';
|
|
24
|
+
import { STALL_AFTER_MS } from './stall-clock.js';
|
|
23
25
|
/** The one framework whose most likely cause differs from every other framework's. */
|
|
24
26
|
const NUXT = 'nuxt';
|
|
25
27
|
/**
|
|
@@ -58,6 +60,17 @@ const RETRY = SELF_RECOVERING_MARKER;
|
|
|
58
60
|
const URL_THEN_LEASE = 'Once you have that URL, open it with reticle_lease {action:"acquire", url} rather than a shell ' +
|
|
59
61
|
'command — it needs no CLI on PATH (reach it with reticle_run {tool:"reticle_lease"} if it is ' +
|
|
60
62
|
'not advertised directly).';
|
|
63
|
+
/**
|
|
64
|
+
* The lease advice, with the reason it cannot be taken when that is the case.
|
|
65
|
+
*
|
|
66
|
+
* A function rather than a constant so the caveat cannot be forgotten at one of the six sites that
|
|
67
|
+
* offer the lease — which is exactly how the offer came to be made by the command that had already
|
|
68
|
+
* diagnosed the problem.
|
|
69
|
+
*/
|
|
70
|
+
function leaseAdvice(base, facts) {
|
|
71
|
+
const caveat = leaseCaveat(facts.leaseBrowser);
|
|
72
|
+
return caveat === undefined ? base : `${base} ${caveat}`;
|
|
73
|
+
}
|
|
61
74
|
const SELF_SERVE = 'You do not have to wait for the human: reticle_lease {action:"acquire", url} opens a browser ' +
|
|
62
75
|
'Reticle drives itself, and returns a sessionId you can use immediately (reach it with ' +
|
|
63
76
|
'reticle_run {tool:"reticle_lease"} if it is not advertised directly; release it when you finish).';
|
|
@@ -205,6 +218,24 @@ function portMismatchClause(facts) {
|
|
|
205
218
|
` Either restart the daemon on ${String(configured)}, or update \`.reticle.json\` to` +
|
|
206
219
|
` ${String(facts.port)} and restart the dev server so the app picks it up.`);
|
|
207
220
|
}
|
|
221
|
+
/**
|
|
222
|
+
* A listener on a well-known Reticle port we did not bind. Observation, not a cause.
|
|
223
|
+
*
|
|
224
|
+
* Leading space so the caller can concatenate it unconditionally. Empty when there is nothing to
|
|
225
|
+
* say, including when the occupied sibling is already the configured-port mismatch — that sentence
|
|
226
|
+
* already named both numbers, and repeating 4460 as a second "cause" would overclaim.
|
|
227
|
+
*/
|
|
228
|
+
function siblingListenerClause(facts) {
|
|
229
|
+
const occupied = facts.siblingListeners ?? [];
|
|
230
|
+
if (0 === occupied.length)
|
|
231
|
+
return '';
|
|
232
|
+
const mismatch = facts.projectPort;
|
|
233
|
+
const extra = mismatch !== undefined && mismatch !== facts.port
|
|
234
|
+
? occupied.filter((port) => port !== mismatch)
|
|
235
|
+
: occupied;
|
|
236
|
+
const note = siblingListenerNote(facts.port, extra);
|
|
237
|
+
return note === undefined ? '' : ` ${note}`;
|
|
238
|
+
}
|
|
208
239
|
/**
|
|
209
240
|
* The tail every "the app is wired and silent" branch ends with, ranked.
|
|
210
241
|
*
|
|
@@ -216,7 +247,7 @@ function rankedCauses(facts) {
|
|
|
216
247
|
const nuxt = facts.framework?.toLowerCase() === NUXT ? `${NUXT_STALE_BUNDLE} ` : '';
|
|
217
248
|
return (`${nuxt}${REAL_CAUSES} If none of those, the app may be dialling a different daemon than this ` +
|
|
218
249
|
`one (on ${String(facts.port)}): check the app's reticle port matches ` +
|
|
219
|
-
`${String(facts.port)}.${portMismatchClause(facts)} ${NON_LOCALHOST_GATE}`);
|
|
250
|
+
`${String(facts.port)}.${portMismatchClause(facts)}${siblingListenerClause(facts)} ${NON_LOCALHOST_GATE}`);
|
|
220
251
|
}
|
|
221
252
|
/**
|
|
222
253
|
* The scope answer, when the config was somewhere else all along.
|
|
@@ -292,7 +323,7 @@ export function diagnoseNoSession(facts) {
|
|
|
292
323
|
}
|
|
293
324
|
return ('no browser session connected, but one WAS connected to this daemon earlier, so the wiring ' +
|
|
294
325
|
'is correct. The tab was closed, navigated away, or hard-reloaded. Ask the human to reopen ' +
|
|
295
|
-
`the app (or run ${OPEN_CMD_BARE}), or reload the tab. ${SELF_SERVE} ${RETRY}`);
|
|
326
|
+
`the app (or run ${OPEN_CMD_BARE}), or reload the tab. ${leaseAdvice(SELF_SERVE, facts)} ${RETRY}`);
|
|
296
327
|
}
|
|
297
328
|
// Ranked ahead of every "we have never seen one" branch below, because it is the one fact that
|
|
298
329
|
// OUTRANKS them: those branches all reason from an absence, and this reasons from a recorded
|
|
@@ -303,7 +334,7 @@ export function diagnoseNoSession(facts) {
|
|
|
303
334
|
? `${unattributedListeners(listening)} If the dev server is not running, start it first ` +
|
|
304
335
|
'(the command is in `next_action`).'
|
|
305
336
|
: unattributedListeners(listening);
|
|
306
|
-
return `${RESTARTED_LEAD} ${OPEN_THE_APP} ${listeners} ${rankedCauses(facts)} ${SELF_SERVE} ${RETRY}`;
|
|
337
|
+
return `${RESTARTED_LEAD} ${OPEN_THE_APP} ${listeners} ${rankedCauses(facts)} ${leaseAdvice(SELF_SERVE, facts)} ${RETRY}`;
|
|
307
338
|
}
|
|
308
339
|
// A config found elsewhere outranks every "you may have no SDK" branch below: those reason from
|
|
309
340
|
// an absence in ONE directory, and this is a file we read in another. Ranked here so the reader is
|
|
@@ -311,7 +342,7 @@ export function diagnoseNoSession(facts) {
|
|
|
311
342
|
if (!initialized && (facts.configsElsewhere ?? []).length > 0) {
|
|
312
343
|
return ('no browser session connected, and this daemon has never seen one. ' +
|
|
313
344
|
`${configsElsewhereClause(facts)} ${unattributedListeners(listening)} ${OPEN_THE_APP} ` +
|
|
314
|
-
`${rankedCauses(facts)} ${SELF_SERVE} ${RETRY}`);
|
|
345
|
+
`${rankedCauses(facts)} ${leaseAdvice(SELF_SERVE, facts)} ${RETRY}`);
|
|
315
346
|
}
|
|
316
347
|
if (0 === listening.length) {
|
|
317
348
|
// Lead with the stronger EVIDENCE, and do not overstate what it is.
|
|
@@ -341,7 +372,7 @@ export function diagnoseNoSession(facts) {
|
|
|
341
372
|
`file ${INIT_CMD} writes, so the app may carry no Reticle SDK — but check the app's ` +
|
|
342
373
|
'OWN directory before re-running `init`: in a monorepo the daemon often runs at the root ' +
|
|
343
374
|
'while the app lives in a subdirectory, and an app wired by the Vite or Babel plugin ' +
|
|
344
|
-
`carries the SDK without that file at all.${searchedClause(facts)} ${URL_THEN_LEASE} ${RETRY}`);
|
|
375
|
+
`carries the SDK without that file at all.${searchedClause(facts)}${siblingListenerClause(facts)} ${leaseAdvice(URL_THEN_LEASE, facts)} ${RETRY}`);
|
|
345
376
|
}
|
|
346
377
|
return (`${stallClause(facts)}no browser session connected, and this daemon has never seen one. ${OPEN_THE_APP} ` +
|
|
347
378
|
'Nothing is listening on the ports Reticle scans ' +
|
|
@@ -359,10 +390,8 @@ export function diagnoseNoSession(facts) {
|
|
|
359
390
|
// Deliberately NOT offering reticle_lease here, and a test pins that: a lease opens a URL, and
|
|
360
391
|
// if nothing is listening there is nothing at any URL to open. Asking for the real one is the
|
|
361
392
|
// only move that can recover the :7699 case.
|
|
362
|
-
`the app IS running, ask the human for its URL rather than assuming it is down. ${URL_THEN_LEASE}
|
|
363
|
-
|
|
364
|
-
// answered above, leading with that certainty instead of behind this scan's guess.
|
|
365
|
-
`${RETRY}`);
|
|
393
|
+
`the app IS running, ask the human for its URL rather than assuming it is down. ${leaseAdvice(URL_THEN_LEASE, facts)}` +
|
|
394
|
+
`${siblingListenerClause(facts)} ${RETRY}`);
|
|
366
395
|
}
|
|
367
396
|
if (!initialized) {
|
|
368
397
|
return ('no browser session connected, and this daemon has never seen one. ' +
|
|
@@ -372,10 +401,10 @@ export function diagnoseNoSession(facts) {
|
|
|
372
401
|
"a subdirectory, or in an app wired by the Vite or Babel plugin. Check the app's OWN " +
|
|
373
402
|
`directory: if it has no config, run ${INIT_CMD} there and restart the dev server; if it ` +
|
|
374
403
|
`has one, the app is wired and simply has no page open — ${OPEN_CMD}. ` +
|
|
375
|
-
`${unattributedListeners(listening)}${searchedClause(facts)} ${RETRY}`);
|
|
404
|
+
`${unattributedListeners(listening)}${searchedClause(facts)}${siblingListenerClause(facts)} ${RETRY}`);
|
|
376
405
|
}
|
|
377
406
|
return (`${stallClause(facts)}no browser session connected, and this daemon has never seen one for this project, which is ` +
|
|
378
407
|
`wired for Reticle. ${OPEN_THE_APP} ${unattributedListeners(listening)} ` +
|
|
379
408
|
'If the page IS open and still does not appear, the app is wired and the SDK is not reaching ' +
|
|
380
|
-
`this daemon (on ${String(port)}). ${rankedCauses(facts)} ${SELF_SERVE} ${RETRY}`);
|
|
409
|
+
`this daemon (on ${String(port)}). ${rankedCauses(facts)} ${leaseAdvice(SELF_SERVE, facts)} ${RETRY}`);
|
|
381
410
|
}
|
|
@@ -29,6 +29,11 @@ interface NextActionFacts {
|
|
|
29
29
|
initialized: boolean;
|
|
30
30
|
listening: readonly number[];
|
|
31
31
|
dev: DevCommand | undefined;
|
|
32
|
+
/** Configs found in other workspace directories: positive evidence of a scope mismatch. */
|
|
33
|
+
configsElsewhere?: readonly {
|
|
34
|
+
directory: string;
|
|
35
|
+
projectId?: string;
|
|
36
|
+
}[];
|
|
32
37
|
/**
|
|
33
38
|
* An app for this project has connected on this port before, from durable state.
|
|
34
39
|
*
|
|
@@ -26,6 +26,25 @@ export function nextActionFor(facts) {
|
|
|
26
26
|
'reticle_lease {action:"acquire", url}.',
|
|
27
27
|
};
|
|
28
28
|
}
|
|
29
|
+
const configsElsewhere = facts.configsElsewhere ?? [];
|
|
30
|
+
if (!facts.initialized && configsElsewhere.length > 0) {
|
|
31
|
+
const named = configsElsewhere
|
|
32
|
+
.map((config) => config.projectId === undefined
|
|
33
|
+
? config.directory
|
|
34
|
+
: `${config.directory} ('${config.projectId}')`)
|
|
35
|
+
.join(', ');
|
|
36
|
+
const projectIds = configsElsewhere.flatMap((config) => config.projectId === undefined ? [] : [config.projectId]);
|
|
37
|
+
const leaseAlternative = 0 === projectIds.length
|
|
38
|
+
? ''
|
|
39
|
+
: ' Alternatively, acquire the app URL explicitly with reticle_lease ' +
|
|
40
|
+
'{action:"acquire", url, projectId} using the matching projectId above.';
|
|
41
|
+
return {
|
|
42
|
+
action: NoSessionAction.OPEN_APP,
|
|
43
|
+
reason: `a \`.reticle.json\` was found outside this daemon's directory: ${named}. This is a ` +
|
|
44
|
+
"scope problem, not an install problem. Restart the daemon from the app's directory (or " +
|
|
45
|
+
`point it there), then open the app.${leaseAlternative}`,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
29
48
|
if (0 === facts.listening.length) {
|
|
30
49
|
const dev = facts.dev;
|
|
31
50
|
if (dev === undefined) {
|
|
@@ -41,9 +60,22 @@ export function nextActionFor(facts) {
|
|
|
41
60
|
action: NoSessionAction.START_DEV_SERVER,
|
|
42
61
|
command: dev.command,
|
|
43
62
|
...(dev.port === undefined ? {} : { port: dev.port }),
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
63
|
+
// "Probably", to match the branch above it and the paragraph beside it.
|
|
64
|
+
//
|
|
65
|
+
// The same fact was stated at two confidence levels by one function: no dev script said the
|
|
66
|
+
// app was "probably not running", and THIS branch — the one that hands over a command to run
|
|
67
|
+
// — asserted it flatly. That is the branch where being wrong costs something, because the
|
|
68
|
+
// agent then starts a SECOND dev server on a second port, which the guard below calls the
|
|
69
|
+
// exact confusion this probe exists to prevent.
|
|
70
|
+
//
|
|
71
|
+
// It also contradicted its own payload: the prose beside it says the scan is narrow, that a
|
|
72
|
+
// server on any other port is invisible to it, and that a running app should be checked for
|
|
73
|
+
// rather than assumed away. Measured on a machine with three dev servers up on ports the scan
|
|
74
|
+
// does not cover, it reported the app was not running.
|
|
75
|
+
reason: 'nothing is listening on the ports Reticle scans, so the app is probably not running — ' +
|
|
76
|
+
'though that scan is narrow, so if it IS up on another port, ask for its URL instead of ' +
|
|
77
|
+
`starting a second one. This is the project's own \`${dev.script}\` script — run it in the ` +
|
|
78
|
+
'background, tell the human it is running, then call reticle_sessions again.',
|
|
47
79
|
};
|
|
48
80
|
}
|
|
49
81
|
// Something IS up. Whatever else is true, do NOT hand back a start command: a second dev server on
|