@reticlehq/server 3.2.0 → 3.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/SKILL.md +16 -13
- package/dist/command/cli/adhoc-suite.d.ts +19 -0
- package/dist/command/cli/adhoc-suite.js +88 -0
- package/dist/command/cli/adhoc-verdict.d.ts +23 -2
- package/dist/command/cli/adhoc-verdict.js +18 -10
- package/dist/command/cli/cli-doctor.js +25 -5
- package/dist/command/cli/cli-parse-grammar.d.ts +7 -0
- package/dist/command/cli/cli-parse-grammar.js +7 -0
- package/dist/command/cli/cli-parse-verify.d.ts +2 -5
- package/dist/command/cli/cli-parse-verify.js +86 -13
- package/dist/command/cli/cli-parse.d.ts +7 -1
- package/dist/command/cli/cli-parse.js +33 -5
- package/dist/command/cli/cli-verify.d.ts +59 -0
- package/dist/command/cli/cli-verify.js +129 -17
- package/dist/command/cli/cloud-kit.d.ts +0 -2
- package/dist/command/cli/cloud-kit.js +1 -1
- package/dist/command/cli/doctor/doctor-sessions-line.d.ts +2 -1
- package/dist/command/cli/launch/cli-launch.d.ts +1 -29
- package/dist/command/cli/launch/cli-launch.js +6 -40
- package/dist/command/cli/ports/resolve/cli-port.d.ts +13 -0
- package/dist/command/cli/ports/resolve/cli-port.js +45 -1
- package/dist/command/cli/report-command.d.ts +16 -0
- package/dist/command/cli/report-command.js +97 -0
- package/dist/command/cli.js +5 -0
- package/dist/command/daemon/binding/daemon-status-probe.d.ts +30 -0
- package/dist/command/daemon/binding/daemon-status-probe.js +45 -0
- package/dist/command/daemon/binding/port-presence.d.ts +1 -1
- package/dist/command/daemon/idle-shutdown.d.ts +2 -1
- package/dist/command/setup/init/init-host.js +8 -0
- package/dist/features/crawl/crawl.d.ts +2 -0
- package/dist/features/crawl/crawl.js +1 -0
- package/dist/features/harness/jev-driver.js +6 -9
- package/dist/features/harness/platform-config.d.ts +10 -0
- package/dist/features/harness/platform-config.js +8 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +40 -95
- package/dist/judgement/capsule/capsule-store.d.ts +31 -2
- package/dist/judgement/capsule/capsule-store.js +58 -6
- package/dist/judgement/capsule/capsule.js +5 -0
- package/dist/judgement/capsule/causal-summary.d.ts +13 -1
- package/dist/judgement/capsule/causal-summary.js +7 -0
- package/dist/judgement/domain/domain-model.js +38 -9
- package/dist/judgement/outcome/assertion-integrity.d.ts +3 -3
- package/dist/judgement/phenomena/phenomena.d.ts +2 -1
- package/dist/judgement/runs/artifact/gap-report.d.ts +17 -0
- package/dist/judgement/runs/artifact/gap-report.js +46 -0
- package/dist/judgement/runs/artifact/gap-summary.d.ts +58 -0
- package/dist/judgement/runs/artifact/gap-summary.js +85 -0
- package/dist/judgement/runs/artifact/render-report.js +4 -1
- package/dist/judgement/runs/artifact/to-artifact.d.ts +7 -2
- package/dist/judgement/runs/artifact/to-artifact.js +1 -1
- package/dist/judgement/runs/context-tools.js +21 -3
- package/dist/judgement/runs/drive-run.js +7 -2
- package/dist/language/flows/annotate-notes/annotate.d.ts +0 -15
- package/dist/language/flows/annotate-notes/annotate.js +66 -18
- package/dist/language/flows/flow-anchor.d.ts +12 -0
- package/dist/language/flows/flow-anchor.js +20 -0
- package/dist/language/flows/flow-expect-grammar.d.ts +2 -2
- package/dist/language/flows/flow-expect-grammar.js +92 -11
- package/dist/language/flows/flow-intent.js +1 -1
- package/dist/language/flows/flow-learning.d.ts +3 -0
- package/dist/language/flows/flow-learning.js +39 -8
- package/dist/language/flows/flow-replay-run.d.ts +31 -9
- package/dist/language/flows/flow-replay-run.js +146 -33
- package/dist/language/flows/flow-replay-types.d.ts +9 -0
- package/dist/language/flows/flow-replay.d.ts +12 -11
- package/dist/language/flows/flow-replay.js +79 -109
- package/dist/language/flows/flow-step-runners.d.ts +26 -10
- package/dist/language/flows/flow-step-runners.js +20 -4
- package/dist/language/flows/flow-success.d.ts +21 -6
- package/dist/language/flows/flow-success.js +50 -138
- package/dist/language/flows/flows.d.ts +8 -4
- package/dist/language/flows/flows.js +11 -2
- package/dist/language/flows/heal.d.ts +9 -1
- package/dist/language/flows/heal.js +24 -2
- package/dist/language/flows/learned-sources.d.ts +21 -0
- package/dist/language/flows/learned-sources.js +84 -0
- package/dist/language/flows/recording/recording-backtrack.d.ts +0 -7
- package/dist/language/flows/recording/recording-backtrack.js +1 -1
- package/dist/language/flows/recording/replay-halt.d.ts +2 -1
- package/dist/language/flows/recording/tape/recordings.d.ts +2 -2
- package/dist/language/flows/replay.js +12 -8
- package/dist/language/flows/stores/annotation-store.d.ts +5 -5
- package/dist/language/flows/stores/assertion-tiers-store.js +2 -3
- package/dist/language/flows/stores/flake-store.js +2 -3
- package/dist/language/flows/suite/coverage.d.ts +1 -1
- package/dist/language/flows/suite/parallel-suite.d.ts +2 -1
- package/dist/language/flows/verify-change-tools.js +14 -0
- package/dist/memory/cloud/cloud-sync.d.ts +0 -2
- package/dist/memory/cloud/cloud-sync.js +1 -1
- package/dist/memory/cloud/harness-config.d.ts +2 -0
- package/dist/memory/cloud/sync-cycle.d.ts +12 -0
- package/dist/memory/cloud/sync-cycle.js +29 -4
- package/dist/memory/cloud/sync-daemon.d.ts +13 -0
- package/dist/memory/cloud/sync-daemon.js +21 -0
- package/dist/memory/cloud/sync-disk.js +11 -6
- package/dist/memory/impact/impact-store.js +6 -0
- package/dist/memory/intent/inline-intent.js +8 -3
- package/dist/memory/intent/intent-shard-store.d.ts +33 -47
- package/dist/memory/intent/intent-shard-store.js +252 -140
- package/dist/memory/intent/intent-shard.d.ts +10 -11
- package/dist/memory/intent/intent-shard.js +1 -1
- package/dist/memory/intent/intent-store.d.ts +13 -8
- package/dist/memory/intent/intent-store.js +46 -90
- package/dist/memory/intent/intent-tools.js +34 -23
- package/dist/memory/journal/ambient-store.js +2 -3
- package/dist/memory/journal/deviation-report.js +4 -1
- package/dist/memory/journal/deviation-service.js +10 -2
- package/dist/memory/journal/drive-flow.d.ts +2 -2
- package/dist/memory/journal/drive-flow.js +21 -12
- package/dist/memory/journal/envelope-store.js +2 -3
- package/dist/memory/journal/journal-recorder.d.ts +21 -0
- package/dist/memory/journal/on-disk/retain-policy.d.ts +34 -0
- package/dist/memory/journal/on-disk/retain-policy.js +44 -0
- package/dist/memory/journal/on-disk/retention.d.ts +14 -10
- package/dist/memory/journal/on-disk/retention.js +13 -12
- package/dist/memory/journal/on-disk/startup-maintenance.d.ts +42 -0
- package/dist/memory/journal/on-disk/startup-maintenance.js +14 -0
- package/dist/memory/journal/on-disk/workspace-gitignore.d.ts +0 -11
- package/dist/memory/journal/on-disk/workspace-gitignore.js +6 -45
- package/dist/memory/journal/on-disk/workspace-tiers.d.ts +52 -0
- package/dist/memory/journal/on-disk/workspace-tiers.js +88 -0
- package/dist/memory/journal/route-template.d.ts +22 -0
- package/dist/memory/journal/route-template.js +44 -0
- package/dist/memory/journal/session-end.d.ts +4 -1
- package/dist/memory/journal/session-end.js +64 -6
- package/dist/memory/journal/session-journal.d.ts +8 -1
- package/dist/memory/journal/session-journal.js +10 -0
- package/dist/memory/project/baselines.d.ts +1 -1
- package/dist/memory/project/dir/reticle-dir.d.ts +1 -1
- package/dist/memory/project/fs/fs-port.js +44 -5
- package/dist/memory/project/fs/write-atomic.d.ts +26 -0
- package/dist/memory/project/fs/write-atomic.js +48 -0
- package/dist/memory/recall/prior/connection-memory.d.ts +6 -0
- package/dist/memory/recall/prior/connection-memory.js +8 -0
- package/dist/memory/recall/registered-projects.d.ts +2 -1
- package/dist/portal/bridge/auth-failure-reason.js +5 -2
- package/dist/portal/input/real-input.js +2 -1
- package/dist/portal/pool/browser-pool.d.ts +10 -0
- package/dist/portal/pool/browser-pool.js +10 -0
- package/dist/portal/pool/playwright-launcher.js +1 -0
- package/dist/portal/pool/pool-contract.d.ts +2 -0
- package/dist/portal/pool/storage-seed.d.ts +2 -1
- package/dist/portal/pool/zero-install.d.ts +27 -0
- package/dist/portal/pool/zero-install.js +54 -0
- package/dist/portal/realm/web-realm.js +6 -0
- package/dist/portal/session/dev-server/dev-server-probe.d.ts +2 -1
- package/dist/portal/session/dev-server/served-document.d.ts +21 -0
- package/dist/portal/session/dev-server/served-document.js +57 -0
- package/dist/portal/session/facts/background-traffic.d.ts +9 -0
- package/dist/portal/session/facts/background-traffic.js +25 -0
- package/dist/portal/session/live-control-tools.js +48 -3
- package/dist/portal/session/presence/attachment-history.d.ts +13 -2
- package/dist/portal/session/session-health.d.ts +61 -0
- package/dist/portal/session/session-health.js +40 -1
- package/dist/portal/session/session-info.d.ts +6 -0
- package/dist/portal/session/session-manager.d.ts +9 -0
- package/dist/portal/session/session-manager.js +23 -2
- package/dist/portal/session/session-successor.d.ts +11 -0
- package/dist/portal/session/session-successor.js +14 -1
- package/dist/portal/session/session-verdict-facts.d.ts +28 -0
- package/dist/portal/session/session-verdict-facts.js +15 -0
- package/dist/portal/session/session.d.ts +5 -0
- package/dist/portal/session/session.js +19 -7
- package/dist/surface/mcp/mcp-proxy.js +3 -3
- package/dist/surface/mcp/proxy-idle-exit.d.ts +2 -1
- package/dist/surface/mcp/server-instructions.js +12 -1
- package/dist/surface/tools/act/already-true.d.ts +40 -0
- package/dist/surface/tools/act/already-true.js +36 -0
- package/dist/surface/tools/act/navigation/navigate-arrival.d.ts +6 -0
- package/dist/surface/tools/act/navigation/navigate-arrival.js +13 -0
- package/dist/surface/tools/act/settle-in-flight.d.ts +6 -2
- package/dist/surface/tools/act/settle-in-flight.js +29 -6
- package/dist/surface/tools/act-capsule.js +22 -7
- package/dist/surface/tools/act-tools.js +25 -24
- package/dist/surface/tools/args/numeric-bounds.d.ts +15 -2
- package/dist/surface/tools/args/numeric-bounds.js +30 -3
- package/dist/surface/tools/assert/assert-grade.js +14 -2
- package/dist/surface/tools/assert/assert-verdict.js +30 -4
- package/dist/surface/tools/browser-tools.js +4 -3
- package/dist/surface/tools/dynamic-tools.js +6 -0
- package/dist/surface/tools/feedback-tools.d.ts +2 -1
- package/dist/surface/tools/feedback-tools.js +1 -1
- package/dist/surface/tools/invoke-tool.js +17 -4
- package/dist/surface/tools/lease-hint.d.ts +8 -0
- package/dist/surface/tools/lease-hint.js +10 -0
- package/dist/surface/tools/lease-tools.d.ts +16 -28
- package/dist/surface/tools/lease-tools.js +53 -5
- package/dist/surface/tools/lineage-tools.js +1 -1
- package/dist/surface/tools/observe-tools.js +14 -6
- package/dist/surface/tools/real-input-attempt.d.ts +0 -5
- package/dist/surface/tools/real-input-attempt.js +1 -1
- package/dist/surface/tools/tool-kit.d.ts +5 -0
- package/dist/surface/tools/tool-kit.js +6 -1
- package/dist/surface/tools/tool-surface.js +17 -5
- package/dist/surface/tools/tools.js +64 -29
- package/dist/telemetry/feedback-local.d.ts +1 -0
- package/dist/telemetry/feedback-local.js +1 -3
- package/dist/telemetry/feedback.d.ts +1 -1
- package/dist/telemetry/hud-metrics.d.ts +32 -0
- package/dist/telemetry/hud-metrics.js +95 -0
- package/dist/telemetry/session-metrics.d.ts +5 -0
- package/dist/telemetry/session-metrics.js +15 -1
- package/dist/wire-journal.d.ts +31 -0
- package/dist/wire-journal.js +100 -0
- package/docs/README.md +1 -0
- package/docs/cli/cloud.mdx +9 -7
- package/docs/cli/init.mdx +1 -0
- package/docs/cli/report.mdx +44 -0
- package/docs/desktop.mdx +1 -1
- package/docs/flows.md +11 -0
- package/docs/frameworks.mdx +113 -1
- package/docs/install-agentic.mdx +36 -0
- package/docs/install-manual.mdx +1 -1
- package/docs/packages/tauri.mdx +1 -1
- package/docs/packages.mdx +1 -1
- package/docs/predicates.mdx +128 -4
- package/docs/tools/query.mdx +10 -10
- package/docs/usage.md +1 -1
- package/docs/what-is-recorded.md +62 -0
- package/package.json +6 -6
- package/dist/judgement/outcome/predicate-to-expect.d.ts +0 -44
- package/dist/judgement/outcome/predicate-to-expect.js +0 -148
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `reticle report [--session <id>] [--hook]` — what this project's latest session claimed, and what held.
|
|
3
|
+
*
|
|
4
|
+
* Read from disk, with no daemon: the journal is on disk, and a Stop hook has to answer in the second
|
|
5
|
+
* after the agent stops, when there may be nothing running to ask. The fold is `gapSummary`, the same
|
|
6
|
+
* one `reticle_context` serves, so the terminal and the agent cannot be told two different stories.
|
|
7
|
+
*/
|
|
8
|
+
import { execFileSync } from 'node:child_process';
|
|
9
|
+
import { existsSync, readdirSync, statSync } from 'node:fs';
|
|
10
|
+
import { join } from 'node:path';
|
|
11
|
+
import { DiscoveryInvite, ReticleDir } from '@reticlehq/core';
|
|
12
|
+
import { gapSummary } from '../../judgement/runs/artifact/gap-summary.js';
|
|
13
|
+
import { SessionJournal } from '../../memory/journal/session-journal.js';
|
|
14
|
+
import { createNodeFileSystem } from '../../memory/project/fs/fs-port.js';
|
|
15
|
+
import { gapHookLine, gapReportLines } from '../../judgement/runs/artifact/gap-report.js';
|
|
16
|
+
/**
|
|
17
|
+
* A session last written longer ago than this is not the turn a hook is asking about.
|
|
18
|
+
*
|
|
19
|
+
* ponytail: a fixed window, not a turn boundary — the Stop hook is not told when the turn began. A
|
|
20
|
+
* drive older than an hour reads as "no claim this session", which errs toward saying so.
|
|
21
|
+
*/
|
|
22
|
+
const HOOK_WINDOW_MS = 60 * 60 * 1000;
|
|
23
|
+
/** The session whose action ledger was written most recently, if any. */
|
|
24
|
+
function newestSession(root) {
|
|
25
|
+
const sessionsDir = join(root, ReticleDir.SESSIONS_SUBDIR);
|
|
26
|
+
if (!existsSync(sessionsDir))
|
|
27
|
+
return undefined;
|
|
28
|
+
let newest;
|
|
29
|
+
for (const id of readdirSync(sessionsDir)) {
|
|
30
|
+
const ledger = join(sessionsDir, id, ReticleDir.JOURNAL_ACTIONS_FILE);
|
|
31
|
+
if (!existsSync(ledger))
|
|
32
|
+
continue;
|
|
33
|
+
const writtenAt = statSync(ledger).mtimeMs;
|
|
34
|
+
if (newest === undefined || writtenAt > newest.writtenAt)
|
|
35
|
+
newest = { id, writtenAt };
|
|
36
|
+
}
|
|
37
|
+
return newest;
|
|
38
|
+
}
|
|
39
|
+
export async function reportFor(input) {
|
|
40
|
+
if (true === input.hook && !input.treeChanged)
|
|
41
|
+
return { lines: [] };
|
|
42
|
+
const root = join(input.cwd, ReticleDir.ROOT);
|
|
43
|
+
const found = input.session === undefined ? newestSession(root) : { id: input.session, writtenAt: input.now };
|
|
44
|
+
const current = found !== undefined && (true !== input.hook || input.now - found.writtenAt <= HOOK_WINDOW_MS)
|
|
45
|
+
? found
|
|
46
|
+
: undefined;
|
|
47
|
+
const actions = current === undefined
|
|
48
|
+
? []
|
|
49
|
+
: await new SessionJournal(createNodeFileSystem(), root, current.id).readActions();
|
|
50
|
+
const gap = gapSummary(actions);
|
|
51
|
+
if (true === input.hook) {
|
|
52
|
+
const line = gapHookLine(gap);
|
|
53
|
+
// A finish with nothing proved is the moment Reticle let somebody down: say who to tell.
|
|
54
|
+
return { lines: line === undefined ? [] : [line, DiscoveryInvite.HUMAN] };
|
|
55
|
+
}
|
|
56
|
+
const header = current === undefined ? [] : [`session ${current.id}`];
|
|
57
|
+
return { lines: [...header, ...gapReportLines(gap), DiscoveryInvite.HUMAN] };
|
|
58
|
+
}
|
|
59
|
+
/** Uncommitted changes in `cwd`. Outside a git repository nothing can say, so it answers yes. */
|
|
60
|
+
function treeChanged(cwd) {
|
|
61
|
+
try {
|
|
62
|
+
return '' !== execFileSync('git', ['status', '--porcelain'], { cwd, encoding: 'utf8' }).trim();
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
return true;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* The command. As a hook it prints Claude Code's `systemMessage` shape and always exits 0: it is
|
|
70
|
+
* print-only by design, so the human sees the gap and the agent is never blocked by it.
|
|
71
|
+
*/
|
|
72
|
+
export async function handleReport(session, hook) {
|
|
73
|
+
const cwd = process.cwd();
|
|
74
|
+
let lines;
|
|
75
|
+
try {
|
|
76
|
+
({ lines } = await reportFor({
|
|
77
|
+
cwd,
|
|
78
|
+
now: Date.now(),
|
|
79
|
+
treeChanged: hook ? treeChanged(cwd) : true,
|
|
80
|
+
...(session === undefined ? {} : { session }),
|
|
81
|
+
hook,
|
|
82
|
+
}));
|
|
83
|
+
}
|
|
84
|
+
catch (error) {
|
|
85
|
+
// A hook that throws turns the agent's stop into an error the user did not cause; it says nothing
|
|
86
|
+
// instead. Run by hand, the reason is the answer (an id that is not a session, a journal it
|
|
87
|
+
// could not read), so it is printed and the exit is non-zero.
|
|
88
|
+
if (hook)
|
|
89
|
+
return;
|
|
90
|
+
process.stderr.write(`reticle report: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
91
|
+
process.exitCode = 1;
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
if (0 === lines.length)
|
|
95
|
+
return;
|
|
96
|
+
process.stdout.write(hook ? `${JSON.stringify({ systemMessage: lines.join('\n') })}\n` : `${lines.join('\n')}\n`);
|
|
97
|
+
}
|
package/dist/command/cli.js
CHANGED
|
@@ -14,6 +14,7 @@ import { attachStatusFields } from '../surface/mcp/attach-memory.js';
|
|
|
14
14
|
import { splitBrainFields, withNextAction } from './cli/status-fields.js';
|
|
15
15
|
import { reticleStateHome } from './daemon/daemon.js';
|
|
16
16
|
import { handleMcp } from './cli/mcp-command.js';
|
|
17
|
+
import { handleReport } from './cli/report-command.js';
|
|
17
18
|
import { resolveDaemonForProject } from './daemon/daemon-resolve.js';
|
|
18
19
|
import { daemonStartOptions } from './cli/daemon-start-options.js';
|
|
19
20
|
import { handleWatch, handleCapsules, handleGate, loadNamedFlows, resolveChangedFiles, } from './cli/cli-flow-commands.js';
|
|
@@ -69,6 +70,7 @@ function handleInit(parsed) {
|
|
|
69
70
|
// past a missing package manager, and could not honour that while never being told about it.
|
|
70
71
|
...(parsed.url === undefined ? {} : { url: parsed.url }),
|
|
71
72
|
captureBodies: true === parsed.captureBodies,
|
|
73
|
+
hooks: true === parsed.hooks,
|
|
72
74
|
// The outcome is reported by confirmInstall instead, once it knows whether an app connected —
|
|
73
75
|
// `init` writing files was never the same thing as `init` working (#269).
|
|
74
76
|
deferOutcome: true,
|
|
@@ -669,6 +671,9 @@ export function main() {
|
|
|
669
671
|
case 'gate':
|
|
670
672
|
void handleGate(parsed.files, parsed.since, parsed.hook);
|
|
671
673
|
break;
|
|
674
|
+
case 'report':
|
|
675
|
+
void handleReport(parsed.session, parsed.hook);
|
|
676
|
+
break;
|
|
672
677
|
case 'watch':
|
|
673
678
|
handleWatch();
|
|
674
679
|
break;
|
|
@@ -1,2 +1,32 @@
|
|
|
1
1
|
/** GET the daemon's /status JSON. Resolves to the parsed body, or undefined on any failure. */
|
|
2
2
|
export declare function fetchStatus(port: number): Promise<unknown>;
|
|
3
|
+
/** One connected tab as `reticle status` reports it — the at-a-glance health line. */
|
|
4
|
+
interface StatusSession {
|
|
5
|
+
sessionId: string;
|
|
6
|
+
url: string;
|
|
7
|
+
projectId?: string;
|
|
8
|
+
throttled: boolean;
|
|
9
|
+
/**
|
|
10
|
+
* The tab is backgrounded. Reported by the daemon on every session; `false` on one too old to say,
|
|
11
|
+
* which reads as visible — the conservative answer, since it keeps the reuse this command has
|
|
12
|
+
* always done rather than opening a tab on a guess.
|
|
13
|
+
*/
|
|
14
|
+
hidden: boolean;
|
|
15
|
+
stale: boolean;
|
|
16
|
+
pendingMarks: number;
|
|
17
|
+
/** The tab is attached and answering nothing — see Session.unresponsive. Absent on an older daemon. */
|
|
18
|
+
unresponsive?: true;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Reduce the daemon's /status JSON to the compact view `reticle status` prints. Pure: narrows the
|
|
22
|
+
* untrusted wire payload (never `any`) and tolerates a missing/partial body so a malformed response
|
|
23
|
+
* degrades to "running, 0 sessions" instead of throwing.
|
|
24
|
+
*/
|
|
25
|
+
export declare function summarizeStatus(payload: unknown): {
|
|
26
|
+
sessionCount: number;
|
|
27
|
+
sessions: StatusSession[];
|
|
28
|
+
why?: string;
|
|
29
|
+
/** The same diagnosis without the differential, for a surface a person reads. */
|
|
30
|
+
whyLead?: string;
|
|
31
|
+
};
|
|
32
|
+
export {};
|
|
@@ -46,3 +46,48 @@ export function fetchStatus(port) {
|
|
|
46
46
|
});
|
|
47
47
|
});
|
|
48
48
|
}
|
|
49
|
+
/**
|
|
50
|
+
* Reduce the daemon's /status JSON to the compact view `reticle status` prints. Pure: narrows the
|
|
51
|
+
* untrusted wire payload (never `any`) and tolerates a missing/partial body so a malformed response
|
|
52
|
+
* degrades to "running, 0 sessions" instead of throwing.
|
|
53
|
+
*/
|
|
54
|
+
export function summarizeStatus(payload) {
|
|
55
|
+
if (typeof payload !== 'object' || null === payload)
|
|
56
|
+
return { sessionCount: 0, sessions: [] };
|
|
57
|
+
const obj = payload;
|
|
58
|
+
// Carried through to the printed line: with no sessions this is the whole answer, and dropping it
|
|
59
|
+
// here would silently undo the reason it is on the wire.
|
|
60
|
+
const why = 'string' === typeof obj['why'] ? obj['why'] : undefined;
|
|
61
|
+
// Absent on a daemon older than this field, which is why every reader falls back to `why`.
|
|
62
|
+
const whyLead = 'string' === typeof obj['whyLead'] ? obj['whyLead'] : undefined;
|
|
63
|
+
const raw = Array.isArray(obj['sessions']) ? obj['sessions'] : [];
|
|
64
|
+
const sessions = raw
|
|
65
|
+
.map((s) => {
|
|
66
|
+
if (typeof s !== 'object' || null === s)
|
|
67
|
+
return null;
|
|
68
|
+
const r = s;
|
|
69
|
+
const sessionId = 'string' === typeof r['sessionId'] ? r['sessionId'] : '';
|
|
70
|
+
if ('' === sessionId)
|
|
71
|
+
return null;
|
|
72
|
+
return {
|
|
73
|
+
sessionId,
|
|
74
|
+
url: 'string' === typeof r['url'] ? r['url'] : '',
|
|
75
|
+
...('string' === typeof r['projectId'] && 0 < r['projectId'].length
|
|
76
|
+
? { projectId: r['projectId'] }
|
|
77
|
+
: {}),
|
|
78
|
+
throttled: true === r['throttled'],
|
|
79
|
+
hidden: true === r['hidden'],
|
|
80
|
+
stale: true === r['stale'],
|
|
81
|
+
pendingMarks: 'number' === typeof r['pendingMarks'] ? r['pendingMarks'] : 0,
|
|
82
|
+
...(true === r['unresponsive'] ? { unresponsive: true } : {}),
|
|
83
|
+
};
|
|
84
|
+
})
|
|
85
|
+
.filter((s) => s !== null);
|
|
86
|
+
const sessionCount = 'number' === typeof obj['sessionCount'] ? obj['sessionCount'] : sessions.length;
|
|
87
|
+
return {
|
|
88
|
+
sessionCount,
|
|
89
|
+
sessions,
|
|
90
|
+
...(why === undefined ? {} : { why }),
|
|
91
|
+
...(whyLead === undefined ? {} : { whyLead }),
|
|
92
|
+
};
|
|
93
|
+
}
|
|
@@ -65,7 +65,7 @@ export declare function probePresence(port: number, probes: {
|
|
|
65
65
|
* these lets the sentence tell them apart when — and only when — the recorded pid for the port
|
|
66
66
|
* matches the process actually holding it.
|
|
67
67
|
*/
|
|
68
|
-
|
|
68
|
+
interface PortHolder {
|
|
69
69
|
/** The pid we recorded for this port, from the daemon pid file. `null` when there is none. */
|
|
70
70
|
ourPid: number | null;
|
|
71
71
|
/** The pid actually holding the port, when the caller could determine it. */
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
interface IdleShutdownOptions {
|
|
2
2
|
/** True when nothing is using the daemon: no agent connected, no browser session, no pool lease. */
|
|
3
3
|
isIdle: () => boolean;
|
|
4
4
|
/** Clean teardown + process exit. Called at most once. */
|
|
@@ -52,3 +52,4 @@ export declare function resolveIdleCheckMs(raw: string | undefined): number;
|
|
|
52
52
|
* broken.
|
|
53
53
|
*/
|
|
54
54
|
export declare function resolveIdleShutdownMs(raw: string | undefined): number;
|
|
55
|
+
export {};
|
|
@@ -3,6 +3,9 @@ import { reportInitOutcome } from '../../../telemetry/init-telemetry.js';
|
|
|
3
3
|
import { reportOnboardingStep } from '../../../telemetry/onboarding-funnel.js';
|
|
4
4
|
import { defaultPairingTokenDir, readOrCreatePairingTokenSync, } from '../../../portal/bridge/pairing-token.js';
|
|
5
5
|
import { declaredInstallSource } from '../../../telemetry/install-source.js';
|
|
6
|
+
/** The variable Claude Code sets on every process it spawns, and the value it sets it to. */
|
|
7
|
+
const CLAUDE_CODE_MARKER_ENV = 'CLAUDECODE';
|
|
8
|
+
const CLAUDE_CODE_MARKER_VALUE = '1';
|
|
6
9
|
export function serverInitHost() {
|
|
7
10
|
return {
|
|
8
11
|
span(name, fields, fn) {
|
|
@@ -33,5 +36,10 @@ export function serverInitHost() {
|
|
|
33
36
|
installSource() {
|
|
34
37
|
return declaredInstallSource();
|
|
35
38
|
},
|
|
39
|
+
// Claude Code marks every process it spawns. If the marker ever changes, init falls back to the
|
|
40
|
+
// notice it printed before, which is the safe direction.
|
|
41
|
+
insideClaudeCode() {
|
|
42
|
+
return CLAUDE_CODE_MARKER_VALUE === process.env[CLAUDE_CODE_MARKER_ENV];
|
|
43
|
+
},
|
|
36
44
|
};
|
|
37
45
|
}
|
|
@@ -16,6 +16,8 @@ export interface CrawlSession {
|
|
|
16
16
|
/** The page under test, so a third-party beacon cannot be reported against a control. Optional
|
|
17
17
|
* for the same reason `currentDocumentId` is. */
|
|
18
18
|
readonly url?: string | undefined;
|
|
19
|
+
/** Same-origin endpoints the project declared as background. Optional like `url`. */
|
|
20
|
+
readonly background?: readonly string[] | undefined;
|
|
19
21
|
/**
|
|
20
22
|
* Is the page BACKGROUNDED? A hidden tab has its rAF clamped and the DOM observer flushes on rAF,
|
|
21
23
|
* so real renders emit nothing and every absence-derived contradiction reads that silence as a
|
|
@@ -347,6 +347,7 @@ export async function crawl(session, opts, sleep) {
|
|
|
347
347
|
currentDocumentId: session.currentDocumentId,
|
|
348
348
|
currentEditEpoch: session.currentEditEpoch,
|
|
349
349
|
appOrigin: session.url,
|
|
350
|
+
background: session.background,
|
|
350
351
|
// A hidden tab cannot be observed, so its silence is not evidence. Stated from the session
|
|
351
352
|
// rather than left to a heartbeat landing inside a 300ms window.
|
|
352
353
|
pageHidden: session.throttled?.(),
|
|
@@ -496,12 +496,10 @@ export function jevDriver(options) {
|
|
|
496
496
|
const unsaved = drive.unsaved[0];
|
|
497
497
|
if (unsaved !== undefined)
|
|
498
498
|
return only(
|
|
499
|
-
//
|
|
500
|
-
//
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
intent: `Autonomous coverage drive of ${slugAfter(unsaved)}: ${String(drive.acted.length)} actions.`,
|
|
504
|
-
}), `save ${unsaved}`);
|
|
499
|
+
// No intent. A drive has no business rule of its own to state, and what it used to send
|
|
500
|
+
// ("Autonomous coverage drive: N actions") was a count of what it did, filed in the ledger
|
|
501
|
+
// as a rule and re-amended every time the count changed.
|
|
502
|
+
request(Tool.FLOW_SAVE, { flowName: unsaved }), `save ${unsaved}`);
|
|
505
503
|
/*
|
|
506
504
|
* THE PLAN'S CHEAP HALF, and it runs before the model is asked anything.
|
|
507
505
|
*
|
|
@@ -640,9 +638,8 @@ export function jevDriver(options) {
|
|
|
640
638
|
ref: picked.ref,
|
|
641
639
|
action,
|
|
642
640
|
...(wasRefused ? { args: { [DANGEROUS_ARG]: true } } : {}),
|
|
643
|
-
//
|
|
644
|
-
//
|
|
645
|
-
intent: `${action} ${picked.desc}`,
|
|
641
|
+
// No `intent`: "click button \"Save\"" describes a step, not a rule, and the ledger refuses it.
|
|
642
|
+
// The recorded step keeps the element's role and name, which is what reads back as a journey.
|
|
646
643
|
...(consequence === undefined ? {} : { until: consequence.predicate }),
|
|
647
644
|
};
|
|
648
645
|
if ('fill' === action) {
|
|
@@ -30,6 +30,16 @@ export interface PlatformModelConfig {
|
|
|
30
30
|
* report "you turned this off" to somebody whose free months quietly ran out.
|
|
31
31
|
*/
|
|
32
32
|
harnessEntitled: boolean;
|
|
33
|
+
/**
|
|
34
|
+
* Whether the platform actually holds a key for the provider this project is pointed at.
|
|
35
|
+
*
|
|
36
|
+
* Entitlement and readiness are different facts and the HUD needs both. A workspace can be fully
|
|
37
|
+
* entitled -- claimed period, `harnessEnabled` on -- against a deployment that has no provider key
|
|
38
|
+
* configured, and every signal then reads healthy while a drive cannot run. Absent reads as READY,
|
|
39
|
+
* on the same rule as the two booleans above: an older API that does not report availability must
|
|
40
|
+
* not have its silence treated as a refusal.
|
|
41
|
+
*/
|
|
42
|
+
providerReady: boolean;
|
|
33
43
|
}
|
|
34
44
|
/** A GET, narrowed to what this file uses, so a test can answer it without a network. */
|
|
35
45
|
export type ConfigFetch = (url: string, init: {
|
|
@@ -41,10 +41,18 @@ function parse(body) {
|
|
|
41
41
|
// Same rule for both, for the same reason: an older API that reports neither must not have its
|
|
42
42
|
// silence read as a refusal.
|
|
43
43
|
const entitled = record['harnessEntitled'];
|
|
44
|
+
// `available` is keyed by provider and says which ones the platform holds a key for. Read only
|
|
45
|
+
// the entry for the provider this project uses: another provider being configured says nothing
|
|
46
|
+
// about whether THIS one can drive.
|
|
47
|
+
const available = record['available'];
|
|
48
|
+
const ready = 'object' === typeof available && null !== available
|
|
49
|
+
? available[provider]
|
|
50
|
+
: undefined;
|
|
44
51
|
return {
|
|
45
52
|
provider,
|
|
46
53
|
harnessEnabled: 'boolean' === typeof enabled ? enabled : true,
|
|
47
54
|
harnessEntitled: 'boolean' === typeof entitled ? entitled : true,
|
|
55
|
+
providerReady: 'boolean' === typeof ready ? ready : true,
|
|
48
56
|
};
|
|
49
57
|
}
|
|
50
58
|
catch {
|
package/dist/index.d.ts
CHANGED
|
@@ -21,7 +21,7 @@ export type { RecordedStep, CompiledProgram } from './language/flows/recording/t
|
|
|
21
21
|
export { FlowStore, recordedStepToFlowStep } from './language/flows/flows.js';
|
|
22
22
|
export type { FlowResult } from './language/flows/flows.js';
|
|
23
23
|
export type { Clock } from './machine/clock.js';
|
|
24
|
-
export { assertSuccess,
|
|
24
|
+
export { assertSuccess, dynamicTestids, successLabel, successToPredicate, } from './language/flows/flow-success.js';
|
|
25
25
|
export { classifyFlowAssertions, FlowAssertionGrade } from './language/flows/flow-classify.js';
|
|
26
26
|
export type { FlowAssertionClassification } from './language/flows/flow-classify.js';
|
|
27
27
|
export { buildDomainModel } from './judgement/domain/domain-model.js';
|
package/dist/index.js
CHANGED
|
@@ -11,7 +11,7 @@ import { PROJECT_REGISTRY_FILE, parseProjectRegistry, projectCandidates, } from
|
|
|
11
11
|
import { discoverProjectConfigs } from './command/cli/config/config-discovery.js';
|
|
12
12
|
import { artifactRootResolver } from './memory/project/artifact-root-resolver.js';
|
|
13
13
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
14
|
-
import { AGENT_STOPPED_NOTICE, RETICLE_DEFAULT_PORT, ReticleCommand, ReticleDir, ReticleEnv, LOOPBACK_HOST, ReplayStatus, EventType, } from '@reticlehq/core';
|
|
14
|
+
import { AGENT_STOPPED_NOTICE, RETICLE_DEFAULT_PORT, bridgeWsUrl, ReticleCommand, ReticleDir, ReticleEnv, LOOPBACK_HOST, ReplayStatus, EventType, } from '@reticlehq/core';
|
|
15
15
|
import { originOf } from './portal/session/session-manager.js';
|
|
16
16
|
import { setBrowserMode, BrowserMode } from './telemetry/browser-mode.js';
|
|
17
17
|
import { replayAndLearn } from './language/flows/flow-learning.js';
|
|
@@ -51,19 +51,16 @@ import { BrowserPool } from './portal/pool/browser-pool.js';
|
|
|
51
51
|
import { AGENT_ALREADY_DRIVING_ELSEWHERE, shouldGreetWithLeaseNotice, } from './portal/session/lease-visibility.js';
|
|
52
52
|
import { playwrightLauncher, resolveMaxContexts } from './portal/pool/playwright-launcher.js';
|
|
53
53
|
import { LeaseReaper } from './portal/pool/lease-reaper.js';
|
|
54
|
-
import { readJournalEnabled, readProjectId } from './command/cli/ports/resolve/cli-port.js';
|
|
55
|
-
import {
|
|
54
|
+
import { findProjectConfig, projectIdsAt, readJournalEnabled, readProjectId, } from './command/cli/ports/resolve/cli-port.js';
|
|
55
|
+
import { hasAnyProjectConnectedBefore } from './memory/recall/prior/connection-memory.js';
|
|
56
56
|
import { reticleStateHome } from './command/daemon/daemon.js';
|
|
57
57
|
import { probeChromium } from './command/cli/doctor/browser/chromium-hint.js';
|
|
58
|
-
import {
|
|
59
|
-
import { makeSessionEnd, recordDriveRun } from './memory/journal/session-end.js';
|
|
60
|
-
import { attachDriveRunFlush } from './memory/journal/drive-run-flush.js';
|
|
58
|
+
import { attachJournal } from './wire-journal.js';
|
|
61
59
|
import { reportOnboardingStep } from './telemetry/onboarding-funnel.js';
|
|
62
60
|
import { AMBIENT_RECORDING } from './language/flows/recording/tape/recordings.js';
|
|
63
|
-
import {
|
|
64
|
-
import { ensureWorkspaceGitignore } from './memory/journal/on-disk/workspace-gitignore.js';
|
|
65
|
-
import { pruneFeedback, pruneSessions, pruneVisualDiffs, } from './memory/journal/on-disk/retention.js';
|
|
61
|
+
import { readRetainPolicy } from './memory/journal/on-disk/retain-policy.js';
|
|
66
62
|
import { log } from './log.js';
|
|
63
|
+
import { readReaderBundle, zeroInstallScript as zeroInstallScriptFor, } from './portal/pool/zero-install.js';
|
|
67
64
|
/** A human-facing one-liner for a panel replay verdict — ✓ passed / ⚠ drifted / ✗ errored. */
|
|
68
65
|
function replayVerdictLine(result) {
|
|
69
66
|
if (result.status === ReplayStatus.OK)
|
|
@@ -86,7 +83,7 @@ export { runTool, SESSION_BOUND_TOOLS, SESSION_EXEMPT_TOOLS } from './surface/to
|
|
|
86
83
|
export { BaselineStore, normalizeLines, diffLines } from './memory/project/baselines.js';
|
|
87
84
|
export { RecordingStore } from './language/flows/recording/tape/recordings.js';
|
|
88
85
|
export { FlowStore, recordedStepToFlowStep } from './language/flows/flows.js';
|
|
89
|
-
export { assertSuccess,
|
|
86
|
+
export { assertSuccess, dynamicTestids, successLabel, successToPredicate, } from './language/flows/flow-success.js';
|
|
90
87
|
export { classifyFlowAssertions, FlowAssertionGrade } from './language/flows/flow-classify.js';
|
|
91
88
|
export { buildDomainModel } from './judgement/domain/domain-model.js';
|
|
92
89
|
export { ProjectStore } from './memory/project/project-store.js';
|
|
@@ -108,12 +105,29 @@ export { resolveBridgeSecurity } from './portal/bridge/bridge-security.js';
|
|
|
108
105
|
* Build the shared browser pool (one headless Chromium, N capped isolated leased contexts). Lazy —
|
|
109
106
|
* no Chromium launches until the first lease — so creating it is free even when never used.
|
|
110
107
|
*/
|
|
111
|
-
|
|
108
|
+
/** Where the zero-install reader connects: this daemon's own bridge, with its pairing token. */
|
|
109
|
+
function readerConnect(port, token) {
|
|
110
|
+
return { url: bridgeWsUrl(port), ...(token === undefined ? {} : { token }) };
|
|
111
|
+
}
|
|
112
|
+
function createBrowserPool(headless, reader) {
|
|
112
113
|
const maxContexts = resolveMaxContexts(process.env[ReticleEnv.MAX_CONTEXTS], cpus().length);
|
|
113
114
|
const genSessionId = () => `lease-${'function' === typeof globalThis.crypto?.randomUUID
|
|
114
115
|
? globalThis.crypto.randomUUID()
|
|
115
116
|
: String(Date.now())}`;
|
|
116
|
-
|
|
117
|
+
// Built on first use and kept: the page that needs it is one whose app never connected.
|
|
118
|
+
let script;
|
|
119
|
+
const zeroInstallScript = () => {
|
|
120
|
+
if (script === undefined) {
|
|
121
|
+
const bundle = readReaderBundle();
|
|
122
|
+
script = bundle === undefined ? null : zeroInstallScriptFor(bundle, reader);
|
|
123
|
+
}
|
|
124
|
+
return script ?? undefined;
|
|
125
|
+
};
|
|
126
|
+
return new BrowserPool(playwrightLauncher({ headless }), {
|
|
127
|
+
maxContexts,
|
|
128
|
+
genSessionId,
|
|
129
|
+
zeroInstallScript,
|
|
130
|
+
});
|
|
117
131
|
}
|
|
118
132
|
/**
|
|
119
133
|
* Route CDP-authoritative network detail onto the driven session's journal.
|
|
@@ -153,82 +167,6 @@ function makeNetworkDetailRouter(bridge, driveUrl) {
|
|
|
153
167
|
}
|
|
154
168
|
};
|
|
155
169
|
}
|
|
156
|
-
/**
|
|
157
|
-
* Wire journal capture, ambient seeding and the journal-tail flush onto a bridge.
|
|
158
|
-
*
|
|
159
|
-
* Both entry points need all three, and both used to hand-roll them. `startDaemon` only ever wired the
|
|
160
|
-
* first, so on the path every user actually takes (`reticle serve` / `reticle mcp`) the journal tail was
|
|
161
|
-
* dropped at session end and the learned ambient map was never persisted OR seeded — meaning ambient
|
|
162
|
-
* learning could not converge across sessions and the last events of every session were lost. The two
|
|
163
|
-
* call sites had already drifted once before, which is why this is one function rather than a
|
|
164
|
-
* copy-paste both are asked to keep in step.
|
|
165
|
-
*/
|
|
166
|
-
function attachJournal(bridge, deps) {
|
|
167
|
-
const journalAttach = makeJournalAttach(deps);
|
|
168
|
-
const ambientStore = new AmbientStore(deps.fs, deps.reticleRoot);
|
|
169
|
-
// Built once, not per session: the resolver walks config discovery and the user-level registry,
|
|
170
|
-
// and neither changes between two tabs connecting a second apart.
|
|
171
|
-
const resolveArtifactRoot = artifactRootResolver(deps.reticleRoot);
|
|
172
|
-
bridge.attachSessionCreate((session) => {
|
|
173
|
-
// Stamp the project's own `.reticle` before ANY counter fires for this session. Without it every
|
|
174
|
-
// verdict is recorded against wherever the daemon was started, which is how one app's evidence
|
|
175
|
-
// reached a different account's production dashboard.
|
|
176
|
-
// The origin is passed for the case where the page never stamped a project id: it is the only
|
|
177
|
-
// distinguishing fact left, and without it every such app shares one bucket.
|
|
178
|
-
session.artifactRoot = resolveArtifactRoot(session.projectId, originOf(session.url)).root;
|
|
179
|
-
// Here rather than on the start path, which was neither the moment we were about to write into a
|
|
180
|
-
// repository nor the root we were about to write into: it created `.reticle/` — holding nothing
|
|
181
|
-
// but the ignore file — wherever the daemon was launched, coming back every boot after the user
|
|
182
|
-
// deleted it, while the journals this ignore protects landed in another tree, uncovered.
|
|
183
|
-
if (deps.enabled)
|
|
184
|
-
void ensureWorkspaceGitignore(deps.fs, session.artifactRoot);
|
|
185
|
-
journalAttach(session);
|
|
186
|
-
// Seed the learned ambient map so a fresh session starts knowing which regions churn, instead of
|
|
187
|
-
// re-learning from zero. Best-effort + async: a late seed still helps, a failure is silent.
|
|
188
|
-
if (deps.enabled) {
|
|
189
|
-
void ambientStore
|
|
190
|
-
.load()
|
|
191
|
-
.then((counts) => session.seedAmbient(counts))
|
|
192
|
-
.catch(() => undefined);
|
|
193
|
-
}
|
|
194
|
-
});
|
|
195
|
-
// Teardown: flush the journal tail to disk + persist what this session learned.
|
|
196
|
-
bridge.attachSessionEnd(makeSessionEnd({
|
|
197
|
-
...deps,
|
|
198
|
-
// Retention runs from teardown, and it must not delete the journal of a session that is still
|
|
199
|
-
// being written. The registry is the only thing that knows which those are.
|
|
200
|
-
liveSessionIds: () => new Set(bridge.sessions.all().map((s) => s.id)),
|
|
201
|
-
}));
|
|
202
|
-
/*
|
|
203
|
-
* And the same write, DURING the session rather than only at the end of it.
|
|
204
|
-
*
|
|
205
|
-
* Teardown was the only writer of a run artifact, so every verdict produced while a tab stayed
|
|
206
|
-
* open was invisible to cloud sync — which can only push artifacts that exist. Reported as "sync
|
|
207
|
-
* is not happening" and diagnosed as the agent forgetting to sync; there is no sync command to
|
|
208
|
-
* forget. The evidence simply was not on disk yet.
|
|
209
|
-
*
|
|
210
|
-
* Safe to call repeatedly because the run id is derived from the session, so this rewrites one
|
|
211
|
-
* artifact rather than accumulating them — a property `recordDriveRun` already had, for the
|
|
212
|
-
* unrelated reason that a reconnecting tab must not publish two overlapping rows.
|
|
213
|
-
*/
|
|
214
|
-
if (deps.enabled) {
|
|
215
|
-
attachDriveRunFlush({
|
|
216
|
-
resolve: (sessionId) => bridge.sessions.get(sessionId),
|
|
217
|
-
write: (session) => recordDriveRun(deps, session),
|
|
218
|
-
});
|
|
219
|
-
}
|
|
220
|
-
if (deps.enabled) {
|
|
221
|
-
// Empty in the ordinary case (nothing has connected yet), but this path also runs on a daemon
|
|
222
|
-
// that is already serving sessions.
|
|
223
|
-
void pruneSessions(deps.fs, deps.reticleRoot, {
|
|
224
|
-
live: new Set(bridge.sessions.all().map((s) => s.id)),
|
|
225
|
-
});
|
|
226
|
-
// The largest thing in the workspace, and until now the only one with no delete path at all.
|
|
227
|
-
void pruneVisualDiffs(deps.fs, deps.reticleRoot);
|
|
228
|
-
// Write-only local copies of reports the outbox already carries.
|
|
229
|
-
void pruneFeedback(deps.fs, deps.reticleRoot);
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
170
|
/**
|
|
233
171
|
* Resolve the drive/real-input provider from options — shared by start and startDaemon so the
|
|
234
172
|
* precedence (driveUrl launch+own → CDP attach → none) and the storageState/injectConnect plumbing
|
|
@@ -364,12 +302,16 @@ export async function start(options = {}) {
|
|
|
364
302
|
const fs = createNodeFileSystem();
|
|
365
303
|
const reticleRoot = options.reticleRoot ?? join(process.cwd(), ReticleDir.ROOT);
|
|
366
304
|
const now = options.now ?? (() => Date.now());
|
|
367
|
-
const
|
|
305
|
+
const retain = readRetainPolicy(findProjectConfig(process.cwd()));
|
|
306
|
+
// `retain.sessions: 0` says keep no journals. Honoured at CAPTURE as well as at the sweep —
|
|
307
|
+
// writing a directory in order to delete it on the way out is work nobody asked for.
|
|
308
|
+
const journalEnabled = readJournalEnabled(process.cwd(), process.env[ReticleEnv.JOURNAL]) && 0 < retain.sessions;
|
|
368
309
|
const flows = new FlowStore(fs, reticleRoot, { now });
|
|
369
310
|
attachJournal(bridge, {
|
|
370
311
|
fs,
|
|
371
312
|
reticleRoot,
|
|
372
313
|
enabled: journalEnabled,
|
|
314
|
+
retain,
|
|
373
315
|
// Bound HERE, where the recorder is already in hand: teardown gets the tape, not the recorder.
|
|
374
316
|
takeAmbientTape: () => recordings.stop(AMBIENT_RECORDING),
|
|
375
317
|
reportStep: reportOnboardingStep,
|
|
@@ -378,7 +320,7 @@ export async function start(options = {}) {
|
|
|
378
320
|
const project = new ProjectStore(fs, reticleRoot, { now });
|
|
379
321
|
attachRouteLearning(bridge, projectStoreResolver(fs, project, reticleRoot, now));
|
|
380
322
|
const annotations = new AnnotationStore();
|
|
381
|
-
pool = createBrowserPool(options.headless ?? true);
|
|
323
|
+
pool = createBrowserPool(options.headless ?? true, readerConnect(port, security.token));
|
|
382
324
|
leaseReaper = new LeaseReaper(pool);
|
|
383
325
|
leaseReaper.start();
|
|
384
326
|
/*
|
|
@@ -419,7 +361,7 @@ export async function start(options = {}) {
|
|
|
419
361
|
...(activeProjectId === undefined ? {} : { projectId: activeProjectId }),
|
|
420
362
|
};
|
|
421
363
|
const profile = resolveToolSurface(options.toolProfile);
|
|
422
|
-
const server = createMcpServer(realInput !== undefined ? { ...deps, realInput } : deps, profile,
|
|
364
|
+
const server = createMcpServer(realInput !== undefined ? { ...deps, realInput } : deps, profile, hasAnyProjectConnectedBefore(reticleStateHome(), port, projectIdsAt(process.cwd())));
|
|
423
365
|
// When the agent (the MCP client) disconnects cleanly, end every active session at once so the
|
|
424
366
|
// HUD doesn't linger. (If the agent instead KILLS this process, the WS dies and the browser
|
|
425
367
|
// self-ends via SESSION_LIFECYCLE.BRIDGE_LOST_MS — see transport.ts.)
|
|
@@ -497,7 +439,9 @@ export async function startDaemon(options = {}) {
|
|
|
497
439
|
const fs = createNodeFileSystem();
|
|
498
440
|
const reticleRoot = options.reticleRoot ?? join(process.cwd(), ReticleDir.ROOT);
|
|
499
441
|
const now = options.now ?? (() => Date.now());
|
|
500
|
-
const
|
|
442
|
+
const retain = readRetainPolicy(findProjectConfig(process.cwd()));
|
|
443
|
+
// See the sibling path: `retain.sessions: 0` stops the capture too, not only the sweep.
|
|
444
|
+
const journalEnabled = readJournalEnabled(process.cwd(), process.env[ReticleEnv.JOURNAL]) && 0 < retain.sessions;
|
|
501
445
|
const flows = new FlowStore(fs, reticleRoot, { now });
|
|
502
446
|
// Built here rather than inside `deps` below, so teardown can save what a drive recorded. Both
|
|
503
447
|
// paths pass the same pair — `daemon-parity.test.ts` is what keeps them from drifting apart.
|
|
@@ -514,6 +458,7 @@ export async function startDaemon(options = {}) {
|
|
|
514
458
|
fs,
|
|
515
459
|
reticleRoot,
|
|
516
460
|
enabled: journalEnabled,
|
|
461
|
+
retain,
|
|
517
462
|
takeAmbientTape: () => recordings.stop(AMBIENT_RECORDING),
|
|
518
463
|
reportStep: reportOnboardingStep,
|
|
519
464
|
flows,
|
|
@@ -522,7 +467,7 @@ export async function startDaemon(options = {}) {
|
|
|
522
467
|
const project = new ProjectStore(fs, reticleRoot, { now });
|
|
523
468
|
attachRouteLearning(bridge, projectStoreResolver(fs, project, reticleRoot, now));
|
|
524
469
|
const annotations = new AnnotationStore();
|
|
525
|
-
const pool = createBrowserPool(options.headless ?? true);
|
|
470
|
+
const pool = createBrowserPool(options.headless ?? true, readerConnect(port, security.token));
|
|
526
471
|
const leaseReaper = new LeaseReaper(pool);
|
|
527
472
|
leaseReaper.start();
|
|
528
473
|
/*
|
|
@@ -596,7 +541,7 @@ export async function startDaemon(options = {}) {
|
|
|
596
541
|
const effectiveDeps = realInput !== undefined ? { ...deps, realInput } : deps;
|
|
597
542
|
// Read per attach, not once: a project that gets wired while this daemon is alive should stop
|
|
598
543
|
// being told to wire itself on the next agent that connects.
|
|
599
|
-
shared.attachMcp(() => createMcpServer(effectiveDeps, profile,
|
|
544
|
+
shared.attachMcp(() => createMcpServer(effectiveDeps, profile, hasAnyProjectConnectedBefore(reticleStateHome(), port, projectIdsAt(process.cwd()))));
|
|
600
545
|
// `reticle drive <url>` when this daemon already owns the port: it asks HERE instead of trying to
|
|
601
546
|
// bind a port we are holding, and gets the same pooled context an agent's reticle_lease returns —
|
|
602
547
|
// through runTool, so it is counted and reported like any other call rather than being a second,
|
|
@@ -698,7 +643,7 @@ export async function startDaemon(options = {}) {
|
|
|
698
643
|
if (vh !== undefined)
|
|
699
644
|
await new Promise((resolve) => vh.server.close(() => resolve()));
|
|
700
645
|
leaseReaper.stop();
|
|
701
|
-
cloudSync.stop()
|
|
646
|
+
await cloudSync.flush(); // not stop(): the last run written is the one nobody has yet
|
|
702
647
|
await loopbackAlias.close?.();
|
|
703
648
|
await pool.shutdown();
|
|
704
649
|
await owned?.dispose();
|