rea-agents 2.1.0 → 2.2.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 (48) hide show
  1. package/README.md +29 -10
  2. package/dist/application/AndroidApplicationService.js +17 -0
  3. package/dist/application/AppleApplicationService.js +17 -0
  4. package/dist/application/ClientRegistrationStatus.js +11 -4
  5. package/dist/application/InventoryProjectionEvidence.js +41 -0
  6. package/dist/application/InvestigationProviders.js +12 -0
  7. package/dist/application/ProcessPairedExperiment.js +72 -0
  8. package/dist/application/Setup.js +3 -3
  9. package/dist/application/SetupClientConfiguration.js +10 -6
  10. package/dist/application/SetupPlan.js +3 -2
  11. package/dist/application/SetupSkill.js +44 -40
  12. package/dist/application/Uninstall.js +7 -1
  13. package/dist/application/Upgrade.js +16 -1
  14. package/dist/cli/setupCommands.js +8 -3
  15. package/dist/cliSetup.js +29 -12
  16. package/dist/contracts/electronToolContracts.js +51 -3
  17. package/dist/contracts/managedToolContracts.js +7 -2
  18. package/dist/contracts/sessionStatusContract.js +26 -0
  19. package/dist/contracts/toolContractTypes.js +12 -1
  20. package/dist/contracts/toolContracts.js +3 -10
  21. package/dist/contracts/toolOutputSchemaGroups.js +51 -9
  22. package/dist/contracts/toolOutputSchemaPrimitives.js +33 -32
  23. package/dist/domain/androidApplication.js +206 -0
  24. package/dist/domain/appleApplication.js +221 -0
  25. package/dist/domain/boundedCartesianProjection.js +15 -0
  26. package/dist/domain/processComparison.js +19 -0
  27. package/dist/domain/processPairedExperiment.js +128 -0
  28. package/dist/domain/replayMachine.js +316 -0
  29. package/dist/domain/replayMachineRuntime.js +215 -0
  30. package/dist/domain/runtimeIdentification.js +210 -0
  31. package/dist/evaluation/CodexAgentEval.js +132 -0
  32. package/dist/generatedPackageMetadata.js +2 -2
  33. package/dist/identity.js +2 -1
  34. package/dist/server/createServer.js +5 -2
  35. package/dist/server/javascriptApplicationResult.js +85 -0
  36. package/dist/server/registerElectronTools.js +19 -5
  37. package/dist/server/registerEvidenceResources.js +2 -0
  38. package/dist/server/registerJavaScriptApplicationGraphResource.js +69 -0
  39. package/dist/server/registerManagedTools.js +18 -1
  40. package/dist/server/registerSessionStatusTool.js +95 -25
  41. package/dist/server/toolResult.js +10 -4
  42. package/package.json +2 -1
  43. package/skills/reverse-engineer-anything/SKILL.md +68 -227
  44. package/skills/reverse-engineer-anything/references/controlled-replay.md +12 -0
  45. package/skills/reverse-engineer-anything/references/evidence-workflows.md +33 -0
  46. package/skills/reverse-engineer-anything/references/javascript-applications.md +25 -0
  47. package/skills/reverse-engineer-anything/references/native-and-artifacts.md +36 -0
  48. package/skills/reverse-engineer-anything/references/runtime-observation.md +24 -0
package/README.md CHANGED
@@ -36,17 +36,18 @@ Reverse engineering normally makes the operator choose a tool, learn its API, mo
36
36
 
37
37
  ## Just ask your agent
38
38
 
39
- Install the REA skill:
39
+ Run setup once. Agent integration installs an aligned MCP registration and the
40
+ bundled routing skill together:
40
41
 
41
42
  ```bash
42
- npx skills add morluto/rea
43
+ npx rea-agents setup
43
44
  ```
44
45
 
45
46
  Then ask:
46
47
 
47
48
  ```text
48
- Use REA to understand how search works in the Notes app, show me the
49
- evidence, and build a similar feature for my project.
49
+ Understand how search works in the Notes app, show me the evidence, and build a
50
+ similar feature for my project.
50
51
  ```
51
52
 
52
53
  Notes is only an example. Name any app you want to understand, or ask the agent to start with an overview.
@@ -97,9 +98,9 @@ shows its complete plan and asks before applying it. Setup does not update
97
98
  Homebrew, Node.js, or npm. `npx rea-agents setup` opens with the work it
98
99
  enables: investigate local apps from an agent, recover evidence through a
99
100
  deep-analysis provider, and reuse REA's guided workflow. It summarizes the
100
- detected agents, then asks which independent capabilities to set up: coding-agent
101
- access through MCP, the shared investigation skill, and—when needed—the Hopper
102
- provider. Nothing is preselected. Choosing MCP access opens a second empty
101
+ detected agents, then asks which capabilities to set up: agent integration
102
+ (MCP plus the matching guided workflow) and—when needed—the Hopper provider.
103
+ Nothing is preselected. Choosing agent integration opens a second empty
103
104
  checklist for the specific detected agents that should receive a registration.
104
105
 
105
106
  REA keeps the journey inline so its history remains in the terminal. Selecting
@@ -131,10 +132,12 @@ Pass installer options after `bash -s --`, for example `--dry-run`, `--no-setup`
131
132
  ### With an agent — recommended
132
133
 
133
134
  ```bash
134
- npx skills add morluto/rea
135
+ npx rea-agents setup
135
136
  ```
136
137
 
137
- Ask your agent to set up REA. It will check your supported host, explain anything it needs to install, ask for approval, and guide you through system prompts. After setup, restart the agent if it asks you to load the full REA toolset.
138
+ Choose Agent Integration in the reviewed setup plan. REA installs the pinned MCP
139
+ registration and its matching routing skill as one transaction. After setup,
140
+ restart the configured agent so it loads the aligned integration.
138
141
 
139
142
  Review the setup plan, approve it if appropriate, then describe the app or feature you want to understand. Hopper can run in its free demo mode; if it shows a first-run prompt, choose the demo or enter an existing license.
140
143
 
@@ -456,17 +459,28 @@ Setup detects Claude Code, Claude Desktop, Codex, Cursor, Gemini CLI, Windsurf,
456
459
 
457
460
  ### Manual MCP configuration
458
461
 
462
+ <!-- x-release-please-start-version -->
463
+
459
464
  ```json
460
465
  {
461
466
  "mcpServers": {
462
467
  "rea": {
463
468
  "command": "npx",
464
- "args": ["-y", "rea-agents@latest", "mcp"]
469
+ "args": ["-y", "rea-agents@2.2.0", "mcp"]
465
470
  }
466
471
  }
467
472
  }
468
473
  ```
469
474
 
475
+ <!-- x-release-please-end -->
476
+
477
+ Persistent registrations should use one exact package version. `rea setup`
478
+ maintains that pin, upgrades the bundled skill at the same time, and gives Codex
479
+ a 30-second startup allowance for a cold package-runner start. An interactive
480
+ `rea upgrade` opens the updated setup plan after installing the new executable;
481
+ structured or non-interactive upgrades tell you to run that sync explicitly.
482
+ Restart clients whose approved registration changed.
483
+
470
484
  MCP clients that support prompts can also discover six ordered investigation
471
485
  workflows through `prompts/list`. Their optional identifier arguments use the
472
486
  current session for bounded `completion/complete` suggestions; see
@@ -693,6 +707,11 @@ Any agent that can run a local MCP server can use the manual configuration. Setu
693
707
 
694
708
  See [CONTRIBUTING.md](CONTRIBUTING.md) for setup, architecture, tests, and release instructions. Generated API documentation is available under [`docs/api`](docs/api/index.html).
695
709
 
710
+ `npm run verify:agent` runs brandless native, JavaScript-application, managed,
711
+ and browser prompts through a real local Codex CLI. Its JSON report measures
712
+ natural MCP use, first-tool routing, repeated calls, actual Codex token usage,
713
+ completion quality, and explicit treatment of authority and unknowns.
714
+
696
715
  ## Project links
697
716
 
698
717
  [npm](https://www.npmjs.com/package/rea-agents) · [Issues](https://github.com/morluto/rea/issues) · [Security](SECURITY.md) · [Contributing](CONTRIBUTING.md) · [Hopper](https://www.hopperapp.com/) · [Ghidra](https://github.com/NationalSecurityAgency/ghidra)
@@ -0,0 +1,17 @@
1
+ import { androidApplicationProjectionInputSchema, projectAndroidApplication, } from "../domain/androidApplication.js";
2
+ import { ANDROID_APPLICATION_PROVIDER } from "./InvestigationProviders.js";
3
+ import { projectInventoryEvidence } from "./InventoryProjectionEvidence.js";
4
+ const OPERATION = "project_android_application_graph";
5
+ /** Project authenticated APK inventory Evidence into Android application Evidence. */
6
+ export const projectAndroidApplicationEvidence = (rawInput) => {
7
+ return projectInventoryEvidence({
8
+ rawInput,
9
+ schema: androidApplicationProjectionInputSchema,
10
+ project: projectAndroidApplication,
11
+ operation: OPERATION,
12
+ predicateType: "rea.android-application-graph/v1",
13
+ provider: ANDROID_APPLICATION_PROVIDER,
14
+ subjectFormat: () => "apk",
15
+ protocolError: "Android application projection produced an invalid result",
16
+ });
17
+ };
@@ -0,0 +1,17 @@
1
+ import { appleApplicationProjectionInputSchema, projectAppleApplication, } from "../domain/appleApplication.js";
2
+ import { APPLE_APPLICATION_PROVIDER } from "./InvestigationProviders.js";
3
+ import { projectInventoryEvidence } from "./InventoryProjectionEvidence.js";
4
+ const OPERATION = "project_apple_application_graph";
5
+ /** Project authenticated IPA inventory Evidence into Apple application evidence. */
6
+ export const projectAppleApplicationEvidence = (rawInput) => {
7
+ return projectInventoryEvidence({
8
+ rawInput,
9
+ schema: appleApplicationProjectionInputSchema,
10
+ project: projectAppleApplication,
11
+ operation: OPERATION,
12
+ predicateType: "rea.apple-application-graph/v1",
13
+ provider: APPLE_APPLICATION_PROVIDER,
14
+ subjectFormat: () => "ipa",
15
+ protocolError: "Apple application projection produced an invalid result",
16
+ });
17
+ };
@@ -6,7 +6,11 @@ import { PRODUCT_IDENTITY } from "../identity.js";
6
6
  import { supportedClients } from "./SupportedClients.js";
7
7
  const objectSchema = z.record(z.string(), z.unknown());
8
8
  const registrationSchema = z
9
- .object({ command: z.string().min(1), args: z.array(z.string()).default([]) })
9
+ .object({
10
+ command: z.string().min(1),
11
+ args: z.array(z.string()).default([]),
12
+ startup_timeout_sec: z.number().positive().optional(),
13
+ })
10
14
  .passthrough();
11
15
  /** Inspect supported client registrations without reading their environment. */
12
16
  export const readClientRegistrationStatuses = async (home, currentCommandPath = resolve(process.argv[1] ?? "unknown")) => {
@@ -25,7 +29,7 @@ export const readClientRegistrationStatuses = async (home, currentCommandPath =
25
29
  }
26
30
  const registration = registrationSchema.parse(raw);
27
31
  const command = [registration.command, ...registration.args];
28
- statuses.push(status(client.name, client.configPath, command, registrationAligned(command, currentCommandPath)
32
+ statuses.push(status(client.name, client.configPath, command, registrationAligned(registration, client.name, currentCommandPath)
29
33
  ? "aligned"
30
34
  : "stale"));
31
35
  }
@@ -35,11 +39,14 @@ export const readClientRegistrationStatuses = async (home, currentCommandPath =
35
39
  }
36
40
  return statuses.sort((left, right) => left.client.localeCompare(right.client));
37
41
  };
38
- const registrationAligned = (command, currentCommandPath) => {
42
+ const registrationAligned = (registration, client, currentCommandPath) => {
43
+ const command = [registration.command, ...registration.args];
44
+ if (client === "codex" && registration.startup_timeout_sec !== 30)
45
+ return false;
39
46
  if (command.length === 4 &&
40
47
  command[0] === "npx" &&
41
48
  command[1] === "-y" &&
42
- command[2] === PRODUCT_IDENTITY.packageSpecifier &&
49
+ command[2] === PRODUCT_IDENTITY.registrationPackageSpecifier &&
43
50
  command[3] === "mcp")
44
51
  return true;
45
52
  return (command.length === 2 &&
@@ -0,0 +1,41 @@
1
+ import { z } from "zod";
2
+ import { AnalysisInputError, AnalysisProtocolError, } from "../domain/errors.js";
3
+ import { createEvidence, } from "../domain/evidence.js";
4
+ import { jsonValueSchema } from "../domain/jsonValue.js";
5
+ import { err, ok } from "../domain/result.js";
6
+ /** Parse one inventory projection and wrap its deterministic result in Evidence v2. */
7
+ export const projectInventoryEvidence = (options) => {
8
+ const parsed = options.schema.safeParse(options.rawInput);
9
+ if (!parsed.success)
10
+ return err(new AnalysisInputError(options.operation, { cause: parsed.error }));
11
+ try {
12
+ const result = options.project(parsed.data);
13
+ const first = parsed.data.inventory_evidence[0];
14
+ return ok(createEvidence(first?.subject === null || first?.subject === undefined
15
+ ? undefined
16
+ : {
17
+ path: first.subject.local_path,
18
+ sha256: result.root_sha256,
19
+ format: options.subjectFormat(first),
20
+ }, options.provider, {
21
+ predicateType: options.predicateType,
22
+ operation: options.operation,
23
+ parameters: {
24
+ inventory_evidence_ids: [...result.source_evidence_ids],
25
+ limits: jsonValueSchema.parse(parsed.data.limits),
26
+ },
27
+ result: jsonValueSchema.parse(result),
28
+ rawResult: null,
29
+ confidence: "inferred",
30
+ authority: "analyst-inference",
31
+ environment: null,
32
+ limitations: result.limitations,
33
+ evidenceLinks: result.source_evidence_ids,
34
+ }));
35
+ }
36
+ catch (cause) {
37
+ return err(cause instanceof TypeError || cause instanceof z.ZodError
38
+ ? new AnalysisInputError(options.operation, { cause })
39
+ : new AnalysisProtocolError(options.protocolError, { cause }));
40
+ }
41
+ };
@@ -27,6 +27,18 @@ export const MANAGED_WORKFLOW_PROVIDER = {
27
27
  name: "REA managed-code workflows",
28
28
  version: "1",
29
29
  };
30
+ /** Provider identity for deterministic Apple application projections. */
31
+ export const APPLE_APPLICATION_PROVIDER = {
32
+ id: "rea-apple-application",
33
+ name: "REA Apple application workflows",
34
+ version: "1",
35
+ };
36
+ /** Provider identity for deterministic Android application projections. */
37
+ export const ANDROID_APPLICATION_PROVIDER = {
38
+ id: "rea-android-application",
39
+ name: "REA Android application workflows",
40
+ version: "1",
41
+ };
30
42
  /** Provider identity for deterministic static JavaScript application analysis. */
31
43
  export const JAVASCRIPT_APPLICATION_PROVIDER = {
32
44
  id: "rea-javascript-application",
@@ -0,0 +1,72 @@
1
+ import { processCaptureCancelled, } from "./ProcessCaptureError.js";
2
+ import { captureProcessScenario } from "./ProcessHarness.js";
3
+ import { compareProcessCaptures } from "../domain/processComparison.js";
4
+ import { PROCESS_COMPARISON_DIMENSIONS } from "../domain/processComparison.js";
5
+ import { analyzeProcessRepeatability, bindProcessScenario, } from "../domain/processPairedExperiment.js";
6
+ import { err, ok } from "../domain/result.js";
7
+ const productionCapturePort = {
8
+ capture: captureProcessScenario,
9
+ };
10
+ const captureRepeats = async (input) => {
11
+ const captures = [];
12
+ for (let index = 0; index < input.repeatCount; index += 1) {
13
+ if (input.signal?.aborted === true)
14
+ return err(processCaptureCancelled());
15
+ const capture = await input.port.capture(input.scenario, input.policy, input.signal);
16
+ if (!capture.ok)
17
+ return err(capture.error);
18
+ captures.push(capture.value);
19
+ }
20
+ return ok(captures);
21
+ };
22
+ /** Execute repeatability-first authority/candidate captures under one contract. */
23
+ export const runPairedProcessExperiment = async (experiment, policy, options = {}) => {
24
+ const port = options.capturePort ?? productionCapturePort;
25
+ const authorityScenario = bindProcessScenario(experiment.shared_scenario, experiment.authority);
26
+ const candidateScenario = bindProcessScenario(experiment.shared_scenario, experiment.candidate);
27
+ const authorityRuns = await captureRepeats({
28
+ scenario: authorityScenario,
29
+ repeatCount: experiment.repeat_count,
30
+ policy,
31
+ port,
32
+ ...(options.signal === undefined ? {} : { signal: options.signal }),
33
+ });
34
+ if (!authorityRuns.ok)
35
+ return authorityRuns;
36
+ const candidateRuns = await captureRepeats({
37
+ scenario: candidateScenario,
38
+ repeatCount: experiment.repeat_count,
39
+ policy,
40
+ port,
41
+ ...(options.signal === undefined ? {} : { signal: options.signal }),
42
+ });
43
+ if (!candidateRuns.ok)
44
+ return candidateRuns;
45
+ const authorityRepeatability = analyzeProcessRepeatability(authorityRuns.value, experiment.required_dimensions);
46
+ const candidateRepeatability = analyzeProcessRepeatability(candidateRuns.value, experiment.required_dimensions);
47
+ const stable = authorityRepeatability.stable && candidateRepeatability.stable;
48
+ const allDimensionsRequired = PROCESS_COMPARISON_DIMENSIONS.every((dimension) => experiment.required_dimensions.includes(dimension));
49
+ const authorityLatest = authorityRuns.value.at(-1);
50
+ const candidateLatest = candidateRuns.value.at(-1);
51
+ const crossSide = stable &&
52
+ allDimensionsRequired &&
53
+ authorityLatest !== undefined &&
54
+ candidateLatest !== undefined
55
+ ? compareProcessCaptures(authorityLatest, candidateLatest, {
56
+ maxCaptureAgeMs: experiment.freshness_policy.max_capture_age_ms,
57
+ ...(options.now === undefined ? {} : { now: options.now }),
58
+ })
59
+ : null;
60
+ return ok({
61
+ authority_runs: authorityRuns.value,
62
+ candidate_runs: candidateRuns.value,
63
+ authority_repeatability: authorityRepeatability,
64
+ candidate_repeatability: candidateRepeatability,
65
+ cross_side: crossSide,
66
+ cross_side_blocked_reason: crossSide !== null
67
+ ? null
68
+ : !allDimensionsRequired
69
+ ? "Cross-side comparison is blocked until every returned dimension is required and repeatable."
70
+ : "Cross-side comparison is blocked because a required same-side dimension is unstable.",
71
+ });
72
+ };
@@ -15,7 +15,7 @@ export { configureJsonClient, configureTomlClient };
15
15
  import { setupInstallFailure, } from "./SetupInstallFailure.js";
16
16
  export { canonicalSkillNeedsInstall, installCanonicalSkill };
17
17
  const registrationCommand = () => process.env.npm_command === "exec"
18
- ? ["npx", "-y", PRODUCT_IDENTITY.packageSpecifier, "mcp"]
18
+ ? PRODUCT_IDENTITY.mcpCommand.split(" ")
19
19
  : [resolve(process.argv[1] ?? PRODUCT_IDENTITY.cliBinary), "mcp"];
20
20
  /** Discover, approve, and apply setup actions idempotently. */
21
21
  export const runSetup = async (options, host = systemSetupHost(), confirm) => {
@@ -47,7 +47,7 @@ export const runSetup = async (options, host = systemSetupHost(), confirm) => {
47
47
  plannedActions,
48
48
  appliedActions,
49
49
  clients,
50
- doctor: await host.doctor(),
50
+ doctor: discovery.initialDoctor,
51
51
  remediation: "Review the setup plan, then rerun interactively or with --yes.",
52
52
  };
53
53
  let detectedClients = selection.detectedClients;
@@ -110,7 +110,7 @@ const resolveSetupSelection = async (options, discovery, confirm) => {
110
110
  installSkill = plannedActions.some(({ kind }) => kind === "install_skill");
111
111
  detectedClients = detectedClients.filter((client) => selected.has(`configure_client:${client.name}`));
112
112
  interactiveApproval = decision.approved;
113
- approved = decision.approved || plannedActions.length === 0;
113
+ approved = decision.approved;
114
114
  }
115
115
  }
116
116
  return {
@@ -9,9 +9,10 @@ import { resolveClientConfigTransactionPath } from "./ClientConfigPath.js";
9
9
  const defaultCommand = () => [
10
10
  "npx",
11
11
  "-y",
12
- PRODUCT_IDENTITY.packageSpecifier,
12
+ PRODUCT_IDENTITY.registrationPackageSpecifier,
13
13
  "mcp",
14
14
  ];
15
+ const CODEX_STARTUP_TIMEOUT_SECONDS = 30;
15
16
  /** Back up, atomically update, and semantically read back one JSON MCP configuration. */
16
17
  export const configureJsonClient = async (client, environment = {}, command = defaultCommand()) => {
17
18
  const transactionPath = await resolveClientConfigTransactionPath(client.configPath);
@@ -34,7 +35,7 @@ export const configureJsonClient = async (client, environment = {}, command = de
34
35
  catch {
35
36
  return { status: "failed", reason: "readback" };
36
37
  }
37
- const desired = clientConfigurationDesired(normalizeProviderEnvironment(environment), command);
38
+ const desired = clientConfigurationDesired(client, normalizeProviderEnvironment(environment), command);
38
39
  if (sameConfiguration(servers[PRODUCT_IDENTITY.mcpServerKey], desired))
39
40
  return { status: "unchanged" };
40
41
  let backupPath;
@@ -95,7 +96,7 @@ export const configureTomlClient = async (client, environment = {}, command = de
95
96
  catch {
96
97
  return { status: "failed", reason: "readback" };
97
98
  }
98
- const desired = clientConfigurationDesired(normalizeProviderEnvironment(environment), command);
99
+ const desired = clientConfigurationDesired(client, normalizeProviderEnvironment(environment), command);
99
100
  if (sameConfiguration(servers[PRODUCT_IDENTITY.mcpServerKey], desired))
100
101
  return { status: "unchanged" };
101
102
  const backupPath = original === undefined ? undefined : `${client.configPath}.rea.backup`;
@@ -127,7 +128,7 @@ export const configureTomlClient = async (client, environment = {}, command = de
127
128
  };
128
129
  /** Determine whether an existing client configuration matches the desired registration. */
129
130
  export const clientConfigurationAligned = async (client, providerEnvironment, command) => {
130
- const desired = clientConfigurationDesired(providerEnvironment, command);
131
+ const desired = clientConfigurationDesired(client, providerEnvironment, command);
131
132
  try {
132
133
  const original = await readFile(client.configPath, "utf8");
133
134
  const document = client.format === "toml"
@@ -162,7 +163,7 @@ export const inspectClientConfiguration = async (client, providerEnvironment, co
162
163
  remediation: "The configuration file could not be read. Check its permissions before rerunning setup.",
163
164
  };
164
165
  }
165
- const desired = clientConfigurationDesired(providerEnvironment, command);
166
+ const desired = clientConfigurationDesired(client, providerEnvironment, command);
166
167
  try {
167
168
  const document = client.format === "toml"
168
169
  ? objectSchema.parse(parseToml(original))
@@ -207,11 +208,14 @@ const restoreConfig = async (path, original) => {
207
208
  // The backup remains available for the remediation reported by setup.
208
209
  }
209
210
  };
210
- const clientConfigurationDesired = (providerEnvironment, command) => {
211
+ const clientConfigurationDesired = (client, providerEnvironment, command) => {
211
212
  const environment = Object.fromEntries(Object.entries(providerEnvironment).sort(([left], [right]) => left.localeCompare(right)));
212
213
  return {
213
214
  command: command[0] ?? PRODUCT_IDENTITY.cliBinary,
214
215
  args: command.slice(1),
216
+ ...(client.name === "codex"
217
+ ? { startup_timeout_sec: CODEX_STARTUP_TIMEOUT_SECONDS }
218
+ : {}),
215
219
  ...(Object.keys(environment).length === 0 ? {} : { env: environment }),
216
220
  };
217
221
  };
@@ -48,8 +48,8 @@ const setupPlan = (input) => [
48
48
  id: "install_skill",
49
49
  kind: "install_skill",
50
50
  label: "REA reverse-engineering skill",
51
- target: join(homedir(), ".agents/skills", PRODUCT_IDENTITY.skillName, "SKILL.md"),
52
- detail: "Install or update the bundled REA reverse-engineering skill.",
51
+ target: join(homedir(), ".agents/skills", PRODUCT_IDENTITY.skillName),
52
+ detail: "Install or update the bundled REA reverse-engineering skill and on-demand references.",
53
53
  external: false,
54
54
  operation: "install",
55
55
  },
@@ -112,6 +112,7 @@ export const discoverSetupActions = async (input) => {
112
112
  : []);
113
113
  const linuxDistribution = host.platform === "linux" ? await host.linuxDistribution() : undefined;
114
114
  return {
115
+ initialDoctor,
115
116
  installHopper,
116
117
  installSkill,
117
118
  detectedClients: clientPlans.map(({ client }) => client),
@@ -2,7 +2,15 @@ import { mkdir, readFile, rm } from "node:fs/promises";
2
2
  import { dirname, join } from "node:path";
3
3
  import writeFileAtomic from "write-file-atomic";
4
4
  import { PRODUCT_IDENTITY } from "../identity.js";
5
- const skillDestination = (home, name) => join(home, ".agents/skills", name, "SKILL.md");
5
+ const SKILL_FILES = [
6
+ "SKILL.md",
7
+ "references/native-and-artifacts.md",
8
+ "references/javascript-applications.md",
9
+ "references/runtime-observation.md",
10
+ "references/evidence-workflows.md",
11
+ "references/controlled-replay.md",
12
+ ];
13
+ const skillRoot = (home) => join(home, ".agents/skills", PRODUCT_IDENTITY.skillName);
6
14
  const readOptionalText = async (path) => {
7
15
  try {
8
16
  return await readFile(path, "utf8");
@@ -13,62 +21,58 @@ const readOptionalText = async (path) => {
13
21
  throw cause;
14
22
  }
15
23
  };
16
- const canonicalSkillFiles = async (home) => ({
17
- destination: skillDestination(home, PRODUCT_IDENTITY.skillName),
18
- content: await readFile(new URL(`../../skills/${PRODUCT_IDENTITY.skillName}/SKILL.md`, import.meta.url), "utf8"),
19
- });
20
- /** Report whether setup would change the managed REA skill. */
24
+ const canonicalSkillFiles = async (home) => Promise.all(SKILL_FILES.map(async (relativePath) => {
25
+ const destination = join(skillRoot(home), relativePath);
26
+ return {
27
+ destination,
28
+ content: await readFile(new URL(`../../skills/${PRODUCT_IDENTITY.skillName}/${relativePath}`, import.meta.url), "utf8"),
29
+ original: await readOptionalText(destination),
30
+ };
31
+ }));
32
+ /** Report whether setup would change any file in the managed REA skill bundle. */
21
33
  export const canonicalSkillNeedsInstall = async (home) => {
22
34
  try {
23
- const { content, destination } = await canonicalSkillFiles(home);
24
- return (await readOptionalText(destination)) !== content;
35
+ return (await canonicalSkillFiles(home)).some(({ content, original }) => original !== content);
25
36
  }
26
37
  catch {
27
38
  return true;
28
39
  }
29
40
  };
30
- /** Transactionally install or upgrade the versioned canonical REA skill. */
41
+ const writeText = (path, content) => writeFileAtomic(path, content, { encoding: "utf8", mode: 0o600 });
42
+ const restoreSkillFiles = async (changed) => {
43
+ for (const { destination, original } of [...changed].reverse()) {
44
+ if (original === undefined)
45
+ await rm(destination, { force: true });
46
+ else
47
+ await writeText(destination, original);
48
+ }
49
+ };
50
+ /** Transactionally install or upgrade the canonical REA skill and references. */
31
51
  export const installCanonicalSkill = async (home) => {
32
- let destination = skillDestination(home, PRODUCT_IDENTITY.skillName);
33
- let backup = `${destination}.rea.backup`;
34
- let original;
35
- let canonicalChanged = false;
52
+ let changed = [];
36
53
  try {
37
54
  const canonical = await canonicalSkillFiles(home);
38
- destination = canonical.destination;
39
- backup = `${destination}.rea.backup`;
40
- const { content } = canonical;
41
- original = await readOptionalText(destination);
42
- if (original === content)
55
+ changed = canonical.filter(({ content, original }) => original !== content);
56
+ if (changed.length === 0)
43
57
  return "unchanged";
44
- await mkdir(dirname(destination), { recursive: true });
45
- if (original !== undefined && original !== content)
46
- await writeFileAtomic(backup, original, {
47
- encoding: "utf8",
48
- mode: 0o600,
49
- });
50
- canonicalChanged = original !== content;
51
- if (canonicalChanged)
52
- await writeFileAtomic(destination, content, {
53
- encoding: "utf8",
54
- mode: 0o600,
55
- });
56
- if ((await readFile(destination, "utf8")) !== content)
57
- throw new Error("skill readback mismatch");
58
+ for (const { destination, original } of changed) {
59
+ await mkdir(dirname(destination), { recursive: true });
60
+ if (original !== undefined)
61
+ await writeText(`${destination}.rea.backup`, original);
62
+ }
63
+ for (const { destination, content } of changed)
64
+ await writeText(destination, content);
65
+ for (const { destination, content } of changed)
66
+ if ((await readFile(destination, "utf8")) !== content)
67
+ throw new Error(`skill readback mismatch: ${destination}`);
58
68
  return "installed";
59
69
  }
60
70
  catch {
61
71
  try {
62
- if (canonicalChanged && original === undefined)
63
- await rm(destination, { force: true });
64
- else if (canonicalChanged && original !== undefined)
65
- await writeFileAtomic(destination, original, {
66
- encoding: "utf8",
67
- mode: 0o600,
68
- });
72
+ await restoreSkillFiles(changed);
69
73
  }
70
74
  catch {
71
- // The canonical backup remains beside the skill for operator recovery.
75
+ // Per-file backups remain beside changed files for operator recovery.
72
76
  }
73
77
  return "failed";
74
78
  }
@@ -128,7 +128,13 @@ const isOwnedRegistration = (value) => {
128
128
  args[0] === "mcp") ||
129
129
  (command === "npx" &&
130
130
  (JSON.stringify(args) ===
131
- JSON.stringify(["-y", PRODUCT_IDENTITY.packageSpecifier, "mcp"]) ||
131
+ JSON.stringify([
132
+ "-y",
133
+ PRODUCT_IDENTITY.registrationPackageSpecifier,
134
+ "mcp",
135
+ ]) ||
136
+ JSON.stringify(args) ===
137
+ JSON.stringify(["-y", PRODUCT_IDENTITY.packageSpecifier, "mcp"]) ||
132
138
  JSON.stringify(args) ===
133
139
  JSON.stringify(["-y", PRODUCT_IDENTITY.packageName, "mcp"]))));
134
140
  };
@@ -9,6 +9,7 @@ import { PRODUCT_IDENTITY } from "../identity.js";
9
9
  const execFileAsync = promisify(execFile);
10
10
  const registryResponseSchema = z.object({ version: z.string().min(1) });
11
11
  const UPGRADE_COMMAND = "npm install --global rea-agents@latest";
12
+ const CLI_SCRIPT_PATH = fileURLToPath(new URL("../../scripts/rea.mjs", import.meta.url));
12
13
  /** Check the npm registry and update the same global REA installation. */
13
14
  export const runUpgrade = async (currentVersion, host = systemUpgradeHost(), output = "human") => {
14
15
  const latestVersion = await host.latestVersion();
@@ -46,6 +47,12 @@ export const runUpgrade = async (currentVersion, host = systemUpgradeHost(), out
46
47
  reason: "install",
47
48
  remediation: `REA could not update through npm. Check npm registry access and global install permissions, then run: ${UPGRADE_COMMAND}`,
48
49
  };
50
+ const integrationSyncResult = await host.syncAgentIntegration(output);
51
+ const integrationSync = integrationSyncResult === undefined
52
+ ? "deferred"
53
+ : integrationSyncResult
54
+ ? "setup_invoked"
55
+ : "failed";
49
56
  return {
50
57
  status: "upgraded",
51
58
  previousVersion: currentVersion,
@@ -53,8 +60,11 @@ export const runUpgrade = async (currentVersion, host = systemUpgradeHost(), out
53
60
  versionCheck: "available",
54
61
  installMethod: "npm",
55
62
  command: UPGRADE_COMMAND,
63
+ integrationSync,
56
64
  clientRestartRequired: true,
57
- remediation: "Rerun rea setup to refresh registrations and the skill, then restart clients that may retain an older MCP server.",
65
+ remediation: integrationSync === "setup_invoked"
66
+ ? "The updated setup journey was opened to align registrations and the skill. Restart clients whose approved registration changed."
67
+ : "Run rea setup --all-detected to align registrations and the skill, then restart clients that may retain an older MCP server.",
58
68
  };
59
69
  };
60
70
  /** Create the npm registry and subprocess effects for a production upgrade. */
@@ -79,6 +89,11 @@ export const systemUpgradeHost = () => ({
79
89
  prefix,
80
90
  `${PRODUCT_IDENTITY.packageName}@latest`,
81
91
  ], output),
92
+ syncAgentIntegration: (output) => output === "structured" ||
93
+ process.stdin.isTTY !== true ||
94
+ process.stderr.isTTY !== true
95
+ ? Promise.resolve(undefined)
96
+ : runCommand(process.execPath, [CLI_SCRIPT_PATH, "setup", "--all-detected"], "human"),
82
97
  });
83
98
  const systemNpmInstallationHost = {
84
99
  canonicalPath: realpath,
@@ -39,7 +39,7 @@ export const registerSetupCommands = (cli, logger) => {
39
39
  skill: z
40
40
  .boolean()
41
41
  .optional()
42
- .describe("Install or skip the bundled REA skill"),
42
+ .describe("Override the bundled skill included with agent integrations"),
43
43
  dryRun: z
44
44
  .boolean()
45
45
  .default(false)
@@ -84,7 +84,8 @@ const runSetupCommand = async (input) => {
84
84
  process.stderr.isTTY === true;
85
85
  const hasExplicitScope = setupHasExplicitScope(options);
86
86
  if (options.yes && !hasExplicitScope)
87
- process.stderr.write("! Implicit `rea setup --yes` scope is deprecated; use `--all-detected --skill` to retain it.\n");
87
+ process.stderr.write("! Implicit `rea setup --yes` scope is deprecated; use `--all-detected` to retain it.\n");
88
+ const agentIntegrationSelected = options.allDetected || options.client.length > 0;
88
89
  const result = await runSetup({
89
90
  approved: options.yes && !options.dryRun,
90
91
  installHopper: options.installHopper,
@@ -93,7 +94,11 @@ const runSetupCommand = async (input) => {
93
94
  ...(!hasExplicitScope || options.allDetected
94
95
  ? {}
95
96
  : { clientIds: options.client }),
96
- ...(!hasExplicitScope ? {} : { installSkill: options.skill ?? false }),
97
+ ...(!hasExplicitScope
98
+ ? {}
99
+ : {
100
+ installSkill: options.skill ?? agentIntegrationSelected,
101
+ }),
97
102
  ...(interactive ? { onProgress: renderSetupProgress } : {}),
98
103
  }, systemSetupHost(createSystemDoctorHost()), interactive
99
104
  ? (actions) => confirmInteractiveSetup(actions, options.accessible)