@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,8 +1,13 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { dirname, join } from 'node:path';
|
|
1
3
|
import { z } from 'zod';
|
|
4
|
+
import { StatementSource, classifyStatement, splitStatements, } from '@reticlehq/core/artifacts';
|
|
5
|
+
import { redactFeedbackText } from '../../telemetry/feedback.js';
|
|
6
|
+
import { reticleDirPaths } from '../project/dir/reticle-dir.js';
|
|
2
7
|
import { IntentStore } from './intent-store.js';
|
|
3
8
|
import { IntentShardStore } from './intent-shard-store.js';
|
|
4
9
|
import { IntentStatus } from './intent-shard.js';
|
|
5
|
-
import { ReticleTool } from '@reticlehq/core';
|
|
10
|
+
import { ReticleDir, ReticleTool, apiKeyFrom } from '@reticlehq/core';
|
|
6
11
|
import { sessionIdShape } from '../../surface/tools/tool-kit.js';
|
|
7
12
|
import { PredicateSchema } from '@reticlehq/engine/question/predicate/predicate.js';
|
|
8
13
|
import { sessionRoot } from '../project/session-root.js';
|
|
@@ -32,7 +37,7 @@ const MIGRATE = 'migrate';
|
|
|
32
37
|
export const INTENT_TOOLS = [
|
|
33
38
|
{
|
|
34
39
|
name: ReticleTool.INTENT,
|
|
35
|
-
description: 'Record what a change is SUPPOSED to make true, as a durable statement ABOUT THE PRODUCT that a teammate who was not here will understand in six months — name the behaviour, not this run or its step number, and never "renders cleanly", which nothing can check. It is SHARED memory: pooled per project and read back by later agents. Capture it while you still know — then verification does not have to re-derive it from the DOM later. { action:"declare", intents:[{ id, statement, surface? }] } takes prose and needs NO predicate: at declare time there is often no route, no ref and no code yet, and a predicate demanded there is just a mechanism. Declare EARLY (as you build) and batch them — one call per feature is the whole budget. { action:"bind", id, binding } attaches the predicate that would prove it once you know how; an intent with no binding is not a failure, it is the most interesting row in the ledger — something meant that nothing can currently prove. { action:"list" } returns what is still open.
|
|
40
|
+
description: 'Record what a change is SUPPOSED to make true, as a durable statement ABOUT THE PRODUCT that a teammate who was not here will understand in six months — name the behaviour, not this run or its step number, and never "renders cleanly", which nothing can check. It is SHARED memory: pooled per project and read back by later agents. Capture it while you still know — then verification does not have to re-derive it from the DOM later. { action:"declare", intents:[{ id, statement, surface? }] } takes prose and needs NO predicate: at declare time there is often no route, no ref and no code yet, and a predicate demanded there is just a mechanism. Declare EARLY (as you build) and batch them — one call per feature is the whole budget. { action:"bind", id, binding } attaches the predicate that would prove it once you know how; an intent with no binding is not a failure, it is the most interesting row in the ledger — something meant that nothing can currently prove. { action:"list" } returns what is still open. Pass `request` (what the user asked, verbatim) once per task. Stored in .reticle/intent/, git-checked.',
|
|
36
41
|
example: {
|
|
37
42
|
action: DECLARE,
|
|
38
43
|
intents: [
|
|
@@ -74,6 +79,7 @@ export const INTENT_TOOLS = [
|
|
|
74
79
|
.min(1)
|
|
75
80
|
.optional()
|
|
76
81
|
.describe('declare only. Batchable — declare every intent for a feature in one call.'),
|
|
82
|
+
request: z.string().optional().describe("declare: the user's words, verbatim."),
|
|
77
83
|
id: z.string().optional().describe('bind only: which intent the predicate proves.'),
|
|
78
84
|
/*
|
|
79
85
|
* The predicate schema, not `unknown`.
|
|
@@ -155,6 +161,12 @@ export const INTENT_TOOLS = [
|
|
|
155
161
|
return { intents: await store.open() };
|
|
156
162
|
}
|
|
157
163
|
const raw = args['intents'];
|
|
164
|
+
const request = asString(args['request']);
|
|
165
|
+
if (request !== undefined && 0 < request.trim().length) {
|
|
166
|
+
await recordRequest(deps, root, request, Array.isArray(raw) ? raw : []);
|
|
167
|
+
if (!Array.isArray(raw) || 0 === raw.length)
|
|
168
|
+
return { request: 'recorded', path: root };
|
|
169
|
+
}
|
|
158
170
|
// Refused, not read as an empty list: `declare` with nothing to declare stored nothing and
|
|
159
171
|
// answered `{ intents: [] }`, which an agent reads as a declaration that worked (#1118).
|
|
160
172
|
if (!Array.isArray(raw) || 0 === raw.length) {
|
|
@@ -171,3 +183,69 @@ export const INTENT_TOOLS = [
|
|
|
171
183
|
},
|
|
172
184
|
},
|
|
173
185
|
];
|
|
186
|
+
const MAX_REQUEST = 20_000;
|
|
187
|
+
const MAX_STATEMENT = 500;
|
|
188
|
+
const MAX_STATEMENTS = 100;
|
|
189
|
+
const PROJECT_CONFIG = '.reticle.json';
|
|
190
|
+
/** Credentials and personal data out, length capped: the same rules feedback reports follow. */
|
|
191
|
+
function redactSecrets(text, max) {
|
|
192
|
+
return redactFeedbackText(text, max).text;
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Whether the request goes to the platform with the runs that verify it.
|
|
196
|
+
*
|
|
197
|
+
* A LINKED project shares it unless `.reticle.json` says `"shareRequests": false`: the platform
|
|
198
|
+
* reads what each run was for, which is half of what a run means. An unlinked project has nowhere
|
|
199
|
+
* to send it, and `"shareRequests": true` still shares from one that links later.
|
|
200
|
+
*/
|
|
201
|
+
function sharesRequests(projectDir, reticleRoot) {
|
|
202
|
+
let setting;
|
|
203
|
+
try {
|
|
204
|
+
const config = JSON.parse(readFileSync(join(projectDir, PROJECT_CONFIG), 'utf8'));
|
|
205
|
+
setting = config?.shareRequests;
|
|
206
|
+
}
|
|
207
|
+
catch {
|
|
208
|
+
setting = undefined;
|
|
209
|
+
}
|
|
210
|
+
if (false === setting)
|
|
211
|
+
return false;
|
|
212
|
+
if (true === setting)
|
|
213
|
+
return true;
|
|
214
|
+
// Linked by `reticle connect` (cloud.json), or by a key in the environment, the way CI syncs.
|
|
215
|
+
return (existsSync(join(reticleRoot, ReticleDir.CLOUD_LINK_FILE)) ||
|
|
216
|
+
apiKeyFrom(process.env) !== undefined);
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* Keep the user's request, and every statement in it and in the declared intents, classified and
|
|
220
|
+
* attributed, for the runs that verify it. Secrets are redacted before anything is written.
|
|
221
|
+
*
|
|
222
|
+
* Shared with the platform from a linked project unless `.reticle.json` says `"shareRequests": false`.
|
|
223
|
+
*/
|
|
224
|
+
async function recordRequest(deps, root, request, declared) {
|
|
225
|
+
const text = redactSecrets(request, MAX_REQUEST);
|
|
226
|
+
const agentStatements = declared.flatMap((d) => {
|
|
227
|
+
const statement = d?.statement;
|
|
228
|
+
return 'string' === typeof statement ? [statement] : [];
|
|
229
|
+
});
|
|
230
|
+
const context = {
|
|
231
|
+
request: text,
|
|
232
|
+
statements: [
|
|
233
|
+
...splitStatements(text).map((t) => ({ text: t, source: StatementSource.USER })),
|
|
234
|
+
...agentStatements.map((t) => ({
|
|
235
|
+
text: redactSecrets(t, MAX_STATEMENT),
|
|
236
|
+
source: StatementSource.AGENT,
|
|
237
|
+
})),
|
|
238
|
+
]
|
|
239
|
+
.slice(0, MAX_STATEMENTS)
|
|
240
|
+
.map((s) => ({
|
|
241
|
+
...s,
|
|
242
|
+
text: s.text.slice(0, MAX_STATEMENT),
|
|
243
|
+
kind: classifyStatement(s.text),
|
|
244
|
+
})),
|
|
245
|
+
at: deps.now(),
|
|
246
|
+
shared: sharesRequests(dirname(root), root),
|
|
247
|
+
};
|
|
248
|
+
const path = reticleDirPaths(root).request;
|
|
249
|
+
await deps.fs.mkdir(dirname(path));
|
|
250
|
+
await deps.fs.writeFile(path, `${JSON.stringify(context, null, 2)}\n`);
|
|
251
|
+
}
|
|
@@ -113,13 +113,13 @@ export declare const RouteEnvelopeSchema: z.ZodObject<{
|
|
|
113
113
|
m2: number;
|
|
114
114
|
max: number;
|
|
115
115
|
};
|
|
116
|
-
|
|
116
|
+
consoleErrors: {
|
|
117
117
|
count: number;
|
|
118
118
|
mean: number;
|
|
119
119
|
m2: number;
|
|
120
120
|
max: number;
|
|
121
121
|
};
|
|
122
|
-
|
|
122
|
+
netErrors: {
|
|
123
123
|
count: number;
|
|
124
124
|
mean: number;
|
|
125
125
|
m2: number;
|
|
@@ -138,13 +138,13 @@ export declare const RouteEnvelopeSchema: z.ZodObject<{
|
|
|
138
138
|
m2: number;
|
|
139
139
|
max: number;
|
|
140
140
|
};
|
|
141
|
-
|
|
141
|
+
consoleErrors: {
|
|
142
142
|
count: number;
|
|
143
143
|
mean: number;
|
|
144
144
|
m2: number;
|
|
145
145
|
max: number;
|
|
146
146
|
};
|
|
147
|
-
|
|
147
|
+
netErrors: {
|
|
148
148
|
count: number;
|
|
149
149
|
mean: number;
|
|
150
150
|
m2: number;
|
|
@@ -167,13 +167,13 @@ export declare const RouteEnvelopeSchema: z.ZodObject<{
|
|
|
167
167
|
m2: number;
|
|
168
168
|
max: number;
|
|
169
169
|
};
|
|
170
|
-
|
|
170
|
+
consoleErrors: {
|
|
171
171
|
count: number;
|
|
172
172
|
mean: number;
|
|
173
173
|
m2: number;
|
|
174
174
|
max: number;
|
|
175
175
|
};
|
|
176
|
-
|
|
176
|
+
netErrors: {
|
|
177
177
|
count: number;
|
|
178
178
|
mean: number;
|
|
179
179
|
m2: number;
|
|
@@ -196,13 +196,13 @@ export declare const RouteEnvelopeSchema: z.ZodObject<{
|
|
|
196
196
|
m2: number;
|
|
197
197
|
max: number;
|
|
198
198
|
};
|
|
199
|
-
|
|
199
|
+
consoleErrors: {
|
|
200
200
|
count: number;
|
|
201
201
|
mean: number;
|
|
202
202
|
m2: number;
|
|
203
203
|
max: number;
|
|
204
204
|
};
|
|
205
|
-
|
|
205
|
+
netErrors: {
|
|
206
206
|
count: number;
|
|
207
207
|
mean: number;
|
|
208
208
|
m2: number;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { EventAttribution, JOURNAL_FILE_VERSION, } from '@reticlehq/core';
|
|
2
2
|
import { isSelfObservation } from './self-observation.js';
|
|
3
|
+
import { currentDrivenBy } from '../../hooks/driven-by.js';
|
|
3
4
|
import { log } from '../../log.js';
|
|
4
5
|
/**
|
|
5
6
|
* Logged the FIRST time a journal write is lost, and never again for that recorder.
|
|
@@ -65,7 +66,7 @@ export class JournalRecorder {
|
|
|
65
66
|
}
|
|
66
67
|
/** Open an attribution window. One action is active at a time (the agent drives sequentially). */
|
|
67
68
|
beginAction(actionId, tool, args) {
|
|
68
|
-
this.#active = { actionId, tool, args, tStart: this.#now() };
|
|
69
|
+
this.#active = { actionId, tool, args, tStart: this.#now(), drivenBy: currentDrivenBy() };
|
|
69
70
|
}
|
|
70
71
|
/** Close the active window: persist its buffered events, then the action record. No-op if none active. */
|
|
71
72
|
finishAction(effect, settled, settledInMs) {
|
|
@@ -86,6 +87,7 @@ export class JournalRecorder {
|
|
|
86
87
|
: { from: active.seqFrom, to: active.seqTo },
|
|
87
88
|
tRange: { from: active.tStart, to: this.#now() },
|
|
88
89
|
at: active.tStart,
|
|
90
|
+
...(active.drivenBy === undefined ? {} : { drivenBy: active.drivenBy }),
|
|
89
91
|
};
|
|
90
92
|
this.#enqueueFlush();
|
|
91
93
|
this.#chain = this.#chain
|
|
@@ -123,6 +125,10 @@ export class JournalRecorder {
|
|
|
123
125
|
// of zero, which is exactly how much time this action attributed events over.
|
|
124
126
|
tRange: { from: at, to: at },
|
|
125
127
|
at,
|
|
128
|
+
...(() => {
|
|
129
|
+
const by = currentDrivenBy();
|
|
130
|
+
return by === undefined ? {} : { drivenBy: by };
|
|
131
|
+
})(),
|
|
126
132
|
};
|
|
127
133
|
this.#enqueueFlush();
|
|
128
134
|
this.#chain = this.#chain
|
|
@@ -11,6 +11,8 @@ export declare const DEFAULT_DIFF_RETENTION = 10;
|
|
|
11
11
|
* for the one case where delivery was refused and somebody has to file the report by hand.
|
|
12
12
|
*/
|
|
13
13
|
export declare const DEFAULT_FEEDBACK_RETENTION = 20;
|
|
14
|
+
/** Harness plans kept on disk: the platform keeps every one, this machine the recent ones. */
|
|
15
|
+
export declare const DEFAULT_PLAN_RETENTION = 50;
|
|
14
16
|
/**
|
|
15
17
|
* The TOTAL the evidence tier of one `.reticle/` may occupy, across sessions, runs, overlay diffs
|
|
16
18
|
* and feedback copies together.
|
|
@@ -141,5 +143,6 @@ export declare function pruneEvidenceBudget(fs: FileSystemPort, root: string, bu
|
|
|
141
143
|
* Capped from here rather than from the writer, which lives in an area this file does not own. A
|
|
142
144
|
* retention rule is the same rule whether the directory holds journals, diffs or reports.
|
|
143
145
|
*/
|
|
146
|
+
export declare function prunePlans(fs: FileSystemPort, root: string, retention?: number): Promise<void>;
|
|
144
147
|
export declare function pruneFeedback(fs: FileSystemPort, root: string, retention?: number): Promise<void>;
|
|
145
148
|
export {};
|
|
@@ -14,6 +14,8 @@ export const DEFAULT_DIFF_RETENTION = 10;
|
|
|
14
14
|
* for the one case where delivery was refused and somebody has to file the report by hand.
|
|
15
15
|
*/
|
|
16
16
|
export const DEFAULT_FEEDBACK_RETENTION = 20;
|
|
17
|
+
/** Harness plans kept on disk: the platform keeps every one, this machine the recent ones. */
|
|
18
|
+
export const DEFAULT_PLAN_RETENTION = 50;
|
|
17
19
|
const DIFF_SUFFIX = '.diff.png';
|
|
18
20
|
const PNG_SUFFIX = '.png';
|
|
19
21
|
/**
|
|
@@ -287,6 +289,9 @@ export async function pruneEvidenceBudget(fs, root, budgetBytes = DEFAULT_EVIDEN
|
|
|
287
289
|
* Capped from here rather than from the writer, which lives in an area this file does not own. A
|
|
288
290
|
* retention rule is the same rule whether the directory holds journals, diffs or reports.
|
|
289
291
|
*/
|
|
292
|
+
export async function prunePlans(fs, root, retention = DEFAULT_PLAN_RETENTION) {
|
|
293
|
+
await pruneByRecency(fs, join(root, ReticleDir.PLANS_SUBDIR), retention);
|
|
294
|
+
}
|
|
290
295
|
export async function pruneFeedback(fs, root, retention = DEFAULT_FEEDBACK_RETENTION) {
|
|
291
296
|
await pruneByRecency(fs, join(root, ReticleDir.FEEDBACK_SUBDIR), retention);
|
|
292
297
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { pruneEvidenceBudget, pruneFeedback, pruneSessions, pruneVisualDiffs, } from './retention.js';
|
|
1
|
+
import { pruneEvidenceBudget, pruneFeedback, prunePlans, pruneSessions, pruneVisualDiffs, } from './retention.js';
|
|
2
2
|
import { DEFAULT_RETAIN } from './retain-policy.js';
|
|
3
3
|
export async function pruneWorkspace(fs, root, live, options = {}) {
|
|
4
4
|
const retain = { ...DEFAULT_RETAIN, ...options };
|
|
@@ -9,6 +9,8 @@ export async function pruneWorkspace(fs, root, live, options = {}) {
|
|
|
9
9
|
await pruneVisualDiffs(fs, root, retain.visual);
|
|
10
10
|
// Write-only local copies of reports the outbox already carries.
|
|
11
11
|
await pruneFeedback(fs, root, retain.feedback);
|
|
12
|
+
// The recent plans are what somebody reads back; the platform holds every one.
|
|
13
|
+
await prunePlans(fs, root);
|
|
12
14
|
// LAST, and the one that was missing: the only bound that limits SIZE rather than count.
|
|
13
15
|
await pruneEvidenceBudget(fs, root, retain.budgetBytes, live);
|
|
14
16
|
}
|
|
@@ -45,6 +45,8 @@ export const WORKSPACE_TIERS = {
|
|
|
45
45
|
[ReticleDir.VISUAL_SUBDIR]: localDir,
|
|
46
46
|
// Write-only: a local copy of what was already sent. The outbox is the record.
|
|
47
47
|
[ReticleDir.FEEDBACK_SUBDIR]: localDir,
|
|
48
|
+
// What the Harness planned on this machine, and how it went. The platform keeps the team's copy.
|
|
49
|
+
[ReticleDir.PLANS_SUBDIR]: localDir,
|
|
48
50
|
[ReticleDir.PROJECT_FILE]: localFile,
|
|
49
51
|
[ReticleDir.AMBIENT_FILE]: localFile,
|
|
50
52
|
[ReticleDir.ENVELOPES_FILE]: localFile,
|
|
@@ -52,6 +54,11 @@ export const WORKSPACE_TIERS = {
|
|
|
52
54
|
// What THIS machine's drives covered, and the best each level reached — the gate's ratchet.
|
|
53
55
|
[ReticleDir.COVERAGE_FILE]: localFile,
|
|
54
56
|
[ReticleDir.TIERS_FILE]: localFile,
|
|
57
|
+
// The user's request in their own words: it leaves only with `shareRequests`, never by git push.
|
|
58
|
+
[ReticleDir.REQUEST_FILE]: localFile,
|
|
59
|
+
[ReticleDir.PLATFORM_MOMENTS_FILE]: localFile,
|
|
60
|
+
// Notes a human pinned in the HUD: free text about the page. The team reads them on the platform.
|
|
61
|
+
[ReticleDir.NOTES_FILE]: localFile,
|
|
55
62
|
// The user's own record of what Reticle did for them, on THIS machine.
|
|
56
63
|
[ReticleDir.IMPACT_FILE]: localFile,
|
|
57
64
|
// This machine's conversation with the server. Its own doc comment says why committing it is
|
|
@@ -72,10 +79,6 @@ export const WORKSPACE_TIERS = {
|
|
|
72
79
|
// credential, so it is safe to commit — the same reasoning that puts `cloud.json` here while its
|
|
73
80
|
// API key stays in ~/.reticle.
|
|
74
81
|
[ReticleDir.HOOKS_FILE]: sharedFile,
|
|
75
|
-
// What a drive types into each labelled field. Committed for two reasons: a replay must send
|
|
76
|
-
// exactly what the recording sent or it is not a replay, and a value a model wrote once should be
|
|
77
|
-
// reviewable and editable by the team rather than regenerated differently on every machine.
|
|
78
|
-
[ReticleDir.FILL_VALUES_FILE]: sharedFile,
|
|
79
82
|
};
|
|
80
83
|
function namesWhere(match) {
|
|
81
84
|
return Object.entries(WORKSPACE_TIERS)
|
|
@@ -13,7 +13,7 @@ import { AmbientStore } from './ambient-store.js';
|
|
|
13
13
|
import { subjectOf } from '@reticlehq/core';
|
|
14
14
|
import { pruneWorkspace } from './on-disk/startup-maintenance.js';
|
|
15
15
|
import { buildVerificationRun } from '../../judgement/runs/artifact/build-verification-run.js';
|
|
16
|
-
import {
|
|
16
|
+
import { driveRunId, driveRunsFrom } from '../../judgement/runs/drive-run.js';
|
|
17
17
|
import { RunStore } from '../../judgement/runs/artifact/run-store.js';
|
|
18
18
|
/**
|
|
19
19
|
* Build the teardown handler the bridge fires when a session is removed. Flushes the journal first (so no
|
|
@@ -173,7 +173,7 @@ export async function recordDriveRun(deps, session) {
|
|
|
173
173
|
if (session.readJournalActions === undefined)
|
|
174
174
|
return;
|
|
175
175
|
const actions = await session.readJournalActions();
|
|
176
|
-
const
|
|
176
|
+
const inputs = driveRunsFrom(actions, {
|
|
177
177
|
// Derived from the session, NOT random. Teardown fires on every socket close, and a reconnecting
|
|
178
178
|
// tab keeps its session id and appends to the same ledger — so a drive across two page reloads
|
|
179
179
|
// would fold the whole ledger twice and publish two overlapping rows, the second a superset of
|
|
@@ -182,7 +182,7 @@ export async function recordDriveRun(deps, session) {
|
|
|
182
182
|
runId: driveRunId(session.id),
|
|
183
183
|
...(session.projectId === undefined ? {} : { projectId: session.projectId }),
|
|
184
184
|
});
|
|
185
|
-
if (
|
|
185
|
+
if (0 === inputs.length)
|
|
186
186
|
return;
|
|
187
187
|
// The protocol reaching the artifact a person actually reads. Only when the session could say
|
|
188
188
|
// where it was: a subject with no locator is not a weaker subject, it is a guess, and the
|
|
@@ -201,7 +201,9 @@ export async function recordDriveRun(deps, session) {
|
|
|
201
201
|
// used to write this artifact with no callback at all, so the evidence from a whole session sat
|
|
202
202
|
// on disk until a timer noticed — which, for the last session of the day, could be never.
|
|
203
203
|
deps.onRunPersisted === undefined ? undefined : { onWrote: deps.onRunPersisted });
|
|
204
|
-
|
|
204
|
+
for (const input of inputs) {
|
|
205
|
+
await store.write(buildVerificationRun({ ...input, ...(subject === undefined ? {} : { subject }) }, deps.now ?? (() => Date.now())));
|
|
206
|
+
}
|
|
205
207
|
}
|
|
206
208
|
/** Persist what this session drove as a replayable flow, when it declared anything provable. */
|
|
207
209
|
async function saveDrivenFlow(deps, session) {
|
|
@@ -1,5 +1,16 @@
|
|
|
1
|
+
import { type ProjectCandidate } from '@reticlehq/core/artifacts';
|
|
1
2
|
import { type ProjectId } from '@reticlehq/core';
|
|
2
3
|
import { type ArtifactRoot } from './artifact-root.js';
|
|
4
|
+
/**
|
|
5
|
+
* Every project this machine knows the directory of: discovered `.reticle.json` files first, then
|
|
6
|
+
* the user-level registry, then the dev servers the build plugin announced. Read on each call, since
|
|
7
|
+
* `init` can run in another terminal while the daemon is up.
|
|
8
|
+
*
|
|
9
|
+
* Exported because sync must cover exactly the projects runs are written into. It kept its own list
|
|
10
|
+
* without the announced dev servers, so a project known only through its running dev server had its
|
|
11
|
+
* runs written to its `.reticle/runs/` and never sent, with nothing reporting a problem.
|
|
12
|
+
*/
|
|
13
|
+
export declare function knownProjectCandidates(): ProjectCandidate[];
|
|
3
14
|
/**
|
|
4
15
|
* The directory of the project a page announced, or undefined when this machine does not know it.
|
|
5
16
|
*
|
|
@@ -10,4 +21,18 @@ import { type ArtifactRoot } from './artifact-root.js';
|
|
|
10
21
|
* own project id.
|
|
11
22
|
*/
|
|
12
23
|
export declare function projectDirectoryFor(projectId: string | undefined): string | undefined;
|
|
13
|
-
|
|
24
|
+
/** Where this resolver gets its IO answers. The defaults are the production ones. */
|
|
25
|
+
export interface ArtifactRootResolverDeps {
|
|
26
|
+
/** Every project this machine knows about. Defaults to discovery + the user registry. */
|
|
27
|
+
candidates?: () => ProjectCandidate[];
|
|
28
|
+
/** Whether the daemon's own directory is a Reticle project. Defaults to looking at disk. */
|
|
29
|
+
daemonIsProject?: (daemonRoot: string) => boolean;
|
|
30
|
+
/**
|
|
31
|
+
* The directory of the dev server that served a page origin, or undefined when unobservable.
|
|
32
|
+
* Required, not defaulted: the production answer reads daemon state and shells out, which is the
|
|
33
|
+
* wiring layer's business (`portal/session/serving-directory.ts`), and a resolver built without it
|
|
34
|
+
* would silently keep sending every worktree's artifacts to the primary checkout.
|
|
35
|
+
*/
|
|
36
|
+
servingDirectory: (origin: string) => string | undefined;
|
|
37
|
+
}
|
|
38
|
+
export declare function artifactRootResolver(daemonRoot: string, deps: ArtifactRootResolverDeps): (projectId: ProjectId | undefined, origin?: string) => ArtifactRoot;
|
|
@@ -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,12 +43,35 @@ function daemonSitsInAProject(daemonRoot) {
|
|
|
34
43
|
return false;
|
|
35
44
|
}
|
|
36
45
|
}
|
|
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
|
+
}
|
|
37
65
|
/**
|
|
38
66
|
* Every project this machine knows the directory of: discovered `.reticle.json` files first, then
|
|
39
|
-
* the user-level registry
|
|
40
|
-
* daemon is up.
|
|
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.
|
|
41
73
|
*/
|
|
42
|
-
function knownProjectCandidates() {
|
|
74
|
+
export function knownProjectCandidates() {
|
|
43
75
|
let registry = emptyProjectRegistry();
|
|
44
76
|
try {
|
|
45
77
|
const path = join(homedir(), ReticleDir.ROOT, PROJECT_REGISTRY_FILE);
|
|
@@ -58,7 +90,35 @@ function knownProjectCandidates() {
|
|
|
58
90
|
catch {
|
|
59
91
|
// Same reasoning: a diagnostic search that throws must not take a tool call with it.
|
|
60
92
|
}
|
|
61
|
-
return projectCandidatesFrom(discovery, registry);
|
|
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
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
catch {
|
|
119
|
+
/* no state directory yet: nothing has been announced */
|
|
120
|
+
}
|
|
121
|
+
return servers;
|
|
62
122
|
}
|
|
63
123
|
/**
|
|
64
124
|
* The directory of the project a page announced, or undefined when this machine does not know it.
|
|
@@ -79,14 +139,31 @@ export function projectDirectoryFor(projectId) {
|
|
|
79
139
|
// them would read the wrong app's package.json and prescribe its packages to the other.
|
|
80
140
|
return 1 === directories.size ? [...directories][0] : undefined;
|
|
81
141
|
}
|
|
82
|
-
export function artifactRootResolver(daemonRoot) {
|
|
83
|
-
const
|
|
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;
|
|
84
153
|
return (projectId, origin) => {
|
|
85
|
-
const
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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
|
+
});
|
|
166
|
+
}
|
|
90
167
|
if (resolved.reason === ArtifactRootReason.MATCHED_PROJECT)
|
|
91
168
|
return resolved;
|
|
92
169
|
// Could not name the project. The old code wrote into the daemon's directory anyway and said
|
|
@@ -98,6 +175,7 @@ export function artifactRootResolver(daemonRoot) {
|
|
|
98
175
|
daemonRoot,
|
|
99
176
|
daemonIsProject,
|
|
100
177
|
home: homedir(),
|
|
178
|
+
...(daemonProjectId === undefined ? {} : { daemonProjectId }),
|
|
101
179
|
...(projectId === undefined ? {} : { projectId }),
|
|
102
180
|
// Only reached when no project id survives the guard. Without it every app that never
|
|
103
181
|
// stamped one shares a single directory -- and that directory holds the durable half, so one
|
|
@@ -107,6 +185,9 @@ export function artifactRootResolver(daemonRoot) {
|
|
|
107
185
|
if (root !== daemonRoot) {
|
|
108
186
|
log('artifact_root_unmatched', {
|
|
109
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,
|
|
110
191
|
reason: resolved.reason,
|
|
111
192
|
root,
|
|
112
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;
|