@mindstudio-ai/remy 0.1.278 → 0.1.280

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/headless.js CHANGED
@@ -813,6 +813,10 @@ function buildSystemPrompt(onboardingState) {
813
813
  {{compiled/auth.md}}
814
814
  </auth>
815
815
 
816
+ <jewels>
817
+ {{compiled/jewels.md}}
818
+ </jewels>
819
+
816
820
  <dev_and_deploy>
817
821
  {{compiled/dev-and-deploy.md}}
818
822
  </dev_and_deploy>
@@ -867,7 +871,7 @@ New projects progress through four onboarding states. The user might skip this e
867
871
 
868
872
  - **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.
869
873
  - **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).
870
- - **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.
874
+ - **buildComplete**: The build is done \u2014 entered via markBuildComplete at the end of the automated post-build flow. Transient: the frontend lands the user on the Build Overview, fully unlocks the editor, and immediately advances to onboardingFinished on its own. The agent does not need to do anything in this state.
871
875
  - **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.
872
876
  </project_onboarding>
873
877
 
@@ -1488,21 +1492,14 @@ var updatePlanStatusTool = {
1488
1492
  }
1489
1493
  };
1490
1494
 
1491
- // src/tools/common/setProjectOnboardingState.ts
1492
- var setProjectOnboardingStateTool = {
1495
+ // src/tools/common/markBuildComplete.ts
1496
+ var markBuildCompleteTool = {
1493
1497
  definition: {
1494
- name: "setProjectOnboardingState",
1495
- description: "Advance the project onboarding state. Forward-only: building \u2192 buildComplete \u2192 onboardingFinished. Normally driven by automated actions \u2014 don't call this out of order during a normal build, or you'll skip stages the user hasn't experienced. Exception: if the project has been in `building` for a while, the build is clearly done (the user is iterating on a working app, deploying, etc.), and the user reports the editor seems stuck \u2014 disabled Preview/Spec/Code tabs, no reveal, etc. \u2014 call `setProjectOnboardingState({ state: 'buildComplete' })` to unstick them. `onboardingFinished` is always set by the frontend after the user dismisses the reveal; never call it yourself.",
1498
+ name: "markBuildComplete",
1499
+ description: 'Mark the initial build as finished. The frontend lands the user on the Build Overview, fully unlocks the editor, and finishes onboarding; the platform sends the one-time "your first build is ready" email. Normally called as the final step of the automated post-build flow \u2014 don\'t call it early, or you\'ll cut the build experience short. Exception: if the project has been in `building` for a while, the build is clearly done (the user is iterating on a working app, deploying, etc.), and the user reports the editor seems stuck \u2014 disabled Preview/Spec/Code tabs \u2014 call this to unstick them. Refused during intake: the user starts the build via "Start Building", never you.',
1496
1500
  inputSchema: {
1497
1501
  type: "object",
1498
- properties: {
1499
- state: {
1500
- type: "string",
1501
- enum: ["building", "buildComplete", "onboardingFinished"],
1502
- description: "The onboarding state to advance to."
1503
- }
1504
- },
1505
- required: ["state"]
1502
+ properties: {}
1506
1503
  }
1507
1504
  },
1508
1505
  async execute() {
@@ -2981,6 +2978,35 @@ var runMethodTool = {
2981
2978
  }
2982
2979
  };
2983
2980
 
2981
+ // src/tools/code/testJewel.ts
2982
+ var testJewelTool = {
2983
+ definition: {
2984
+ name: "testJewel",
2985
+ description: "Run a method's jewel directly against a test input and return its pair record. Use after writing or modifying a jewel to verify the subject projection, the proposal, and the grading \u2014 the method itself is NOT executed (no data changes) and nothing reaches the pair ledger. Pass `humanInput` (the exact input a human would have submitted) for a full graded run: the jewel derives the subject from it, proposes, and grades its proposal against it as ground truth \u2014 the record carries a verdict (agree/disagree/skip). Or pass `subject` for an ungraded eval run (propose only). Returns the full pair record (subject, proposed, reasoning, actual, verdict, notes, error) plus captured console output and duration. A `pair.error` means your jewel code threw in that phase \u2014 fix and re-run. Seed realistic data first (runScenario) so the jewel's reads have a world to look at.",
2986
+ inputSchema: {
2987
+ type: "object",
2988
+ properties: {
2989
+ method: {
2990
+ type: "string",
2991
+ description: 'The shadowed method\'s export name (camelCase, e.g. "updateIssue"). Its manifest entry must have a `jewel` pointer.'
2992
+ },
2993
+ humanInput: {
2994
+ type: "object",
2995
+ description: "Shadow-style run: the exact method input a human would have submitted. The jewel derives its subject from this, proposes, and grades against it \u2014 the record gets a verdict. Use realistic inputs; this is how you verify agreement."
2996
+ },
2997
+ subject: {
2998
+ type: "object",
2999
+ description: "Eval run: a subject to propose against with no ground truth (record is ungraded). Use to probe the jewel on cases where you don't know the right answer yet. Pass exactly one of humanInput or subject."
3000
+ }
3001
+ },
3002
+ required: ["method"]
3003
+ }
3004
+ },
3005
+ async execute() {
3006
+ return "ok";
3007
+ }
3008
+ };
3009
+
2984
3010
  // src/tools/code/queryDatabase.ts
2985
3011
  var queryDatabaseTool = {
2986
3012
  definition: {
@@ -5524,6 +5550,147 @@ This reference lives at ${skill.path}. Re-read it with readFile if you need it a
5524
5550
  }
5525
5551
  }
5526
5552
 
5553
+ // src/subagents/designExpert/tools/createWireframe.ts
5554
+ var createWireframe_exports = {};
5555
+ __export(createWireframe_exports, {
5556
+ WIREFRAMES_DIR: () => WIREFRAMES_DIR,
5557
+ definition: () => definition10,
5558
+ execute: () => execute10
5559
+ });
5560
+ import { mkdir as mkdir2, writeFile as writeFile2 } from "fs/promises";
5561
+ import { join as join2 } from "path";
5562
+ var log9 = createLogger("createWireframe");
5563
+ var WIREFRAMES_DIR = "src/.wireframes";
5564
+ var UPLOAD_TIMEOUT_MS2 = 3e4;
5565
+ var definition10 = {
5566
+ name: "createWireframe",
5567
+ description: "Create a wireframe from self-contained HTML+CSS. Returns a markdown reference line \u2014 include that exact line in your response wherever the wireframe belongs, with your notes in the surrounding prose. To revise a wireframe, create a new one (read the old file first if you are iterating on it).",
5568
+ inputSchema: {
5569
+ type: "object",
5570
+ properties: {
5571
+ name: {
5572
+ type: "string",
5573
+ description: 'Short display name, e.g. "Feed Post Card". Becomes the caption and the filename slug.'
5574
+ },
5575
+ description: {
5576
+ type: "string",
5577
+ description: "One line on what the wireframe shows \u2014 layout, states, motion."
5578
+ },
5579
+ html: {
5580
+ type: "string",
5581
+ description: "The complete HTML document (<html>\u2026</html>), self-contained vanilla HTML/CSS/JS. No frontmatter \u2014 it is added for you."
5582
+ }
5583
+ },
5584
+ required: ["name", "description", "html"]
5585
+ }
5586
+ };
5587
+ function slugify(name) {
5588
+ return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 40).replace(/-+$/, "") || "wireframe";
5589
+ }
5590
+ function singleLine(value) {
5591
+ return value.replace(/\s+/g, " ").trim();
5592
+ }
5593
+ function validate(name, html) {
5594
+ const warnings = [];
5595
+ if (!name.trim()) {
5596
+ return { error: "Error: name is required.", warnings };
5597
+ }
5598
+ if (!html.trim()) {
5599
+ return { error: "Error: html is required.", warnings };
5600
+ }
5601
+ if (/^\s*---/.test(html)) {
5602
+ return {
5603
+ error: "Error: html must not include frontmatter \u2014 pass name and description as parameters and the frontmatter is added for you.",
5604
+ warnings
5605
+ };
5606
+ }
5607
+ if (/<img[\s>]/i.test(html)) {
5608
+ return {
5609
+ error: "Error: wireframes must not contain <img> tags. Use skeleton placeholders (grey boxes, gradients) instead of images.",
5610
+ warnings
5611
+ };
5612
+ }
5613
+ if (!/<style[\s>]/i.test(html) && !/style=/i.test(html)) {
5614
+ return {
5615
+ error: "Error: no CSS found. Wireframes are self-contained HTML+CSS \u2014 style the component container (not the body) so it reads against the transparent background.",
5616
+ warnings
5617
+ };
5618
+ }
5619
+ const lines = html.split("\n").length;
5620
+ if (lines > 120) {
5621
+ warnings.push(
5622
+ `The wireframe is ${lines} lines \u2014 past the ~100-line budget. Wireframes isolate one small piece; consider whether this is building too much.`
5623
+ );
5624
+ }
5625
+ return { warnings };
5626
+ }
5627
+ async function uploadMirror(context, slug, content) {
5628
+ const { apiConfig } = context;
5629
+ if (!apiConfig?.appId) {
5630
+ return { ok: false, note: "no app id in this session" };
5631
+ }
5632
+ try {
5633
+ const url = `${apiConfig.baseUrl}/_internal/v2/apps/${apiConfig.appId}/dev/wireframes`;
5634
+ const res = await fetch(url, {
5635
+ method: "POST",
5636
+ headers: {
5637
+ "Content-Type": "application/json",
5638
+ Authorization: `Bearer ${apiConfig.apiKey}`
5639
+ },
5640
+ body: JSON.stringify({ slug, content }),
5641
+ signal: AbortSignal.timeout(UPLOAD_TIMEOUT_MS2)
5642
+ });
5643
+ if (!res.ok) {
5644
+ return { ok: false, note: `HTTP ${res.status}` };
5645
+ }
5646
+ return { ok: true };
5647
+ } catch (err) {
5648
+ return { ok: false, note: err.message };
5649
+ }
5650
+ }
5651
+ async function execute10(input, onLog, context) {
5652
+ if (!context) {
5653
+ return "Error: createWireframe requires execution context";
5654
+ }
5655
+ const name = String(input.name ?? "");
5656
+ const description = String(input.description ?? "");
5657
+ const html = String(input.html ?? "");
5658
+ const { error, warnings } = validate(name, html);
5659
+ if (error) {
5660
+ return error;
5661
+ }
5662
+ const slug = `${slugify(name)}-${Math.random().toString(36).slice(2, 8)}`;
5663
+ const relPath = `${WIREFRAMES_DIR}/${slug}.html`;
5664
+ const content = [
5665
+ "---",
5666
+ `name: ${singleLine(name)}`,
5667
+ `description: ${singleLine(description)}`,
5668
+ "---",
5669
+ html.trim(),
5670
+ ""
5671
+ ].join("\n");
5672
+ await mkdir2(join2(PROJECT_ROOT, WIREFRAMES_DIR), { recursive: true });
5673
+ await writeFile2(join2(PROJECT_ROOT, relPath), content, "utf8");
5674
+ onLog?.(`Wrote ${relPath}, mirroring for preview...`);
5675
+ const mirror = await uploadMirror(context, slug, content);
5676
+ if (!mirror.ok) {
5677
+ log9.warn("Wireframe mirror upload failed", { slug, note: mirror.note });
5678
+ }
5679
+ const lines = [
5680
+ `Created wireframe "${singleLine(name)}" at ${relPath}.`,
5681
+ `Reference it in your response and in specs with exactly: ![${singleLine(name)}](${relPath})`
5682
+ ];
5683
+ if (!mirror.ok) {
5684
+ lines.push(
5685
+ `Note: the preview mirror upload failed (${mirror.note}) \u2014 the file is intact and usable by the developer, but the wireframe may not render visually in the dashboard. Do not retry; mention the layout in prose as well.`
5686
+ );
5687
+ }
5688
+ if (warnings.length > 0) {
5689
+ lines.push(...warnings.map((w) => `Note: ${w}`));
5690
+ }
5691
+ return lines.join("\n");
5692
+ }
5693
+
5527
5694
  // src/subagents/designExpert/tools/index.ts
5528
5695
  var tools = {
5529
5696
  searchGoogle: searchGoogle_exports,
@@ -5538,7 +5705,9 @@ var tools = {
5538
5705
  editImages: editImages_exports,
5539
5706
  renderImage: renderImage_exports,
5540
5707
  polishCopy: polishCopy_exports,
5541
- loadSkill: loadSkill_exports
5708
+ loadSkill: loadSkill_exports,
5709
+ // Appended last: tool order is part of the subagent's prompt-cache prefix.
5710
+ createWireframe: createWireframe_exports
5542
5711
  };
5543
5712
  var DESIGN_EXPERT_TOOLS = [
5544
5713
  ...COMMON_READ_TOOLS,
@@ -6516,7 +6685,7 @@ function deriveContext(parent, toolCallId) {
6516
6685
  }
6517
6686
  var ALL_TOOLS = [
6518
6687
  // Common
6519
- setProjectOnboardingStateTool,
6688
+ markBuildCompleteTool,
6520
6689
  promptUserTool,
6521
6690
  confirmDestructiveActionTool,
6522
6691
  askMindStudioSdkTool,
@@ -6559,7 +6728,8 @@ var ALL_TOOLS = [
6559
6728
  restartProcessTool,
6560
6729
  // Appended rather than grouped: position is part of the cache prefix, so a
6561
6730
  // new tool goes at the end to leave every existing session's prefix intact.
6562
- loadSkillTool
6731
+ loadSkillTool,
6732
+ testJewelTool
6563
6733
  ];
6564
6734
  var SUBAGENT_TOOL_NAMES = /* @__PURE__ */ new Set([
6565
6735
  "visualDesignExpert",
@@ -6585,7 +6755,7 @@ function executeTool(name, input, context) {
6585
6755
  }
6586
6756
 
6587
6757
  // src/compaction/index.ts
6588
- var log9 = createLogger("compaction");
6758
+ var log10 = createLogger("compaction");
6589
6759
  var CONVERSATION_SUMMARY_PROMPT = readAsset("compaction", "conversation.md");
6590
6760
  var SUBAGENT_SUMMARY_PROMPT = readAsset("compaction", "subagent.md");
6591
6761
  var SUMMARIZABLE_SUBAGENTS = ["visualDesignExpert", "productVision"];
@@ -6634,7 +6804,7 @@ async function compactConversation(messages, apiConfig, model) {
6634
6804
  if (text) {
6635
6805
  summaries.push({ name, text });
6636
6806
  } else {
6637
- log9.warn("Subagent summary unusable \u2014 leaving its history intact", {
6807
+ log10.warn("Subagent summary unusable \u2014 leaving its history intact", {
6638
6808
  name
6639
6809
  });
6640
6810
  }
@@ -6662,7 +6832,7 @@ async function compactConversation(messages, apiConfig, model) {
6662
6832
  }
6663
6833
  ]
6664
6834
  }));
6665
- log9.info("Compaction complete", {
6835
+ log10.info("Compaction complete", {
6666
6836
  summaries: summaries.length,
6667
6837
  recentNarrativeChars: recent.length
6668
6838
  });
@@ -6868,7 +7038,7 @@ async function generateSummary(apiConfig, name, compactionPrompt, messagesToSumm
6868
7038
  messagesToSummarize.slice(0, mid),
6869
7039
  messagesToSummarize.slice(mid)
6870
7040
  ];
6871
- log9.info("Chunking summary", {
7041
+ log10.info("Chunking summary", {
6872
7042
  name,
6873
7043
  messageCount: messagesToSummarize.length,
6874
7044
  serializedLength: serialized.length,
@@ -6898,7 +7068,7 @@ async function generateSummary(apiConfig, name, compactionPrompt, messagesToSumm
6898
7068
  const parts = results.filter((p) => p !== null);
6899
7069
  return parts.length > 0 ? parts.join("\n\n---\n\n") : null;
6900
7070
  }
6901
- log9.info("Generating summary", {
7071
+ log10.info("Generating summary", {
6902
7072
  name,
6903
7073
  messageCount: messagesToSummarize.length,
6904
7074
  serializedLength: serialized.length
@@ -6914,10 +7084,10 @@ async function generateSummary(apiConfig, name, compactionPrompt, messagesToSumm
6914
7084
  return null;
6915
7085
  }
6916
7086
  if (summaryText.length >= MIN_SUMMARY_CHARS) {
6917
- log9.info("Summary generated", { name, summaryLength: summaryText.length });
7087
+ log10.info("Summary generated", { name, summaryLength: summaryText.length });
6918
7088
  return summaryText;
6919
7089
  }
6920
- log9.warn("Summary too short to be real", {
7090
+ log10.warn("Summary too short to be real", {
6921
7091
  name,
6922
7092
  summaryLength: summaryText.length,
6923
7093
  minimum: MIN_SUMMARY_CHARS,
@@ -6977,12 +7147,12 @@ Write the summary of the conversation above, following your instructions.`;
6977
7147
  toolNames: []
6978
7148
  });
6979
7149
  } else if (event.type === "error") {
6980
- log9.error("Summary generation failed", { name, error: event.error });
7150
+ log10.error("Summary generation failed", { name, error: event.error });
6981
7151
  return null;
6982
7152
  }
6983
7153
  }
6984
7154
  if (!summaryText.trim()) {
6985
- log9.warn("Empty summary generated", { name });
7155
+ log10.warn("Empty summary generated", { name });
6986
7156
  return null;
6987
7157
  }
6988
7158
  return summaryText.trim();
@@ -6991,7 +7161,7 @@ Write the summary of the conversation above, following your instructions.`;
6991
7161
  // src/session.ts
6992
7162
  import fs21 from "fs";
6993
7163
  import path11 from "path";
6994
- var log10 = createLogger("session");
7164
+ var log11 = createLogger("session");
6995
7165
  var SESSION_FILE = ".remy-session.json";
6996
7166
  var ARCHIVE_DIR = ".logs/sessions";
6997
7167
  var ARCHIVE_NAME_RE = /^(cleared|rotated)-.*\.json$/;
@@ -7010,7 +7180,7 @@ function loadSession(state) {
7010
7180
  }
7011
7181
  if (Array.isArray(data.messages) && data.messages.length > 0) {
7012
7182
  state.messages = sanitizeMessages(data.messages);
7013
- log10.info("Session loaded", {
7183
+ log11.info("Session loaded", {
7014
7184
  messageCount: state.messages.length,
7015
7185
  ...state.models && { models: state.models }
7016
7186
  });
@@ -7020,7 +7190,7 @@ function loadSession(state) {
7020
7190
  try {
7021
7191
  const quarantine = `${SESSION_FILE}.corrupt-${Date.now()}`;
7022
7192
  fs21.renameSync(SESSION_FILE, quarantine);
7023
- log10.warn(`Session file unreadable \u2014 quarantined to ${quarantine}`);
7193
+ log11.warn(`Session file unreadable \u2014 quarantined to ${quarantine}`);
7024
7194
  } catch {
7025
7195
  }
7026
7196
  }
@@ -7085,7 +7255,7 @@ function archiveMessages(messages, label, models) {
7085
7255
  }
7086
7256
  writeFileAtomicSync(dest, JSON.stringify(payload));
7087
7257
  archiveCountCache.set(path11.basename(dest), count);
7088
- log10.info("Session archived", { label, dest, messageCount: count });
7258
+ log11.info("Session archived", { label, dest, messageCount: count });
7089
7259
  pruneArchives();
7090
7260
  return dest;
7091
7261
  }
@@ -7122,7 +7292,7 @@ function pruneArchives() {
7122
7292
  }
7123
7293
  }
7124
7294
  if (removed > 0) {
7125
- log10.info("Session archives pruned", {
7295
+ log11.info("Session archives pruned", {
7126
7296
  removed,
7127
7297
  freedBytes: freed,
7128
7298
  keptBytes: kept
@@ -7156,7 +7326,7 @@ function parseArchive(name) {
7156
7326
  }
7157
7327
  return messages;
7158
7328
  } catch (err) {
7159
- log10.warn("Session archive unreadable", { name, error: err?.message });
7329
+ log11.warn("Session archive unreadable", { name, error: err?.message });
7160
7330
  return null;
7161
7331
  }
7162
7332
  }
@@ -7271,7 +7441,7 @@ function getHistoryPage(state, opts) {
7271
7441
  }
7272
7442
  messages.splice(0, cut);
7273
7443
  startIndex += cut;
7274
- log10.info("History page trimmed to byte budget", {
7444
+ log11.info("History page trimmed to byte budget", {
7275
7445
  dropped: cut,
7276
7446
  kept: messages.length,
7277
7447
  startIndex
@@ -7302,7 +7472,7 @@ function rotate(state) {
7302
7472
  }
7303
7473
  archiveMessages(messages.slice(0, cut), "rotated", state.models);
7304
7474
  state.messages = messages.slice(cut);
7305
- log10.info("Session rotated", {
7475
+ log11.info("Session rotated", {
7306
7476
  archived: cut,
7307
7477
  retained: state.messages.length
7308
7478
  });
@@ -7315,9 +7485,9 @@ function saveSession(state) {
7315
7485
  serialized = JSON.stringify(buildPayload(state));
7316
7486
  }
7317
7487
  writeFileAtomicSync(SESSION_FILE, serialized);
7318
- log10.info("Session saved", { messageCount: state.messages.length });
7488
+ log11.info("Session saved", { messageCount: state.messages.length });
7319
7489
  } catch (err) {
7320
- log10.warn("Session save failed", { error: err.message });
7490
+ log11.warn("Session save failed", { error: err.message });
7321
7491
  }
7322
7492
  }
7323
7493
  function clearSession(state) {
@@ -7326,7 +7496,7 @@ function clearSession(state) {
7326
7496
  archiveMessages(state.messages, "cleared", state.models);
7327
7497
  }
7328
7498
  } catch (err) {
7329
- log10.warn("Session archive on clear failed", { error: err.message });
7499
+ log11.warn("Session archive on clear failed", { error: err.message });
7330
7500
  }
7331
7501
  state.messages = [];
7332
7502
  try {
@@ -7334,14 +7504,14 @@ function clearSession(state) {
7334
7504
  fs21.unlinkSync(SESSION_FILE);
7335
7505
  }
7336
7506
  } catch (err) {
7337
- log10.warn("Session clear: could not remove live file", {
7507
+ log11.warn("Session clear: could not remove live file", {
7338
7508
  error: err.message
7339
7509
  });
7340
7510
  }
7341
7511
  }
7342
7512
 
7343
7513
  // src/compaction/trigger.ts
7344
- var log11 = createLogger("compaction:trigger");
7514
+ var log12 = createLogger("compaction:trigger");
7345
7515
  var pending = null;
7346
7516
  var inflightCompaction = null;
7347
7517
  function getInflightCompaction() {
@@ -7380,7 +7550,7 @@ function applyPendingSummaries(state) {
7380
7550
  idx = at === -1 ? 0 : at + 1;
7381
7551
  }
7382
7552
  state.messages.splice(idx, 0, ...drained.checkpoints);
7383
- log11.info("Checkpoint applied", {
7553
+ log12.info("Checkpoint applied", {
7384
7554
  index: idx,
7385
7555
  messageCount: state.messages.length
7386
7556
  });
@@ -7414,7 +7584,7 @@ function triggerCompaction(state, apiConfig, opts = {}) {
7414
7584
  return inflightCompaction;
7415
7585
  }
7416
7586
  if (pending) {
7417
- log11.info("Compaction skipped \u2014 a checkpoint is already waiting to apply");
7587
+ log12.info("Compaction skipped \u2014 a checkpoint is already waiting to apply");
7418
7588
  return Promise.resolve(null);
7419
7589
  }
7420
7590
  const { blocking = false, requestId, model, origin, toolCallId } = opts;
@@ -7431,12 +7601,12 @@ function triggerCompaction(state, apiConfig, opts = {}) {
7431
7601
  requestId,
7432
7602
  ...summaries.length > 0 && { summaries }
7433
7603
  });
7434
- log11.info("Compaction complete");
7604
+ log12.info("Compaction complete");
7435
7605
  return summaries;
7436
7606
  }).catch((err) => {
7437
7607
  const message = err.message || "Compaction failed";
7438
7608
  listener?.({ type: "complete", error: message, requestId });
7439
- log11.error("Compaction failed", { error: message });
7609
+ log12.error("Compaction failed", { error: message });
7440
7610
  throw err;
7441
7611
  }).finally(() => {
7442
7612
  inflightCompaction = null;
@@ -7448,7 +7618,7 @@ function triggerCompaction(state, apiConfig, opts = {}) {
7448
7618
  import fs22 from "fs";
7449
7619
  import path12 from "path";
7450
7620
  import { createHash } from "crypto";
7451
- var log12 = createLogger("brandExtraction");
7621
+ var log13 = createLogger("brandExtraction");
7452
7622
  var EXTRACT_PROMPT = readAsset("brandExtraction", "extract.md");
7453
7623
  var BRAND_FILE = ".remy-brand.json";
7454
7624
  var CACHE_FILE = ".remy-brand.cache.json";
@@ -7456,17 +7626,17 @@ async function runExtraction(apiConfig, model) {
7456
7626
  const inputHash = computeInputHash();
7457
7627
  const cached3 = readCache();
7458
7628
  if (cached3 && cached3.inputHash === inputHash) {
7459
- log12.debug("Brand inputs unchanged \u2014 skipping extraction", { inputHash });
7629
+ log13.debug("Brand inputs unchanged \u2014 skipping extraction", { inputHash });
7460
7630
  return null;
7461
7631
  }
7462
- log12.info("Extracting brand", { inputHash });
7632
+ log13.info("Extracting brand", { inputHash });
7463
7633
  const brand = await extractBrand(apiConfig, model);
7464
7634
  if (!brand) {
7465
- log12.warn("Brand extraction failed \u2014 leaving cache untouched");
7635
+ log13.warn("Brand extraction failed \u2014 leaving cache untouched");
7466
7636
  return null;
7467
7637
  }
7468
7638
  persistBrand(brand, inputHash);
7469
- log12.info("Brand persisted", { inputHash });
7639
+ log13.info("Brand persisted", { inputHash });
7470
7640
  return brand;
7471
7641
  }
7472
7642
  function isDedicatedBrandFile(filePath) {
@@ -7552,7 +7722,7 @@ function parseFrontmatter3(filePath) {
7552
7722
  async function extractBrand(apiConfig, model) {
7553
7723
  const corpus = buildCorpus();
7554
7724
  if (!corpus.trim()) {
7555
- log12.debug("No spec corpus \u2014 emitting empty brand");
7725
+ log13.debug("No spec corpus \u2014 emitting empty brand");
7556
7726
  return { version: 1 };
7557
7727
  }
7558
7728
  let responseText = "";
@@ -7586,17 +7756,17 @@ async function extractBrand(apiConfig, model) {
7586
7756
  toolNames: []
7587
7757
  });
7588
7758
  } else if (event.type === "error") {
7589
- log12.error("Brand extraction stream error", { error: event.error });
7759
+ log13.error("Brand extraction stream error", { error: event.error });
7590
7760
  return null;
7591
7761
  }
7592
7762
  }
7593
7763
  } catch (err) {
7594
- log12.error("Brand extraction threw", { error: err?.message });
7764
+ log13.error("Brand extraction threw", { error: err?.message });
7595
7765
  return null;
7596
7766
  }
7597
7767
  const parsed = parseJsonResponse(responseText);
7598
7768
  if (!parsed) {
7599
- log12.warn("Brand extraction returned unparseable JSON", {
7769
+ log13.warn("Brand extraction returned unparseable JSON", {
7600
7770
  preview: responseText.slice(0, 200)
7601
7771
  });
7602
7772
  return null;
@@ -7767,7 +7937,7 @@ function readCache() {
7767
7937
  }
7768
7938
 
7769
7939
  // src/brandExtraction/trigger.ts
7770
- var log13 = createLogger("brandExtraction:trigger");
7940
+ var log14 = createLogger("brandExtraction:trigger");
7771
7941
  var inflight = false;
7772
7942
  var dirty = false;
7773
7943
  function triggerBrandExtraction(apiConfig, model) {
@@ -7777,7 +7947,7 @@ function triggerBrandExtraction(apiConfig, model) {
7777
7947
  }
7778
7948
  inflight = true;
7779
7949
  void runExtraction(apiConfig, model).catch((err) => {
7780
- log13.error("Brand extraction failed", { error: err?.message });
7950
+ log14.error("Brand extraction failed", { error: err?.message });
7781
7951
  }).finally(() => {
7782
7952
  inflight = false;
7783
7953
  if (dirty) {
@@ -8041,7 +8211,7 @@ function friendlyError(raw) {
8041
8211
  }
8042
8212
 
8043
8213
  // src/agent.ts
8044
- var log14 = createLogger("agent");
8214
+ var log15 = createLogger("agent");
8045
8215
  var BRAND_TRIGGERING_TOOLS = /* @__PURE__ */ new Set([
8046
8216
  "writeSpec",
8047
8217
  "editSpec",
@@ -8057,11 +8227,12 @@ function getToolCalls(blocks) {
8057
8227
  }
8058
8228
  var EXTERNAL_TOOLS = /* @__PURE__ */ new Set([
8059
8229
  "promptUser",
8060
- "setProjectOnboardingState",
8230
+ "markBuildComplete",
8061
8231
  "presentPublishPlan",
8062
8232
  "confirmDestructiveAction",
8063
8233
  "runScenario",
8064
8234
  "runMethod",
8235
+ "testJewel",
8065
8236
  "queryDatabase",
8066
8237
  "browserCommand",
8067
8238
  "setProjectMetadata"
@@ -8105,7 +8276,7 @@ async function runTurn(params) {
8105
8276
  (n, e) => n + (e.attachments?.length ?? 0),
8106
8277
  0
8107
8278
  );
8108
- log14.info("Turn started", {
8279
+ log15.info("Turn started", {
8109
8280
  requestId,
8110
8281
  model,
8111
8282
  buildModel: buildModelOverride,
@@ -8154,7 +8325,7 @@ async function runTurn(params) {
8154
8325
  }
8155
8326
  const isFirstMessage = state.messages.filter((m) => m.role === "user").length === 1;
8156
8327
  const STATUS_EXCLUDED_TOOLS = /* @__PURE__ */ new Set([
8157
- "setProjectOnboardingState",
8328
+ "markBuildComplete",
8158
8329
  "setProjectMetadata",
8159
8330
  "editsFinished"
8160
8331
  ]);
@@ -8400,7 +8571,7 @@ async function runTurn(params) {
8400
8571
  const acc = toolInputAccumulators.get(event.id);
8401
8572
  const wasStreamed = acc?.started ?? false;
8402
8573
  const isInputStreaming = !!tool?.streaming?.partialInput;
8403
- log14.info("Tool received", {
8574
+ log15.info("Tool received", {
8404
8575
  requestId,
8405
8576
  toolCallId: event.id,
8406
8577
  name: event.name
@@ -8519,7 +8690,7 @@ async function runTurn(params) {
8519
8690
  const toolCalls = getToolCalls(contentBlocks);
8520
8691
  if (toolCalls.length === 0 && (stopReason === "repetition" || stopReason === "max_tokens") && abnormalStopRecoveries < MAX_ABNORMAL_STOP_RECOVERIES && !signal?.aborted) {
8521
8692
  abnormalStopRecoveries++;
8522
- log14.warn("Abnormal stop \u2014 nudging model to continue", {
8693
+ log15.warn("Abnormal stop \u2014 nudging model to continue", {
8523
8694
  requestId,
8524
8695
  stopReason,
8525
8696
  attempt: abnormalStopRecoveries
@@ -8547,7 +8718,7 @@ async function runTurn(params) {
8547
8718
  });
8548
8719
  return;
8549
8720
  }
8550
- log14.info("Tools executing", {
8721
+ log15.info("Tools executing", {
8551
8722
  requestId,
8552
8723
  count: toolCalls.length,
8553
8724
  tools: toolCalls.map((tc) => tc.name)
@@ -8594,7 +8765,7 @@ async function runTurn(params) {
8594
8765
  let result;
8595
8766
  if (EXTERNAL_TOOLS.has(tc.name) && resolveExternalTool) {
8596
8767
  saveSession(state);
8597
- log14.info("Waiting for external tool result", {
8768
+ log15.info("Waiting for external tool result", {
8598
8769
  requestId,
8599
8770
  toolCallId: tc.id,
8600
8771
  name: tc.name
@@ -8662,7 +8833,7 @@ async function runTurn(params) {
8662
8833
  if (!isBackgroundCall(tc)) {
8663
8834
  toolRegistry?.unregister(tc.id);
8664
8835
  }
8665
- log14.info("Tool completed", {
8836
+ log15.info("Tool completed", {
8666
8837
  requestId,
8667
8838
  toolCallId: tc.id,
8668
8839
  name: tc.name,
@@ -8739,9 +8910,9 @@ async function runTurn(params) {
8739
8910
 
8740
8911
  // src/headless/attachments.ts
8741
8912
  import { mkdirSync, existsSync } from "fs";
8742
- import { writeFile as writeFile2 } from "fs/promises";
8743
- import { basename as basename2, join as join2, extname as extname2 } from "path";
8744
- var log15 = createLogger("headless:attachments");
8913
+ import { writeFile as writeFile3 } from "fs/promises";
8914
+ import { basename as basename2, join as join3, extname as extname2 } from "path";
8915
+ var log16 = createLogger("headless:attachments");
8745
8916
  var UPLOADS_DIR = "src/.user-uploads";
8746
8917
  function filenameFromUrl(url) {
8747
8918
  try {
@@ -8753,7 +8924,7 @@ function filenameFromUrl(url) {
8753
8924
  }
8754
8925
  }
8755
8926
  function resolveUniqueFilename(name, claimed) {
8756
- const isFree = (candidate) => !claimed.has(candidate) && !existsSync(join2(UPLOADS_DIR, candidate));
8927
+ const isFree = (candidate) => !claimed.has(candidate) && !existsSync(join3(UPLOADS_DIR, candidate));
8757
8928
  if (isFree(name)) {
8758
8929
  return name;
8759
8930
  }
@@ -8788,7 +8959,7 @@ async function persistAttachments(attachments) {
8788
8959
  const results = await Promise.allSettled(
8789
8960
  nonVoice.map(async (att, i) => {
8790
8961
  const name = names[i];
8791
- const localPath = join2(UPLOADS_DIR, name);
8962
+ const localPath = join3(UPLOADS_DIR, name);
8792
8963
  const res = await fetch(att.url, {
8793
8964
  signal: AbortSignal.timeout(3e4)
8794
8965
  });
@@ -8796,8 +8967,8 @@ async function persistAttachments(attachments) {
8796
8967
  throw new Error(`HTTP ${res.status} downloading ${att.url}`);
8797
8968
  }
8798
8969
  const buffer = Buffer.from(await res.arrayBuffer());
8799
- await writeFile2(localPath, buffer);
8800
- log15.info("Attachment saved", {
8970
+ await writeFile3(localPath, buffer);
8971
+ log16.info("Attachment saved", {
8801
8972
  filename: name,
8802
8973
  path: localPath,
8803
8974
  bytes: buffer.length
@@ -8810,8 +8981,8 @@ async function persistAttachments(attachments) {
8810
8981
  });
8811
8982
  if (textRes.ok) {
8812
8983
  extractedTextPath = `${localPath}.txt`;
8813
- await writeFile2(extractedTextPath, await textRes.text(), "utf-8");
8814
- log15.info("Extracted text saved", { path: extractedTextPath });
8984
+ await writeFile3(extractedTextPath, await textRes.text(), "utf-8");
8985
+ log16.info("Extracted text saved", { path: extractedTextPath });
8815
8986
  }
8816
8987
  } catch {
8817
8988
  }
@@ -9017,8 +9188,10 @@ var MessageQueue = class {
9017
9188
  };
9018
9189
 
9019
9190
  // src/headless/index.ts
9020
- var log16 = createLogger("headless");
9191
+ var log17 = createLogger("headless");
9021
9192
  var EXTERNAL_TOOL_TIMEOUT_MS = 3e5;
9193
+ var LONG_RUNNING_TOOLS = /* @__PURE__ */ new Set(["runMethod", "testJewel"]);
9194
+ var LONG_RUNNING_TOOL_TIMEOUT_MS = 18e5;
9022
9195
  var USER_FACING_TOOLS = /* @__PURE__ */ new Set([
9023
9196
  "promptUser",
9024
9197
  "confirmDestructiveAction",
@@ -9240,7 +9413,7 @@ var HeadlessSession = class {
9240
9413
  }
9241
9414
  const line = JSON.stringify(payload) + "\n";
9242
9415
  if (event === "history") {
9243
- log16.info("Wrote history event to stdout", {
9416
+ log17.info("Wrote history event to stdout", {
9244
9417
  requestId,
9245
9418
  bytes: line.length
9246
9419
  });
@@ -9326,7 +9499,7 @@ var HeadlessSession = class {
9326
9499
  if (this.sessionStats.lastContextSize <= FORCED_COMPACTION_THRESHOLD_TOKENS) {
9327
9500
  return;
9328
9501
  }
9329
- log16.info("Forced compaction gate triggered", {
9502
+ log17.info("Forced compaction gate triggered", {
9330
9503
  contextSize: this.sessionStats.lastContextSize,
9331
9504
  threshold: FORCED_COMPACTION_THRESHOLD_TOKENS,
9332
9505
  requestId
@@ -9345,7 +9518,7 @@ var HeadlessSession = class {
9345
9518
  onBackgroundComplete = (toolCallId, name, result, subAgentMessages) => {
9346
9519
  const notify = getToolByName(name)?.backgroundNotify ?? "wake";
9347
9520
  this.pendingBlockUpdates.push({ toolCallId, result, subAgentMessages });
9348
- log16.info("Background complete", {
9521
+ log17.info("Background complete", {
9349
9522
  toolCallId,
9350
9523
  name,
9351
9524
  notify,
@@ -9397,13 +9570,14 @@ var HeadlessSession = class {
9397
9570
  return Promise.resolve(early);
9398
9571
  }
9399
9572
  const shouldTimeout = !USER_FACING_TOOLS.has(name);
9573
+ const timeoutMs = LONG_RUNNING_TOOLS.has(name) ? LONG_RUNNING_TOOL_TIMEOUT_MS : EXTERNAL_TOOL_TIMEOUT_MS;
9400
9574
  return new Promise((resolve4) => {
9401
9575
  const timeout = shouldTimeout ? setTimeout(() => {
9402
9576
  this.pendingTools.delete(id);
9403
9577
  resolve4(
9404
- "Error: Tool timed out \u2014 no response from the app environment after 5 minutes."
9578
+ `Error: Tool timed out \u2014 no response from the app environment after ${Math.round(timeoutMs / 6e4)} minutes.`
9405
9579
  );
9406
- }, EXTERNAL_TOOL_TIMEOUT_MS) : void 0;
9580
+ }, timeoutMs) : void 0;
9407
9581
  this.pendingTools.set(id, {
9408
9582
  resolve: (result) => {
9409
9583
  clearTimeout(timeout);
@@ -9607,7 +9781,7 @@ var HeadlessSession = class {
9607
9781
  const { documents, images } = await persistAttachments(attachments);
9608
9782
  return buildUploadHeader(documents, images) || void 0;
9609
9783
  } catch (err) {
9610
- log16.warn("Attachment persistence failed", { error: err.message });
9784
+ log17.warn("Attachment persistence failed", { error: err.message });
9611
9785
  return void 0;
9612
9786
  }
9613
9787
  }
@@ -9621,7 +9795,7 @@ var HeadlessSession = class {
9621
9795
  async runSingleTurn(parsed, requestId, fromChain = false, queued = false) {
9622
9796
  const attachments = parsed.attachments;
9623
9797
  if (attachments?.length) {
9624
- log16.info("Message has attachments", {
9798
+ log17.info("Message has attachments", {
9625
9799
  count: attachments.length,
9626
9800
  urls: attachments.map((a) => a.url)
9627
9801
  });
@@ -9809,7 +9983,7 @@ var HeadlessSession = class {
9809
9983
  error: "Turn ended unexpectedly"
9810
9984
  });
9811
9985
  }
9812
- log16.info("Turn complete", {
9986
+ log17.info("Turn complete", {
9813
9987
  requestId,
9814
9988
  durationMs: Date.now() - this.turnStart
9815
9989
  });
@@ -9821,7 +9995,7 @@ var HeadlessSession = class {
9821
9995
  error: err.message
9822
9996
  });
9823
9997
  }
9824
- log16.warn("Command failed", {
9998
+ log17.warn("Command failed", {
9825
9999
  action: "message",
9826
10000
  requestId,
9827
10001
  error: err.message
@@ -10031,7 +10205,7 @@ var HeadlessSession = class {
10031
10205
  try {
10032
10206
  parsed = JSON.parse(line);
10033
10207
  } catch (err) {
10034
- log16.warn("Invalid JSON on stdin", {
10208
+ log17.warn("Invalid JSON on stdin", {
10035
10209
  error: err.message,
10036
10210
  lineLength: line.length,
10037
10211
  preview: line.slice(0, 200)
@@ -10040,7 +10214,7 @@ var HeadlessSession = class {
10040
10214
  return;
10041
10215
  }
10042
10216
  const { action, requestId } = parsed;
10043
- log16.info("Command received", { action, requestId });
10217
+ log17.info("Command received", { action, requestId });
10044
10218
  if (action === "tool_result" && parsed.id) {
10045
10219
  const id = parsed.id;
10046
10220
  const result = parsed.result ?? "";
@@ -10049,7 +10223,7 @@ var HeadlessSession = class {
10049
10223
  this.pendingTools.delete(id);
10050
10224
  pending2.resolve(result);
10051
10225
  } else if (!this.running) {
10052
- log16.info("Late tool_result while idle, dismissing", { id });
10226
+ log17.info("Late tool_result while idle, dismissing", { id });
10053
10227
  this.emit("completed", { success: true }, requestId);
10054
10228
  } else {
10055
10229
  this.earlyResults.set(id, result);
@@ -10062,7 +10236,7 @@ var HeadlessSession = class {
10062
10236
  ...typeof parsed.before === "number" ? { before: parsed.before } : {},
10063
10237
  ...typeof parsed.limit === "number" ? { limit: parsed.limit } : {}
10064
10238
  });
10065
- log16.info("History response", {
10239
+ log17.info("History response", {
10066
10240
  requestId,
10067
10241
  startIndex: page.startIndex,
10068
10242
  endIndex: page.endIndex,