@reticlehq/server 2.11.0 → 2.12.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.
Files changed (252) hide show
  1. package/SKILL.md +26 -2
  2. package/dist/bridge/bridge.d.ts +0 -13
  3. package/dist/bridge/bridge.js +1 -1
  4. package/dist/bridge/bridge.test-harness.d.ts +2 -0
  5. package/dist/bridge/bridge.test-harness.js +8 -1
  6. package/dist/capsule/capsule-store.d.ts +2 -1
  7. package/dist/capsule/capsule-store.js +1 -1
  8. package/dist/capsule/causal-summary.d.ts +2 -2
  9. package/dist/chromium-launch-options.d.ts +28 -0
  10. package/dist/chromium-launch-options.js +29 -0
  11. package/dist/cli/cli-doctor.js +8 -0
  12. package/dist/cli/cli-flow-commands.d.ts +3 -1
  13. package/dist/cli/cli-flow-commands.js +34 -4
  14. package/dist/cli/cli-kill.d.ts +2 -1
  15. package/dist/cli/cli-parse.d.ts +3 -1
  16. package/dist/cli/cli-parse.js +29 -7
  17. package/dist/cli/cli-update-commands.js +20 -1
  18. package/dist/cli/cli-verify.d.ts +1 -1
  19. package/dist/cli/cli-verify.js +2 -2
  20. package/dist/cli/cloud-cli.js +12 -6
  21. package/dist/cli/config-discovery.d.ts +2 -1
  22. package/dist/cli/doctor-rows.d.ts +3 -1
  23. package/dist/cli/doctor-rows.js +3 -1
  24. package/dist/cli/gate-exit.d.ts +21 -0
  25. package/dist/cli/gate-exit.js +21 -0
  26. package/dist/cli/gate-hook-message.d.ts +33 -0
  27. package/dist/cli/gate-hook-message.js +46 -0
  28. package/dist/cli/mcp-command.d.ts +25 -0
  29. package/dist/cli/mcp-command.js +143 -0
  30. package/dist/cli/sibling-ports.d.ts +37 -0
  31. package/dist/cli/sibling-ports.js +56 -0
  32. package/dist/cli.js +37 -110
  33. package/dist/cloud/cloud-config.d.ts +2 -1
  34. package/dist/cloud/cloud-sync.d.ts +11 -0
  35. package/dist/cloud/cloud-sync.js +32 -0
  36. package/dist/crawl/crawl.d.ts +9 -13
  37. package/dist/crawl/crawl.js +20 -3
  38. package/dist/daemon/daemon-resolve.d.ts +79 -0
  39. package/dist/daemon/daemon-resolve.js +118 -0
  40. package/dist/daemon/free-port.d.ts +29 -0
  41. package/dist/daemon/free-port.js +55 -0
  42. package/dist/daemon/heartbeat.d.ts +0 -4
  43. package/dist/daemon/heartbeat.js +2 -2
  44. package/dist/dev/stale-issue-guard.d.ts +0 -2
  45. package/dist/dev/stale-issue-guard.js +1 -1
  46. package/dist/events/contradictions.d.ts +41 -0
  47. package/dist/events/contradictions.js +125 -12
  48. package/dist/events/declared.d.ts +14 -0
  49. package/dist/events/declared.js +33 -0
  50. package/dist/events/echo-mismatch.js +14 -0
  51. package/dist/events/observed-in-window.d.ts +12 -0
  52. package/dist/events/observed-in-window.js +17 -0
  53. package/dist/events/predicate-asks.d.ts +13 -0
  54. package/dist/events/predicate-asks.js +22 -0
  55. package/dist/events/predicate-eval.js +64 -22
  56. package/dist/events/predicate-parse.d.ts +0 -6
  57. package/dist/events/predicate-parse.js +43 -3
  58. package/dist/events/predicate-schema.d.ts +44 -0
  59. package/dist/events/predicate-schema.js +126 -23
  60. package/dist/events/predicate.d.ts +15 -0
  61. package/dist/events/predicate.js +41 -2
  62. package/dist/flows/coverage.d.ts +0 -8
  63. package/dist/flows/coverage.js +1 -1
  64. package/dist/flows/decision.d.ts +5 -1
  65. package/dist/flows/decision.js +33 -7
  66. package/dist/flows/flake.d.ts +0 -2
  67. package/dist/flows/flake.js +1 -1
  68. package/dist/flows/flow-intent.d.ts +52 -0
  69. package/dist/flows/flow-intent.js +123 -0
  70. package/dist/flows/flow-replay-run.js +27 -4
  71. package/dist/flows/flow-success.js +15 -3
  72. package/dist/flows/flow-tools.js +32 -4
  73. package/dist/flows/flows.d.ts +6 -1
  74. package/dist/flows/flows.js +34 -22
  75. package/dist/flows/predicate-to-expect.js +6 -3
  76. package/dist/flows/server-verify.js +16 -2
  77. package/dist/flows/verify-change-tools.js +4 -1
  78. package/dist/honesty/blind-spots.d.ts +0 -17
  79. package/dist/honesty/blind-spots.js +1 -1
  80. package/dist/honesty/feature-capture.d.ts +150 -0
  81. package/dist/honesty/feature-capture.js +233 -0
  82. package/dist/honesty/instrumentation-gaps.d.ts +30 -6
  83. package/dist/honesty/instrumentation-gaps.js +61 -5
  84. package/dist/honesty/tool-hit-rate.d.ts +86 -0
  85. package/dist/honesty/tool-hit-rate.js +90 -0
  86. package/dist/honesty/undeclared-change.d.ts +51 -0
  87. package/dist/honesty/undeclared-change.js +56 -0
  88. package/dist/honesty/verified.d.ts +15 -0
  89. package/dist/honesty/verified.js +68 -4
  90. package/dist/http-server.d.ts +0 -1
  91. package/dist/http-server.js +1 -1
  92. package/dist/hunt/hunt-report.d.ts +0 -2
  93. package/dist/hunt/hunt-report.js +1 -1
  94. package/dist/impact/impact-store.d.ts +0 -5
  95. package/dist/impact/impact-store.js +1 -1
  96. package/dist/index.d.ts +4 -1
  97. package/dist/init/agent-rules.d.ts +17 -0
  98. package/dist/init/agent-rules.js +31 -1
  99. package/dist/init/capabilities.d.ts +0 -2
  100. package/dist/init/capabilities.js +1 -1
  101. package/dist/init/closing-hint.d.ts +36 -0
  102. package/dist/init/closing-hint.js +120 -0
  103. package/dist/init/confirm.d.ts +0 -25
  104. package/dist/init/confirm.js +3 -3
  105. package/dist/init/cra.d.ts +14 -1
  106. package/dist/init/cra.js +43 -12
  107. package/dist/init/dev-script.d.ts +48 -0
  108. package/dist/init/dev-script.js +63 -0
  109. package/dist/init/mcp.d.ts +16 -5
  110. package/dist/init/mcp.js +26 -10
  111. package/dist/init/plan-framework.d.ts +0 -5
  112. package/dist/init/plan-framework.js +83 -9
  113. package/dist/init/plan.d.ts +10 -30
  114. package/dist/init/plan.js +30 -15
  115. package/dist/init/refresh-rules.d.ts +39 -0
  116. package/dist/init/refresh-rules.js +51 -0
  117. package/dist/init/run.d.ts +0 -26
  118. package/dist/init/run.js +14 -87
  119. package/dist/init/snippets.js +53 -10
  120. package/dist/input/real-input.d.ts +2 -0
  121. package/dist/input/real-input.js +3 -2
  122. package/dist/input/scroll-find.d.ts +7 -4
  123. package/dist/input/scroll-find.js +63 -25
  124. package/dist/intent/inline-intent.d.ts +46 -0
  125. package/dist/intent/inline-intent.js +103 -0
  126. package/dist/intent/intent-store.d.ts +37 -0
  127. package/dist/intent/intent-store.js +112 -0
  128. package/dist/intent/intent-tools.d.ts +2 -0
  129. package/dist/intent/intent-tools.js +80 -0
  130. package/dist/intent/open-intents.d.ts +21 -0
  131. package/dist/intent/open-intents.js +25 -0
  132. package/dist/journal/envelope.d.ts +26 -26
  133. package/dist/journal/journal-recorder.d.ts +23 -0
  134. package/dist/journal/journal-recorder.js +35 -0
  135. package/dist/mcp/attach-memory.d.ts +67 -0
  136. package/dist/mcp/attach-memory.js +142 -0
  137. package/dist/mcp/mcp-proxy.d.ts +0 -7
  138. package/dist/mcp/mcp-proxy.js +14 -2
  139. package/dist/mcp/mcp.d.ts +2 -0
  140. package/dist/mcp/mcp.js +45 -23
  141. package/dist/mcp/server-instructions.js +45 -3
  142. package/dist/mcp/tool-catalog-cache.d.ts +8 -0
  143. package/dist/mcp/tool-catalog-cache.js +10 -0
  144. package/dist/oracles/flow-instrument-gaps.d.ts +4 -4
  145. package/dist/oracles/flow-instrument-gaps.js +4 -5
  146. package/dist/oracles/self-instrument.d.ts +9 -9
  147. package/dist/oracles/self-instrument.js +19 -13
  148. package/dist/pool/browser-pool.d.ts +12 -1
  149. package/dist/pool/browser-pool.js +31 -1
  150. package/dist/pool/playwright-launcher.js +3 -1
  151. package/dist/project/fs-port.d.ts +3 -0
  152. package/dist/project/fs-port.js +3 -2
  153. package/dist/project/memory-fs.js +6 -1
  154. package/dist/project/project-tools.js +3 -32
  155. package/dist/project/reticle-dir.d.ts +2 -0
  156. package/dist/project/reticle-dir.js +1 -0
  157. package/dist/runs/context-tools.d.ts +2 -0
  158. package/dist/runs/context-tools.js +89 -0
  159. package/dist/runs/run-context.d.ts +40 -0
  160. package/dist/runs/run-context.js +158 -0
  161. package/dist/runs/run-diff.d.ts +2 -1
  162. package/dist/runs/verification-sync.js +2 -2
  163. package/dist/session/dev-server-probe.d.ts +0 -8
  164. package/dist/session/dev-server-probe.js +1 -1
  165. package/dist/session/last-act.d.ts +7 -1
  166. package/dist/session/last-act.js +2 -2
  167. package/dist/session/live-control-tools.js +1 -42
  168. package/dist/session/no-session-diagnosis.d.ts +10 -0
  169. package/dist/session/no-session-diagnosis.js +24 -7
  170. package/dist/session/no-session-next-action.d.ts +5 -0
  171. package/dist/session/no-session-next-action.js +35 -3
  172. package/dist/session/no-session-watch.d.ts +8 -0
  173. package/dist/session/no-session-watch.js +82 -64
  174. package/dist/session/output-budget.js +4 -1
  175. package/dist/session/session-health.d.ts +9 -0
  176. package/dist/session/session-health.js +23 -0
  177. package/dist/session/session-manager.js +50 -1
  178. package/dist/session/session-successor.d.ts +37 -0
  179. package/dist/session/session-successor.js +78 -0
  180. package/dist/session/session.d.ts +31 -1
  181. package/dist/session/session.js +75 -2
  182. package/dist/telemetry/automation-hint.d.ts +2 -1
  183. package/dist/telemetry/feedback-outbox.d.ts +2 -1
  184. package/dist/telemetry/project-profile.d.ts +0 -8
  185. package/dist/telemetry/project-profile.js +1 -1
  186. package/dist/telemetry/run-telemetry.d.ts +0 -2
  187. package/dist/telemetry/run-telemetry.js +1 -1
  188. package/dist/telemetry/verification-of.js +2 -2
  189. package/dist/tools/act-tools.js +126 -21
  190. package/dist/tools/assert-source.d.ts +22 -0
  191. package/dist/tools/assert-source.js +53 -0
  192. package/dist/tools/assert-verdict.d.ts +20 -3
  193. package/dist/tools/assert-verdict.js +36 -4
  194. package/dist/tools/browser-tools.js +0 -20
  195. package/dist/tools/complete-snapshot.d.ts +48 -0
  196. package/dist/tools/complete-snapshot.js +75 -0
  197. package/dist/tools/coverage-tools.d.ts +10 -1
  198. package/dist/tools/coverage-tools.js +123 -69
  199. package/dist/tools/dynamic-tools.js +29 -8
  200. package/dist/tools/error-recovery.d.ts +24 -14
  201. package/dist/tools/error-recovery.js +70 -9
  202. package/dist/tools/feedback-tools.d.ts +1 -1
  203. package/dist/tools/feedback-tools.js +2 -2
  204. package/dist/tools/invoke-tool.js +62 -2
  205. package/dist/tools/lease-hint.d.ts +10 -0
  206. package/dist/tools/lease-hint.js +53 -4
  207. package/dist/tools/lease-tools.d.ts +5 -1
  208. package/dist/tools/lease-tools.js +22 -8
  209. package/dist/tools/navigate-arrival.d.ts +0 -2
  210. package/dist/tools/navigate-arrival.js +1 -1
  211. package/dist/tools/observe-tools.js +63 -26
  212. package/dist/tools/read-tools.js +5 -3
  213. package/dist/tools/real-input-attempt.d.ts +22 -0
  214. package/dist/tools/real-input-attempt.js +153 -1
  215. package/dist/tools/reconcile-tools.js +18 -4
  216. package/dist/tools/resolve-target.d.ts +0 -7
  217. package/dist/tools/sdk-marker-probe.d.ts +29 -0
  218. package/dist/tools/sdk-marker-probe.js +57 -0
  219. package/dist/tools/snapshot-delta.d.ts +1 -1
  220. package/dist/tools/tool-kit.d.ts +12 -0
  221. package/dist/tools/tool-kit.js +16 -0
  222. package/dist/tools/tool-names.d.ts +3 -0
  223. package/dist/tools/tool-names.js +3 -0
  224. package/dist/tools/tool-surface.d.ts +85 -6
  225. package/dist/tools/tool-surface.js +122 -2
  226. package/dist/tools/tools.js +22 -4
  227. package/dist/tools/unadvertised-help.js +4 -4
  228. package/dist/tools/verify-next-baton.d.ts +53 -0
  229. package/dist/tools/verify-next-baton.js +52 -0
  230. package/dist/update/update-nudge.d.ts +7 -10
  231. package/dist/update/update-nudge.js +12 -3
  232. package/dist/update/updater.d.ts +1 -1
  233. package/docs/README.md +2 -1
  234. package/docs/cli/cloud.mdx +1 -1
  235. package/docs/cli/doctor.mdx +1 -0
  236. package/docs/cli/verify.mdx +4 -5
  237. package/docs/desktop.mdx +1 -1
  238. package/docs/docs.json +2 -2
  239. package/docs/gates.md +1 -1
  240. package/docs/install-manual.mdx +1 -1
  241. package/docs/packages/browser.mdx +2 -2
  242. package/docs/packages/server.mdx +1 -1
  243. package/docs/packages.mdx +8 -6
  244. package/docs/platform-integration.md +17 -0
  245. package/docs/predicates.mdx +8 -0
  246. package/docs/reticle-manual.md +106 -0
  247. package/docs/tools-tools-and-run.mdx +2 -4
  248. package/docs/troubleshooting.mdx +9 -0
  249. package/docs/usage.md +3 -2
  250. package/package.json +2 -2
  251. package/dist/session/session-readiness.d.ts +0 -34
  252. package/dist/session/session-readiness.js +0 -36
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Bring an already-installed project's agent rules up to the version that is running.
3
+ *
4
+ * `mergeMarkedInstruction` has always been able to do this: the managed block is marker-delimited,
5
+ * idempotence is decided by comparing CONTENT rather than by seeing a marker, and everything
6
+ * outside the markers is left exactly as the human wrote it. What was missing is anything that
7
+ * CALLS it after the first install.
8
+ *
9
+ * It is reachable only from `buildPlan`, so the rules refresh when somebody re-runs `init` and
10
+ * never otherwise. A project set up on an older release keeps that release's instructions forever,
11
+ * which means every improvement to them — what to do when a capabilities file registers nothing,
12
+ * that an install is not finished until a verdict exists, that intent is captured while building —
13
+ * reaches new projects only.
14
+ *
15
+ * That is the wrong way round. The people who most need better instructions are the ones already
16
+ * installed and not getting value, and `reticle update` is the moment they are already touching the
17
+ * install.
18
+ */
19
+ import { join } from 'node:path';
20
+ import { mergeMarkedInstruction, AgentRuleStatus, hasManagedBlock } from './agent-rules.js';
21
+ /** The instruction files the managed block lives in. Cursor's rule file is regenerated whole. */
22
+ export const RULE_FILES = ['CLAUDE.md', 'AGENTS.md'];
23
+ /**
24
+ * Refresh every instruction file that already carries a Reticle block.
25
+ *
26
+ * Deliberately does NOT create one where none exists. A project with no Reticle block in its
27
+ * CLAUDE.md either never ran `init` here or had the block removed on purpose, and `update` is not
28
+ * the place to make that decision for somebody — it is a version command, and quietly writing
29
+ * instructions into a file a user curates would be a surprise.
30
+ */
31
+ export function refreshAgentRules(cwd, io) {
32
+ const updated = [];
33
+ const unchanged = [];
34
+ for (const name of RULE_FILES) {
35
+ const path = join(cwd, name);
36
+ const existing = io.read(path);
37
+ if (null === existing || !hasManagedBlock(existing)) {
38
+ unchanged.push(name);
39
+ continue;
40
+ }
41
+ const result = mergeMarkedInstruction(existing);
42
+ if (AgentRuleStatus.APPLY === result.status && result.content !== existing) {
43
+ io.write(path, result.content);
44
+ updated.push(name);
45
+ }
46
+ else {
47
+ unchanged.push(name);
48
+ }
49
+ }
50
+ return { updated, unchanged };
51
+ }
@@ -3,8 +3,6 @@
3
3
  * plan (pure), optionally write the apply-steps, and print a human-readable report. All filesystem
4
4
  * access goes through `InitIo` so the orchestration is unit-testable with an in-memory IO.
5
5
  */
6
- import { Framework } from './detect.js';
7
- import { StepStatus } from './plan.js';
8
6
  import type { InitOutcome } from '@reticlehq/core';
9
7
  /**
10
8
  * Resolve the lockfiles set used to pick the package manager. A lockfile in the project root wins;
@@ -93,28 +91,4 @@ export interface InitResult {
93
91
  * it knows it is in a workspace; this is the matching walk DOWN.
94
92
  */
95
93
  export declare function findWorkspaceApps(io: Pick<InitIo, 'exists' | 'readFile' | 'listDirs'>): string[];
96
- /**
97
- * The standing ask, printed at the end of every `init`.
98
- *
99
- * Addressed to the agent as much as the human: whichever of them just ran this command is the one
100
- * holding the experience, and neither has a Reticle tool surface to file through at this point.
101
- *
102
- * Printed by the `runInit` wrapper, NOT by `report()` — see the comment there for why, and for how
103
- * the workspace redirect is kept from asking twice.
104
- */
105
- export declare const FEEDBACK_HINT: string;
106
- /**
107
- * The last two lines a user reads, and the order matters.
108
- *
109
- * This used to say only "Restart <dev server>, then ask your agent: List Reticle sessions" — and
110
- * asking the agent was the one thing that could not work yet. `init` registers the MCP server, but
111
- * an agent client reads its tool list when it STARTS and never re-reads it, so the session that
112
- * just ran `init` has no `reticle_*` tools however clean the install was. The user follows the
113
- * instruction, the agent answers "unknown tool", and the obvious conclusion is that the install
114
- * failed.
115
- *
116
- * So the reload is named FIRST, before the sentence that depends on it. Omitted entirely when this
117
- * run did not register MCP (`--no-mcp`), where it would be advice about something we did not do.
118
- */
119
- export declare function restartHint(framework: Framework, mcpStatus: StepStatus | undefined): string;
120
94
  export declare function runInit(options: InitOptions, io: InitIo): InitResult;
package/dist/init/run.js CHANGED
@@ -5,6 +5,8 @@
5
5
  */
6
6
  import { dirname, join } from 'node:path';
7
7
  import { noPackageJsonMessage } from './non-js-project.js';
8
+ import { devCommandFrom } from './dev-script.js';
9
+ import { restartHint, FEEDBACK_HINT } from './closing-hint.js';
8
10
  import { spanSync } from '../trace.js';
9
11
  import { readFileSync } from 'node:fs';
10
12
  import { homedir } from 'node:os';
@@ -15,7 +17,7 @@ import { pickAstroHost } from './astro-host.js';
15
17
  import { workspaceParents } from './workspace-apps.js';
16
18
  import { chooseWorkspaceApp } from './app-choice.js';
17
19
  import { isConnectStep } from './connect-steps.js';
18
- import { CLI, CURSOR_RULE_PATH, RETICLE_MD_PATH } from './agent-rules.js';
20
+ import { CURSOR_RULE_PATH, RETICLE_MD_PATH } from './agent-rules.js';
19
21
  import { CRA_ENV_PATH } from './cra.js';
20
22
  /** CRA's bundled entry, in the order create-react-app itself generates them. */
21
23
  const CRA_ENTRY_CANDIDATES = ['src/index.tsx', 'src/index.jsx', 'src/index.ts', 'src/index.js'];
@@ -352,6 +354,7 @@ function gatherPlanInput(options, io, pkgRaw) {
352
354
  claudeCli,
353
355
  mcpExists,
354
356
  cursorPresent,
357
+ platform: process.platform,
355
358
  detectedClients,
356
359
  cursorProjectPresent: io.exists(CURSOR_DIR_RELPATH),
357
360
  cursorConfig,
@@ -375,6 +378,7 @@ function gatherPlanInput(options, io, pkgRaw) {
375
378
  nextReticleDevPath: devLocation.path,
376
379
  nextReticleDevImport: devLocation.importSpecifier,
377
380
  nextReticleDevExists: io.exists(devLocation.path),
381
+ nextReticleDevSource: io.readFile(devLocation.path),
378
382
  svelteKitHooksExists: io.exists(SVELTEKIT_HOOKS),
379
383
  craEntry: craEntryOf(io),
380
384
  craEnv: io.readFile(CRA_ENV_PATH),
@@ -468,91 +472,11 @@ export function findWorkspaceApps(io) {
468
472
  }
469
473
  return [...new Set(found)];
470
474
  }
471
- /**
472
- * The standing ask, printed at the end of every `init`.
473
- *
474
- * Addressed to the agent as much as the human: whichever of them just ran this command is the one
475
- * holding the experience, and neither has a Reticle tool surface to file through at this point.
476
- *
477
- * Printed by the `runInit` wrapper, NOT by `report()` — see the comment there for why, and for how
478
- * the workspace redirect is kept from asking twice.
479
- */
480
- export const FEEDBACK_HINT = 'Anything wrong, missing, or awkward — in this setup or in Reticle itself? Tell us; it is the ' +
481
- 'one thing that decides what gets fixed:\n' +
482
- ` ${CLI} feedback --agent --kind <bug|gap|ambiguity|feature_request|improvement> "what happened" (agents)\n` +
483
- ` ${CLI} feedback "what worked, what didn't" (humans)`;
484
- function devServerRestart(framework) {
485
- if (framework === Framework.NEXT)
486
- return 'Restart `next dev`';
487
- if (framework === Framework.VITE)
488
- return 'Restart `vite`';
489
- if (framework === Framework.ASTRO)
490
- return 'Restart `astro dev`';
491
- if (framework === Framework.SVELTEKIT)
492
- return 'Restart your dev server (`npm run dev`)';
493
- return 'Reload your app on localhost';
494
- }
495
- /**
496
- * The last two lines a user reads, and the order matters.
497
- *
498
- * This used to say only "Restart <dev server>, then ask your agent: List Reticle sessions" — and
499
- * asking the agent was the one thing that could not work yet. `init` registers the MCP server, but
500
- * an agent client reads its tool list when it STARTS and never re-reads it, so the session that
501
- * just ran `init` has no `reticle_*` tools however clean the install was. The user follows the
502
- * instruction, the agent answers "unknown tool", and the obvious conclusion is that the install
503
- * failed.
504
- *
505
- * So the reload is named FIRST, before the sentence that depends on it. Omitted entirely when this
506
- * run did not register MCP (`--no-mcp`), where it would be advice about something we did not do.
507
- */
508
- export function restartHint(framework, mcpStatus) {
509
- const dev = `${devServerRestart(framework)}.`;
510
- // NAME THE COMMAND THAT PROVES IT, not one that merely asks.
511
- //
512
- // `init` writes files and stops; the install is not finished until an app carrying the SDK has
513
- // actually dialled the daemon, and nothing here confirmed that. The field shape is unambiguous:
514
- // people complete the agent half, never complete the app half, and keep a daemon running for
515
- // weeks with nothing to drive — so this is the last instruction most of them read.
516
- //
517
- // "Ask your agent: List Reticle sessions" asks a question whose failure is a dead end. `reticle
518
- // status` ANSWERS it: as of 2.7.0 it reports the session, or says why there is none — no app
519
- // running, an app running that never dialled us, a tab that closed — with the fix for each.
520
- const prove = 'Then run `npx @reticlehq/server status` — it confirms the app connected, or says exactly why ' +
521
- 'it has not.';
522
- // Nothing registered this run (`--no-mcp`, or a client that needs a manual edit): a restart picks
523
- // up nothing, so advice about one would be advice about something we did not do.
524
- if (!wasMcpRegistered(mcpStatus))
525
- return `${dev} ${prove}`;
526
- // ALREADY: the tools are reachable RIGHT NOW, and this is the branch that used to lie.
527
- //
528
- // `wasMcpRegistered` is true for both APPLY and ALREADY, which is correct for the funnel field it
529
- // was written for and wrong here, because the two cases have opposite answers to "must anybody
530
- // restart". Every second project on a machine, and every Claude Code plugin install, took the
531
- // APPLY text: "restart your agent — the tools only appear after that". An agent reads this AFTER
532
- // whatever its skill file said, and it describes the output in front of it, so it wins. The agent
533
- // stops with the project wired and nothing driven, which is the exact failure the install path
534
- // exists to prevent, caused by the install path itself.
535
- if (mcpStatus === StepStatus.ALREADY) {
536
- return (`${dev}\n` +
537
- 'The Reticle MCP server was already registered on this machine, so there is no restart to do ' +
538
- 'and the tools are available now.\n' +
539
- `${prove}\n` +
540
- 'Once it shows a session, drive a flow — that is the install finished. Carry straight on; ' +
541
- 'nothing here is waiting on a human.');
542
- }
543
- return (`${dev}\n` +
544
- 'Then restart your agent so it picks up the new MCP server — restart Claude Code, reload the ' +
545
- 'window in Cursor, or hit Start in `.vscode/mcp.json` in VS Code.\n' +
546
- 'The tools only appear after that: your agent read its server list before Reticle existed, ' +
547
- 'and no slash command re-reads it (`/mcp` manages servers already loaded, so it cannot pick ' +
548
- 'up a new one). This is once per machine — Reticle is registered globally, so every later ' +
549
- 'project starts with the tools already there.\n' +
550
- `${prove}\n` +
551
- 'Once it shows a session, ask your agent to drive a flow — that is the install finished.');
552
- }
553
475
  const SKIPPED_DETAIL = 'skipped — the dependency install above failed, and wiring the app to a package that is not ' +
554
476
  'installed stops it booting. Run that install, then re-run `reticle init`.';
555
- function report(plan, dryRun, failed, skipped, degraded, io, projectDir, agentRoot) {
477
+ function report(plan, dryRun, failed, skipped, degraded, io, projectDir, agentRoot,
478
+ /** The project's own dev command, so the closing line names what a human would type. */
479
+ devCommand) {
556
480
  io.print(dryRun ? 'reticle init (dry run, no files written)' : 'reticle init');
557
481
  // Every path below is printed RELATIVE, and until now nothing said what to. Reported from the
558
482
  // field as "[✓] Reticle config → .reticle.json" followed by the file not being there: the app was
@@ -612,7 +536,7 @@ function report(plan, dryRun, failed, skipped, degraded, io, projectDir, agentRo
612
536
  '"no browser session connected" until then. Everything else is already in place.');
613
537
  io.print('');
614
538
  }
615
- io.print(restartHint(plan.framework, resolvedStatus(plan, MCP_TARGET, failed, skipped)));
539
+ io.print(restartHint(plan.framework, resolvedStatus(plan, MCP_TARGET, failed, skipped), devCommand));
616
540
  return { ok: !connectPending, applied, manual };
617
541
  }
618
542
  /**
@@ -834,12 +758,15 @@ function runInitSteps(options, io) {
834
758
  // Init is the flow a user experiences the wait of personally, and the fixture gate measures it at
835
759
  // 1–6s per app with no explanation of the spread. These three spans split that number into detect
836
760
  // (filesystem probing), plan (pure), and apply (writes + package-manager and CLI subprocesses).
837
- const plan = spanSync('init.plan', {}, () => buildPlan(gatherPlanInput(options, io, pkgRaw)));
761
+ const planInput = gatherPlanInput(options, io, pkgRaw);
762
+ const plan = spanSync('init.plan', {}, () => buildPlan(planInput));
838
763
  const effects = options.dryRun
839
764
  ? { failed: new Set(), skipped: new Set(), degraded: new Map() }
840
765
  : spanSync('init.apply', { steps: plan.steps.length }, () => applyEffects(plan, io));
841
766
  const { failed, skipped, degraded } = effects;
842
- const result = report(plan, options.dryRun, failed, skipped, degraded, io, options.cwd, agentRootOf(options));
767
+ // The project's own dev command, so the closing line names what a human would actually type.
768
+ const devCommand = devCommandFrom(pkgRaw, planInput.detection.packageManager);
769
+ const result = report(plan, options.dryRun, failed, skipped, degraded, io, options.cwd, agentRootOf(options), devCommand);
843
770
  // A dry run is a preview, not an outcome — reporting it would inflate both success and failure.
844
771
  if (options.dryRun)
845
772
  return result;
@@ -2,7 +2,7 @@
2
2
  * Generated file contents and copy-paste snippets for `reticle init`. Kept as named constants/builders
3
3
  * so the runner never inlines free strings.
4
4
  */
5
- import { RETICLE_DEFAULT_PORT, ReticleDir, bridgeWsUrl } from '@reticlehq/core';
5
+ import { RETICLE_DEFAULT_PORT, ReticleDir, bridgeWsUrl, RETICLE_CLIENT_HOST, RETICLE_WS_PATH, } from '@reticlehq/core';
6
6
  import { UiLibrary } from './detect.js';
7
7
  import { SERVER_VERSION } from '../version/server-version.js';
8
8
  /**
@@ -17,9 +17,9 @@ import { SERVER_VERSION } from '../version/server-version.js';
17
17
  *
18
18
  * jsDelivr rather than esm.sh, measured: a third of the requests and a third of the bytes for the
19
19
  * same result. `/+esm` is what makes it work, and the bare package URL does NOT: every file in
20
- * `dist` carries bare specifiers for `@reticlehq/core` and `@testing-library/dom`, so an unbundled
21
- * entry point dies on the first import. That is also why adding `unpkg`/`jsdelivr` fields to
22
- * package.json would not help.
20
+ * `dist` still carries bare workspace specifiers such as `@reticlehq/core`, so an unbundled entry
21
+ * point dies on the first import. That is also why adding `unpkg`/`jsdelivr` fields to package.json
22
+ * would not help.
23
23
  *
24
24
  * PINNED to this server's version on purpose. A floating import upgrades the page SDK underneath a
25
25
  * daemon that did not move, which is `version_skew` arriving by a route nothing checks.
@@ -151,7 +151,10 @@ export function ReticleDev() {
151
151
  // the root makes source paths repo-relative instead of absolute.
152
152
  const token = process.env.NEXT_PUBLIC_RETICLE_TOKEN;
153
153
  const root = process.env.NEXT_PUBLIC_RETICLE_ROOT;
154
- reticle.connect({ ${fields}...(token ? { token } : {}), ...(root ? { root } : {}) });
154
+ // withReticle() finds the daemon serving this project on every dev-server start. It wins over
155
+ // any port written into this file at install time, so moving the daemon needs no edit here.
156
+ const url = process.env.NEXT_PUBLIC_RETICLE_URL;
157
+ reticle.connect({ ${fields}...(url ? { url } : {}), ...(token ? { token } : {}), ...(root ? { root } : {}) });
155
158
 
156
159
  // ── Start with ONE flow. ──────────────────────────────────────────────────────────────────
157
160
  // Registering a store is the highest-value line here: it lets the agent check what the app
@@ -204,11 +207,51 @@ layoutPath) {
204
207
  ? `\n url: '${bridgeWsUrl(port)}',`
205
208
  : '';
206
209
  const id = projectId !== undefined && projectId.length > 0 ? `\n projectId: '${projectId}',` : '';
210
+ // Astro owns its Vite instance, so it has a build-time channel of its own: the same `define` that
211
+ // already inlines the token can inline the daemon URL, resolved every time the config is read —
212
+ // that is, on every `astro dev`. The port above is written once at install time and goes stale the
213
+ // moment the daemon moves; this does not.
214
+ //
215
+ // Needs the projectId to know WHICH daemon is ours. Without one there is nothing to match on, and
216
+ // adopting a daemon serving another project would report its state as this app's, so the whole
217
+ // block is omitted rather than made to guess.
218
+ const canDiscover = projectId !== undefined && projectId.length > 0;
219
+ // Interpolated from core rather than typed into the template: this is the same wire string
220
+ // `bridgeWsUrl` builds, and a generator that spells it out by hand is exactly how the four call
221
+ // sites that constant exists to unify drifted apart in the first place.
222
+ const RETICLE_CLIENT_HOST_LITERAL = `ws://${RETICLE_CLIENT_HOST}:`;
223
+ const urlHelper = canDiscover
224
+ ? `
225
+ // The live daemon serving THIS project, re-read on every \`astro dev\`. Same rule as the Vite and
226
+ // Next plugins: skip dead daemons, match on projectId, lowest port wins, '' when none matches.
227
+ function reticleUrl() {
228
+ const dir = process.env['RETICLE_PAIRING_TOKEN_DIR'] || join(homedir(), '.reticle');
229
+ let best;
230
+ try {
231
+ for (const file of readdirSync(dir)) {
232
+ if (!file.startsWith('daemon-') || !file.endsWith('.json')) continue;
233
+ let entry;
234
+ try { entry = JSON.parse(readFileSync(join(dir, file), 'utf8')); } catch { continue; }
235
+ if (entry.projectId !== '${projectId}') continue;
236
+ try { process.kill(entry.pid, 0); } catch { continue; }
237
+ if (best === undefined || entry.port < best) best = entry.port;
238
+ }
239
+ } catch { return ''; }
240
+ return best === undefined ? '' : '${RETICLE_CLIENT_HOST_LITERAL}' + best + '${RETICLE_WS_PATH}';
241
+ }
242
+ `
243
+ : '';
244
+ const urlDefine = canDiscover ? `\n __RETICLE_URL__: JSON.stringify(reticleUrl()),` : '';
245
+ const urlRead = canDiscover
246
+ ? `\n const url = typeof __RETICLE_URL__ !== 'undefined' ? __RETICLE_URL__ : '';`
247
+ : '';
248
+ // After the baked port, so the discovered one wins: later key in an object literal.
249
+ const urlSpread = canDiscover ? `\n ...(url.length > 0 ? { url } : {}),` : '';
207
250
  return `Astro renders its own HTML, so the connect goes in a page <script> and the pairing token is inlined by the config.
208
251
 
209
252
  1. In astro.config.mjs — inline the daemon's token and raise the build target:
210
253
 
211
- import { readFileSync } from 'node:fs';
254
+ import { readFileSync, readdirSync } from 'node:fs';
212
255
  import { homedir } from 'node:os';
213
256
  import { join } from 'node:path';
214
257
 
@@ -216,14 +259,14 @@ layoutPath) {
216
259
  const dir = process.env['RETICLE_PAIRING_TOKEN_DIR'] || join(homedir(), '.reticle');
217
260
  try { return readFileSync(join(dir, 'pairing-token'), 'utf8').trim(); } catch { return ''; }
218
261
  }
219
-
262
+ ${urlHelper}
220
263
  export default defineConfig({
221
264
  vite: {
222
265
  // Astro's default target down-levels the modern SDK bundle and fails on a destructuring transform.
223
266
  build: { target: 'es2022' },
224
267
  optimizeDeps: { esbuildOptions: { target: 'es2022' } },
225
268
  define: {
226
- __RETICLE_TOKEN__: JSON.stringify(reticleToken()),
269
+ __RETICLE_TOKEN__: JSON.stringify(reticleToken()),${urlDefine}
227
270
  // Without this, source pointers come back as absolute paths from YOUR machine — useless in a
228
271
  // report. Every other framework gets it from its build plugin; Astro owns its Vite instance.
229
272
  __RETICLE_ROOT__: JSON.stringify(process.cwd()),
@@ -236,10 +279,10 @@ ${layoutHost(layoutPath)}
236
279
  <script>
237
280
  if (import.meta.env.DEV) {
238
281
  const token = typeof __RETICLE_TOKEN__ !== 'undefined' ? __RETICLE_TOKEN__ : '';
239
- const root = typeof __RETICLE_ROOT__ !== 'undefined' ? __RETICLE_ROOT__ : '';
282
+ const root = typeof __RETICLE_ROOT__ !== 'undefined' ? __RETICLE_ROOT__ : '';${urlRead}
240
283
  const { reticle, install } = await import('@reticlehq/react');
241
284
  install();
242
- reticle.connect({${id}${extra}
285
+ reticle.connect({${id}${extra}${urlSpread}
243
286
  ...(token.length > 0 ? { token } : {}),
244
287
  ...(root.length > 0 ? { root } : {}),
245
288
  });
@@ -171,6 +171,8 @@ export interface LaunchedProviderOptions {
171
171
  */
172
172
  onNetworkDetail?: (detail: NetworkDetail) => void;
173
173
  }
174
+ /** The only place the dynamic value import of Playwright lives for the launched (drive) path. */
175
+ export declare const launchedChromium: LaunchFn;
174
176
  /**
175
177
  * Launches and OWNS a Playwright Chromium, navigates it to `driveUrl`, then drives native
176
178
  * input on that page. Headless-capable so @reticlehq/test / CI can run hover/drag unattended.
@@ -1,3 +1,4 @@
1
+ import { chromiumLaunchOptions } from '../chromium-launch-options.js';
1
2
  import { gotoOptions } from '../pool/playwright-launcher.js';
2
3
  import { BrowserLaunchKind } from '@reticlehq/core';
3
4
  import { getSessionMetrics } from '../telemetry/session-metrics.js';
@@ -220,7 +221,7 @@ export class CdpRealInputProvider {
220
221
  }
221
222
  const INJECT_CONNECT_WAIT_MS = 8_000;
222
223
  /** The only place the dynamic value import of Playwright lives for the launched (drive) path. */
223
- const launchedChromium = async (headless) => {
224
+ export const launchedChromium = async (headless) => {
224
225
  let mod;
225
226
  try {
226
227
  mod = await import('playwright');
@@ -230,7 +231,7 @@ const launchedChromium = async (headless) => {
230
231
  }
231
232
  const settle = getSessionMetrics().recordConnectAttempt(BrowserLaunchKind.LAUNCHED);
232
233
  try {
233
- const browser = await mod.chromium.launch({ headless });
234
+ const browser = await mod.chromium.launch(chromiumLaunchOptions(headless));
234
235
  settle();
235
236
  return browser;
236
237
  }
@@ -36,10 +36,13 @@ export interface ScrollFindResult {
36
36
  note?: string;
37
37
  }
38
38
  /**
39
- * Reveal an element that a windowed/virtualized list has not mounted yet. Queries
40
- * once (it may already be visible), then scrolls the container ~one viewport at a time, re-querying
41
- * after each, until the element appears, the list ends, or the maxScrolls budget is spent. Pure
42
- * orchestration over the session command seam — fully unit-testable with a fake.
39
+ * Reveal an element that a windowed/virtualized list has not mounted yet. Queries once (it may
40
+ * already be visible), optionally jumps straight to a bisection estimate, then walks the container
41
+ * a viewport at a time re-querying after each step. Downward first; when the bottom is reached the
42
+ * walk TURNS AROUND and continues upward, because a list that is already scrolled to its end keeps
43
+ * its earlier rows mounted above the viewport and a downward-only search answered "exhausted" on
44
+ * its first step (#505). `exhausted` therefore means BOTH directions are spent, not "the bottom is
45
+ * here". Pure orchestration over the session command seam — fully unit-testable with a fake.
43
46
  */
44
47
  export declare function scrollToFind(session: ScrollFindSession, q: ScrollFindQuery, opts?: {
45
48
  maxScrolls?: number;
@@ -13,61 +13,99 @@ async function queryFirst(session, q) {
13
13
  return undefined;
14
14
  }
15
15
  /**
16
- * A note attached to `{ found: false, exhausted: true }` only when the failure is
17
- * actually informative: the document itself had nothing to scroll (scrolled:false with
18
- * no explicit container), so the target is likely inside a list with its own scroll
19
- * container. Reaching the genuine end of a scrolled container stays quiet.
16
+ * A note attached to `{ found: false, exhausted: true }` only when the failure is actually
17
+ * informative: nothing in the container ever moved (so the "list" being searched was never
18
+ * scrollable and the target is likely inside one with its own scroll container). Reaching the
19
+ * genuine ends of a container that DID scroll stays quiet — that is exhaustion, not confusion.
20
20
  */
21
- function exhaustNote(q, data) {
21
+ function exhaustNote(q, data, nothingEverScrolled) {
22
22
  if (q.container !== undefined)
23
23
  return {};
24
- if (false !== data['scrolled'])
24
+ if (true === data['scrolled'] || !nothingEverScrolled)
25
25
  return {};
26
26
  return {
27
27
  note: "The document did not scroll — the target may be inside a list with its own scroll container. Pass that container's ref as `container`.",
28
28
  };
29
29
  }
30
+ /** One viewport-height step upward when the last result carried no clientHeight to size it by. */
31
+ const UP_STEP_FALLBACK_PX = 400;
30
32
  /**
31
- * Reveal an element that a windowed/virtualized list has not mounted yet. Queries
32
- * once (it may already be visible), then scrolls the container ~one viewport at a time, re-querying
33
- * after each, until the element appears, the list ends, or the maxScrolls budget is spent. Pure
34
- * orchestration over the session command seam — fully unit-testable with a fake.
33
+ * Size the upward step off the container the SCROLL result just described, falling back to a
34
+ * plain viewport height when the result did not carry one.
35
+ */
36
+ function upStepPx(last) {
37
+ const h = last['clientHeight'];
38
+ return 'number' === typeof h && h > 0 ? h : UP_STEP_FALLBACK_PX;
39
+ }
40
+ /**
41
+ * Reveal an element that a windowed/virtualized list has not mounted yet. Queries once (it may
42
+ * already be visible), optionally jumps straight to a bisection estimate, then walks the container
43
+ * a viewport at a time re-querying after each step. Downward first; when the bottom is reached the
44
+ * walk TURNS AROUND and continues upward, because a list that is already scrolled to its end keeps
45
+ * its earlier rows mounted above the viewport and a downward-only search answered "exhausted" on
46
+ * its first step (#505). `exhausted` therefore means BOTH directions are spent, not "the bottom is
47
+ * here". Pure orchestration over the session command seam — fully unit-testable with a fake.
35
48
  */
36
49
  export async function scrollToFind(session, q, opts = {}) {
37
50
  const max = opts.maxScrolls ?? SCROLL_FIND_DEFAULTS.MAX_SCROLLS;
38
51
  const first = await queryFirst(session, q);
39
52
  if (first !== undefined)
40
53
  return { found: true, element: first, scrolls: 0, exhausted: false };
41
- // Bisection: if the caller knows the target index and list size, jump to the estimated offset
42
- // in one scroll command rather than stepping a viewport at a time. Then refine linearly.
54
+ const baseArgs = () => q.container !== undefined ? { ref: q.container } : {};
43
55
  let scrolls = 0;
56
+ let last = {};
57
+ // Bisection: if the caller knows the target index and list size, jump to the estimated offset in
58
+ // one scroll command rather than stepping a viewport at a time. The estimate can land on either
59
+ // side of the target, so refinement below still searches both directions from wherever this put us.
44
60
  if (q.targetIndex !== undefined && q.totalCount !== undefined && q.totalCount > 1) {
45
61
  const fraction = Math.min(1, Math.max(0, q.targetIndex / q.totalCount));
46
- const sr = await session.command(ReticleCommand.SCROLL, {
47
- ...(q.container !== undefined ? { ref: q.container } : {}),
48
- fraction,
49
- });
62
+ const sr = await session.command(ReticleCommand.SCROLL, { ...baseArgs(), fraction });
50
63
  scrolls += 1;
64
+ last = asRecord(sr.result);
51
65
  const hit = await queryFirst(session, q);
52
66
  if (hit !== undefined)
53
67
  return { found: true, element: hit, scrolls, exhausted: false };
54
- // Fall through to linear refinement from current position (already near the target).
68
+ }
69
+ let downwardSpent = false;
70
+ let everScrolled = false;
71
+ // The downward pass. Spending its whole budget without reaching the end means rows may remain
72
+ // further down, so that answer is `exhausted:false` and the upward pass never runs.
73
+ for (let i = 0; i < max && !downwardSpent; i += 1) {
74
+ const sr = await session.command(ReticleCommand.SCROLL, baseArgs());
75
+ scrolls += 1;
55
76
  const data = asRecord(sr.result);
56
- if (true === data['atEnd'] || false === data['scrolled']) {
57
- return { found: false, scrolls, exhausted: true, ...exhaustNote(q, data) };
58
- }
77
+ last = data;
78
+ if (true === data['scrolled'])
79
+ everScrolled = true;
80
+ const hit = await queryFirst(session, q);
81
+ if (hit !== undefined)
82
+ return { found: true, element: hit, scrolls, exhausted: false };
83
+ // Reached the bottom, or the container would not move downward — turn around before giving up.
84
+ if (true === data['atEnd'] || true !== data['scrolled'])
85
+ downwardSpent = true;
86
+ }
87
+ // If the downward pass spent its budget it never saw the end, so there is nothing to turn around
88
+ // for; report honestly rather than doubling a call whose caller asked for `max` steps.
89
+ if (!downwardSpent) {
90
+ return { found: false, scrolls, exhausted: false };
59
91
  }
92
+ // The upward pass gets the caller's budget again, not what is left of it: a list at the bottom of
93
+ // 14k px needs ~90 scrolls to reach its end and ~90 more to get back, so sharing one budget would
94
+ // strand the search halfway and answer `exhausted:false` about a list both of whose ends are known.
60
95
  for (let i = 0; i < max; i += 1) {
61
- const sr = await session.command(ReticleCommand.SCROLL, q.container !== undefined ? { ref: q.container } : {});
96
+ const sr = await session.command(ReticleCommand.SCROLL, { ...baseArgs(), dy: -upStepPx(last) });
62
97
  scrolls += 1;
63
98
  const data = asRecord(sr.result);
99
+ last = data;
100
+ if (true === data['scrolled'])
101
+ everScrolled = true;
64
102
  const hit = await queryFirst(session, q);
65
103
  if (hit !== undefined)
66
104
  return { found: true, element: hit, scrolls, exhausted: false };
67
- // Reached the bottom or the container would not move — no more rows to reveal.
68
- if (true === data['atEnd'] || false === data['scrolled']) {
69
- return { found: false, scrolls, exhausted: true, ...exhaustNote(q, data) };
105
+ // The top refusing to move means both directions are spent.
106
+ if (true !== data['scrolled']) {
107
+ return { found: false, scrolls, exhausted: true, ...exhaustNote(q, data, !everScrolled) };
70
108
  }
71
109
  }
72
- return { found: false, scrolls, exhausted: false }; // spent the budget; more may lie further down
110
+ return { found: false, scrolls, exhausted: false }; // spent the upward budget; more may lie above
73
111
  }
@@ -0,0 +1,46 @@
1
+ import type { ToolDeps } from '../tools/tool-kit.js';
2
+ /**
3
+ * The ledger id a piece of inline prose is declared under.
4
+ *
5
+ * Derived from the statement rather than minted, so the same sentence declared on five verdicts is
6
+ * one row rather than five, and a DIFFERENT sentence is a different row rather than a false
7
+ * amendment of the first. The slug is there because the id is what an agent later types to reference
8
+ * the intent, and a bare digest is unreadable; the digest is there because the slug is truncated and
9
+ * two long statements sharing a prefix must not share a row.
10
+ */
11
+ export declare function inlineIntentId(statement: string): string;
12
+ /** What `provenBy` records: which tool drew the verdict, and when. */
13
+ export declare function inlineVerdictId(tool: string, at: number): string;
14
+ /**
15
+ * Put an inline intent in the ledger and attach the predicate about to be evaluated. Returns the id
16
+ * the verdict may later discharge, or undefined when there was no intent to record.
17
+ *
18
+ * `intent` carries EITHER prose OR the id of an intent already in the ledger. One field rather than
19
+ * two because the surface is the scarce thing here and the two cases are told apart by the ledger
20
+ * itself: a string that names an existing row is a reference, anything else is prose. A statement
21
+ * that happens to equal an existing id is the same intent said twice, so pointing at the row is the
22
+ * right answer there too, not a collision.
23
+ *
24
+ * A referenced intent is never re-declared, which would overwrite the agent's own words with an id,
25
+ * and its binding is left alone when it already has one — a predicate bound deliberately through
26
+ * `reticle_intent { action: "bind" }` is a stronger statement than whatever this one call asserts,
27
+ * and quietly replacing it is exactly the narrowing the ledger exists to make visible.
28
+ *
29
+ * Best-effort by construction: a ledger that cannot be written is a small problem, and a verdict
30
+ * that fails to return because of one is a large problem.
31
+ */
32
+ export declare function linkInlineIntent(deps: ToolDeps, sessionId: string | undefined, intent: string | undefined,
33
+ /** The predicate this verdict will evaluate, or undefined when it proves nothing (a bare settle). */
34
+ binding: unknown): Promise<string | undefined>;
35
+ /**
36
+ * Record that this verdict proved the intent. Call only for a verdict that actually proved something.
37
+ *
38
+ * The ledger's own rule does the rest: `dischargeIntent` refuses an intent with no binding, so an
39
+ * intent declared inline on a wait that asserted no consequence stays open and honest instead of
40
+ * collecting a proof nothing earned.
41
+ */
42
+ export declare function dischargeInlineIntent(deps: ToolDeps, sessionId: string | undefined, id: string | undefined, proof: {
43
+ verdictId: string;
44
+ grade: string;
45
+ at: number;
46
+ }): Promise<void>;