@reticlehq/server 3.5.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/cli-flow-commands.js +4 -8
- package/dist/command/cli/cli-parse.d.ts +1 -1
- package/dist/command/cli/cli-parse.js +10 -10
- 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 +51 -12
- package/dist/command/cli/cloud-login.d.ts +8 -0
- package/dist/command/cli/cloud-login.js +36 -2
- 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 +6 -4
- 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 +28 -22
- 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 +8 -0
- package/dist/command/setup/bringup/dev-server-wait.js +19 -0
- 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 +18 -1
- package/dist/command/setup/node-effects.js +2 -1
- package/dist/command/setup/remaining-steps.js +2 -1
- package/dist/command/setup/run-setup.d.ts +5 -0
- package/dist/command/setup/run-setup.js +35 -27
- package/dist/command/setup/setup-command.d.ts +2 -1
- package/dist/command/setup/setup-command.js +2 -0
- 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 -73
- package/dist/features/harness/fill-values.js +3 -169
- 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 +76 -82
- 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 +12 -3
- package/dist/memory/cloud/sync-cycle.js +16 -3
- 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 +80 -2
- package/dist/memory/journal/envelope.d.ts +8 -8
- 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 +26 -1
- package/dist/memory/project/artifact-root-resolver.js +94 -13
- 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 +3 -1
- package/dist/portal/bridge/bridge.js +19 -2
- 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 +3 -3
- 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 +7 -2
- package/dist/portal/session/session.js +21 -18
- 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.js +11 -1
- 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 +27 -13
- 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 +23 -7
- 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/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 +37 -4
- 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 -206
- 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 +52 -1
- 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 +15 -1
- package/dist/surface/tools/tool-kit.js +4 -6
- package/dist/surface/tools/tools.js +20 -48
- package/dist/telemetry/hud-metrics.d.ts +2 -0
- package/dist/telemetry/hud-metrics.js +17 -0
- 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/agent-cheatsheet.md +1 -1
- package/docs/autodrive.md +29 -25
- 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 +1 -1
- package/docs/desktop.mdx +5 -1
- package/docs/environment.mdx +3 -7
- package/docs/getting-started.md +13 -11
- package/docs/human-control.md +1 -2
- package/docs/index.mdx +4 -3
- package/docs/install-manual.mdx +2 -2
- package/docs/packages/tauri.mdx +2 -2
- package/docs/packages/vite-plugin.mdx +22 -1
- package/docs/packages.mdx +1 -1
- package/docs/quickstart.mdx +17 -7
- package/docs/telemetry.md +1 -1
- package/docs/tools/session-and-feedback.mdx +9 -9
- package/docs/usage.md +5 -2
- 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 -58
- package/dist/features/harness/openai-driver.js +0 -206
- 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,4 +1,4 @@
|
|
|
1
|
-
import { join } from 'node:path';
|
|
1
|
+
import { isAbsolute, join, relative, sep } from 'node:path';
|
|
2
2
|
import { SAFE_SEGMENT_PATTERN, ReticleDir, fnv1a, projectCandidates, } from '@reticlehq/core';
|
|
3
3
|
/**
|
|
4
4
|
* Which `.reticle/` a session's artifacts belong in.
|
|
@@ -51,6 +51,10 @@ export function resolveArtifactRoot(query) {
|
|
|
51
51
|
return { root: daemonRoot, reason: ArtifactRootReason.NO_MATCH };
|
|
52
52
|
}
|
|
53
53
|
if (matches.length > 1) {
|
|
54
|
+
const served = checkoutContaining(matches, query.servingDirectory);
|
|
55
|
+
if (served !== undefined) {
|
|
56
|
+
return { root: join(served, ReticleDir.ROOT), reason: ArtifactRootReason.MATCHED_PROJECT };
|
|
57
|
+
}
|
|
54
58
|
return {
|
|
55
59
|
root: daemonRoot,
|
|
56
60
|
reason: ArtifactRootReason.AMBIGUOUS,
|
|
@@ -66,6 +70,24 @@ export function resolveArtifactRoot(query) {
|
|
|
66
70
|
}
|
|
67
71
|
return { root: join(directory, ReticleDir.ROOT), reason: ArtifactRootReason.MATCHED_PROJECT };
|
|
68
72
|
}
|
|
73
|
+
/**
|
|
74
|
+
* The checkout the serving directory sits in, or undefined when it is in none of them.
|
|
75
|
+
*
|
|
76
|
+
* Deepest wins, because a worktree can live INSIDE the primary checkout (`.claude/worktrees/x`) and
|
|
77
|
+
* then sits under both; the deeper one is the one the server actually runs in. `relative` rather
|
|
78
|
+
* than a string prefix, so `/wt/pr-77` is not read as inside `/wt/pr-7`, and on every platform.
|
|
79
|
+
*/
|
|
80
|
+
function checkoutContaining(matches, servingDirectory) {
|
|
81
|
+
if (servingDirectory === undefined || 0 === servingDirectory.length)
|
|
82
|
+
return undefined;
|
|
83
|
+
const inside = matches
|
|
84
|
+
.map((candidate) => candidate.directory)
|
|
85
|
+
.filter((directory) => {
|
|
86
|
+
const rel = relative(directory, servingDirectory);
|
|
87
|
+
return '' === rel || ('..' !== rel && !rel.startsWith(`..${sep}`) && !isAbsolute(rel));
|
|
88
|
+
});
|
|
89
|
+
return inside.sort((a, b) => b.length - a.length)[0];
|
|
90
|
+
}
|
|
69
91
|
/**
|
|
70
92
|
* One entry per directory.
|
|
71
93
|
*
|
|
@@ -92,11 +114,21 @@ function dedupeByDirectory(matches) {
|
|
|
92
114
|
* is a cache of something `init` saw once. Order only decides which duplicate survives dedupe, and
|
|
93
115
|
* the one confirmed a moment ago is the better survivor.
|
|
94
116
|
*/
|
|
95
|
-
export function projectCandidatesFrom(discovery, registry) {
|
|
117
|
+
export function projectCandidatesFrom(discovery, registry, devServers = []) {
|
|
96
118
|
const discovered = discovery.found.flatMap((config) => config.projectId === undefined || 0 === config.projectId.length
|
|
97
119
|
? []
|
|
98
120
|
: [{ projectId: config.projectId, directory: config.directory }]);
|
|
99
|
-
|
|
121
|
+
const known = [...discovered, ...projectCandidates(registry)];
|
|
122
|
+
// A dev server's own announcement of its project id and root, ONLY for an id nothing above names.
|
|
123
|
+
// It is the one source that knows an app wired by the build plugin alone (no `.reticle.json`, never
|
|
124
|
+
// `init`-ed), whose runs otherwise went to `unmatched/`, which nothing syncs. It fills gaps and never
|
|
125
|
+
// competes: `init` at a monorepo root names the root while the dev server names the app's subfolder,
|
|
126
|
+
// and counting both refused a project that resolved fine before.
|
|
127
|
+
const named = new Set(known.map((candidate) => candidate.projectId));
|
|
128
|
+
const announced = devServers.flatMap((server) => server.projectId === undefined || 0 === server.projectId.length || named.has(server.projectId)
|
|
129
|
+
? []
|
|
130
|
+
: [{ projectId: server.projectId, directory: server.root }]);
|
|
131
|
+
return [...known, ...announced];
|
|
100
132
|
}
|
|
101
133
|
/** Where evidence goes when no project could be named and the daemon is a guest in this tree. */
|
|
102
134
|
export const UNMATCHED_SUBDIR = 'unmatched';
|
|
@@ -132,23 +164,70 @@ const ORIGIN_BUCKET_PREFIX = 'origin-';
|
|
|
132
164
|
* Two cases, and only one of them was ever the intended behaviour:
|
|
133
165
|
*
|
|
134
166
|
* - the daemon is sitting IN a Reticle project (a developer who ran `reticle serve` in their own
|
|
135
|
-
* app). Its root is the right answer, and it is
|
|
136
|
-
*
|
|
137
|
-
*
|
|
167
|
+
* app), and the session is asking about THAT project. Its root is the right answer, and it is
|
|
168
|
+
* the case the old fallback was written for.
|
|
169
|
+
* - otherwise — a guest in the tree, or a daemon in its own checkout serving a DIFFERENT project.
|
|
170
|
+
* Then the evidence goes to the user's own `~/.reticle/unmatched/<projectId>`, which is
|
|
171
|
+
* Reticle's to write.
|
|
138
172
|
*
|
|
139
173
|
* It is never dropped. A verdict with nowhere to live is a worse failure than one in an unexpected
|
|
140
174
|
* place, and the caller says out loud where it went.
|
|
141
175
|
*
|
|
176
|
+
* ## The second case in that list used to be missing, and it is not an edge case
|
|
177
|
+
*
|
|
178
|
+
* `daemonIsProject` answers "is the daemon sitting in SOME Reticle project". The guard below read it
|
|
179
|
+
* as "this session's project", and those are different questions whenever the two ids differ. Run
|
|
180
|
+
* the daemon in repo A, then connect an app from a git worktree B of that same repo: B has its own
|
|
181
|
+
* `projectId` in its own `.reticle.json`, and discovery cannot see it — it walks out from A's
|
|
182
|
+
* `process.cwd()` and never crosses into a sibling checkout — so this function is reached with a
|
|
183
|
+
* NAMED project and the daemon's root came back anyway. B's intents, flows and runs were written
|
|
184
|
+
* into A's checkout, and the returned path said A, so nothing looked wrong.
|
|
185
|
+
*
|
|
186
|
+
* A real identity always wins. The daemon's root is only the answer when it is that identity's own
|
|
187
|
+
* root, or when no identity was named at all.
|
|
188
|
+
*
|
|
142
189
|
* `projectId` arrives in HELLO from the page, so it is untrusted input on a path join and is held
|
|
143
|
-
* to one safe segment — the same guard session ids and flow names already pass.
|
|
190
|
+
* to one safe segment — the same guard session ids and flow names already pass. `daemonProjectId`
|
|
191
|
+
* is read from the daemon's own `.reticle.json` and gets the same treatment, because equality
|
|
192
|
+
* against a guarded value is only as strong as the weaker side.
|
|
144
193
|
*/
|
|
145
194
|
export function unmatchedRoot(query) {
|
|
146
|
-
if (query.daemonIsProject)
|
|
195
|
+
if (query.daemonIsProject && isTheDaemonsOwnProject(query.daemonProjectId, query.projectId))
|
|
147
196
|
return query.daemonRoot;
|
|
148
197
|
const id = query.projectId ?? '';
|
|
149
198
|
const named = SAFE_SEGMENT_PATTERN.test(id) && !id.includes('..');
|
|
150
199
|
return join(query.home, ReticleDir.ROOT, UNMATCHED_SUBDIR, named ? id : unnamedSegment(query.origin));
|
|
151
200
|
}
|
|
201
|
+
/**
|
|
202
|
+
* May the daemon write this session's artifacts into its own `.reticle/`?
|
|
203
|
+
*
|
|
204
|
+
* Two ways to qualify, and the difference between them is the whole bug: belonging to the directory
|
|
205
|
+
* is not the same as owning it.
|
|
206
|
+
*
|
|
207
|
+
* - Nobody named a project (a pre-2.0 SDK sends no projectId). There is no identity to disagree
|
|
208
|
+
* with, and the daemon's own root is the only thing on offer.
|
|
209
|
+
* - The named project IS the daemon's own, and the daemon can prove it — `.reticle.json` sits in
|
|
210
|
+
* its directory and declares the same id. Both sides go through the same segment guard, because
|
|
211
|
+
* an equality between a guarded value and an unguarded one is only as strong as the loose side.
|
|
212
|
+
*
|
|
213
|
+
* A NAMED project the daemon cannot match declines, including when it has a `.reticle/` but no
|
|
214
|
+
* readable id. Declining costs the session its home directory and buys the guarantee that one
|
|
215
|
+
* checkout never receives another's ledger; the alternative is a wrong answer that reports success.
|
|
216
|
+
*
|
|
217
|
+
* The two ids are REQUIRED parameters rather than one object, and that is load-bearing. Taking a
|
|
218
|
+
* partial object let a caller omit a field and still compile — and an omitted `projectId` reads as
|
|
219
|
+
* "nobody named a project", which answers `true` and puts the session straight back into the daemon's
|
|
220
|
+
* checkout. That is the original defect, reachable by a typo rather than by a decision.
|
|
221
|
+
*/
|
|
222
|
+
function isTheDaemonsOwnProject(daemonProjectId, projectId) {
|
|
223
|
+
if (projectId === undefined || 0 === projectId.length)
|
|
224
|
+
return true;
|
|
225
|
+
if (daemonProjectId === undefined || 0 === daemonProjectId.length)
|
|
226
|
+
return false;
|
|
227
|
+
return (daemonProjectId === projectId &&
|
|
228
|
+
SAFE_SEGMENT_PATTERN.test(daemonProjectId) &&
|
|
229
|
+
!daemonProjectId.includes('..'));
|
|
230
|
+
}
|
|
152
231
|
/** Hashed, not spelled: an origin carries a host and a port, and neither belongs in a path segment. */
|
|
153
232
|
function unnamedSegment(origin) {
|
|
154
233
|
if (origin === undefined || 0 === origin.trim().length)
|
|
@@ -17,8 +17,6 @@ export interface ReticleDirPaths {
|
|
|
17
17
|
impact: string;
|
|
18
18
|
/**.../.reticle/intent.json — the LEGACY single ledger, read until the first write migrates it into .reticle/intent/ */
|
|
19
19
|
intent: string;
|
|
20
|
-
/**.../.reticle/fill-values.json (what a drive types into each labelled field — git-checked) */
|
|
21
|
-
fillValues: string;
|
|
22
20
|
/**.../.reticle/visual (PNG baselines + diffs) */
|
|
23
21
|
visual: string;
|
|
24
22
|
/**.../.reticle/runs (verification-run artifacts) */
|
|
@@ -37,6 +35,10 @@ export interface ReticleDirPaths {
|
|
|
37
35
|
coverage: string;
|
|
38
36
|
/**.../.reticle/assertion-tiers.json (last-passing assertion tiers; anti-reward-hacking baseline) */
|
|
39
37
|
tiers: string;
|
|
38
|
+
/**.../.reticle/request.json (the user's latest request, relayed by the agent, classified) */
|
|
39
|
+
request: string;
|
|
40
|
+
/** .reticle/plans/ — every Harness drive plan made here, and how it went. */
|
|
41
|
+
plans: string;
|
|
40
42
|
}
|
|
41
43
|
export declare function reticleDirPaths(root: string): ReticleDirPaths;
|
|
42
44
|
/** The verification-run artifact path for `runId`.reticle/runs/<runId>.json). */
|
|
@@ -10,7 +10,6 @@ export function reticleDirPaths(root) {
|
|
|
10
10
|
project: join(root, ReticleDir.PROJECT_FILE),
|
|
11
11
|
impact: join(root, ReticleDir.IMPACT_FILE),
|
|
12
12
|
intent: join(root, ReticleDir.INTENT_FILE),
|
|
13
|
-
fillValues: join(root, ReticleDir.FILL_VALUES_FILE),
|
|
14
13
|
visual: join(root, ReticleDir.VISUAL_SUBDIR),
|
|
15
14
|
runs: join(root, ReticleDir.RUNS_SUBDIR),
|
|
16
15
|
sessions: join(root, ReticleDir.SESSIONS_SUBDIR),
|
|
@@ -20,6 +19,9 @@ export function reticleDirPaths(root) {
|
|
|
20
19
|
flake: join(root, ReticleDir.FLAKE_FILE),
|
|
21
20
|
coverage: join(root, ReticleDir.COVERAGE_FILE),
|
|
22
21
|
tiers: join(root, ReticleDir.TIERS_FILE),
|
|
22
|
+
// Beside `intent/`, not in it: `intent/` is committed, and this is the user's own words.
|
|
23
|
+
request: join(root, ReticleDir.REQUEST_FILE),
|
|
24
|
+
plans: join(root, ReticleDir.PLANS_SUBDIR),
|
|
23
25
|
};
|
|
24
26
|
}
|
|
25
27
|
/** The verification-run artifact path for `runId`.reticle/runs/<runId>.json). */
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
+
import { describeSync, overallStatus, readSyncSummary } from './sync-status.js';
|
|
2
3
|
import { sessionRoot } from './session-root.js';
|
|
3
4
|
import { projectForRoot } from './project-for-root.js';
|
|
4
5
|
import { ProjectReadError, RunStatus } from '@reticlehq/core';
|
|
@@ -94,6 +95,7 @@ export const PROJECT_TOOLS = [
|
|
|
94
95
|
description: 'Read cross-run history from .reticle/project.json — the memory of how past runs behaved. With { name } it also returns the last run for that flow plus a diff-vs-last summary (status change, regressed flag, consoleErrors/driftSteps deltas) so you can answer "did it behave like last time?". `diff` is the lightweight status/console/drift delta vs the previous run of that NAME; `runDiff` is the per-flow duration/status delta between the two most-recent full verification artifacts ("step 3: 412ms -> 987ms (+140%), +2 requests"). When logged in to Reticle, also returns `cloud`: the team\'s server-side regression report (broken/changed flows vs before) — the durable memory a fresh or context-lost agent can rely on even when local history is empty. Returns { runs, learned?, lastRun?, diff?, runDiff?, cloud? } or { error, reason, cloud? }.',
|
|
95
96
|
inputSchema: {
|
|
96
97
|
name: z.string().optional().describe('Filter runs by this name. Omit to return all runs.'),
|
|
98
|
+
push: z.boolean().optional().describe('Sync with the platform now, before answering.'),
|
|
97
99
|
limit: countSchema
|
|
98
100
|
.optional()
|
|
99
101
|
.describe('Most-recent N runs to return. Defaults to 25 — the full history is unbounded and grows with every run (measured at ~20KB / ~5,000 tokens on a modest project), which is a large slice of an agent context for data it mostly does not read. `totalRuns` always reports the true count, so a cap is never silent.'),
|
|
@@ -110,14 +112,31 @@ export const PROJECT_TOOLS = [
|
|
|
110
112
|
cloud: z.unknown().optional(),
|
|
111
113
|
},
|
|
112
114
|
handler: async (deps, args) => {
|
|
113
|
-
|
|
114
|
-
|
|
115
|
+
// A push asked for is done first, so the status below describes what it achieved.
|
|
116
|
+
if (true === args['push'])
|
|
117
|
+
await deps.syncNow?.().catch(() => undefined);
|
|
118
|
+
const root = sessionRoot(deps, asString(args['sessionId']));
|
|
119
|
+
const linked = (await deps.linkedCloud?.().catch(() => null)) ?? null;
|
|
120
|
+
const summary = readSyncSummary(root, null !== linked);
|
|
121
|
+
const sync = {
|
|
122
|
+
...summary,
|
|
123
|
+
status: overallStatus(summary),
|
|
124
|
+
said: describeSync(summary, deps.now()),
|
|
125
|
+
};
|
|
126
|
+
const regression = await cloudRegression(deps, asString(args['sessionId']));
|
|
127
|
+
// Where this project's work stands with the platform rides in `cloud` beside the team's
|
|
128
|
+
// regression report: the two answers to "what does the platform know about this project".
|
|
129
|
+
const cloud = {
|
|
130
|
+
...('object' === typeof regression && null !== regression ? regression : {}),
|
|
131
|
+
sync,
|
|
132
|
+
};
|
|
133
|
+
const withCloud = (obj) => ({ ...obj, cloud });
|
|
115
134
|
const project = projectForRoot(deps, sessionRoot(deps, asString(args['sessionId'])));
|
|
116
135
|
const read = await project.read();
|
|
117
136
|
if (!read.ok) {
|
|
118
137
|
return withCloud({
|
|
119
138
|
error: read.reason === ProjectReadError.MISSING
|
|
120
|
-
? 'no .reticle/project.json yet — run
|
|
139
|
+
? 'no .reticle/project.json yet — the first verified run writes it: drive one step with reticle_act_and_wait and an `until`'
|
|
121
140
|
: '.reticle/project.json is malformed — it will self-heal on the next recorded run',
|
|
122
141
|
reason: read.reason,
|
|
123
142
|
});
|
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
import type { ProjectId } from '@reticlehq/core';
|
|
2
2
|
import type { ToolDeps } from '../../surface/tools/tool-kit.js';
|
|
3
|
+
/**
|
|
4
|
+
* Which project a call is about, and the page origin it came through.
|
|
5
|
+
*
|
|
6
|
+
* The projectId alone is not an address: every git worktree carries the same committed
|
|
7
|
+
* `.reticle.json`, so a daemon serving several worktrees sees one id for all of them. The origin
|
|
8
|
+
* names the dev server that served the page, and the resolver uses it to pick the checkout that
|
|
9
|
+
* server runs in. Carried as one value so no caller can resolve with half of it -- a flow saved
|
|
10
|
+
* through the worktree and replayed through the id alone would land in one tree and be read from
|
|
11
|
+
* another.
|
|
12
|
+
*/
|
|
13
|
+
export interface ProjectTarget {
|
|
14
|
+
projectId: ProjectId | undefined;
|
|
15
|
+
origin?: string | undefined;
|
|
16
|
+
}
|
|
17
|
+
/** The target for a caller that genuinely holds no session -- the id is all there is. */
|
|
18
|
+
export declare function projectOnly(projectId: ProjectId | undefined): ProjectTarget;
|
|
3
19
|
/**
|
|
4
20
|
* The `.reticle` directory a tool call's artifacts belong in.
|
|
5
21
|
*
|
|
@@ -24,7 +40,7 @@ export declare function sessionRoot(deps: ToolDeps, sessionId: string | undefine
|
|
|
24
40
|
* place and the id from another. `sessionRoot` is now this function plus a lookup, so the two
|
|
25
41
|
* cannot disagree about what a resolved root is.
|
|
26
42
|
*/
|
|
27
|
-
export declare function
|
|
43
|
+
export declare function rootForTarget(deps: ToolDeps, target: ProjectTarget): string;
|
|
28
44
|
/**
|
|
29
45
|
* The PROJECT directory -- one level above `.reticle` -- for a caller that runs a tool in a tree
|
|
30
46
|
* rather than writing a file into one. `git diff` is the case: it was being run in the daemon's
|
|
@@ -50,3 +66,10 @@ export declare function projectDirFor(deps: ToolDeps, sessionId: string | undefi
|
|
|
50
66
|
* there is no answer either.
|
|
51
67
|
*/
|
|
52
68
|
export declare function sessionProjectId(deps: ToolDeps, sessionId: string | undefined): ProjectId | undefined;
|
|
69
|
+
/**
|
|
70
|
+
* `sessionTarget` for the paths that degrade instead of refusing: replay and learning resolve a
|
|
71
|
+
* missing session to the global store, and keep doing so -- with the origin when there is one.
|
|
72
|
+
*/
|
|
73
|
+
export declare function safeSessionTarget(deps: ToolDeps, sessionId: string | undefined): ProjectTarget;
|
|
74
|
+
/** `sessionProjectId` plus the origin the session's page was served from. Same refusals. */
|
|
75
|
+
export declare function sessionTarget(deps: ToolDeps, sessionId: string | undefined): ProjectTarget;
|
|
@@ -1,4 +1,23 @@
|
|
|
1
1
|
import { dirname } from 'node:path';
|
|
2
|
+
/**
|
|
3
|
+
* The scheme://host:port a session's page was served from. Local rather than the session manager's
|
|
4
|
+
* copy because `memory/project` does not reach `portal/session`, and five lines are cheaper than a
|
|
5
|
+
* new dependency between the two.
|
|
6
|
+
*/
|
|
7
|
+
function originOf(url) {
|
|
8
|
+
if (url === undefined)
|
|
9
|
+
return undefined;
|
|
10
|
+
try {
|
|
11
|
+
return new URL(url).origin;
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
return undefined;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
/** The target for a caller that genuinely holds no session -- the id is all there is. */
|
|
18
|
+
export function projectOnly(projectId) {
|
|
19
|
+
return { projectId };
|
|
20
|
+
}
|
|
2
21
|
/**
|
|
3
22
|
* The `.reticle` directory a tool call's artifacts belong in.
|
|
4
23
|
*
|
|
@@ -26,7 +45,7 @@ export function sessionRoot(deps, sessionId) {
|
|
|
26
45
|
// nothing its answer could change, so that one case skips the lookup and costs what it always
|
|
27
46
|
// did. Every embedder of this engine, and every older construction of ToolDeps, is on it.
|
|
28
47
|
const nothingToRefuse = deps.artifactRootFor === undefined && sessionId === undefined;
|
|
29
|
-
return
|
|
48
|
+
return rootForTarget(deps, nothingToRefuse ? projectOnly(undefined) : sessionTarget(deps, sessionId));
|
|
30
49
|
}
|
|
31
50
|
/**
|
|
32
51
|
* The same answer for a caller that already holds the projectId rather than a sessionId.
|
|
@@ -37,8 +56,8 @@ export function sessionRoot(deps, sessionId) {
|
|
|
37
56
|
* place and the id from another. `sessionRoot` is now this function plus a lookup, so the two
|
|
38
57
|
* cannot disagree about what a resolved root is.
|
|
39
58
|
*/
|
|
40
|
-
export function
|
|
41
|
-
return deps.artifactRootFor?.(projectId).root ?? deps.reticleRoot;
|
|
59
|
+
export function rootForTarget(deps, target) {
|
|
60
|
+
return deps.artifactRootFor?.(target.projectId, target.origin).root ?? deps.reticleRoot;
|
|
42
61
|
}
|
|
43
62
|
/**
|
|
44
63
|
* The PROJECT directory -- one level above `.reticle` -- for a caller that runs a tool in a tree
|
|
@@ -74,12 +93,29 @@ export function projectDirFor(deps, sessionId) {
|
|
|
74
93
|
* from this file so they cannot disagree again.
|
|
75
94
|
*/
|
|
76
95
|
export function sessionProjectId(deps, sessionId) {
|
|
96
|
+
return sessionTarget(deps, sessionId).projectId;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* `sessionTarget` for the paths that degrade instead of refusing: replay and learning resolve a
|
|
100
|
+
* missing session to the global store, and keep doing so -- with the origin when there is one.
|
|
101
|
+
*/
|
|
102
|
+
export function safeSessionTarget(deps, sessionId) {
|
|
103
|
+
try {
|
|
104
|
+
return sessionTarget(deps, sessionId);
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
return projectOnly(undefined);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
/** `sessionProjectId` plus the origin the session's page was served from. Same refusals. */
|
|
111
|
+
export function sessionTarget(deps, sessionId) {
|
|
77
112
|
try {
|
|
78
|
-
|
|
113
|
+
const session = deps.sessions.resolve(sessionId);
|
|
114
|
+
return { projectId: session.projectId, origin: originOf(session.url) };
|
|
79
115
|
}
|
|
80
116
|
catch (cause) {
|
|
81
117
|
if (sessionId === undefined)
|
|
82
|
-
return undefined;
|
|
118
|
+
return projectOnly(undefined);
|
|
83
119
|
throw unresolvedTargetRefusal(sessionId, cause);
|
|
84
120
|
}
|
|
85
121
|
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { CloudSyncState } from '../cloud/sync-cycle.js';
|
|
2
|
+
export declare const SyncStatus: {
|
|
3
|
+
readonly ON_PLATFORM: "on-platform";
|
|
4
|
+
readonly PENDING: "pending";
|
|
5
|
+
readonly REFUSED: "refused";
|
|
6
|
+
readonly LOCAL_ONLY: "local-only";
|
|
7
|
+
};
|
|
8
|
+
export type SyncStatus = (typeof SyncStatus)[keyof typeof SyncStatus];
|
|
9
|
+
export interface SyncSummary {
|
|
10
|
+
linked: boolean;
|
|
11
|
+
/** Runs on this machine. */
|
|
12
|
+
runs: number;
|
|
13
|
+
onPlatform: number;
|
|
14
|
+
pending: number;
|
|
15
|
+
/** The runs the platform refused in their current form, with its reason. */
|
|
16
|
+
refused: {
|
|
17
|
+
runId: string;
|
|
18
|
+
reason: string;
|
|
19
|
+
}[];
|
|
20
|
+
/** Refusals beyond the ones named. */
|
|
21
|
+
refusedMore: number;
|
|
22
|
+
lastPushAt?: number;
|
|
23
|
+
/** Why the last cycle failed, when it did. */
|
|
24
|
+
lastError?: string;
|
|
25
|
+
}
|
|
26
|
+
export declare function summarizeSync(input: {
|
|
27
|
+
linked: boolean;
|
|
28
|
+
runs: readonly {
|
|
29
|
+
runId: string;
|
|
30
|
+
payload: unknown;
|
|
31
|
+
}[];
|
|
32
|
+
state: CloudSyncState;
|
|
33
|
+
}): SyncSummary;
|
|
34
|
+
/** This project's summary, read from its `.reticle`. Never throws: an unreadable record is empty. */
|
|
35
|
+
export declare function readSyncSummary(reticleRoot: string, linked: boolean): SyncSummary;
|
|
36
|
+
/** One status for the whole project: the worst thing true of it. */
|
|
37
|
+
export declare function overallStatus(summary: SyncSummary): SyncStatus;
|
|
38
|
+
/** The summary in one sentence an agent can repeat to a person. */
|
|
39
|
+
export declare function describeSync(summary: SyncSummary, now: number): string;
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where this project's work stands with the Reticle platform, run by run.
|
|
3
|
+
*
|
|
4
|
+
* Read from the sync cycle's own record (`.reticle/cloud-state.json`): the content hash of every run
|
|
5
|
+
* the platform ACCEPTED, and the reason for every run it REFUSED. A run whose current content matches
|
|
6
|
+
* an accepted hash is on the platform; one whose current content was refused is refused, with the
|
|
7
|
+
* platform's own sentence; anything else is waiting for the next cycle. On a machine that is not
|
|
8
|
+
* linked, everything is local only.
|
|
9
|
+
*
|
|
10
|
+
* Nothing here talks to the network. It answers "did my work go up?" from what the machine already
|
|
11
|
+
* knows, so it can ride on every verdict without costing a request.
|
|
12
|
+
*/
|
|
13
|
+
import { diskSource, readCloudState } from '../cloud/sync-disk.js';
|
|
14
|
+
import { hashPayload } from '../cloud/sync-hash.js';
|
|
15
|
+
export const SyncStatus = {
|
|
16
|
+
ON_PLATFORM: 'on-platform',
|
|
17
|
+
PENDING: 'pending',
|
|
18
|
+
REFUSED: 'refused',
|
|
19
|
+
LOCAL_ONLY: 'local-only',
|
|
20
|
+
};
|
|
21
|
+
/** How many refusals a summary names before it only counts them. */
|
|
22
|
+
const MAX_NAMED_REFUSALS = 3;
|
|
23
|
+
export function summarizeSync(input) {
|
|
24
|
+
const { linked, runs, state } = input;
|
|
25
|
+
let onPlatform = 0;
|
|
26
|
+
let pending = 0;
|
|
27
|
+
const refused = [];
|
|
28
|
+
if (linked) {
|
|
29
|
+
for (const run of runs) {
|
|
30
|
+
const hash = hashPayload(run.payload);
|
|
31
|
+
const refusal = state.refusedRuns?.[run.runId];
|
|
32
|
+
if (state.sentRunHashes?.[run.runId] === hash)
|
|
33
|
+
onPlatform += 1;
|
|
34
|
+
else if (refusal !== undefined && refusal.payloadHash === hash)
|
|
35
|
+
refused.push({ runId: run.runId, reason: refusal.reason });
|
|
36
|
+
else
|
|
37
|
+
pending += 1;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return {
|
|
41
|
+
linked,
|
|
42
|
+
runs: runs.length,
|
|
43
|
+
onPlatform,
|
|
44
|
+
pending,
|
|
45
|
+
refused: refused.slice(0, MAX_NAMED_REFUSALS),
|
|
46
|
+
refusedMore: Math.max(0, refused.length - MAX_NAMED_REFUSALS),
|
|
47
|
+
...(state.lastPushAt === undefined ? {} : { lastPushAt: state.lastPushAt }),
|
|
48
|
+
...(state.lastError === undefined || 0 === state.lastError.length
|
|
49
|
+
? {}
|
|
50
|
+
: { lastError: state.lastError }),
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
/** This project's summary, read from its `.reticle`. Never throws: an unreadable record is empty. */
|
|
54
|
+
export function readSyncSummary(reticleRoot, linked) {
|
|
55
|
+
let runs = [];
|
|
56
|
+
try {
|
|
57
|
+
runs = diskSource(reticleRoot).runs();
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
runs = [];
|
|
61
|
+
}
|
|
62
|
+
return summarizeSync({ linked, runs, state: readCloudState(reticleRoot) });
|
|
63
|
+
}
|
|
64
|
+
/** One status for the whole project: the worst thing true of it. */
|
|
65
|
+
export function overallStatus(summary) {
|
|
66
|
+
if (!summary.linked)
|
|
67
|
+
return SyncStatus.LOCAL_ONLY;
|
|
68
|
+
if (0 < summary.refused.length)
|
|
69
|
+
return SyncStatus.REFUSED;
|
|
70
|
+
if (0 < summary.pending || summary.lastError !== undefined)
|
|
71
|
+
return SyncStatus.PENDING;
|
|
72
|
+
return SyncStatus.ON_PLATFORM;
|
|
73
|
+
}
|
|
74
|
+
/** The summary in one sentence an agent can repeat to a person. */
|
|
75
|
+
export function describeSync(summary, now) {
|
|
76
|
+
if (!summary.linked)
|
|
77
|
+
return `${String(summary.runs)} run(s) exist only on this machine; nothing is synced until the project is linked (npx @reticlehq/server connect).`;
|
|
78
|
+
const ago = summary.lastPushAt === undefined
|
|
79
|
+
? 'nothing pushed yet'
|
|
80
|
+
: `last push ${String(Math.max(0, Math.round((now - summary.lastPushAt) / 1000)))}s ago`;
|
|
81
|
+
const parts = [`${String(summary.onPlatform)} of ${String(summary.runs)} run(s) on the platform`];
|
|
82
|
+
if (0 < summary.pending)
|
|
83
|
+
parts.push(`${String(summary.pending)} waiting for the next push`);
|
|
84
|
+
const named = summary.refused.map((r) => `${r.runId}: ${r.reason}`);
|
|
85
|
+
if (0 < named.length)
|
|
86
|
+
parts.push(`refused by the platform: ${named.join('; ')}${0 < summary.refusedMore ? ` (+${String(summary.refusedMore)} more)` : ''}`);
|
|
87
|
+
if (summary.lastError !== undefined)
|
|
88
|
+
parts.push(`the last push failed: ${summary.lastError}`);
|
|
89
|
+
return `${parts.join('; ')} (${ago}).`;
|
|
90
|
+
}
|
|
@@ -30,6 +30,8 @@ const ADVICE = {
|
|
|
30
30
|
[MemoryUnavailable.NOT_LINKED]: 'this project is not linked to a Reticle workspace, so there is no shared memory to read. Run `npx @reticlehq/server login` in the project to link it.',
|
|
31
31
|
[MemoryUnavailable.DISABLED]: 'this project is linked but memory sync is turned off in .reticle/cloud.json, so nothing is being shared. Enable `sync.memory` to use it.',
|
|
32
32
|
[MemoryUnavailable.UNREACHABLE]: 'the workspace could not be reached, or refused the key. Nothing is wrong with this project — try again, or check the link with `reticle status`.',
|
|
33
|
+
[MemoryUnavailable.UNVERIFIED]: 'the server answered, but the answer could not be shown to be about THIS project, so none of it is being reported. This is not an empty corpus: do NOT conclude this project knows nothing. Check the link with `reticle status`, and retry once the workspace is reachable.',
|
|
34
|
+
[MemoryUnavailable.UNREADABLE]: 'the workspace answered about THIS project, but the body was a shape this build cannot read entries out of. Retrying will not change it — this is not an empty corpus and not a link problem. Check the workspace version, and that `reticle memory` from the same terminal returns a `{ projectId, entries }` body.',
|
|
33
35
|
};
|
|
34
36
|
export const MEMORY_TOOLS = [
|
|
35
37
|
{
|
|
@@ -7,6 +7,25 @@ export declare const MemoryUnavailable: {
|
|
|
7
7
|
readonly DISABLED: "memory-sync-disabled";
|
|
8
8
|
/** The server was reached and said no, or could not be reached at all. */
|
|
9
9
|
readonly UNREACHABLE: "unreachable";
|
|
10
|
+
/**
|
|
11
|
+
* The server answered, and the answer could not be shown to be about THIS project: its envelope
|
|
12
|
+
* named another one, or named none at all.
|
|
13
|
+
*
|
|
14
|
+
* Its own reason rather than an empty list, because the two are opposite facts about the corpus.
|
|
15
|
+
* "This project knows nothing" is something an agent can act on; "we cannot tell whose knowledge
|
|
16
|
+
* this is" is something it must not, and an empty list reports the first while meaning the second.
|
|
17
|
+
*/
|
|
18
|
+
readonly UNVERIFIED: "unverified";
|
|
19
|
+
/**
|
|
20
|
+
* The server answered about THIS project, and the body is a shape this build cannot read entries
|
|
21
|
+
* out of — no `entries` field, or one that is not a list.
|
|
22
|
+
*
|
|
23
|
+
* Apart from `UNREACHABLE` on purpose. That reason's advice is "try again, or check the link", and
|
|
24
|
+
* neither applies: the workspace WAS reached, its answer IS this project's, and the shape will be
|
|
25
|
+
* the same on the second attempt. Sending an agent to retry it costs a round trip and teaches it
|
|
26
|
+
* to distrust a link that is fine, which is what a reason is for.
|
|
27
|
+
*/
|
|
28
|
+
readonly UNREADABLE: "unreadable";
|
|
10
29
|
};
|
|
11
30
|
export type MemoryUnavailable = (typeof MemoryUnavailable)[keyof typeof MemoryUnavailable];
|
|
12
31
|
export interface KnownThing {
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
*/
|
|
14
14
|
import { cloudFetch } from '../cloud/cloud-sync.js';
|
|
15
15
|
import { resolveProjectCloud } from '../cloud/cloud-config.js';
|
|
16
|
+
import { isReadableMemoryScope, memoryReadUrl, scopedMemoryEntries, scopeOfMemoryResponse, } from '../cloud/memory-scope.js';
|
|
16
17
|
/** Named so a read from the tool surface is distinguishable from the CLI's and from replay's. */
|
|
17
18
|
const MCP_AGENT_ID = 'reticle-mcp';
|
|
18
19
|
/** Why a lookup returned nothing. Each one is a different thing for the agent to do next. */
|
|
@@ -23,6 +24,25 @@ export const MemoryUnavailable = {
|
|
|
23
24
|
DISABLED: 'memory-sync-disabled',
|
|
24
25
|
/** The server was reached and said no, or could not be reached at all. */
|
|
25
26
|
UNREACHABLE: 'unreachable',
|
|
27
|
+
/**
|
|
28
|
+
* The server answered, and the answer could not be shown to be about THIS project: its envelope
|
|
29
|
+
* named another one, or named none at all.
|
|
30
|
+
*
|
|
31
|
+
* Its own reason rather than an empty list, because the two are opposite facts about the corpus.
|
|
32
|
+
* "This project knows nothing" is something an agent can act on; "we cannot tell whose knowledge
|
|
33
|
+
* this is" is something it must not, and an empty list reports the first while meaning the second.
|
|
34
|
+
*/
|
|
35
|
+
UNVERIFIED: 'unverified',
|
|
36
|
+
/**
|
|
37
|
+
* The server answered about THIS project, and the body is a shape this build cannot read entries
|
|
38
|
+
* out of — no `entries` field, or one that is not a list.
|
|
39
|
+
*
|
|
40
|
+
* Apart from `UNREACHABLE` on purpose. That reason's advice is "try again, or check the link", and
|
|
41
|
+
* neither applies: the workspace WAS reached, its answer IS this project's, and the shape will be
|
|
42
|
+
* the same on the second attempt. Sending an agent to retry it costs a round trip and teaches it
|
|
43
|
+
* to distrust a link that is fine, which is what a reason is for.
|
|
44
|
+
*/
|
|
45
|
+
UNREADABLE: 'unreadable',
|
|
26
46
|
};
|
|
27
47
|
/** Defensive: the wire is somebody else's server, so every field is checked before it is trusted. */
|
|
28
48
|
const asKnown = (raw) => {
|
|
@@ -58,9 +78,15 @@ export async function readProjectMemory(fs, root, home, env, opts) {
|
|
|
58
78
|
return { ok: false, reason: MemoryUnavailable.NOT_LINKED };
|
|
59
79
|
if (!cloud.policy.memory)
|
|
60
80
|
return { ok: false, reason: MemoryUnavailable.DISABLED };
|
|
61
|
-
|
|
81
|
+
// Scoped to the LINKED project, not to whatever the key happens to cover. A workspace can hold
|
|
82
|
+
// more than one repo, and a read that named no project could be answered with another one's
|
|
83
|
+
// established knowledge — indistinguishable from this project's, and acted on. See memory-scope.
|
|
84
|
+
const url = memoryReadUrl(cloud.config.url, {
|
|
85
|
+
projectId: cloud.projectId,
|
|
86
|
+
subject: opts.subject,
|
|
87
|
+
});
|
|
62
88
|
try {
|
|
63
|
-
const res = await cloudFetch(
|
|
89
|
+
const res = await cloudFetch(url, {
|
|
64
90
|
method: 'GET',
|
|
65
91
|
headers: {
|
|
66
92
|
authorization: `Bearer ${cloud.config.apiKey}`,
|
|
@@ -75,10 +101,23 @@ export async function readProjectMemory(fs, root, home, env, opts) {
|
|
|
75
101
|
// property yields the function, and every downstream field is undefined — a silent "the project
|
|
76
102
|
// knows nothing" that is indistinguishable from the honest empty case. It cost a live drive to
|
|
77
103
|
// find once already.
|
|
78
|
-
const body =
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
104
|
+
const body = await res.json();
|
|
105
|
+
// The envelope first, and it decides everything. The platform names the project on the response
|
|
106
|
+
// and NOT on each entry, so an entry-level filter alone keeps a whole sibling workspace while
|
|
107
|
+
// looking like it did something — see `memory-scope.ts`. A response that names another project,
|
|
108
|
+
// or names none, is UNVERIFIED: not empty, and not this project's.
|
|
109
|
+
const entries = scopedMemoryEntries(body, cloud.projectId);
|
|
110
|
+
if (entries === undefined) {
|
|
111
|
+
return {
|
|
112
|
+
ok: false,
|
|
113
|
+
reason: isReadableMemoryScope(scopeOfMemoryResponse(body, cloud.projectId))
|
|
114
|
+
? MemoryUnavailable.UNREADABLE // scoped fine, but no readable `entries` — a shape we do not know
|
|
115
|
+
: MemoryUnavailable.UNVERIFIED,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
// The entry-level pass is redundant against the envelope check above and kept for a server that
|
|
119
|
+
// labels entries instead. `total` is counted AFTER it, so a truncated list still tells the truth
|
|
120
|
+
// about how many of THIS project's statements there were.
|
|
82
121
|
const known = entries.map(asKnown).filter((k) => k !== null);
|
|
83
122
|
return {
|
|
84
123
|
ok: true,
|
|
@@ -110,7 +110,9 @@ export declare class Bridge {
|
|
|
110
110
|
* Register a handler for the panel's harness switch. Optional for the same reason as the above: a
|
|
111
111
|
* bridge built without one ignores the request rather than refusing it.
|
|
112
112
|
*/
|
|
113
|
-
attachHarnessRequest(handler: (enabled: boolean) => void): void;
|
|
113
|
+
attachHarnessRequest(handler: (enabled: boolean, session: Session) => void): void;
|
|
114
|
+
/** Register a handler for the panel's Sign in. Optional, like the above: without one it is ignored. */
|
|
115
|
+
attachSigninRequest(handler: () => void): void;
|
|
114
116
|
/** Register a handler to run when a session connects. Additive — every handler runs. */
|
|
115
117
|
attachSessionCreate(handler: (session: Session) => void): void;
|
|
116
118
|
close(): Promise<void>;
|