@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
|
@@ -7,12 +7,21 @@
|
|
|
7
7
|
*
|
|
8
8
|
* It lives here rather than in the daemon bootstrap because it is a subject, not wiring — and
|
|
9
9
|
* because the bootstrap is at its line cap, which is the cohesion signal that rule doing its job.
|
|
10
|
+
*
|
|
11
|
+
* ## Why `artifactRootResolver` takes a `deps`
|
|
12
|
+
*
|
|
13
|
+
* Both of its IO answers come from the REAL disk and the REAL `process.cwd()`: which projects this
|
|
14
|
+
* machine knows, and whether the daemon's own directory declares one. That left the file untestable
|
|
15
|
+
* at the level where its defect actually lives — the `unmatchedRoot` specs next door hand a
|
|
16
|
+
* `daemonProjectId` straight in, so a mistake in READING it would leave every one of them green
|
|
17
|
+
* while the worktree bug reproduced unchanged. The defaults are the production answers; the seam is
|
|
18
|
+
* what lets a test say "these are the projects on this machine" without one.
|
|
10
19
|
*/
|
|
11
|
-
import { existsSync, readFileSync } from 'node:fs';
|
|
20
|
+
import { existsSync, readFileSync, readdirSync } from 'node:fs';
|
|
12
21
|
import { dirname, join } from 'node:path';
|
|
13
22
|
import { homedir } from 'node:os';
|
|
14
23
|
import { PROJECT_REGISTRY_FILE, emptyProjectRegistry, parseProjectRegistry, } from '@reticlehq/core/artifacts';
|
|
15
|
-
import { ReticleDir } from '@reticlehq/core';
|
|
24
|
+
import { DevServerEntrySchema, ReticleDir, ReticleEnv, devServerRegistryPort, } from '@reticlehq/core';
|
|
16
25
|
import { discoverProjectConfigs, hasProjectConfig, } from '../../command/cli/config/config-discovery.js';
|
|
17
26
|
import { log } from '../../log.js';
|
|
18
27
|
import { ArtifactRootReason, projectCandidatesFrom, resolveArtifactRoot, unmatchedRoot, } from './artifact-root.js';
|
|
@@ -34,32 +43,127 @@ function daemonSitsInAProject(daemonRoot) {
|
|
|
34
43
|
return false;
|
|
35
44
|
}
|
|
36
45
|
}
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
46
|
+
/**
|
|
47
|
+
* The project a candidate list says lives at the daemon's own directory.
|
|
48
|
+
*
|
|
49
|
+
* Not the same question as `daemonSitsInAProject`: that one asks whether the tree invited Reticle in
|
|
50
|
+
* at all (either mark counts, including a bare `.reticle/` from a version that predates the config
|
|
51
|
+
* file), and this one asks WHICH project it is. The difference is the whole of #1244 — a daemon in
|
|
52
|
+
* repo A serving an app from a git worktree B belongs to A and is being asked about B.
|
|
53
|
+
*
|
|
54
|
+
* Takes the candidates rather than gathering them, so the answer is a function of one list and a test
|
|
55
|
+
* can supply it. Read from that same list the resolver already builds, so there is one answer to
|
|
56
|
+
* "where does this project live" rather than a second `.reticle.json` parser here. A daemon whose
|
|
57
|
+
* root was passed explicitly (`--root`, or an embedder's own `reticleRoot`) is not in that list and
|
|
58
|
+
* answers undefined, which is the safe direction: it cannot prove the named project is its own, so it
|
|
59
|
+
* declines the daemon root and the session goes to the unmatched bucket instead.
|
|
60
|
+
*/
|
|
61
|
+
function daemonOwnProjectId(daemonRoot, candidates) {
|
|
62
|
+
const directory = dirname(daemonRoot);
|
|
63
|
+
return candidates.find((candidate) => candidate.directory === directory)?.projectId;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Every project this machine knows the directory of: discovered `.reticle.json` files first, then
|
|
67
|
+
* the user-level registry, then the dev servers the build plugin announced. Read on each call, since
|
|
68
|
+
* `init` can run in another terminal while the daemon is up.
|
|
69
|
+
*
|
|
70
|
+
* Exported because sync must cover exactly the projects runs are written into. It kept its own list
|
|
71
|
+
* without the announced dev servers, so a project known only through its running dev server had its
|
|
72
|
+
* runs written to its `.reticle/runs/` and never sent, with nothing reporting a problem.
|
|
73
|
+
*/
|
|
74
|
+
export function knownProjectCandidates() {
|
|
75
|
+
let registry = emptyProjectRegistry();
|
|
76
|
+
try {
|
|
77
|
+
const path = join(homedir(), ReticleDir.ROOT, PROJECT_REGISTRY_FILE);
|
|
78
|
+
registry = existsSync(path)
|
|
79
|
+
? parseProjectRegistry(JSON.parse(readFileSync(path, 'utf8')))
|
|
80
|
+
: registry;
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
// A cache that cannot be read is an empty cache, never an error: the daemon still resolves
|
|
84
|
+
// through discovery, and falls back to its own root exactly as it did before this existed.
|
|
85
|
+
}
|
|
86
|
+
let discovery = { found: [], searched: [] };
|
|
87
|
+
try {
|
|
88
|
+
discovery = discoverProjectConfigs(process.cwd());
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
// Same reasoning: a diagnostic search that throws must not take a tool call with it.
|
|
92
|
+
}
|
|
93
|
+
return projectCandidatesFrom(discovery, registry, announcedDevServers());
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* The dev servers the build plugin announced, with the project id and root each serves. Read from
|
|
97
|
+
* the same state directory the plugin writes to. Unreadable or malformed entries are skipped: a stale
|
|
98
|
+
* announcement only ever names a directory that really was that project.
|
|
99
|
+
*/
|
|
100
|
+
function announcedDevServers() {
|
|
101
|
+
const override = process.env[ReticleEnv.STATE_DIR];
|
|
102
|
+
const dir = override !== undefined && 0 < override.length ? override : join(homedir(), ReticleDir.ROOT);
|
|
103
|
+
const servers = [];
|
|
104
|
+
try {
|
|
105
|
+
for (const name of readdirSync(dir)) {
|
|
106
|
+
if (null === devServerRegistryPort(name))
|
|
107
|
+
continue;
|
|
108
|
+
try {
|
|
109
|
+
const parsed = DevServerEntrySchema.safeParse(JSON.parse(readFileSync(join(dir, name), 'utf8')));
|
|
110
|
+
if (parsed.success)
|
|
111
|
+
servers.push({ projectId: parsed.data.projectId, root: parsed.data.root });
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
/* one bad announcement does not hide the others */
|
|
115
|
+
}
|
|
54
116
|
}
|
|
55
|
-
|
|
56
|
-
|
|
117
|
+
}
|
|
118
|
+
catch {
|
|
119
|
+
/* no state directory yet: nothing has been announced */
|
|
120
|
+
}
|
|
121
|
+
return servers;
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* The directory of the project a page announced, or undefined when this machine does not know it.
|
|
125
|
+
*
|
|
126
|
+
* A daemon is shared: started from `$HOME`, a monorepo root, or another project's MCP client, its
|
|
127
|
+
* cwd is often not the app that sent the HELLO. The version-skew remedy read `package.json` from
|
|
128
|
+
* cwd anyway, so it fell back to the generic sensor package and told a Next.js project to install
|
|
129
|
+
* `@reticlehq/browser` (#1135). This is the same lookup the artifact root uses, keyed by the page's
|
|
130
|
+
* own project id.
|
|
131
|
+
*/
|
|
132
|
+
export function projectDirectoryFor(projectId) {
|
|
133
|
+
if (projectId === undefined)
|
|
134
|
+
return undefined;
|
|
135
|
+
const directories = new Set(knownProjectCandidates()
|
|
136
|
+
.filter((candidate) => candidate.projectId === projectId)
|
|
137
|
+
.map((candidate) => candidate.directory));
|
|
138
|
+
// Two checkouts declaring one id are refused, as the artifact root refuses them: naming one of
|
|
139
|
+
// them would read the wrong app's package.json and prescribe its packages to the other.
|
|
140
|
+
return 1 === directories.size ? [...directories][0] : undefined;
|
|
141
|
+
}
|
|
142
|
+
export function artifactRootResolver(daemonRoot, deps) {
|
|
143
|
+
const candidates = deps.candidates ?? knownProjectCandidates;
|
|
144
|
+
const sitsInAProject = deps.daemonIsProject ?? daemonSitsInAProject;
|
|
145
|
+
const { servingDirectory } = deps;
|
|
146
|
+
const daemonIsProject = sitsInAProject(daemonRoot);
|
|
147
|
+
// Resolved beside it and ONCE, for the same reason: a directory's `.reticle.json` does not change
|
|
148
|
+
// between two tabs connecting. Only read when the daemon really is in a project, since it is the
|
|
149
|
+
// sole thing that can license a write into the daemon's own tree.
|
|
150
|
+
const daemonProjectId = daemonIsProject
|
|
151
|
+
? daemonOwnProjectId(daemonRoot, candidates())
|
|
152
|
+
: undefined;
|
|
153
|
+
return (projectId, origin) => {
|
|
154
|
+
const known = candidates();
|
|
155
|
+
let resolved = resolveArtifactRoot({ projectId, candidates: known, daemonRoot });
|
|
156
|
+
// Several checkouts declare this project — every git worktree carries the same committed
|
|
157
|
+
// `.reticle.json`. Only now is the serving process looked up: it shells out, and the tie is the
|
|
158
|
+
// one case where where the page was SERVED from is the deciding fact.
|
|
159
|
+
if (resolved.reason === ArtifactRootReason.AMBIGUOUS && origin !== undefined) {
|
|
160
|
+
resolved = resolveArtifactRoot({
|
|
161
|
+
projectId,
|
|
162
|
+
candidates: known,
|
|
163
|
+
daemonRoot,
|
|
164
|
+
servingDirectory: servingDirectory(origin),
|
|
165
|
+
});
|
|
57
166
|
}
|
|
58
|
-
const resolved = resolveArtifactRoot({
|
|
59
|
-
projectId,
|
|
60
|
-
candidates: projectCandidatesFrom(discovery, registry),
|
|
61
|
-
daemonRoot,
|
|
62
|
-
});
|
|
63
167
|
if (resolved.reason === ArtifactRootReason.MATCHED_PROJECT)
|
|
64
168
|
return resolved;
|
|
65
169
|
// Could not name the project. The old code wrote into the daemon's directory anyway and said
|
|
@@ -71,6 +175,7 @@ export function artifactRootResolver(daemonRoot) {
|
|
|
71
175
|
daemonRoot,
|
|
72
176
|
daemonIsProject,
|
|
73
177
|
home: homedir(),
|
|
178
|
+
...(daemonProjectId === undefined ? {} : { daemonProjectId }),
|
|
74
179
|
...(projectId === undefined ? {} : { projectId }),
|
|
75
180
|
// Only reached when no project id survives the guard. Without it every app that never
|
|
76
181
|
// stamped one shares a single directory -- and that directory holds the durable half, so one
|
|
@@ -80,6 +185,9 @@ export function artifactRootResolver(daemonRoot) {
|
|
|
80
185
|
if (root !== daemonRoot) {
|
|
81
186
|
log('artifact_root_unmatched', {
|
|
82
187
|
projectId: projectId ?? null,
|
|
188
|
+
// The other half of the comparison, so a reader can tell "the daemon was a guest" from
|
|
189
|
+
// "the daemon is in a DIFFERENT project" without reading the config files by hand.
|
|
190
|
+
daemonProjectId: daemonProjectId ?? null,
|
|
83
191
|
reason: resolved.reason,
|
|
84
192
|
root,
|
|
85
193
|
});
|
|
@@ -43,6 +43,13 @@ interface ArtifactRootQuery {
|
|
|
43
43
|
candidates: readonly ProjectCandidate[];
|
|
44
44
|
/** Where artifacts go when the project cannot be identified. Already a `.reticle` path. */
|
|
45
45
|
daemonRoot: string;
|
|
46
|
+
/**
|
|
47
|
+
* The directory the dev server that served this page runs in, when the caller could observe it.
|
|
48
|
+
* Only consulted to break an AMBIGUOUS tie: every git worktree carries the same committed
|
|
49
|
+
* `.reticle.json`, so the projectId cannot tell them apart, but the serving process runs inside
|
|
50
|
+
* exactly one of them.
|
|
51
|
+
*/
|
|
52
|
+
servingDirectory?: string | undefined;
|
|
46
53
|
}
|
|
47
54
|
export interface ArtifactRoot {
|
|
48
55
|
/** Absolute path to the `.reticle` directory. Always present, on every branch. */
|
|
@@ -68,7 +75,10 @@ export declare function resolveArtifactRoot(query: ArtifactRootQuery): ArtifactR
|
|
|
68
75
|
* is a cache of something `init` saw once. Order only decides which duplicate survives dedupe, and
|
|
69
76
|
* the one confirmed a moment ago is the better survivor.
|
|
70
77
|
*/
|
|
71
|
-
export declare function projectCandidatesFrom(discovery: ConfigDiscovery, registry: ProjectRegistry
|
|
78
|
+
export declare function projectCandidatesFrom(discovery: ConfigDiscovery, registry: ProjectRegistry, devServers?: readonly {
|
|
79
|
+
projectId?: string | undefined;
|
|
80
|
+
root: string;
|
|
81
|
+
}[]): ProjectCandidate[];
|
|
72
82
|
/** Where evidence goes when no project could be named and the daemon is a guest in this tree. */
|
|
73
83
|
export declare const UNMATCHED_SUBDIR = "unmatched";
|
|
74
84
|
/**
|
|
@@ -83,20 +93,47 @@ export declare const UNMATCHED_SUBDIR = "unmatched";
|
|
|
83
93
|
* Two cases, and only one of them was ever the intended behaviour:
|
|
84
94
|
*
|
|
85
95
|
* - the daemon is sitting IN a Reticle project (a developer who ran `reticle serve` in their own
|
|
86
|
-
* app). Its root is the right answer, and it is
|
|
87
|
-
*
|
|
88
|
-
*
|
|
96
|
+
* app), and the session is asking about THAT project. Its root is the right answer, and it is
|
|
97
|
+
* the case the old fallback was written for.
|
|
98
|
+
* - otherwise — a guest in the tree, or a daemon in its own checkout serving a DIFFERENT project.
|
|
99
|
+
* Then the evidence goes to the user's own `~/.reticle/unmatched/<projectId>`, which is
|
|
100
|
+
* Reticle's to write.
|
|
89
101
|
*
|
|
90
102
|
* It is never dropped. A verdict with nowhere to live is a worse failure than one in an unexpected
|
|
91
103
|
* place, and the caller says out loud where it went.
|
|
92
104
|
*
|
|
105
|
+
* ## The second case in that list used to be missing, and it is not an edge case
|
|
106
|
+
*
|
|
107
|
+
* `daemonIsProject` answers "is the daemon sitting in SOME Reticle project". The guard below read it
|
|
108
|
+
* as "this session's project", and those are different questions whenever the two ids differ. Run
|
|
109
|
+
* the daemon in repo A, then connect an app from a git worktree B of that same repo: B has its own
|
|
110
|
+
* `projectId` in its own `.reticle.json`, and discovery cannot see it — it walks out from A's
|
|
111
|
+
* `process.cwd()` and never crosses into a sibling checkout — so this function is reached with a
|
|
112
|
+
* NAMED project and the daemon's root came back anyway. B's intents, flows and runs were written
|
|
113
|
+
* into A's checkout, and the returned path said A, so nothing looked wrong.
|
|
114
|
+
*
|
|
115
|
+
* A real identity always wins. The daemon's root is only the answer when it is that identity's own
|
|
116
|
+
* root, or when no identity was named at all.
|
|
117
|
+
*
|
|
93
118
|
* `projectId` arrives in HELLO from the page, so it is untrusted input on a path join and is held
|
|
94
|
-
* to one safe segment — the same guard session ids and flow names already pass.
|
|
119
|
+
* to one safe segment — the same guard session ids and flow names already pass. `daemonProjectId`
|
|
120
|
+
* is read from the daemon's own `.reticle.json` and gets the same treatment, because equality
|
|
121
|
+
* against a guarded value is only as strong as the weaker side.
|
|
95
122
|
*/
|
|
96
123
|
export declare function unmatchedRoot(query: {
|
|
97
124
|
daemonRoot: string;
|
|
98
125
|
/** Whether the daemon's own directory is a Reticle project — an IO question, answered by the caller. */
|
|
99
126
|
daemonIsProject: boolean;
|
|
127
|
+
/**
|
|
128
|
+
* The daemon's OWN projectId, when its directory declares one. Absent means the daemon cannot
|
|
129
|
+
* prove which project it belongs to, so a named project that is not demonstrably its own declines.
|
|
130
|
+
*
|
|
131
|
+
* A plain `string` rather than a `ProjectId`, because it arrives from a `ProjectCandidate` — the
|
|
132
|
+
* registry's view of a `.reticle.json` — and the brand is only minted where the id is read out of
|
|
133
|
+
* that file for this daemon. Comparing it against a branded session id is unaffected: the brand is
|
|
134
|
+
* a `string`, and the guard below is what actually holds it to one safe path segment.
|
|
135
|
+
*/
|
|
136
|
+
daemonProjectId?: string | undefined;
|
|
100
137
|
/** The user's home directory. Passed in rather than read, so this stays pure. */
|
|
101
138
|
home: string;
|
|
102
139
|
projectId?: ProjectId | undefined;
|
|
@@ -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
|
}
|