rea-agents 2.2.0 → 2.4.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 (179) hide show
  1. package/README.md +57 -10
  2. package/dist/application/ApplicationWorkflowEvidenceResolver.js +162 -0
  3. package/dist/application/ArtifactExtraction.js +5 -1
  4. package/dist/application/ArtifactGraphConstruction.js +3 -15
  5. package/dist/application/ArtifactInventory/classify.js +5 -1
  6. package/dist/application/ArtifactInventory/reader.js +2 -0
  7. package/dist/application/BinarySession.js +9 -2
  8. package/dist/application/BinarySessionStatus.js +40 -1
  9. package/dist/application/CapabilityInventory.js +26 -3
  10. package/dist/application/CommandShimReplay.js +6 -3
  11. package/dist/application/CompositeProvider.js +5 -2
  12. package/dist/application/DirectAnalysis.js +1 -20
  13. package/dist/application/DirectAnalysisStatus.js +83 -0
  14. package/dist/application/Doctor.js +34 -25
  15. package/dist/application/DoctorDiagnostics.js +1 -4
  16. package/dist/application/DoctorProjection.js +43 -0
  17. package/dist/application/DoctorScope.js +102 -0
  18. package/dist/application/EvidenceReferenceResolver.js +26 -0
  19. package/dist/application/JavaScriptApplicationEvidence.js +1 -1
  20. package/dist/application/JavaScriptApplicationEvidenceGraph.js +10 -1
  21. package/dist/application/JavaScriptApplicationService.js +3 -2
  22. package/dist/application/JavaScriptApplicationWorkflowEvidence.js +26 -0
  23. package/dist/application/JavaScriptApplicationWorkflowService.js +49 -1
  24. package/dist/application/JavaScriptArtifactReconstruction.js +7 -0
  25. package/dist/application/JavaScriptModuleRelationships.js +25 -1
  26. package/dist/application/JavaScriptReturnShapeProjection.js +111 -0
  27. package/dist/application/JavaScriptSemanticGraphBuilder.js +294 -0
  28. package/dist/application/JavaScriptSemanticGraphConstruction.js +144 -0
  29. package/dist/application/JavaScriptSemanticGraphEvidence.js +86 -0
  30. package/dist/application/JavaScriptSemanticGraphFlowProjection.js +72 -0
  31. package/dist/application/JavaScriptSemanticGraphProjection.js +33 -0
  32. package/dist/application/JavaScriptSemanticTraceService.js +45 -0
  33. package/dist/application/LoopbackReplay.js +277 -133
  34. package/dist/application/LoopbackReplayRecorder.js +101 -0
  35. package/dist/application/PermissionAuthority.js +7 -3
  36. package/dist/application/ProcessCaptureEnvironment.js +26 -0
  37. package/dist/application/ProcessCaptureJournal.js +95 -0
  38. package/dist/application/ProcessCaptureLifecycle.js +25 -13
  39. package/dist/application/ProcessCheckpoints.js +35 -4
  40. package/dist/application/ProcessCli.js +26 -4
  41. package/dist/application/ProcessHarness.js +64 -83
  42. package/dist/application/ProcessNormalization.js +9 -2
  43. package/dist/application/ProcessReactiveCoordinator.js +225 -0
  44. package/dist/application/ProcessReactiveEffects.js +141 -0
  45. package/dist/application/ProcessReactiveHarness.js +85 -0
  46. package/dist/application/ProcessReactiveObservations.js +105 -0
  47. package/dist/application/ProcessSampling.js +2 -0
  48. package/dist/application/SessionProviderRouter.js +7 -6
  49. package/dist/application/Setup.js +11 -10
  50. package/dist/application/SetupPlan.js +1 -1
  51. package/dist/application/SetupRegistrationEnvironment.js +12 -0
  52. package/dist/application/TerminalRenderer.js +3 -1
  53. package/dist/browser/CdpEndpoint.js +37 -4
  54. package/dist/catalogIdentity.js +16 -7
  55. package/dist/cli/coreAnalysisCommands.js +34 -2
  56. package/dist/cli/javascriptApplicationAnalysis.js +19 -0
  57. package/dist/cli/setupCommands.js +81 -32
  58. package/dist/cli/utilityCommands.js +10 -2
  59. package/dist/cliApplicationCommands.js +79 -16
  60. package/dist/cliCommandNames.js +3 -0
  61. package/dist/cliElectronCommands.js +23 -33
  62. package/dist/cliObservationOptions.js +1 -1
  63. package/dist/cliOutput.js +56 -0
  64. package/dist/cliProcessCommands.js +32 -1
  65. package/dist/cliSetup.js +30 -21
  66. package/dist/contracts/applicationToolContracts.js +54 -7
  67. package/dist/contracts/applicationWorkflowInputContracts.js +119 -0
  68. package/dist/contracts/electronToolContracts.js +53 -32
  69. package/dist/contracts/javascriptApplicationWorkflowExamples.js +32 -9
  70. package/dist/contracts/processCaptureExample.js +1 -0
  71. package/dist/contracts/replayMachineExample.js +37 -0
  72. package/dist/contracts/toolContractExamples.js +2 -0
  73. package/dist/contracts/toolContracts.js +5 -1
  74. package/dist/contracts/toolEffects.js +3 -0
  75. package/dist/contracts/toolOutputSchemaGroups.js +2 -0
  76. package/dist/contracts/toolOutputSchemaPrimitives.js +43 -0
  77. package/dist/doctorRuntime.js +41 -0
  78. package/dist/domain/analysisErrorPresentation.js +5 -3
  79. package/dist/domain/analysisErrorProjection.js +7 -1
  80. package/dist/domain/analysisSnapshot.js +2 -0
  81. package/dist/domain/artifactGraph.js +2 -0
  82. package/dist/domain/binaryTarget.js +3 -6
  83. package/dist/domain/changedBehavior.js +5 -4
  84. package/dist/domain/completionLedgerGeneration.js +190 -0
  85. package/dist/domain/errors.js +4 -2
  86. package/dist/domain/evidence.js +2 -0
  87. package/dist/domain/evidenceCompletionLedger.js +113 -0
  88. package/dist/domain/hopperStartupFailure.js +9 -0
  89. package/dist/domain/javascriptApplicationAnalysis.js +44 -2
  90. package/dist/domain/javascriptExportShapeComparison.js +145 -0
  91. package/dist/domain/javascriptExportShapeComparisonIdentity.js +11 -0
  92. package/dist/domain/javascriptExportShapeComparisonSchemas.js +277 -0
  93. package/dist/domain/javascriptExportShapeSelection.js +132 -0
  94. package/dist/domain/javascriptExportShapeVariants.js +272 -0
  95. package/dist/domain/javascriptRuntimeReconciliationParsing.js +2 -2
  96. package/dist/domain/javascriptSemanticAnalysis.js +17 -36
  97. package/dist/domain/javascriptSemanticCallResolution.js +129 -0
  98. package/dist/domain/javascriptSemanticCalls.js +269 -0
  99. package/dist/domain/javascriptSemanticGraph.js +356 -0
  100. package/dist/domain/javascriptSemanticGraphSchemas.js +329 -0
  101. package/dist/domain/javascriptSemanticGraphSerialization.js +23 -0
  102. package/dist/domain/javascriptSemanticIr.js +14 -1
  103. package/dist/domain/javascriptSemanticPrimitives.js +36 -0
  104. package/dist/domain/javascriptSemanticProjection.js +36 -4
  105. package/dist/domain/javascriptSemanticProvenance.js +29 -0
  106. package/dist/domain/javascriptSemanticQuery.js +294 -0
  107. package/dist/domain/javascriptSemanticQueryAssessment.js +74 -0
  108. package/dist/domain/javascriptSemanticQueryIdentity.js +86 -0
  109. package/dist/domain/javascriptSemanticQueryRelations.js +15 -0
  110. package/dist/domain/javascriptSemanticQuerySchemas.js +188 -0
  111. package/dist/domain/javascriptSemanticReturns.js +187 -0
  112. package/dist/domain/javascriptSemanticState.js +32 -0
  113. package/dist/domain/javascriptSemanticTraceSchemas.js +14 -0
  114. package/dist/domain/javascriptSemanticValues.js +54 -70
  115. package/dist/domain/processCapture.js +100 -0
  116. package/dist/domain/processCaptureReactiveValidation.js +175 -0
  117. package/dist/domain/processCaptureValidation.js +103 -12
  118. package/dist/domain/processComparison.js +78 -25
  119. package/dist/domain/processObservation.js +162 -0
  120. package/dist/domain/processReactiveCheckpointDataflow.js +32 -0
  121. package/dist/domain/processReactiveMatching.js +210 -0
  122. package/dist/domain/processReactiveRuntime.js +120 -0
  123. package/dist/domain/processReactiveScenario.js +361 -0
  124. package/dist/domain/processReactiveScenarioPreflight.js +120 -0
  125. package/dist/domain/processReactiveTransition.js +161 -0
  126. package/dist/domain/processScenario.js +33 -0
  127. package/dist/domain/processTraceComparison.js +43 -0
  128. package/dist/domain/processTraceDimensionProjection.js +50 -0
  129. package/dist/domain/processTraceEvaluation.js +312 -0
  130. package/dist/domain/processTraceSpecification.js +373 -0
  131. package/dist/domain/reconstructionVerification.js +5 -4
  132. package/dist/domain/replayMachine.js +18 -2
  133. package/dist/domain/replayMachineRun.js +216 -0
  134. package/dist/domain/replayMachineRuntime.js +88 -9
  135. package/dist/domain/staticRuntimeCorrelation.js +4 -2
  136. package/dist/domain/zipPackageFormat.js +20 -0
  137. package/dist/evaluation/CodexAgentEval.js +24 -0
  138. package/dist/generatedPackageMetadata.js +1 -1
  139. package/dist/ghidra/GhidraClient.js +13 -2
  140. package/dist/ghidra/GhidraDiagnostics.js +1 -0
  141. package/dist/ghidra/GhidraLauncher.js +7 -3
  142. package/dist/ghidra/GhidraProvider.js +8 -1
  143. package/dist/hopper/BridgeLauncher.js +56 -14
  144. package/dist/hopper/HopperClient.js +23 -10
  145. package/dist/hopper/HopperDiagnostics.js +14 -0
  146. package/dist/hopper/HopperProcessDiagnostic.js +14 -0
  147. package/dist/hopper/HopperProvider.js +8 -1
  148. package/dist/hopper/LinuxPrivateDisplayDiagnostic.js +72 -0
  149. package/dist/hopper/LinuxPrivateDisplayProbe.js +217 -0
  150. package/dist/main/transport.js +2 -0
  151. package/dist/mcpDoctor.js +310 -0
  152. package/dist/native/NativeMacOSProvider.js +1 -0
  153. package/dist/process/ProcessOwnership.js +200 -53
  154. package/dist/process/ProviderRunLineage.js +21 -0
  155. package/dist/server/createServer.js +4 -1
  156. package/dist/server/javascriptApplicationResult.js +22 -1
  157. package/dist/server/registerApplicationTools/characterization.js +3 -3
  158. package/dist/server/registerApplicationTools/compareExportShapes.js +37 -0
  159. package/dist/server/registerApplicationTools/compareVersions.js +12 -6
  160. package/dist/server/registerApplicationTools/controlledReplay.js +2 -2
  161. package/dist/server/registerApplicationTools/coverage.js +3 -3
  162. package/dist/server/registerApplicationTools/helpers.js +37 -25
  163. package/dist/server/registerApplicationTools/traceFeature.js +9 -5
  164. package/dist/server/registerApplicationTools/traceSemantics.js +30 -0
  165. package/dist/server/registerApplicationTools.js +4 -0
  166. package/dist/server/registerEvidenceResources.js +5 -1
  167. package/dist/server/registerProcessComparisonTool.js +17 -7
  168. package/dist/server/registerReplayMachineTool.js +20 -0
  169. package/dist/server/registerSessionTools.js +12 -7
  170. package/dist/server/sessionAvailabilityPolicy.js +2 -1
  171. package/dist/serverIdentity.js +19 -0
  172. package/package.json +62 -30
  173. package/scripts/hopper-demo-x11.py +459 -61
  174. package/scripts/prepack.mjs +55 -0
  175. package/scripts/prepare.mjs +28 -0
  176. package/scripts/rea.mjs +26 -5
  177. package/skills/reverse-engineer-anything/SKILL.md +4 -3
  178. package/skills/reverse-engineer-anything/references/javascript-applications.md +10 -0
  179. package/skills/reverse-engineer-anything/references/native-and-artifacts.md +2 -2
@@ -0,0 +1,101 @@
1
+ import { ReplayMachineRuntime, } from "../domain/replayMachineRuntime.js";
2
+ /** Bounded protocol and replay-machine observations for one process run. */
3
+ export class ReplayRecorder {
4
+ scenario;
5
+ recordEvent;
6
+ events = [];
7
+ started = Date.now();
8
+ machine;
9
+ #machineQueue = Promise.resolve();
10
+ #machineAdmissionOpen = true;
11
+ truncated = false;
12
+ constructor(scenario, recordEvent = () => undefined) {
13
+ this.scenario = scenario;
14
+ this.recordEvent = recordEvent;
15
+ this.machine =
16
+ scenario.replay.machine === null
17
+ ? undefined
18
+ : new ReplayMachineRuntime(scenario.replay.machine);
19
+ }
20
+ get transitions() {
21
+ return this.machine?.timeline ?? [];
22
+ }
23
+ atMs() {
24
+ return (Math.floor((Date.now() - this.started) /
25
+ this.scenario.normalization.time_bucket_ms) * this.scenario.normalization.time_bucket_ms);
26
+ }
27
+ rawAtMs() {
28
+ return Date.now() - this.started;
29
+ }
30
+ enqueueMachine(operation) {
31
+ if (!this.#machineAdmissionOpen)
32
+ return Promise.reject(new Error("replay machine is closing"));
33
+ const result = this.#machineQueue.then(operation);
34
+ this.#machineQueue = result.then(() => undefined, () => undefined);
35
+ return result;
36
+ }
37
+ stopMachineAdmission() {
38
+ this.#machineAdmissionOpen = false;
39
+ }
40
+ async drainMachine() {
41
+ await this.#machineQueue;
42
+ }
43
+ dispatchMachine(event) {
44
+ if (this.machine === undefined)
45
+ throw new Error("replay machine is not configured");
46
+ return this.machine.dispatch(event);
47
+ }
48
+ recordTransition(decision) {
49
+ if (decision.transition !== null)
50
+ this.recordEvent("replay_transitions", decision.transition.sequence);
51
+ }
52
+ record(event) {
53
+ if (this.events.length >= this.scenario.limits.protocol_events) {
54
+ this.truncated = true;
55
+ return;
56
+ }
57
+ const sequence = this.events.length;
58
+ this.events.push({ sequence, ...event });
59
+ this.recordEvent("protocol_events", sequence);
60
+ }
61
+ }
62
+ export const requestHeaders = (request) => Object.fromEntries(Object.entries(request.headers).flatMap(([name, value]) => value === undefined
63
+ ? []
64
+ : [[name, Array.isArray(value) ? value.join(", ") : value]]));
65
+ export const readReplayRequestBody = async (request, limit) => {
66
+ const chunks = [];
67
+ let bytes = 0;
68
+ for await (const chunk of request) {
69
+ const value = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
70
+ bytes += value.byteLength;
71
+ if (bytes > limit)
72
+ return undefined;
73
+ chunks.push(value);
74
+ }
75
+ return Buffer.concat(chunks).toString("utf8");
76
+ };
77
+ export const recordMachineEvent = (recorder, event, decision) => {
78
+ recorder.record({
79
+ ...event,
80
+ data: recorder.machine?.redact(event.data) ?? event.data,
81
+ outcome: decision.outcome,
82
+ });
83
+ recorder.recordTransition(decision);
84
+ };
85
+ export const waitForReplayDelay = (durationMs) => new Promise((resolveDelay) => setTimeout(resolveDelay, durationMs));
86
+ export const listenOnLoopback = async (server) => {
87
+ await new Promise((resolveListen, rejectListen) => {
88
+ server.once("error", rejectListen);
89
+ server.listen(0, "127.0.0.1", () => {
90
+ server.off("error", rejectListen);
91
+ resolveListen();
92
+ });
93
+ });
94
+ const address = server.address();
95
+ if (address === null || typeof address === "string")
96
+ throw new Error("loopback replay did not acquire a TCP port");
97
+ return address.port;
98
+ };
99
+ export const closeReplayServer = (server) => new Promise((resolveClose, rejectClose) => {
100
+ server.close((error) => error === undefined ? resolveClose() : rejectClose(error));
101
+ });
@@ -74,13 +74,15 @@ const errorReason = (error) => isNotFound(error) ? "not_found" : "io";
74
74
  export class PermissionAuthority {
75
75
  policy;
76
76
  configuredAuthority;
77
- constructor(policy, configuredAuthority) {
77
+ outsideCeilingRestartRequired;
78
+ constructor(policy, configuredAuthority, outsideCeilingRestartRequired = false) {
78
79
  this.policy = policy;
79
80
  this.configuredAuthority = configuredAuthority;
81
+ this.outsideCeilingRestartRequired = outsideCeilingRestartRequired;
80
82
  }
81
83
  /** Create a connection-owned overlay for once and session grants. */
82
84
  createConnectionAuthority() {
83
- return new PermissionAuthority(createPermissionPolicy([]), this.configuredAuthority ?? this);
85
+ return new PermissionAuthority(createPermissionPolicy([]), this.configuredAuthority ?? this, true);
84
86
  }
85
87
  /** Canonicalize and authorize one exact operation before its side effect. */
86
88
  async authorize(request, access, options = {}) {
@@ -165,7 +167,9 @@ export class PermissionAuthority {
165
167
  const denied = evaluatePermission(effective, request);
166
168
  if (denied.allowed)
167
169
  return ok({ decision: denied, owner: this });
168
- return err(new PermissionRequiredError(request, denied.missing, ceilingFor(effective, request.capability), options.elicitationSupported ?? false, options.restartRequired ?? false));
170
+ const outsideAdministratorCeiling = denied.reason === "outside_administrator_ceiling";
171
+ return err(new PermissionRequiredError(request, denied.missing, ceilingFor(effective, request.capability), !outsideAdministratorCeiling && (options.elicitationSupported ?? false), options.restartRequired ??
172
+ (outsideAdministratorCeiling && this.outsideCeilingRestartRequired)));
169
173
  }
170
174
  /** Atomically replace optional persisted project grants. */
171
175
  async replaceProjectGrants(grants) {
@@ -0,0 +1,26 @@
1
+ /** Build the explicitly admitted environment for a captured process. */
2
+ export const makeProcessCaptureEnvironment = (options) => {
3
+ const { scenario, home, replay, shimReplay, runId } = options;
4
+ const environment = {
5
+ ...scenario.environment,
6
+ HOME: home,
7
+ TERM: "xterm-256color",
8
+ REA_PROCESS_RUN_ID: runId,
9
+ };
10
+ for (const name of scenario.inherit_environment) {
11
+ const value = process.env[name];
12
+ if (value !== undefined)
13
+ environment[name] = value;
14
+ }
15
+ environment.REA_REPLAY_HTTP_URL = replay.httpUrl;
16
+ environment.REA_REPLAY_WEBSOCKET_URL = replay.websocketUrl;
17
+ environment.REA_SHIM_LEDGER_URL = shimReplay.url;
18
+ // PATH is evidence-producing input. Inheriting the host PATH here would let
19
+ // the authority and reconstruction probe different tools without recording
20
+ // that difference. Scenarios must opt in through environment or
21
+ // inherit_environment; deterministic shims always take precedence.
22
+ environment.PATH = [shimReplay.binPath, environment.PATH ?? ""]
23
+ .filter((part) => part.length > 0)
24
+ .join(":");
25
+ return environment;
26
+ };
@@ -0,0 +1,95 @@
1
+ import { normalizeProcessElapsedTime, normalizeProcessText, } from "./ProcessNormalization.js";
2
+ /** Create one monotonic event journal for a process capture. */
3
+ export const createProcessCaptureJournal = () => {
4
+ const entries = [];
5
+ const listeners = new Set();
6
+ const notifications = [];
7
+ let delivering = false;
8
+ const deliverNotifications = () => {
9
+ if (delivering)
10
+ return;
11
+ delivering = true;
12
+ const failures = [];
13
+ try {
14
+ for (;;) {
15
+ const notification = notifications.shift();
16
+ if (notification === undefined)
17
+ break;
18
+ for (const listener of Array.from(listeners))
19
+ try {
20
+ listener(notification);
21
+ }
22
+ catch (cause) {
23
+ failures.push(cause);
24
+ }
25
+ }
26
+ }
27
+ finally {
28
+ delivering = false;
29
+ }
30
+ if (failures.length > 0)
31
+ throw new AggregateError(failures, "process capture journal subscriber failed");
32
+ };
33
+ const record = (collection, index) => {
34
+ const entry = { capture_order: entries.length, collection, index };
35
+ entries.push(entry);
36
+ notifications.push(entry);
37
+ deliverNotifications();
38
+ return entry;
39
+ };
40
+ return {
41
+ entries,
42
+ record,
43
+ recordEvent: (collection, index) => void record(collection, index),
44
+ subscribe: (listener) => {
45
+ listeners.add(listener);
46
+ return () => listeners.delete(listener);
47
+ },
48
+ };
49
+ };
50
+ /** Schedule scenario interactions and record each observed dispatch atomically. */
51
+ export const scheduleScenarioInteractions = (options) => {
52
+ const { scenario, getTerminal, timers, interactions, renderer, started } = options;
53
+ for (const [eventIndex, event] of scenario.events.entries()) {
54
+ const timer = setTimeout(() => {
55
+ options.dispatchedEventIndexes.add(eventIndex);
56
+ const terminal = getTerminal();
57
+ const dispatchedAt = Math.max(0, Date.now() - started);
58
+ let outcome = "dispatched";
59
+ if (terminal === undefined)
60
+ outcome = "target_exited";
61
+ else {
62
+ try {
63
+ if (event.type === "input")
64
+ terminal.write(event.data);
65
+ else if (event.type === "resize") {
66
+ terminal.resize(event.columns, event.rows);
67
+ renderer.resize(event.columns, event.rows, normalizeProcessElapsedTime(dispatchedAt, scenario.normalization.time_bucket_ms));
68
+ }
69
+ else
70
+ terminal.kill(event.signal);
71
+ }
72
+ catch {
73
+ outcome = "failed";
74
+ }
75
+ }
76
+ const sequence = interactions.length;
77
+ interactions.push({
78
+ sequence,
79
+ scheduled_at_ms: event.at_ms,
80
+ dispatched_at_ms: dispatchedAt,
81
+ type: event.type,
82
+ data: event.type === "input"
83
+ ? event.sensitive
84
+ ? `<redacted-input:${String(Buffer.byteLength(event.data))}-bytes>`
85
+ : normalizeProcessText(event.data, scenario, "<no-temporary-root>", -1)
86
+ : event.type === "resize"
87
+ ? `${String(event.columns)}x${String(event.rows)}`
88
+ : event.signal,
89
+ outcome,
90
+ });
91
+ options.recordEvent("interaction_events", sequence);
92
+ }, event.at_ms);
93
+ timers.add(timer);
94
+ }
95
+ };
@@ -10,7 +10,7 @@ import { cleanupOwnedProcessGroup, observeOwnedProcessGroup, } from "../process/
10
10
  import { classifyFilesystemEffects, ProcessCheckpoints, } from "./ProcessCheckpoints.js";
11
11
  import { ProcessCaptureError } from "./ProcessCaptureError.js";
12
12
  import { assertNotCancelled, assertRealPathAuthority, } from "./ProcessCaptureAuthority.js";
13
- import { normalizeProcessSamples, normalizeProcessText, redactProtocolEvents, } from "./ProcessNormalization.js";
13
+ import { normalizeProcessElapsedTime, normalizeProcessSamples, normalizeProcessText, redactProtocolEvents, } from "./ProcessNormalization.js";
14
14
  import { PROCESS_PROVIDER } from "./ProcessEvidence.js";
15
15
  import { TerminalRenderer } from "./TerminalRenderer.js";
16
16
  export const buildCaptureResult = (options) => ({
@@ -18,9 +18,7 @@ export const buildCaptureResult = (options) => ({
18
18
  manifest: options.manifest,
19
19
  normalization: options.scenario.normalization,
20
20
  frames: options.frames,
21
- rendered_frames: [...options.renderedFrames]
22
- .sort((left, right) => left.at_ms - right.at_ms || left.sequence - right.sequence)
23
- .map((frame, sequence) => ({ ...frame, sequence })),
21
+ rendered_frames: options.renderedFrames,
24
22
  interaction_events: options.interactions,
25
23
  exit: {
26
24
  code: options.exit.reason === "exited" && options.exit.exitCode >= 0
@@ -34,6 +32,9 @@ export const buildCaptureResult = (options) => ({
34
32
  filesystem_checkpoints: options.checkpoints,
35
33
  shim_events: options.shimEvents,
36
34
  protocol_events: redactProtocolEvents(options.replay.events, options.scenario),
35
+ replay_transitions: options.replay.transitions,
36
+ reactive_run: options.reactiveRun,
37
+ event_journal: options.eventJournal,
37
38
  files_before: options.before.files,
38
39
  files_after: options.after.files,
39
40
  filesystem_effects: classifyFilesystemEffects(options.before.files, options.after.files),
@@ -91,9 +92,11 @@ export const createRunManifest = async (scenario, startedAt, completedAt) => {
91
92
  replay_plan_sha256: digestProcessCommitment(scenario.replay),
92
93
  };
93
94
  };
94
- export const observeSettlement = async (runId, processGroupIds, settleMs) => {
95
- if (process.platform === "win32")
95
+ export const observeSettlement = async (runId, processGroupIds, settleMs, recordEvent = () => undefined) => {
96
+ if (process.platform === "win32") {
97
+ recordEvent("lifecycle", 1);
96
98
  return { state: "unverifiable", elapsed_ms: 0 };
99
+ }
97
100
  const started = Date.now();
98
101
  let consecutiveEmpty = 0;
99
102
  let deadlineReached = false;
@@ -103,12 +106,16 @@ export const observeSettlement = async (runId, processGroupIds, settleMs) => {
103
106
  leaderPid: processGroupId,
104
107
  processGroupId,
105
108
  })));
106
- if (observations.some(({ state }) => state === "unverifiable"))
109
+ if (observations.some(({ state }) => state === "unverifiable")) {
110
+ recordEvent("lifecycle", 1);
107
111
  return { state: "unverifiable", elapsed_ms: Date.now() - started };
112
+ }
108
113
  if (observations.every(({ state }) => state === "empty")) {
109
114
  consecutiveEmpty += 1;
110
- if (consecutiveEmpty >= 2)
115
+ if (consecutiveEmpty >= 2) {
116
+ recordEvent("lifecycle", 1);
111
117
  return { state: "quiesced", elapsed_ms: Date.now() - started };
118
+ }
112
119
  }
113
120
  else
114
121
  consecutiveEmpty = 0;
@@ -117,19 +124,22 @@ export const observeSettlement = async (runId, processGroupIds, settleMs) => {
117
124
  break;
118
125
  await new Promise((resolve) => setTimeout(resolve, 50));
119
126
  }
127
+ recordEvent("lifecycle", 1);
120
128
  return { state: "alive_at_deadline", elapsed_ms: Date.now() - started };
121
129
  };
122
- export const awaitTerminalExit = async ({ terminal, scenario, started, lastOutput, signal, timers, interactions, dispatchedEventIndexes, }) => new Promise((resolveExit) => {
130
+ export const awaitTerminalExit = async ({ terminal, scenario, started, lastOutput, signal, timers, interactions, dispatchedEventIndexes, recordEvent, }) => new Promise((resolveExit) => {
123
131
  // The kill caused by a deadline is observed later as an ordinary PTY exit.
124
132
  // Keep the initiating lifecycle reason so comparisons distinguish a target
125
133
  // exit from harness-owned timeout, idle-timeout, and cancellation cleanup.
126
134
  let reason = "exited";
127
135
  terminal.onExit((exit) => {
136
+ recordEvent("lifecycle", 0);
128
137
  for (const [eventIndex, event] of scenario.events.entries()) {
129
138
  if (dispatchedEventIndexes.has(eventIndex))
130
139
  continue;
140
+ const sequence = interactions.length;
131
141
  interactions.push({
132
- sequence: interactions.length,
142
+ sequence,
133
143
  scheduled_at_ms: event.at_ms,
134
144
  dispatched_at_ms: Math.max(0, Date.now() - started),
135
145
  type: event.type,
@@ -140,6 +150,7 @@ export const awaitTerminalExit = async ({ terminal, scenario, started, lastOutpu
140
150
  : event.signal,
141
151
  outcome: "target_exited",
142
152
  });
153
+ recordEvent("interaction_events", sequence);
143
154
  }
144
155
  for (const timer of timers) {
145
156
  clearTimeout(timer);
@@ -223,14 +234,15 @@ export const captureTerminalFrames = (options) => {
223
234
  return;
224
235
  }
225
236
  outputBytes += bytes;
226
- const atMs = Math.floor((Date.now() - options.started) /
227
- options.scenario.normalization.time_bucket_ms) * options.scenario.normalization.time_bucket_ms;
237
+ const atMs = normalizeProcessElapsedTime(Date.now() - options.started, options.scenario.normalization.time_bucket_ms);
228
238
  const normalized = normalizeProcessText(data, options.scenario, options.temporaryRoot, options.terminal.pid);
239
+ const sequence = options.frames.length;
229
240
  options.frames.push({
230
- sequence: options.frames.length,
241
+ sequence,
231
242
  at_ms: atMs,
232
243
  data: normalized,
233
244
  });
245
+ options.recordEvent("frames", sequence);
234
246
  options.renderer.write(data, atMs);
235
247
  options.checkpoints.observeTerminal(normalized);
236
248
  });
@@ -22,17 +22,19 @@ export const classifyFilesystemEffects = (before, after) => {
22
22
  export class ProcessCheckpoints {
23
23
  scenario;
24
24
  started;
25
- signal;
26
25
  #captures = [];
27
26
  #captured = new Set();
28
27
  #terminalCounts = new Map();
29
28
  #terminalTails = new Map();
30
29
  #timers = new Set();
30
+ #signal;
31
+ #recordEvent;
31
32
  #pending = Promise.resolve();
32
- constructor(scenario, started, before, signal) {
33
+ constructor(scenario, started, before, options = { signal: undefined }) {
33
34
  this.scenario = scenario;
34
35
  this.started = started;
35
- this.signal = signal;
36
+ this.#signal = options.signal;
37
+ this.#recordEvent = options.recordEvent ?? (() => undefined);
36
38
  this.#captures.push({
37
39
  name: "before",
38
40
  at_ms: 0,
@@ -40,6 +42,7 @@ export class ProcessCheckpoints {
40
42
  effects: [],
41
43
  truncated: before.truncated,
42
44
  });
45
+ this.#recordEvent("filesystem_checkpoints", 0);
43
46
  this.#captured.add("before");
44
47
  for (const checkpoint of scenario.checkpoints) {
45
48
  if (checkpoint.trigger.type !== "time")
@@ -74,6 +77,14 @@ export class ProcessCheckpoints {
74
77
  }
75
78
  /** Queue one idempotent named snapshot. */
76
79
  capture(name) {
80
+ this.#scheduleCapture(name, this.#signal);
81
+ }
82
+ /** Read one committed checkpoint for synchronous journal projection. */
83
+ at(index) {
84
+ const checkpoint = this.#captures[index];
85
+ return checkpoint === undefined ? undefined : structuredClone(checkpoint);
86
+ }
87
+ #scheduleCapture(name, signal) {
77
88
  if (this.#captured.has(name))
78
89
  return;
79
90
  this.#captured.add(name);
@@ -81,8 +92,9 @@ export class ProcessCheckpoints {
81
92
  // together. Each effect set is intentionally relative to the immediately
82
93
  // preceding checkpoint, which preserves transient create/delete behavior.
83
94
  this.#pending = this.#pending.then(async () => {
84
- const snapshot = await snapshotRoots(this.scenario, this.signal);
95
+ const snapshot = await snapshotRoots(this.scenario, signal);
85
96
  const previous = this.#captures.at(-1)?.files ?? [];
97
+ const index = this.#captures.length;
86
98
  this.#captures.push({
87
99
  name,
88
100
  at_ms: Math.max(0, Date.now() - this.started),
@@ -90,14 +102,32 @@ export class ProcessCheckpoints {
90
102
  effects: classifyFilesystemEffects(previous, snapshot.files),
91
103
  truncated: snapshot.truncated,
92
104
  });
105
+ this.#recordEvent("filesystem_checkpoints", index);
93
106
  });
94
107
  }
108
+ /** Queue one named snapshot and return its exact captured record. */
109
+ async captureAndRead(name, signal) {
110
+ const captureSignal = signal === undefined
111
+ ? this.#signal
112
+ : this.#signal === undefined
113
+ ? signal
114
+ : AbortSignal.any([this.#signal, signal]);
115
+ this.#scheduleCapture(name, captureSignal);
116
+ const scheduled = this.#pending;
117
+ await scheduled;
118
+ const index = this.#captures.findIndex(({ name: capturedName }) => capturedName === name);
119
+ const checkpoint = this.#captures[index];
120
+ if (index < 0 || checkpoint === undefined)
121
+ throw new Error(`filesystem checkpoint was not captured: ${name}`);
122
+ return { index, checkpoint: structuredClone(checkpoint) };
123
+ }
95
124
  /** Finish pending snapshots and return them in observation order. */
96
125
  async finish(after) {
97
126
  for (const timer of this.#timers)
98
127
  clearTimeout(timer);
99
128
  await this.#pending;
100
129
  const previous = this.#captures.at(-1)?.files ?? [];
130
+ const index = this.#captures.length;
101
131
  this.#captures.push({
102
132
  name: "after_settlement",
103
133
  at_ms: Math.max(0, Date.now() - this.started),
@@ -105,6 +135,7 @@ export class ProcessCheckpoints {
105
135
  effects: classifyFilesystemEffects(previous, after.files),
106
136
  truncated: after.truncated,
107
137
  });
138
+ this.#recordEvent("filesystem_checkpoints", index);
108
139
  return structuredClone(this.#captures);
109
140
  }
110
141
  /** Cancel scheduled captures and await any snapshot already in progress. */
@@ -1,8 +1,10 @@
1
1
  import { readFile } from "node:fs/promises";
2
2
  import { parseConfig } from "../config.js";
3
- import { AnalysisError, AnalysisProtocolError, PermissionRequiredError, projectAnalysisError, } from "../domain/errors.js";
3
+ import { AnalysisError, AnalysisInputError, AnalysisProtocolError, PermissionRequiredError, projectAnalysisError, } from "../domain/errors.js";
4
4
  import { createEvidence, parseEvidence } from "../domain/evidence.js";
5
5
  import { jsonValueSchema } from "../domain/jsonValue.js";
6
+ import { projectInputIssues } from "../domain/inputIssueProjection.js";
7
+ import { processTraceSpecificationSchema } from "../domain/processTraceComparison.js";
6
8
  import { compareProcessCaptures, LEGACY_PROCESS_CAPTURE_MESSAGE, parseProcessScenario, parseProcessCapture, } from "../domain/processCapture.js";
7
9
  import { captureProcessScenario } from "./ProcessHarness.js";
8
10
  import { processCapturePermissionRequest } from "./ProcessCapturePermission.js";
@@ -43,24 +45,37 @@ export const captureProcessScenarioFile = async (path) => {
43
45
  }
44
46
  };
45
47
  /** Compare two capture Evidence files and emit derived comparison Evidence. */
46
- export const compareProcessEvidenceFiles = async (leftPath, rightPath) => {
48
+ export const compareProcessEvidenceFiles = async (leftPath, rightPath, traceSpecPath) => {
47
49
  try {
48
50
  const left = parseCaptureEvidence(await readJson(leftPath));
49
51
  const right = parseCaptureEvidence(await readJson(rightPath));
50
- const comparison = compareProcessCaptures(left.capture, right.capture);
52
+ const traceSpecification = traceSpecPath === undefined
53
+ ? undefined
54
+ : parseTraceSpecification(await readJson(traceSpecPath));
55
+ const comparison = traceSpecification === undefined
56
+ ? compareProcessCaptures(left.capture, right.capture)
57
+ : compareProcessCaptures(left.capture, right.capture, {
58
+ traceSpecification,
59
+ });
51
60
  return createEvidence(undefined, PROCESS_PROVIDER, {
52
- predicateType: "rea.process-comparison/v3",
61
+ predicateType: traceSpecification === undefined
62
+ ? "rea.process-comparison/v3"
63
+ : "rea.process-comparison/v4",
53
64
  operation: "compare_process_captures",
54
65
  parameters: {
55
66
  left_evidence_id: left.id,
56
67
  right_evidence_id: right.id,
57
68
  left_normalization: left.capture.normalization,
58
69
  right_normalization: right.capture.normalization,
70
+ ...(traceSpecification === undefined
71
+ ? {}
72
+ : { trace_spec: jsonValueSchema.parse(traceSpecification) }),
59
73
  },
60
74
  result: jsonValueSchema.parse(comparison),
61
75
  confidence: "derived",
62
76
  authority: "analyst-inference",
63
77
  limitations: comparison.limitations,
78
+ locations: [...left.locations, ...right.locations],
64
79
  evidenceLinks: [left.id, right.id],
65
80
  });
66
81
  }
@@ -88,12 +103,19 @@ const parseCaptureEvidence = (input) => {
88
103
  return {
89
104
  id: evidence.evidence_id,
90
105
  capture: parseProcessCapture(evidence.normalized_result),
106
+ locations: evidence.locations,
91
107
  };
92
108
  }
93
109
  catch {
94
110
  throw invalidCaptureEvidence();
95
111
  }
96
112
  };
113
+ const parseTraceSpecification = (input) => {
114
+ const parsed = processTraceSpecificationSchema.safeParse(input);
115
+ if (parsed.success)
116
+ return parsed.data;
117
+ throw new AnalysisInputError("compare-process-captures", { cause: parsed.error }, projectInputIssues(parsed.error.issues, input));
118
+ };
97
119
  const invalidCaptureEvidence = () => new ProcessCliFailure("invalid_input", "Capture evidence is malformed. Create new capture evidence, then try again.");
98
120
  const readJson = async (path) => {
99
121
  let bytes;