@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
package/dist/session/session.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { recordImpact } from '../impact/impact-recorder.js';
|
|
2
2
|
import { LastAct } from './last-act.js';
|
|
3
|
+
import { GapLedger } from '../honesty/gap-ledger.js';
|
|
4
|
+
import { CaptureLedger } from '../honesty/feature-capture.js';
|
|
3
5
|
import { commandTimeoutMessage } from './command-timeout.js';
|
|
4
6
|
import { readHealthEvent } from './session-health.js';
|
|
5
7
|
/** The HUD does not need 200 frames to watch a counter climb; the last state always lands. */
|
|
@@ -106,6 +108,30 @@ export class Session {
|
|
|
106
108
|
#journalReader;
|
|
107
109
|
/** What this session has learned by watching its own stream: ambient churn + blind-spot levels. */
|
|
108
110
|
#observed = new ObservedState();
|
|
111
|
+
/**
|
|
112
|
+
* Which document is on screen right now — the one the most recent stamped event was observed under.
|
|
113
|
+
*
|
|
114
|
+
* DERIVED, never announced. The SDK mints a document id once per real document and stamps it on
|
|
115
|
+
* every event, so the stream already carries the answer; asking the page for it separately would be
|
|
116
|
+
* a second source of truth that can disagree with the evidence it is supposed to scope. A full
|
|
117
|
+
* navigation or a reload builds a new document, mints a new id, and the first event carrying it
|
|
118
|
+
* moves this forward — which is exactly the moment the previous document's evidence stops being
|
|
119
|
+
* about the world. An SPA route change keeps the same document and so keeps the same id, which is
|
|
120
|
+
* correct rather than a limitation: same JavaScript context, same in-flight requests, same evidence.
|
|
121
|
+
*
|
|
122
|
+
* An UNSTAMPED event never clears this. An SDK older than the field stamps nothing, and letting one
|
|
123
|
+
* such event blank the current document would make every later comparison vacuous.
|
|
124
|
+
*/
|
|
125
|
+
#documentId;
|
|
126
|
+
/**
|
|
127
|
+
* The edit epoch of the most recent stamped event. DERIVED for the same reason `#documentId` is:
|
|
128
|
+
* the stream already carries it, and asking the page separately would be a second source of truth.
|
|
129
|
+
*
|
|
130
|
+
* A hot update advances it INSIDE the same document, which is the case `#documentId` structurally
|
|
131
|
+
* cannot see — no navigation, same page, replaced code. An unstamped event never clears it: most
|
|
132
|
+
* pages have no hot-update channel at all, so absence is "unknown", never "back to no edits".
|
|
133
|
+
*/
|
|
134
|
+
#editEpoch;
|
|
109
135
|
constructor(hello, socket, clock) {
|
|
110
136
|
this.id = hello.sessionId;
|
|
111
137
|
this.projectId = hello.projectId;
|
|
@@ -200,8 +226,20 @@ export class Session {
|
|
|
200
226
|
staleMs() {
|
|
201
227
|
return this.#clock() - this.#startedAt;
|
|
202
228
|
}
|
|
229
|
+
/** The document the most recent stamped event was observed under. See `#documentId`. */
|
|
230
|
+
get currentDocumentId() {
|
|
231
|
+
return this.#documentId;
|
|
232
|
+
}
|
|
233
|
+
/** The edit epoch the most recent stamped event was observed under. See `#editEpoch`. */
|
|
234
|
+
get currentEditEpoch() {
|
|
235
|
+
return this.#editEpoch;
|
|
236
|
+
}
|
|
203
237
|
/** Re-stamp an incoming event with server-relative time, buffer it, and fan out. */
|
|
204
238
|
pushEvent(event, byteSize) {
|
|
239
|
+
if (event.documentId !== undefined)
|
|
240
|
+
this.#documentId = event.documentId;
|
|
241
|
+
if (event.editEpoch !== undefined)
|
|
242
|
+
this.#editEpoch = event.editEpoch;
|
|
205
243
|
if (event.type === EventType.PAGE_HEALTH) {
|
|
206
244
|
const r = readHealthEvent(event.data);
|
|
207
245
|
this.applyHealth(r.hidden ?? this.#hidden, r.focused ?? this.#focused, r.runtime, r.engine, r.brand);
|
|
@@ -343,19 +381,50 @@ export class Session {
|
|
|
343
381
|
* action id. Ids are minted independently of command correlation ids so the journal is self-contained.
|
|
344
382
|
*/
|
|
345
383
|
beginAction(tool, args) {
|
|
346
|
-
this.#
|
|
347
|
-
const actionId = `${ACTION_ID_PREFIX}${String(this.#actionSeq)}`;
|
|
384
|
+
const actionId = this.#mintActionId();
|
|
348
385
|
// Held here, not only in the journal: with the journal off every event was unattributed, and
|
|
349
386
|
// pushEvent reads that as ambient churn the settle oracle then ignores. See action-attribution test.
|
|
350
387
|
this.#activeActionId = actionId;
|
|
351
388
|
this.#journal?.beginAction(actionId, tool, args);
|
|
352
389
|
return actionId;
|
|
353
390
|
}
|
|
391
|
+
/**
|
|
392
|
+
* Record one action WITHOUT opening an attribution window — for a tool that returns a verdict but
|
|
393
|
+
* drives nothing, so no event it observes was caused by it. See `JournalRecorder.recordAction`.
|
|
394
|
+
*/
|
|
395
|
+
recordAction(tool, args, effect) {
|
|
396
|
+
const actionId = this.#mintActionId();
|
|
397
|
+
this.#journal?.recordAction(actionId, tool, args, effect);
|
|
398
|
+
return actionId;
|
|
399
|
+
}
|
|
400
|
+
#mintActionId() {
|
|
401
|
+
this.#actionSeq += 1;
|
|
402
|
+
return `${ACTION_ID_PREFIX}${String(this.#actionSeq)}`;
|
|
403
|
+
}
|
|
404
|
+
/**
|
|
405
|
+
* How many actions this session has dispatched.
|
|
406
|
+
*
|
|
407
|
+
* The counter that mints journal action ids, not a second one — so it equals the journal's own
|
|
408
|
+
* length, and `reticle_context`'s `step`, whenever this session journals at all.
|
|
409
|
+
*/
|
|
410
|
+
get actionCount() {
|
|
411
|
+
return this.#actionSeq;
|
|
412
|
+
}
|
|
354
413
|
/** Close the active action window, persisting its action record with the settle outcome. */
|
|
355
414
|
finishAction(effect, settled, settledInMs) {
|
|
356
415
|
this.#activeActionId = undefined;
|
|
357
416
|
this.#journal?.finishAction(effect, settled, settledInMs);
|
|
358
417
|
}
|
|
418
|
+
/**
|
|
419
|
+
* The durable action ledger for this session — the ledger the run context is folded out of.
|
|
420
|
+
*
|
|
421
|
+
* `[]` when nothing journals this session (journaling disabled, or an id that is not a safe path
|
|
422
|
+
* segment). An absent ledger reads as an empty run, never as a missing one: the fold simply has
|
|
423
|
+
* nothing to say, which is the honest answer.
|
|
424
|
+
*/
|
|
425
|
+
async readJournalActions() {
|
|
426
|
+
return (await this.#journalReader?.readActions?.()) ?? [];
|
|
427
|
+
}
|
|
359
428
|
/** Flush any buffered journal events to disk (call on session end). */
|
|
360
429
|
async flushJournal() {
|
|
361
430
|
await this.#journal?.flush();
|
|
@@ -370,6 +439,19 @@ export class Session {
|
|
|
370
439
|
* from it, and the split between those two tool calls is the whole reason it exists.
|
|
371
440
|
*/
|
|
372
441
|
lastAct = new LastAct();
|
|
442
|
+
/**
|
|
443
|
+
* What this app still cannot tell Reticle, as of the most recent verdict.
|
|
444
|
+
*
|
|
445
|
+
* Lives on the session because that is the scope the question belongs to: "am I done with this
|
|
446
|
+
* app" is a session question, and a gap closed halfway through must not follow the agent to the
|
|
447
|
+
* end of it.
|
|
448
|
+
*/
|
|
449
|
+
gaps = new GapLedger();
|
|
450
|
+
/**
|
|
451
|
+
* The read-only calls the journal does not keep, so "was `reticle_context` ever called" has
|
|
452
|
+
* something to fold. See honesty/feature-capture.ts — it is the only state that instrument adds.
|
|
453
|
+
*/
|
|
454
|
+
capture = new CaptureLedger();
|
|
373
455
|
// ── Server-authoritative liveness (immune to browser-tab throttling) ──────────────
|
|
374
456
|
/**
|
|
375
457
|
* Stamp the wall-clock of the latest AGENT command (called whenever a tool resolves this session).
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* When a run with nothing connected is worth mentioning to a human.
|
|
3
|
+
*
|
|
4
|
+
* Ten minutes sits well past setup latency — reading a doc, restarting a dev server, opening a tab —
|
|
5
|
+
* and short enough that the warning still arrives while somebody is at the keyboard.
|
|
6
|
+
*/
|
|
7
|
+
export declare const STALL_AFTER_MS: number;
|
|
8
|
+
/** Start the clock, from the same place the instrumentation clock is marked. */
|
|
9
|
+
export declare function markStallClock(now: number): void;
|
|
10
|
+
/**
|
|
11
|
+
* How long this daemon has waited with no app, or undefined if the question is moot.
|
|
12
|
+
*
|
|
13
|
+
* Moot means the clock was never started, or an app already connected. A run that starts slowly and
|
|
14
|
+
* then works has nothing to report, which is why this returns undefined rather than a duration.
|
|
15
|
+
*/
|
|
16
|
+
export declare function stallUptime(now: number): number | undefined;
|
|
17
|
+
/** Tests only. */
|
|
18
|
+
export declare function resetStallClock(): void;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How long this daemon has waited with no app connected.
|
|
3
|
+
*
|
|
4
|
+
* This is what survived `instrumentation_stalled`. The EVENT was removed: it claimed to separate
|
|
5
|
+
* "never wired" from "died before reporting" and did not, because it only fired on a flush tick or a
|
|
6
|
+
* graceful shutdown, so a killed daemon was invisible to it exactly as before. Everything it carried
|
|
7
|
+
* is derivable from `daemon_started` minus `app_instrumented`, joined on `sessionId` — and that set
|
|
8
|
+
* difference is strictly MORE complete, because it does catch the killed daemons. A second way to
|
|
9
|
+
* compute the same number that disagrees with the first is worse than not having it.
|
|
10
|
+
*
|
|
11
|
+
* The CLOCK is a different thing and stays. Two live features read it, neither of them a metric: the
|
|
12
|
+
* no-session diagnosis tells an agent how long this daemon has been waiting, and the daemon warns on
|
|
13
|
+
* stderr when a run passes the threshold with nothing connected. Both are help for somebody stuck
|
|
14
|
+
* mid-install, which is the moment they are least able to work it out themselves.
|
|
15
|
+
*
|
|
16
|
+
* It lives under `session/` rather than `telemetry/` now, beside the two files that read it. The old
|
|
17
|
+
* home is why the removal looked like it would take the diagnosis with it.
|
|
18
|
+
*/
|
|
19
|
+
import { appEverConnected } from '../telemetry/app-instrumented.js';
|
|
20
|
+
/**
|
|
21
|
+
* When a run with nothing connected is worth mentioning to a human.
|
|
22
|
+
*
|
|
23
|
+
* Ten minutes sits well past setup latency — reading a doc, restarting a dev server, opening a tab —
|
|
24
|
+
* and short enough that the warning still arrives while somebody is at the keyboard.
|
|
25
|
+
*/
|
|
26
|
+
export const STALL_AFTER_MS = 10 * 60 * 1000;
|
|
27
|
+
let daemonStartedAt;
|
|
28
|
+
/** Start the clock, from the same place the instrumentation clock is marked. */
|
|
29
|
+
export function markStallClock(now) {
|
|
30
|
+
daemonStartedAt = now;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* How long this daemon has waited with no app, or undefined if the question is moot.
|
|
34
|
+
*
|
|
35
|
+
* Moot means the clock was never started, or an app already connected. A run that starts slowly and
|
|
36
|
+
* then works has nothing to report, which is why this returns undefined rather than a duration.
|
|
37
|
+
*/
|
|
38
|
+
export function stallUptime(now) {
|
|
39
|
+
if (daemonStartedAt === undefined)
|
|
40
|
+
return undefined;
|
|
41
|
+
if (appEverConnected())
|
|
42
|
+
return undefined;
|
|
43
|
+
return now - daemonStartedAt;
|
|
44
|
+
}
|
|
45
|
+
/** Tests only. */
|
|
46
|
+
export function resetStallClock() {
|
|
47
|
+
daemonStartedAt = undefined;
|
|
48
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { AutomationHint } from '@reticlehq/core';
|
|
2
|
-
|
|
2
|
+
interface AutomationInput {
|
|
3
3
|
env: NodeJS.ProcessEnv;
|
|
4
4
|
fileExists: (path: string) => boolean;
|
|
5
5
|
hasTty: boolean;
|
|
@@ -15,3 +15,4 @@ export interface AutomationInput {
|
|
|
15
15
|
export declare function resolveAutomationHint(input: AutomationInput): AutomationHint | undefined;
|
|
16
16
|
/** The real reading, taken once per process. */
|
|
17
17
|
export declare function currentAutomationHint(env: NodeJS.ProcessEnv): AutomationHint | undefined;
|
|
18
|
+
export {};
|
|
@@ -13,8 +13,7 @@ import { profileProject } from './project-profile.js';
|
|
|
13
13
|
import { startUpdateCheck } from '../update/update-nudge.js';
|
|
14
14
|
import { markDaemonStart } from './mcp-connection.js';
|
|
15
15
|
import { markInstrumentationClock } from './app-instrumented.js';
|
|
16
|
-
import { markStallClock,
|
|
17
|
-
import { readProjectId } from '../cli/cli-port.js';
|
|
16
|
+
import { markStallClock, STALL_AFTER_MS, stallUptime } from '../session/stall-clock.js';
|
|
18
17
|
/** Let the daemon finish coming up before walking the source tree for a profile. */
|
|
19
18
|
const PROJECT_PROFILE_DELAY_MS = 5_000;
|
|
20
19
|
/**
|
|
@@ -74,19 +73,18 @@ export function installDaemonTelemetry(cwd, now = () => Date.now()) {
|
|
|
74
73
|
/* a profile is a nice-to-have; it must never touch the daemon */
|
|
75
74
|
}
|
|
76
75
|
}, PROJECT_PROFILE_DELAY_MS).unref();
|
|
76
|
+
// Once per daemon run: a warning repeated every ten minutes at somebody already stuck is noise.
|
|
77
|
+
let stallWarned = false;
|
|
77
78
|
const flushWindow = () => {
|
|
78
|
-
// BEFORE the idle return, and that ordering
|
|
79
|
-
// empty window by definition — no app, so no tool
|
|
80
|
-
//
|
|
81
|
-
//
|
|
82
|
-
//
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
agentAttached: metrics.agentEverAttached,
|
|
88
|
-
}, now);
|
|
89
|
-
if (stalled) {
|
|
79
|
+
// BEFORE the idle return, and that ordering still matters for the reason the removed event
|
|
80
|
+
// documented: a stalled install produces an empty window by definition — no app, so no tool
|
|
81
|
+
// calls, so nothing to roll up — so checking after the early return would never fire on the one
|
|
82
|
+
// case worth warning about. What is left here is the WARNING, for a human watching the log; the
|
|
83
|
+
// metric that used to ride alongside it was removed as a second, less complete way to compute
|
|
84
|
+
// `daemon_started` minus `app_instrumented`.
|
|
85
|
+
const waited = stallUptime(now());
|
|
86
|
+
if (!stallWarned && waited !== undefined && waited >= STALL_AFTER_MS) {
|
|
87
|
+
stallWarned = true;
|
|
90
88
|
const minutes = Math.round(STALL_AFTER_MS / 60_000);
|
|
91
89
|
process.stderr.write(`[reticle] no app has connected after ${String(minutes)} minutes — ` +
|
|
92
90
|
'the install may be incomplete. Restart the dev server if you added the Reticle plugin.\n');
|
|
@@ -113,15 +111,6 @@ export function installDaemonTelemetry(cwd, now = () => Date.now()) {
|
|
|
113
111
|
stopped ??= (async () => {
|
|
114
112
|
clearInterval(flush);
|
|
115
113
|
clearTimeout(firstFlush);
|
|
116
|
-
// Last chance to say that no app ever arrived. The periodic path cannot report an
|
|
117
|
-
// UNATTENDED stall at all — that daemon idle-exits at five minutes and the periodic
|
|
118
|
-
// threshold is ten — so without this the event only ever describes daemons with an agent
|
|
119
|
-
// attached, which is not the population it was built to measure. Self-limiting: if the
|
|
120
|
-
// interval already reported, this returns false and emits nothing.
|
|
121
|
-
reportInstrumentationStall({
|
|
122
|
-
initialized: readProjectId(cwd) !== undefined,
|
|
123
|
-
agentAttached: metrics.agentEverAttached,
|
|
124
|
-
}, now, { atShutdown: true });
|
|
125
114
|
// The rich one: the whole session in a single event — duration, the tool histogram, error
|
|
126
115
|
// shapes, verifications, browser launches. This is what replaced the per-tool-call event.
|
|
127
116
|
await getTelemetry().emit(TelemetryEventKind.DAEMON_STOPPED, {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/** One queued report: the payload, plus what is needed to find and remove it again. */
|
|
2
|
-
|
|
2
|
+
interface OutboxEntry {
|
|
3
3
|
id: string;
|
|
4
4
|
t: string;
|
|
5
5
|
payload: unknown;
|
|
@@ -20,3 +20,4 @@ export declare function readOutbox(home?: string): OutboxEntry[];
|
|
|
20
20
|
* Volume is a handful of lines a week, so the cost is irrelevant.
|
|
21
21
|
*/
|
|
22
22
|
export declare function markDelivered(id: string | null, home?: string): void;
|
|
23
|
+
export {};
|
|
@@ -13,9 +13,6 @@ export declare const FeatureFamily: {
|
|
|
13
13
|
readonly BUG_CAPSULES: "bug_capsules";
|
|
14
14
|
readonly CROSS_RUN_HISTORY: "cross_run_history";
|
|
15
15
|
};
|
|
16
|
-
export type FeatureFamily = (typeof FeatureFamily)[keyof typeof FeatureFamily];
|
|
17
|
-
/** Count source files, stopping at the cap (the cap itself lands in the largest bucket, correctly). */
|
|
18
|
-
export declare function countSourceFiles(root: string, readDir?: DirReader): number;
|
|
19
16
|
export declare function sizeBucket(sourceFiles: number): ProjectSize;
|
|
20
17
|
/**
|
|
21
18
|
* Project age in whole WEEKS, from the repo's first commit.
|
|
@@ -30,11 +27,6 @@ export declare function sizeBucket(sourceFiles: number): ProjectSize;
|
|
|
30
27
|
export declare function projectAgeWeeks(root: string, now: number, read?: FileReader): number | undefined;
|
|
31
28
|
/** True when package.json declares workspaces — monorepos exercise very different code paths. */
|
|
32
29
|
export declare function isMonorepo(root: string, read?: FileReader): boolean;
|
|
33
|
-
interface DirEntry {
|
|
34
|
-
name: string;
|
|
35
|
-
isDirectory: boolean;
|
|
36
|
-
}
|
|
37
|
-
type DirReader = (dir: string) => DirEntry[];
|
|
38
30
|
type FileReader = (path: string) => string;
|
|
39
31
|
/**
|
|
40
32
|
* Build the full profile. Best-effort throughout: an unreadable project yields a profile of zeroes
|
|
@@ -45,7 +45,7 @@ const MAX_FILES_SCANNED = 6000;
|
|
|
45
45
|
const MAX_WALK_DEPTH = 8;
|
|
46
46
|
const MS_PER_WEEK = 7 * 24 * 60 * 60 * 1000;
|
|
47
47
|
/** Count source files, stopping at the cap (the cap itself lands in the largest bucket, correctly). */
|
|
48
|
-
|
|
48
|
+
function countSourceFiles(root, readDir = defaultReadDir) {
|
|
49
49
|
let count = 0;
|
|
50
50
|
const walk = (dir, depth) => {
|
|
51
51
|
if (depth > MAX_WALK_DEPTH || count >= MAX_FILES_SCANNED)
|
|
@@ -11,8 +11,6 @@
|
|
|
11
11
|
* how a replay works.
|
|
12
12
|
*/
|
|
13
13
|
import { type ReticleVerificationRun } from '@reticlehq/core';
|
|
14
|
-
/** The tool name reported for this path, so CI runs are distinguishable from agent-driven ones. */
|
|
15
|
-
export declare const VERIFY_SURFACE = "reticle_verify";
|
|
16
14
|
/**
|
|
17
15
|
* Report one completed verification run: the verdict, and one bug per failing flow.
|
|
18
16
|
*
|
|
@@ -14,7 +14,7 @@ import { BugAttribution, BugSource, RunFlowStatus, TelemetryActor, TelemetryEven
|
|
|
14
14
|
import { getSessionMetrics } from './session-metrics.js';
|
|
15
15
|
import { getTelemetry } from './telemetry.js';
|
|
16
16
|
/** The tool name reported for this path, so CI runs are distinguishable from agent-driven ones. */
|
|
17
|
-
|
|
17
|
+
const VERIFY_SURFACE = 'reticle_verify';
|
|
18
18
|
/**
|
|
19
19
|
* Report one completed verification run: the verdict, and one bug per failing flow.
|
|
20
20
|
*
|
|
@@ -133,7 +133,7 @@ export declare class SessionMetrics {
|
|
|
133
133
|
/**
|
|
134
134
|
* Has an agent ever attached to this daemon?
|
|
135
135
|
*
|
|
136
|
-
* Read by the
|
|
136
|
+
* Read by the stall clock, which needs to tell two very different silences apart:
|
|
137
137
|
* a daemon nobody is using, and a daemon with an agent sitting on it that has no app to drive.
|
|
138
138
|
* The second is the funnel's failure case and the first is not a problem at all.
|
|
139
139
|
*/
|
|
@@ -575,7 +575,7 @@ export class SessionMetrics {
|
|
|
575
575
|
/**
|
|
576
576
|
* Has an agent ever attached to this daemon?
|
|
577
577
|
*
|
|
578
|
-
* Read by the
|
|
578
|
+
* Read by the stall clock, which needs to tell two very different silences apart:
|
|
579
579
|
* a daemon nobody is using, and a daemon with an agent sitting on it that has no app to drive.
|
|
580
580
|
* The second is the funnel's failure case and the first is not a problem at all.
|
|
581
581
|
*/
|
|
@@ -1,4 +1,15 @@
|
|
|
1
|
-
import { TelemetryEventKind, ProjectIdSource, type Crash, type Feedback, type Identity, type BugFound, type InitOutcome, type InstallSource, type McpConnection, type McpOutage, type ToolRefusal, type AppInstrumentation, type
|
|
1
|
+
import { TelemetryEventKind, ProjectIdSource, type Crash, type Feedback, type Identity, type BugFound, type InitOutcome, type InstallSource, type McpConnection, type McpOutage, type ToolRefusal, type AppInstrumentation, type ProjectProfile, type SessionSummary, type TelemetryActor, type Verification, type VersionChange } from '@reticlehq/core';
|
|
2
|
+
/**
|
|
3
|
+
* PostHog's group type for a licensed customer. Sent as `$groups` so PostHog can aggregate a whole
|
|
4
|
+
* organisation natively: per-company retention, "which orgs went quiet", one row per customer rather
|
|
5
|
+
* than one per machine.
|
|
6
|
+
*
|
|
7
|
+
* The KEY is the licence id and nothing else. The organisation's NAME is never sent from a customer's
|
|
8
|
+
* machine, here or anywhere; the name reaches PostHog only if we push it ourselves from the issuance
|
|
9
|
+
* ledger, which is the one place that mapping lives. So an event still carries no identity, and the
|
|
10
|
+
* analytics side learns a customer list only when we deliberately upload one.
|
|
11
|
+
*/
|
|
12
|
+
export declare const POSTHOG_GROUP_ORGANIZATION = "organization";
|
|
2
13
|
/**
|
|
3
14
|
* Feedback gets its own, much larger budget — and a retry.
|
|
4
15
|
*
|
|
@@ -78,8 +89,6 @@ export interface TelemetryExtra {
|
|
|
78
89
|
outage?: McpOutage;
|
|
79
90
|
/** `app_instrumented`: an app carrying the SDK reached this daemon for the first time. */
|
|
80
91
|
instrumentation?: AppInstrumentation;
|
|
81
|
-
/** `instrumentation_stalled`: the daemon waited and no app ever arrived. */
|
|
82
|
-
stall?: InstrumentationStall;
|
|
83
92
|
/** `reticle_installed` / `init_completed`: which published route brought this install in. */
|
|
84
93
|
installSource?: InstallSource;
|
|
85
94
|
}
|
|
@@ -42,6 +42,17 @@ const POSTHOG_KEY = 'phc_q4yCxKMXHfWQ43VAz4HxJXZFLqFi4nBbg5v3avemggYo';
|
|
|
42
42
|
* still key on distinct_id.
|
|
43
43
|
*/
|
|
44
44
|
const POSTHOG_PERSONLESS = { $process_person_profile: false };
|
|
45
|
+
/**
|
|
46
|
+
* PostHog's group type for a licensed customer. Sent as `$groups` so PostHog can aggregate a whole
|
|
47
|
+
* organisation natively: per-company retention, "which orgs went quiet", one row per customer rather
|
|
48
|
+
* than one per machine.
|
|
49
|
+
*
|
|
50
|
+
* The KEY is the licence id and nothing else. The organisation's NAME is never sent from a customer's
|
|
51
|
+
* machine, here or anywhere; the name reaches PostHog only if we push it ourselves from the issuance
|
|
52
|
+
* ledger, which is the one place that mapping lives. So an event still carries no identity, and the
|
|
53
|
+
* analytics side learns a customer list only when we deliberately upload one.
|
|
54
|
+
*/
|
|
55
|
+
export const POSTHOG_GROUP_ORGANIZATION = 'organization';
|
|
45
56
|
const SEND_TIMEOUT_MS = 2000;
|
|
46
57
|
/**
|
|
47
58
|
* Feedback gets its own, much larger budget — and a retry.
|
|
@@ -337,7 +348,6 @@ export const createTelemetry = (opts) => {
|
|
|
337
348
|
...(extra?.refusal !== undefined ? { refusal: extra.refusal } : {}),
|
|
338
349
|
...(extra?.outage !== undefined ? { outage: extra.outage } : {}),
|
|
339
350
|
...(extra?.instrumentation !== undefined ? { instrumentation: extra.instrumentation } : {}),
|
|
340
|
-
...(extra?.stall !== undefined ? { stall: extra.stall } : {}),
|
|
341
351
|
// A SCALAR, so it needs the event build and the wire schema but no entry in `blocks` below —
|
|
342
352
|
// there is nothing to flatten. It still needs both of those, which is the trap `outage` fell
|
|
343
353
|
// into: a field declared on TelemetryExtra and missing from either one is dropped in silence.
|
|
@@ -348,14 +358,17 @@ export const createTelemetry = (opts) => {
|
|
|
348
358
|
// `blocks` entry. Resolved per event off the event's own clock rather than once at startup — an
|
|
349
359
|
// eleven-hour session must not report the status it booted with. All three are absent unless a
|
|
350
360
|
// release has an issuer key baked, so an OSS payload is byte-identical to before.
|
|
351
|
-
|
|
361
|
+
// The RESOLVED env, not process.env: createTelemetry takes an env for injection, and reading
|
|
362
|
+
// the ambient one here would silently ignore it. Identical in production, wrong everywhere the
|
|
363
|
+
// seam is actually used.
|
|
364
|
+
...licenseFacts(eventTs, env),
|
|
352
365
|
};
|
|
353
366
|
// Map the core contract onto PostHog's capture shape: id/name/time move up, the rest are properties.
|
|
354
367
|
// The feedback body is FLATTENED into `feedback_*` properties rather than sent as a nested object:
|
|
355
368
|
// PostHog filters and breakdowns operate on top-level properties, so a nested `feedback.stack`
|
|
356
369
|
// would be invisible to the exact "which stacks report the most bugs" question this exists to
|
|
357
370
|
// answer. One prefix keeps them grouped in the UI without a nested path.
|
|
358
|
-
const { anonymousId: distinctId, event: name, ts, sessionId: eventSessionId, feedback, session, project, verification, versionChange, crash, identity, connection, init, bug, refusal, outage, instrumentation,
|
|
371
|
+
const { anonymousId: distinctId, event: name, ts, sessionId: eventSessionId, feedback, session, project, verification, versionChange, crash, identity, connection, init, bug, refusal, outage, instrumentation, ...rest } = event;
|
|
359
372
|
// `$session_id` is PostHog's OWN session property, so sending ours under that name lights up
|
|
360
373
|
// its built-in session views and funnels for free rather than requiring custom HogQL everywhere.
|
|
361
374
|
// `sessionId` rides only on events that happened inside a daemon run. A one-shot CLI command
|
|
@@ -386,7 +399,6 @@ export const createTelemetry = (opts) => {
|
|
|
386
399
|
refusal,
|
|
387
400
|
outage,
|
|
388
401
|
instrumentation,
|
|
389
|
-
stall,
|
|
390
402
|
};
|
|
391
403
|
for (const [prefix, block] of Object.entries(blocks)) {
|
|
392
404
|
for (const [key, value] of Object.entries(block ?? {})) {
|
|
@@ -397,7 +409,15 @@ export const createTelemetry = (opts) => {
|
|
|
397
409
|
event: name,
|
|
398
410
|
distinct_id: distinctId,
|
|
399
411
|
timestamp: new Date(ts).toISOString(),
|
|
400
|
-
properties: {
|
|
412
|
+
properties: {
|
|
413
|
+
...properties,
|
|
414
|
+
...POSTHOG_PERSONLESS,
|
|
415
|
+
// Only on a licensed build. An unlicensed event has no organisation to belong to, and an
|
|
416
|
+
// empty group key would mint a phantom "no org" bucket that every OSS install falls into.
|
|
417
|
+
...(event.licenseId !== undefined
|
|
418
|
+
? { $groups: { [POSTHOG_GROUP_ORGANIZATION]: event.licenseId } }
|
|
419
|
+
: {}),
|
|
420
|
+
},
|
|
401
421
|
};
|
|
402
422
|
const body = JSON.stringify({ api_key: apiKey, batch: [capture] });
|
|
403
423
|
const filePath = env[Env.FILE];
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* be readable and testable on its own rather than inferred from a ternary inside a dispatcher.
|
|
14
14
|
*/
|
|
15
15
|
import { CaptureLoss, VerifiedReason } from '@reticlehq/core';
|
|
16
|
-
import {
|
|
16
|
+
import { VERDICT_TOOLS } from '../tools/feedback-tools.js';
|
|
17
17
|
import { getBrowserMode } from './browser-mode.js';
|
|
18
18
|
/**
|
|
19
19
|
* The deciding clause, read off the result `decideVerified` wrote it onto.
|
|
@@ -92,7 +92,7 @@ function passedOf(result, status) {
|
|
|
92
92
|
* it verified. That is the product's thesis reduced to a boolean.
|
|
93
93
|
*/
|
|
94
94
|
export function verificationOf(toolName, result, durationMs, brand) {
|
|
95
|
-
if (!
|
|
95
|
+
if (!VERDICT_TOOLS.has(toolName))
|
|
96
96
|
return undefined;
|
|
97
97
|
// A paused session REFUSES the call — nothing driven, nothing asserted. The refusal carries a
|
|
98
98
|
// verdict field so the agent never reads undefined off it, and that field must not be mistaken
|