@reticlehq/server 3.1.0 → 3.2.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 (228) hide show
  1. package/SKILL.md +17 -18
  2. package/dist/command/cli/auth/device-flow.d.ts +2 -2
  3. package/dist/command/cli/cli-doctor.js +13 -1
  4. package/dist/command/cli/cli-flow-commands.d.ts +2 -1
  5. package/dist/command/cli/cli-kill.js +1 -1
  6. package/dist/command/cli/cli-parse.d.ts +2 -1
  7. package/dist/command/cli/cli-parse.js +5 -2
  8. package/dist/command/cli/cli-verify.js +2 -2
  9. package/dist/command/cli/cloud-cli.js +7 -6
  10. package/dist/command/cli/cloud-kit.js +2 -6
  11. package/dist/command/cli/config/config-discovery.d.ts +12 -0
  12. package/dist/command/cli/config/config-discovery.js +19 -0
  13. package/dist/command/cli/doctor/doctor-rows.d.ts +12 -0
  14. package/dist/command/cli/doctor/doctor-rows.js +12 -0
  15. package/dist/command/cli/drive/drive-command.js +1 -1
  16. package/dist/command/cli/launch/cli-launch.d.ts +10 -2
  17. package/dist/command/cli/launch/cli-launch.js +32 -39
  18. package/dist/command/cli/lifecycle/daemon-lifecycle.d.ts +33 -0
  19. package/dist/command/cli/lifecycle/daemon-lifecycle.js +198 -0
  20. package/dist/command/cli/mcp-command.js +5 -3
  21. package/dist/command/cli/ports/resolve/cli-port.d.ts +2 -1
  22. package/dist/command/cli/ports/resolve/cli-port.js +5 -1
  23. package/dist/command/cli/status/status-lines.d.ts +28 -0
  24. package/dist/command/cli/status/status-lines.js +117 -0
  25. package/dist/command/cli.d.ts +1 -1
  26. package/dist/command/cli.js +24 -177
  27. package/dist/command/daemon/binding/daemon-status-probe.d.ts +2 -0
  28. package/dist/command/daemon/binding/daemon-status-probe.js +48 -0
  29. package/dist/command/daemon/binding/loopback-reach.d.ts +42 -0
  30. package/dist/command/daemon/binding/loopback-reach.js +63 -0
  31. package/dist/command/daemon/daemon-resilience.d.ts +13 -1
  32. package/dist/command/daemon/daemon-resilience.js +12 -1
  33. package/dist/command/daemon/daemon.d.ts +2 -11
  34. package/dist/command/daemon/daemon.js +8 -11
  35. package/dist/command/daemon/lifetime/heartbeat.d.ts +8 -0
  36. package/dist/command/daemon/lifetime/heartbeat.js +10 -0
  37. package/dist/command/daemon/lifetime/log-cap.d.ts +68 -0
  38. package/dist/command/daemon/lifetime/log-cap.js +115 -0
  39. package/dist/command/setup/agent-notice-memory.d.ts +16 -0
  40. package/dist/command/setup/agent-notice-memory.js +65 -0
  41. package/dist/command/setup/bringup/dev-server-wait.d.ts +1 -0
  42. package/dist/command/setup/bringup/dev-server-wait.js +16 -0
  43. package/dist/command/setup/bringup/ensure-daemon.js +1 -1
  44. package/dist/command/setup/init/init-runtime.js +13 -1
  45. package/dist/command/setup/node-effects.js +1 -1
  46. package/dist/command/setup/probe/page-probe.js +9 -2
  47. package/dist/command/setup/run-setup.d.ts +25 -7
  48. package/dist/command/setup/run-setup.js +51 -9
  49. package/dist/command/setup/setup-command.js +31 -3
  50. package/dist/command/setup/terminal/confirm.js +2 -1
  51. package/dist/command/update/updater.d.ts +27 -0
  52. package/dist/command/update/updater.js +35 -2
  53. package/dist/features/crawl/crawl-tools.js +3 -1
  54. package/dist/features/crawl/crawl.js +2 -1
  55. package/dist/features/harness/drive-report.d.ts +73 -0
  56. package/dist/features/harness/drive-report.js +196 -0
  57. package/dist/features/harness/driver.js +29 -8
  58. package/dist/features/harness/drivers.d.ts +19 -0
  59. package/dist/features/harness/drivers.js +19 -0
  60. package/dist/features/harness/fill-values.d.ts +67 -0
  61. package/dist/features/harness/fill-values.js +153 -0
  62. package/dist/features/harness/jev-driver.d.ts +188 -0
  63. package/dist/features/harness/jev-driver.js +664 -0
  64. package/dist/features/harness/openai-driver.d.ts +46 -0
  65. package/dist/features/harness/openai-driver.js +197 -0
  66. package/dist/features/harness/platform-config.d.ts +52 -0
  67. package/dist/features/harness/platform-config.js +89 -0
  68. package/dist/features/visual/visual-tools.js +26 -2
  69. package/dist/index.js +60 -55
  70. package/dist/judgement/domain/domain-tools.js +4 -2
  71. package/dist/judgement/runs/verification-sync.d.ts +2 -2
  72. package/dist/judgement/runs/verification-sync.js +9 -2
  73. package/dist/language/flows/affected-tools.js +6 -3
  74. package/dist/language/flows/flow-replay-run.d.ts +25 -2
  75. package/dist/language/flows/flow-replay-run.js +73 -27
  76. package/dist/language/flows/flow-replay.d.ts +17 -0
  77. package/dist/language/flows/flow-replay.js +148 -100
  78. package/dist/language/flows/flow-result.d.ts +18 -1
  79. package/dist/language/flows/flow-result.js +18 -2
  80. package/dist/language/flows/flow-store-for-session.d.ts +2 -1
  81. package/dist/language/flows/flow-tools.js +8 -4
  82. package/dist/language/flows/flows.d.ts +8 -7
  83. package/dist/language/flows/flows.js +7 -3
  84. package/dist/language/flows/narrow-write.d.ts +4 -4
  85. package/dist/language/flows/replay.js +1 -1
  86. package/dist/language/flows/suite-selection.d.ts +15 -1
  87. package/dist/language/flows/suite-selection.js +1 -1
  88. package/dist/language/flows/verify-change-tools.js +7 -4
  89. package/dist/memory/cloud/account-state.d.ts +1 -1
  90. package/dist/memory/cloud/account-state.js +5 -4
  91. package/dist/memory/cloud/cloud-config.d.ts +9 -0
  92. package/dist/memory/cloud/cloud-config.js +9 -0
  93. package/dist/memory/cloud/cloud-sync.d.ts +7 -2
  94. package/dist/memory/cloud/cloud-sync.js +10 -5
  95. package/dist/memory/cloud/harness-config.d.ts +10 -0
  96. package/dist/memory/cloud/harness-config.js +58 -0
  97. package/dist/memory/cloud/harness-offer.d.ts +44 -0
  98. package/dist/memory/cloud/harness-offer.js +146 -0
  99. package/dist/memory/cloud/harness-switch.d.ts +11 -0
  100. package/dist/memory/cloud/harness-switch.js +53 -0
  101. package/dist/memory/cloud/sync-daemon.d.ts +22 -2
  102. package/dist/memory/cloud/sync-daemon.js +24 -0
  103. package/dist/memory/impact/impact-recorder.d.ts +8 -0
  104. package/dist/memory/impact/impact-recorder.js +2 -1
  105. package/dist/memory/impact/impact-store.d.ts +5 -0
  106. package/dist/memory/impact/impact-store.js +19 -0
  107. package/dist/memory/intent/intent-shard-store.js +2 -2
  108. package/dist/memory/intent/intent-shard.d.ts +12 -12
  109. package/dist/memory/journal/attach-journal.d.ts +11 -0
  110. package/dist/memory/journal/attach-journal.js +1 -1
  111. package/dist/memory/journal/journal-query.d.ts +35 -1
  112. package/dist/memory/journal/journal-query.js +46 -0
  113. package/dist/memory/journal/journal-recorder.d.ts +50 -2
  114. package/dist/memory/journal/journal-recorder.js +45 -7
  115. package/dist/memory/journal/on-disk/retention.d.ts +87 -2
  116. package/dist/memory/journal/on-disk/retention.js +177 -8
  117. package/dist/memory/journal/on-disk/workspace-gitignore.d.ts +9 -0
  118. package/dist/memory/journal/on-disk/workspace-gitignore.js +9 -2
  119. package/dist/memory/journal/self-observation.d.ts +3 -0
  120. package/dist/memory/journal/self-observation.js +67 -0
  121. package/dist/memory/journal/session-end.d.ts +13 -3
  122. package/dist/memory/journal/session-end.js +24 -4
  123. package/dist/memory/journal/session-journal.d.ts +115 -5
  124. package/dist/memory/journal/session-journal.js +479 -32
  125. package/dist/memory/project/artifact-root-resolver.d.ts +3 -0
  126. package/dist/memory/project/artifact-root-resolver.js +89 -0
  127. package/dist/memory/project/artifact-root.d.ts +39 -2
  128. package/dist/memory/project/artifact-root.js +58 -1
  129. package/dist/memory/project/dir/fill-value-store.d.ts +32 -0
  130. package/dist/memory/project/dir/fill-value-store.js +88 -0
  131. package/dist/memory/project/dir/reticle-dir.d.ts +14 -4
  132. package/dist/memory/project/dir/reticle-dir.js +11 -0
  133. package/dist/memory/project/fs/fs-port.d.ts +22 -1
  134. package/dist/memory/project/fs/fs-port.js +33 -5
  135. package/dist/memory/project/learned-routes.d.ts +16 -2
  136. package/dist/memory/project/learned-routes.js +26 -10
  137. package/dist/memory/project/project-for-root.d.ts +30 -0
  138. package/dist/memory/project/project-for-root.js +43 -0
  139. package/dist/memory/project/project-tools.js +10 -5
  140. package/dist/memory/project/session-root.d.ts +35 -6
  141. package/dist/memory/project/session-root.js +70 -8
  142. package/dist/portal/bridge/auth-failure-reason.d.ts +9 -0
  143. package/dist/portal/bridge/auth-failure-reason.js +15 -1
  144. package/dist/portal/bridge/bridge.d.ts +6 -1
  145. package/dist/portal/bridge/bridge.js +36 -2
  146. package/dist/portal/session/facts/session-replaced.d.ts +19 -0
  147. package/dist/portal/session/facts/session-replaced.js +22 -0
  148. package/dist/portal/session/no-session-diagnosis.d.ts +1 -0
  149. package/dist/portal/session/no-session-diagnosis.js +67 -60
  150. package/dist/portal/session/no-session-next-action.js +3 -2
  151. package/dist/portal/session/no-session-watch.js +14 -10
  152. package/dist/portal/session/rebind.d.ts +18 -0
  153. package/dist/portal/session/rebind.js +38 -0
  154. package/dist/portal/session/session-manager.d.ts +4 -0
  155. package/dist/portal/session/session-manager.js +19 -1
  156. package/dist/portal/session/session.d.ts +21 -10
  157. package/dist/portal/session/session.js +29 -42
  158. package/dist/status-payload.d.ts +12 -1
  159. package/dist/status-payload.js +4 -1
  160. package/dist/surface/http-server.js +3 -5
  161. package/dist/surface/mcp/faults/mcp-outage.d.ts +2 -1
  162. package/dist/surface/mcp/faults/mcp-outage.js +20 -4
  163. package/dist/surface/mcp/mcp-proxy.d.ts +10 -1
  164. package/dist/surface/mcp/mcp-proxy.js +20 -9
  165. package/dist/surface/mcp/mcp.d.ts +10 -1
  166. package/dist/surface/mcp/mcp.js +59 -29
  167. package/dist/surface/mcp/proxy/proxy-daemon-probe.d.ts +15 -2
  168. package/dist/surface/mcp/proxy/proxy-daemon-probe.js +22 -5
  169. package/dist/surface/mcp/proxy/proxy-log.d.ts +5 -11
  170. package/dist/surface/mcp/proxy/proxy-log.js +7 -22
  171. package/dist/surface/tools/act/act-preflight.js +6 -1
  172. package/dist/surface/tools/act/sequence-grade.js +13 -0
  173. package/dist/surface/tools/act-capsule.js +27 -1
  174. package/dist/surface/tools/assert/assert-verdict.js +44 -17
  175. package/dist/surface/tools/coverage-tools.js +5 -2
  176. package/dist/surface/tools/dynamic-tools.js +37 -46
  177. package/dist/surface/tools/error-recovery.d.ts +6 -0
  178. package/dist/surface/tools/error-recovery.js +15 -0
  179. package/dist/surface/tools/explore-tools.js +80 -7
  180. package/dist/surface/tools/feedback-tools.js +49 -2
  181. package/dist/surface/tools/first-sentence.d.ts +24 -0
  182. package/dist/surface/tools/first-sentence.js +0 -0
  183. package/dist/surface/tools/harness-explore.d.ts +125 -1
  184. package/dist/surface/tools/harness-explore.js +401 -15
  185. package/dist/surface/tools/harness-plan.d.ts +76 -0
  186. package/dist/surface/tools/harness-plan.js +156 -0
  187. package/dist/surface/tools/harness-toolset.js +12 -1
  188. package/dist/surface/tools/invoke-tool.js +65 -5
  189. package/dist/surface/tools/live-call-text.d.ts +6 -0
  190. package/dist/surface/tools/live-call-text.js +36 -2
  191. package/dist/surface/tools/observe-tools.js +55 -9
  192. package/dist/surface/tools/read/query-windowed.d.ts +1 -1
  193. package/dist/surface/tools/read/query-windowed.js +1 -1
  194. package/dist/surface/tools/read-tools.js +1 -2
  195. package/dist/surface/tools/tool-kit.d.ts +18 -3
  196. package/dist/surface/tools/tools-helpers.d.ts +8 -7
  197. package/dist/surface/tools/tools-helpers.js +11 -13
  198. package/dist/surface/tools/tools.js +6 -1
  199. package/dist/telemetry/bug-found.js +32 -2
  200. package/dist/telemetry/feedback-context.js +3 -3
  201. package/dist/telemetry/harness-drive.d.ts +6 -0
  202. package/dist/telemetry/harness-drive.js +39 -0
  203. package/dist/telemetry/mcp-connection.d.ts +14 -1
  204. package/dist/telemetry/mcp-connection.js +17 -2
  205. package/dist/telemetry/onboarding-firsts.d.ts +30 -1
  206. package/dist/telemetry/onboarding-firsts.js +54 -12
  207. package/dist/telemetry/verification-of.js +31 -3
  208. package/docs/agent-cheatsheet.md +7 -3
  209. package/docs/autodrive.md +42 -2
  210. package/docs/benchmarks.md +1 -1
  211. package/docs/capabilities.mdx +1 -1
  212. package/docs/cli/cloud.mdx +3 -3
  213. package/docs/cli/doctor.mdx +1 -0
  214. package/docs/cli/status.mdx +35 -20
  215. package/docs/cli.mdx +8 -18
  216. package/docs/desktop.mdx +1 -1
  217. package/docs/faq.mdx +1 -1
  218. package/docs/install-manual.mdx +1 -1
  219. package/docs/packages/tauri.mdx +1 -1
  220. package/docs/packages.mdx +1 -1
  221. package/docs/predicates.mdx +16 -0
  222. package/docs/system-map.md +6 -4
  223. package/docs/telemetry.md +2 -1
  224. package/docs/token-efficiency.md +2 -2
  225. package/docs/tools/overview.mdx +1 -1
  226. package/docs/troubleshooting.mdx +8 -2
  227. package/docs/usage.md +1 -1
  228. package/package.json +6 -6
@@ -1,4 +1,5 @@
1
1
  import { FlowErrorCode } from '@reticlehq/core';
2
+ import { type ProjectId } from '@reticlehq/core';
2
3
  /** Discriminated result so callers never branch on free strings. */
3
4
  export type FlowResult<T> = {
4
5
  ok: true;
@@ -14,5 +15,21 @@ export type FlowResult<T> = {
14
15
  * An unsafe or absent value collapses to `undefined` — the flat, global store — so a malformed id
15
16
  * can never escape `.reticle/flows/`. The store defends the disk boundary itself rather than
16
17
  * trusting whatever arrived in a session's HELLO.
18
+ *
19
+ * Held to one SEGMENT, not to a flow name. A project id becomes a DIRECTORY; a flow name is an
20
+ * ADDRESS, and since composites began naming sub-journeys as `onboarding/signup` its pattern has
21
+ * allowed separators. Validating a directory name with the address guard let a page choose how deep
22
+ * under `.reticle/` Reticle wrote — not an escape, since neither pattern admits `..` or a leading
23
+ * separator, but the page naming directories all the same.
24
+ *
25
+ * A FILTER, not a mint. It answers "is this id safe on disk", never "is this id a project" — a
26
+ * session id passes `SAFE_SEGMENT_PATTERN` just as cleanly, so minting the brand here would launder
27
+ * the exact confusion `ProjectId` exists to catch. The brand arrives already attached, from HELLO
28
+ * or from `.reticle.json`; see `ProjectId` in core.
29
+ *
30
+ * The narrowing said it out loud and nobody was listening: `isValidFlowName` narrows its argument to
31
+ * `FlowName`, for a value that is a project. The sibling guards for session and run ids have used
32
+ * the segment pattern since that widening, and `id-guards-are-not-flow-names.test.ts` exists to
33
+ * hold the distinction — this is the fourth id joined into a path, and the one that file missed.
17
34
  */
18
- export declare const safeProjectId: (projectId?: string) => string | undefined;
35
+ export declare const safeProjectId: (projectId?: ProjectId) => ProjectId | undefined;
@@ -1,10 +1,26 @@
1
1
  import { FlowErrorCode } from '@reticlehq/core';
2
- import { isValidFlowName } from '../../memory/project/dir/reticle-dir.js';
2
+ import { SAFE_SEGMENT_PATTERN } from '@reticlehq/core';
3
3
  /**
4
4
  * A project id that is safe to put in a path, or nothing.
5
5
  *
6
6
  * An unsafe or absent value collapses to `undefined` — the flat, global store — so a malformed id
7
7
  * can never escape `.reticle/flows/`. The store defends the disk boundary itself rather than
8
8
  * trusting whatever arrived in a session's HELLO.
9
+ *
10
+ * Held to one SEGMENT, not to a flow name. A project id becomes a DIRECTORY; a flow name is an
11
+ * ADDRESS, and since composites began naming sub-journeys as `onboarding/signup` its pattern has
12
+ * allowed separators. Validating a directory name with the address guard let a page choose how deep
13
+ * under `.reticle/` Reticle wrote — not an escape, since neither pattern admits `..` or a leading
14
+ * separator, but the page naming directories all the same.
15
+ *
16
+ * A FILTER, not a mint. It answers "is this id safe on disk", never "is this id a project" — a
17
+ * session id passes `SAFE_SEGMENT_PATTERN` just as cleanly, so minting the brand here would launder
18
+ * the exact confusion `ProjectId` exists to catch. The brand arrives already attached, from HELLO
19
+ * or from `.reticle.json`; see `ProjectId` in core.
20
+ *
21
+ * The narrowing said it out loud and nobody was listening: `isValidFlowName` narrows its argument to
22
+ * `FlowName`, for a value that is a project. The sibling guards for session and run ids have used
23
+ * the segment pattern since that widening, and `id-guards-are-not-flow-names.test.ts` exists to
24
+ * hold the distinction — this is the fourth id joined into a path, and the one that file missed.
9
25
  */
10
- export const safeProjectId = (projectId) => projectId !== undefined && isValidFlowName(projectId) ? projectId : undefined;
26
+ export const safeProjectId = (projectId) => projectId !== undefined && SAFE_SEGMENT_PATTERN.test(projectId) ? projectId : undefined;
@@ -24,8 +24,9 @@
24
24
  * returned unchanged.
25
25
  */
26
26
  import { FlowStore } from './flows.js';
27
+ import type { ProjectId } from '@reticlehq/core';
27
28
  import type { ToolDeps } from '../../surface/tools/tool-kit.js';
28
- export declare function flowsForSession(deps: ToolDeps, projectId: string | undefined): {
29
+ export declare function flowsForSession(deps: ToolDeps, projectId: ProjectId | undefined): {
29
30
  flows: FlowStore;
30
31
  root: string;
31
32
  };
@@ -20,6 +20,7 @@ import { recordingBacktrackWarning } from './recording/recording-backtrack.js';
20
20
  import { isValidFlowName, flowPath } from '../../memory/project/dir/reticle-dir.js';
21
21
  import {} from './flows.js';
22
22
  import { flowsForSession } from './flow-store-for-session.js';
23
+ import { rootForProjectId } from '../../memory/project/session-root.js';
23
24
  import { resolveSuiteSelection } from './suite-selection.js';
24
25
  import { replayNamedFlow, flowErrorMessage, latestRecordedFlow, sessionProjectId, } from './flow-replay-run.js';
25
26
  import { persistAndSyncVerificationRun, } from '../../judgement/runs/verification-sync.js';
@@ -34,7 +35,10 @@ import { replayAndLearn } from './flow-learning.js';
34
35
  */
35
36
  async function syncSavedFlowToCloud(deps, flow, projectId) {
36
37
  // Per-project cloud: sync a saved flow only when cloud is attached AND flow sync is enabled.
37
- const cloud = await resolveProjectCloud(deps.fs, deps.reticleRoot, homedir(), process.env);
38
+ // The link file of the project this flow belongs to — the flow itself was already saved through
39
+ // `flowsForSession`, and reading the credential from a different tree is how one project's work
40
+ // reaches another project's dashboard.
41
+ const cloud = await resolveProjectCloud(deps.fs, rootForProjectId(deps, projectId), homedir(), process.env);
38
42
  if (null === cloud.config || !cloud.policy.flows)
39
43
  return; // not attached / flows disabled → local only
40
44
  const result = await syncFlowToCloud(flow, cloud.config, projectId, cloudFetch);
@@ -573,7 +577,7 @@ export const FLOW_TOOLS = [
573
577
  ...(selected.unsatisfied === undefined ? {} : { unsatisfied: selected.unsatisfied }),
574
578
  };
575
579
  // verify:server — hand the whole suite to the hosted runner; it records the verification itself.
576
- const cloud = await resolveProjectCloud(deps.fs, deps.reticleRoot, homedir(), process.env);
580
+ const cloud = await resolveProjectCloud(deps.fs, rootForProjectId(deps, projectId), homedir(), process.env);
577
581
  const server = await runServerVerify(deps, cloud, sessionId, requested);
578
582
  if (server !== null)
579
583
  return server;
@@ -637,7 +641,7 @@ export const FLOW_TOOLS = [
637
641
  : leaseFailureReplay(requested[i] ?? '', o.error),
638
642
  durationMs: o.ok && o.value !== undefined ? o.value.durationMs : 0,
639
643
  }));
640
- const flaky = await recordSuiteFlakes(deps.fs, deps.reticleRoot, parallelRuns);
644
+ const flaky = await recordSuiteFlakes(deps.fs, rootForProjectId(deps, projectId), parallelRuns);
641
645
  await persistAndSyncVerificationRun(deps, timed, projectId);
642
646
  const verdict = buildSuiteVerdict(parallelRuns, selected.knownRoutes, SuiteIsolation.PER_FLOW_LEASE);
643
647
  return {
@@ -673,7 +677,7 @@ export const FLOW_TOOLS = [
673
677
  // `reticle flow` on the command line, so an AGENT running this tool a hundred times learned
674
678
  // nothing about which flows are intermittent. Same product, same ledger, two surfaces, and the
675
679
  // one an agent uses was the blind half.
676
- const flaky = await recordSuiteFlakes(deps.fs, deps.reticleRoot, runs);
680
+ const flaky = await recordSuiteFlakes(deps.fs, rootForProjectId(deps, projectId), runs);
677
681
  // Emit the consolidated run artifact (Runs tab) + best-effort cloud push. Never blocks the verdict.
678
682
  await persistAndSyncVerificationRun(deps, timed, projectId);
679
683
  const verdict = buildSuiteVerdict(runs, selected.knownRoutes, SuiteIsolation.SHARED_SESSION);
@@ -1,3 +1,4 @@
1
+ import { type ProjectId } from '@reticlehq/core';
1
2
  export { REDACTED_FILL } from './fields/flow-secret-field.js';
2
3
  import { type FlowResult } from './flow-result.js';
3
4
  export type { FlowResult } from './flow-result.js';
@@ -63,15 +64,15 @@ export declare class FlowStore {
63
64
  * Optionally fold structured annotations (per-step expect, dynamic[], success) onto
64
65
  * the flow before writing. Omitting `annotations` reproduces the same bytes.
65
66
  */
66
- save(program: CompiledProgram, annotations?: FlowAnnotations, projectId?: string): Promise<FlowResult<SaveSummary>>;
67
+ save(program: CompiledProgram, annotations?: FlowAnnotations, projectId?: ProjectId): Promise<FlowResult<SaveSummary>>;
67
68
  /**
68
69
  * Persist an already-anchored FlowFile captured in-page (no recompilation). The
69
70
  * browser resolved every semantic anchor at capture time; here we only validate the name +
70
71
  * re-run FlowFileSchema before writing. save is left untouched.
71
72
  */
72
- saveFlow(flow: FlowFile, projectId?: string): Promise<FlowResult<SaveSummary>>;
73
+ saveFlow(flow: FlowFile, projectId?: ProjectId): Promise<FlowResult<SaveSummary>>;
73
74
  /** Write back only what a replay LEARNED. See `#changeInPlace` for why it is not `saveFlow`. */
74
- recordLearned(name: string, learned: NonNullable<FlowFile['learned']>, projectId?: string): Promise<FlowResult<{
75
+ recordLearned(name: string, learned: NonNullable<FlowFile['learned']>, projectId?: ProjectId): Promise<FlowResult<{
75
76
  name: string;
76
77
  }>>;
77
78
  /**
@@ -83,7 +84,7 @@ export declare class FlowStore {
83
84
  * it with proposals that already cleared HEAL_CONFIDENCE_MIN. A change whose `from` no longer
84
85
  * matches the step's anchor is skipped — idempotent and defensive, never throwing.
85
86
  */
86
- heal(name: string, changes: HealChange[], projectId?: string): Promise<FlowResult<{
87
+ heal(name: string, changes: HealChange[], projectId?: ProjectId): Promise<FlowResult<{
87
88
  name: string;
88
89
  changed: HealChange[];
89
90
  }>>;
@@ -97,17 +98,17 @@ export declare class FlowStore {
97
98
  * the caller as a reportable error, never be silently filtered away. Callers that build a path from a
98
99
  * name must validate first; `flowPath` takes a branded FlowName precisely so the compiler insists.
99
100
  */
100
- list(projectId?: string): Promise<string[]>;
101
+ list(projectId?: ProjectId): Promise<string[]>;
101
102
  /**
102
103
  * Read + zod-validate a flow by name. With a `projectId`, prefers the per-project copy and falls
103
104
  * back to a legacy flat (untagged) flow of the same name — so pre-existing flows keep loading.
104
105
  */
105
- load(name: string, projectId?: string): Promise<FlowResult<FlowFile>>;
106
+ load(name: string, projectId?: ProjectId): Promise<FlowResult<FlowFile>>;
106
107
  /**
107
108
  * Delete a flow's file so a renamed/obsolete flow stops lingering in the replay list. Resolves the
108
109
  * same path `load` would (per-project copy, else legacy flat, else a subdir scan for the no-pid
109
110
  * caller), then removes it. NOT_FOUND when nothing resolves — deleting an absent flow is an error,
110
111
  * not a silent no-op, so a typo doesn't read as success.
111
112
  */
112
- remove(name: string, projectId?: string): Promise<FlowResult<void>>;
113
+ remove(name: string, projectId?: ProjectId): Promise<FlowResult<void>>;
113
114
  }
@@ -1,4 +1,4 @@
1
- import { asFlowName } from '@reticlehq/core';
1
+ import { asFlowName, asProjectId } from '@reticlehq/core';
2
2
  import { REDACTED_FILL } from './fields/flow-secret-field.js';
3
3
  export { REDACTED_FILL } from './fields/flow-secret-field.js';
4
4
  import { safeProjectId } from './flow-result.js';
@@ -408,8 +408,11 @@ export class FlowStore {
408
408
  catch {
409
409
  return legacy.sort();
410
410
  }
411
+ // Every subdirectory of `.reticle/flows/` is a project's, by construction: this class is the
412
+ // only thing that creates one, and it creates it from a projectId that already passed
413
+ // `safeProjectId`. Reading the name back is the third provenance boundary, not a cast.
411
414
  const subdirs = entries.filter((e) => !e.endsWith(FLOW_SUFFIX));
412
- const nested = (await Promise.all(subdirs.map((d) => this.#namesIn(flowDir(this.#root, d))))).flat();
415
+ const nested = (await Promise.all(subdirs.map((d) => this.#namesIn(flowDir(this.#root, asProjectId(d)))))).flat();
413
416
  return [...new Set([...legacy, ...nested])].sort();
414
417
  }
415
418
  /** The path a flow actually lives at: nested (per-project) if present, else legacy flat, else null. */
@@ -441,8 +444,9 @@ export class FlowStore {
441
444
  catch {
442
445
  return null;
443
446
  }
447
+ // Same provenance as `#namesIn`'s scan above: a subdir of `.reticle/flows/` is a projectId.
444
448
  for (const dir of entries.filter((e) => !e.endsWith(FLOW_SUFFIX))) {
445
- const nested = flowPath(this.#root, name, dir);
449
+ const nested = flowPath(this.#root, name, asProjectId(dir));
446
450
  if (await this.#fs.exists(nested))
447
451
  return nested;
448
452
  }
@@ -1,4 +1,4 @@
1
- import { type FlowFile } from '@reticlehq/core';
1
+ import { type ProjectId, type FlowFile } from '@reticlehq/core';
2
2
  import { type FlowResult } from './flow-result.js';
3
3
  /**
4
4
  * Change one thing about a saved flow, and write the same file that load resolved.
@@ -21,12 +21,12 @@ import { type FlowResult } from './flow-result.js';
21
21
  * every rule the document has ever been subject to.
22
22
  */
23
23
  export interface NarrowWritePort {
24
- load: (name: string, projectId?: string) => Promise<FlowResult<FlowFile>>;
25
- resolvePath: (name: string, projectId?: string) => Promise<string | null>;
24
+ load: (name: string, projectId?: ProjectId) => Promise<FlowResult<FlowFile>>;
25
+ resolvePath: (name: string, projectId?: ProjectId) => Promise<string | null>;
26
26
  write: (path: string, contents: string) => Promise<void>;
27
27
  serialize: (flow: FlowFile) => string;
28
28
  }
29
- export declare function changeInPlace<T>(port: NarrowWritePort, name: string, projectId: string | undefined, change: (flow: FlowFile) => {
29
+ export declare function changeInPlace<T>(port: NarrowWritePort, name: string, projectId: ProjectId | undefined, change: (flow: FlowFile) => {
30
30
  next: FlowFile;
31
31
  value: T;
32
32
  }): Promise<FlowResult<T>>;
@@ -93,7 +93,7 @@ route) {
93
93
  // that is never recorded cannot contribute to either.
94
94
  const step = compileActStep(args, res);
95
95
  // Keep the assertion the agent actually made. `act_and_wait { until }` IS the agent saying what
96
- // success means — 12 of 14 calls in a day carried one — and dropping it produced a flow graded
96
+ // success means — the large majority of calls carry one — and dropping it produced a flow graded
97
97
  // "assertion-free: it will pass even if the feature is broken", which is the regression-suite
98
98
  // story failing at its last step. Only kinds FlowExpect can express survive; see
99
99
  // predicate-to-expect.ts.
@@ -1,3 +1,17 @@
1
+ /**
2
+ * Which flows a suite run replays, resolved against the store.
3
+ *
4
+ * Shared by the MCP tool and the `reticle verify` command so the two cannot disagree about what a
5
+ * selection means.
6
+ *
7
+ * Lives beside the flow store rather than under `suite/` on purpose: it needs `flowsForSession`, and
8
+ * from `suite/` that import made `suite` and `flows` need EACH OTHER -- a mutual pair, which the
9
+ * reach guard forbids growing because two directories that need each other cannot be read, moved or
10
+ * tested apart. Here it adds no edge at all: `flows` reaching itself is nothing, and `cli -> flows`
11
+ * already existed. The pure rule lives in core (`selectFlows`); this is the part that has to read
12
+ * the files, because a label lives in one and `list` returns only names.
13
+ */
14
+ import { type ProjectId } from '@reticlehq/core';
1
15
  import type { ToolDeps } from '../../surface/tools/tool-kit.js';
2
16
  /**
3
17
  * Resolve what a suite run should replay, and what it is holding back.
@@ -9,7 +23,7 @@ import type { ToolDeps } from '../../surface/tools/tool-kit.js';
9
23
  * would turn a corrupt file into missing coverage nobody is told about, and replay reports the parse
10
24
  * failure far better than a silent omission does.
11
25
  */
12
- export declare function resolveSuiteSelection(deps: ToolDeps, projectId: string | undefined, args: Record<string, unknown>): Promise<{
26
+ export declare function resolveSuiteSelection(deps: ToolDeps, projectId: ProjectId | undefined, args: Record<string, unknown>): Promise<{
13
27
  run: string[];
14
28
  quarantined: string[];
15
29
  unmatched: string[];
@@ -11,7 +11,7 @@
11
11
  * already existed. The pure rule lives in core (`selectFlows`); this is the part that has to read
12
12
  * the files, because a label lives in one and `list` returns only names.
13
13
  */
14
- import { FLOW_FILE_VERSION, orderFlows, selectFlows } from '@reticlehq/core';
14
+ import { FLOW_FILE_VERSION, orderFlows, selectFlows, } from '@reticlehq/core';
15
15
  import { flowsForSession } from './flow-store-for-session.js';
16
16
  /**
17
17
  * Resolve what a suite run should replay, and what it is holding back.
@@ -7,6 +7,7 @@ import { ReticleTool } from '@reticlehq/core';
7
7
  import { asNumber, asRecord, asString } from '@reticlehq/core';
8
8
  import { workerCountSchema } from '../../surface/tools/args/numeric-bounds.js';
9
9
  import { loadNamedFlows, resolveChangedFiles } from '../../command/cli/cli-flow-commands.js';
10
+ import { projectDirFor } from '../../memory/project/session-root.js';
10
11
  import { affectedSavedFlows } from './change/flow-sources.js';
11
12
  import { FLOW_TOOLS } from './flow-tools.js';
12
13
  import { findContradictions } from '@reticlehq/engine/disagreement/contradictions.js';
@@ -62,10 +63,12 @@ export const VERIFY_CHANGE_TOOLS = [
62
63
  ? args['files'].filter((f) => 'string' === typeof f)
63
64
  : [];
64
65
  const since = asString(args['since']);
65
- // The daemon's cwd is not the project — it is wherever the MCP host started it, which for a
66
- // globally-registered server is `/` or `$HOME`. `deps.reticleRoot` is the project directory
67
- // this daemon was configured with, and it is the right tree to diff.
68
- const changed = await resolveChangedFiles(files, since, deps.reticleRoot);
66
+ // The tree to diff is the SESSION's project, and it is neither of the two things this line
67
+ // used to say it was: `deps.reticleRoot` is the daemon's `.reticle` SUBDIRECTORY, and the
68
+ // daemon's cwd is wherever the MCP host started it — `/` or `$HOME` for a globally-registered
69
+ // server. Both answer about the wrong repository while reporting a clean "nothing changed",
70
+ // which reads as a pass. `projectDirFor` is the directory holding the resolved root.
71
+ const changed = await resolveChangedFiles(files, since, projectDirFor(deps, asString(args['sessionId'])));
69
72
  const changedFiles = changed.files;
70
73
  // Same root flow_save wrote to. A read that resolved differently would report "no flows
71
74
  // covered this change" over flows that exist, which reads as a clean result rather than
@@ -2,7 +2,7 @@
2
2
  * Is this machine signed in to a Reticle workspace?
3
3
  *
4
4
  * Asked by the HUD, and there are two sources that must not be reimplemented per caller: the session
5
- * file `reticle login` writes to `~/.reticle/session.json`, and `RETICLE_CLOUD_KEY` for an agent that
5
+ * file `reticle login` writes to `~/.reticle/session.json`, and `RETICLE_API_KEY` for an agent that
6
6
  * was handed a key instead. Everything that gates on login — the defect sync button, the prompt to sign
7
7
  * in — resolves through here.
8
8
  *
@@ -2,7 +2,7 @@
2
2
  * Is this machine signed in to a Reticle workspace?
3
3
  *
4
4
  * Asked by the HUD, and there are two sources that must not be reimplemented per caller: the session
5
- * file `reticle login` writes to `~/.reticle/session.json`, and `RETICLE_CLOUD_KEY` for an agent that
5
+ * file `reticle login` writes to `~/.reticle/session.json`, and `RETICLE_API_KEY` for an agent that
6
6
  * was handed a key instead. Everything that gates on login — the defect sync button, the prompt to sign
7
7
  * in — resolves through here.
8
8
  *
@@ -11,7 +11,7 @@
11
11
  */
12
12
  import { readFileSync } from 'node:fs';
13
13
  import { join } from 'node:path';
14
- import { ReticleDir } from '@reticlehq/core';
14
+ import { ReticleDir, apiKeyFrom } from '@reticlehq/core';
15
15
  import { CloudEnv } from './cloud-sync.js';
16
16
  import { SESSION_FILE } from '../../command/cli/cloud-kit.js';
17
17
  /** Signed OUT is the safe answer: it shows the way in, where a wrong "signed in" hides it. */
@@ -57,9 +57,10 @@ function fromSessionFile(home) {
57
57
  * acting.
58
58
  */
59
59
  export function readAccountState(home, env) {
60
- const key = env[CloudEnv.KEY];
60
+ // Through the resolver, so a machine still exporting the key's previous name is still signed in.
61
+ const key = apiKeyFrom(env);
61
62
  const url = env[CloudEnv.URL];
62
- if ('string' === typeof key && key.length > 0) {
63
+ if (key !== undefined) {
63
64
  return {
64
65
  signedIn: true,
65
66
  ...('string' === typeof url && url.length > 0 ? { host: url.replace(/\/+$/, '') } : {}),
@@ -67,4 +67,13 @@ export declare const credentialSlot: (url: string, projectId: string, orgId?: st
67
67
  * so the single-project / CI flow is unchanged. `homeDir` is injected (testable; `os.homedir()` at call).
68
68
  */
69
69
  export declare function resolveProjectCloud(fs: FileSystemPort, reticleRoot: string, homeDir: string, env: NodeJS.ProcessEnv): Promise<ProjectCloud>;
70
+ /**
71
+ * The linked credential, as a port the tool surface can be handed.
72
+ *
73
+ * Lives here, beside the resolver it wraps, rather than in the daemon bootstrap: `index.ts` sits
74
+ * exactly on the thousand-line cap, so a caller that needs this in three lines has to put those
75
+ * three lines somewhere they belong. The harness reads it so a machine that has run `reticle link`
76
+ * needs no exported key — see `withLinkedCredential`.
77
+ */
78
+ export declare const linkedCloudPort: (fs: FileSystemPort, reticleRoot: string, homeDir: string, env: NodeJS.ProcessEnv) => () => Promise<CloudConfig | null>;
70
79
  export {};
@@ -145,3 +145,12 @@ export async function resolveProjectCloud(fs, reticleRoot, homeDir, env) {
145
145
  const config = key !== null ? { url: link.url.replace(/\/+$/, ''), apiKey: key } : null;
146
146
  return { config, policy, verify: link.verify, projectId: link.projectId };
147
147
  }
148
+ /**
149
+ * The linked credential, as a port the tool surface can be handed.
150
+ *
151
+ * Lives here, beside the resolver it wraps, rather than in the daemon bootstrap: `index.ts` sits
152
+ * exactly on the thousand-line cap, so a caller that needs this in three lines has to put those
153
+ * three lines somewhere they belong. The harness reads it so a machine that has run `reticle link`
154
+ * needs no exported key — see `withLinkedCredential`.
155
+ */
156
+ export const linkedCloudPort = (fs, reticleRoot, homeDir, env) => async () => (await resolveProjectCloud(fs, reticleRoot, homeDir, env)).config;
@@ -10,10 +10,15 @@
10
10
  */
11
11
  import { z } from 'zod';
12
12
  import { type FlowFile, type ReticleVerificationRun, type RunRecord, type VerifyProgressEvent } from '@reticlehq/core';
13
- /** Env var names — the presence of BOTH is what "logged in" means for cloud sync. */
13
+ /**
14
+ * Env var names — the presence of BOTH is what "logged in" means for cloud sync.
15
+ *
16
+ * `KEY` names the CURRENT spelling only. The previous one is still honoured, through `apiKeyFrom`
17
+ * below, which is the one place that knows there are two.
18
+ */
14
19
  export declare const CloudEnv: {
15
20
  readonly URL: "RETICLE_CLOUD_URL";
16
- readonly KEY: "RETICLE_CLOUD_KEY";
21
+ readonly KEY: "RETICLE_API_KEY";
17
22
  };
18
23
  export interface CloudConfig {
19
24
  url: string;
@@ -9,11 +9,16 @@
9
9
  * cloud copy is an enhancement, so a push error is logged and swallowed.
10
10
  */
11
11
  import { z } from 'zod';
12
- import { VERIFY_PROGRESS_MAX_EVENTS, } from '@reticlehq/core';
13
- /** Env var names — the presence of BOTH is what "logged in" means for cloud sync. */
12
+ import { VERIFY_PROGRESS_MAX_EVENTS, apiKeyFrom, } from '@reticlehq/core';
13
+ /**
14
+ * Env var names — the presence of BOTH is what "logged in" means for cloud sync.
15
+ *
16
+ * `KEY` names the CURRENT spelling only. The previous one is still honoured, through `apiKeyFrom`
17
+ * below, which is the one place that knows there are two.
18
+ */
14
19
  export const CloudEnv = {
15
20
  URL: 'RETICLE_CLOUD_URL',
16
- KEY: 'RETICLE_CLOUD_KEY',
21
+ KEY: 'RETICLE_API_KEY',
17
22
  };
18
23
  /** Paths the OSS server pushes to (match the cloud app's contract). */
19
24
  const CLOUD_FLOWS_PATH = '/v1/flows';
@@ -25,10 +30,10 @@ const CLOUD_PROGRESS_PATH = '/v1/connect/progress';
25
30
  /** Resolve cloud credentials from the environment, or null when not logged in (sync disabled). */
26
31
  export function resolveCloudConfig(env) {
27
32
  const url = env[CloudEnv.URL];
28
- const apiKey = env[CloudEnv.KEY];
33
+ const apiKey = apiKeyFrom(env);
29
34
  if (typeof url !== 'string' || 0 === url.length)
30
35
  return null;
31
- if (typeof apiKey !== 'string' || 0 === apiKey.length)
36
+ if (apiKey === undefined)
32
37
  return null;
33
38
  return { url: url.replace(/\/+$/, ''), apiKey };
34
39
  }
@@ -0,0 +1,10 @@
1
+ /** Narrowed to what this file stores: the caller's own type is richer and irrelevant here. */
2
+ export interface HarnessConfigView {
3
+ provider: string;
4
+ harnessEnabled: boolean;
5
+ harnessEntitled: boolean;
6
+ }
7
+ export interface ConfigSource {
8
+ read(): HarnessConfigView | undefined;
9
+ }
10
+ export declare function harnessConfigSource(load: () => Promise<HarnessConfigView | undefined>, now?: () => number): ConfigSource;
@@ -0,0 +1,58 @@
1
+ /**
2
+ * The harness switch, cached for a HUD that reads it inline.
3
+ *
4
+ * `fetchPlatformConfig` already asks the platform and parses the answer; this adds the one thing the
5
+ * impact snapshot needs and the drive path does not — a SYNCHRONOUS read. The snapshot is rebuilt
6
+ * dozens of times a session, the answer changes about as often as somebody opens a settings page,
7
+ * and a panel that awaited the network on every repaint would be a panel that stutters.
8
+ *
9
+ * Deliberately the same shape as `harnessOfferSource` next door rather than a shared abstraction:
10
+ * two callers with the same caching problem is not yet a pattern, and the day one of them needs a
11
+ * different staleness rule the shared version becomes the awkward one.
12
+ *
13
+ * Every failure answers `undefined`, which the HUD must read as "we have not heard" and render as NO
14
+ * control — never as "off". Showing a switch that cannot be honoured is worse than showing none.
15
+ *
16
+ * The loader arrives as a PORT rather than an import. Reading the platform lives in
17
+ * `features/harness`, and a cloud-memory file reaching into a feature is the reach the directory
18
+ * guard refuses — correctly, and for the same reason it refused the tool surface reaching the other
19
+ * way. Whoever wires the daemon owns both sides and can hand one to the other.
20
+ */
21
+ /** How long a cached answer is trusted. A person who just changed it expects the panel to notice. */
22
+ const FRESH_MS = 30 * 1_000;
23
+ export function harnessConfigSource(load, now = () => Date.now()) {
24
+ let cached;
25
+ // `undefined` rather than 0: "never asked" is a different state from "asked at the epoch", and
26
+ // conflating them made a source with an injected clock never take its first read.
27
+ let fetchedAt;
28
+ let inFlight = false;
29
+ const refresh = () => {
30
+ if (inFlight)
31
+ return;
32
+ inFlight = true;
33
+ void load()
34
+ .then((cfg) => {
35
+ // A failed read keeps the LAST good answer rather than blanking the control mid-session:
36
+ // one dropped request is not evidence that somebody changed their mind.
37
+ if (cfg !== undefined)
38
+ cached = cfg;
39
+ fetchedAt = now();
40
+ })
41
+ .catch(() => {
42
+ fetchedAt = now();
43
+ })
44
+ .finally(() => {
45
+ inFlight = false;
46
+ });
47
+ };
48
+ // Asked at construction, not first read: the daemon pushes a snapshot the instant a session
49
+ // attaches, and on a page nobody drives that is the only push there will ever be.
50
+ refresh();
51
+ return {
52
+ read() {
53
+ if (fetchedAt === undefined || FRESH_MS <= now() - fetchedAt)
54
+ refresh();
55
+ return cached;
56
+ },
57
+ };
58
+ }
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Whether this workspace has taken the three months of free harness, read by the thing that would
3
+ * advertise it.
4
+ *
5
+ * The HUD can only advertise honestly if it knows, and only the platform knows. This is the read —
6
+ * filed beside the other reads of the linked account rather than with the harness itself, because
7
+ * nothing that DRIVES needs it; the thing that needs it is the snapshot the HUD paints from. Same
8
+ * shape as the harness's own `platform-config.ts` and for the same reasons: it is INFORMATION, not a
9
+ * credential, so every failure answers `undefined`, which every caller must treat as "say nothing"
10
+ * rather than as "not claimed". An offline laptop must not start advertising something the person
11
+ * already owns.
12
+ *
13
+ * It is also SYNCHRONOUS at the point of use. The impact snapshot is built inline, dozens of times a
14
+ * session, and the answer changes about once per workspace lifetime, so a cache that refreshes in
15
+ * the background is the whole design: readers get what we last heard, and a stale answer is only
16
+ * ever stale in the safe direction — the card the person is looking at is the one they can act on.
17
+ */
18
+ import { type HarnessOffer } from '@reticlehq/core';
19
+ /** A GET, narrowed to what this file uses, so a test can answer it without a network. */
20
+ export type OfferFetch = (url: string, init: {
21
+ method: string;
22
+ headers: Record<string, string>;
23
+ signal?: AbortSignal;
24
+ }) => Promise<{
25
+ ok: boolean;
26
+ status: number;
27
+ text(): Promise<string>;
28
+ }>;
29
+ /** Ask the platform where this workspace stands. `undefined` means "no answer", never "not claimed". */
30
+ export declare function fetchHarnessOffer(env: Record<string, string | undefined>, claimUrl: string | undefined, doFetch?: OfferFetch, timeoutMs?: number): Promise<HarnessOffer | undefined>;
31
+ /** What the impact snapshot reads: the last answer, with a refresh kicked off when it goes stale. */
32
+ export interface OfferSource {
33
+ read(): HarnessOffer | undefined;
34
+ }
35
+ /**
36
+ * A self-refreshing cache over the read above.
37
+ *
38
+ * Deliberately NOT a timer. A daemon that lives for days would otherwise hold an interval whose only
39
+ * job is to re-ask a question nobody is asking, and it would have to be torn down in a place that
40
+ * already has nine teardowns. Refreshing on a stale READ means the work only happens while somebody
41
+ * is actually looking, and the first read of a process — which is always stale — starts the fetch
42
+ * that the second read answers from.
43
+ */
44
+ export declare function harnessOfferSource(env: Record<string, string | undefined>, claimUrl: () => string | undefined, now?: () => number, load?: (claim: string | undefined) => Promise<HarnessOffer | undefined>): OfferSource;