@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
|
@@ -23,7 +23,7 @@ export { AGENT_FLAG, BUG_FLAG, FEEDBACK_KINDS, KIND_FLAG, RATING_FLAG, } from '.
|
|
|
23
23
|
export const CLI_USAGE = `usage: npx @reticlehq/server <command> (or \`reticle <command>\` once the bin is on your PATH)
|
|
24
24
|
|
|
25
25
|
reticle init [--dry-run] [--port N] [--no-mcp] [--no-install] [--app <dir>]
|
|
26
|
-
[--env KEY=VALUE]... [--files-only] (wire Reticle into the project in this directory)
|
|
26
|
+
[--env KEY=VALUE]... [--files-only] [--hooks] (wire Reticle into the project in this directory)
|
|
27
27
|
init is ONBOARDING: it wires the project, boots the app and proves a session
|
|
28
28
|
connected. It does not drive. The FIRST RUN is the stage that proves a flow:
|
|
29
29
|
reticle verify <url> --explore --persona "<who does what>", or the same thing
|
|
@@ -56,12 +56,18 @@ export const CLI_USAGE = `usage: npx @reticlehq/server <command> (or \`reticl
|
|
|
56
56
|
reticle verify <url> [--port N] [--headed] [--timeout N] [--storage-state <file>] [--session-id <id>] (one-shot: drive the URL, verify saved flows, exit 0=pass)
|
|
57
57
|
[--explore] [--persona <who>] (no saved flows? let Reticle drive the app itself and record them)
|
|
58
58
|
[--select <label>] (repeatable: verify only flows carrying these labels — no model, exit 0=pass)
|
|
59
|
-
[--expect '<json predicate>']
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
59
|
+
[--expect '<json predicate>' | --expect-file <path>] (one verdict, no saved
|
|
60
|
+
flows needed. --expect-file is the form no shell can mangle: cmd.exe keeps the
|
|
61
|
+
single quotes the docs show, and PowerShell strips the inner double quotes, and
|
|
62
|
+
a predicate that does not parse produces no verdict at all — asks the daemon that
|
|
63
|
+
is already running, so nothing is bound and nothing is stopped. It NEEDS that
|
|
64
|
+
daemon: with none on the port it refuses and names the flag, rather than reporting
|
|
65
|
+
on your saved flows instead. Cannot be combined with --storage-state, which this
|
|
66
|
+
path has nowhere to load. This is the path when your client never loaded the
|
|
67
|
+
reticle_* tools. exit 0 ONLY on verified:"yes" — "unknown" is not a pass)
|
|
63
68
|
reticle affected [--since <ref>] [file...] (which saved flows must re-verify for the changed files)
|
|
64
69
|
reticle gate [--since <ref>] [file...] (exit non-zero unless passing artifacts cover the affected flows)
|
|
70
|
+
reticle report [--session <id>] [--hook] (what the latest session claimed, and what held)
|
|
65
71
|
reticle watch [url] (on save, report which saved flows must re-verify)
|
|
66
72
|
reticle drive <url> [--headless] (foreground mode, for debugging)
|
|
67
73
|
reticle mcp [--port N] [--drive <url>] [--headless] (MCP stdio proxy; auto-starts daemon if needed)
|
|
@@ -102,6 +108,9 @@ const CAPSULES_COMMAND = 'capsules';
|
|
|
102
108
|
const GATE_COMMAND = 'gate';
|
|
103
109
|
/** Hook mode: prose a human can read, and silence when there was simply nothing to check. */
|
|
104
110
|
const HOOK_FLAG = '--hook';
|
|
111
|
+
/** `reticle report [--session <id>] [--hook]` — what the latest session claimed, and what held. */
|
|
112
|
+
const REPORT_COMMAND = 'report';
|
|
113
|
+
const SESSION_FLAG = '--session';
|
|
105
114
|
const WATCH_COMMAND = 'watch';
|
|
106
115
|
const UPDATE_COMMAND = 'update';
|
|
107
116
|
const ROLLBACK_COMMAND = 'rollback';
|
|
@@ -146,6 +155,7 @@ const KNOWN_COMMANDS = new Set([
|
|
|
146
155
|
HUNT_COMMAND,
|
|
147
156
|
CAPSULES_COMMAND,
|
|
148
157
|
GATE_COMMAND,
|
|
158
|
+
REPORT_COMMAND,
|
|
149
159
|
WATCH_COMMAND,
|
|
150
160
|
UPDATE_COMMAND,
|
|
151
161
|
ROLLBACK_COMMAND,
|
|
@@ -228,6 +238,8 @@ const FILES_ONLY_FLAG = '--files-only';
|
|
|
228
238
|
* and `init` runs unattended. Whoever passes this has decided; nothing decides it for them.
|
|
229
239
|
*/
|
|
230
240
|
const CAPTURE_BODIES_FLAG = '--capture-bodies';
|
|
241
|
+
/** `--hooks`: also install the print-only Claude Code Stop hook (`reticle report --hook`). */
|
|
242
|
+
const HOOKS_FLAG = '--hooks';
|
|
231
243
|
/**
|
|
232
244
|
* The rest of the runtime surface.
|
|
233
245
|
*
|
|
@@ -410,6 +422,7 @@ function parseInitFlags(args) {
|
|
|
410
422
|
const env = [];
|
|
411
423
|
let filesOnly = false;
|
|
412
424
|
let captureBodies = false;
|
|
425
|
+
let hooks = false;
|
|
413
426
|
let json = false;
|
|
414
427
|
let open = true;
|
|
415
428
|
let relaunch = false;
|
|
@@ -457,6 +470,9 @@ function parseInitFlags(args) {
|
|
|
457
470
|
else if (arg === CAPTURE_BODIES_FLAG) {
|
|
458
471
|
captureBodies = true;
|
|
459
472
|
}
|
|
473
|
+
else if (arg === HOOKS_FLAG) {
|
|
474
|
+
hooks = true;
|
|
475
|
+
}
|
|
460
476
|
else if (arg === JSON_FLAG) {
|
|
461
477
|
json = true;
|
|
462
478
|
}
|
|
@@ -520,6 +536,7 @@ function parseInitFlags(args) {
|
|
|
520
536
|
env,
|
|
521
537
|
filesOnly,
|
|
522
538
|
captureBodies,
|
|
539
|
+
hooks,
|
|
523
540
|
json,
|
|
524
541
|
open,
|
|
525
542
|
relaunch,
|
|
@@ -606,6 +623,7 @@ export function parseCliArgs(argv, defaultPort, defaultHeadless = false) {
|
|
|
606
623
|
env: r.env,
|
|
607
624
|
filesOnly: r.filesOnly,
|
|
608
625
|
captureBodies: r.captureBodies,
|
|
626
|
+
hooks: r.hooks,
|
|
609
627
|
json: r.json,
|
|
610
628
|
open: r.open,
|
|
611
629
|
relaunch: r.relaunch,
|
|
@@ -772,6 +790,7 @@ export function parseCliArgs(argv, defaultPort, defaultHeadless = false) {
|
|
|
772
790
|
...(r.storageState !== undefined ? { storageState: r.storageState } : {}),
|
|
773
791
|
...(r.sessionId !== undefined ? { sessionId: r.sessionId } : {}),
|
|
774
792
|
...(r.expect !== undefined ? { expect: r.expect } : {}),
|
|
793
|
+
...(r.expectFile !== undefined ? { expectFile: r.expectFile } : {}),
|
|
775
794
|
...(true === r.explore ? { explore: true } : {}),
|
|
776
795
|
...(r.persona !== undefined ? { persona: r.persona } : {}),
|
|
777
796
|
...(r.select !== undefined ? { select: r.select } : {}),
|
|
@@ -802,6 +821,15 @@ export function parseCliArgs(argv, defaultPort, defaultHeadless = false) {
|
|
|
802
821
|
...(hook ? { hook: true } : {}),
|
|
803
822
|
};
|
|
804
823
|
}
|
|
824
|
+
case REPORT_COMMAND: {
|
|
825
|
+
const at = rest.indexOf(SESSION_FLAG);
|
|
826
|
+
const session = -1 === at ? undefined : rest[at + 1];
|
|
827
|
+
return {
|
|
828
|
+
kind: 'report',
|
|
829
|
+
hook: rest.includes(HOOK_FLAG),
|
|
830
|
+
...(session === undefined ? {} : { session }),
|
|
831
|
+
};
|
|
832
|
+
}
|
|
805
833
|
case WATCH_COMMAND: {
|
|
806
834
|
// `reticle watch [url]` — on file save, report which saved flows must re-verify.
|
|
807
835
|
const url = rest.find((arg) => !arg.startsWith('-'));
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
*/
|
|
12
12
|
import { type ReticleVerificationRun } from '@reticlehq/core';
|
|
13
13
|
import { type RunningServer } from '../../index.js';
|
|
14
|
+
import { PortPresence } from '../daemon/binding/port-presence.js';
|
|
14
15
|
import { type VerifyProgressListener } from '../../judgement/runs/reticle-runner.js';
|
|
15
16
|
import type { ToolDeps } from '../../surface/tools/tool-kit.js';
|
|
16
17
|
/** The live capabilities runVerify needs — faked in tests so the logic runs without a browser. */
|
|
@@ -104,10 +105,68 @@ export declare function urlParts(url: string): {
|
|
|
104
105
|
* first bullet — not this change.
|
|
105
106
|
*/
|
|
106
107
|
export declare function portBusyMessage(port: number): string;
|
|
108
|
+
/**
|
|
109
|
+
* What `--expect` is answered with when the port is not serving a daemon.
|
|
110
|
+
*
|
|
111
|
+
* It used to be answered with nothing at all. The predicate was read only inside the daemon branch,
|
|
112
|
+
* so on a free or foreign port the flag was dropped without a word and the command carried on into
|
|
113
|
+
* the saved-flows path — where it reported, accurately and about something else entirely, that there
|
|
114
|
+
* were no saved flows to verify. Byte-identical output to the same command with no `--expect` on it.
|
|
115
|
+
*
|
|
116
|
+
* That is the documented recovery for a client that never loaded the `reticle_*` tools, so it failed
|
|
117
|
+
* for the people with no other route to a verdict, and it failed in the one way they could not
|
|
118
|
+
* diagnose: the refusal they got was true.
|
|
119
|
+
*/
|
|
120
|
+
export declare function expectNeedsDaemonMessage(port: number, presence: PortPresence): string;
|
|
121
|
+
/** What `verify` does next, once the options and the state of the port are both known. */
|
|
122
|
+
export declare const VerifyRoute: {
|
|
123
|
+
/** Ask the daemon that already owns the port for a one-shot verdict on the predicate. */
|
|
124
|
+
readonly ADHOC: "adhoc";
|
|
125
|
+
/** Ask the daemon that already owns the port to replay the saved flows. See runAdhocSuite. */
|
|
126
|
+
readonly ADHOC_SUITE: "adhoc-suite";
|
|
127
|
+
/** Boot our own daemon, drive the url, replay the flows saved on disk. */
|
|
128
|
+
readonly FLOWS: "flows";
|
|
129
|
+
/** Answer the caller, and run nothing. */
|
|
130
|
+
readonly REFUSE: "refuse";
|
|
131
|
+
};
|
|
132
|
+
export type VerifyRoute = (typeof VerifyRoute)[keyof typeof VerifyRoute];
|
|
133
|
+
export type VerifyPlan = {
|
|
134
|
+
route: typeof VerifyRoute.ADHOC;
|
|
135
|
+
} | {
|
|
136
|
+
route: typeof VerifyRoute.ADHOC_SUITE;
|
|
137
|
+
} | {
|
|
138
|
+
route: typeof VerifyRoute.FLOWS;
|
|
139
|
+
} | {
|
|
140
|
+
route: typeof VerifyRoute.REFUSE;
|
|
141
|
+
message: string;
|
|
142
|
+
};
|
|
143
|
+
/**
|
|
144
|
+
* All four routes this command has, in one pure function.
|
|
145
|
+
*
|
|
146
|
+
* They used to be a pair of nested `if`s that consulted `--expect` only after the port had already
|
|
147
|
+
* answered as a daemon, which is how one of the four went missing: a predicate on any other port
|
|
148
|
+
* state fell through to a path that cannot see it. Stating the cases together is what makes an
|
|
149
|
+
* unhandled one visible, and it lets every one of them be driven with no port, no daemon and no
|
|
150
|
+
* browser — which is where an argument-level decision belongs.
|
|
151
|
+
*/
|
|
152
|
+
export declare function routeVerify(args: {
|
|
153
|
+
/** True when the caller supplied a predicate, by `--expect` or `--expect-file`. */
|
|
154
|
+
hasPredicate: boolean;
|
|
155
|
+
/**
|
|
156
|
+
* True when the caller asked for something only a browser of OUR OWN can do: `--explore` (a model
|
|
157
|
+
* driving a fresh page), `--headed`, or `--storage-state`. The running daemon's tab can honour
|
|
158
|
+
* none of them, so replaying there would silently ignore the request.
|
|
159
|
+
*/
|
|
160
|
+
wantsOwnBrowser?: boolean;
|
|
161
|
+
presence: PortPresence;
|
|
162
|
+
port: number;
|
|
163
|
+
}): VerifyPlan;
|
|
107
164
|
export declare function handleVerify(parsed: {
|
|
108
165
|
url: string;
|
|
109
166
|
/** A parsed predicate for a flow-free, one-shot verdict against the running daemon. */
|
|
110
167
|
expect?: unknown;
|
|
168
|
+
/** The same predicate in a file, which is the form no shell can mangle. Resolved below. */
|
|
169
|
+
expectFile?: string;
|
|
111
170
|
headless: boolean;
|
|
112
171
|
timeoutMs?: number;
|
|
113
172
|
storageState?: string;
|
|
@@ -10,14 +10,17 @@
|
|
|
10
10
|
* — is unit-tested without launching a real browser.
|
|
11
11
|
*/
|
|
12
12
|
import { join, basename } from 'node:path';
|
|
13
|
+
import { readFileSync } from 'node:fs';
|
|
13
14
|
import { runAdhocVerdict } from './adhoc-verdict.js';
|
|
15
|
+
import { runAdhocSuite } from './adhoc-suite.js';
|
|
14
16
|
import { exploreApp, harnessAvailable, MSG_NO_HARNESS_KEY, } from '../../surface/tools/harness-explore.js';
|
|
15
17
|
import { resolveSuiteSelection } from '../../language/flows/suite-selection.js';
|
|
16
18
|
import { readOrCreatePairingTokenSync, defaultPairingTokenDir, } from '../../portal/bridge/pairing-token.js';
|
|
17
19
|
import { randomUUID } from 'node:crypto';
|
|
18
20
|
import { RETICLE_DEFAULT_PORT, ReticleEnv, bridgeWsUrl, isLoopbackHostname, ReticleDir, RunAgentKind, RunFramework, RunProfile, RunTrigger, VerdictStatus, VerifyPhase, } from '@reticlehq/core';
|
|
19
21
|
import { start } from '../../index.js';
|
|
20
|
-
import { probePresence, PortPresence } from '../daemon/binding/port-presence.js';
|
|
22
|
+
import { probePresence, describePresence, PortPresence, } from '../daemon/binding/port-presence.js';
|
|
23
|
+
import { EXPECT_FLAG } from './cli-parse-grammar.js';
|
|
21
24
|
import { probeDaemon } from '../../surface/mcp/mcp-proxy.js';
|
|
22
25
|
import { fetchStatus } from '../daemon/binding/daemon-status-probe.js';
|
|
23
26
|
import { cloudFetch, createProgressReporter, resolveCloudConfig, syncProgressToCloud, syncRunToCloud, SyncOutcome, } from '../../memory/cloud/cloud-sync.js';
|
|
@@ -365,8 +368,92 @@ export function portBusyMessage(port) {
|
|
|
365
368
|
"your agent's MCP link and the proxy usually respawns a daemon into the gap before verify can " +
|
|
366
369
|
'bind — prefer one of the above.');
|
|
367
370
|
}
|
|
371
|
+
/**
|
|
372
|
+
* What `--expect` is answered with when the port is not serving a daemon.
|
|
373
|
+
*
|
|
374
|
+
* It used to be answered with nothing at all. The predicate was read only inside the daemon branch,
|
|
375
|
+
* so on a free or foreign port the flag was dropped without a word and the command carried on into
|
|
376
|
+
* the saved-flows path — where it reported, accurately and about something else entirely, that there
|
|
377
|
+
* were no saved flows to verify. Byte-identical output to the same command with no `--expect` on it.
|
|
378
|
+
*
|
|
379
|
+
* That is the documented recovery for a client that never loaded the `reticle_*` tools, so it failed
|
|
380
|
+
* for the people with no other route to a verdict, and it failed in the one way they could not
|
|
381
|
+
* diagnose: the refusal they got was true.
|
|
382
|
+
*/
|
|
383
|
+
export function expectNeedsDaemonMessage(port, presence) {
|
|
384
|
+
return (`${EXPECT_FLAG} asks the Reticle daemon that already owns port ${String(port)} for the ` +
|
|
385
|
+
'verdict — it binds nothing and launches nothing, which is what lets it answer while the port ' +
|
|
386
|
+
`is busy. Right now ${describePresence(presence, port)}.\n\n` +
|
|
387
|
+
' The predicate did not run, and nothing was proved.\n\n' +
|
|
388
|
+
' • Start a daemon on that port, pointed at the app, then re-run this command: ' +
|
|
389
|
+
`npx @reticlehq/server serve --port ${String(port)} --drive <url>\n` +
|
|
390
|
+
` • Or drop ${EXPECT_FLAG}, and verify drives the url itself and replays your saved flows — ` +
|
|
391
|
+
'a different question, and it proves nothing about your predicate.');
|
|
392
|
+
}
|
|
393
|
+
/** What `verify` does next, once the options and the state of the port are both known. */
|
|
394
|
+
export const VerifyRoute = {
|
|
395
|
+
/** Ask the daemon that already owns the port for a one-shot verdict on the predicate. */
|
|
396
|
+
ADHOC: 'adhoc',
|
|
397
|
+
/** Ask the daemon that already owns the port to replay the saved flows. See runAdhocSuite. */
|
|
398
|
+
ADHOC_SUITE: 'adhoc-suite',
|
|
399
|
+
/** Boot our own daemon, drive the url, replay the flows saved on disk. */
|
|
400
|
+
FLOWS: 'flows',
|
|
401
|
+
/** Answer the caller, and run nothing. */
|
|
402
|
+
REFUSE: 'refuse',
|
|
403
|
+
};
|
|
404
|
+
/**
|
|
405
|
+
* All four routes this command has, in one pure function.
|
|
406
|
+
*
|
|
407
|
+
* They used to be a pair of nested `if`s that consulted `--expect` only after the port had already
|
|
408
|
+
* answered as a daemon, which is how one of the four went missing: a predicate on any other port
|
|
409
|
+
* state fell through to a path that cannot see it. Stating the cases together is what makes an
|
|
410
|
+
* unhandled one visible, and it lets every one of them be driven with no port, no daemon and no
|
|
411
|
+
* browser — which is where an argument-level decision belongs.
|
|
412
|
+
*/
|
|
413
|
+
export function routeVerify(args) {
|
|
414
|
+
if (args.hasPredicate) {
|
|
415
|
+
return args.presence === PortPresence.DAEMON
|
|
416
|
+
? { route: VerifyRoute.ADHOC }
|
|
417
|
+
: { route: VerifyRoute.REFUSE, message: expectNeedsDaemonMessage(args.port, args.presence) };
|
|
418
|
+
}
|
|
419
|
+
if (args.presence !== PortPresence.DAEMON)
|
|
420
|
+
return { route: VerifyRoute.FLOWS };
|
|
421
|
+
// No predicate, but there may be SAVED FLOWS, and a daemon already owns the port: replay them
|
|
422
|
+
// there rather than refusing, unless the request needs a browser that path never opens.
|
|
423
|
+
return true === args.wantsOwnBrowser
|
|
424
|
+
? { route: VerifyRoute.REFUSE, message: portBusyMessage(args.port) }
|
|
425
|
+
: { route: VerifyRoute.ADHOC_SUITE };
|
|
426
|
+
}
|
|
368
427
|
export function handleVerify(parsed) {
|
|
369
428
|
const now = () => Date.now();
|
|
429
|
+
/*
|
|
430
|
+
* `--expect-file` is `--expect` with the shell taken out of the loop.
|
|
431
|
+
*
|
|
432
|
+
* Resolved here rather than in the parser because the parser is pure and reads no disk, and
|
|
433
|
+
* because the two failures a reader can hit — the file is not there, and what is in it is not
|
|
434
|
+
* JSON — are worth saying apart. Either one ends the run before anything binds: a predicate that
|
|
435
|
+
* could not be read produces NO verdict, and exiting 1 with the reason is the only honest answer.
|
|
436
|
+
*/
|
|
437
|
+
let expectation = parsed.expect;
|
|
438
|
+
if (parsed.expectFile !== undefined) {
|
|
439
|
+
let text;
|
|
440
|
+
try {
|
|
441
|
+
text = readFileSync(parsed.expectFile, 'utf8');
|
|
442
|
+
}
|
|
443
|
+
catch {
|
|
444
|
+
process.stderr.write(`--expect-file: cannot read ${parsed.expectFile}\n`);
|
|
445
|
+
process.exit(1);
|
|
446
|
+
return;
|
|
447
|
+
}
|
|
448
|
+
try {
|
|
449
|
+
expectation = JSON.parse(text);
|
|
450
|
+
}
|
|
451
|
+
catch {
|
|
452
|
+
process.stderr.write(`--expect-file: ${parsed.expectFile} is not valid JSON, so no predicate was read\n`);
|
|
453
|
+
process.exit(1);
|
|
454
|
+
return;
|
|
455
|
+
}
|
|
456
|
+
}
|
|
370
457
|
const reticleRoot = join(process.cwd(), ReticleDir.ROOT);
|
|
371
458
|
const projectName = basename(process.cwd()) || DEFAULT_PROJECT_NAME;
|
|
372
459
|
const ports = {
|
|
@@ -389,20 +476,32 @@ export function handleVerify(parsed) {
|
|
|
389
476
|
// this used to reach the user as a raw node stack rather than as an answer.
|
|
390
477
|
void (async () => {
|
|
391
478
|
const port = parsed.port ?? RETICLE_DEFAULT_PORT;
|
|
392
|
-
|
|
393
|
-
|
|
479
|
+
const presence = await probePresence(port, { tcpOpen: probeDaemon, status: fetchStatus });
|
|
480
|
+
const plan = routeVerify({
|
|
481
|
+
hasPredicate: expectation !== undefined,
|
|
482
|
+
wantsOwnBrowser: true === parsed.explore || !parsed.headless || parsed.storageState !== undefined,
|
|
483
|
+
presence,
|
|
484
|
+
port,
|
|
485
|
+
});
|
|
486
|
+
switch (plan.route) {
|
|
487
|
+
// Either the busy port, or a predicate this state cannot answer. Both are refusals that name
|
|
488
|
+
// what was asked for — never a report about a check nobody requested.
|
|
489
|
+
case VerifyRoute.REFUSE:
|
|
490
|
+
ports.fail(plan.message);
|
|
491
|
+
ports.exit(EXIT_FAIL);
|
|
492
|
+
return;
|
|
394
493
|
// A daemon owning the port is the NORMAL state after a working install, and it used to be the
|
|
395
494
|
// end of the road: `verify` refused, the other verdict paths need saved flows a first-install
|
|
396
495
|
// project does not have, and stopping the daemon cuts the agent's own MCP link. An agent could
|
|
397
496
|
// hold a live, correctly-wired app and have no way to reach a verdict at all.
|
|
398
497
|
//
|
|
399
|
-
// With `--expect` there is
|
|
400
|
-
//
|
|
401
|
-
|
|
498
|
+
// With `--expect` there is a way, and it does not need the port: ask the daemon that already
|
|
499
|
+
// owns it. See runAdhocVerdict.
|
|
500
|
+
case VerifyRoute.ADHOC: {
|
|
402
501
|
const verdict = await runAdhocVerdict({
|
|
403
502
|
port,
|
|
404
503
|
...(parsed.url !== undefined && '' !== parsed.url ? { url: parsed.url } : {}),
|
|
405
|
-
predicate:
|
|
504
|
+
predicate: expectation,
|
|
406
505
|
...(parsed.sessionId === undefined ? {} : { sessionId: parsed.sessionId }),
|
|
407
506
|
...((t) => (t === undefined || 0 === t.length ? {} : { token: t }))(readOrCreatePairingTokenSync(defaultPairingTokenDir())),
|
|
408
507
|
});
|
|
@@ -411,16 +510,29 @@ export function handleVerify(parsed) {
|
|
|
411
510
|
ports.exit(verdict.code);
|
|
412
511
|
return;
|
|
413
512
|
}
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
513
|
+
// No predicate, a daemon on the port, and saved flows to run: the other half of the same
|
|
514
|
+
// dead end. The whole promise of a saved suite is that it runs again without an agent.
|
|
515
|
+
case VerifyRoute.ADHOC_SUITE: {
|
|
516
|
+
const suite = await runAdhocSuite({
|
|
517
|
+
port,
|
|
518
|
+
...(parsed.select === undefined ? {} : { select: parsed.select }),
|
|
519
|
+
...(parsed.sessionId === undefined ? {} : { sessionId: parsed.sessionId }),
|
|
520
|
+
...((t) => (t === undefined || 0 === t.length ? {} : { token: t }))(readOrCreatePairingTokenSync(defaultPairingTokenDir())),
|
|
521
|
+
});
|
|
522
|
+
for (const line of suite.lines)
|
|
523
|
+
ports.out(line);
|
|
524
|
+
ports.exit(suite.code);
|
|
525
|
+
return;
|
|
526
|
+
}
|
|
527
|
+
case VerifyRoute.FLOWS:
|
|
528
|
+
await runVerify({
|
|
529
|
+
url: parsed.url,
|
|
530
|
+
timeoutMs: parsed.timeoutMs ?? DEFAULT_SESSION_TIMEOUT_MS,
|
|
531
|
+
...(true === parsed.explore ? { explore: true } : {}),
|
|
532
|
+
...(parsed.persona === undefined ? {} : { persona: parsed.persona }),
|
|
533
|
+
...(parsed.select === undefined ? {} : { select: parsed.select }),
|
|
534
|
+
}, ports);
|
|
535
|
+
return;
|
|
417
536
|
}
|
|
418
|
-
await runVerify({
|
|
419
|
-
url: parsed.url,
|
|
420
|
-
timeoutMs: parsed.timeoutMs ?? DEFAULT_SESSION_TIMEOUT_MS,
|
|
421
|
-
...(true === parsed.explore ? { explore: true } : {}),
|
|
422
|
-
...(parsed.persona === undefined ? {} : { persona: parsed.persona }),
|
|
423
|
-
...(parsed.select === undefined ? {} : { select: parsed.select }),
|
|
424
|
-
}, ports);
|
|
425
537
|
})();
|
|
426
538
|
}
|
|
@@ -1,6 +1,4 @@
|
|
|
1
1
|
import { type Session } from './auth/cloud-session.js';
|
|
2
|
-
/** Where `reticle login` dials when nothing says otherwise: the hosted service. */
|
|
3
|
-
export declare const DEFAULT_URL = "https://app.reticle.sh";
|
|
4
2
|
export declare const RETICLE_DIR = ".reticle";
|
|
5
3
|
export declare const SESSION_FILE = "session.json";
|
|
6
4
|
export declare const CREDENTIALS_FILE = "credentials.json";
|
|
@@ -16,7 +16,7 @@ import { z } from 'zod';
|
|
|
16
16
|
import { cloudFetch } from '../../memory/cloud/cloud-sync.js';
|
|
17
17
|
import { normalizeUrl, readSessionFrom, resolveSessionFor, sessionPath, } from './auth/cloud-session.js';
|
|
18
18
|
/** Where `reticle login` dials when nothing says otherwise: the hosted service. */
|
|
19
|
-
|
|
19
|
+
const DEFAULT_URL = 'https://app.reticle.sh';
|
|
20
20
|
export const RETICLE_DIR = '.reticle';
|
|
21
21
|
export const SESSION_FILE = 'session.json';
|
|
22
22
|
export const CREDENTIALS_FILE = 'credentials.json';
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/** What this reads off `/status`. Everything is optional — an older daemon may send none of it. */
|
|
2
|
-
|
|
2
|
+
interface SessionFacts {
|
|
3
3
|
sessionCount?: unknown;
|
|
4
4
|
why?: unknown;
|
|
5
5
|
}
|
|
@@ -10,3 +10,4 @@ export interface SessionsLine {
|
|
|
10
10
|
why?: string;
|
|
11
11
|
}
|
|
12
12
|
export declare function sessionsLine(facts: SessionFacts): SessionsLine;
|
|
13
|
+
export {};
|
|
@@ -3,34 +3,7 @@
|
|
|
3
3
|
* "show me the app" flow (`reticle open`). Split out of cli.ts so that file stays under the size cap.
|
|
4
4
|
* The decision logic is pure (unit-tested); the IO (fetch, OS browser launch) is injected/isolated.
|
|
5
5
|
*/
|
|
6
|
-
|
|
7
|
-
interface StatusSession {
|
|
8
|
-
sessionId: string;
|
|
9
|
-
url: string;
|
|
10
|
-
throttled: boolean;
|
|
11
|
-
/**
|
|
12
|
-
* The tab is backgrounded. Reported by the daemon on every session; `false` on one too old to say,
|
|
13
|
-
* which reads as visible — the conservative answer, since it keeps the reuse this command has
|
|
14
|
-
* always done rather than opening a tab on a guess.
|
|
15
|
-
*/
|
|
16
|
-
hidden: boolean;
|
|
17
|
-
stale: boolean;
|
|
18
|
-
pendingMarks: number;
|
|
19
|
-
/** The tab is attached and answering nothing — see Session.unresponsive. Absent on an older daemon. */
|
|
20
|
-
unresponsive?: true;
|
|
21
|
-
}
|
|
22
|
-
/**
|
|
23
|
-
* Reduce the daemon's /status JSON to the compact view `reticle status` prints. Pure: narrows the
|
|
24
|
-
* untrusted wire payload (never `any`) and tolerates a missing/partial body so a malformed response
|
|
25
|
-
* degrades to "running, 0 sessions" instead of throwing.
|
|
26
|
-
*/
|
|
27
|
-
export declare function summarizeStatus(payload: unknown): {
|
|
28
|
-
sessionCount: number;
|
|
29
|
-
sessions: StatusSession[];
|
|
30
|
-
why?: string;
|
|
31
|
-
/** The same diagnosis without the differential, for a surface a person reads. */
|
|
32
|
-
whyLead?: string;
|
|
33
|
-
};
|
|
6
|
+
export { summarizeStatus } from '../../daemon/binding/daemon-status-probe.js';
|
|
34
7
|
/**
|
|
35
8
|
* Warn when the daemon already on this port is a different version than this CLI, and attach anyway.
|
|
36
9
|
*
|
|
@@ -97,4 +70,3 @@ export declare function openInBrowser(url: string, platform?: NodeJS.Platform, r
|
|
|
97
70
|
* by spawning a real launcher on a developer machine.
|
|
98
71
|
*/
|
|
99
72
|
export declare function launcherFailure(code: number | null, signal: NodeJS.Signals | null): string | null;
|
|
100
|
-
export {};
|
|
@@ -7,47 +7,13 @@ import { SERVER_VERSION } from '../../version/identity/server-version.js';
|
|
|
7
7
|
import { log } from '../../../log.js';
|
|
8
8
|
import { fetchStatus } from '../../daemon/binding/daemon-status-probe.js';
|
|
9
9
|
/**
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
10
|
+
* CLI launch + status helpers — the daemon-introspection (`reticle status`) and the one-command
|
|
11
|
+
* "show me the app" flow (`reticle open`). Split out of cli.ts so that file stays under the size cap.
|
|
12
|
+
* The decision logic is pure (unit-tested); the IO (fetch, OS browser launch) is injected/isolated.
|
|
13
13
|
*/
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
const obj = payload;
|
|
18
|
-
// Carried through to the printed line: with no sessions this is the whole answer, and dropping it
|
|
19
|
-
// here would silently undo the reason it is on the wire.
|
|
20
|
-
const why = 'string' === typeof obj['why'] ? obj['why'] : undefined;
|
|
21
|
-
// Absent on a daemon older than this field, which is why every reader falls back to `why`.
|
|
22
|
-
const whyLead = 'string' === typeof obj['whyLead'] ? obj['whyLead'] : undefined;
|
|
23
|
-
const raw = Array.isArray(obj['sessions']) ? obj['sessions'] : [];
|
|
24
|
-
const sessions = raw
|
|
25
|
-
.map((s) => {
|
|
26
|
-
if (typeof s !== 'object' || null === s)
|
|
27
|
-
return null;
|
|
28
|
-
const r = s;
|
|
29
|
-
const sessionId = 'string' === typeof r['sessionId'] ? r['sessionId'] : '';
|
|
30
|
-
if ('' === sessionId)
|
|
31
|
-
return null;
|
|
32
|
-
return {
|
|
33
|
-
sessionId,
|
|
34
|
-
url: 'string' === typeof r['url'] ? r['url'] : '',
|
|
35
|
-
throttled: true === r['throttled'],
|
|
36
|
-
hidden: true === r['hidden'],
|
|
37
|
-
stale: true === r['stale'],
|
|
38
|
-
pendingMarks: 'number' === typeof r['pendingMarks'] ? r['pendingMarks'] : 0,
|
|
39
|
-
...(true === r['unresponsive'] ? { unresponsive: true } : {}),
|
|
40
|
-
};
|
|
41
|
-
})
|
|
42
|
-
.filter((s) => s !== null);
|
|
43
|
-
const sessionCount = 'number' === typeof obj['sessionCount'] ? obj['sessionCount'] : sessions.length;
|
|
44
|
-
return {
|
|
45
|
-
sessionCount,
|
|
46
|
-
sessions,
|
|
47
|
-
...(why === undefined ? {} : { why }),
|
|
48
|
-
...(whyLead === undefined ? {} : { whyLead }),
|
|
49
|
-
};
|
|
50
|
-
}
|
|
14
|
+
// Parsing the daemon's /status payload lives beside the probe that fetches it; re-exported here for
|
|
15
|
+
// the callers that already import it from this module.
|
|
16
|
+
export { summarizeStatus } from '../../daemon/binding/daemon-status-probe.js';
|
|
51
17
|
/** A string field off the /status body, or undefined on a daemon too old to report it. */
|
|
52
18
|
function statusField(payload, key) {
|
|
53
19
|
if (typeof payload !== 'object' || null === payload)
|
|
@@ -41,6 +41,19 @@ export declare function readProjectPort(cwd: string): number | undefined;
|
|
|
41
41
|
* undefined if the file is absent/unreadable or has no non-empty string projectId.
|
|
42
42
|
*/
|
|
43
43
|
export declare function readProjectId(cwd: string): ProjectId | undefined;
|
|
44
|
+
/**
|
|
45
|
+
* Every project id this directory can speak for, for "has THIS project ever connected".
|
|
46
|
+
*
|
|
47
|
+
* `readProjectId` knows only what `init` wrote. The build plugins derive the same id from the package
|
|
48
|
+
* name and the app root WITHOUT writing `.reticle.json`, so a monorepo root (the app lives in
|
|
49
|
+
* `apps/web`) and an app wired by the plugin alone both read `undefined`, and the handshake told a
|
|
50
|
+
* working project nothing had ever connected. Deriving the plugin's own id cannot reopen the opposite
|
|
51
|
+
* lie: an id only counts once an app with exactly that id has dialled in.
|
|
52
|
+
*
|
|
53
|
+
* Workspace discovery is init's `findWorkspaceApps`, reused so the two cannot disagree about which
|
|
54
|
+
* directories are apps. Best-effort: a directory it cannot read contributes nothing.
|
|
55
|
+
*/
|
|
56
|
+
export declare function projectIdsAt(cwd: string): ProjectId[];
|
|
44
57
|
/**
|
|
45
58
|
* The framework `reticle init` stamped into `.reticle.json`, when there is one.
|
|
46
59
|
*
|
|
@@ -8,7 +8,8 @@
|
|
|
8
8
|
* 4. RETICLE_DEFAULT_PORT (4400)
|
|
9
9
|
*/
|
|
10
10
|
import { readFileSync } from 'node:fs';
|
|
11
|
-
import { dirname, resolve } from 'node:path';
|
|
11
|
+
import { dirname, join, resolve } from 'node:path';
|
|
12
|
+
import { SILENT_HOST, buildNodeIo, deriveProjectId, findWorkspaceApps, packageName, } from '@reticlehq/init';
|
|
12
13
|
import { asProjectId } from '@reticlehq/core';
|
|
13
14
|
/** The project config `reticle init` writes. */
|
|
14
15
|
export const RETICLE_CONFIG_BASENAME = '.reticle.json';
|
|
@@ -84,6 +85,49 @@ export function readProjectId(cwd) {
|
|
|
84
85
|
return asProjectId(id);
|
|
85
86
|
return undefined;
|
|
86
87
|
}
|
|
88
|
+
const PACKAGE_JSON = 'package.json';
|
|
89
|
+
/** The package name in `dir/package.json`, or undefined when there is no readable one. */
|
|
90
|
+
function packageNameAt(dir) {
|
|
91
|
+
try {
|
|
92
|
+
return packageName(JSON.parse(readFileSync(join(dir, PACKAGE_JSON), 'utf8')));
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
return undefined;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
/** The id an app at `dir` carries: the one `init` recorded, else the one the build plugin derives. */
|
|
99
|
+
function projectIdFor(dir) {
|
|
100
|
+
return readProjectId(dir) ?? asProjectId(deriveProjectId(packageNameAt(dir), dir));
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Every project id this directory can speak for, for "has THIS project ever connected".
|
|
104
|
+
*
|
|
105
|
+
* `readProjectId` knows only what `init` wrote. The build plugins derive the same id from the package
|
|
106
|
+
* name and the app root WITHOUT writing `.reticle.json`, so a monorepo root (the app lives in
|
|
107
|
+
* `apps/web`) and an app wired by the plugin alone both read `undefined`, and the handshake told a
|
|
108
|
+
* working project nothing had ever connected. Deriving the plugin's own id cannot reopen the opposite
|
|
109
|
+
* lie: an id only counts once an app with exactly that id has dialled in.
|
|
110
|
+
*
|
|
111
|
+
* Workspace discovery is init's `findWorkspaceApps`, reused so the two cannot disagree about which
|
|
112
|
+
* directories are apps. Best-effort: a directory it cannot read contributes nothing.
|
|
113
|
+
*/
|
|
114
|
+
export function projectIdsAt(cwd) {
|
|
115
|
+
const recorded = readProjectId(cwd);
|
|
116
|
+
if (recorded !== undefined)
|
|
117
|
+
return [recorded];
|
|
118
|
+
const ids = [];
|
|
119
|
+
if (packageNameAt(cwd) !== undefined)
|
|
120
|
+
ids.push(projectIdFor(cwd));
|
|
121
|
+
try {
|
|
122
|
+
for (const app of findWorkspaceApps(buildNodeIo(cwd, SILENT_HOST))) {
|
|
123
|
+
ids.push(projectIdFor(join(cwd, app)));
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
catch {
|
|
127
|
+
// Discovery touches the filesystem; an unreadable tree is simply no further evidence.
|
|
128
|
+
}
|
|
129
|
+
return [...new Set(ids)];
|
|
130
|
+
}
|
|
87
131
|
/**
|
|
88
132
|
* The framework `reticle init` stamped into `.reticle.json`, when there is one.
|
|
89
133
|
*
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export interface ReportInput {
|
|
2
|
+
cwd: string;
|
|
3
|
+
now: number;
|
|
4
|
+
/** Did the working tree change? The hook stays silent on a turn that edited nothing. */
|
|
5
|
+
treeChanged: boolean;
|
|
6
|
+
session?: string;
|
|
7
|
+
hook?: boolean;
|
|
8
|
+
}
|
|
9
|
+
export declare function reportFor(input: ReportInput): Promise<{
|
|
10
|
+
lines: string[];
|
|
11
|
+
}>;
|
|
12
|
+
/**
|
|
13
|
+
* The command. As a hook it prints Claude Code's `systemMessage` shape and always exits 0: it is
|
|
14
|
+
* print-only by design, so the human sees the gap and the agent is never blocked by it.
|
|
15
|
+
*/
|
|
16
|
+
export declare function handleReport(session: string | undefined, hook: boolean): Promise<void>;
|