rea-agents 1.2.0 → 1.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.
Files changed (79) hide show
  1. package/README.md +74 -27
  2. package/bridge/hopper_bridge.py +161 -12
  3. package/dist/application/AnalysisSnapshotCache.js +131 -0
  4. package/dist/application/AnalysisSnapshotFiles.js +31 -0
  5. package/dist/application/ArtifactInventory.js +4 -0
  6. package/dist/application/AuthorizedArtifactInventory.js +16 -0
  7. package/dist/application/BinarySession.js +66 -36
  8. package/dist/application/BinarySessionPort.js +1 -0
  9. package/dist/application/BoundedJsonFiles.js +109 -0
  10. package/dist/application/CrossVersionInventory.js +98 -0
  11. package/dist/application/CrossVersionInvestigation.js +295 -0
  12. package/dist/application/DirectAnalysis.js +115 -22
  13. package/dist/application/Doctor.js +30 -1
  14. package/dist/application/EvidenceBundleFiles.js +15 -108
  15. package/dist/application/EvidenceLedger.js +3 -2
  16. package/dist/application/InvestigationProviders.js +48 -0
  17. package/dist/application/InvestigationWorkspaceStore.js +214 -0
  18. package/dist/application/LinuxHopper.js +72 -48
  19. package/dist/application/ProcessCaptureAuthority.js +2 -2
  20. package/dist/application/ProcessCaptureError.js +12 -0
  21. package/dist/application/ProcessCaptureLifecycle.js +19 -1
  22. package/dist/application/ProcessCli.js +98 -31
  23. package/dist/application/ProcessHarness.js +2 -2
  24. package/dist/application/ReferenceSourceImportEntries.js +18 -3
  25. package/dist/application/ReferenceSourceImportTypes.js +32 -0
  26. package/dist/application/Setup.js +33 -60
  27. package/dist/application/SetupClients.js +19 -0
  28. package/dist/application/SetupInstallFailure.js +53 -0
  29. package/dist/application/SetupPlan.js +31 -0
  30. package/dist/application/Uninstall.js +33 -23
  31. package/dist/application/UnknownEvidence.js +33 -0
  32. package/dist/application/Upgrade.js +1 -1
  33. package/dist/application/runtime.js +1 -1
  34. package/dist/artifacts/ArtifactProvider.js +2 -5
  35. package/dist/cli.js +31 -11
  36. package/dist/cliEvidenceCommands.js +11 -12
  37. package/dist/cliInvestigationCommands.js +86 -0
  38. package/dist/cliOutput.js +41 -0
  39. package/dist/cliProcessCommands.js +14 -4
  40. package/dist/config.js +39 -24
  41. package/dist/contracts/promptContracts.js +256 -0
  42. package/dist/contracts/sessionLifecycleInputs.js +11 -0
  43. package/dist/contracts/toolContractTypes.js +1 -0
  44. package/dist/contracts/toolContracts.js +10 -6
  45. package/dist/contracts/toolOutputSchemas.js +33 -4
  46. package/dist/domain/analysisSnapshot.js +149 -0
  47. package/dist/domain/changedBehavior.js +30 -7
  48. package/dist/domain/errors.js +180 -41
  49. package/dist/domain/evidence.js +4 -2
  50. package/dist/domain/evidenceBundle.js +28 -0
  51. package/dist/domain/hopperStartupFailure.js +55 -0
  52. package/dist/domain/investigationWorkspace.js +214 -0
  53. package/dist/domain/jsonValue.js +2 -0
  54. package/dist/domain/nativeInspection.js +3 -2
  55. package/dist/domain/processCapture.js +5 -4
  56. package/dist/domain/processComparison.js +5 -4
  57. package/dist/hopper/BridgeLauncher.js +47 -20
  58. package/dist/hopper/HopperClient.js +17 -2
  59. package/dist/hopper/HopperProvider.js +1 -0
  60. package/dist/main.js +47 -24
  61. package/dist/server/createServer.js +2 -0
  62. package/dist/server/promptCompletion.js +144 -0
  63. package/dist/server/registerEnhancedTools.js +2 -6
  64. package/dist/server/registerEvidenceTools.js +2 -8
  65. package/dist/server/registerFunctionComparisonTool.js +1 -1
  66. package/dist/server/registerInvestigationTools.js +31 -11
  67. package/dist/server/registerOfficialTools.js +6 -13
  68. package/dist/server/registerProcessComparisonTool.js +1 -7
  69. package/dist/server/registerPrompts.js +67 -0
  70. package/dist/server/registerSessionStatusTool.js +11 -0
  71. package/dist/server/registerSessionTools.js +52 -18
  72. package/dist/server/sessionEvidence.js +2 -8
  73. package/dist/server/sessionToolPolicies.js +1 -42
  74. package/dist/server/toolResult.js +7 -3
  75. package/install.sh +11 -11
  76. package/package.json +5 -3
  77. package/scripts/hopper-demo-x11.py +349 -0
  78. package/scripts/rea.mjs +6 -3
  79. package/skills/rea-analysis/SKILL.md +9 -2
@@ -6,6 +6,13 @@ import writeFileAtomic from "write-file-atomic";
6
6
  import { z } from "zod";
7
7
  import { PRODUCT_IDENTITY } from "../identity.js";
8
8
  import { supportedClients } from "./Setup.js";
9
+ const systemFileSystem = {
10
+ readText: (path) => readFile(path, "utf8"),
11
+ copy: (source, destination) => copyFile(source, destination),
12
+ writeText: (path, contents) => writeFileAtomic(path, contents, { encoding: "utf8" }),
13
+ stat: (path) => lstat(path),
14
+ remove: (path) => rm(path, { recursive: true }),
15
+ };
9
16
  /** Remove only REA-owned registrations and managed files, optionally including local state. */
10
17
  export const runUninstall = async (purgeData, host = systemUninstallHost()) => {
11
18
  const items = [];
@@ -27,26 +34,26 @@ export const runUninstall = async (purgeData, host = systemUninstallHost()) => {
27
34
  };
28
35
  };
29
36
  /** Create uninstall effects contained to detected client configs and REA-owned paths. */
30
- export const systemUninstallHost = (home = homedir()) => ({
37
+ export const systemUninstallHost = (home = homedir(), fileSystem = systemFileSystem) => ({
31
38
  clients: () => Promise.resolve(supportedClients(home)),
32
- removeClient,
33
- removeSkill: () => removeManagedPath(join(home, ".agents/skills", PRODUCT_IDENTITY.skillName), "skill"),
39
+ removeClient: (client) => removeClient(client, fileSystem),
40
+ removeSkill: () => removeManagedPath(join(home, ".agents/skills", PRODUCT_IDENTITY.skillName), "skill", fileSystem),
34
41
  purgeData: async () => [
35
- await removeManagedPath(join(home, ".rea/cache"), "cache"),
36
- await removeManagedPath(join(home, ".rea/state"), "state"),
42
+ await removeManagedPath(join(home, ".rea/cache"), "cache", fileSystem),
43
+ await removeManagedPath(join(home, ".rea/state"), "state", fileSystem),
37
44
  ],
38
45
  });
39
- const removeClient = async (client) => {
46
+ const removeClient = async (client, fileSystem) => {
40
47
  if (client.format === "unsupported")
41
48
  return item(client.name, "skipped", "This client has no documented local MCP configuration boundary.");
42
49
  let original;
43
50
  try {
44
- original = await readFile(client.configPath, "utf8");
51
+ original = await fileSystem.readText(client.configPath);
45
52
  }
46
53
  catch (cause) {
47
54
  return isMissing(cause)
48
55
  ? item(client.name, "skipped", "Configuration does not exist.")
49
- : item(client.name, "failed", "Configuration could not be read.");
56
+ : item(client.name, "failed", "Configuration could not be read. Check file permissions, then rerun uninstall.");
50
57
  }
51
58
  let document;
52
59
  let key;
@@ -57,7 +64,7 @@ const removeClient = async (client) => {
57
64
  servers = optionalObject(document[key]);
58
65
  }
59
66
  catch {
60
- return item(client.name, "failed", "Malformed configuration was preserved without mutation.");
67
+ return item(client.name, "failed", `Configuration is not valid ${client.format === "toml" ? "TOML" : "JSON"} and was not changed. Repair it, then rerun uninstall.`);
61
68
  }
62
69
  const registration = servers[PRODUCT_IDENTITY.mcpServerKey];
63
70
  if (registration === undefined)
@@ -69,28 +76,31 @@ const removeClient = async (client) => {
69
76
  document[key] = remaining;
70
77
  const backupPath = `${client.configPath}.rea.backup`;
71
78
  try {
72
- await copyFile(client.configPath, backupPath);
79
+ await fileSystem.copy(client.configPath, backupPath);
73
80
  }
74
81
  catch {
75
- return item(client.name, "failed", "Configuration backup failed; no mutation was attempted.");
82
+ return item(client.name, "failed", "Configuration could not be backed up, so no change was made. Check file permissions, then rerun uninstall.");
76
83
  }
77
84
  try {
78
85
  const encoded = client.format === "toml"
79
86
  ? stringifyToml(document)
80
87
  : `${JSON.stringify(document, null, 2)}\n`;
81
- await writeFileAtomic(client.configPath, encoded, { encoding: "utf8" });
88
+ await fileSystem.writeText(client.configPath, encoded);
82
89
  const readback = objectSchema.parse(client.format === "toml"
83
- ? parseToml(await readFile(client.configPath, "utf8"))
84
- : JSON.parse(await readFile(client.configPath, "utf8")));
90
+ ? parseToml(await fileSystem.readText(client.configPath))
91
+ : JSON.parse(await fileSystem.readText(client.configPath)));
85
92
  if (PRODUCT_IDENTITY.mcpServerKey in optionalObject(readback[key]))
86
93
  throw new Error("registration readback mismatch");
87
94
  return item(client.name, "removed", `Removed registration from ${client.configPath}.`);
88
95
  }
89
96
  catch {
90
- await writeFileAtomic(client.configPath, original, {
91
- encoding: "utf8",
92
- }).catch(() => undefined);
93
- return item(client.name, "failed", `Configuration update failed; the original was restored and ${backupPath} was retained.`);
97
+ try {
98
+ await fileSystem.writeText(client.configPath, original);
99
+ }
100
+ catch {
101
+ return item(client.name, "failed", "Configuration could not be updated or restored. Restore its `.rea.backup` manually, then rerun uninstall.");
102
+ }
103
+ return item(client.name, "failed", "Configuration could not be updated. The original was restored and its `.rea.backup` was retained. Repair the configuration or restore the backup, then rerun uninstall.");
94
104
  }
95
105
  };
96
106
  /** Ownership rule for persistent MCP registrations created by REA setup. */
@@ -107,18 +117,18 @@ const isOwnedRegistration = (value) => {
107
117
  JSON.stringify(args) ===
108
118
  JSON.stringify(["-y", PRODUCT_IDENTITY.packageName, "mcp"])));
109
119
  };
110
- const removeManagedPath = async (path, name) => {
120
+ const removeManagedPath = async (path, name, fileSystem) => {
111
121
  try {
112
- const stats = await lstat(path);
122
+ const stats = await fileSystem.stat(path);
113
123
  if (stats.isSymbolicLink())
114
- return item(name, "retained", `Refused to follow symlink ${path}.`);
115
- await rm(path, { recursive: true });
124
+ return item(name, "retained", "REA did not remove this item because its managed path is a symbolic link. Verify the link target before removing it manually.");
125
+ await fileSystem.remove(path);
116
126
  return item(name, "removed", `Removed ${path}.`);
117
127
  }
118
128
  catch (cause) {
119
129
  return isMissing(cause)
120
130
  ? item(name, "skipped", `${path} does not exist.`)
121
- : item(name, "failed", `${path} could not be removed.`);
131
+ : item(name, "failed", "This item could not be removed. Check file permissions, then rerun uninstall.");
122
132
  }
123
133
  };
124
134
  const item = (name, status, detail) => ({ name, status, detail });
@@ -0,0 +1,33 @@
1
+ import { createEvidence } from "../domain/evidence.js";
2
+ /** Provider identity for analyst-authored residual-unknown mutations. */
3
+ export const UNKNOWN_REGISTRY_PROVIDER = {
4
+ id: "rea-unknown-registry",
5
+ name: "REA residual unknown registry",
6
+ version: "1",
7
+ };
8
+ /** Create immutable evidence for one approved residual-unknown mutation. */
9
+ export const unknownMutationEvidence = (target, input) => createEvidence(target, UNKNOWN_REGISTRY_PROVIDER, {
10
+ predicateType: "rea.residual-unknown-mutation/v1",
11
+ operation: "record_unknown",
12
+ parameters: { domain: input.domain, severity: input.severity },
13
+ result: {
14
+ action: "record",
15
+ question: input.question,
16
+ required_authority: input.required_authority,
17
+ required_confidence: input.required_confidence,
18
+ },
19
+ confidence: "derived",
20
+ authority: "analyst-inference",
21
+ evidenceLinks: unknownEvidenceLinks(input),
22
+ limitations: [
23
+ "Registry mutation evidence records analyst intent, not proof of the answer.",
24
+ ],
25
+ });
26
+ /** Deduplicate evidence links carried by an unknown mutation. */
27
+ export const unknownEvidenceLinks = (input) => [
28
+ ...input.supporting_evidence_ids,
29
+ ...input.contradicting_evidence_ids,
30
+ ...("resolution" in input && input.resolution !== null
31
+ ? input.resolution.evidence_ids
32
+ : []),
33
+ ].filter((id, index, values) => values.indexOf(id) === index);
@@ -33,7 +33,7 @@ export const runUpgrade = async (currentVersion, host = systemUpgradeHost(), out
33
33
  currentVersion,
34
34
  latestVersion: latestVersion ?? null,
35
35
  reason: "install",
36
- remediation: `${UPGRADE_COMMAND} failed.`,
36
+ remediation: `REA could not update through npm. Check npm registry access and global install permissions, then run: ${UPGRADE_COMMAND}`,
37
37
  };
38
38
  return {
39
39
  status: "upgraded",
@@ -14,5 +14,5 @@ export const createBinarySession = (config, logger = silentLogger) => {
14
14
  new ArtifactProvider(config.artifactNativeMountEnabled),
15
15
  new NativeMacOSProvider(),
16
16
  new HopperProvider(config, logger),
17
- ]));
17
+ ]), { snapshotsEnabled: config.hopperLoaderArgs.length === 0 });
18
18
  };
@@ -5,11 +5,8 @@ import { ARTIFACT_TOOL_CONTRACTS, artifactInventoryInputSchema, artifactExtracti
5
5
  import { AnalysisCapabilityUnavailableError, ArtifactOperationError, } from "../domain/errors.js";
6
6
  import { err, ok } from "../domain/result.js";
7
7
  import { ArtifactReaderFailure, } from "./ArtifactReader.js";
8
- const IDENTITY = Object.freeze({
9
- id: "rea-artifact-graph",
10
- name: "REA safe artifact graph provider",
11
- version: "1",
12
- });
8
+ import { ARTIFACT_GRAPH_PROVIDER } from "../application/InvestigationProviders.js";
9
+ const IDENTITY = Object.freeze(ARTIFACT_GRAPH_PROVIDER);
13
10
  /** Read-only inventory and exclusively owned extraction provider. */
14
11
  export class ArtifactProvider {
15
12
  nativeMountEnabled;
package/dist/cli.js CHANGED
@@ -13,6 +13,9 @@ import { parseConfig } from "./config.js";
13
13
  import { importReferenceSource } from "./application/ReferenceSourceImport.js";
14
14
  import { registerEvidenceCommands } from "./cliEvidenceCommands.js";
15
15
  import { registerProcessCommands } from "./cliProcessCommands.js";
16
+ import { registerInvestigationCommands } from "./cliInvestigationCommands.js";
17
+ import { projectAnalysisError } from "./domain/errors.js";
18
+ import { projectReferenceSourceImportError } from "./application/ReferenceSourceImportTypes.js";
16
19
  /**
17
20
  * Build the one-shot Incur CLI without starting Hopper at import time.
18
21
  * Analysis commands acquire and close their own sessions; bare `mcp` and
@@ -24,7 +27,7 @@ export const createCli = () => {
24
27
  : parseLogLevel(process.env.REA_LOG_LEVEL));
25
28
  const cli = Cli.create(PRODUCT_IDENTITY.cliBinary, {
26
29
  version: process.env.REA_PACKAGE_VERSION ?? "0.0.0-development",
27
- description: "Reverse engineer anything from your terminal or coding agent.",
30
+ description: "Reverse engineer anything from your terminal or agent.",
28
31
  mcp: {
29
32
  command: PRODUCT_IDENTITY.mcpCommand,
30
33
  instructions: "Ask what software, artifact, protocol, or behavior the user wants to understand, then choose the available investigation capabilities that can produce evidence.",
@@ -48,6 +51,7 @@ export const createCli = () => {
48
51
  registerCapabilityCommands(cli, logger);
49
52
  registerNativeCommands(cli, logger);
50
53
  registerArtifactCommands(cli, logger);
54
+ registerInvestigationCommands(cli, logger);
51
55
  registerEvidenceCommands(cli, logger);
52
56
  registerReferenceSourceCommand(cli, logger);
53
57
  registerProcessCommands(cli, logger);
@@ -57,6 +61,11 @@ const registerCoreCommands = (cli, logger) => {
57
61
  const overviewOptions = z.object({
58
62
  detail: z.enum(["concise", "detailed"]).default("concise"),
59
63
  limit: z.number().int().min(1).max(50).default(10),
64
+ snapshot: z
65
+ .string()
66
+ .min(1)
67
+ .optional()
68
+ .describe("Load and update a local analysis snapshot"),
60
69
  });
61
70
  cli.command("analyze", {
62
71
  description: "Get an overview of an app",
@@ -64,7 +73,7 @@ const registerCoreCommands = (cli, logger) => {
64
73
  path: z.string().describe("App, program, or Hopper database path"),
65
74
  }),
66
75
  options: overviewOptions,
67
- run: ({ args, options }) => logCliCommand(logger, "analyze", () => runDirectAnalysis(args.path, "binary_overview", { detail: options.detail, limit: options.limit }, logger)),
76
+ run: ({ args, options }) => logCliCommand(logger, "analyze", () => runDirectAnalysis(args.path, "binary_overview", { detail: options.detail, limit: options.limit }, { logger, snapshotPath: options.snapshot })),
68
77
  });
69
78
  cli.command("inspect", {
70
79
  description: "Inspect an app overview with evidence",
@@ -72,7 +81,7 @@ const registerCoreCommands = (cli, logger) => {
72
81
  path: z.string().describe("App, program, or Hopper database path"),
73
82
  }),
74
83
  options: overviewOptions,
75
- run: ({ args, options }) => logCliCommand(logger, "inspect", () => runDirectAnalysis(args.path, "binary_overview", { detail: options.detail, limit: options.limit }, logger)),
84
+ run: ({ args, options }) => logCliCommand(logger, "inspect", () => runDirectAnalysis(args.path, "binary_overview", { detail: options.detail, limit: options.limit }, { logger, snapshotPath: options.snapshot })),
76
85
  });
77
86
  cli.command("decompile", {
78
87
  description: "Read one part of an app as code",
@@ -80,12 +89,13 @@ const registerCoreCommands = (cli, logger) => {
80
89
  path: z.string().describe("App or program path"),
81
90
  address: z.string().describe("Procedure address"),
82
91
  }),
83
- run: ({ args }) => logCliCommand(logger, "decompile", () => runDirectAnalysis(args.path, "procedure_pseudo_code", { procedure: args.address }, logger)),
92
+ options: z.object({ snapshot: z.string().min(1).optional() }),
93
+ run: ({ args, options }) => logCliCommand(logger, "decompile", () => runDirectAnalysis(args.path, "procedure_pseudo_code", { procedure: args.address }, { logger, snapshotPath: options.snapshot })),
84
94
  });
85
95
  };
86
96
  const registerSetupCommands = (cli, logger) => {
87
97
  cli.command("setup", {
88
- description: "Install requirements and configure coding agents",
98
+ description: "Install requirements and configure agents",
89
99
  options: z.object({
90
100
  yes: z
91
101
  .boolean()
@@ -153,7 +163,8 @@ const registerXrefsCommand = (cli, logger) => {
153
163
  path: z.string().describe("App or program path"),
154
164
  address: z.string().describe("Hexadecimal address"),
155
165
  }),
156
- run: ({ args }) => logCliCommand(logger, "xrefs", () => runDirectAnalysis(args.path, "xrefs", { address: args.address }, logger)),
166
+ options: z.object({ snapshot: z.string().min(1).optional() }),
167
+ run: ({ args, options }) => logCliCommand(logger, "xrefs", () => runDirectAnalysis(args.path, "xrefs", { address: args.address }, { logger, snapshotPath: options.snapshot })),
157
168
  });
158
169
  };
159
170
  const registerTraceCommand = (cli, logger) => {
@@ -167,6 +178,7 @@ const registerTraceCommand = (cli, logger) => {
167
178
  caseSensitive: z.boolean().default(false),
168
179
  limit: z.number().int().min(1).max(100).default(20),
169
180
  maxOperations: z.number().int().min(1).max(100).default(20),
181
+ snapshot: z.string().min(1).optional(),
170
182
  }),
171
183
  alias: {
172
184
  caseSensitive: "case-sensitive",
@@ -177,7 +189,7 @@ const registerTraceCommand = (cli, logger) => {
177
189
  case_sensitive: options.caseSensitive,
178
190
  limit: options.limit,
179
191
  max_operations: options.maxOperations,
180
- }, logger)),
192
+ }, { logger, snapshotPath: options.snapshot })),
181
193
  });
182
194
  };
183
195
  const registerCapabilityCommands = (cli, logger) => {
@@ -202,6 +214,7 @@ const registerFunctionCommand = (cli, logger) => {
202
214
  limit: z.number().int().min(1).max(500).default(100),
203
215
  maxPseudocodeChars: z.number().int().min(1).max(100_000).default(20_000),
204
216
  maxInstructions: z.number().int().min(1).max(5_000).default(500),
217
+ snapshot: z.string().min(1).optional(),
205
218
  }),
206
219
  alias: {
207
220
  includeAssembly: "include-assembly",
@@ -214,7 +227,7 @@ const registerFunctionCommand = (cli, logger) => {
214
227
  limit: options.limit,
215
228
  max_pseudocode_chars: options.maxPseudocodeChars,
216
229
  max_instructions: options.maxInstructions,
217
- }, logger)),
230
+ }, { logger, snapshotPath: options.snapshot })),
218
231
  });
219
232
  };
220
233
  const registerSearchCommand = (cli, logger) => {
@@ -230,6 +243,7 @@ const registerSearchCommand = (cli, logger) => {
230
243
  caseSensitive: z.boolean().default(false),
231
244
  offset: z.number().int().min(0).default(0),
232
245
  limit: z.number().int().min(1).max(100).default(100),
246
+ snapshot: z.string().min(1).optional(),
233
247
  }),
234
248
  alias: { caseSensitive: "case-sensitive" },
235
249
  run: ({ args, options }) => logCliCommand(logger, "search", () => runDirectAnalysis(args.path, options.kind === "strings" ? "search_strings" : "search_procedures", {
@@ -238,7 +252,7 @@ const registerSearchCommand = (cli, logger) => {
238
252
  case_sensitive: options.caseSensitive,
239
253
  offset: options.offset,
240
254
  limit: options.limit,
241
- }, logger)),
255
+ }, { logger, snapshotPath: options.snapshot })),
242
256
  });
243
257
  };
244
258
  const registerReferenceSourceCommand = (cli, logger) => {
@@ -252,7 +266,10 @@ const registerReferenceSourceCommand = (cli, logger) => {
252
266
  run: ({ args }) => logCliCommand(logger, "import-reference-source", async () => {
253
267
  const config = parseConfig(process.env);
254
268
  if (!config.ok)
255
- return { error: config.error._tag, message: config.error.message };
269
+ return {
270
+ error: "Import failed",
271
+ ...projectAnalysisError(config.error),
272
+ };
256
273
  const imported = await importReferenceSource({
257
274
  root: args.root,
258
275
  caller: "rea-cli",
@@ -262,7 +279,10 @@ const registerReferenceSourceCommand = (cli, logger) => {
262
279
  });
263
280
  return imported.ok
264
281
  ? imported.value
265
- : { error: imported.error.code, message: imported.error.message };
282
+ : {
283
+ error: "Import failed",
284
+ ...projectReferenceSourceImportError(imported.error),
285
+ };
266
286
  }),
267
287
  });
268
288
  };
@@ -2,6 +2,7 @@ import { Cli, z } from "incur";
2
2
  import { compareEvidenceBundlesCommand, exportEvidenceBundleCommand, importEvidenceBundleCommand, } from "./application/EvidenceBundleCommands.js";
3
3
  import { parseConfig } from "./config.js";
4
4
  import { logCliCommand } from "./cliLogging.js";
5
+ import { projectAnalysisError } from "./domain/errors.js";
5
6
  /** Register filesystem-gated Evidence v2 commands. */
6
7
  export const registerEvidenceCommands = (cli, logger) => {
7
8
  cli.command("evidence-import", {
@@ -12,11 +13,9 @@ export const registerEvidenceCommands = (cli, logger) => {
12
13
  run: ({ args }) => logCliCommand(logger, "evidence-import", async () => {
13
14
  const config = parseConfig(process.env);
14
15
  if (!config.ok)
15
- return { error: config.error._tag, message: config.error.message };
16
+ return cliError(config.error);
16
17
  const imported = await importEvidenceBundleCommand(args.path, config.value.evidenceFilePolicy);
17
- return imported.ok
18
- ? imported.value
19
- : { error: imported.error._tag, message: imported.error.message };
18
+ return imported.ok ? imported.value : cliError(imported.error);
20
19
  }),
21
20
  });
22
21
  cli.command("evidence-export", {
@@ -31,11 +30,9 @@ export const registerEvidenceCommands = (cli, logger) => {
31
30
  run: ({ args, options }) => logCliCommand(logger, "evidence-export", async () => {
32
31
  const config = parseConfig(process.env);
33
32
  if (!config.ok)
34
- return { error: config.error._tag, message: config.error.message };
33
+ return cliError(config.error);
35
34
  const exported = await exportEvidenceBundleCommand(args.source, args.output, options.overwrite, config.value.evidenceFilePolicy);
36
- return exported.ok
37
- ? exported.value
38
- : { error: exported.error._tag, message: exported.error.message };
35
+ return exported.ok ? exported.value : cliError(exported.error);
39
36
  }),
40
37
  });
41
38
  cli.command("compare", {
@@ -52,7 +49,7 @@ export const registerEvidenceCommands = (cli, logger) => {
52
49
  run: ({ args, options }) => logCliCommand(logger, "compare", async () => {
53
50
  const config = parseConfig(process.env);
54
51
  if (!config.ok)
55
- return { error: config.error._tag, message: config.error.message };
52
+ return cliError(config.error);
56
53
  const compared = await compareEvidenceBundlesCommand({
57
54
  leftPath: args.left,
58
55
  rightPath: args.right,
@@ -60,9 +57,11 @@ export const registerEvidenceCommands = (cli, logger) => {
60
57
  limit: options.limit,
61
58
  policy: config.value.evidenceFilePolicy,
62
59
  });
63
- return compared.ok
64
- ? compared.value
65
- : { error: compared.error._tag, message: compared.error.message };
60
+ return compared.ok ? compared.value : cliError(compared.error);
66
61
  }),
67
62
  });
68
63
  };
64
+ const cliError = (error) => ({
65
+ error: "Analysis failed",
66
+ ...projectAnalysisError(error),
67
+ });
@@ -0,0 +1,86 @@
1
+ import { Cli, z } from "incur";
2
+ import { runCrossVersionInvestigation } from "./application/CrossVersionInvestigation.js";
3
+ import { logCliCommand } from "./cliLogging.js";
4
+ import { parseConfig } from "./config.js";
5
+ const investigationOptionsSchema = z.object({
6
+ yes: z
7
+ .boolean()
8
+ .default(false)
9
+ .describe("Approve persistent workspace writes"),
10
+ workspaceName: z.string().trim().min(1).max(200).default("default"),
11
+ expectedRevision: z.number().int().min(1).optional(),
12
+ maxEntries: z.number().int().min(1).max(50_000).default(10_000),
13
+ maxTotalBytes: z
14
+ .number()
15
+ .int()
16
+ .min(1)
17
+ .max(Number.MAX_SAFE_INTEGER)
18
+ .default(1_073_741_824),
19
+ maxEntryBytes: z
20
+ .number()
21
+ .int()
22
+ .min(1)
23
+ .max(Number.MAX_SAFE_INTEGER)
24
+ .default(268_435_456),
25
+ pageSize: z.number().int().min(1).max(500).default(500),
26
+ changeLimit: z.number().int().min(1).max(500).default(500),
27
+ });
28
+ /** Register the persistent cross-version CLI workflow. */
29
+ export const registerInvestigationCommands = (cli, logger) => {
30
+ cli.command("investigate-versions", {
31
+ description: "Run or resume a persistent cross-version artifact investigation",
32
+ args: z.object({
33
+ leftPath: z.string().describe("Earlier artifact or directory path"),
34
+ rightPath: z.string().describe("Later artifact or directory path"),
35
+ workspacePath: z.string().describe("Persistent workspace JSON path"),
36
+ }),
37
+ options: investigationOptionsSchema,
38
+ alias: {
39
+ workspaceName: "workspace-name",
40
+ expectedRevision: "expected-revision",
41
+ maxEntries: "max-entries",
42
+ maxTotalBytes: "max-total-bytes",
43
+ maxEntryBytes: "max-entry-bytes",
44
+ pageSize: "page-size",
45
+ changeLimit: "change-limit",
46
+ yes: "y",
47
+ },
48
+ run: ({ args, options }) => logCliCommand(logger, "investigate-versions", () => runInvestigation(args, options)),
49
+ });
50
+ };
51
+ const runInvestigation = async (args, options) => {
52
+ if (!options.yes)
53
+ return {
54
+ error: "ApprovalRequired",
55
+ message: "Persistent workspace writes require explicit --yes approval.",
56
+ };
57
+ const config = parseConfig(process.env);
58
+ if (!config.ok)
59
+ return { error: config.error._tag, message: config.error.message };
60
+ const investigated = await runCrossVersionInvestigation({
61
+ approved: true,
62
+ workspace_path: args.workspacePath,
63
+ workspace_name: options.workspaceName,
64
+ ...(options.expectedRevision === undefined
65
+ ? {}
66
+ : { expected_workspace_revision: options.expectedRevision }),
67
+ left_path: args.leftPath,
68
+ right_path: args.rightPath,
69
+ options: {
70
+ max_entries: options.maxEntries,
71
+ max_total_bytes: options.maxTotalBytes,
72
+ max_entry_bytes: options.maxEntryBytes,
73
+ max_compression_ratio: 1_000,
74
+ max_depth: 20,
75
+ max_path_bytes: 4_096,
76
+ page_size: options.pageSize,
77
+ change_limit: options.changeLimit,
78
+ },
79
+ }, config.value.evidenceFilePolicy, { inputRoots: config.value.investigationInputRoots });
80
+ return investigated.ok
81
+ ? investigated.value.evidence
82
+ : {
83
+ error: investigated.error._tag,
84
+ message: investigated.error.message,
85
+ };
86
+ };
@@ -0,0 +1,41 @@
1
+ const SAFE_VALIDATION_MESSAGE = "REA could not read the command arguments. Run `rea --help`, correct the arguments, then try again.";
2
+ const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
3
+ const validationError = (value) => {
4
+ if (!isRecord(value))
5
+ return undefined;
6
+ if (value.code === "VALIDATION_ERROR")
7
+ return value;
8
+ if (value.ok === false && isRecord(value.error))
9
+ return value.error.code === "VALIDATION_ERROR" ? value.error : undefined;
10
+ return undefined;
11
+ };
12
+ /** Remove validator internals from Incur's caller-visible CLI output. */
13
+ export const sanitizeCliOutput = (output) => {
14
+ const trimmed = output.trimStart();
15
+ if (trimmed.startsWith("{")) {
16
+ try {
17
+ const parsed = JSON.parse(trimmed);
18
+ const error = validationError(parsed);
19
+ if (error !== undefined) {
20
+ const safeError = {
21
+ code: "VALIDATION_ERROR",
22
+ message: SAFE_VALIDATION_MESSAGE,
23
+ };
24
+ if (error === parsed)
25
+ return `${JSON.stringify(safeError)}\n`;
26
+ if (!isRecord(parsed))
27
+ return output;
28
+ return `${JSON.stringify({ ...parsed, error: safeError })}\n`;
29
+ }
30
+ }
31
+ catch {
32
+ return output;
33
+ }
34
+ return output;
35
+ }
36
+ if (/^ok: false\r?\nerror:\r?\n code: VALIDATION_ERROR(?:\r?\n|$)/u.test(trimmed))
37
+ return `ok: false\nerror:\n code: VALIDATION_ERROR\n message: "${SAFE_VALIDATION_MESSAGE}"\n`;
38
+ if (!/^code: VALIDATION_ERROR(?:\r?\n|$)/u.test(trimmed))
39
+ return output;
40
+ return `code: VALIDATION_ERROR\nmessage: "${SAFE_VALIDATION_MESSAGE}"\n`;
41
+ };
@@ -1,12 +1,22 @@
1
- import { Cli, z } from "incur";
2
- import { captureProcessScenarioFile, compareProcessEvidenceFiles, } from "./application/ProcessCli.js";
1
+ import { Cli, Errors, z } from "incur";
2
+ import { captureProcessScenarioFile, compareProcessEvidenceFiles, isProcessCliErrorOutput, } from "./application/ProcessCli.js";
3
3
  import { logCliCommand } from "./cliLogging.js";
4
+ const requireProcessCommandSuccess = async (execute) => {
5
+ const value = await execute();
6
+ if (isProcessCliErrorOutput(value))
7
+ throw new Errors.IncurError({
8
+ code: "PROCESS_COMMAND_FAILED",
9
+ message: value.message,
10
+ retryable: false,
11
+ });
12
+ return value;
13
+ };
4
14
  /** Register Process Capture v4 one-shot commands through shared application services. */
5
15
  export const registerProcessCommands = (cli, logger) => {
6
16
  cli.command("capture-process", {
7
17
  description: "Capture one approved Process Capture v4 JSON scenario",
8
18
  args: z.object({ scenario: z.string().describe("Scenario JSON path") }),
9
- run: ({ args }) => logCliCommand(logger, "capture-process", () => captureProcessScenarioFile(args.scenario)),
19
+ run: ({ args }) => logCliCommand(logger, "capture-process", () => requireProcessCommandSuccess(() => captureProcessScenarioFile(args.scenario))),
10
20
  });
11
21
  cli.command("compare-process-captures", {
12
22
  description: "Compare two Process Capture v4 Evidence JSON files",
@@ -14,6 +24,6 @@ export const registerProcessCommands = (cli, logger) => {
14
24
  left: z.string().describe("Left capture Evidence JSON path"),
15
25
  right: z.string().describe("Right capture Evidence JSON path"),
16
26
  }),
17
- run: ({ args }) => logCliCommand(logger, "compare-process-captures", () => compareProcessEvidenceFiles(args.left, args.right)),
27
+ run: ({ args }) => logCliCommand(logger, "compare-process-captures", () => requireProcessCommandSuccess(() => compareProcessEvidenceFiles(args.left, args.right))),
18
28
  });
19
29
  };