@reticlehq/server 2.10.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.
- package/SKILL.md +73 -11
- package/dist/bridge/bridge.d.ts +0 -13
- package/dist/bridge/bridge.js +1 -1
- package/dist/bridge/bridge.test-harness.d.ts +2 -0
- package/dist/bridge/bridge.test-harness.js +8 -1
- package/dist/capsule/capsule-store.d.ts +2 -1
- package/dist/capsule/capsule-store.js +1 -1
- package/dist/capsule/causal-summary.d.ts +2 -2
- package/dist/chromium-launch-options.d.ts +28 -0
- package/dist/chromium-launch-options.js +29 -0
- package/dist/cli/cli-doctor.js +8 -0
- package/dist/cli/cli-flow-commands.d.ts +3 -1
- package/dist/cli/cli-flow-commands.js +34 -4
- package/dist/cli/cli-kill.d.ts +2 -1
- package/dist/cli/cli-parse.d.ts +3 -1
- package/dist/cli/cli-parse.js +29 -7
- package/dist/cli/cli-update-commands.js +20 -1
- package/dist/cli/cli-verify.d.ts +1 -1
- package/dist/cli/cli-verify.js +2 -2
- package/dist/cli/cloud-cli.js +12 -6
- package/dist/cli/config-discovery.d.ts +2 -1
- package/dist/cli/doctor-rows.d.ts +3 -1
- package/dist/cli/doctor-rows.js +3 -1
- package/dist/cli/gate-exit.d.ts +21 -0
- package/dist/cli/gate-exit.js +21 -0
- package/dist/cli/gate-hook-message.d.ts +33 -0
- package/dist/cli/gate-hook-message.js +46 -0
- package/dist/cli/mcp-command.d.ts +25 -0
- package/dist/cli/mcp-command.js +143 -0
- package/dist/cli/sibling-ports.d.ts +37 -0
- package/dist/cli/sibling-ports.js +56 -0
- package/dist/cli.js +37 -110
- package/dist/cloud/cloud-config.d.ts +2 -1
- package/dist/cloud/cloud-sync.d.ts +11 -0
- package/dist/cloud/cloud-sync.js +32 -0
- package/dist/crawl/crawl.d.ts +9 -13
- package/dist/crawl/crawl.js +20 -3
- package/dist/daemon/daemon-resolve.d.ts +79 -0
- package/dist/daemon/daemon-resolve.js +118 -0
- package/dist/daemon/free-port.d.ts +29 -0
- package/dist/daemon/free-port.js +55 -0
- package/dist/daemon/heartbeat.d.ts +0 -4
- package/dist/daemon/heartbeat.js +2 -2
- package/dist/dev/stale-issue-guard.d.ts +41 -0
- package/dist/dev/stale-issue-guard.js +75 -0
- package/dist/domain/domain-tools.js +3 -2
- package/dist/events/contradictions.d.ts +41 -0
- package/dist/events/contradictions.js +125 -12
- package/dist/events/declared.d.ts +14 -0
- package/dist/events/declared.js +33 -0
- package/dist/events/echo-mismatch.js +14 -0
- package/dist/events/observed-in-window.d.ts +12 -0
- package/dist/events/observed-in-window.js +17 -0
- package/dist/events/predicate-asks.d.ts +30 -0
- package/dist/events/predicate-asks.js +48 -0
- package/dist/events/predicate-eval.js +254 -25
- package/dist/events/predicate-parse.d.ts +0 -6
- package/dist/events/predicate-parse.js +51 -5
- package/dist/events/predicate-schema.d.ts +62 -0
- package/dist/events/predicate-schema.js +162 -4
- package/dist/events/predicate.d.ts +15 -0
- package/dist/events/predicate.js +41 -2
- package/dist/flows/coverage.d.ts +0 -8
- package/dist/flows/coverage.js +1 -1
- package/dist/flows/decision.d.ts +5 -1
- package/dist/flows/decision.js +33 -7
- package/dist/flows/flake.d.ts +0 -2
- package/dist/flows/flake.js +1 -1
- package/dist/flows/flow-intent.d.ts +52 -0
- package/dist/flows/flow-intent.js +123 -0
- package/dist/flows/flow-replay-run.js +27 -4
- package/dist/flows/flow-success.js +15 -3
- package/dist/flows/flow-tools.js +65 -8
- package/dist/flows/flows.d.ts +6 -1
- package/dist/flows/flows.js +34 -22
- package/dist/flows/predicate-to-expect.js +14 -3
- package/dist/flows/server-verify.js +16 -2
- package/dist/flows/verify-change-tools.js +9 -2
- package/dist/honesty/blind-spots.d.ts +8 -16
- package/dist/honesty/blind-spots.js +18 -5
- package/dist/honesty/feature-capture.d.ts +150 -0
- package/dist/honesty/feature-capture.js +233 -0
- package/dist/honesty/gap-ledger.d.ts +52 -0
- package/dist/honesty/gap-ledger.js +63 -0
- package/dist/honesty/instrumentation-gaps.d.ts +79 -0
- package/dist/honesty/instrumentation-gaps.js +77 -0
- package/dist/honesty/observability.d.ts +37 -0
- package/dist/honesty/observability.js +40 -0
- package/dist/honesty/tool-hit-rate.d.ts +86 -0
- package/dist/honesty/tool-hit-rate.js +90 -0
- package/dist/honesty/undeclared-change.d.ts +51 -0
- package/dist/honesty/undeclared-change.js +56 -0
- package/dist/honesty/verified.d.ts +15 -0
- package/dist/honesty/verified.js +68 -4
- package/dist/http-server.d.ts +0 -1
- package/dist/http-server.js +1 -1
- package/dist/hunt/hunt-report.d.ts +0 -2
- package/dist/hunt/hunt-report.js +1 -1
- package/dist/impact/impact-store.d.ts +0 -5
- package/dist/impact/impact-store.js +1 -1
- package/dist/index.d.ts +4 -2
- package/dist/index.js +41 -0
- package/dist/init/agent-rules.d.ts +17 -0
- package/dist/init/agent-rules.js +31 -1
- package/dist/init/capabilities.d.ts +0 -2
- package/dist/init/capabilities.js +1 -1
- package/dist/init/closing-hint.d.ts +36 -0
- package/dist/init/closing-hint.js +120 -0
- package/dist/init/confirm.d.ts +0 -25
- package/dist/init/confirm.js +3 -3
- package/dist/init/cra.d.ts +14 -1
- package/dist/init/cra.js +43 -12
- package/dist/init/dev-script.d.ts +48 -0
- package/dist/init/dev-script.js +63 -0
- package/dist/init/mcp.d.ts +16 -5
- package/dist/init/mcp.js +26 -10
- package/dist/init/node-io.js +3 -0
- package/dist/init/plan-framework.d.ts +0 -5
- package/dist/init/plan-framework.js +83 -9
- package/dist/init/plan.d.ts +10 -30
- package/dist/init/plan.js +30 -15
- package/dist/init/refresh-rules.d.ts +39 -0
- package/dist/init/refresh-rules.js +51 -0
- package/dist/init/run.d.ts +2 -10
- package/dist/init/run.js +26 -68
- package/dist/init/slash-command.d.ts +1 -1
- package/dist/init/slash-command.js +33 -10
- package/dist/init/snippets.js +53 -10
- package/dist/input/real-input.d.ts +2 -0
- package/dist/input/real-input.js +3 -2
- package/dist/input/scroll-find.d.ts +7 -4
- package/dist/input/scroll-find.js +63 -25
- package/dist/intent/inline-intent.d.ts +46 -0
- package/dist/intent/inline-intent.js +103 -0
- package/dist/intent/intent-store.d.ts +37 -0
- package/dist/intent/intent-store.js +112 -0
- package/dist/intent/intent-tools.d.ts +2 -0
- package/dist/intent/intent-tools.js +80 -0
- package/dist/intent/open-intents.d.ts +21 -0
- package/dist/intent/open-intents.js +25 -0
- package/dist/journal/envelope.d.ts +2 -2
- package/dist/journal/journal-recorder.d.ts +23 -0
- package/dist/journal/journal-recorder.js +35 -0
- package/dist/license/license.d.ts +8 -2
- package/dist/license/license.js +10 -15
- package/dist/mcp/attach-memory.d.ts +67 -0
- package/dist/mcp/attach-memory.js +142 -0
- package/dist/mcp/mcp-proxy.d.ts +0 -7
- package/dist/mcp/mcp-proxy.js +14 -2
- package/dist/mcp/mcp.d.ts +2 -0
- package/dist/mcp/mcp.js +45 -23
- package/dist/mcp/server-instructions.js +45 -3
- package/dist/mcp/tool-catalog-cache.d.ts +8 -0
- package/dist/mcp/tool-catalog-cache.js +10 -0
- package/dist/oracles/flow-instrument-gaps.d.ts +4 -4
- package/dist/oracles/flow-instrument-gaps.js +4 -5
- package/dist/oracles/self-instrument.d.ts +9 -9
- package/dist/oracles/self-instrument.js +19 -13
- package/dist/pool/browser-pool.d.ts +12 -1
- package/dist/pool/browser-pool.js +31 -1
- package/dist/pool/playwright-launcher.js +3 -1
- package/dist/project/artifact-root.d.ts +83 -0
- package/dist/project/artifact-root.js +112 -0
- package/dist/project/fs-port.d.ts +3 -0
- package/dist/project/fs-port.js +3 -2
- package/dist/project/memory-fs.d.ts +9 -0
- package/dist/project/memory-fs.js +93 -0
- package/dist/project/project-store.d.ts +17 -0
- package/dist/project/project-store.js +33 -0
- package/dist/project/project-tools.js +3 -32
- package/dist/project/remember-project.d.ts +24 -0
- package/dist/project/remember-project.js +40 -0
- package/dist/project/reticle-dir.d.ts +2 -0
- package/dist/project/reticle-dir.js +1 -0
- package/dist/project/session-root.d.ts +15 -0
- package/dist/project/session-root.js +31 -0
- package/dist/runs/context-tools.d.ts +2 -0
- package/dist/runs/context-tools.js +89 -0
- package/dist/runs/run-context.d.ts +40 -0
- package/dist/runs/run-context.js +158 -0
- package/dist/runs/run-diff.d.ts +2 -1
- package/dist/runs/run-tools.js +2 -1
- package/dist/runs/verification-sync.js +2 -2
- package/dist/session/dev-server-probe.d.ts +0 -8
- package/dist/session/dev-server-probe.js +1 -1
- package/dist/session/last-act.d.ts +7 -1
- package/dist/session/last-act.js +2 -2
- package/dist/session/lease-availability.d.ts +40 -0
- package/dist/session/lease-availability.js +43 -0
- package/dist/session/live-control-tools.js +1 -42
- package/dist/session/no-session-diagnosis.d.ts +20 -0
- package/dist/session/no-session-diagnosis.js +41 -12
- package/dist/session/no-session-next-action.d.ts +5 -0
- package/dist/session/no-session-next-action.js +35 -3
- package/dist/session/no-session-watch.d.ts +8 -0
- package/dist/session/no-session-watch.js +83 -65
- package/dist/session/output-budget.js +4 -1
- package/dist/session/session-health.d.ts +9 -0
- package/dist/session/session-health.js +23 -0
- package/dist/session/session-manager.js +50 -1
- package/dist/session/session-successor.d.ts +37 -0
- package/dist/session/session-successor.js +78 -0
- package/dist/session/session.d.ts +40 -1
- package/dist/session/session.js +84 -2
- package/dist/session/stall-clock.d.ts +18 -0
- package/dist/session/stall-clock.js +48 -0
- package/dist/telemetry/automation-hint.d.ts +2 -1
- package/dist/telemetry/daemon-telemetry.js +12 -23
- package/dist/telemetry/feedback-outbox.d.ts +2 -1
- package/dist/telemetry/project-profile.d.ts +0 -8
- package/dist/telemetry/project-profile.js +1 -1
- package/dist/telemetry/run-telemetry.d.ts +0 -2
- package/dist/telemetry/run-telemetry.js +1 -1
- package/dist/telemetry/session-metrics.d.ts +1 -1
- package/dist/telemetry/session-metrics.js +1 -1
- package/dist/telemetry/telemetry.d.ts +12 -3
- package/dist/telemetry/telemetry.js +25 -5
- package/dist/telemetry/verification-of.js +2 -2
- package/dist/tools/act-tools.js +159 -24
- package/dist/tools/assert-source.d.ts +22 -0
- package/dist/tools/assert-source.js +53 -0
- package/dist/tools/assert-verdict.d.ts +21 -2
- package/dist/tools/assert-verdict.js +59 -4
- package/dist/tools/browser-tools.js +0 -20
- package/dist/tools/complete-snapshot.d.ts +48 -0
- package/dist/tools/complete-snapshot.js +75 -0
- package/dist/tools/contract-tools.js +5 -3
- package/dist/tools/coverage-tools.d.ts +10 -1
- package/dist/tools/coverage-tools.js +124 -37
- package/dist/tools/dynamic-tools.js +29 -8
- package/dist/tools/error-recovery.d.ts +24 -14
- package/dist/tools/error-recovery.js +70 -9
- package/dist/tools/feedback-tools.d.ts +1 -1
- package/dist/tools/feedback-tools.js +2 -2
- package/dist/tools/invoke-tool.js +62 -2
- package/dist/tools/lease-hint.d.ts +10 -0
- package/dist/tools/lease-hint.js +53 -4
- package/dist/tools/lease-tools.d.ts +5 -1
- package/dist/tools/lease-tools.js +22 -8
- package/dist/tools/navigate-arrival.d.ts +0 -2
- package/dist/tools/navigate-arrival.js +1 -1
- package/dist/tools/observe-tools.js +70 -27
- package/dist/tools/read-tools.js +5 -3
- package/dist/tools/real-input-attempt.d.ts +22 -0
- package/dist/tools/real-input-attempt.js +153 -1
- package/dist/tools/reconcile-tools.js +18 -4
- package/dist/tools/resolve-target.d.ts +0 -7
- package/dist/tools/sdk-marker-probe.d.ts +29 -0
- package/dist/tools/sdk-marker-probe.js +57 -0
- package/dist/tools/snapshot-delta.d.ts +1 -1
- package/dist/tools/tool-kit.d.ts +28 -1
- package/dist/tools/tool-kit.js +16 -0
- package/dist/tools/tool-names.d.ts +3 -0
- package/dist/tools/tool-names.js +3 -0
- package/dist/tools/tool-surface.d.ts +85 -6
- package/dist/tools/tool-surface.js +122 -2
- package/dist/tools/tools.js +22 -4
- package/dist/tools/unadvertised-help.js +4 -4
- package/dist/tools/verify-next-baton.d.ts +53 -0
- package/dist/tools/verify-next-baton.js +52 -0
- package/dist/update/update-nudge.d.ts +7 -10
- package/dist/update/update-nudge.js +12 -3
- package/dist/update/updater.d.ts +1 -1
- package/docs/README.md +3 -1
- package/docs/cli/cloud.mdx +1 -1
- package/docs/cli/doctor.mdx +1 -0
- package/docs/cli/verify.mdx +4 -5
- package/docs/desktop.mdx +1 -1
- package/docs/docs.json +3 -3
- package/docs/enterprise.md +3 -1
- package/docs/gates.md +1 -1
- package/docs/install-manual.mdx +1 -1
- package/docs/license-activation.mdx +145 -0
- package/docs/packages/browser.mdx +2 -2
- package/docs/packages/server.mdx +1 -1
- package/docs/packages.mdx +8 -6
- package/docs/platform-integration.md +17 -0
- package/docs/predicates.mdx +8 -0
- package/docs/reticle-manual.md +106 -0
- package/docs/telemetry-contract.md +1 -12
- package/docs/telemetry-events.mdx +1 -34
- package/docs/telemetry.md +0 -1
- package/docs/tools-tools-and-run.mdx +2 -4
- package/docs/troubleshooting.mdx +9 -0
- package/docs/usage.md +3 -2
- package/package.json +2 -2
- package/dist/session/session-readiness.d.ts +0 -34
- package/dist/session/session-readiness.js +0 -36
- package/dist/telemetry/instrumentation-stall.d.ts +0 -57
- package/dist/telemetry/instrumentation-stall.js +0 -103
|
@@ -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,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
|
-
|
|
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
|
-
|
|
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. */
|
|
@@ -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();
|
|
@@ -111,7 +111,7 @@ export interface GateContext {
|
|
|
111
111
|
* Gate an enterprise feature. No-op in dev/eval (requireLicense:false). In production it throws
|
|
112
112
|
* EnterpriseLicenseError unless a valid, unexpired key that covers `feature` is present.
|
|
113
113
|
*/
|
|
114
|
-
export declare function assertEnterprise(feature: string, ctx: GateContext): void;
|
|
114
|
+
export declare function assertEnterprise(feature: string, ctx: GateContext, env?: NodeJS.ProcessEnv, baked?: string): void;
|
|
115
115
|
/** Env names that carry the activation: the operator's key, and the issuer public key baked at release. */
|
|
116
116
|
export declare const LICENSE_KEY_ENV = "RETICLE_LICENSE_KEY";
|
|
117
117
|
export declare const LICENSE_PUBLIC_KEY_ENV = "RETICLE_LICENSE_PUBLIC_KEY";
|
|
@@ -131,7 +131,13 @@ export interface LicenseReport {
|
|
|
131
131
|
* neither, so it told an interested reader to set a variable without saying what it would unlock or
|
|
132
132
|
* how to obtain it. A dead end at exactly the moment somebody is asking.
|
|
133
133
|
*/
|
|
134
|
-
|
|
134
|
+
/**
|
|
135
|
+
* What a licence unlocks in this build. Present on every branch a PROSPECTIVE customer lands on,
|
|
136
|
+
* because being told to set a variable with no idea what it unlocks is the dead end this exists to
|
|
137
|
+
* close. Absent once a licence is `active`: that reader has already bought, so naming features at
|
|
138
|
+
* them reveals the product surface and answers nothing they asked.
|
|
139
|
+
*/
|
|
140
|
+
gated?: readonly string[];
|
|
135
141
|
contact: string;
|
|
136
142
|
/**
|
|
137
143
|
* The key is valid, and scoped to features this build does not gate, so it unlocks nothing here.
|
package/dist/license/license.js
CHANGED
|
@@ -114,26 +114,18 @@ export class EnterpriseLicenseError extends Error {
|
|
|
114
114
|
this.reason = reason;
|
|
115
115
|
}
|
|
116
116
|
}
|
|
117
|
-
/** The issuer public key from the environment (set at release / by the operator); undefined if unset. */
|
|
118
|
-
function issuerPublicKey() {
|
|
119
|
-
const pem = process.env[LICENSE_PUBLIC_KEY_ENV];
|
|
120
|
-
if (pem === undefined || 0 === pem.length)
|
|
121
|
-
return undefined;
|
|
122
|
-
try {
|
|
123
|
-
return createPublicKey(pem);
|
|
124
|
-
}
|
|
125
|
-
catch {
|
|
126
|
-
return undefined;
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
117
|
/**
|
|
130
118
|
* Gate an enterprise feature. No-op in dev/eval (requireLicense:false). In production it throws
|
|
131
119
|
* EnterpriseLicenseError unless a valid, unexpired key that covers `feature` is present.
|
|
132
120
|
*/
|
|
133
|
-
export function assertEnterprise(feature, ctx) {
|
|
121
|
+
export function assertEnterprise(feature, ctx, env = process.env, baked) {
|
|
134
122
|
if (!ctx.requireLicense)
|
|
135
123
|
return;
|
|
136
|
-
|
|
124
|
+
// Resolved exactly as the env-entry point resolves it: BAKED first, env only as the dev/test
|
|
125
|
+
// hatch. This path used to read the environment alone, so on a real release, where the key is
|
|
126
|
+
// baked and no env var is set, it denied every valid licence with `no-issuer-key` while
|
|
127
|
+
// assertEnterpriseFromEnv allowed the same key on the same build. Two gates, one truth.
|
|
128
|
+
const publicKey = ctx.publicKey ?? loadPublicKey(resolveIssuerPublicKeyPem(env, baked));
|
|
137
129
|
if (publicKey === undefined)
|
|
138
130
|
throw new EnterpriseLicenseError(feature, 'no-issuer-key');
|
|
139
131
|
const check = verifyLicenseKey(ctx.key, publicKey, ctx.now());
|
|
@@ -225,7 +217,10 @@ export function describeLicense(now, env = process.env, baked = BAKED_ISSUER_PUB
|
|
|
225
217
|
? `licensed to ${org} (${plan}), expires ${new Date(exp).toISOString()}. This key covers ${features?.join(', ')}, and nothing this build gates (${LICENSE_OFFER.gated.join(', ')}) is included. Contact ${LICENSE_CONTACT}`
|
|
226
218
|
: `licensed to ${org} (${plan}), expires ${new Date(exp).toISOString()}`;
|
|
227
219
|
return {
|
|
228
|
-
|
|
220
|
+
contact: LICENSE_CONTACT,
|
|
221
|
+
// The one active case that still needs the list: a key covering nothing this build gates is
|
|
222
|
+
// reported here, and the sentence explaining it is unreadable without both sides named.
|
|
223
|
+
...(coversNothingHere ? { gated: LICENSE_OFFER.gated } : {}),
|
|
229
224
|
status: LicenseActivation.ACTIVE,
|
|
230
225
|
licenseId: lid,
|
|
231
226
|
org,
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Two durable bits about the CLIENT side of the install: has an MCP client ever started Reticle's
|
|
3
|
+
* MCP server on this port, and has one ever asked it for the tool list.
|
|
4
|
+
*
|
|
5
|
+
* Some hosts register the server, start it, and then never send `tools/list` until the client is
|
|
6
|
+
* restarted. The agent has no Reticle tools, the user has no message, and from our side that install
|
|
7
|
+
* is byte-identical to one somebody wired and abandoned: daemon idle, no sessions, nothing to say.
|
|
8
|
+
* `status` could report "registered" and "connected"; it could not report the state in between,
|
|
9
|
+
* which is the one with an answer ("restart your client").
|
|
10
|
+
*
|
|
11
|
+
* Both bits record something that HAPPENED — the proxy ran, a `tools/list` arrived — never an
|
|
12
|
+
* inference from a config file. A config entry proves somebody wrote a line of JSON; it does not
|
|
13
|
+
* prove a client ever read it, which is precisely the thing in question here.
|
|
14
|
+
*
|
|
15
|
+
* Deliberately the same shape as `connection-memory.ts`: one small JSON file per port under the
|
|
16
|
+
* daemon's state home, best-effort in both directions. It is a HINT, so every failure degrades to
|
|
17
|
+
* "we do not know" rather than throwing — a diagnostic that can crash the process it is diagnosing
|
|
18
|
+
* is worse than no diagnostic at all.
|
|
19
|
+
*
|
|
20
|
+
* KNOWN LIMIT, and why the messages below say so out loud: the record is per PORT, not per client.
|
|
21
|
+
* Two clients on one machine share it, so a healthy one masks a broken one, and the record itself
|
|
22
|
+
* only begins on the version that introduced it — a long-working install reads as never attached
|
|
23
|
+
* until its client next starts the server. Both limits are stated in the text the user reads,
|
|
24
|
+
* because an honest "here is what I can see, here is what to check" beats a confident wrong no.
|
|
25
|
+
*/
|
|
26
|
+
/** What the client side of this install has been observed doing on this port. */
|
|
27
|
+
export declare const AttachState: {
|
|
28
|
+
/** No client has started Reticle's MCP server here, as far as this record goes. */
|
|
29
|
+
readonly NEVER_ATTACHED: "never-attached";
|
|
30
|
+
/** A client started it and never asked for the tool list — the state that needs a restart. */
|
|
31
|
+
readonly NEVER_ENUMERATED: "never-enumerated";
|
|
32
|
+
/** Started and enumerated. Nothing to say. */
|
|
33
|
+
readonly ENUMERATED: "enumerated";
|
|
34
|
+
};
|
|
35
|
+
export type AttachState = (typeof AttachState)[keyof typeof AttachState];
|
|
36
|
+
/** Record that an MCP client started the proxy on this port. */
|
|
37
|
+
export declare function rememberProxyStarted(stateDir: string, port: number): void;
|
|
38
|
+
/**
|
|
39
|
+
* Record that a client asked for the tool list on this port.
|
|
40
|
+
*
|
|
41
|
+
* Enumeration implies attachment — a `tools/list` cannot arrive at a server nobody started — so this
|
|
42
|
+
* sets both bits, and a record written by an older proxy that only ever set one is still read
|
|
43
|
+
* correctly.
|
|
44
|
+
*/
|
|
45
|
+
export declare function rememberEnumerated(stateDir: string, port: number): void;
|
|
46
|
+
/** Which of the three client-side states this port is in. */
|
|
47
|
+
export declare function attachState(stateDir: string, port: number): AttachState;
|
|
48
|
+
/**
|
|
49
|
+
* The sentence to print for a state, or `undefined` when there is nothing to fix.
|
|
50
|
+
*
|
|
51
|
+
* The healthy case has to stay silent: advice printed beside a working client reads as though
|
|
52
|
+
* something is still wrong, which is its own kind of lie.
|
|
53
|
+
*/
|
|
54
|
+
export declare function describeAttachState(state: AttachState): string | undefined;
|
|
55
|
+
/** What `reticle status` reports about the client side of the install. */
|
|
56
|
+
export interface AttachStatusFields {
|
|
57
|
+
mcpClient: AttachState;
|
|
58
|
+
mcpClientAction?: string;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* The client-side fields for one `status` run.
|
|
62
|
+
*
|
|
63
|
+
* The state is reported on EVERY run, healthy included — "the client has read the tools" is the
|
|
64
|
+
* fact that was missing, and reporting it only when broken leaves the reader unable to tell a
|
|
65
|
+
* healthy client from a version too old to look. The advice rides along only when there is any.
|
|
66
|
+
*/
|
|
67
|
+
export declare function attachStatusFields(stateDir: string, port: number): AttachStatusFields;
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Two durable bits about the CLIENT side of the install: has an MCP client ever started Reticle's
|
|
3
|
+
* MCP server on this port, and has one ever asked it for the tool list.
|
|
4
|
+
*
|
|
5
|
+
* Some hosts register the server, start it, and then never send `tools/list` until the client is
|
|
6
|
+
* restarted. The agent has no Reticle tools, the user has no message, and from our side that install
|
|
7
|
+
* is byte-identical to one somebody wired and abandoned: daemon idle, no sessions, nothing to say.
|
|
8
|
+
* `status` could report "registered" and "connected"; it could not report the state in between,
|
|
9
|
+
* which is the one with an answer ("restart your client").
|
|
10
|
+
*
|
|
11
|
+
* Both bits record something that HAPPENED — the proxy ran, a `tools/list` arrived — never an
|
|
12
|
+
* inference from a config file. A config entry proves somebody wrote a line of JSON; it does not
|
|
13
|
+
* prove a client ever read it, which is precisely the thing in question here.
|
|
14
|
+
*
|
|
15
|
+
* Deliberately the same shape as `connection-memory.ts`: one small JSON file per port under the
|
|
16
|
+
* daemon's state home, best-effort in both directions. It is a HINT, so every failure degrades to
|
|
17
|
+
* "we do not know" rather than throwing — a diagnostic that can crash the process it is diagnosing
|
|
18
|
+
* is worse than no diagnostic at all.
|
|
19
|
+
*
|
|
20
|
+
* KNOWN LIMIT, and why the messages below say so out loud: the record is per PORT, not per client.
|
|
21
|
+
* Two clients on one machine share it, so a healthy one masks a broken one, and the record itself
|
|
22
|
+
* only begins on the version that introduced it — a long-working install reads as never attached
|
|
23
|
+
* until its client next starts the server. Both limits are stated in the text the user reads,
|
|
24
|
+
* because an honest "here is what I can see, here is what to check" beats a confident wrong no.
|
|
25
|
+
*/
|
|
26
|
+
import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
27
|
+
import { join } from 'node:path';
|
|
28
|
+
/** What the client side of this install has been observed doing on this port. */
|
|
29
|
+
export const AttachState = {
|
|
30
|
+
/** No client has started Reticle's MCP server here, as far as this record goes. */
|
|
31
|
+
NEVER_ATTACHED: 'never-attached',
|
|
32
|
+
/** A client started it and never asked for the tool list — the state that needs a restart. */
|
|
33
|
+
NEVER_ENUMERATED: 'never-enumerated',
|
|
34
|
+
/** Started and enumerated. Nothing to say. */
|
|
35
|
+
ENUMERATED: 'enumerated',
|
|
36
|
+
};
|
|
37
|
+
/**
|
|
38
|
+
* What to do about a client that started Reticle and never listed its tools.
|
|
39
|
+
*
|
|
40
|
+
* Names the cause AND the action, like the rest of the doctor/status vocabulary, and names its own
|
|
41
|
+
* blind spot: this record cannot see WHICH client asked, so it must not tell somebody running two
|
|
42
|
+
* of them that the one in front of them is the broken one.
|
|
43
|
+
*/
|
|
44
|
+
const NEVER_ENUMERATED_ACTION = 'an MCP client started Reticle on this port and never asked for the tool list, so the tools are ' +
|
|
45
|
+
'here and your client has not read them — restart your MCP client, which is what makes it list ' +
|
|
46
|
+
'them again. This record is per port, not per client, so with more than one client registered ' +
|
|
47
|
+
'the one that never listed may not be the one you are looking at.';
|
|
48
|
+
/**
|
|
49
|
+
* What to do when nothing has ever attached.
|
|
50
|
+
*
|
|
51
|
+
* The hedge is not padding. This record begins the first time a client starts the server on a
|
|
52
|
+
* version that keeps it, so an install that has worked for months reads as never attached until its
|
|
53
|
+
* client next starts Reticle — and telling that user their server is unregistered would send them
|
|
54
|
+
* to re-run `init` over a working setup.
|
|
55
|
+
*/
|
|
56
|
+
const NEVER_ATTACHED_ACTION = 'no MCP client has started Reticle on this port, so either the server is not registered with your ' +
|
|
57
|
+
'client or nothing has attached since this record began — register it with `npx ' +
|
|
58
|
+
'@reticlehq/server init`, then restart your MCP client and run status again.';
|
|
59
|
+
const EMPTY = { attached: false, enumerated: false };
|
|
60
|
+
function memoryPath(stateDir, port) {
|
|
61
|
+
return join(stateDir, `attach-${String(port)}.json`);
|
|
62
|
+
}
|
|
63
|
+
/** The record for a port. Empty on absent, unreadable, or malformed state. */
|
|
64
|
+
function read(stateDir, port) {
|
|
65
|
+
try {
|
|
66
|
+
const parsed = JSON.parse(readFileSync(memoryPath(stateDir, port), 'utf8'));
|
|
67
|
+
if ('object' !== typeof parsed || null === parsed)
|
|
68
|
+
return EMPTY;
|
|
69
|
+
const record = parsed;
|
|
70
|
+
return {
|
|
71
|
+
attached: true === record.attached,
|
|
72
|
+
enumerated: true === record.enumerated,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
return EMPTY;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
/** Write the record back, best-effort. A hint must never be the reason a proxy fails to serve. */
|
|
80
|
+
function write(stateDir, port, record) {
|
|
81
|
+
try {
|
|
82
|
+
mkdirSync(stateDir, { recursive: true });
|
|
83
|
+
writeFileSync(memoryPath(stateDir, port), JSON.stringify(record), 'utf8');
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
// An unwritable state home is already reported elsewhere; it must not break the client path.
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
/** Record that an MCP client started the proxy on this port. */
|
|
90
|
+
export function rememberProxyStarted(stateDir, port) {
|
|
91
|
+
const known = read(stateDir, port);
|
|
92
|
+
if (known.attached)
|
|
93
|
+
return;
|
|
94
|
+
write(stateDir, port, { ...known, attached: true });
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Record that a client asked for the tool list on this port.
|
|
98
|
+
*
|
|
99
|
+
* Enumeration implies attachment — a `tools/list` cannot arrive at a server nobody started — so this
|
|
100
|
+
* sets both bits, and a record written by an older proxy that only ever set one is still read
|
|
101
|
+
* correctly.
|
|
102
|
+
*/
|
|
103
|
+
export function rememberEnumerated(stateDir, port) {
|
|
104
|
+
const known = read(stateDir, port);
|
|
105
|
+
if (known.enumerated)
|
|
106
|
+
return;
|
|
107
|
+
write(stateDir, port, { attached: true, enumerated: true });
|
|
108
|
+
}
|
|
109
|
+
/** Which of the three client-side states this port is in. */
|
|
110
|
+
export function attachState(stateDir, port) {
|
|
111
|
+
const known = read(stateDir, port);
|
|
112
|
+
if (known.enumerated)
|
|
113
|
+
return AttachState.ENUMERATED;
|
|
114
|
+
return known.attached ? AttachState.NEVER_ENUMERATED : AttachState.NEVER_ATTACHED;
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* The sentence to print for a state, or `undefined` when there is nothing to fix.
|
|
118
|
+
*
|
|
119
|
+
* The healthy case has to stay silent: advice printed beside a working client reads as though
|
|
120
|
+
* something is still wrong, which is its own kind of lie.
|
|
121
|
+
*/
|
|
122
|
+
export function describeAttachState(state) {
|
|
123
|
+
if (state === AttachState.NEVER_ENUMERATED)
|
|
124
|
+
return NEVER_ENUMERATED_ACTION;
|
|
125
|
+
if (state === AttachState.NEVER_ATTACHED)
|
|
126
|
+
return NEVER_ATTACHED_ACTION;
|
|
127
|
+
return undefined;
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* The client-side fields for one `status` run.
|
|
131
|
+
*
|
|
132
|
+
* The state is reported on EVERY run, healthy included — "the client has read the tools" is the
|
|
133
|
+
* fact that was missing, and reporting it only when broken leaves the reader unable to tell a
|
|
134
|
+
* healthy client from a version too old to look. The advice rides along only when there is any.
|
|
135
|
+
*/
|
|
136
|
+
export function attachStatusFields(stateDir, port) {
|
|
137
|
+
const state = attachState(stateDir, port);
|
|
138
|
+
const action = describeAttachState(state);
|
|
139
|
+
// OMITTED rather than undefined: `exactOptionalPropertyTypes` is on, and an absent key is also
|
|
140
|
+
// what keeps the healthy line to one fact.
|
|
141
|
+
return { mcpClient: state, ...(action === undefined ? {} : { mcpClientAction: action }) };
|
|
142
|
+
}
|
package/dist/mcp/mcp-proxy.d.ts
CHANGED
|
@@ -47,13 +47,6 @@ export declare class PendingRequests {
|
|
|
47
47
|
* with the MCP server still perfectly alive. An agent cannot tell that apart from "still working".
|
|
48
48
|
*/
|
|
49
49
|
export declare function streamLossReplies(pending: PendingRequests, reason: string, nextStep?: string): string[];
|
|
50
|
-
/**
|
|
51
|
-
* The one shape every "the transport ate your call" answer takes, whichever leg died.
|
|
52
|
-
*
|
|
53
|
-
* `nextStep` is appended when the caller knows one. Without it this reply describes a condition and
|
|
54
|
-
* stops there, which is all a first-run user got out of their first ever tool call.
|
|
55
|
-
*/
|
|
56
|
-
export declare function transportLossReply(id: unknown, reason: string, nextStep?: string): string;
|
|
57
50
|
/**
|
|
58
51
|
* How long a request may sit in the queue, unsent, before the proxy answers it itself.
|
|
59
52
|
*
|