@reticlehq/server 2.11.0 → 2.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (252) hide show
  1. package/SKILL.md +26 -2
  2. package/dist/bridge/bridge.d.ts +0 -13
  3. package/dist/bridge/bridge.js +1 -1
  4. package/dist/bridge/bridge.test-harness.d.ts +2 -0
  5. package/dist/bridge/bridge.test-harness.js +8 -1
  6. package/dist/capsule/capsule-store.d.ts +2 -1
  7. package/dist/capsule/capsule-store.js +1 -1
  8. package/dist/capsule/causal-summary.d.ts +2 -2
  9. package/dist/chromium-launch-options.d.ts +28 -0
  10. package/dist/chromium-launch-options.js +29 -0
  11. package/dist/cli/cli-doctor.js +8 -0
  12. package/dist/cli/cli-flow-commands.d.ts +3 -1
  13. package/dist/cli/cli-flow-commands.js +34 -4
  14. package/dist/cli/cli-kill.d.ts +2 -1
  15. package/dist/cli/cli-parse.d.ts +3 -1
  16. package/dist/cli/cli-parse.js +29 -7
  17. package/dist/cli/cli-update-commands.js +20 -1
  18. package/dist/cli/cli-verify.d.ts +1 -1
  19. package/dist/cli/cli-verify.js +2 -2
  20. package/dist/cli/cloud-cli.js +12 -6
  21. package/dist/cli/config-discovery.d.ts +2 -1
  22. package/dist/cli/doctor-rows.d.ts +3 -1
  23. package/dist/cli/doctor-rows.js +3 -1
  24. package/dist/cli/gate-exit.d.ts +21 -0
  25. package/dist/cli/gate-exit.js +21 -0
  26. package/dist/cli/gate-hook-message.d.ts +33 -0
  27. package/dist/cli/gate-hook-message.js +46 -0
  28. package/dist/cli/mcp-command.d.ts +25 -0
  29. package/dist/cli/mcp-command.js +143 -0
  30. package/dist/cli/sibling-ports.d.ts +37 -0
  31. package/dist/cli/sibling-ports.js +56 -0
  32. package/dist/cli.js +37 -110
  33. package/dist/cloud/cloud-config.d.ts +2 -1
  34. package/dist/cloud/cloud-sync.d.ts +11 -0
  35. package/dist/cloud/cloud-sync.js +32 -0
  36. package/dist/crawl/crawl.d.ts +9 -13
  37. package/dist/crawl/crawl.js +20 -3
  38. package/dist/daemon/daemon-resolve.d.ts +79 -0
  39. package/dist/daemon/daemon-resolve.js +118 -0
  40. package/dist/daemon/free-port.d.ts +29 -0
  41. package/dist/daemon/free-port.js +55 -0
  42. package/dist/daemon/heartbeat.d.ts +0 -4
  43. package/dist/daemon/heartbeat.js +2 -2
  44. package/dist/dev/stale-issue-guard.d.ts +0 -2
  45. package/dist/dev/stale-issue-guard.js +1 -1
  46. package/dist/events/contradictions.d.ts +41 -0
  47. package/dist/events/contradictions.js +125 -12
  48. package/dist/events/declared.d.ts +14 -0
  49. package/dist/events/declared.js +33 -0
  50. package/dist/events/echo-mismatch.js +14 -0
  51. package/dist/events/observed-in-window.d.ts +12 -0
  52. package/dist/events/observed-in-window.js +17 -0
  53. package/dist/events/predicate-asks.d.ts +13 -0
  54. package/dist/events/predicate-asks.js +22 -0
  55. package/dist/events/predicate-eval.js +64 -22
  56. package/dist/events/predicate-parse.d.ts +0 -6
  57. package/dist/events/predicate-parse.js +43 -3
  58. package/dist/events/predicate-schema.d.ts +44 -0
  59. package/dist/events/predicate-schema.js +126 -23
  60. package/dist/events/predicate.d.ts +15 -0
  61. package/dist/events/predicate.js +41 -2
  62. package/dist/flows/coverage.d.ts +0 -8
  63. package/dist/flows/coverage.js +1 -1
  64. package/dist/flows/decision.d.ts +5 -1
  65. package/dist/flows/decision.js +33 -7
  66. package/dist/flows/flake.d.ts +0 -2
  67. package/dist/flows/flake.js +1 -1
  68. package/dist/flows/flow-intent.d.ts +52 -0
  69. package/dist/flows/flow-intent.js +123 -0
  70. package/dist/flows/flow-replay-run.js +27 -4
  71. package/dist/flows/flow-success.js +15 -3
  72. package/dist/flows/flow-tools.js +32 -4
  73. package/dist/flows/flows.d.ts +6 -1
  74. package/dist/flows/flows.js +34 -22
  75. package/dist/flows/predicate-to-expect.js +6 -3
  76. package/dist/flows/server-verify.js +16 -2
  77. package/dist/flows/verify-change-tools.js +4 -1
  78. package/dist/honesty/blind-spots.d.ts +0 -17
  79. package/dist/honesty/blind-spots.js +1 -1
  80. package/dist/honesty/feature-capture.d.ts +150 -0
  81. package/dist/honesty/feature-capture.js +233 -0
  82. package/dist/honesty/instrumentation-gaps.d.ts +30 -6
  83. package/dist/honesty/instrumentation-gaps.js +61 -5
  84. package/dist/honesty/tool-hit-rate.d.ts +86 -0
  85. package/dist/honesty/tool-hit-rate.js +90 -0
  86. package/dist/honesty/undeclared-change.d.ts +51 -0
  87. package/dist/honesty/undeclared-change.js +56 -0
  88. package/dist/honesty/verified.d.ts +15 -0
  89. package/dist/honesty/verified.js +68 -4
  90. package/dist/http-server.d.ts +0 -1
  91. package/dist/http-server.js +1 -1
  92. package/dist/hunt/hunt-report.d.ts +0 -2
  93. package/dist/hunt/hunt-report.js +1 -1
  94. package/dist/impact/impact-store.d.ts +0 -5
  95. package/dist/impact/impact-store.js +1 -1
  96. package/dist/index.d.ts +4 -1
  97. package/dist/init/agent-rules.d.ts +17 -0
  98. package/dist/init/agent-rules.js +31 -1
  99. package/dist/init/capabilities.d.ts +0 -2
  100. package/dist/init/capabilities.js +1 -1
  101. package/dist/init/closing-hint.d.ts +36 -0
  102. package/dist/init/closing-hint.js +120 -0
  103. package/dist/init/confirm.d.ts +0 -25
  104. package/dist/init/confirm.js +3 -3
  105. package/dist/init/cra.d.ts +14 -1
  106. package/dist/init/cra.js +43 -12
  107. package/dist/init/dev-script.d.ts +48 -0
  108. package/dist/init/dev-script.js +63 -0
  109. package/dist/init/mcp.d.ts +16 -5
  110. package/dist/init/mcp.js +26 -10
  111. package/dist/init/plan-framework.d.ts +0 -5
  112. package/dist/init/plan-framework.js +83 -9
  113. package/dist/init/plan.d.ts +10 -30
  114. package/dist/init/plan.js +30 -15
  115. package/dist/init/refresh-rules.d.ts +39 -0
  116. package/dist/init/refresh-rules.js +51 -0
  117. package/dist/init/run.d.ts +0 -26
  118. package/dist/init/run.js +14 -87
  119. package/dist/init/snippets.js +53 -10
  120. package/dist/input/real-input.d.ts +2 -0
  121. package/dist/input/real-input.js +3 -2
  122. package/dist/input/scroll-find.d.ts +7 -4
  123. package/dist/input/scroll-find.js +63 -25
  124. package/dist/intent/inline-intent.d.ts +46 -0
  125. package/dist/intent/inline-intent.js +103 -0
  126. package/dist/intent/intent-store.d.ts +37 -0
  127. package/dist/intent/intent-store.js +112 -0
  128. package/dist/intent/intent-tools.d.ts +2 -0
  129. package/dist/intent/intent-tools.js +80 -0
  130. package/dist/intent/open-intents.d.ts +21 -0
  131. package/dist/intent/open-intents.js +25 -0
  132. package/dist/journal/envelope.d.ts +26 -26
  133. package/dist/journal/journal-recorder.d.ts +23 -0
  134. package/dist/journal/journal-recorder.js +35 -0
  135. package/dist/mcp/attach-memory.d.ts +67 -0
  136. package/dist/mcp/attach-memory.js +142 -0
  137. package/dist/mcp/mcp-proxy.d.ts +0 -7
  138. package/dist/mcp/mcp-proxy.js +14 -2
  139. package/dist/mcp/mcp.d.ts +2 -0
  140. package/dist/mcp/mcp.js +45 -23
  141. package/dist/mcp/server-instructions.js +45 -3
  142. package/dist/mcp/tool-catalog-cache.d.ts +8 -0
  143. package/dist/mcp/tool-catalog-cache.js +10 -0
  144. package/dist/oracles/flow-instrument-gaps.d.ts +4 -4
  145. package/dist/oracles/flow-instrument-gaps.js +4 -5
  146. package/dist/oracles/self-instrument.d.ts +9 -9
  147. package/dist/oracles/self-instrument.js +19 -13
  148. package/dist/pool/browser-pool.d.ts +12 -1
  149. package/dist/pool/browser-pool.js +31 -1
  150. package/dist/pool/playwright-launcher.js +3 -1
  151. package/dist/project/fs-port.d.ts +3 -0
  152. package/dist/project/fs-port.js +3 -2
  153. package/dist/project/memory-fs.js +6 -1
  154. package/dist/project/project-tools.js +3 -32
  155. package/dist/project/reticle-dir.d.ts +2 -0
  156. package/dist/project/reticle-dir.js +1 -0
  157. package/dist/runs/context-tools.d.ts +2 -0
  158. package/dist/runs/context-tools.js +89 -0
  159. package/dist/runs/run-context.d.ts +40 -0
  160. package/dist/runs/run-context.js +158 -0
  161. package/dist/runs/run-diff.d.ts +2 -1
  162. package/dist/runs/verification-sync.js +2 -2
  163. package/dist/session/dev-server-probe.d.ts +0 -8
  164. package/dist/session/dev-server-probe.js +1 -1
  165. package/dist/session/last-act.d.ts +7 -1
  166. package/dist/session/last-act.js +2 -2
  167. package/dist/session/live-control-tools.js +1 -42
  168. package/dist/session/no-session-diagnosis.d.ts +10 -0
  169. package/dist/session/no-session-diagnosis.js +24 -7
  170. package/dist/session/no-session-next-action.d.ts +5 -0
  171. package/dist/session/no-session-next-action.js +35 -3
  172. package/dist/session/no-session-watch.d.ts +8 -0
  173. package/dist/session/no-session-watch.js +82 -64
  174. package/dist/session/output-budget.js +4 -1
  175. package/dist/session/session-health.d.ts +9 -0
  176. package/dist/session/session-health.js +23 -0
  177. package/dist/session/session-manager.js +50 -1
  178. package/dist/session/session-successor.d.ts +37 -0
  179. package/dist/session/session-successor.js +78 -0
  180. package/dist/session/session.d.ts +31 -1
  181. package/dist/session/session.js +75 -2
  182. package/dist/telemetry/automation-hint.d.ts +2 -1
  183. package/dist/telemetry/feedback-outbox.d.ts +2 -1
  184. package/dist/telemetry/project-profile.d.ts +0 -8
  185. package/dist/telemetry/project-profile.js +1 -1
  186. package/dist/telemetry/run-telemetry.d.ts +0 -2
  187. package/dist/telemetry/run-telemetry.js +1 -1
  188. package/dist/telemetry/verification-of.js +2 -2
  189. package/dist/tools/act-tools.js +126 -21
  190. package/dist/tools/assert-source.d.ts +22 -0
  191. package/dist/tools/assert-source.js +53 -0
  192. package/dist/tools/assert-verdict.d.ts +20 -3
  193. package/dist/tools/assert-verdict.js +36 -4
  194. package/dist/tools/browser-tools.js +0 -20
  195. package/dist/tools/complete-snapshot.d.ts +48 -0
  196. package/dist/tools/complete-snapshot.js +75 -0
  197. package/dist/tools/coverage-tools.d.ts +10 -1
  198. package/dist/tools/coverage-tools.js +123 -69
  199. package/dist/tools/dynamic-tools.js +29 -8
  200. package/dist/tools/error-recovery.d.ts +24 -14
  201. package/dist/tools/error-recovery.js +70 -9
  202. package/dist/tools/feedback-tools.d.ts +1 -1
  203. package/dist/tools/feedback-tools.js +2 -2
  204. package/dist/tools/invoke-tool.js +62 -2
  205. package/dist/tools/lease-hint.d.ts +10 -0
  206. package/dist/tools/lease-hint.js +53 -4
  207. package/dist/tools/lease-tools.d.ts +5 -1
  208. package/dist/tools/lease-tools.js +22 -8
  209. package/dist/tools/navigate-arrival.d.ts +0 -2
  210. package/dist/tools/navigate-arrival.js +1 -1
  211. package/dist/tools/observe-tools.js +63 -26
  212. package/dist/tools/read-tools.js +5 -3
  213. package/dist/tools/real-input-attempt.d.ts +22 -0
  214. package/dist/tools/real-input-attempt.js +153 -1
  215. package/dist/tools/reconcile-tools.js +18 -4
  216. package/dist/tools/resolve-target.d.ts +0 -7
  217. package/dist/tools/sdk-marker-probe.d.ts +29 -0
  218. package/dist/tools/sdk-marker-probe.js +57 -0
  219. package/dist/tools/snapshot-delta.d.ts +1 -1
  220. package/dist/tools/tool-kit.d.ts +12 -0
  221. package/dist/tools/tool-kit.js +16 -0
  222. package/dist/tools/tool-names.d.ts +3 -0
  223. package/dist/tools/tool-names.js +3 -0
  224. package/dist/tools/tool-surface.d.ts +85 -6
  225. package/dist/tools/tool-surface.js +122 -2
  226. package/dist/tools/tools.js +22 -4
  227. package/dist/tools/unadvertised-help.js +4 -4
  228. package/dist/tools/verify-next-baton.d.ts +53 -0
  229. package/dist/tools/verify-next-baton.js +52 -0
  230. package/dist/update/update-nudge.d.ts +7 -10
  231. package/dist/update/update-nudge.js +12 -3
  232. package/dist/update/updater.d.ts +1 -1
  233. package/docs/README.md +2 -1
  234. package/docs/cli/cloud.mdx +1 -1
  235. package/docs/cli/doctor.mdx +1 -0
  236. package/docs/cli/verify.mdx +4 -5
  237. package/docs/desktop.mdx +1 -1
  238. package/docs/docs.json +2 -2
  239. package/docs/gates.md +1 -1
  240. package/docs/install-manual.mdx +1 -1
  241. package/docs/packages/browser.mdx +2 -2
  242. package/docs/packages/server.mdx +1 -1
  243. package/docs/packages.mdx +8 -6
  244. package/docs/platform-integration.md +17 -0
  245. package/docs/predicates.mdx +8 -0
  246. package/docs/reticle-manual.md +106 -0
  247. package/docs/tools-tools-and-run.mdx +2 -4
  248. package/docs/troubleshooting.mdx +9 -0
  249. package/docs/usage.md +3 -2
  250. package/package.json +2 -2
  251. package/dist/session/session-readiness.d.ts +0 -34
  252. package/dist/session/session-readiness.js +0 -36
@@ -0,0 +1,103 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { IntentStore } from './intent-store.js';
3
+ import { sessionRoot } from '../project/session-root.js';
4
+ /**
5
+ * Intent declared INLINE, on the tool that is already drawing the verdict.
6
+ *
7
+ * `reticle_intent` is on the extended surface, so declaring intent costs an agent a discovery, a
8
+ * decision and a round trip before it has done anything — three places the capture silently fails,
9
+ * and it does. `reticle_act_and_wait` and `reticle_assert` are in every agent's tool list already,
10
+ * so one optional argument there makes the declaration free and discoverable by construction.
11
+ *
12
+ * This is a shortcut into the EXISTING ledger, never a second one. It writes `.reticle/intent.json`
13
+ * through `IntentStore` exactly as `reticle_intent { action: "declare" }` does, so a reviewer reads
14
+ * one file in one vocabulary and cannot tell from the row which door the intent came in by. The
15
+ * pattern is `flows/flow-intent.ts`, which does the same job for a saved flow.
16
+ */
17
+ /** Namespaced like `flow:`, so an inline id never collides with one an agent chose by hand. */
18
+ const INLINE_INTENT_ID_PREFIX = 'inline:';
19
+ const ID_SEPARATOR = ':';
20
+ const SLUG_SEPARATOR = '-';
21
+ const SLUG_MAX = 48;
22
+ const HASH_LENGTH = 8;
23
+ const HASH_ALGORITHM = 'sha1';
24
+ const NON_SLUG = /[^a-z0-9]+/g;
25
+ const SLUG_EDGES = /^-+|-+$/g;
26
+ /**
27
+ * The ledger id a piece of inline prose is declared under.
28
+ *
29
+ * Derived from the statement rather than minted, so the same sentence declared on five verdicts is
30
+ * one row rather than five, and a DIFFERENT sentence is a different row rather than a false
31
+ * amendment of the first. The slug is there because the id is what an agent later types to reference
32
+ * the intent, and a bare digest is unreadable; the digest is there because the slug is truncated and
33
+ * two long statements sharing a prefix must not share a row.
34
+ */
35
+ export function inlineIntentId(statement) {
36
+ const slug = statement
37
+ .toLowerCase()
38
+ .replace(NON_SLUG, SLUG_SEPARATOR)
39
+ .replace(SLUG_EDGES, '')
40
+ .slice(0, SLUG_MAX)
41
+ .replace(SLUG_EDGES, '');
42
+ const digest = createHash(HASH_ALGORITHM).update(statement).digest('hex').slice(0, HASH_LENGTH);
43
+ return `${INLINE_INTENT_ID_PREFIX}${slug}${SLUG_SEPARATOR}${digest}`;
44
+ }
45
+ /** What `provenBy` records: which tool drew the verdict, and when. */
46
+ export function inlineVerdictId(tool, at) {
47
+ return `${tool}${ID_SEPARATOR}${String(at)}`;
48
+ }
49
+ /**
50
+ * Put an inline intent in the ledger and attach the predicate about to be evaluated. Returns the id
51
+ * the verdict may later discharge, or undefined when there was no intent to record.
52
+ *
53
+ * `intent` carries EITHER prose OR the id of an intent already in the ledger. One field rather than
54
+ * two because the surface is the scarce thing here and the two cases are told apart by the ledger
55
+ * itself: a string that names an existing row is a reference, anything else is prose. A statement
56
+ * that happens to equal an existing id is the same intent said twice, so pointing at the row is the
57
+ * right answer there too, not a collision.
58
+ *
59
+ * A referenced intent is never re-declared, which would overwrite the agent's own words with an id,
60
+ * and its binding is left alone when it already has one — a predicate bound deliberately through
61
+ * `reticle_intent { action: "bind" }` is a stronger statement than whatever this one call asserts,
62
+ * and quietly replacing it is exactly the narrowing the ledger exists to make visible.
63
+ *
64
+ * Best-effort by construction: a ledger that cannot be written is a small problem, and a verdict
65
+ * that fails to return because of one is a large problem.
66
+ */
67
+ export async function linkInlineIntent(deps, sessionId, intent,
68
+ /** The predicate this verdict will evaluate, or undefined when it proves nothing (a bare settle). */
69
+ binding) {
70
+ if (intent === undefined || 0 === intent.trim().length)
71
+ return undefined;
72
+ try {
73
+ const store = new IntentStore(deps.fs, sessionRoot(deps, sessionId), { now: deps.now });
74
+ const existing = (await store.read()).find((row) => row.id === intent);
75
+ const id = existing?.id ?? inlineIntentId(intent);
76
+ if (existing === undefined)
77
+ await store.declare([{ id, statement: intent }]);
78
+ if (binding !== undefined && existing?.binding === undefined)
79
+ await store.bind(id, binding);
80
+ return id;
81
+ }
82
+ catch {
83
+ return undefined;
84
+ }
85
+ }
86
+ /**
87
+ * Record that this verdict proved the intent. Call only for a verdict that actually proved something.
88
+ *
89
+ * The ledger's own rule does the rest: `dischargeIntent` refuses an intent with no binding, so an
90
+ * intent declared inline on a wait that asserted no consequence stays open and honest instead of
91
+ * collecting a proof nothing earned.
92
+ */
93
+ export async function dischargeInlineIntent(deps, sessionId, id, proof) {
94
+ if (id === undefined)
95
+ return;
96
+ try {
97
+ const store = new IntentStore(deps.fs, sessionRoot(deps, sessionId), { now: deps.now });
98
+ await store.discharge(id, proof);
99
+ }
100
+ catch {
101
+ // The verdict already stood. The proof is simply not recorded — see dischargeFlowIntent.
102
+ }
103
+ }
@@ -0,0 +1,37 @@
1
+ import { type Intent, type IntentSurface } from '@reticlehq/core';
2
+ import type { FileSystemPort } from '../project/fs-port.js';
3
+ export interface Clock {
4
+ now: () => number;
5
+ }
6
+ export declare class IntentStore {
7
+ #private;
8
+ constructor(fs: FileSystemPort, root: string, clock: Clock);
9
+ /** Every intent, in declaration order. */
10
+ read(): Promise<Intent[]>;
11
+ /** Everything not yet proved — what an agent asking "am I done?" still owes. */
12
+ open(): Promise<Intent[]>;
13
+ /**
14
+ * Declare one or more intents.
15
+ *
16
+ * Batched because the marginal cost of the whole mechanism has to stay at one call per feature; an
17
+ * agent that must make five calls to declare five things will make none.
18
+ */
19
+ declare(entries: readonly {
20
+ id: string;
21
+ statement: string;
22
+ surface?: IntentSurface;
23
+ }[]): Promise<Intent[]>;
24
+ /** Attach the predicate that would prove an intent. False when the id names nothing. */
25
+ bind(id: string, binding: unknown): Promise<boolean>;
26
+ /**
27
+ * Record that a verdict proved an intent.
28
+ *
29
+ * False rather than a throw on an unknown or unbound id: discharge runs off the back of a verdict,
30
+ * and must never be the reason one fails to return.
31
+ */
32
+ discharge(id: string, proof: {
33
+ verdictId: string;
34
+ grade: string;
35
+ at: number;
36
+ }): Promise<boolean>;
37
+ }
@@ -0,0 +1,112 @@
1
+ import { bindIntent, declareIntent, dischargeIntent, emptyIntentFile, openIntents, parseIntentFile, upsertIntent, } from '@reticlehq/core';
2
+ import { reticleDirPaths } from '../project/reticle-dir.js';
3
+ import { withFileLock } from '../project/file-lock.js';
4
+ /**
5
+ * The intent ledger on disk — `.reticle/intent.json`, git-checked and meant to be read in review.
6
+ *
7
+ * Git-checked deliberately. The one real defence against an agent quietly narrowing what it meant to
8
+ * match what it can already prove is that a human sees the narrowing in a diff, and that only works
9
+ * if the file lives with the code and travels with the PR. It is also why the writes are byte-stable:
10
+ * a ledger that churns on every run is one whose diffs nobody reads.
11
+ *
12
+ * Every operation goes through the same file lock the other stores use, because two sessions driving
13
+ * one project is ordinary and a lost declaration would be silent.
14
+ */
15
+ const JSON_INDENT = 2;
16
+ export class IntentStore {
17
+ #fs;
18
+ #root;
19
+ #clock;
20
+ constructor(fs, root, clock) {
21
+ this.#fs = fs;
22
+ this.#root = root;
23
+ this.#clock = clock;
24
+ }
25
+ #path() {
26
+ return reticleDirPaths(this.#root).intent;
27
+ }
28
+ /**
29
+ * Load the ledger, failing soft to empty.
30
+ *
31
+ * This is a git-checked file an agent can write and a human can hand-merge, so a malformed one is
32
+ * genuinely reachable — a conflict marker left in place is the obvious case. Throwing here would
33
+ * take down the verdict that was only asking what was still open, which trades a small problem for
34
+ * a large one.
35
+ */
36
+ async #load() {
37
+ try {
38
+ const raw = await this.#fs.readFile(this.#path());
39
+ return parseIntentFile(JSON.parse(raw));
40
+ }
41
+ catch {
42
+ return emptyIntentFile();
43
+ }
44
+ }
45
+ /** Byte-stable: 2-space indent, one trailing newline. An unchanged ledger produces no diff. */
46
+ async #save(file) {
47
+ await this.#fs.mkdir(reticleDirPaths(this.#root).root);
48
+ await this.#fs.writeFile(this.#path(), `${JSON.stringify(file, null, JSON_INDENT)}\n`);
49
+ }
50
+ /** Every intent, in declaration order. */
51
+ async read() {
52
+ return Object.values((await this.#load()).intents);
53
+ }
54
+ /** Everything not yet proved — what an agent asking "am I done?" still owes. */
55
+ async open() {
56
+ return openIntents(await this.#load());
57
+ }
58
+ /**
59
+ * Declare one or more intents.
60
+ *
61
+ * Batched because the marginal cost of the whole mechanism has to stay at one call per feature; an
62
+ * agent that must make five calls to declare five things will make none.
63
+ */
64
+ async declare(entries) {
65
+ if (0 === entries.length)
66
+ return [];
67
+ return withFileLock(this.#path(), async () => {
68
+ let file = await this.#load();
69
+ const now = this.#clock.now();
70
+ const declared = [];
71
+ for (const entry of entries) {
72
+ const intent = declareIntent({ ...entry, now });
73
+ file = upsertIntent(file, intent);
74
+ const stored = file.intents[intent.id];
75
+ if (stored !== undefined)
76
+ declared.push(stored);
77
+ }
78
+ await this.#save(file);
79
+ return declared;
80
+ });
81
+ }
82
+ /** Attach the predicate that would prove an intent. False when the id names nothing. */
83
+ async bind(id, binding) {
84
+ return withFileLock(this.#path(), async () => {
85
+ const file = await this.#load();
86
+ const existing = file.intents[id];
87
+ if (existing === undefined)
88
+ return false;
89
+ await this.#save(upsertIntent(file, bindIntent(existing, binding)));
90
+ return true;
91
+ });
92
+ }
93
+ /**
94
+ * Record that a verdict proved an intent.
95
+ *
96
+ * False rather than a throw on an unknown or unbound id: discharge runs off the back of a verdict,
97
+ * and must never be the reason one fails to return.
98
+ */
99
+ async discharge(id, proof) {
100
+ return withFileLock(this.#path(), async () => {
101
+ const file = await this.#load();
102
+ const existing = file.intents[id];
103
+ if (existing === undefined)
104
+ return false;
105
+ const proved = dischargeIntent(existing, proof);
106
+ if (proved === existing)
107
+ return false;
108
+ await this.#save(upsertIntent(file, proved));
109
+ return true;
110
+ });
111
+ }
112
+ }
@@ -0,0 +1,2 @@
1
+ import type { ToolDef } from '../tools/tool-kit.js';
2
+ export declare const INTENT_TOOLS: ToolDef[];
@@ -0,0 +1,80 @@
1
+ import { z } from 'zod';
2
+ import { IntentStore } from './intent-store.js';
3
+ import { ReticleTool } from '../tools/tool-names.js';
4
+ import { sessionIdShape } from '../tools/tool-kit.js';
5
+ import { sessionRoot } from '../project/session-root.js';
6
+ import { asString } from '../tools/tools-helpers.js';
7
+ /**
8
+ * Declare what a change was SUPPOSED to make true, while somebody still knows.
9
+ *
10
+ * One tool with three actions rather than three tools, because the surface is capped and because an
11
+ * agent that has to discover three names to use one idea uses none of them.
12
+ *
13
+ * There is deliberately no `discharge` action. A verdict discharges an intent by satisfying its
14
+ * binding, and a discharge an agent has to remember to file would be the same forgetting problem
15
+ * this exists to solve, moved one layer up.
16
+ */
17
+ const DECLARE = 'declare';
18
+ const LIST = 'list';
19
+ const BIND = 'bind';
20
+ export const INTENT_TOOLS = [
21
+ {
22
+ name: ReticleTool.INTENT,
23
+ description: 'Record what a change is SUPPOSED to make true, in your own words, while you still know — then verification does not have to re-derive it from the DOM later. { action:"declare", intents:[{ id, statement, surface? }] } takes prose and needs NO predicate: at declare time there is often no route, no ref and no code yet, and a predicate demanded there is just a mechanism. Declare EARLY (as you build) and batch them — one call per feature is the whole budget. { action:"bind", id, binding } attaches the predicate that would prove it once you know how; an intent with no binding is not a failure, it is the most interesting row in the ledger — something meant that nothing can currently prove. { action:"list" } returns what is still open. Stored in .reticle/intent.json, git-checked so a human sees in review if an intent was later narrowed to match what was easy to prove.',
24
+ example: {
25
+ action: DECLARE,
26
+ intents: [
27
+ { id: 'checkin', statement: 'clicking Send check-in makes the badge read "checked in"' },
28
+ ],
29
+ },
30
+ inputSchema: {
31
+ action: z.enum([DECLARE, LIST, BIND]),
32
+ intents: z
33
+ .array(z.object({
34
+ id: z.string(),
35
+ statement: z.string(),
36
+ surface: z
37
+ .object({
38
+ route: z.string().optional(),
39
+ flow: z.string().optional(),
40
+ files: z.array(z.string()).optional(),
41
+ })
42
+ .optional(),
43
+ }))
44
+ .optional()
45
+ .describe('declare only. Batchable — declare every intent for a feature in one call.'),
46
+ id: z.string().optional().describe('bind only: which intent the predicate proves.'),
47
+ binding: z
48
+ .unknown()
49
+ .optional()
50
+ .describe("bind only: the predicate that would prove it, in reticle_assert's shape."),
51
+ ...sessionIdShape,
52
+ },
53
+ outputSchema: {
54
+ intents: z
55
+ .array(z.unknown())
56
+ .optional()
57
+ .describe('The intents this call declared, or on `list` everything still open — each { id, statement, state, declaredAt, binding?, surface?, provenBy?, amended? }. `state` is declared (prose only), bound (a predicate exists), or proved (a verdict satisfied it).'),
58
+ bound: z.boolean().optional().describe('bind only: false when the id names no intent.'),
59
+ path: z.string().optional().describe('Where the ledger was written.'),
60
+ },
61
+ handler: async (deps, args) => {
62
+ const root = sessionRoot(deps, asString(args['sessionId']));
63
+ const store = new IntentStore(deps.fs, root, { now: deps.now });
64
+ const action = asString(args['action']);
65
+ if (BIND === action) {
66
+ const id = asString(args['id']) ?? '';
67
+ return { bound: await store.bind(id, args['binding']) };
68
+ }
69
+ if (LIST === action) {
70
+ return { intents: await store.open() };
71
+ }
72
+ const raw = args['intents'];
73
+ const entries = Array.isArray(raw)
74
+ ? raw
75
+ : [];
76
+ const declared = await store.declare(entries);
77
+ return { intents: declared, path: root };
78
+ },
79
+ },
80
+ ];
@@ -0,0 +1,21 @@
1
+ import type { Intent } from '@reticlehq/core';
2
+ import type { ToolDeps } from '../tools/tool-kit.js';
3
+ /**
4
+ * What this project still owes — the ledger's undischarged intents, for a tool that has `deps` and a
5
+ * session id rather than a store.
6
+ *
7
+ * One helper rather than the same three-line construction at each site: the ROOT resolution is the
8
+ * part that must not drift. A caller that resolved the daemon's own directory while the declaring
9
+ * tool wrote to the project's would read an empty ledger and report that nothing was declared, which
10
+ * is a false accusation rather than an error.
11
+ */
12
+ export declare function openSessionIntents(deps: ToolDeps, sessionId: string | undefined): Promise<Intent[]>;
13
+ /**
14
+ * EVERY intent in the ledger, proved ones included — the denominator the open list is a subset of.
15
+ *
16
+ * Separate from `openSessionIntents` rather than a flag on it, because the two answer different
17
+ * questions: "what does this project still owe" drives a verdict's honesty, and "was anything ever
18
+ * declared" is what decides whether `reticle_intent` is used at all. Same root resolution, for the
19
+ * same reason it is shared above.
20
+ */
21
+ export declare function allSessionIntents(deps: ToolDeps, sessionId: string | undefined): Promise<Intent[]>;
@@ -0,0 +1,25 @@
1
+ import { IntentStore } from './intent-store.js';
2
+ import { sessionRoot } from '../project/session-root.js';
3
+ /**
4
+ * What this project still owes — the ledger's undischarged intents, for a tool that has `deps` and a
5
+ * session id rather than a store.
6
+ *
7
+ * One helper rather than the same three-line construction at each site: the ROOT resolution is the
8
+ * part that must not drift. A caller that resolved the daemon's own directory while the declaring
9
+ * tool wrote to the project's would read an empty ledger and report that nothing was declared, which
10
+ * is a false accusation rather than an error.
11
+ */
12
+ export async function openSessionIntents(deps, sessionId) {
13
+ return new IntentStore(deps.fs, sessionRoot(deps, sessionId), { now: deps.now }).open();
14
+ }
15
+ /**
16
+ * EVERY intent in the ledger, proved ones included — the denominator the open list is a subset of.
17
+ *
18
+ * Separate from `openSessionIntents` rather than a flag on it, because the two answer different
19
+ * questions: "what does this project still owe" drives a verdict's honesty, and "was anything ever
20
+ * declared" is what decides whether `reticle_intent` is used at all. Same root resolution, for the
21
+ * same reason it is shared above.
22
+ */
23
+ export async function allSessionIntents(deps, sessionId) {
24
+ return new IntentStore(deps.fs, sessionRoot(deps, sessionId), { now: deps.now }).read();
25
+ }
@@ -8,7 +8,7 @@ import type { SegmentRollup } from './rollups.js';
8
8
  * (slower, more errors, more requests) — a segment doing less than expected is not a regression.
9
9
  */
10
10
  /** Online mean/variance/max for one metric (Welford's algorithm). Immutable: addStat returns a copy. */
11
- export interface MetricStats {
11
+ interface MetricStats {
12
12
  count: number;
13
13
  mean: number;
14
14
  /** Sum of squared deviations (Welford M2); variance = m2/(count-1). */
@@ -22,7 +22,7 @@ export declare function stddev(stats: MetricStats): number;
22
22
  export declare function zScore(stats: MetricStats, x: number): number;
23
23
  /** The metrics an envelope tracks per route. */
24
24
  declare const ENVELOPE_METRICS: readonly ["durationMs", "net", "netErrors", "consoleErrors"];
25
- export type EnvelopeMetric = (typeof ENVELOPE_METRICS)[number];
25
+ type EnvelopeMetric = (typeof ENVELOPE_METRICS)[number];
26
26
  export interface RouteEnvelope {
27
27
  route: string;
28
28
  /** Runs that contributed a sample — below MIN_ENVELOPE_SAMPLES the envelope is too green to judge. */
@@ -43,14 +43,14 @@ export declare const RouteEnvelopeSchema: z.ZodObject<{
43
43
  max: z.ZodNumber;
44
44
  }, "strip", z.ZodTypeAny, {
45
45
  count: number;
46
- max: number;
47
46
  mean: number;
48
47
  m2: number;
48
+ max: number;
49
49
  }, {
50
50
  count: number;
51
- max: number;
52
51
  mean: number;
53
52
  m2: number;
53
+ max: number;
54
54
  }>;
55
55
  net: z.ZodObject<{
56
56
  count: z.ZodNumber;
@@ -59,14 +59,14 @@ export declare const RouteEnvelopeSchema: z.ZodObject<{
59
59
  max: z.ZodNumber;
60
60
  }, "strip", z.ZodTypeAny, {
61
61
  count: number;
62
- max: number;
63
62
  mean: number;
64
63
  m2: number;
64
+ max: number;
65
65
  }, {
66
66
  count: number;
67
- max: number;
68
67
  mean: number;
69
68
  m2: number;
69
+ max: number;
70
70
  }>;
71
71
  netErrors: z.ZodObject<{
72
72
  count: z.ZodNumber;
@@ -75,14 +75,14 @@ export declare const RouteEnvelopeSchema: z.ZodObject<{
75
75
  max: z.ZodNumber;
76
76
  }, "strip", z.ZodTypeAny, {
77
77
  count: number;
78
- max: number;
79
78
  mean: number;
80
79
  m2: number;
80
+ max: number;
81
81
  }, {
82
82
  count: number;
83
- max: number;
84
83
  mean: number;
85
84
  m2: number;
85
+ max: number;
86
86
  }>;
87
87
  consoleErrors: z.ZodObject<{
88
88
  count: z.ZodNumber;
@@ -91,64 +91,64 @@ export declare const RouteEnvelopeSchema: z.ZodObject<{
91
91
  max: z.ZodNumber;
92
92
  }, "strip", z.ZodTypeAny, {
93
93
  count: number;
94
- max: number;
95
94
  mean: number;
96
95
  m2: number;
96
+ max: number;
97
97
  }, {
98
98
  count: number;
99
- max: number;
100
99
  mean: number;
101
100
  m2: number;
101
+ max: number;
102
102
  }>;
103
103
  }, "strip", z.ZodTypeAny, {
104
104
  durationMs: {
105
105
  count: number;
106
- max: number;
107
106
  mean: number;
108
107
  m2: number;
108
+ max: number;
109
109
  };
110
110
  net: {
111
111
  count: number;
112
- max: number;
113
112
  mean: number;
114
113
  m2: number;
114
+ max: number;
115
115
  };
116
116
  consoleErrors: {
117
117
  count: number;
118
- max: number;
119
118
  mean: number;
120
119
  m2: number;
120
+ max: number;
121
121
  };
122
122
  netErrors: {
123
123
  count: number;
124
- max: number;
125
124
  mean: number;
126
125
  m2: number;
126
+ max: number;
127
127
  };
128
128
  }, {
129
129
  durationMs: {
130
130
  count: number;
131
- max: number;
132
131
  mean: number;
133
132
  m2: number;
133
+ max: number;
134
134
  };
135
135
  net: {
136
136
  count: number;
137
- max: number;
138
137
  mean: number;
139
138
  m2: number;
139
+ max: number;
140
140
  };
141
141
  consoleErrors: {
142
142
  count: number;
143
- max: number;
144
143
  mean: number;
145
144
  m2: number;
145
+ max: number;
146
146
  };
147
147
  netErrors: {
148
148
  count: number;
149
- max: number;
150
149
  mean: number;
151
150
  m2: number;
151
+ max: number;
152
152
  };
153
153
  }>;
154
154
  }, "strip", z.ZodTypeAny, {
@@ -157,27 +157,27 @@ export declare const RouteEnvelopeSchema: z.ZodObject<{
157
157
  stats: {
158
158
  durationMs: {
159
159
  count: number;
160
- max: number;
161
160
  mean: number;
162
161
  m2: number;
162
+ max: number;
163
163
  };
164
164
  net: {
165
165
  count: number;
166
- max: number;
167
166
  mean: number;
168
167
  m2: number;
168
+ max: number;
169
169
  };
170
170
  consoleErrors: {
171
171
  count: number;
172
- max: number;
173
172
  mean: number;
174
173
  m2: number;
174
+ max: number;
175
175
  };
176
176
  netErrors: {
177
177
  count: number;
178
- max: number;
179
178
  mean: number;
180
179
  m2: number;
180
+ max: number;
181
181
  };
182
182
  };
183
183
  }, {
@@ -186,27 +186,27 @@ export declare const RouteEnvelopeSchema: z.ZodObject<{
186
186
  stats: {
187
187
  durationMs: {
188
188
  count: number;
189
- max: number;
190
189
  mean: number;
191
190
  m2: number;
191
+ max: number;
192
192
  };
193
193
  net: {
194
194
  count: number;
195
- max: number;
196
195
  mean: number;
197
196
  m2: number;
197
+ max: number;
198
198
  };
199
199
  consoleErrors: {
200
200
  count: number;
201
- max: number;
202
201
  mean: number;
203
202
  m2: number;
203
+ max: number;
204
204
  };
205
205
  netErrors: {
206
206
  count: number;
207
- max: number;
208
207
  mean: number;
209
208
  m2: number;
209
+ max: number;
210
210
  };
211
211
  };
212
212
  }>;
@@ -7,6 +7,11 @@ export interface JournalSink {
7
7
  /** Read side of the durable journal — the fall-through source when the ring buffer has evicted. */
8
8
  export interface JournalReader {
9
9
  readEvents(): Promise<ReticleEvent[]>;
10
+ /**
11
+ * The action ledger. Optional because a test double is a partial reader, and nothing that only
12
+ * needs events should be forced to grow a second method to satisfy the type.
13
+ */
14
+ readActions?(): Promise<JournalAction[]>;
10
15
  }
11
16
  interface JournalRecorderOptions {
12
17
  /** Injected elapsed-ms clock (never read from Date here — the clock-injection rule). */
@@ -32,6 +37,24 @@ export declare class JournalRecorder {
32
37
  beginAction(actionId: string, tool: string, args: Record<string, unknown>): void;
33
38
  /** Close the active window: persist its buffered events, then the action record. No-op if none active. */
34
39
  finishAction(effect?: unknown, settled?: boolean, settledInMs?: number): void;
40
+ /**
41
+ * Append one action record for a tool that PROVED something without driving the page.
42
+ *
43
+ * `reticle_assert` produces a verdict and moves nothing, so it must never open an attribution
44
+ * window: while one is open every observed event is stamped with that action's id, and stamping
45
+ * ambient traffic with an assertion's id would manufacture a causal link that does not exist —
46
+ * exactly what `EventAttribution.WINDOW` is carefully labelled to avoid claiming. So this writes
47
+ * the record and leaves `#active` precisely as it found it: an act window open across an assert
48
+ * keeps attributing to itself, which is the truth of what caused those events.
49
+ *
50
+ * `settled` is left unset for the same reason — nothing was dispatched, so there is no settle
51
+ * outcome to report, and the run fold skips actions that state none rather than inventing one.
52
+ *
53
+ * Ordering rides the same chain as `finishAction`: buffered events flush first, so a record is
54
+ * always persisted after the events that preceded it. Records land in the order verdicts were
55
+ * REACHED, so an assert taken mid-window is written before the act it interrupted.
56
+ */
57
+ recordAction(actionId: string, tool: string, args: Record<string, unknown>, effect?: unknown): void;
35
58
  /** Persist any buffered events now (call on session end). Awaits the write chain to settle. */
36
59
  flush(): Promise<void>;
37
60
  }
@@ -65,6 +65,41 @@ export class JournalRecorder {
65
65
  this.#enqueueFlush();
66
66
  this.#chain = this.#chain.then(() => this.#sink.appendAction(action)).catch(() => undefined);
67
67
  }
68
+ /**
69
+ * Append one action record for a tool that PROVED something without driving the page.
70
+ *
71
+ * `reticle_assert` produces a verdict and moves nothing, so it must never open an attribution
72
+ * window: while one is open every observed event is stamped with that action's id, and stamping
73
+ * ambient traffic with an assertion's id would manufacture a causal link that does not exist —
74
+ * exactly what `EventAttribution.WINDOW` is carefully labelled to avoid claiming. So this writes
75
+ * the record and leaves `#active` precisely as it found it: an act window open across an assert
76
+ * keeps attributing to itself, which is the truth of what caused those events.
77
+ *
78
+ * `settled` is left unset for the same reason — nothing was dispatched, so there is no settle
79
+ * outcome to report, and the run fold skips actions that state none rather than inventing one.
80
+ *
81
+ * Ordering rides the same chain as `finishAction`: buffered events flush first, so a record is
82
+ * always persisted after the events that preceded it. Records land in the order verdicts were
83
+ * REACHED, so an assert taken mid-window is written before the act it interrupted.
84
+ */
85
+ recordAction(actionId, tool, args, effect) {
86
+ const at = this.#now();
87
+ const action = {
88
+ v: JOURNAL_FILE_VERSION,
89
+ actionId,
90
+ tool,
91
+ args,
92
+ effect,
93
+ // No window was opened, so no event carries this id: `seqRange` is omitted rather than
94
+ // invented, because any range here would claim an attribution that never happened. `tRange` is
95
+ // required by the schema and the honest value is the instant the verdict was recorded — a span
96
+ // of zero, which is exactly how much time this action attributed events over.
97
+ tRange: { from: at, to: at },
98
+ at,
99
+ };
100
+ this.#enqueueFlush();
101
+ this.#chain = this.#chain.then(() => this.#sink.appendAction(action)).catch(() => undefined);
102
+ }
68
103
  /** Persist any buffered events now (call on session end). Awaits the write chain to settle. */
69
104
  async flush() {
70
105
  this.#enqueueFlush();