@mindstudio-ai/remy 0.1.319 → 0.1.320

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.
@@ -0,0 +1,7 @@
1
+ ---
2
+ trigger: reviewExistingProject
3
+ ---
4
+
5
+ This is an automated message triggered by the user choosing "Bring an existing project" on the Remy start screen and uploading what they have. They can't see this message. The uploads are attached and already saved under `src/.user-uploads/` (paths are in the attachment header; a folder upload arrives as one zip). Repository URL, blank if none: {{repoUrl}}
6
+
7
+ Let the user know you're going to review their project, and then hand the upload paths (and the URL, if any) to `reviewExistingProject` first, before saying anything substantive, and do not open the archive yourself. Then run intake as usual on top of what comes back: tell the user in a few lines what you found and what you think they were building, and put the review's questions to them as a form. The prior attempt is evidence of intent, not a spec. Oftentimes, the user is uploading something that never worked or was half-finished (hence why they're here!). What actually gets built in this session is decided with the user, not inherited from the upload. Treat the results of the review as a *communication shortcut* to more quickly align with the user, not as a guide for "importing" their project!
package/dist/headless.js CHANGED
@@ -441,6 +441,13 @@ var MODEL_SURFACES = {
441
441
  modelType: "text",
442
442
  userPickable: true
443
443
  },
444
+ reviewExistingProject: {
445
+ default: "claude-5-sonnet",
446
+ label: "Existing Project Review",
447
+ description: "Reviews a project you bring from another tool and reports what is worth carrying forward.",
448
+ modelType: "text",
449
+ userPickable: true
450
+ },
444
451
  copyEditor: {
445
452
  default: "claude-5-sonnet",
446
453
  label: "Copy Agent",
@@ -1420,6 +1427,9 @@ async function listRecursive(dir) {
1420
1427
  for (const entry of entries) {
1421
1428
  const fullPath = path5.join(dir, entry.name);
1422
1429
  if (entry.isDirectory()) {
1430
+ if (entry.name.startsWith(".")) {
1431
+ continue;
1432
+ }
1423
1433
  results.push(`${fullPath}/`);
1424
1434
  results.push(...await listRecursive(fullPath));
1425
1435
  } else {
@@ -2646,8 +2656,8 @@ function formatSize(bytes) {
2646
2656
  }
2647
2657
  async function formatFile(dirPath, name, indent) {
2648
2658
  try {
2649
- const stat4 = await fs15.stat(path8.join(dirPath, name));
2650
- return `${indent}${name}${" ".repeat(Math.max(1, 30 - indent.length - name.length))}${formatSize(stat4.size)}`;
2659
+ const stat6 = await fs15.stat(path8.join(dirPath, name));
2660
+ return `${indent}${name}${" ".repeat(Math.max(1, 30 - indent.length - name.length))}${formatSize(stat6.size)}`;
2651
2661
  } catch {
2652
2662
  return `${indent}${name}`;
2653
2663
  }
@@ -3437,6 +3447,48 @@ ${partial}` : "[INTERRUPTED] Tool execution was stopped.";
3437
3447
  }
3438
3448
  };
3439
3449
 
3450
+ // src/recording.ts
3451
+ function liftRecording(result) {
3452
+ if (!result.includes('"recording"')) {
3453
+ return { result };
3454
+ }
3455
+ let parsed;
3456
+ try {
3457
+ parsed = JSON.parse(result);
3458
+ } catch {
3459
+ return { result };
3460
+ }
3461
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
3462
+ return { result };
3463
+ }
3464
+ const obj = parsed;
3465
+ const r = obj.recording;
3466
+ if (!r || typeof r !== "object") {
3467
+ return { result };
3468
+ }
3469
+ const ref = r;
3470
+ if (typeof ref.path !== "string" || typeof ref.sessionId !== "string" || typeof ref.runId !== "string" || typeof ref.seq !== "number") {
3471
+ return { result };
3472
+ }
3473
+ const recording = {
3474
+ path: ref.path,
3475
+ sessionId: ref.sessionId,
3476
+ runId: ref.runId,
3477
+ seq: ref.seq,
3478
+ containsSnapshot: ref.containsSnapshot === true,
3479
+ startTs: typeof ref.startTs === "number" ? ref.startTs : 0,
3480
+ endTs: typeof ref.endTs === "number" ? ref.endTs : 0,
3481
+ width: typeof ref.width === "number" ? ref.width : 0,
3482
+ height: typeof ref.height === "number" ? ref.height : 0
3483
+ };
3484
+ const rest = { ...obj };
3485
+ delete rest.recording;
3486
+ return {
3487
+ result: JSON.stringify({ ...rest, recorded: true }),
3488
+ recording
3489
+ };
3490
+ }
3491
+
3440
3492
  // src/historyLimits.ts
3441
3493
  var MAX_TOOL_RESULT_BYTES = 256 * 1024;
3442
3494
  function capToolResult(result, maxBytes = MAX_TOOL_RESULT_BYTES) {
@@ -3479,6 +3531,13 @@ function capMessageForHistory(msg, maxBytes = MAX_TOOL_RESULT_BYTES) {
3479
3531
  continue;
3480
3532
  }
3481
3533
  if (typeof block.result === "string") {
3534
+ if (block.name === "browserCommand" && !block.recording) {
3535
+ const lifted = liftRecording(block.result);
3536
+ if (lifted.recording) {
3537
+ block.result = lifted.result;
3538
+ block.recording = lifted.recording;
3539
+ }
3540
+ }
3482
3541
  block.result = capToolResult(block.result, maxBytes);
3483
3542
  }
3484
3543
  if (typeof block.backgroundResult === "string") {
@@ -4075,25 +4134,36 @@ ${partial}` : "[INTERRUPTED] Agent was interrupted before producing output.",
4075
4134
  }
4076
4135
  let settle;
4077
4136
  const resultPromise = new Promise((res) => {
4078
- settle = (result, isError) => res({ id: tc.id, result, isError });
4137
+ settle = (result, isError, recording) => res({
4138
+ id: tc.id,
4139
+ result,
4140
+ isError,
4141
+ ...recording ? { recording } : {}
4142
+ });
4079
4143
  });
4080
4144
  let toolAbort = new AbortController();
4081
4145
  const cascadeAbort = () => toolAbort.abort();
4082
4146
  signal?.addEventListener("abort", cascadeAbort, { once: true });
4083
4147
  let settled = false;
4084
- const safeSettle = (result, isError) => {
4148
+ const safeSettle = (result, isError, recording) => {
4085
4149
  if (settled) {
4086
4150
  return;
4087
4151
  }
4088
4152
  settled = true;
4089
4153
  signal?.removeEventListener("abort", cascadeAbort);
4090
- settle(result, isError);
4154
+ settle(result, isError, recording);
4091
4155
  };
4092
4156
  const run2 = async (input) => {
4093
4157
  try {
4094
4158
  let result;
4159
+ let recording;
4095
4160
  if (externalTools.has(tc.name) && resolveExternalTool) {
4096
4161
  result = await resolveExternalTool(tc.id, tc.name, input);
4162
+ if (tc.name === "browserCommand") {
4163
+ const lifted = liftRecording(result);
4164
+ result = lifted.result;
4165
+ recording = lifted.recording;
4166
+ }
4097
4167
  } else {
4098
4168
  const onLog = (line) => emit({
4099
4169
  type: "tool_input_delta",
@@ -4109,7 +4179,11 @@ ${partial}` : "[INTERRUPTED] Agent was interrupted before producing output.",
4109
4179
  subAgentMessages
4110
4180
  );
4111
4181
  }
4112
- safeSettle(capToolResult(result), result.startsWith("Error"));
4182
+ safeSettle(
4183
+ capToolResult(result),
4184
+ result.startsWith("Error"),
4185
+ recording
4186
+ );
4113
4187
  } catch (err) {
4114
4188
  safeSettle(`Error: ${err.message}`, true);
4115
4189
  }
@@ -4151,7 +4225,8 @@ ${partial}` : "[INTERRUPTED] Agent was interrupted before producing output.",
4151
4225
  id: tc.id,
4152
4226
  name: tc.name,
4153
4227
  result: r.result,
4154
- isError: r.isError
4228
+ isError: r.isError,
4229
+ ...r.recording ? { recording: r.recording } : {}
4155
4230
  });
4156
4231
  return r;
4157
4232
  })
@@ -4165,6 +4240,9 @@ ${partial}` : "[INTERRUPTED] Agent was interrupted before producing output.",
4165
4240
  block.result = r.result;
4166
4241
  block.isError = r.isError;
4167
4242
  block.completedAt = Date.now();
4243
+ if (r.recording) {
4244
+ block.recording = r.recording;
4245
+ }
4168
4246
  const innerMsgs = subAgentMessages.get(r.id);
4169
4247
  if (innerMsgs) {
4170
4248
  block.subAgentMessages = capSubAgentTranscript(innerMsgs);
@@ -4295,7 +4373,7 @@ var BROWSER_TOOLS = [
4295
4373
  },
4296
4374
  {
4297
4375
  name: "browserCommand",
4298
- description: "Interact with the app's live preview by sending browser commands. Commands execute sequentially with an animated cursor. Always start with a snapshot to see the current state and get ref identifiers. The result includes a snapshot field with the final page state after all steps complete. On error, the failing step has an error field and execution stops. Batches that contain an interactive step (click, type, select) also return a `recording` object \u2014 one chunk of a continuous per-session rrweb recording that the viewer stitches into a single replay (not a standalone per-call clip). Timeout: 120s.",
4376
+ description: "Interact with the app's live preview by sending browser commands. Commands execute sequentially with an animated cursor. Always start with a snapshot to see the current state and get ref identifiers. The result includes a snapshot field with the final page state after all steps complete. On error, the failing step has an error field and execution stops. Batches that contain an interactive step (click, type, select) are recorded for the user to replay and return `recorded: true`. Timeout: 120s.",
4299
4377
  inputSchema: {
4300
4378
  type: "object",
4301
4379
  properties: {
@@ -4413,7 +4491,7 @@ function walkMdFiles(dir, skip) {
4413
4491
  for (const entry of fs17.readdirSync(dir, { withFileTypes: true })) {
4414
4492
  const full = path9.join(dir, entry.name);
4415
4493
  if (entry.isDirectory()) {
4416
- if (!skip?.has(entry.name)) {
4494
+ if (!skip?.has(entry.name) && !entry.name.startsWith(".")) {
4417
4495
  files.push(...walkMdFiles(full, skip));
4418
4496
  }
4419
4497
  } else if (entry.name.endsWith(".md")) {
@@ -4711,8 +4789,14 @@ async function runBrowserAutomation(task, context, opts) {
4711
4789
  });
4712
4790
  context.subAgentMessages?.set(context.toolCallId, result.messages);
4713
4791
  const preferred = opts?.capture === "viewport" ? lastCapture.viewport ?? lastCapture.fullPage : lastCapture.fullPage ?? lastCapture.viewport;
4792
+ const recorded = result.messages.some(
4793
+ (m) => m.role === "assistant" && Array.isArray(m.content) && m.content.some(
4794
+ (b) => b.type === "tool" && b.name === "browserCommand" && !!b.recording
4795
+ )
4796
+ );
4714
4797
  return {
4715
4798
  text: result.text,
4799
+ recorded,
4716
4800
  ...preferred?.url ? { screenshot: preferred } : {}
4717
4801
  };
4718
4802
  } finally {
@@ -4739,12 +4823,18 @@ var browserAutomationTool = {
4739
4823
  return "Error: browser automation requires execution context (only available in headless mode)";
4740
4824
  }
4741
4825
  const result = await runBrowserAutomation(input.task, context);
4826
+ let text = result.text;
4742
4827
  if (result.screenshot) {
4743
- return `${result.text}
4828
+ text += `
4744
4829
 
4745
4830
  ![Final state](${result.screenshot.url})`;
4746
4831
  }
4747
- return result.text;
4832
+ if (result.recorded) {
4833
+ text += `
4834
+
4835
+ Replay of this run: ![Browser test replay](replay:${context.toolCallId})`;
4836
+ }
4837
+ return text;
4748
4838
  }
4749
4839
  };
4750
4840
 
@@ -7078,6 +7168,138 @@ After reconciling the spec, refresh the Build Overview: author the complete upda
7078
7168
  }
7079
7169
  };
7080
7170
 
7171
+ // src/subagents/reviewExistingProject/tools.ts
7172
+ var REVIEW_TOOLS = [
7173
+ ...COMMON_READ_TOOLS,
7174
+ bashTool.definition
7175
+ ];
7176
+
7177
+ // src/subagents/reviewExistingProject/validatePaths.ts
7178
+ import { stat as stat4 } from "fs/promises";
7179
+ import { join as join5 } from "path";
7180
+ var UPLOAD_REF_RE = /`(src\/\.user-uploads\/[^`\n]+?)\/?`/g;
7181
+ async function validateUploadPaths(text) {
7182
+ const refs = /* @__PURE__ */ new Set();
7183
+ for (const m of text.matchAll(UPLOAD_REF_RE)) {
7184
+ refs.add(m[1]);
7185
+ }
7186
+ if (refs.size === 0) {
7187
+ return null;
7188
+ }
7189
+ const missing = [];
7190
+ for (const ref of refs) {
7191
+ const exists = await stat4(join5(PROJECT_ROOT, ref)).then(
7192
+ () => true,
7193
+ () => false
7194
+ );
7195
+ if (!exists) {
7196
+ missing.push(ref);
7197
+ }
7198
+ }
7199
+ if (missing.length === 0) {
7200
+ return null;
7201
+ }
7202
+ return [
7203
+ `Your review cites paths under src/.user-uploads/ that do not exist on disk:`,
7204
+ ...missing.map((p) => `- ${p}`),
7205
+ ``,
7206
+ `Check the real location with listDir \u2014 the trimmed tree may have landed under a different name, or the original archive may already have been deleted \u2014 and correct or remove each path. Then send your complete review again from the top \u2014 it fully replaces your previous response, so include everything, not just the fixes.`
7207
+ ].join("\n");
7208
+ }
7209
+
7210
+ // src/subagents/reviewExistingProject/index.ts
7211
+ var BASE_PROMPT7 = readAsset("subagents/reviewExistingProject", "prompt.md");
7212
+ function buildTask(input) {
7213
+ const parts = [];
7214
+ const paths = (input.paths ?? []).filter(
7215
+ (p) => typeof p === "string" && p.trim()
7216
+ );
7217
+ if (paths.length > 0) {
7218
+ parts.push(
7219
+ `Uploads (local paths):
7220
+ ${paths.map((p) => `- ${p.trim()}`).join("\n")}`
7221
+ );
7222
+ }
7223
+ if (input.repoUrl?.trim()) {
7224
+ parts.push(`Repository URL: ${input.repoUrl.trim()}`);
7225
+ }
7226
+ if (input.context?.trim()) {
7227
+ parts.push(`Context from the conversation: ${input.context.trim()}`);
7228
+ }
7229
+ return parts.join("\n\n");
7230
+ }
7231
+ async function runReviewExistingProject(input, context) {
7232
+ const task = buildTask(input);
7233
+ if (!task) {
7234
+ return "Error: reviewExistingProject needs at least one upload path or a repository URL.";
7235
+ }
7236
+ const specIndex = loadSpecIndex();
7237
+ const parts = [BASE_PROMPT7, loadPlatformBrief()];
7238
+ parts.push("<!-- cache_breakpoint -->");
7239
+ if (specIndex) {
7240
+ parts.push(specIndex);
7241
+ }
7242
+ const system = parts.join("\n\n");
7243
+ const result = await runSubAgent({
7244
+ system,
7245
+ task,
7246
+ tools: REVIEW_TOOLS,
7247
+ externalTools: /* @__PURE__ */ new Set(),
7248
+ executeTool: (name, toolInput, toolCallId, onLog, sams) => {
7249
+ const childCtx = toolCallId ? {
7250
+ ...deriveContext(context, toolCallId, onLog),
7251
+ subAgentMessages: sams
7252
+ } : context;
7253
+ return executeTool(name, toolInput, childCtx);
7254
+ },
7255
+ apiConfig: context.apiConfig,
7256
+ model: resolveModel("reviewExistingProject", context.models, context.model),
7257
+ subAgentId: "reviewExistingProject",
7258
+ signal: context.signal,
7259
+ parentToolId: context.toolCallId,
7260
+ requestId: context.requestId,
7261
+ onEvent: context.onEvent,
7262
+ resolveExternalTool: context.resolveExternalTool,
7263
+ toolRegistry: context.toolRegistry,
7264
+ // Every quoted src/.user-uploads/ path in the review must exist on disk
7265
+ // (see validatePaths.ts) — the paths are the part the caller acts on.
7266
+ validateResult: (text) => validateUploadPaths(text)
7267
+ });
7268
+ context.subAgentMessages?.set(context.toolCallId, result.messages);
7269
+ return result.text;
7270
+ }
7271
+ var reviewExistingProjectTool = {
7272
+ definition: {
7273
+ name: "reviewExistingProject",
7274
+ description: "Your reviewer for anything a user brings from a prior/extant project or attempt at a project: a zip of a codebase from another coding agent, an export from a vibe coding platform, a folder of specs and docs, a public git URL. It unpacks the upload, strips the noise, lands the trimmed tree in src/.user-uploads/, and returns a short review: what the project was trying to be, the materials worth carrying forward with their paths, what to ignore, state signals, and the questions only the user can answer. Treat the review as evidence of what the user wants, never as a spec. Do not open the archive yourself.",
7275
+ inputSchema: {
7276
+ type: "object",
7277
+ properties: {
7278
+ paths: {
7279
+ type: "array",
7280
+ items: { type: "string" },
7281
+ description: "Local paths under src/.user-uploads/ to the uploaded archive(s) and/or loose files, exactly as given in the attachment header."
7282
+ },
7283
+ context: {
7284
+ type: "string",
7285
+ description: "One or two lines about the user and what they said they want. The reviewer cannot see the conversation."
7286
+ },
7287
+ repoUrl: {
7288
+ type: "string",
7289
+ description: "A public git repository URL to shallow-clone and review, instead of or alongside the uploads."
7290
+ }
7291
+ },
7292
+ required: []
7293
+ }
7294
+ },
7295
+ async execute(input, context) {
7296
+ if (!context) {
7297
+ return "Error: reviewExistingProject requires execution context";
7298
+ }
7299
+ return runReviewExistingProject(input, context);
7300
+ }
7301
+ };
7302
+
7081
7303
  // src/tools/common/scrapeWebUrl.ts
7082
7304
  var scrapeWebUrlTool = {
7083
7305
  definition: {
@@ -7172,7 +7394,8 @@ var ALL_TOOLS = [
7172
7394
  // new tool goes at the end to leave every existing session's prefix intact.
7173
7395
  loadSkillTool,
7174
7396
  testJewelTool,
7175
- researchTool
7397
+ researchTool,
7398
+ reviewExistingProjectTool
7176
7399
  ];
7177
7400
  var SUBAGENT_TOOL_NAMES = /* @__PURE__ */ new Set([
7178
7401
  "visualDesignExpert",
@@ -7182,7 +7405,8 @@ var SUBAGENT_TOOL_NAMES = /* @__PURE__ */ new Set([
7182
7405
  "specSync",
7183
7406
  "runAutomatedBrowserTest",
7184
7407
  "askMindStudioSdk",
7185
- "research"
7408
+ "research",
7409
+ "reviewExistingProject"
7186
7410
  ]);
7187
7411
  function getToolDefinitions() {
7188
7412
  return ALL_TOOLS.map((t) => t.definition);
@@ -8172,7 +8396,9 @@ function walkMdFiles2(dir) {
8172
8396
  for (const entry of entries) {
8173
8397
  const full = path12.join(dir, entry.name);
8174
8398
  if (entry.isDirectory()) {
8175
- results.push(...walkMdFiles2(full));
8399
+ if (!entry.name.startsWith(".")) {
8400
+ results.push(...walkMdFiles2(full));
8401
+ }
8176
8402
  } else if (entry.name.endsWith(".md")) {
8177
8403
  results.push(full);
8178
8404
  }
@@ -9572,11 +9798,15 @@ async function runTurn(params) {
9572
9798
  }
9573
9799
 
9574
9800
  // src/headless/attachments.ts
9575
- import { mkdirSync, existsSync } from "fs";
9576
- import { writeFile as writeFile3 } from "fs/promises";
9577
- import { basename as basename2, join as join5, extname as extname3 } from "path";
9801
+ import { mkdirSync, existsSync, createWriteStream } from "fs";
9802
+ import { writeFile as writeFile3, stat as stat5 } from "fs/promises";
9803
+ import { Readable } from "stream";
9804
+ import { pipeline } from "stream/promises";
9805
+ import { basename as basename2, join as join6, extname as extname3 } from "path";
9578
9806
  var log16 = createLogger("headless:attachments");
9579
9807
  var UPLOADS_DIR = "src/.user-uploads";
9808
+ var IMAGE_DOWNLOAD_TIMEOUT_MS = 3e4;
9809
+ var DOCUMENT_DOWNLOAD_TIMEOUT_MS = 3e5;
9580
9810
  function filenameFromUrl(url) {
9581
9811
  try {
9582
9812
  const pathname = new URL(url).pathname;
@@ -9587,7 +9817,7 @@ function filenameFromUrl(url) {
9587
9817
  }
9588
9818
  }
9589
9819
  function resolveUniqueFilename(name, claimed) {
9590
- const isFree = (candidate) => !claimed.has(candidate) && !existsSync(join5(UPLOADS_DIR, candidate));
9820
+ const isFree = (candidate) => !claimed.has(candidate) && !existsSync(join6(UPLOADS_DIR, candidate));
9591
9821
  if (isFree(name)) {
9592
9822
  return name;
9593
9823
  }
@@ -9622,19 +9852,23 @@ async function persistAttachmentList(attachments) {
9622
9852
  const results = await Promise.allSettled(
9623
9853
  nonVoice.map(async (att, i) => {
9624
9854
  const name = names[i];
9625
- const localPath = join5(UPLOADS_DIR, name);
9855
+ const localPath = join6(UPLOADS_DIR, name);
9856
+ const timeoutMs = isImageAttachment(att) ? IMAGE_DOWNLOAD_TIMEOUT_MS : DOCUMENT_DOWNLOAD_TIMEOUT_MS;
9626
9857
  const res = await fetch(att.url, {
9627
- signal: AbortSignal.timeout(3e4)
9858
+ signal: AbortSignal.timeout(timeoutMs)
9628
9859
  });
9629
- if (!res.ok) {
9860
+ if (!res.ok || !res.body) {
9630
9861
  throw new Error(`HTTP ${res.status} downloading ${att.url}`);
9631
9862
  }
9632
- const buffer = Buffer.from(await res.arrayBuffer());
9633
- await writeFile3(localPath, buffer);
9863
+ await pipeline(
9864
+ Readable.fromWeb(res.body),
9865
+ createWriteStream(localPath)
9866
+ );
9867
+ const { size } = await stat5(localPath);
9634
9868
  log16.info("Attachment saved", {
9635
9869
  filename: name,
9636
9870
  path: localPath,
9637
- bytes: buffer.length
9871
+ bytes: size
9638
9872
  });
9639
9873
  let extractedTextPath;
9640
9874
  if (att.extractedTextUrl) {
@@ -10505,6 +10739,7 @@ var HeadlessSession = class {
10505
10739
  name: e.name,
10506
10740
  result: e.result,
10507
10741
  isError: e.isError,
10742
+ ...e.recording && { recording: e.recording },
10508
10743
  ...e.parentToolId && { parentToolId: e.parentToolId }
10509
10744
  },
10510
10745
  rid
package/dist/index.js CHANGED
@@ -824,6 +824,9 @@ async function listRecursive(dir) {
824
824
  for (const entry of entries) {
825
825
  const fullPath = path2.join(dir, entry.name);
826
826
  if (entry.isDirectory()) {
827
+ if (entry.name.startsWith(".")) {
828
+ continue;
829
+ }
827
830
  results.push(`${fullPath}/`);
828
831
  results.push(...await listRecursive(fullPath));
829
832
  } else {
@@ -1947,7 +1950,7 @@ var init_compaction = __esm({
1947
1950
  init_api();
1948
1951
  init_assets();
1949
1952
  init_sentinel();
1950
- init_tools9();
1953
+ init_tools10();
1951
1954
  init_logger();
1952
1955
  init_usageLedger();
1953
1956
  log2 = createLogger("compaction");
@@ -2058,6 +2061,13 @@ var init_surfaces = __esm({
2058
2061
  modelType: "text",
2059
2062
  userPickable: true
2060
2063
  },
2064
+ reviewExistingProject: {
2065
+ default: "claude-5-sonnet",
2066
+ label: "Existing Project Review",
2067
+ description: "Reviews a project you bring from another tool and reports what is worth carrying forward.",
2068
+ modelType: "text",
2069
+ userPickable: true
2070
+ },
2061
2071
  copyEditor: {
2062
2072
  default: "claude-5-sonnet",
2063
2073
  label: "Copy Agent",
@@ -2332,6 +2342,53 @@ var init_cleanMessages = __esm({
2332
2342
  }
2333
2343
  });
2334
2344
 
2345
+ // src/recording.ts
2346
+ function liftRecording(result) {
2347
+ if (!result.includes('"recording"')) {
2348
+ return { result };
2349
+ }
2350
+ let parsed;
2351
+ try {
2352
+ parsed = JSON.parse(result);
2353
+ } catch {
2354
+ return { result };
2355
+ }
2356
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
2357
+ return { result };
2358
+ }
2359
+ const obj = parsed;
2360
+ const r = obj.recording;
2361
+ if (!r || typeof r !== "object") {
2362
+ return { result };
2363
+ }
2364
+ const ref = r;
2365
+ if (typeof ref.path !== "string" || typeof ref.sessionId !== "string" || typeof ref.runId !== "string" || typeof ref.seq !== "number") {
2366
+ return { result };
2367
+ }
2368
+ const recording = {
2369
+ path: ref.path,
2370
+ sessionId: ref.sessionId,
2371
+ runId: ref.runId,
2372
+ seq: ref.seq,
2373
+ containsSnapshot: ref.containsSnapshot === true,
2374
+ startTs: typeof ref.startTs === "number" ? ref.startTs : 0,
2375
+ endTs: typeof ref.endTs === "number" ? ref.endTs : 0,
2376
+ width: typeof ref.width === "number" ? ref.width : 0,
2377
+ height: typeof ref.height === "number" ? ref.height : 0
2378
+ };
2379
+ const rest = { ...obj };
2380
+ delete rest.recording;
2381
+ return {
2382
+ result: JSON.stringify({ ...rest, recorded: true }),
2383
+ recording
2384
+ };
2385
+ }
2386
+ var init_recording = __esm({
2387
+ "src/recording.ts"() {
2388
+ "use strict";
2389
+ }
2390
+ });
2391
+
2335
2392
  // src/historyLimits.ts
2336
2393
  function capToolResult(result, maxBytes = MAX_TOOL_RESULT_BYTES) {
2337
2394
  const total = Buffer.byteLength(result, "utf-8");
@@ -2371,6 +2428,13 @@ function capMessageForHistory(msg, maxBytes = MAX_TOOL_RESULT_BYTES) {
2371
2428
  continue;
2372
2429
  }
2373
2430
  if (typeof block.result === "string") {
2431
+ if (block.name === "browserCommand" && !block.recording) {
2432
+ const lifted = liftRecording(block.result);
2433
+ if (lifted.recording) {
2434
+ block.result = lifted.result;
2435
+ block.recording = lifted.recording;
2436
+ }
2437
+ }
2374
2438
  block.result = capToolResult(block.result, maxBytes);
2375
2439
  }
2376
2440
  if (typeof block.backgroundResult === "string") {
@@ -2391,6 +2455,7 @@ var MAX_TOOL_RESULT_BYTES, MAX_SUBAGENT_RESULT_BYTES, MAX_SUBAGENT_TRANSCRIPT_BY
2391
2455
  var init_historyLimits = __esm({
2392
2456
  "src/historyLimits.ts"() {
2393
2457
  "use strict";
2458
+ init_recording();
2394
2459
  MAX_TOOL_RESULT_BYTES = 256 * 1024;
2395
2460
  MAX_SUBAGENT_RESULT_BYTES = 32 * 1024;
2396
2461
  MAX_SUBAGENT_TRANSCRIPT_BYTES = 512 * 1024;
@@ -3788,8 +3853,8 @@ function formatSize(bytes) {
3788
3853
  }
3789
3854
  async function formatFile(dirPath, name, indent) {
3790
3855
  try {
3791
- const stat4 = await fs15.stat(path8.join(dirPath, name));
3792
- return `${indent}${name}${" ".repeat(Math.max(1, 30 - indent.length - name.length))}${formatSize(stat4.size)}`;
3856
+ const stat6 = await fs15.stat(path8.join(dirPath, name));
3857
+ return `${indent}${name}${" ".repeat(Math.max(1, 30 - indent.length - name.length))}${formatSize(stat6.size)}`;
3793
3858
  } catch {
3794
3859
  return `${indent}${name}`;
3795
3860
  }
@@ -5093,25 +5158,36 @@ ${partial}` : "[INTERRUPTED] Agent was interrupted before producing output.",
5093
5158
  }
5094
5159
  let settle;
5095
5160
  const resultPromise = new Promise((res) => {
5096
- settle = (result, isError) => res({ id: tc.id, result, isError });
5161
+ settle = (result, isError, recording) => res({
5162
+ id: tc.id,
5163
+ result,
5164
+ isError,
5165
+ ...recording ? { recording } : {}
5166
+ });
5097
5167
  });
5098
5168
  let toolAbort = new AbortController();
5099
5169
  const cascadeAbort = () => toolAbort.abort();
5100
5170
  signal?.addEventListener("abort", cascadeAbort, { once: true });
5101
5171
  let settled = false;
5102
- const safeSettle = (result, isError) => {
5172
+ const safeSettle = (result, isError, recording) => {
5103
5173
  if (settled) {
5104
5174
  return;
5105
5175
  }
5106
5176
  settled = true;
5107
5177
  signal?.removeEventListener("abort", cascadeAbort);
5108
- settle(result, isError);
5178
+ settle(result, isError, recording);
5109
5179
  };
5110
5180
  const run2 = async (input) => {
5111
5181
  try {
5112
5182
  let result;
5183
+ let recording;
5113
5184
  if (externalTools.has(tc.name) && resolveExternalTool) {
5114
5185
  result = await resolveExternalTool(tc.id, tc.name, input);
5186
+ if (tc.name === "browserCommand") {
5187
+ const lifted = liftRecording(result);
5188
+ result = lifted.result;
5189
+ recording = lifted.recording;
5190
+ }
5115
5191
  } else {
5116
5192
  const onLog = (line) => emit({
5117
5193
  type: "tool_input_delta",
@@ -5127,7 +5203,11 @@ ${partial}` : "[INTERRUPTED] Agent was interrupted before producing output.",
5127
5203
  subAgentMessages
5128
5204
  );
5129
5205
  }
5130
- safeSettle(capToolResult(result), result.startsWith("Error"));
5206
+ safeSettle(
5207
+ capToolResult(result),
5208
+ result.startsWith("Error"),
5209
+ recording
5210
+ );
5131
5211
  } catch (err) {
5132
5212
  safeSettle(`Error: ${err.message}`, true);
5133
5213
  }
@@ -5169,7 +5249,8 @@ ${partial}` : "[INTERRUPTED] Agent was interrupted before producing output.",
5169
5249
  id: tc.id,
5170
5250
  name: tc.name,
5171
5251
  result: r.result,
5172
- isError: r.isError
5252
+ isError: r.isError,
5253
+ ...r.recording ? { recording: r.recording } : {}
5173
5254
  });
5174
5255
  return r;
5175
5256
  })
@@ -5183,6 +5264,9 @@ ${partial}` : "[INTERRUPTED] Agent was interrupted before producing output.",
5183
5264
  block.result = r.result;
5184
5265
  block.isError = r.isError;
5185
5266
  block.completedAt = Date.now();
5267
+ if (r.recording) {
5268
+ block.recording = r.recording;
5269
+ }
5186
5270
  const innerMsgs = subAgentMessages.get(r.id);
5187
5271
  if (innerMsgs) {
5188
5272
  block.subAgentMessages = capSubAgentTranscript(innerMsgs);
@@ -5274,6 +5358,7 @@ var init_runner = __esm({
5274
5358
  init_usageLedger();
5275
5359
  init_toolRegistry();
5276
5360
  init_historyLimits();
5361
+ init_recording();
5277
5362
  init_statusWatcher();
5278
5363
  init_cleanMessages();
5279
5364
  log8 = createLogger("sub-agent");
@@ -5343,7 +5428,7 @@ var init_tools2 = __esm({
5343
5428
  },
5344
5429
  {
5345
5430
  name: "browserCommand",
5346
- description: "Interact with the app's live preview by sending browser commands. Commands execute sequentially with an animated cursor. Always start with a snapshot to see the current state and get ref identifiers. The result includes a snapshot field with the final page state after all steps complete. On error, the failing step has an error field and execution stops. Batches that contain an interactive step (click, type, select) also return a `recording` object \u2014 one chunk of a continuous per-session rrweb recording that the viewer stitches into a single replay (not a standalone per-call clip). Timeout: 120s.",
5431
+ description: "Interact with the app's live preview by sending browser commands. Commands execute sequentially with an animated cursor. Always start with a snapshot to see the current state and get ref identifiers. The result includes a snapshot field with the final page state after all steps complete. On error, the failing step has an error field and execution stops. Batches that contain an interactive step (click, type, select) are recorded for the user to replay and return `recorded: true`. Timeout: 120s.",
5347
5432
  inputSchema: {
5348
5433
  type: "object",
5349
5434
  properties: {
@@ -5463,7 +5548,7 @@ function walkMdFiles(dir, skip) {
5463
5548
  for (const entry of fs16.readdirSync(dir, { withFileTypes: true })) {
5464
5549
  const full = path9.join(dir, entry.name);
5465
5550
  if (entry.isDirectory()) {
5466
- if (!skip?.has(entry.name)) {
5551
+ if (!skip?.has(entry.name) && !entry.name.startsWith(".")) {
5467
5552
  files.push(...walkMdFiles(full, skip));
5468
5553
  }
5469
5554
  } else if (entry.name.endsWith(".md")) {
@@ -5772,8 +5857,14 @@ async function runBrowserAutomation(task, context, opts) {
5772
5857
  });
5773
5858
  context.subAgentMessages?.set(context.toolCallId, result.messages);
5774
5859
  const preferred = opts?.capture === "viewport" ? lastCapture.viewport ?? lastCapture.fullPage : lastCapture.fullPage ?? lastCapture.viewport;
5860
+ const recorded = result.messages.some(
5861
+ (m) => m.role === "assistant" && Array.isArray(m.content) && m.content.some(
5862
+ (b) => b.type === "tool" && b.name === "browserCommand" && !!b.recording
5863
+ )
5864
+ );
5775
5865
  return {
5776
5866
  text: result.text,
5867
+ recorded,
5777
5868
  ...preferred?.url ? { screenshot: preferred } : {}
5778
5869
  };
5779
5870
  } finally {
@@ -5784,7 +5875,7 @@ var log9, CAPTURE_COMMANDS, browserAutomationTool;
5784
5875
  var init_browserAutomation = __esm({
5785
5876
  "src/subagents/browserAutomation/index.ts"() {
5786
5877
  "use strict";
5787
- init_tools9();
5878
+ init_tools10();
5788
5879
  init_runner();
5789
5880
  init_tools2();
5790
5881
  init_tools();
@@ -5818,12 +5909,18 @@ var init_browserAutomation = __esm({
5818
5909
  return "Error: browser automation requires execution context (only available in headless mode)";
5819
5910
  }
5820
5911
  const result = await runBrowserAutomation(input.task, context);
5912
+ let text = result.text;
5821
5913
  if (result.screenshot) {
5822
- return `${result.text}
5914
+ text += `
5823
5915
 
5824
5916
  ![Final state](${result.screenshot.url})`;
5825
5917
  }
5826
- return result.text;
5918
+ if (result.recorded) {
5919
+ text += `
5920
+
5921
+ Replay of this run: ![Browser test replay](replay:${context.toolCallId})`;
5922
+ }
5923
+ return text;
5827
5924
  }
5828
5925
  };
5829
5926
  }
@@ -6082,7 +6179,7 @@ var init_research = __esm({
6082
6179
  init_assets();
6083
6180
  init_runner();
6084
6181
  init_context();
6085
- init_tools9();
6182
+ init_tools10();
6086
6183
  init_tools3();
6087
6184
  init_surfaces();
6088
6185
  BASE_PROMPT2 = readAsset("subagents/research", "prompt.md");
@@ -6950,7 +7047,7 @@ var init_copyEditor = __esm({
6950
7047
  init_assets();
6951
7048
  init_runner();
6952
7049
  init_context();
6953
- init_tools9();
7050
+ init_tools10();
6954
7051
  init_tools4();
6955
7052
  init_surfaces();
6956
7053
  BASE_PROMPT3 = readAsset("subagents/copyEditor", "prompt.md");
@@ -7280,7 +7377,7 @@ var research, tools, DESIGN_EXPERT_TOOLS;
7280
7377
  var init_tools5 = __esm({
7281
7378
  "src/subagents/designExpert/tools/index.ts"() {
7282
7379
  "use strict";
7283
- init_tools9();
7380
+ init_tools10();
7284
7381
  init_tools();
7285
7382
  init_research();
7286
7383
  init_scrapeWebUrl();
@@ -7809,7 +7906,7 @@ var DESCRIPTION, RENDER_WRITE_TOOL_NAMES, DESIGN_EXPERT_RENDER_TOOLS, designExpe
7809
7906
  var init_designExpert = __esm({
7810
7907
  "src/subagents/designExpert/index.ts"() {
7811
7908
  "use strict";
7812
- init_tools9();
7909
+ init_tools10();
7813
7910
  init_runner();
7814
7911
  init_tools5();
7815
7912
  init_tools();
@@ -8062,7 +8159,7 @@ var productVisionTool;
8062
8159
  var init_productVision = __esm({
8063
8160
  "src/subagents/productVision/index.ts"() {
8064
8161
  "use strict";
8065
- init_tools9();
8162
+ init_tools10();
8066
8163
  init_runner();
8067
8164
  init_tools6();
8068
8165
  init_tools();
@@ -8217,7 +8314,7 @@ var init_codeSanityCheck = __esm({
8217
8314
  init_assets();
8218
8315
  init_runner();
8219
8316
  init_context();
8220
- init_tools9();
8317
+ init_tools10();
8221
8318
  init_tools7();
8222
8319
  init_research();
8223
8320
  init_tools3();
@@ -8454,7 +8551,7 @@ var init_specSync = __esm({
8454
8551
  init_assets();
8455
8552
  init_runner();
8456
8553
  init_context();
8457
- init_tools9();
8554
+ init_tools10();
8458
8555
  init_writeBuildOverview();
8459
8556
  init_tools8();
8460
8557
  init_lock();
@@ -8560,6 +8657,166 @@ After reconciling the spec, refresh the Build Overview: author the complete upda
8560
8657
  }
8561
8658
  });
8562
8659
 
8660
+ // src/subagents/reviewExistingProject/tools.ts
8661
+ var REVIEW_TOOLS;
8662
+ var init_tools9 = __esm({
8663
+ "src/subagents/reviewExistingProject/tools.ts"() {
8664
+ "use strict";
8665
+ init_tools();
8666
+ init_bash();
8667
+ REVIEW_TOOLS = [
8668
+ ...COMMON_READ_TOOLS,
8669
+ bashTool.definition
8670
+ ];
8671
+ }
8672
+ });
8673
+
8674
+ // src/subagents/reviewExistingProject/validatePaths.ts
8675
+ import { stat as stat4 } from "fs/promises";
8676
+ import { join as join5 } from "path";
8677
+ async function validateUploadPaths(text) {
8678
+ const refs = /* @__PURE__ */ new Set();
8679
+ for (const m of text.matchAll(UPLOAD_REF_RE)) {
8680
+ refs.add(m[1]);
8681
+ }
8682
+ if (refs.size === 0) {
8683
+ return null;
8684
+ }
8685
+ const missing = [];
8686
+ for (const ref of refs) {
8687
+ const exists = await stat4(join5(PROJECT_ROOT, ref)).then(
8688
+ () => true,
8689
+ () => false
8690
+ );
8691
+ if (!exists) {
8692
+ missing.push(ref);
8693
+ }
8694
+ }
8695
+ if (missing.length === 0) {
8696
+ return null;
8697
+ }
8698
+ return [
8699
+ `Your review cites paths under src/.user-uploads/ that do not exist on disk:`,
8700
+ ...missing.map((p) => `- ${p}`),
8701
+ ``,
8702
+ `Check the real location with listDir \u2014 the trimmed tree may have landed under a different name, or the original archive may already have been deleted \u2014 and correct or remove each path. Then send your complete review again from the top \u2014 it fully replaces your previous response, so include everything, not just the fixes.`
8703
+ ].join("\n");
8704
+ }
8705
+ var UPLOAD_REF_RE;
8706
+ var init_validatePaths = __esm({
8707
+ "src/subagents/reviewExistingProject/validatePaths.ts"() {
8708
+ "use strict";
8709
+ init_projectRoot();
8710
+ UPLOAD_REF_RE = /`(src\/\.user-uploads\/[^`\n]+?)\/?`/g;
8711
+ }
8712
+ });
8713
+
8714
+ // src/subagents/reviewExistingProject/index.ts
8715
+ function buildTask(input) {
8716
+ const parts = [];
8717
+ const paths = (input.paths ?? []).filter(
8718
+ (p) => typeof p === "string" && p.trim()
8719
+ );
8720
+ if (paths.length > 0) {
8721
+ parts.push(
8722
+ `Uploads (local paths):
8723
+ ${paths.map((p) => `- ${p.trim()}`).join("\n")}`
8724
+ );
8725
+ }
8726
+ if (input.repoUrl?.trim()) {
8727
+ parts.push(`Repository URL: ${input.repoUrl.trim()}`);
8728
+ }
8729
+ if (input.context?.trim()) {
8730
+ parts.push(`Context from the conversation: ${input.context.trim()}`);
8731
+ }
8732
+ return parts.join("\n\n");
8733
+ }
8734
+ async function runReviewExistingProject(input, context) {
8735
+ const task = buildTask(input);
8736
+ if (!task) {
8737
+ return "Error: reviewExistingProject needs at least one upload path or a repository URL.";
8738
+ }
8739
+ const specIndex = loadSpecIndex();
8740
+ const parts = [BASE_PROMPT7, loadPlatformBrief()];
8741
+ parts.push("<!-- cache_breakpoint -->");
8742
+ if (specIndex) {
8743
+ parts.push(specIndex);
8744
+ }
8745
+ const system = parts.join("\n\n");
8746
+ const result = await runSubAgent({
8747
+ system,
8748
+ task,
8749
+ tools: REVIEW_TOOLS,
8750
+ externalTools: /* @__PURE__ */ new Set(),
8751
+ executeTool: (name, toolInput, toolCallId, onLog, sams) => {
8752
+ const childCtx = toolCallId ? {
8753
+ ...deriveContext(context, toolCallId, onLog),
8754
+ subAgentMessages: sams
8755
+ } : context;
8756
+ return executeTool(name, toolInput, childCtx);
8757
+ },
8758
+ apiConfig: context.apiConfig,
8759
+ model: resolveModel("reviewExistingProject", context.models, context.model),
8760
+ subAgentId: "reviewExistingProject",
8761
+ signal: context.signal,
8762
+ parentToolId: context.toolCallId,
8763
+ requestId: context.requestId,
8764
+ onEvent: context.onEvent,
8765
+ resolveExternalTool: context.resolveExternalTool,
8766
+ toolRegistry: context.toolRegistry,
8767
+ // Every quoted src/.user-uploads/ path in the review must exist on disk
8768
+ // (see validatePaths.ts) — the paths are the part the caller acts on.
8769
+ validateResult: (text) => validateUploadPaths(text)
8770
+ });
8771
+ context.subAgentMessages?.set(context.toolCallId, result.messages);
8772
+ return result.text;
8773
+ }
8774
+ var BASE_PROMPT7, reviewExistingProjectTool;
8775
+ var init_reviewExistingProject = __esm({
8776
+ "src/subagents/reviewExistingProject/index.ts"() {
8777
+ "use strict";
8778
+ init_assets();
8779
+ init_runner();
8780
+ init_context();
8781
+ init_tools10();
8782
+ init_tools9();
8783
+ init_validatePaths();
8784
+ init_surfaces();
8785
+ BASE_PROMPT7 = readAsset("subagents/reviewExistingProject", "prompt.md");
8786
+ reviewExistingProjectTool = {
8787
+ definition: {
8788
+ name: "reviewExistingProject",
8789
+ description: "Your reviewer for anything a user brings from a prior/extant project or attempt at a project: a zip of a codebase from another coding agent, an export from a vibe coding platform, a folder of specs and docs, a public git URL. It unpacks the upload, strips the noise, lands the trimmed tree in src/.user-uploads/, and returns a short review: what the project was trying to be, the materials worth carrying forward with their paths, what to ignore, state signals, and the questions only the user can answer. Treat the review as evidence of what the user wants, never as a spec. Do not open the archive yourself.",
8790
+ inputSchema: {
8791
+ type: "object",
8792
+ properties: {
8793
+ paths: {
8794
+ type: "array",
8795
+ items: { type: "string" },
8796
+ description: "Local paths under src/.user-uploads/ to the uploaded archive(s) and/or loose files, exactly as given in the attachment header."
8797
+ },
8798
+ context: {
8799
+ type: "string",
8800
+ description: "One or two lines about the user and what they said they want. The reviewer cannot see the conversation."
8801
+ },
8802
+ repoUrl: {
8803
+ type: "string",
8804
+ description: "A public git repository URL to shallow-clone and review, instead of or alongside the uploads."
8805
+ }
8806
+ },
8807
+ required: []
8808
+ }
8809
+ },
8810
+ async execute(input, context) {
8811
+ if (!context) {
8812
+ return "Error: reviewExistingProject requires execution context";
8813
+ }
8814
+ return runReviewExistingProject(input, context);
8815
+ }
8816
+ };
8817
+ }
8818
+ });
8819
+
8563
8820
  // src/tools/common/scrapeWebUrl.ts
8564
8821
  var scrapeWebUrlTool;
8565
8822
  var init_scrapeWebUrl2 = __esm({
@@ -8630,7 +8887,7 @@ function executeTool(name, input, context) {
8630
8887
  return tool.execute(input, context);
8631
8888
  }
8632
8889
  var ALL_TOOLS, SUBAGENT_TOOL_NAMES;
8633
- var init_tools9 = __esm({
8890
+ var init_tools10 = __esm({
8634
8891
  "src/tools/index.ts"() {
8635
8892
  "use strict";
8636
8893
  init_readSpec();
@@ -8669,6 +8926,7 @@ var init_tools9 = __esm({
8669
8926
  init_copyEditor();
8670
8927
  init_specSync();
8671
8928
  init_research();
8929
+ init_reviewExistingProject();
8672
8930
  init_scrapeWebUrl2();
8673
8931
  init_writeBuildOverview();
8674
8932
  ALL_TOOLS = [
@@ -8717,7 +8975,8 @@ var init_tools9 = __esm({
8717
8975
  // new tool goes at the end to leave every existing session's prefix intact.
8718
8976
  loadSkillTool,
8719
8977
  testJewelTool,
8720
- researchTool
8978
+ researchTool,
8979
+ reviewExistingProjectTool
8721
8980
  ];
8722
8981
  SUBAGENT_TOOL_NAMES = /* @__PURE__ */ new Set([
8723
8982
  "visualDesignExpert",
@@ -8727,7 +8986,8 @@ var init_tools9 = __esm({
8727
8986
  "specSync",
8728
8987
  "runAutomatedBrowserTest",
8729
8988
  "askMindStudioSdk",
8730
- "research"
8989
+ "research",
8990
+ "reviewExistingProject"
8731
8991
  ]);
8732
8992
  }
8733
8993
  });
@@ -9244,7 +9504,9 @@ function walkMdFiles2(dir) {
9244
9504
  for (const entry of entries) {
9245
9505
  const full = path11.join(dir, entry.name);
9246
9506
  if (entry.isDirectory()) {
9247
- results.push(...walkMdFiles2(full));
9507
+ if (!entry.name.startsWith(".")) {
9508
+ results.push(...walkMdFiles2(full));
9509
+ }
9248
9510
  } else if (entry.name.endsWith(".md")) {
9249
9511
  results.push(full);
9250
9512
  }
@@ -10239,7 +10501,7 @@ var init_agent = __esm({
10239
10501
  "src/agent.ts"() {
10240
10502
  "use strict";
10241
10503
  init_api();
10242
- init_tools9();
10504
+ init_tools10();
10243
10505
  init_session();
10244
10506
  init_logger();
10245
10507
  init_usageLedger();
@@ -10526,9 +10788,11 @@ var init_config = __esm({
10526
10788
  });
10527
10789
 
10528
10790
  // src/headless/attachments.ts
10529
- import { mkdirSync, existsSync } from "fs";
10530
- import { writeFile as writeFile3 } from "fs/promises";
10531
- import { basename as basename2, join as join5, extname as extname3 } from "path";
10791
+ import { mkdirSync, existsSync, createWriteStream } from "fs";
10792
+ import { writeFile as writeFile3, stat as stat5 } from "fs/promises";
10793
+ import { Readable } from "stream";
10794
+ import { pipeline } from "stream/promises";
10795
+ import { basename as basename2, join as join6, extname as extname3 } from "path";
10532
10796
  function filenameFromUrl(url) {
10533
10797
  try {
10534
10798
  const pathname = new URL(url).pathname;
@@ -10539,7 +10803,7 @@ function filenameFromUrl(url) {
10539
10803
  }
10540
10804
  }
10541
10805
  function resolveUniqueFilename(name, claimed) {
10542
- const isFree = (candidate) => !claimed.has(candidate) && !existsSync(join5(UPLOADS_DIR, candidate));
10806
+ const isFree = (candidate) => !claimed.has(candidate) && !existsSync(join6(UPLOADS_DIR, candidate));
10543
10807
  if (isFree(name)) {
10544
10808
  return name;
10545
10809
  }
@@ -10573,19 +10837,23 @@ async function persistAttachmentList(attachments) {
10573
10837
  const results = await Promise.allSettled(
10574
10838
  nonVoice.map(async (att, i) => {
10575
10839
  const name = names[i];
10576
- const localPath = join5(UPLOADS_DIR, name);
10840
+ const localPath = join6(UPLOADS_DIR, name);
10841
+ const timeoutMs = isImageAttachment(att) ? IMAGE_DOWNLOAD_TIMEOUT_MS : DOCUMENT_DOWNLOAD_TIMEOUT_MS;
10577
10842
  const res = await fetch(att.url, {
10578
- signal: AbortSignal.timeout(3e4)
10843
+ signal: AbortSignal.timeout(timeoutMs)
10579
10844
  });
10580
- if (!res.ok) {
10845
+ if (!res.ok || !res.body) {
10581
10846
  throw new Error(`HTTP ${res.status} downloading ${att.url}`);
10582
10847
  }
10583
- const buffer = Buffer.from(await res.arrayBuffer());
10584
- await writeFile3(localPath, buffer);
10848
+ await pipeline(
10849
+ Readable.fromWeb(res.body),
10850
+ createWriteStream(localPath)
10851
+ );
10852
+ const { size } = await stat5(localPath);
10585
10853
  log16.info("Attachment saved", {
10586
10854
  filename: name,
10587
10855
  path: localPath,
10588
- bytes: buffer.length
10856
+ bytes: size
10589
10857
  });
10590
10858
  let extractedTextPath;
10591
10859
  if (att.extractedTextUrl) {
@@ -10645,13 +10913,15 @@ function buildUploadHeader(documents, images) {
10645
10913
  return `[Uploaded files]
10646
10914
  ${lines.join("\n")}`;
10647
10915
  }
10648
- var log16, UPLOADS_DIR, IMAGE_EXTENSIONS;
10916
+ var log16, UPLOADS_DIR, IMAGE_DOWNLOAD_TIMEOUT_MS, DOCUMENT_DOWNLOAD_TIMEOUT_MS, IMAGE_EXTENSIONS;
10649
10917
  var init_attachments = __esm({
10650
10918
  "src/headless/attachments.ts"() {
10651
10919
  "use strict";
10652
10920
  init_logger();
10653
10921
  log16 = createLogger("headless:attachments");
10654
10922
  UPLOADS_DIR = "src/.user-uploads";
10923
+ IMAGE_DOWNLOAD_TIMEOUT_MS = 3e4;
10924
+ DOCUMENT_DOWNLOAD_TIMEOUT_MS = 3e5;
10655
10925
  IMAGE_EXTENSIONS = /* @__PURE__ */ new Set([".png", ".jpg", ".jpeg", ".gif", ".webp"]);
10656
10926
  }
10657
10927
  });
@@ -10955,7 +11225,7 @@ var init_headless = __esm({
10955
11225
  init_attachments();
10956
11226
  init_planFile();
10957
11227
  init_stats();
10958
- init_tools9();
11228
+ init_tools10();
10959
11229
  init_messageQueue();
10960
11230
  init_resolve();
10961
11231
  init_sentinel();
@@ -11513,6 +11783,7 @@ var init_headless = __esm({
11513
11783
  name: e.name,
11514
11784
  result: e.result,
11515
11785
  isError: e.isError,
11786
+ ...e.recording && { recording: e.recording },
11516
11787
  ...e.parentToolId && { parentToolId: e.parentToolId }
11517
11788
  },
11518
11789
  rid
@@ -55,7 +55,7 @@ But know when to stop exploring. Once there's a clear concept with a specific au
55
55
  2. **Structured forms** — Use `promptUser` with `type: "form"` to collect details. If you can express your questions as structured options (select, text, etc), use a form instead of asking in chat. Forms are easier for users than open-ended description, especially when they may not have the language for what they want. Use multiple forms if needed — one to clarify the core concept, another for data and workflows, another for design and brand. Each form should build on what you've already learned. Always use `type: "form"` during intake.
56
56
  3. **Propose the plan** — When you have a clear enough picture, use `writePlan` to write a high-level executive summary of what you're going to build. This is not a technical spec or a file list. Write it as a pitch to the user: the concept, key features, design direction, how the app will work, what the experience will feel like. The user can approve, discuss, or reject. Do NOT start writing spec files or code until the plan is approved.
57
57
 
58
- During intake, the only tools you should use are `promptUser` (for forms) and `writePlan` (to propose the plan). Do not call sub-agents, write files, or do any other work. Intake is about understanding the user and proposing what to build. Everything else happens after the plan is approved.
58
+ During intake, the only tools you should use are `promptUser` (for forms), `writePlan` (to propose the plan), and `reviewExistingProject` when the user has brought an existing project. Do not call other sub-agents, write files, or do any other work. Intake is about understanding the user and proposing what to build. Everything else happens after the plan is approved.
59
59
 
60
60
  ### What NOT to Do
61
61
 
@@ -44,6 +44,12 @@ Send it anything where the answer lives outside this project — objective quest
44
44
 
45
45
  Brief it neutrally: state the question and any concrete context, do not lead it by hinting at the answer you expect. It is tasked with testing your assumptions, and it will tell you when the evidence says you're wrong; that report is the valuable one, so don't tilt it. You still have `scrapeWebUrl` for directly reading a URL the user gives you — that's fetching, not research.
46
46
 
47
+ ### Existing Project Review (`reviewExistingProject`)
48
+
49
+ Your reviewer for anything a user brings from a prior/extant project or attempt at a project: a zip of a codebase from another coding agent, an export from a vibe coding platform, a folder of specs and docs, a public git URL. It unpacks the upload itself in `src/.user-uploads/`, strips the noise, and returns a short review: what the project was trying to be, the materials worth carrying forward with their paths, what to ignore, state signals, and the questions only the user can answer.
50
+
51
+ Send it the raw upload paths and a line of context about the user. Treat the review as evidence about what the user wants, the same as a deck or a transcript, never as a spec. Its questions belong in your intake form. It is the one sub-agent you may call during intake, because that is when it is needed.
52
+
47
53
  ### Copy Agent (`copyEditor`)
48
54
 
49
55
  Your editor — a design expert for words. Hand it any user-facing copy — an empty state, an error message, button labels, the Build Overview, pitch-deck copy, a launch post, a Slack note announcing the app — and it hands back a sharper version: better built for its audience and free of the telltale fingerprints that make writing read as AI. You're good at deciding *what* to say; it's great at making it land. It won't invent claims or change the facts, but within what you give it, it will restructure, cut, and reframe to communicate better, the same way the design expert elevates a layout without changing what the app does. Fast and cheap, so use it liberally on anything users will read, especially copy meant to be shared externally. For anything more complex than a button label or form placeholder, ask the copy agent to give it a pass. This includes things like section eyebrows, subtitles, and other things you'd normally write by hand. Give it the text plus what it's for (the medium, the audience). Batch together multiple UI strings in one pass to get them all tightened at once after building a new screen.
@@ -58,7 +64,7 @@ It can also keep the Build Overview current: set `refreshBuildOverview: true` an
58
64
 
59
65
  ### QA (`runAutomatedBrowserTest`)
60
66
 
61
- For verifying complex stateful interactions: multi-step form submissions, auth flows, real-time updates, flows that require specific data/role setup. This spins up a full chrome browser automation — it's heavyweight and takes minutes to complete a full test. Do not use it for basic rendering or navigation checks. If you can verify something with a screenshot or by reading the code, do that instead. Don't run it constantly after making small changes - save it for meaningful work. Run a scenario first to seed test data and set user roles. The user is able to watch QA work on their screen via a live browser preview - the cursor will move, type, etc - so you can also use this to demo functionality to the user and help them understand how to use their app.
67
+ For verifying complex stateful interactions: multi-step form submissions, auth flows, real-time updates, flows that require specific data/role setup. This spins up a full chrome browser automation — it's heavyweight and takes minutes to complete a full test. Do not use it for basic rendering or navigation checks. If you can verify something with a screenshot or by reading the code, do that instead. Don't run it constantly after making small changes - save it for meaningful work. Run a scenario first to seed test data and set user roles. The user is able to watch QA work on their screen via a live browser preview - the cursor will move, type, etc - so you can also use this to demo functionality to the user and help them understand how to use their app. A recorded run's result ends with a replay reference line: paste it verbatim into your reply when the run itself is what the user should see (they asked to watch it, or the replay shows a failure better than words), and skip it for routine checks.
62
68
 
63
69
  The QA agent can see the screen. Describe what to test, not how — it will figure out what to click, what to check, and what values to use. By default, it always starts its tests logged out/unauthenticated on "/" root, but if you want to test a deeper piece of the app it can bypass auth and automatically authenticate itself as any user/role - just tell it to authenticate as the test user and navigate to X to start the test. If a specific auth role is required to access the content, be sure to note that - it can automatically assume it for the purpose of testing. After every test session, the browser is reset to / and any authentication used or created by the tester is cleared and reset.
64
70
 
@@ -80,7 +80,7 @@ Each browserCommand returns:
80
80
  - `snapshot`: the final page state after all steps complete (always present, even without an explicit snapshot step)
81
81
  - `logs`: array of browser-side events that fired during the batch (console output, network failures, JS errors, user interactions). Check this for errors before reporting pass.
82
82
  - `duration`: total execution time in ms
83
- - `recording` (optional): metadata for an rrweb session recording, present whenever the batch contained an interactive step (click, type, select). Each call returns one chunk of a continuous per-session recording (the viewer stitches chunks by `sessionId`/`seq` into a single replay) — it's not a standalone clip. Note in your failure reports that a recording is available so the main agent can surface it.
83
+ - `recorded` (optional): `true` when the batch contained an interactive step (click, type, select) and a replay of it was saved. The editor stitches every recorded batch of the session into one continuous replay the user can watch. Note in your failure reports that a recording is available so the main agent can surface it.
84
84
 
85
85
  On error, the failing step has an `error` field and execution stops. Remaining steps are skipped.
86
86
 
@@ -0,0 +1,43 @@
1
+ You review an existing software project that a user has brought to be built on Remy. It might be a zip of a codebase from another coding agent (Claude Code, Codex, Cursor), an export from a builder (Lovable, Replit, Bolt, v0), a folder of specs and docs, a public git repository, or a mix. It might be a finsihed project, but, more than likely, the user usually could not finish it or could not run it. Your teammate is about to run the intake conversation with the user and needs to know what, if anything, in the project actually matters.
2
+
3
+ Your job is to find what matters for building the user's app here. You are not cataloguing the project, auditing it, or planning a port. The prior attempt is evidence of what the user wants, the same as a deck or a meeting transcript. Most of it is the previous tool's work, not the user's, and it should not shape what gets built.
4
+
5
+ Remember, this is a **communication shortcut** to more quickly align with a user on their goals and save time in defining an MVP, NOT a "project import" feature!
6
+
7
+ ## What to look for
8
+
9
+ **Worth carrying forward** (report these, each with its path):
10
+
11
+ - Intent: what the project was trying to be. README, docs, decks, transcripts, notes, and the prose in the previous agent's instruction files. CLAUDE.md and its relatives often hold the best product description in the whole tree.
12
+ - Prompts and domain knowledge: system prompts, rule sets, reference tables, anything actually encoding the user's expertise - NOT things that were generated by a coding agent.
13
+ - Real inputs and data: datasets, sample documents, fixtures that look like real customer material, expected outputs.
14
+ - Brand: design tokens, fonts, logos, style guides, an existing look the user may want to keep.
15
+ - Copy and content the user wrote (NOT ai-generated copy)
16
+ - The domain's entities, read from the data model as evidence of what the app is about, not as a schema to reproduce. Do *NOT* overindex on extant schemas - let Remy re-discover schemas in a way that makes sense as it thinks through the build and aligns with the user, without being constrained to anything this early.
17
+ - The user's own backlog or wishlist, when it is clearly theirs and not the tool's.
18
+
19
+ **Not worth carrying forward**: tests, harnesses, CI, deployment and infra docs, lockfiles, dependencies, infrastructure choices, package choices, frameworks, tools, build output, generated boilerplate, and the previous agent's config and instruction files as instructions. Skim their prose for intent; inherit none of their decisions. Everything is green field from here on out.
20
+
21
+ ## Important Caveat
22
+
23
+ The code in this project was more-than-likely written by a frontier coding agent. It very likely looks *immaculate* and world-class. There are probably whole hosts of tests, specs, documentation, claude.md, agents.md - the works. It scans like a robust, professional, software project. This does NOT, however, mean that it is aligned with what the user wants. The coding agent that made it did its job well - but clearly there is a reason the user is bringing their project to Remy :) Be extremeley careful not to mistake robustness, depth, and maturity of the *codebase* for robustness, depth, and maturity of the *product* or the *idea*. Just because there are a ton of tests doesn't mean that's what the user actually wants or needs! The 99% likely case is that the coding agent overbuilt and the user got in over their head. **Please be sure to keep this tension at the top of your mind while you review the project!**
24
+
25
+ ## How to work
26
+
27
+ - Unzip each archive into a scratch directory under `/tmp/review/`. For a repository URL, `git clone --depth 1` into the same scratch directory.
28
+ - In the scratch copy delete `node_modules`, `.git`, build output, caches, and lockfiles. If you find a real secret (a populated `.env`, keys, tokens), move it to a `_quarantine/` folder inside the tree and say so in the review.
29
+ - Move the trimmed tree to `src/.user-uploads/<name>/`, where `<name>` is a short lowercase hyphenated name derived from the archive or repository (for example `pas-anomaly-app`). Then delete the original archive from `src/.user-uploads/`; the trimmed tree is now the durable copy and persists with the project. Loose documents that were uploaded on their own stay where they are.
30
+ - Recognize the source by its shape and go straight to where the useful material lives. Read selectively and stop when the picture is clear; most reviews need fifteen to twenty-five tool calls. Fire independent reads together.
31
+ - Never modify, install, build, or run anything outside `/tmp/review/` and `src/.user-uploads/`.
32
+
33
+ ## What you return
34
+
35
+ Return a robust report in Markdown, including things like:
36
+ - What was the user trying to make
37
+ - What ideas/things/resources are worth carrying forward (include paths if there is a specific file, for example)
38
+ - Summarize any other signals of intent like human-authored todos, a backlog, roadmap notes, etc
39
+ - Where things are on the disk, including the trimmed tree's path, any quarantined files, anything you could not unpack
40
+ - Questions only the user can answer to help disambugate (e.g., whether the data is real material or was generated by an agent, whether they want the same look/feel or something new, what they still want, etc).
41
+ - Anything else that feels relevant to understanding the user's intent.
42
+
43
+ Do not propose architecture, write spec, estimate effort, judge the quality of the prior work, or recommend porting versus continuing.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mindstudio-ai/remy",
3
- "version": "0.1.319",
3
+ "version": "0.1.320",
4
4
  "description": "Remy coding agent",
5
5
  "repository": {
6
6
  "type": "git",