@reticlehq/server 3.1.0 → 3.2.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 +17 -18
- package/dist/command/cli/auth/device-flow.d.ts +2 -2
- package/dist/command/cli/cli-doctor.js +13 -1
- package/dist/command/cli/cli-flow-commands.d.ts +2 -1
- package/dist/command/cli/cli-kill.js +1 -1
- package/dist/command/cli/cli-parse.d.ts +2 -1
- package/dist/command/cli/cli-parse.js +5 -2
- package/dist/command/cli/cli-verify.js +2 -2
- package/dist/command/cli/cloud-cli.js +7 -6
- package/dist/command/cli/cloud-kit.js +2 -6
- package/dist/command/cli/config/config-discovery.d.ts +12 -0
- package/dist/command/cli/config/config-discovery.js +19 -0
- package/dist/command/cli/doctor/doctor-rows.d.ts +12 -0
- package/dist/command/cli/doctor/doctor-rows.js +12 -0
- package/dist/command/cli/drive/drive-command.js +1 -1
- package/dist/command/cli/launch/cli-launch.d.ts +10 -2
- package/dist/command/cli/launch/cli-launch.js +32 -39
- package/dist/command/cli/lifecycle/daemon-lifecycle.d.ts +33 -0
- package/dist/command/cli/lifecycle/daemon-lifecycle.js +198 -0
- package/dist/command/cli/mcp-command.js +5 -3
- package/dist/command/cli/ports/resolve/cli-port.d.ts +2 -1
- package/dist/command/cli/ports/resolve/cli-port.js +5 -1
- package/dist/command/cli/status/status-lines.d.ts +28 -0
- package/dist/command/cli/status/status-lines.js +117 -0
- package/dist/command/cli.d.ts +1 -1
- package/dist/command/cli.js +24 -177
- package/dist/command/daemon/binding/daemon-status-probe.d.ts +2 -0
- package/dist/command/daemon/binding/daemon-status-probe.js +48 -0
- package/dist/command/daemon/binding/loopback-reach.d.ts +42 -0
- package/dist/command/daemon/binding/loopback-reach.js +63 -0
- package/dist/command/daemon/daemon-resilience.d.ts +13 -1
- package/dist/command/daemon/daemon-resilience.js +12 -1
- package/dist/command/daemon/daemon.d.ts +2 -11
- package/dist/command/daemon/daemon.js +8 -11
- package/dist/command/daemon/lifetime/heartbeat.d.ts +8 -0
- package/dist/command/daemon/lifetime/heartbeat.js +10 -0
- package/dist/command/daemon/lifetime/log-cap.d.ts +68 -0
- package/dist/command/daemon/lifetime/log-cap.js +115 -0
- package/dist/command/setup/agent-notice-memory.d.ts +16 -0
- package/dist/command/setup/agent-notice-memory.js +65 -0
- package/dist/command/setup/bringup/dev-server-wait.d.ts +1 -0
- package/dist/command/setup/bringup/dev-server-wait.js +16 -0
- package/dist/command/setup/bringup/ensure-daemon.js +1 -1
- package/dist/command/setup/init/init-runtime.js +13 -1
- package/dist/command/setup/node-effects.js +1 -1
- package/dist/command/setup/probe/page-probe.js +9 -2
- package/dist/command/setup/run-setup.d.ts +25 -7
- package/dist/command/setup/run-setup.js +51 -9
- package/dist/command/setup/setup-command.js +31 -3
- package/dist/command/setup/terminal/confirm.js +2 -1
- package/dist/command/update/updater.d.ts +27 -0
- package/dist/command/update/updater.js +35 -2
- package/dist/features/crawl/crawl-tools.js +3 -1
- package/dist/features/crawl/crawl.js +2 -1
- package/dist/features/harness/drive-report.d.ts +73 -0
- package/dist/features/harness/drive-report.js +196 -0
- package/dist/features/harness/driver.js +29 -8
- package/dist/features/harness/drivers.d.ts +19 -0
- package/dist/features/harness/drivers.js +19 -0
- package/dist/features/harness/fill-values.d.ts +67 -0
- package/dist/features/harness/fill-values.js +153 -0
- package/dist/features/harness/jev-driver.d.ts +188 -0
- package/dist/features/harness/jev-driver.js +664 -0
- package/dist/features/harness/openai-driver.d.ts +46 -0
- package/dist/features/harness/openai-driver.js +197 -0
- package/dist/features/harness/platform-config.d.ts +52 -0
- package/dist/features/harness/platform-config.js +89 -0
- package/dist/features/visual/visual-tools.js +26 -2
- package/dist/index.js +60 -55
- package/dist/judgement/domain/domain-tools.js +4 -2
- package/dist/judgement/runs/verification-sync.d.ts +2 -2
- package/dist/judgement/runs/verification-sync.js +9 -2
- package/dist/language/flows/affected-tools.js +6 -3
- package/dist/language/flows/flow-replay-run.d.ts +25 -2
- package/dist/language/flows/flow-replay-run.js +73 -27
- package/dist/language/flows/flow-replay.d.ts +17 -0
- package/dist/language/flows/flow-replay.js +148 -100
- package/dist/language/flows/flow-result.d.ts +18 -1
- package/dist/language/flows/flow-result.js +18 -2
- package/dist/language/flows/flow-store-for-session.d.ts +2 -1
- package/dist/language/flows/flow-tools.js +8 -4
- package/dist/language/flows/flows.d.ts +8 -7
- package/dist/language/flows/flows.js +7 -3
- package/dist/language/flows/narrow-write.d.ts +4 -4
- package/dist/language/flows/replay.js +1 -1
- package/dist/language/flows/suite-selection.d.ts +15 -1
- package/dist/language/flows/suite-selection.js +1 -1
- package/dist/language/flows/verify-change-tools.js +7 -4
- package/dist/memory/cloud/account-state.d.ts +1 -1
- package/dist/memory/cloud/account-state.js +5 -4
- package/dist/memory/cloud/cloud-config.d.ts +9 -0
- package/dist/memory/cloud/cloud-config.js +9 -0
- package/dist/memory/cloud/cloud-sync.d.ts +7 -2
- package/dist/memory/cloud/cloud-sync.js +10 -5
- package/dist/memory/cloud/harness-config.d.ts +10 -0
- package/dist/memory/cloud/harness-config.js +58 -0
- package/dist/memory/cloud/harness-offer.d.ts +44 -0
- package/dist/memory/cloud/harness-offer.js +146 -0
- package/dist/memory/cloud/harness-switch.d.ts +11 -0
- package/dist/memory/cloud/harness-switch.js +53 -0
- package/dist/memory/cloud/sync-daemon.d.ts +22 -2
- package/dist/memory/cloud/sync-daemon.js +24 -0
- package/dist/memory/impact/impact-recorder.d.ts +8 -0
- package/dist/memory/impact/impact-recorder.js +2 -1
- package/dist/memory/impact/impact-store.d.ts +5 -0
- package/dist/memory/impact/impact-store.js +19 -0
- package/dist/memory/intent/intent-shard-store.js +2 -2
- package/dist/memory/intent/intent-shard.d.ts +12 -12
- package/dist/memory/journal/attach-journal.d.ts +11 -0
- package/dist/memory/journal/attach-journal.js +1 -1
- package/dist/memory/journal/journal-query.d.ts +35 -1
- package/dist/memory/journal/journal-query.js +46 -0
- package/dist/memory/journal/journal-recorder.d.ts +50 -2
- package/dist/memory/journal/journal-recorder.js +45 -7
- package/dist/memory/journal/on-disk/retention.d.ts +87 -2
- package/dist/memory/journal/on-disk/retention.js +177 -8
- package/dist/memory/journal/on-disk/workspace-gitignore.d.ts +9 -0
- package/dist/memory/journal/on-disk/workspace-gitignore.js +9 -2
- package/dist/memory/journal/self-observation.d.ts +3 -0
- package/dist/memory/journal/self-observation.js +67 -0
- package/dist/memory/journal/session-end.d.ts +13 -3
- package/dist/memory/journal/session-end.js +24 -4
- package/dist/memory/journal/session-journal.d.ts +115 -5
- package/dist/memory/journal/session-journal.js +479 -32
- package/dist/memory/project/artifact-root-resolver.d.ts +3 -0
- package/dist/memory/project/artifact-root-resolver.js +89 -0
- package/dist/memory/project/artifact-root.d.ts +39 -2
- package/dist/memory/project/artifact-root.js +58 -1
- package/dist/memory/project/dir/fill-value-store.d.ts +32 -0
- package/dist/memory/project/dir/fill-value-store.js +88 -0
- package/dist/memory/project/dir/reticle-dir.d.ts +14 -4
- package/dist/memory/project/dir/reticle-dir.js +11 -0
- package/dist/memory/project/fs/fs-port.d.ts +22 -1
- package/dist/memory/project/fs/fs-port.js +33 -5
- package/dist/memory/project/learned-routes.d.ts +16 -2
- package/dist/memory/project/learned-routes.js +26 -10
- package/dist/memory/project/project-for-root.d.ts +30 -0
- package/dist/memory/project/project-for-root.js +43 -0
- package/dist/memory/project/project-tools.js +10 -5
- package/dist/memory/project/session-root.d.ts +35 -6
- package/dist/memory/project/session-root.js +70 -8
- package/dist/portal/bridge/auth-failure-reason.d.ts +9 -0
- package/dist/portal/bridge/auth-failure-reason.js +15 -1
- package/dist/portal/bridge/bridge.d.ts +6 -1
- package/dist/portal/bridge/bridge.js +36 -2
- package/dist/portal/session/facts/session-replaced.d.ts +19 -0
- package/dist/portal/session/facts/session-replaced.js +22 -0
- package/dist/portal/session/no-session-diagnosis.d.ts +1 -0
- package/dist/portal/session/no-session-diagnosis.js +67 -60
- package/dist/portal/session/no-session-next-action.js +3 -2
- package/dist/portal/session/no-session-watch.js +14 -10
- package/dist/portal/session/rebind.d.ts +18 -0
- package/dist/portal/session/rebind.js +38 -0
- package/dist/portal/session/session-manager.d.ts +4 -0
- package/dist/portal/session/session-manager.js +19 -1
- package/dist/portal/session/session.d.ts +21 -10
- package/dist/portal/session/session.js +29 -42
- package/dist/status-payload.d.ts +12 -1
- package/dist/status-payload.js +4 -1
- package/dist/surface/http-server.js +3 -5
- package/dist/surface/mcp/faults/mcp-outage.d.ts +2 -1
- package/dist/surface/mcp/faults/mcp-outage.js +20 -4
- package/dist/surface/mcp/mcp-proxy.d.ts +10 -1
- package/dist/surface/mcp/mcp-proxy.js +20 -9
- package/dist/surface/mcp/mcp.d.ts +10 -1
- package/dist/surface/mcp/mcp.js +59 -29
- package/dist/surface/mcp/proxy/proxy-daemon-probe.d.ts +15 -2
- package/dist/surface/mcp/proxy/proxy-daemon-probe.js +22 -5
- package/dist/surface/mcp/proxy/proxy-log.d.ts +5 -11
- package/dist/surface/mcp/proxy/proxy-log.js +7 -22
- package/dist/surface/tools/act/act-preflight.js +6 -1
- package/dist/surface/tools/act/sequence-grade.js +13 -0
- package/dist/surface/tools/act-capsule.js +27 -1
- package/dist/surface/tools/assert/assert-verdict.js +44 -17
- package/dist/surface/tools/coverage-tools.js +5 -2
- package/dist/surface/tools/dynamic-tools.js +37 -46
- package/dist/surface/tools/error-recovery.d.ts +6 -0
- package/dist/surface/tools/error-recovery.js +15 -0
- package/dist/surface/tools/explore-tools.js +80 -7
- package/dist/surface/tools/feedback-tools.js +49 -2
- package/dist/surface/tools/first-sentence.d.ts +24 -0
- package/dist/surface/tools/first-sentence.js +0 -0
- package/dist/surface/tools/harness-explore.d.ts +125 -1
- package/dist/surface/tools/harness-explore.js +401 -15
- package/dist/surface/tools/harness-plan.d.ts +76 -0
- package/dist/surface/tools/harness-plan.js +156 -0
- package/dist/surface/tools/harness-toolset.js +12 -1
- package/dist/surface/tools/invoke-tool.js +65 -5
- package/dist/surface/tools/live-call-text.d.ts +6 -0
- package/dist/surface/tools/live-call-text.js +36 -2
- package/dist/surface/tools/observe-tools.js +55 -9
- package/dist/surface/tools/read/query-windowed.d.ts +1 -1
- package/dist/surface/tools/read/query-windowed.js +1 -1
- package/dist/surface/tools/read-tools.js +1 -2
- package/dist/surface/tools/tool-kit.d.ts +18 -3
- package/dist/surface/tools/tools-helpers.d.ts +8 -7
- package/dist/surface/tools/tools-helpers.js +11 -13
- package/dist/surface/tools/tools.js +6 -1
- package/dist/telemetry/bug-found.js +32 -2
- package/dist/telemetry/feedback-context.js +3 -3
- package/dist/telemetry/harness-drive.d.ts +6 -0
- package/dist/telemetry/harness-drive.js +39 -0
- package/dist/telemetry/mcp-connection.d.ts +14 -1
- package/dist/telemetry/mcp-connection.js +17 -2
- package/dist/telemetry/onboarding-firsts.d.ts +30 -1
- package/dist/telemetry/onboarding-firsts.js +54 -12
- package/dist/telemetry/verification-of.js +31 -3
- package/docs/agent-cheatsheet.md +7 -3
- package/docs/autodrive.md +42 -2
- package/docs/benchmarks.md +1 -1
- package/docs/capabilities.mdx +1 -1
- package/docs/cli/cloud.mdx +3 -3
- package/docs/cli/doctor.mdx +1 -0
- package/docs/cli/status.mdx +35 -20
- package/docs/cli.mdx +8 -18
- package/docs/desktop.mdx +1 -1
- package/docs/faq.mdx +1 -1
- package/docs/install-manual.mdx +1 -1
- package/docs/packages/tauri.mdx +1 -1
- package/docs/packages.mdx +1 -1
- package/docs/predicates.mdx +16 -0
- package/docs/system-map.md +6 -4
- package/docs/telemetry.md +2 -1
- package/docs/token-efficiency.md +2 -2
- package/docs/tools/overview.mdx +1 -1
- package/docs/troubleshooting.mdx +8 -2
- package/docs/usage.md +1 -1
- package/package.json +6 -6
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How big the daemon's log may get, and the three things that keep it there.
|
|
3
|
+
*
|
|
4
|
+
* `rotateDaemonLog` runs at spawn. `guardDaemonLog` runs while the daemon lives, because a detached
|
|
5
|
+
* daemon writes for days — a beat every thirty seconds plus every session, tool call and reconnect —
|
|
6
|
+
* and a size checked once at start is a size nothing will ever check again. A log measured at 98% of
|
|
7
|
+
* the cap and still climbing had no remaining mechanism that would trim it, and the tracker carries
|
|
8
|
+
* the same failure with a bigger number: the daemon log filling a disk, and a user who left over it.
|
|
9
|
+
* `pruneOldDaemonLogs` is the other half of the disk — one file per port ever bound, forever.
|
|
10
|
+
*
|
|
11
|
+
* Here rather than in daemon.ts because the size check has to be called from the heartbeat, and
|
|
12
|
+
* `lifetime` is a sink: nothing in it may reach back into its parent. Same reason nothing here
|
|
13
|
+
* logs — the caller that has a logger emits `DAEMON_LOG_TRUNCATED_EVENT`.
|
|
14
|
+
*/
|
|
15
|
+
/**
|
|
16
|
+
* How large a daemon log may get before it is rolled over to `<name>.1`.
|
|
17
|
+
*
|
|
18
|
+
* It was unbounded. A real dev machine reached **24MB** on one port, which is not merely untidy: it
|
|
19
|
+
* is the difference between a log somebody opens and a log somebody gives up on, and it is disk that
|
|
20
|
+
* nothing ever reclaims. One previous generation is kept, because the question people bring to this
|
|
21
|
+
* file ("what happened just now?") is answered by the current one and the question they bring next
|
|
22
|
+
* ("and just before that?") is answered by the other.
|
|
23
|
+
*/
|
|
24
|
+
export declare const MAX_DAEMON_LOG_BYTES: number;
|
|
25
|
+
/**
|
|
26
|
+
* Reclaim a log that has already run past `max`, by TRUNCATING IT IN PLACE. Returns bytes reclaimed.
|
|
27
|
+
*
|
|
28
|
+
* The one definition behind both logs Reticle writes: the daemon log, and the MCP proxy log in
|
|
29
|
+
* surface/mcp/proxy/proxy-log.ts, which delegates to this. An uncapped proxy log has filled a disk
|
|
30
|
+
* and broken unrelated builds, Docker and ordinary shell commands with ENOSPC; the daemon log is
|
|
31
|
+
* larger, longer-lived and had the same hole, so it gets the same answer rather than a second one.
|
|
32
|
+
*
|
|
33
|
+
* In place, never renamed or unlinked: a rename moves the bytes without reclaiming a byte, and a
|
|
34
|
+
* file a running process still holds open keeps its blocks allocated after an unlink until that
|
|
35
|
+
* handle closes. `: > ~/.reticle/daemon-4400.log` is the operation being copied here.
|
|
36
|
+
*
|
|
37
|
+
* Best-effort, in the same spirit as `rotateDaemonLog`: dying over housekeeping is strictly worse
|
|
38
|
+
* than a large file.
|
|
39
|
+
*/
|
|
40
|
+
export declare function recoverOversizedLog(deps: {
|
|
41
|
+
size(): number;
|
|
42
|
+
truncate(): void;
|
|
43
|
+
}, max?: number): number;
|
|
44
|
+
/** Emitted into the fresh log immediately after a truncation, so the gap explains itself. */
|
|
45
|
+
export declare const DAEMON_LOG_TRUNCATED_EVENT = "reticle_daemon_log_truncated";
|
|
46
|
+
/** Keep a RUNNING daemon's log under the cap. Returns the bytes reclaimed, 0 when it was fine. */
|
|
47
|
+
export declare function guardDaemonLog(deps?: {
|
|
48
|
+
size(): number;
|
|
49
|
+
truncate(): void;
|
|
50
|
+
}): number;
|
|
51
|
+
/**
|
|
52
|
+
* How long a daemon log outlives its daemon.
|
|
53
|
+
*
|
|
54
|
+
* Every port ever bound leaves a `daemon-<port>.log` behind and nothing removed any of them — one
|
|
55
|
+
* machine held 128 files in `~/.reticle`, logs the large majority. Nothing enumerates them either:
|
|
56
|
+
* every reader (`doctor`, the startup-cause reader, the lifecycle messages) opens the log of the
|
|
57
|
+
* port it is asking about right now, so a log for a port that will never be bound again is read by
|
|
58
|
+
* nobody. A week is long enough to still have last Tuesday's crash when somebody asks about it.
|
|
59
|
+
*/
|
|
60
|
+
export declare const DAEMON_LOG_RETENTION_MS: number;
|
|
61
|
+
/**
|
|
62
|
+
* Delete daemon logs nothing has written to in the retention window. Returns the names removed.
|
|
63
|
+
*
|
|
64
|
+
* Age is the whole test, and it doubles as the liveness probe: a live daemon beats into its log
|
|
65
|
+
* every thirty seconds, so a log older than a week has no daemon behind it by construction.
|
|
66
|
+
* Best-effort throughout — a log that will not delete is not a reason to fail a daemon start.
|
|
67
|
+
*/
|
|
68
|
+
export declare function pruneOldDaemonLogs(home: string, nowMs: number): string[];
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How big the daemon's log may get, and the three things that keep it there.
|
|
3
|
+
*
|
|
4
|
+
* `rotateDaemonLog` runs at spawn. `guardDaemonLog` runs while the daemon lives, because a detached
|
|
5
|
+
* daemon writes for days — a beat every thirty seconds plus every session, tool call and reconnect —
|
|
6
|
+
* and a size checked once at start is a size nothing will ever check again. A log measured at 98% of
|
|
7
|
+
* the cap and still climbing had no remaining mechanism that would trim it, and the tracker carries
|
|
8
|
+
* the same failure with a bigger number: the daemon log filling a disk, and a user who left over it.
|
|
9
|
+
* `pruneOldDaemonLogs` is the other half of the disk — one file per port ever bound, forever.
|
|
10
|
+
*
|
|
11
|
+
* Here rather than in daemon.ts because the size check has to be called from the heartbeat, and
|
|
12
|
+
* `lifetime` is a sink: nothing in it may reach back into its parent. Same reason nothing here
|
|
13
|
+
* logs — the caller that has a logger emits `DAEMON_LOG_TRUNCATED_EVENT`.
|
|
14
|
+
*/
|
|
15
|
+
import { fstatSync, ftruncateSync, readdirSync, statSync, unlinkSync } from 'node:fs';
|
|
16
|
+
import { join } from 'node:path';
|
|
17
|
+
/**
|
|
18
|
+
* How large a daemon log may get before it is rolled over to `<name>.1`.
|
|
19
|
+
*
|
|
20
|
+
* It was unbounded. A real dev machine reached **24MB** on one port, which is not merely untidy: it
|
|
21
|
+
* is the difference between a log somebody opens and a log somebody gives up on, and it is disk that
|
|
22
|
+
* nothing ever reclaims. One previous generation is kept, because the question people bring to this
|
|
23
|
+
* file ("what happened just now?") is answered by the current one and the question they bring next
|
|
24
|
+
* ("and just before that?") is answered by the other.
|
|
25
|
+
*/
|
|
26
|
+
export const MAX_DAEMON_LOG_BYTES = 8 * 1024 * 1024;
|
|
27
|
+
/**
|
|
28
|
+
* Reclaim a log that has already run past `max`, by TRUNCATING IT IN PLACE. Returns bytes reclaimed.
|
|
29
|
+
*
|
|
30
|
+
* The one definition behind both logs Reticle writes: the daemon log, and the MCP proxy log in
|
|
31
|
+
* surface/mcp/proxy/proxy-log.ts, which delegates to this. An uncapped proxy log has filled a disk
|
|
32
|
+
* and broken unrelated builds, Docker and ordinary shell commands with ENOSPC; the daemon log is
|
|
33
|
+
* larger, longer-lived and had the same hole, so it gets the same answer rather than a second one.
|
|
34
|
+
*
|
|
35
|
+
* In place, never renamed or unlinked: a rename moves the bytes without reclaiming a byte, and a
|
|
36
|
+
* file a running process still holds open keeps its blocks allocated after an unlink until that
|
|
37
|
+
* handle closes. `: > ~/.reticle/daemon-4400.log` is the operation being copied here.
|
|
38
|
+
*
|
|
39
|
+
* Best-effort, in the same spirit as `rotateDaemonLog`: dying over housekeeping is strictly worse
|
|
40
|
+
* than a large file.
|
|
41
|
+
*/
|
|
42
|
+
export function recoverOversizedLog(deps, max = MAX_DAEMON_LOG_BYTES) {
|
|
43
|
+
try {
|
|
44
|
+
const size = deps.size();
|
|
45
|
+
if (max >= size)
|
|
46
|
+
return 0;
|
|
47
|
+
deps.truncate();
|
|
48
|
+
return size;
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
return 0;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* The daemon's log, as the daemon itself can address it: its own stderr.
|
|
56
|
+
*
|
|
57
|
+
* `spawnDaemon` hands the child `stdio: ['ignore', logFd, logFd]`, so inside a running daemon fd 2
|
|
58
|
+
* IS `daemon-<port>.log`, opened with O_APPEND — which is what makes truncating it safe: an
|
|
59
|
+
* appending write always lands at the new end of file, so there is no sparse hole and no lost
|
|
60
|
+
* handle. No path, no port and no second open are needed to reach it. When the daemon runs in the
|
|
61
|
+
* foreground fd 2 is a terminal, whose reported size is 0, so this is a no-op there.
|
|
62
|
+
*/
|
|
63
|
+
const DAEMON_LOG_FD = 2;
|
|
64
|
+
/** Emitted into the fresh log immediately after a truncation, so the gap explains itself. */
|
|
65
|
+
export const DAEMON_LOG_TRUNCATED_EVENT = 'reticle_daemon_log_truncated';
|
|
66
|
+
const daemonLogFdOps = {
|
|
67
|
+
size: () => fstatSync(DAEMON_LOG_FD).size,
|
|
68
|
+
truncate: () => ftruncateSync(DAEMON_LOG_FD, 0),
|
|
69
|
+
};
|
|
70
|
+
/** Keep a RUNNING daemon's log under the cap. Returns the bytes reclaimed, 0 when it was fine. */
|
|
71
|
+
export function guardDaemonLog(deps = daemonLogFdOps) {
|
|
72
|
+
return recoverOversizedLog(deps);
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* How long a daemon log outlives its daemon.
|
|
76
|
+
*
|
|
77
|
+
* Every port ever bound leaves a `daemon-<port>.log` behind and nothing removed any of them — one
|
|
78
|
+
* machine held 128 files in `~/.reticle`, logs the large majority. Nothing enumerates them either:
|
|
79
|
+
* every reader (`doctor`, the startup-cause reader, the lifecycle messages) opens the log of the
|
|
80
|
+
* port it is asking about right now, so a log for a port that will never be bound again is read by
|
|
81
|
+
* nobody. A week is long enough to still have last Tuesday's crash when somebody asks about it.
|
|
82
|
+
*/
|
|
83
|
+
export const DAEMON_LOG_RETENTION_MS = 7 * 24 * 60 * 60 * 1000;
|
|
84
|
+
/**
|
|
85
|
+
* Delete daemon logs nothing has written to in the retention window. Returns the names removed.
|
|
86
|
+
*
|
|
87
|
+
* Age is the whole test, and it doubles as the liveness probe: a live daemon beats into its log
|
|
88
|
+
* every thirty seconds, so a log older than a week has no daemon behind it by construction.
|
|
89
|
+
* Best-effort throughout — a log that will not delete is not a reason to fail a daemon start.
|
|
90
|
+
*/
|
|
91
|
+
export function pruneOldDaemonLogs(home, nowMs) {
|
|
92
|
+
const pruned = [];
|
|
93
|
+
let files;
|
|
94
|
+
try {
|
|
95
|
+
files = readdirSync(home);
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
return pruned; // no ~/.reticle yet → nothing to prune
|
|
99
|
+
}
|
|
100
|
+
for (const file of files) {
|
|
101
|
+
if (null === /^daemon-\d+\.log(\.1)?$/.exec(file))
|
|
102
|
+
continue;
|
|
103
|
+
const path = join(home, file);
|
|
104
|
+
try {
|
|
105
|
+
if (DAEMON_LOG_RETENTION_MS > nowMs - statSync(path).mtimeMs)
|
|
106
|
+
continue;
|
|
107
|
+
unlinkSync(path);
|
|
108
|
+
pruned.push(file);
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
// unreadable or already gone — either way it is not this process's problem
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return pruned;
|
|
115
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/** One client we could not write to, and why. */
|
|
2
|
+
export interface ManualNotice {
|
|
3
|
+
readonly name: string;
|
|
4
|
+
readonly file: string;
|
|
5
|
+
readonly why: string;
|
|
6
|
+
}
|
|
7
|
+
/** Where the stamp lives, under the state home the caller resolves. */
|
|
8
|
+
export declare const NOTICE_MEMORY_BASENAME = "agent-notices.json";
|
|
9
|
+
/** Identity of a notice: the same client, the same file, for the same reason. */
|
|
10
|
+
export declare function noticeKey(n: ManualNotice): string;
|
|
11
|
+
/** Pure. The notices `said` has not already recorded, in order, without repeats. */
|
|
12
|
+
export declare function unsaidNotices(said: readonly string[], notices: readonly ManualNotice[]): ManualNotice[];
|
|
13
|
+
/** Every key recorded before. An unreadable or malformed stamp reads as "nothing was said". */
|
|
14
|
+
export declare function readSaid(stateHome: string): string[];
|
|
15
|
+
/** Record these keys beside whatever was there. A failed write costs a repeated line, nothing more. */
|
|
16
|
+
export declare function rememberSaid(stateHome: string, keys: readonly string[]): void;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which "add the reticle entry by hand" lines have already been said on this machine.
|
|
3
|
+
*
|
|
4
|
+
* Two commands register agents, deliberately: the installer does it because it once reached fewer
|
|
5
|
+
* clients than `init` and sent people to a project directory to finish a half-done job, and `init`
|
|
6
|
+
* does it because that is where the project gets wired. Both then narrate the clients whose config
|
|
7
|
+
* we refuse to rewrite -- a Zed settings file with comments in it, a Continue YAML somebody
|
|
8
|
+
* formatted -- so a `curl | sh` followed by `reticle init` prints the same two paragraphs twice in
|
|
9
|
+
* one sitting, and prints them again on every re-run after that, forever.
|
|
10
|
+
*
|
|
11
|
+
* The REGISTRATION stays in both places. The sentence is the thing that repeats, and it is a
|
|
12
|
+
* sentence about a MACHINE rather than about a run: a config we will not rewrite stays un-rewritten
|
|
13
|
+
* until somebody edits it, at which point the notice's own text changes and it is said again.
|
|
14
|
+
*
|
|
15
|
+
* Stamped by content, not by run. If the stamp cannot be read or written the notice simply prints,
|
|
16
|
+
* which is the failure that costs a duplicate line rather than a missed instruction.
|
|
17
|
+
*/
|
|
18
|
+
import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
19
|
+
import { join } from 'node:path';
|
|
20
|
+
/** Where the stamp lives, under the state home the caller resolves. */
|
|
21
|
+
export const NOTICE_MEMORY_BASENAME = 'agent-notices.json';
|
|
22
|
+
/** Identity of a notice: the same client, the same file, for the same reason. */
|
|
23
|
+
export function noticeKey(n) {
|
|
24
|
+
return JSON.stringify([n.name, n.file, n.why]);
|
|
25
|
+
}
|
|
26
|
+
/** Pure. The notices `said` has not already recorded, in order, without repeats. */
|
|
27
|
+
export function unsaidNotices(said, notices) {
|
|
28
|
+
const seen = new Set(said);
|
|
29
|
+
const out = [];
|
|
30
|
+
for (const n of notices) {
|
|
31
|
+
const key = noticeKey(n);
|
|
32
|
+
if (seen.has(key))
|
|
33
|
+
continue;
|
|
34
|
+
seen.add(key);
|
|
35
|
+
out.push(n);
|
|
36
|
+
}
|
|
37
|
+
return out;
|
|
38
|
+
}
|
|
39
|
+
/** Every key recorded before. An unreadable or malformed stamp reads as "nothing was said". */
|
|
40
|
+
export function readSaid(stateHome) {
|
|
41
|
+
try {
|
|
42
|
+
const raw = readFileSync(join(stateHome, NOTICE_MEMORY_BASENAME), 'utf8');
|
|
43
|
+
const parsed = JSON.parse(raw);
|
|
44
|
+
if (!Array.isArray(parsed))
|
|
45
|
+
return [];
|
|
46
|
+
return parsed.filter((k) => 'string' === typeof k);
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
return [];
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
/** Record these keys beside whatever was there. A failed write costs a repeated line, nothing more. */
|
|
53
|
+
export function rememberSaid(stateHome, keys) {
|
|
54
|
+
if (0 === keys.length)
|
|
55
|
+
return;
|
|
56
|
+
const merged = [...new Set([...readSaid(stateHome), ...keys])];
|
|
57
|
+
try {
|
|
58
|
+
mkdirSync(stateHome, { recursive: true });
|
|
59
|
+
writeFileSync(join(stateHome, NOTICE_MEMORY_BASENAME), JSON.stringify(merged, null, 2));
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
// Said once too often is the cost. Silence here is deliberate: a stamp that cannot be written
|
|
63
|
+
// must not fail an install.
|
|
64
|
+
}
|
|
65
|
+
}
|
|
@@ -56,6 +56,7 @@ export declare const WAIT_CEILING_MS: number;
|
|
|
56
56
|
* that is a live server whatever its parent did.
|
|
57
57
|
*/
|
|
58
58
|
export declare function judgeWait(facts: WaitFacts): WaitVerdict;
|
|
59
|
+
export declare function portBusyMessage(output: string): string | undefined;
|
|
59
60
|
export declare function announcedUrl(output: string): string | undefined;
|
|
60
61
|
/**
|
|
61
62
|
* The url to watch: whatever the tool announced, else one composed from a port we OBSERVED our own
|
|
@@ -45,6 +45,22 @@ export function judgeWait(facts) {
|
|
|
45
45
|
? WaitVerdict.HUNG
|
|
46
46
|
: WaitVerdict.WAITING;
|
|
47
47
|
}
|
|
48
|
+
/**
|
|
49
|
+
* CRA (and webpack) print a prompt rather than a URL when their default port is taken, then exit
|
|
50
|
+
* or hang waiting for a tty. Setup used to report only "exited without serving anything", which
|
|
51
|
+
* hid the port that was actually busy (#802).
|
|
52
|
+
*/
|
|
53
|
+
const CRA_PORT_BUSY = /Something is already running on port (\d+)/i;
|
|
54
|
+
const NAMED_PORT_BUSY = /Port (\d+) is (?:already )?in use/i;
|
|
55
|
+
const EADDRINUSE_PORT = /(?:EADDRINUSE|address already in use)[^\d]*(\d+)/i;
|
|
56
|
+
export function portBusyMessage(output) {
|
|
57
|
+
const match = CRA_PORT_BUSY.exec(output) ?? NAMED_PORT_BUSY.exec(output) ?? EADDRINUSE_PORT.exec(output);
|
|
58
|
+
const port = match?.[1];
|
|
59
|
+
if (undefined === port)
|
|
60
|
+
return undefined;
|
|
61
|
+
return (`port ${port} is already in use — something else is serving there, so this launcher did not ` +
|
|
62
|
+
`bind. Pass --url http://localhost:${port} if that process is this app, or free the port and re-run.`);
|
|
63
|
+
}
|
|
48
64
|
/** A url the dev server announced, if it has announced one. Never composed, only read. */
|
|
49
65
|
const URL_IN_OUTPUT = /https?:\/\/(?:localhost|127\.0\.0\.1|\[::1\]):\d+/;
|
|
50
66
|
export function announcedUrl(output) {
|
|
@@ -16,7 +16,7 @@ import { spawnDaemon } from '../../daemon/daemon.js';
|
|
|
16
16
|
import { probePresence, presenceIsUsable } from '../../daemon/binding/port-presence.js';
|
|
17
17
|
import { waitForDaemon, probeDaemon } from '../../../surface/mcp/mcp-proxy.js';
|
|
18
18
|
import { daemonSpawnArgs } from '../../cli/daemon-start-options.js';
|
|
19
|
-
import { fetchStatus } from '../../
|
|
19
|
+
import { fetchStatus } from '../../daemon/binding/daemon-status-probe.js';
|
|
20
20
|
export const EnsureDaemon = {
|
|
21
21
|
/** Something usable was already there. */
|
|
22
22
|
ADOPTED: 'adopted',
|
|
@@ -15,7 +15,7 @@ import { relaunchDecision } from '../bringup/relaunch.js';
|
|
|
15
15
|
import { claudeTranscriptExists, codexSessionFor } from '../terminal/transcripts.js';
|
|
16
16
|
import { probePresence } from '../../daemon/binding/port-presence.js';
|
|
17
17
|
import { probeDaemon } from '../../../surface/mcp/proxy/proxy-daemon-probe.js';
|
|
18
|
-
import { fetchStatus } from '../../
|
|
18
|
+
import { fetchStatus } from '../../daemon/binding/daemon-status-probe.js';
|
|
19
19
|
import { collectEnv, DEFAULT_PHASE_TIMEOUT_MS } from '../setup-options.js';
|
|
20
20
|
/** How often the runtime phases look again: fast enough not to be the wait, slow enough to be free. */
|
|
21
21
|
const POLL_MS = 250;
|
|
@@ -129,6 +129,18 @@ export async function continueAfterInit(parsed, result, io, cwd) {
|
|
|
129
129
|
return;
|
|
130
130
|
}
|
|
131
131
|
io.print('');
|
|
132
|
+
// The precondition for everything the closing asks for, printed on the path that could not say
|
|
133
|
+
// it. An agent client reads its MCP server list when it STARTS and never re-reads it, so on the
|
|
134
|
+
// run that first registers Reticle the `reticle_*` tools are not in the session that just asked
|
|
135
|
+
// for them -- and this path closes by telling that session to call `reticle_act_and_wait`.
|
|
136
|
+
// `restartHint` has said this for a long time and is printed only when init stops at the files.
|
|
137
|
+
if (true === result.mcpNewlyRegistered) {
|
|
138
|
+
io.print('Reticle was registered with your agent by this run, and an agent reads its tool list only ' +
|
|
139
|
+
'when it starts: the `reticle_*` tools are NOT in this session yet. Restart it first — ' +
|
|
140
|
+
'`npx @reticlehq/server init --relaunch` prints the exact resume command for Claude Code ' +
|
|
141
|
+
'and Codex. Once per machine.');
|
|
142
|
+
io.print('');
|
|
143
|
+
}
|
|
132
144
|
if (outcome.ok && !outcome.flowSaved) {
|
|
133
145
|
// Success, and no flow. Saying "a flow was driven" here would replace a wrong exit code with
|
|
134
146
|
// a wrong sentence, which is the worse of the two: the exit code is read by CI and the
|
|
@@ -15,7 +15,7 @@ import { spawn, spawnSync } from 'node:child_process';
|
|
|
15
15
|
import { Socket } from 'node:net';
|
|
16
16
|
import { existsSync, readdirSync } from 'node:fs';
|
|
17
17
|
import { join } from 'node:path';
|
|
18
|
-
import { fetchStatus } from '../
|
|
18
|
+
import { fetchStatus } from '../daemon/binding/daemon-status-probe.js';
|
|
19
19
|
import { descendants, parseLsofPorts, parseNetstatListeners, parseWmicProcesses, } from './terminal/listeners.js';
|
|
20
20
|
import { loopbackProbeUrls } from './probe/loopback-probe-urls.js';
|
|
21
21
|
const WINDOWS = 'win32' === process.platform;
|
|
@@ -43,8 +43,15 @@ export function describePage(finding, url) {
|
|
|
43
43
|
return (`${url} is served, and the SDK is NOT in the page. That is almost always a dev server still ` +
|
|
44
44
|
'serving the bundle it built before the build config was edited: restart it and hard-reload.');
|
|
45
45
|
case PageFinding.SDK_PRESENT:
|
|
46
|
-
|
|
47
|
-
|
|
46
|
+
// This used to end "so it loaded and returned early", which is a claim about a code path this
|
|
47
|
+
// check cannot see — and at least one real occurrence was the opposite. On a Windows install
|
|
48
|
+
// gate cell the page console read "could not open a websocket to ws://localhost:<port>, 3
|
|
49
|
+
// attempts, all failed": the SDK dialled three times and the socket failed, having returned
|
|
50
|
+
// early from nothing. What is knowable here is that the SDK is present and no session arrived.
|
|
51
|
+
return (`The SDK IS in the page at ${url} and never dialled the bridge: a localhost guard, a ` +
|
|
52
|
+
'production-only check, a bridge port that differs on the two sides, or `localhost` ' +
|
|
53
|
+
'resolving to IPv6 where the daemon is only on IPv4 — `npx @reticlehq/server doctor` names ' +
|
|
54
|
+
'that last one, and the page console names the first three.');
|
|
48
55
|
}
|
|
49
56
|
}
|
|
50
57
|
/**
|
|
@@ -39,6 +39,18 @@ export interface SetupOutcome {
|
|
|
39
39
|
readonly notes: string[];
|
|
40
40
|
}
|
|
41
41
|
/** Everything the sequence needs from the world, so none of it is reached for directly. */
|
|
42
|
+
/**
|
|
43
|
+
* The daemon's reason for "no session", in the two lengths its two readers need.
|
|
44
|
+
*
|
|
45
|
+
* Same fact, not two facts: `full` is the lead plus the differential behind it, so a reader given
|
|
46
|
+
* only one of them is never given something the other contradicts.
|
|
47
|
+
*/
|
|
48
|
+
export interface DaemonReason {
|
|
49
|
+
/** One line, printed to the person watching the install. */
|
|
50
|
+
readonly lead: string;
|
|
51
|
+
/** The whole differential, recorded for the agent reading `--json`. Defaults to the lead. */
|
|
52
|
+
readonly full?: string;
|
|
53
|
+
}
|
|
42
54
|
export interface SetupEffects {
|
|
43
55
|
/** Start the dev server. Resolves once started; the caller owns stopping it. */
|
|
44
56
|
readonly startDevServer: (command: string, cwd: string) => Promise<void>;
|
|
@@ -70,6 +82,19 @@ export interface SetupEffects {
|
|
|
70
82
|
*/
|
|
71
83
|
readonly openBrowser: (url: string) => Promise<void>;
|
|
72
84
|
readonly listSessions: () => Promise<CandidateSession[]>;
|
|
85
|
+
/**
|
|
86
|
+
* The daemon's own account of why nothing connected, if it can be asked.
|
|
87
|
+
*
|
|
88
|
+
* The daemon is the only party that can see a hello it REFUSED, and a refusal is the one piece of
|
|
89
|
+
* positive evidence in the whole failure -- only an SDK dials the bridge, so a turned-away page
|
|
90
|
+
* proves the app is running, instrumented and pointed here. `page-probe.ts` has said since it was
|
|
91
|
+
* written that this sentence is the one to lead with and that the page finding merely adds the
|
|
92
|
+
* page-side fact; only the second half was wired.
|
|
93
|
+
*
|
|
94
|
+
* Optional, and allowed to fail: setup must still report what it saw when the daemon cannot be
|
|
95
|
+
* reached, which is itself one of the states this runs in.
|
|
96
|
+
*/
|
|
97
|
+
readonly daemonWhy?: () => Promise<DaemonReason | undefined>;
|
|
73
98
|
readonly now: () => number;
|
|
74
99
|
readonly sleep: (ms: number) => Promise<void>;
|
|
75
100
|
readonly note: (line: string) => void;
|
|
@@ -95,11 +120,4 @@ export interface SetupInput {
|
|
|
95
120
|
readonly phaseTimeoutMs: number;
|
|
96
121
|
readonly pollMs: number;
|
|
97
122
|
}
|
|
98
|
-
/**
|
|
99
|
-
* Run the phases. Stops at the first one that cannot continue, and always says what is left.
|
|
100
|
-
*
|
|
101
|
-
* A phase that fails is not an error to throw: the caller is usually an agent, and a thrown
|
|
102
|
-
* exception loses the four things it needs — how far this got, the url, the session, and what to do
|
|
103
|
-
* about it.
|
|
104
|
-
*/
|
|
105
123
|
export declare function runSetupPhases(input: SetupInput, fx: SetupEffects): Promise<SetupOutcome>;
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* way of going wrong, and the alternative to injecting them is a test that boots a real dev server
|
|
14
14
|
* and a real browser to find out what happens when neither works.
|
|
15
15
|
*/
|
|
16
|
-
import { judgeWait, QUIET_MEANS_HUNG_MS, urlToWatch, WaitVerdict, } from './bringup/dev-server-wait.js';
|
|
16
|
+
import { judgeWait, portBusyMessage, QUIET_MEANS_HUNG_MS, urlToWatch, WaitVerdict, } from './bringup/dev-server-wait.js';
|
|
17
17
|
import { waitProgressLine } from './terminal/wait-progress.js';
|
|
18
18
|
/**
|
|
19
19
|
* Windows gets longer to say something before silence counts as a wedge.
|
|
@@ -69,6 +69,23 @@ const stop = (input, phase, partial, notes) => ({
|
|
|
69
69
|
* exception loses the four things it needs — how far this got, the url, the session, and what to do
|
|
70
70
|
* about it.
|
|
71
71
|
*/
|
|
72
|
+
/**
|
|
73
|
+
* Ask the daemon why, and treat every failure as "it did not say".
|
|
74
|
+
*
|
|
75
|
+
* A daemon that cannot be reached is one of the states this runs in, so an error here is data, not
|
|
76
|
+
* an exception: the page finding below still prints and the run still ends with its own verdict.
|
|
77
|
+
*/
|
|
78
|
+
async function daemonWhy(fx) {
|
|
79
|
+
if (undefined === fx.daemonWhy)
|
|
80
|
+
return undefined;
|
|
81
|
+
try {
|
|
82
|
+
const why = await fx.daemonWhy();
|
|
83
|
+
return undefined === why || 0 === why.lead.length ? undefined : why;
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
return undefined;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
72
89
|
export async function runSetupPhases(input, fx) {
|
|
73
90
|
const notes = [];
|
|
74
91
|
const note = (line) => {
|
|
@@ -139,7 +156,8 @@ export async function runSetupPhases(input, fx) {
|
|
|
139
156
|
break;
|
|
140
157
|
}
|
|
141
158
|
if (WaitVerdict.DEAD === verdict) {
|
|
142
|
-
note(
|
|
159
|
+
note(portBusyMessage(fx.devServerOutput()) ??
|
|
160
|
+
'The dev server exited without serving anything.');
|
|
143
161
|
return stop(input, SetupPhase.DEV_SERVER, {}, notes);
|
|
144
162
|
}
|
|
145
163
|
if (WaitVerdict.HUNG === verdict) {
|
|
@@ -238,7 +256,25 @@ export async function runSetupPhases(input, fx) {
|
|
|
238
256
|
'the capture helper not installed, or a CSP that blocks the bridge: run `npx @reticlehq/server doctor`.');
|
|
239
257
|
}
|
|
240
258
|
else {
|
|
241
|
-
//
|
|
259
|
+
// The daemon first, when it has something to say: it is the only party that can see a hello it
|
|
260
|
+
// refused, and that outranks anything inferred from an absence. Then the page, which is the
|
|
261
|
+
// one thing the daemon cannot know, and worth one fetch.
|
|
262
|
+
const why = await daemonWhy(fx);
|
|
263
|
+
if (undefined !== why) {
|
|
264
|
+
// Two readers, two lengths, one fetch.
|
|
265
|
+
//
|
|
266
|
+
// The person watching an install gets the LEAD — that is what trimming this diagnosis was
|
|
267
|
+
// for. The agent reads `notes` out of `--json`, and what it acts on is the differential:
|
|
268
|
+
// which ports were actually scanned (so an empty result is not proof), why a missing
|
|
269
|
+
// `.reticle.json` is expected in a monorepo, and the lease that opens a URL on a box with
|
|
270
|
+
// no browser at all.
|
|
271
|
+
//
|
|
272
|
+
// Printing only the lead put BOTH readers on the lead, which silently emptied the agent
|
|
273
|
+
// surface: `init --json` stopped mentioning the lease, and `break/break-matrix.mjs`
|
|
274
|
+
// (`no-browser-to-open`) went red for exactly that reason.
|
|
275
|
+
fx.note(why.lead);
|
|
276
|
+
notes.push(why.full ?? why.lead);
|
|
277
|
+
}
|
|
242
278
|
note(describePage(readPage(await fx.probePage(url)), url));
|
|
243
279
|
}
|
|
244
280
|
return stop(input, SetupPhase.CONNECT, { url }, notes);
|
|
@@ -256,12 +292,18 @@ export async function runSetupPhases(input, fx) {
|
|
|
256
292
|
// Both routes, because `explore` needs a model: without ANTHROPIC_API_KEY it answers "No model
|
|
257
293
|
// configured to drive the app". Naming only that one hands the reader a dead end on any machine
|
|
258
294
|
// without a key, which is the same defect this release spent its time removing everywhere else.
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
295
|
+
// Three short lines rather than one paragraph, and each says who it is for.
|
|
296
|
+
//
|
|
297
|
+
// This was a single 524-character note, measured on a real first run: eighty-three words, no
|
|
298
|
+
// break, naming `reticle_*` tools at a reader who had just typed `reticle init` in a terminal and
|
|
299
|
+
// has no such tools to call. The facts were right and the shape made them unreadable, which on
|
|
300
|
+
// the last line of onboarding is the same as not saying them.
|
|
301
|
+
note(`Connected. ${url} is instrumented. Onboarding is done; nothing is verified yet.`);
|
|
302
|
+
note('Agent: drive one flow and end it with `reticle_act_and_wait` or `reticle_assert`. Those two ' +
|
|
303
|
+
'produce a verdict; nothing else does.');
|
|
304
|
+
note('Or hand over the whole drive: `reticle_verify { action: "explore", persona: "<who does ' +
|
|
305
|
+
'what>" }` records what it drove, so later runs replay with no model in the loop. Needs ' +
|
|
306
|
+
'ANTHROPIC_API_KEY.');
|
|
265
307
|
return {
|
|
266
308
|
ok: true,
|
|
267
309
|
reachedPhase: SetupPhase.CONNECT,
|
|
@@ -22,7 +22,10 @@ import { reticleStateHome } from '../daemon/daemon.js';
|
|
|
22
22
|
import { readDevServers } from '../daemon/dev-servers.js';
|
|
23
23
|
import { urlOfExistingApp } from './probe/existing-app.js';
|
|
24
24
|
import { listSessions, OwnedDevServer, probePage } from './node-effects.js';
|
|
25
|
+
import { fetchStatus } from '../daemon/binding/daemon-status-probe.js';
|
|
26
|
+
import { summarizeStatus } from '../cli/launch/cli-launch.js';
|
|
25
27
|
import { runSetupPhases, } from './run-setup.js';
|
|
28
|
+
import { noticeKey, readSaid, rememberSaid, unsaidNotices } from './agent-notice-memory.js';
|
|
26
29
|
/**
|
|
27
30
|
* The dev server a crash should take with it, if one is running right now.
|
|
28
31
|
*
|
|
@@ -89,10 +92,20 @@ export function registerOtherAgents(print) {
|
|
|
89
92
|
if (0 < wrote.length) {
|
|
90
93
|
print(`registered the MCP server with ${wrote.length} more agent(s): ${wrote.map((r) => r.name).join(', ')}`);
|
|
91
94
|
}
|
|
92
|
-
// A format we will not rewrite is somebody's to edit, so it has to be said rather than skipped
|
|
93
|
-
|
|
94
|
-
|
|
95
|
+
// A format we will not rewrite is somebody's to edit, so it has to be said rather than skipped --
|
|
96
|
+
// but said ONCE. Two commands call this function by design, so a `curl | sh` followed by
|
|
97
|
+
// `reticle init` printed the same two paragraphs twice in one sitting, and again on every re-run
|
|
98
|
+
// after that. See agent-notice-memory.ts: the stamp is keyed on the notice's own text, so a
|
|
99
|
+
// config that changes is reported again.
|
|
100
|
+
const stateHome = reticleStateHome();
|
|
101
|
+
const manual = results
|
|
102
|
+
.filter((r) => 'manual' === r.action)
|
|
103
|
+
.map((r) => ({ name: r.name, file: r.file, why: r.why }));
|
|
104
|
+
const fresh = unsaidNotices(readSaid(stateHome), manual);
|
|
105
|
+
for (const notice of fresh) {
|
|
106
|
+
print(`${notice.name}: ${notice.why} — add the reticle entry to ${notice.file} by hand.`);
|
|
95
107
|
}
|
|
108
|
+
rememberSaid(stateHome, fresh.map((n) => noticeKey(n)));
|
|
96
109
|
const skills = applyAgentSkills(agentIo, { home, platform });
|
|
97
110
|
if (0 < skills.length)
|
|
98
111
|
print(`wrote the /reticle skill for ${skills.length} agent(s)`);
|
|
@@ -166,6 +179,21 @@ export async function runSetupCommand(input, print) {
|
|
|
166
179
|
}
|
|
167
180
|
},
|
|
168
181
|
listSessions: () => listSessions(input.bridgePort),
|
|
182
|
+
// `summarizeStatus` already narrows this payload for `reticle status`; reusing it here keeps one
|
|
183
|
+
// reader of the wire shape rather than two that can disagree about which key carries the reason.
|
|
184
|
+
// Both lengths, from the one read: the lead is printed to the person watching the install, the
|
|
185
|
+
// full differential is recorded for the agent reading `--json`. Returning only the lead here is
|
|
186
|
+
// what emptied the agent surface — see the note at the call site in `run-setup.ts`.
|
|
187
|
+
daemonWhy: async () => {
|
|
188
|
+
const status = summarizeStatus(await fetchStatus(input.bridgePort));
|
|
189
|
+
const full = status.why;
|
|
190
|
+
// A daemon too old to report the short one falls back to the long one, because saying the
|
|
191
|
+
// right thing at length beats saying nothing.
|
|
192
|
+
const lead = status.whyLead ?? full;
|
|
193
|
+
if (undefined === lead)
|
|
194
|
+
return undefined;
|
|
195
|
+
return undefined === full ? { lead } : { lead, full };
|
|
196
|
+
},
|
|
169
197
|
now: () => Date.now(),
|
|
170
198
|
sleep: (ms) => new Promise((r) => setTimeout(r, ms)),
|
|
171
199
|
note: print,
|
|
@@ -29,7 +29,8 @@
|
|
|
29
29
|
* it worked.
|
|
30
30
|
*/
|
|
31
31
|
import { InitConfirmation, RETICLE_DEFAULT_PORT } from '@reticlehq/core';
|
|
32
|
-
import {
|
|
32
|
+
import { summarizeStatus } from '../../cli/launch/cli-launch.js';
|
|
33
|
+
import { fetchStatus } from '../../daemon/binding/daemon-status-probe.js';
|
|
33
34
|
import { readDevServers } from '../../daemon/dev-servers.js';
|
|
34
35
|
import { devServersForProject } from '@reticlehq/core/artifacts';
|
|
35
36
|
import { readProjectId } from '../../cli/ports/resolve/cli-port.js';
|
|
@@ -29,6 +29,33 @@ type ExecutionKind = (typeof ExecutionKind)[keyof typeof ExecutionKind];
|
|
|
29
29
|
* `apply_update` npm-installs into the user's own project (polluting their package.json).
|
|
30
30
|
*/
|
|
31
31
|
export declare function classifyExecutionKind(script: string): ExecutionKind;
|
|
32
|
+
/**
|
|
33
|
+
* Spawn npm, through a shell on Windows because `npm.cmd` cannot be spawned without one.
|
|
34
|
+
*
|
|
35
|
+
* Since the CVE-2024-27980 fix -- in every supported Node -- spawning a `.cmd` with no shell throws
|
|
36
|
+
* EINVAL. This repo has already paid for that lesson once, at `init/src/node-io.ts`: `reticle setup
|
|
37
|
+
* mcp` probed for the Claude CLI with a bare `execFileSync`, the probe threw, and the installer
|
|
38
|
+
* told the user their machine had no Claude Code on it. The same shape sat here, in the SELF-UPDATE
|
|
39
|
+
* path -- the channel every other fix reaches a user through, and the one place a silent failure
|
|
40
|
+
* strands somebody on an old version indefinitely.
|
|
41
|
+
*
|
|
42
|
+
* Arguments are quoted with init's own rule rather than a second copy of it. They are internal
|
|
43
|
+
* today (`install`, `-g`, and a package spec whose version comes from the registry), but a shell
|
|
44
|
+
* turns "internal" into "whatever the registry said", and the quoting is one import.
|
|
45
|
+
*/
|
|
46
|
+
export declare function npmSpawn(args: string[],
|
|
47
|
+
/**
|
|
48
|
+
* Injected so the Windows branch is testable on every platform.
|
|
49
|
+
*
|
|
50
|
+
* The bug this fixes could only be OBSERVED on Windows, and a check that can only fail on a
|
|
51
|
+
* machine nobody here runs is a check that fails in CI at the earliest and in the field at the
|
|
52
|
+
* latest. The rule is a pure function of the platform string, so it is one.
|
|
53
|
+
*/
|
|
54
|
+
on?: string): {
|
|
55
|
+
file: string;
|
|
56
|
+
argv: string[];
|
|
57
|
+
shell: boolean;
|
|
58
|
+
};
|
|
32
59
|
interface NpmInstall {
|
|
33
60
|
args: string[];
|
|
34
61
|
cwd?: string;
|