@reticlehq/server 2.10.0 → 2.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/SKILL.md +73 -11
- package/dist/bridge/bridge.d.ts +0 -13
- package/dist/bridge/bridge.js +1 -1
- package/dist/bridge/bridge.test-harness.d.ts +2 -0
- package/dist/bridge/bridge.test-harness.js +8 -1
- package/dist/capsule/capsule-store.d.ts +2 -1
- package/dist/capsule/capsule-store.js +1 -1
- package/dist/capsule/causal-summary.d.ts +2 -2
- package/dist/chromium-launch-options.d.ts +28 -0
- package/dist/chromium-launch-options.js +29 -0
- package/dist/cli/cli-doctor.js +8 -0
- package/dist/cli/cli-flow-commands.d.ts +3 -1
- package/dist/cli/cli-flow-commands.js +34 -4
- package/dist/cli/cli-kill.d.ts +2 -1
- package/dist/cli/cli-parse.d.ts +3 -1
- package/dist/cli/cli-parse.js +29 -7
- package/dist/cli/cli-update-commands.js +20 -1
- package/dist/cli/cli-verify.d.ts +1 -1
- package/dist/cli/cli-verify.js +2 -2
- package/dist/cli/cloud-cli.js +12 -6
- package/dist/cli/config-discovery.d.ts +2 -1
- package/dist/cli/doctor-rows.d.ts +3 -1
- package/dist/cli/doctor-rows.js +3 -1
- package/dist/cli/gate-exit.d.ts +21 -0
- package/dist/cli/gate-exit.js +21 -0
- package/dist/cli/gate-hook-message.d.ts +33 -0
- package/dist/cli/gate-hook-message.js +46 -0
- package/dist/cli/mcp-command.d.ts +25 -0
- package/dist/cli/mcp-command.js +143 -0
- package/dist/cli/sibling-ports.d.ts +37 -0
- package/dist/cli/sibling-ports.js +56 -0
- package/dist/cli.js +37 -110
- package/dist/cloud/cloud-config.d.ts +2 -1
- package/dist/cloud/cloud-sync.d.ts +11 -0
- package/dist/cloud/cloud-sync.js +32 -0
- package/dist/crawl/crawl.d.ts +9 -13
- package/dist/crawl/crawl.js +20 -3
- package/dist/daemon/daemon-resolve.d.ts +79 -0
- package/dist/daemon/daemon-resolve.js +118 -0
- package/dist/daemon/free-port.d.ts +29 -0
- package/dist/daemon/free-port.js +55 -0
- package/dist/daemon/heartbeat.d.ts +0 -4
- package/dist/daemon/heartbeat.js +2 -2
- package/dist/dev/stale-issue-guard.d.ts +41 -0
- package/dist/dev/stale-issue-guard.js +75 -0
- package/dist/domain/domain-tools.js +3 -2
- package/dist/events/contradictions.d.ts +41 -0
- package/dist/events/contradictions.js +125 -12
- package/dist/events/declared.d.ts +14 -0
- package/dist/events/declared.js +33 -0
- package/dist/events/echo-mismatch.js +14 -0
- package/dist/events/observed-in-window.d.ts +12 -0
- package/dist/events/observed-in-window.js +17 -0
- package/dist/events/predicate-asks.d.ts +30 -0
- package/dist/events/predicate-asks.js +48 -0
- package/dist/events/predicate-eval.js +254 -25
- package/dist/events/predicate-parse.d.ts +0 -6
- package/dist/events/predicate-parse.js +51 -5
- package/dist/events/predicate-schema.d.ts +62 -0
- package/dist/events/predicate-schema.js +162 -4
- package/dist/events/predicate.d.ts +15 -0
- package/dist/events/predicate.js +41 -2
- package/dist/flows/coverage.d.ts +0 -8
- package/dist/flows/coverage.js +1 -1
- package/dist/flows/decision.d.ts +5 -1
- package/dist/flows/decision.js +33 -7
- package/dist/flows/flake.d.ts +0 -2
- package/dist/flows/flake.js +1 -1
- package/dist/flows/flow-intent.d.ts +52 -0
- package/dist/flows/flow-intent.js +123 -0
- package/dist/flows/flow-replay-run.js +27 -4
- package/dist/flows/flow-success.js +15 -3
- package/dist/flows/flow-tools.js +65 -8
- package/dist/flows/flows.d.ts +6 -1
- package/dist/flows/flows.js +34 -22
- package/dist/flows/predicate-to-expect.js +14 -3
- package/dist/flows/server-verify.js +16 -2
- package/dist/flows/verify-change-tools.js +9 -2
- package/dist/honesty/blind-spots.d.ts +8 -16
- package/dist/honesty/blind-spots.js +18 -5
- package/dist/honesty/feature-capture.d.ts +150 -0
- package/dist/honesty/feature-capture.js +233 -0
- package/dist/honesty/gap-ledger.d.ts +52 -0
- package/dist/honesty/gap-ledger.js +63 -0
- package/dist/honesty/instrumentation-gaps.d.ts +79 -0
- package/dist/honesty/instrumentation-gaps.js +77 -0
- package/dist/honesty/observability.d.ts +37 -0
- package/dist/honesty/observability.js +40 -0
- package/dist/honesty/tool-hit-rate.d.ts +86 -0
- package/dist/honesty/tool-hit-rate.js +90 -0
- package/dist/honesty/undeclared-change.d.ts +51 -0
- package/dist/honesty/undeclared-change.js +56 -0
- package/dist/honesty/verified.d.ts +15 -0
- package/dist/honesty/verified.js +68 -4
- package/dist/http-server.d.ts +0 -1
- package/dist/http-server.js +1 -1
- package/dist/hunt/hunt-report.d.ts +0 -2
- package/dist/hunt/hunt-report.js +1 -1
- package/dist/impact/impact-store.d.ts +0 -5
- package/dist/impact/impact-store.js +1 -1
- package/dist/index.d.ts +4 -2
- package/dist/index.js +41 -0
- package/dist/init/agent-rules.d.ts +17 -0
- package/dist/init/agent-rules.js +31 -1
- package/dist/init/capabilities.d.ts +0 -2
- package/dist/init/capabilities.js +1 -1
- package/dist/init/closing-hint.d.ts +36 -0
- package/dist/init/closing-hint.js +120 -0
- package/dist/init/confirm.d.ts +0 -25
- package/dist/init/confirm.js +3 -3
- package/dist/init/cra.d.ts +14 -1
- package/dist/init/cra.js +43 -12
- package/dist/init/dev-script.d.ts +48 -0
- package/dist/init/dev-script.js +63 -0
- package/dist/init/mcp.d.ts +16 -5
- package/dist/init/mcp.js +26 -10
- package/dist/init/node-io.js +3 -0
- package/dist/init/plan-framework.d.ts +0 -5
- package/dist/init/plan-framework.js +83 -9
- package/dist/init/plan.d.ts +10 -30
- package/dist/init/plan.js +30 -15
- package/dist/init/refresh-rules.d.ts +39 -0
- package/dist/init/refresh-rules.js +51 -0
- package/dist/init/run.d.ts +2 -10
- package/dist/init/run.js +26 -68
- package/dist/init/slash-command.d.ts +1 -1
- package/dist/init/slash-command.js +33 -10
- package/dist/init/snippets.js +53 -10
- package/dist/input/real-input.d.ts +2 -0
- package/dist/input/real-input.js +3 -2
- package/dist/input/scroll-find.d.ts +7 -4
- package/dist/input/scroll-find.js +63 -25
- package/dist/intent/inline-intent.d.ts +46 -0
- package/dist/intent/inline-intent.js +103 -0
- package/dist/intent/intent-store.d.ts +37 -0
- package/dist/intent/intent-store.js +112 -0
- package/dist/intent/intent-tools.d.ts +2 -0
- package/dist/intent/intent-tools.js +80 -0
- package/dist/intent/open-intents.d.ts +21 -0
- package/dist/intent/open-intents.js +25 -0
- package/dist/journal/envelope.d.ts +2 -2
- package/dist/journal/journal-recorder.d.ts +23 -0
- package/dist/journal/journal-recorder.js +35 -0
- package/dist/license/license.d.ts +8 -2
- package/dist/license/license.js +10 -15
- package/dist/mcp/attach-memory.d.ts +67 -0
- package/dist/mcp/attach-memory.js +142 -0
- package/dist/mcp/mcp-proxy.d.ts +0 -7
- package/dist/mcp/mcp-proxy.js +14 -2
- package/dist/mcp/mcp.d.ts +2 -0
- package/dist/mcp/mcp.js +45 -23
- package/dist/mcp/server-instructions.js +45 -3
- package/dist/mcp/tool-catalog-cache.d.ts +8 -0
- package/dist/mcp/tool-catalog-cache.js +10 -0
- package/dist/oracles/flow-instrument-gaps.d.ts +4 -4
- package/dist/oracles/flow-instrument-gaps.js +4 -5
- package/dist/oracles/self-instrument.d.ts +9 -9
- package/dist/oracles/self-instrument.js +19 -13
- package/dist/pool/browser-pool.d.ts +12 -1
- package/dist/pool/browser-pool.js +31 -1
- package/dist/pool/playwright-launcher.js +3 -1
- package/dist/project/artifact-root.d.ts +83 -0
- package/dist/project/artifact-root.js +112 -0
- package/dist/project/fs-port.d.ts +3 -0
- package/dist/project/fs-port.js +3 -2
- package/dist/project/memory-fs.d.ts +9 -0
- package/dist/project/memory-fs.js +93 -0
- package/dist/project/project-store.d.ts +17 -0
- package/dist/project/project-store.js +33 -0
- package/dist/project/project-tools.js +3 -32
- package/dist/project/remember-project.d.ts +24 -0
- package/dist/project/remember-project.js +40 -0
- package/dist/project/reticle-dir.d.ts +2 -0
- package/dist/project/reticle-dir.js +1 -0
- package/dist/project/session-root.d.ts +15 -0
- package/dist/project/session-root.js +31 -0
- package/dist/runs/context-tools.d.ts +2 -0
- package/dist/runs/context-tools.js +89 -0
- package/dist/runs/run-context.d.ts +40 -0
- package/dist/runs/run-context.js +158 -0
- package/dist/runs/run-diff.d.ts +2 -1
- package/dist/runs/run-tools.js +2 -1
- package/dist/runs/verification-sync.js +2 -2
- package/dist/session/dev-server-probe.d.ts +0 -8
- package/dist/session/dev-server-probe.js +1 -1
- package/dist/session/last-act.d.ts +7 -1
- package/dist/session/last-act.js +2 -2
- package/dist/session/lease-availability.d.ts +40 -0
- package/dist/session/lease-availability.js +43 -0
- package/dist/session/live-control-tools.js +1 -42
- package/dist/session/no-session-diagnosis.d.ts +20 -0
- package/dist/session/no-session-diagnosis.js +41 -12
- package/dist/session/no-session-next-action.d.ts +5 -0
- package/dist/session/no-session-next-action.js +35 -3
- package/dist/session/no-session-watch.d.ts +8 -0
- package/dist/session/no-session-watch.js +83 -65
- package/dist/session/output-budget.js +4 -1
- package/dist/session/session-health.d.ts +9 -0
- package/dist/session/session-health.js +23 -0
- package/dist/session/session-manager.js +50 -1
- package/dist/session/session-successor.d.ts +37 -0
- package/dist/session/session-successor.js +78 -0
- package/dist/session/session.d.ts +40 -1
- package/dist/session/session.js +84 -2
- package/dist/session/stall-clock.d.ts +18 -0
- package/dist/session/stall-clock.js +48 -0
- package/dist/telemetry/automation-hint.d.ts +2 -1
- package/dist/telemetry/daemon-telemetry.js +12 -23
- package/dist/telemetry/feedback-outbox.d.ts +2 -1
- package/dist/telemetry/project-profile.d.ts +0 -8
- package/dist/telemetry/project-profile.js +1 -1
- package/dist/telemetry/run-telemetry.d.ts +0 -2
- package/dist/telemetry/run-telemetry.js +1 -1
- package/dist/telemetry/session-metrics.d.ts +1 -1
- package/dist/telemetry/session-metrics.js +1 -1
- package/dist/telemetry/telemetry.d.ts +12 -3
- package/dist/telemetry/telemetry.js +25 -5
- package/dist/telemetry/verification-of.js +2 -2
- package/dist/tools/act-tools.js +159 -24
- package/dist/tools/assert-source.d.ts +22 -0
- package/dist/tools/assert-source.js +53 -0
- package/dist/tools/assert-verdict.d.ts +21 -2
- package/dist/tools/assert-verdict.js +59 -4
- package/dist/tools/browser-tools.js +0 -20
- package/dist/tools/complete-snapshot.d.ts +48 -0
- package/dist/tools/complete-snapshot.js +75 -0
- package/dist/tools/contract-tools.js +5 -3
- package/dist/tools/coverage-tools.d.ts +10 -1
- package/dist/tools/coverage-tools.js +124 -37
- package/dist/tools/dynamic-tools.js +29 -8
- package/dist/tools/error-recovery.d.ts +24 -14
- package/dist/tools/error-recovery.js +70 -9
- package/dist/tools/feedback-tools.d.ts +1 -1
- package/dist/tools/feedback-tools.js +2 -2
- package/dist/tools/invoke-tool.js +62 -2
- package/dist/tools/lease-hint.d.ts +10 -0
- package/dist/tools/lease-hint.js +53 -4
- package/dist/tools/lease-tools.d.ts +5 -1
- package/dist/tools/lease-tools.js +22 -8
- package/dist/tools/navigate-arrival.d.ts +0 -2
- package/dist/tools/navigate-arrival.js +1 -1
- package/dist/tools/observe-tools.js +70 -27
- package/dist/tools/read-tools.js +5 -3
- package/dist/tools/real-input-attempt.d.ts +22 -0
- package/dist/tools/real-input-attempt.js +153 -1
- package/dist/tools/reconcile-tools.js +18 -4
- package/dist/tools/resolve-target.d.ts +0 -7
- package/dist/tools/sdk-marker-probe.d.ts +29 -0
- package/dist/tools/sdk-marker-probe.js +57 -0
- package/dist/tools/snapshot-delta.d.ts +1 -1
- package/dist/tools/tool-kit.d.ts +28 -1
- package/dist/tools/tool-kit.js +16 -0
- package/dist/tools/tool-names.d.ts +3 -0
- package/dist/tools/tool-names.js +3 -0
- package/dist/tools/tool-surface.d.ts +85 -6
- package/dist/tools/tool-surface.js +122 -2
- package/dist/tools/tools.js +22 -4
- package/dist/tools/unadvertised-help.js +4 -4
- package/dist/tools/verify-next-baton.d.ts +53 -0
- package/dist/tools/verify-next-baton.js +52 -0
- package/dist/update/update-nudge.d.ts +7 -10
- package/dist/update/update-nudge.js +12 -3
- package/dist/update/updater.d.ts +1 -1
- package/docs/README.md +3 -1
- package/docs/cli/cloud.mdx +1 -1
- package/docs/cli/doctor.mdx +1 -0
- package/docs/cli/verify.mdx +4 -5
- package/docs/desktop.mdx +1 -1
- package/docs/docs.json +3 -3
- package/docs/enterprise.md +3 -1
- package/docs/gates.md +1 -1
- package/docs/install-manual.mdx +1 -1
- package/docs/license-activation.mdx +145 -0
- package/docs/packages/browser.mdx +2 -2
- package/docs/packages/server.mdx +1 -1
- package/docs/packages.mdx +8 -6
- package/docs/platform-integration.md +17 -0
- package/docs/predicates.mdx +8 -0
- package/docs/reticle-manual.md +106 -0
- package/docs/telemetry-contract.md +1 -12
- package/docs/telemetry-events.mdx +1 -34
- package/docs/telemetry.md +0 -1
- package/docs/tools-tools-and-run.mdx +2 -4
- package/docs/troubleshooting.mdx +9 -0
- package/docs/usage.md +3 -2
- package/package.json +2 -2
- package/dist/session/session-readiness.d.ts +0 -34
- package/dist/session/session-readiness.js +0 -36
- package/dist/telemetry/instrumentation-stall.d.ts +0 -57
- package/dist/telemetry/instrumentation-stall.js +0 -103
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { InstrumentationGap } from '@reticlehq/core';
|
|
2
|
+
/**
|
|
3
|
+
* Of the controls this session drove, how many could Reticle fully observe?
|
|
4
|
+
*
|
|
5
|
+
* The first honest denominator this product has ever had. `reticle_verify{action:"coverage"}`
|
|
6
|
+
* already answers "which controls did you not drive"; this answers the question underneath it —
|
|
7
|
+
* *of the ones you did drive, how many could the app actually answer for?* A high untouched-count is
|
|
8
|
+
* work remaining for the agent. A low observability is work remaining in the APP, and no amount of
|
|
9
|
+
* driving closes it.
|
|
10
|
+
*
|
|
11
|
+
* ## Two things it deliberately refuses to do
|
|
12
|
+
*
|
|
13
|
+
* **It does not report a percentage for an empty run.** 0/0 rendered as 100% is the most flattering
|
|
14
|
+
* possible reading of no evidence at all, and a coverage number that starts perfect is one nobody
|
|
15
|
+
* believes the first time it drops.
|
|
16
|
+
*
|
|
17
|
+
* **It does not count app-level gaps against a control.** An unregistered store or an unsignalled
|
|
18
|
+
* route is missing from the application, not from a button. Attributing it to whichever control
|
|
19
|
+
* happened to be driven would put the number on the wrong thing and make it move for reasons the
|
|
20
|
+
* agent cannot act on. Those gaps are reported alongside, not divided into.
|
|
21
|
+
*/
|
|
22
|
+
export interface Observability {
|
|
23
|
+
/** Distinct controls driven this session. */
|
|
24
|
+
driven: number;
|
|
25
|
+
/** How many of them Reticle could fully observe. */
|
|
26
|
+
observable: number;
|
|
27
|
+
/** Rounded percentage. OMITTED when nothing was driven — see above. */
|
|
28
|
+
percent?: number;
|
|
29
|
+
}
|
|
30
|
+
export declare function observabilityOf(drivenRefs: Iterable<string>, gaps: readonly InstrumentationGap[]): Observability;
|
|
31
|
+
export interface CoverageRegression {
|
|
32
|
+
was: number;
|
|
33
|
+
now: number;
|
|
34
|
+
}
|
|
35
|
+
export declare function coverageRegressed(best: {
|
|
36
|
+
percent: number;
|
|
37
|
+
} | undefined, current: Observability): CoverageRegression | undefined;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
export function observabilityOf(
|
|
2
|
+
// Iterable, not an array: the session holds these as a Set, and making the caller spread it would
|
|
3
|
+
// be a conversion that exists only to satisfy a signature.
|
|
4
|
+
drivenRefs, gaps) {
|
|
5
|
+
const driven = new Set(drivenRefs);
|
|
6
|
+
const impaired = new Set();
|
|
7
|
+
for (const gap of gaps) {
|
|
8
|
+
// App-level gaps carry no ref, and a gap on a control this session never drove is about some
|
|
9
|
+
// earlier route — neither belongs in a ratio over what WAS driven.
|
|
10
|
+
if (gap.ref !== undefined && driven.has(gap.ref))
|
|
11
|
+
impaired.add(gap.ref);
|
|
12
|
+
}
|
|
13
|
+
const observable = driven.size - impaired.size;
|
|
14
|
+
if (0 === driven.size)
|
|
15
|
+
return { driven: 0, observable: 0 };
|
|
16
|
+
return { driven: driven.size, observable, percent: Math.round((observable / driven.size) * 100) };
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* The floor under the number, shipped with it rather than after it.
|
|
20
|
+
*
|
|
21
|
+
* A coverage figure with no downgrade check is a figure that gets gamed, and we would be the ones
|
|
22
|
+
* teaching agents to game it: the cheapest way to stop a gap firing is to stop asserting the thing
|
|
23
|
+
* that revealed it. The same reasoning already runs for flow assertions in `assertion-tiers-store`,
|
|
24
|
+
* where a flow that passes more weakly than it used to is a finding rather than a pass.
|
|
25
|
+
*
|
|
26
|
+
* Returns undefined — not a false — when there is nothing honest to compare. A first run has no
|
|
27
|
+
* best to fall from, and a run that drove almost nothing is evidence of a short session rather than
|
|
28
|
+
* of a regression.
|
|
29
|
+
*/
|
|
30
|
+
/** Below this, a run is too small for its ratio to mean anything. */
|
|
31
|
+
const MIN_DRIVEN_TO_COMPARE = 3;
|
|
32
|
+
export function coverageRegressed(best, current) {
|
|
33
|
+
if (best === undefined)
|
|
34
|
+
return undefined;
|
|
35
|
+
if (current.percent === undefined)
|
|
36
|
+
return undefined;
|
|
37
|
+
if (current.driven < MIN_DRIVEN_TO_COMPARE)
|
|
38
|
+
return undefined;
|
|
39
|
+
return current.percent < best.percent ? { was: best.percent, now: current.percent } : undefined;
|
|
40
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which tools this session called, and how reachable each one was.
|
|
3
|
+
*
|
|
4
|
+
* The generalisation of the capture ledger: it recorded a short list of read tools to answer one
|
|
5
|
+
* feature's disproof, and the question that followed is the same question asked of the whole table —
|
|
6
|
+
* most of the surface has never been observed being called at all, and a decision about what to cut
|
|
7
|
+
* or consolidate is about to be made. This is the instrument for that decision, and nothing else.
|
|
8
|
+
*
|
|
9
|
+
* ## Why the count alone is not enough
|
|
10
|
+
*
|
|
11
|
+
* A tool that goes uncalled because nobody was told it exists and a tool that goes uncalled because
|
|
12
|
+
* nobody wants it look identical in a call count. They are opposite conclusions. So every count is
|
|
13
|
+
* reported beside the tool's REACHABILITY — advertised on the default surface, advertised only on
|
|
14
|
+
* the extended one, or reachable solely by naming it to `reticle_run` — and the interesting cell is
|
|
15
|
+
* the one where an agent found a run-only tool anyway.
|
|
16
|
+
*
|
|
17
|
+
* ## The tiers are derived, never restated
|
|
18
|
+
*
|
|
19
|
+
* `tierOfTool` reads `CORE_TOOL_NAMES` and `EXTENDED_TOOL_NAMES` directly. A tool promoted or
|
|
20
|
+
* demoted between surfaces changes tier here with nothing edited. A local copy of either list would
|
|
21
|
+
* go stale on the first move and would then be wrong about the exact axis this exists to measure —
|
|
22
|
+
* and it would be wrong silently, in a report used to justify deleting things.
|
|
23
|
+
*
|
|
24
|
+
* ## What these numbers do NOT establish
|
|
25
|
+
*
|
|
26
|
+
* - **One session is not a hit rate.** This is evidence about ONE run by ONE agent against ONE app.
|
|
27
|
+
* A hit rate is a distribution over many sessions; a single session quoted as one is an anecdote
|
|
28
|
+
* with a denominator drawn on it.
|
|
29
|
+
* - **Never-called is not useless.** A tool one team reaches for once a month in an emergency and a
|
|
30
|
+
* tool nobody has ever called are the same zero here. Frequency alone cannot separate them, so a
|
|
31
|
+
* cut decided on frequency alone will delete the wrong things.
|
|
32
|
+
* - **Absent is not zero.** A session with no ledger reports `observed: false`. It has not
|
|
33
|
+
* established that no tools were called; it has established that nothing was watching.
|
|
34
|
+
* - The tools that ran BEFORE this instrument existed in the process, or against a session this
|
|
35
|
+
* daemon adopted after the fact, are simply missing rather than counted low.
|
|
36
|
+
*/
|
|
37
|
+
/** How an agent could have reached a tool. Derived from the surface sets — see above. */
|
|
38
|
+
export declare const ToolTier: {
|
|
39
|
+
/** Advertised directly on the surface every user gets. */
|
|
40
|
+
readonly DEFAULT: "default";
|
|
41
|
+
/** Advertised only when the full schema-carrying surface is switched on. */
|
|
42
|
+
readonly EXTENDED: "extended";
|
|
43
|
+
/** Advertised nowhere: reachable only by naming it to `reticle_run`. */
|
|
44
|
+
readonly RUN_ONLY: "run-only";
|
|
45
|
+
};
|
|
46
|
+
export type ToolTier = (typeof ToolTier)[keyof typeof ToolTier];
|
|
47
|
+
/** Which surface would have put this tool in front of an agent, read off that surface's own set. */
|
|
48
|
+
export declare function tierOfTool(tool: string): ToolTier;
|
|
49
|
+
/** How many tools fell in each tier on one side of the called/never-called split. */
|
|
50
|
+
export interface TierCounts {
|
|
51
|
+
default: number;
|
|
52
|
+
extended: number;
|
|
53
|
+
runOnly: number;
|
|
54
|
+
}
|
|
55
|
+
export interface ToolHitRate {
|
|
56
|
+
/** False when this session carries no ledger — "not watched", never "nothing was called". */
|
|
57
|
+
observed: boolean;
|
|
58
|
+
/** The four-plus-two cells: every tier, called and never-called. Omitted when not observed. */
|
|
59
|
+
cells?: {
|
|
60
|
+
called: TierCounts;
|
|
61
|
+
neverCalled: TierCounts;
|
|
62
|
+
};
|
|
63
|
+
/** Each tool called this session, most-called first. */
|
|
64
|
+
called?: readonly {
|
|
65
|
+
tool: string;
|
|
66
|
+
tier: ToolTier;
|
|
67
|
+
calls: number;
|
|
68
|
+
}[];
|
|
69
|
+
/** Each tool in the table this session never called, by name. */
|
|
70
|
+
neverCalled?: readonly {
|
|
71
|
+
tool: string;
|
|
72
|
+
tier: ToolTier;
|
|
73
|
+
}[];
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Cross one session's per-tool counts with the tool table.
|
|
77
|
+
*
|
|
78
|
+
* `allTools` is the DISPATCHABLE table — the names `reticle_run` will accept — passed in rather than
|
|
79
|
+
* imported, because the table is assembled in the module that owns this tool and importing it back
|
|
80
|
+
* would close a cycle. A tool counted but absent from the table still appears, so a name the table
|
|
81
|
+
* lost is visible rather than dropped.
|
|
82
|
+
*/
|
|
83
|
+
export declare function foldToolHitRate(input: {
|
|
84
|
+
calls: ReadonlyMap<string, number> | undefined;
|
|
85
|
+
allTools: readonly string[];
|
|
86
|
+
}): ToolHitRate;
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { CORE_TOOL_NAMES, EXTENDED_TOOL_NAMES } from '../tools/tool-surface.js';
|
|
2
|
+
/**
|
|
3
|
+
* Which tools this session called, and how reachable each one was.
|
|
4
|
+
*
|
|
5
|
+
* The generalisation of the capture ledger: it recorded a short list of read tools to answer one
|
|
6
|
+
* feature's disproof, and the question that followed is the same question asked of the whole table —
|
|
7
|
+
* most of the surface has never been observed being called at all, and a decision about what to cut
|
|
8
|
+
* or consolidate is about to be made. This is the instrument for that decision, and nothing else.
|
|
9
|
+
*
|
|
10
|
+
* ## Why the count alone is not enough
|
|
11
|
+
*
|
|
12
|
+
* A tool that goes uncalled because nobody was told it exists and a tool that goes uncalled because
|
|
13
|
+
* nobody wants it look identical in a call count. They are opposite conclusions. So every count is
|
|
14
|
+
* reported beside the tool's REACHABILITY — advertised on the default surface, advertised only on
|
|
15
|
+
* the extended one, or reachable solely by naming it to `reticle_run` — and the interesting cell is
|
|
16
|
+
* the one where an agent found a run-only tool anyway.
|
|
17
|
+
*
|
|
18
|
+
* ## The tiers are derived, never restated
|
|
19
|
+
*
|
|
20
|
+
* `tierOfTool` reads `CORE_TOOL_NAMES` and `EXTENDED_TOOL_NAMES` directly. A tool promoted or
|
|
21
|
+
* demoted between surfaces changes tier here with nothing edited. A local copy of either list would
|
|
22
|
+
* go stale on the first move and would then be wrong about the exact axis this exists to measure —
|
|
23
|
+
* and it would be wrong silently, in a report used to justify deleting things.
|
|
24
|
+
*
|
|
25
|
+
* ## What these numbers do NOT establish
|
|
26
|
+
*
|
|
27
|
+
* - **One session is not a hit rate.** This is evidence about ONE run by ONE agent against ONE app.
|
|
28
|
+
* A hit rate is a distribution over many sessions; a single session quoted as one is an anecdote
|
|
29
|
+
* with a denominator drawn on it.
|
|
30
|
+
* - **Never-called is not useless.** A tool one team reaches for once a month in an emergency and a
|
|
31
|
+
* tool nobody has ever called are the same zero here. Frequency alone cannot separate them, so a
|
|
32
|
+
* cut decided on frequency alone will delete the wrong things.
|
|
33
|
+
* - **Absent is not zero.** A session with no ledger reports `observed: false`. It has not
|
|
34
|
+
* established that no tools were called; it has established that nothing was watching.
|
|
35
|
+
* - The tools that ran BEFORE this instrument existed in the process, or against a session this
|
|
36
|
+
* daemon adopted after the fact, are simply missing rather than counted low.
|
|
37
|
+
*/
|
|
38
|
+
/** How an agent could have reached a tool. Derived from the surface sets — see above. */
|
|
39
|
+
export const ToolTier = {
|
|
40
|
+
/** Advertised directly on the surface every user gets. */
|
|
41
|
+
DEFAULT: 'default',
|
|
42
|
+
/** Advertised only when the full schema-carrying surface is switched on. */
|
|
43
|
+
EXTENDED: 'extended',
|
|
44
|
+
/** Advertised nowhere: reachable only by naming it to `reticle_run`. */
|
|
45
|
+
RUN_ONLY: 'run-only',
|
|
46
|
+
};
|
|
47
|
+
/** Which surface would have put this tool in front of an agent, read off that surface's own set. */
|
|
48
|
+
export function tierOfTool(tool) {
|
|
49
|
+
if (CORE_TOOL_NAMES.has(tool))
|
|
50
|
+
return ToolTier.DEFAULT;
|
|
51
|
+
if (EXTENDED_TOOL_NAMES.has(tool))
|
|
52
|
+
return ToolTier.EXTENDED;
|
|
53
|
+
return ToolTier.RUN_ONLY;
|
|
54
|
+
}
|
|
55
|
+
function tally(tools) {
|
|
56
|
+
return {
|
|
57
|
+
default: tools.filter((tier) => ToolTier.DEFAULT === tier).length,
|
|
58
|
+
extended: tools.filter((tier) => ToolTier.EXTENDED === tier).length,
|
|
59
|
+
runOnly: tools.filter((tier) => ToolTier.RUN_ONLY === tier).length,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Cross one session's per-tool counts with the tool table.
|
|
64
|
+
*
|
|
65
|
+
* `allTools` is the DISPATCHABLE table — the names `reticle_run` will accept — passed in rather than
|
|
66
|
+
* imported, because the table is assembled in the module that owns this tool and importing it back
|
|
67
|
+
* would close a cycle. A tool counted but absent from the table still appears, so a name the table
|
|
68
|
+
* lost is visible rather than dropped.
|
|
69
|
+
*/
|
|
70
|
+
export function foldToolHitRate(input) {
|
|
71
|
+
const { calls } = input;
|
|
72
|
+
if (calls === undefined)
|
|
73
|
+
return { observed: false };
|
|
74
|
+
const called = [...calls]
|
|
75
|
+
.map(([tool, count]) => ({ tool, tier: tierOfTool(tool), calls: count }))
|
|
76
|
+
.sort((a, b) => b.calls - a.calls || a.tool.localeCompare(b.tool));
|
|
77
|
+
const neverCalled = input.allTools
|
|
78
|
+
.filter((tool) => !calls.has(tool))
|
|
79
|
+
.map((tool) => ({ tool, tier: tierOfTool(tool) }))
|
|
80
|
+
.sort((a, b) => a.tool.localeCompare(b.tool));
|
|
81
|
+
return {
|
|
82
|
+
observed: true,
|
|
83
|
+
cells: {
|
|
84
|
+
called: tally(called.map((entry) => entry.tier)),
|
|
85
|
+
neverCalled: tally(neverCalled.map((entry) => entry.tier)),
|
|
86
|
+
},
|
|
87
|
+
called,
|
|
88
|
+
neverCalled,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Is this verdict being drawn over code nobody said anything about?
|
|
3
|
+
*
|
|
4
|
+
* Declaring intent is opt-in, and nothing has ever noticed that a change landed and nobody said what
|
|
5
|
+
* it was supposed to make true. That is the moment it matters: a change with no declared intent can
|
|
6
|
+
* only be verified against itself, which is the definition of a false green.
|
|
7
|
+
*
|
|
8
|
+
* ## Why this fires on EVERY verdict, not once per change
|
|
9
|
+
*
|
|
10
|
+
* The house rule is that a gap nobody hit is a backlog, and a backlog reported as a finding is how
|
|
11
|
+
* an agent learns to stop reading findings. This clears that bar on the same test every other
|
|
12
|
+
* instrumentation gap clears it on: **did the absence make THIS verdict weaker?** An unwatched store
|
|
13
|
+
* weakens every state assertion made over it, not merely the first, so it is reported on every one.
|
|
14
|
+
* An undeclared change is the same shape — while nothing says what the edit was for, each verdict
|
|
15
|
+
* drawn after it is checked against nothing but itself, and that is true of the fifth one exactly as
|
|
16
|
+
* it was of the first. Reporting it once and then falling silent would leave four verdicts carrying
|
|
17
|
+
* a weakness they no longer disclose, which is the failure mode this whole surface exists to prevent.
|
|
18
|
+
*
|
|
19
|
+
* It is also not a backlog, because it is not a survey of the project: it is answered from what this
|
|
20
|
+
* session observed (an epoch that moved) about the verdict being returned right now, and it is
|
|
21
|
+
* closed by one call the agent can make immediately. A finding that stays open only because the
|
|
22
|
+
* agent has not made that call is a finding about the current verdict, not a chore.
|
|
23
|
+
*
|
|
24
|
+
* ## The silences, and why each one is required
|
|
25
|
+
*
|
|
26
|
+
* - **An edit was actually OBSERVED.** The epoch degrades to `NO_EDITS_OBSERVED` outside Vite, and
|
|
27
|
+
* that means "no edits observed", never "no edits happened" — a finding built on an absence
|
|
28
|
+
* Reticle cannot see would be a fabrication.
|
|
29
|
+
* - **The ledger holds nothing undischarged.** An open intent IS the declaration; silence is the
|
|
30
|
+
* correct response to it, and declaring one is what turns this off from the next verdict on.
|
|
31
|
+
*
|
|
32
|
+
* ## Why "covers the work" is answered as "anything open at all"
|
|
33
|
+
*
|
|
34
|
+
* The tempting answer is to match the intent's `surface` against the files that changed. Reticle
|
|
35
|
+
* cannot do that honestly: it observes that an epoch advanced, not WHAT advanced — the hot-update
|
|
36
|
+
* channel reports a counter, not a diff — so any file-level matching would be inference dressed as a
|
|
37
|
+
* fact, and it would fire on an agent that had declared its intent perfectly well. Over-reporting on
|
|
38
|
+
* a correct agent is the direction of error that trains it to stop reading findings.
|
|
39
|
+
*
|
|
40
|
+
* So the question asked is the coarse one, and it is asked honestly: has the agent declared anything
|
|
41
|
+
* that is still outstanding? If it has, it is working to a stated purpose and this stays quiet.
|
|
42
|
+
*/
|
|
43
|
+
/**
|
|
44
|
+
* `openIntents` is a thunk rather than a list because reading the ledger touches disk, and the epoch
|
|
45
|
+
* check answers without it for every page that never reported a hot update — which is most of them.
|
|
46
|
+
* Once an edit HAS been observed the read happens per verdict, and that is the intended cost: the
|
|
47
|
+
* ledger is the only thing that can end it, so it has to be re-asked each time.
|
|
48
|
+
*/
|
|
49
|
+
export declare function isChangeUndeclared(
|
|
50
|
+
/** The epoch the session is under now. Undefined when the page never stamped one. */
|
|
51
|
+
currentEpoch: number | undefined, openIntents: () => Promise<readonly unknown[]>): Promise<boolean>;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { NO_EDITS_OBSERVED } from '@reticlehq/core';
|
|
2
|
+
/**
|
|
3
|
+
* Is this verdict being drawn over code nobody said anything about?
|
|
4
|
+
*
|
|
5
|
+
* Declaring intent is opt-in, and nothing has ever noticed that a change landed and nobody said what
|
|
6
|
+
* it was supposed to make true. That is the moment it matters: a change with no declared intent can
|
|
7
|
+
* only be verified against itself, which is the definition of a false green.
|
|
8
|
+
*
|
|
9
|
+
* ## Why this fires on EVERY verdict, not once per change
|
|
10
|
+
*
|
|
11
|
+
* The house rule is that a gap nobody hit is a backlog, and a backlog reported as a finding is how
|
|
12
|
+
* an agent learns to stop reading findings. This clears that bar on the same test every other
|
|
13
|
+
* instrumentation gap clears it on: **did the absence make THIS verdict weaker?** An unwatched store
|
|
14
|
+
* weakens every state assertion made over it, not merely the first, so it is reported on every one.
|
|
15
|
+
* An undeclared change is the same shape — while nothing says what the edit was for, each verdict
|
|
16
|
+
* drawn after it is checked against nothing but itself, and that is true of the fifth one exactly as
|
|
17
|
+
* it was of the first. Reporting it once and then falling silent would leave four verdicts carrying
|
|
18
|
+
* a weakness they no longer disclose, which is the failure mode this whole surface exists to prevent.
|
|
19
|
+
*
|
|
20
|
+
* It is also not a backlog, because it is not a survey of the project: it is answered from what this
|
|
21
|
+
* session observed (an epoch that moved) about the verdict being returned right now, and it is
|
|
22
|
+
* closed by one call the agent can make immediately. A finding that stays open only because the
|
|
23
|
+
* agent has not made that call is a finding about the current verdict, not a chore.
|
|
24
|
+
*
|
|
25
|
+
* ## The silences, and why each one is required
|
|
26
|
+
*
|
|
27
|
+
* - **An edit was actually OBSERVED.** The epoch degrades to `NO_EDITS_OBSERVED` outside Vite, and
|
|
28
|
+
* that means "no edits observed", never "no edits happened" — a finding built on an absence
|
|
29
|
+
* Reticle cannot see would be a fabrication.
|
|
30
|
+
* - **The ledger holds nothing undischarged.** An open intent IS the declaration; silence is the
|
|
31
|
+
* correct response to it, and declaring one is what turns this off from the next verdict on.
|
|
32
|
+
*
|
|
33
|
+
* ## Why "covers the work" is answered as "anything open at all"
|
|
34
|
+
*
|
|
35
|
+
* The tempting answer is to match the intent's `surface` against the files that changed. Reticle
|
|
36
|
+
* cannot do that honestly: it observes that an epoch advanced, not WHAT advanced — the hot-update
|
|
37
|
+
* channel reports a counter, not a diff — so any file-level matching would be inference dressed as a
|
|
38
|
+
* fact, and it would fire on an agent that had declared its intent perfectly well. Over-reporting on
|
|
39
|
+
* a correct agent is the direction of error that trains it to stop reading findings.
|
|
40
|
+
*
|
|
41
|
+
* So the question asked is the coarse one, and it is asked honestly: has the agent declared anything
|
|
42
|
+
* that is still outstanding? If it has, it is working to a stated purpose and this stays quiet.
|
|
43
|
+
*/
|
|
44
|
+
/**
|
|
45
|
+
* `openIntents` is a thunk rather than a list because reading the ledger touches disk, and the epoch
|
|
46
|
+
* check answers without it for every page that never reported a hot update — which is most of them.
|
|
47
|
+
* Once an edit HAS been observed the read happens per verdict, and that is the intended cost: the
|
|
48
|
+
* ledger is the only thing that can end it, so it has to be re-asked each time.
|
|
49
|
+
*/
|
|
50
|
+
export async function isChangeUndeclared(
|
|
51
|
+
/** The epoch the session is under now. Undefined when the page never stamped one. */
|
|
52
|
+
currentEpoch, openIntents) {
|
|
53
|
+
if (undefined === currentEpoch || NO_EDITS_OBSERVED === currentEpoch)
|
|
54
|
+
return false;
|
|
55
|
+
return 0 === (await openIntents()).length;
|
|
56
|
+
}
|
|
@@ -76,6 +76,21 @@ interface VerifiedInputs {
|
|
|
76
76
|
* Empty means nothing went unread.
|
|
77
77
|
*/
|
|
78
78
|
outcomeUnread?: readonly string[];
|
|
79
|
+
/**
|
|
80
|
+
* The declared consequence that held does not depend on the response body — an exact string
|
|
81
|
+
* rendered, a store path, a signal. See `declaresBodyIndependentChannel`.
|
|
82
|
+
*
|
|
83
|
+
* The unread-body clause exists for the case where the body is the ONLY channel that could have
|
|
84
|
+
* contradicted the screen (a 200 with batch/GraphQL errors inside). When a body-independent
|
|
85
|
+
* declaration held, that is no longer true, and grading `unknown` costs a real verdict. The unread
|
|
86
|
+
* labels still ride out in `because`, the way absence-derived contradictions still ride out in
|
|
87
|
+
* `contradictions`.
|
|
88
|
+
*
|
|
89
|
+
* Narrow: it does not soften a 202, an observed contradiction, a dirty capture, or a net-only
|
|
90
|
+
* declaration. `declaredConsequence` alone is not enough — a `{ kind: "net", status: 200 }` is a
|
|
91
|
+
* declaration whose remaining channel IS the body.
|
|
92
|
+
*/
|
|
93
|
+
independentOfBody?: boolean;
|
|
79
94
|
/**
|
|
80
95
|
* What the wait was for and what the window held when it ended — read ONLY by the two clauses that
|
|
81
96
|
* answer UNSETTLED, which is the commonest reason a verdict comes back `unknown` and was also the
|
package/dist/honesty/verified.js
CHANGED
|
@@ -31,6 +31,24 @@ export function decideVerified(inputs) {
|
|
|
31
31
|
'to repeat',
|
|
32
32
|
};
|
|
33
33
|
}
|
|
34
|
+
// ABOVE the failure clause, for the third time and the same reason: absence is not evidence.
|
|
35
|
+
//
|
|
36
|
+
// Omitting `until` waits for the page to SETTLE. Nobody declared a consequence, so a settle that
|
|
37
|
+
// times out says "the page was still busy when I stopped looking" — the definition of an
|
|
38
|
+
// absence-derived finding. The failure clause below took it at face value and returned
|
|
39
|
+
// `verified: "no"` with `because: 'the declared consequence did not hold'`, naming a consequence
|
|
40
|
+
// that does not exist. Measured on the hard fixture, whose push updates mean it never goes idle:
|
|
41
|
+
// a healthy pagination click was graded NO. Any app with polling or a live feed gets accused of a
|
|
42
|
+
// defect for being alive, which is the false NEGATIVE the ABSENCE_DERIVED doctrine exists to stop.
|
|
43
|
+
if (false === pass && false === inputs.declaredConsequence) {
|
|
44
|
+
return {
|
|
45
|
+
verified: Verified.UNKNOWN,
|
|
46
|
+
verifiedReason: VerifiedReason.UNSETTLED,
|
|
47
|
+
because: 'no consequence was declared, so this only waited for the page to go idle and it never ' +
|
|
48
|
+
'did — that is a statement about when Reticle stopped looking, not about the app. Pass ' +
|
|
49
|
+
'`until` naming what the action should cause (a signal, a request, a route, or store state)',
|
|
50
|
+
};
|
|
51
|
+
}
|
|
34
52
|
// A failed assertion is the most actionable fact there is; it leads — including over
|
|
35
53
|
// `alreadyTrue`, because a condition that held before AND fails now is a real regression.
|
|
36
54
|
if (false === pass) {
|
|
@@ -82,6 +100,27 @@ export function decideVerified(inputs) {
|
|
|
82
100
|
inputs.unsettled !== undefined &&
|
|
83
101
|
!openWrite &&
|
|
84
102
|
contradictions.every((c) => c.kind === ContradictionKind.REQUEST_NEVER_SETTLED);
|
|
103
|
+
// `signal-without-consequence` gets its own sentence, because the generic one is FALSE for it.
|
|
104
|
+
//
|
|
105
|
+
// That sentence says the window "closed before the app finished" and then explains that a poll or
|
|
106
|
+
// a timer kept the page busy. Nothing kept this page busy — nothing happened at all, which is the
|
|
107
|
+
// entire finding. Measured on the bench fixture: the correct UNKNOWN arrived wearing an
|
|
108
|
+
// explanation that told the agent to wait longer or assert something else, when the fact to act on
|
|
109
|
+
// was that the app announced a consequence it did not deliver. A right verdict with a wrong reason
|
|
110
|
+
// sends an agent to the wrong place, which costs as much as the wrong verdict did.
|
|
111
|
+
const signalOnly = contradictions.length > 0 &&
|
|
112
|
+
contradictions.every((c) => c.kind === ContradictionKind.SIGNAL_WITHOUT_CONSEQUENCE);
|
|
113
|
+
if (signalOnly) {
|
|
114
|
+
return {
|
|
115
|
+
verified: Verified.UNKNOWN,
|
|
116
|
+
verifiedReason: VerifiedReason.EVIDENCE_INCOMPLETE,
|
|
117
|
+
because: 'the app fired the signal you waited for and NOTHING else moved — no DOM, no store, no ' +
|
|
118
|
+
'route, no request — so the only evidence is the app saying so. A signal emitted from the ' +
|
|
119
|
+
'value the app was ASKED for, rather than the one it committed, looks exactly like this. ' +
|
|
120
|
+
'Check the consequence directly (reticle_snapshot for what rendered, reticle_state for what ' +
|
|
121
|
+
'the store holds) before trusting it',
|
|
122
|
+
};
|
|
123
|
+
}
|
|
85
124
|
if (contradictions.length > 0 && !settlementOnly) {
|
|
86
125
|
const kinds = contradictions.map((c) => c.kind).join(', ');
|
|
87
126
|
return {
|
|
@@ -142,7 +181,21 @@ export function decideVerified(inputs) {
|
|
|
142
181
|
// Gated on `true === settled` rather than on a caller's say-so. A call that returned before the app
|
|
143
182
|
// stopped moving has not earned this, whatever it declared — which is what stops `no-fault` from
|
|
144
183
|
// becoming the green-forever button an always-available "nothing was wrong" would be.
|
|
145
|
-
|
|
184
|
+
// Two ways to have proved nothing, and only one of them used to be caught. Grade NONE is the
|
|
185
|
+
// engine's own reading — no signal, no request, no state change, nothing pinned. An explicit
|
|
186
|
+
// `declaredConsequence: false` is the CALLER saying it asserted nothing, which `act_and_wait`
|
|
187
|
+
// reports whenever `until` is omitted (an omitted `until` becomes `{kind:"settled"}` — a
|
|
188
|
+
// sleep-replacement, not an assertion).
|
|
189
|
+
//
|
|
190
|
+
// Grading alone missed the second case, because a click that merely moves the DOM lifts the grade
|
|
191
|
+
// to PRESENCE and walks past this branch into a full green. Measured live: clicking a submit
|
|
192
|
+
// button, and an inert text input, with no `until` both returned `verified:"yes"` /
|
|
193
|
+
// `verifiedReason:"proved"`. The claim it printed — "assertion held at presence grade" — was true
|
|
194
|
+
// about a grade and false about an assertion, because there was no assertion.
|
|
195
|
+
//
|
|
196
|
+
// It compounds: `reticle_context` persists proven claims, so an undeclared green becomes
|
|
197
|
+
// established fact for the agent that re-enters after a compaction.
|
|
198
|
+
if (honesty.grade === HonestyGrade.NONE || false === inputs.declaredConsequence) {
|
|
146
199
|
if (true === settled) {
|
|
147
200
|
return {
|
|
148
201
|
verified: Verified.NO_FAULT,
|
|
@@ -178,7 +231,15 @@ export function decideVerified(inputs) {
|
|
|
178
231
|
//
|
|
179
232
|
// UNKNOWN, not NO: nothing is known to have failed. The remedy is in the sentence, because an
|
|
180
233
|
// agent that cannot act on a caveat will learn to skip it.
|
|
181
|
-
|
|
234
|
+
//
|
|
235
|
+
// Skipped when the caller declared a consequence that does not depend on the body and it held.
|
|
236
|
+
// The clause was written for the body being the ONLY remaining channel; a unique row on screen, a
|
|
237
|
+
// store path, or a signal that held is a different channel, and grading unknown there cost a real
|
|
238
|
+
// verdict (a 201 plus the exact message text, agent went to enable capture instead of finishing).
|
|
239
|
+
// The unread write still rides out in `because`. A net-only declaration does not skip: then the
|
|
240
|
+
// body is still the remaining channel.
|
|
241
|
+
const unreadHeldIndependently = declaredHeld && true === inputs.independentOfBody;
|
|
242
|
+
if (outcomeUnread !== undefined && outcomeUnread.length > 0 && !unreadHeldIndependently) {
|
|
182
243
|
return {
|
|
183
244
|
verified: Verified.UNKNOWN,
|
|
184
245
|
verifiedReason: VerifiedReason.OUTCOME_UNREAD,
|
|
@@ -214,11 +275,14 @@ export function decideVerified(inputs) {
|
|
|
214
275
|
const notIdle = false === settled
|
|
215
276
|
? ', though the page never went idle — this rests on the consequence you declared, not on the page going quiet'
|
|
216
277
|
: '';
|
|
278
|
+
const unreadCaveat = unreadHeldIndependently && outcomeUnread !== undefined && outcomeUnread.length > 0
|
|
279
|
+
? `, though a write returned 2xx with a response body that was never recorded (${outcomeUnread.join('; ')}) — a 200 describes the transport, not the result`
|
|
280
|
+
: '';
|
|
217
281
|
return {
|
|
218
282
|
verified: Verified.YES,
|
|
219
283
|
verifiedReason: VerifiedReason.PROVED,
|
|
220
284
|
because: true === honesty.coverage?.partial
|
|
221
|
-
? `assertion held at ${honesty.grade} grade with no channel disagreeing, but coverage was PARTIAL — see \`coverage\` for what went unobserved${notIdle}`
|
|
222
|
-
: `assertion held at ${honesty.grade} grade over a clean capture with no channel disagreeing${notIdle}`,
|
|
285
|
+
? `assertion held at ${honesty.grade} grade with no channel disagreeing, but coverage was PARTIAL — see \`coverage\` for what went unobserved${notIdle}${unreadCaveat}`
|
|
286
|
+
: `assertion held at ${honesty.grade} grade over a clean capture with no channel disagreeing${notIdle}${unreadCaveat}`,
|
|
223
287
|
};
|
|
224
288
|
}
|
package/dist/http-server.d.ts
CHANGED
package/dist/http-server.js
CHANGED
|
@@ -330,7 +330,7 @@ function schemeOf(url) {
|
|
|
330
330
|
return undefined;
|
|
331
331
|
}
|
|
332
332
|
}
|
|
333
|
-
|
|
333
|
+
function driveSchemeRefusal(url) {
|
|
334
334
|
const scheme = schemeOf(url);
|
|
335
335
|
if (scheme !== undefined && DRIVEABLE_SCHEMES.includes(scheme))
|
|
336
336
|
return undefined;
|
|
@@ -18,8 +18,6 @@
|
|
|
18
18
|
* Pure functions over already-collected reports — the driving is `reticle_crawl`'s job, and running
|
|
19
19
|
* it across a commit range is a shell loop. This is only the part that must not be done by eye.
|
|
20
20
|
*/
|
|
21
|
-
/** The contradiction kinds — cross-channel disagreement, as opposed to a single-channel fault. */
|
|
22
|
-
export declare const CONTRADICTION_KINDS: readonly string[];
|
|
23
21
|
export interface HuntAnomaly {
|
|
24
22
|
kind: string;
|
|
25
23
|
ref?: string;
|
package/dist/hunt/hunt-report.js
CHANGED
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
* it across a commit range is a shell loop. This is only the part that must not be done by eye.
|
|
20
20
|
*/
|
|
21
21
|
/** The contradiction kinds — cross-channel disagreement, as opposed to a single-channel fault. */
|
|
22
|
-
|
|
22
|
+
const CONTRADICTION_KINDS = [
|
|
23
23
|
'ui-advanced-request-failed',
|
|
24
24
|
'signal-contradicted',
|
|
25
25
|
'response-ignored',
|
|
@@ -3,11 +3,6 @@ export interface ImpactPaths {
|
|
|
3
3
|
project: string;
|
|
4
4
|
global: string;
|
|
5
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
6
|
/** Read a scope, tolerating absence and corruption: a broken file starts over rather than throwing. */
|
|
12
7
|
export declare function readScope(path: string, now: number): ImpactScope;
|
|
13
8
|
/** YYYY-MM-DD in local time - the day boundary a person recognises, not UTC's. */
|
|
@@ -18,7 +18,7 @@ const WRITE_DEBOUNCE_MS = 800;
|
|
|
18
18
|
* `reticleRoot` is the project's own `.reticle` directory (what every other store here is handed);
|
|
19
19
|
* the global scope always lives beside the daemon's own state in `~/.reticle`.
|
|
20
20
|
*/
|
|
21
|
-
|
|
21
|
+
function impactPaths(reticleRoot) {
|
|
22
22
|
return {
|
|
23
23
|
project: join(reticleRoot, ReticleDir.IMPACT_FILE),
|
|
24
24
|
global: join(homedir(), ReticleDir.ROOT, ReticleDir.IMPACT_FILE),
|
package/dist/index.d.ts
CHANGED
|
@@ -117,7 +117,10 @@ export interface StartOptions {
|
|
|
117
117
|
pairingTokenDir?: string;
|
|
118
118
|
/** injectable clock for contract.json's generatedAt stamp. Defaults to Date.now. */
|
|
119
119
|
now?: () => number;
|
|
120
|
-
/**
|
|
120
|
+
/**
|
|
121
|
+
* Retired profile name (`core`/`full`/…). Old values still map. The live switch is
|
|
122
|
+
* RETICLE_ADVERTISE_ALL_TOOLS=1; the default is the lean surface.
|
|
123
|
+
*/
|
|
121
124
|
toolProfile?: string;
|
|
122
125
|
/** Start the OEM/CI verify HTTP endpoint alongside the daemon (`reticle serve --http`). */
|
|
123
126
|
httpVerify?: boolean;
|
|
@@ -151,7 +154,6 @@ export interface RunningServer {
|
|
|
151
154
|
close: () => Promise<void>;
|
|
152
155
|
}
|
|
153
156
|
export { resolveBridgeSecurity } from './bridge/bridge-security.js';
|
|
154
|
-
/** Start the Reticle bridge (browser WS endpoint) and, by default, the MCP stdio server. */
|
|
155
157
|
export declare function start(options?: StartOptions): Promise<RunningServer>;
|
|
156
158
|
/**
|
|
157
159
|
* Start the Reticle bridge in daemon mode: a single HTTP server handles both the WebSocket
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
import { join } from 'node:path';
|
|
2
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
3
|
+
import { homedir } from 'node:os';
|
|
4
|
+
import { PROJECT_REGISTRY_FILE, emptyProjectRegistry, parseProjectRegistry } from '@reticlehq/core';
|
|
5
|
+
import { discoverProjectConfigs } from './cli/config-discovery.js';
|
|
6
|
+
import { projectCandidatesFrom, resolveArtifactRoot, } from './project/artifact-root.js';
|
|
2
7
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
3
8
|
import { AGENT_STOPPED_NOTICE, RETICLE_DEFAULT_PORT, ReticleCommand, ReticleDir, ReticleEnv, LOOPBACK_HOST, ReplayStatus, EventType, } from '@reticlehq/core';
|
|
4
9
|
import { originOf } from './session/session-manager.js';
|
|
@@ -251,6 +256,41 @@ async function resolveRealInput(options, onNavigateError, onNetworkDetail) {
|
|
|
251
256
|
return {};
|
|
252
257
|
}
|
|
253
258
|
/** Start the Reticle bridge (browser WS endpoint) and, by default, the MCP stdio server. */
|
|
259
|
+
/**
|
|
260
|
+
* Resolve a session's artifact root, from everything this machine knows about where projects live.
|
|
261
|
+
*
|
|
262
|
+
* Built once per daemon and closed over by every tool call. The two sources are read lazily and
|
|
263
|
+
* cheaply on each call rather than snapshotted at startup: `init` can run in another terminal while
|
|
264
|
+
* this daemon is up, and a resolution that used a startup snapshot would keep answering with a map
|
|
265
|
+
* from before the project the agent is now driving existed.
|
|
266
|
+
*/
|
|
267
|
+
function artifactRootResolver(daemonRoot) {
|
|
268
|
+
return (projectId) => {
|
|
269
|
+
let registry = emptyProjectRegistry();
|
|
270
|
+
try {
|
|
271
|
+
const path = join(homedir(), ReticleDir.ROOT, PROJECT_REGISTRY_FILE);
|
|
272
|
+
registry = existsSync(path)
|
|
273
|
+
? parseProjectRegistry(JSON.parse(readFileSync(path, 'utf8')))
|
|
274
|
+
: registry;
|
|
275
|
+
}
|
|
276
|
+
catch {
|
|
277
|
+
// A cache that cannot be read is an empty cache, never an error: the daemon still resolves
|
|
278
|
+
// through discovery, and falls back to its own root exactly as it did before this existed.
|
|
279
|
+
}
|
|
280
|
+
let discovery = { found: [], searched: [] };
|
|
281
|
+
try {
|
|
282
|
+
discovery = discoverProjectConfigs(process.cwd());
|
|
283
|
+
}
|
|
284
|
+
catch {
|
|
285
|
+
// Same reasoning: a diagnostic search that throws must not take a tool call with it.
|
|
286
|
+
}
|
|
287
|
+
return resolveArtifactRoot({
|
|
288
|
+
projectId,
|
|
289
|
+
candidates: projectCandidatesFrom(discovery, registry),
|
|
290
|
+
daemonRoot,
|
|
291
|
+
});
|
|
292
|
+
};
|
|
293
|
+
}
|
|
254
294
|
export async function start(options = {}) {
|
|
255
295
|
const port = options.port ?? RETICLE_DEFAULT_PORT;
|
|
256
296
|
// Open the user's impact record before anything can connect. Not inside the MCP branch: a daemon
|
|
@@ -302,6 +342,7 @@ export async function start(options = {}) {
|
|
|
302
342
|
project,
|
|
303
343
|
fs,
|
|
304
344
|
reticleRoot,
|
|
345
|
+
artifactRootFor: artifactRootResolver(reticleRoot),
|
|
305
346
|
now,
|
|
306
347
|
bridgePort: port,
|
|
307
348
|
browserProbe: probeChromium,
|