@mindstudio-ai/remy 0.1.139 → 0.1.141

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,5 @@
1
+ ---
2
+ trigger: approvePlan
3
+ ---
4
+
5
+ The user has approved the implementation plan in .remy-plan.md. Proceed with implementing the plan. Delete .remy-plan.md when all planned work is complete.
@@ -8,7 +8,7 @@ First, review the specific item and think about how it fits with the existing sp
8
8
 
9
9
  Then, ask the user any clarifying questions about anything that is ambiguous or requires additional input. Consult the team for any design work, architecture review, or SDK guidance - even if they're just quick questions (that's what the team is there for - they want to help and feel valuable!). Additive feature work is the most fun to build, but make sure you take a step back first and consider any technical debt or organization implications - we need to keep the codebase clean, tidy, bug-free, and easy/intuitive to manage. When adding new features, this might mean extracting shared helpers to separate files, breaking apart components into multiple files, making new folders, etc. - Don't go overboard, but also don't keep adding to one giant file until it ends up unmanageable. Consider organization and structure when building the plan.
10
10
 
11
- Then, put together a plan to build out the feature. Present the plan to the user for their approval.
11
+ Then, put together a plan to build out the feature. Write the plan with `writePlan` for the user's approval.
12
12
 
13
13
  When they've approved the plan, be sure to update the spec first - remember, the spec is the source of truth about the product. Then, build everything in one turn, using the spec as the master plan.
14
14
 
@@ -8,4 +8,4 @@ First, explore the project and get a sense of what has built.
8
8
 
9
9
  Read specific files and trace paths, don't just guess at how something works based on partial information. When you are finished exploring, identiy any high-impact areas for cleaning up the code. This can include things like directory organization, splitting things into separate files to make the project easier to scan, breaking up large components or screens, deduplicating copy-pasted code, removing dead code, and anything else that will leave the project more robust, reliable, and easier to scan/work on for developers. Do not optimize for the sake of optimizing, only focus on reducing real technical debt and leaving the product better and a more pleasant experience for other developers working on it.
10
10
 
11
- When you have a plan, run it by the `codeSanityCheck` to get a second set of eyes. Then, present the plan to the user. The technical detail is important so the user has a sense of what the plan entails, but remember that the user is not very technical, so it is equally important to help them understand the why behind any refactors.
11
+ When you have a plan, run it by the `codeSanityCheck` to get a second set of eyes. Then, write the plan with `writePlan` for the user to review. The technical detail is important so the user has a sense of what the plan entails, but remember that the user is not very technical, so it is equally important to help them understand the why behind any refactors.
@@ -0,0 +1,5 @@
1
+ ---
2
+ trigger: rejectPlan
3
+ ---
4
+
5
+ The user has rejected the implementation plan. Acknowledge and continue the conversation.
package/dist/headless.js CHANGED
@@ -6,7 +6,7 @@ var __export = (target, all) => {
6
6
 
7
7
  // src/headless.ts
8
8
  import { createInterface } from "readline";
9
- import { writeFileSync } from "fs";
9
+ import { writeFileSync, readFileSync, unlinkSync } from "fs";
10
10
 
11
11
  // src/logger.ts
12
12
  import fs from "fs";
@@ -292,6 +292,28 @@ function parseFrontmatter(filePath) {
292
292
  return { name: "", description: "", type: "" };
293
293
  }
294
294
  }
295
+ function loadPlanStatus() {
296
+ try {
297
+ const content = fs4.readFileSync(".remy-plan.md", "utf-8");
298
+ const match = content.match(/^---\n([\s\S]*?)\n---/);
299
+ const status = match?.[1]?.match(/^status:\s*(.+)$/m)?.[1]?.trim();
300
+ if (status === "pending") {
301
+ return `
302
+ <pending_plan>
303
+ You have a pending implementation plan in .remy-plan.md awaiting user approval. Do NOT begin implementing the plan until the user approves it. You may continue chatting, answering questions, and revising the plan if asked. To revise, call writePlan again with updated content.
304
+ </pending_plan>`;
305
+ }
306
+ if (status === "approved") {
307
+ return `
308
+ <approved_plan>
309
+ The user has approved your implementation plan in .remy-plan.md. You may reference it during implementation. Delete the file when you have finished all planned work.
310
+ </approved_plan>`;
311
+ }
312
+ return "";
313
+ } catch {
314
+ return "";
315
+ }
316
+ }
295
317
  function loadProjectFileListing() {
296
318
  try {
297
319
  const entries = fs4.readdirSync(".", { withFileTypes: true });
@@ -421,7 +443,7 @@ ${isLspConfigured() ? `<typescript_lsp>
421
443
  </code_authoring_instructions>
422
444
 
423
445
  {{static/instructions.md}}
424
-
446
+ ${loadPlanStatus()}
425
447
  <conversation_summaries>
426
448
  Your conversation history may include <prior_conversation_summary> blocks in the user's messages. These are automated summaries of earlier messages that have been compacted to save context space. The user does not see this summary, they see the full conversation history in their UI. Treat the summary as ground truth for what happened before, but do not reference it directly to the user ("as mentioned in the summary..."). Just continue naturally as if you remember the prior work.
427
449
 
@@ -1363,12 +1385,14 @@ var presentPublishPlanTool = {
1363
1385
  }
1364
1386
  };
1365
1387
 
1366
- // src/tools/spec/presentPlan.ts
1367
- var presentPlanTool = {
1388
+ // src/tools/spec/writePlan.ts
1389
+ import fs9 from "fs/promises";
1390
+ var PLAN_FILE = ".remy-plan.md";
1391
+ var writePlanTool = {
1368
1392
  clearable: false,
1369
1393
  definition: {
1370
- name: "presentPlan",
1371
- description: "Present an implementation plan for user approval before making changes. Use this only for large, multi-step changes like new features, new interface types, or when the user explicitly asks to see a plan. Most work should be done autonomously without a plan. Write a clear markdown summary of what you intend to do in plain language \u2014 describe the changes from the user's perspective, not as a list of files and code paths. If the user rejects with feedback, revise and present again.",
1394
+ name: "writePlan",
1395
+ description: "Write an implementation plan for user approval before making changes. Use this only for large, multi-step changes like new features, new interface types, or when the user explicitly asks to see a plan. Most work should be done autonomously without a plan. Write a clear markdown summary of what you intend to do in plain language \u2014 describe the changes from the user's perspective, not as a list of files and code paths. The plan is saved to .remy-plan.md and the user can review, discuss, and approve or reject it. If the user asks for revisions, call this tool again with updated content to overwrite the plan.",
1372
1396
  inputSchema: {
1373
1397
  type: "object",
1374
1398
  properties: {
@@ -1380,9 +1404,15 @@ var presentPlanTool = {
1380
1404
  required: ["content"]
1381
1405
  }
1382
1406
  },
1383
- streaming: {},
1384
- async execute() {
1385
- return "approved";
1407
+ async execute(input) {
1408
+ const content = input.content;
1409
+ const file = `---
1410
+ status: pending
1411
+ ---
1412
+
1413
+ ${content}`;
1414
+ await fs9.writeFile(PLAN_FILE, file, "utf-8");
1415
+ return "Plan written to .remy-plan.md. Waiting for user approval.";
1386
1416
  }
1387
1417
  };
1388
1418
 
@@ -1546,7 +1576,7 @@ var confirmDestructiveActionTool = {
1546
1576
  clearable: false,
1547
1577
  definition: {
1548
1578
  name: "confirmDestructiveAction",
1549
- description: "Confirm a destructive or irreversible action with the user. Use for things like deleting data, resetting the database, or discarding draft work. Do not use after presentSyncPlan, presentPublishPlan, or presentPlan (those already include approval). Do not use before onboarding state transitions.",
1579
+ description: "Confirm a destructive or irreversible action with the user. Use for things like deleting data, resetting the database, or discarding draft work. Do not use after presentSyncPlan, presentPublishPlan, or writePlan (those already include approval). Do not use before onboarding state transitions.",
1550
1580
  inputSchema: {
1551
1581
  type: "object",
1552
1582
  properties: {
@@ -1763,7 +1793,7 @@ var compactConversationTool = {
1763
1793
  };
1764
1794
 
1765
1795
  // src/tools/code/readFile.ts
1766
- import fs9 from "fs/promises";
1796
+ import fs10 from "fs/promises";
1767
1797
  var DEFAULT_MAX_LINES2 = 500;
1768
1798
  function isBinary(buffer) {
1769
1799
  const sample = buffer.subarray(0, 8192);
@@ -1800,7 +1830,7 @@ var readFileTool = {
1800
1830
  },
1801
1831
  async execute(input) {
1802
1832
  try {
1803
- const buffer = await fs9.readFile(input.path);
1833
+ const buffer = await fs10.readFile(input.path);
1804
1834
  if (isBinary(buffer)) {
1805
1835
  const size = buffer.length;
1806
1836
  const unit = size > 1024 * 1024 ? `${(size / (1024 * 1024)).toFixed(1)}MB` : `${(size / 1024).toFixed(1)}KB`;
@@ -1834,7 +1864,7 @@ var readFileTool = {
1834
1864
  };
1835
1865
 
1836
1866
  // src/tools/code/writeFile.ts
1837
- import fs10 from "fs/promises";
1867
+ import fs11 from "fs/promises";
1838
1868
  import path6 from "path";
1839
1869
  var writeFileTool = {
1840
1870
  clearable: true,
@@ -1872,7 +1902,7 @@ var writeFileTool = {
1872
1902
  lastNewlineCount = newlineCount;
1873
1903
  const lastNewline = partial.content.lastIndexOf("\n");
1874
1904
  const completeContent = partial.content.substring(0, lastNewline + 1);
1875
- const oldContent = await fs10.readFile(partial.path, "utf-8").catch(() => "");
1905
+ const oldContent = await fs11.readFile(partial.path, "utf-8").catch(() => "");
1876
1906
  return `Writing ${partial.path} (${newlineCount} lines)
1877
1907
  ${unifiedDiff(partial.path, oldContent, completeContent)}`;
1878
1908
  }
@@ -1881,13 +1911,13 @@ ${unifiedDiff(partial.path, oldContent, completeContent)}`;
1881
1911
  async execute(input) {
1882
1912
  const release = await acquireFileLock(input.path);
1883
1913
  try {
1884
- await fs10.mkdir(path6.dirname(input.path), { recursive: true });
1914
+ await fs11.mkdir(path6.dirname(input.path), { recursive: true });
1885
1915
  let oldContent = null;
1886
1916
  try {
1887
- oldContent = await fs10.readFile(input.path, "utf-8");
1917
+ oldContent = await fs11.readFile(input.path, "utf-8");
1888
1918
  } catch {
1889
1919
  }
1890
- await fs10.writeFile(input.path, input.content, "utf-8");
1920
+ await fs11.writeFile(input.path, input.content, "utf-8");
1891
1921
  const lineCount = input.content.split("\n").length;
1892
1922
  const label = oldContent !== null ? "Wrote" : "Created";
1893
1923
  return `${label} ${input.path} (${lineCount} lines)
@@ -1901,7 +1931,7 @@ ${unifiedDiff(input.path, oldContent ?? "", input.content)}`;
1901
1931
  };
1902
1932
 
1903
1933
  // src/tools/code/editFile/index.ts
1904
- import fs11 from "fs/promises";
1934
+ import fs12 from "fs/promises";
1905
1935
 
1906
1936
  // src/tools/code/editFile/_helpers.ts
1907
1937
  function buildLineOffsets(content) {
@@ -2014,7 +2044,7 @@ var editFileTool = {
2014
2044
  async execute(input) {
2015
2045
  const release = await acquireFileLock(input.path);
2016
2046
  try {
2017
- const content = await fs11.readFile(input.path, "utf-8");
2047
+ const content = await fs12.readFile(input.path, "utf-8");
2018
2048
  const { old_string, new_string, replace_all } = input;
2019
2049
  const occurrences = findOccurrences(content, old_string);
2020
2050
  if (replace_all) {
@@ -2030,7 +2060,7 @@ var editFileTool = {
2030
2060
  new_string
2031
2061
  );
2032
2062
  }
2033
- await fs11.writeFile(input.path, updated, "utf-8");
2063
+ await fs12.writeFile(input.path, updated, "utf-8");
2034
2064
  return `Replaced ${occurrences.length} occurrence${occurrences.length > 1 ? "s" : ""} in ${input.path}
2035
2065
  ${unifiedDiff(input.path, content, updated)}`;
2036
2066
  }
@@ -2041,7 +2071,7 @@ ${unifiedDiff(input.path, content, updated)}`;
2041
2071
  old_string.length,
2042
2072
  new_string
2043
2073
  );
2044
- await fs11.writeFile(input.path, updated, "utf-8");
2074
+ await fs12.writeFile(input.path, updated, "utf-8");
2045
2075
  return `Updated ${input.path}
2046
2076
  ${unifiedDiff(input.path, content, updated)}`;
2047
2077
  }
@@ -2057,7 +2087,7 @@ ${unifiedDiff(input.path, content, updated)}`;
2057
2087
  flex.matchedText.length,
2058
2088
  new_string
2059
2089
  );
2060
- await fs11.writeFile(input.path, updated, "utf-8");
2090
+ await fs12.writeFile(input.path, updated, "utf-8");
2061
2091
  return `Updated ${input.path} (matched with flexible whitespace at line ${flex.line})
2062
2092
  ${unifiedDiff(input.path, content, updated)}`;
2063
2093
  }
@@ -2268,12 +2298,12 @@ var globTool = {
2268
2298
  };
2269
2299
 
2270
2300
  // src/tools/code/listDir.ts
2271
- import fs12 from "fs/promises";
2301
+ import fs13 from "fs/promises";
2272
2302
  import path7 from "path";
2273
2303
  var EXCLUDE = /* @__PURE__ */ new Set([".git", "node_modules"]);
2274
2304
  var MAX_CHILDREN = 15;
2275
2305
  async function readAndSort(dirPath) {
2276
- const entries = await fs12.readdir(dirPath, { withFileTypes: true });
2306
+ const entries = await fs13.readdir(dirPath, { withFileTypes: true });
2277
2307
  return entries.filter((e) => !EXCLUDE.has(e.name)).sort((a, b) => {
2278
2308
  if (a.isDirectory() && !b.isDirectory()) {
2279
2309
  return -1;
@@ -2314,7 +2344,7 @@ function formatSize(bytes) {
2314
2344
  }
2315
2345
  async function formatFile(dirPath, name, indent) {
2316
2346
  try {
2317
- const stat = await fs12.stat(path7.join(dirPath, name));
2347
+ const stat = await fs13.stat(path7.join(dirPath, name));
2318
2348
  return `${indent}${name}${" ".repeat(Math.max(1, 30 - indent.length - name.length))}${formatSize(stat.size)}`;
2319
2349
  } catch {
2320
2350
  return `${indent}${name}`;
@@ -2352,7 +2382,30 @@ var listDirTool = {
2352
2382
  const capped = children.slice(0, MAX_CHILDREN);
2353
2383
  for (const child of capped) {
2354
2384
  if (child.isDirectory()) {
2355
- lines.push(` ${child.name}/`);
2385
+ const [childDisplay, childFinalPath] = await collapsePath(
2386
+ finalPath,
2387
+ child.name
2388
+ );
2389
+ lines.push(` ${childDisplay}/`);
2390
+ try {
2391
+ const grandchildren = await readAndSort(childFinalPath);
2392
+ const gcCapped = grandchildren.slice(0, MAX_CHILDREN);
2393
+ for (const gc of gcCapped) {
2394
+ if (gc.isDirectory()) {
2395
+ lines.push(` ${gc.name}/`);
2396
+ } else {
2397
+ lines.push(
2398
+ await formatFile(childFinalPath, gc.name, " ")
2399
+ );
2400
+ }
2401
+ }
2402
+ if (grandchildren.length > MAX_CHILDREN) {
2403
+ lines.push(
2404
+ ` ... and ${grandchildren.length - MAX_CHILDREN} more`
2405
+ );
2406
+ }
2407
+ } catch {
2408
+ }
2356
2409
  } else {
2357
2410
  lines.push(await formatFile(finalPath, child.name, " "));
2358
2411
  }
@@ -3358,11 +3411,11 @@ var BROWSER_TOOLS = [
3358
3411
  var BROWSER_EXTERNAL_TOOLS = /* @__PURE__ */ new Set(["browserCommand"]);
3359
3412
 
3360
3413
  // src/subagents/browserAutomation/prompt.ts
3361
- import fs13 from "fs";
3414
+ import fs14 from "fs";
3362
3415
  var BASE_PROMPT = readAsset("subagents/browserAutomation", "prompt.md");
3363
3416
  function getBrowserAutomationPrompt() {
3364
3417
  try {
3365
- const appSpec = fs13.readFileSync("src/app.md", "utf-8").trim();
3418
+ const appSpec = fs14.readFileSync("src/app.md", "utf-8").trim();
3366
3419
  return `${BASE_PROMPT}
3367
3420
 
3368
3421
  <!-- cache_breakpoint -->
@@ -4204,12 +4257,12 @@ async function executeDesignExpertTool(name, input, context, toolCallId, onLog)
4204
4257
  }
4205
4258
 
4206
4259
  // src/subagents/common/context.ts
4207
- import fs14 from "fs";
4260
+ import fs15 from "fs";
4208
4261
  import path8 from "path";
4209
4262
  function walkMdFiles2(dir, skip) {
4210
4263
  const files = [];
4211
4264
  try {
4212
- for (const entry of fs14.readdirSync(dir, { withFileTypes: true })) {
4265
+ for (const entry of fs15.readdirSync(dir, { withFileTypes: true })) {
4213
4266
  const full = path8.join(dir, entry.name);
4214
4267
  if (entry.isDirectory()) {
4215
4268
  if (!skip?.has(entry.name)) {
@@ -4225,7 +4278,7 @@ function walkMdFiles2(dir, skip) {
4225
4278
  }
4226
4279
  function parseFrontmatter2(filePath) {
4227
4280
  try {
4228
- const content = fs14.readFileSync(filePath, "utf-8");
4281
+ const content = fs15.readFileSync(filePath, "utf-8");
4229
4282
  const match = content.match(/^---\n([\s\S]*?)\n---/);
4230
4283
  if (!match) {
4231
4284
  return {};
@@ -4271,7 +4324,7 @@ function loadRoadmapIndex() {
4271
4324
  const parts = [];
4272
4325
  try {
4273
4326
  const indexJson = JSON.parse(
4274
- fs14.readFileSync("src/roadmap/index.json", "utf-8")
4327
+ fs15.readFileSync("src/roadmap/index.json", "utf-8")
4275
4328
  );
4276
4329
  if (indexJson.lanes?.length > 0) {
4277
4330
  const laneLines = indexJson.lanes.map(
@@ -4374,7 +4427,7 @@ The first-party SDK (@mindstudio-ai/agent) provides access to 200+ AI models (Op
4374
4427
  }
4375
4428
 
4376
4429
  // src/subagents/designExpert/data/sampleCache.ts
4377
- import fs15 from "fs";
4430
+ import fs16 from "fs";
4378
4431
  var SAMPLE_FILE = ".remy-design-sample.json";
4379
4432
  var cached = null;
4380
4433
  function generateIndices(poolSize, sampleSize) {
@@ -4388,14 +4441,14 @@ function generateIndices(poolSize, sampleSize) {
4388
4441
  }
4389
4442
  function load() {
4390
4443
  try {
4391
- return JSON.parse(fs15.readFileSync(SAMPLE_FILE, "utf-8"));
4444
+ return JSON.parse(fs16.readFileSync(SAMPLE_FILE, "utf-8"));
4392
4445
  } catch {
4393
4446
  return null;
4394
4447
  }
4395
4448
  }
4396
4449
  function save(indices) {
4397
4450
  try {
4398
- fs15.writeFileSync(SAMPLE_FILE, JSON.stringify(indices));
4451
+ fs16.writeFileSync(SAMPLE_FILE, JSON.stringify(indices));
4399
4452
  } catch {
4400
4453
  }
4401
4454
  }
@@ -4724,7 +4777,7 @@ var VISION_TOOLS = [
4724
4777
  ];
4725
4778
 
4726
4779
  // src/subagents/productVision/executor.ts
4727
- import fs16 from "fs";
4780
+ import fs17 from "fs";
4728
4781
  import path9 from "path";
4729
4782
  var ROADMAP_DIR = "src/roadmap";
4730
4783
  var PITCH_DECK_SHELL = readAsset(
@@ -4739,13 +4792,13 @@ async function executeVisionTool(name, input, context) {
4739
4792
  case "writeFile": {
4740
4793
  const filePath = resolve(input.path);
4741
4794
  try {
4742
- fs16.mkdirSync(ROADMAP_DIR, { recursive: true });
4795
+ fs17.mkdirSync(ROADMAP_DIR, { recursive: true });
4743
4796
  let oldContent = null;
4744
4797
  try {
4745
- oldContent = fs16.readFileSync(filePath, "utf-8");
4798
+ oldContent = fs17.readFileSync(filePath, "utf-8");
4746
4799
  } catch {
4747
4800
  }
4748
- fs16.writeFileSync(filePath, input.content, "utf-8");
4801
+ fs17.writeFileSync(filePath, input.content, "utf-8");
4749
4802
  const lineCount = input.content.split("\n").length;
4750
4803
  const label = oldContent !== null ? "Wrote" : "Created";
4751
4804
  return `${label} ${filePath} (${lineCount} lines)
@@ -4757,11 +4810,11 @@ ${unifiedDiff(filePath, oldContent ?? "", input.content)}`;
4757
4810
  case "deleteFile": {
4758
4811
  const filePath = resolve(input.path);
4759
4812
  try {
4760
- if (!fs16.existsSync(filePath)) {
4813
+ if (!fs17.existsSync(filePath)) {
4761
4814
  return `Error: ${filePath} does not exist`;
4762
4815
  }
4763
- const oldContent = fs16.readFileSync(filePath, "utf-8");
4764
- fs16.unlinkSync(filePath);
4816
+ const oldContent = fs17.readFileSync(filePath, "utf-8");
4817
+ fs17.unlinkSync(filePath);
4765
4818
  return `Deleted ${filePath}
4766
4819
  ${unifiedDiff(filePath, oldContent, "")}`;
4767
4820
  } catch (err) {
@@ -4774,8 +4827,8 @@ ${unifiedDiff(filePath, oldContent, "")}`;
4774
4827
  }
4775
4828
  const filePath = resolve("pitch.html");
4776
4829
  try {
4777
- fs16.mkdirSync(ROADMAP_DIR, { recursive: true });
4778
- const existing = fs16.existsSync(filePath) ? fs16.readFileSync(filePath, "utf-8").trim() : "";
4830
+ fs17.mkdirSync(ROADMAP_DIR, { recursive: true });
4831
+ const existing = fs17.existsSync(filePath) ? fs17.readFileSync(filePath, "utf-8").trim() : "";
4779
4832
  const currentDeck = existing || PITCH_DECK_SHELL;
4780
4833
  const task = `
4781
4834
  <pitch_content>${input.task}</pitch_content>
@@ -4800,7 +4853,7 @@ Respond only with the complete HTML file and absolutely no other text. Your resp
4800
4853
  /```(?:html|wireframe)\n([\s\S]*?)```/
4801
4854
  );
4802
4855
  const html = htmlMatch ? htmlMatch[1].trim() : result;
4803
- fs16.writeFileSync(filePath, html, "utf-8");
4856
+ fs17.writeFileSync(filePath, html, "utf-8");
4804
4857
  return `Pitch deck written successfully.`;
4805
4858
  } catch (err) {
4806
4859
  return `Error generating pitch deck: ${err.message}`;
@@ -5045,7 +5098,7 @@ var ALL_TOOLS = [
5045
5098
  clearSyncStatusTool,
5046
5099
  presentSyncPlanTool,
5047
5100
  presentPublishPlanTool,
5048
- presentPlanTool,
5101
+ writePlanTool,
5049
5102
  // Spec
5050
5103
  readSpecTool,
5051
5104
  writeSpecTool,
@@ -5087,12 +5140,12 @@ function executeTool(name, input, context) {
5087
5140
  }
5088
5141
 
5089
5142
  // src/session.ts
5090
- import fs17 from "fs";
5143
+ import fs18 from "fs";
5091
5144
  var log7 = createLogger("session");
5092
5145
  var SESSION_FILE = ".remy-session.json";
5093
5146
  function loadSession(state) {
5094
5147
  try {
5095
- const raw = fs17.readFileSync(SESSION_FILE, "utf-8");
5148
+ const raw = fs18.readFileSync(SESSION_FILE, "utf-8");
5096
5149
  const data = JSON.parse(raw);
5097
5150
  if (Array.isArray(data.messages) && data.messages.length > 0) {
5098
5151
  state.messages = sanitizeMessages(data.messages);
@@ -5141,7 +5194,7 @@ function sanitizeMessages(messages) {
5141
5194
  }
5142
5195
  function saveSession(state) {
5143
5196
  try {
5144
- fs17.writeFileSync(
5197
+ fs18.writeFileSync(
5145
5198
  SESSION_FILE,
5146
5199
  JSON.stringify({ messages: state.messages }, null, 2),
5147
5200
  "utf-8"
@@ -5154,7 +5207,7 @@ function saveSession(state) {
5154
5207
  function clearSession(state) {
5155
5208
  state.messages = [];
5156
5209
  try {
5157
- fs17.unlinkSync(SESSION_FILE);
5210
+ fs18.unlinkSync(SESSION_FILE);
5158
5211
  } catch {
5159
5212
  }
5160
5213
  }
@@ -5382,7 +5435,6 @@ var EXTERNAL_TOOLS = /* @__PURE__ */ new Set([
5382
5435
  "clearSyncStatus",
5383
5436
  "presentSyncPlan",
5384
5437
  "presentPublishPlan",
5385
- "presentPlan",
5386
5438
  "confirmDestructiveAction",
5387
5439
  "runScenario",
5388
5440
  "runMethod",
@@ -6133,7 +6185,6 @@ ${xmlParts}
6133
6185
  const USER_FACING_TOOLS = /* @__PURE__ */ new Set([
6134
6186
  "promptUser",
6135
6187
  "confirmDestructiveAction",
6136
- "presentPlan",
6137
6188
  "presentSyncPlan",
6138
6189
  "presentPublishPlan"
6139
6190
  ]);
@@ -6351,6 +6402,23 @@ ${xmlParts}
6351
6402
  userMessage = resolved;
6352
6403
  }
6353
6404
  const isHidden = resolved !== null || !!parsed.hidden;
6405
+ const rawText = parsed.text ?? "";
6406
+ if (rawText.startsWith("@@automated::approvePlan@@")) {
6407
+ try {
6408
+ const plan = readFileSync(".remy-plan.md", "utf-8");
6409
+ writeFileSync(
6410
+ ".remy-plan.md",
6411
+ plan.replace(/^status:\s*pending/m, "status: approved"),
6412
+ "utf-8"
6413
+ );
6414
+ } catch {
6415
+ }
6416
+ } else if (rawText.startsWith("@@automated::rejectPlan@@")) {
6417
+ try {
6418
+ unlinkSync(".remy-plan.md");
6419
+ } catch {
6420
+ }
6421
+ }
6354
6422
  const onboardingState = parsed.onboardingState ?? "onboardingFinished";
6355
6423
  const system = buildSystemPrompt(
6356
6424
  onboardingState,
package/dist/index.js CHANGED
@@ -853,16 +853,18 @@ var init_presentPublishPlan = __esm({
853
853
  }
854
854
  });
855
855
 
856
- // src/tools/spec/presentPlan.ts
857
- var presentPlanTool;
858
- var init_presentPlan = __esm({
859
- "src/tools/spec/presentPlan.ts"() {
856
+ // src/tools/spec/writePlan.ts
857
+ import fs6 from "fs/promises";
858
+ var PLAN_FILE, writePlanTool;
859
+ var init_writePlan = __esm({
860
+ "src/tools/spec/writePlan.ts"() {
860
861
  "use strict";
861
- presentPlanTool = {
862
+ PLAN_FILE = ".remy-plan.md";
863
+ writePlanTool = {
862
864
  clearable: false,
863
865
  definition: {
864
- name: "presentPlan",
865
- description: "Present an implementation plan for user approval before making changes. Use this only for large, multi-step changes like new features, new interface types, or when the user explicitly asks to see a plan. Most work should be done autonomously without a plan. Write a clear markdown summary of what you intend to do in plain language \u2014 describe the changes from the user's perspective, not as a list of files and code paths. If the user rejects with feedback, revise and present again.",
866
+ name: "writePlan",
867
+ description: "Write an implementation plan for user approval before making changes. Use this only for large, multi-step changes like new features, new interface types, or when the user explicitly asks to see a plan. Most work should be done autonomously without a plan. Write a clear markdown summary of what you intend to do in plain language \u2014 describe the changes from the user's perspective, not as a list of files and code paths. The plan is saved to .remy-plan.md and the user can review, discuss, and approve or reject it. If the user asks for revisions, call this tool again with updated content to overwrite the plan.",
866
868
  inputSchema: {
867
869
  type: "object",
868
870
  properties: {
@@ -874,9 +876,15 @@ var init_presentPlan = __esm({
874
876
  required: ["content"]
875
877
  }
876
878
  },
877
- streaming: {},
878
- async execute() {
879
- return "approved";
879
+ async execute(input) {
880
+ const content = input.content;
881
+ const file = `---
882
+ status: pending
883
+ ---
884
+
885
+ ${content}`;
886
+ await fs6.writeFile(PLAN_FILE, file, "utf-8");
887
+ return "Plan written to .remy-plan.md. Waiting for user approval.";
880
888
  }
881
889
  };
882
890
  }
@@ -1058,7 +1066,7 @@ var init_confirmDestructiveAction = __esm({
1058
1066
  clearable: false,
1059
1067
  definition: {
1060
1068
  name: "confirmDestructiveAction",
1061
- description: "Confirm a destructive or irreversible action with the user. Use for things like deleting data, resetting the database, or discarding draft work. Do not use after presentSyncPlan, presentPublishPlan, or presentPlan (those already include approval). Do not use before onboarding state transitions.",
1069
+ description: "Confirm a destructive or irreversible action with the user. Use for things like deleting data, resetting the database, or discarding draft work. Do not use after presentSyncPlan, presentPublishPlan, or writePlan (those already include approval). Do not use before onboarding state transitions.",
1062
1070
  inputSchema: {
1063
1071
  type: "object",
1064
1072
  properties: {
@@ -1279,12 +1287,12 @@ var init_setProjectMetadata = __esm({
1279
1287
  });
1280
1288
 
1281
1289
  // src/assets.ts
1282
- import fs6 from "fs";
1290
+ import fs7 from "fs";
1283
1291
  import path3 from "path";
1284
1292
  function findRoot(start) {
1285
1293
  let dir = start;
1286
1294
  while (dir !== path3.dirname(dir)) {
1287
- if (fs6.existsSync(path3.join(dir, "package.json"))) {
1295
+ if (fs7.existsSync(path3.join(dir, "package.json"))) {
1288
1296
  return dir;
1289
1297
  }
1290
1298
  dir = path3.dirname(dir);
@@ -1297,7 +1305,7 @@ function assetPath(...segments) {
1297
1305
  function readAsset(...segments) {
1298
1306
  const full = assetPath(...segments);
1299
1307
  try {
1300
- return fs6.readFileSync(full, "utf-8").trim();
1308
+ return fs7.readFileSync(full, "utf-8").trim();
1301
1309
  } catch {
1302
1310
  throw new Error(`Required asset missing: ${full}`);
1303
1311
  }
@@ -1305,7 +1313,7 @@ function readAsset(...segments) {
1305
1313
  function readJsonAsset(fallback, ...segments) {
1306
1314
  const full = assetPath(...segments);
1307
1315
  try {
1308
- return JSON.parse(fs6.readFileSync(full, "utf-8"));
1316
+ return JSON.parse(fs7.readFileSync(full, "utf-8"));
1309
1317
  } catch {
1310
1318
  return fallback;
1311
1319
  }
@@ -1317,7 +1325,7 @@ var init_assets = __esm({
1317
1325
  ROOT = findRoot(
1318
1326
  import.meta.dirname ?? path3.dirname(new URL(import.meta.url).pathname)
1319
1327
  );
1320
- ASSETS_BASE = fs6.existsSync(path3.join(ROOT, "dist", "prompt")) ? path3.join(ROOT, "dist") : path3.join(ROOT, "src");
1328
+ ASSETS_BASE = fs7.existsSync(path3.join(ROOT, "dist", "prompt")) ? path3.join(ROOT, "dist") : path3.join(ROOT, "src");
1321
1329
  }
1322
1330
  });
1323
1331
 
@@ -1606,12 +1614,12 @@ var init_lsp = __esm({
1606
1614
  });
1607
1615
 
1608
1616
  // src/prompt/static/projectContext.ts
1609
- import fs7 from "fs";
1617
+ import fs8 from "fs";
1610
1618
  import path4 from "path";
1611
1619
  function loadProjectInstructions() {
1612
1620
  for (const file of AGENT_INSTRUCTION_FILES) {
1613
1621
  try {
1614
- const content = fs7.readFileSync(file, "utf-8").trim();
1622
+ const content = fs8.readFileSync(file, "utf-8").trim();
1615
1623
  if (content) {
1616
1624
  return `
1617
1625
  ## Project Instructions (${file})
@@ -1624,7 +1632,7 @@ ${content}`;
1624
1632
  }
1625
1633
  function loadProjectManifest() {
1626
1634
  try {
1627
- const manifest = fs7.readFileSync("mindstudio.json", "utf-8");
1635
+ const manifest = fs8.readFileSync("mindstudio.json", "utf-8");
1628
1636
  return `
1629
1637
  ## Project Manifest (mindstudio.json)
1630
1638
  \`\`\`json
@@ -1665,7 +1673,7 @@ ${entries.join("\n")}`;
1665
1673
  function walkMdFiles(dir) {
1666
1674
  const results = [];
1667
1675
  try {
1668
- const entries = fs7.readdirSync(dir, { withFileTypes: true });
1676
+ const entries = fs8.readdirSync(dir, { withFileTypes: true });
1669
1677
  for (const entry of entries) {
1670
1678
  const full = path4.join(dir, entry.name);
1671
1679
  if (entry.isDirectory()) {
@@ -1680,7 +1688,7 @@ function walkMdFiles(dir) {
1680
1688
  }
1681
1689
  function parseFrontmatter(filePath) {
1682
1690
  try {
1683
- const content = fs7.readFileSync(filePath, "utf-8");
1691
+ const content = fs8.readFileSync(filePath, "utf-8");
1684
1692
  const match = content.match(/^---\n([\s\S]*?)\n---/);
1685
1693
  if (!match) {
1686
1694
  return { name: "", description: "", type: "" };
@@ -1694,9 +1702,31 @@ function parseFrontmatter(filePath) {
1694
1702
  return { name: "", description: "", type: "" };
1695
1703
  }
1696
1704
  }
1705
+ function loadPlanStatus() {
1706
+ try {
1707
+ const content = fs8.readFileSync(".remy-plan.md", "utf-8");
1708
+ const match = content.match(/^---\n([\s\S]*?)\n---/);
1709
+ const status = match?.[1]?.match(/^status:\s*(.+)$/m)?.[1]?.trim();
1710
+ if (status === "pending") {
1711
+ return `
1712
+ <pending_plan>
1713
+ You have a pending implementation plan in .remy-plan.md awaiting user approval. Do NOT begin implementing the plan until the user approves it. You may continue chatting, answering questions, and revising the plan if asked. To revise, call writePlan again with updated content.
1714
+ </pending_plan>`;
1715
+ }
1716
+ if (status === "approved") {
1717
+ return `
1718
+ <approved_plan>
1719
+ The user has approved your implementation plan in .remy-plan.md. You may reference it during implementation. Delete the file when you have finished all planned work.
1720
+ </approved_plan>`;
1721
+ }
1722
+ return "";
1723
+ } catch {
1724
+ return "";
1725
+ }
1726
+ }
1697
1727
  function loadProjectFileListing() {
1698
1728
  try {
1699
- const entries = fs7.readdirSync(".", { withFileTypes: true });
1729
+ const entries = fs8.readdirSync(".", { withFileTypes: true });
1700
1730
  const listing = entries.filter((e) => e.name !== ".git" && e.name !== "node_modules").sort((a, b) => {
1701
1731
  if (a.isDirectory() && !b.isDirectory()) {
1702
1732
  return -1;
@@ -1845,7 +1875,7 @@ ${isLspConfigured() ? `<typescript_lsp>
1845
1875
  </code_authoring_instructions>
1846
1876
 
1847
1877
  {{static/instructions.md}}
1848
-
1878
+ ${loadPlanStatus()}
1849
1879
  <conversation_summaries>
1850
1880
  Your conversation history may include <prior_conversation_summary> blocks in the user's messages. These are automated summaries of earlier messages that have been compacted to save context space. The user does not see this summary, they see the full conversation history in their UI. Treat the summary as ground truth for what happened before, but do not reference it directly to the user ("as mentioned in the summary..."). Just continue naturally as if you remember the prior work.
1851
1881
 
@@ -1884,10 +1914,10 @@ var init_prompt = __esm({
1884
1914
  });
1885
1915
 
1886
1916
  // src/session.ts
1887
- import fs8 from "fs";
1917
+ import fs9 from "fs";
1888
1918
  function loadSession(state) {
1889
1919
  try {
1890
- const raw = fs8.readFileSync(SESSION_FILE, "utf-8");
1920
+ const raw = fs9.readFileSync(SESSION_FILE, "utf-8");
1891
1921
  const data = JSON.parse(raw);
1892
1922
  if (Array.isArray(data.messages) && data.messages.length > 0) {
1893
1923
  state.messages = sanitizeMessages(data.messages);
@@ -1936,7 +1966,7 @@ function sanitizeMessages(messages) {
1936
1966
  }
1937
1967
  function saveSession(state) {
1938
1968
  try {
1939
- fs8.writeFileSync(
1969
+ fs9.writeFileSync(
1940
1970
  SESSION_FILE,
1941
1971
  JSON.stringify({ messages: state.messages }, null, 2),
1942
1972
  "utf-8"
@@ -1949,7 +1979,7 @@ function saveSession(state) {
1949
1979
  function clearSession(state) {
1950
1980
  state.messages = [];
1951
1981
  try {
1952
- fs8.unlinkSync(SESSION_FILE);
1982
+ fs9.unlinkSync(SESSION_FILE);
1953
1983
  } catch {
1954
1984
  }
1955
1985
  }
@@ -2023,7 +2053,7 @@ var init_compactConversation = __esm({
2023
2053
  });
2024
2054
 
2025
2055
  // src/tools/code/readFile.ts
2026
- import fs9 from "fs/promises";
2056
+ import fs10 from "fs/promises";
2027
2057
  function isBinary(buffer) {
2028
2058
  const sample = buffer.subarray(0, 8192);
2029
2059
  for (let i = 0; i < sample.length; i++) {
@@ -2064,7 +2094,7 @@ var init_readFile = __esm({
2064
2094
  },
2065
2095
  async execute(input) {
2066
2096
  try {
2067
- const buffer = await fs9.readFile(input.path);
2097
+ const buffer = await fs10.readFile(input.path);
2068
2098
  if (isBinary(buffer)) {
2069
2099
  const size = buffer.length;
2070
2100
  const unit = size > 1024 * 1024 ? `${(size / (1024 * 1024)).toFixed(1)}MB` : `${(size / 1024).toFixed(1)}KB`;
@@ -2100,7 +2130,7 @@ var init_readFile = __esm({
2100
2130
  });
2101
2131
 
2102
2132
  // src/tools/code/writeFile.ts
2103
- import fs10 from "fs/promises";
2133
+ import fs11 from "fs/promises";
2104
2134
  import path5 from "path";
2105
2135
  var writeFileTool;
2106
2136
  var init_writeFile = __esm({
@@ -2144,7 +2174,7 @@ var init_writeFile = __esm({
2144
2174
  lastNewlineCount = newlineCount;
2145
2175
  const lastNewline = partial.content.lastIndexOf("\n");
2146
2176
  const completeContent = partial.content.substring(0, lastNewline + 1);
2147
- const oldContent = await fs10.readFile(partial.path, "utf-8").catch(() => "");
2177
+ const oldContent = await fs11.readFile(partial.path, "utf-8").catch(() => "");
2148
2178
  return `Writing ${partial.path} (${newlineCount} lines)
2149
2179
  ${unifiedDiff(partial.path, oldContent, completeContent)}`;
2150
2180
  }
@@ -2153,13 +2183,13 @@ ${unifiedDiff(partial.path, oldContent, completeContent)}`;
2153
2183
  async execute(input) {
2154
2184
  const release = await acquireFileLock(input.path);
2155
2185
  try {
2156
- await fs10.mkdir(path5.dirname(input.path), { recursive: true });
2186
+ await fs11.mkdir(path5.dirname(input.path), { recursive: true });
2157
2187
  let oldContent = null;
2158
2188
  try {
2159
- oldContent = await fs10.readFile(input.path, "utf-8");
2189
+ oldContent = await fs11.readFile(input.path, "utf-8");
2160
2190
  } catch {
2161
2191
  }
2162
- await fs10.writeFile(input.path, input.content, "utf-8");
2192
+ await fs11.writeFile(input.path, input.content, "utf-8");
2163
2193
  const lineCount = input.content.split("\n").length;
2164
2194
  const label = oldContent !== null ? "Wrote" : "Created";
2165
2195
  return `${label} ${input.path} (${lineCount} lines)
@@ -2259,7 +2289,7 @@ var init_helpers2 = __esm({
2259
2289
  });
2260
2290
 
2261
2291
  // src/tools/code/editFile/index.ts
2262
- import fs11 from "fs/promises";
2292
+ import fs12 from "fs/promises";
2263
2293
  var editFileTool;
2264
2294
  var init_editFile = __esm({
2265
2295
  "src/tools/code/editFile/index.ts"() {
@@ -2298,7 +2328,7 @@ var init_editFile = __esm({
2298
2328
  async execute(input) {
2299
2329
  const release = await acquireFileLock(input.path);
2300
2330
  try {
2301
- const content = await fs11.readFile(input.path, "utf-8");
2331
+ const content = await fs12.readFile(input.path, "utf-8");
2302
2332
  const { old_string, new_string, replace_all } = input;
2303
2333
  const occurrences = findOccurrences(content, old_string);
2304
2334
  if (replace_all) {
@@ -2314,7 +2344,7 @@ var init_editFile = __esm({
2314
2344
  new_string
2315
2345
  );
2316
2346
  }
2317
- await fs11.writeFile(input.path, updated, "utf-8");
2347
+ await fs12.writeFile(input.path, updated, "utf-8");
2318
2348
  return `Replaced ${occurrences.length} occurrence${occurrences.length > 1 ? "s" : ""} in ${input.path}
2319
2349
  ${unifiedDiff(input.path, content, updated)}`;
2320
2350
  }
@@ -2325,7 +2355,7 @@ ${unifiedDiff(input.path, content, updated)}`;
2325
2355
  old_string.length,
2326
2356
  new_string
2327
2357
  );
2328
- await fs11.writeFile(input.path, updated, "utf-8");
2358
+ await fs12.writeFile(input.path, updated, "utf-8");
2329
2359
  return `Updated ${input.path}
2330
2360
  ${unifiedDiff(input.path, content, updated)}`;
2331
2361
  }
@@ -2341,7 +2371,7 @@ ${unifiedDiff(input.path, content, updated)}`;
2341
2371
  flex.matchedText.length,
2342
2372
  new_string
2343
2373
  );
2344
- await fs11.writeFile(input.path, updated, "utf-8");
2374
+ await fs12.writeFile(input.path, updated, "utf-8");
2345
2375
  return `Updated ${input.path} (matched with flexible whitespace at line ${flex.line})
2346
2376
  ${unifiedDiff(input.path, content, updated)}`;
2347
2377
  }
@@ -2572,10 +2602,10 @@ var init_glob = __esm({
2572
2602
  });
2573
2603
 
2574
2604
  // src/tools/code/listDir.ts
2575
- import fs12 from "fs/promises";
2605
+ import fs13 from "fs/promises";
2576
2606
  import path6 from "path";
2577
2607
  async function readAndSort(dirPath) {
2578
- const entries = await fs12.readdir(dirPath, { withFileTypes: true });
2608
+ const entries = await fs13.readdir(dirPath, { withFileTypes: true });
2579
2609
  return entries.filter((e) => !EXCLUDE.has(e.name)).sort((a, b) => {
2580
2610
  if (a.isDirectory() && !b.isDirectory()) {
2581
2611
  return -1;
@@ -2616,7 +2646,7 @@ function formatSize(bytes) {
2616
2646
  }
2617
2647
  async function formatFile(dirPath, name, indent) {
2618
2648
  try {
2619
- const stat = await fs12.stat(path6.join(dirPath, name));
2649
+ const stat = await fs13.stat(path6.join(dirPath, name));
2620
2650
  return `${indent}${name}${" ".repeat(Math.max(1, 30 - indent.length - name.length))}${formatSize(stat.size)}`;
2621
2651
  } catch {
2622
2652
  return `${indent}${name}`;
@@ -2660,7 +2690,30 @@ var init_listDir = __esm({
2660
2690
  const capped = children.slice(0, MAX_CHILDREN);
2661
2691
  for (const child of capped) {
2662
2692
  if (child.isDirectory()) {
2663
- lines.push(` ${child.name}/`);
2693
+ const [childDisplay, childFinalPath] = await collapsePath(
2694
+ finalPath,
2695
+ child.name
2696
+ );
2697
+ lines.push(` ${childDisplay}/`);
2698
+ try {
2699
+ const grandchildren = await readAndSort(childFinalPath);
2700
+ const gcCapped = grandchildren.slice(0, MAX_CHILDREN);
2701
+ for (const gc of gcCapped) {
2702
+ if (gc.isDirectory()) {
2703
+ lines.push(` ${gc.name}/`);
2704
+ } else {
2705
+ lines.push(
2706
+ await formatFile(childFinalPath, gc.name, " ")
2707
+ );
2708
+ }
2709
+ }
2710
+ if (grandchildren.length > MAX_CHILDREN) {
2711
+ lines.push(
2712
+ ` ... and ${grandchildren.length - MAX_CHILDREN} more`
2713
+ );
2714
+ }
2715
+ } catch {
2716
+ }
2664
2717
  } else {
2665
2718
  lines.push(await formatFile(finalPath, child.name, " "));
2666
2719
  }
@@ -3754,10 +3807,10 @@ var init_tools = __esm({
3754
3807
  });
3755
3808
 
3756
3809
  // src/subagents/browserAutomation/prompt.ts
3757
- import fs13 from "fs";
3810
+ import fs14 from "fs";
3758
3811
  function getBrowserAutomationPrompt() {
3759
3812
  try {
3760
- const appSpec = fs13.readFileSync("src/app.md", "utf-8").trim();
3813
+ const appSpec = fs14.readFileSync("src/app.md", "utf-8").trim();
3761
3814
  return `${BASE_PROMPT}
3762
3815
 
3763
3816
  <!-- cache_breakpoint -->
@@ -4721,12 +4774,12 @@ var init_tools3 = __esm({
4721
4774
  });
4722
4775
 
4723
4776
  // src/subagents/common/context.ts
4724
- import fs14 from "fs";
4777
+ import fs15 from "fs";
4725
4778
  import path7 from "path";
4726
4779
  function walkMdFiles2(dir, skip) {
4727
4780
  const files = [];
4728
4781
  try {
4729
- for (const entry of fs14.readdirSync(dir, { withFileTypes: true })) {
4782
+ for (const entry of fs15.readdirSync(dir, { withFileTypes: true })) {
4730
4783
  const full = path7.join(dir, entry.name);
4731
4784
  if (entry.isDirectory()) {
4732
4785
  if (!skip?.has(entry.name)) {
@@ -4742,7 +4795,7 @@ function walkMdFiles2(dir, skip) {
4742
4795
  }
4743
4796
  function parseFrontmatter2(filePath) {
4744
4797
  try {
4745
- const content = fs14.readFileSync(filePath, "utf-8");
4798
+ const content = fs15.readFileSync(filePath, "utf-8");
4746
4799
  const match = content.match(/^---\n([\s\S]*?)\n---/);
4747
4800
  if (!match) {
4748
4801
  return {};
@@ -4788,7 +4841,7 @@ function loadRoadmapIndex() {
4788
4841
  const parts = [];
4789
4842
  try {
4790
4843
  const indexJson = JSON.parse(
4791
- fs14.readFileSync("src/roadmap/index.json", "utf-8")
4844
+ fs15.readFileSync("src/roadmap/index.json", "utf-8")
4792
4845
  );
4793
4846
  if (indexJson.lanes?.length > 0) {
4794
4847
  const laneLines = indexJson.lanes.map(
@@ -4896,7 +4949,7 @@ var init_context = __esm({
4896
4949
  });
4897
4950
 
4898
4951
  // src/subagents/designExpert/data/sampleCache.ts
4899
- import fs15 from "fs";
4952
+ import fs16 from "fs";
4900
4953
  function generateIndices(poolSize, sampleSize) {
4901
4954
  const n = Math.min(sampleSize, poolSize);
4902
4955
  const indices = Array.from({ length: poolSize }, (_, i) => i);
@@ -4908,14 +4961,14 @@ function generateIndices(poolSize, sampleSize) {
4908
4961
  }
4909
4962
  function load() {
4910
4963
  try {
4911
- return JSON.parse(fs15.readFileSync(SAMPLE_FILE, "utf-8"));
4964
+ return JSON.parse(fs16.readFileSync(SAMPLE_FILE, "utf-8"));
4912
4965
  } catch {
4913
4966
  return null;
4914
4967
  }
4915
4968
  }
4916
4969
  function save(indices) {
4917
4970
  try {
4918
- fs15.writeFileSync(SAMPLE_FILE, JSON.stringify(indices));
4971
+ fs16.writeFileSync(SAMPLE_FILE, JSON.stringify(indices));
4919
4972
  } catch {
4920
4973
  }
4921
4974
  }
@@ -5312,7 +5365,7 @@ var init_tools4 = __esm({
5312
5365
  });
5313
5366
 
5314
5367
  // src/subagents/productVision/executor.ts
5315
- import fs16 from "fs";
5368
+ import fs17 from "fs";
5316
5369
  import path8 from "path";
5317
5370
  function resolve(filePath) {
5318
5371
  return path8.join(ROADMAP_DIR, filePath);
@@ -5322,13 +5375,13 @@ async function executeVisionTool(name, input, context) {
5322
5375
  case "writeFile": {
5323
5376
  const filePath = resolve(input.path);
5324
5377
  try {
5325
- fs16.mkdirSync(ROADMAP_DIR, { recursive: true });
5378
+ fs17.mkdirSync(ROADMAP_DIR, { recursive: true });
5326
5379
  let oldContent = null;
5327
5380
  try {
5328
- oldContent = fs16.readFileSync(filePath, "utf-8");
5381
+ oldContent = fs17.readFileSync(filePath, "utf-8");
5329
5382
  } catch {
5330
5383
  }
5331
- fs16.writeFileSync(filePath, input.content, "utf-8");
5384
+ fs17.writeFileSync(filePath, input.content, "utf-8");
5332
5385
  const lineCount = input.content.split("\n").length;
5333
5386
  const label = oldContent !== null ? "Wrote" : "Created";
5334
5387
  return `${label} ${filePath} (${lineCount} lines)
@@ -5340,11 +5393,11 @@ ${unifiedDiff(filePath, oldContent ?? "", input.content)}`;
5340
5393
  case "deleteFile": {
5341
5394
  const filePath = resolve(input.path);
5342
5395
  try {
5343
- if (!fs16.existsSync(filePath)) {
5396
+ if (!fs17.existsSync(filePath)) {
5344
5397
  return `Error: ${filePath} does not exist`;
5345
5398
  }
5346
- const oldContent = fs16.readFileSync(filePath, "utf-8");
5347
- fs16.unlinkSync(filePath);
5399
+ const oldContent = fs17.readFileSync(filePath, "utf-8");
5400
+ fs17.unlinkSync(filePath);
5348
5401
  return `Deleted ${filePath}
5349
5402
  ${unifiedDiff(filePath, oldContent, "")}`;
5350
5403
  } catch (err) {
@@ -5357,8 +5410,8 @@ ${unifiedDiff(filePath, oldContent, "")}`;
5357
5410
  }
5358
5411
  const filePath = resolve("pitch.html");
5359
5412
  try {
5360
- fs16.mkdirSync(ROADMAP_DIR, { recursive: true });
5361
- const existing = fs16.existsSync(filePath) ? fs16.readFileSync(filePath, "utf-8").trim() : "";
5413
+ fs17.mkdirSync(ROADMAP_DIR, { recursive: true });
5414
+ const existing = fs17.existsSync(filePath) ? fs17.readFileSync(filePath, "utf-8").trim() : "";
5362
5415
  const currentDeck = existing || PITCH_DECK_SHELL;
5363
5416
  const task = `
5364
5417
  <pitch_content>${input.task}</pitch_content>
@@ -5383,7 +5436,7 @@ Respond only with the complete HTML file and absolutely no other text. Your resp
5383
5436
  /```(?:html|wireframe)\n([\s\S]*?)```/
5384
5437
  );
5385
5438
  const html = htmlMatch ? htmlMatch[1].trim() : result;
5386
- fs16.writeFileSync(filePath, html, "utf-8");
5439
+ fs17.writeFileSync(filePath, html, "utf-8");
5387
5440
  return `Pitch deck written successfully.`;
5388
5441
  } catch (err) {
5389
5442
  return `Error generating pitch deck: ${err.message}`;
@@ -5697,7 +5750,7 @@ var init_tools6 = __esm({
5697
5750
  init_clearSyncStatus();
5698
5751
  init_presentSyncPlan();
5699
5752
  init_presentPublishPlan();
5700
- init_presentPlan();
5753
+ init_writePlan();
5701
5754
  init_setProjectOnboardingState();
5702
5755
  init_promptUser();
5703
5756
  init_confirmDestructiveAction();
@@ -5741,7 +5794,7 @@ var init_tools6 = __esm({
5741
5794
  clearSyncStatusTool,
5742
5795
  presentSyncPlanTool,
5743
5796
  presentPublishPlanTool,
5744
- presentPlanTool,
5797
+ writePlanTool,
5745
5798
  // Spec
5746
5799
  readSpecTool,
5747
5800
  writeSpecTool,
@@ -6497,7 +6550,6 @@ var init_agent = __esm({
6497
6550
  "clearSyncStatus",
6498
6551
  "presentSyncPlan",
6499
6552
  "presentPublishPlan",
6500
- "presentPlan",
6501
6553
  "confirmDestructiveAction",
6502
6554
  "runScenario",
6503
6555
  "runMethod",
@@ -6509,12 +6561,12 @@ var init_agent = __esm({
6509
6561
  });
6510
6562
 
6511
6563
  // src/config.ts
6512
- import fs17 from "fs";
6564
+ import fs18 from "fs";
6513
6565
  import path9 from "path";
6514
6566
  import os from "os";
6515
6567
  function loadConfigFile() {
6516
6568
  try {
6517
- const raw = fs17.readFileSync(CONFIG_PATH, "utf-8");
6569
+ const raw = fs18.readFileSync(CONFIG_PATH, "utf-8");
6518
6570
  log9.debug("Loaded config file", { path: CONFIG_PATH });
6519
6571
  return JSON.parse(raw);
6520
6572
  } catch (err) {
@@ -6684,7 +6736,7 @@ __export(headless_exports, {
6684
6736
  startHeadless: () => startHeadless
6685
6737
  });
6686
6738
  import { createInterface } from "readline";
6687
- import { writeFileSync } from "fs";
6739
+ import { writeFileSync, readFileSync, unlinkSync } from "fs";
6688
6740
  function emit(event, data, requestId) {
6689
6741
  const payload = { event, ...data };
6690
6742
  if (requestId) {
@@ -6825,7 +6877,6 @@ ${xmlParts}
6825
6877
  const USER_FACING_TOOLS = /* @__PURE__ */ new Set([
6826
6878
  "promptUser",
6827
6879
  "confirmDestructiveAction",
6828
- "presentPlan",
6829
6880
  "presentSyncPlan",
6830
6881
  "presentPublishPlan"
6831
6882
  ]);
@@ -7043,6 +7094,23 @@ ${xmlParts}
7043
7094
  userMessage = resolved;
7044
7095
  }
7045
7096
  const isHidden = resolved !== null || !!parsed.hidden;
7097
+ const rawText = parsed.text ?? "";
7098
+ if (rawText.startsWith("@@automated::approvePlan@@")) {
7099
+ try {
7100
+ const plan = readFileSync(".remy-plan.md", "utf-8");
7101
+ writeFileSync(
7102
+ ".remy-plan.md",
7103
+ plan.replace(/^status:\s*pending/m, "status: approved"),
7104
+ "utf-8"
7105
+ );
7106
+ } catch {
7107
+ }
7108
+ } else if (rawText.startsWith("@@automated::rejectPlan@@")) {
7109
+ try {
7110
+ unlinkSync(".remy-plan.md");
7111
+ } catch {
7112
+ }
7113
+ }
7046
7114
  const onboardingState = parsed.onboardingState ?? "onboardingFinished";
7047
7115
  const system = buildSystemPrompt(
7048
7116
  onboardingState,
@@ -7240,7 +7308,7 @@ var init_headless = __esm({
7240
7308
  // src/index.tsx
7241
7309
  import { render } from "ink";
7242
7310
  import os2 from "os";
7243
- import fs18 from "fs";
7311
+ import fs19 from "fs";
7244
7312
  import path10 from "path";
7245
7313
 
7246
7314
  // src/tui/App.tsx
@@ -7559,7 +7627,7 @@ for (let i = 0; i < args.length; i++) {
7559
7627
  var startupLog = createLogger("startup");
7560
7628
  function printDebugInfo(config) {
7561
7629
  const pkg = JSON.parse(
7562
- fs18.readFileSync(
7630
+ fs19.readFileSync(
7563
7631
  path10.join(import.meta.dirname, "..", "package.json"),
7564
7632
  "utf-8"
7565
7633
  )
@@ -245,20 +245,54 @@ export function getApprovalState(approvals: Approval[]) {
245
245
 
246
246
  ## Streaming
247
247
 
248
- Methods can stream token-by-token output (useful for AI-generated content):
248
+ Methods can push real-time updates to the frontend using `stream()`. This is the standard pattern for any method that takes more than a few seconds.
249
249
 
250
250
  ```typescript
251
- // Frontend
252
- const result = await api.generateReport(
253
- { month: 'march' },
251
+ import { mindstudio, stream } from '@mindstudio-ai/agent';
252
+
253
+ export async function enrichProfile(input: { name: string }) {
254
+ await stream('Researching...');
255
+
256
+ const { content } = await mindstudio.generateText(
257
+ { message: `Find background info on ${input.name}` },
258
+ { onLog: (event) => stream({ status: event.value }) },
259
+ );
260
+
261
+ await stream({ status: 'generating_image', progress: 0.5 });
262
+
263
+ const { imageUrl } = await mindstudio.generateImage(
264
+ { prompt: `Professional portrait illustration of ${input.name}` },
265
+ { onLog: (event) => stream({ status: event.value }) },
266
+ );
267
+
268
+ return { bio: content, imageUrl };
269
+ }
270
+ ```
271
+
272
+ Two data types:
273
+ - `stream('text')` sends a text token (like LLM streaming output)
274
+ - `stream({ ... })` sends structured data (progress, status, intermediate results)
275
+
276
+ Every SDK action accepts an `onLog` callback that emits execution progress. Pipe it through `stream()` so the frontend sees what's happening inside each action in real time. Use `stream()` directly for your own status messages between actions.
277
+
278
+ When there's no active stream (method not called with `stream: true`, CLI execution, background jobs), `stream()` is a silent no-op. Always safe to include unconditionally.
279
+
280
+ ### Frontend
281
+
282
+ The frontend calls the method with `stream: true` and receives updates via `onToken`. The `text` value is accumulated (not a delta), so replace your display content each time.
283
+
284
+ ```typescript
285
+ const result = await api.enrichProfile(
286
+ { name: 'Alice' },
254
287
  {
255
288
  stream: true,
256
- onToken: (text) => setPreview(text),
289
+ onToken: (text) => setResponseText(text),
257
290
  },
258
291
  );
292
+ // result is the same final output you'd get without streaming
259
293
  ```
260
294
 
261
- The platform handles the SSE transport. The method returns normally streaming is managed by the SDK and platform, not by your method code.
295
+ Use `onStreamError` for transient error handling. The method's promise still resolves with the final return value once execution completes.
262
296
 
263
297
  ## Raw Request Context (API Interface)
264
298
 
@@ -30,6 +30,8 @@ For any work involving AI models, external actions (web scraping, email, SMS), o
30
30
 
31
31
  For multi-step tasks with branching logic (research, enrichment, content pipelines), use `runTask()` instead of manually chaining SDK actions. It runs an autonomous agent loop that composes actions, retries on failure, and returns structured JSON. See the task agents reference for details.
32
32
 
33
+ For methods that take more than a few seconds, use `stream()` from `@mindstudio-ai/agent` to push real-time progress to the frontend. Pipe `onLog` from SDK actions through `stream()` so users see what's happening. The frontend calls the method with `stream: true` and gets updates via `onToken`. See the methods reference for the full pattern.
34
+
33
35
  ### Auth
34
36
  - Not every app needs auth, and even for apps that do need auth, not every screen needs auth. Think intentionally about places where auth is required. Don't make auth be the first thing a user sees - that's jarring. Only show auth at intuitive and natural moments in the user's journey - be thoughtful about how to implement auth in the UI.
35
37
  - Frontend interfaces are always untrusted. Always enforce auth in backend methods. Use frontend auth and role information as a hint to conditionally show/hide UI to make the experience pleasant and seamless for users depending on their state, but remember to always use backend methods for gating data that is conditional on auth.
@@ -9,7 +9,7 @@
9
9
  - Always keep the spec up to date after making changes to the code, especially when adding features or building things from the roadmap.
10
10
  - Change only what the task requires. Match existing styles. Keep solutions simple.
11
11
  - Read files before editing them. Understand the context before making changes.
12
- - When the user asks you to make a change, execute it fully — all steps, no pausing for confirmation. Use `confirmDestructiveAction` to gate before destructive or irreversible actions (e.g., deleting data, resetting the database). For large changes that touch many files or involve significant design decisions, use `presentPlan` to get user approval first — but only when the scope genuinely warrants it or the user asks to see a plan. Most work should be done autonomously.
12
+ - When the user asks you to make a change, execute it fully — all steps, no pausing for confirmation. Use `confirmDestructiveAction` to gate before destructive or irreversible actions (e.g., deleting data, resetting the database). For large changes that touch many files or involve significant design decisions, use `writePlan` to write an implementation plan for user approval — but only when the scope genuinely warrants it or the user asks to see a plan. The plan is saved to `.remy-plan.md` and the user can review, discuss, and refine it before approving. Do not begin implementation until the plan is approved. Most work should be done autonomously without a plan.
13
13
  - Work with what you already know. If you've read a file in this session, use what you learned rather than reading it again. If a subagent already researched something, use its findings. Every tool call costs time; prefer acting on information you have over re-gathering it.
14
14
  - When multiple tool calls are independent, make them all in a single turn. Reading three files, writing two methods, or running a scenario while taking a screenshot: batch them instead of doing one per turn.
15
15
  - After two failed attempts at the same approach, tell the user what's going wrong.
@@ -17,7 +17,7 @@
17
17
  - Pushing to main branch will trigger a deploy. The user presses the publish button in the interface to request publishing.
18
18
 
19
19
  ### Build Notes
20
- For complex tasks — especially an initial buildout from a spec or making multiple changes in a single turn — write a `.remy-notes.md` scratchpad in the project root. Use it to track progress: a checklist of what's been built and what's remaining. Do not include implementation details or other decisions in the notes - it is solely for keeping track of tasks. Read the spec files directly when you need design details, implementation decisions, or other reference materials - never write them to the notes file. Delete the notes file when your work is done.
20
+ For complex tasks — especially an initial buildout from a spec or making multiple changes in a single turn — write a `.remy-notes.md` scratchpad in the project root. Use it to track progress: a checklist of what's been built and what's remaining. Do not include implementation details or other decisions in the notes - it is solely for keeping track of tasks. Read the spec files directly when you need design details, implementation decisions, or other reference materials - never write them to the notes file. Delete the notes file when your work is done. When implementing an approved plan, `.remy-plan.md` serves as your reference. Delete it when all planned work is complete.
21
21
 
22
22
  ## Communication
23
23
  The user can already see your tool calls, so most of your work is visible without narration. Focus text output on three things:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mindstudio-ai/remy",
3
- "version": "0.1.139",
3
+ "version": "0.1.141",
4
4
  "description": "MindStudio coding agent",
5
5
  "repository": {
6
6
  "type": "git",