@reticlehq/server 3.8.0 → 3.8.2
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 +2 -2
- package/dist/command/cli/answers/gate-hook-message.d.ts +2 -0
- package/dist/command/cli/answers/gate-hook-message.js +5 -1
- package/dist/command/cli/auth/cloud-session.d.ts +1 -19
- package/dist/command/cli/auth/device-flow.d.ts +3 -37
- package/dist/command/cli/cli-doctor.js +30 -23
- package/dist/command/cli/cli-flow-commands.js +7 -1
- package/dist/command/cli/cli-parse-serve.d.ts +24 -0
- package/dist/command/cli/cli-parse-serve.js +124 -0
- package/dist/command/cli/cli-parse.d.ts +1 -10
- package/dist/command/cli/cli-parse.js +62 -121
- package/dist/command/cli/cli-usage.js +5 -2
- package/dist/command/cli/cloud-link.d.ts +1 -20
- package/dist/command/cli/doctor/doctor-agent-link.d.ts +23 -0
- package/dist/command/cli/doctor/doctor-agent-link.js +28 -0
- package/dist/command/cli/ports/port-holder.js +6 -1
- package/dist/command/cli/project-name.d.ts +1 -7
- package/dist/command/cli/project-name.js +1 -0
- package/dist/command/daemon/daemon.d.ts +6 -0
- package/dist/command/daemon/daemon.js +52 -2
- package/dist/command/daemon/win32-escape.d.ts +32 -0
- package/dist/command/daemon/win32-escape.js +119 -0
- package/dist/command/setup/bringup/dev-server-files.d.ts +1 -9
- package/dist/command/setup/bringup/dev-server-wait.js +5 -1
- package/dist/command/version/version-skew.d.ts +2 -0
- package/dist/command/version/version-skew.js +7 -3
- package/dist/features/crawl/crawl-tools.js +2 -2
- package/dist/features/exhaust/ledger.d.ts +15 -96
- package/dist/features/exhaust/ledger.js +3 -3
- package/dist/features/license/license.d.ts +2 -14
- package/dist/hooks/hook-commands.js +19 -9
- package/dist/judgement/capsule/capsule-store.d.ts +2 -39
- package/dist/language/flows/annotate-tools.js +1 -1
- package/dist/language/flows/fields/flow-secret-field.js +1 -1
- package/dist/language/flows/flow-expect-grammar.js +14 -6
- package/dist/language/flows/flow-lease.d.ts +27 -0
- package/dist/language/flows/flow-lease.js +59 -0
- package/dist/language/flows/flow-replay-run.d.ts +1 -87
- package/dist/language/flows/flow-replay-run.js +88 -283
- package/dist/language/flows/flow-replayable.d.ts +1 -1
- package/dist/language/flows/flow-start-path.d.ts +108 -0
- package/dist/language/flows/flow-start-path.js +313 -0
- package/dist/language/flows/flow-tools.d.ts +2 -23
- package/dist/language/flows/flow-tools.js +34 -66
- package/dist/language/flows/recording/flake.d.ts +3 -27
- package/dist/language/flows/recording/flake.js +1 -1
- package/dist/language/flows/stores/assertion-tiers-store.js +1 -1
- package/dist/machine/git-tracks.js +1 -0
- package/dist/memory/cloud/cloud-sync.d.ts +2 -24
- package/dist/memory/intent/intent-shard.d.ts +41 -266
- package/dist/memory/journal/envelope-store.js +1 -1
- package/dist/memory/journal/envelope.d.ts +6 -154
- package/dist/memory/journal/on-disk/ambient-file.d.ts +1 -7
- package/dist/memory/journal/on-disk/ambient-file.js +1 -1
- package/dist/memory/journal/self-observation.js +3 -1
- package/dist/memory/journal/session-journal.js +39 -20
- package/dist/portal/input/drive-error.d.ts +6 -0
- package/dist/portal/input/drive-error.js +9 -0
- package/dist/portal/input/real-input-press.d.ts +34 -0
- package/dist/portal/input/real-input-press.js +150 -0
- package/dist/portal/input/real-input.d.ts +6 -23
- package/dist/portal/input/real-input.js +95 -192
- package/dist/portal/input/viewport-tools.d.ts +19 -15
- package/dist/portal/pool/browser-pool.d.ts +5 -55
- package/dist/portal/pool/browser-pool.js +121 -100
- package/dist/portal/pool/pool-lease.d.ts +128 -0
- package/dist/portal/pool/pool-lease.js +49 -0
- package/dist/portal/production/production-activation.d.ts +5 -27
- package/dist/portal/session/control-envelope.d.ts +1 -1
- package/dist/portal/session/zero-install-first-call.d.ts +3 -0
- package/dist/portal/session/zero-install-first-call.js +33 -12
- package/dist/surface/mcp/mcp.d.ts +3 -3
- package/dist/surface/mcp/mcp.js +4 -6
- package/dist/surface/tools/act/act-danger.js +32 -17
- package/dist/surface/tools/act/durable.js +1 -1
- package/dist/surface/tools/act-sequence-tool.js +3 -3
- package/dist/surface/tools/act-tools.js +15 -16
- package/dist/surface/tools/agent/compact-envelope.d.ts +2 -2
- package/dist/surface/tools/coverage-tools.js +1 -1
- package/dist/surface/tools/dynamic-tools.d.ts +8 -0
- package/dist/surface/tools/dynamic-tools.js +36 -16
- package/dist/surface/tools/empty-session-list.d.ts +5 -29
- package/dist/surface/tools/feedback-tools.js +1 -1
- package/dist/surface/tools/gap-novelty.d.ts +1 -1
- package/dist/surface/tools/gaps/capability-absences.js +4 -3
- package/dist/surface/tools/harness-gate.d.ts +1 -1
- package/dist/surface/tools/harness-toolset.js +3 -4
- package/dist/surface/tools/lease-permissions.d.ts +1 -1
- package/dist/surface/tools/lease-readiness.d.ts +23 -1
- package/dist/surface/tools/lease-readiness.js +75 -9
- package/dist/surface/tools/lease-tools.d.ts +1 -52
- package/dist/surface/tools/lease-tools.js +79 -142
- package/dist/surface/tools/observe-tools.js +3 -1
- package/dist/surface/tools/read-tools.js +5 -5
- package/dist/surface/tools/real-input-attempt.js +70 -31
- package/dist/surface/tools/session-ports.d.ts +57 -0
- package/dist/surface/tools/session-ports.js +102 -0
- package/dist/surface/tools/tool-kit.d.ts +5 -3
- package/dist/surface/tools/tools.js +1 -1
- package/dist/telemetry/daemon-telemetry.d.ts +3 -1
- package/dist/telemetry/daemon-telemetry.js +9 -2
- package/dist/telemetry/telemetry.d.ts +3 -1
- package/dist/telemetry/telemetry.js +2 -0
- package/docs/agent-cheatsheet.md +12 -0
- package/docs/cli/gate.mdx +1 -1
- package/docs/cli/verify.mdx +8 -1
- package/docs/first-drive.md +7 -0
- package/docs/multi-agent-testing.md +2 -0
- package/docs/packages/react.mdx +14 -2
- package/docs/packages/vite-plugin.mdx +1 -0
- package/docs/predicates.mdx +6 -0
- package/docs/skill.md +3 -3
- package/docs/troubleshooting.mdx +10 -0
- package/docs/usage.md +1 -1
- package/package.json +8 -9
package/SKILL.md
CHANGED
|
@@ -138,7 +138,7 @@ Stop at the first row that fits. Replaying a saved journey costs a few hundred t
|
|
|
138
138
|
| "Did my edit break anything?" | `reticle_verify { action: "change", files: ["src/App.tsx"] }` |
|
|
139
139
|
| "Does every saved journey still work?" | `reticle_verify { action: "flows" }` |
|
|
140
140
|
| "Does this new behaviour work?" | `reticle_act_and_wait` on the step that ENDS the journey |
|
|
141
|
-
| No MCP
|
|
141
|
+
| No MCP tools (a cloud agent, a worker) | `npx @reticlehq/server serve`, then `npx @reticlehq/server verify <url> --expect '<predicate JSON>'`. `open <url> --navigate` moves a connected tab to `<url>` |
|
|
142
142
|
|
|
143
143
|
`change` answers `unknown` when no saved flow covers your files: nothing ran, so drive it yourself and never read it as a pass. A locator that was renamed rather than broken: `reticle_verify { action: "heal", flowName }` proposes the rebind, and `apply: true` writes it after it proves the saved consequence again.
|
|
144
144
|
|
|
@@ -161,7 +161,7 @@ A journey in one call: `reticle_act { steps: [{ target, action, args?, expect? }
|
|
|
161
161
|
| ref no longer resolves | the DOM re-rendered: target by `testid` or `role`+`name` |
|
|
162
162
|
| predicate did not parse | it names the field: fix the shape against the table above |
|
|
163
163
|
| destructive action blocked | `args.confirmDangerous: true`, only if the user wants that press |
|
|
164
|
-
| version skew | `npx @reticlehq/server
|
|
164
|
+
| version skew | update the CLI and the app SDK (`npx @reticlehq/server update` in the app's folder), then restart the MCP host |
|
|
165
165
|
| port held by a non-Reticle process | stop that process, or run Reticle on another `--port` |
|
|
166
166
|
| tab served from another worktree | start the dev server in your own worktree; keep the port |
|
|
167
167
|
|
|
@@ -23,6 +23,8 @@ interface GateHookInput {
|
|
|
23
23
|
deleted: readonly string[];
|
|
24
24
|
/** Changed interactive components no saved flow has ever driven. */
|
|
25
25
|
unflowed?: readonly string[];
|
|
26
|
+
/** Saved flows exist but none records a source location, so no flow can be matched to a file. */
|
|
27
|
+
flowsWithoutSources?: boolean;
|
|
26
28
|
/** Coverage levels below their best, already worded (`branched 100% -> 50%`). */
|
|
27
29
|
coverageRegressed?: readonly string[];
|
|
28
30
|
/** Changed files the browser loaded and never ran a function of. */
|
|
@@ -14,6 +14,8 @@ import { GateExit } from './gate-exit.js';
|
|
|
14
14
|
/** Named so a reader of the hook output knows the flag is real and not a suggestion. */
|
|
15
15
|
export const GATE_SKIP_ENV = 'RETICLE_SKIP_GATE';
|
|
16
16
|
const MAX_NAMED = 5;
|
|
17
|
+
/** Where source stamps come from; the file extension alone does not say which plugin applies. */
|
|
18
|
+
const STAMP_ADVICE = 'source stamps come from @reticlehq/vite-plugin (JSX and Svelte), @reticlehq/next or @reticlehq/babel-plugin. Vue and Angular templates have no source stamps yet';
|
|
17
19
|
/** A short list, then a count — a hook that prints forty names is a hook people turn off. */
|
|
18
20
|
function name(list) {
|
|
19
21
|
const shown = list.slice(0, MAX_NAMED).join(', ');
|
|
@@ -40,7 +42,9 @@ export function gateHookMessage(exit, input) {
|
|
|
40
42
|
if (input.deleted.length > 0)
|
|
41
43
|
lines.push(` coverage deleted while its files changed: ${name(input.deleted)}`);
|
|
42
44
|
if (input.unflowed !== undefined && input.unflowed.length > 0)
|
|
43
|
-
lines.push(
|
|
45
|
+
lines.push(true === input.flowsWithoutSources
|
|
46
|
+
? ` changed, but the saved flows carry no source locations, so the gate cannot match a flow to them: ${name(input.unflowed)} — ${STAMP_ADVICE}`
|
|
47
|
+
: ` changed, and no saved flow drives it: ${name(input.unflowed)} — drive it with reticle_act_and_wait and a declared \`until\``);
|
|
44
48
|
if (input.coverageRegressed !== undefined && input.coverageRegressed.length > 0)
|
|
45
49
|
lines.push(` coverage fell: ${name(input.coverageRegressed)} — something new is uncovered; reticle_verify { action: "crawl", exhaustive: true } names it. If the drop was intended, \`reticle gate --accept-coverage\` records it`);
|
|
46
50
|
if (input.unexecuted !== undefined && input.unexecuted.length > 0)
|
|
@@ -14,27 +14,9 @@ declare const SessionSchema: z.ZodObject<{
|
|
|
14
14
|
url: z.ZodString;
|
|
15
15
|
token: z.ZodString;
|
|
16
16
|
orgName: z.ZodString;
|
|
17
|
-
/**
|
|
18
|
-
* WHICH tenant this session signed into. Optional because every session file written before
|
|
19
|
-
* org-scoped credentials lacks it; the link path treats "unknown" as "cannot prove a stored key
|
|
20
|
-
* is ours" and mints, which is the safe direction.
|
|
21
|
-
*/
|
|
22
17
|
orgId: z.ZodOptional<z.ZodString>;
|
|
23
|
-
/** Who signed in. Absent in sessions written before it was recorded. */
|
|
24
18
|
email: z.ZodOptional<z.ZodString>;
|
|
25
|
-
},
|
|
26
|
-
token: string;
|
|
27
|
-
url: string;
|
|
28
|
-
orgName: string;
|
|
29
|
-
orgId?: string | undefined;
|
|
30
|
-
email?: string | undefined;
|
|
31
|
-
}, {
|
|
32
|
-
token: string;
|
|
33
|
-
url: string;
|
|
34
|
-
orgName: string;
|
|
35
|
-
orgId?: string | undefined;
|
|
36
|
-
email?: string | undefined;
|
|
37
|
-
}>;
|
|
19
|
+
}, z.core.$strip>;
|
|
38
20
|
export type Session = z.infer<typeof SessionSchema>;
|
|
39
21
|
/** Trailing slashes are not identity: `https://x/` and `https://x` are one host. */
|
|
40
22
|
export declare const normalizeUrl: (url: string) => string;
|
|
@@ -18,21 +18,7 @@ export declare const DeviceStartSchema: z.ZodObject<{
|
|
|
18
18
|
verificationUriComplete: z.ZodString;
|
|
19
19
|
interval: z.ZodNumber;
|
|
20
20
|
expiresAt: z.ZodNumber;
|
|
21
|
-
},
|
|
22
|
-
expiresAt: number;
|
|
23
|
-
deviceCode: string;
|
|
24
|
-
userCode: string;
|
|
25
|
-
verificationUri: string;
|
|
26
|
-
verificationUriComplete: string;
|
|
27
|
-
interval: number;
|
|
28
|
-
}, {
|
|
29
|
-
expiresAt: number;
|
|
30
|
-
deviceCode: string;
|
|
31
|
-
userCode: string;
|
|
32
|
-
verificationUri: string;
|
|
33
|
-
verificationUriComplete: string;
|
|
34
|
-
interval: number;
|
|
35
|
-
}>;
|
|
21
|
+
}, z.core.$strip>;
|
|
36
22
|
/**
|
|
37
23
|
* What `/v1/auth/device/token` answers while polling. Everything past `status` is optional because
|
|
38
24
|
* only an APPROVED poll carries a token, and `org.id` is absent on a cloud older than org-scoped
|
|
@@ -44,28 +30,8 @@ export declare const DevicePollSchema: z.ZodObject<{
|
|
|
44
30
|
org: z.ZodOptional<z.ZodObject<{
|
|
45
31
|
id: z.ZodOptional<z.ZodString>;
|
|
46
32
|
name: z.ZodString;
|
|
47
|
-
},
|
|
48
|
-
|
|
49
|
-
id?: string | undefined;
|
|
50
|
-
}, {
|
|
51
|
-
name: string;
|
|
52
|
-
id?: string | undefined;
|
|
53
|
-
}>>;
|
|
54
|
-
}, "strip", z.ZodTypeAny, {
|
|
55
|
-
status: string;
|
|
56
|
-
token?: string | undefined;
|
|
57
|
-
org?: {
|
|
58
|
-
name: string;
|
|
59
|
-
id?: string | undefined;
|
|
60
|
-
} | undefined;
|
|
61
|
-
}, {
|
|
62
|
-
status: string;
|
|
63
|
-
token?: string | undefined;
|
|
64
|
-
org?: {
|
|
65
|
-
name: string;
|
|
66
|
-
id?: string | undefined;
|
|
67
|
-
} | undefined;
|
|
68
|
-
}>;
|
|
33
|
+
}, z.core.$strip>>;
|
|
34
|
+
}, z.core.$strip>;
|
|
69
35
|
/**
|
|
70
36
|
* Where a sign-in started, carried to the approval page as `from` so the platform can attribute the
|
|
71
37
|
* signup. The platform's `/device` reads the same closed set (plus `app`, its own).
|
|
@@ -31,6 +31,7 @@ import { createNodeFileSystem } from '../../memory/project/fs/fs-port.js';
|
|
|
31
31
|
import { describeUnsynced } from '../../memory/cloud/unsynced-roots.js';
|
|
32
32
|
import { machineUnsyncedRoots } from '../../memory/project/sync-status.js';
|
|
33
33
|
import { attachState, describeAttachState } from '../../surface/mcp/attach-memory.js';
|
|
34
|
+
import { agentLinkText } from './doctor/doctor-agent-link.js';
|
|
34
35
|
import { findOccupiedSiblings, siblingListenerNote } from './ports/sibling-ports.js';
|
|
35
36
|
import { daemonsServingProjectElsewhere, resolveDaemonForProject, splitBrainNote, wrongDaemonNote, } from '../daemon/daemon-resolve.js';
|
|
36
37
|
import { isAlive } from '../daemon/daemon.js';
|
|
@@ -43,8 +44,9 @@ import { hasConnectedDocument, observeWebDocument, } from '../../portal/session/
|
|
|
43
44
|
*/
|
|
44
45
|
/** Narrow the `/status` payload to the two fields the daemon line reads. */
|
|
45
46
|
function asIdentity(payload) {
|
|
46
|
-
if (typeof payload !== 'object' || null === payload)
|
|
47
|
+
if (typeof payload !== 'object' || null === payload) {
|
|
47
48
|
return {};
|
|
49
|
+
}
|
|
48
50
|
const record = payload;
|
|
49
51
|
const pick = (key) => {
|
|
50
52
|
const value = record[key];
|
|
@@ -99,8 +101,9 @@ export async function handleDoctor(port) {
|
|
|
99
101
|
contract: CONTRACT_FINGERPRINT,
|
|
100
102
|
});
|
|
101
103
|
line(built.text);
|
|
102
|
-
if (built.skew !== undefined)
|
|
104
|
+
if (built.skew !== undefined) {
|
|
103
105
|
line(doctorRow(DoctorRow.VERSION, `✗ ${built.skew}`));
|
|
106
|
+
}
|
|
104
107
|
// The same payload already carries whether anything has CONNECTED, and doctor was reading
|
|
105
108
|
// `version` off it and dropping the rest. Without this the command is silent on the one step the
|
|
106
109
|
// funnel stalls on: wired correctly, daemon up, and no page has ever dialled in.
|
|
@@ -128,18 +131,22 @@ export async function handleDoctor(port) {
|
|
|
128
131
|
else {
|
|
129
132
|
line(doctorRow(DoctorRow.DAEMON, `✗ not running on :${port} — your agent runs \`reticle mcp\` (or \`reticle serve\`)`));
|
|
130
133
|
}
|
|
131
|
-
if (sessions !== undefined)
|
|
134
|
+
if (sessions !== undefined) {
|
|
132
135
|
line(sessions.text);
|
|
133
|
-
|
|
136
|
+
}
|
|
137
|
+
for (const row of coverageLines(daemonStatus)) {
|
|
134
138
|
line(row);
|
|
139
|
+
}
|
|
135
140
|
// Runs nothing will send, named by folder with the one command that sends them.
|
|
136
|
-
for (const entry of await machineUnsyncedRoots(createNodeFileSystem(), homedir(), process.env))
|
|
141
|
+
for (const entry of await machineUnsyncedRoots(createNodeFileSystem(), homedir(), process.env)) {
|
|
137
142
|
line(doctorRow(DoctorRow.UNSYNCED, `✗ ${describeUnsynced(entry, process.cwd())}`));
|
|
143
|
+
}
|
|
138
144
|
line(doctorRow(DoctorRow.BRIDGE_PORT, `${port} (your app must dial THIS port, not your dev-server port)`));
|
|
139
145
|
const projectPort = readProjectPort(process.cwd());
|
|
140
146
|
const mismatch = diagnosePortMismatch(port, projectPort);
|
|
141
|
-
if (mismatch !== undefined)
|
|
147
|
+
if (mismatch !== undefined) {
|
|
142
148
|
line(doctorRow(DoctorRow.PORT_CHECK, `✗ ${mismatch}`));
|
|
149
|
+
}
|
|
143
150
|
const projectId = readProjectId(process.cwd());
|
|
144
151
|
// The hop between the agent and this daemon, stated as its own row rather than inferred from the
|
|
145
152
|
// ones above it. `sessions` says the BROWSER reached the daemon; nothing said whether the AGENT
|
|
@@ -151,26 +158,23 @@ export async function handleDoctor(port) {
|
|
|
151
158
|
// attached here" and sends the reader to re-register a server that is registered and running —
|
|
152
159
|
// the confident wrong answer for a link that is not missing but misaimed.
|
|
153
160
|
const split = splitBrainLine(port, projectId);
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
// answers "did this ever work" while looking like "does this work" is the wrong
|
|
164
|
-
// shape here even though every word of it is true.
|
|
165
|
-
'✓ an MCP client attached here and called tools before (a record, not a live check)'
|
|
166
|
-
: `✗ ${attachAction}`));
|
|
161
|
+
// The record alone cannot answer whether the link works NOW (#1710): when the port is held
|
|
162
|
+
// but `/status` does not answer, the record below would still read healthy or blame the
|
|
163
|
+
// client, while the daemon row above already names the one fix for the held port. So in that
|
|
164
|
+
// state the agent link row only points back at the daemon row instead of repeating the
|
|
165
|
+
// record or adding a second command. No probe runs here: `/status` and `/mcp/sse` share one
|
|
166
|
+
// HTTP server, so silence on `/status` already answers the MCP question, and an SSE connect
|
|
167
|
+
// would flip agent presence on a daemon with no agents attached.
|
|
168
|
+
const foreign = presence === PortPresence.FOREIGN;
|
|
169
|
+
line(doctorRow(DoctorRow.AGENT_LINK, agentLinkText({ split, attachAction, foreign })));
|
|
167
170
|
// Remaining half of #261. The SDK dialling a port we are not on is invisible as a refused inbound,
|
|
168
171
|
// but a listener on a well-known Reticle port is visible. Report the observation; do not conclude
|
|
169
172
|
// it is the daemon this app wants — somebody else's daemon looks the same from here.
|
|
170
173
|
const occupiedSiblings = await findOccupiedSiblings(port, probeDaemon);
|
|
171
174
|
const siblingNote = siblingListenerNote(port, occupiedSiblings);
|
|
172
|
-
if (siblingNote !== undefined)
|
|
175
|
+
if (siblingNote !== undefined) {
|
|
173
176
|
line(doctorRow(DoctorRow.SIBLING, siblingNote));
|
|
177
|
+
}
|
|
174
178
|
// The check this checklist was missing: is the APP wired, not just the tools. Everything above can
|
|
175
179
|
// be green in a project that has never been through `init`, and that combination is precisely the
|
|
176
180
|
// one `doctor` gets run to explain.
|
|
@@ -182,8 +186,9 @@ export async function handleDoctor(port) {
|
|
|
182
186
|
configPresent: existsSync(join(process.cwd(), RETICLE_CONFIG_BASENAME)),
|
|
183
187
|
}));
|
|
184
188
|
// Are prompts reaching `.reticle/`, and is the work reaching the user's own workspace.
|
|
185
|
-
for (const row of await captureAndSyncLines(process.cwd(), process.env))
|
|
189
|
+
for (const row of await captureAndSyncLines(process.cwd(), process.env)) {
|
|
186
190
|
line(row);
|
|
191
|
+
}
|
|
187
192
|
// Where to LOOK when something is wrong. The daemon has always written a structured log here and
|
|
188
193
|
// nothing ever said so, so the first move in every investigation was reading source instead of
|
|
189
194
|
// reading the log. `RETICLE_TRACE=1` turns the same stream into a per-stage trace — see
|
|
@@ -255,8 +260,9 @@ export async function handleDoctor(port) {
|
|
|
255
260
|
line(doctorRow(DoctorRow.DESKTOP, '✓ desktop wiring looks right'));
|
|
256
261
|
}
|
|
257
262
|
const keychain = keychainNoteFor({ read: readProjectFile });
|
|
258
|
-
if (keychain !== undefined)
|
|
263
|
+
if (keychain !== undefined) {
|
|
259
264
|
line(doctorRow(DoctorRow.DESKTOP, `⚠ ${keychain}`));
|
|
265
|
+
}
|
|
260
266
|
}
|
|
261
267
|
/**
|
|
262
268
|
* Two rows: does each agent hand its prompts to Reticle, and does the daemon send this
|
|
@@ -283,7 +289,8 @@ async function captureAndSyncLines(cwd, env) {
|
|
|
283
289
|
? 'no push yet'
|
|
284
290
|
: `last push ${new Date(state.lastPushAt).toISOString()}`;
|
|
285
291
|
lines.push(doctorRow(DoctorRow.SYNC, `${on ? '✓ on' : '✗ off'}: ${gate.why}; ${last}`));
|
|
286
|
-
if (state.lastError !== undefined)
|
|
292
|
+
if (state.lastError !== undefined) {
|
|
287
293
|
lines.push(doctorRow(DoctorRow.SYNC, `✗ last error: ${state.lastError}`));
|
|
294
|
+
}
|
|
288
295
|
return lines;
|
|
289
296
|
}
|
|
@@ -169,9 +169,13 @@ acceptCoverage = false) {
|
|
|
169
169
|
.map(([name]) => name);
|
|
170
170
|
// Only against a suite that exists: a project with no flows yet is NOTHING_TO_CHECK, and
|
|
171
171
|
// demanding a flow per edited component on its first day would block every stop.
|
|
172
|
+
const flowSourcesAll = toFlowSources(allFlows);
|
|
172
173
|
const unflowed = 0 === allFlows.length
|
|
173
174
|
? []
|
|
174
|
-
: unflowedFiles(
|
|
175
|
+
: unflowedFiles(flowSourcesAll, changed, (file) => isInteractiveSource(readChangedFile(process.cwd(), file)));
|
|
176
|
+
// Flows exist but none carries a source stamp (a stack without source stamping, or flows saved before stamping was added): `unflowed`
|
|
177
|
+
// then names every changed component, which would read as "nothing drove it" when a flow did.
|
|
178
|
+
const flowsWithoutSources = allFlows.length > 0 && flowSourcesAll.every((f) => 0 === (f.sources ?? []).length);
|
|
175
179
|
// The coverage ledger, when one exists: a level that fell below its best, and changed code the
|
|
176
180
|
// browser loaded and never ran. A project that never folded coverage has an empty ledger and
|
|
177
181
|
// neither can fire.
|
|
@@ -217,6 +221,7 @@ acceptCoverage = false) {
|
|
|
217
221
|
...(result.downgraded.length > 0 ? { downgraded: result.downgraded } : {}),
|
|
218
222
|
...(result.deleted.length > 0 ? { deletedCoverage: result.deleted } : {}),
|
|
219
223
|
...(result.unflowed.length > 0 ? { unflowed: result.unflowed } : {}),
|
|
224
|
+
...(result.unflowed.length > 0 && flowsWithoutSources ? { flowsWithoutSources } : {}),
|
|
220
225
|
...(result.coverageRegressed.length > 0
|
|
221
226
|
? { coverageRegressed: result.coverageRegressed }
|
|
222
227
|
: {}),
|
|
@@ -257,6 +262,7 @@ acceptCoverage = false) {
|
|
|
257
262
|
downgraded: result.downgraded.map((d) => d.flow),
|
|
258
263
|
deleted: result.deleted,
|
|
259
264
|
unflowed: result.unflowed,
|
|
265
|
+
flowsWithoutSources,
|
|
260
266
|
coverageRegressed: result.coverageRegressed.map((r) => `${r.level} ${String(r.was)}% -> ${String(r.now)}%`),
|
|
261
267
|
unexecuted: result.unexecuted,
|
|
262
268
|
regressed: regressed?.length ?? 0,
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Force a hidden browser. The default is now HEADED: a run nobody can see is a run nobody trusts,
|
|
3
|
+
* and every "did it actually do anything?" question cost a round-trip. CI passes this (or just sets
|
|
4
|
+
* CI, which flips the default) because there is no display there to be headed on.
|
|
5
|
+
*/
|
|
6
|
+
export declare const HEADLESS_FLAG = "--headless";
|
|
7
|
+
export declare const DRIVE_FLAG = "--drive";
|
|
8
|
+
export declare const HTTP_FLAG = "--http";
|
|
9
|
+
export declare const HTTP_PORT_FLAG = "--http-port";
|
|
10
|
+
export declare const HTTP_TOKEN_FLAG = "--http-token";
|
|
11
|
+
type ServeFlags = {
|
|
12
|
+
kind: 'ok';
|
|
13
|
+
port: number;
|
|
14
|
+
driveUrl?: string;
|
|
15
|
+
headless: boolean;
|
|
16
|
+
http: boolean;
|
|
17
|
+
httpPort?: number;
|
|
18
|
+
httpToken?: string;
|
|
19
|
+
} | {
|
|
20
|
+
kind: 'error';
|
|
21
|
+
message: string;
|
|
22
|
+
};
|
|
23
|
+
export declare function parseServeFlags(args: string[], defaultPort: number, defaultHeadless: boolean): ServeFlags;
|
|
24
|
+
export {};
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The serve / mcp / `_daemon` flag grammar, including the verify HTTP endpoint.
|
|
3
|
+
*
|
|
4
|
+
* Split out of `cli-parse.ts` when that file crossed the line cap. These three commands share one
|
|
5
|
+
* flag set: the bridge port, a drive URL, headless, and the verify endpoint `--http` starts.
|
|
6
|
+
*/
|
|
7
|
+
import { HEADED_FLAG, PORT_FLAG, missingValue, notANumber, unknownArgument, } from './cli-parse-grammar.js';
|
|
8
|
+
/**
|
|
9
|
+
* Force a hidden browser. The default is now HEADED: a run nobody can see is a run nobody trusts,
|
|
10
|
+
* and every "did it actually do anything?" question cost a round-trip. CI passes this (or just sets
|
|
11
|
+
* CI, which flips the default) because there is no display there to be headed on.
|
|
12
|
+
*/
|
|
13
|
+
export const HEADLESS_FLAG = '--headless';
|
|
14
|
+
export const DRIVE_FLAG = '--drive';
|
|
15
|
+
export const HTTP_FLAG = '--http';
|
|
16
|
+
export const HTTP_PORT_FLAG = '--http-port';
|
|
17
|
+
export const HTTP_TOKEN_FLAG = '--http-token';
|
|
18
|
+
const requiresHttp = (flag) => ({
|
|
19
|
+
kind: 'error',
|
|
20
|
+
message: `${flag} requires ${HTTP_FLAG} — it configures the verify endpoint ${HTTP_FLAG} starts`,
|
|
21
|
+
});
|
|
22
|
+
/**
|
|
23
|
+
* The verify endpoint cannot use the bridge port.
|
|
24
|
+
*
|
|
25
|
+
* `reticle serve --http --http-port 4400` reached `listen` and died with a raw
|
|
26
|
+
* `EADDRINUSE 127.0.0.1:4400`. That number is this daemon's bridge port, and MCP is already
|
|
27
|
+
* served there. Name both ports and stop before anything listens (#1786).
|
|
28
|
+
*/
|
|
29
|
+
const httpPortIsBridge = (httpPort, bridgePort) => ({
|
|
30
|
+
kind: 'error',
|
|
31
|
+
message: `${HTTP_PORT_FLAG} ${String(httpPort)} is the bridge port ${String(bridgePort)}. ` +
|
|
32
|
+
'Pick another port. MCP is already served on the bridge port.',
|
|
33
|
+
});
|
|
34
|
+
export function parseServeFlags(args, defaultPort, defaultHeadless) {
|
|
35
|
+
let port = defaultPort;
|
|
36
|
+
let driveUrl;
|
|
37
|
+
let headless = defaultHeadless;
|
|
38
|
+
let http = false;
|
|
39
|
+
let httpPort;
|
|
40
|
+
let httpToken;
|
|
41
|
+
let i = 0;
|
|
42
|
+
while (i < args.length) {
|
|
43
|
+
const arg = args[i];
|
|
44
|
+
// `i < args.length` already guarantees this, but the index signature does not say so and the
|
|
45
|
+
// unknown-argument error needs a real string to name.
|
|
46
|
+
if (arg === undefined) {
|
|
47
|
+
break;
|
|
48
|
+
}
|
|
49
|
+
if (arg === PORT_FLAG) {
|
|
50
|
+
i++;
|
|
51
|
+
const n = args[i];
|
|
52
|
+
if (n === undefined) {
|
|
53
|
+
return missingValue(PORT_FLAG);
|
|
54
|
+
}
|
|
55
|
+
const parsed = parseInt(n, 10);
|
|
56
|
+
if (isNaN(parsed)) {
|
|
57
|
+
return notANumber(PORT_FLAG, n);
|
|
58
|
+
}
|
|
59
|
+
port = parsed;
|
|
60
|
+
}
|
|
61
|
+
else if (arg === DRIVE_FLAG) {
|
|
62
|
+
i++;
|
|
63
|
+
driveUrl = args[i];
|
|
64
|
+
if (driveUrl === undefined) {
|
|
65
|
+
return missingValue(DRIVE_FLAG);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
else if (arg === HEADED_FLAG) {
|
|
69
|
+
headless = false;
|
|
70
|
+
}
|
|
71
|
+
else if (arg === HEADLESS_FLAG) {
|
|
72
|
+
headless = true;
|
|
73
|
+
}
|
|
74
|
+
else if (arg === HTTP_FLAG) {
|
|
75
|
+
http = true;
|
|
76
|
+
}
|
|
77
|
+
else if (arg === HTTP_PORT_FLAG) {
|
|
78
|
+
i++;
|
|
79
|
+
const n = args[i];
|
|
80
|
+
if (n === undefined) {
|
|
81
|
+
return missingValue(HTTP_PORT_FLAG);
|
|
82
|
+
}
|
|
83
|
+
const parsed = parseInt(n, 10);
|
|
84
|
+
if (isNaN(parsed)) {
|
|
85
|
+
return notANumber(HTTP_PORT_FLAG, n);
|
|
86
|
+
}
|
|
87
|
+
httpPort = parsed;
|
|
88
|
+
}
|
|
89
|
+
else if (arg === HTTP_TOKEN_FLAG) {
|
|
90
|
+
i++;
|
|
91
|
+
httpToken = args[i];
|
|
92
|
+
if (httpToken === undefined) {
|
|
93
|
+
return missingValue(HTTP_TOKEN_FLAG);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
else {
|
|
97
|
+
return unknownArgument(arg);
|
|
98
|
+
}
|
|
99
|
+
i++;
|
|
100
|
+
}
|
|
101
|
+
// Without `--http` these flags configure an endpoint nothing starts, and they were accepted and
|
|
102
|
+
// silently ignored — worse than being rejected, because the whole reason to pass them is to be
|
|
103
|
+
// honoured (#687).
|
|
104
|
+
if (!http && httpPort !== undefined) {
|
|
105
|
+
return requiresHttp(HTTP_PORT_FLAG);
|
|
106
|
+
}
|
|
107
|
+
if (!http && httpToken !== undefined) {
|
|
108
|
+
return requiresHttp(HTTP_TOKEN_FLAG);
|
|
109
|
+
}
|
|
110
|
+
// `port` is the bridge port this command will bind. The caller passes the resolved default
|
|
111
|
+
// (RETICLE_PORT, the project port, this project's daemon, or 4400). `--port` replaces it.
|
|
112
|
+
if (httpPort !== undefined && httpPort === port) {
|
|
113
|
+
return httpPortIsBridge(httpPort, port);
|
|
114
|
+
}
|
|
115
|
+
return {
|
|
116
|
+
kind: 'ok',
|
|
117
|
+
port,
|
|
118
|
+
headless,
|
|
119
|
+
http,
|
|
120
|
+
...(driveUrl !== undefined ? { driveUrl } : {}),
|
|
121
|
+
...(httpPort !== undefined ? { httpPort } : {}),
|
|
122
|
+
...(httpToken !== undefined ? { httpToken } : {}),
|
|
123
|
+
};
|
|
124
|
+
}
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
import { type ParsedFeedback } from './cli-parse-feedback.js';
|
|
7
7
|
import { HEADED_FLAG, PORT_FLAG } from './cli-parse-grammar.js';
|
|
8
8
|
export { HEADED_FLAG, PORT_FLAG };
|
|
9
|
+
export { DRIVE_FLAG, HEADLESS_FLAG, HTTP_FLAG, HTTP_PORT_FLAG, HTTP_TOKEN_FLAG, } from './cli-parse-serve.js';
|
|
9
10
|
export { AGENT_FLAG, BUG_FLAG, CONTACT_FLAG, FEEDBACK_KINDS, KIND_FLAG, RATING_FLAG, } from './cli-parse-feedback.js';
|
|
10
11
|
import { CLI_USAGE } from './cli-usage.js';
|
|
11
12
|
export { CLI_USAGE };
|
|
@@ -38,16 +39,6 @@ export declare const PROBE_COMMANDS: ReadonlySet<string>;
|
|
|
38
39
|
export declare const LOCAL_COMMANDS: readonly string[];
|
|
39
40
|
/** The subcommand name if we recognize it, else `unknown`. Bare `reticle` reports `help`. */
|
|
40
41
|
export declare function knownCommand(arg: string | undefined): string;
|
|
41
|
-
/**
|
|
42
|
-
* Force a hidden browser. The default is now HEADED: a run nobody can see is a run nobody trusts,
|
|
43
|
-
* and every "did it actually do anything?" question cost a round-trip. CI passes this (or just sets
|
|
44
|
-
* CI, which flips the default) because there is no display there to be headed on.
|
|
45
|
-
*/
|
|
46
|
-
export declare const HEADLESS_FLAG = "--headless";
|
|
47
|
-
export declare const DRIVE_FLAG = "--drive";
|
|
48
|
-
export declare const HTTP_FLAG = "--http";
|
|
49
|
-
export declare const HTTP_PORT_FLAG = "--http-port";
|
|
50
|
-
export declare const HTTP_TOKEN_FLAG = "--http-token";
|
|
51
42
|
/**
|
|
52
43
|
* A predicate for a flow-free, one-shot verdict against the daemon that is already running.
|
|
53
44
|
*
|