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
@@ -1,42 +1,69 @@
1
1
  import { readFile } from "node:fs/promises";
2
2
  import { parseConfig } from "../config.js";
3
+ import { AnalysisError, projectAnalysisError } from "../domain/errors.js";
3
4
  import { createEvidence, parseEvidence } from "../domain/evidence.js";
4
5
  import { jsonValueSchema } from "../domain/jsonValue.js";
5
6
  import { compareProcessCaptures, LEGACY_PROCESS_CAPTURE_MESSAGE, parseProcessScenario, parseProcessCapture, } from "../domain/processCapture.js";
6
7
  import { captureProcessScenario } from "./ProcessHarness.js";
7
8
  import { PROCESS_PROVIDER, createProcessCaptureEvidence, } from "./ProcessEvidence.js";
8
9
  const MAX_INPUT_BYTES = 64 * 1024 * 1024;
10
+ /** Distinguish a safe process-command failure from successful Evidence. */
11
+ export const isProcessCliErrorOutput = (value) => typeof value === "object" &&
12
+ value !== null &&
13
+ "error" in value &&
14
+ value.error === "Process command failed" &&
15
+ "category" in value &&
16
+ typeof value.category === "string" &&
17
+ "message" in value &&
18
+ typeof value.message === "string";
9
19
  /** Capture one JSON scenario through the same policy and evidence contract as MCP. */
10
20
  export const captureProcessScenarioFile = async (path) => {
11
- const scenario = parseProcessScenario(await readJson(path));
12
- const config = parseConfig(process.env);
13
- if (!config.ok)
14
- throw config.error;
15
- const captured = await captureProcessScenario(scenario, config.value.processExecutionPolicy);
16
- if (!captured.ok)
17
- throw captured.error;
18
- return createProcessCaptureEvidence(scenario, captured.value);
21
+ try {
22
+ const input = await readJson(path);
23
+ let scenario;
24
+ try {
25
+ scenario = parseProcessScenario(input);
26
+ }
27
+ catch {
28
+ throw new ProcessCliFailure("invalid_input", "Process scenario is invalid. Check its required fields and limits, then try again.");
29
+ }
30
+ const config = parseConfig(process.env);
31
+ if (!config.ok)
32
+ return cliAnalysisError(config.error);
33
+ const captured = await captureProcessScenario(scenario, config.value.processExecutionPolicy);
34
+ if (!captured.ok)
35
+ return cliAnalysisError(captured.error);
36
+ return createProcessCaptureEvidence(scenario, captured.value);
37
+ }
38
+ catch (cause) {
39
+ return projectProcessCliError(cause);
40
+ }
19
41
  };
20
42
  /** Compare two capture Evidence files and emit derived comparison Evidence. */
21
43
  export const compareProcessEvidenceFiles = async (leftPath, rightPath) => {
22
- const left = parseCaptureEvidence(await readJson(leftPath));
23
- const right = parseCaptureEvidence(await readJson(rightPath));
24
- const comparison = compareProcessCaptures(left.capture, right.capture);
25
- return createEvidence(undefined, PROCESS_PROVIDER, {
26
- predicateType: "rea.process-comparison/v3",
27
- operation: "compare_process_captures",
28
- parameters: {
29
- left_evidence_id: left.id,
30
- right_evidence_id: right.id,
31
- left_normalization: left.capture.normalization,
32
- right_normalization: right.capture.normalization,
33
- },
34
- result: jsonValueSchema.parse(comparison),
35
- confidence: "derived",
36
- authority: "analyst-inference",
37
- limitations: comparison.limitations,
38
- evidenceLinks: [left.id, right.id],
39
- });
44
+ try {
45
+ const left = parseCaptureEvidence(await readJson(leftPath));
46
+ const right = parseCaptureEvidence(await readJson(rightPath));
47
+ const comparison = compareProcessCaptures(left.capture, right.capture);
48
+ return createEvidence(undefined, PROCESS_PROVIDER, {
49
+ predicateType: "rea.process-comparison/v3",
50
+ operation: "compare_process_captures",
51
+ parameters: {
52
+ left_evidence_id: left.id,
53
+ right_evidence_id: right.id,
54
+ left_normalization: left.capture.normalization,
55
+ right_normalization: right.capture.normalization,
56
+ },
57
+ result: jsonValueSchema.parse(comparison),
58
+ confidence: "derived",
59
+ authority: "analyst-inference",
60
+ limitations: comparison.limitations,
61
+ evidenceLinks: [left.id, right.id],
62
+ });
63
+ }
64
+ catch (cause) {
65
+ return projectProcessCliError(cause);
66
+ }
40
67
  };
41
68
  const parseCaptureEvidence = (input) => {
42
69
  const evidence = parseEvidence(input);
@@ -45,8 +72,8 @@ const parseCaptureEvidence = (input) => {
45
72
  evidence.provider.id !== PROCESS_PROVIDER.id ||
46
73
  evidence.provider.version !== PROCESS_PROVIDER.version) {
47
74
  if (evidence.predicate_type === "rea.process-capture/v3")
48
- throw new TypeError(LEGACY_PROCESS_CAPTURE_MESSAGE);
49
- throw new TypeError("Expected REA Process Capture v4 Evidence");
75
+ throw new ProcessCliFailure("invalid_input", LEGACY_PROCESS_CAPTURE_MESSAGE);
76
+ throw new ProcessCliFailure("invalid_input", "Capture evidence is not from the current process-capture workflow. Create new capture evidence, then try again.");
50
77
  }
51
78
  return {
52
79
  id: evidence.evidence_id,
@@ -54,8 +81,48 @@ const parseCaptureEvidence = (input) => {
54
81
  };
55
82
  };
56
83
  const readJson = async (path) => {
57
- const bytes = await readFile(path);
84
+ let bytes;
85
+ try {
86
+ bytes = await readFile(path);
87
+ }
88
+ catch {
89
+ throw new ProcessCliFailure("invalid_input", "Process input file could not be read. Check that the path exists and is readable.");
90
+ }
58
91
  if (bytes.length > MAX_INPUT_BYTES)
59
- throw new TypeError("Process JSON input exceeds byte limit");
60
- return JSON.parse(bytes.toString("utf8"));
92
+ throw new ProcessCliFailure("truncated", "Process input file is too large. Reduce it below 64 MiB, then try again.");
93
+ try {
94
+ return JSON.parse(bytes.toString("utf8"));
95
+ }
96
+ catch {
97
+ throw new ProcessCliFailure("invalid_input", "Process input file is not valid JSON. Repair the file, then try again.");
98
+ }
99
+ };
100
+ class ProcessCliFailure extends Error {
101
+ category;
102
+ userMessage;
103
+ constructor(category, userMessage) {
104
+ super(userMessage);
105
+ this.category = category;
106
+ this.userMessage = userMessage;
107
+ }
108
+ }
109
+ const cliAnalysisError = (error) => ({
110
+ error: "Process command failed",
111
+ ...projectAnalysisError(error),
112
+ });
113
+ /** Project any process CLI failure without exposing its cause. */
114
+ export const projectProcessCliError = (cause) => {
115
+ if (cause instanceof ProcessCliFailure)
116
+ return {
117
+ error: "Process command failed",
118
+ category: cause.category,
119
+ message: cause.userMessage,
120
+ };
121
+ if (cause instanceof AnalysisError)
122
+ return cliAnalysisError(cause);
123
+ return {
124
+ error: "Process command failed",
125
+ category: "execution_failure",
126
+ message: "Process command could not complete. Check the input files and run `rea doctor`, then try again.",
127
+ };
61
128
  };
@@ -1,7 +1,7 @@
1
1
  import { spawn } from "node-pty";
2
2
  import { validateProcessCapture } from "../domain/processCapture.js";
3
3
  import { err, ok } from "../domain/result.js";
4
- import { ProcessCaptureError } from "./ProcessCaptureError.js";
4
+ import { ProcessCaptureError, processCaptureCancelled, } from "./ProcessCaptureError.js";
5
5
  export { ProcessCaptureError } from "./ProcessCaptureError.js";
6
6
  import { startLoopbackReplay } from "./LoopbackReplay.js";
7
7
  import { startProcessSampler } from "./ProcessSampling.js";
@@ -218,7 +218,7 @@ const completeCapture = async (options) => {
218
218
  runtime.checkpoints.trigger("root_exit");
219
219
  const { reason } = options.exit;
220
220
  if (reason === "cancelled")
221
- throw new ProcessCaptureError("process capture was cancelled");
221
+ throw processCaptureCancelled();
222
222
  const settlement = await observeSettlement(options.runId, [
223
223
  runtime.terminal.pid,
224
224
  ...options.samples.flatMap(({ process_group_id }) => process_group_id === null ? [] : [process_group_id]),
@@ -3,16 +3,31 @@ import { posix } from "node:path";
3
3
  import { classifyReferenceSourcePath, detectReferenceSourceLanguage, } from "../domain/referenceSourceClassification.js";
4
4
  import { parseReferenceSourceImports } from "../domain/referenceSourceImportParsing.js";
5
5
  import { PARSEABLE_REFERENCE_SOURCE_LANGUAGES } from "./ReferenceSourceImportTypes.js";
6
+ /** Project a low-level entry failure into safe import guidance. */
7
+ export const projectReferenceSourceEntryFailure = (entry) => {
8
+ if (entry.code === "cancelled")
9
+ return "This entry was not read because the import was cancelled. Start the import again when ready.";
10
+ if (entry.code === "limit")
11
+ return "This entry was not read because an import limit was reached. Import a smaller directory or raise the configured limits.";
12
+ if (entry.code === "unsupported")
13
+ return "This entry cannot be read safely on this system. Exclude it or import the directory on a supported system.";
14
+ if (entry.kind === "directory")
15
+ return "This directory could not be read. Check its permissions, then try again.";
16
+ if (entry.kind === "symlink")
17
+ return "This symbolic link could not be read safely. Check the link and its permissions, then try again.";
18
+ return "This file could not be read. Check its permissions, then try again.";
19
+ };
6
20
  const hashBytes = (bytes) => createHash("sha256").update(bytes).digest("hex");
7
21
  const failedEntry = (entry) => {
8
22
  const classifications = classifyReferenceSourcePath(entry.path);
23
+ const limitation = projectReferenceSourceEntryFailure(entry);
9
24
  if (entry.kind === "directory")
10
25
  return {
11
26
  path: entry.path,
12
27
  kind: "directory",
13
28
  classifications,
14
29
  tree_state: entry.code === "limit" ? "partial" : "unreadable",
15
- limitations: [entry.message],
30
+ limitations: [limitation],
16
31
  };
17
32
  if (entry.kind === "symlink")
18
33
  return {
@@ -21,7 +36,7 @@ const failedEntry = (entry) => {
21
36
  target: "<unreadable>",
22
37
  target_state: "unreadable",
23
38
  classifications,
24
- limitations: [entry.message],
39
+ limitations: [limitation],
25
40
  };
26
41
  return {
27
42
  path: entry.path,
@@ -31,7 +46,7 @@ const failedEntry = (entry) => {
31
46
  language: null,
32
47
  classifications: entry.kind === "file" ? classifications : ["unknown"],
33
48
  content_state: entry.code === "limit" ? "too-large" : "unreadable",
34
- limitations: [entry.message],
49
+ limitations: [limitation],
35
50
  };
36
51
  };
37
52
  const resolveInternalSpecifier = (fromPath, specifier, filePaths) => {
@@ -1,3 +1,35 @@
1
+ /** Safe CLI projection for a historical-source import failure. */
2
+ export const projectReferenceSourceImportError = (error) => {
3
+ if (error.code === "cancelled")
4
+ return {
5
+ category: "cancelled",
6
+ message: "Reference source import was cancelled. Start it again when ready.",
7
+ };
8
+ if (error.code === "invalid-limits")
9
+ return {
10
+ category: "invalid_input",
11
+ message: "Reference source limits are invalid. Use positive integer limits, then try again.",
12
+ };
13
+ if (error.code === "invalid-root")
14
+ return {
15
+ category: "invalid_input",
16
+ message: "Reference source directory could not be opened. Check that the path exists, is readable, and points to a directory.",
17
+ };
18
+ if (error.code === "policy")
19
+ return {
20
+ category: "permission_required",
21
+ message: "Reference source directory is not approved. Add its directory to `REA_REFERENCE_ROOTS_JSON`, restart REA, then try again.",
22
+ };
23
+ if (error.code === "io")
24
+ return {
25
+ category: "execution_failure",
26
+ message: "Reference source files could not be read. Check directory permissions and try again.",
27
+ };
28
+ return {
29
+ category: "execution_failure",
30
+ message: "Reference source could not be indexed. Check that the source tree is readable, then try again.",
31
+ };
32
+ };
1
33
  export const DEFAULT_REFERENCE_SOURCE_IGNORE_PATTERNS = [
2
34
  ".git/",
3
35
  ".git/hooks/",
@@ -9,50 +9,14 @@ import { supportsNodeVersion } from "../domain/runtimeVersion.js";
9
9
  import { runDoctor, systemDoctorHost } from "./Doctor.js";
10
10
  import { installLinuxHopper, readLinuxDistribution, } from "./LinuxHopper.js";
11
11
  import { installMacHopper } from "./MacHopper.js";
12
+ import { configureDetectedClients } from "./SetupClients.js";
12
13
  import { installCanonicalSkill } from "./SetupSkill.js";
14
+ import { setupPlan } from "./SetupPlan.js";
15
+ import { setupInstallFailure, } from "./SetupInstallFailure.js";
13
16
  export { installCanonicalSkill } from "./SetupSkill.js";
14
17
  const registrationCommand = () => process.env.npm_command === "exec"
15
18
  ? ["npx", "-y", PRODUCT_IDENTITY.packageName, "mcp"]
16
19
  : [resolve(process.argv[1] ?? PRODUCT_IDENTITY.cliBinary), "mcp"];
17
- const setupPlan = (platform, hopperPath, clients) => [
18
- ...(hopperPath === undefined
19
- ? [
20
- {
21
- kind: "install_hopper",
22
- target: platform === "darwin"
23
- ? "~/Applications/Hopper Disassembler.app"
24
- : "system package manager",
25
- detail: "Download the official Hopper package, verify it, install it, and open Hopper for activation.",
26
- external: true,
27
- },
28
- ]
29
- : []),
30
- ...clients
31
- .filter(({ format }) => format !== "unsupported")
32
- .map((client) => ({
33
- kind: "configure_client",
34
- target: client.configPath,
35
- detail: `Add the REA MCP registration for ${client.name}; preserve unrelated configuration.`,
36
- external: false,
37
- })),
38
- {
39
- kind: "install_skill",
40
- target: "~/.agents/skills/rea-analysis/SKILL.md",
41
- detail: "Install or update the bundled REA analysis skill.",
42
- external: false,
43
- },
44
- ];
45
- const configureDetectedClients = async (options) => {
46
- for (const client of options.detectedClients) {
47
- const result = await options.host.configureClient(client, options.hopperPath, registrationCommand());
48
- options.clients[client.name] = result;
49
- if (result.status === "failed")
50
- return `${client.name} configuration ${result.reason} verification failed; no successful configuration was reported.`;
51
- if (result.status === "configured")
52
- options.appliedActions.push(`configured_${client.name}`);
53
- }
54
- return undefined;
55
- };
56
20
  /**
57
21
  * Install prerequisites and configure detected clients idempotently.
58
22
  * Discovery always precedes mutation. Interactive confirmation or explicit
@@ -74,8 +38,13 @@ export const runSetup = async (options, host = systemSetupHost(), confirm) => {
74
38
  if (unsupported !== undefined)
75
39
  return fail(unsupported);
76
40
  let hopperPath = await host.hopperPath();
41
+ const initialDoctor = await host.doctor();
42
+ const linuxHopperRepairNeeded = initialDoctor.checks.some(({ name, ok, detail }) => !ok &&
43
+ (name === "hopper-demo-runtime" ||
44
+ (name === "hopper-version" && detail === "/opt/hopper/bin/Hopper")));
45
+ const installHopper = hopperPath === undefined || linuxHopperRepairNeeded;
77
46
  const detectedClients = await host.detectedClients();
78
- plannedActions = setupPlan(host.platform, hopperPath, detectedClients);
47
+ plannedActions = setupPlan(host.platform, installHopper, detectedClients);
79
48
  let approved = options.approved;
80
49
  let interactiveApproval = false;
81
50
  if (!approved && confirm !== undefined && !options.structured) {
@@ -91,24 +60,26 @@ export const runSetup = async (options, host = systemSetupHost(), confirm) => {
91
60
  doctor: await host.doctor(),
92
61
  remediation: "Review the setup plan, then rerun interactively or with --yes.",
93
62
  };
94
- if (hopperPath === undefined &&
95
- (interactiveApproval || options.installHopper)) {
96
- hopperPath = await host.installHopper();
97
- if (hopperPath === undefined)
63
+ if (installHopper && (interactiveApproval || options.installHopper)) {
64
+ const installed = await host.installHopper();
65
+ if (installed.status === "failed")
98
66
  return {
99
67
  status: "needs_human",
100
68
  plannedActions,
101
69
  appliedActions,
102
70
  clients,
103
71
  doctor: await host.doctor(),
104
- remediation: "Hopper installation failed; install Hopper manually or rerun setup after resolving the reported system error.",
72
+ code: installed.code,
73
+ remediation: installed.remediation,
105
74
  };
75
+ hopperPath = installed.launcherPath;
106
76
  appliedActions.push("installed_hopper");
107
77
  }
108
78
  const clientFailure = await configureDetectedClients({
109
79
  host,
110
80
  detectedClients,
111
81
  hopperPath,
82
+ command: registrationCommand(),
112
83
  clients,
113
84
  appliedActions,
114
85
  });
@@ -116,29 +87,29 @@ export const runSetup = async (options, host = systemSetupHost(), confirm) => {
116
87
  return fail(clientFailure);
117
88
  const skill = await host.installSkill();
118
89
  if (skill === "failed")
119
- return fail("Agent skill installation or readback failed.");
90
+ return fail("REA analysis skill could not be installed or verified. Check permissions for `~/.agents/skills`, then rerun setup.");
120
91
  if (skill === "installed")
121
92
  appliedActions.push("installed_skill");
122
93
  const doctor = await host.doctor();
123
- const activationRequired = appliedActions.includes("installed_hopper");
124
- const ready = doctor.healthy && !activationRequired;
94
+ const remediation = finalSetupRemediation(host.platform, appliedActions.includes("installed_hopper"), doctor.healthy, hopperPath);
125
95
  return {
126
- status: ready ? "ready" : "needs_human",
96
+ status: remediation === undefined ? "ready" : "needs_human",
127
97
  plannedActions,
128
98
  appliedActions,
129
99
  clients,
130
100
  doctor,
131
- ...(ready
132
- ? {}
133
- : {
134
- remediation: activationRequired
135
- ? "Open Hopper, complete its one-time activation, then rerun rea doctor --json."
136
- : hopperPath === undefined
137
- ? "Hopper is optional for non-Hopper providers. Rerun with --yes --install-hopper for deep native analysis."
138
- : "Run rea doctor and apply each reported remediation.",
139
- }),
101
+ ...(remediation === undefined ? {} : { remediation }),
140
102
  };
141
103
  };
104
+ const finalSetupRemediation = (platform, installedHopper, healthy, hopperPath) => {
105
+ if (platform === "darwin" && installedHopper)
106
+ return "Open Hopper, choose its demo mode or activate a license, then rerun rea doctor --json.";
107
+ if (healthy)
108
+ return undefined;
109
+ return hopperPath === undefined
110
+ ? "Hopper is optional for non-Hopper providers. Rerun with --yes --install-hopper for deep native analysis."
111
+ : "Run rea doctor and apply each reported remediation.";
112
+ };
142
113
  const hostRemediation = async (host) => {
143
114
  if (host.platform !== "darwin" && host.platform !== "linux")
144
115
  return "REA supports Hopper on macOS and selected 64-bit Linux distributions.";
@@ -167,7 +138,9 @@ const systemSetupHost = () => {
167
138
  const result = process.platform === "linux"
168
139
  ? await installLinuxHopper()
169
140
  : await installMacHopper();
170
- return result.status === "installed" ? result.launcherPath : undefined;
141
+ if (result.status === "installed")
142
+ return result;
143
+ return setupInstallFailure(result.reason);
171
144
  },
172
145
  detectedClients: () => detectClients(homedir()),
173
146
  configureClient: (client, hopperPath, command) => client.format === "unsupported"
@@ -179,7 +152,7 @@ const systemSetupHost = () => {
179
152
  doctor: () => runDoctor(undefined, doctorHost),
180
153
  };
181
154
  };
182
- /** Detect supported coding agents from stable per-user installation markers. */
155
+ /** Detect supported agents from stable per-user installation markers. */
183
156
  export const detectClients = async (home) => {
184
157
  const detected = [];
185
158
  for (const candidate of supportedClients(home))
@@ -0,0 +1,19 @@
1
+ const failedConfigurationMessage = (reason) => {
2
+ if (reason === "backup")
3
+ return "Agent configuration could not be backed up, so no change was made. Check file permissions, then rerun setup.";
4
+ if (reason === "write")
5
+ return "Agent configuration could not be updated. Check file permissions, then rerun setup.";
6
+ return "Agent configuration could not be verified after writing. Repair the configuration file or restore its `.rea.backup`, then rerun setup.";
7
+ };
8
+ /** Configure each detected agent, stopping after the first failed transaction. */
9
+ export const configureDetectedClients = async (options) => {
10
+ for (const client of options.detectedClients) {
11
+ const result = await options.host.configureClient(client, options.hopperPath, options.command);
12
+ options.clients[client.name] = result;
13
+ if (result.status === "failed")
14
+ return failedConfigurationMessage(result.reason);
15
+ if (result.status === "configured")
16
+ options.appliedActions.push(`configured_${client.name}`);
17
+ }
18
+ return undefined;
19
+ };
@@ -0,0 +1,53 @@
1
+ /** Map provider installer failures to stable setup codes and safe recovery. */
2
+ export const setupInstallFailure = (reason) => {
3
+ switch (reason) {
4
+ case "unsupported_host":
5
+ return {
6
+ status: "failed",
7
+ code: "unsupported_host",
8
+ remediation: "Use a supported host, then rerun rea setup.",
9
+ };
10
+ case "integrity":
11
+ return {
12
+ status: "failed",
13
+ code: "integrity_mismatch",
14
+ remediation: "The Hopper download failed integrity verification. Retry setup; if it repeats, update REA before installing.",
15
+ };
16
+ case "authorization_or_package_manager":
17
+ return {
18
+ status: "failed",
19
+ code: "authorization_or_package_manager_failed",
20
+ remediation: "Allow the system package-manager operation or install the planned Hopper package and Linux runtime dependencies, then rerun rea setup.",
21
+ };
22
+ case "launcher_missing":
23
+ return {
24
+ status: "failed",
25
+ code: "launcher_missing",
26
+ remediation: "The package operation completed but Hopper is not runnable. Run rea doctor and repair the reported launcher issue.",
27
+ };
28
+ case "runtime_dependencies":
29
+ return {
30
+ status: "failed",
31
+ code: "runtime_dependency_unavailable",
32
+ remediation: "Hopper was installed but required shared libraries are missing. Rerun rea setup, then apply the hopper-demo-runtime remediation from rea doctor.",
33
+ };
34
+ case "unsupported_hopper_build":
35
+ return {
36
+ status: "failed",
37
+ code: "unsupported_hopper_build",
38
+ remediation: "The installed Hopper launcher does not match the build supported by this REA release. Unset HOPPER_LAUNCHER_PATH, reinstall through rea setup, or update REA.",
39
+ };
40
+ case "cancelled":
41
+ return {
42
+ status: "failed",
43
+ code: "setup_cancelled",
44
+ remediation: "Setup was cancelled before Hopper was installed. Rerun rea setup when ready.",
45
+ };
46
+ default:
47
+ return {
48
+ status: "failed",
49
+ code: "download_failed",
50
+ remediation: "REA could not download or unpack the official Hopper package. Check network and filesystem access, then retry setup.",
51
+ };
52
+ }
53
+ };
@@ -0,0 +1,31 @@
1
+ /** Build the complete setup mutation plan before approval. */
2
+ export const setupPlan = (platform, installHopper, clients) => [
3
+ ...(installHopper
4
+ ? [
5
+ {
6
+ kind: "install_hopper",
7
+ target: platform === "darwin"
8
+ ? "~/Applications/Hopper Disassembler.app"
9
+ : "system package manager",
10
+ detail: platform === "linux"
11
+ ? "Download, verify, and install Hopper plus its Xvfb demo-session dependencies. For the supported demo build, REA uses a private display and selects Hopper's offered demo mode for each analysis session."
12
+ : "Download the official Hopper package, verify it, and install it. Hopper may show its demo or license prompt when first opened.",
13
+ external: true,
14
+ },
15
+ ]
16
+ : []),
17
+ ...clients
18
+ .filter(({ format }) => format !== "unsupported")
19
+ .map((client) => ({
20
+ kind: "configure_client",
21
+ target: client.configPath,
22
+ detail: `Add the REA MCP registration for ${client.name}; preserve unrelated configuration.`,
23
+ external: false,
24
+ })),
25
+ {
26
+ kind: "install_skill",
27
+ target: "~/.agents/skills/rea-analysis/SKILL.md",
28
+ detail: "Install or update the bundled REA analysis skill.",
29
+ external: false,
30
+ },
31
+ ];