@mindstudio-ai/remy 0.1.239 → 0.1.240

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/index.js CHANGED
@@ -1522,7 +1522,7 @@ var init_assets = __esm({
1522
1522
  });
1523
1523
 
1524
1524
  // src/compaction/index.ts
1525
- async function compactConversation(messages, apiConfig, system, tools2, model) {
1525
+ async function compactConversation(messages, apiConfig, model) {
1526
1526
  const endIndex = findSafeInsertionPoint(messages);
1527
1527
  const summaries = [];
1528
1528
  const tasks = [];
@@ -1530,6 +1530,7 @@ async function compactConversation(messages, apiConfig, system, tools2, model) {
1530
1530
  messages,
1531
1531
  endIndex
1532
1532
  );
1533
+ let conversationFailed = false;
1533
1534
  if (conversationMessages.length > 0) {
1534
1535
  tasks.push(
1535
1536
  generateSummary(
@@ -1537,12 +1538,12 @@ async function compactConversation(messages, apiConfig, system, tools2, model) {
1537
1538
  "conversation",
1538
1539
  CONVERSATION_SUMMARY_PROMPT,
1539
1540
  conversationMessages,
1540
- system,
1541
- tools2,
1542
1541
  model
1543
1542
  ).then((text) => {
1544
1543
  if (text) {
1545
1544
  summaries.push({ name: "conversation", text });
1545
+ } else {
1546
+ conversationFailed = true;
1546
1547
  }
1547
1548
  })
1548
1549
  );
@@ -1560,18 +1561,25 @@ async function compactConversation(messages, apiConfig, system, tools2, model) {
1560
1561
  name,
1561
1562
  SUBAGENT_SUMMARY_PROMPT,
1562
1563
  subagentMessages,
1563
- system,
1564
- tools2,
1565
1564
  model
1566
1565
  ).then((text) => {
1567
1566
  if (text) {
1568
1567
  summaries.push({ name, text });
1568
+ } else {
1569
+ log2.warn("Subagent summary unusable \u2014 leaving its history intact", {
1570
+ name
1571
+ });
1569
1572
  }
1570
1573
  })
1571
1574
  );
1572
1575
  }
1573
1576
  }
1574
1577
  await Promise.all(tasks);
1578
+ if (conversationFailed) {
1579
+ throw new Error(
1580
+ "Could not summarize the conversation \u2014 the model did not return a usable summary. History left intact."
1581
+ );
1582
+ }
1575
1583
  const checkpointMessages = summaries.map((s) => ({
1576
1584
  role: "user",
1577
1585
  hidden: true,
@@ -1716,65 +1724,106 @@ function serializeForSummary(messages) {
1716
1724
  }
1717
1725
  return lines.join("\n\n");
1718
1726
  }
1719
- async function generateSummary(apiConfig, name, compactionPrompt, messagesToSummarize, mainSystem, mainTools, model) {
1727
+ async function generateSummary(apiConfig, name, compactionPrompt, messagesToSummarize, model, opts = {}) {
1720
1728
  const serialized = serializeForSummary(messagesToSummarize);
1721
1729
  if (!serialized.trim()) {
1722
1730
  return null;
1723
1731
  }
1724
- if (serialized.length > CHUNK_CHAR_LIMIT && messagesToSummarize.length > 1) {
1732
+ const splittable = messagesToSummarize.length > 1;
1733
+ const allowRetry = opts.allowRetry ?? true;
1734
+ if (splittable && (opts.forceChunk || serialized.length > CHUNK_CHAR_LIMIT)) {
1725
1735
  const mid = Math.floor(messagesToSummarize.length / 2);
1736
+ const halves = [
1737
+ messagesToSummarize.slice(0, mid),
1738
+ messagesToSummarize.slice(mid)
1739
+ ];
1726
1740
  log2.info("Chunking summary", {
1727
1741
  name,
1728
1742
  messageCount: messagesToSummarize.length,
1729
- serializedLength: serialized.length
1743
+ serializedLength: serialized.length,
1744
+ forced: !!opts.forceChunk
1730
1745
  });
1731
- const [first, second] = await Promise.all([
1732
- generateSummary(
1733
- apiConfig,
1734
- `${name} [pt1]`,
1735
- compactionPrompt,
1736
- messagesToSummarize.slice(0, mid),
1737
- mainSystem,
1738
- mainTools,
1739
- model
1740
- ),
1741
- generateSummary(
1742
- apiConfig,
1743
- `${name} [pt2]`,
1744
- compactionPrompt,
1745
- messagesToSummarize.slice(mid),
1746
- mainSystem,
1747
- mainTools,
1748
- model
1746
+ const results = await Promise.all(
1747
+ halves.map(
1748
+ (half, i) => generateSummary(
1749
+ apiConfig,
1750
+ `${name} [pt${i + 1}]`,
1751
+ compactionPrompt,
1752
+ half,
1753
+ model,
1754
+ // A split driven by size gets its children a retry of their own. A
1755
+ // split that IS the retry does not, or a model that will not
1756
+ // summarize at any size fans out call after call before giving up.
1757
+ { allowRetry: opts.forceChunk ? false : allowRetry }
1758
+ )
1749
1759
  )
1750
- ]);
1751
- const parts = [first, second].filter((p) => !!p);
1760
+ );
1761
+ const lost = results.some(
1762
+ (r, i) => r === null && serializeForSummary(halves[i]).trim()
1763
+ );
1764
+ if (lost) {
1765
+ return null;
1766
+ }
1767
+ const parts = results.filter((p) => p !== null);
1752
1768
  return parts.length > 0 ? parts.join("\n\n---\n\n") : null;
1753
1769
  }
1754
1770
  log2.info("Generating summary", {
1755
1771
  name,
1756
1772
  messageCount: messagesToSummarize.length,
1757
- cacheReuse: !!mainSystem
1773
+ serializedLength: serialized.length
1758
1774
  });
1759
- let summaryText = "";
1760
- const useMainCache = !!mainSystem;
1761
- const system = useMainCache ? mainSystem : compactionPrompt;
1762
- const tools2 = [];
1763
- const userContent = useMainCache ? `${compactionPrompt}
1775
+ const summaryText = await runSummaryCall(
1776
+ apiConfig,
1777
+ name,
1778
+ compactionPrompt,
1779
+ serialized,
1780
+ model
1781
+ );
1782
+ if (summaryText === null) {
1783
+ return null;
1784
+ }
1785
+ if (summaryText.length >= MIN_SUMMARY_CHARS) {
1786
+ log2.info("Summary generated", { name, summaryLength: summaryText.length });
1787
+ return summaryText;
1788
+ }
1789
+ log2.warn("Summary too short to be real", {
1790
+ name,
1791
+ summaryLength: summaryText.length,
1792
+ minimum: MIN_SUMMARY_CHARS,
1793
+ serializedLength: serialized.length,
1794
+ retrying: splittable && allowRetry
1795
+ });
1796
+ if (!splittable || !allowRetry) {
1797
+ return null;
1798
+ }
1799
+ return generateSummary(
1800
+ apiConfig,
1801
+ name,
1802
+ compactionPrompt,
1803
+ messagesToSummarize,
1804
+ model,
1805
+ { forceChunk: true, allowRetry: false }
1806
+ );
1807
+ }
1808
+ async function runSummaryCall(apiConfig, name, compactionPrompt, serialized, model) {
1809
+ const userContent = `Conversation to summarize:
1764
1810
 
1765
- ---
1811
+ ${serialized}
1766
1812
 
1767
- Conversation to summarize:
1813
+ ---
1768
1814
 
1769
- ${serialized}` : serialized;
1815
+ Write the summary of the conversation above, following your instructions.`;
1816
+ let summaryText = "";
1770
1817
  const iterStart = Date.now();
1771
- for await (const event of streamChat({
1818
+ for await (const event of streamChatWithRetry({
1772
1819
  ...apiConfig,
1773
1820
  model,
1774
1821
  subAgentId: "conversationSummarizer",
1775
- system,
1822
+ system: compactionPrompt,
1776
1823
  messages: [{ role: "user", content: userContent }],
1777
- tools: tools2
1824
+ // Always empty. With a toolset available the model picks `tool_use` over
1825
+ // producing a summary, leaving summaryText empty.
1826
+ tools: []
1778
1827
  })) {
1779
1828
  if (event.type === "text") {
1780
1829
  summaryText += event.text;
@@ -1801,10 +1850,9 @@ ${serialized}` : serialized;
1801
1850
  log2.warn("Empty summary generated", { name });
1802
1851
  return null;
1803
1852
  }
1804
- log2.info("Summary generated", { name, summaryLength: summaryText.length });
1805
1853
  return summaryText.trim();
1806
1854
  }
1807
- var log2, CONVERSATION_SUMMARY_PROMPT, SUBAGENT_SUMMARY_PROMPT, SUMMARIZABLE_SUBAGENTS, CHUNK_CHAR_LIMIT;
1855
+ var log2, CONVERSATION_SUMMARY_PROMPT, SUBAGENT_SUMMARY_PROMPT, SUMMARIZABLE_SUBAGENTS, CHUNK_CHAR_LIMIT, MIN_SUMMARY_CHARS;
1808
1856
  var init_compaction = __esm({
1809
1857
  "src/compaction/index.ts"() {
1810
1858
  "use strict";
@@ -1817,137 +1865,8 @@ var init_compaction = __esm({
1817
1865
  CONVERSATION_SUMMARY_PROMPT = readAsset("compaction", "conversation.md");
1818
1866
  SUBAGENT_SUMMARY_PROMPT = readAsset("compaction", "subagent.md");
1819
1867
  SUMMARIZABLE_SUBAGENTS = ["visualDesignExpert", "productVision"];
1820
- CHUNK_CHAR_LIMIT = 24e5;
1821
- }
1822
- });
1823
-
1824
- // src/prompt/static/projectContext.ts
1825
- import fs10 from "fs";
1826
- import path4 from "path";
1827
- function loadProjectManifest() {
1828
- try {
1829
- const manifest = fs10.readFileSync("mindstudio.json", "utf-8");
1830
- return `
1831
- ## Project Manifest (mindstudio.json)
1832
- \`\`\`json
1833
- ${manifest}
1834
- \`\`\``;
1835
- } catch {
1836
- return "";
1837
- }
1838
- }
1839
- function loadSpecFileMetadata() {
1840
- try {
1841
- const files = walkMdFiles("src");
1842
- if (files.length === 0) {
1843
- return "";
1844
- }
1845
- const entries = [];
1846
- for (const filePath of files) {
1847
- const { name, description, type } = parseFrontmatter(filePath);
1848
- let line = `- ${filePath}`;
1849
- if (name) {
1850
- line += ` \u2014 "${name}"`;
1851
- }
1852
- if (type) {
1853
- line += ` (${type})`;
1854
- }
1855
- if (description) {
1856
- line += ` \u2014 ${description}`;
1857
- }
1858
- entries.push(line);
1859
- }
1860
- return `
1861
- ## Spec Files
1862
- ${entries.join("\n")}`;
1863
- } catch {
1864
- return "";
1865
- }
1866
- }
1867
- function walkMdFiles(dir) {
1868
- const results = [];
1869
- try {
1870
- const entries = fs10.readdirSync(dir, { withFileTypes: true });
1871
- for (const entry of entries) {
1872
- const full = path4.join(dir, entry.name);
1873
- if (entry.isDirectory()) {
1874
- results.push(...walkMdFiles(full));
1875
- } else if (entry.name.endsWith(".md")) {
1876
- results.push(full);
1877
- }
1878
- }
1879
- } catch {
1880
- }
1881
- return results.sort();
1882
- }
1883
- function parseFrontmatter(filePath) {
1884
- try {
1885
- const content = fs10.readFileSync(filePath, "utf-8");
1886
- const match = content.match(/^---\n([\s\S]*?)\n---/);
1887
- if (!match) {
1888
- return { name: "", description: "", type: "" };
1889
- }
1890
- const fm = match[1];
1891
- const name = fm.match(/^name:\s*(.+)$/m)?.[1]?.trim() ?? "";
1892
- const description = fm.match(/^description:\s*(.+)$/m)?.[1]?.trim() ?? "";
1893
- const type = fm.match(/^type:\s*(.+)$/m)?.[1]?.trim() ?? "";
1894
- return { name, description, type };
1895
- } catch {
1896
- return { name: "", description: "", type: "" };
1897
- }
1898
- }
1899
- function loadPlanStatus(onboardingState) {
1900
- try {
1901
- const content = fs10.readFileSync(".remy-plan.md", "utf-8");
1902
- const match = content.match(/^---\n([\s\S]*?)\n---/);
1903
- const status = match?.[1]?.match(/^status:\s*(.+)$/m)?.[1]?.trim();
1904
- if (status === "pending" && onboardingState === "intake") {
1905
- return `
1906
- <pending_initial_plan>
1907
- This is your initial plan, proposed via writePlan and awaiting the user's decision. The user approves it by pressing "Start Building" \u2014 you'll get an approveInitialPlan message when they do, and it will tell you to begin. Until then, stay in intake: keep discussing, and revise with writePlan if the user asks. Don't start building on your own, even if the user says "looks good" in chat.
1908
- </pending_initial_plan>`;
1909
- }
1910
- if (status === "pending") {
1911
- return `
1912
- <pending_plan>
1913
- 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. When the user approves the plan (via chat or any other signal), call updatePlanStatus with status "approved" before beginning any implementation work.
1914
- </pending_plan>`;
1915
- }
1916
- if (status === "approved") {
1917
- return `
1918
- <approved_plan>
1919
- 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.
1920
- </approved_plan>`;
1921
- }
1922
- return "";
1923
- } catch {
1924
- return "";
1925
- }
1926
- }
1927
- function loadProjectFileListing() {
1928
- try {
1929
- const entries = fs10.readdirSync(".", { withFileTypes: true });
1930
- const listing = entries.filter((e) => e.name !== ".git" && e.name !== "node_modules").sort((a, b) => {
1931
- if (a.isDirectory() && !b.isDirectory()) {
1932
- return -1;
1933
- }
1934
- if (!a.isDirectory() && b.isDirectory()) {
1935
- return 1;
1936
- }
1937
- return a.name.localeCompare(b.name);
1938
- }).map((e) => e.isDirectory() ? `${e.name}/` : e.name).join("\n");
1939
- return `
1940
- ## Project Files
1941
- \`\`\`
1942
- ${listing}
1943
- \`\`\``;
1944
- } catch {
1945
- return "";
1946
- }
1947
- }
1948
- var init_projectContext = __esm({
1949
- "src/prompt/static/projectContext.ts"() {
1950
- "use strict";
1868
+ CHUNK_CHAR_LIMIT = 2e5;
1869
+ MIN_SUMMARY_CHARS = 400;
1951
1870
  }
1952
1871
  });
1953
1872
 
@@ -2111,220 +2030,430 @@ var init_surfaces = __esm({
2111
2030
  }
2112
2031
  });
2113
2032
 
2114
- // src/orgContext.ts
2115
- async function initOrgContext(config) {
2116
- try {
2117
- cached = await fetchRemyContext(config);
2118
- const orgDefaultModels2 = filterModelPicks(cached?.defaultModels);
2119
- setOrgDefaultModels(orgDefaultModels2);
2120
- log3.debug("org context loaded", {
2121
- delegatedAvailable: cached?.auth?.delegatedAvailable ?? false,
2122
- requireDelegatedOnly: cached?.auth?.requireDelegatedOnly ?? false,
2123
- hasOrgName: !!cached?.org?.name,
2124
- orgDefaultModels: orgDefaultModels2
2125
- });
2126
- } catch (err) {
2127
- cached = null;
2128
- setOrgDefaultModels({});
2129
- log3.debug("org context init failed", { error: err.message });
2130
- }
2033
+ // src/automatedActions/sentinel.ts
2034
+ function sentinel(name) {
2035
+ return `@@automated::${name}@@`;
2131
2036
  }
2132
- function getOrgContext() {
2133
- return cached;
2037
+ function automatedMessage(name, body) {
2038
+ return body ? `${sentinel(name)}
2039
+ ${body}` : sentinel(name);
2134
2040
  }
2135
- function renderOrgContextBlock() {
2136
- const ctx = cached;
2137
- const auth = ctx?.auth;
2138
- if (!auth || !auth.delegatedAvailable && !auth.requireDelegatedOnly) {
2139
- return "";
2140
- }
2141
- const lines = ["<org_auth_context>"];
2142
- if (ctx?.org?.name && ctx?.org?.name !== "Personal Workspace") {
2143
- lines.push(`This app is owned by the organization "${ctx.org.name}".`);
2144
- }
2145
- if (auth.delegatedAvailable) {
2146
- lines.push(
2147
- '"Sign in with Remy" (platform-delegated sign-in) is an available auth type for this app: organization members can sign in without a verification code.'
2148
- );
2041
+ function hasSentinel(text, name) {
2042
+ return text.startsWith(sentinel(name));
2043
+ }
2044
+ function isAutomatedMessage(text) {
2045
+ return text.startsWith("@@automated::");
2046
+ }
2047
+ function parseSentinel(text) {
2048
+ const match = text.match(/^@@automated::(\w+)@@(.*)/s);
2049
+ if (!match) {
2050
+ return null;
2149
2051
  }
2150
- if (auth.requireDelegatedOnly) {
2151
- lines.push(
2152
- "This organization requires delegated sign-in: non-delegated human auth methods (email-code, sms-code) are blocked at the platform edge for its apps."
2153
- );
2052
+ return { name: match[1], remainder: match[2] };
2053
+ }
2054
+ function stripSentinelLine(text) {
2055
+ return text.replace(/^@@automated::[^@]*@@[^\n]*\n?/, "");
2056
+ }
2057
+ function buildBackgroundResultsMessage(results) {
2058
+ const xml = results.map(
2059
+ (r) => `<tool_result id="${r.toolCallId}" name="${r.name}">
2060
+ ${r.result}
2061
+ </tool_result>`
2062
+ ).join("\n\n");
2063
+ const plural = results.length > 1 ? "s" : "";
2064
+ const body = `This is an automated message containing the result${plural} of ${results.length > 1 ? "tool calls" : "a tool call"} that ${results.length > 1 ? "have" : "has"} been working in the background. This is not a direct message from the user.
2065
+ <background_results>
2066
+ ${xml}
2067
+ </background_results>`;
2068
+ return automatedMessage("background_results", body);
2069
+ }
2070
+ function mergeBackgroundResultsMessages(messages) {
2071
+ const results = [];
2072
+ const toolRe = /<tool_result id="([^"]+)" name="([^"]+)">\n([\s\S]*?)\n<\/tool_result>/g;
2073
+ for (const msg of messages) {
2074
+ for (const m of msg.matchAll(toolRe)) {
2075
+ results.push({ toolCallId: m[1], name: m[2], result: m[3] });
2076
+ }
2154
2077
  }
2155
- lines.push("</org_auth_context>");
2156
- return lines.join("\n");
2078
+ return buildBackgroundResultsMessage(results);
2157
2079
  }
2158
- var log3, cached;
2159
- var init_orgContext = __esm({
2160
- "src/orgContext.ts"() {
2080
+ var init_sentinel = __esm({
2081
+ "src/automatedActions/sentinel.ts"() {
2161
2082
  "use strict";
2162
- init_api();
2163
- init_surfaces();
2164
- init_logger();
2165
- log3 = createLogger("orgContext");
2166
- cached = null;
2167
2083
  }
2168
2084
  });
2169
2085
 
2170
- // src/prompt/index.ts
2171
- function resolveIncludes(template) {
2172
- const result = template.replace(
2173
- /\{\{([^}]+)\}\}/g,
2174
- (_, filePath) => readAsset("prompt", filePath.trim())
2175
- );
2176
- return result.replace(/\n{3,}/g, "\n\n").trim();
2086
+ // src/subagents/common/cleanMessages.ts
2087
+ function findLastSummaryCheckpoint(messages, name) {
2088
+ for (let i = messages.length - 1; i >= 0; i--) {
2089
+ const msg = messages[i];
2090
+ if (!Array.isArray(msg.content)) {
2091
+ continue;
2092
+ }
2093
+ for (const block of msg.content) {
2094
+ if (block.type === "summary" && block.name === name) {
2095
+ return i;
2096
+ }
2097
+ }
2098
+ }
2099
+ return -1;
2177
2100
  }
2178
- function buildSystemPrompt(onboardingState, viewContext) {
2179
- const projectContext = [
2180
- loadProjectManifest(),
2181
- loadSpecFileMetadata(),
2182
- loadProjectFileListing()
2183
- ].filter(Boolean).join("\n");
2184
- const now = (/* @__PURE__ */ new Date()).toLocaleDateString("en-US", {
2185
- month: "long",
2186
- day: "numeric",
2187
- year: "numeric"
2188
- });
2189
- const template = `
2190
- {{static/identity.md}}
2191
-
2192
- Current date: ${now}
2193
-
2194
- <platform_docs>
2195
- <platform>
2196
- {{compiled/platform.md}}
2197
- </platform>
2198
-
2199
- <manifest>
2200
- {{compiled/manifest.md}}
2201
- </manifest>
2202
-
2203
- <tables>
2204
- {{compiled/tables.md}}
2205
- </tables>
2206
-
2207
- <methods>
2208
- {{compiled/methods.md}}
2209
- </methods>
2210
-
2211
- <auth>
2212
- {{compiled/auth.md}}
2213
- </auth>
2214
-
2215
- <dev_and_deploy>
2216
- {{compiled/dev-and-deploy.md}}
2217
- </dev_and_deploy>
2218
-
2219
- <design>
2220
- {{compiled/design.md}}
2221
- </design>
2222
-
2223
- <building_agent_interfaces>
2224
- {{compiled/agent-interfaces.md}}
2225
- </building_agent_interfaces>
2226
-
2227
- <building_mcp_interfaces>
2228
- {{compiled/mcp-interfaces.md}}
2229
- </building_mcp_interfaces>
2230
-
2231
- <media_cdn>
2232
- {{compiled/media-cdn.md}}
2233
- </media_cdn>
2234
-
2235
- <interfaces>
2236
- {{compiled/interfaces.md}}
2237
- </interfaces>
2238
-
2239
- <scenarios>
2240
- {{compiled/scenarios.md}}
2241
- </scenarios>
2242
-
2243
- <secrets>
2244
- {{compiled/secrets.md}}
2245
- </secrets>
2246
- </platform_docs>
2247
-
2248
- <mindstudio_agent_sdk_docs>
2249
- {{compiled/sdk-actions.md}}
2250
-
2251
- {{compiled/task-agents.md}}
2252
- </mindstudio_agent_sdk_docs>
2253
-
2254
- <mindstudio_flavored_markdown_spec_docs>
2255
- {{compiled/msfm.md}}
2256
- </mindstudio_flavored_markdown_spec_docs>
2257
-
2258
- <intake_mode_instructions>
2259
- {{static/intake.md}}
2260
- </intake_mode_instructions>
2261
-
2262
- <spec_authoring_instructions>
2263
- {{static/authoring.md}}
2264
- </spec_authoring_instructions>
2265
-
2266
- <team>
2267
- {{static/team.md}}
2268
- </team>
2269
-
2270
- <code_authoring_instructions>
2271
- {{static/coding.md}}
2272
-
2273
- <typescript_lsp>
2274
- {{static/lsp.md}}
2275
- </typescript_lsp>
2276
- </code_authoring_instructions>
2277
-
2278
- <conversation_summaries>
2279
- 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.
2280
-
2281
- Old tool results are periodically cleared from the conversation to save context space. This is automatic and expected \u2014 you don't need to note down or preserve information from tool results. If you need to reference something from an earlier tool call, just re-read the file or re-run the query.
2282
- </conversation_summaries>
2283
-
2284
- <project_onboarding>
2285
- New projects progress through three onboarding states. The user might skip this entirely and jump straight into working on the existing scaffold (which defaults to onboardingFinished), but ideally new projects move through each phase:
2286
-
2287
- - **intake**: Gathering requirements. The project has scaffold code (a "hello world" starter) but it's not the user's app yet. Focus on understanding what they want to build, not on the existing code. Intake ends with a plan proposal via writePlan.
2288
- - **building**: The user approved the initial plan. The agent is writing the spec and building the app. This can take a while and involves heavy tool use (spec authoring, design expert consultation, code generation, verification, polishing).
2289
- - **buildComplete**: The build is done. The frontend is showing the user a reveal experience (pitch deck, app preview). The agent does not need to do anything in this state; the frontend advances to onboardingFinished when the user is ready.
2290
- - **onboardingFinished**: The project is built and ready. Full development mode with all tools available. From here on, keep spec and code in sync as changes are made.
2291
- </project_onboarding>
2292
-
2293
- {{static/instructions.md}}
2294
-
2295
- <!-- cache_breakpoint -->
2296
-
2297
- <current_project_onboarding_state>
2298
- ${onboardingState ?? "onboardingFinished"}
2299
- </current_project_onboarding_state>
2101
+ function fixOrphanedToolCalls(messages) {
2102
+ const toolResultIds = /* @__PURE__ */ new Set();
2103
+ for (const msg of messages) {
2104
+ if (msg.role === "user" && msg.toolCallId) {
2105
+ toolResultIds.add(msg.toolCallId);
2106
+ }
2107
+ }
2108
+ const result = [];
2109
+ for (const msg of messages) {
2110
+ result.push(msg);
2111
+ if (msg.role !== "assistant" || !Array.isArray(msg.content)) {
2112
+ continue;
2113
+ }
2114
+ const toolBlocks = msg.content.filter(
2115
+ (b) => b.type === "tool"
2116
+ );
2117
+ const orphans = toolBlocks.filter((tc) => !toolResultIds.has(tc.id));
2118
+ for (const tc of orphans) {
2119
+ result.push({
2120
+ role: "user",
2121
+ content: "Error: tool result lost (session recovered)",
2122
+ toolCallId: tc.id,
2123
+ isToolError: true
2124
+ });
2125
+ toolResultIds.add(tc.id);
2126
+ }
2127
+ }
2128
+ return result;
2129
+ }
2130
+ function cleanMessagesForApi(messages) {
2131
+ const checkpointIdx = findLastSummaryCheckpoint(messages, "conversation");
2132
+ let startIdx = 0;
2133
+ const prefix = [];
2134
+ if (checkpointIdx !== -1) {
2135
+ const checkpointMsg = messages[checkpointIdx];
2136
+ const blocks = checkpointMsg.content;
2137
+ const summaryBlock = blocks.find(
2138
+ (b) => b.type === "summary" && b.name === "conversation"
2139
+ );
2140
+ if (summaryBlock && summaryBlock.type === "summary") {
2141
+ prefix.push({
2142
+ role: "user",
2143
+ content: `<conversation_summary>
2144
+ ${summaryBlock.text}
2145
+ </conversation_summary>`,
2146
+ hidden: true
2147
+ });
2148
+ }
2149
+ startIdx = checkpointIdx + 1;
2150
+ }
2151
+ const messagesToProcess = fixOrphanedToolCalls(messages.slice(startIdx));
2152
+ const toolUseIds = /* @__PURE__ */ new Set();
2153
+ for (const msg of messagesToProcess) {
2154
+ if (msg.role === "assistant" && Array.isArray(msg.content)) {
2155
+ for (const block of msg.content) {
2156
+ if (block.type === "tool") {
2157
+ toolUseIds.add(block.id);
2158
+ }
2159
+ }
2160
+ }
2161
+ }
2162
+ const cleaned = messagesToProcess.filter((msg) => {
2163
+ if (Array.isArray(msg.content)) {
2164
+ const blocks = msg.content;
2165
+ if (blocks.some((b) => b.type === "summary")) {
2166
+ return false;
2167
+ }
2168
+ }
2169
+ if (msg.role === "assistant" && Array.isArray(msg.content) && msg.content.length === 0) {
2170
+ return false;
2171
+ }
2172
+ if (msg.role === "user" && msg.toolCallId && !toolUseIds.has(msg.toolCallId)) {
2173
+ return false;
2174
+ }
2175
+ return true;
2176
+ }).map((msg) => {
2177
+ if (msg.role === "user" && typeof msg.content === "string") {
2178
+ const { attachmentHeader, ...rest } = msg;
2179
+ let content = isAutomatedMessage(msg.content) ? stripSentinelLine(msg.content) : msg.content;
2180
+ if (attachmentHeader) {
2181
+ content = content ? `${attachmentHeader}
2300
2182
 
2301
- <project_context>
2302
- ${projectContext}
2303
- </project_context>
2183
+ ${content}` : attachmentHeader;
2184
+ }
2185
+ return { ...rest, content };
2186
+ }
2187
+ if (!Array.isArray(msg.content)) {
2188
+ return msg;
2189
+ }
2190
+ const blocks = msg.content;
2191
+ const text = blocks.filter((b) => b.type === "text").map((b) => b.text).join("");
2192
+ const toolCalls = blocks.filter((b) => b.type === "tool").map((b) => ({ id: b.id, name: b.name, input: b.input }));
2193
+ const cleaned2 = {
2194
+ role: msg.role,
2195
+ content: text
2196
+ };
2197
+ if (toolCalls.length > 0) {
2198
+ cleaned2.toolCalls = toolCalls;
2199
+ }
2200
+ if (msg.providerMetadata) {
2201
+ cleaned2.providerMetadata = msg.providerMetadata;
2202
+ }
2203
+ if (msg.hidden) {
2204
+ cleaned2.hidden = true;
2205
+ }
2206
+ return cleaned2;
2207
+ });
2208
+ return [...prefix, ...cleaned];
2209
+ }
2210
+ var init_cleanMessages = __esm({
2211
+ "src/subagents/common/cleanMessages.ts"() {
2212
+ "use strict";
2213
+ init_sentinel();
2214
+ }
2215
+ });
2304
2216
 
2305
- ${renderOrgContextBlock()}
2217
+ // src/toolResultCap.ts
2218
+ function capToolResult(result) {
2219
+ const total = Buffer.byteLength(result, "utf-8");
2220
+ if (total <= MAX_TOOL_RESULT_BYTES) {
2221
+ return result;
2222
+ }
2223
+ const head = Buffer.from(result, "utf-8").subarray(0, MAX_TOOL_RESULT_BYTES).toString("utf-8");
2224
+ return head + `
2306
2225
 
2307
- <view_context>
2308
- The user is currently in ${viewContext?.mode ?? "code"} mode.
2309
- ${viewContext?.activeFile ? `Active file: ${viewContext.activeFile}` : ""}
2310
- </view_context>
2226
+ (tool result truncated at ${(MAX_TOOL_RESULT_BYTES / 1024).toFixed(0)}KB of ${(total / 1024).toFixed(0)}KB \u2014 too large to keep in context. Narrow the call (select fewer fields, paginate, or query a subset) instead of fetching everything.)`;
2227
+ }
2228
+ var MAX_TOOL_RESULT_BYTES;
2229
+ var init_toolResultCap = __esm({
2230
+ "src/toolResultCap.ts"() {
2231
+ "use strict";
2232
+ MAX_TOOL_RESULT_BYTES = 256 * 1024;
2233
+ }
2234
+ });
2311
2235
 
2312
- ${loadPlanStatus(onboardingState)}
2313
- `;
2314
- return resolveIncludes(template);
2236
+ // src/session.ts
2237
+ import fs10 from "fs";
2238
+ import path4 from "path";
2239
+ function loadSession(state) {
2240
+ pruneArchives();
2241
+ try {
2242
+ const raw = fs10.readFileSync(SESSION_FILE, "utf-8");
2243
+ const data = JSON.parse(raw);
2244
+ if (data.models && typeof data.models === "object") {
2245
+ state.models = data.models;
2246
+ }
2247
+ if (Array.isArray(data.messages) && data.messages.length > 0) {
2248
+ state.messages = sanitizeMessages(data.messages);
2249
+ log3.info("Session loaded", {
2250
+ messageCount: state.messages.length,
2251
+ ...state.models && { models: state.models }
2252
+ });
2253
+ return true;
2254
+ }
2255
+ } catch {
2256
+ }
2257
+ return false;
2258
+ }
2259
+ function capOversizedResults(msg) {
2260
+ if (msg.role === "assistant" && Array.isArray(msg.content)) {
2261
+ for (const block of msg.content) {
2262
+ if (block.type === "tool" && typeof block.result === "string") {
2263
+ block.result = capToolResult(block.result);
2264
+ }
2265
+ }
2266
+ } else if (msg.role === "user" && msg.toolCallId && typeof msg.content === "string") {
2267
+ msg.content = capToolResult(msg.content);
2268
+ }
2269
+ }
2270
+ function sanitizeMessages(messages) {
2271
+ const result = [];
2272
+ for (let i = 0; i < messages.length; i++) {
2273
+ const msg = messages[i];
2274
+ capOversizedResults(msg);
2275
+ result.push(msg);
2276
+ if (msg.role !== "assistant" || !Array.isArray(msg.content)) {
2277
+ continue;
2278
+ }
2279
+ const toolBlocks = msg.content.filter(
2280
+ (b) => b.type === "tool"
2281
+ );
2282
+ if (toolBlocks.length === 0) {
2283
+ continue;
2284
+ }
2285
+ const resultIds = /* @__PURE__ */ new Set();
2286
+ for (let j = i + 1; j < messages.length; j++) {
2287
+ const next = messages[j];
2288
+ if (next.role === "user" && next.toolCallId) {
2289
+ resultIds.add(next.toolCallId);
2290
+ } else {
2291
+ break;
2292
+ }
2293
+ }
2294
+ for (const tc of toolBlocks) {
2295
+ if (!resultIds.has(tc.id)) {
2296
+ result.push({
2297
+ role: "user",
2298
+ content: "Error: tool result lost (session recovered)",
2299
+ toolCallId: tc.id,
2300
+ isToolError: true
2301
+ });
2302
+ }
2303
+ }
2304
+ }
2305
+ return result;
2306
+ }
2307
+ function buildPayload(state) {
2308
+ const payload = { messages: state.messages };
2309
+ if (state.models && Object.keys(state.models).length > 0) {
2310
+ payload.models = state.models;
2311
+ }
2312
+ return payload;
2313
+ }
2314
+ function archiveMessages(messages, label, models) {
2315
+ fs10.mkdirSync(ARCHIVE_DIR, { recursive: true });
2316
+ const ts = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
2317
+ let dest = path4.join(ARCHIVE_DIR, `${label}-${ts}.json`);
2318
+ let n = 1;
2319
+ while (fs10.existsSync(dest)) {
2320
+ dest = path4.join(ARCHIVE_DIR, `${label}-${ts}-${n++}.json`);
2321
+ }
2322
+ const payload = { messages };
2323
+ if (models && Object.keys(models).length > 0) {
2324
+ payload.models = models;
2325
+ }
2326
+ fs10.writeFileSync(dest, JSON.stringify(payload), "utf-8");
2327
+ log3.info("Session archived", { label, dest, messageCount: messages.length });
2328
+ pruneArchives();
2329
+ return dest;
2330
+ }
2331
+ function pruneArchives() {
2332
+ try {
2333
+ const entries = fs10.readdirSync(ARCHIVE_DIR).filter((name) => /^(cleared|rotated)-.*\.json$/.test(name));
2334
+ if (entries.length <= 1) {
2335
+ return;
2336
+ }
2337
+ const sortKey = (name) => name.replace(/^(cleared|rotated)-/, "");
2338
+ const archives = entries.map((name) => ({
2339
+ name,
2340
+ size: fs10.statSync(path4.join(ARCHIVE_DIR, name)).size
2341
+ })).sort((a, b) => sortKey(b.name).localeCompare(sortKey(a.name)));
2342
+ let kept = 0;
2343
+ let cut = archives.length;
2344
+ for (let i = 0; i < archives.length; i++) {
2345
+ if (i === 0 || kept + archives[i].size <= ARCHIVE_RETENTION_BYTES) {
2346
+ kept += archives[i].size;
2347
+ } else {
2348
+ cut = i;
2349
+ break;
2350
+ }
2351
+ }
2352
+ let removed = 0;
2353
+ let freed = 0;
2354
+ for (let i = cut; i < archives.length; i++) {
2355
+ try {
2356
+ fs10.unlinkSync(path4.join(ARCHIVE_DIR, archives[i].name));
2357
+ freed += archives[i].size;
2358
+ removed++;
2359
+ } catch {
2360
+ }
2361
+ }
2362
+ if (removed > 0) {
2363
+ log3.info("Session archives pruned", {
2364
+ removed,
2365
+ freedBytes: freed,
2366
+ keptBytes: kept
2367
+ });
2368
+ }
2369
+ } catch {
2370
+ }
2371
+ }
2372
+ function rotate(state) {
2373
+ const messages = state.messages;
2374
+ if (messages.length === 0) {
2375
+ return false;
2376
+ }
2377
+ let tailBytes = 0;
2378
+ let scrollbackStart = 0;
2379
+ for (let i = messages.length - 1; i >= 0; i--) {
2380
+ tailBytes += Buffer.byteLength(JSON.stringify(messages[i]), "utf-8") + 1;
2381
+ if (tailBytes >= RETAIN_TAIL_BYTES) {
2382
+ scrollbackStart = i;
2383
+ break;
2384
+ }
2385
+ }
2386
+ const checkpointIdx = findLastSummaryCheckpoint(messages, "conversation");
2387
+ let cut = checkpointIdx === -1 ? scrollbackStart : Math.min(scrollbackStart, checkpointIdx);
2388
+ cut = findSafeInsertionPoint(messages, cut);
2389
+ if (cut <= 0) {
2390
+ return false;
2391
+ }
2392
+ archiveMessages(messages.slice(0, cut), "rotated", state.models);
2393
+ state.messages = messages.slice(cut);
2394
+ log3.info("Session rotated", {
2395
+ archived: cut,
2396
+ retained: state.messages.length
2397
+ });
2398
+ return true;
2315
2399
  }
2316
- var init_prompt = __esm({
2317
- "src/prompt/index.ts"() {
2400
+ function saveSession(state) {
2401
+ try {
2402
+ let serialized = JSON.stringify(buildPayload(state));
2403
+ if (Buffer.byteLength(serialized, "utf-8") > ROTATE_THRESHOLD_BYTES && rotate(state)) {
2404
+ serialized = JSON.stringify(buildPayload(state));
2405
+ }
2406
+ fs10.writeFileSync(SESSION_FILE, serialized, "utf-8");
2407
+ log3.info("Session saved", { messageCount: state.messages.length });
2408
+ } catch (err) {
2409
+ log3.warn("Session save failed", { error: err.message });
2410
+ }
2411
+ }
2412
+ function clearSession(state) {
2413
+ try {
2414
+ if (state.messages.length > 0) {
2415
+ archiveMessages(state.messages, "cleared", state.models);
2416
+ }
2417
+ } catch (err) {
2418
+ log3.warn("Session archive on clear failed", { error: err.message });
2419
+ }
2420
+ state.messages = [];
2421
+ try {
2422
+ if (fs10.existsSync(SESSION_FILE)) {
2423
+ fs10.unlinkSync(SESSION_FILE);
2424
+ }
2425
+ } catch (err) {
2426
+ log3.warn("Session clear: could not remove live file", {
2427
+ error: err.message
2428
+ });
2429
+ }
2430
+ }
2431
+ var log3, SESSION_FILE, ARCHIVE_DIR, ROTATE_THRESHOLD_BYTES, RETAIN_TAIL_BYTES, ARCHIVE_RETENTION_BYTES;
2432
+ var init_session = __esm({
2433
+ "src/session.ts"() {
2318
2434
  "use strict";
2319
- init_assets();
2320
- init_projectContext();
2321
- init_orgContext();
2435
+ init_logger();
2436
+ init_compaction();
2437
+ init_cleanMessages();
2438
+ init_toolResultCap();
2439
+ log3 = createLogger("session");
2440
+ SESSION_FILE = ".remy-session.json";
2441
+ ARCHIVE_DIR = ".logs/sessions";
2442
+ ROTATE_THRESHOLD_BYTES = 32 * 1024 * 1024;
2443
+ RETAIN_TAIL_BYTES = 16 * 1024 * 1024;
2444
+ ARCHIVE_RETENTION_BYTES = 64 * 1024 * 1024;
2322
2445
  }
2323
2446
  });
2324
2447
 
2325
2448
  // src/compaction/trigger.ts
2326
- function getPendingSummaries() {
2327
- return pendingSummaries.splice(0);
2449
+ function applyPendingSummaries(state) {
2450
+ const summaries = pendingSummaries.splice(0);
2451
+ if (summaries.length === 0) {
2452
+ return;
2453
+ }
2454
+ const idx = findSafeInsertionPoint(state.messages);
2455
+ state.messages.splice(idx, 0, ...summaries);
2456
+ saveSession(state);
2328
2457
  }
2329
2458
  function setCompactionListener(l) {
2330
2459
  listener = l;
@@ -2335,13 +2464,9 @@ function triggerCompaction(state, apiConfig, opts = {}) {
2335
2464
  }
2336
2465
  const { blocking = false, requestId, model } = opts;
2337
2466
  listener?.({ type: "started", blocking, requestId });
2338
- const system = buildSystemPrompt("onboardingFinished");
2339
- const tools2 = getToolDefinitions("onboardingFinished");
2340
2467
  inflightCompaction = compactConversation(
2341
2468
  state.messages,
2342
2469
  apiConfig,
2343
- system,
2344
- tools2,
2345
2470
  resolveModel("conversationSummarizer", state.models, model)
2346
2471
  ).then((summaries) => {
2347
2472
  pendingSummaries.push(...summaries);
@@ -2362,10 +2487,9 @@ var init_trigger = __esm({
2362
2487
  "src/compaction/trigger.ts"() {
2363
2488
  "use strict";
2364
2489
  init_compaction();
2365
- init_prompt();
2366
- init_tools8();
2367
2490
  init_logger();
2368
2491
  init_surfaces();
2492
+ init_session();
2369
2493
  log4 = createLogger("compaction:trigger");
2370
2494
  pendingSummaries = [];
2371
2495
  inflightCompaction = null;
@@ -3604,25 +3728,6 @@ ${partial}` : "[INTERRUPTED] Tool execution was stopped.";
3604
3728
  }
3605
3729
  });
3606
3730
 
3607
- // src/toolResultCap.ts
3608
- function capToolResult(result) {
3609
- const total = Buffer.byteLength(result, "utf-8");
3610
- if (total <= MAX_TOOL_RESULT_BYTES) {
3611
- return result;
3612
- }
3613
- const head = Buffer.from(result, "utf-8").subarray(0, MAX_TOOL_RESULT_BYTES).toString("utf-8");
3614
- return head + `
3615
-
3616
- (tool result truncated at ${(MAX_TOOL_RESULT_BYTES / 1024).toFixed(0)}KB of ${(total / 1024).toFixed(0)}KB \u2014 too large to keep in context. Narrow the call (select fewer fields, paginate, or query a subset) instead of fetching everything.)`;
3617
- }
3618
- var MAX_TOOL_RESULT_BYTES;
3619
- var init_toolResultCap = __esm({
3620
- "src/toolResultCap.ts"() {
3621
- "use strict";
3622
- MAX_TOOL_RESULT_BYTES = 256 * 1024;
3623
- }
3624
- });
3625
-
3626
3731
  // src/statusWatcher.ts
3627
3732
  function startStatusWatcher(config) {
3628
3733
  const { apiConfig, getContext, onStatus, interval = 5e3, signal } = config;
@@ -3679,195 +3784,11 @@ function startStatusWatcher(config) {
3679
3784
  resume() {
3680
3785
  pauseCount = Math.max(0, pauseCount - 1);
3681
3786
  }
3682
- };
3683
- }
3684
- var init_statusWatcher = __esm({
3685
- "src/statusWatcher.ts"() {
3686
- "use strict";
3687
- }
3688
- });
3689
-
3690
- // src/automatedActions/sentinel.ts
3691
- function sentinel(name) {
3692
- return `@@automated::${name}@@`;
3693
- }
3694
- function automatedMessage(name, body) {
3695
- return body ? `${sentinel(name)}
3696
- ${body}` : sentinel(name);
3697
- }
3698
- function hasSentinel(text, name) {
3699
- return text.startsWith(sentinel(name));
3700
- }
3701
- function isAutomatedMessage(text) {
3702
- return text.startsWith("@@automated::");
3703
- }
3704
- function parseSentinel(text) {
3705
- const match = text.match(/^@@automated::(\w+)@@(.*)/s);
3706
- if (!match) {
3707
- return null;
3708
- }
3709
- return { name: match[1], remainder: match[2] };
3710
- }
3711
- function stripSentinelLine(text) {
3712
- return text.replace(/^@@automated::[^@]*@@[^\n]*\n?/, "");
3713
- }
3714
- function buildBackgroundResultsMessage(results) {
3715
- const xml = results.map(
3716
- (r) => `<tool_result id="${r.toolCallId}" name="${r.name}">
3717
- ${r.result}
3718
- </tool_result>`
3719
- ).join("\n\n");
3720
- const plural = results.length > 1 ? "s" : "";
3721
- const body = `This is an automated message containing the result${plural} of ${results.length > 1 ? "tool calls" : "a tool call"} that ${results.length > 1 ? "have" : "has"} been working in the background. This is not a direct message from the user.
3722
- <background_results>
3723
- ${xml}
3724
- </background_results>`;
3725
- return automatedMessage("background_results", body);
3726
- }
3727
- function mergeBackgroundResultsMessages(messages) {
3728
- const results = [];
3729
- const toolRe = /<tool_result id="([^"]+)" name="([^"]+)">\n([\s\S]*?)\n<\/tool_result>/g;
3730
- for (const msg of messages) {
3731
- for (const m of msg.matchAll(toolRe)) {
3732
- results.push({ toolCallId: m[1], name: m[2], result: m[3] });
3733
- }
3734
- }
3735
- return buildBackgroundResultsMessage(results);
3736
- }
3737
- var init_sentinel = __esm({
3738
- "src/automatedActions/sentinel.ts"() {
3739
- "use strict";
3740
- }
3741
- });
3742
-
3743
- // src/subagents/common/cleanMessages.ts
3744
- function findLastSummaryCheckpoint(messages, name) {
3745
- for (let i = messages.length - 1; i >= 0; i--) {
3746
- const msg = messages[i];
3747
- if (!Array.isArray(msg.content)) {
3748
- continue;
3749
- }
3750
- for (const block of msg.content) {
3751
- if (block.type === "summary" && block.name === name) {
3752
- return i;
3753
- }
3754
- }
3755
- }
3756
- return -1;
3757
- }
3758
- function fixOrphanedToolCalls(messages) {
3759
- const toolResultIds = /* @__PURE__ */ new Set();
3760
- for (const msg of messages) {
3761
- if (msg.role === "user" && msg.toolCallId) {
3762
- toolResultIds.add(msg.toolCallId);
3763
- }
3764
- }
3765
- const result = [];
3766
- for (const msg of messages) {
3767
- result.push(msg);
3768
- if (msg.role !== "assistant" || !Array.isArray(msg.content)) {
3769
- continue;
3770
- }
3771
- const toolBlocks = msg.content.filter(
3772
- (b) => b.type === "tool"
3773
- );
3774
- const orphans = toolBlocks.filter((tc) => !toolResultIds.has(tc.id));
3775
- for (const tc of orphans) {
3776
- result.push({
3777
- role: "user",
3778
- content: "Error: tool result lost (session recovered)",
3779
- toolCallId: tc.id,
3780
- isToolError: true
3781
- });
3782
- toolResultIds.add(tc.id);
3783
- }
3784
- }
3785
- return result;
3786
- }
3787
- function cleanMessagesForApi(messages) {
3788
- const checkpointIdx = findLastSummaryCheckpoint(messages, "conversation");
3789
- let startIdx = 0;
3790
- const prefix = [];
3791
- if (checkpointIdx !== -1) {
3792
- const checkpointMsg = messages[checkpointIdx];
3793
- const blocks = checkpointMsg.content;
3794
- const summaryBlock = blocks.find(
3795
- (b) => b.type === "summary" && b.name === "conversation"
3796
- );
3797
- if (summaryBlock && summaryBlock.type === "summary") {
3798
- prefix.push({
3799
- role: "user",
3800
- content: `<conversation_summary>
3801
- ${summaryBlock.text}
3802
- </conversation_summary>`,
3803
- hidden: true
3804
- });
3805
- }
3806
- startIdx = checkpointIdx + 1;
3807
- }
3808
- const messagesToProcess = fixOrphanedToolCalls(messages.slice(startIdx));
3809
- const toolUseIds = /* @__PURE__ */ new Set();
3810
- for (const msg of messagesToProcess) {
3811
- if (msg.role === "assistant" && Array.isArray(msg.content)) {
3812
- for (const block of msg.content) {
3813
- if (block.type === "tool") {
3814
- toolUseIds.add(block.id);
3815
- }
3816
- }
3817
- }
3818
- }
3819
- const cleaned = messagesToProcess.filter((msg) => {
3820
- if (Array.isArray(msg.content)) {
3821
- const blocks = msg.content;
3822
- if (blocks.some((b) => b.type === "summary")) {
3823
- return false;
3824
- }
3825
- }
3826
- if (msg.role === "assistant" && Array.isArray(msg.content) && msg.content.length === 0) {
3827
- return false;
3828
- }
3829
- if (msg.role === "user" && msg.toolCallId && !toolUseIds.has(msg.toolCallId)) {
3830
- return false;
3831
- }
3832
- return true;
3833
- }).map((msg) => {
3834
- if (msg.role === "user" && typeof msg.content === "string") {
3835
- const { attachmentHeader, ...rest } = msg;
3836
- let content = isAutomatedMessage(msg.content) ? stripSentinelLine(msg.content) : msg.content;
3837
- if (attachmentHeader) {
3838
- content = content ? `${attachmentHeader}
3839
-
3840
- ${content}` : attachmentHeader;
3841
- }
3842
- return { ...rest, content };
3843
- }
3844
- if (!Array.isArray(msg.content)) {
3845
- return msg;
3846
- }
3847
- const blocks = msg.content;
3848
- const text = blocks.filter((b) => b.type === "text").map((b) => b.text).join("");
3849
- const toolCalls = blocks.filter((b) => b.type === "tool").map((b) => ({ id: b.id, name: b.name, input: b.input }));
3850
- const cleaned2 = {
3851
- role: msg.role,
3852
- content: text
3853
- };
3854
- if (toolCalls.length > 0) {
3855
- cleaned2.toolCalls = toolCalls;
3856
- }
3857
- if (msg.providerMetadata) {
3858
- cleaned2.providerMetadata = msg.providerMetadata;
3859
- }
3860
- if (msg.hidden) {
3861
- cleaned2.hidden = true;
3862
- }
3863
- return cleaned2;
3864
- });
3865
- return [...prefix, ...cleaned];
3787
+ };
3866
3788
  }
3867
- var init_cleanMessages = __esm({
3868
- "src/subagents/common/cleanMessages.ts"() {
3789
+ var init_statusWatcher = __esm({
3790
+ "src/statusWatcher.ts"() {
3869
3791
  "use strict";
3870
- init_sentinel();
3871
3792
  }
3872
3793
  });
3873
3794
 
@@ -4485,7 +4406,7 @@ ${appSpec}
4485
4406
  }
4486
4407
  }
4487
4408
  var BASE_PROMPT;
4488
- var init_prompt2 = __esm({
4409
+ var init_prompt = __esm({
4489
4410
  "src/subagents/browserAutomation/prompt.ts"() {
4490
4411
  "use strict";
4491
4412
  init_assets();
@@ -4632,7 +4553,7 @@ var init_browserAutomation = __esm({
4632
4553
  "use strict";
4633
4554
  init_runner();
4634
4555
  init_tools();
4635
- init_prompt2();
4556
+ init_prompt();
4636
4557
  init_sidecar();
4637
4558
  init_browserLock();
4638
4559
  init_screenshot();
@@ -5443,14 +5364,14 @@ var init_editImages = __esm({
5443
5364
  // src/subagents/common/context.ts
5444
5365
  import fs16 from "fs";
5445
5366
  import path7 from "path";
5446
- function walkMdFiles2(dir, skip) {
5367
+ function walkMdFiles(dir, skip) {
5447
5368
  const files = [];
5448
5369
  try {
5449
5370
  for (const entry of fs16.readdirSync(dir, { withFileTypes: true })) {
5450
5371
  const full = path7.join(dir, entry.name);
5451
5372
  if (entry.isDirectory()) {
5452
5373
  if (!skip?.has(entry.name)) {
5453
- files.push(...walkMdFiles2(full, skip));
5374
+ files.push(...walkMdFiles(full, skip));
5454
5375
  }
5455
5376
  } else if (entry.name.endsWith(".md")) {
5456
5377
  files.push(full);
@@ -5460,7 +5381,7 @@ function walkMdFiles2(dir, skip) {
5460
5381
  }
5461
5382
  return files.sort();
5462
5383
  }
5463
- function parseFrontmatter2(filePath) {
5384
+ function parseFrontmatter(filePath) {
5464
5385
  try {
5465
5386
  const content = fs16.readFileSync(filePath, "utf-8");
5466
5387
  const match = content.match(/^---\n([\s\S]*?)\n---/);
@@ -5482,12 +5403,12 @@ function parseFrontmatter2(filePath) {
5482
5403
  }
5483
5404
  }
5484
5405
  function loadSpecIndex() {
5485
- const files = walkMdFiles2("src", /* @__PURE__ */ new Set(["roadmap"]));
5406
+ const files = walkMdFiles("src", /* @__PURE__ */ new Set(["roadmap"]));
5486
5407
  if (files.length === 0) {
5487
5408
  return "";
5488
5409
  }
5489
5410
  const lines = files.map((f) => {
5490
- const fm = parseFrontmatter2(f);
5411
+ const fm = parseFrontmatter(f);
5491
5412
  let line = `- ${f}`;
5492
5413
  if (fm.name) {
5493
5414
  line += ` \u2014 "${fm.name}"`;
@@ -5525,10 +5446,10 @@ ${indexJson.standalone.map((s) => `- ${s}`).join("\n")}`
5525
5446
  }
5526
5447
  } catch {
5527
5448
  }
5528
- const files = walkMdFiles2("src/roadmap");
5449
+ const files = walkMdFiles("src/roadmap");
5529
5450
  if (files.length > 0) {
5530
5451
  const lines = files.map((f) => {
5531
- const fm = parseFrontmatter2(f);
5452
+ const fm = parseFrontmatter(f);
5532
5453
  let line = `- ${f}`;
5533
5454
  if (fm.name) {
5534
5455
  line += ` \u2014 "${fm.name}"`;
@@ -5757,6 +5678,62 @@ var init_tools4 = __esm({
5757
5678
  }
5758
5679
  });
5759
5680
 
5681
+ // src/orgContext.ts
5682
+ async function initOrgContext(config) {
5683
+ try {
5684
+ cached = await fetchRemyContext(config);
5685
+ const orgDefaultModels2 = filterModelPicks(cached?.defaultModels);
5686
+ setOrgDefaultModels(orgDefaultModels2);
5687
+ log9.debug("org context loaded", {
5688
+ delegatedAvailable: cached?.auth?.delegatedAvailable ?? false,
5689
+ requireDelegatedOnly: cached?.auth?.requireDelegatedOnly ?? false,
5690
+ hasOrgName: !!cached?.org?.name,
5691
+ orgDefaultModels: orgDefaultModels2
5692
+ });
5693
+ } catch (err) {
5694
+ cached = null;
5695
+ setOrgDefaultModels({});
5696
+ log9.debug("org context init failed", { error: err.message });
5697
+ }
5698
+ }
5699
+ function getOrgContext() {
5700
+ return cached;
5701
+ }
5702
+ function renderOrgContextBlock() {
5703
+ const ctx = cached;
5704
+ const auth = ctx?.auth;
5705
+ if (!auth || !auth.delegatedAvailable && !auth.requireDelegatedOnly) {
5706
+ return "";
5707
+ }
5708
+ const lines = ["<org_auth_context>"];
5709
+ if (ctx?.org?.name && ctx?.org?.name !== "Personal Workspace") {
5710
+ lines.push(`This app is owned by the organization "${ctx.org.name}".`);
5711
+ }
5712
+ if (auth.delegatedAvailable) {
5713
+ lines.push(
5714
+ '"Sign in with Remy" (platform-delegated sign-in) is an available auth type for this app: organization members can sign in without a verification code.'
5715
+ );
5716
+ }
5717
+ if (auth.requireDelegatedOnly) {
5718
+ lines.push(
5719
+ "This organization requires delegated sign-in: non-delegated human auth methods (email-code, sms-code) are blocked at the platform edge for its apps."
5720
+ );
5721
+ }
5722
+ lines.push("</org_auth_context>");
5723
+ return lines.join("\n");
5724
+ }
5725
+ var log9, cached;
5726
+ var init_orgContext = __esm({
5727
+ "src/orgContext.ts"() {
5728
+ "use strict";
5729
+ init_api();
5730
+ init_surfaces();
5731
+ init_logger();
5732
+ log9 = createLogger("orgContext");
5733
+ cached = null;
5734
+ }
5735
+ });
5736
+
5760
5737
  // src/subagents/designExpert/data/sampleCache.ts
5761
5738
  import fs17 from "fs";
5762
5739
  function generateIndices(poolSize, sampleSize) {
@@ -5978,7 +5955,7 @@ This project is in the "${state}" phase. The codebase is a placeholder scaffold
5978
5955
  return prompt;
5979
5956
  }
5980
5957
  var SUBAGENT, RUNTIME_PLACEHOLDERS, PROMPT_TEMPLATE;
5981
- var init_prompt3 = __esm({
5958
+ var init_prompt2 = __esm({
5982
5959
  "src/subagents/designExpert/prompt.ts"() {
5983
5960
  "use strict";
5984
5961
  init_assets();
@@ -6100,7 +6077,7 @@ var init_designExpert = __esm({
6100
6077
  init_runner();
6101
6078
  init_tools4();
6102
6079
  init_tools2();
6103
- init_prompt3();
6080
+ init_prompt2();
6104
6081
  init_history();
6105
6082
  init_surfaces();
6106
6083
  init_writeFile();
@@ -6336,7 +6313,7 @@ function getProductVisionPrompt() {
6336
6313
  return parts.join("\n\n");
6337
6314
  }
6338
6315
  var BASE_PROMPT3;
6339
- var init_prompt4 = __esm({
6316
+ var init_prompt3 = __esm({
6340
6317
  "src/subagents/productVision/prompt.ts"() {
6341
6318
  "use strict";
6342
6319
  init_assets();
@@ -6356,7 +6333,7 @@ var init_productVision = __esm({
6356
6333
  init_tools5();
6357
6334
  init_tools2();
6358
6335
  init_executor();
6359
- init_prompt4();
6336
+ init_prompt3();
6360
6337
  init_history();
6361
6338
  init_surfaces();
6362
6339
  productVisionTool = {
@@ -6618,544 +6595,332 @@ ${readAsset(
6618
6595
  return "Error: spec sync requires execution context";
6619
6596
  }
6620
6597
  if (context.onboardingState !== "onboardingFinished") {
6621
- return "Spec sync runs only after the build is finished (onboardingFinished). During intake and the initial build you author the spec directly, so there is nothing to reconcile yet.";
6622
- }
6623
- const specIndex = loadSpecIndex();
6624
- const parts = [
6625
- BASE_PROMPT5,
6626
- loadPlatformBrief(),
6627
- MSFM_DOCS,
6628
- "<!-- cache_breakpoint -->"
6629
- ];
6630
- if (specIndex) {
6631
- parts.push(specIndex);
6632
- }
6633
- const system = parts.join("\n\n");
6634
- const result = await runSubAgent({
6635
- system,
6636
- task: input.task,
6637
- tools: SPEC_SYNC_TOOLS,
6638
- externalTools: /* @__PURE__ */ new Set(),
6639
- executeTool: (name, toolInput) => executeTool(name, toolInput, context),
6640
- apiConfig: context.apiConfig,
6641
- model: resolveModel("specSync", context.models, context.model),
6642
- subAgentId: "specSync",
6643
- signal: context.signal,
6644
- parentToolId: context.toolCallId,
6645
- requestId: context.requestId,
6646
- onEvent: context.onEvent,
6647
- resolveExternalTool: context.resolveExternalTool,
6648
- toolRegistry: context.toolRegistry,
6649
- // Always background + serialized: never blocks Remy's turn, and two
6650
- // reconciliations queue (FIFO) instead of running at once.
6651
- background: true,
6652
- acquireLock: acquireSpecSyncLock,
6653
- onBackgroundComplete: (bgResult) => {
6654
- context.onBackgroundComplete?.(
6655
- context.toolCallId,
6656
- "specSync",
6657
- bgResult.text,
6658
- bgResult.messages
6659
- );
6660
- }
6661
- });
6662
- context.subAgentMessages?.set(context.toolCallId, result.messages);
6663
- return result.text;
6664
- }
6665
- };
6666
- }
6667
- });
6668
-
6669
- // src/tools/common/scrapeWebUrl.ts
6670
- var scrapeWebUrlTool;
6671
- var init_scrapeWebUrl2 = __esm({
6672
- "src/tools/common/scrapeWebUrl.ts"() {
6673
- "use strict";
6674
- init_runMindstudioCli();
6675
- scrapeWebUrlTool = {
6676
- clearable: false,
6677
- definition: {
6678
- name: "scrapeWebUrl",
6679
- description: "Scrape the content of a web page. Returns the HTML of the page as markdown text. Optionally capture a screenshot if you need see the visual design. Use this when you need to fetch or analyze content from a website",
6680
- inputSchema: {
6681
- type: "object",
6682
- properties: {
6683
- url: {
6684
- type: "string",
6685
- description: "The URL to fetch."
6686
- },
6687
- screenshot: {
6688
- type: "boolean",
6689
- description: "Capture a screenshot of the page in addition to the text content. Adds latency; only use when you need to see the visual design."
6690
- }
6691
- },
6692
- required: ["url"]
6693
- }
6694
- },
6695
- async execute(input, context) {
6696
- const url = input.url;
6697
- const screenshot = input.screenshot;
6698
- const pageOptions = { onlyMainContent: true };
6699
- if (screenshot) {
6700
- pageOptions.screenshot = true;
6701
- }
6702
- return runMindstudioCli(
6703
- [
6704
- "scrape-url",
6705
- "--url",
6706
- url,
6707
- "--page-options",
6708
- JSON.stringify(pageOptions)
6709
- ],
6710
- { onLog: context?.onLog }
6711
- );
6712
- }
6713
- };
6714
- }
6715
- });
6716
-
6717
- // src/tools/spec/writeBuildOverview.ts
6718
- import fs19 from "fs";
6719
- function initialDelivery() {
6720
- return `### Your deliverable
6721
- Write the complete Build Overview to \`${OVERVIEW_FILE}\`. The file does not exist yet \u2014 start from this scaffold, which carries the technical hygiene for the iframed render context (keep the viewport meta and the reset). Layout, sections, type, and styling are yours to compose.
6722
-
6723
- <overview_shell>
6724
- ${OVERVIEW_SHELL}
6725
- </overview_shell>
6726
-
6727
- Reply with a one-line summary of what you wrote \u2014 not the HTML.`;
6728
- }
6729
- function refreshDelivery() {
6730
- return `### Your deliverable
6731
- The Build Overview already exists at \`${OVERVIEW_FILE}\`. Read it, then update it to reflect <overview_copy>, preserving its established skin \u2014 change only what the copy changed.
6732
-
6733
- Reply with a one-line summary of what you changed \u2014 not the HTML.`;
6734
- }
6735
- var OVERVIEW_FILE, DESIGN_BRIEF, OVERVIEW_SHELL, buildOverviewTool;
6736
- var init_writeBuildOverview = __esm({
6737
- "src/tools/spec/writeBuildOverview.ts"() {
6738
- "use strict";
6739
- init_designExpert();
6740
- OVERVIEW_FILE = "src/overview.html";
6741
- DESIGN_BRIEF = `We are building the Build Overview for this app \u2014 the home page of its Spec tab. It is a calm, dense, one-page reference of everything the app actually contains, including the parts the user can't see. It renders flush inside the Spec tab's content panel (the IDE supplies the surrounding nav).
6742
-
6743
- Take the plain-language copy in <overview_copy> and lay it out and skin it into a single, beautiful, self-contained HTML document in the app's own brand.
6744
-
6745
- ### The single hard rule
6746
- The copy in <overview_copy> is final \u2014 it was authored and edited before it reached you. Treat it as locked content to typeset, not a draft to improve. Reproduce the words exactly: do not rewrite, rephrase, shorten, expand, reorder, or "polish" them, and do not run them through any copy tool. This is the opposite of your usual role \u2014 here you own layout, typography, and visual design only, and the words (every number, name, label, claim, and sentence) are fixed. A single changed word or wrong number breaks this document's purpose.
6747
-
6748
- ### What it is (and is not)
6749
- - A typeset reference dossier: composed, dense, a little cool \u2014 substantial at a glance, then readable. Density communicates substance; sparse and airy reads as "not much here."
6750
- - Brand-skinned: pull the palette, type system, and a single accent from the app's spec. The accent leads labels and figures; it is a mark, not a fill.
6751
- - NOT a slide deck \u2014 no theatrics, no building to a climax, no persuasion.
6752
- - NOT a docs site \u2014 no raised or shadowed cards, no hover-lift, no clickable-feeling surfaces, no sticky table of contents.
6753
- - NOT a sparse memo \u2014 a restrained single-column page under-sells the work.
6754
- - No header chrome and no footer: open straight on the app's logo and name so it sits flush with the IDE.
6755
-
6756
- ### Constraints
6757
- - A single self-contained HTML file. Fonts may load from a CDN; everything else (CSS, the logo SVG) is inline.
6758
- - Responsive: fills the embedded panel width and collapses gracefully at narrow widths.`;
6759
- OVERVIEW_SHELL = `<!DOCTYPE html>
6760
- <html lang="en">
6761
- <head>
6762
- <meta charset="utf-8">
6763
- <!-- This renders inside an iframe in the IDE. Keep it a simple, self-contained
6764
- static document \u2014 no scroll/zoom/accessibility scaffolding beyond this head.
6765
- The user-scalable=no viewport is intentional; leave it. -->
6766
- <meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no">
6767
- <title>Build Overview</title>
6768
- <!-- SKIN: link the app's fonts here (CDN \u2014 e.g. Fontshare / Google Fonts). -->
6769
- <style>
6770
- *, *::before, *::after { margin: 0; padding: 0; box-sizing: border-box; }
6771
- </style>
6772
- </head>
6773
- <body>
6774
- <!-- Compose the Build Overview here. Layout, sections, type, and styling are
6775
- yours \u2014 keep everything inline and self-contained, brand-skinned from the
6776
- app's spec. -->
6777
- </body>
6778
- </html>`;
6779
- buildOverviewTool = {
6780
- clearable: false,
6781
- definition: {
6782
- name: "writeBuildOverview",
6783
- description: "Generate or refresh the Build Overview \u2014 the project's home page in the Spec tab: a single-page, plain-language reference of everything the app actually contains, including the parts the user can't see (data stores, backend operations, access and roles, background jobs, seeded scenarios, the design system). You author the full copy: read the manifest and spec and state, plainly and exactly, what genuinely exists \u2014 real names and accurate counts \u2014 in calm, declarative, present-tense outcome language, with no persuasion or hype. Describe only what exists. Pass the complete copy as `content`; the design expert lays it out and skins it to the app's brand using your copy verbatim \u2014 it typesets your words, it does not rewrite them, so polish the copy before you pass it. Generate it at the end of a build and refresh it after meaningful work.",
6784
- inputSchema: {
6785
- type: "object",
6786
- properties: {
6787
- content: {
6788
- type: "string",
6789
- description: "The full Build Overview copy you authored: everything the app contains, in plain present-tense outcome language, with real names and exact counts. The design expert lays this out and skins it to the brand verbatim \u2014 it styles your words, it does not rewrite them \u2014 so this should be the final copy."
6790
- }
6791
- },
6792
- required: ["content"]
6793
- }
6794
- },
6795
- async execute(input, context) {
6796
- if (!context) {
6797
- return "Error: writeBuildOverview requires execution context for design expert delegation";
6798
- }
6799
- const content = (input.content ?? "").trim();
6800
- if (!content) {
6801
- return "Error: writeBuildOverview requires non-empty `content` (the overview copy).";
6598
+ return "Spec sync runs only after the build is finished (onboardingFinished). During intake and the initial build you author the spec directly, so there is nothing to reconcile yet.";
6802
6599
  }
6803
- const exists = fs19.existsSync(OVERVIEW_FILE);
6804
- const task = `<overview_copy>${content}</overview_copy>
6805
-
6806
- ${DESIGN_BRIEF}
6807
-
6808
- ${exists ? refreshDelivery() : initialDelivery()}`;
6809
- try {
6810
- if (exists) {
6811
- input.background = true;
6812
- const result2 = await runDesignExpertRender(
6813
- { task, background: true, reportingName: "writeBuildOverview" },
6814
- context
6600
+ const specIndex = loadSpecIndex();
6601
+ const parts = [
6602
+ BASE_PROMPT5,
6603
+ loadPlatformBrief(),
6604
+ MSFM_DOCS,
6605
+ "<!-- cache_breakpoint -->"
6606
+ ];
6607
+ if (specIndex) {
6608
+ parts.push(specIndex);
6609
+ }
6610
+ const system = parts.join("\n\n");
6611
+ const result = await runSubAgent({
6612
+ system,
6613
+ task: input.task,
6614
+ tools: SPEC_SYNC_TOOLS,
6615
+ externalTools: /* @__PURE__ */ new Set(),
6616
+ executeTool: (name, toolInput) => executeTool(name, toolInput, context),
6617
+ apiConfig: context.apiConfig,
6618
+ model: resolveModel("specSync", context.models, context.model),
6619
+ subAgentId: "specSync",
6620
+ signal: context.signal,
6621
+ parentToolId: context.toolCallId,
6622
+ requestId: context.requestId,
6623
+ onEvent: context.onEvent,
6624
+ resolveExternalTool: context.resolveExternalTool,
6625
+ toolRegistry: context.toolRegistry,
6626
+ // Always background + serialized: never blocks Remy's turn, and two
6627
+ // reconciliations queue (FIFO) instead of running at once.
6628
+ background: true,
6629
+ acquireLock: acquireSpecSyncLock,
6630
+ onBackgroundComplete: (bgResult) => {
6631
+ context.onBackgroundComplete?.(
6632
+ context.toolCallId,
6633
+ "specSync",
6634
+ bgResult.text,
6635
+ bgResult.messages
6815
6636
  );
6816
- context.subAgentMessages?.set(context.toolCallId, result2.messages);
6817
- return result2.text;
6818
- }
6819
- const result = await runDesignExpertRender({ task }, context);
6820
- context.subAgentMessages?.set(context.toolCallId, result.messages);
6821
- if (!fs19.existsSync(OVERVIEW_FILE)) {
6822
- return `Error: the design expert did not write ${OVERVIEW_FILE}. Its reply was:
6823
- ${result.text}`;
6824
6637
  }
6825
- return `Build overview written to ${OVERVIEW_FILE}. ${result.text}`;
6826
- } catch (err) {
6827
- return `Error generating build overview: ${err.message}`;
6828
- }
6638
+ });
6639
+ context.subAgentMessages?.set(context.toolCallId, result.messages);
6640
+ return result.text;
6829
6641
  }
6830
6642
  };
6831
6643
  }
6832
6644
  });
6833
6645
 
6834
- // src/tools/index.ts
6835
- function deriveContext(parent, toolCallId) {
6836
- return { ...parent, toolCallId };
6837
- }
6838
- function getToolDefinitions(_onboardingState) {
6839
- return ALL_TOOLS.map((t) => t.definition);
6840
- }
6841
- function getToolByName(name) {
6842
- return ALL_TOOLS.find((t) => t.definition.name === name);
6843
- }
6844
- function executeTool(name, input, context) {
6845
- const tool = getToolByName(name);
6846
- if (!tool) {
6847
- return Promise.resolve(`Error: Unknown tool "${name}"`);
6848
- }
6849
- return tool.execute(input, context);
6850
- }
6851
- var ALL_TOOLS, CLEARABLE_TOOLS, SUBAGENT_TOOL_NAMES;
6852
- var init_tools8 = __esm({
6853
- "src/tools/index.ts"() {
6646
+ // src/tools/common/scrapeWebUrl.ts
6647
+ var scrapeWebUrlTool;
6648
+ var init_scrapeWebUrl2 = __esm({
6649
+ "src/tools/common/scrapeWebUrl.ts"() {
6854
6650
  "use strict";
6855
- init_readSpec();
6856
- init_writeSpec();
6857
- init_editSpec();
6858
- init_listSpecFiles();
6859
- init_presentPublishPlan();
6860
- init_writePlan();
6861
- init_updatePlanStatus();
6862
- init_setProjectOnboardingState();
6863
- init_promptUser();
6864
- init_confirmDestructiveAction();
6865
- init_sdkConsultant();
6866
- init_searchGoogle();
6867
- init_setProjectMetadata();
6868
- init_compactConversation();
6869
- init_readFile();
6870
- init_writeFile();
6871
- init_editFile();
6872
- init_bash();
6873
- init_grep();
6874
- init_glob();
6875
- init_listDir();
6876
- init_editsFinished();
6877
- init_lspDiagnostics();
6878
- init_restartProcess();
6879
- init_runScenario();
6880
- init_runMethod();
6881
- init_queryDatabase();
6882
- init_screenshot2();
6883
- init_browserAutomation();
6884
- init_designExpert();
6885
- init_productVision();
6886
- init_codeSanityCheck();
6887
- init_copyEditor();
6888
- init_specSync();
6889
- init_scrapeWebUrl2();
6890
- init_writeBuildOverview();
6891
- ALL_TOOLS = [
6892
- // Common
6893
- setProjectOnboardingStateTool,
6894
- promptUserTool,
6895
- confirmDestructiveActionTool,
6896
- askMindStudioSdkTool,
6897
- scrapeWebUrlTool,
6898
- searchGoogleTool,
6899
- setProjectMetadataTool,
6900
- designExpertTool,
6901
- productVisionTool,
6902
- codeSanityCheckTool,
6903
- copyEditorTool,
6904
- specSyncTool,
6905
- // no-ops until onboardingFinished; kept here so the tool list stays cache-stable
6906
- buildOverviewTool,
6907
- compactConversationTool,
6908
- // Post-onboarding
6909
- presentPublishPlanTool,
6910
- writePlanTool,
6911
- updatePlanStatusTool,
6912
- // Spec
6913
- readSpecTool,
6914
- writeSpecTool,
6915
- editSpecTool,
6916
- listSpecFilesTool,
6917
- // Code
6918
- readFileTool,
6919
- writeFileTool,
6920
- editFileTool,
6921
- bashTool,
6922
- grepTool,
6923
- globTool,
6924
- listDirTool,
6925
- editsFinishedTool,
6926
- runScenarioTool,
6927
- runMethodTool,
6928
- queryDatabaseTool,
6929
- screenshotTool,
6930
- browserAutomationTool,
6931
- // LSP
6932
- lspDiagnosticsTool,
6933
- restartProcessTool
6934
- ];
6935
- CLEARABLE_TOOLS = new Set(
6936
- ALL_TOOLS.filter((t) => t.clearable).map((t) => t.definition.name)
6937
- );
6938
- SUBAGENT_TOOL_NAMES = /* @__PURE__ */ new Set([
6939
- "visualDesignExpert",
6940
- "productVision",
6941
- "codeSanityCheck",
6942
- "copyEditor",
6943
- "specSync",
6944
- "runAutomatedBrowserTest",
6945
- "askMindStudioSdk"
6946
- ]);
6947
- }
6948
- });
6949
-
6950
- // src/session.ts
6951
- import fs20 from "fs";
6952
- import path9 from "path";
6953
- function loadSession(state) {
6954
- pruneArchives();
6955
- try {
6956
- const raw = fs20.readFileSync(SESSION_FILE, "utf-8");
6957
- const data = JSON.parse(raw);
6958
- if (data.models && typeof data.models === "object") {
6959
- state.models = data.models;
6960
- }
6961
- if (Array.isArray(data.messages) && data.messages.length > 0) {
6962
- state.messages = sanitizeMessages(data.messages);
6963
- log9.info("Session loaded", {
6964
- messageCount: state.messages.length,
6965
- ...state.models && { models: state.models }
6966
- });
6967
- return true;
6968
- }
6969
- } catch {
6970
- }
6971
- return false;
6972
- }
6973
- function capOversizedResults(msg) {
6974
- if (msg.role === "assistant" && Array.isArray(msg.content)) {
6975
- for (const block of msg.content) {
6976
- if (block.type === "tool" && typeof block.result === "string") {
6977
- block.result = capToolResult(block.result);
6978
- }
6979
- }
6980
- } else if (msg.role === "user" && msg.toolCallId && typeof msg.content === "string") {
6981
- msg.content = capToolResult(msg.content);
6982
- }
6983
- }
6984
- function sanitizeMessages(messages) {
6985
- const result = [];
6986
- for (let i = 0; i < messages.length; i++) {
6987
- const msg = messages[i];
6988
- capOversizedResults(msg);
6989
- result.push(msg);
6990
- if (msg.role !== "assistant" || !Array.isArray(msg.content)) {
6991
- continue;
6992
- }
6993
- const toolBlocks = msg.content.filter(
6994
- (b) => b.type === "tool"
6995
- );
6996
- if (toolBlocks.length === 0) {
6997
- continue;
6998
- }
6999
- const resultIds = /* @__PURE__ */ new Set();
7000
- for (let j = i + 1; j < messages.length; j++) {
7001
- const next = messages[j];
7002
- if (next.role === "user" && next.toolCallId) {
7003
- resultIds.add(next.toolCallId);
7004
- } else {
7005
- break;
7006
- }
7007
- }
7008
- for (const tc of toolBlocks) {
7009
- if (!resultIds.has(tc.id)) {
7010
- result.push({
7011
- role: "user",
7012
- content: "Error: tool result lost (session recovered)",
7013
- toolCallId: tc.id,
7014
- isToolError: true
7015
- });
6651
+ init_runMindstudioCli();
6652
+ scrapeWebUrlTool = {
6653
+ clearable: false,
6654
+ definition: {
6655
+ name: "scrapeWebUrl",
6656
+ description: "Scrape the content of a web page. Returns the HTML of the page as markdown text. Optionally capture a screenshot if you need see the visual design. Use this when you need to fetch or analyze content from a website",
6657
+ inputSchema: {
6658
+ type: "object",
6659
+ properties: {
6660
+ url: {
6661
+ type: "string",
6662
+ description: "The URL to fetch."
6663
+ },
6664
+ screenshot: {
6665
+ type: "boolean",
6666
+ description: "Capture a screenshot of the page in addition to the text content. Adds latency; only use when you need to see the visual design."
6667
+ }
6668
+ },
6669
+ required: ["url"]
6670
+ }
6671
+ },
6672
+ async execute(input, context) {
6673
+ const url = input.url;
6674
+ const screenshot = input.screenshot;
6675
+ const pageOptions = { onlyMainContent: true };
6676
+ if (screenshot) {
6677
+ pageOptions.screenshot = true;
6678
+ }
6679
+ return runMindstudioCli(
6680
+ [
6681
+ "scrape-url",
6682
+ "--url",
6683
+ url,
6684
+ "--page-options",
6685
+ JSON.stringify(pageOptions)
6686
+ ],
6687
+ { onLog: context?.onLog }
6688
+ );
7016
6689
  }
7017
- }
7018
- }
7019
- return result;
7020
- }
7021
- function buildPayload(state) {
7022
- const payload = { messages: state.messages };
7023
- if (state.models && Object.keys(state.models).length > 0) {
7024
- payload.models = state.models;
6690
+ };
7025
6691
  }
7026
- return payload;
6692
+ });
6693
+
6694
+ // src/tools/spec/writeBuildOverview.ts
6695
+ import fs19 from "fs";
6696
+ function initialDelivery() {
6697
+ return `### Your deliverable
6698
+ Write the complete Build Overview to \`${OVERVIEW_FILE}\`. The file does not exist yet \u2014 start from this scaffold, which carries the technical hygiene for the iframed render context (keep the viewport meta and the reset). Layout, sections, type, and styling are yours to compose.
6699
+
6700
+ <overview_shell>
6701
+ ${OVERVIEW_SHELL}
6702
+ </overview_shell>
6703
+
6704
+ Reply with a one-line summary of what you wrote \u2014 not the HTML.`;
7027
6705
  }
7028
- function archiveMessages(messages, label, models) {
7029
- fs20.mkdirSync(ARCHIVE_DIR, { recursive: true });
7030
- const ts = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
7031
- let dest = path9.join(ARCHIVE_DIR, `${label}-${ts}.json`);
7032
- let n = 1;
7033
- while (fs20.existsSync(dest)) {
7034
- dest = path9.join(ARCHIVE_DIR, `${label}-${ts}-${n++}.json`);
7035
- }
7036
- const payload = { messages };
7037
- if (models && Object.keys(models).length > 0) {
7038
- payload.models = models;
7039
- }
7040
- fs20.writeFileSync(dest, JSON.stringify(payload), "utf-8");
7041
- log9.info("Session archived", { label, dest, messageCount: messages.length });
7042
- pruneArchives();
7043
- return dest;
6706
+ function refreshDelivery() {
6707
+ return `### Your deliverable
6708
+ The Build Overview already exists at \`${OVERVIEW_FILE}\`. Read it, then update it to reflect <overview_copy>, preserving its established skin \u2014 change only what the copy changed.
6709
+
6710
+ Reply with a one-line summary of what you changed \u2014 not the HTML.`;
7044
6711
  }
7045
- function pruneArchives() {
7046
- try {
7047
- const entries = fs20.readdirSync(ARCHIVE_DIR).filter((name) => /^(cleared|rotated)-.*\.json$/.test(name));
7048
- if (entries.length <= 1) {
7049
- return;
7050
- }
7051
- const sortKey = (name) => name.replace(/^(cleared|rotated)-/, "");
7052
- const archives = entries.map((name) => ({
7053
- name,
7054
- size: fs20.statSync(path9.join(ARCHIVE_DIR, name)).size
7055
- })).sort((a, b) => sortKey(b.name).localeCompare(sortKey(a.name)));
7056
- let kept = 0;
7057
- let cut = archives.length;
7058
- for (let i = 0; i < archives.length; i++) {
7059
- if (i === 0 || kept + archives[i].size <= ARCHIVE_RETENTION_BYTES) {
7060
- kept += archives[i].size;
7061
- } else {
7062
- cut = i;
7063
- break;
7064
- }
7065
- }
7066
- let removed = 0;
7067
- let freed = 0;
7068
- for (let i = cut; i < archives.length; i++) {
7069
- try {
7070
- fs20.unlinkSync(path9.join(ARCHIVE_DIR, archives[i].name));
7071
- freed += archives[i].size;
7072
- removed++;
7073
- } catch {
6712
+ var OVERVIEW_FILE, DESIGN_BRIEF, OVERVIEW_SHELL, buildOverviewTool;
6713
+ var init_writeBuildOverview = __esm({
6714
+ "src/tools/spec/writeBuildOverview.ts"() {
6715
+ "use strict";
6716
+ init_designExpert();
6717
+ OVERVIEW_FILE = "src/overview.html";
6718
+ DESIGN_BRIEF = `We are building the Build Overview for this app \u2014 the home page of its Spec tab. It is a calm, dense, one-page reference of everything the app actually contains, including the parts the user can't see. It renders flush inside the Spec tab's content panel (the IDE supplies the surrounding nav).
6719
+
6720
+ Take the plain-language copy in <overview_copy> and lay it out and skin it into a single, beautiful, self-contained HTML document in the app's own brand.
6721
+
6722
+ ### The single hard rule
6723
+ The copy in <overview_copy> is final \u2014 it was authored and edited before it reached you. Treat it as locked content to typeset, not a draft to improve. Reproduce the words exactly: do not rewrite, rephrase, shorten, expand, reorder, or "polish" them, and do not run them through any copy tool. This is the opposite of your usual role \u2014 here you own layout, typography, and visual design only, and the words (every number, name, label, claim, and sentence) are fixed. A single changed word or wrong number breaks this document's purpose.
6724
+
6725
+ ### What it is (and is not)
6726
+ - A typeset reference dossier: composed, dense, a little cool \u2014 substantial at a glance, then readable. Density communicates substance; sparse and airy reads as "not much here."
6727
+ - Brand-skinned: pull the palette, type system, and a single accent from the app's spec. The accent leads labels and figures; it is a mark, not a fill.
6728
+ - NOT a slide deck \u2014 no theatrics, no building to a climax, no persuasion.
6729
+ - NOT a docs site \u2014 no raised or shadowed cards, no hover-lift, no clickable-feeling surfaces, no sticky table of contents.
6730
+ - NOT a sparse memo \u2014 a restrained single-column page under-sells the work.
6731
+ - No header chrome and no footer: open straight on the app's logo and name so it sits flush with the IDE.
6732
+
6733
+ ### Constraints
6734
+ - A single self-contained HTML file. Fonts may load from a CDN; everything else (CSS, the logo SVG) is inline.
6735
+ - Responsive: fills the embedded panel width and collapses gracefully at narrow widths.`;
6736
+ OVERVIEW_SHELL = `<!DOCTYPE html>
6737
+ <html lang="en">
6738
+ <head>
6739
+ <meta charset="utf-8">
6740
+ <!-- This renders inside an iframe in the IDE. Keep it a simple, self-contained
6741
+ static document \u2014 no scroll/zoom/accessibility scaffolding beyond this head.
6742
+ The user-scalable=no viewport is intentional; leave it. -->
6743
+ <meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no">
6744
+ <title>Build Overview</title>
6745
+ <!-- SKIN: link the app's fonts here (CDN \u2014 e.g. Fontshare / Google Fonts). -->
6746
+ <style>
6747
+ *, *::before, *::after { margin: 0; padding: 0; box-sizing: border-box; }
6748
+ </style>
6749
+ </head>
6750
+ <body>
6751
+ <!-- Compose the Build Overview here. Layout, sections, type, and styling are
6752
+ yours \u2014 keep everything inline and self-contained, brand-skinned from the
6753
+ app's spec. -->
6754
+ </body>
6755
+ </html>`;
6756
+ buildOverviewTool = {
6757
+ clearable: false,
6758
+ definition: {
6759
+ name: "writeBuildOverview",
6760
+ description: "Generate or refresh the Build Overview \u2014 the project's home page in the Spec tab: a single-page, plain-language reference of everything the app actually contains, including the parts the user can't see (data stores, backend operations, access and roles, background jobs, seeded scenarios, the design system). You author the full copy: read the manifest and spec and state, plainly and exactly, what genuinely exists \u2014 real names and accurate counts \u2014 in calm, declarative, present-tense outcome language, with no persuasion or hype. Describe only what exists. Pass the complete copy as `content`; the design expert lays it out and skins it to the app's brand using your copy verbatim \u2014 it typesets your words, it does not rewrite them, so polish the copy before you pass it. Generate it at the end of a build and refresh it after meaningful work.",
6761
+ inputSchema: {
6762
+ type: "object",
6763
+ properties: {
6764
+ content: {
6765
+ type: "string",
6766
+ description: "The full Build Overview copy you authored: everything the app contains, in plain present-tense outcome language, with real names and exact counts. The design expert lays this out and skins it to the brand verbatim \u2014 it styles your words, it does not rewrite them \u2014 so this should be the final copy."
6767
+ }
6768
+ },
6769
+ required: ["content"]
6770
+ }
6771
+ },
6772
+ async execute(input, context) {
6773
+ if (!context) {
6774
+ return "Error: writeBuildOverview requires execution context for design expert delegation";
6775
+ }
6776
+ const content = (input.content ?? "").trim();
6777
+ if (!content) {
6778
+ return "Error: writeBuildOverview requires non-empty `content` (the overview copy).";
6779
+ }
6780
+ const exists = fs19.existsSync(OVERVIEW_FILE);
6781
+ const task = `<overview_copy>${content}</overview_copy>
6782
+
6783
+ ${DESIGN_BRIEF}
6784
+
6785
+ ${exists ? refreshDelivery() : initialDelivery()}`;
6786
+ try {
6787
+ if (exists) {
6788
+ input.background = true;
6789
+ const result2 = await runDesignExpertRender(
6790
+ { task, background: true, reportingName: "writeBuildOverview" },
6791
+ context
6792
+ );
6793
+ context.subAgentMessages?.set(context.toolCallId, result2.messages);
6794
+ return result2.text;
6795
+ }
6796
+ const result = await runDesignExpertRender({ task }, context);
6797
+ context.subAgentMessages?.set(context.toolCallId, result.messages);
6798
+ if (!fs19.existsSync(OVERVIEW_FILE)) {
6799
+ return `Error: the design expert did not write ${OVERVIEW_FILE}. Its reply was:
6800
+ ${result.text}`;
6801
+ }
6802
+ return `Build overview written to ${OVERVIEW_FILE}. ${result.text}`;
6803
+ } catch (err) {
6804
+ return `Error generating build overview: ${err.message}`;
6805
+ }
7074
6806
  }
7075
- }
7076
- if (removed > 0) {
7077
- log9.info("Session archives pruned", {
7078
- removed,
7079
- freedBytes: freed,
7080
- keptBytes: kept
7081
- });
7082
- }
7083
- } catch {
6807
+ };
7084
6808
  }
6809
+ });
6810
+
6811
+ // src/tools/index.ts
6812
+ function deriveContext(parent, toolCallId) {
6813
+ return { ...parent, toolCallId };
7085
6814
  }
7086
- function rotate(state) {
7087
- const messages = state.messages;
7088
- if (messages.length === 0) {
7089
- return false;
7090
- }
7091
- let tailBytes = 0;
7092
- let scrollbackStart = 0;
7093
- for (let i = messages.length - 1; i >= 0; i--) {
7094
- tailBytes += Buffer.byteLength(JSON.stringify(messages[i]), "utf-8") + 1;
7095
- if (tailBytes >= RETAIN_TAIL_BYTES) {
7096
- scrollbackStart = i;
7097
- break;
7098
- }
7099
- }
7100
- const checkpointIdx = findLastSummaryCheckpoint(messages, "conversation");
7101
- let cut = checkpointIdx === -1 ? scrollbackStart : Math.min(scrollbackStart, checkpointIdx);
7102
- cut = findSafeInsertionPoint(messages, cut);
7103
- if (cut <= 0) {
7104
- return false;
7105
- }
7106
- archiveMessages(messages.slice(0, cut), "rotated", state.models);
7107
- state.messages = messages.slice(cut);
7108
- log9.info("Session rotated", {
7109
- archived: cut,
7110
- retained: state.messages.length
7111
- });
7112
- return true;
6815
+ function getToolDefinitions(_onboardingState) {
6816
+ return ALL_TOOLS.map((t) => t.definition);
7113
6817
  }
7114
- function saveSession(state) {
7115
- try {
7116
- let serialized = JSON.stringify(buildPayload(state));
7117
- if (Buffer.byteLength(serialized, "utf-8") > ROTATE_THRESHOLD_BYTES && rotate(state)) {
7118
- serialized = JSON.stringify(buildPayload(state));
7119
- }
7120
- fs20.writeFileSync(SESSION_FILE, serialized, "utf-8");
7121
- log9.info("Session saved", { messageCount: state.messages.length });
7122
- } catch (err) {
7123
- log9.warn("Session save failed", { error: err.message });
7124
- }
6818
+ function getToolByName(name) {
6819
+ return ALL_TOOLS.find((t) => t.definition.name === name);
7125
6820
  }
7126
- function clearSession(state) {
7127
- try {
7128
- if (state.messages.length > 0) {
7129
- archiveMessages(state.messages, "cleared", state.models);
7130
- }
7131
- } catch (err) {
7132
- log9.warn("Session archive on clear failed", { error: err.message });
7133
- }
7134
- state.messages = [];
7135
- try {
7136
- if (fs20.existsSync(SESSION_FILE)) {
7137
- fs20.unlinkSync(SESSION_FILE);
7138
- }
7139
- } catch (err) {
7140
- log9.warn("Session clear: could not remove live file", {
7141
- error: err.message
7142
- });
6821
+ function executeTool(name, input, context) {
6822
+ const tool = getToolByName(name);
6823
+ if (!tool) {
6824
+ return Promise.resolve(`Error: Unknown tool "${name}"`);
7143
6825
  }
6826
+ return tool.execute(input, context);
7144
6827
  }
7145
- var log9, SESSION_FILE, ARCHIVE_DIR, ROTATE_THRESHOLD_BYTES, RETAIN_TAIL_BYTES, ARCHIVE_RETENTION_BYTES;
7146
- var init_session = __esm({
7147
- "src/session.ts"() {
6828
+ var ALL_TOOLS, CLEARABLE_TOOLS, SUBAGENT_TOOL_NAMES;
6829
+ var init_tools8 = __esm({
6830
+ "src/tools/index.ts"() {
7148
6831
  "use strict";
7149
- init_logger();
7150
- init_compaction();
7151
- init_cleanMessages();
7152
- init_toolResultCap();
7153
- log9 = createLogger("session");
7154
- SESSION_FILE = ".remy-session.json";
7155
- ARCHIVE_DIR = ".logs/sessions";
7156
- ROTATE_THRESHOLD_BYTES = 32 * 1024 * 1024;
7157
- RETAIN_TAIL_BYTES = 16 * 1024 * 1024;
7158
- ARCHIVE_RETENTION_BYTES = 64 * 1024 * 1024;
6832
+ init_readSpec();
6833
+ init_writeSpec();
6834
+ init_editSpec();
6835
+ init_listSpecFiles();
6836
+ init_presentPublishPlan();
6837
+ init_writePlan();
6838
+ init_updatePlanStatus();
6839
+ init_setProjectOnboardingState();
6840
+ init_promptUser();
6841
+ init_confirmDestructiveAction();
6842
+ init_sdkConsultant();
6843
+ init_searchGoogle();
6844
+ init_setProjectMetadata();
6845
+ init_compactConversation();
6846
+ init_readFile();
6847
+ init_writeFile();
6848
+ init_editFile();
6849
+ init_bash();
6850
+ init_grep();
6851
+ init_glob();
6852
+ init_listDir();
6853
+ init_editsFinished();
6854
+ init_lspDiagnostics();
6855
+ init_restartProcess();
6856
+ init_runScenario();
6857
+ init_runMethod();
6858
+ init_queryDatabase();
6859
+ init_screenshot2();
6860
+ init_browserAutomation();
6861
+ init_designExpert();
6862
+ init_productVision();
6863
+ init_codeSanityCheck();
6864
+ init_copyEditor();
6865
+ init_specSync();
6866
+ init_scrapeWebUrl2();
6867
+ init_writeBuildOverview();
6868
+ ALL_TOOLS = [
6869
+ // Common
6870
+ setProjectOnboardingStateTool,
6871
+ promptUserTool,
6872
+ confirmDestructiveActionTool,
6873
+ askMindStudioSdkTool,
6874
+ scrapeWebUrlTool,
6875
+ searchGoogleTool,
6876
+ setProjectMetadataTool,
6877
+ designExpertTool,
6878
+ productVisionTool,
6879
+ codeSanityCheckTool,
6880
+ copyEditorTool,
6881
+ specSyncTool,
6882
+ // no-ops until onboardingFinished; kept here so the tool list stays cache-stable
6883
+ buildOverviewTool,
6884
+ compactConversationTool,
6885
+ // Post-onboarding
6886
+ presentPublishPlanTool,
6887
+ writePlanTool,
6888
+ updatePlanStatusTool,
6889
+ // Spec
6890
+ readSpecTool,
6891
+ writeSpecTool,
6892
+ editSpecTool,
6893
+ listSpecFilesTool,
6894
+ // Code
6895
+ readFileTool,
6896
+ writeFileTool,
6897
+ editFileTool,
6898
+ bashTool,
6899
+ grepTool,
6900
+ globTool,
6901
+ listDirTool,
6902
+ editsFinishedTool,
6903
+ runScenarioTool,
6904
+ runMethodTool,
6905
+ queryDatabaseTool,
6906
+ screenshotTool,
6907
+ browserAutomationTool,
6908
+ // LSP
6909
+ lspDiagnosticsTool,
6910
+ restartProcessTool
6911
+ ];
6912
+ CLEARABLE_TOOLS = new Set(
6913
+ ALL_TOOLS.filter((t) => t.clearable).map((t) => t.definition.name)
6914
+ );
6915
+ SUBAGENT_TOOL_NAMES = /* @__PURE__ */ new Set([
6916
+ "visualDesignExpert",
6917
+ "productVision",
6918
+ "codeSanityCheck",
6919
+ "copyEditor",
6920
+ "specSync",
6921
+ "runAutomatedBrowserTest",
6922
+ "askMindStudioSdk"
6923
+ ]);
7159
6924
  }
7160
6925
  });
7161
6926
 
@@ -7369,8 +7134,8 @@ var init_errors = __esm({
7369
7134
  });
7370
7135
 
7371
7136
  // src/brandExtraction/index.ts
7372
- import fs21 from "fs";
7373
- import path10 from "path";
7137
+ import fs20 from "fs";
7138
+ import path9 from "path";
7374
7139
  import { createHash } from "crypto";
7375
7140
  async function runExtraction(apiConfig, model) {
7376
7141
  const inputHash = computeInputHash();
@@ -7390,15 +7155,15 @@ async function runExtraction(apiConfig, model) {
7390
7155
  return brand;
7391
7156
  }
7392
7157
  function isBrandRelevant(filePath) {
7393
- if (filePath === path10.join("src", "app.md")) {
7158
+ if (filePath === path9.join("src", "app.md")) {
7394
7159
  return true;
7395
7160
  }
7396
- const { type } = parseFrontmatter3(filePath);
7161
+ const { type } = parseFrontmatter2(filePath);
7397
7162
  return type.startsWith("design/color") || type.startsWith("design/typography");
7398
7163
  }
7399
7164
  function computeInputHash() {
7400
7165
  const entries = [];
7401
- for (const filePath of walkMdFiles3("src")) {
7166
+ for (const filePath of walkMdFiles2("src")) {
7402
7167
  if (isBrandRelevant(filePath)) {
7403
7168
  entries.push({ path: filePath, content: readSafe(filePath) });
7404
7169
  }
@@ -7416,19 +7181,19 @@ function sha256(input) {
7416
7181
  }
7417
7182
  function readSafe(filePath) {
7418
7183
  try {
7419
- return fs21.readFileSync(filePath, "utf-8");
7184
+ return fs20.readFileSync(filePath, "utf-8");
7420
7185
  } catch {
7421
7186
  return "";
7422
7187
  }
7423
7188
  }
7424
- function walkMdFiles3(dir) {
7189
+ function walkMdFiles2(dir) {
7425
7190
  const results = [];
7426
7191
  try {
7427
- const entries = fs21.readdirSync(dir, { withFileTypes: true });
7192
+ const entries = fs20.readdirSync(dir, { withFileTypes: true });
7428
7193
  for (const entry of entries) {
7429
- const full = path10.join(dir, entry.name);
7194
+ const full = path9.join(dir, entry.name);
7430
7195
  if (entry.isDirectory()) {
7431
- results.push(...walkMdFiles3(full));
7196
+ results.push(...walkMdFiles2(full));
7432
7197
  } else if (entry.name.endsWith(".md")) {
7433
7198
  results.push(full);
7434
7199
  }
@@ -7437,9 +7202,9 @@ function walkMdFiles3(dir) {
7437
7202
  }
7438
7203
  return results.sort();
7439
7204
  }
7440
- function parseFrontmatter3(filePath) {
7205
+ function parseFrontmatter2(filePath) {
7441
7206
  try {
7442
- const content = fs21.readFileSync(filePath, "utf-8");
7207
+ const content = fs20.readFileSync(filePath, "utf-8");
7443
7208
  const match = content.match(/^---\n([\s\S]*?)\n---/);
7444
7209
  if (!match) {
7445
7210
  return { type: "" };
@@ -7503,7 +7268,7 @@ async function extractBrand(apiConfig, model) {
7503
7268
  return validateBrand(parsed);
7504
7269
  }
7505
7270
  function buildCorpus() {
7506
- const all = walkMdFiles3("src");
7271
+ const all = walkMdFiles2("src");
7507
7272
  const ordered = [
7508
7273
  ...all.filter(isBrandRelevant),
7509
7274
  ...all.filter((f) => !isBrandRelevant(f))
@@ -7636,14 +7401,14 @@ function pickFont(raw) {
7636
7401
  }
7637
7402
  function persistBrand(brand, inputHash) {
7638
7403
  const tmp = `${BRAND_FILE}.tmp`;
7639
- fs21.writeFileSync(tmp, JSON.stringify(brand, null, 2), "utf-8");
7640
- fs21.renameSync(tmp, BRAND_FILE);
7404
+ fs20.writeFileSync(tmp, JSON.stringify(brand, null, 2), "utf-8");
7405
+ fs20.renameSync(tmp, BRAND_FILE);
7641
7406
  const cache = { inputHash, generatedAt: Date.now() };
7642
- fs21.writeFileSync(CACHE_FILE, JSON.stringify(cache, null, 2), "utf-8");
7407
+ fs20.writeFileSync(CACHE_FILE, JSON.stringify(cache, null, 2), "utf-8");
7643
7408
  }
7644
7409
  function readCache() {
7645
7410
  try {
7646
- const raw = fs21.readFileSync(CACHE_FILE, "utf-8");
7411
+ const raw = fs20.readFileSync(CACHE_FILE, "utf-8");
7647
7412
  const parsed = JSON.parse(raw);
7648
7413
  if (parsed && typeof parsed.inputHash === "string" && typeof parsed.generatedAt === "number") {
7649
7414
  return parsed;
@@ -8264,64 +8029,349 @@ async function runTurn(params) {
8264
8029
  }
8265
8030
  }
8266
8031
  }
8267
- const lastNonExcluded = toolCalls.filter(
8268
- (tc) => !STATUS_EXCLUDED_TOOLS.has(tc.name)
8269
- );
8270
- lastCompletedTools = lastNonExcluded.map((tc) => tc.name).join(", ");
8271
- lastCompletedInput = JSON.stringify(lastNonExcluded.at(-1)?.input ?? {});
8272
- lastCompletedResult = results.at(-1)?.result ?? "";
8273
- for (const r of results) {
8274
- state.messages.push({
8275
- role: "user",
8276
- content: r.result,
8277
- toolCallId: r.id,
8278
- isToolError: r.isError
8279
- });
8032
+ const lastNonExcluded = toolCalls.filter(
8033
+ (tc) => !STATUS_EXCLUDED_TOOLS.has(tc.name)
8034
+ );
8035
+ lastCompletedTools = lastNonExcluded.map((tc) => tc.name).join(", ");
8036
+ lastCompletedInput = JSON.stringify(lastNonExcluded.at(-1)?.input ?? {});
8037
+ lastCompletedResult = results.at(-1)?.result ?? "";
8038
+ for (const r of results) {
8039
+ state.messages.push({
8040
+ role: "user",
8041
+ content: r.result,
8042
+ toolCallId: r.id,
8043
+ isToolError: r.isError
8044
+ });
8045
+ }
8046
+ if (signal?.aborted) {
8047
+ onEvent({ type: "turn_cancelled" });
8048
+ saveSession(state);
8049
+ return;
8050
+ }
8051
+ }
8052
+ }
8053
+ var log12, BRAND_TRIGGERING_TOOLS, EXTERNAL_TOOLS, USER_BLOCKING_EXTERNAL_TOOLS;
8054
+ var init_agent = __esm({
8055
+ "src/agent.ts"() {
8056
+ "use strict";
8057
+ init_api();
8058
+ init_tools8();
8059
+ init_session();
8060
+ init_logger();
8061
+ init_usageLedger();
8062
+ init_parsePartialJson();
8063
+ init_statusWatcher();
8064
+ init_errors();
8065
+ init_cleanMessages();
8066
+ init_tools8();
8067
+ init_sentinel();
8068
+ init_trigger2();
8069
+ init_surfaces();
8070
+ init_toolRegistry();
8071
+ init_toolResultCap();
8072
+ log12 = createLogger("agent");
8073
+ BRAND_TRIGGERING_TOOLS = /* @__PURE__ */ new Set(["writeSpec", "editSpec"]);
8074
+ EXTERNAL_TOOLS = /* @__PURE__ */ new Set([
8075
+ "promptUser",
8076
+ "setProjectOnboardingState",
8077
+ "presentPublishPlan",
8078
+ "confirmDestructiveAction",
8079
+ "runScenario",
8080
+ "runMethod",
8081
+ "queryDatabase",
8082
+ "browserCommand",
8083
+ "setProjectMetadata"
8084
+ ]);
8085
+ USER_BLOCKING_EXTERNAL_TOOLS = /* @__PURE__ */ new Set([
8086
+ "promptUser",
8087
+ "presentPublishPlan",
8088
+ "confirmDestructiveAction"
8089
+ ]);
8090
+ }
8091
+ });
8092
+
8093
+ // src/prompt/static/projectContext.ts
8094
+ import fs21 from "fs";
8095
+ import path10 from "path";
8096
+ function loadProjectManifest() {
8097
+ try {
8098
+ const manifest = fs21.readFileSync("mindstudio.json", "utf-8");
8099
+ return `
8100
+ ## Project Manifest (mindstudio.json)
8101
+ \`\`\`json
8102
+ ${manifest}
8103
+ \`\`\``;
8104
+ } catch {
8105
+ return "";
8106
+ }
8107
+ }
8108
+ function loadSpecFileMetadata() {
8109
+ try {
8110
+ const files = walkMdFiles3("src");
8111
+ if (files.length === 0) {
8112
+ return "";
8113
+ }
8114
+ const entries = [];
8115
+ for (const filePath of files) {
8116
+ const { name, description, type } = parseFrontmatter3(filePath);
8117
+ let line = `- ${filePath}`;
8118
+ if (name) {
8119
+ line += ` \u2014 "${name}"`;
8120
+ }
8121
+ if (type) {
8122
+ line += ` (${type})`;
8123
+ }
8124
+ if (description) {
8125
+ line += ` \u2014 ${description}`;
8126
+ }
8127
+ entries.push(line);
8128
+ }
8129
+ return `
8130
+ ## Spec Files
8131
+ ${entries.join("\n")}`;
8132
+ } catch {
8133
+ return "";
8134
+ }
8135
+ }
8136
+ function walkMdFiles3(dir) {
8137
+ const results = [];
8138
+ try {
8139
+ const entries = fs21.readdirSync(dir, { withFileTypes: true });
8140
+ for (const entry of entries) {
8141
+ const full = path10.join(dir, entry.name);
8142
+ if (entry.isDirectory()) {
8143
+ results.push(...walkMdFiles3(full));
8144
+ } else if (entry.name.endsWith(".md")) {
8145
+ results.push(full);
8146
+ }
8147
+ }
8148
+ } catch {
8149
+ }
8150
+ return results.sort();
8151
+ }
8152
+ function parseFrontmatter3(filePath) {
8153
+ try {
8154
+ const content = fs21.readFileSync(filePath, "utf-8");
8155
+ const match = content.match(/^---\n([\s\S]*?)\n---/);
8156
+ if (!match) {
8157
+ return { name: "", description: "", type: "" };
8158
+ }
8159
+ const fm = match[1];
8160
+ const name = fm.match(/^name:\s*(.+)$/m)?.[1]?.trim() ?? "";
8161
+ const description = fm.match(/^description:\s*(.+)$/m)?.[1]?.trim() ?? "";
8162
+ const type = fm.match(/^type:\s*(.+)$/m)?.[1]?.trim() ?? "";
8163
+ return { name, description, type };
8164
+ } catch {
8165
+ return { name: "", description: "", type: "" };
8166
+ }
8167
+ }
8168
+ function loadPlanStatus(onboardingState) {
8169
+ try {
8170
+ const content = fs21.readFileSync(".remy-plan.md", "utf-8");
8171
+ const match = content.match(/^---\n([\s\S]*?)\n---/);
8172
+ const status = match?.[1]?.match(/^status:\s*(.+)$/m)?.[1]?.trim();
8173
+ if (status === "pending" && onboardingState === "intake") {
8174
+ return `
8175
+ <pending_initial_plan>
8176
+ This is your initial plan, proposed via writePlan and awaiting the user's decision. The user approves it by pressing "Start Building" \u2014 you'll get an approveInitialPlan message when they do, and it will tell you to begin. Until then, stay in intake: keep discussing, and revise with writePlan if the user asks. Don't start building on your own, even if the user says "looks good" in chat.
8177
+ </pending_initial_plan>`;
8178
+ }
8179
+ if (status === "pending") {
8180
+ return `
8181
+ <pending_plan>
8182
+ 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. When the user approves the plan (via chat or any other signal), call updatePlanStatus with status "approved" before beginning any implementation work.
8183
+ </pending_plan>`;
8280
8184
  }
8281
- if (signal?.aborted) {
8282
- onEvent({ type: "turn_cancelled" });
8283
- saveSession(state);
8284
- return;
8185
+ if (status === "approved") {
8186
+ return `
8187
+ <approved_plan>
8188
+ 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.
8189
+ </approved_plan>`;
8285
8190
  }
8191
+ return "";
8192
+ } catch {
8193
+ return "";
8286
8194
  }
8287
8195
  }
8288
- var log12, BRAND_TRIGGERING_TOOLS, EXTERNAL_TOOLS, USER_BLOCKING_EXTERNAL_TOOLS;
8289
- var init_agent = __esm({
8290
- "src/agent.ts"() {
8196
+ function loadProjectFileListing() {
8197
+ try {
8198
+ const entries = fs21.readdirSync(".", { withFileTypes: true });
8199
+ const listing = entries.filter((e) => e.name !== ".git" && e.name !== "node_modules").sort((a, b) => {
8200
+ if (a.isDirectory() && !b.isDirectory()) {
8201
+ return -1;
8202
+ }
8203
+ if (!a.isDirectory() && b.isDirectory()) {
8204
+ return 1;
8205
+ }
8206
+ return a.name.localeCompare(b.name);
8207
+ }).map((e) => e.isDirectory() ? `${e.name}/` : e.name).join("\n");
8208
+ return `
8209
+ ## Project Files
8210
+ \`\`\`
8211
+ ${listing}
8212
+ \`\`\``;
8213
+ } catch {
8214
+ return "";
8215
+ }
8216
+ }
8217
+ var init_projectContext = __esm({
8218
+ "src/prompt/static/projectContext.ts"() {
8291
8219
  "use strict";
8292
- init_api();
8293
- init_tools8();
8294
- init_session();
8295
- init_logger();
8296
- init_usageLedger();
8297
- init_parsePartialJson();
8298
- init_statusWatcher();
8299
- init_errors();
8300
- init_cleanMessages();
8301
- init_tools8();
8302
- init_sentinel();
8303
- init_trigger2();
8304
- init_surfaces();
8305
- init_toolRegistry();
8306
- init_toolResultCap();
8307
- log12 = createLogger("agent");
8308
- BRAND_TRIGGERING_TOOLS = /* @__PURE__ */ new Set(["writeSpec", "editSpec"]);
8309
- EXTERNAL_TOOLS = /* @__PURE__ */ new Set([
8310
- "promptUser",
8311
- "setProjectOnboardingState",
8312
- "presentPublishPlan",
8313
- "confirmDestructiveAction",
8314
- "runScenario",
8315
- "runMethod",
8316
- "queryDatabase",
8317
- "browserCommand",
8318
- "setProjectMetadata"
8319
- ]);
8320
- USER_BLOCKING_EXTERNAL_TOOLS = /* @__PURE__ */ new Set([
8321
- "promptUser",
8322
- "presentPublishPlan",
8323
- "confirmDestructiveAction"
8324
- ]);
8220
+ }
8221
+ });
8222
+
8223
+ // src/prompt/index.ts
8224
+ function resolveIncludes(template) {
8225
+ const result = template.replace(
8226
+ /\{\{([^}]+)\}\}/g,
8227
+ (_, filePath) => readAsset("prompt", filePath.trim())
8228
+ );
8229
+ return result.replace(/\n{3,}/g, "\n\n").trim();
8230
+ }
8231
+ function buildSystemPrompt(onboardingState, viewContext) {
8232
+ const projectContext = [
8233
+ loadProjectManifest(),
8234
+ loadSpecFileMetadata(),
8235
+ loadProjectFileListing()
8236
+ ].filter(Boolean).join("\n");
8237
+ const now = (/* @__PURE__ */ new Date()).toLocaleDateString("en-US", {
8238
+ month: "long",
8239
+ day: "numeric",
8240
+ year: "numeric"
8241
+ });
8242
+ const template = `
8243
+ {{static/identity.md}}
8244
+
8245
+ Current date: ${now}
8246
+
8247
+ <platform_docs>
8248
+ <platform>
8249
+ {{compiled/platform.md}}
8250
+ </platform>
8251
+
8252
+ <manifest>
8253
+ {{compiled/manifest.md}}
8254
+ </manifest>
8255
+
8256
+ <tables>
8257
+ {{compiled/tables.md}}
8258
+ </tables>
8259
+
8260
+ <methods>
8261
+ {{compiled/methods.md}}
8262
+ </methods>
8263
+
8264
+ <auth>
8265
+ {{compiled/auth.md}}
8266
+ </auth>
8267
+
8268
+ <dev_and_deploy>
8269
+ {{compiled/dev-and-deploy.md}}
8270
+ </dev_and_deploy>
8271
+
8272
+ <design>
8273
+ {{compiled/design.md}}
8274
+ </design>
8275
+
8276
+ <building_agent_interfaces>
8277
+ {{compiled/agent-interfaces.md}}
8278
+ </building_agent_interfaces>
8279
+
8280
+ <building_mcp_interfaces>
8281
+ {{compiled/mcp-interfaces.md}}
8282
+ </building_mcp_interfaces>
8283
+
8284
+ <media_cdn>
8285
+ {{compiled/media-cdn.md}}
8286
+ </media_cdn>
8287
+
8288
+ <interfaces>
8289
+ {{compiled/interfaces.md}}
8290
+ </interfaces>
8291
+
8292
+ <scenarios>
8293
+ {{compiled/scenarios.md}}
8294
+ </scenarios>
8295
+
8296
+ <secrets>
8297
+ {{compiled/secrets.md}}
8298
+ </secrets>
8299
+ </platform_docs>
8300
+
8301
+ <mindstudio_agent_sdk_docs>
8302
+ {{compiled/sdk-actions.md}}
8303
+
8304
+ {{compiled/task-agents.md}}
8305
+ </mindstudio_agent_sdk_docs>
8306
+
8307
+ <mindstudio_flavored_markdown_spec_docs>
8308
+ {{compiled/msfm.md}}
8309
+ </mindstudio_flavored_markdown_spec_docs>
8310
+
8311
+ <intake_mode_instructions>
8312
+ {{static/intake.md}}
8313
+ </intake_mode_instructions>
8314
+
8315
+ <spec_authoring_instructions>
8316
+ {{static/authoring.md}}
8317
+ </spec_authoring_instructions>
8318
+
8319
+ <team>
8320
+ {{static/team.md}}
8321
+ </team>
8322
+
8323
+ <code_authoring_instructions>
8324
+ {{static/coding.md}}
8325
+
8326
+ <typescript_lsp>
8327
+ {{static/lsp.md}}
8328
+ </typescript_lsp>
8329
+ </code_authoring_instructions>
8330
+
8331
+ <conversation_summaries>
8332
+ 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.
8333
+
8334
+ Old tool results are periodically cleared from the conversation to save context space. This is automatic and expected \u2014 you don't need to note down or preserve information from tool results. If you need to reference something from an earlier tool call, just re-read the file or re-run the query.
8335
+ </conversation_summaries>
8336
+
8337
+ <project_onboarding>
8338
+ New projects progress through three onboarding states. The user might skip this entirely and jump straight into working on the existing scaffold (which defaults to onboardingFinished), but ideally new projects move through each phase:
8339
+
8340
+ - **intake**: Gathering requirements. The project has scaffold code (a "hello world" starter) but it's not the user's app yet. Focus on understanding what they want to build, not on the existing code. Intake ends with a plan proposal via writePlan.
8341
+ - **building**: The user approved the initial plan. The agent is writing the spec and building the app. This can take a while and involves heavy tool use (spec authoring, design expert consultation, code generation, verification, polishing).
8342
+ - **buildComplete**: The build is done. The frontend is showing the user a reveal experience (pitch deck, app preview). The agent does not need to do anything in this state; the frontend advances to onboardingFinished when the user is ready.
8343
+ - **onboardingFinished**: The project is built and ready. Full development mode with all tools available. From here on, keep spec and code in sync as changes are made.
8344
+ </project_onboarding>
8345
+
8346
+ {{static/instructions.md}}
8347
+
8348
+ <!-- cache_breakpoint -->
8349
+
8350
+ <current_project_onboarding_state>
8351
+ ${onboardingState ?? "onboardingFinished"}
8352
+ </current_project_onboarding_state>
8353
+
8354
+ <project_context>
8355
+ ${projectContext}
8356
+ </project_context>
8357
+
8358
+ ${renderOrgContextBlock()}
8359
+
8360
+ <view_context>
8361
+ The user is currently in ${viewContext?.mode ?? "code"} mode.
8362
+ ${viewContext?.activeFile ? `Active file: ${viewContext.activeFile}` : ""}
8363
+ </view_context>
8364
+
8365
+ ${loadPlanStatus(onboardingState)}
8366
+ `;
8367
+ return resolveIncludes(template);
8368
+ }
8369
+ var init_prompt4 = __esm({
8370
+ "src/prompt/index.ts"() {
8371
+ "use strict";
8372
+ init_assets();
8373
+ init_projectContext();
8374
+ init_orgContext();
8325
8375
  }
8326
8376
  });
8327
8377
 
@@ -8721,9 +8771,8 @@ var init_headless = __esm({
8721
8771
  init_logger();
8722
8772
  init_config();
8723
8773
  init_orgContext();
8724
- init_prompt();
8774
+ init_prompt4();
8725
8775
  init_trigger();
8726
- init_compaction();
8727
8776
  init_trigger2();
8728
8777
  init_lsp();
8729
8778
  init_agent();
@@ -8835,7 +8884,9 @@ var init_headless = __esm({
8835
8884
  const data = event.error ? { error: event.error } : {};
8836
8885
  this.emit("compaction_complete", data, event.requestId);
8837
8886
  this.sessionStats.compactionInProgress = false;
8838
- this.sessionStats.lastContextSize = 0;
8887
+ if (!event.error) {
8888
+ this.sessionStats.lastContextSize = 0;
8889
+ }
8839
8890
  this.sessionStats.messageCount = this.state.messages.length;
8840
8891
  this.persistStats();
8841
8892
  }
@@ -8969,20 +9020,10 @@ var init_headless = __esm({
8969
9020
  requestId,
8970
9021
  model: this.opts.model
8971
9022
  });
8972
- this.applyPendingSummaries();
9023
+ applyPendingSummaries(this.state);
8973
9024
  } catch {
8974
9025
  }
8975
9026
  }
8976
- /** Drain pending compaction summaries and insert at a safe point. */
8977
- applyPendingSummaries() {
8978
- const summaries = getPendingSummaries();
8979
- if (summaries.length === 0) {
8980
- return;
8981
- }
8982
- const idx = findSafeInsertionPoint(this.state.messages);
8983
- this.state.messages.splice(idx, 0, ...summaries);
8984
- saveSession(this.state);
8985
- }
8986
9027
  onBackgroundComplete = (toolCallId, name, result, subAgentMessages) => {
8987
9028
  this.pendingBlockUpdates.push({ toolCallId, result, subAgentMessages });
8988
9029
  log15.info("Background complete", {
@@ -9311,7 +9352,7 @@ var init_headless = __esm({
9311
9352
  error: err.message
9312
9353
  });
9313
9354
  }
9314
- this.applyPendingSummaries();
9355
+ applyPendingSummaries(this.state);
9315
9356
  this.applyPendingBlockUpdates();
9316
9357
  }
9317
9358
  async handleMessage(parsed, requestId) {
@@ -9615,7 +9656,7 @@ var init_headless = __esm({
9615
9656
  model: this.opts.model
9616
9657
  });
9617
9658
  if (!this.running) {
9618
- this.applyPendingSummaries();
9659
+ applyPendingSummaries(this.state);
9619
9660
  }
9620
9661
  this.emit("completed", { success: true }, requestId);
9621
9662
  } catch (err) {
@@ -9795,7 +9836,8 @@ function MessageList({ turns }) {
9795
9836
 
9796
9837
  // src/tui/App.tsx
9797
9838
  init_agent();
9798
- init_prompt();
9839
+ init_trigger();
9840
+ init_prompt4();
9799
9841
  init_session();
9800
9842
  import { jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
9801
9843
  function App({ apiConfig, model }) {
@@ -9933,6 +9975,7 @@ Error: ${err.message}`,
9933
9975
  done: true
9934
9976
  }));
9935
9977
  }
9978
+ applyPendingSummaries(agentState);
9936
9979
  abortRef.current = null;
9937
9980
  setIsRunning(false);
9938
9981
  },