@reticlehq/server 2.8.0 → 2.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (292) hide show
  1. package/SKILL.md +130 -813
  2. package/dist/bridge/bridge.d.ts +23 -0
  3. package/dist/bridge/bridge.js +80 -2
  4. package/dist/bridge/bridge.test-harness.d.ts +5 -0
  5. package/dist/bridge/bridge.test-harness.js +6 -4
  6. package/dist/capsule/causal-summary.d.ts +16 -1
  7. package/dist/capsule/causal-summary.js +2 -1
  8. package/dist/cli/cli-doctor.js +41 -13
  9. package/dist/cli/cli-flow-commands.js +8 -8
  10. package/dist/cli/cli-launch.d.ts +0 -17
  11. package/dist/cli/cli-launch.js +0 -28
  12. package/dist/cli/cli-port.d.ts +24 -0
  13. package/dist/cli/cli-port.js +66 -35
  14. package/dist/cli/cli-verify.d.ts +21 -0
  15. package/dist/cli/cli-verify.js +45 -1
  16. package/dist/cli/config-discovery.d.ts +35 -0
  17. package/dist/cli/config-discovery.js +163 -0
  18. package/dist/cli/doctor-daemon-line.js +4 -1
  19. package/dist/cli/doctor-project-line.d.ts +37 -0
  20. package/dist/cli/doctor-project-line.js +35 -0
  21. package/dist/cli/doctor-rows.d.ts +51 -0
  22. package/dist/cli/doctor-rows.js +54 -0
  23. package/dist/cli/doctor-sessions-line.js +6 -3
  24. package/dist/cli/drive-attach.d.ts +99 -0
  25. package/dist/cli/drive-attach.js +203 -0
  26. package/dist/cli/drive-command.d.ts +13 -0
  27. package/dist/cli/drive-command.js +103 -0
  28. package/dist/cli/open-note.d.ts +20 -0
  29. package/dist/cli/open-note.js +37 -0
  30. package/dist/cli/status-next-action.d.ts +37 -0
  31. package/dist/cli/status-next-action.js +61 -0
  32. package/dist/cli.js +65 -70
  33. package/dist/events/contradictions.d.ts +32 -0
  34. package/dist/events/contradictions.js +9 -5
  35. package/dist/events/declared.d.ts +45 -0
  36. package/dist/events/declared.js +73 -0
  37. package/dist/events/echo-mismatch.d.ts +8 -2
  38. package/dist/events/echo-mismatch.js +33 -5
  39. package/dist/events/event-filters.d.ts +1 -1
  40. package/dist/events/event-filters.js +7 -4
  41. package/dist/events/predicate-eval.d.ts +17 -91
  42. package/dist/events/predicate-eval.js +85 -326
  43. package/dist/events/predicate-parse.js +31 -3
  44. package/dist/events/predicate-route.d.ts +32 -0
  45. package/dist/events/predicate-route.js +102 -0
  46. package/dist/events/predicate-schema.d.ts +99 -0
  47. package/dist/events/predicate-schema.js +309 -0
  48. package/dist/events/predicate.d.ts +8 -0
  49. package/dist/events/predicate.js +127 -5
  50. package/dist/flows/annotate-tools.js +3 -0
  51. package/dist/flows/annotate.js +18 -4
  52. package/dist/flows/coverage.d.ts +38 -5
  53. package/dist/flows/coverage.js +30 -4
  54. package/dist/flows/decision.d.ts +15 -0
  55. package/dist/flows/decision.js +1 -1
  56. package/dist/flows/flow-replay-run.js +8 -1
  57. package/dist/flows/flow-tools.js +9 -1
  58. package/dist/flows/replay.js +3 -0
  59. package/dist/honesty/accepted-write.d.ts +3 -0
  60. package/dist/honesty/accepted-write.js +8 -3
  61. package/dist/honesty/already-true.js +15 -0
  62. package/dist/honesty/blind-spots.js +4 -0
  63. package/dist/honesty/honesty.d.ts +11 -0
  64. package/dist/honesty/honesty.js +1 -0
  65. package/dist/honesty/uncaptured-bodies.d.ts +4 -2
  66. package/dist/honesty/uncaptured-bodies.js +9 -2
  67. package/dist/honesty/unread-outcome.d.ts +8 -0
  68. package/dist/honesty/unread-outcome.js +36 -9
  69. package/dist/honesty/unsettled.d.ts +9 -0
  70. package/dist/honesty/unsettled.js +7 -3
  71. package/dist/honesty/verified.d.ts +29 -3
  72. package/dist/honesty/verified.js +51 -8
  73. package/dist/http-server.d.ts +19 -0
  74. package/dist/http-server.js +202 -4
  75. package/dist/impact/impact-recorder.d.ts +23 -0
  76. package/dist/impact/impact-recorder.js +76 -0
  77. package/dist/impact/impact-store.d.ts +47 -0
  78. package/dist/impact/impact-store.js +171 -0
  79. package/dist/index.d.ts +7 -0
  80. package/dist/index.js +44 -10
  81. package/dist/init/agent-rules.d.ts +20 -0
  82. package/dist/init/agent-rules.js +36 -0
  83. package/dist/init/astro-patch.js +27 -1
  84. package/dist/init/confirm.d.ts +21 -3
  85. package/dist/init/confirm.js +32 -5
  86. package/dist/init/connect-steps.d.ts +2 -0
  87. package/dist/init/connect-steps.js +50 -0
  88. package/dist/init/csp-check.d.ts +38 -0
  89. package/dist/init/csp-check.js +92 -0
  90. package/dist/init/csp-doctor.d.ts +25 -0
  91. package/dist/init/csp-doctor.js +49 -0
  92. package/dist/init/cursor.js +6 -9
  93. package/dist/init/detect.d.ts +8 -0
  94. package/dist/init/detect.js +33 -3
  95. package/dist/init/mcp-clients.d.ts +1 -0
  96. package/dist/init/mcp-clients.js +51 -7
  97. package/dist/init/mcp.d.ts +15 -0
  98. package/dist/init/mcp.js +19 -0
  99. package/dist/init/plan-framework.d.ts +10 -0
  100. package/dist/init/plan-framework.js +25 -1
  101. package/dist/init/plan.d.ts +7 -2
  102. package/dist/init/plan.js +66 -30
  103. package/dist/init/run.js +11 -5
  104. package/dist/init/slash-command.d.ts +1 -1
  105. package/dist/init/slash-command.js +12 -4
  106. package/dist/init/snippets.d.ts +1 -1
  107. package/dist/init/snippets.js +35 -7
  108. package/dist/init/vite-config.js +24 -2
  109. package/dist/input/network-mock-tools.js +8 -1
  110. package/dist/input/scroll-find.d.ts +6 -0
  111. package/dist/input/scroll-find.js +17 -2
  112. package/dist/input/scroll-tools.js +4 -0
  113. package/dist/journal/attach-journal.d.ts +9 -1
  114. package/dist/journal/attach-journal.js +13 -2
  115. package/dist/journal/journal-query.d.ts +13 -0
  116. package/dist/journal/journal-query.js +28 -3
  117. package/dist/journal/workspace-gitignore.d.ts +10 -0
  118. package/dist/journal/workspace-gitignore.js +59 -0
  119. package/dist/mcp/mcp-proxy.d.ts +5 -66
  120. package/dist/mcp/mcp-proxy.js +180 -163
  121. package/dist/mcp/mcp.d.ts +10 -1
  122. package/dist/mcp/mcp.js +47 -27
  123. package/dist/mcp/proxy-handshake.d.ts +82 -1
  124. package/dist/mcp/proxy-handshake.js +109 -2
  125. package/dist/mcp/proxy-lifecycle.d.ts +10 -1
  126. package/dist/mcp/proxy-lifecycle.js +11 -2
  127. package/dist/mcp/proxy-log.d.ts +63 -0
  128. package/dist/mcp/proxy-log.js +132 -0
  129. package/dist/mcp/server-instructions.d.ts +17 -0
  130. package/dist/mcp/server-instructions.js +56 -0
  131. package/dist/mcp/shared-params.d.ts +27 -0
  132. package/dist/mcp/shared-params.js +36 -0
  133. package/dist/project/reticle-dir.d.ts +2 -0
  134. package/dist/project/reticle-dir.js +1 -0
  135. package/dist/session/connection-memory.d.ts +59 -0
  136. package/dist/session/connection-memory.js +108 -0
  137. package/dist/session/dev-command.d.ts +26 -0
  138. package/dist/session/dev-command.js +89 -0
  139. package/dist/session/dev-server-probe.d.ts +68 -0
  140. package/dist/session/dev-server-probe.js +102 -7
  141. package/dist/session/last-act.d.ts +26 -0
  142. package/dist/session/last-act.js +35 -1
  143. package/dist/session/live-control-tools.js +42 -5
  144. package/dist/session/no-session-diagnosis.d.ts +48 -2
  145. package/dist/session/no-session-diagnosis.js +176 -20
  146. package/dist/session/no-session-next-action.d.ts +46 -0
  147. package/dist/session/no-session-next-action.js +85 -0
  148. package/dist/session/no-session-watch.d.ts +40 -1
  149. package/dist/session/no-session-watch.js +165 -11
  150. package/dist/session/pending-commands.d.ts +12 -0
  151. package/dist/session/pending-commands.js +13 -1
  152. package/dist/session/session-manager.d.ts +4 -0
  153. package/dist/session/session-manager.js +35 -5
  154. package/dist/session/session.d.ts +10 -1
  155. package/dist/session/session.js +146 -5
  156. package/dist/telemetry/app-instrumented.d.ts +8 -0
  157. package/dist/telemetry/app-instrumented.js +10 -0
  158. package/dist/telemetry/bug-found.d.ts +18 -0
  159. package/dist/telemetry/bug-found.js +14 -0
  160. package/dist/telemetry/daemon-telemetry.js +24 -0
  161. package/dist/telemetry/install-source.d.ts +20 -0
  162. package/dist/telemetry/install-source.js +34 -0
  163. package/dist/telemetry/instrumentation-stall.d.ts +50 -0
  164. package/dist/telemetry/instrumentation-stall.js +90 -0
  165. package/dist/telemetry/session-metrics.d.ts +8 -0
  166. package/dist/telemetry/session-metrics.js +10 -0
  167. package/dist/telemetry/telemetry.d.ts +3 -1
  168. package/dist/telemetry/telemetry.js +13 -2
  169. package/dist/tools/act-tools.js +166 -29
  170. package/dist/tools/assert-verdict.js +32 -7
  171. package/dist/tools/browser-tools.js +10 -0
  172. package/dist/tools/dynamic-tools.js +16 -7
  173. package/dist/tools/error-recovery.js +9 -5
  174. package/dist/tools/invoke-tool.js +20 -3
  175. package/dist/tools/lease-hint.d.ts +30 -8
  176. package/dist/tools/lease-hint.js +65 -14
  177. package/dist/tools/lease-tools.d.ts +11 -0
  178. package/dist/tools/lease-tools.js +141 -96
  179. package/dist/tools/observe-tools.js +9 -3
  180. package/dist/tools/react-grace.d.ts +4 -0
  181. package/dist/tools/react-grace.js +6 -3
  182. package/dist/tools/read-tools.js +47 -1
  183. package/dist/tools/resolve-target.d.ts +33 -0
  184. package/dist/tools/resolve-target.js +53 -0
  185. package/dist/tools/settle-in-flight.d.ts +15 -0
  186. package/dist/tools/settle-in-flight.js +42 -2
  187. package/dist/tools/snapshot-delta.d.ts +3 -0
  188. package/dist/tools/snapshot-delta.js +67 -6
  189. package/dist/tools/tool-kit.d.ts +8 -0
  190. package/dist/tools/tool-surface.d.ts +60 -0
  191. package/dist/tools/tool-surface.js +85 -19
  192. package/dist/tools/tools.js +45 -4
  193. package/dist/tools/unadvertised-help.js +9 -6
  194. package/docs/README.md +1 -2
  195. package/docs/actions.mdx +41 -8
  196. package/docs/agent-cheatsheet.md +4 -2
  197. package/docs/architecture.md +6 -0
  198. package/docs/benchmarks.md +60 -12
  199. package/docs/best-practices.mdx +11 -6
  200. package/docs/capabilities.mdx +201 -54
  201. package/docs/cli/affected.mdx +6 -4
  202. package/docs/cli/capsules.mdx +9 -8
  203. package/docs/cli/cloud.mdx +30 -4
  204. package/docs/cli/doctor.mdx +41 -8
  205. package/docs/cli/drive.mdx +34 -15
  206. package/docs/cli/gate.mdx +6 -6
  207. package/docs/cli/help.mdx +8 -6
  208. package/docs/cli/hunt.mdx +18 -2
  209. package/docs/cli/identify.mdx +16 -2
  210. package/docs/cli/init.mdx +35 -8
  211. package/docs/cli/kill.mdx +5 -5
  212. package/docs/cli/license.mdx +1 -1
  213. package/docs/cli/open.mdx +6 -0
  214. package/docs/cli/restart.mdx +6 -6
  215. package/docs/cli/serve.mdx +10 -3
  216. package/docs/cli/status.mdx +29 -5
  217. package/docs/cli/stop.mdx +8 -0
  218. package/docs/cli/verify.mdx +20 -1
  219. package/docs/cli/version.mdx +1 -1
  220. package/docs/cli/watch.mdx +9 -2
  221. package/docs/cli.mdx +44 -22
  222. package/docs/debugging.md +3 -1
  223. package/docs/deploy-checks.md +30 -1
  224. package/docs/desktop.mdx +276 -80
  225. package/docs/docs.json +13 -12
  226. package/docs/enterprise.md +2 -0
  227. package/docs/faq.mdx +49 -35
  228. package/docs/fixtures.md +2 -0
  229. package/docs/flows.md +3 -1
  230. package/docs/for-agents.md +3 -3
  231. package/docs/frameworks.mdx +46 -7
  232. package/docs/gate-plan.md +4 -2
  233. package/docs/gates.md +7 -5
  234. package/docs/getting-started.md +53 -12
  235. package/docs/human-control.md +2 -0
  236. package/docs/index.mdx +43 -1
  237. package/docs/install-agentic.mdx +13 -4
  238. package/docs/install-manual.mdx +6 -4
  239. package/docs/instrumentation.mdx +7 -1
  240. package/docs/integration-patterns.md +3 -1
  241. package/docs/local-registry.md +4 -2
  242. package/docs/multi-agent-testing.md +3 -1
  243. package/docs/packages/babel-plugin.mdx +15 -6
  244. package/docs/packages/browser.mdx +2 -2
  245. package/docs/packages/core.mdx +26 -16
  246. package/docs/packages/electron.mdx +5 -7
  247. package/docs/packages/eslint-plugin.mdx +5 -3
  248. package/docs/packages/next.mdx +3 -5
  249. package/docs/packages/react.mdx +3 -3
  250. package/docs/packages/server.mdx +2 -2
  251. package/docs/packages/tauri.mdx +6 -6
  252. package/docs/packages/test.mdx +18 -7
  253. package/docs/packages/vite-plugin.mdx +4 -6
  254. package/docs/packages.mdx +14 -13
  255. package/docs/platform-integration.md +2 -0
  256. package/docs/predicates.mdx +76 -7
  257. package/docs/quickstart.mdx +51 -5
  258. package/docs/recipes.mdx +62 -22
  259. package/docs/skill-file.mdx +3 -1
  260. package/docs/state-management.mdx +2 -0
  261. package/docs/system-map.md +3 -1
  262. package/docs/telemetry-contract.md +16 -5
  263. package/docs/telemetry-events.mdx +63 -18
  264. package/docs/telemetry.md +8 -5
  265. package/docs/testing.md +35 -1
  266. package/docs/token-efficiency.md +8 -2
  267. package/docs/tools-act-and-wait.mdx +64 -14
  268. package/docs/tools-act-sequence.mdx +34 -17
  269. package/docs/tools-act.mdx +24 -11
  270. package/docs/tools-assert.mdx +69 -10
  271. package/docs/tools-console.mdx +11 -7
  272. package/docs/tools-inspect.mdx +11 -8
  273. package/docs/tools-navigate.mdx +41 -5
  274. package/docs/tools-network.mdx +42 -9
  275. package/docs/tools-observe.mdx +66 -10
  276. package/docs/tools-overview.mdx +26 -23
  277. package/docs/tools-query.mdx +52 -8
  278. package/docs/tools-session-and-feedback.mdx +30 -9
  279. package/docs/tools-sessions.mdx +39 -10
  280. package/docs/tools-snapshot.mdx +57 -3
  281. package/docs/tools-state.mdx +24 -1
  282. package/docs/tools-tools-and-run.mdx +40 -9
  283. package/docs/tools-wait-for.mdx +34 -15
  284. package/docs/troubleshooting.mdx +80 -10
  285. package/docs/usage.md +43 -21
  286. package/docs/vs-chrome-devtools-mcp.mdx +41 -0
  287. package/docs/vs-playwright-mcp.mdx +42 -2
  288. package/docs/vs-screenshots.mdx +42 -1
  289. package/docs/what-is-reticle.mdx +1 -1
  290. package/docs/why-reticle.mdx +3 -1
  291. package/package.json +5 -5
  292. package/docs/desktop-apps.md +0 -265
@@ -0,0 +1,47 @@
1
+ import { type ImpactCounts, type ImpactScope, type ImpactSnapshot } from '@reticlehq/core';
2
+ export interface ImpactPaths {
3
+ project: string;
4
+ global: string;
5
+ }
6
+ /**
7
+ * `reticleRoot` is the project's own `.reticle` directory (what every other store here is handed);
8
+ * the global scope always lives beside the daemon's own state in `~/.reticle`.
9
+ */
10
+ export declare function impactPaths(reticleRoot: string): ImpactPaths;
11
+ /** Read a scope, tolerating absence and corruption: a broken file starts over rather than throwing. */
12
+ export declare function readScope(path: string, now: number): ImpactScope;
13
+ /** YYYY-MM-DD in local time - the day boundary a person recognises, not UTC's. */
14
+ export declare function isoDay(now: number): string;
15
+ /**
16
+ * What a fold knows beyond the counters.
17
+ *
18
+ * `runMs` is a SESSION's lifetime, and it only ever feeds the "longest run" record. It is not a
19
+ * counter: adding it to a total would answer a question nobody asked, and the record used to be fed
20
+ * a single tool call's duration instead, which made the report's superlative a number of
21
+ * milliseconds that never grew past one click.
22
+ */
23
+ export interface ImpactFoldMeta {
24
+ runMs?: number;
25
+ }
26
+ /** Fold one delta into a scope: totals, today's bucket, records and streak. Pure. */
27
+ export declare function applyDelta(scope: ImpactScope, delta: Partial<ImpactCounts>, now: number, meta?: ImpactFoldMeta): ImpactScope;
28
+ /**
29
+ * The live impact record for one project.
30
+ *
31
+ * Holds both scopes in memory, folds deltas in synchronously (so a reader always sees the truth),
32
+ * and flushes to disk on a debounce.
33
+ */
34
+ export declare class ImpactStore {
35
+ #private;
36
+ constructor(opts: {
37
+ reticleRoot: string;
38
+ projectName?: string;
39
+ now?: () => number;
40
+ });
41
+ /** Notified after every fold, so the HUD can be pushed a fresh snapshot. */
42
+ onChange(fn: () => void): void;
43
+ record(delta: Partial<ImpactCounts>, meta?: ImpactFoldMeta): void;
44
+ snapshot(): ImpactSnapshot;
45
+ /** Write now (process exit, tests). */
46
+ flush(): void;
47
+ }
@@ -0,0 +1,171 @@
1
+ import { mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
2
+ import { dirname, join } from 'node:path';
3
+ import { homedir } from 'node:os';
4
+ import { ReticleDir, IMPACT_DAILY_BUCKETS, IMPACT_SCHEMA_VERSION, ImpactScopeSchema, addImpactCounts, emptyImpactCounts, emptyImpactRecords, estimateImpactSavings, } from '@reticlehq/core';
5
+ /**
6
+ * Where Reticle keeps the record of what it has done for you.
7
+ *
8
+ * Two scopes, two files, both local and never uploaded: the project's own `.reticle/impact.json`,
9
+ * and a machine-wide `~/.reticle/impact.json` that answers "what has Reticle done for me overall"
10
+ * across every app you have instrumented.
11
+ *
12
+ * This is NOT telemetry. Telemetry answers our questions about the product and leaves the machine;
13
+ * this answers the user's question about their own work and never does.
14
+ */
15
+ /** Writes are debounced: a verification loop is 50-200 calls, and each one is a counter bump. */
16
+ const WRITE_DEBOUNCE_MS = 800;
17
+ /**
18
+ * `reticleRoot` is the project's own `.reticle` directory (what every other store here is handed);
19
+ * the global scope always lives beside the daemon's own state in `~/.reticle`.
20
+ */
21
+ export function impactPaths(reticleRoot) {
22
+ return {
23
+ project: join(reticleRoot, ReticleDir.IMPACT_FILE),
24
+ global: join(homedir(), ReticleDir.ROOT, ReticleDir.IMPACT_FILE),
25
+ };
26
+ }
27
+ function emptyScope(now) {
28
+ const counts = emptyImpactCounts();
29
+ return {
30
+ counts,
31
+ days: [],
32
+ records: emptyImpactRecords(),
33
+ savings: estimateImpactSavings(counts),
34
+ since: now,
35
+ };
36
+ }
37
+ /** Read a scope, tolerating absence and corruption: a broken file starts over rather than throwing. */
38
+ export function readScope(path, now) {
39
+ try {
40
+ const parsed = JSON.parse(readFileSync(path, 'utf8'));
41
+ const result = ImpactScopeSchema.safeParse(parsed);
42
+ return result.success ? result.data : emptyScope(now);
43
+ }
44
+ catch {
45
+ return emptyScope(now);
46
+ }
47
+ }
48
+ /**
49
+ * Write a scope atomically.
50
+ *
51
+ * tmp + rename, because two daemons can serve two apps in the same repo at once and a half-written
52
+ * counters file is one the next read discards - silently losing the whole history.
53
+ */
54
+ function writeScope(path, scope) {
55
+ try {
56
+ mkdirSync(dirname(path), { recursive: true });
57
+ const tmp = `${path}.${String(process.pid)}.tmp`;
58
+ writeFileSync(tmp, JSON.stringify(scope, null, 2), 'utf8');
59
+ renameSync(tmp, path);
60
+ }
61
+ catch {
62
+ // A stats file that cannot be written must never break a tool call. The counters stay in
63
+ // memory and the next write attempt carries them.
64
+ }
65
+ }
66
+ /** YYYY-MM-DD in local time - the day boundary a person recognises, not UTC's. */
67
+ export function isoDay(now) {
68
+ const d = new Date(now);
69
+ const month = String(d.getMonth() + 1).padStart(2, '0');
70
+ const day = String(d.getDate()).padStart(2, '0');
71
+ return `${String(d.getFullYear())}-${month}-${day}`;
72
+ }
73
+ /** Whether `b` is the calendar day right after `a` - the streak rule. */
74
+ function isNextDay(a, b) {
75
+ const prev = new Date(`${a}T00:00:00`);
76
+ const next = new Date(`${b}T00:00:00`);
77
+ return 1 === Math.round((next.getTime() - prev.getTime()) / 86_400_000);
78
+ }
79
+ /** Fold one delta into a scope: totals, today's bucket, records and streak. Pure. */
80
+ export function applyDelta(scope, delta, now, meta = {}) {
81
+ const today = isoDay(now);
82
+ const counts = addImpactCounts(scope.counts, delta);
83
+ const days = scope.days.slice();
84
+ const last = days[days.length - 1];
85
+ if (last !== undefined && last.date === today) {
86
+ days[days.length - 1] = { date: today, counts: addImpactCounts(last.counts, delta) };
87
+ }
88
+ else {
89
+ days.push({ date: today, counts: addImpactCounts(emptyImpactCounts(), delta) });
90
+ }
91
+ while (days.length > IMPACT_DAILY_BUCKETS)
92
+ days.shift();
93
+ const todayCounts = days[days.length - 1]?.counts ?? emptyImpactCounts();
94
+ const previousDay = days[days.length - 2];
95
+ const records = { ...scope.records };
96
+ records.bestVerdictDay = Math.max(records.bestVerdictDay, todayCounts.verdicts);
97
+ records.bestDefectDay = Math.max(records.bestDefectDay, todayCounts.failed);
98
+ if (last === undefined || last.date !== today) {
99
+ // A new day joins the streak only if it follows yesterday; otherwise it starts a new one.
100
+ records.streakDays =
101
+ previousDay !== undefined && isNextDay(previousDay.date, today) ? records.streakDays + 1 : 1;
102
+ }
103
+ else if (0 === records.streakDays) {
104
+ records.streakDays = 1;
105
+ }
106
+ records.bestStreakDays = Math.max(records.bestStreakDays, records.streakDays);
107
+ records.longestRunMs = Math.max(records.longestRunMs, meta.runMs ?? 0);
108
+ return { counts, days, records, savings: estimateImpactSavings(counts), since: scope.since };
109
+ }
110
+ /**
111
+ * The live impact record for one project.
112
+ *
113
+ * Holds both scopes in memory, folds deltas in synchronously (so a reader always sees the truth),
114
+ * and flushes to disk on a debounce.
115
+ */
116
+ export class ImpactStore {
117
+ #paths;
118
+ #now;
119
+ #projectName;
120
+ #project;
121
+ #global;
122
+ #timer;
123
+ #onChange;
124
+ constructor(opts) {
125
+ this.#paths = impactPaths(opts.reticleRoot);
126
+ this.#now = opts.now ?? (() => Date.now());
127
+ this.#projectName = opts.projectName;
128
+ const now = this.#now();
129
+ this.#project = readScope(this.#paths.project, now);
130
+ this.#global = readScope(this.#paths.global, now);
131
+ }
132
+ /** Notified after every fold, so the HUD can be pushed a fresh snapshot. */
133
+ onChange(fn) {
134
+ this.#onChange = fn;
135
+ }
136
+ record(delta, meta = {}) {
137
+ const now = this.#now();
138
+ this.#project = applyDelta(this.#project, delta, now, meta);
139
+ this.#global = applyDelta(this.#global, delta, now, meta);
140
+ this.#scheduleFlush();
141
+ this.#onChange?.();
142
+ }
143
+ snapshot() {
144
+ const snap = {
145
+ schemaVersion: IMPACT_SCHEMA_VERSION,
146
+ project: this.#project,
147
+ global: this.#global,
148
+ };
149
+ if (this.#projectName !== undefined)
150
+ snap.projectName = this.#projectName;
151
+ return snap;
152
+ }
153
+ /** Write now (process exit, tests). */
154
+ flush() {
155
+ if (this.#timer !== undefined) {
156
+ clearTimeout(this.#timer);
157
+ this.#timer = undefined;
158
+ }
159
+ writeScope(this.#paths.project, this.#project);
160
+ writeScope(this.#paths.global, this.#global);
161
+ }
162
+ #scheduleFlush() {
163
+ if (this.#timer !== undefined)
164
+ return;
165
+ this.#timer = setTimeout(() => {
166
+ this.#timer = undefined;
167
+ this.flush();
168
+ }, WRITE_DEBOUNCE_MS);
169
+ this.#timer.unref?.();
170
+ }
171
+ }
package/dist/index.d.ts CHANGED
@@ -123,6 +123,13 @@ export interface RunningServer {
123
123
  agentAttached?: () => boolean;
124
124
  /** The pairing token the bridge is enforcing (explicit, env, or auto-provisioned); undefined if none. */
125
125
  token?: string;
126
+ /**
127
+ * Tell every attached MCP proxy this daemon is retiring, before anything closes.
128
+ *
129
+ * Optional because only the daemon entry point serves MCP over SSE — `start()` speaks stdio, where
130
+ * the client owns the process and there is no stream to warn. See SharedServer.announceShutdown.
131
+ */
132
+ announceShutdown?: () => void;
126
133
  close: () => Promise<void>;
127
134
  }
128
135
  export { resolveBridgeSecurity } from './bridge/bridge-security.js';
package/dist/index.js CHANGED
@@ -11,6 +11,7 @@ import { resolveBridgeSecurityWithAutoToken } from './bridge/bridge-security.js'
11
11
  import { Bridge } from './bridge/bridge.js';
12
12
  import { BaselineStore } from './project/baselines.js';
13
13
  import { RecordingStore } from './flows/recordings.js';
14
+ import { initImpact } from './impact/impact-recorder.js';
14
15
  import { FlowStore } from './flows/flows.js';
15
16
  import { buildFlowChips } from './flows/flow-scope.js';
16
17
  import { ProjectStore } from './project/project-store.js';
@@ -23,6 +24,8 @@ import { createRunnerPort } from './runs/runner-port.js';
23
24
  import { RunStore } from './runs/run-store.js';
24
25
  import { startVerifyServer } from './runs/verify-server.js';
25
26
  import { createMcpServer } from './mcp/mcp.js';
27
+ import { LEASE_ACQUIRE_TOOL } from './tools/lease-tools.js';
28
+ import { runTool } from './tools/invoke-tool.js';
26
29
  import { SessionReaper, endAllSessions, MCP_DISCONNECT_SUMMARY } from './session/session-reaper.js';
27
30
  import { wireSessionScope } from './session/no-session-watch.js';
28
31
  import { buildIdlePredicate } from './daemon/daemon-usefulness.js';
@@ -34,9 +37,13 @@ import { BrowserPool } from './pool/browser-pool.js';
34
37
  import { playwrightLauncher, resolveMaxContexts } from './pool/playwright-launcher.js';
35
38
  import { LeaseReaper } from './pool/lease-reaper.js';
36
39
  import { readJournalEnabled, readProjectId } from './cli/cli-port.js';
40
+ import { hasProjectConnectedBefore } from './session/connection-memory.js';
41
+ import { reticleStateHome } from './daemon/daemon.js';
42
+ import { probeChromium } from './cli/chromium-hint.js';
37
43
  import { makeJournalAttach } from './journal/attach-journal.js';
38
44
  import { makeSessionEnd } from './journal/session-end.js';
39
45
  import { AmbientStore } from './journal/ambient-store.js';
46
+ import { ensureWorkspaceGitignore } from './journal/workspace-gitignore.js';
40
47
  import { pruneSessions } from './journal/retention.js';
41
48
  import { log } from './log.js';
42
49
  /** A human-facing one-liner for a panel replay verdict — ✓ passed / ⚠ drifted / ✗ errored. */
@@ -172,8 +179,14 @@ function attachJournal(bridge, deps) {
172
179
  });
173
180
  // Teardown: flush the journal tail to disk + persist what this session learned.
174
181
  bridge.attachSessionEnd(makeSessionEnd(deps));
175
- if (deps.enabled)
182
+ if (deps.enabled) {
176
183
  void pruneSessions(deps.fs, deps.reticleRoot);
184
+ // Here rather than in `init`, because this is the moment we are actually about to write into
185
+ // somebody's repository — and the paths that reach it without ever running `init` (a plugin
186
+ // install, a hand-added client config) are exactly the ones that would otherwise leave an
187
+ // unexplained pile of untracked files behind. Best-effort and write-once; see the helper.
188
+ void ensureWorkspaceGitignore(deps.fs, deps.reticleRoot);
189
+ }
177
190
  }
178
191
  /**
179
192
  * Resolve the drive/real-input provider from options — shared by start and startDaemon so the
@@ -224,6 +237,10 @@ async function resolveRealInput(options, onNavigateError, onNetworkDetail) {
224
237
  /** Start the Reticle bridge (browser WS endpoint) and, by default, the MCP stdio server. */
225
238
  export async function start(options = {}) {
226
239
  const port = options.port ?? RETICLE_DEFAULT_PORT;
240
+ // Open the user's impact record before anything can connect. Not inside the MCP branch: a daemon
241
+ // serving a browser with no agent attached still has a HUD to answer, and a report that reads
242
+ // "nothing recorded yet" over a month of history on disk is the worst version of this feature.
243
+ initImpact({ reticleRoot: options.reticleRoot ?? join(process.cwd(), ReticleDir.ROOT) });
227
244
  const security = await resolveBridgeSecurityWithAutoToken(options);
228
245
  const bridge = new Bridge({ port, ...security });
229
246
  // Server-authoritative liveness: a Node-side reaper (immune to browser throttling) ends sessions
@@ -271,12 +288,13 @@ export async function start(options = {}) {
271
288
  reticleRoot,
272
289
  now,
273
290
  bridgePort: port,
291
+ browserProbe: probeChromium,
274
292
  // The daemon's OWN project, so a tool can tell "this session is mine" from "this session
275
293
  // belongs to a sibling app under the same daemon". contract_save refuses on the second.
276
294
  ...(activeProjectId === undefined ? {} : { projectId: activeProjectId }),
277
295
  };
278
296
  const profile = resolveToolSurface(options.toolProfile);
279
- const server = createMcpServer(realInput !== undefined ? { ...deps, realInput } : deps, profile);
297
+ const server = createMcpServer(realInput !== undefined ? { ...deps, realInput } : deps, profile, hasProjectConnectedBefore(reticleStateHome(), port, activeProjectId));
280
298
  // When the agent (the MCP client) disconnects cleanly, end every active session at once so the
281
299
  // HUD doesn't linger. (If the agent instead KILLS this process, the WS dies and the browser
282
300
  // self-ends via SESSION_LIFECYCLE.BRIDGE_LOST_MS — see transport.ts.)
@@ -308,6 +326,12 @@ export async function start(options = {}) {
308
326
  */
309
327
  export async function startDaemon(options = {}) {
310
328
  const port = options.port ?? RETICLE_DEFAULT_PORT;
329
+ // The SAME line as in `start`, because these are two entry points that each wire their own world
330
+ // and the daemon is the one that actually serves people. Wired only in `start`, the impact record
331
+ // was never opened in the process the HUD talks to: tool calls still recorded (the dispatch
332
+ // chokepoint opens it lazily), but a tab that connected before the first tool call was pushed
333
+ // nothing, so the report read "nothing recorded yet" over a file with history in it.
334
+ initImpact({ reticleRoot: options.reticleRoot ?? join(process.cwd(), ReticleDir.ROOT) });
311
335
  const security = await resolveBridgeSecurityWithAutoToken(options);
312
336
  const shared = createSharedServer(security.token === undefined ? {} : { token: security.token });
313
337
  const bridge = new Bridge({ port, server: shared.httpServer, ...security });
@@ -332,13 +356,6 @@ export async function startDaemon(options = {}) {
332
356
  });
333
357
  const reaper = new SessionReaper(bridge.sessions);
334
358
  reaper.start();
335
- // Scope auto-selection to the active project (from .reticle.json) so a stray tab from another app is
336
- // never picked when the agent omits a sessionId. Explicit per-call scope/sessionId still overrides.
337
- // Scope + the no-session diagnosis: "no browser session connected" is the error that ends most
338
- // sessions, and the agent is told to check two things it cannot see. See no-session-diagnosis.ts.
339
- // The pool reader is passed lazily: `pool` is assigned above but may be undefined when this
340
- // daemon runs without one, and the diagnosis must degrade to the tab message rather than lie.
341
- wireSessionScope(bridge.sessions, readProjectId(process.cwd()), port, () => pool?.reapedLeaseCount() ?? 0);
342
359
  const { realInput, owned } = await resolveRealInput(options, () => shared.close(), makeNetworkDetailRouter(bridge, options.driveUrl));
343
360
  const fs = createNodeFileSystem();
344
361
  const reticleRoot = options.reticleRoot ?? join(process.cwd(), ReticleDir.ROOT);
@@ -352,6 +369,14 @@ export async function startDaemon(options = {}) {
352
369
  const pool = createBrowserPool(options.headless ?? true);
353
370
  const leaseReaper = new LeaseReaper(pool);
354
371
  leaseReaper.start();
372
+ // Scope auto-selection to the active project (from .reticle.json) so a stray tab from another app is
373
+ // never picked when the agent omits a sessionId. Explicit per-call scope/sessionId still overrides.
374
+ // Scope + the no-session diagnosis: "no browser session connected" is the error that ends most
375
+ // sessions, and the agent is told to check two things it cannot see. See no-session-diagnosis.ts.
376
+ // Ordered AFTER the pool: the watch may open a browser itself the moment it finds a listening dev
377
+ // server for a wired project (see no-session-watch.ts), and it opens it through the pool — the same
378
+ // path reticle_lease takes, in this process, binding nothing.
379
+ wireSessionScope(bridge.sessions, readProjectId(process.cwd()), port, () => pool.reapedLeaseCount(), (url) => pool.acquire(url));
355
380
  const deps = {
356
381
  sessions: bridge.sessions,
357
382
  pool,
@@ -364,10 +389,18 @@ export async function startDaemon(options = {}) {
364
389
  reticleRoot,
365
390
  now,
366
391
  bridgePort: port,
392
+ browserProbe: probeChromium,
367
393
  };
368
394
  const profile = resolveToolSurface(options.toolProfile);
369
395
  const effectiveDeps = realInput !== undefined ? { ...deps, realInput } : deps;
370
- shared.attachMcp(() => createMcpServer(effectiveDeps, profile));
396
+ // Read per attach, not once: a project that gets wired while this daemon is alive should stop
397
+ // being told to wire itself on the next agent that connects.
398
+ shared.attachMcp(() => createMcpServer(effectiveDeps, profile, hasProjectConnectedBefore(reticleStateHome(), port, readProjectId(process.cwd()))));
399
+ // `reticle drive <url>` when this daemon already owns the port: it asks HERE instead of trying to
400
+ // bind a port we are holding, and gets the same pooled context an agent's reticle_lease returns —
401
+ // through runTool, so it is counted and reported like any other call rather than being a second,
402
+ // invisible dispatch path. See cli/drive-attach.ts for why attaching beats refereeing the race.
403
+ shared.attachDrive((url) => runTool(LEASE_ACQUIRE_TOOL, effectiveDeps, { url }));
371
404
  // Optional OEM/CI verify endpoint: a host platform POSTs to /verify and gets an ReticleVerificationRun,
372
405
  // driving the same flow-replay machinery the agent uses — no MCP stdio, no human. Each verdict is
373
406
  // persisted via RunStore. Localhost-bound + token-guarded. Off unless `reticle serve --http`.
@@ -454,6 +487,7 @@ export async function startDaemon(options = {}) {
454
487
  // Exposed so the shutdown watcher can give an ATTACHED daemon a longer grace. The predicate above
455
488
  // says WHETHER it is idle; this says how long that quiet should be tolerated — see idle-grace.
456
489
  agentAttached: () => agentConnected,
490
+ announceShutdown: () => shared.announceShutdown(),
457
491
  close: async () => {
458
492
  reaper.stop();
459
493
  await cleanupCaptureDirectories();
@@ -47,6 +47,26 @@ export declare const CURSOR_RULE_PATH = ".cursor/rules/reticle.mdc";
47
47
  * the package's own name so the two can never drift apart.
48
48
  */
49
49
  export declare const CLI: string;
50
+ /**
51
+ * Who starts the dev server, stated once and rendered everywhere it is needed.
52
+ *
53
+ * The old rule was "Reticle never starts the dev server for you — that's your job", and agents obeyed
54
+ * it exactly: they read it, declined to start anything, and ended the turn with nothing verified. An
55
+ * instruction that reliably produces no verification is a defect in the instruction.
56
+ *
57
+ * The AGENT starts it, not the daemon. A daemon that spawns a build process is invisible to the
58
+ * person whose machine it is running on, was never consented to, and orphans when the daemon exits.
59
+ * The agent already has shell access in this repo, already runs install and build commands, and runs
60
+ * inside a host that asks the human for permission — so a dev server it starts is visible in the
61
+ * transcript, attributable, and stoppable.
62
+ *
63
+ * The guards are the whole reason this is safe, so no renderer may drop them: adopt what is already
64
+ * listening, read the command rather than invent one, never kill, background it and SAY so, and leave
65
+ * the permission prompt to the host.
66
+ *
67
+ * Exported because slash-command.ts renders the same rule; there must not be two versions of it.
68
+ */
69
+ export declare const DEV_SERVER_POLICY = "**If no dev server is listening, start one yourself.** Read the project's own dev script out of `package.json` (`dev`, `start`, whatever this project calls it), run it in the BACKGROUND, tell the user in one line that it is running and how to stop it, then carry on. Stopping to ask is how a verification turn ends with nothing verified.\n\nFive guards, none optional:\n\n1. **Never start a second one.** If something is already listening on the app's port, use it.\n2. **Never guess the command.** It comes from `package.json` scripts. If there is no recognisable dev script, say so and stop rather than inventing one.\n3. **Never kill anything.** Not a dev server, not a daemon, not a port holder \u2014 including one you started.\n4. **Background it, and say so.** The user must know a server is running and how to stop it. A dev server the human does not know about is the same failure one step later.\n5. **The permission prompt belongs to your host.** Never try to bypass, suppress or auto-approve it, and take a refusal as the answer.";
50
70
  /** The full-rules file, written verbatim (no markers: `init` owns this file entirely). */
51
71
  export declare function reticleMdFile(): string;
52
72
  export declare const AgentRuleStatus: {
@@ -52,6 +52,34 @@ const RULE_END = '<!-- reticle:end -->';
52
52
  * the package's own name so the two can never drift apart.
53
53
  */
54
54
  export const CLI = `${NPX} ${RETICLE_NPM_PACKAGE}`;
55
+ /**
56
+ * Who starts the dev server, stated once and rendered everywhere it is needed.
57
+ *
58
+ * The old rule was "Reticle never starts the dev server for you — that's your job", and agents obeyed
59
+ * it exactly: they read it, declined to start anything, and ended the turn with nothing verified. An
60
+ * instruction that reliably produces no verification is a defect in the instruction.
61
+ *
62
+ * The AGENT starts it, not the daemon. A daemon that spawns a build process is invisible to the
63
+ * person whose machine it is running on, was never consented to, and orphans when the daemon exits.
64
+ * The agent already has shell access in this repo, already runs install and build commands, and runs
65
+ * inside a host that asks the human for permission — so a dev server it starts is visible in the
66
+ * transcript, attributable, and stoppable.
67
+ *
68
+ * The guards are the whole reason this is safe, so no renderer may drop them: adopt what is already
69
+ * listening, read the command rather than invent one, never kill, background it and SAY so, and leave
70
+ * the permission prompt to the host.
71
+ *
72
+ * Exported because slash-command.ts renders the same rule; there must not be two versions of it.
73
+ */
74
+ export const DEV_SERVER_POLICY = `**If no dev server is listening, start one yourself.** Read the project's own dev script out of \`package.json\` (\`dev\`, \`start\`, whatever this project calls it), run it in the BACKGROUND, tell the user in one line that it is running and how to stop it, then carry on. Stopping to ask is how a verification turn ends with nothing verified.
75
+
76
+ Five guards, none optional:
77
+
78
+ 1. **Never start a second one.** If something is already listening on the app's port, use it.
79
+ 2. **Never guess the command.** It comes from \`package.json\` scripts. If there is no recognisable dev script, say so and stop rather than inventing one.
80
+ 3. **Never kill anything.** Not a dev server, not a daemon, not a port holder — including one you started.
81
+ 4. **Background it, and say so.** The user must know a server is running and how to stop it. A dev server the human does not know about is the same failure one step later.
82
+ 5. **The permission prompt belongs to your host.** Never try to bypass, suppress or auto-approve it, and take a refusal as the answer.`;
55
83
  /**
56
84
  * The operative rules, carried by every file that loads on every turn.
57
85
  *
@@ -75,6 +103,12 @@ This app is instrumented by **Reticle**, an in-app verification layer exposed as
75
103
  - **Only \`reticle_act_and_wait\` and \`reticle_assert\` produce a verdict.** \`reticle_act\` and everything else move or read the app and prove nothing, so a session ending without one of those two has no result however many tools it used.
76
104
  - Covered flows: \`${CLI} gate\` reports which recorded flows the changed files affect and whether they still pass.
77
105
 
106
+ **Nothing connected? Get the app running.**
107
+
108
+ ${DEV_SERVER_POLICY}
109
+
110
+ A dev server that is already running does not pick up an edited build config or a newly created plugin file — restart it and hard-reload the tab. And if a server IS listening and still nothing connects, the cause is the SDK not loading in the page, not a missing dev server; do not tell the user to start one they are already running.
111
+
78
112
  **Honesty, which is the whole point:**
79
113
 
80
114
  - **\`verified: "unknown"\` is not a pass.** It means Reticle drove the app and could not tell what happened; \`verifiedReason\` says which clause decided that. Report it as unknown, never as working.
@@ -82,6 +116,8 @@ This app is instrumented by **Reticle**, an in-app verification layer exposed as
82
116
  - **If Reticle cannot run** (no daemon, or this is not a running web app), say so. Do not skip verification silently.
83
117
  - **Setup is not finished until one real flow has been driven and produced a verdict.** \`init\` exiting 0, the tools appearing, and a session being listed are all things that happen before anything has been verified.
84
118
 
119
+ **The \`/reticle\` skill runs this whole loop for you** — detect, connect, drive one flow, report. If your client does not have it, install it once: \`/plugin marketplace add reticlehq/reticle\` then \`/plugin install reticle@reticlehq\` in Claude Code, or \`npx skills add reticlehq/reticle\` anywhere the skills CLI works.
120
+
85
121
  **Report Reticle's own defects with \`reticle_feedback\` the moment you notice**, then carry on with your task. You are the user Reticle is built for and the only one who can say what it cost you, and that knowledge is gone when your context is.
86
122
 
87
123
  📄 **The rest is in [RETICLE.md](./${RETICLE_MD_PATH}): what to do when the tools are missing, when a result carries \`version_skew\` or \`update_available\`, when \`reticle_state\` comes back empty, and how to write a feedback report that can be acted on. Read it when you hit one of those, not before.**`;
@@ -11,7 +11,7 @@
11
11
  * Both patchers bail to `manual` (the printed recipe) on any shape they do not fully recognise.
12
12
  * Half-editing a build config is worse than a documented manual step.
13
13
  */
14
- import { RETICLE_DEFAULT_PORT, bridgeWsUrl } from '@reticlehq/core';
14
+ import { RETICLE_DEFAULT_PORT, ReticleDir, bridgeWsUrl } from '@reticlehq/core';
15
15
  import { PatchKind } from './patch-kind.js';
16
16
  import { UiLibrary } from './detect.js';
17
17
  import { sdkImport } from './snippets.js';
@@ -31,6 +31,26 @@ const LAYOUT_MARKER = 'reticle.connect';
31
31
  * never got it.
32
32
  */
33
33
  const SDK_INCLUDE_LITERAL = `'@reticlehq/react'`;
34
+ /**
35
+ * The daemon's journal directory, kept out of the dev server's watcher.
36
+ *
37
+ * The daemon writes `.reticle/` into the project root and rewrites `ambient.json` atomically
38
+ * (`.tmp` + rename) for as long as a session is live. Astro dev is a Vite dev server watching that
39
+ * root, and `.reticle/` is not in Vite's default ignore list — so every journal write read as a
40
+ * project file changing and Vite answered with a full page reload. That closes a loop with no exit:
41
+ * the page loads, the SDK connects and streams events, the daemon journals them, Vite reloads the
42
+ * page, the SDK reconnects. It ran several times a second for as long as the dev server was up,
43
+ * and the symptom looked nothing like the cause — stale refs, actions dying mid-flight, and a log
44
+ * full of connect/disconnect pairs that read as a flapping SDK.
45
+ *
46
+ * The Vite plugin sets the same ignore in its `config` hook. Astro is hand-patched and never loads
47
+ * that plugin, so it needs its own.
48
+ *
49
+ * A RegExp, not a glob. chokidar dropped glob support in v4 and Vite 7+ ships v4/v5, where a
50
+ * double-star pattern is accepted and matches nothing — the ignore would be visibly present in the
51
+ * config and do nothing at all. Measured against the chokidar this repo resolves.
52
+ */
53
+ const WATCH_IGNORE_LITERAL = `/(^|[\\\\/])\\.${ReticleDir.ROOT.slice(1)}([\\\\/]|$)/`;
34
54
  /**
35
55
  * An `include:` the app already declared — ours joins that array instead of adding a second key.
36
56
  * Deliberately not anchored to a newline: `optimizeDeps: { include: ['x'] }` on one line is the
@@ -72,12 +92,17 @@ const VITE_KEYS = [
72
92
  key: 'define',
73
93
  inner: `\n __RETICLE_TOKEN__: JSON.stringify(reticleToken()),\n __RETICLE_ROOT__: JSON.stringify(process.cwd()),`,
74
94
  },
95
+ // Merged in first, so a `server: { port }` the app already set keeps its port. An app that
96
+ // already sets `server.watch` itself is the one shape this loses to — the inner `watch` key would
97
+ // be duplicated and the app's would win — which is the same nested-merge ceiling `build` has.
98
+ { key: 'server', inner: `\n watch: { ignored: [${WATCH_IGNORE_LITERAL}] },` },
75
99
  ];
76
100
  /** Whole-key form, for a `vite:` block that does not have the key at all. */
77
101
  const WHOLE = {
78
102
  build: `\n build: { target: 'es2022' },`,
79
103
  optimizeDeps: `\n optimizeDeps: { include: [${SDK_INCLUDE_LITERAL}], esbuildOptions: { target: 'es2022' } },`,
80
104
  define: `\n define: {\n __RETICLE_TOKEN__: JSON.stringify(reticleToken()),\n __RETICLE_ROOT__: JSON.stringify(process.cwd()),\n },`,
105
+ server: `\n server: { watch: { ignored: [${WATCH_IGNORE_LITERAL}] } },`,
81
106
  };
82
107
  /**
83
108
  * Add our keys to an existing `vite: { ... }` block, merging into any the app already set.
@@ -157,6 +182,7 @@ const VITE_BLOCK = ` vite: {
157
182
  __RETICLE_TOKEN__: JSON.stringify(reticleToken()),
158
183
  __RETICLE_ROOT__: JSON.stringify(process.cwd()),
159
184
  },
185
+ server: { watch: { ignored: [${WATCH_IGNORE_LITERAL}] } },
160
186
  },
161
187
  `;
162
188
  export function patchAstroConfig(source) {
@@ -19,9 +19,11 @@
19
19
  * keeps today's cost and today's exit behaviour, and blocking never happens where blocking is
20
20
  * wrong. The signal is the standard "am I being piped" one and needs no argument parsing, which
21
21
  * also keeps `init`'s documented flag surface where it is.
22
- * - It never starts anything. Reticle attaches to what is already running and does not manage a
23
- * dev server; that rule holds here too. So the honest shape is: confirm the app if it connects,
24
- * otherwise name the ONE command that is outstanding and the one that proves it worked.
22
+ * - It never starts anything. A dev server started by a long-lived daemon is invisible to the
23
+ * person whose machine it runs on and orphans when the daemon exits, so the daemon stays out of
24
+ * it — the AGENT starts one, under the guards in agent-rules.ts, because it is attributable and
25
+ * stoppable there. So the honest shape here is: confirm the app if it connects, otherwise name
26
+ * the ONE command that is outstanding and the one that proves it worked.
25
27
  */
26
28
  import { InitConfirmation } from '@reticlehq/core';
27
29
  import type { InitOutcome } from '@reticlehq/core';
@@ -76,6 +78,22 @@ export declare function confirmAppConnected(deps: ConfirmDeps): Promise<InitConf
76
78
  * up when the agent loads the tools — so it is a notice.
77
79
  */
78
80
  export declare function confirmationMessage(confirmation: InitConfirmation, port: number): string;
81
+ /**
82
+ * What a non-TTY run says instead of waiting.
83
+ *
84
+ * `init` deliberately does not block when nobody is at the terminal — a 12-second wait in a script
85
+ * is wrong, and an agent driving `init` through a shell is never a TTY. But silence was the wrong
86
+ * other half of that decision: the agent path IS the prescribed path (every skill and every README
87
+ * block tells an agent to run exactly this command), so the one message joining "init finished" to
88
+ * "an app connected" was withheld from most of the people who run it. They were left with files on
89
+ * disk, an exit code of 0, and nothing saying the page half had not happened.
90
+ *
91
+ * It claims nothing it did not check. It did not look for a session, so it does not say whether one
92
+ * exists — a one-shot look would be worse than none, because a daemon is shared across every project
93
+ * on the machine and "a session exists" is satisfied by somebody else's app, which is the false
94
+ * green `confirmAppConnected` refuses by design.
95
+ */
96
+ export declare function unwatchedMessage(port: number): string;
79
97
  /**
80
98
  * Finish `init` on evidence rather than on a write.
81
99
  *
@@ -19,9 +19,11 @@
19
19
  * keeps today's cost and today's exit behaviour, and blocking never happens where blocking is
20
20
  * wrong. The signal is the standard "am I being piped" one and needs no argument parsing, which
21
21
  * also keeps `init`'s documented flag surface where it is.
22
- * - It never starts anything. Reticle attaches to what is already running and does not manage a
23
- * dev server; that rule holds here too. So the honest shape is: confirm the app if it connects,
24
- * otherwise name the ONE command that is outstanding and the one that proves it worked.
22
+ * - It never starts anything. A dev server started by a long-lived daemon is invisible to the
23
+ * person whose machine it runs on and orphans when the daemon exits, so the daemon stays out of
24
+ * it — the AGENT starts one, under the guards in agent-rules.ts, because it is attributable and
25
+ * stoppable there. So the honest shape here is: confirm the app if it connects, otherwise name
26
+ * the ONE command that is outstanding and the one that proves it worked.
25
27
  */
26
28
  import { InitConfirmation, RETICLE_DEFAULT_PORT } from '@reticlehq/core';
27
29
  import { fetchStatus, summarizeStatus } from '../cli/cli-launch.js';
@@ -89,10 +91,32 @@ export function confirmationMessage(confirmation, port) {
89
91
  }
90
92
  return (' [⚠] no app has connected yet — the files are written, the page half of the install is not ' +
91
93
  'done.\n' +
92
- ' Restart your dev server (Reticle attaches to it, it never starts it for you) and load ' +
93
- 'the app in a browser.\n' +
94
+ ' Restart your dev server and load the app in a browser — or ask your agent to, which it is ' +
95
+ 'now told to do for you, in the background and with the command from your own scripts.\n' +
94
96
  ` ${PROVE_COMMAND} then confirms it, or says exactly why it has not connected.`);
95
97
  }
98
+ /**
99
+ * What a non-TTY run says instead of waiting.
100
+ *
101
+ * `init` deliberately does not block when nobody is at the terminal — a 12-second wait in a script
102
+ * is wrong, and an agent driving `init` through a shell is never a TTY. But silence was the wrong
103
+ * other half of that decision: the agent path IS the prescribed path (every skill and every README
104
+ * block tells an agent to run exactly this command), so the one message joining "init finished" to
105
+ * "an app connected" was withheld from most of the people who run it. They were left with files on
106
+ * disk, an exit code of 0, and nothing saying the page half had not happened.
107
+ *
108
+ * It claims nothing it did not check. It did not look for a session, so it does not say whether one
109
+ * exists — a one-shot look would be worse than none, because a daemon is shared across every project
110
+ * on the machine and "a session exists" is satisfied by somebody else's app, which is the false
111
+ * green `confirmAppConnected` refuses by design.
112
+ */
113
+ export function unwatchedMessage(port) {
114
+ return (' [ℹ] the files are written. An app CONNECTING is what finishes the install, and this run did ' +
115
+ 'not wait to see one.\n' +
116
+ ' Start the dev server, load the app in a browser, then ' +
117
+ `${PROVE_COMMAND} — it confirms the app, or says exactly why it has not connected.\n` +
118
+ ` Bridge port ${String(port)}.`);
119
+ }
96
120
  const WATCHING = (windowMs) => ` watching for an app to connect (up to ${String(Math.round(windowMs / 1000))}s) — Ctrl-C is safe, ` +
97
121
  'everything above is already written.';
98
122
  /**
@@ -110,6 +134,9 @@ export async function confirmInstall(result, io, deps, report = reportInitOutcom
110
134
  if (outcome === undefined)
111
135
  return;
112
136
  if (!deps.interactive) {
137
+ // Still no waiting — see unwatchedMessage for why it speaks anyway.
138
+ io.print('');
139
+ io.print(unwatchedMessage(deps.port));
113
140
  report(outcome);
114
141
  return;
115
142
  }
@@ -0,0 +1,2 @@
1
+ /** True when this step is what makes the app dial the daemon. */
2
+ export declare function isConnectStep(title: string): boolean;