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