@reticlehq/server 2.8.0 → 2.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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/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-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/events/contradictions.d.ts +32 -0
- package/dist/events/contradictions.js +9 -5
- package/dist/events/declared.d.ts +45 -0
- package/dist/events/declared.js +73 -0
- package/dist/events/echo-mismatch.d.ts +8 -2
- 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/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/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 +51 -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 +7 -0
- package/dist/index.js +44 -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 +15 -0
- package/dist/init/mcp.js +19 -0
- 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/mcp/mcp-proxy.d.ts +5 -66
- package/dist/mcp/mcp-proxy.js +180 -163
- package/dist/mcp/mcp.d.ts +10 -1
- package/dist/mcp/mcp.js +47 -27
- package/dist/mcp/proxy-handshake.d.ts +82 -1
- package/dist/mcp/proxy-handshake.js +109 -2
- 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/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 +48 -2
- package/dist/session/no-session-diagnosis.js +176 -20
- 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 +165 -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.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 +24 -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 +50 -0
- package/dist/telemetry/instrumentation-stall.js +90 -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 +13 -2
- 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 +16 -7
- package/dist/tools/error-recovery.js +9 -5
- package/dist/tools/invoke-tool.js +20 -3
- 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 +8 -0
- package/dist/tools/tool-surface.d.ts +60 -0
- package/dist/tools/tool-surface.js +85 -19
- package/dist/tools/tools.js +45 -4
- 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 +4 -2
- package/docs/architecture.md +6 -0
- package/docs/benchmarks.md +60 -12
- package/docs/best-practices.mdx +11 -6
- package/docs/capabilities.mdx +201 -54
- 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 +18 -2
- 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 +30 -1
- package/docs/desktop.mdx +276 -80
- package/docs/docs.json +13 -12
- package/docs/enterprise.md +2 -0
- package/docs/faq.mdx +49 -35
- package/docs/fixtures.md +2 -0
- package/docs/flows.md +3 -1
- package/docs/for-agents.md +3 -3
- package/docs/frameworks.mdx +46 -7
- package/docs/gate-plan.md +4 -2
- package/docs/gates.md +7 -5
- 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 +62 -22
- 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 +16 -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 +30 -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 +40 -9
- package/docs/tools-wait-for.mdx +34 -15
- package/docs/troubleshooting.mdx +80 -10
- package/docs/usage.md +43 -21
- 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,47 @@
|
|
|
1
|
+
import { type ImpactCounts, type ImpactScope, type ImpactSnapshot } from '@reticlehq/core';
|
|
2
|
+
export interface ImpactPaths {
|
|
3
|
+
project: string;
|
|
4
|
+
global: string;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* `reticleRoot` is the project's own `.reticle` directory (what every other store here is handed);
|
|
8
|
+
* the global scope always lives beside the daemon's own state in `~/.reticle`.
|
|
9
|
+
*/
|
|
10
|
+
export declare function impactPaths(reticleRoot: string): ImpactPaths;
|
|
11
|
+
/** Read a scope, tolerating absence and corruption: a broken file starts over rather than throwing. */
|
|
12
|
+
export declare function readScope(path: string, now: number): ImpactScope;
|
|
13
|
+
/** YYYY-MM-DD in local time - the day boundary a person recognises, not UTC's. */
|
|
14
|
+
export declare function isoDay(now: number): string;
|
|
15
|
+
/**
|
|
16
|
+
* What a fold knows beyond the counters.
|
|
17
|
+
*
|
|
18
|
+
* `runMs` is a SESSION's lifetime, and it only ever feeds the "longest run" record. It is not a
|
|
19
|
+
* counter: adding it to a total would answer a question nobody asked, and the record used to be fed
|
|
20
|
+
* a single tool call's duration instead, which made the report's superlative a number of
|
|
21
|
+
* milliseconds that never grew past one click.
|
|
22
|
+
*/
|
|
23
|
+
export interface ImpactFoldMeta {
|
|
24
|
+
runMs?: number;
|
|
25
|
+
}
|
|
26
|
+
/** Fold one delta into a scope: totals, today's bucket, records and streak. Pure. */
|
|
27
|
+
export declare function applyDelta(scope: ImpactScope, delta: Partial<ImpactCounts>, now: number, meta?: ImpactFoldMeta): ImpactScope;
|
|
28
|
+
/**
|
|
29
|
+
* The live impact record for one project.
|
|
30
|
+
*
|
|
31
|
+
* Holds both scopes in memory, folds deltas in synchronously (so a reader always sees the truth),
|
|
32
|
+
* and flushes to disk on a debounce.
|
|
33
|
+
*/
|
|
34
|
+
export declare class ImpactStore {
|
|
35
|
+
#private;
|
|
36
|
+
constructor(opts: {
|
|
37
|
+
reticleRoot: string;
|
|
38
|
+
projectName?: string;
|
|
39
|
+
now?: () => number;
|
|
40
|
+
});
|
|
41
|
+
/** Notified after every fold, so the HUD can be pushed a fresh snapshot. */
|
|
42
|
+
onChange(fn: () => void): void;
|
|
43
|
+
record(delta: Partial<ImpactCounts>, meta?: ImpactFoldMeta): void;
|
|
44
|
+
snapshot(): ImpactSnapshot;
|
|
45
|
+
/** Write now (process exit, tests). */
|
|
46
|
+
flush(): void;
|
|
47
|
+
}
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import { mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { dirname, join } from 'node:path';
|
|
3
|
+
import { homedir } from 'node:os';
|
|
4
|
+
import { ReticleDir, IMPACT_DAILY_BUCKETS, IMPACT_SCHEMA_VERSION, ImpactScopeSchema, addImpactCounts, emptyImpactCounts, emptyImpactRecords, estimateImpactSavings, } from '@reticlehq/core';
|
|
5
|
+
/**
|
|
6
|
+
* Where Reticle keeps the record of what it has done for you.
|
|
7
|
+
*
|
|
8
|
+
* Two scopes, two files, both local and never uploaded: the project's own `.reticle/impact.json`,
|
|
9
|
+
* and a machine-wide `~/.reticle/impact.json` that answers "what has Reticle done for me overall"
|
|
10
|
+
* across every app you have instrumented.
|
|
11
|
+
*
|
|
12
|
+
* This is NOT telemetry. Telemetry answers our questions about the product and leaves the machine;
|
|
13
|
+
* this answers the user's question about their own work and never does.
|
|
14
|
+
*/
|
|
15
|
+
/** Writes are debounced: a verification loop is 50-200 calls, and each one is a counter bump. */
|
|
16
|
+
const WRITE_DEBOUNCE_MS = 800;
|
|
17
|
+
/**
|
|
18
|
+
* `reticleRoot` is the project's own `.reticle` directory (what every other store here is handed);
|
|
19
|
+
* the global scope always lives beside the daemon's own state in `~/.reticle`.
|
|
20
|
+
*/
|
|
21
|
+
export function impactPaths(reticleRoot) {
|
|
22
|
+
return {
|
|
23
|
+
project: join(reticleRoot, ReticleDir.IMPACT_FILE),
|
|
24
|
+
global: join(homedir(), ReticleDir.ROOT, ReticleDir.IMPACT_FILE),
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
function emptyScope(now) {
|
|
28
|
+
const counts = emptyImpactCounts();
|
|
29
|
+
return {
|
|
30
|
+
counts,
|
|
31
|
+
days: [],
|
|
32
|
+
records: emptyImpactRecords(),
|
|
33
|
+
savings: estimateImpactSavings(counts),
|
|
34
|
+
since: now,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
/** Read a scope, tolerating absence and corruption: a broken file starts over rather than throwing. */
|
|
38
|
+
export function readScope(path, now) {
|
|
39
|
+
try {
|
|
40
|
+
const parsed = JSON.parse(readFileSync(path, 'utf8'));
|
|
41
|
+
const result = ImpactScopeSchema.safeParse(parsed);
|
|
42
|
+
return result.success ? result.data : emptyScope(now);
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
return emptyScope(now);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Write a scope atomically.
|
|
50
|
+
*
|
|
51
|
+
* tmp + rename, because two daemons can serve two apps in the same repo at once and a half-written
|
|
52
|
+
* counters file is one the next read discards - silently losing the whole history.
|
|
53
|
+
*/
|
|
54
|
+
function writeScope(path, scope) {
|
|
55
|
+
try {
|
|
56
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
57
|
+
const tmp = `${path}.${String(process.pid)}.tmp`;
|
|
58
|
+
writeFileSync(tmp, JSON.stringify(scope, null, 2), 'utf8');
|
|
59
|
+
renameSync(tmp, path);
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
// A stats file that cannot be written must never break a tool call. The counters stay in
|
|
63
|
+
// memory and the next write attempt carries them.
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
/** YYYY-MM-DD in local time - the day boundary a person recognises, not UTC's. */
|
|
67
|
+
export function isoDay(now) {
|
|
68
|
+
const d = new Date(now);
|
|
69
|
+
const month = String(d.getMonth() + 1).padStart(2, '0');
|
|
70
|
+
const day = String(d.getDate()).padStart(2, '0');
|
|
71
|
+
return `${String(d.getFullYear())}-${month}-${day}`;
|
|
72
|
+
}
|
|
73
|
+
/** Whether `b` is the calendar day right after `a` - the streak rule. */
|
|
74
|
+
function isNextDay(a, b) {
|
|
75
|
+
const prev = new Date(`${a}T00:00:00`);
|
|
76
|
+
const next = new Date(`${b}T00:00:00`);
|
|
77
|
+
return 1 === Math.round((next.getTime() - prev.getTime()) / 86_400_000);
|
|
78
|
+
}
|
|
79
|
+
/** Fold one delta into a scope: totals, today's bucket, records and streak. Pure. */
|
|
80
|
+
export function applyDelta(scope, delta, now, meta = {}) {
|
|
81
|
+
const today = isoDay(now);
|
|
82
|
+
const counts = addImpactCounts(scope.counts, delta);
|
|
83
|
+
const days = scope.days.slice();
|
|
84
|
+
const last = days[days.length - 1];
|
|
85
|
+
if (last !== undefined && last.date === today) {
|
|
86
|
+
days[days.length - 1] = { date: today, counts: addImpactCounts(last.counts, delta) };
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
days.push({ date: today, counts: addImpactCounts(emptyImpactCounts(), delta) });
|
|
90
|
+
}
|
|
91
|
+
while (days.length > IMPACT_DAILY_BUCKETS)
|
|
92
|
+
days.shift();
|
|
93
|
+
const todayCounts = days[days.length - 1]?.counts ?? emptyImpactCounts();
|
|
94
|
+
const previousDay = days[days.length - 2];
|
|
95
|
+
const records = { ...scope.records };
|
|
96
|
+
records.bestVerdictDay = Math.max(records.bestVerdictDay, todayCounts.verdicts);
|
|
97
|
+
records.bestDefectDay = Math.max(records.bestDefectDay, todayCounts.failed);
|
|
98
|
+
if (last === undefined || last.date !== today) {
|
|
99
|
+
// A new day joins the streak only if it follows yesterday; otherwise it starts a new one.
|
|
100
|
+
records.streakDays =
|
|
101
|
+
previousDay !== undefined && isNextDay(previousDay.date, today) ? records.streakDays + 1 : 1;
|
|
102
|
+
}
|
|
103
|
+
else if (0 === records.streakDays) {
|
|
104
|
+
records.streakDays = 1;
|
|
105
|
+
}
|
|
106
|
+
records.bestStreakDays = Math.max(records.bestStreakDays, records.streakDays);
|
|
107
|
+
records.longestRunMs = Math.max(records.longestRunMs, meta.runMs ?? 0);
|
|
108
|
+
return { counts, days, records, savings: estimateImpactSavings(counts), since: scope.since };
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* The live impact record for one project.
|
|
112
|
+
*
|
|
113
|
+
* Holds both scopes in memory, folds deltas in synchronously (so a reader always sees the truth),
|
|
114
|
+
* and flushes to disk on a debounce.
|
|
115
|
+
*/
|
|
116
|
+
export class ImpactStore {
|
|
117
|
+
#paths;
|
|
118
|
+
#now;
|
|
119
|
+
#projectName;
|
|
120
|
+
#project;
|
|
121
|
+
#global;
|
|
122
|
+
#timer;
|
|
123
|
+
#onChange;
|
|
124
|
+
constructor(opts) {
|
|
125
|
+
this.#paths = impactPaths(opts.reticleRoot);
|
|
126
|
+
this.#now = opts.now ?? (() => Date.now());
|
|
127
|
+
this.#projectName = opts.projectName;
|
|
128
|
+
const now = this.#now();
|
|
129
|
+
this.#project = readScope(this.#paths.project, now);
|
|
130
|
+
this.#global = readScope(this.#paths.global, now);
|
|
131
|
+
}
|
|
132
|
+
/** Notified after every fold, so the HUD can be pushed a fresh snapshot. */
|
|
133
|
+
onChange(fn) {
|
|
134
|
+
this.#onChange = fn;
|
|
135
|
+
}
|
|
136
|
+
record(delta, meta = {}) {
|
|
137
|
+
const now = this.#now();
|
|
138
|
+
this.#project = applyDelta(this.#project, delta, now, meta);
|
|
139
|
+
this.#global = applyDelta(this.#global, delta, now, meta);
|
|
140
|
+
this.#scheduleFlush();
|
|
141
|
+
this.#onChange?.();
|
|
142
|
+
}
|
|
143
|
+
snapshot() {
|
|
144
|
+
const snap = {
|
|
145
|
+
schemaVersion: IMPACT_SCHEMA_VERSION,
|
|
146
|
+
project: this.#project,
|
|
147
|
+
global: this.#global,
|
|
148
|
+
};
|
|
149
|
+
if (this.#projectName !== undefined)
|
|
150
|
+
snap.projectName = this.#projectName;
|
|
151
|
+
return snap;
|
|
152
|
+
}
|
|
153
|
+
/** Write now (process exit, tests). */
|
|
154
|
+
flush() {
|
|
155
|
+
if (this.#timer !== undefined) {
|
|
156
|
+
clearTimeout(this.#timer);
|
|
157
|
+
this.#timer = undefined;
|
|
158
|
+
}
|
|
159
|
+
writeScope(this.#paths.project, this.#project);
|
|
160
|
+
writeScope(this.#paths.global, this.#global);
|
|
161
|
+
}
|
|
162
|
+
#scheduleFlush() {
|
|
163
|
+
if (this.#timer !== undefined)
|
|
164
|
+
return;
|
|
165
|
+
this.#timer = setTimeout(() => {
|
|
166
|
+
this.#timer = undefined;
|
|
167
|
+
this.flush();
|
|
168
|
+
}, WRITE_DEBOUNCE_MS);
|
|
169
|
+
this.#timer.unref?.();
|
|
170
|
+
}
|
|
171
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -123,6 +123,13 @@ export interface RunningServer {
|
|
|
123
123
|
agentAttached?: () => boolean;
|
|
124
124
|
/** The pairing token the bridge is enforcing (explicit, env, or auto-provisioned); undefined if none. */
|
|
125
125
|
token?: string;
|
|
126
|
+
/**
|
|
127
|
+
* Tell every attached MCP proxy this daemon is retiring, before anything closes.
|
|
128
|
+
*
|
|
129
|
+
* Optional because only the daemon entry point serves MCP over SSE — `start()` speaks stdio, where
|
|
130
|
+
* the client owns the process and there is no stream to warn. See SharedServer.announceShutdown.
|
|
131
|
+
*/
|
|
132
|
+
announceShutdown?: () => void;
|
|
126
133
|
close: () => Promise<void>;
|
|
127
134
|
}
|
|
128
135
|
export { resolveBridgeSecurity } from './bridge/bridge-security.js';
|
package/dist/index.js
CHANGED
|
@@ -11,6 +11,7 @@ import { resolveBridgeSecurityWithAutoToken } from './bridge/bridge-security.js'
|
|
|
11
11
|
import { Bridge } from './bridge/bridge.js';
|
|
12
12
|
import { BaselineStore } from './project/baselines.js';
|
|
13
13
|
import { RecordingStore } from './flows/recordings.js';
|
|
14
|
+
import { initImpact } from './impact/impact-recorder.js';
|
|
14
15
|
import { FlowStore } from './flows/flows.js';
|
|
15
16
|
import { buildFlowChips } from './flows/flow-scope.js';
|
|
16
17
|
import { ProjectStore } from './project/project-store.js';
|
|
@@ -23,6 +24,8 @@ import { createRunnerPort } from './runs/runner-port.js';
|
|
|
23
24
|
import { RunStore } from './runs/run-store.js';
|
|
24
25
|
import { startVerifyServer } from './runs/verify-server.js';
|
|
25
26
|
import { createMcpServer } from './mcp/mcp.js';
|
|
27
|
+
import { LEASE_ACQUIRE_TOOL } from './tools/lease-tools.js';
|
|
28
|
+
import { runTool } from './tools/invoke-tool.js';
|
|
26
29
|
import { SessionReaper, endAllSessions, MCP_DISCONNECT_SUMMARY } from './session/session-reaper.js';
|
|
27
30
|
import { wireSessionScope } from './session/no-session-watch.js';
|
|
28
31
|
import { buildIdlePredicate } from './daemon/daemon-usefulness.js';
|
|
@@ -34,9 +37,13 @@ import { BrowserPool } from './pool/browser-pool.js';
|
|
|
34
37
|
import { playwrightLauncher, resolveMaxContexts } from './pool/playwright-launcher.js';
|
|
35
38
|
import { LeaseReaper } from './pool/lease-reaper.js';
|
|
36
39
|
import { readJournalEnabled, readProjectId } from './cli/cli-port.js';
|
|
40
|
+
import { hasProjectConnectedBefore } from './session/connection-memory.js';
|
|
41
|
+
import { reticleStateHome } from './daemon/daemon.js';
|
|
42
|
+
import { probeChromium } from './cli/chromium-hint.js';
|
|
37
43
|
import { makeJournalAttach } from './journal/attach-journal.js';
|
|
38
44
|
import { makeSessionEnd } from './journal/session-end.js';
|
|
39
45
|
import { AmbientStore } from './journal/ambient-store.js';
|
|
46
|
+
import { ensureWorkspaceGitignore } from './journal/workspace-gitignore.js';
|
|
40
47
|
import { pruneSessions } from './journal/retention.js';
|
|
41
48
|
import { log } from './log.js';
|
|
42
49
|
/** A human-facing one-liner for a panel replay verdict — ✓ passed / ⚠ drifted / ✗ errored. */
|
|
@@ -172,8 +179,14 @@ function attachJournal(bridge, deps) {
|
|
|
172
179
|
});
|
|
173
180
|
// Teardown: flush the journal tail to disk + persist what this session learned.
|
|
174
181
|
bridge.attachSessionEnd(makeSessionEnd(deps));
|
|
175
|
-
if (deps.enabled)
|
|
182
|
+
if (deps.enabled) {
|
|
176
183
|
void pruneSessions(deps.fs, deps.reticleRoot);
|
|
184
|
+
// Here rather than in `init`, because this is the moment we are actually about to write into
|
|
185
|
+
// somebody's repository — and the paths that reach it without ever running `init` (a plugin
|
|
186
|
+
// install, a hand-added client config) are exactly the ones that would otherwise leave an
|
|
187
|
+
// unexplained pile of untracked files behind. Best-effort and write-once; see the helper.
|
|
188
|
+
void ensureWorkspaceGitignore(deps.fs, deps.reticleRoot);
|
|
189
|
+
}
|
|
177
190
|
}
|
|
178
191
|
/**
|
|
179
192
|
* Resolve the drive/real-input provider from options — shared by start and startDaemon so the
|
|
@@ -224,6 +237,10 @@ async function resolveRealInput(options, onNavigateError, onNetworkDetail) {
|
|
|
224
237
|
/** Start the Reticle bridge (browser WS endpoint) and, by default, the MCP stdio server. */
|
|
225
238
|
export async function start(options = {}) {
|
|
226
239
|
const port = options.port ?? RETICLE_DEFAULT_PORT;
|
|
240
|
+
// Open the user's impact record before anything can connect. Not inside the MCP branch: a daemon
|
|
241
|
+
// serving a browser with no agent attached still has a HUD to answer, and a report that reads
|
|
242
|
+
// "nothing recorded yet" over a month of history on disk is the worst version of this feature.
|
|
243
|
+
initImpact({ reticleRoot: options.reticleRoot ?? join(process.cwd(), ReticleDir.ROOT) });
|
|
227
244
|
const security = await resolveBridgeSecurityWithAutoToken(options);
|
|
228
245
|
const bridge = new Bridge({ port, ...security });
|
|
229
246
|
// Server-authoritative liveness: a Node-side reaper (immune to browser throttling) ends sessions
|
|
@@ -271,12 +288,13 @@ export async function start(options = {}) {
|
|
|
271
288
|
reticleRoot,
|
|
272
289
|
now,
|
|
273
290
|
bridgePort: port,
|
|
291
|
+
browserProbe: probeChromium,
|
|
274
292
|
// The daemon's OWN project, so a tool can tell "this session is mine" from "this session
|
|
275
293
|
// belongs to a sibling app under the same daemon". contract_save refuses on the second.
|
|
276
294
|
...(activeProjectId === undefined ? {} : { projectId: activeProjectId }),
|
|
277
295
|
};
|
|
278
296
|
const profile = resolveToolSurface(options.toolProfile);
|
|
279
|
-
const server = createMcpServer(realInput !== undefined ? { ...deps, realInput } : deps, profile);
|
|
297
|
+
const server = createMcpServer(realInput !== undefined ? { ...deps, realInput } : deps, profile, hasProjectConnectedBefore(reticleStateHome(), port, activeProjectId));
|
|
280
298
|
// When the agent (the MCP client) disconnects cleanly, end every active session at once so the
|
|
281
299
|
// HUD doesn't linger. (If the agent instead KILLS this process, the WS dies and the browser
|
|
282
300
|
// self-ends via SESSION_LIFECYCLE.BRIDGE_LOST_MS — see transport.ts.)
|
|
@@ -308,6 +326,12 @@ export async function start(options = {}) {
|
|
|
308
326
|
*/
|
|
309
327
|
export async function startDaemon(options = {}) {
|
|
310
328
|
const port = options.port ?? RETICLE_DEFAULT_PORT;
|
|
329
|
+
// The SAME line as in `start`, because these are two entry points that each wire their own world
|
|
330
|
+
// and the daemon is the one that actually serves people. Wired only in `start`, the impact record
|
|
331
|
+
// was never opened in the process the HUD talks to: tool calls still recorded (the dispatch
|
|
332
|
+
// chokepoint opens it lazily), but a tab that connected before the first tool call was pushed
|
|
333
|
+
// nothing, so the report read "nothing recorded yet" over a file with history in it.
|
|
334
|
+
initImpact({ reticleRoot: options.reticleRoot ?? join(process.cwd(), ReticleDir.ROOT) });
|
|
311
335
|
const security = await resolveBridgeSecurityWithAutoToken(options);
|
|
312
336
|
const shared = createSharedServer(security.token === undefined ? {} : { token: security.token });
|
|
313
337
|
const bridge = new Bridge({ port, server: shared.httpServer, ...security });
|
|
@@ -332,13 +356,6 @@ export async function startDaemon(options = {}) {
|
|
|
332
356
|
});
|
|
333
357
|
const reaper = new SessionReaper(bridge.sessions);
|
|
334
358
|
reaper.start();
|
|
335
|
-
// Scope auto-selection to the active project (from .reticle.json) so a stray tab from another app is
|
|
336
|
-
// never picked when the agent omits a sessionId. Explicit per-call scope/sessionId still overrides.
|
|
337
|
-
// Scope + the no-session diagnosis: "no browser session connected" is the error that ends most
|
|
338
|
-
// sessions, and the agent is told to check two things it cannot see. See no-session-diagnosis.ts.
|
|
339
|
-
// The pool reader is passed lazily: `pool` is assigned above but may be undefined when this
|
|
340
|
-
// daemon runs without one, and the diagnosis must degrade to the tab message rather than lie.
|
|
341
|
-
wireSessionScope(bridge.sessions, readProjectId(process.cwd()), port, () => pool?.reapedLeaseCount() ?? 0);
|
|
342
359
|
const { realInput, owned } = await resolveRealInput(options, () => shared.close(), makeNetworkDetailRouter(bridge, options.driveUrl));
|
|
343
360
|
const fs = createNodeFileSystem();
|
|
344
361
|
const reticleRoot = options.reticleRoot ?? join(process.cwd(), ReticleDir.ROOT);
|
|
@@ -352,6 +369,14 @@ export async function startDaemon(options = {}) {
|
|
|
352
369
|
const pool = createBrowserPool(options.headless ?? true);
|
|
353
370
|
const leaseReaper = new LeaseReaper(pool);
|
|
354
371
|
leaseReaper.start();
|
|
372
|
+
// Scope auto-selection to the active project (from .reticle.json) so a stray tab from another app is
|
|
373
|
+
// never picked when the agent omits a sessionId. Explicit per-call scope/sessionId still overrides.
|
|
374
|
+
// Scope + the no-session diagnosis: "no browser session connected" is the error that ends most
|
|
375
|
+
// sessions, and the agent is told to check two things it cannot see. See no-session-diagnosis.ts.
|
|
376
|
+
// Ordered AFTER the pool: the watch may open a browser itself the moment it finds a listening dev
|
|
377
|
+
// server for a wired project (see no-session-watch.ts), and it opens it through the pool — the same
|
|
378
|
+
// path reticle_lease takes, in this process, binding nothing.
|
|
379
|
+
wireSessionScope(bridge.sessions, readProjectId(process.cwd()), port, () => pool.reapedLeaseCount(), (url) => pool.acquire(url));
|
|
355
380
|
const deps = {
|
|
356
381
|
sessions: bridge.sessions,
|
|
357
382
|
pool,
|
|
@@ -364,10 +389,18 @@ export async function startDaemon(options = {}) {
|
|
|
364
389
|
reticleRoot,
|
|
365
390
|
now,
|
|
366
391
|
bridgePort: port,
|
|
392
|
+
browserProbe: probeChromium,
|
|
367
393
|
};
|
|
368
394
|
const profile = resolveToolSurface(options.toolProfile);
|
|
369
395
|
const effectiveDeps = realInput !== undefined ? { ...deps, realInput } : deps;
|
|
370
|
-
|
|
396
|
+
// Read per attach, not once: a project that gets wired while this daemon is alive should stop
|
|
397
|
+
// being told to wire itself on the next agent that connects.
|
|
398
|
+
shared.attachMcp(() => createMcpServer(effectiveDeps, profile, hasProjectConnectedBefore(reticleStateHome(), port, readProjectId(process.cwd()))));
|
|
399
|
+
// `reticle drive <url>` when this daemon already owns the port: it asks HERE instead of trying to
|
|
400
|
+
// bind a port we are holding, and gets the same pooled context an agent's reticle_lease returns —
|
|
401
|
+
// through runTool, so it is counted and reported like any other call rather than being a second,
|
|
402
|
+
// invisible dispatch path. See cli/drive-attach.ts for why attaching beats refereeing the race.
|
|
403
|
+
shared.attachDrive((url) => runTool(LEASE_ACQUIRE_TOOL, effectiveDeps, { url }));
|
|
371
404
|
// Optional OEM/CI verify endpoint: a host platform POSTs to /verify and gets an ReticleVerificationRun,
|
|
372
405
|
// driving the same flow-replay machinery the agent uses — no MCP stdio, no human. Each verdict is
|
|
373
406
|
// persisted via RunStore. Localhost-bound + token-guarded. Off unless `reticle serve --http`.
|
|
@@ -454,6 +487,7 @@ export async function startDaemon(options = {}) {
|
|
|
454
487
|
// Exposed so the shutdown watcher can give an ATTACHED daemon a longer grace. The predicate above
|
|
455
488
|
// says WHETHER it is idle; this says how long that quiet should be tolerated — see idle-grace.
|
|
456
489
|
agentAttached: () => agentConnected,
|
|
490
|
+
announceShutdown: () => shared.announceShutdown(),
|
|
457
491
|
close: async () => {
|
|
458
492
|
reaper.stop();
|
|
459
493
|
await cleanupCaptureDirectories();
|
|
@@ -47,6 +47,26 @@ export declare const CURSOR_RULE_PATH = ".cursor/rules/reticle.mdc";
|
|
|
47
47
|
* the package's own name so the two can never drift apart.
|
|
48
48
|
*/
|
|
49
49
|
export declare const CLI: string;
|
|
50
|
+
/**
|
|
51
|
+
* Who starts the dev server, stated once and rendered everywhere it is needed.
|
|
52
|
+
*
|
|
53
|
+
* The old rule was "Reticle never starts the dev server for you — that's your job", and agents obeyed
|
|
54
|
+
* it exactly: they read it, declined to start anything, and ended the turn with nothing verified. An
|
|
55
|
+
* instruction that reliably produces no verification is a defect in the instruction.
|
|
56
|
+
*
|
|
57
|
+
* The AGENT starts it, not the daemon. A daemon that spawns a build process is invisible to the
|
|
58
|
+
* person whose machine it is running on, was never consented to, and orphans when the daemon exits.
|
|
59
|
+
* The agent already has shell access in this repo, already runs install and build commands, and runs
|
|
60
|
+
* inside a host that asks the human for permission — so a dev server it starts is visible in the
|
|
61
|
+
* transcript, attributable, and stoppable.
|
|
62
|
+
*
|
|
63
|
+
* The guards are the whole reason this is safe, so no renderer may drop them: adopt what is already
|
|
64
|
+
* listening, read the command rather than invent one, never kill, background it and SAY so, and leave
|
|
65
|
+
* the permission prompt to the host.
|
|
66
|
+
*
|
|
67
|
+
* Exported because slash-command.ts renders the same rule; there must not be two versions of it.
|
|
68
|
+
*/
|
|
69
|
+
export declare const DEV_SERVER_POLICY = "**If no dev server is listening, start one yourself.** Read the project's own dev script out of `package.json` (`dev`, `start`, whatever this project calls it), run it in the BACKGROUND, tell the user in one line that it is running and how to stop it, then carry on. Stopping to ask is how a verification turn ends with nothing verified.\n\nFive guards, none optional:\n\n1. **Never start a second one.** If something is already listening on the app's port, use it.\n2. **Never guess the command.** It comes from `package.json` scripts. If there is no recognisable dev script, say so and stop rather than inventing one.\n3. **Never kill anything.** Not a dev server, not a daemon, not a port holder \u2014 including one you started.\n4. **Background it, and say so.** The user must know a server is running and how to stop it. A dev server the human does not know about is the same failure one step later.\n5. **The permission prompt belongs to your host.** Never try to bypass, suppress or auto-approve it, and take a refusal as the answer.";
|
|
50
70
|
/** The full-rules file, written verbatim (no markers: `init` owns this file entirely). */
|
|
51
71
|
export declare function reticleMdFile(): string;
|
|
52
72
|
export declare const AgentRuleStatus: {
|
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
|
}
|