@reticlehq/server 3.2.0 → 3.3.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 +16 -13
- package/dist/command/cli/adhoc-suite.d.ts +19 -0
- package/dist/command/cli/adhoc-suite.js +88 -0
- package/dist/command/cli/adhoc-verdict.d.ts +23 -2
- package/dist/command/cli/adhoc-verdict.js +18 -10
- package/dist/command/cli/cli-doctor.js +25 -5
- package/dist/command/cli/cli-parse-grammar.d.ts +7 -0
- package/dist/command/cli/cli-parse-grammar.js +7 -0
- package/dist/command/cli/cli-parse-verify.d.ts +2 -5
- package/dist/command/cli/cli-parse-verify.js +86 -13
- package/dist/command/cli/cli-parse.d.ts +7 -1
- package/dist/command/cli/cli-parse.js +33 -5
- package/dist/command/cli/cli-verify.d.ts +59 -0
- package/dist/command/cli/cli-verify.js +129 -17
- package/dist/command/cli/cloud-kit.d.ts +0 -2
- package/dist/command/cli/cloud-kit.js +1 -1
- package/dist/command/cli/doctor/doctor-sessions-line.d.ts +2 -1
- package/dist/command/cli/launch/cli-launch.d.ts +1 -29
- package/dist/command/cli/launch/cli-launch.js +6 -40
- package/dist/command/cli/ports/resolve/cli-port.d.ts +13 -0
- package/dist/command/cli/ports/resolve/cli-port.js +45 -1
- package/dist/command/cli/report-command.d.ts +16 -0
- package/dist/command/cli/report-command.js +97 -0
- package/dist/command/cli.js +5 -0
- package/dist/command/daemon/binding/daemon-status-probe.d.ts +30 -0
- package/dist/command/daemon/binding/daemon-status-probe.js +45 -0
- package/dist/command/daemon/binding/port-presence.d.ts +1 -1
- package/dist/command/daemon/idle-shutdown.d.ts +2 -1
- package/dist/command/setup/init/init-host.js +8 -0
- package/dist/features/crawl/crawl.d.ts +2 -0
- package/dist/features/crawl/crawl.js +1 -0
- package/dist/features/harness/jev-driver.js +6 -9
- package/dist/features/harness/platform-config.d.ts +10 -0
- package/dist/features/harness/platform-config.js +8 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +40 -95
- package/dist/judgement/capsule/capsule-store.d.ts +31 -2
- package/dist/judgement/capsule/capsule-store.js +58 -6
- package/dist/judgement/capsule/capsule.js +5 -0
- package/dist/judgement/capsule/causal-summary.d.ts +13 -1
- package/dist/judgement/capsule/causal-summary.js +7 -0
- package/dist/judgement/domain/domain-model.js +38 -9
- package/dist/judgement/outcome/assertion-integrity.d.ts +3 -3
- package/dist/judgement/phenomena/phenomena.d.ts +2 -1
- package/dist/judgement/runs/artifact/gap-report.d.ts +17 -0
- package/dist/judgement/runs/artifact/gap-report.js +46 -0
- package/dist/judgement/runs/artifact/gap-summary.d.ts +58 -0
- package/dist/judgement/runs/artifact/gap-summary.js +85 -0
- package/dist/judgement/runs/artifact/render-report.js +4 -1
- package/dist/judgement/runs/artifact/to-artifact.d.ts +7 -2
- package/dist/judgement/runs/artifact/to-artifact.js +1 -1
- package/dist/judgement/runs/context-tools.js +21 -3
- package/dist/judgement/runs/drive-run.js +7 -2
- package/dist/language/flows/annotate-notes/annotate.d.ts +0 -15
- package/dist/language/flows/annotate-notes/annotate.js +66 -18
- package/dist/language/flows/flow-anchor.d.ts +12 -0
- package/dist/language/flows/flow-anchor.js +20 -0
- package/dist/language/flows/flow-expect-grammar.d.ts +2 -2
- package/dist/language/flows/flow-expect-grammar.js +92 -11
- package/dist/language/flows/flow-intent.js +1 -1
- package/dist/language/flows/flow-learning.d.ts +3 -0
- package/dist/language/flows/flow-learning.js +39 -8
- package/dist/language/flows/flow-replay-run.d.ts +31 -9
- package/dist/language/flows/flow-replay-run.js +146 -33
- package/dist/language/flows/flow-replay-types.d.ts +9 -0
- package/dist/language/flows/flow-replay.d.ts +12 -11
- package/dist/language/flows/flow-replay.js +79 -109
- package/dist/language/flows/flow-step-runners.d.ts +26 -10
- package/dist/language/flows/flow-step-runners.js +20 -4
- package/dist/language/flows/flow-success.d.ts +21 -6
- package/dist/language/flows/flow-success.js +50 -138
- package/dist/language/flows/flows.d.ts +8 -4
- package/dist/language/flows/flows.js +11 -2
- package/dist/language/flows/heal.d.ts +9 -1
- package/dist/language/flows/heal.js +24 -2
- package/dist/language/flows/learned-sources.d.ts +21 -0
- package/dist/language/flows/learned-sources.js +84 -0
- package/dist/language/flows/recording/recording-backtrack.d.ts +0 -7
- package/dist/language/flows/recording/recording-backtrack.js +1 -1
- package/dist/language/flows/recording/replay-halt.d.ts +2 -1
- package/dist/language/flows/recording/tape/recordings.d.ts +2 -2
- package/dist/language/flows/replay.js +12 -8
- package/dist/language/flows/stores/annotation-store.d.ts +5 -5
- package/dist/language/flows/stores/assertion-tiers-store.js +2 -3
- package/dist/language/flows/stores/flake-store.js +2 -3
- package/dist/language/flows/suite/coverage.d.ts +1 -1
- package/dist/language/flows/suite/parallel-suite.d.ts +2 -1
- package/dist/language/flows/verify-change-tools.js +14 -0
- package/dist/memory/cloud/cloud-sync.d.ts +0 -2
- package/dist/memory/cloud/cloud-sync.js +1 -1
- package/dist/memory/cloud/harness-config.d.ts +2 -0
- package/dist/memory/cloud/sync-cycle.d.ts +12 -0
- package/dist/memory/cloud/sync-cycle.js +29 -4
- package/dist/memory/cloud/sync-daemon.d.ts +13 -0
- package/dist/memory/cloud/sync-daemon.js +21 -0
- package/dist/memory/cloud/sync-disk.js +11 -6
- package/dist/memory/impact/impact-store.js +6 -0
- package/dist/memory/intent/inline-intent.js +8 -3
- package/dist/memory/intent/intent-shard-store.d.ts +33 -47
- package/dist/memory/intent/intent-shard-store.js +252 -140
- package/dist/memory/intent/intent-shard.d.ts +10 -11
- package/dist/memory/intent/intent-shard.js +1 -1
- package/dist/memory/intent/intent-store.d.ts +13 -8
- package/dist/memory/intent/intent-store.js +46 -90
- package/dist/memory/intent/intent-tools.js +34 -23
- package/dist/memory/journal/ambient-store.js +2 -3
- package/dist/memory/journal/deviation-report.js +4 -1
- package/dist/memory/journal/deviation-service.js +10 -2
- package/dist/memory/journal/drive-flow.d.ts +2 -2
- package/dist/memory/journal/drive-flow.js +21 -12
- package/dist/memory/journal/envelope-store.js +2 -3
- package/dist/memory/journal/journal-recorder.d.ts +21 -0
- package/dist/memory/journal/on-disk/retain-policy.d.ts +34 -0
- package/dist/memory/journal/on-disk/retain-policy.js +44 -0
- package/dist/memory/journal/on-disk/retention.d.ts +14 -10
- package/dist/memory/journal/on-disk/retention.js +13 -12
- package/dist/memory/journal/on-disk/startup-maintenance.d.ts +42 -0
- package/dist/memory/journal/on-disk/startup-maintenance.js +14 -0
- package/dist/memory/journal/on-disk/workspace-gitignore.d.ts +0 -11
- package/dist/memory/journal/on-disk/workspace-gitignore.js +6 -45
- package/dist/memory/journal/on-disk/workspace-tiers.d.ts +52 -0
- package/dist/memory/journal/on-disk/workspace-tiers.js +88 -0
- package/dist/memory/journal/route-template.d.ts +22 -0
- package/dist/memory/journal/route-template.js +44 -0
- package/dist/memory/journal/session-end.d.ts +4 -1
- package/dist/memory/journal/session-end.js +64 -6
- package/dist/memory/journal/session-journal.d.ts +8 -1
- package/dist/memory/journal/session-journal.js +10 -0
- package/dist/memory/project/baselines.d.ts +1 -1
- package/dist/memory/project/dir/reticle-dir.d.ts +1 -1
- package/dist/memory/project/fs/fs-port.js +44 -5
- package/dist/memory/project/fs/write-atomic.d.ts +26 -0
- package/dist/memory/project/fs/write-atomic.js +48 -0
- package/dist/memory/recall/prior/connection-memory.d.ts +6 -0
- package/dist/memory/recall/prior/connection-memory.js +8 -0
- package/dist/memory/recall/registered-projects.d.ts +2 -1
- package/dist/portal/bridge/auth-failure-reason.js +5 -2
- package/dist/portal/input/real-input.js +2 -1
- package/dist/portal/pool/browser-pool.d.ts +10 -0
- package/dist/portal/pool/browser-pool.js +10 -0
- package/dist/portal/pool/playwright-launcher.js +1 -0
- package/dist/portal/pool/pool-contract.d.ts +2 -0
- package/dist/portal/pool/storage-seed.d.ts +2 -1
- package/dist/portal/pool/zero-install.d.ts +27 -0
- package/dist/portal/pool/zero-install.js +54 -0
- package/dist/portal/realm/web-realm.js +6 -0
- package/dist/portal/session/dev-server/dev-server-probe.d.ts +2 -1
- package/dist/portal/session/dev-server/served-document.d.ts +21 -0
- package/dist/portal/session/dev-server/served-document.js +57 -0
- package/dist/portal/session/facts/background-traffic.d.ts +9 -0
- package/dist/portal/session/facts/background-traffic.js +25 -0
- package/dist/portal/session/live-control-tools.js +48 -3
- package/dist/portal/session/presence/attachment-history.d.ts +13 -2
- package/dist/portal/session/session-health.d.ts +61 -0
- package/dist/portal/session/session-health.js +40 -1
- package/dist/portal/session/session-info.d.ts +6 -0
- package/dist/portal/session/session-manager.d.ts +9 -0
- package/dist/portal/session/session-manager.js +23 -2
- package/dist/portal/session/session-successor.d.ts +11 -0
- package/dist/portal/session/session-successor.js +14 -1
- package/dist/portal/session/session-verdict-facts.d.ts +28 -0
- package/dist/portal/session/session-verdict-facts.js +15 -0
- package/dist/portal/session/session.d.ts +5 -0
- package/dist/portal/session/session.js +19 -7
- package/dist/surface/mcp/mcp-proxy.js +3 -3
- package/dist/surface/mcp/proxy-idle-exit.d.ts +2 -1
- package/dist/surface/mcp/server-instructions.js +12 -1
- package/dist/surface/tools/act/already-true.d.ts +40 -0
- package/dist/surface/tools/act/already-true.js +36 -0
- package/dist/surface/tools/act/navigation/navigate-arrival.d.ts +6 -0
- package/dist/surface/tools/act/navigation/navigate-arrival.js +13 -0
- package/dist/surface/tools/act/settle-in-flight.d.ts +6 -2
- package/dist/surface/tools/act/settle-in-flight.js +29 -6
- package/dist/surface/tools/act-capsule.js +22 -7
- package/dist/surface/tools/act-tools.js +25 -24
- package/dist/surface/tools/args/numeric-bounds.d.ts +15 -2
- package/dist/surface/tools/args/numeric-bounds.js +30 -3
- package/dist/surface/tools/assert/assert-grade.js +14 -2
- package/dist/surface/tools/assert/assert-verdict.js +30 -4
- package/dist/surface/tools/browser-tools.js +4 -3
- package/dist/surface/tools/dynamic-tools.js +6 -0
- package/dist/surface/tools/feedback-tools.d.ts +2 -1
- package/dist/surface/tools/feedback-tools.js +1 -1
- package/dist/surface/tools/invoke-tool.js +17 -4
- package/dist/surface/tools/lease-hint.d.ts +8 -0
- package/dist/surface/tools/lease-hint.js +10 -0
- package/dist/surface/tools/lease-tools.d.ts +16 -28
- package/dist/surface/tools/lease-tools.js +53 -5
- package/dist/surface/tools/lineage-tools.js +1 -1
- package/dist/surface/tools/observe-tools.js +14 -6
- package/dist/surface/tools/real-input-attempt.d.ts +0 -5
- package/dist/surface/tools/real-input-attempt.js +1 -1
- package/dist/surface/tools/tool-kit.d.ts +5 -0
- package/dist/surface/tools/tool-kit.js +6 -1
- package/dist/surface/tools/tool-surface.js +17 -5
- package/dist/surface/tools/tools.js +64 -29
- package/dist/telemetry/feedback-local.d.ts +1 -0
- package/dist/telemetry/feedback-local.js +1 -3
- package/dist/telemetry/feedback.d.ts +1 -1
- package/dist/telemetry/hud-metrics.d.ts +32 -0
- package/dist/telemetry/hud-metrics.js +95 -0
- package/dist/telemetry/session-metrics.d.ts +5 -0
- package/dist/telemetry/session-metrics.js +15 -1
- package/dist/wire-journal.d.ts +31 -0
- package/dist/wire-journal.js +100 -0
- package/docs/README.md +1 -0
- package/docs/cli/cloud.mdx +9 -7
- package/docs/cli/init.mdx +1 -0
- package/docs/cli/report.mdx +44 -0
- package/docs/desktop.mdx +1 -1
- package/docs/flows.md +11 -0
- package/docs/frameworks.mdx +113 -1
- package/docs/install-agentic.mdx +36 -0
- package/docs/install-manual.mdx +1 -1
- package/docs/packages/tauri.mdx +1 -1
- package/docs/packages.mdx +1 -1
- package/docs/predicates.mdx +128 -4
- package/docs/tools/query.mdx +10 -10
- package/docs/usage.md +1 -1
- package/docs/what-is-recorded.md +62 -0
- package/package.json +6 -6
- package/dist/judgement/outcome/predicate-to-expect.d.ts +0 -44
- package/dist/judgement/outcome/predicate-to-expect.js +0 -148
|
@@ -29,6 +29,18 @@ declare const CapsuleSchema: z.ZodObject<{
|
|
|
29
29
|
observed: z.ZodString;
|
|
30
30
|
/** Minimal steps to reproduce (prefix-trimmed). */
|
|
31
31
|
steps: z.ZodArray<z.ZodType<FlowStep, z.ZodTypeDef, FlowStep>, "many">;
|
|
32
|
+
/**
|
|
33
|
+
* How many times this exact failure has been captured, and when it last was.
|
|
34
|
+
*
|
|
35
|
+
* A repeat is not a new capsule -- it is the same one, again. "This failed 37 times, most
|
|
36
|
+
* recently at T" is strictly MORE than 37 files each holding one timestamp, so folding the
|
|
37
|
+
* duplicates in adds information rather than discarding it.
|
|
38
|
+
*
|
|
39
|
+
* Optional, because a capsule written before these existed has neither and must not be read as
|
|
40
|
+
* though it were seen once when nobody counted.
|
|
41
|
+
*/
|
|
42
|
+
seen: z.ZodOptional<z.ZodNumber>;
|
|
43
|
+
lastSeenAt: z.ZodOptional<z.ZodNumber>;
|
|
32
44
|
}, "strip", z.ZodTypeAny, {
|
|
33
45
|
expected: string;
|
|
34
46
|
origin: string;
|
|
@@ -38,6 +50,8 @@ declare const CapsuleSchema: z.ZodObject<{
|
|
|
38
50
|
createdAt: number;
|
|
39
51
|
observed: string;
|
|
40
52
|
flow?: string | undefined;
|
|
53
|
+
seen?: number | undefined;
|
|
54
|
+
lastSeenAt?: number | undefined;
|
|
41
55
|
}, {
|
|
42
56
|
expected: string;
|
|
43
57
|
origin: string;
|
|
@@ -47,6 +61,8 @@ declare const CapsuleSchema: z.ZodObject<{
|
|
|
47
61
|
createdAt: number;
|
|
48
62
|
observed: string;
|
|
49
63
|
flow?: string | undefined;
|
|
64
|
+
seen?: number | undefined;
|
|
65
|
+
lastSeenAt?: number | undefined;
|
|
50
66
|
}>;
|
|
51
67
|
export type Capsule = z.infer<typeof CapsuleSchema>;
|
|
52
68
|
/** Capsule ids are filenames — refuse anything that could escape the capsules directory. */
|
|
@@ -63,8 +79,21 @@ export declare class CapsuleStore {
|
|
|
63
79
|
/** Every readable capsule, newest-first — what `reticle capsules` prints. */
|
|
64
80
|
all(): Promise<Capsule[]>;
|
|
65
81
|
}
|
|
66
|
-
/**
|
|
67
|
-
|
|
82
|
+
/**
|
|
83
|
+
* What makes two captures the SAME failure.
|
|
84
|
+
*
|
|
85
|
+
* Everything that describes the failure, and nothing that describes the capture: `createdAt` is
|
|
86
|
+
* excluded because it is precisely what turned every retry of a broken step into a new file, and
|
|
87
|
+
* `id` because it is derived from this. `flow` is included -- the same assertion failing in two
|
|
88
|
+
* different journeys is two findings, not one.
|
|
89
|
+
*/
|
|
90
|
+
export declare function capsuleFingerprint(capsule: Omit<Capsule, 'id' | 'createdAt' | 'seen' | 'lastSeenAt'>): string;
|
|
91
|
+
/**
|
|
92
|
+
* Deterministic, path-safe capsule id: time-ordered so `list` sorts newest-first for free, with the
|
|
93
|
+
* fingerprint on the END so a duplicate can be found by listing the directory rather than reading
|
|
94
|
+
* every file in it.
|
|
95
|
+
*/
|
|
96
|
+
export declare function capsuleId(now: number, label: string, fingerprint: string): string;
|
|
68
97
|
/** The minimal reproduction: the steps up to and including the one that failed (prefix trim). */
|
|
69
98
|
export declare function minimalSteps(steps: readonly FlowStep[], failedIndex: number): FlowStep[];
|
|
70
99
|
export {};
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { join } from 'node:path';
|
|
2
|
+
import { fnv1a } from '@reticlehq/core';
|
|
3
|
+
import { writeFileAtomic } from '../../memory/project/fs/write-atomic.js';
|
|
2
4
|
import { z } from 'zod';
|
|
3
5
|
import { FlowStepSchema } from '@reticlehq/core';
|
|
4
6
|
import { reticleDirPaths } from '../../memory/project/dir/reticle-dir.js';
|
|
@@ -30,6 +32,18 @@ const CapsuleSchema = z.object({
|
|
|
30
32
|
observed: z.string(),
|
|
31
33
|
/** Minimal steps to reproduce (prefix-trimmed). */
|
|
32
34
|
steps: z.array(FlowStepSchema),
|
|
35
|
+
/**
|
|
36
|
+
* How many times this exact failure has been captured, and when it last was.
|
|
37
|
+
*
|
|
38
|
+
* A repeat is not a new capsule -- it is the same one, again. "This failed 37 times, most
|
|
39
|
+
* recently at T" is strictly MORE than 37 files each holding one timestamp, so folding the
|
|
40
|
+
* duplicates in adds information rather than discarding it.
|
|
41
|
+
*
|
|
42
|
+
* Optional, because a capsule written before these existed has neither and must not be read as
|
|
43
|
+
* though it were seen once when nobody counted.
|
|
44
|
+
*/
|
|
45
|
+
seen: z.number().int().positive().optional(),
|
|
46
|
+
lastSeenAt: z.number().optional(),
|
|
33
47
|
});
|
|
34
48
|
/** Capsule ids are filenames — refuse anything that could escape the capsules directory. */
|
|
35
49
|
export function isValidCapsuleId(id) {
|
|
@@ -51,9 +65,26 @@ export class CapsuleStore {
|
|
|
51
65
|
return false;
|
|
52
66
|
try {
|
|
53
67
|
await this.#fs.mkdir(this.#dir);
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
68
|
+
/*
|
|
69
|
+
* A capture of a failure already on disk folds into it rather than adding a file. Found by
|
|
70
|
+
* LISTING: the fingerprint is the id's suffix, so this costs one directory read instead of
|
|
71
|
+
* one read per capsule. An id written before fingerprints existed simply never matches, which
|
|
72
|
+
* is the correct outcome -- it is left exactly as it was.
|
|
73
|
+
*
|
|
74
|
+
* `createdAt` keeps the FIRST sighting. A repeat is the same failure happening again, not a
|
|
75
|
+
* new one, and the interesting pair is when it started and when it last happened.
|
|
76
|
+
*/
|
|
77
|
+
const fingerprint = capsuleFingerprint(capsule);
|
|
78
|
+
const existingId = (await this.list()).find((id) => id.endsWith(`-${fingerprint}`));
|
|
79
|
+
const previous = existingId === undefined ? undefined : await this.read(existingId);
|
|
80
|
+
const folded = previous === undefined
|
|
81
|
+
? { ...capsule, seen: 1, lastSeenAt: capsule.createdAt }
|
|
82
|
+
: {
|
|
83
|
+
...previous,
|
|
84
|
+
seen: (previous.seen ?? 1) + 1,
|
|
85
|
+
lastSeenAt: capsule.createdAt,
|
|
86
|
+
};
|
|
87
|
+
await writeFileAtomic(this.#fs, this.#pathFor(folded.id), `${JSON.stringify(folded, null, 2)}\n`);
|
|
57
88
|
return true;
|
|
58
89
|
}
|
|
59
90
|
catch {
|
|
@@ -100,10 +131,31 @@ export class CapsuleStore {
|
|
|
100
131
|
return out;
|
|
101
132
|
}
|
|
102
133
|
}
|
|
103
|
-
/**
|
|
104
|
-
|
|
134
|
+
/**
|
|
135
|
+
* What makes two captures the SAME failure.
|
|
136
|
+
*
|
|
137
|
+
* Everything that describes the failure, and nothing that describes the capture: `createdAt` is
|
|
138
|
+
* excluded because it is precisely what turned every retry of a broken step into a new file, and
|
|
139
|
+
* `id` because it is derived from this. `flow` is included -- the same assertion failing in two
|
|
140
|
+
* different journeys is two findings, not one.
|
|
141
|
+
*/
|
|
142
|
+
export function capsuleFingerprint(capsule) {
|
|
143
|
+
return fnv1a(JSON.stringify([
|
|
144
|
+
capsule.flow ?? '',
|
|
145
|
+
capsule.origin,
|
|
146
|
+
capsule.expected,
|
|
147
|
+
capsule.observed,
|
|
148
|
+
capsule.steps,
|
|
149
|
+
]));
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Deterministic, path-safe capsule id: time-ordered so `list` sorts newest-first for free, with the
|
|
153
|
+
* fingerprint on the END so a duplicate can be found by listing the directory rather than reading
|
|
154
|
+
* every file in it.
|
|
155
|
+
*/
|
|
156
|
+
export function capsuleId(now, label, fingerprint) {
|
|
105
157
|
const safe = label.replace(/[^a-zA-Z0-9._-]/g, '-').slice(0, 40);
|
|
106
|
-
return `${String(now)}-${safe.length > 0 ? safe : 'capsule'}`;
|
|
158
|
+
return `${String(now)}-${safe.length > 0 ? safe : 'capsule'}-${fingerprint}`;
|
|
107
159
|
}
|
|
108
160
|
/** The minimal reproduction: the steps up to and including the one that failed (prefix trim). */
|
|
109
161
|
export function minimalSteps(steps, failedIndex) {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { ConsequenceKind, EventType } from '@reticlehq/core';
|
|
2
|
+
import { isDocumentInitiated } from '@reticlehq/engine/question/predicate/predicate-eval.js';
|
|
2
3
|
import { causalSummary } from './causal-summary.js';
|
|
3
4
|
import { firstDivergence } from './divergence.js';
|
|
4
5
|
/**
|
|
@@ -34,6 +35,10 @@ export function blastRadius(expected, observed) {
|
|
|
34
35
|
continue;
|
|
35
36
|
if (declaredUrls.some((fragment) => url.includes(fragment)))
|
|
36
37
|
continue;
|
|
38
|
+
// A subresource the document fetched is the page loading, not a request the action sent: one
|
|
39
|
+
// navigation is a couple of hundred of them. IPC and beacons carry an initiator too, and stay.
|
|
40
|
+
if (isDocumentInitiated(event))
|
|
41
|
+
continue;
|
|
37
42
|
const method = event.data['method'];
|
|
38
43
|
add(`net ${'string' === typeof method ? method : 'request'} ${url}`);
|
|
39
44
|
continue;
|
|
@@ -63,6 +63,18 @@ export interface CausalSummary {
|
|
|
63
63
|
elided?: Partial<Record<CappedList, number>>;
|
|
64
64
|
route?: string;
|
|
65
65
|
signals: string[];
|
|
66
|
+
/**
|
|
67
|
+
* Form fields whose value moved in this window, by NAME.
|
|
68
|
+
*
|
|
69
|
+
* Names only, never values: a form is where somebody types their password, and the observer that
|
|
70
|
+
* reports this already refuses to carry one. The names are what an assertion is written against,
|
|
71
|
+
* and a field moving when nothing should have touched it (or not moving when something should)
|
|
72
|
+
* is the silently-cleared-field class — which until this channel existed left no trace at all,
|
|
73
|
+
* because React sets the input PROPERTY and MutationObserver watches attributes.
|
|
74
|
+
*
|
|
75
|
+
* Present only when a field moved, so a page with no form pays nothing.
|
|
76
|
+
*/
|
|
77
|
+
fieldsChanged?: string[];
|
|
66
78
|
layoutShift?: number;
|
|
67
79
|
longTasks: number;
|
|
68
80
|
/**
|
|
@@ -114,6 +126,6 @@ interface SummaryContext {
|
|
|
114
126
|
*/
|
|
115
127
|
export declare const MAX_SUMMARY_ENTRIES = 20;
|
|
116
128
|
/** The lists that are capped, named so `elided` can say which one lost entries. */
|
|
117
|
-
type CappedList = 'statePathsChanged' | 'storageKeysChanged' | 'stateDiffs' | 'storageDiffs' | 'signals';
|
|
129
|
+
type CappedList = 'statePathsChanged' | 'storageKeysChanged' | 'stateDiffs' | 'storageDiffs' | 'signals' | 'fieldsChanged';
|
|
118
130
|
export declare function causalSummary(events: readonly ReticleEvent[], context?: SummaryContext): CausalSummary;
|
|
119
131
|
export {};
|
|
@@ -69,6 +69,7 @@ export function causalSummary(events, context = {}) {
|
|
|
69
69
|
const storageDiffs = [];
|
|
70
70
|
let route;
|
|
71
71
|
const signals = [];
|
|
72
|
+
const fieldsChanged = [];
|
|
72
73
|
let layoutShift;
|
|
73
74
|
let longTasks = 0;
|
|
74
75
|
for (const event of events) {
|
|
@@ -131,6 +132,9 @@ export function causalSummary(events, context = {}) {
|
|
|
131
132
|
case EventType.SIGNAL:
|
|
132
133
|
pushUnique(signals, data['name']);
|
|
133
134
|
break;
|
|
135
|
+
case EventType.FIELD_CHANGE:
|
|
136
|
+
pushUnique(fieldsChanged, data['field']);
|
|
137
|
+
break;
|
|
134
138
|
case EventType.PERF:
|
|
135
139
|
if (data['metric'] === PerfMetric.CLS && 'number' === typeof data['value']) {
|
|
136
140
|
layoutShift = Math.max(layoutShift ?? 0, data['value']);
|
|
@@ -163,6 +167,9 @@ export function causalSummary(events, context = {}) {
|
|
|
163
167
|
storageDiffs: cap(storageDiffs, 'storageDiffs', elided),
|
|
164
168
|
...(route === undefined ? {} : { route }),
|
|
165
169
|
signals: cap(signals, 'signals', elided),
|
|
170
|
+
...(0 === fieldsChanged.length
|
|
171
|
+
? {}
|
|
172
|
+
: { fieldsChanged: cap(fieldsChanged, 'fieldsChanged', elided) }),
|
|
166
173
|
...(0 === Object.keys(elided).length ? {} : { elided }),
|
|
167
174
|
...(layoutShift === undefined ? {} : { layoutShift }),
|
|
168
175
|
longTasks,
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
*
|
|
13
13
|
* Pure: no IO, no clock. The tool layer loads flows + contract and calls this.
|
|
14
14
|
*/
|
|
15
|
-
import { AnchorKind, } from '@reticlehq/core';
|
|
15
|
+
import { AnchorKind, PredicateKind, } from '@reticlehq/core';
|
|
16
16
|
import { classifyFlowAssertions, FlowAssertionGrade } from '../../language/flows/flow-classify.js';
|
|
17
17
|
import { successLabel } from '../../language/flows/flow-success.js';
|
|
18
18
|
import { flowRisk, latestRun, rankByRisk, RiskLevel } from './flow-risk.js';
|
|
@@ -25,16 +25,45 @@ function flatten(steps) {
|
|
|
25
25
|
}
|
|
26
26
|
return out;
|
|
27
27
|
}
|
|
28
|
+
/*
|
|
29
|
+
* The vocabulary a flow uses, read out of a predicate TREE.
|
|
30
|
+
*
|
|
31
|
+
* These were property reads when `expect` was a struct with one slot per kind. A predicate nests, and
|
|
32
|
+
* the domain model is about what a flow TALKS ABOUT, so the walk goes all the way down: a signal
|
|
33
|
+
* named inside an `allOf` two levels deep is still a signal this flow depends on, and missing it
|
|
34
|
+
* would quietly shrink the model that decides which flows a change affects.
|
|
35
|
+
*/
|
|
36
|
+
function walkClauses(predicate) {
|
|
37
|
+
if (predicate === undefined)
|
|
38
|
+
return [];
|
|
39
|
+
if (PredicateKind.ALL_OF === predicate.kind || PredicateKind.ANY_OF === predicate.kind) {
|
|
40
|
+
return predicate.predicates.flatMap(walkClauses);
|
|
41
|
+
}
|
|
42
|
+
if (PredicateKind.NOT === predicate.kind)
|
|
43
|
+
return walkClauses(predicate.predicate);
|
|
44
|
+
return [predicate];
|
|
45
|
+
}
|
|
46
|
+
function signalNamesIn(predicate) {
|
|
47
|
+
return walkClauses(predicate)
|
|
48
|
+
.filter((c) => PredicateKind.SIGNAL === c.kind)
|
|
49
|
+
.map((c) => (PredicateKind.SIGNAL === c.kind ? c.name : undefined))
|
|
50
|
+
.filter((name) => name !== undefined);
|
|
51
|
+
}
|
|
52
|
+
function elementTestidsIn(predicate) {
|
|
53
|
+
return walkClauses(predicate)
|
|
54
|
+
.map((c) => (PredicateKind.ELEMENT === c.kind ? c.query.testid : undefined))
|
|
55
|
+
.filter((testid) => testid !== undefined);
|
|
56
|
+
}
|
|
28
57
|
function flowSignals(flow) {
|
|
29
58
|
const set = new Set();
|
|
30
59
|
for (const step of flatten(flow.steps)) {
|
|
31
60
|
if (step.anchor.kind === AnchorKind.SIGNAL)
|
|
32
61
|
set.add(step.anchor.name);
|
|
33
|
-
|
|
34
|
-
set.add(
|
|
62
|
+
for (const name of signalNamesIn(step.expect))
|
|
63
|
+
set.add(name);
|
|
35
64
|
}
|
|
36
|
-
|
|
37
|
-
set.add(
|
|
65
|
+
for (const name of signalNamesIn(flow.success))
|
|
66
|
+
set.add(name);
|
|
38
67
|
return [...set];
|
|
39
68
|
}
|
|
40
69
|
function flowTestids(flow) {
|
|
@@ -42,11 +71,11 @@ function flowTestids(flow) {
|
|
|
42
71
|
for (const step of flatten(flow.steps)) {
|
|
43
72
|
if (step.anchor.kind === AnchorKind.TESTID)
|
|
44
73
|
set.add(step.anchor.value);
|
|
45
|
-
|
|
46
|
-
set.add(
|
|
74
|
+
for (const testid of elementTestidsIn(step.expect))
|
|
75
|
+
set.add(testid);
|
|
47
76
|
}
|
|
48
|
-
|
|
49
|
-
set.add(
|
|
77
|
+
for (const testid of elementTestidsIn(flow.success))
|
|
78
|
+
set.add(testid);
|
|
50
79
|
return [...set];
|
|
51
80
|
}
|
|
52
81
|
const EMPTY_CONTRACT = { testids: [], signals: [], stores: [], flows: [] };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type
|
|
1
|
+
import { type Predicate } from '@reticlehq/core';
|
|
2
2
|
/**
|
|
3
3
|
* Anti-reward-hacking (research-mandated). Test-gated agents pass gates by WEAKENING assertions — the
|
|
4
4
|
* canonical documented failure. Flows are git-checked and agent-editable, so a mustHold that dropped
|
|
@@ -7,10 +7,10 @@ import { type FlowExpect } from '@reticlehq/core';
|
|
|
7
7
|
* and flow_verify surface it.
|
|
8
8
|
*/
|
|
9
9
|
/** True when `after` weakened `before` — a real consequence became a fakeable presence-only check. */
|
|
10
|
-
export declare function isAssertionDowngrade(before:
|
|
10
|
+
export declare function isAssertionDowngrade(before: Predicate | undefined, after: Predicate | undefined): boolean;
|
|
11
11
|
export interface StepExpect {
|
|
12
12
|
step: number;
|
|
13
|
-
expect?:
|
|
13
|
+
expect?: Predicate;
|
|
14
14
|
}
|
|
15
15
|
interface DowngradeFinding {
|
|
16
16
|
step: number;
|
|
@@ -4,7 +4,7 @@ import { PhenomenonType, type JournalAction, type ReticleEvent } from '@reticleh
|
|
|
4
4
|
* scanning by the agent: the layer names the fault, the agent acts on the name. `pre-hydration-click`
|
|
5
5
|
* lands once the hydration signal (a later item) exists; the three here need only journal data.
|
|
6
6
|
*/
|
|
7
|
-
|
|
7
|
+
interface Finding {
|
|
8
8
|
phenomenon: PhenomenonType;
|
|
9
9
|
evidence: Record<string, unknown>;
|
|
10
10
|
}
|
|
@@ -24,3 +24,4 @@ export declare function detectPreHydrationClicks(events: readonly ReticleEvent[]
|
|
|
24
24
|
export declare function detectSwallowedErrors(events: readonly ReticleEvent[]): Finding[];
|
|
25
25
|
/** Run every journal-only matcher over a session's events + actions. */
|
|
26
26
|
export declare function detectPhenomena(events: readonly ReticleEvent[], actions: readonly JournalAction[]): Finding[];
|
|
27
|
+
export {};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The session gap, as a person reads it — and as a Stop hook prints it.
|
|
3
|
+
*
|
|
4
|
+
* `gapSummary` folds the journal into what was claimed and what held. This is its only renderer, so
|
|
5
|
+
* `reticle report` and the hook cannot describe the same session two different ways.
|
|
6
|
+
*/
|
|
7
|
+
import type { GapSummary } from './gap-summary.js';
|
|
8
|
+
/**
|
|
9
|
+
* One line when the agent finishes without a single `yes`, and nothing otherwise.
|
|
10
|
+
*
|
|
11
|
+
* Silent after a `yes` on purpose: a hook that speaks on every stop is a hook people turn off, and the
|
|
12
|
+
* question it exists to raise is "did anything get proved at all". Every answer that is not a yes is
|
|
13
|
+
* counted by name — an `unknown` is never folded into anything, because that fold is the false green.
|
|
14
|
+
*/
|
|
15
|
+
export declare function gapHookLine(gap: GapSummary): string | undefined;
|
|
16
|
+
/** The whole gap: held of claimed first, then each kind of not-held with who can act on it. */
|
|
17
|
+
export declare function gapReportLines(gap: GapSummary): string[];
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
const HOOK_PREFIX = 'Reticle: not verified.';
|
|
2
|
+
const NO_CLAIM_HOOK = `${HOOK_PREFIX} No claim was checked this session`;
|
|
3
|
+
const NO_CLAIM_REPORT = 'no claims this session, so nothing was verified';
|
|
4
|
+
const plural = (n, word) => `${String(n)} ${word}${1 === n ? '' : 's'}`;
|
|
5
|
+
/**
|
|
6
|
+
* One line when the agent finishes without a single `yes`, and nothing otherwise.
|
|
7
|
+
*
|
|
8
|
+
* Silent after a `yes` on purpose: a hook that speaks on every stop is a hook people turn off, and the
|
|
9
|
+
* question it exists to raise is "did anything get proved at all". Every answer that is not a yes is
|
|
10
|
+
* counted by name — an `unknown` is never folded into anything, because that fold is the false green.
|
|
11
|
+
*/
|
|
12
|
+
export function gapHookLine(gap) {
|
|
13
|
+
if (0 < gap.held)
|
|
14
|
+
return undefined;
|
|
15
|
+
if (0 === gap.claims)
|
|
16
|
+
return NO_CLAIM_HOOK;
|
|
17
|
+
const parts = [
|
|
18
|
+
0 < gap.failed ? `${String(gap.failed)} failed` : undefined,
|
|
19
|
+
0 < gap.undecided ? `${String(gap.undecided)} unknown` : undefined,
|
|
20
|
+
0 < gap.nothingToProve ? `${String(gap.nothingToProve)} proved nothing` : undefined,
|
|
21
|
+
].filter((part) => part !== undefined);
|
|
22
|
+
return `${HOOK_PREFIX} ${plural(gap.claims, 'claim')}: ${parts.join(', ')}`;
|
|
23
|
+
}
|
|
24
|
+
/** The whole gap: held of claimed first, then each kind of not-held with who can act on it. */
|
|
25
|
+
export function gapReportLines(gap) {
|
|
26
|
+
if (0 === gap.claims)
|
|
27
|
+
return [NO_CLAIM_REPORT];
|
|
28
|
+
const lines = [`${String(gap.held)} of ${plural(gap.claims, 'claim')} held`];
|
|
29
|
+
if (0 < gap.failed) {
|
|
30
|
+
const caught = 0 < gap.falseGreensCaught
|
|
31
|
+
? `, ${String(gap.falseGreensCaught)} of them a green a channel contradicted`
|
|
32
|
+
: '';
|
|
33
|
+
lines.push(`${String(gap.failed)} failed${caught}`);
|
|
34
|
+
}
|
|
35
|
+
if (0 < gap.undecided) {
|
|
36
|
+
const owners = Object.entries(gap.undecidedBy)
|
|
37
|
+
.map(([owner, n]) => `${owner}: ${String(n)}`)
|
|
38
|
+
.join(', ');
|
|
39
|
+
lines.push(`${String(gap.undecided)} unknown${'' === owners ? '' : ` (${owners})`}`);
|
|
40
|
+
}
|
|
41
|
+
if (0 < gap.nothingToProve)
|
|
42
|
+
lines.push(`${String(gap.nothingToProve)} proved nothing`);
|
|
43
|
+
for (const failure of gap.failures)
|
|
44
|
+
lines.push(` ✗ ${failure.claim}`);
|
|
45
|
+
return lines;
|
|
46
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The gap, as a number the person who ran the session can see.
|
|
3
|
+
*
|
|
4
|
+
* Reticle's whole claim is that it measures the distance between what an agent believed happened
|
|
5
|
+
* and what did. It measures that on every verdict, writes the answer into the journal, and shows it
|
|
6
|
+
* to nobody: the counts exist, the reasons exist, the contradictions exist, and no surface anywhere
|
|
7
|
+
* says "you made 14 claims, 9 held, 2 were false, 3 could not be decided, and here is who owns each
|
|
8
|
+
* of those three". The value of the product is the one thing it never reports.
|
|
9
|
+
*
|
|
10
|
+
* A FOLD over the journal, never a second store — the rule `run-context.ts` next door states, for
|
|
11
|
+
* the same reason: two ways to compute one number is worse than one way, because the day they
|
|
12
|
+
* disagree the disagreement lands inside a verdict and a user finds it before we do.
|
|
13
|
+
*
|
|
14
|
+
* Nothing here sends anything anywhere, and that is deliberate rather than incidental. This is the
|
|
15
|
+
* LOCAL half. Whether a fleet-wide version of the same number is worth collecting is a separate
|
|
16
|
+
* decision with a separate owner, and building the local one first is what makes that decision
|
|
17
|
+
* answerable from something other than a guess.
|
|
18
|
+
*/
|
|
19
|
+
import { VerifiedReason, VerdictAttribution, type JournalAction } from '@reticlehq/core';
|
|
20
|
+
/** One claim that did not hold, in the words it was made and the engine's reason for refusing it. */
|
|
21
|
+
export interface GapFailure {
|
|
22
|
+
claim: string;
|
|
23
|
+
reason?: VerifiedReason;
|
|
24
|
+
}
|
|
25
|
+
export interface GapSummary {
|
|
26
|
+
/** Actions that asserted something. An action that only drove the app is not a claim. */
|
|
27
|
+
claims: number;
|
|
28
|
+
held: number;
|
|
29
|
+
failed: number;
|
|
30
|
+
/** `unknown`: Reticle could not tell. Never folded into held or failed — that is the false green. */
|
|
31
|
+
undecided: number;
|
|
32
|
+
/**
|
|
33
|
+
* `no-fault`: nothing was declared, so nothing was checked.
|
|
34
|
+
*
|
|
35
|
+
* The fourth answer, kept apart from the other three. Folding it anywhere is how a session that
|
|
36
|
+
* proved nothing reads as a session that proved something.
|
|
37
|
+
*/
|
|
38
|
+
nothingToProve: number;
|
|
39
|
+
/**
|
|
40
|
+
* WHO can act on each undecided verdict, counted per owner.
|
|
41
|
+
*
|
|
42
|
+
* The only part of an `unknown` a reader can do anything with: the four owners imply four
|
|
43
|
+
* different next moves — wait, fix the app, fix the harness, look again. Empty when nothing was
|
|
44
|
+
* undecided, and a verdict that recorded no reason contributes to no owner rather than to a
|
|
45
|
+
* guessed one.
|
|
46
|
+
*/
|
|
47
|
+
undecidedBy: Partial<Record<VerdictAttribution, number>>;
|
|
48
|
+
/**
|
|
49
|
+
* Failures where a CHANNEL disagreed with what the app showed.
|
|
50
|
+
*
|
|
51
|
+
* Counted apart from an ordinary failure because they mean different things to the reader: an
|
|
52
|
+
* assertion that failed is a check doing its job, and a contradiction is a green that would have
|
|
53
|
+
* been believed. This number is the product's own headline, measured on the reader's own session.
|
|
54
|
+
*/
|
|
55
|
+
falseGreensCaught: number;
|
|
56
|
+
failures: GapFailure[];
|
|
57
|
+
}
|
|
58
|
+
export declare function gapSummary(actions: readonly JournalAction[]): GapSummary;
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The gap, as a number the person who ran the session can see.
|
|
3
|
+
*
|
|
4
|
+
* Reticle's whole claim is that it measures the distance between what an agent believed happened
|
|
5
|
+
* and what did. It measures that on every verdict, writes the answer into the journal, and shows it
|
|
6
|
+
* to nobody: the counts exist, the reasons exist, the contradictions exist, and no surface anywhere
|
|
7
|
+
* says "you made 14 claims, 9 held, 2 were false, 3 could not be decided, and here is who owns each
|
|
8
|
+
* of those three". The value of the product is the one thing it never reports.
|
|
9
|
+
*
|
|
10
|
+
* A FOLD over the journal, never a second store — the rule `run-context.ts` next door states, for
|
|
11
|
+
* the same reason: two ways to compute one number is worse than one way, because the day they
|
|
12
|
+
* disagree the disagreement lands inside a verdict and a user finds it before we do.
|
|
13
|
+
*
|
|
14
|
+
* Nothing here sends anything anywhere, and that is deliberate rather than incidental. This is the
|
|
15
|
+
* LOCAL half. Whether a fleet-wide version of the same number is worth collecting is a separate
|
|
16
|
+
* decision with a separate owner, and building the local one first is what makes that decision
|
|
17
|
+
* answerable from something other than a guess.
|
|
18
|
+
*/
|
|
19
|
+
import { JournalVerdictEffectSchema, Verified, VerifiedReason, VerdictAttribution, verdictAttributionOf, } from '@reticlehq/core';
|
|
20
|
+
/**
|
|
21
|
+
* The reason, as the engine's own enum, or undefined.
|
|
22
|
+
*
|
|
23
|
+
* The journal stores `reason` as a free string rather than the enum — deliberately, because the
|
|
24
|
+
* ledger has to keep reading records written by an older engine whose vocabulary it cannot know.
|
|
25
|
+
* So a reason this build does not recognise is treated exactly like an absent one: no owner. The
|
|
26
|
+
* alternative is attributing an unknown word to one of four buckets, which is inventing an
|
|
27
|
+
* attribution nobody recorded.
|
|
28
|
+
*/
|
|
29
|
+
function knownReason(reason) {
|
|
30
|
+
if (reason === undefined)
|
|
31
|
+
return undefined;
|
|
32
|
+
const known = Object.values(VerifiedReason);
|
|
33
|
+
return known.includes(reason) ? reason : undefined;
|
|
34
|
+
}
|
|
35
|
+
/** The verdict a verification tool recorded on this action, if it recorded one. */
|
|
36
|
+
function verdictOf(action) {
|
|
37
|
+
const parsed = JournalVerdictEffectSchema.safeParse(action.effect);
|
|
38
|
+
return parsed.success ? parsed.data : undefined;
|
|
39
|
+
}
|
|
40
|
+
export function gapSummary(actions) {
|
|
41
|
+
const summary = {
|
|
42
|
+
claims: 0,
|
|
43
|
+
held: 0,
|
|
44
|
+
failed: 0,
|
|
45
|
+
undecided: 0,
|
|
46
|
+
nothingToProve: 0,
|
|
47
|
+
undecidedBy: {},
|
|
48
|
+
falseGreensCaught: 0,
|
|
49
|
+
failures: [],
|
|
50
|
+
};
|
|
51
|
+
for (const action of actions) {
|
|
52
|
+
const verdict = verdictOf(action);
|
|
53
|
+
if (verdict === undefined)
|
|
54
|
+
continue;
|
|
55
|
+
summary.claims += 1;
|
|
56
|
+
switch (verdict.verified) {
|
|
57
|
+
case Verified.YES:
|
|
58
|
+
summary.held += 1;
|
|
59
|
+
break;
|
|
60
|
+
case Verified.NO: {
|
|
61
|
+
summary.failed += 1;
|
|
62
|
+
if (VerifiedReason.CONTRADICTED === verdict.reason)
|
|
63
|
+
summary.falseGreensCaught += 1;
|
|
64
|
+
const reason = knownReason(verdict.reason);
|
|
65
|
+
summary.failures.push(reason === undefined ? { claim: verdict.claim } : { claim: verdict.claim, reason });
|
|
66
|
+
break;
|
|
67
|
+
}
|
|
68
|
+
case Verified.UNKNOWN: {
|
|
69
|
+
summary.undecided += 1;
|
|
70
|
+
// A record written before `reason` existed has none, and must not be read as though it did:
|
|
71
|
+
// counting it under an owner would invent an attribution nobody recorded.
|
|
72
|
+
const reason = knownReason(verdict.reason);
|
|
73
|
+
const owner = reason === undefined ? undefined : verdictAttributionOf(reason);
|
|
74
|
+
if (owner !== undefined && VerdictAttribution.NONE !== owner) {
|
|
75
|
+
summary.undecidedBy[owner] = (summary.undecidedBy[owner] ?? 0) + 1;
|
|
76
|
+
}
|
|
77
|
+
break;
|
|
78
|
+
}
|
|
79
|
+
case Verified.NO_FAULT:
|
|
80
|
+
summary.nothingToProve += 1;
|
|
81
|
+
break;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return summary;
|
|
85
|
+
}
|
|
@@ -35,7 +35,10 @@ const CHECK_GLYPH = {
|
|
|
35
35
|
[Verified.NO_FAULT]: '–',
|
|
36
36
|
};
|
|
37
37
|
function checkLine(c) {
|
|
38
|
-
|
|
38
|
+
// No kind, no prefix. A check folded from a journal that never recorded one has nothing to name
|
|
39
|
+
// here, and printing a placeholder would read as a kind.
|
|
40
|
+
const read = c.kind === undefined ? '' : `${c.kind}: `;
|
|
41
|
+
return ` ${CHECK_GLYPH[c.status]} ${read}${c.predicate}`;
|
|
39
42
|
}
|
|
40
43
|
function riskLine(r) {
|
|
41
44
|
return ` ⚠ ${r.surface} (${r.severity})${r.gated ? ' [GATED]' : ''} — ${r.detail}`;
|
|
@@ -22,8 +22,13 @@ export declare const OVP_ARTIFACT_VERSION = 1;
|
|
|
22
22
|
export interface ExportedCheck {
|
|
23
23
|
/** What was claimed, as the person or agent wrote it. */
|
|
24
24
|
readonly claim: string;
|
|
25
|
-
/**
|
|
26
|
-
|
|
25
|
+
/**
|
|
26
|
+
* Which source of truth answering it required.
|
|
27
|
+
*
|
|
28
|
+
* Absent when the run did not record one -- which is the same rule the rest of this file follows:
|
|
29
|
+
* a kind nobody wrote down is left out, never filled in with a plausible one.
|
|
30
|
+
*/
|
|
31
|
+
readonly reads?: PredicateKind;
|
|
27
32
|
/**
|
|
28
33
|
* Was the claim written down before the action, or after it?
|
|
29
34
|
*
|
|
@@ -31,7 +31,7 @@ function exportedChecks(run) {
|
|
|
31
31
|
for (const check of run.checks) {
|
|
32
32
|
decided.push({
|
|
33
33
|
claim: check.predicate,
|
|
34
|
-
reads: check.kind,
|
|
34
|
+
...(check.kind === undefined ? {} : { reads: check.kind }),
|
|
35
35
|
verdict: check.status,
|
|
36
36
|
// Spread, so a value the run did not record leaves the key out rather than writing a
|
|
37
37
|
// comfortable default. `declaredBeforeActing: false` says the claim came afterwards; absent
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import {} from '@reticlehq/core';
|
|
3
3
|
import { runContextFor } from './artifact/run-context.js';
|
|
4
|
+
import { gapSummary } from './artifact/gap-summary.js';
|
|
4
5
|
import { openSessionIntents } from '../../memory/intent/open-intents.js';
|
|
5
6
|
import { ReticleTool } from '@reticlehq/core';
|
|
6
7
|
import { sessionIdShape } from '../../surface/tools/tool-kit.js';
|
|
@@ -27,9 +28,13 @@ const CONTEXT_OUTPUT_SCHEMA = {
|
|
|
27
28
|
proven: z
|
|
28
29
|
.array(z.unknown())
|
|
29
30
|
.describe('Claims a verdict already settled, each `{ claim, verified, source?, doc?, epoch? }`. Read this before re-driving something: re-proving it is slow, and assuming it is a false green.'),
|
|
31
|
+
gap: z
|
|
32
|
+
.object({})
|
|
33
|
+
.passthrough()
|
|
34
|
+
.describe('The distance between what was claimed and what held, over THIS session: `{ claims, held, failed, undecided, nothingToProve, undecidedBy, falseGreensCaught, failures }`. `undecidedBy` says WHO can act on each unknown — environment (wait), code (fix the app), harness (fix Reticle or the call), could-not-see (look again). Folded from the same journal as everything else here, so it cannot disagree with it.'),
|
|
30
35
|
remaining: z
|
|
31
36
|
.array(z.string())
|
|
32
|
-
.describe('The intents from .reticle/intent
|
|
37
|
+
.describe('The intents from .reticle/intent/ that no verdict has discharged. Derived from the ledger, never a guess at what you meant to do next.'),
|
|
33
38
|
};
|
|
34
39
|
/** The ledger and the live page state this run is folded out of. `[]`/undefined when nothing is connected. */
|
|
35
40
|
async function evidenceFor(deps, sessionId) {
|
|
@@ -57,14 +62,27 @@ async function evidenceFor(deps, sessionId) {
|
|
|
57
62
|
export const CONTEXT_TOOLS = [
|
|
58
63
|
{
|
|
59
64
|
name: ReticleTool.CONTEXT,
|
|
60
|
-
description: 'What THIS run has already established, so you do not rediscover it. Call it when your own copy is gone: right after a compaction, at the top of a fresh sub-agent, or at the start of a turn you did not begin. Returns `established` (what Reticle OBSERVED, with the source file:line where one was reported), `proven` (claims a verdict already settled, so you neither re-prove them nor assume them) and `remaining` (the intents from .reticle/intent
|
|
65
|
+
description: 'What THIS run has already established, so you do not rediscover it. Call it when your own copy is gone: right after a compaction, at the top of a fresh sub-agent, or at the start of a turn you did not begin. Returns `established` (what Reticle OBSERVED, with the source file:line where one was reported), `proven` (claims a verdict already settled, so you neither re-prove them nor assume them) and `remaining` (the intents from .reticle/intent/ that nothing has discharged). It is a FOLD over the journal, never a second store, so it cannot disagree with the ledger. Bounded and superseding rather than accumulating, and anything observed under a replaced document or before your last source edit is already dropped rather than presented as current. Only what Reticle observed goes in: it never reports what you intended or believed.',
|
|
61
66
|
example: {},
|
|
62
67
|
inputSchema: { ...sessionIdShape },
|
|
63
68
|
outputSchema: CONTEXT_OUTPUT_SCHEMA,
|
|
64
69
|
handler: async (deps, args) => {
|
|
65
70
|
const sessionId = asString(args['sessionId']);
|
|
66
71
|
const evidence = await evidenceFor(deps, sessionId);
|
|
67
|
-
|
|
72
|
+
const context = runContextFor({
|
|
73
|
+
...evidence,
|
|
74
|
+
intents: await openSessionIntents(deps, sessionId),
|
|
75
|
+
});
|
|
76
|
+
/*
|
|
77
|
+
* The gap rides on `reticle_context` rather than on a tool of its own.
|
|
78
|
+
*
|
|
79
|
+
* This is the one call an agent makes when it needs to know what the run has already settled,
|
|
80
|
+
* which is exactly the moment "and what did NOT settle, and whose problem each of those is"
|
|
81
|
+
* is worth reading. It is folded from the same journal the rest of this response is folded
|
|
82
|
+
* from, so it costs one more pass over an array already in hand and cannot disagree with the
|
|
83
|
+
* `proven` list beside it.
|
|
84
|
+
*/
|
|
85
|
+
return { ...context, gap: gapSummary(evidence.actions) };
|
|
68
86
|
},
|
|
69
87
|
},
|
|
70
88
|
];
|