@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.
Files changed (115) hide show
  1. package/SKILL.md +2 -2
  2. package/dist/command/cli/answers/gate-hook-message.d.ts +2 -0
  3. package/dist/command/cli/answers/gate-hook-message.js +5 -1
  4. package/dist/command/cli/auth/cloud-session.d.ts +1 -19
  5. package/dist/command/cli/auth/device-flow.d.ts +3 -37
  6. package/dist/command/cli/cli-doctor.js +30 -23
  7. package/dist/command/cli/cli-flow-commands.js +7 -1
  8. package/dist/command/cli/cli-parse-serve.d.ts +24 -0
  9. package/dist/command/cli/cli-parse-serve.js +124 -0
  10. package/dist/command/cli/cli-parse.d.ts +1 -10
  11. package/dist/command/cli/cli-parse.js +62 -121
  12. package/dist/command/cli/cli-usage.js +5 -2
  13. package/dist/command/cli/cloud-link.d.ts +1 -20
  14. package/dist/command/cli/doctor/doctor-agent-link.d.ts +23 -0
  15. package/dist/command/cli/doctor/doctor-agent-link.js +28 -0
  16. package/dist/command/cli/ports/port-holder.js +6 -1
  17. package/dist/command/cli/project-name.d.ts +1 -7
  18. package/dist/command/cli/project-name.js +1 -0
  19. package/dist/command/daemon/daemon.d.ts +6 -0
  20. package/dist/command/daemon/daemon.js +52 -2
  21. package/dist/command/daemon/win32-escape.d.ts +32 -0
  22. package/dist/command/daemon/win32-escape.js +119 -0
  23. package/dist/command/setup/bringup/dev-server-files.d.ts +1 -9
  24. package/dist/command/setup/bringup/dev-server-wait.js +5 -1
  25. package/dist/command/version/version-skew.d.ts +2 -0
  26. package/dist/command/version/version-skew.js +7 -3
  27. package/dist/features/crawl/crawl-tools.js +2 -2
  28. package/dist/features/exhaust/ledger.d.ts +15 -96
  29. package/dist/features/exhaust/ledger.js +3 -3
  30. package/dist/features/license/license.d.ts +2 -14
  31. package/dist/hooks/hook-commands.js +19 -9
  32. package/dist/judgement/capsule/capsule-store.d.ts +2 -39
  33. package/dist/language/flows/annotate-tools.js +1 -1
  34. package/dist/language/flows/fields/flow-secret-field.js +1 -1
  35. package/dist/language/flows/flow-expect-grammar.js +14 -6
  36. package/dist/language/flows/flow-lease.d.ts +27 -0
  37. package/dist/language/flows/flow-lease.js +59 -0
  38. package/dist/language/flows/flow-replay-run.d.ts +1 -87
  39. package/dist/language/flows/flow-replay-run.js +88 -283
  40. package/dist/language/flows/flow-replayable.d.ts +1 -1
  41. package/dist/language/flows/flow-start-path.d.ts +108 -0
  42. package/dist/language/flows/flow-start-path.js +313 -0
  43. package/dist/language/flows/flow-tools.d.ts +2 -23
  44. package/dist/language/flows/flow-tools.js +34 -66
  45. package/dist/language/flows/recording/flake.d.ts +3 -27
  46. package/dist/language/flows/recording/flake.js +1 -1
  47. package/dist/language/flows/stores/assertion-tiers-store.js +1 -1
  48. package/dist/machine/git-tracks.js +1 -0
  49. package/dist/memory/cloud/cloud-sync.d.ts +2 -24
  50. package/dist/memory/intent/intent-shard.d.ts +41 -266
  51. package/dist/memory/journal/envelope-store.js +1 -1
  52. package/dist/memory/journal/envelope.d.ts +6 -154
  53. package/dist/memory/journal/on-disk/ambient-file.d.ts +1 -7
  54. package/dist/memory/journal/on-disk/ambient-file.js +1 -1
  55. package/dist/memory/journal/self-observation.js +3 -1
  56. package/dist/memory/journal/session-journal.js +39 -20
  57. package/dist/portal/input/drive-error.d.ts +6 -0
  58. package/dist/portal/input/drive-error.js +9 -0
  59. package/dist/portal/input/real-input-press.d.ts +34 -0
  60. package/dist/portal/input/real-input-press.js +150 -0
  61. package/dist/portal/input/real-input.d.ts +6 -23
  62. package/dist/portal/input/real-input.js +95 -192
  63. package/dist/portal/input/viewport-tools.d.ts +19 -15
  64. package/dist/portal/pool/browser-pool.d.ts +5 -55
  65. package/dist/portal/pool/browser-pool.js +121 -100
  66. package/dist/portal/pool/pool-lease.d.ts +128 -0
  67. package/dist/portal/pool/pool-lease.js +49 -0
  68. package/dist/portal/production/production-activation.d.ts +5 -27
  69. package/dist/portal/session/control-envelope.d.ts +1 -1
  70. package/dist/portal/session/zero-install-first-call.d.ts +3 -0
  71. package/dist/portal/session/zero-install-first-call.js +33 -12
  72. package/dist/surface/mcp/mcp.d.ts +3 -3
  73. package/dist/surface/mcp/mcp.js +4 -6
  74. package/dist/surface/tools/act/act-danger.js +32 -17
  75. package/dist/surface/tools/act/durable.js +1 -1
  76. package/dist/surface/tools/act-sequence-tool.js +3 -3
  77. package/dist/surface/tools/act-tools.js +15 -16
  78. package/dist/surface/tools/agent/compact-envelope.d.ts +2 -2
  79. package/dist/surface/tools/coverage-tools.js +1 -1
  80. package/dist/surface/tools/dynamic-tools.d.ts +8 -0
  81. package/dist/surface/tools/dynamic-tools.js +36 -16
  82. package/dist/surface/tools/empty-session-list.d.ts +5 -29
  83. package/dist/surface/tools/feedback-tools.js +1 -1
  84. package/dist/surface/tools/gap-novelty.d.ts +1 -1
  85. package/dist/surface/tools/gaps/capability-absences.js +4 -3
  86. package/dist/surface/tools/harness-gate.d.ts +1 -1
  87. package/dist/surface/tools/harness-toolset.js +3 -4
  88. package/dist/surface/tools/lease-permissions.d.ts +1 -1
  89. package/dist/surface/tools/lease-readiness.d.ts +23 -1
  90. package/dist/surface/tools/lease-readiness.js +75 -9
  91. package/dist/surface/tools/lease-tools.d.ts +1 -52
  92. package/dist/surface/tools/lease-tools.js +79 -142
  93. package/dist/surface/tools/observe-tools.js +3 -1
  94. package/dist/surface/tools/read-tools.js +5 -5
  95. package/dist/surface/tools/real-input-attempt.js +70 -31
  96. package/dist/surface/tools/session-ports.d.ts +57 -0
  97. package/dist/surface/tools/session-ports.js +102 -0
  98. package/dist/surface/tools/tool-kit.d.ts +5 -3
  99. package/dist/surface/tools/tools.js +1 -1
  100. package/dist/telemetry/daemon-telemetry.d.ts +3 -1
  101. package/dist/telemetry/daemon-telemetry.js +9 -2
  102. package/dist/telemetry/telemetry.d.ts +3 -1
  103. package/dist/telemetry/telemetry.js +2 -0
  104. package/docs/agent-cheatsheet.md +12 -0
  105. package/docs/cli/gate.mdx +1 -1
  106. package/docs/cli/verify.mdx +8 -1
  107. package/docs/first-drive.md +7 -0
  108. package/docs/multi-agent-testing.md +2 -0
  109. package/docs/packages/react.mdx +14 -2
  110. package/docs/packages/vite-plugin.mdx +1 -0
  111. package/docs/predicates.mdx +6 -0
  112. package/docs/skill.md +3 -3
  113. package/docs/troubleshooting.mdx +10 -0
  114. package/docs/usage.md +1 -1
  115. 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 at all | `npx @reticlehq/server verify <url> --expect '<predicate JSON>'` |
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 stop`, then retry |
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(` changed, and no saved flow drives it: ${name(input.unflowed)} — drive it with reticle_act_and_wait and a declared \`until\``);
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
- }, "strip", z.ZodTypeAny, {
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
- }, "strip", z.ZodTypeAny, {
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
- }, "strip", z.ZodTypeAny, {
48
- name: string;
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
- for (const row of coverageLines(daemonStatus))
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
- line(doctorRow(DoctorRow.AGENT_LINK, split !== undefined
155
- ? `✗ ${split}`
156
- : attachAction === undefined
157
- ? // A RECORD, not a live check, and the row now says so. The attach memory is durable
158
- // and per-port: once a client has attached, enumerated and called, this stays green
159
- // forever. A registration pointing at a path that no longer exists produces a client
160
- // that cannot start at all, and this row reported the link healthy throughout —
161
- // measured, on this machine, for the two days after `packages/` was dissolved and
162
- // nothing updated ~/.claude.json. `doctor` is what a stuck human runs, so a row that
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(toFlowSources(allFlows), changed, (file) => isInteractiveSource(readChangedFile(process.cwd(), file)));
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
  *