harnery 0.34.1 → 0.35.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.
Files changed (212) hide show
  1. package/dist/commander.d.ts.map +1 -1
  2. package/dist/commander.js +2 -0
  3. package/dist/commands/agents.d.ts +15 -4
  4. package/dist/commands/agents.d.ts.map +1 -1
  5. package/dist/commands/agents.js +89 -175
  6. package/dist/commands/browse.d.ts.map +1 -1
  7. package/dist/commands/browse.js +26 -2
  8. package/dist/commands/decision.d.ts.map +1 -1
  9. package/dist/commands/decision.js +2 -2
  10. package/dist/commands/events.d.ts.map +1 -1
  11. package/dist/commands/events.js +18 -1
  12. package/dist/commands/presence.d.ts.map +1 -1
  13. package/dist/commands/presence.js +2 -2
  14. package/dist/commands/semantic.d.ts +4 -0
  15. package/dist/commands/semantic.d.ts.map +1 -0
  16. package/dist/commands/semantic.js +147 -0
  17. package/dist/commands/web.d.ts.map +1 -1
  18. package/dist/commands/web.js +17 -0
  19. package/dist/core/agents/canonical-emit.d.ts +3 -0
  20. package/dist/core/agents/canonical-emit.d.ts.map +1 -1
  21. package/dist/core/agents/canonical-emit.js +12 -0
  22. package/dist/core/agents/claim-path.d.ts +11 -0
  23. package/dist/core/agents/claim-path.d.ts.map +1 -0
  24. package/dist/core/agents/claim-path.js +22 -0
  25. package/dist/core/agents/coord-client.d.ts +29 -3
  26. package/dist/core/agents/coord-client.d.ts.map +1 -1
  27. package/dist/core/agents/coord-client.js +88 -9
  28. package/dist/core/agents/live-authority-v3.d.ts.map +1 -1
  29. package/dist/core/agents/live-authority-v3.js +1 -3
  30. package/dist/core/agents/live-lifecycle-v3.js +3 -3
  31. package/dist/core/agents/live-observation-v3.d.ts.map +1 -1
  32. package/dist/core/agents/live-observation-v3.js +2 -2
  33. package/dist/core/agents/render/prompt-context.js +12 -10
  34. package/dist/core/agents/rules/stop-hook.d.ts.map +1 -1
  35. package/dist/core/agents/rules/stop-hook.js +23 -11
  36. package/dist/core/agents/session-name-display.d.ts +1 -0
  37. package/dist/core/agents/session-name-display.d.ts.map +1 -1
  38. package/dist/core/agents/session-name-display.js +8 -1
  39. package/dist/core/agents/state/heartbeat-reader.d.ts +46 -0
  40. package/dist/core/agents/state/heartbeat-reader.d.ts.map +1 -0
  41. package/dist/core/agents/state/heartbeat-reader.js +25 -0
  42. package/dist/core/agents/state/heartbeat-writer.d.ts +3 -48
  43. package/dist/core/agents/state/heartbeat-writer.d.ts.map +1 -1
  44. package/dist/core/agents/state/heartbeat-writer.js +16 -34
  45. package/dist/core/agents/state/live-coordination-view.d.ts +1 -1
  46. package/dist/core/agents/state/live-coordination-view.d.ts.map +1 -1
  47. package/dist/core/agents/state/live-coordination-view.js +25 -1
  48. package/dist/core/agents/state/names.d.ts +9 -1
  49. package/dist/core/agents/state/names.d.ts.map +1 -1
  50. package/dist/core/agents/state/names.js +190 -182
  51. package/dist/core/agents/state/stale-sweep.d.ts.map +1 -1
  52. package/dist/core/agents/state/stale-sweep.js +23 -5
  53. package/dist/core/artifacts/constants.d.ts +5 -0
  54. package/dist/core/artifacts/constants.d.ts.map +1 -0
  55. package/dist/core/artifacts/constants.js +4 -0
  56. package/dist/core/artifacts/index.d.ts +3 -3
  57. package/dist/core/artifacts/index.d.ts.map +1 -1
  58. package/dist/core/artifacts/index.js +50 -7
  59. package/dist/core/events/v3/base-contract.d.ts +795 -0
  60. package/dist/core/events/v3/base-contract.d.ts.map +1 -1
  61. package/dist/core/events/v3/base-contract.js +33 -0
  62. package/dist/core/events/v3/capabilities.d.ts +11 -0
  63. package/dist/core/events/v3/capabilities.d.ts.map +1 -1
  64. package/dist/core/events/v3/capabilities.js +23 -0
  65. package/dist/core/events/v3/generated.d.ts +1 -1
  66. package/dist/core/events/v3/generated.js +1 -1
  67. package/dist/core/events/v3/index.d.ts +4 -3
  68. package/dist/core/events/v3/index.d.ts.map +1 -1
  69. package/dist/core/events/v3/index.js +4 -3
  70. package/dist/core/events/v3/latency.d.ts +31 -1
  71. package/dist/core/events/v3/latency.d.ts.map +1 -1
  72. package/dist/core/events/v3/latency.js +127 -3
  73. package/dist/core/events/v3/live-routing.d.ts +30 -0
  74. package/dist/core/events/v3/live-routing.d.ts.map +1 -1
  75. package/dist/core/events/v3/live-routing.js +109 -1
  76. package/dist/core/events/v3/producers/command-recorder.d.ts.map +1 -1
  77. package/dist/core/events/v3/producers/command-recorder.js +10 -1
  78. package/dist/core/events/v3/producers/coordination-recorder.d.ts.map +1 -1
  79. package/dist/core/events/v3/producers/coordination-recorder.js +14 -3
  80. package/dist/core/events/v3/producers/hook-base.d.ts +17 -1
  81. package/dist/core/events/v3/producers/hook-base.d.ts.map +1 -1
  82. package/dist/core/events/v3/producers/hook-base.js +82 -1
  83. package/dist/core/events/v3/producers/intake.d.ts +6 -0
  84. package/dist/core/events/v3/producers/intake.d.ts.map +1 -1
  85. package/dist/core/events/v3/producers/intake.js +11 -0
  86. package/dist/core/events/v3/producers/recorder.d.ts +62 -0
  87. package/dist/core/events/v3/producers/recorder.d.ts.map +1 -1
  88. package/dist/core/events/v3/producers/recorder.js +654 -18
  89. package/dist/core/events/v3/runtime-telemetry-capabilities.d.ts +51 -0
  90. package/dist/core/events/v3/runtime-telemetry-capabilities.d.ts.map +1 -0
  91. package/dist/core/events/v3/runtime-telemetry-capabilities.js +54 -0
  92. package/dist/core/events/v3/turn-telemetry.d.ts +9 -0
  93. package/dist/core/events/v3/turn-telemetry.d.ts.map +1 -1
  94. package/dist/core/hooks/adapter/parse.d.ts +3 -0
  95. package/dist/core/hooks/adapter/parse.d.ts.map +1 -1
  96. package/dist/core/hooks/adapter/parse.js +14 -0
  97. package/dist/core/hooks/adapter/runtime-telemetry.d.ts +119 -0
  98. package/dist/core/hooks/adapter/runtime-telemetry.d.ts.map +1 -0
  99. package/dist/core/hooks/adapter/runtime-telemetry.js +568 -0
  100. package/dist/core/hooks/cli.js +111 -36
  101. package/dist/core/hooks/remediation-cap.d.ts +34 -0
  102. package/dist/core/hooks/remediation-cap.d.ts.map +1 -0
  103. package/dist/core/hooks/remediation-cap.js +71 -0
  104. package/dist/core/hooks/resolve/transcript.d.ts +25 -9
  105. package/dist/core/hooks/resolve/transcript.d.ts.map +1 -1
  106. package/dist/core/hooks/resolve/transcript.js +22 -10
  107. package/dist/core/semantic/adapters.d.ts +34 -0
  108. package/dist/core/semantic/adapters.d.ts.map +1 -0
  109. package/dist/core/semantic/adapters.js +222 -0
  110. package/dist/core/semantic/contract.d.ts +588 -0
  111. package/dist/core/semantic/contract.d.ts.map +1 -0
  112. package/dist/core/semantic/contract.js +230 -0
  113. package/dist/core/semantic/evidence.d.ts +11 -0
  114. package/dist/core/semantic/evidence.d.ts.map +1 -0
  115. package/dist/core/semantic/evidence.js +262 -0
  116. package/dist/core/semantic/index.d.ts +10 -0
  117. package/dist/core/semantic/index.d.ts.map +1 -0
  118. package/dist/core/semantic/index.js +9 -0
  119. package/dist/core/semantic/once.d.ts +33 -0
  120. package/dist/core/semantic/once.d.ts.map +1 -0
  121. package/dist/core/semantic/once.js +293 -0
  122. package/dist/core/semantic/prompt.d.ts +12 -0
  123. package/dist/core/semantic/prompt.d.ts.map +1 -0
  124. package/dist/core/semantic/prompt.js +70 -0
  125. package/dist/core/semantic/scheduler.d.ts +16 -0
  126. package/dist/core/semantic/scheduler.d.ts.map +1 -0
  127. package/dist/core/semantic/scheduler.js +67 -0
  128. package/dist/core/semantic/service-status.d.ts +31 -0
  129. package/dist/core/semantic/service-status.d.ts.map +1 -0
  130. package/dist/core/semantic/service-status.js +84 -0
  131. package/dist/core/semantic/service.d.ts +51 -0
  132. package/dist/core/semantic/service.d.ts.map +1 -0
  133. package/dist/core/semantic/service.js +470 -0
  134. package/dist/core/semantic/storage.d.ts +71 -0
  135. package/dist/core/semantic/storage.d.ts.map +1 -0
  136. package/dist/core/semantic/storage.js +179 -0
  137. package/dist/core/semantic/validate.d.ts +16 -0
  138. package/dist/core/semantic/validate.d.ts.map +1 -0
  139. package/dist/core/semantic/validate.js +174 -0
  140. package/dist/lib/browser/client.d.ts +11 -0
  141. package/dist/lib/browser/client.d.ts.map +1 -1
  142. package/dist/lib/browser/client.js +13 -0
  143. package/dist/lib/browser/index.d.ts +1 -0
  144. package/dist/lib/browser/index.d.ts.map +1 -1
  145. package/dist/lib/browser/standalone-html.d.ts +25 -0
  146. package/dist/lib/browser/standalone-html.d.ts.map +1 -0
  147. package/dist/lib/browser/standalone-html.js +284 -0
  148. package/dist/lib/instructions/apply.d.ts.map +1 -1
  149. package/dist/lib/instructions/apply.js +1 -0
  150. package/dist/lib/instructions/templates.d.ts +2 -0
  151. package/dist/lib/instructions/templates.d.ts.map +1 -1
  152. package/dist/lib/instructions/templates.js +144 -4
  153. package/package.json +6 -1
  154. package/schemas/event-v3.schema.json +2758 -478
  155. package/src/commander.ts +2 -0
  156. package/src/commands/agents.ts +102 -167
  157. package/src/commands/browse.ts +25 -2
  158. package/src/commands/decision.ts +8 -2
  159. package/src/commands/events.ts +21 -1
  160. package/src/commands/presence.ts +7 -2
  161. package/src/commands/semantic.ts +179 -0
  162. package/src/commands/web.ts +15 -0
  163. package/src/core/agents/canonical-emit.ts +15 -0
  164. package/src/core/agents/claim-path.ts +23 -0
  165. package/src/core/agents/coord-client.ts +92 -8
  166. package/src/core/agents/live-authority-v3.ts +1 -4
  167. package/src/core/agents/live-lifecycle-v3.ts +8 -3
  168. package/src/core/agents/live-observation-v3.ts +7 -2
  169. package/src/core/agents/render/prompt-context.ts +13 -10
  170. package/src/core/agents/rules/stop-hook.ts +24 -11
  171. package/src/core/agents/session-name-display.ts +9 -1
  172. package/src/core/agents/state/heartbeat-reader.ts +69 -0
  173. package/src/core/agents/state/heartbeat-writer.ts +20 -84
  174. package/src/core/agents/state/live-coordination-view.ts +26 -1
  175. package/src/core/agents/state/names.ts +190 -182
  176. package/src/core/agents/state/stale-sweep.ts +25 -5
  177. package/src/core/artifacts/constants.ts +4 -0
  178. package/src/core/artifacts/index.ts +49 -7
  179. package/src/core/events/v3/base-contract.ts +39 -0
  180. package/src/core/events/v3/capabilities.ts +34 -0
  181. package/src/core/events/v3/generated.ts +1 -1
  182. package/src/core/events/v3/index.ts +21 -0
  183. package/src/core/events/v3/latency.ts +213 -2
  184. package/src/core/events/v3/live-routing.ts +120 -1
  185. package/src/core/events/v3/producers/command-recorder.ts +10 -1
  186. package/src/core/events/v3/producers/coordination-recorder.ts +17 -5
  187. package/src/core/events/v3/producers/hook-base.ts +115 -1
  188. package/src/core/events/v3/producers/intake.ts +18 -0
  189. package/src/core/events/v3/producers/recorder.ts +856 -21
  190. package/src/core/events/v3/runtime-telemetry-capabilities.ts +137 -0
  191. package/src/core/events/v3/turn-telemetry.ts +10 -0
  192. package/src/core/hooks/adapter/parse.ts +16 -0
  193. package/src/core/hooks/adapter/runtime-telemetry.ts +805 -0
  194. package/src/core/hooks/cli.ts +123 -39
  195. package/src/core/hooks/remediation-cap.ts +77 -0
  196. package/src/core/hooks/resolve/transcript.ts +37 -11
  197. package/src/core/semantic/adapters.ts +293 -0
  198. package/src/core/semantic/contract.ts +298 -0
  199. package/src/core/semantic/evidence.ts +336 -0
  200. package/src/core/semantic/index.ts +127 -0
  201. package/src/core/semantic/once.ts +410 -0
  202. package/src/core/semantic/prompt.ts +84 -0
  203. package/src/core/semantic/scheduler.ts +107 -0
  204. package/src/core/semantic/service-status.ts +118 -0
  205. package/src/core/semantic/service.ts +580 -0
  206. package/src/core/semantic/storage.ts +286 -0
  207. package/src/core/semantic/validate.ts +212 -0
  208. package/src/lib/browser/client.ts +21 -0
  209. package/src/lib/browser/index.ts +4 -0
  210. package/src/lib/browser/standalone-html.ts +312 -0
  211. package/src/lib/instructions/apply.ts +1 -0
  212. package/src/lib/instructions/templates.ts +149 -4
@@ -0,0 +1,286 @@
1
+ import { randomBytes } from "node:crypto";
2
+ import {
3
+ chmodSync,
4
+ existsSync,
5
+ mkdirSync,
6
+ readdirSync,
7
+ readFileSync,
8
+ renameSync,
9
+ rmSync,
10
+ statSync,
11
+ writeFileSync,
12
+ } from "node:fs";
13
+ import { dirname, join, resolve } from "node:path";
14
+ import { canonicalJsonV3, sha256V3 } from "../events/v3/canonical.ts";
15
+ import type { LedgerCursorV3 } from "../events/v3/reader.ts";
16
+ import {
17
+ SEMANTIC_EVIDENCE_CONTRACT_VERSION,
18
+ SEMANTIC_PROMPT_CONTRACT_VERSION,
19
+ type SemanticAgentReadModelV2,
20
+ type SemanticConfiguredModel,
21
+ type SemanticHarness,
22
+ } from "./contract.ts";
23
+ import { validateSemanticReadModel } from "./validate.ts";
24
+
25
+ export const SEMANTIC_MANIFEST_SCHEMA_VERSION = 2 as const;
26
+ const MAX_JSON_BYTES = 512 * 1024;
27
+ const MAX_CACHE_FILES = 500;
28
+ const MAX_CACHE_AGE_MS = 30 * 24 * 60 * 60 * 1_000;
29
+
30
+ export interface SemanticReaderResolution {
31
+ harness: SemanticHarness;
32
+ configured_model: SemanticConfiguredModel;
33
+ resolved_model_id?: string;
34
+ model_attestation?: "verified" | "requested-only";
35
+ available: boolean;
36
+ reason_code?:
37
+ | "harness_unavailable"
38
+ | "authentication_unavailable"
39
+ | "model_unavailable"
40
+ | "model_mismatch";
41
+ discovered_at: string;
42
+ }
43
+
44
+ export interface SemanticPendingItem {
45
+ generation_id: string;
46
+ evidence_digest: `sha256:${string}`;
47
+ band: 1 | 2;
48
+ pending_since: string;
49
+ }
50
+
51
+ export interface SemanticCallReceipt {
52
+ generation_id: string;
53
+ started_at: string;
54
+ }
55
+
56
+ export interface SemanticManifestV2 {
57
+ schema_version: typeof SEMANTIC_MANIFEST_SCHEMA_VERSION;
58
+ ledger_genesis_id: string;
59
+ cursor?: LedgerCursorV3;
60
+ configuration_digest: `sha256:${string}`;
61
+ evidence_contract_version: typeof SEMANTIC_EVIDENCE_CONTRACT_VERSION;
62
+ prompt_contract_version: typeof SEMANTIC_PROMPT_CONTRACT_VERSION;
63
+ adapter_resolutions: Partial<Record<SemanticHarness, SemanticReaderResolution>>;
64
+ pending: SemanticPendingItem[];
65
+ call_history: SemanticCallReceipt[];
66
+ last_first_band_generation_id?: string;
67
+ newest_successful_pass?: string;
68
+ updated_at: string;
69
+ }
70
+
71
+ export interface SemanticCacheIdentity {
72
+ evidence_digest: `sha256:${string}`;
73
+ source_harness: SemanticHarness;
74
+ configured_model: SemanticConfiguredModel;
75
+ resolved_model_id: string;
76
+ evidence_contract_version: number;
77
+ prompt_contract_version: number;
78
+ }
79
+
80
+ export function semanticPaths(coordRootRaw: string) {
81
+ const root = join(resolve(coordRootRaw), ".harnery", "semantic", "v2");
82
+ return {
83
+ root,
84
+ manifest: join(root, "manifest.json"),
85
+ agents: join(root, "agents"),
86
+ cache: join(root, "cache"),
87
+ service: join(root, "service.json"),
88
+ stop: join(root, "stop.json"),
89
+ lease: join(root, "lease.json"),
90
+ log: join(root, "service.log"),
91
+ };
92
+ }
93
+
94
+ export function semanticConfigurationDigest(
95
+ resolutions: Partial<Record<SemanticHarness, SemanticReaderResolution>>,
96
+ ): `sha256:${string}` {
97
+ const stable = Object.fromEntries(
98
+ Object.entries(resolutions)
99
+ .sort(([left], [right]) => left.localeCompare(right))
100
+ .map(([harness, resolution]) => [
101
+ harness,
102
+ resolution
103
+ ? {
104
+ configured_model: resolution.configured_model,
105
+ ...(resolution.resolved_model_id
106
+ ? { resolved_model_id: resolution.resolved_model_id }
107
+ : {}),
108
+ ...(resolution.model_attestation
109
+ ? { model_attestation: resolution.model_attestation }
110
+ : {}),
111
+ available: resolution.available,
112
+ ...(resolution.reason_code ? { reason_code: resolution.reason_code } : {}),
113
+ }
114
+ : null,
115
+ ]),
116
+ );
117
+ return sha256V3(canonicalJsonV3(stable));
118
+ }
119
+
120
+ export function semanticCacheKey(identity: SemanticCacheIdentity): `sha256:${string}` {
121
+ return sha256V3(canonicalJsonV3(identity));
122
+ }
123
+
124
+ export function readSemanticManifest(coordRootRaw: string): SemanticManifestV2 | undefined {
125
+ const path = semanticPaths(coordRootRaw).manifest;
126
+ if (!existsSync(path)) return undefined;
127
+ const value = readBoundedJson<SemanticManifestV2>(path, "semantic manifest");
128
+ if (
129
+ value.schema_version !== SEMANTIC_MANIFEST_SCHEMA_VERSION ||
130
+ value.evidence_contract_version !== SEMANTIC_EVIDENCE_CONTRACT_VERSION ||
131
+ value.prompt_contract_version !== SEMANTIC_PROMPT_CONTRACT_VERSION ||
132
+ !Array.isArray(value.pending) ||
133
+ !Array.isArray(value.call_history)
134
+ ) {
135
+ throw new Error("semantic manifest has an unsupported schema");
136
+ }
137
+ return value;
138
+ }
139
+
140
+ export function writeSemanticManifest(coordRootRaw: string, manifest: SemanticManifestV2): void {
141
+ writePrivateJsonAtomic(semanticPaths(coordRootRaw).manifest, manifest);
142
+ }
143
+
144
+ export function readSemanticAgentDocument(
145
+ coordRootRaw: string,
146
+ generationId: string,
147
+ ): SemanticAgentReadModelV2 | undefined {
148
+ requireGenerationId(generationId);
149
+ const path = join(semanticPaths(coordRootRaw).agents, `${generationId}.json`);
150
+ if (!existsSync(path)) return undefined;
151
+ const value = readBoundedJson<unknown>(path, "semantic agent document");
152
+ const verdict = validateSemanticReadModel(value);
153
+ if (!verdict.ok)
154
+ throw new Error(`semantic agent document is invalid: ${verdict.issues.join(",")}`);
155
+ return verdict.value;
156
+ }
157
+
158
+ export function listSemanticAgentDocuments(coordRootRaw: string): SemanticAgentReadModelV2[] {
159
+ const dir = semanticPaths(coordRootRaw).agents;
160
+ if (!existsSync(dir)) return [];
161
+ return readdirSync(dir)
162
+ .filter((name) => /^gen_[a-zA-Z0-9-]+\.json$/.test(name))
163
+ .sort()
164
+ .flatMap((name) => {
165
+ try {
166
+ const value = readBoundedJson<unknown>(join(dir, name), "semantic agent document");
167
+ const verdict = validateSemanticReadModel(value);
168
+ return verdict.ok ? [verdict.value] : [];
169
+ } catch {
170
+ return [];
171
+ }
172
+ });
173
+ }
174
+
175
+ export function inspectSemanticDocument(
176
+ coordRootRaw: string,
177
+ instanceOrGeneration: string,
178
+ ): SemanticAgentReadModelV2 | undefined {
179
+ if (instanceOrGeneration.startsWith("gen_")) {
180
+ return readSemanticAgentDocument(coordRootRaw, instanceOrGeneration);
181
+ }
182
+ return listSemanticAgentDocuments(coordRootRaw)
183
+ .filter((document) => document.instance_id === instanceOrGeneration)
184
+ .sort((left, right) => right.generated_at.localeCompare(left.generated_at))[0];
185
+ }
186
+
187
+ export function writeSemanticAgentDocument(
188
+ coordRootRaw: string,
189
+ document: SemanticAgentReadModelV2,
190
+ ): void {
191
+ const verdict = validateSemanticReadModel(document);
192
+ if (!verdict.ok)
193
+ throw new Error(`refusing invalid semantic document: ${verdict.issues.join(",")}`);
194
+ writePrivateJsonAtomic(
195
+ join(semanticPaths(coordRootRaw).agents, `${document.generation_id}.json`),
196
+ document,
197
+ );
198
+ }
199
+
200
+ export function readSemanticCache(
201
+ coordRootRaw: string,
202
+ cacheKey: string,
203
+ ): SemanticAgentReadModelV2 | undefined {
204
+ requireDigest(cacheKey);
205
+ const path = join(semanticPaths(coordRootRaw).cache, `${cacheKey.slice("sha256:".length)}.json`);
206
+ if (!existsSync(path)) return undefined;
207
+ const value = readBoundedJson<unknown>(path, "semantic cache entry");
208
+ const verdict = validateSemanticReadModel(value);
209
+ return verdict.ok && verdict.value.reader_outcome === "accepted" ? verdict.value : undefined;
210
+ }
211
+
212
+ export function writeSemanticCache(
213
+ coordRootRaw: string,
214
+ cacheKey: string,
215
+ document: SemanticAgentReadModelV2,
216
+ ): void {
217
+ requireDigest(cacheKey);
218
+ if (document.reader_outcome !== "accepted") {
219
+ throw new Error("only accepted semantic documents may enter the cache");
220
+ }
221
+ writePrivateJsonAtomic(
222
+ join(semanticPaths(coordRootRaw).cache, `${cacheKey.slice("sha256:".length)}.json`),
223
+ document,
224
+ );
225
+ }
226
+
227
+ export function invalidateSemanticDerivedState(coordRootRaw: string): void {
228
+ const paths = semanticPaths(coordRootRaw);
229
+ rmSync(paths.agents, { recursive: true, force: true });
230
+ rmSync(paths.cache, { recursive: true, force: true });
231
+ rmSync(paths.manifest, { force: true });
232
+ }
233
+
234
+ export function pruneSemanticStorage(
235
+ coordRootRaw: string,
236
+ options: { keepGenerations: ReadonlySet<string>; now?: number; maxCacheFiles?: number },
237
+ ): void {
238
+ const paths = semanticPaths(coordRootRaw);
239
+ if (existsSync(paths.agents)) {
240
+ for (const name of readdirSync(paths.agents)) {
241
+ const generationId = name.endsWith(".json") ? name.slice(0, -5) : "";
242
+ if (generationId && !options.keepGenerations.has(generationId)) {
243
+ rmSync(join(paths.agents, name), { force: true });
244
+ }
245
+ }
246
+ }
247
+ if (!existsSync(paths.cache)) return;
248
+ const now = options.now ?? Date.now();
249
+ const maxFiles = Math.min(MAX_CACHE_FILES, Math.max(1, options.maxCacheFiles ?? MAX_CACHE_FILES));
250
+ const entries = readdirSync(paths.cache)
251
+ .filter((name) => /^[a-f0-9]{64}\.json$/.test(name))
252
+ .map((name) => ({ name, mtime: statSync(join(paths.cache, name)).mtimeMs }))
253
+ .sort((left, right) => right.mtime - left.mtime);
254
+ for (const [index, entry] of entries.entries()) {
255
+ if (index >= maxFiles || now - entry.mtime > MAX_CACHE_AGE_MS) {
256
+ rmSync(join(paths.cache, entry.name), { force: true });
257
+ }
258
+ }
259
+ }
260
+
261
+ function requireGenerationId(value: string): void {
262
+ if (!/^gen_[0-9a-zA-Z-]{1,128}$/.test(value)) throw new Error("invalid semantic generation id");
263
+ }
264
+
265
+ function requireDigest(value: string): void {
266
+ if (!/^sha256:[a-f0-9]{64}$/.test(value)) throw new Error("invalid semantic cache key");
267
+ }
268
+
269
+ function readBoundedJson<T>(path: string, label: string): T {
270
+ const size = statSync(path).size;
271
+ if (size <= 0 || size > MAX_JSON_BYTES) throw new Error(`${label} has invalid size ${size}`);
272
+ return JSON.parse(readFileSync(path, "utf8")) as T;
273
+ }
274
+
275
+ function writePrivateJsonAtomic(path: string, value: unknown): void {
276
+ const body = `${JSON.stringify(value, null, 2)}\n`;
277
+ if (Buffer.byteLength(body) > MAX_JSON_BYTES) {
278
+ throw new Error(`semantic file exceeds ${MAX_JSON_BYTES} bytes`);
279
+ }
280
+ mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
281
+ chmodSync(dirname(path), 0o700);
282
+ const temporary = `${path}.tmp-${process.pid}-${randomBytes(4).toString("hex")}`;
283
+ writeFileSync(temporary, body, { encoding: "utf8", flag: "wx", mode: 0o600 });
284
+ renameSync(temporary, path);
285
+ chmodSync(path, 0o600);
286
+ }
@@ -0,0 +1,212 @@
1
+ import { Value } from "@sinclair/typebox/value";
2
+ import {
3
+ SEMANTIC_TAGS,
4
+ type SemanticAgentReadModelV2,
5
+ SemanticAgentReadModelV2Schema,
6
+ type SemanticEvidenceKind,
7
+ type SemanticEvidenceV1,
8
+ type SemanticField,
9
+ type SemanticMeaningV2,
10
+ type SemanticModelReplyV2,
11
+ SemanticModelReplyV2Schema,
12
+ } from "./contract.ts";
13
+
14
+ export interface SemanticValidationSuccess<T> {
15
+ ok: true;
16
+ value: T;
17
+ }
18
+
19
+ export interface SemanticValidationFailure {
20
+ ok: false;
21
+ issues: string[];
22
+ }
23
+
24
+ export type SemanticValidationResult<T> = SemanticValidationSuccess<T> | SemanticValidationFailure;
25
+
26
+ const PRIVATE_TEXT_PATTERNS: Array<[RegExp, string]> = [
27
+ [
28
+ /(?:^|\s)(?:[A-Za-z]:\\|\\\\|\/(?:[A-Za-z0-9._-]+\/)+[A-Za-z0-9._-]+|\.\.\/|\.harnery\/)/i,
29
+ "path",
30
+ ],
31
+ [/https?:\/\//i, "url"],
32
+ [/(?:^|\s)--[a-z][a-z0-9-]*(?:\s|=|$)/i, "command_flag"],
33
+ [/(?:^|\s)[A-Z][A-Z0-9_]{2,}=/, "environment_assignment"],
34
+ [/`[^`]+`/, "command_or_code"],
35
+ [/\b(?:api[_-]?key|secret|password|token)\s*[:=]\s*\S+/i, "secret_shape"],
36
+ ];
37
+
38
+ const FORBIDDEN_CLAIM_PATTERNS: Array<[RegExp, string]> = [
39
+ [/\b\d{1,3}\s*%(?=\s|$|[.,])/, "percent_complete"],
40
+ [/\b(?:minutes?|hours?|days?)\s+(?:left|remaining)\b/i, "time_remaining"],
41
+ [/\b(?:frustrated|confused|happy|sad|excited|anxious|angry)\b/i, "emotion"],
42
+ [/\b(?:excellent|great|poor|bad)\s+(?:quality|work)\b/i, "quality"],
43
+ ];
44
+
45
+ const RESULT_KINDS = new Set<SemanticEvidenceKind>(["progress", "artifact", "action", "terminal"]);
46
+ const ATTENTION_KINDS = new Set<SemanticEvidenceKind>([
47
+ "wait",
48
+ "lifecycle",
49
+ "error",
50
+ "claim-conflict",
51
+ ]);
52
+ const COMPLETION_LANGUAGE =
53
+ /\b(?:complete|completed|done|finished|shipped|passed|succeeded|deployed|published)\b/i;
54
+
55
+ export function semanticPrivacyIssue(value: string): string | undefined {
56
+ for (const [pattern, issue] of PRIVATE_TEXT_PATTERNS) {
57
+ if (pattern.test(value)) return issue;
58
+ }
59
+ return undefined;
60
+ }
61
+
62
+ export function isSemanticPrivacySafe(value: string): boolean {
63
+ return semanticPrivacyIssue(value) === undefined;
64
+ }
65
+
66
+ export function validateSemanticEvidencePrivacy(
67
+ evidence: SemanticEvidenceV1,
68
+ ): SemanticValidationResult<SemanticEvidenceV1> {
69
+ const text: Array<[string, string | undefined]> = [
70
+ ["task", evidence.task?.value],
71
+ ["operation", evidence.operation?.label],
72
+ ["attention", evidence.attention?.label],
73
+ ...evidence.recent.map(
74
+ (item, index) => [`recent[${index}]`, item.label] as [string, string | undefined],
75
+ ),
76
+ ];
77
+ const issues = text.flatMap(([field, value]) => {
78
+ if (!value) return [];
79
+ const issue = semanticPrivacyIssue(value);
80
+ return issue ? [`${field}:${issue}`] : [];
81
+ });
82
+ return issues.length ? { ok: false, issues } : { ok: true, value: evidence };
83
+ }
84
+
85
+ export function validateSemanticModelReply(
86
+ input: unknown,
87
+ evidence: SemanticEvidenceV1,
88
+ ): SemanticValidationResult<SemanticModelReplyV2> {
89
+ if (!Value.Check(SemanticModelReplyV2Schema, input)) {
90
+ return {
91
+ ok: false,
92
+ issues: [...Value.Errors(SemanticModelReplyV2Schema, input)]
93
+ .slice(0, 12)
94
+ .map((error) => `schema:${error.path || "/"}:${error.message}`),
95
+ };
96
+ }
97
+ const reply = input as SemanticModelReplyV2;
98
+ const issues: string[] = [];
99
+ if (reply.generation_id !== evidence.generation_id) issues.push("generation_mismatch");
100
+ if (reply.evidence_digest !== evidence.evidence_digest) issues.push("evidence_digest_mismatch");
101
+
102
+ const knownIds = new Set(evidence.evidence_event_ids);
103
+ const kindById = evidenceKindByEventId(evidence);
104
+ for (const [name, field] of meaningFields(reply.meaning)) {
105
+ if (name === "tags" && Array.isArray(field.value) && field.value.length === 0) {
106
+ if (field.evidence_event_ids.length !== 0) issues.push("empty_tags_must_not_cite");
107
+ } else if (field.evidence_event_ids.length === 0) {
108
+ issues.push(`${name}:missing_citation`);
109
+ }
110
+ for (const eventId of field.evidence_event_ids) {
111
+ if (!knownIds.has(eventId)) issues.push(`${name}:unknown_citation`);
112
+ }
113
+ if (name === "next_step") {
114
+ if (field.basis !== "prediction" || field.confidence !== "low") {
115
+ issues.push("next_step:must_be_low_confidence_prediction");
116
+ }
117
+ } else if (field.basis !== "model-synthesis") {
118
+ issues.push(`${name}:must_be_model_synthesis`);
119
+ }
120
+ if (name === "expression_cue" && field.confidence === "high") {
121
+ issues.push("expression_cue:confidence_must_be_medium_or_low");
122
+ }
123
+ if (typeof field.value === "string") {
124
+ const privacyIssue = semanticPrivacyIssue(field.value);
125
+ if (privacyIssue) issues.push(`${name}:privacy_${privacyIssue}`);
126
+ for (const [pattern, issue] of FORBIDDEN_CLAIM_PATTERNS) {
127
+ if (pattern.test(field.value)) issues.push(`${name}:${issue}`);
128
+ }
129
+ if (/\bblocked\b/i.test(field.value) && name !== "attention") {
130
+ issues.push(`${name}:unsupported_blocker_language`);
131
+ }
132
+ if (
133
+ COMPLETION_LANGUAGE.test(field.value) &&
134
+ !citationsSupport(field, kindById, RESULT_KINDS)
135
+ ) {
136
+ issues.push(`${name}:unsupported_completion_language`);
137
+ }
138
+ }
139
+ }
140
+
141
+ if (
142
+ reply.meaning.recent_result &&
143
+ !citationsSupport(reply.meaning.recent_result, kindById, RESULT_KINDS)
144
+ ) {
145
+ issues.push("recent_result:unsupported_evidence_kind");
146
+ }
147
+ if (
148
+ reply.meaning.attention &&
149
+ !citationsSupport(reply.meaning.attention, kindById, ATTENTION_KINDS)
150
+ ) {
151
+ issues.push("attention:unsupported_evidence_kind");
152
+ }
153
+ if (reply.meaning.tags) {
154
+ for (const tag of reply.meaning.tags.value) {
155
+ if (!(SEMANTIC_TAGS as readonly string[]).includes(tag)) issues.push("tags:unsupported_tag");
156
+ }
157
+ }
158
+ return issues.length ? { ok: false, issues: [...new Set(issues)] } : { ok: true, value: reply };
159
+ }
160
+
161
+ export function validateSemanticReadModel(
162
+ input: unknown,
163
+ expected?: Pick<SemanticEvidenceV1, "instance_id" | "generation_id" | "evidence_digest">,
164
+ ): SemanticValidationResult<SemanticAgentReadModelV2> {
165
+ if (!Value.Check(SemanticAgentReadModelV2Schema, input)) {
166
+ return {
167
+ ok: false,
168
+ issues: [...Value.Errors(SemanticAgentReadModelV2Schema, input)]
169
+ .slice(0, 12)
170
+ .map((error) => `schema:${error.path || "/"}:${error.message}`),
171
+ };
172
+ }
173
+ const value = input as SemanticAgentReadModelV2;
174
+ const issues: string[] = [];
175
+ if (expected) {
176
+ if (value.instance_id !== expected.instance_id) issues.push("instance_mismatch");
177
+ if (value.generation_id !== expected.generation_id) issues.push("generation_mismatch");
178
+ if (value.source.evidence_digest !== expected.evidence_digest) {
179
+ issues.push("evidence_digest_mismatch");
180
+ }
181
+ }
182
+ return issues.length ? { ok: false, issues } : { ok: true, value };
183
+ }
184
+
185
+ function meaningFields(meaning: SemanticMeaningV2): Array<[string, SemanticField<unknown>]> {
186
+ return (Object.entries(meaning) as Array<[string, SemanticField<unknown>]>).filter(([, value]) =>
187
+ Boolean(value && typeof value === "object" && Array.isArray(value.evidence_event_ids)),
188
+ );
189
+ }
190
+
191
+ function evidenceKindByEventId(evidence: SemanticEvidenceV1): Map<string, SemanticEvidenceKind> {
192
+ const map = new Map<string, SemanticEvidenceKind>();
193
+ if (evidence.task) map.set(evidence.task.event_id, "task");
194
+ if (evidence.lifecycle) map.set(evidence.lifecycle.event_id, "lifecycle");
195
+ if (evidence.intent) map.set(evidence.intent.event_id, "intent");
196
+ if (evidence.operation) map.set(evidence.operation.event_id, "operation");
197
+ for (const wait of evidence.waits) map.set(wait.event_id, "wait");
198
+ for (const recent of evidence.recent) map.set(recent.event_id, recent.kind);
199
+ if (evidence.attention) map.set(evidence.attention.event_id, evidence.attention.kind);
200
+ return map;
201
+ }
202
+
203
+ function citationsSupport(
204
+ field: SemanticField<unknown>,
205
+ kindById: Map<string, SemanticEvidenceKind>,
206
+ allowed: Set<SemanticEvidenceKind>,
207
+ ): boolean {
208
+ return field.evidence_event_ids.some((eventId) => {
209
+ const kind = kindById.get(eventId);
210
+ return kind !== undefined && allowed.has(kind);
211
+ });
212
+ }
@@ -42,6 +42,12 @@ import {
42
42
  buildRuntsCheck,
43
43
  type RuntsResult,
44
44
  } from "./runts.js";
45
+ import {
46
+ buildStandaloneHtmlScript,
47
+ DEFAULT_MAX_RESOURCE_BYTES,
48
+ DEFAULT_MAX_TOTAL_RESOURCE_BYTES,
49
+ type StandaloneHtmlResult,
50
+ } from "./standalone-html.js";
45
51
  import {
46
52
  buildClearTargetSizeAnnotationsScript,
47
53
  buildTargetSizeAnnotateScript,
@@ -1011,6 +1017,21 @@ export class Browser {
1011
1017
  return await page.content();
1012
1018
  }
1013
1019
 
1020
+ /**
1021
+ * Self-contained snapshot of the page: stylesheets inlined, fonts and images
1022
+ * embedded as data: URIs, everything else rewritten to absolute URLs on the
1023
+ * captured origin. Use this for anything written to a file — a raw
1024
+ * `htmlContent()` dump renders unstyled once it leaves the page's own origin.
1025
+ */
1026
+ async standaloneHtml(
1027
+ opts: { maxResourceBytes?: number; maxTotalResourceBytes?: number } = {},
1028
+ ): Promise<StandaloneHtmlResult> {
1029
+ return await this.currentPage.evaluate(buildStandaloneHtmlScript(), {
1030
+ maxResourceBytes: opts.maxResourceBytes ?? DEFAULT_MAX_RESOURCE_BYTES,
1031
+ maxTotalResourceBytes: opts.maxTotalResourceBytes ?? DEFAULT_MAX_TOTAL_RESOURCE_BYTES,
1032
+ });
1033
+ }
1034
+
1014
1035
  async click(selector: string): Promise<void> {
1015
1036
  await this.currentPage.click(selector);
1016
1037
  }
@@ -140,6 +140,10 @@ export {
140
140
  sendBrowserSessionRequest,
141
141
  startBrowserSessionServer,
142
142
  } from "./session-control.js";
143
+ export type {
144
+ StandaloneHtmlRequest,
145
+ StandaloneHtmlResult,
146
+ } from "./standalone-html.js";
143
147
  export type {
144
148
  TargetSizeNode,
145
149
  TargetSizeOutcome,