@mindstudio-ai/remy 0.1.318 → 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.
- package/dist/automatedActions/reviewExistingProject.md +7 -0
- package/dist/headless.js +260 -24
- package/dist/index.js +309 -37
- package/dist/prompt/static/intake.md +1 -1
- package/dist/prompt/static/team.md +7 -1
- package/dist/subagents/browserAutomation/prompt.md +1 -1
- package/dist/subagents/reviewExistingProject/prompt.md +43 -0
- package/package.json +1 -1
|
@@ -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",
|
|
@@ -512,6 +519,7 @@ var TEXT_MODELS = {
|
|
|
512
519
|
"gpt-5.6-sol": { forceCompactAt: 6e5, suggestCompactAt: 25e4 },
|
|
513
520
|
"gpt-5.6-terra": { forceCompactAt: 6e5, suggestCompactAt: 25e4 },
|
|
514
521
|
"gpt-5.6-luna": { forceCompactAt: 6e5, suggestCompactAt: 25e4 },
|
|
522
|
+
"gpt-6-astra": { forceCompactAt: 6e5, suggestCompactAt: 25e4 },
|
|
515
523
|
// Google ~1M-context; only 3.1-pro is tiered (higher rates above 200K).
|
|
516
524
|
"gemini-3-pro": { forceCompactAt: 85e4 },
|
|
517
525
|
"gemini-3.1-pro": { forceCompactAt: 85e4, suggestCompactAt: 18e4 },
|
|
@@ -1419,6 +1427,9 @@ async function listRecursive(dir) {
|
|
|
1419
1427
|
for (const entry of entries) {
|
|
1420
1428
|
const fullPath = path5.join(dir, entry.name);
|
|
1421
1429
|
if (entry.isDirectory()) {
|
|
1430
|
+
if (entry.name.startsWith(".")) {
|
|
1431
|
+
continue;
|
|
1432
|
+
}
|
|
1422
1433
|
results.push(`${fullPath}/`);
|
|
1423
1434
|
results.push(...await listRecursive(fullPath));
|
|
1424
1435
|
} else {
|
|
@@ -2645,8 +2656,8 @@ function formatSize(bytes) {
|
|
|
2645
2656
|
}
|
|
2646
2657
|
async function formatFile(dirPath, name, indent) {
|
|
2647
2658
|
try {
|
|
2648
|
-
const
|
|
2649
|
-
return `${indent}${name}${" ".repeat(Math.max(1, 30 - indent.length - name.length))}${formatSize(
|
|
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)}`;
|
|
2650
2661
|
} catch {
|
|
2651
2662
|
return `${indent}${name}`;
|
|
2652
2663
|
}
|
|
@@ -3436,6 +3447,48 @@ ${partial}` : "[INTERRUPTED] Tool execution was stopped.";
|
|
|
3436
3447
|
}
|
|
3437
3448
|
};
|
|
3438
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
|
+
|
|
3439
3492
|
// src/historyLimits.ts
|
|
3440
3493
|
var MAX_TOOL_RESULT_BYTES = 256 * 1024;
|
|
3441
3494
|
function capToolResult(result, maxBytes = MAX_TOOL_RESULT_BYTES) {
|
|
@@ -3478,6 +3531,13 @@ function capMessageForHistory(msg, maxBytes = MAX_TOOL_RESULT_BYTES) {
|
|
|
3478
3531
|
continue;
|
|
3479
3532
|
}
|
|
3480
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
|
+
}
|
|
3481
3541
|
block.result = capToolResult(block.result, maxBytes);
|
|
3482
3542
|
}
|
|
3483
3543
|
if (typeof block.backgroundResult === "string") {
|
|
@@ -4074,25 +4134,36 @@ ${partial}` : "[INTERRUPTED] Agent was interrupted before producing output.",
|
|
|
4074
4134
|
}
|
|
4075
4135
|
let settle;
|
|
4076
4136
|
const resultPromise = new Promise((res) => {
|
|
4077
|
-
settle = (result, isError) => res({
|
|
4137
|
+
settle = (result, isError, recording) => res({
|
|
4138
|
+
id: tc.id,
|
|
4139
|
+
result,
|
|
4140
|
+
isError,
|
|
4141
|
+
...recording ? { recording } : {}
|
|
4142
|
+
});
|
|
4078
4143
|
});
|
|
4079
4144
|
let toolAbort = new AbortController();
|
|
4080
4145
|
const cascadeAbort = () => toolAbort.abort();
|
|
4081
4146
|
signal?.addEventListener("abort", cascadeAbort, { once: true });
|
|
4082
4147
|
let settled = false;
|
|
4083
|
-
const safeSettle = (result, isError) => {
|
|
4148
|
+
const safeSettle = (result, isError, recording) => {
|
|
4084
4149
|
if (settled) {
|
|
4085
4150
|
return;
|
|
4086
4151
|
}
|
|
4087
4152
|
settled = true;
|
|
4088
4153
|
signal?.removeEventListener("abort", cascadeAbort);
|
|
4089
|
-
settle(result, isError);
|
|
4154
|
+
settle(result, isError, recording);
|
|
4090
4155
|
};
|
|
4091
4156
|
const run2 = async (input) => {
|
|
4092
4157
|
try {
|
|
4093
4158
|
let result;
|
|
4159
|
+
let recording;
|
|
4094
4160
|
if (externalTools.has(tc.name) && resolveExternalTool) {
|
|
4095
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
|
+
}
|
|
4096
4167
|
} else {
|
|
4097
4168
|
const onLog = (line) => emit({
|
|
4098
4169
|
type: "tool_input_delta",
|
|
@@ -4108,7 +4179,11 @@ ${partial}` : "[INTERRUPTED] Agent was interrupted before producing output.",
|
|
|
4108
4179
|
subAgentMessages
|
|
4109
4180
|
);
|
|
4110
4181
|
}
|
|
4111
|
-
safeSettle(
|
|
4182
|
+
safeSettle(
|
|
4183
|
+
capToolResult(result),
|
|
4184
|
+
result.startsWith("Error"),
|
|
4185
|
+
recording
|
|
4186
|
+
);
|
|
4112
4187
|
} catch (err) {
|
|
4113
4188
|
safeSettle(`Error: ${err.message}`, true);
|
|
4114
4189
|
}
|
|
@@ -4150,7 +4225,8 @@ ${partial}` : "[INTERRUPTED] Agent was interrupted before producing output.",
|
|
|
4150
4225
|
id: tc.id,
|
|
4151
4226
|
name: tc.name,
|
|
4152
4227
|
result: r.result,
|
|
4153
|
-
isError: r.isError
|
|
4228
|
+
isError: r.isError,
|
|
4229
|
+
...r.recording ? { recording: r.recording } : {}
|
|
4154
4230
|
});
|
|
4155
4231
|
return r;
|
|
4156
4232
|
})
|
|
@@ -4164,6 +4240,9 @@ ${partial}` : "[INTERRUPTED] Agent was interrupted before producing output.",
|
|
|
4164
4240
|
block.result = r.result;
|
|
4165
4241
|
block.isError = r.isError;
|
|
4166
4242
|
block.completedAt = Date.now();
|
|
4243
|
+
if (r.recording) {
|
|
4244
|
+
block.recording = r.recording;
|
|
4245
|
+
}
|
|
4167
4246
|
const innerMsgs = subAgentMessages.get(r.id);
|
|
4168
4247
|
if (innerMsgs) {
|
|
4169
4248
|
block.subAgentMessages = capSubAgentTranscript(innerMsgs);
|
|
@@ -4294,7 +4373,7 @@ var BROWSER_TOOLS = [
|
|
|
4294
4373
|
},
|
|
4295
4374
|
{
|
|
4296
4375
|
name: "browserCommand",
|
|
4297
|
-
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)
|
|
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.",
|
|
4298
4377
|
inputSchema: {
|
|
4299
4378
|
type: "object",
|
|
4300
4379
|
properties: {
|
|
@@ -4412,7 +4491,7 @@ function walkMdFiles(dir, skip) {
|
|
|
4412
4491
|
for (const entry of fs17.readdirSync(dir, { withFileTypes: true })) {
|
|
4413
4492
|
const full = path9.join(dir, entry.name);
|
|
4414
4493
|
if (entry.isDirectory()) {
|
|
4415
|
-
if (!skip?.has(entry.name)) {
|
|
4494
|
+
if (!skip?.has(entry.name) && !entry.name.startsWith(".")) {
|
|
4416
4495
|
files.push(...walkMdFiles(full, skip));
|
|
4417
4496
|
}
|
|
4418
4497
|
} else if (entry.name.endsWith(".md")) {
|
|
@@ -4710,8 +4789,14 @@ async function runBrowserAutomation(task, context, opts) {
|
|
|
4710
4789
|
});
|
|
4711
4790
|
context.subAgentMessages?.set(context.toolCallId, result.messages);
|
|
4712
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
|
+
);
|
|
4713
4797
|
return {
|
|
4714
4798
|
text: result.text,
|
|
4799
|
+
recorded,
|
|
4715
4800
|
...preferred?.url ? { screenshot: preferred } : {}
|
|
4716
4801
|
};
|
|
4717
4802
|
} finally {
|
|
@@ -4738,12 +4823,18 @@ var browserAutomationTool = {
|
|
|
4738
4823
|
return "Error: browser automation requires execution context (only available in headless mode)";
|
|
4739
4824
|
}
|
|
4740
4825
|
const result = await runBrowserAutomation(input.task, context);
|
|
4826
|
+
let text = result.text;
|
|
4741
4827
|
if (result.screenshot) {
|
|
4742
|
-
|
|
4828
|
+
text += `
|
|
4743
4829
|
|
|
4744
4830
|
`;
|
|
4745
4831
|
}
|
|
4746
|
-
|
|
4832
|
+
if (result.recorded) {
|
|
4833
|
+
text += `
|
|
4834
|
+
|
|
4835
|
+
Replay of this run: `;
|
|
4836
|
+
}
|
|
4837
|
+
return text;
|
|
4747
4838
|
}
|
|
4748
4839
|
};
|
|
4749
4840
|
|
|
@@ -7077,6 +7168,138 @@ After reconciling the spec, refresh the Build Overview: author the complete upda
|
|
|
7077
7168
|
}
|
|
7078
7169
|
};
|
|
7079
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
|
+
|
|
7080
7303
|
// src/tools/common/scrapeWebUrl.ts
|
|
7081
7304
|
var scrapeWebUrlTool = {
|
|
7082
7305
|
definition: {
|
|
@@ -7171,7 +7394,8 @@ var ALL_TOOLS = [
|
|
|
7171
7394
|
// new tool goes at the end to leave every existing session's prefix intact.
|
|
7172
7395
|
loadSkillTool,
|
|
7173
7396
|
testJewelTool,
|
|
7174
|
-
researchTool
|
|
7397
|
+
researchTool,
|
|
7398
|
+
reviewExistingProjectTool
|
|
7175
7399
|
];
|
|
7176
7400
|
var SUBAGENT_TOOL_NAMES = /* @__PURE__ */ new Set([
|
|
7177
7401
|
"visualDesignExpert",
|
|
@@ -7181,7 +7405,8 @@ var SUBAGENT_TOOL_NAMES = /* @__PURE__ */ new Set([
|
|
|
7181
7405
|
"specSync",
|
|
7182
7406
|
"runAutomatedBrowserTest",
|
|
7183
7407
|
"askMindStudioSdk",
|
|
7184
|
-
"research"
|
|
7408
|
+
"research",
|
|
7409
|
+
"reviewExistingProject"
|
|
7185
7410
|
]);
|
|
7186
7411
|
function getToolDefinitions() {
|
|
7187
7412
|
return ALL_TOOLS.map((t) => t.definition);
|
|
@@ -8171,7 +8396,9 @@ function walkMdFiles2(dir) {
|
|
|
8171
8396
|
for (const entry of entries) {
|
|
8172
8397
|
const full = path12.join(dir, entry.name);
|
|
8173
8398
|
if (entry.isDirectory()) {
|
|
8174
|
-
|
|
8399
|
+
if (!entry.name.startsWith(".")) {
|
|
8400
|
+
results.push(...walkMdFiles2(full));
|
|
8401
|
+
}
|
|
8175
8402
|
} else if (entry.name.endsWith(".md")) {
|
|
8176
8403
|
results.push(full);
|
|
8177
8404
|
}
|
|
@@ -9571,11 +9798,15 @@ async function runTurn(params) {
|
|
|
9571
9798
|
}
|
|
9572
9799
|
|
|
9573
9800
|
// src/headless/attachments.ts
|
|
9574
|
-
import { mkdirSync, existsSync } from "fs";
|
|
9575
|
-
import { writeFile as writeFile3 } from "fs/promises";
|
|
9576
|
-
import {
|
|
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";
|
|
9577
9806
|
var log16 = createLogger("headless:attachments");
|
|
9578
9807
|
var UPLOADS_DIR = "src/.user-uploads";
|
|
9808
|
+
var IMAGE_DOWNLOAD_TIMEOUT_MS = 3e4;
|
|
9809
|
+
var DOCUMENT_DOWNLOAD_TIMEOUT_MS = 3e5;
|
|
9579
9810
|
function filenameFromUrl(url) {
|
|
9580
9811
|
try {
|
|
9581
9812
|
const pathname = new URL(url).pathname;
|
|
@@ -9586,7 +9817,7 @@ function filenameFromUrl(url) {
|
|
|
9586
9817
|
}
|
|
9587
9818
|
}
|
|
9588
9819
|
function resolveUniqueFilename(name, claimed) {
|
|
9589
|
-
const isFree = (candidate) => !claimed.has(candidate) && !existsSync(
|
|
9820
|
+
const isFree = (candidate) => !claimed.has(candidate) && !existsSync(join6(UPLOADS_DIR, candidate));
|
|
9590
9821
|
if (isFree(name)) {
|
|
9591
9822
|
return name;
|
|
9592
9823
|
}
|
|
@@ -9621,19 +9852,23 @@ async function persistAttachmentList(attachments) {
|
|
|
9621
9852
|
const results = await Promise.allSettled(
|
|
9622
9853
|
nonVoice.map(async (att, i) => {
|
|
9623
9854
|
const name = names[i];
|
|
9624
|
-
const localPath =
|
|
9855
|
+
const localPath = join6(UPLOADS_DIR, name);
|
|
9856
|
+
const timeoutMs = isImageAttachment(att) ? IMAGE_DOWNLOAD_TIMEOUT_MS : DOCUMENT_DOWNLOAD_TIMEOUT_MS;
|
|
9625
9857
|
const res = await fetch(att.url, {
|
|
9626
|
-
signal: AbortSignal.timeout(
|
|
9858
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
9627
9859
|
});
|
|
9628
|
-
if (!res.ok) {
|
|
9860
|
+
if (!res.ok || !res.body) {
|
|
9629
9861
|
throw new Error(`HTTP ${res.status} downloading ${att.url}`);
|
|
9630
9862
|
}
|
|
9631
|
-
|
|
9632
|
-
|
|
9863
|
+
await pipeline(
|
|
9864
|
+
Readable.fromWeb(res.body),
|
|
9865
|
+
createWriteStream(localPath)
|
|
9866
|
+
);
|
|
9867
|
+
const { size } = await stat5(localPath);
|
|
9633
9868
|
log16.info("Attachment saved", {
|
|
9634
9869
|
filename: name,
|
|
9635
9870
|
path: localPath,
|
|
9636
|
-
bytes:
|
|
9871
|
+
bytes: size
|
|
9637
9872
|
});
|
|
9638
9873
|
let extractedTextPath;
|
|
9639
9874
|
if (att.extractedTextUrl) {
|
|
@@ -10504,6 +10739,7 @@ var HeadlessSession = class {
|
|
|
10504
10739
|
name: e.name,
|
|
10505
10740
|
result: e.result,
|
|
10506
10741
|
isError: e.isError,
|
|
10742
|
+
...e.recording && { recording: e.recording },
|
|
10507
10743
|
...e.parentToolId && { parentToolId: e.parentToolId }
|
|
10508
10744
|
},
|
|
10509
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
|
-
|
|
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",
|
|
@@ -2129,6 +2139,7 @@ var init_surfaces = __esm({
|
|
|
2129
2139
|
"gpt-5.6-sol": { forceCompactAt: 6e5, suggestCompactAt: 25e4 },
|
|
2130
2140
|
"gpt-5.6-terra": { forceCompactAt: 6e5, suggestCompactAt: 25e4 },
|
|
2131
2141
|
"gpt-5.6-luna": { forceCompactAt: 6e5, suggestCompactAt: 25e4 },
|
|
2142
|
+
"gpt-6-astra": { forceCompactAt: 6e5, suggestCompactAt: 25e4 },
|
|
2132
2143
|
// Google ~1M-context; only 3.1-pro is tiered (higher rates above 200K).
|
|
2133
2144
|
"gemini-3-pro": { forceCompactAt: 85e4 },
|
|
2134
2145
|
"gemini-3.1-pro": { forceCompactAt: 85e4, suggestCompactAt: 18e4 },
|
|
@@ -2331,6 +2342,53 @@ var init_cleanMessages = __esm({
|
|
|
2331
2342
|
}
|
|
2332
2343
|
});
|
|
2333
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
|
+
|
|
2334
2392
|
// src/historyLimits.ts
|
|
2335
2393
|
function capToolResult(result, maxBytes = MAX_TOOL_RESULT_BYTES) {
|
|
2336
2394
|
const total = Buffer.byteLength(result, "utf-8");
|
|
@@ -2370,6 +2428,13 @@ function capMessageForHistory(msg, maxBytes = MAX_TOOL_RESULT_BYTES) {
|
|
|
2370
2428
|
continue;
|
|
2371
2429
|
}
|
|
2372
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
|
+
}
|
|
2373
2438
|
block.result = capToolResult(block.result, maxBytes);
|
|
2374
2439
|
}
|
|
2375
2440
|
if (typeof block.backgroundResult === "string") {
|
|
@@ -2390,6 +2455,7 @@ var MAX_TOOL_RESULT_BYTES, MAX_SUBAGENT_RESULT_BYTES, MAX_SUBAGENT_TRANSCRIPT_BY
|
|
|
2390
2455
|
var init_historyLimits = __esm({
|
|
2391
2456
|
"src/historyLimits.ts"() {
|
|
2392
2457
|
"use strict";
|
|
2458
|
+
init_recording();
|
|
2393
2459
|
MAX_TOOL_RESULT_BYTES = 256 * 1024;
|
|
2394
2460
|
MAX_SUBAGENT_RESULT_BYTES = 32 * 1024;
|
|
2395
2461
|
MAX_SUBAGENT_TRANSCRIPT_BYTES = 512 * 1024;
|
|
@@ -3787,8 +3853,8 @@ function formatSize(bytes) {
|
|
|
3787
3853
|
}
|
|
3788
3854
|
async function formatFile(dirPath, name, indent) {
|
|
3789
3855
|
try {
|
|
3790
|
-
const
|
|
3791
|
-
return `${indent}${name}${" ".repeat(Math.max(1, 30 - indent.length - name.length))}${formatSize(
|
|
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)}`;
|
|
3792
3858
|
} catch {
|
|
3793
3859
|
return `${indent}${name}`;
|
|
3794
3860
|
}
|
|
@@ -5092,25 +5158,36 @@ ${partial}` : "[INTERRUPTED] Agent was interrupted before producing output.",
|
|
|
5092
5158
|
}
|
|
5093
5159
|
let settle;
|
|
5094
5160
|
const resultPromise = new Promise((res) => {
|
|
5095
|
-
settle = (result, isError) => res({
|
|
5161
|
+
settle = (result, isError, recording) => res({
|
|
5162
|
+
id: tc.id,
|
|
5163
|
+
result,
|
|
5164
|
+
isError,
|
|
5165
|
+
...recording ? { recording } : {}
|
|
5166
|
+
});
|
|
5096
5167
|
});
|
|
5097
5168
|
let toolAbort = new AbortController();
|
|
5098
5169
|
const cascadeAbort = () => toolAbort.abort();
|
|
5099
5170
|
signal?.addEventListener("abort", cascadeAbort, { once: true });
|
|
5100
5171
|
let settled = false;
|
|
5101
|
-
const safeSettle = (result, isError) => {
|
|
5172
|
+
const safeSettle = (result, isError, recording) => {
|
|
5102
5173
|
if (settled) {
|
|
5103
5174
|
return;
|
|
5104
5175
|
}
|
|
5105
5176
|
settled = true;
|
|
5106
5177
|
signal?.removeEventListener("abort", cascadeAbort);
|
|
5107
|
-
settle(result, isError);
|
|
5178
|
+
settle(result, isError, recording);
|
|
5108
5179
|
};
|
|
5109
5180
|
const run2 = async (input) => {
|
|
5110
5181
|
try {
|
|
5111
5182
|
let result;
|
|
5183
|
+
let recording;
|
|
5112
5184
|
if (externalTools.has(tc.name) && resolveExternalTool) {
|
|
5113
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
|
+
}
|
|
5114
5191
|
} else {
|
|
5115
5192
|
const onLog = (line) => emit({
|
|
5116
5193
|
type: "tool_input_delta",
|
|
@@ -5126,7 +5203,11 @@ ${partial}` : "[INTERRUPTED] Agent was interrupted before producing output.",
|
|
|
5126
5203
|
subAgentMessages
|
|
5127
5204
|
);
|
|
5128
5205
|
}
|
|
5129
|
-
safeSettle(
|
|
5206
|
+
safeSettle(
|
|
5207
|
+
capToolResult(result),
|
|
5208
|
+
result.startsWith("Error"),
|
|
5209
|
+
recording
|
|
5210
|
+
);
|
|
5130
5211
|
} catch (err) {
|
|
5131
5212
|
safeSettle(`Error: ${err.message}`, true);
|
|
5132
5213
|
}
|
|
@@ -5168,7 +5249,8 @@ ${partial}` : "[INTERRUPTED] Agent was interrupted before producing output.",
|
|
|
5168
5249
|
id: tc.id,
|
|
5169
5250
|
name: tc.name,
|
|
5170
5251
|
result: r.result,
|
|
5171
|
-
isError: r.isError
|
|
5252
|
+
isError: r.isError,
|
|
5253
|
+
...r.recording ? { recording: r.recording } : {}
|
|
5172
5254
|
});
|
|
5173
5255
|
return r;
|
|
5174
5256
|
})
|
|
@@ -5182,6 +5264,9 @@ ${partial}` : "[INTERRUPTED] Agent was interrupted before producing output.",
|
|
|
5182
5264
|
block.result = r.result;
|
|
5183
5265
|
block.isError = r.isError;
|
|
5184
5266
|
block.completedAt = Date.now();
|
|
5267
|
+
if (r.recording) {
|
|
5268
|
+
block.recording = r.recording;
|
|
5269
|
+
}
|
|
5185
5270
|
const innerMsgs = subAgentMessages.get(r.id);
|
|
5186
5271
|
if (innerMsgs) {
|
|
5187
5272
|
block.subAgentMessages = capSubAgentTranscript(innerMsgs);
|
|
@@ -5273,6 +5358,7 @@ var init_runner = __esm({
|
|
|
5273
5358
|
init_usageLedger();
|
|
5274
5359
|
init_toolRegistry();
|
|
5275
5360
|
init_historyLimits();
|
|
5361
|
+
init_recording();
|
|
5276
5362
|
init_statusWatcher();
|
|
5277
5363
|
init_cleanMessages();
|
|
5278
5364
|
log8 = createLogger("sub-agent");
|
|
@@ -5342,7 +5428,7 @@ var init_tools2 = __esm({
|
|
|
5342
5428
|
},
|
|
5343
5429
|
{
|
|
5344
5430
|
name: "browserCommand",
|
|
5345
|
-
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)
|
|
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.",
|
|
5346
5432
|
inputSchema: {
|
|
5347
5433
|
type: "object",
|
|
5348
5434
|
properties: {
|
|
@@ -5462,7 +5548,7 @@ function walkMdFiles(dir, skip) {
|
|
|
5462
5548
|
for (const entry of fs16.readdirSync(dir, { withFileTypes: true })) {
|
|
5463
5549
|
const full = path9.join(dir, entry.name);
|
|
5464
5550
|
if (entry.isDirectory()) {
|
|
5465
|
-
if (!skip?.has(entry.name)) {
|
|
5551
|
+
if (!skip?.has(entry.name) && !entry.name.startsWith(".")) {
|
|
5466
5552
|
files.push(...walkMdFiles(full, skip));
|
|
5467
5553
|
}
|
|
5468
5554
|
} else if (entry.name.endsWith(".md")) {
|
|
@@ -5771,8 +5857,14 @@ async function runBrowserAutomation(task, context, opts) {
|
|
|
5771
5857
|
});
|
|
5772
5858
|
context.subAgentMessages?.set(context.toolCallId, result.messages);
|
|
5773
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
|
+
);
|
|
5774
5865
|
return {
|
|
5775
5866
|
text: result.text,
|
|
5867
|
+
recorded,
|
|
5776
5868
|
...preferred?.url ? { screenshot: preferred } : {}
|
|
5777
5869
|
};
|
|
5778
5870
|
} finally {
|
|
@@ -5783,7 +5875,7 @@ var log9, CAPTURE_COMMANDS, browserAutomationTool;
|
|
|
5783
5875
|
var init_browserAutomation = __esm({
|
|
5784
5876
|
"src/subagents/browserAutomation/index.ts"() {
|
|
5785
5877
|
"use strict";
|
|
5786
|
-
|
|
5878
|
+
init_tools10();
|
|
5787
5879
|
init_runner();
|
|
5788
5880
|
init_tools2();
|
|
5789
5881
|
init_tools();
|
|
@@ -5817,12 +5909,18 @@ var init_browserAutomation = __esm({
|
|
|
5817
5909
|
return "Error: browser automation requires execution context (only available in headless mode)";
|
|
5818
5910
|
}
|
|
5819
5911
|
const result = await runBrowserAutomation(input.task, context);
|
|
5912
|
+
let text = result.text;
|
|
5820
5913
|
if (result.screenshot) {
|
|
5821
|
-
|
|
5914
|
+
text += `
|
|
5822
5915
|
|
|
5823
5916
|
`;
|
|
5824
5917
|
}
|
|
5825
|
-
|
|
5918
|
+
if (result.recorded) {
|
|
5919
|
+
text += `
|
|
5920
|
+
|
|
5921
|
+
Replay of this run: `;
|
|
5922
|
+
}
|
|
5923
|
+
return text;
|
|
5826
5924
|
}
|
|
5827
5925
|
};
|
|
5828
5926
|
}
|
|
@@ -6081,7 +6179,7 @@ var init_research = __esm({
|
|
|
6081
6179
|
init_assets();
|
|
6082
6180
|
init_runner();
|
|
6083
6181
|
init_context();
|
|
6084
|
-
|
|
6182
|
+
init_tools10();
|
|
6085
6183
|
init_tools3();
|
|
6086
6184
|
init_surfaces();
|
|
6087
6185
|
BASE_PROMPT2 = readAsset("subagents/research", "prompt.md");
|
|
@@ -6949,7 +7047,7 @@ var init_copyEditor = __esm({
|
|
|
6949
7047
|
init_assets();
|
|
6950
7048
|
init_runner();
|
|
6951
7049
|
init_context();
|
|
6952
|
-
|
|
7050
|
+
init_tools10();
|
|
6953
7051
|
init_tools4();
|
|
6954
7052
|
init_surfaces();
|
|
6955
7053
|
BASE_PROMPT3 = readAsset("subagents/copyEditor", "prompt.md");
|
|
@@ -7279,7 +7377,7 @@ var research, tools, DESIGN_EXPERT_TOOLS;
|
|
|
7279
7377
|
var init_tools5 = __esm({
|
|
7280
7378
|
"src/subagents/designExpert/tools/index.ts"() {
|
|
7281
7379
|
"use strict";
|
|
7282
|
-
|
|
7380
|
+
init_tools10();
|
|
7283
7381
|
init_tools();
|
|
7284
7382
|
init_research();
|
|
7285
7383
|
init_scrapeWebUrl();
|
|
@@ -7808,7 +7906,7 @@ var DESCRIPTION, RENDER_WRITE_TOOL_NAMES, DESIGN_EXPERT_RENDER_TOOLS, designExpe
|
|
|
7808
7906
|
var init_designExpert = __esm({
|
|
7809
7907
|
"src/subagents/designExpert/index.ts"() {
|
|
7810
7908
|
"use strict";
|
|
7811
|
-
|
|
7909
|
+
init_tools10();
|
|
7812
7910
|
init_runner();
|
|
7813
7911
|
init_tools5();
|
|
7814
7912
|
init_tools();
|
|
@@ -8061,7 +8159,7 @@ var productVisionTool;
|
|
|
8061
8159
|
var init_productVision = __esm({
|
|
8062
8160
|
"src/subagents/productVision/index.ts"() {
|
|
8063
8161
|
"use strict";
|
|
8064
|
-
|
|
8162
|
+
init_tools10();
|
|
8065
8163
|
init_runner();
|
|
8066
8164
|
init_tools6();
|
|
8067
8165
|
init_tools();
|
|
@@ -8216,7 +8314,7 @@ var init_codeSanityCheck = __esm({
|
|
|
8216
8314
|
init_assets();
|
|
8217
8315
|
init_runner();
|
|
8218
8316
|
init_context();
|
|
8219
|
-
|
|
8317
|
+
init_tools10();
|
|
8220
8318
|
init_tools7();
|
|
8221
8319
|
init_research();
|
|
8222
8320
|
init_tools3();
|
|
@@ -8453,7 +8551,7 @@ var init_specSync = __esm({
|
|
|
8453
8551
|
init_assets();
|
|
8454
8552
|
init_runner();
|
|
8455
8553
|
init_context();
|
|
8456
|
-
|
|
8554
|
+
init_tools10();
|
|
8457
8555
|
init_writeBuildOverview();
|
|
8458
8556
|
init_tools8();
|
|
8459
8557
|
init_lock();
|
|
@@ -8559,6 +8657,166 @@ After reconciling the spec, refresh the Build Overview: author the complete upda
|
|
|
8559
8657
|
}
|
|
8560
8658
|
});
|
|
8561
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
|
+
|
|
8562
8820
|
// src/tools/common/scrapeWebUrl.ts
|
|
8563
8821
|
var scrapeWebUrlTool;
|
|
8564
8822
|
var init_scrapeWebUrl2 = __esm({
|
|
@@ -8629,7 +8887,7 @@ function executeTool(name, input, context) {
|
|
|
8629
8887
|
return tool.execute(input, context);
|
|
8630
8888
|
}
|
|
8631
8889
|
var ALL_TOOLS, SUBAGENT_TOOL_NAMES;
|
|
8632
|
-
var
|
|
8890
|
+
var init_tools10 = __esm({
|
|
8633
8891
|
"src/tools/index.ts"() {
|
|
8634
8892
|
"use strict";
|
|
8635
8893
|
init_readSpec();
|
|
@@ -8668,6 +8926,7 @@ var init_tools9 = __esm({
|
|
|
8668
8926
|
init_copyEditor();
|
|
8669
8927
|
init_specSync();
|
|
8670
8928
|
init_research();
|
|
8929
|
+
init_reviewExistingProject();
|
|
8671
8930
|
init_scrapeWebUrl2();
|
|
8672
8931
|
init_writeBuildOverview();
|
|
8673
8932
|
ALL_TOOLS = [
|
|
@@ -8716,7 +8975,8 @@ var init_tools9 = __esm({
|
|
|
8716
8975
|
// new tool goes at the end to leave every existing session's prefix intact.
|
|
8717
8976
|
loadSkillTool,
|
|
8718
8977
|
testJewelTool,
|
|
8719
|
-
researchTool
|
|
8978
|
+
researchTool,
|
|
8979
|
+
reviewExistingProjectTool
|
|
8720
8980
|
];
|
|
8721
8981
|
SUBAGENT_TOOL_NAMES = /* @__PURE__ */ new Set([
|
|
8722
8982
|
"visualDesignExpert",
|
|
@@ -8726,7 +8986,8 @@ var init_tools9 = __esm({
|
|
|
8726
8986
|
"specSync",
|
|
8727
8987
|
"runAutomatedBrowserTest",
|
|
8728
8988
|
"askMindStudioSdk",
|
|
8729
|
-
"research"
|
|
8989
|
+
"research",
|
|
8990
|
+
"reviewExistingProject"
|
|
8730
8991
|
]);
|
|
8731
8992
|
}
|
|
8732
8993
|
});
|
|
@@ -9243,7 +9504,9 @@ function walkMdFiles2(dir) {
|
|
|
9243
9504
|
for (const entry of entries) {
|
|
9244
9505
|
const full = path11.join(dir, entry.name);
|
|
9245
9506
|
if (entry.isDirectory()) {
|
|
9246
|
-
|
|
9507
|
+
if (!entry.name.startsWith(".")) {
|
|
9508
|
+
results.push(...walkMdFiles2(full));
|
|
9509
|
+
}
|
|
9247
9510
|
} else if (entry.name.endsWith(".md")) {
|
|
9248
9511
|
results.push(full);
|
|
9249
9512
|
}
|
|
@@ -10238,7 +10501,7 @@ var init_agent = __esm({
|
|
|
10238
10501
|
"src/agent.ts"() {
|
|
10239
10502
|
"use strict";
|
|
10240
10503
|
init_api();
|
|
10241
|
-
|
|
10504
|
+
init_tools10();
|
|
10242
10505
|
init_session();
|
|
10243
10506
|
init_logger();
|
|
10244
10507
|
init_usageLedger();
|
|
@@ -10525,9 +10788,11 @@ var init_config = __esm({
|
|
|
10525
10788
|
});
|
|
10526
10789
|
|
|
10527
10790
|
// src/headless/attachments.ts
|
|
10528
|
-
import { mkdirSync, existsSync } from "fs";
|
|
10529
|
-
import { writeFile as writeFile3 } from "fs/promises";
|
|
10530
|
-
import {
|
|
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";
|
|
10531
10796
|
function filenameFromUrl(url) {
|
|
10532
10797
|
try {
|
|
10533
10798
|
const pathname = new URL(url).pathname;
|
|
@@ -10538,7 +10803,7 @@ function filenameFromUrl(url) {
|
|
|
10538
10803
|
}
|
|
10539
10804
|
}
|
|
10540
10805
|
function resolveUniqueFilename(name, claimed) {
|
|
10541
|
-
const isFree = (candidate) => !claimed.has(candidate) && !existsSync(
|
|
10806
|
+
const isFree = (candidate) => !claimed.has(candidate) && !existsSync(join6(UPLOADS_DIR, candidate));
|
|
10542
10807
|
if (isFree(name)) {
|
|
10543
10808
|
return name;
|
|
10544
10809
|
}
|
|
@@ -10572,19 +10837,23 @@ async function persistAttachmentList(attachments) {
|
|
|
10572
10837
|
const results = await Promise.allSettled(
|
|
10573
10838
|
nonVoice.map(async (att, i) => {
|
|
10574
10839
|
const name = names[i];
|
|
10575
|
-
const localPath =
|
|
10840
|
+
const localPath = join6(UPLOADS_DIR, name);
|
|
10841
|
+
const timeoutMs = isImageAttachment(att) ? IMAGE_DOWNLOAD_TIMEOUT_MS : DOCUMENT_DOWNLOAD_TIMEOUT_MS;
|
|
10576
10842
|
const res = await fetch(att.url, {
|
|
10577
|
-
signal: AbortSignal.timeout(
|
|
10843
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
10578
10844
|
});
|
|
10579
|
-
if (!res.ok) {
|
|
10845
|
+
if (!res.ok || !res.body) {
|
|
10580
10846
|
throw new Error(`HTTP ${res.status} downloading ${att.url}`);
|
|
10581
10847
|
}
|
|
10582
|
-
|
|
10583
|
-
|
|
10848
|
+
await pipeline(
|
|
10849
|
+
Readable.fromWeb(res.body),
|
|
10850
|
+
createWriteStream(localPath)
|
|
10851
|
+
);
|
|
10852
|
+
const { size } = await stat5(localPath);
|
|
10584
10853
|
log16.info("Attachment saved", {
|
|
10585
10854
|
filename: name,
|
|
10586
10855
|
path: localPath,
|
|
10587
|
-
bytes:
|
|
10856
|
+
bytes: size
|
|
10588
10857
|
});
|
|
10589
10858
|
let extractedTextPath;
|
|
10590
10859
|
if (att.extractedTextUrl) {
|
|
@@ -10644,13 +10913,15 @@ function buildUploadHeader(documents, images) {
|
|
|
10644
10913
|
return `[Uploaded files]
|
|
10645
10914
|
${lines.join("\n")}`;
|
|
10646
10915
|
}
|
|
10647
|
-
var log16, UPLOADS_DIR, IMAGE_EXTENSIONS;
|
|
10916
|
+
var log16, UPLOADS_DIR, IMAGE_DOWNLOAD_TIMEOUT_MS, DOCUMENT_DOWNLOAD_TIMEOUT_MS, IMAGE_EXTENSIONS;
|
|
10648
10917
|
var init_attachments = __esm({
|
|
10649
10918
|
"src/headless/attachments.ts"() {
|
|
10650
10919
|
"use strict";
|
|
10651
10920
|
init_logger();
|
|
10652
10921
|
log16 = createLogger("headless:attachments");
|
|
10653
10922
|
UPLOADS_DIR = "src/.user-uploads";
|
|
10923
|
+
IMAGE_DOWNLOAD_TIMEOUT_MS = 3e4;
|
|
10924
|
+
DOCUMENT_DOWNLOAD_TIMEOUT_MS = 3e5;
|
|
10654
10925
|
IMAGE_EXTENSIONS = /* @__PURE__ */ new Set([".png", ".jpg", ".jpeg", ".gif", ".webp"]);
|
|
10655
10926
|
}
|
|
10656
10927
|
});
|
|
@@ -10954,7 +11225,7 @@ var init_headless = __esm({
|
|
|
10954
11225
|
init_attachments();
|
|
10955
11226
|
init_planFile();
|
|
10956
11227
|
init_stats();
|
|
10957
|
-
|
|
11228
|
+
init_tools10();
|
|
10958
11229
|
init_messageQueue();
|
|
10959
11230
|
init_resolve();
|
|
10960
11231
|
init_sentinel();
|
|
@@ -11512,6 +11783,7 @@ var init_headless = __esm({
|
|
|
11512
11783
|
name: e.name,
|
|
11513
11784
|
result: e.result,
|
|
11514
11785
|
isError: e.isError,
|
|
11786
|
+
...e.recording && { recording: e.recording },
|
|
11515
11787
|
...e.parentToolId && { parentToolId: e.parentToolId }
|
|
11516
11788
|
},
|
|
11517
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)
|
|
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
|
-
- `
|
|
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.
|