@reticlehq/server 3.8.2 → 3.9.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/LICENSE +201 -104
- package/NOTICE +5 -0
- package/README.md +1 -1
- package/bin/launch.js +13 -0
- package/bin/package.json +3 -0
- package/bin/reticle.js +66 -8
- package/dist/chromium-launch-options.d.ts +25 -0
- package/dist/chromium-launch-options.js +21 -1
- package/dist/command/cli/adhoc-verdict.js +34 -7
- package/dist/command/cli/answers/gate-exit.d.ts +6 -2
- package/dist/command/cli/answers/gate-exit.js +12 -2
- package/dist/command/cli/ci-hint.d.ts +44 -0
- package/dist/command/cli/ci-hint.js +72 -0
- package/dist/command/cli/cli-doctor.d.ts +6 -1
- package/dist/command/cli/cli-doctor.js +45 -4
- package/dist/command/cli/cli-flow-commands.d.ts +7 -1
- package/dist/command/cli/cli-flow-commands.js +39 -17
- package/dist/command/cli/cli-kill.d.ts +21 -0
- package/dist/command/cli/cli-kill.js +46 -3
- package/dist/command/cli/cli-parse.d.ts +5 -2
- package/dist/command/cli/cli-parse.js +10 -5
- package/dist/command/cli/cli-usage.js +3 -2
- package/dist/command/cli/cli-verify.d.ts +6 -1
- package/dist/command/cli/cli-verify.js +32 -13
- package/dist/command/cli/doctor/browser/chromium-hint.d.ts +6 -1
- package/dist/command/cli/doctor/browser/chromium-hint.js +11 -5
- package/dist/command/cli/doctor/doctor-fix.d.ts +44 -0
- package/dist/command/cli/doctor/doctor-fix.js +89 -0
- package/dist/command/cli/doctor/doctor-launcher-line.d.ts +23 -0
- package/dist/command/cli/doctor/doctor-launcher-line.js +60 -0
- package/dist/command/cli/doctor/doctor-rows.d.ts +4 -0
- package/dist/command/cli/doctor/doctor-rows.js +4 -0
- package/dist/command/cli/doctor-repairs.d.ts +64 -0
- package/dist/command/cli/doctor-repairs.js +214 -0
- package/dist/command/cli/drive/drive-command.js +3 -7
- package/dist/command/cli/first-use-checks.d.ts +41 -0
- package/dist/command/cli/first-use-checks.js +127 -0
- package/dist/command/cli/first-verdict-node.d.ts +3 -0
- package/dist/command/cli/first-verdict-node.js +189 -0
- package/dist/command/cli/first-verdict.d.ts +126 -0
- package/dist/command/cli/first-verdict.js +267 -0
- package/dist/command/cli/launch/sdk-version-match.d.ts +3 -2
- package/dist/command/cli/launch/sdk-version-match.js +3 -2
- package/dist/command/cli/lifecycle/daemon-lifecycle.js +6 -5
- package/dist/command/cli/mcp-command.js +13 -4
- package/dist/command/cli/ports/port-holder.d.ts +2 -0
- package/dist/command/cli/ports/port-holder.js +11 -3
- package/dist/command/cli/setup-mcp-cli.d.ts +27 -1
- package/dist/command/cli/setup-mcp-cli.js +98 -23
- package/dist/command/cli/status/status-lines.d.ts +1 -0
- package/dist/command/cli/status/status-lines.js +50 -17
- package/dist/command/cli/status-fields.js +17 -3
- package/dist/command/cli/try-command.d.ts +7 -0
- package/dist/command/cli/try-command.js +11 -3
- package/dist/command/cli.js +70 -33
- package/dist/command/daemon/binding/daemon-status-probe.d.ts +3 -1
- package/dist/command/daemon/binding/daemon-status-probe.js +4 -2
- package/dist/command/daemon/daemon-argv.d.ts +9 -0
- package/dist/command/daemon/daemon-argv.js +11 -0
- package/dist/command/daemon/daemon.d.ts +10 -3
- package/dist/command/daemon/daemon.js +17 -6
- package/dist/command/daemon/lifetime/daemon-exit.d.ts +24 -0
- package/dist/command/daemon/lifetime/daemon-exit.js +39 -0
- package/dist/command/daemon/lifetime/death-cause.d.ts +15 -0
- package/dist/command/daemon/lifetime/death-cause.js +130 -0
- package/dist/command/daemon/lifetime/heartbeat.d.ts +7 -15
- package/dist/command/daemon/lifetime/heartbeat.js +4 -13
- package/dist/command/daemon/port-state.d.ts +14 -0
- package/dist/command/daemon/port-state.js +31 -0
- package/dist/command/setup/agent-configs.d.ts +2 -0
- package/dist/command/setup/agent-configs.js +42 -15
- package/dist/command/setup/agent-next-steps.d.ts +77 -0
- package/dist/command/setup/agent-next-steps.js +284 -0
- package/dist/command/setup/agent-writer.js +11 -6
- package/dist/command/setup/init/init-host.js +2 -0
- package/dist/command/setup/init/init-runtime.d.ts +6 -1
- package/dist/command/setup/init/init-runtime.js +57 -21
- package/dist/command/setup/mcp-launcher.d.ts +3 -0
- package/dist/command/setup/mcp-launcher.js +11 -0
- package/dist/command/setup/setup-command.js +30 -12
- package/dist/command/setup/setup-mcp.d.ts +8 -0
- package/dist/command/setup/setup-mcp.js +47 -20
- package/dist/command/setup/start-dev-server.d.ts +8 -0
- package/dist/command/setup/start-dev-server.js +43 -0
- package/dist/command/version/identity/server-version.d.ts +2 -0
- package/dist/command/version/identity/server-version.js +2 -0
- package/dist/command/version/version-skew.d.ts +7 -0
- package/dist/command/version/version-skew.js +13 -2
- package/dist/features/license/license.d.ts +11 -71
- package/dist/features/license/license.js +43 -136
- package/dist/features/visual/desktop-capture.d.ts +33 -0
- package/dist/features/visual/desktop-capture.js +94 -0
- package/dist/features/visual/step-shots.js +139 -35
- package/dist/features/visual/visual-tools.d.ts +0 -18
- package/dist/features/visual/visual-tools.js +2 -81
- package/dist/import-graph.js +5 -6
- package/dist/index.d.ts +3 -1
- package/dist/index.js +14 -5
- package/dist/judgement/runs/run-ledger.d.ts +7 -5
- package/dist/judgement/runs/run-ledger.js +98 -69
- package/dist/judgement/runs/verification-sync.d.ts +6 -3
- package/dist/judgement/runs/verification-sync.js +21 -10
- package/dist/language/flows/flow-store-for-session.js +7 -5
- package/dist/language/flows/recording/tape/recordings.d.ts +2 -13
- package/dist/language/flows/recording/tape/recordings.js +41 -44
- package/dist/language/flows/replay.d.ts +17 -7
- package/dist/language/flows/replay.js +95 -44
- package/dist/launch-chromium.d.ts +1 -0
- package/dist/launch-chromium.js +49 -12
- package/dist/memory/cloud/sync-screenshots.js +6 -1
- package/dist/portal/bridge/bridge.d.ts +13 -4
- package/dist/portal/bridge/bridge.js +10 -9
- package/dist/portal/input/network-mock.js +3 -2
- package/dist/portal/input/real-input.d.ts +15 -0
- package/dist/portal/input/real-input.js +59 -3
- package/dist/portal/input/viewport-tools.js +6 -3
- package/dist/portal/pool/browser-pool.d.ts +5 -1
- package/dist/portal/pool/browser-pool.js +32 -38
- package/dist/portal/pool/context-permissions.d.ts +20 -17
- package/dist/portal/pool/context-permissions.js +63 -1
- package/dist/portal/pool/csp-widen.d.ts +62 -0
- package/dist/portal/pool/csp-widen.js +132 -0
- package/dist/portal/pool/lease-page-error.d.ts +29 -0
- package/dist/portal/pool/lease-page-error.js +18 -0
- package/dist/portal/pool/page-error-journal.d.ts +57 -0
- package/dist/portal/pool/page-error-journal.js +181 -0
- package/dist/portal/pool/playwright-launcher.js +21 -8
- package/dist/portal/pool/pool-contract.d.ts +22 -0
- package/dist/portal/pool/pool-lease.d.ts +35 -2
- package/dist/portal/pool/pool-lease.js +23 -0
- package/dist/portal/pool/zero-install.js +69 -1
- package/dist/portal/session/dev-server/dev-command.d.ts +8 -0
- package/dist/portal/session/dev-server/dev-command.js +56 -8
- package/dist/portal/session/no-session-diagnosis.js +10 -3
- package/dist/portal/session/no-session-next-action.d.ts +12 -0
- package/dist/portal/session/no-session-next-action.js +44 -4
- package/dist/portal/session/no-session-watch.js +3 -1
- package/dist/portal/session/page-commands/undeclared-command.d.ts +9 -3
- package/dist/portal/session/page-commands/undeclared-command.js +16 -7
- package/dist/portal/session/recorded-gaps.d.ts +1 -1
- package/dist/portal/session/recorded-gaps.js +5 -2
- package/dist/portal/session/session-manager.js +40 -21
- package/dist/portal/session/session.js +4 -3
- package/dist/portal/session/zero-install-first-call.d.ts +20 -1
- package/dist/portal/session/zero-install-first-call.js +34 -2
- package/dist/start-wiring.d.ts +29 -2
- package/dist/start-wiring.js +73 -9
- package/dist/surface/mcp/mcp-proxy.js +10 -2
- package/dist/surface/mcp/mcp.js +5 -3
- package/dist/surface/mcp/peer/agents-connected.d.ts +6 -0
- package/dist/surface/mcp/peer/agents-connected.js +58 -0
- package/dist/surface/mcp/proxy/proxy-daemon-probe.js +17 -5
- package/dist/surface/tools/act/act-preflight.js +37 -17
- package/dist/surface/tools/act/act-target.js +3 -1
- package/dist/surface/tools/act/after-match.d.ts +7 -0
- package/dist/surface/tools/act/after-match.js +33 -0
- package/dist/surface/tools/act/resolve-target.d.ts +2 -0
- package/dist/surface/tools/act/resolve-target.js +35 -8
- package/dist/surface/tools/act/sequence-grade.d.ts +3 -1
- package/dist/surface/tools/act/sequence-grade.js +8 -1
- package/dist/surface/tools/act-sequence-tool.js +3 -0
- package/dist/surface/tools/act-tools.js +10 -10
- package/dist/surface/tools/assert/assert-verdict.js +28 -1
- package/dist/surface/tools/browser-tools.js +75 -7
- package/dist/surface/tools/dynamic-tools.js +2 -1
- package/dist/surface/tools/empty-session-list.d.ts +2 -0
- package/dist/surface/tools/empty-session-list.js +4 -2
- package/dist/surface/tools/error-recovery.js +43 -22
- package/dist/surface/tools/invoke-tool.js +27 -20
- package/dist/surface/tools/lease-hint.d.ts +7 -0
- package/dist/surface/tools/lease-hint.js +10 -3
- package/dist/surface/tools/lease-readiness.d.ts +21 -0
- package/dist/surface/tools/lease-readiness.js +47 -0
- package/dist/surface/tools/lease-seed.d.ts +2 -0
- package/dist/surface/tools/lease-seed.js +2 -1
- package/dist/surface/tools/lease-tools.js +43 -15
- package/dist/surface/tools/repeat-hint.d.ts +40 -0
- package/dist/surface/tools/repeat-hint.js +171 -0
- package/dist/surface/tools/run-report.d.ts +2 -2
- package/dist/surface/tools/run-report.js +27 -15
- package/dist/surface/tools/tool-kit.d.ts +12 -0
- package/dist/surface/tools/tool-kit.js +7 -0
- package/dist/surface/tools/tools.js +26 -13
- package/dist/telemetry/argument-shape.js +16 -7
- package/dist/telemetry/cli-telemetry.d.ts +10 -1
- package/dist/telemetry/cli-telemetry.js +15 -0
- package/dist/telemetry/init-telemetry.d.ts +2 -1
- package/dist/telemetry/init-telemetry.js +47 -2
- package/dist/telemetry/install-trace.js +23 -4
- package/dist/telemetry/onboarding-funnel.d.ts +19 -2
- package/dist/telemetry/onboarding-funnel.js +25 -3
- package/dist/telemetry/proxy-telemetry.d.ts +2 -1
- package/dist/telemetry/proxy-telemetry.js +2 -2
- package/dist/telemetry/session-metrics.d.ts +1 -1
- package/dist/telemetry/session-metrics.js +67 -27
- package/dist/telemetry/telemetry.d.ts +3 -1
- package/dist/telemetry/telemetry.js +60 -28
- package/dist/telemetry/tool-refused.d.ts +2 -2
- package/dist/telemetry/tool-refused.js +9 -2
- package/docs/architecture.md +3 -6
- package/docs/cli/doctor.mdx +41 -2
- package/docs/cli/help.mdx +4 -2
- package/docs/cli/license.mdx +4 -5
- package/docs/cli/setup.mdx +13 -2
- package/docs/cli/status.mdx +4 -0
- package/docs/cli.mdx +1 -1
- package/docs/deploy-checks.md +25 -0
- package/docs/desktop.mdx +1 -1
- package/docs/enterprise.md +15 -20
- package/docs/environment.mdx +1 -0
- package/docs/first-drive.md +1 -1
- package/docs/install-manual.mdx +16 -1
- package/docs/packages/engine.mdx +1 -1
- package/docs/packages/server.mdx +1 -1
- package/docs/packages/tauri.mdx +1 -1
- package/docs/packages/test.mdx +1 -1
- package/docs/packages.mdx +1 -1
- package/docs/platform-integration.md +1 -1
- package/docs/skill.md +1 -1
- package/docs/telemetry.md +4 -2
- package/package.json +13 -9
- package/LICENSE-ENTERPRISE +0 -37
- package/dist/features/ee/audit-log.d.ts +0 -14
- package/dist/features/ee/audit-log.js +0 -11
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One line at the end of `reticle gate`, `reticle verify` and `reticle affected` when they run in a
|
|
3
|
+
* CI runner or a container. It names the pull request check and the page that tells more about it.
|
|
4
|
+
*
|
|
5
|
+
* Rules: one line; never in an interactive terminal; never in a machine output mode; off with
|
|
6
|
+
* `RETICLE_NO_CI_HINT`. It goes to stderr. The stdout of `verify` carries the verdict lines that a
|
|
7
|
+
* pipeline reads, and stderr already carries the human notes of these commands.
|
|
8
|
+
*
|
|
9
|
+
* No network call. The latest version comes from the update manifest that the update check already
|
|
10
|
+
* caches on disk. When there is no cache, the line does not compare versions.
|
|
11
|
+
*
|
|
12
|
+
* The caller gives the automation hint (`currentAutomationHint`): `telemetry` already reaches this
|
|
13
|
+
* directory, so this directory does not reach back.
|
|
14
|
+
*/
|
|
15
|
+
import { AutomationHint } from '@reticlehq/core/telemetry';
|
|
16
|
+
/** Set to `1` to stop the line. */
|
|
17
|
+
export declare const CI_HINT_SILENCE_ENV = "RETICLE_NO_CI_HINT";
|
|
18
|
+
/** The page that tells about the pull request check. */
|
|
19
|
+
export declare const CI_HINT_URL = "https://reticle.sh/ci";
|
|
20
|
+
export interface CiHintInput {
|
|
21
|
+
env: NodeJS.ProcessEnv;
|
|
22
|
+
automation: AutomationHint | undefined;
|
|
23
|
+
/** A person watches the output in a terminal. */
|
|
24
|
+
interactive: boolean;
|
|
25
|
+
/** The command writes output for a program to read (JSON, an agent hook). */
|
|
26
|
+
machineOutput: boolean;
|
|
27
|
+
currentVersion: string;
|
|
28
|
+
/** The latest version from the cached update check, if there is one. */
|
|
29
|
+
latestVersion: string | undefined;
|
|
30
|
+
}
|
|
31
|
+
/** The line, or undefined when it must not print. Pure. */
|
|
32
|
+
export declare function ciHintLine(input: CiHintInput): string | undefined;
|
|
33
|
+
export interface CiHintPorts {
|
|
34
|
+
env: NodeJS.ProcessEnv;
|
|
35
|
+
interactive: () => boolean;
|
|
36
|
+
latestVersion: () => string | undefined;
|
|
37
|
+
write: (text: string) => void;
|
|
38
|
+
}
|
|
39
|
+
/** Write the line to stderr when it applies. Call it last, after the command's own output. */
|
|
40
|
+
export declare function writeCiHint(args: {
|
|
41
|
+
automation: AutomationHint | undefined;
|
|
42
|
+
machineOutput: boolean;
|
|
43
|
+
ports?: CiHintPorts;
|
|
44
|
+
}): void;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One line at the end of `reticle gate`, `reticle verify` and `reticle affected` when they run in a
|
|
3
|
+
* CI runner or a container. It names the pull request check and the page that tells more about it.
|
|
4
|
+
*
|
|
5
|
+
* Rules: one line; never in an interactive terminal; never in a machine output mode; off with
|
|
6
|
+
* `RETICLE_NO_CI_HINT`. It goes to stderr. The stdout of `verify` carries the verdict lines that a
|
|
7
|
+
* pipeline reads, and stderr already carries the human notes of these commands.
|
|
8
|
+
*
|
|
9
|
+
* No network call. The latest version comes from the update manifest that the update check already
|
|
10
|
+
* caches on disk. When there is no cache, the line does not compare versions.
|
|
11
|
+
*
|
|
12
|
+
* The caller gives the automation hint (`currentAutomationHint`): `telemetry` already reaches this
|
|
13
|
+
* directory, so this directory does not reach back.
|
|
14
|
+
*/
|
|
15
|
+
import { AutomationHint } from '@reticlehq/core/telemetry';
|
|
16
|
+
import { parseMajor } from '@reticlehq/init';
|
|
17
|
+
import { loadManifest } from '../update/update-checker.js';
|
|
18
|
+
import { SERVER_VERSION } from '../version/identity/server-version.js';
|
|
19
|
+
/** Set to `1` to stop the line. */
|
|
20
|
+
export const CI_HINT_SILENCE_ENV = 'RETICLE_NO_CI_HINT';
|
|
21
|
+
/** The page that tells about the pull request check. */
|
|
22
|
+
export const CI_HINT_URL = 'https://reticle.sh/ci';
|
|
23
|
+
/** The env values that do not turn the line off. */
|
|
24
|
+
const NOT_SILENCED = new Set(['', '0', 'false']);
|
|
25
|
+
/** The automation hints that get the line. A hosted workspace or an agent sandbox is a person. */
|
|
26
|
+
const CI_HINT_AUTOMATION = new Set([
|
|
27
|
+
AutomationHint.CI_RUNNER,
|
|
28
|
+
AutomationHint.CONTAINER,
|
|
29
|
+
]);
|
|
30
|
+
const CiHintCopy = {
|
|
31
|
+
CHECK: `Reticle: a pull request check shows each replay, the run history and a merge block. Read ${CI_HINT_URL}`,
|
|
32
|
+
BEHIND: (current, latest) => `Reticle: this engine (${current}) is a major version behind ${latest}. Update it, then read ${CI_HINT_URL}`,
|
|
33
|
+
};
|
|
34
|
+
/** The line, or undefined when it must not print. Pure. */
|
|
35
|
+
export function ciHintLine(input) {
|
|
36
|
+
if (!NOT_SILENCED.has(input.env[CI_HINT_SILENCE_ENV] ?? '')) {
|
|
37
|
+
return undefined;
|
|
38
|
+
}
|
|
39
|
+
if (input.interactive || input.machineOutput) {
|
|
40
|
+
return undefined;
|
|
41
|
+
}
|
|
42
|
+
if (input.automation === undefined || !CI_HINT_AUTOMATION.has(input.automation)) {
|
|
43
|
+
return undefined;
|
|
44
|
+
}
|
|
45
|
+
const current = parseMajor(input.currentVersion);
|
|
46
|
+
const latest = parseMajor(input.latestVersion);
|
|
47
|
+
if (current !== undefined && latest !== undefined && latest > current) {
|
|
48
|
+
return CiHintCopy.BEHIND(input.currentVersion, input.latestVersion ?? '');
|
|
49
|
+
}
|
|
50
|
+
return CiHintCopy.CHECK;
|
|
51
|
+
}
|
|
52
|
+
const realPorts = () => ({
|
|
53
|
+
env: process.env,
|
|
54
|
+
interactive: () => true === process.stdout.isTTY || true === process.stderr.isTTY,
|
|
55
|
+
latestVersion: () => loadManifest()?.latestVersion,
|
|
56
|
+
write: (text) => process.stderr.write(text),
|
|
57
|
+
});
|
|
58
|
+
/** Write the line to stderr when it applies. Call it last, after the command's own output. */
|
|
59
|
+
export function writeCiHint(args) {
|
|
60
|
+
const ports = args.ports ?? realPorts();
|
|
61
|
+
const line = ciHintLine({
|
|
62
|
+
env: ports.env,
|
|
63
|
+
automation: args.automation,
|
|
64
|
+
interactive: ports.interactive(),
|
|
65
|
+
machineOutput: args.machineOutput,
|
|
66
|
+
currentVersion: SERVER_VERSION,
|
|
67
|
+
latestVersion: ports.latestVersion(),
|
|
68
|
+
});
|
|
69
|
+
if (line !== undefined) {
|
|
70
|
+
ports.write(`${line}\n`);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
@@ -1 +1,6 @@
|
|
|
1
|
-
|
|
1
|
+
import type { StepReporter } from './setup-mcp-cli.js';
|
|
2
|
+
/**
|
|
3
|
+
* `reticle doctor`, and with `fix`, `reticle doctor --fix`. Without `fix` it changes nothing and
|
|
4
|
+
* ends with how many rows `--fix` can repair. With `fix` the exit code is 1 while any row fails.
|
|
5
|
+
*/
|
|
6
|
+
export declare function handleDoctor(port: number, fix?: boolean, reportStep?: StepReporter): Promise<void>;
|
|
@@ -11,7 +11,8 @@ import { daemonLine } from './doctor/doctor-daemon-line.js';
|
|
|
11
11
|
import { projectWiringLine } from './doctor/doctor-project-line.js';
|
|
12
12
|
import { hasProjectConnectedBefore } from '../../memory/recall/prior/connection-memory.js';
|
|
13
13
|
import { sessionsLine } from './doctor/doctor-sessions-line.js';
|
|
14
|
-
import { captureLookup, describeForeignHolder
|
|
14
|
+
import { captureLookup, describeForeignHolder } from './ports/port-holder.js';
|
|
15
|
+
import { identifyPortHolder } from '../daemon/port-state.js';
|
|
15
16
|
import { chromiumHint } from './doctor/browser/chromium-hint.js';
|
|
16
17
|
import { probeChromiumWithFallback } from '../../launch-chromium.js';
|
|
17
18
|
import { SERVER_VERSION } from '../version/identity/server-version.js';
|
|
@@ -32,6 +33,8 @@ import { describeUnsynced } from '../../memory/cloud/unsynced-roots.js';
|
|
|
32
33
|
import { machineUnsyncedRoots } from '../../memory/project/sync-status.js';
|
|
33
34
|
import { attachState, describeAttachState } from '../../surface/mcp/attach-memory.js';
|
|
34
35
|
import { agentLinkText } from './doctor/doctor-agent-link.js';
|
|
36
|
+
import { countNeeded, fixFooter, runDoctorFix } from './doctor/doctor-fix.js';
|
|
37
|
+
import { machineRepairs, mcpRegistrationRow, readLauncherLine, readMcpRegistration, } from './doctor-repairs.js';
|
|
35
38
|
import { findOccupiedSiblings, siblingListenerNote } from './ports/sibling-ports.js';
|
|
36
39
|
import { daemonsServingProjectElsewhere, resolveDaemonForProject, splitBrainNote, wrongDaemonNote, } from '../daemon/daemon-resolve.js';
|
|
37
40
|
import { isAlive } from '../daemon/daemon.js';
|
|
@@ -73,9 +76,41 @@ function splitBrainLine(port, projectId) {
|
|
|
73
76
|
const elsewhere = splitBrainNote(port, daemonsServingProjectElsewhere(projectId, port, home, isAlive, (other) => hasProjectConnectedBefore(home, other, projectId)));
|
|
74
77
|
return elsewhere ?? wrongDaemonNote(port, resolveDaemonForProject(projectId, home, isAlive));
|
|
75
78
|
}
|
|
76
|
-
|
|
79
|
+
const writeLine = (s) => {
|
|
80
|
+
process.stdout.write(`${s}\n`);
|
|
81
|
+
};
|
|
82
|
+
/**
|
|
83
|
+
* `reticle doctor`, and with `fix`, `reticle doctor --fix`. Without `fix` it changes nothing and
|
|
84
|
+
* ends with how many rows `--fix` can repair. With `fix` the exit code is 1 while any row fails.
|
|
85
|
+
*/
|
|
86
|
+
export async function handleDoctor(port, fix = false, reportStep = () => { }) {
|
|
87
|
+
const repairs = machineRepairs(port, reportStep);
|
|
88
|
+
if (fix) {
|
|
89
|
+
const green = await runDoctorFix({
|
|
90
|
+
doctor: (show) => doctorLines(port, show),
|
|
91
|
+
repairs,
|
|
92
|
+
print: writeLine,
|
|
93
|
+
});
|
|
94
|
+
if (!green) {
|
|
95
|
+
process.exitCode = 1;
|
|
96
|
+
}
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
await doctorLines(port, true);
|
|
100
|
+
const footer = fixFooter(await countNeeded(repairs));
|
|
101
|
+
if (footer !== undefined) {
|
|
102
|
+
writeLine('');
|
|
103
|
+
writeLine(footer);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
/** Every doctor row, in order. Each row is printed as it is found when `show` is true. */
|
|
107
|
+
async function doctorLines(port, show) {
|
|
108
|
+
const lines = [];
|
|
77
109
|
const line = (s) => {
|
|
78
|
-
|
|
110
|
+
lines.push(s);
|
|
111
|
+
if (show) {
|
|
112
|
+
writeLine(s);
|
|
113
|
+
}
|
|
79
114
|
};
|
|
80
115
|
line('reticle doctor');
|
|
81
116
|
line(doctorRow(DoctorRow.NODE, process.version));
|
|
@@ -84,6 +119,7 @@ export async function handleDoctor(port) {
|
|
|
84
119
|
let sessions;
|
|
85
120
|
let daemonStatus;
|
|
86
121
|
line(doctorRow(DoctorRow.CHROMIUM, chromiumHint(await probeChromiumWithFallback())));
|
|
122
|
+
line(mcpRegistrationRow(readMcpRegistration()));
|
|
87
123
|
// Ask the PORT, not just the pid file. "not running on :4400" has been printed about a port that
|
|
88
124
|
// was demonstrably occupied, which sends the reader to start a daemon that cannot bind. The three
|
|
89
125
|
// states are genuinely different problems with different fixes, so doctor names which one it is.
|
|
@@ -126,7 +162,7 @@ export async function handleDoctor(port) {
|
|
|
126
162
|
// pipeline that also kills the agent's own MCP proxy.
|
|
127
163
|
// `pid` is what our pid file recorded for this port. When it matches the process actually
|
|
128
164
|
// holding it, this is our OWN daemon wedged, not a stranger — and the fix is different.
|
|
129
|
-
line(doctorRow(DoctorRow.DAEMON, `✗ ${describeForeignHolder(port,
|
|
165
|
+
line(doctorRow(DoctorRow.DAEMON, `✗ ${describeForeignHolder(port, identifyPortHolder(port, captureLookup), pid)}`));
|
|
130
166
|
}
|
|
131
167
|
else {
|
|
132
168
|
line(doctorRow(DoctorRow.DAEMON, `✗ not running on :${port} — your agent runs \`reticle mcp\` (or \`reticle serve\`)`));
|
|
@@ -167,6 +203,10 @@ export async function handleDoctor(port) {
|
|
|
167
203
|
// would flip agent presence on a daemon with no agents attached.
|
|
168
204
|
const foreign = presence === PortPresence.FOREIGN;
|
|
169
205
|
line(doctorRow(DoctorRow.AGENT_LINK, agentLinkText({ split, attachAction, foreign })));
|
|
206
|
+
const launcher = readLauncherLine(process.cwd());
|
|
207
|
+
if (launcher !== undefined) {
|
|
208
|
+
line(doctorRow(DoctorRow.LAUNCHER, launcher));
|
|
209
|
+
}
|
|
170
210
|
// Remaining half of #261. The SDK dialling a port we are not on is invisible as a refused inbound,
|
|
171
211
|
// but a listener on a well-known Reticle port is visible. Report the observation; do not conclude
|
|
172
212
|
// it is the daemon this app wants — somebody else's daemon looks the same from here.
|
|
@@ -263,6 +303,7 @@ export async function handleDoctor(port) {
|
|
|
263
303
|
if (keychain !== undefined) {
|
|
264
304
|
line(doctorRow(DoctorRow.DESKTOP, `⚠ ${keychain}`));
|
|
265
305
|
}
|
|
306
|
+
return lines;
|
|
266
307
|
}
|
|
267
308
|
/**
|
|
268
309
|
* Two rows: does each agent hand its prompts to Reticle, and does the daemon send this
|
|
@@ -9,6 +9,7 @@ import { type ChangedFiles } from '../../language/flows/change/git-changed.js';
|
|
|
9
9
|
import { type ProjectId } from '@reticlehq/core';
|
|
10
10
|
import { type FileSystemPort } from '../../memory/project/fs/fs-port.js';
|
|
11
11
|
import { type NamedFlow } from '../../language/flows/change/flow-sources.js';
|
|
12
|
+
import { type GateCompleted } from '@reticlehq/core/telemetry';
|
|
12
13
|
/** Load the {name, steps} of every saved flow for the active project. */
|
|
13
14
|
/** Explicit files plus, when --since is given, the git-changed files since that ref. */
|
|
14
15
|
/**
|
|
@@ -30,4 +31,9 @@ export declare function handleGate(files: string[], since: string | undefined,
|
|
|
30
31
|
/** Hook mode: prose for a human, and silence when there was simply nothing to check. */
|
|
31
32
|
hook?: boolean,
|
|
32
33
|
/** Accept today's coverage levels as the best before judging, for a drop that was intended. */
|
|
33
|
-
acceptCoverage?: boolean
|
|
34
|
+
acceptCoverage?: boolean,
|
|
35
|
+
/**
|
|
36
|
+
* Called once with how the gate ended, on every path. `cli.ts` passes the `gate_completed`
|
|
37
|
+
* sender: telemetry already reaches this directory, so this directory does not reach back.
|
|
38
|
+
*/
|
|
39
|
+
reportGateCompleted?: (gate: GateCompleted) => void): Promise<void>;
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* opens first. These five functions form one cohesive group: they all read the flow store and report on
|
|
6
6
|
* verification state, and none of them touch daemon lifecycle, which is what the rest of cli.ts does.
|
|
7
7
|
*/
|
|
8
|
-
import { GateExit, MSG_GATE_NO_FLOWS, msgGateRegression, msgGateRefused, gateExitWith, } from './answers/gate-exit.js';
|
|
8
|
+
import { GATE_OUTCOME, GateExit, MSG_GATE_NO_FLOWS, msgGateRegression, msgGateRefused, gateExitWith, } from './answers/gate-exit.js';
|
|
9
9
|
import { knownRegression } from './known-regression.js';
|
|
10
10
|
import { gateHookMessage, GATE_SKIP_ENV } from './answers/gate-hook-message.js';
|
|
11
11
|
import { readProjectId } from './ports/resolve/cli-port.js';
|
|
@@ -25,6 +25,7 @@ import { detectDowngrades } from '../../judgement/outcome/assertion-integrity.js
|
|
|
25
25
|
import { computeCoverage, flowCoverageReport } from '../../language/flows/suite/coverage.js';
|
|
26
26
|
import { readFileSync, statSync } from 'node:fs';
|
|
27
27
|
import { log } from '../../log.js';
|
|
28
|
+
import { GateOutcome } from '@reticlehq/core/telemetry';
|
|
28
29
|
/** Load the {name, steps} of every saved flow for the active project. */
|
|
29
30
|
/** Explicit files plus, when --since is given, the git-changed files since that ref. */
|
|
30
31
|
/**
|
|
@@ -34,8 +35,9 @@ import { log } from '../../log.js';
|
|
|
34
35
|
*/
|
|
35
36
|
export async function resolveChangedFiles(files, since, cwd) {
|
|
36
37
|
// No ref means no question was asked of git, which is not a failure to answer one.
|
|
37
|
-
if (since === undefined)
|
|
38
|
+
if (since === undefined) {
|
|
38
39
|
return { files, failed: false };
|
|
40
|
+
}
|
|
39
41
|
const changed = await changedFilesSince(since, cwd);
|
|
40
42
|
return {
|
|
41
43
|
files: [...new Set([...files, ...changed.files])],
|
|
@@ -56,8 +58,9 @@ export async function loadNamedFlows(fs, reticleRoot, projectId) {
|
|
|
56
58
|
const flows = [];
|
|
57
59
|
for (const name of await store.list(projectId)) {
|
|
58
60
|
const loaded = await store.load(name, projectId);
|
|
59
|
-
if (loaded.ok)
|
|
61
|
+
if (loaded.ok) {
|
|
60
62
|
flows.push({ name: loaded.value.name, steps: loaded.value.steps });
|
|
63
|
+
}
|
|
61
64
|
}
|
|
62
65
|
return flows;
|
|
63
66
|
}
|
|
@@ -81,8 +84,9 @@ function atChangedPath(cwd, file, read) {
|
|
|
81
84
|
return undefined;
|
|
82
85
|
}
|
|
83
86
|
};
|
|
84
|
-
if (isAbsolute(file))
|
|
87
|
+
if (isAbsolute(file)) {
|
|
85
88
|
return attempt(file);
|
|
89
|
+
}
|
|
86
90
|
const parts = file.split('/');
|
|
87
91
|
const cwdParts = cwd.split(sep);
|
|
88
92
|
for (let i = 0; i < parts.length; i += 1) {
|
|
@@ -91,8 +95,9 @@ function atChangedPath(cwd, file, read) {
|
|
|
91
95
|
continue;
|
|
92
96
|
}
|
|
93
97
|
const found = attempt(join(cwd, ...parts.slice(i)));
|
|
94
|
-
if (found !== undefined)
|
|
98
|
+
if (found !== undefined) {
|
|
95
99
|
return found;
|
|
100
|
+
}
|
|
96
101
|
}
|
|
97
102
|
return undefined;
|
|
98
103
|
}
|
|
@@ -122,8 +127,9 @@ async function currentPassing(fs, root, flows, affected, changed) {
|
|
|
122
127
|
const own = 0 === sources.length ? changed : changed.filter((f) => sourceMatchesChange(f, sources));
|
|
123
128
|
const stale = affected.includes(name) &&
|
|
124
129
|
own.some((f) => !((changedFileModifiedAt(cwd, f) ?? Infinity) < at));
|
|
125
|
-
if (!stale)
|
|
130
|
+
if (!stale) {
|
|
126
131
|
passing.push(name);
|
|
132
|
+
}
|
|
127
133
|
}
|
|
128
134
|
return passing;
|
|
129
135
|
}
|
|
@@ -131,7 +137,12 @@ export async function handleGate(files, since,
|
|
|
131
137
|
/** Hook mode: prose for a human, and silence when there was simply nothing to check. */
|
|
132
138
|
hook = false,
|
|
133
139
|
/** Accept today's coverage levels as the best before judging, for a drop that was intended. */
|
|
134
|
-
acceptCoverage = false
|
|
140
|
+
acceptCoverage = false,
|
|
141
|
+
/**
|
|
142
|
+
* Called once with how the gate ended, on every path. `cli.ts` passes the `gate_completed`
|
|
143
|
+
* sender: telemetry already reaches this directory, so this directory does not reach back.
|
|
144
|
+
*/
|
|
145
|
+
reportGateCompleted = () => undefined) {
|
|
135
146
|
try {
|
|
136
147
|
const fs = createNodeFileSystem();
|
|
137
148
|
const reticleRoot = join(process.cwd(), ReticleDir.ROOT);
|
|
@@ -207,13 +218,17 @@ acceptCoverage = false) {
|
|
|
207
218
|
// once the exit code is: it said "pass": true over an exit of 1 or 3.
|
|
208
219
|
const known = await knownRegression(reticleRoot);
|
|
209
220
|
const regressed = known.broken;
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
221
|
+
let local = GateExit.FAIL;
|
|
222
|
+
if (pass) {
|
|
223
|
+
local = GateExit.PASS;
|
|
224
|
+
}
|
|
225
|
+
else if (result.pass && flowCoverage.outcome !== undefined) {
|
|
226
|
+
local = GateExit.NOTHING_TO_CHECK;
|
|
227
|
+
}
|
|
215
228
|
// Fails closed in CI, not in an agent's stop hook, where it would block over a key, not a change.
|
|
216
229
|
const code = gateExitWith(local, regressed?.length, !hook && known.refused !== undefined);
|
|
230
|
+
// What the gate judged, sent before a hook's skip can turn the exit code into a pass.
|
|
231
|
+
reportGateCompleted({ outcome: GATE_OUTCOME[code], flowsChecked: affected.length });
|
|
217
232
|
log('reticle_gate', {
|
|
218
233
|
pass: GateExit.PASS === code,
|
|
219
234
|
uncovered: result.uncovered,
|
|
@@ -237,14 +252,18 @@ acceptCoverage = false) {
|
|
|
237
252
|
// A regression the platform already knows fails the build here too, with `reticle runs
|
|
238
253
|
// regression`'s code, so the gate is the one command CI needs. Best-effort: unlinked or offline
|
|
239
254
|
// changes nothing.
|
|
240
|
-
if (regressed !== undefined && 0 < regressed.length)
|
|
255
|
+
if (regressed !== undefined && 0 < regressed.length) {
|
|
241
256
|
log('reticle_gate_regression', { broken: regressed });
|
|
242
|
-
|
|
257
|
+
}
|
|
258
|
+
if (known.refused !== undefined) {
|
|
243
259
|
process.stderr.write(`${msgGateRefused(known.refused)}\n`);
|
|
244
|
-
|
|
260
|
+
}
|
|
261
|
+
if (GateExit.NOTHING_TO_CHECK === code && !hook) {
|
|
245
262
|
process.stderr.write(`${MSG_GATE_NO_FLOWS}\n`);
|
|
246
|
-
|
|
263
|
+
}
|
|
264
|
+
if (GateExit.REGRESSION === code && !hook && regressed !== undefined) {
|
|
247
265
|
process.stderr.write(`${msgGateRegression(regressed)}\n`);
|
|
266
|
+
}
|
|
248
267
|
if (GateExit.PASS !== code) {
|
|
249
268
|
process.exitCode = code;
|
|
250
269
|
if (hook) {
|
|
@@ -267,13 +286,16 @@ acceptCoverage = false) {
|
|
|
267
286
|
unexecuted: result.unexecuted,
|
|
268
287
|
regressed: regressed?.length ?? 0,
|
|
269
288
|
});
|
|
270
|
-
if (message !== undefined)
|
|
289
|
+
if (message !== undefined) {
|
|
271
290
|
process.stderr.write(`${message}\n`);
|
|
291
|
+
}
|
|
272
292
|
}
|
|
273
293
|
}
|
|
274
294
|
}
|
|
275
295
|
catch (error) {
|
|
276
296
|
log('reticle_gate_failed', { error: error instanceof Error ? error.message : String(error) });
|
|
297
|
+
// No flow count: the gate broke before it judged, so a count would describe nothing.
|
|
298
|
+
reportGateCompleted({ outcome: GateOutcome.ERROR });
|
|
277
299
|
process.exitCode = GateExit.FAIL;
|
|
278
300
|
}
|
|
279
301
|
}
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
* written, or belongs to a daemon from another checkout. `kill` asks the PORT, which is the thing the
|
|
18
18
|
* user actually wants freed, and falls back to the pid file only where the lookup cannot run.
|
|
19
19
|
*/
|
|
20
|
+
import { PortPresence } from '../daemon/binding/port-presence.js';
|
|
20
21
|
import { type PortHolder } from './ports/port-holder.js';
|
|
21
22
|
/** What the plan says to do with the port. */
|
|
22
23
|
export declare const KillAction: {
|
|
@@ -62,6 +63,26 @@ export declare function planKill(input: {
|
|
|
62
63
|
* wrong token, or no answer: the caller then sends the signal as before.
|
|
63
64
|
*/
|
|
64
65
|
export declare function requestShutdown(port: number, token?: string | undefined): Promise<boolean>;
|
|
66
|
+
/**
|
|
67
|
+
* How long the second look waits for `/status`. The usual probe gives up after 1 second, and on a
|
|
68
|
+
* busy machine a healthy daemon took longer than that. A daemon that answers slowly is busy, not
|
|
69
|
+
* stuck, and other agents can use it.
|
|
70
|
+
*/
|
|
71
|
+
export declare const WEDGED_STATUS_TIMEOUT_MS = 10000;
|
|
72
|
+
/**
|
|
73
|
+
* Stop a Reticle daemon that holds `port` and does not answer. True when it stopped one (#1937).
|
|
74
|
+
*
|
|
75
|
+
* The MCP proxy used to move to another port when it found one. The app's SDK dials the old port,
|
|
76
|
+
* so the new daemon got no page. Stopping the stuck daemon keeps the port the app dials. The holder
|
|
77
|
+
* must have a Reticle daemon's command line, so a stranger and the proxy itself are never signalled.
|
|
78
|
+
*/
|
|
79
|
+
export declare function stopWedgedDaemon(port: number, deps?: {
|
|
80
|
+
/** `statusTimeoutMs`: how long to wait for `/status`. Absent: the usual short probe. */
|
|
81
|
+
presence: (statusTimeoutMs?: number) => Promise<PortPresence>;
|
|
82
|
+
holder: () => PortHolder | null;
|
|
83
|
+
kill: () => Promise<boolean>;
|
|
84
|
+
wait: (ms: number) => Promise<void>;
|
|
85
|
+
}): Promise<boolean>;
|
|
65
86
|
/** `reticle kill` — free the port. Resolves to whether the port is now free. */
|
|
66
87
|
export declare function runKill(port: number, force: boolean): Promise<boolean>;
|
|
67
88
|
export {};
|
|
@@ -22,7 +22,8 @@ import { isAlive, readPid, removePid } from '../daemon/daemon.js';
|
|
|
22
22
|
import { probeDaemon } from '../../surface/mcp/mcp-proxy.js';
|
|
23
23
|
import { log } from '../../log.js';
|
|
24
24
|
import { fetchStatus } from '../daemon/binding/daemon-status-probe.js';
|
|
25
|
-
import { captureLookup
|
|
25
|
+
import { captureLookup } from './ports/port-holder.js';
|
|
26
|
+
import { identifyPortHolder } from '../daemon/port-state.js';
|
|
26
27
|
import * as http from 'node:http';
|
|
27
28
|
import { LOOPBACK_HOST, SHUTDOWN_PATH } from '@reticlehq/core';
|
|
28
29
|
import { storedPairingToken } from '../../portal/bridge/pairing-token.js';
|
|
@@ -50,7 +51,8 @@ export function planKill(input) {
|
|
|
50
51
|
return { action: KillAction.NOTHING, forced: false, identifiedListener: false };
|
|
51
52
|
return { action: KillAction.KILL, pid: recordedPid, forced: false, identifiedListener: false };
|
|
52
53
|
}
|
|
53
|
-
|
|
54
|
+
// `reticleDaemon`: its command line says so, though it answers nothing and no pid file names it.
|
|
55
|
+
const ours = answersStatus || listener.pid === recordedPid || true === listener.reticleDaemon;
|
|
54
56
|
if (ours || force) {
|
|
55
57
|
return {
|
|
56
58
|
action: KillAction.KILL,
|
|
@@ -156,12 +158,53 @@ async function terminate(pid, asked = false) {
|
|
|
156
158
|
*/
|
|
157
159
|
const PROXY_SPARED_NOTE = 'only the listener was signalled. Any `reticle mcp` proxy on this port holds a client connection, ' +
|
|
158
160
|
'not the port, and was left alone — it goes dormant and starts a fresh daemon on the next tool call.';
|
|
161
|
+
/**
|
|
162
|
+
* How long a daemon that holds the port gets to answer before it counts as stuck. A daemon that has
|
|
163
|
+
* just bound answers in far less, so a second look after this does not stop one that is starting.
|
|
164
|
+
*/
|
|
165
|
+
const WEDGED_GRACE_MS = 2_000;
|
|
166
|
+
/**
|
|
167
|
+
* How long the second look waits for `/status`. The usual probe gives up after 1 second, and on a
|
|
168
|
+
* busy machine a healthy daemon took longer than that. A daemon that answers slowly is busy, not
|
|
169
|
+
* stuck, and other agents can use it.
|
|
170
|
+
*/
|
|
171
|
+
export const WEDGED_STATUS_TIMEOUT_MS = 10_000;
|
|
172
|
+
/**
|
|
173
|
+
* Stop a Reticle daemon that holds `port` and does not answer. True when it stopped one (#1937).
|
|
174
|
+
*
|
|
175
|
+
* The MCP proxy used to move to another port when it found one. The app's SDK dials the old port,
|
|
176
|
+
* so the new daemon got no page. Stopping the stuck daemon keeps the port the app dials. The holder
|
|
177
|
+
* must have a Reticle daemon's command line, so a stranger and the proxy itself are never signalled.
|
|
178
|
+
*/
|
|
179
|
+
export async function stopWedgedDaemon(port, deps = {
|
|
180
|
+
presence: (statusTimeoutMs) => probePresence(port, {
|
|
181
|
+
tcpOpen: probeDaemon,
|
|
182
|
+
status: (p) => fetchStatus(p, LOOPBACK_HOST, false, statusTimeoutMs),
|
|
183
|
+
}),
|
|
184
|
+
holder: () => identifyPortHolder(port, captureLookup),
|
|
185
|
+
kill: () => runKill(port, false),
|
|
186
|
+
wait: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
|
|
187
|
+
}) {
|
|
188
|
+
if (PortPresence.FOREIGN !== (await deps.presence())) {
|
|
189
|
+
return false;
|
|
190
|
+
}
|
|
191
|
+
const holder = deps.holder();
|
|
192
|
+
if (true !== holder?.reticleDaemon) {
|
|
193
|
+
return false;
|
|
194
|
+
}
|
|
195
|
+
await deps.wait(WEDGED_GRACE_MS);
|
|
196
|
+
if (PortPresence.FOREIGN !== (await deps.presence(WEDGED_STATUS_TIMEOUT_MS))) {
|
|
197
|
+
return false;
|
|
198
|
+
}
|
|
199
|
+
log('reticle_wedged_daemon_stopping', { port, pid: holder.pid });
|
|
200
|
+
return deps.kill();
|
|
201
|
+
}
|
|
159
202
|
/** `reticle kill` — free the port. Resolves to whether the port is now free. */
|
|
160
203
|
export async function runKill(port, force) {
|
|
161
204
|
const presence = await probePresence(port, { tcpOpen: probeDaemon, status: fetchStatus });
|
|
162
205
|
const recorded = readPid(port);
|
|
163
206
|
const plan = planKill({
|
|
164
|
-
listener:
|
|
207
|
+
listener: identifyPortHolder(port, captureLookup),
|
|
165
208
|
recordedPid: null !== recorded && isAlive(recorded) ? recorded : null,
|
|
166
209
|
answersStatus: presence === PortPresence.DAEMON,
|
|
167
210
|
force,
|
|
@@ -4,8 +4,9 @@
|
|
|
4
4
|
* and re-exports this surface.
|
|
5
5
|
*/
|
|
6
6
|
import { type ParsedFeedback } from './cli-parse-feedback.js';
|
|
7
|
+
import { DAEMON_INNER_COMMAND } from '../daemon/daemon-argv.js';
|
|
7
8
|
import { HEADED_FLAG, PORT_FLAG } from './cli-parse-grammar.js';
|
|
8
|
-
export { HEADED_FLAG, PORT_FLAG };
|
|
9
|
+
export { DAEMON_INNER_COMMAND, HEADED_FLAG, PORT_FLAG };
|
|
9
10
|
export { DRIVE_FLAG, HEADLESS_FLAG, HTTP_FLAG, HTTP_PORT_FLAG, HTTP_TOKEN_FLAG, } from './cli-parse-serve.js';
|
|
10
11
|
export { AGENT_FLAG, BUG_FLAG, CONTACT_FLAG, FEEDBACK_KINDS, KIND_FLAG, RATING_FLAG, } from './cli-parse-feedback.js';
|
|
11
12
|
import { CLI_USAGE } from './cli-usage.js';
|
|
@@ -19,7 +20,6 @@ export declare const TelemetryAction: {
|
|
|
19
20
|
readonly DISABLE: "disable";
|
|
20
21
|
};
|
|
21
22
|
export type TelemetryAction = (typeof TelemetryAction)[keyof typeof TelemetryAction];
|
|
22
|
-
export declare const DAEMON_INNER_COMMAND = "_daemon";
|
|
23
23
|
/**
|
|
24
24
|
* Every subcommand a person can type, as one closed vocabulary. Telemetry reports which one ran, and
|
|
25
25
|
* this is what keeps that property low-cardinality and non-identifying: an argument we do not
|
|
@@ -110,8 +110,11 @@ export type CliResult = {
|
|
|
110
110
|
} | {
|
|
111
111
|
kind: 'doctor';
|
|
112
112
|
port: number;
|
|
113
|
+
fix: boolean;
|
|
113
114
|
} | {
|
|
114
115
|
kind: 'setup-mcp';
|
|
116
|
+
} | {
|
|
117
|
+
kind: 'setup-wait-agents';
|
|
115
118
|
} | {
|
|
116
119
|
kind: 'setup-install';
|
|
117
120
|
runtimeSecs: number;
|
|
@@ -6,10 +6,11 @@
|
|
|
6
6
|
import { parseFeedbackArgs } from './cli-parse-feedback.js';
|
|
7
7
|
import { HEADLESS_FLAG, parseServeFlags } from './cli-parse-serve.js';
|
|
8
8
|
import { parseVerifySuffix } from './cli-parse-verify.js';
|
|
9
|
+
import { DAEMON_INNER_COMMAND } from '../daemon/daemon-argv.js';
|
|
9
10
|
import { HEADED_FLAG, PORT_FLAG, VERIFY_COMMAND, missingOperand, missingValue, notANumber, retiredFlag, unknownArgument, CLOUD_COMMANDS, REMOVED_COMMANDS, removedNote, } from './cli-parse-grammar.js';
|
|
10
11
|
// Re-exported because the daemon's own argv builder reaches for these through this module, which is
|
|
11
12
|
// where the grammar has always lived as far as every caller is concerned.
|
|
12
|
-
export { HEADED_FLAG, PORT_FLAG };
|
|
13
|
+
export { DAEMON_INNER_COMMAND, HEADED_FLAG, PORT_FLAG };
|
|
13
14
|
export { DRIVE_FLAG, HEADLESS_FLAG, HTTP_FLAG, HTTP_PORT_FLAG, HTTP_TOKEN_FLAG, } from './cli-parse-serve.js';
|
|
14
15
|
import { FORCE_FLAG } from './cli-kill.js';
|
|
15
16
|
// Re-exported so every existing importer of these flags is unaffected by the file split.
|
|
@@ -44,6 +45,7 @@ const TELEMETRY_COMMAND = 'telemetry';
|
|
|
44
45
|
const FEEDBACK_COMMAND = 'feedback';
|
|
45
46
|
const DOCTOR_COMMAND = 'doctor';
|
|
46
47
|
const SETUP_COMMAND = 'setup';
|
|
48
|
+
const SETUP_WAIT_AGENTS = 'wait-agents';
|
|
47
49
|
/** `reticle hook`: the agent prompt hook. `cli.ts` runs it before this parser, and before telemetry. */
|
|
48
50
|
export const HOOK_COMMAND = 'hook';
|
|
49
51
|
/** The `reticle telemetry` sub-actions. Bare `reticle telemetry` means `status`. */
|
|
@@ -52,7 +54,6 @@ export const TelemetryAction = {
|
|
|
52
54
|
ENABLE: 'enable',
|
|
53
55
|
DISABLE: 'disable',
|
|
54
56
|
};
|
|
55
|
-
export const DAEMON_INNER_COMMAND = '_daemon';
|
|
56
57
|
/**
|
|
57
58
|
* Every subcommand a person can type, as one closed vocabulary. Telemetry reports which one ran, and
|
|
58
59
|
* this is what keeps that property low-cardinality and non-identifying: an argument we do not
|
|
@@ -138,6 +139,7 @@ export function knownCommand(arg) {
|
|
|
138
139
|
return KNOWN_COMMANDS.has(arg) ? arg : (COMMAND_FLAG_ALIASES.get(arg) ?? UNKNOWN_COMMAND);
|
|
139
140
|
}
|
|
140
141
|
const QUIET_FLAG = '--quiet';
|
|
142
|
+
const FIX_FLAG = '--fix';
|
|
141
143
|
const DRY_RUN_FLAG = '--dry-run';
|
|
142
144
|
const YES_FLAG = '--yes';
|
|
143
145
|
const NO_MCP_FLAG = '--no-mcp';
|
|
@@ -589,7 +591,7 @@ export function parseCliArgs(argv, defaultPort, defaultHeadless = false) {
|
|
|
589
591
|
}
|
|
590
592
|
case DOCTOR_COMMAND: {
|
|
591
593
|
const port = parsePortFlag(rest, defaultPort);
|
|
592
|
-
return { kind: 'doctor', port };
|
|
594
|
+
return { kind: 'doctor', port, fix: rest.includes(FIX_FLAG) };
|
|
593
595
|
}
|
|
594
596
|
/*
|
|
595
597
|
* `setup mcp` — register the MCP server with the coding agents on this machine.
|
|
@@ -621,8 +623,11 @@ export function parseCliArgs(argv, defaultPort, defaultHeadless = false) {
|
|
|
621
623
|
mcp: !rest.includes('--no-mcp'),
|
|
622
624
|
};
|
|
623
625
|
}
|
|
624
|
-
//
|
|
625
|
-
|
|
626
|
+
// `setup wait-agents` — the installer's last step: wait for the agents to connect.
|
|
627
|
+
if (SETUP_WAIT_AGENTS === what) {
|
|
628
|
+
return { kind: 'setup-wait-agents' };
|
|
629
|
+
}
|
|
630
|
+
// A bare `reticle setup` falls to help rather than guessing.
|
|
626
631
|
if (what !== 'mcp') {
|
|
627
632
|
return { kind: 'help', topic: SETUP_COMMAND };
|
|
628
633
|
}
|
|
@@ -92,7 +92,7 @@ const COMMANDS = [
|
|
|
92
92
|
{
|
|
93
93
|
name: 'verify',
|
|
94
94
|
synopsis: 'verify <url> [--explore] [--expect <json>]',
|
|
95
|
-
summary: 'Check
|
|
95
|
+
summary: 'Check saved flows, or one claim with --expect, against the running app; exit 0 = pass',
|
|
96
96
|
group: HelpGroup.CHECK,
|
|
97
97
|
detail: VERIFY_DETAIL,
|
|
98
98
|
},
|
|
@@ -110,8 +110,9 @@ const COMMANDS = [
|
|
|
110
110
|
},
|
|
111
111
|
{
|
|
112
112
|
name: 'doctor',
|
|
113
|
-
synopsis: 'doctor [--port N]',
|
|
113
|
+
synopsis: 'doctor [--fix] [--port N]',
|
|
114
114
|
summary: 'Find what is wrong with the setup, and what is missing from coverage',
|
|
115
|
+
detail: '--fix repairs what Reticle can repair safely, and names the step for the rest.',
|
|
115
116
|
group: HelpGroup.MACHINE,
|
|
116
117
|
},
|
|
117
118
|
{
|
|
@@ -230,5 +230,10 @@ export declare function handleVerify(parsed: {
|
|
|
230
230
|
resultsJson?: string;
|
|
231
231
|
/** Acknowledge the replay guard's destructive steps ("Place order") for this run only. */
|
|
232
232
|
confirmDangerous?: boolean;
|
|
233
|
-
}
|
|
233
|
+
},
|
|
234
|
+
/**
|
|
235
|
+
* Called once, after the last verdict line and before the exit. `cli.ts` passes the CI line:
|
|
236
|
+
* `telemetry` already reaches this directory, so this directory does not reach back.
|
|
237
|
+
*/
|
|
238
|
+
beforeExit?: () => void): void;
|
|
234
239
|
export {};
|