@reticlehq/server 3.4.0 → 3.6.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/README.md +1 -1
- package/SKILL.md +16 -8
- package/dist/chromium-launch-options.d.ts +3 -1
- package/dist/chromium-launch-options.js +2 -1
- package/dist/command/cli/adhoc-verdict.d.ts +1 -0
- package/dist/command/cli/adhoc-verdict.js +6 -3
- package/dist/command/cli/answers/open-note.d.ts +2 -0
- package/dist/command/cli/answers/open-note.js +22 -0
- package/dist/command/cli/auth/cloud-session.d.ts +4 -4
- package/dist/command/cli/auth/device-flow.d.ts +2 -2
- package/dist/command/cli/cli-flow-commands.js +4 -8
- package/dist/command/cli/cli-parse.d.ts +1 -1
- package/dist/command/cli/cli-parse.js +13 -11
- package/dist/command/cli/cli-verify.d.ts +1 -1
- package/dist/command/cli/cli-verify.js +35 -25
- package/dist/command/cli/cloud-cli.js +110 -12
- package/dist/command/cli/cloud-login.d.ts +9 -1
- package/dist/command/cli/cloud-login.js +43 -8
- package/dist/command/cli/demo/demo-page.js +11 -3
- package/dist/command/cli/doctor/browser/chromium-hint.d.ts +46 -0
- package/dist/command/cli/doctor/browser/chromium-hint.js +99 -1
- package/dist/command/cli/drive/drive-attach.d.ts +27 -2
- package/dist/command/cli/drive/drive-attach.js +71 -4
- package/dist/command/cli/drive/drive-command.d.ts +2 -1
- package/dist/command/cli/drive/drive-command.js +13 -7
- package/dist/command/cli/lifecycle/daemon-lifecycle.js +4 -1
- package/dist/command/cli/ports/port-holder.d.ts +10 -0
- package/dist/command/cli/ports/port-holder.js +24 -0
- package/dist/command/cli/setup-mcp-cli.js +15 -5
- package/dist/command/cli/tutorial.d.ts +2 -2
- package/dist/command/cli/tutorial.js +6 -9
- package/dist/command/cli.d.ts +6 -0
- package/dist/command/cli.js +45 -23
- package/dist/command/daemon/binding/daemon-status-probe.d.ts +2 -0
- package/dist/command/daemon/binding/daemon-status-probe.js +1 -0
- package/dist/command/daemon/daemon-resolve.d.ts +14 -0
- package/dist/command/daemon/daemon-resolve.js +33 -2
- package/dist/command/setup/agent-writer.d.ts +1 -1
- package/dist/command/setup/agent-writer.js +3 -2
- package/dist/command/setup/bringup/connect-lease.d.ts +1 -0
- package/dist/command/setup/bringup/connect-lease.js +1 -0
- package/dist/command/setup/bringup/dev-server-wait.d.ts +10 -0
- package/dist/command/setup/bringup/dev-server-wait.js +20 -1
- package/dist/command/setup/init/init-port.d.ts +4 -0
- package/dist/command/setup/init/init-port.js +10 -0
- package/dist/command/setup/init/init-runtime.d.ts +10 -0
- package/dist/command/setup/init/init-runtime.js +71 -9
- package/dist/command/setup/node-effects.js +8 -2
- package/dist/command/setup/remaining-steps.js +2 -1
- package/dist/command/setup/run-setup.d.ts +7 -0
- package/dist/command/setup/run-setup.js +38 -25
- package/dist/command/setup/setup-command.d.ts +2 -1
- package/dist/command/setup/setup-command.js +2 -0
- package/dist/command/setup/setup-mcp.d.ts +2 -0
- package/dist/command/setup/setup-mcp.js +14 -5
- package/dist/command/version/peer-announce.d.ts +6 -2
- package/dist/command/version/peer-announce.js +8 -6
- package/dist/command/version/version-nudge.d.ts +17 -3
- package/dist/command/version/version-nudge.js +25 -4
- package/dist/command/version/version-skew.js +11 -2
- package/dist/features/crawl/crawl-tools.js +4 -0
- package/dist/features/crawl/crawl.d.ts +6 -0
- package/dist/features/crawl/crawl.js +9 -1
- package/dist/features/exhaust/ledger.d.ts +7 -0
- package/dist/features/exhaust/ledger.js +35 -9
- package/dist/features/harness/app-findings.d.ts +2 -0
- package/dist/features/harness/app-findings.js +52 -0
- package/dist/features/harness/drive-report.d.ts +27 -1
- package/dist/features/harness/drive-report.js +162 -11
- package/dist/features/harness/drivers.d.ts +10 -14
- package/dist/features/harness/drivers.js +10 -14
- package/dist/features/harness/fill-values.d.ts +3 -69
- package/dist/features/harness/fill-values.js +3 -133
- package/dist/features/harness/goals.d.ts +21 -0
- package/dist/features/harness/goals.js +77 -0
- package/dist/features/harness/harness.d.ts +21 -0
- package/dist/features/harness/harness.js +69 -2
- package/dist/features/harness/platform/drive-target.d.ts +30 -0
- package/dist/features/harness/platform/drive-target.js +81 -0
- package/dist/features/harness/platform/local-apps.d.ts +94 -0
- package/dist/features/harness/platform/local-apps.js +204 -0
- package/dist/features/harness/platform/remote-drive.d.ts +180 -0
- package/dist/features/harness/platform/remote-drive.js +365 -0
- package/dist/features/harness/platform/script.d.ts +48 -0
- package/dist/features/harness/platform/script.js +128 -0
- package/dist/features/harness/platform/server-driver.d.ts +41 -0
- package/dist/features/harness/platform/server-driver.js +179 -0
- package/dist/features/harness/platform-config.d.ts +7 -0
- package/dist/features/harness/platform-config.js +14 -2
- package/dist/features/harness/script/app-controls.d.ts +5 -0
- package/dist/features/harness/script/app-controls.js +40 -0
- package/dist/features/harness/script/lane-ids.d.ts +14 -0
- package/dist/features/harness/script/lane-ids.js +16 -0
- package/dist/features/harness/script/script-run.d.ts +41 -0
- package/dist/features/harness/script/script-run.js +299 -0
- package/dist/features/license/license.d.ts +2 -2
- package/dist/features/visual/visual-tools.d.ts +10 -1
- package/dist/features/visual/visual-tools.js +40 -3
- package/dist/hooks/driven-by.d.ts +4 -0
- package/dist/hooks/driven-by.js +16 -0
- package/dist/hooks/hook-commands.js +15 -4
- package/dist/hooks/tool-log.d.ts +9 -0
- package/dist/hooks/tool-log.js +29 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +91 -89
- package/dist/judgement/capsule/capsule-store.d.ts +4 -4
- package/dist/judgement/runs/artifact/build-verification-run.js +6 -0
- package/dist/judgement/runs/artifact/replay-mapping.d.ts +1 -1
- package/dist/judgement/runs/artifact/replay-mapping.js +16 -6
- package/dist/judgement/runs/artifact/run-store.d.ts +1 -1
- package/dist/judgement/runs/artifact/run-store.js +26 -1
- package/dist/judgement/runs/drive-run.d.ts +22 -0
- package/dist/judgement/runs/drive-run.js +99 -1
- package/dist/judgement/runs/repair-prompt.js +1 -1
- package/dist/judgement/runs/reticle-runner.js +8 -1
- package/dist/judgement/runs/verification-sync.d.ts +3 -2
- package/dist/judgement/runs/verification-sync.js +4 -3
- package/dist/language/flows/change/gate.d.ts +10 -0
- package/dist/language/flows/change/gate.js +22 -0
- package/dist/language/flows/decision.js +34 -0
- package/dist/language/flows/flow-anchor.d.ts +54 -8
- package/dist/language/flows/flow-anchor.js +88 -3
- package/dist/language/flows/flow-cross-step.d.ts +15 -0
- package/dist/language/flows/flow-cross-step.js +21 -0
- package/dist/language/flows/flow-learning.js +8 -11
- package/dist/language/flows/flow-mutate-tools.js +5 -3
- package/dist/language/flows/flow-replay-run.d.ts +23 -9
- package/dist/language/flows/flow-replay-run.js +101 -94
- package/dist/language/flows/flow-replay.d.ts +23 -1
- package/dist/language/flows/flow-replay.js +92 -19
- package/dist/language/flows/flow-scope.js +5 -1
- package/dist/language/flows/flow-step-runners.d.ts +20 -2
- package/dist/language/flows/flow-step-runners.js +236 -42
- package/dist/language/flows/flow-store-for-session.d.ts +2 -2
- package/dist/language/flows/flow-store-for-session.js +2 -2
- package/dist/language/flows/flow-tools.js +58 -34
- package/dist/language/flows/flows.js +5 -2
- package/dist/language/flows/heal-run.js +6 -4
- package/dist/language/flows/recording/flake.d.ts +2 -2
- package/dist/language/flows/recording/tape/recordings.d.ts +30 -4
- package/dist/language/flows/recording/tape/recordings.js +40 -8
- package/dist/language/flows/replay-from-hud.d.ts +20 -0
- package/dist/language/flows/replay-from-hud.js +51 -0
- package/dist/language/flows/replay.d.ts +9 -5
- package/dist/language/flows/replay.js +27 -9
- package/dist/language/flows/suite/suite-flakes.js +2 -0
- package/dist/language/flows/suite-selection.d.ts +2 -15
- package/dist/language/flows/suite-selection.js +4 -3
- package/dist/language/flows/verify-change-tools.js +8 -4
- package/dist/launch-chromium.d.ts +27 -5
- package/dist/launch-chromium.js +38 -8
- package/dist/memory/cloud/cloud-config.d.ts +12 -0
- package/dist/memory/cloud/cloud-config.js +15 -0
- package/dist/memory/cloud/cloud-sync.d.ts +17 -14
- package/dist/memory/cloud/cloud-sync.js +12 -1
- package/dist/memory/cloud/harness-config.d.ts +26 -1
- package/dist/memory/cloud/harness-config.js +99 -4
- package/dist/memory/cloud/hud-notices-source.d.ts +21 -0
- package/dist/memory/cloud/hud-notices-source.js +128 -0
- package/dist/memory/cloud/memory-scope.d.ts +144 -0
- package/dist/memory/cloud/memory-scope.js +205 -0
- package/dist/memory/cloud/sync-cycle.d.ts +21 -3
- package/dist/memory/cloud/sync-cycle.js +45 -5
- package/dist/memory/cloud/sync-daemon.js +7 -5
- package/dist/memory/cloud/sync-disk.d.ts +1 -1
- package/dist/memory/cloud/sync-disk.js +5 -8
- package/dist/memory/impact/account-watch.d.ts +19 -0
- package/dist/memory/impact/account-watch.js +34 -0
- package/dist/memory/impact/impact-recorder.d.ts +8 -0
- package/dist/memory/impact/impact-recorder.js +12 -2
- package/dist/memory/impact/impact-store.d.ts +16 -0
- package/dist/memory/impact/impact-store.js +142 -26
- package/dist/memory/intent/intent-shard.d.ts +2 -2
- package/dist/memory/intent/intent-tools.js +88 -5
- package/dist/memory/journal/envelope.d.ts +16 -16
- package/dist/memory/journal/journal-query.d.ts +14 -0
- package/dist/memory/journal/journal-query.js +15 -0
- package/dist/memory/journal/journal-recorder.js +7 -1
- package/dist/memory/journal/on-disk/retention.d.ts +3 -0
- package/dist/memory/journal/on-disk/retention.js +5 -0
- package/dist/memory/journal/on-disk/startup-maintenance.js +3 -1
- package/dist/memory/journal/on-disk/workspace-tiers.js +7 -4
- package/dist/memory/journal/session-end.js +6 -4
- package/dist/memory/project/artifact-root-resolver.d.ts +36 -1
- package/dist/memory/project/artifact-root-resolver.js +134 -26
- package/dist/memory/project/artifact-root.d.ts +42 -5
- package/dist/memory/project/artifact-root.js +87 -8
- package/dist/memory/project/dir/reticle-dir.d.ts +4 -2
- package/dist/memory/project/dir/reticle-dir.js +3 -1
- package/dist/memory/project/project-tools.js +22 -3
- package/dist/memory/project/session-root.d.ts +24 -1
- package/dist/memory/project/session-root.js +41 -5
- package/dist/memory/project/sync-status.d.ts +39 -0
- package/dist/memory/project/sync-status.js +90 -0
- package/dist/memory/recall/memory-tools.js +2 -0
- package/dist/memory/recall/project-memory.d.ts +19 -0
- package/dist/memory/recall/project-memory.js +45 -6
- package/dist/portal/bridge/bridge.d.ts +7 -4
- package/dist/portal/bridge/bridge.js +25 -3
- package/dist/portal/bridge/pairing-token.d.ts +9 -0
- package/dist/portal/bridge/pairing-token.js +17 -0
- package/dist/portal/input/real-input.d.ts +81 -10
- package/dist/portal/input/real-input.js +207 -22
- package/dist/portal/input/viewport-tools.js +19 -9
- package/dist/portal/pool/browser-pool.d.ts +22 -9
- package/dist/portal/pool/browser-pool.js +70 -35
- package/dist/portal/pool/playwright-launcher.d.ts +13 -0
- package/dist/portal/pool/playwright-launcher.js +17 -1
- package/dist/portal/pool/pool-contract.d.ts +7 -4
- package/dist/portal/session/command-payload.d.ts +8 -0
- package/dist/portal/session/command-payload.js +19 -0
- package/dist/portal/session/dev-server/dev-command.d.ts +12 -0
- package/dist/portal/session/dev-server/dev-command.js +61 -12
- package/dist/portal/session/facts/departure-note.d.ts +22 -0
- package/dist/portal/session/facts/departure-note.js +47 -0
- package/dist/portal/session/facts/session-replaced.d.ts +5 -0
- package/dist/portal/session/facts/session-replaced.js +5 -0
- package/dist/portal/session/first-run-wiring.d.ts +42 -0
- package/dist/portal/session/first-run-wiring.js +132 -0
- package/dist/portal/session/human/notes-ledger.d.ts +20 -0
- package/dist/portal/session/human/notes-ledger.js +55 -0
- package/dist/portal/session/human/review-store.d.ts +12 -0
- package/dist/portal/session/human/review-store.js +18 -10
- package/dist/portal/session/live-control-tools.d.ts +0 -13
- package/dist/portal/session/live-control-tools.js +28 -3
- package/dist/portal/session/no-session-diagnosis.d.ts +7 -0
- package/dist/portal/session/no-session-diagnosis.js +15 -0
- package/dist/portal/session/no-session-next-action.d.ts +11 -0
- package/dist/portal/session/no-session-next-action.js +31 -5
- package/dist/portal/session/no-session-watch.d.ts +14 -1
- package/dist/portal/session/no-session-watch.js +63 -10
- package/dist/portal/session/presence/session-mirror.js +3 -0
- package/dist/portal/session/serving-directory.d.ts +27 -0
- package/dist/portal/session/serving-directory.js +45 -0
- package/dist/portal/session/session-health.d.ts +7 -0
- package/dist/portal/session/session-health.js +6 -0
- package/dist/portal/session/session-info.d.ts +8 -1
- package/dist/portal/session/session-info.js +6 -0
- package/dist/portal/session/session-manager.d.ts +21 -0
- package/dist/portal/session/session-manager.js +44 -8
- package/dist/portal/session/session-reaper.js +4 -1
- package/dist/portal/session/session-successor.d.ts +5 -0
- package/dist/portal/session/session-tools.js +26 -3
- package/dist/portal/session/session.d.ts +12 -10
- package/dist/portal/session/session.js +30 -31
- package/dist/start-options.d.ts +6 -0
- package/dist/status-payload.d.ts +10 -0
- package/dist/status-payload.js +23 -0
- package/dist/surface/http-server.d.ts +1 -1
- package/dist/surface/http-server.js +15 -4
- package/dist/surface/mcp/mcp-proxy.d.ts +5 -0
- package/dist/surface/mcp/mcp-proxy.js +36 -5
- package/dist/surface/mcp/mcp.d.ts +11 -6
- package/dist/surface/mcp/mcp.js +28 -14
- package/dist/surface/mcp/proxy/proxy-log.js +3 -5
- package/dist/surface/mcp/server-instructions.d.ts +4 -2
- package/dist/surface/mcp/server-instructions.js +14 -4
- package/dist/surface/mcp/shared-params.js +2 -2
- package/dist/surface/tools/act/navigation/navigate-arrival.d.ts +8 -0
- package/dist/surface/tools/act/navigation/navigate-arrival.js +56 -2
- package/dist/surface/tools/act/settle-in-flight.js +25 -3
- package/dist/surface/tools/act-sequence-tool.js +2 -2
- package/dist/surface/tools/act-tools.js +25 -9
- package/dist/surface/tools/assert/assert-verdict.d.ts +1 -1
- package/dist/surface/tools/assert/assert-verdict.js +17 -2
- package/dist/surface/tools/browser-tools.js +8 -1
- package/dist/surface/tools/chat-drives.d.ts +26 -0
- package/dist/surface/tools/chat-drives.js +165 -0
- package/dist/surface/tools/dynamic-tools.js +29 -2
- package/dist/surface/tools/empty-session-list.d.ts +41 -0
- package/dist/surface/tools/empty-session-list.js +80 -0
- package/dist/surface/tools/error-recovery.d.ts +6 -0
- package/dist/surface/tools/error-recovery.js +38 -6
- package/dist/surface/tools/explore-tools.d.ts +10 -1
- package/dist/surface/tools/explore-tools.js +82 -13
- package/dist/surface/tools/feedback-tools.js +14 -0
- package/dist/surface/tools/harness-explore.d.ts +96 -14
- package/dist/surface/tools/harness-explore.js +227 -202
- package/dist/surface/tools/harness-plan.d.ts +12 -0
- package/dist/surface/tools/harness-plan.js +31 -0
- package/dist/surface/tools/harness-script.d.ts +8 -0
- package/dist/surface/tools/harness-script.js +342 -0
- package/dist/surface/tools/harness-toolset.d.ts +14 -1
- package/dist/surface/tools/harness-toolset.js +77 -6
- package/dist/surface/tools/invoke-tool.js +64 -9
- package/dist/surface/tools/lease-seed.d.ts +29 -0
- package/dist/surface/tools/lease-seed.js +123 -0
- package/dist/surface/tools/lease-tools.d.ts +3 -23
- package/dist/surface/tools/lease-tools.js +30 -131
- package/dist/surface/tools/live-call-text.js +2 -2
- package/dist/surface/tools/next-step.d.ts +19 -0
- package/dist/surface/tools/next-step.js +159 -0
- package/dist/surface/tools/observe-tools.js +2 -4
- package/dist/surface/tools/platform-moment.d.ts +12 -0
- package/dist/surface/tools/platform-moment.js +168 -0
- package/dist/surface/tools/query-hint-schema.d.ts +7 -0
- package/dist/surface/tools/query-hint-schema.js +10 -0
- package/dist/surface/tools/read-tools.js +8 -2
- package/dist/surface/tools/real-input-attempt.js +96 -13
- package/dist/surface/tools/tool-kit.d.ts +25 -1
- package/dist/surface/tools/tool-kit.js +4 -6
- package/dist/surface/tools/tools.js +20 -48
- package/dist/telemetry/argument-shape.js +1 -1
- package/dist/telemetry/hud-metrics.d.ts +2 -0
- package/dist/telemetry/hud-metrics.js +17 -0
- package/dist/telemetry/onboarding-firsts.d.ts +2 -2
- package/dist/telemetry/onboarding-firsts.js +2 -2
- package/dist/telemetry/telemetry.d.ts +12 -0
- package/dist/telemetry/telemetry.js +43 -14
- package/dist/telemetry/verification-of.js +1 -0
- package/dist/wire-journal.js +4 -1
- package/docs/README.md +1 -1
- package/docs/actions.mdx +1 -2
- package/docs/agent-cheatsheet.md +1 -1
- package/docs/autodrive.md +29 -25
- package/docs/cli/cloud.mdx +6 -4
- package/docs/cli/doctor.mdx +6 -0
- package/docs/cli/drive.mdx +8 -2
- package/docs/cli/init.mdx +11 -13
- package/docs/cli/status.mdx +5 -1
- package/docs/cli/version.mdx +3 -3
- package/docs/cli.mdx +14 -8
- package/docs/desktop.mdx +5 -1
- package/docs/environment.mdx +3 -7
- package/docs/getting-started.md +14 -2
- package/docs/human-control.md +1 -2
- package/docs/index.mdx +4 -3
- package/docs/install-agentic.mdx +1 -1
- package/docs/install-manual.mdx +2 -2
- package/docs/packages/next.mdx +3 -0
- package/docs/packages/tauri.mdx +2 -2
- package/docs/packages/vite-plugin.mdx +24 -1
- package/docs/packages.mdx +1 -1
- package/docs/quickstart.mdx +23 -11
- package/docs/telemetry.md +1 -1
- package/docs/tools/act.mdx +1 -1
- package/docs/tools/session-and-feedback.mdx +9 -9
- package/docs/usage.md +5 -3
- package/docs/what-is-recorded.md +13 -3
- package/docs/what-is-reticle.mdx +3 -1
- package/package.json +8 -12
- package/dist/features/harness/driver.d.ts +0 -50
- package/dist/features/harness/driver.js +0 -236
- package/dist/features/harness/jev-driver.d.ts +0 -188
- package/dist/features/harness/jev-driver.js +0 -678
- package/dist/features/harness/openai-driver.d.ts +0 -46
- package/dist/features/harness/openai-driver.js +0 -194
- package/dist/memory/project/dir/fill-value-store.d.ts +0 -32
- package/dist/memory/project/dir/fill-value-store.js +0 -88
- package/docs/local-registry.md +0 -143
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The derived records that ride along with runs, and the file each one lives in.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* One list, so adding a record is one line and cannot be half-done: the bundle, the hashing, the
|
|
5
|
+
* reporting and the disk source (`sync-disk.ts`) all read it.
|
|
6
6
|
*/
|
|
7
|
-
declare const DERIVED_RECORDS: readonly [{
|
|
7
|
+
export declare const DERIVED_RECORDS: readonly [{
|
|
8
8
|
readonly kind: "impact";
|
|
9
9
|
readonly file: "impact.json";
|
|
10
10
|
}, {
|
|
@@ -19,6 +19,12 @@ declare const DERIVED_RECORDS: readonly [{
|
|
|
19
19
|
}, {
|
|
20
20
|
readonly kind: "assertion-tiers";
|
|
21
21
|
readonly file: "assertion-tiers.json";
|
|
22
|
+
}, {
|
|
23
|
+
readonly kind: "coverage";
|
|
24
|
+
readonly file: "coverage.json";
|
|
25
|
+
}, {
|
|
26
|
+
readonly kind: "notes";
|
|
27
|
+
readonly file: "notes.json";
|
|
22
28
|
}];
|
|
23
29
|
type DerivedKind = (typeof DERIVED_RECORDS)[number]['kind'];
|
|
24
30
|
/** What the machine reads from disk. Injected so a cycle is testable with no filesystem at all. */
|
|
@@ -75,6 +81,15 @@ export interface CloudSyncState {
|
|
|
75
81
|
* fills the gap the server declared and never overrides the server on a question it answered.
|
|
76
82
|
*/
|
|
77
83
|
sentRunIds?: string[];
|
|
84
|
+
/**
|
|
85
|
+
* The content hash of each run as the server last ACCEPTED it, for the runs still held locally.
|
|
86
|
+
*
|
|
87
|
+
* A run is not immutable: a session's live drive run is rewritten after every verdict under the
|
|
88
|
+
* same id. Sending by id alone delivered its first version and never the rest, so the server held
|
|
89
|
+
* a tab's first few verdicts and none after. The server upserts a run by id, so a changed run is
|
|
90
|
+
* simply sent again; this is how the machine tells a changed run from one it already delivered.
|
|
91
|
+
*/
|
|
92
|
+
sentRunHashes?: Record<string, string>;
|
|
78
93
|
/**
|
|
79
94
|
* Runs the server refused, by run id: its reason, and the two things that could change the answer
|
|
80
95
|
* (a hash of the run file, and of what the platform said it reads). A refused run is not offered
|
|
@@ -188,6 +203,9 @@ interface SyncDeps {
|
|
|
188
203
|
text: string;
|
|
189
204
|
}>;
|
|
190
205
|
}
|
|
206
|
+
/** The server's per-part answer: accepted count and rejection list, each read defensively. */
|
|
207
|
+
/** A 200 that never confirmed the runs it was sent. Kept queued, and said, rather than dropped. */
|
|
208
|
+
export declare const MSG_RUNS_UNCONFIRMED = "sync 200 without a runs result: the server did not confirm the runs it was sent, so they stay queued";
|
|
191
209
|
/**
|
|
192
210
|
* Run one cycle. Never throws: the local record is already safe on disk, and a sync that can take
|
|
193
211
|
* down the thing it is backing up is worse than no sync.
|
|
@@ -39,10 +39,10 @@ const SYNC_PULL_PATH = '/v1/sync/pull';
|
|
|
39
39
|
/**
|
|
40
40
|
* The derived records that ride along with runs, and the file each one lives in.
|
|
41
41
|
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
42
|
+
* One list, so adding a record is one line and cannot be half-done: the bundle, the hashing, the
|
|
43
|
+
* reporting and the disk source (`sync-disk.ts`) all read it.
|
|
44
44
|
*/
|
|
45
|
-
const DERIVED_RECORDS = [
|
|
45
|
+
export const DERIVED_RECORDS = [
|
|
46
46
|
{ kind: 'impact', file: ReticleDir.IMPACT_FILE },
|
|
47
47
|
{ kind: 'flake', file: ReticleDir.FLAKE_FILE },
|
|
48
48
|
{ kind: 'intent', file: ReticleDir.INTENT_FILE },
|
|
@@ -50,6 +50,10 @@ const DERIVED_RECORDS = [
|
|
|
50
50
|
// cannot tell a server a page that drifted from one that always behaved that way.
|
|
51
51
|
{ kind: 'envelopes', file: ReticleDir.ENVELOPES_FILE },
|
|
52
52
|
{ kind: 'assertion-tiers', file: ReticleDir.TIERS_FILE },
|
|
53
|
+
// Reticle Coverage: what the platform's Overview shows as controls proved and routes reached.
|
|
54
|
+
{ kind: 'coverage', file: ReticleDir.COVERAGE_FILE },
|
|
55
|
+
// The notes a human pinned in the HUD, pending and resolved: the review trail a team reads.
|
|
56
|
+
{ kind: 'notes', file: ReticleDir.NOTES_FILE },
|
|
53
57
|
];
|
|
54
58
|
const asJson = (text) => {
|
|
55
59
|
try {
|
|
@@ -75,6 +79,8 @@ const flowName = (flow, index) => {
|
|
|
75
79
|
const FLOW_VERSION_FIX = 'update the platform or remove `compare` from the flow';
|
|
76
80
|
const RUN_VERSION_FIX = 'update the platform';
|
|
77
81
|
/** The server's per-part answer: accepted count and rejection list, each read defensively. */
|
|
82
|
+
/** A 200 that never confirmed the runs it was sent. Kept queued, and said, rather than dropped. */
|
|
83
|
+
export const MSG_RUNS_UNCONFIRMED = 'sync 200 without a runs result: the server did not confirm the runs it was sent, so they stay queued';
|
|
78
84
|
function partResult(body, part) {
|
|
79
85
|
const raw = isRecord(body[part]) ? body[part] : {};
|
|
80
86
|
const accepted = 'number' === typeof raw['accepted'] ? raw['accepted'] : 0;
|
|
@@ -92,6 +98,16 @@ function partResult(body, part) {
|
|
|
92
98
|
* Split runs into requests bounded by count and by serialized size. A run bigger than the byte
|
|
93
99
|
* bound on its own still goes, alone: it cannot be split, and holding it back would hide it.
|
|
94
100
|
*/
|
|
101
|
+
/** The delivery record, kept only for the runs still on disk — the same bound as `sentRunIds`. */
|
|
102
|
+
function heldHashes(runs, recorded) {
|
|
103
|
+
const out = {};
|
|
104
|
+
for (const { runId } of runs) {
|
|
105
|
+
const hash = recorded[runId];
|
|
106
|
+
if (hash !== undefined)
|
|
107
|
+
out[runId] = hash;
|
|
108
|
+
}
|
|
109
|
+
return out;
|
|
110
|
+
}
|
|
95
111
|
function batchRuns(runs) {
|
|
96
112
|
const batches = [];
|
|
97
113
|
let current = [];
|
|
@@ -194,8 +210,21 @@ export async function runSyncCycle(deps) {
|
|
|
194
210
|
const priorRefusals = isRecord(deps.state.refusedRuns) ? deps.state.refusedRuns : {};
|
|
195
211
|
const refusedRuns = {};
|
|
196
212
|
const notRetried = [];
|
|
213
|
+
// Same rule as every record read from disk: only a string can equal a hash we computed.
|
|
214
|
+
const recorded = {};
|
|
215
|
+
if (isRecord(deps.state.sentRunHashes))
|
|
216
|
+
for (const [id, hash] of Object.entries(deps.state.sentRunHashes))
|
|
217
|
+
if ('string' === typeof hash)
|
|
218
|
+
recorded[id] = hash;
|
|
219
|
+
/*
|
|
220
|
+
* Changed means no record, or a record that differs. No record has to count: a machine upgraded
|
|
221
|
+
* onto this can hold runs the server has only in an older version, and a tab that has closed
|
|
222
|
+
* never changes again to earn a re-send. So each such run is
|
|
223
|
+
* sent once and recorded, a one-time cost bounded by what retention keeps.
|
|
224
|
+
*/
|
|
225
|
+
const delivered = (run) => known.has(run.runId) && recorded[run.runId] === hashPayload(run.payload);
|
|
197
226
|
const unsent = allRuns.filter((run) => {
|
|
198
|
-
if (
|
|
227
|
+
if (delivered(run))
|
|
199
228
|
return false;
|
|
200
229
|
const prior = priorRefusals[run.runId];
|
|
201
230
|
const unchanged = prior !== undefined &&
|
|
@@ -298,6 +327,13 @@ export async function runSyncCycle(deps) {
|
|
|
298
327
|
break;
|
|
299
328
|
}
|
|
300
329
|
const answer = isRecord(pushed.json) ? pushed.json : {};
|
|
330
|
+
// Runs were sent and the answer says nothing about them: a proxy's 200 page, an empty body.
|
|
331
|
+
// Delivered means the server said so. Recording them as sent here dropped them for good while
|
|
332
|
+
// the cycle reported ok; they stay queued, and the reason is the cycle's error.
|
|
333
|
+
if (batch.length > 0 && !isRecord(answer['runs'])) {
|
|
334
|
+
pushError = MSG_RUNS_UNCONFIRMED;
|
|
335
|
+
break;
|
|
336
|
+
}
|
|
301
337
|
const runs = partResult(answer, 'runs');
|
|
302
338
|
runsSent += runs.accepted;
|
|
303
339
|
// Accepted means the server has it. A rejected run was refused by index, so it is exactly as
|
|
@@ -305,8 +341,10 @@ export async function runSyncCycle(deps) {
|
|
|
305
341
|
const refusedHere = new Map(runs.rejected.map((r) => [r.index, r.reason]));
|
|
306
342
|
batch.forEach((run, index) => {
|
|
307
343
|
const reason = refusedHere.get(index);
|
|
308
|
-
if (reason === undefined)
|
|
344
|
+
if (reason === undefined) {
|
|
309
345
|
known.add(run.runId);
|
|
346
|
+
recorded[run.runId] = hashPayload(run.payload);
|
|
347
|
+
}
|
|
310
348
|
else
|
|
311
349
|
refusedRuns[run.runId] = { reason, payloadHash: hashPayload(run.payload), acceptsHash };
|
|
312
350
|
});
|
|
@@ -369,6 +407,7 @@ export async function runSyncCycle(deps) {
|
|
|
369
407
|
nextState.refusedSets = refusedSets;
|
|
370
408
|
if (pushError !== undefined) {
|
|
371
409
|
nextState.sentRunIds = allRuns.map((r) => r.runId).filter((id) => known.has(id));
|
|
410
|
+
nextState.sentRunHashes = heldHashes(allRuns, recorded);
|
|
372
411
|
deps.sink.writeState({ ...nextState, lastError: pushError });
|
|
373
412
|
return {
|
|
374
413
|
...empty,
|
|
@@ -389,6 +428,7 @@ export async function runSyncCycle(deps) {
|
|
|
389
428
|
* will never be attempted — so the record follows the artifacts and needs no number of its own.
|
|
390
429
|
*/
|
|
391
430
|
nextState.sentRunIds = allRuns.map((r) => r.runId).filter((id) => known.has(id));
|
|
431
|
+
nextState.sentRunHashes = heldHashes(allRuns, recorded);
|
|
392
432
|
// 3. COLLECT — always, even when there was nothing to send.
|
|
393
433
|
const query = nextState.cursor === undefined ? '' : `?since=${encodeURIComponent(nextState.cursor)}`;
|
|
394
434
|
const pull = await call(`${SYNC_PULL_PATH}${query}`);
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
* laptop on a train would otherwise fill the log with the same line four hundred times and teach
|
|
22
22
|
* everyone to ignore it.
|
|
23
23
|
*/
|
|
24
|
+
import { syncRequest } from './cloud-sync.js';
|
|
24
25
|
import { log } from '../../log.js';
|
|
25
26
|
import { emitSyncHook } from '../../hooks/hook-emit.js';
|
|
26
27
|
import { describeSync, runSyncCycle } from './sync-cycle.js';
|
|
@@ -69,10 +70,7 @@ const movedAnything = (report) => report.runsSent > 0 ||
|
|
|
69
70
|
report.capsulesSent > 0 ||
|
|
70
71
|
report.derivedSent.length > 0 ||
|
|
71
72
|
report.pulled > 0;
|
|
72
|
-
const defaultRequest =
|
|
73
|
-
const res = await fetch(url, init);
|
|
74
|
-
return { status: res.status, text: await res.text() };
|
|
75
|
-
};
|
|
73
|
+
const defaultRequest = (url, init) => syncRequest(url, init);
|
|
76
74
|
/**
|
|
77
75
|
* Start the loop. Safe to call for an UNLINKED project: it resolves the link on every tick and does
|
|
78
76
|
* nothing until one appears, which is what lets `reticle link` take effect without a restart.
|
|
@@ -147,7 +145,11 @@ export function startSyncDaemon(deps) {
|
|
|
147
145
|
continue;
|
|
148
146
|
try {
|
|
149
147
|
const cloud = await cloudFor(root);
|
|
150
|
-
|
|
148
|
+
// Linked HERE, by its own link file. An unlinked root resolves to the environment's key,
|
|
149
|
+
// which is the right answer for the root this daemon stands in and somebody else's project
|
|
150
|
+
// for every other one: a daemon started with RETICLE_API_KEY pushed every project this
|
|
151
|
+
// machine had seen into that one, a status and a pull per root on every cycle.
|
|
152
|
+
if (null === cloud.config || null === cloud.projectId)
|
|
151
153
|
continue;
|
|
152
154
|
const report = await pushRoot(root, cloud);
|
|
153
155
|
if (report === undefined)
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type CloudSyncState, type PulledIssues, type SyncSink, type SyncSource } from './sync-cycle.js';
|
|
2
2
|
/** What the cycle reads, backed by a real `.reticle` directory. */
|
|
3
3
|
export declare function diskSource(reticleRoot: string): SyncSource;
|
|
4
4
|
/** What the cycle writes back into `.reticle/`. */
|
|
@@ -10,9 +10,11 @@
|
|
|
10
10
|
* one unsynced record, not a crashed sync. There is no state here worth defending against a parse
|
|
11
11
|
* error — the file will be rewritten on the next tool call anyway.
|
|
12
12
|
*/
|
|
13
|
+
import { shareableRun } from '@reticlehq/core/artifacts';
|
|
13
14
|
import { existsSync, mkdirSync, readFileSync, readdirSync, renameSync, writeFileSync, } from 'node:fs';
|
|
14
15
|
import { dirname, join } from 'node:path';
|
|
15
16
|
import { IntentDir, ReticleDir } from '@reticlehq/core';
|
|
17
|
+
import { DERIVED_RECORDS, } from './sync-cycle.js';
|
|
16
18
|
import { subjectFor } from '../intent/intent-subject.js';
|
|
17
19
|
const JSON_SUFFIX = '.json';
|
|
18
20
|
/** Read + parse, or undefined for absent/unreadable/malformed. Never throws. */
|
|
@@ -79,13 +81,7 @@ function readFlows(root) {
|
|
|
79
81
|
return [];
|
|
80
82
|
}
|
|
81
83
|
}
|
|
82
|
-
const DERIVED_FILE =
|
|
83
|
-
impact: ReticleDir.IMPACT_FILE,
|
|
84
|
-
flake: ReticleDir.FLAKE_FILE,
|
|
85
|
-
intent: ReticleDir.INTENT_FILE,
|
|
86
|
-
envelopes: ReticleDir.ENVELOPES_FILE,
|
|
87
|
-
'assertion-tiers': ReticleDir.TIERS_FILE,
|
|
88
|
-
};
|
|
84
|
+
const DERIVED_FILE = Object.fromEntries(DERIVED_RECORDS.map((record) => [record.kind, record.file]));
|
|
89
85
|
/** The directory the sharded intent store writes into, beside the legacy flat file. */
|
|
90
86
|
const INTENT_SUBDIR = ReticleDir.INTENT_SUBDIR;
|
|
91
87
|
const INTENT_INDEX = IntentDir.INDEX_FILE;
|
|
@@ -170,7 +166,8 @@ export function diskSource(reticleRoot) {
|
|
|
170
166
|
runs: () => readJsonDir(join(reticleRoot, ReticleDir.RUNS_SUBDIR))
|
|
171
167
|
.map((payload) => {
|
|
172
168
|
const id = payload?.runId;
|
|
173
|
-
|
|
169
|
+
// The prompt context leaves the machine only when the project opted in.
|
|
170
|
+
return 'string' === typeof id ? { runId: id, payload: shareableRun(payload) } : undefined;
|
|
174
171
|
})
|
|
175
172
|
// A run artifact with no id cannot be diffed against the server's list, so sending it would
|
|
176
173
|
// mean re-sending it every cycle forever. Dropped rather than uploaded repeatedly.
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Repaint the HUD when sign-in or a project link changes on disk.
|
|
3
|
+
*
|
|
4
|
+
* The impact snapshot already reads both files fresh, but it is only PUSHED when a tab attaches,
|
|
5
|
+
* a tool call lands, or the HUD's own Sign in completes. `reticle logout` and `reticle link` run in
|
|
6
|
+
* another process, so neither caused a push: a signed-out machine kept showing the old name, and a
|
|
7
|
+
* freshly linked project kept saying "Not linked", until somebody reloaded or drove something.
|
|
8
|
+
*
|
|
9
|
+
* ponytail: stat polling (`fs.watchFile`), not `fs.watch`. It works on a file that does not exist
|
|
10
|
+
* yet — the usual state of `cloud.json` before a link — and costs one stat per file per interval.
|
|
11
|
+
*/
|
|
12
|
+
/** How often each watched file is stat'ed. A second reads as live to somebody switching windows. */
|
|
13
|
+
export declare const ACCOUNT_WATCH_INTERVAL_MS = 1000;
|
|
14
|
+
export interface AccountFilesWatch {
|
|
15
|
+
/** Start watching one file. Watching the same path twice is a no-op. */
|
|
16
|
+
watch(path: string): void;
|
|
17
|
+
close(): void;
|
|
18
|
+
}
|
|
19
|
+
export declare function watchAccountFiles(onChange: () => void, intervalMs?: number): AccountFilesWatch;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Repaint the HUD when sign-in or a project link changes on disk.
|
|
3
|
+
*
|
|
4
|
+
* The impact snapshot already reads both files fresh, but it is only PUSHED when a tab attaches,
|
|
5
|
+
* a tool call lands, or the HUD's own Sign in completes. `reticle logout` and `reticle link` run in
|
|
6
|
+
* another process, so neither caused a push: a signed-out machine kept showing the old name, and a
|
|
7
|
+
* freshly linked project kept saying "Not linked", until somebody reloaded or drove something.
|
|
8
|
+
*
|
|
9
|
+
* ponytail: stat polling (`fs.watchFile`), not `fs.watch`. It works on a file that does not exist
|
|
10
|
+
* yet — the usual state of `cloud.json` before a link — and costs one stat per file per interval.
|
|
11
|
+
*/
|
|
12
|
+
import { unwatchFile, watchFile } from 'node:fs';
|
|
13
|
+
/** How often each watched file is stat'ed. A second reads as live to somebody switching windows. */
|
|
14
|
+
export const ACCOUNT_WATCH_INTERVAL_MS = 1_000;
|
|
15
|
+
export function watchAccountFiles(onChange, intervalMs = ACCOUNT_WATCH_INTERVAL_MS) {
|
|
16
|
+
const watched = new Set();
|
|
17
|
+
const listener = () => {
|
|
18
|
+
onChange();
|
|
19
|
+
};
|
|
20
|
+
return {
|
|
21
|
+
watch(path) {
|
|
22
|
+
if (watched.has(path))
|
|
23
|
+
return;
|
|
24
|
+
watched.add(path);
|
|
25
|
+
// Not persistent: a watcher must never be the reason a daemon cannot exit.
|
|
26
|
+
watchFile(path, { persistent: false, interval: intervalMs }, listener);
|
|
27
|
+
},
|
|
28
|
+
close() {
|
|
29
|
+
for (const path of watched)
|
|
30
|
+
unwatchFile(path, listener);
|
|
31
|
+
watched.clear();
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
}
|
|
@@ -16,9 +16,17 @@ export declare function initImpact(opts: {
|
|
|
16
16
|
* directory may not reach for it. The daemon owns both sides.
|
|
17
17
|
*/
|
|
18
18
|
config?: ConfigSource;
|
|
19
|
+
/** Resolve credentials per project when one daemon serves multiple linked apps. */
|
|
20
|
+
configForRoot?: (root: string) => ConfigSource;
|
|
21
|
+
/** Reticle Coverage for a project root, supplied by the daemon (the ledger is a feature's). */
|
|
22
|
+
coverageForRoot?: (root: string) => () => Record<string, number> | undefined;
|
|
19
23
|
reticleRoot: string | undefined;
|
|
20
24
|
projectName?: string;
|
|
21
25
|
now?: () => number;
|
|
26
|
+
/** Where the machine-wide `~/.reticle` lives. Tests pass a temp dir so they never write the real one. */
|
|
27
|
+
globalRoot?: string;
|
|
28
|
+
/** This build's version, for HUD notices that need a newer SDK. */
|
|
29
|
+
sdkVersion?: string;
|
|
22
30
|
}): ImpactStore | undefined;
|
|
23
31
|
/** The live store for a root (default when omitted), if one has been initialised. */
|
|
24
32
|
export declare function impactStore(root?: string): ImpactStore | undefined;
|
|
@@ -31,7 +31,13 @@ function storeFor(root) {
|
|
|
31
31
|
return undefined;
|
|
32
32
|
let found = stores.get(key);
|
|
33
33
|
if (found === undefined) {
|
|
34
|
-
|
|
34
|
+
const { configForRoot, coverageForRoot, ...opts } = storeOpts;
|
|
35
|
+
found = new ImpactStore({
|
|
36
|
+
...opts,
|
|
37
|
+
...(configForRoot === undefined ? {} : { config: configForRoot(key) }),
|
|
38
|
+
...(coverageForRoot === undefined ? {} : { coverage: coverageForRoot(key) }),
|
|
39
|
+
reticleRoot: key,
|
|
40
|
+
});
|
|
35
41
|
stores.set(key, found);
|
|
36
42
|
}
|
|
37
43
|
return found;
|
|
@@ -50,11 +56,15 @@ export function initImpact(opts) {
|
|
|
50
56
|
return storeFor(undefined);
|
|
51
57
|
if (defaultRoot === undefined) {
|
|
52
58
|
defaultRoot = opts.reticleRoot;
|
|
53
|
-
const { projectName, now, config } = opts;
|
|
59
|
+
const { projectName, now, config, configForRoot, coverageForRoot, globalRoot, sdkVersion } = opts;
|
|
54
60
|
storeOpts = {
|
|
55
61
|
...(projectName === undefined ? {} : { projectName }),
|
|
56
62
|
...(now === undefined ? {} : { now }),
|
|
63
|
+
...(globalRoot === undefined ? {} : { globalRoot }),
|
|
64
|
+
...(sdkVersion === undefined ? {} : { sdkVersion }),
|
|
57
65
|
...(config === undefined ? {} : { config }),
|
|
66
|
+
...(configForRoot === undefined ? {} : { configForRoot }),
|
|
67
|
+
...(coverageForRoot === undefined ? {} : { coverageForRoot }),
|
|
58
68
|
};
|
|
59
69
|
}
|
|
60
70
|
return storeFor(opts.reticleRoot);
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type OfferSource } from '../cloud/harness-offer.js';
|
|
2
2
|
import type { ConfigSource } from '../cloud/harness-config.js';
|
|
3
|
+
import { type NoticesSource } from '../cloud/hud-notices-source.js';
|
|
3
4
|
import { type ImpactCounts, type ImpactDefect, type AccountState, type ImpactScope, type ImpactSnapshot } from '@reticlehq/core';
|
|
4
5
|
/** Read a scope, tolerating absence and corruption: a broken file starts over rather than throwing. */
|
|
5
6
|
export declare function readScope(path: string, now: number): ImpactScope;
|
|
@@ -23,6 +24,15 @@ export interface ImpactFoldMeta {
|
|
|
23
24
|
}
|
|
24
25
|
/** Fold one delta into a scope: totals, today's bucket, records and streak. Pure. */
|
|
25
26
|
export declare function applyDelta(scope: ImpactScope, delta: Partial<ImpactCounts>, now: number, meta?: ImpactFoldMeta): ImpactScope;
|
|
27
|
+
/**
|
|
28
|
+
* All time, never smaller than the project being looked at.
|
|
29
|
+
*
|
|
30
|
+
* The two scopes live in different files: the project's in its `.reticle`, the machine's in the
|
|
31
|
+
* home directory. A project whose history began before this machine's ledger (an upgrade, a new
|
|
32
|
+
* laptop, a moved checkout) showed more under "This project" than under "All time", which no reader
|
|
33
|
+
* can believe. The machine has done at least what any one of its projects has.
|
|
34
|
+
*/
|
|
35
|
+
export declare function atLeastProject(global: ImpactScope, project: ImpactScope): ImpactScope;
|
|
26
36
|
/**
|
|
27
37
|
* The live impact record for one project.
|
|
28
38
|
*
|
|
@@ -42,6 +52,12 @@ export declare class ImpactStore {
|
|
|
42
52
|
/** Where the workspace stands with the free harness offer. Injected so no test touches a network. */
|
|
43
53
|
offer?: OfferSource;
|
|
44
54
|
config?: ConfigSource;
|
|
55
|
+
/** The HUD rail's notices. Defaults to the daemon's cached copy of the published file. */
|
|
56
|
+
notices?: NoticesSource;
|
|
57
|
+
/** This build's version, for notices that need a newer SDK. Passed in: the store reads no package. */
|
|
58
|
+
sdkVersion?: string;
|
|
59
|
+
/** Reticle Coverage per level, as percentages. Injected: the ledger lives in `features/exhaust`. */
|
|
60
|
+
coverage?: () => Record<string, number> | undefined;
|
|
45
61
|
});
|
|
46
62
|
/** Notified after every fold, so the HUD can be pushed a fresh snapshot. */
|
|
47
63
|
onChange(fn: () => void): void;
|
|
@@ -1,23 +1,25 @@
|
|
|
1
1
|
import { mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { describeSync, overallStatus, readSyncSummary } from '../project/sync-status.js';
|
|
2
3
|
import { dirname, join } from 'node:path';
|
|
3
4
|
import { homedir } from 'node:os';
|
|
4
5
|
import { readAccountState } from '../cloud/account-state.js';
|
|
5
6
|
import { harnessOfferSource } from '../cloud/harness-offer.js';
|
|
7
|
+
import { hudNoticesSource } from '../cloud/hud-notices-source.js';
|
|
8
|
+
import { selectNotices } from '@reticlehq/core/hud';
|
|
6
9
|
import { ReticleDir, IMPACT_DAILY_BUCKETS, IMPACT_DEFECT_LIMIT, IMPACT_SCHEMA_VERSION, ImpactScopeSchema, addImpactCounts, emptyImpactCounts, emptyImpactRecords, estimateImpactSavings, } from '@reticlehq/core';
|
|
7
10
|
/**
|
|
8
11
|
* Where Reticle keeps the record of what it has done for you.
|
|
9
12
|
*
|
|
10
|
-
* Two scopes, two files
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
+
* Two scopes, two files: the project's own `.reticle/impact.json`, and a machine-wide
|
|
14
|
+
* `~/.reticle/impact.json` that answers "what has Reticle done for me overall" across every app you
|
|
15
|
+
* have instrumented.
|
|
13
16
|
*
|
|
14
|
-
* This is NOT telemetry. Telemetry answers our questions about the product
|
|
15
|
-
*
|
|
17
|
+
* This is NOT telemetry. Telemetry answers our questions about the product; this answers the user's
|
|
18
|
+
* question about their own work. A linked project's record syncs to that user's own dashboard (the
|
|
19
|
+
* sync cycle's `impact` kind); the machine-wide record never leaves the machine.
|
|
16
20
|
*/
|
|
17
21
|
/** Writes are debounced: a verification loop is 50-200 calls, and each one is a counter bump. */
|
|
18
22
|
const WRITE_DEBOUNCE_MS = 800;
|
|
19
|
-
/** The project's cloud binding, written by `reticle link`. Absent for an unlinked project. */
|
|
20
|
-
const CLOUD_LINK_FILE = 'cloud.json';
|
|
21
23
|
/**
|
|
22
24
|
* `reticleRoot` is the project's own `.reticle` directory (what every other store here is handed);
|
|
23
25
|
* the global scope lives beside the daemon's own state in `~/.reticle`.
|
|
@@ -42,7 +44,7 @@ function impactPaths(reticleRoot, globalRoot) {
|
|
|
42
44
|
*/
|
|
43
45
|
function readDashboardUrl(reticleRoot) {
|
|
44
46
|
try {
|
|
45
|
-
const raw = JSON.parse(readFileSync(join(reticleRoot, CLOUD_LINK_FILE), 'utf8'));
|
|
47
|
+
const raw = JSON.parse(readFileSync(join(reticleRoot, ReticleDir.CLOUD_LINK_FILE), 'utf8'));
|
|
46
48
|
if ('object' !== typeof raw || null === raw)
|
|
47
49
|
return undefined;
|
|
48
50
|
const value = raw['dashboardUrl'];
|
|
@@ -92,6 +94,12 @@ function writeScope(path, scope) {
|
|
|
92
94
|
// memory and the next write attempt carries them.
|
|
93
95
|
}
|
|
94
96
|
}
|
|
97
|
+
/** The daemon's cached copy of the HUD notices file, beside the machine-wide impact record. */
|
|
98
|
+
const HUD_NOTICES_CACHE_FILE = 'hud-notices.json';
|
|
99
|
+
/** How long a sync status is reused before it is read again. */
|
|
100
|
+
const SYNC_STATUS_EVERY_MS = 10_000;
|
|
101
|
+
/** When the caller did not say which build this is: matches only notices with no `minSdk`. */
|
|
102
|
+
const UNKNOWN_SDK_VERSION = '0.0.0';
|
|
95
103
|
/** YYYY-MM-DD in local time - the day boundary a person recognises, not UTC's. */
|
|
96
104
|
export function isoDay(now) {
|
|
97
105
|
const d = new Date(now);
|
|
@@ -105,33 +113,55 @@ function isNextDay(a, b) {
|
|
|
105
113
|
const next = new Date(`${b}T00:00:00`);
|
|
106
114
|
return 1 === Math.round((next.getTime() - prev.getTime()) / 86_400_000);
|
|
107
115
|
}
|
|
116
|
+
/**
|
|
117
|
+
* Consecutive days ending at the newest recorded day, read from the days themselves.
|
|
118
|
+
*
|
|
119
|
+
* It was a counter bumped when a fold opened a new day and compared only with the bucket before it,
|
|
120
|
+
* so one stray day in between (a late flush from another daemon, a writer with a frozen clock) reset
|
|
121
|
+
* it to 1. Counting back from the newest date means no out-of-order write can shorten it.
|
|
122
|
+
*/
|
|
123
|
+
function streakEndingAtLatest(sortedDates) {
|
|
124
|
+
let streak = 0;
|
|
125
|
+
let later;
|
|
126
|
+
for (let i = sortedDates.length - 1; i >= 0; i -= 1) {
|
|
127
|
+
const date = sortedDates[i];
|
|
128
|
+
if (date === undefined)
|
|
129
|
+
break;
|
|
130
|
+
if (later !== undefined && !isNextDay(date, later))
|
|
131
|
+
break;
|
|
132
|
+
streak += 1;
|
|
133
|
+
later = date;
|
|
134
|
+
}
|
|
135
|
+
return streak;
|
|
136
|
+
}
|
|
108
137
|
/** Fold one delta into a scope: totals, today's bucket, records and streak. Pure. */
|
|
109
138
|
export function applyDelta(scope, delta, now, meta = {}) {
|
|
110
139
|
const today = isoDay(now);
|
|
111
140
|
const counts = addImpactCounts(scope.counts, delta);
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
days.push({ date: today, counts: addImpactCounts(emptyImpactCounts(), delta) });
|
|
141
|
+
// One bucket per date, wherever the delta's date falls. The machine-wide record has many writers
|
|
142
|
+
// and a late flush carries an older date, so "the last bucket is today" is not something a fold
|
|
143
|
+
// may assume — appending on that assumption duplicated dates and put them out of order.
|
|
144
|
+
const byDate = new Map();
|
|
145
|
+
for (const day of scope.days) {
|
|
146
|
+
byDate.set(day.date, addImpactCounts(byDate.get(day.date) ?? emptyImpactCounts(), day.counts));
|
|
119
147
|
}
|
|
148
|
+
const todayCounts = addImpactCounts(byDate.get(today) ?? emptyImpactCounts(), delta);
|
|
149
|
+
byDate.set(today, todayCounts);
|
|
150
|
+
const dates = [...byDate.keys()].sort();
|
|
151
|
+
const days = dates.map((date) => ({ date, counts: byDate.get(date) ?? emptyImpactCounts() }));
|
|
120
152
|
while (days.length > IMPACT_DAILY_BUCKETS)
|
|
121
153
|
days.shift();
|
|
122
|
-
const todayCounts = days[days.length - 1]?.counts ?? emptyImpactCounts();
|
|
123
|
-
const previousDay = days[days.length - 2];
|
|
124
154
|
const records = { ...scope.records };
|
|
125
155
|
records.bestVerdictDay = Math.max(records.bestVerdictDay, todayCounts.verdicts);
|
|
126
156
|
records.bestDefectDay = Math.max(records.bestDefectDay, todayCounts.failed);
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
records.streakDays
|
|
134
|
-
|
|
157
|
+
// The window keeps IMPACT_DAILY_BUCKETS days, so a streak longer than it cannot be read off the
|
|
158
|
+
// dates alone: when the whole window is one unbroken run, it continues the streak already held.
|
|
159
|
+
const inWindow = streakEndingAtLatest(dates);
|
|
160
|
+
const unbroken = inWindow === dates.length && scope.days.length >= IMPACT_DAILY_BUCKETS;
|
|
161
|
+
const newDay = !scope.days.some((day) => day.date === today);
|
|
162
|
+
records.streakDays = unbroken
|
|
163
|
+
? Math.max(inWindow, scope.records.streakDays + (newDay ? 1 : 0))
|
|
164
|
+
: inWindow;
|
|
135
165
|
records.bestStreakDays = Math.max(records.bestStreakDays, records.streakDays);
|
|
136
166
|
records.longestRunMs = Math.max(records.longestRunMs, meta.runMs ?? 0);
|
|
137
167
|
/*
|
|
@@ -150,6 +180,41 @@ export function applyDelta(scope, delta, now, meta = {}) {
|
|
|
150
180
|
defects,
|
|
151
181
|
};
|
|
152
182
|
}
|
|
183
|
+
/** The larger of two counters, key by key. */
|
|
184
|
+
function maxEach(a, b) {
|
|
185
|
+
const out = { ...a };
|
|
186
|
+
for (const key of Object.keys(b))
|
|
187
|
+
out[key] = Math.max(a[key] ?? 0, b[key] ?? 0);
|
|
188
|
+
return out;
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* All time, never smaller than the project being looked at.
|
|
192
|
+
*
|
|
193
|
+
* The two scopes live in different files: the project's in its `.reticle`, the machine's in the
|
|
194
|
+
* home directory. A project whose history began before this machine's ledger (an upgrade, a new
|
|
195
|
+
* laptop, a moved checkout) showed more under "This project" than under "All time", which no reader
|
|
196
|
+
* can believe. The machine has done at least what any one of its projects has.
|
|
197
|
+
*/
|
|
198
|
+
export function atLeastProject(global, project) {
|
|
199
|
+
// The earlier of the two starts; zero is "never recorded", not the epoch.
|
|
200
|
+
const starts = [global.since, project.since].filter((t) => 0 < t);
|
|
201
|
+
return {
|
|
202
|
+
...global,
|
|
203
|
+
counts: maxEach(global.counts, project.counts),
|
|
204
|
+
records: maxEach(global.records, project.records),
|
|
205
|
+
savings: {
|
|
206
|
+
tokens: {
|
|
207
|
+
...global.savings.tokens,
|
|
208
|
+
value: Math.max(global.savings.tokens.value, project.savings.tokens.value),
|
|
209
|
+
},
|
|
210
|
+
minutes: {
|
|
211
|
+
...global.savings.minutes,
|
|
212
|
+
value: Math.max(global.savings.minutes.value, project.savings.minutes.value),
|
|
213
|
+
},
|
|
214
|
+
},
|
|
215
|
+
since: 0 === starts.length ? 0 : Math.min(...starts),
|
|
216
|
+
};
|
|
217
|
+
}
|
|
153
218
|
/**
|
|
154
219
|
* The live impact record for one project.
|
|
155
220
|
*
|
|
@@ -158,6 +223,9 @@ export function applyDelta(scope, delta, now, meta = {}) {
|
|
|
158
223
|
*/
|
|
159
224
|
export class ImpactStore {
|
|
160
225
|
#paths;
|
|
226
|
+
#root;
|
|
227
|
+
/** The sync status, re-read at most every SYNC_STATUS_EVERY_MS: it reads every run file. */
|
|
228
|
+
#sync;
|
|
161
229
|
#now;
|
|
162
230
|
#projectName;
|
|
163
231
|
#dashboardUrl;
|
|
@@ -178,7 +246,11 @@ export class ImpactStore {
|
|
|
178
246
|
/** Where this workspace stands with the free harness offer. Cached; see harness-offer.ts. */
|
|
179
247
|
#offer;
|
|
180
248
|
#config;
|
|
249
|
+
#notices;
|
|
250
|
+
#sdkVersion;
|
|
251
|
+
#coverage;
|
|
181
252
|
constructor(opts) {
|
|
253
|
+
this.#coverage = opts.coverage;
|
|
182
254
|
this.#paths = impactPaths(opts.reticleRoot, opts.globalRoot ?? homedir());
|
|
183
255
|
// Resolved per snapshot, not cached: a user who runs `reticle login` in another terminal must
|
|
184
256
|
// see the HUD change without restarting the daemon that is watching their app.
|
|
@@ -186,9 +258,16 @@ export class ImpactStore {
|
|
|
186
258
|
this.#now = opts.now ?? (() => Date.now());
|
|
187
259
|
this.#projectName = opts.projectName;
|
|
188
260
|
this.#dashboardUrl = readDashboardUrl(opts.reticleRoot);
|
|
261
|
+
this.#root = opts.reticleRoot;
|
|
189
262
|
// The claim happens in the console, so the link this project was linked to IS the claim link —
|
|
190
263
|
// built here rather than in the HUD, which has no way to know where this project points.
|
|
191
264
|
this.#offer = opts.offer ?? harnessOfferSource(process.env, () => this.#dashboardUrl);
|
|
265
|
+
this.#notices =
|
|
266
|
+
opts.notices ??
|
|
267
|
+
hudNoticesSource({
|
|
268
|
+
cacheFile: join(opts.globalRoot ?? homedir(), ReticleDir.ROOT, HUD_NOTICES_CACHE_FILE),
|
|
269
|
+
});
|
|
270
|
+
this.#sdkVersion = opts.sdkVersion ?? UNKNOWN_SDK_VERSION;
|
|
192
271
|
// No default: the loader lives in `features/harness` and this file may not reach for it.
|
|
193
272
|
// A store built without one simply reports no harness config, which the HUD renders as no row.
|
|
194
273
|
this.#config = opts.config ?? { read: () => undefined };
|
|
@@ -196,6 +275,23 @@ export class ImpactStore {
|
|
|
196
275
|
this.#project = readScope(this.#paths.project, now);
|
|
197
276
|
this.#global = readScope(this.#paths.global, now);
|
|
198
277
|
}
|
|
278
|
+
#syncStatus() {
|
|
279
|
+
const now = this.#now();
|
|
280
|
+
if (this.#sync !== undefined && now - this.#sync.at < SYNC_STATUS_EVERY_MS)
|
|
281
|
+
return this.#sync.value;
|
|
282
|
+
const summary = readSyncSummary(this.#root, true);
|
|
283
|
+
const value = {
|
|
284
|
+
status: overallStatus(summary),
|
|
285
|
+
runs: summary.runs,
|
|
286
|
+
onPlatform: summary.onPlatform,
|
|
287
|
+
pending: summary.pending,
|
|
288
|
+
refused: summary.refused.length + summary.refusedMore,
|
|
289
|
+
...(summary.lastPushAt === undefined ? {} : { lastPushAt: summary.lastPushAt }),
|
|
290
|
+
said: describeSync(summary, now),
|
|
291
|
+
};
|
|
292
|
+
this.#sync = { at: now, value };
|
|
293
|
+
return value;
|
|
294
|
+
}
|
|
199
295
|
/** Notified after every fold, so the HUD can be pushed a fresh snapshot. */
|
|
200
296
|
onChange(fn) {
|
|
201
297
|
this.#onChange = fn;
|
|
@@ -214,10 +310,13 @@ export class ImpactStore {
|
|
|
214
310
|
const snap = {
|
|
215
311
|
schemaVersion: IMPACT_SCHEMA_VERSION,
|
|
216
312
|
project: this.#project,
|
|
217
|
-
global: this.#global,
|
|
313
|
+
global: atLeastProject(this.#global, this.#project),
|
|
218
314
|
};
|
|
219
315
|
if (this.#projectName !== undefined)
|
|
220
316
|
snap.projectName = this.#projectName;
|
|
317
|
+
// Re-read, like the account: `reticle link` runs in another terminal while this store lives as
|
|
318
|
+
// long as the daemon, so a construction-time read said "Not linked" until a restart.
|
|
319
|
+
this.#dashboardUrl = readDashboardUrl(this.#root);
|
|
221
320
|
if (this.#dashboardUrl !== undefined)
|
|
222
321
|
snap.dashboardUrl = this.#dashboardUrl;
|
|
223
322
|
snap.account = this.#account();
|
|
@@ -236,6 +335,23 @@ export class ImpactStore {
|
|
|
236
335
|
const cfg = this.#config.read();
|
|
237
336
|
if (cfg !== undefined)
|
|
238
337
|
snap.harnessConfig = cfg;
|
|
338
|
+
// Chosen here, where the account and the entitlement are both known. Absent when nothing applies,
|
|
339
|
+
// so the HUD falls back to the slides bundled with the SDK.
|
|
340
|
+
const notices = selectNotices(this.#notices.read(), {
|
|
341
|
+
signedIn: snap.account?.signedIn,
|
|
342
|
+
entitled: cfg?.harnessEntitled,
|
|
343
|
+
sdkVersion: this.#sdkVersion,
|
|
344
|
+
now: this.#now(),
|
|
345
|
+
});
|
|
346
|
+
if (0 < notices.length)
|
|
347
|
+
snap.notices = notices;
|
|
348
|
+
const coverage = this.#coverage?.();
|
|
349
|
+
if (coverage !== undefined)
|
|
350
|
+
snap.coverage = coverage;
|
|
351
|
+
// Only for a linked project: an unlinked one has nothing on the platform, and the HUD already
|
|
352
|
+
// offers the way to link it.
|
|
353
|
+
if (this.#dashboardUrl !== undefined)
|
|
354
|
+
snap.sync = this.#syncStatus();
|
|
239
355
|
return snap;
|
|
240
356
|
}
|
|
241
357
|
/** Write now (process exit, tests). */
|