@mindstudio-ai/remy 0.1.289 → 0.1.290

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
@@ -2496,7 +2496,7 @@ var bashTool = {
2496
2496
  };
2497
2497
 
2498
2498
  // src/tools/code/grep.ts
2499
- import { exec } from "child_process";
2499
+ import { execFile } from "child_process";
2500
2500
  var DEFAULT_MAX = 50;
2501
2501
  function clampContext(v) {
2502
2502
  const n = Math.floor(Number(v));
@@ -2570,57 +2570,87 @@ var grepTool = {
2570
2570
  1,
2571
2571
  Math.floor(Number(input.maxResults) || DEFAULT_MAX)
2572
2572
  );
2573
- const globFlag = input.glob ? ` --glob '${input.glob}'` : "";
2574
- const escaped = input.pattern.replace(/'/g, "'\\''");
2575
2573
  const mode = input.outputMode === "count" || input.outputMode === "filesWithMatches" ? input.outputMode : "content";
2576
- const ci = input.caseInsensitive ? " -i" : "";
2577
- let ctx = "";
2574
+ const ctx = [];
2578
2575
  if (mode === "content") {
2579
2576
  if (input.context != null) {
2580
2577
  const c = clampContext(input.context);
2581
2578
  if (c > 0) {
2582
- ctx = ` -C ${c}`;
2579
+ ctx.push("-C", String(c));
2583
2580
  }
2584
2581
  } else {
2585
2582
  const b = input.contextBefore != null ? clampContext(input.contextBefore) : 0;
2586
2583
  const a = input.contextAfter != null ? clampContext(input.contextAfter) : 0;
2587
2584
  if (b > 0) {
2588
- ctx += ` -B ${b}`;
2585
+ ctx.push("-B", String(b));
2589
2586
  }
2590
2587
  if (a > 0) {
2591
- ctx += ` -A ${a}`;
2588
+ ctx.push("-A", String(a));
2592
2589
  }
2593
2590
  }
2594
2591
  }
2592
+ const ci = input.caseInsensitive ? ["-i"] : [];
2595
2593
  let rgFlags;
2596
2594
  let grepFlags;
2597
2595
  if (mode === "count") {
2598
- rgFlags = `--count${ci}`;
2599
- grepFlags = `-rc${ci}`;
2596
+ rgFlags = ["--count", ...ci];
2597
+ grepFlags = ["-rc", ...ci];
2600
2598
  } else if (mode === "filesWithMatches") {
2601
- rgFlags = `-l${ci}`;
2602
- grepFlags = `-rl${ci}`;
2599
+ rgFlags = ["-l", ...ci];
2600
+ grepFlags = ["-rl", ...ci];
2603
2601
  } else {
2604
- rgFlags = `-n --no-heading${ci}${ctx} --max-count=${max}`;
2605
- grepFlags = `-rn${ci}${ctx} --max-count=${max}`;
2606
- }
2607
- const rgCmd = `rg ${rgFlags}${globFlag} '${escaped}' ${searchPath}`;
2608
- const grepCmd = `grep ${grepFlags} '${escaped}' ${searchPath} --include='*.ts' --include='*.tsx' --include='*.js' --include='*.json' --include='*.md'`;
2609
- return new Promise((resolve4) => {
2610
- exec(rgCmd, { maxBuffer: 512 * 1024 }, (err, stdout) => {
2611
- if (stdout?.trim()) {
2612
- resolve4(formatResults(stdout, max, mode));
2613
- return;
2602
+ rgFlags = ["-n", "--no-heading", ...ci, ...ctx, `--max-count=${max}`];
2603
+ grepFlags = ["-rn", ...ci, ...ctx, `--max-count=${max}`];
2604
+ }
2605
+ const rgArgs = [
2606
+ ...rgFlags,
2607
+ ...input.glob ? ["--glob", input.glob] : [],
2608
+ "--",
2609
+ input.pattern,
2610
+ searchPath
2611
+ ];
2612
+ const grepArgs = [
2613
+ ...grepFlags,
2614
+ "--exclude-dir=node_modules",
2615
+ "--exclude-dir=.git",
2616
+ "--include=*.ts",
2617
+ "--include=*.tsx",
2618
+ "--include=*.js",
2619
+ "--include=*.json",
2620
+ "--include=*.md",
2621
+ "--",
2622
+ input.pattern,
2623
+ searchPath
2624
+ ];
2625
+ const run = (cmd, cmdArgs) => new Promise((resolve4) => {
2626
+ const child = execFile(
2627
+ cmd,
2628
+ cmdArgs,
2629
+ { maxBuffer: 512 * 1024, timeout: 3e4 },
2630
+ (err, stdout) => {
2631
+ resolve4({
2632
+ stdout: stdout ?? "",
2633
+ timedOut: err?.killed === true
2634
+ });
2614
2635
  }
2615
- exec(grepCmd, { maxBuffer: 512 * 1024 }, (_err, grepStdout) => {
2616
- if (grepStdout?.trim()) {
2617
- resolve4(formatResults(grepStdout, max, mode));
2618
- } else {
2619
- resolve4("No matches found.");
2620
- }
2621
- });
2622
- });
2636
+ );
2637
+ child.stdin?.end();
2623
2638
  });
2639
+ const rg = await run("rg", rgArgs);
2640
+ if (rg.stdout.trim()) {
2641
+ return formatResults(rg.stdout, max, mode);
2642
+ }
2643
+ if (rg.timedOut) {
2644
+ return `Error: search timed out after 30s in ${searchPath} \u2014 narrow the path or pattern.`;
2645
+ }
2646
+ const grep = await run("grep", grepArgs);
2647
+ if (grep.stdout.trim()) {
2648
+ return formatResults(grep.stdout, max, mode);
2649
+ }
2650
+ if (grep.timedOut) {
2651
+ return `Error: search timed out after 30s in ${searchPath} \u2014 narrow the path or pattern.`;
2652
+ }
2653
+ return "No matches found.";
2624
2654
  }
2625
2655
  };
2626
2656
 
@@ -3176,6 +3206,13 @@ var SCREENSHOT_ANALYSIS_PROMPT = `Describe everything visible on screen from top
3176
3206
  var ANALYSIS_RESPONSE_FORMAT = `Respond only with your analysis as Markdown and absolutely no other text. Do not use emojis - use unicode if you need symbols.`;
3177
3207
  function buildScreenshotAnalysisPrompt(opts) {
3178
3208
  let p = opts?.prompt || SCREENSHOT_ANALYSIS_PROMPT;
3209
+ if (opts?.additionalQuestions) {
3210
+ p += `
3211
+
3212
+ After the analysis above, also answer the following specific questions about the screenshot:
3213
+
3214
+ ${opts.additionalQuestions}`;
3215
+ }
3179
3216
  if (opts?.styleMap) {
3180
3217
  p += `
3181
3218
 
@@ -4583,7 +4620,8 @@ async function runBrowserAutomation(task, context, opts) {
4583
4620
  step: {
4584
4621
  imageUrl: s.result.url,
4585
4622
  prompt: buildScreenshotAnalysisPrompt({
4586
- styleMap: s.result.styleMap
4623
+ styleMap: s.result.styleMap,
4624
+ additionalQuestions: opts?.analysisPrompt
4587
4625
  }),
4588
4626
  visionModelOverride: visionOverride
4589
4627
  }
@@ -4598,7 +4636,13 @@ async function runBrowserAutomation(task, context, opts) {
4598
4636
  if (i >= analyses.length) {
4599
4637
  return;
4600
4638
  }
4601
- step.result.analysis = analyses[i]?.output?.analysis || analyses[i]?.output || "";
4639
+ const analysis = analyses[i]?.output?.analysis || analyses[i]?.output || "";
4640
+ step.result.analysis = analysis;
4641
+ const kind = step.command === "screenshotFullPage" ? "fullPage" : "viewport";
4642
+ const harvested = lastCapture[kind];
4643
+ if (harvested && typeof analysis === "string" && analysis && harvested.url === step.result.url) {
4644
+ harvested.analysis = analysis;
4645
+ }
4602
4646
  });
4603
4647
  } catch {
4604
4648
  log8.debug("Failed to parse batch analysis result", {
@@ -4618,7 +4662,7 @@ async function runBrowserAutomation(task, context, opts) {
4618
4662
  const preferred = opts?.capture === "viewport" ? lastCapture.viewport ?? lastCapture.fullPage : lastCapture.fullPage ?? lastCapture.viewport;
4619
4663
  return {
4620
4664
  text: result.text,
4621
- ...preferred?.url ? { screenshot: { url: preferred.url, styleMap: preferred.styleMap } } : {}
4665
+ ...preferred?.url ? { screenshot: preferred } : {}
4622
4666
  };
4623
4667
  } finally {
4624
4668
  release();
@@ -4666,7 +4710,7 @@ var screenshotDefinition = {
4666
4710
  },
4667
4711
  prompt: {
4668
4712
  type: "string",
4669
- description: "Optional question about the screenshot. If omitted, returns a general description of what's visible."
4713
+ description: "Optional specific questions about the screenshot, answered alongside the general description of what's visible. If omitted, returns just the general description."
4670
4714
  },
4671
4715
  imageUrl: {
4672
4716
  type: "string",
@@ -4714,15 +4758,25 @@ async function executeScreenshot(input, onLog, context) {
4714
4758
  const shotKind = fullPage ? "full-page" : "viewport";
4715
4759
  const task = input.path ? `Navigate to "${input.path}", then: ${input.instructions}. After completing these steps, take a ${shotKind} screenshot.` : `${input.instructions}. After completing these steps, take a ${shotKind} screenshot.`;
4716
4760
  const result = await runBrowserAutomation(task, context, {
4717
- capture: fullPage ? "fullPage" : "viewport"
4761
+ capture: fullPage ? "fullPage" : "viewport",
4762
+ analysisPrompt: input.prompt
4718
4763
  });
4719
4764
  if (!result.screenshot) {
4720
4765
  return result.text;
4721
4766
  }
4767
+ const { url, styleMap, analysis } = result.screenshot;
4768
+ if (analysis) {
4769
+ onLog?.(JSON.stringify({ url, analysis }));
4770
+ return JSON.stringify({
4771
+ url,
4772
+ analysis,
4773
+ ...styleMap ? { styleMap } : {}
4774
+ });
4775
+ }
4722
4776
  return await streamScreenshotAnalysis({
4723
- image: result.screenshot.url,
4777
+ image: url,
4724
4778
  prompt: input.prompt,
4725
- styleMap: result.screenshot.styleMap,
4779
+ styleMap,
4726
4780
  onLog,
4727
4781
  model,
4728
4782
  apiConfig: context?.apiConfig
@@ -7542,6 +7596,7 @@ function clearSession(state) {
7542
7596
  log11.warn("Session archive on clear failed", { error: err.message });
7543
7597
  }
7544
7598
  state.messages = [];
7599
+ state.models = void 0;
7545
7600
  try {
7546
7601
  if (fs21.existsSync(SESSION_FILE)) {
7547
7602
  fs21.unlinkSync(SESSION_FILE);
@@ -10190,7 +10245,10 @@ var HeadlessSession = class {
10190
10245
  //////////////////////////////////////////////////////////////////////////////
10191
10246
  handleClear() {
10192
10247
  clearSession(this.state);
10193
- return {};
10248
+ return {
10249
+ modelSurfaces: getEffectiveModelSurfaces(),
10250
+ allowedModelsByType: ALLOWED_MODELS_BY_TYPE
10251
+ };
10194
10252
  }
10195
10253
  /** Change per-agent model picks without clearing history. Takes effect on
10196
10254
  * the next turn — the model is resolved live, per LLM call, from
package/dist/index.js CHANGED
@@ -2825,6 +2825,7 @@ function clearSession(state) {
2825
2825
  log3.warn("Session archive on clear failed", { error: err.message });
2826
2826
  }
2827
2827
  state.messages = [];
2828
+ state.models = void 0;
2828
2829
  try {
2829
2830
  if (fs10.existsSync(SESSION_FILE)) {
2830
2831
  fs10.unlinkSync(SESSION_FILE);
@@ -3593,7 +3594,7 @@ var init_bash = __esm({
3593
3594
  });
3594
3595
 
3595
3596
  // src/tools/code/grep.ts
3596
- import { exec } from "child_process";
3597
+ import { execFile } from "child_process";
3597
3598
  function clampContext(v) {
3598
3599
  const n = Math.floor(Number(v));
3599
3600
  return Number.isFinite(n) ? Math.min(100, Math.max(0, n)) : 0;
@@ -3671,57 +3672,87 @@ var init_grep = __esm({
3671
3672
  1,
3672
3673
  Math.floor(Number(input.maxResults) || DEFAULT_MAX)
3673
3674
  );
3674
- const globFlag = input.glob ? ` --glob '${input.glob}'` : "";
3675
- const escaped = input.pattern.replace(/'/g, "'\\''");
3676
3675
  const mode = input.outputMode === "count" || input.outputMode === "filesWithMatches" ? input.outputMode : "content";
3677
- const ci = input.caseInsensitive ? " -i" : "";
3678
- let ctx = "";
3676
+ const ctx = [];
3679
3677
  if (mode === "content") {
3680
3678
  if (input.context != null) {
3681
3679
  const c = clampContext(input.context);
3682
3680
  if (c > 0) {
3683
- ctx = ` -C ${c}`;
3681
+ ctx.push("-C", String(c));
3684
3682
  }
3685
3683
  } else {
3686
3684
  const b = input.contextBefore != null ? clampContext(input.contextBefore) : 0;
3687
3685
  const a = input.contextAfter != null ? clampContext(input.contextAfter) : 0;
3688
3686
  if (b > 0) {
3689
- ctx += ` -B ${b}`;
3687
+ ctx.push("-B", String(b));
3690
3688
  }
3691
3689
  if (a > 0) {
3692
- ctx += ` -A ${a}`;
3690
+ ctx.push("-A", String(a));
3693
3691
  }
3694
3692
  }
3695
3693
  }
3694
+ const ci = input.caseInsensitive ? ["-i"] : [];
3696
3695
  let rgFlags;
3697
3696
  let grepFlags;
3698
3697
  if (mode === "count") {
3699
- rgFlags = `--count${ci}`;
3700
- grepFlags = `-rc${ci}`;
3698
+ rgFlags = ["--count", ...ci];
3699
+ grepFlags = ["-rc", ...ci];
3701
3700
  } else if (mode === "filesWithMatches") {
3702
- rgFlags = `-l${ci}`;
3703
- grepFlags = `-rl${ci}`;
3701
+ rgFlags = ["-l", ...ci];
3702
+ grepFlags = ["-rl", ...ci];
3704
3703
  } else {
3705
- rgFlags = `-n --no-heading${ci}${ctx} --max-count=${max}`;
3706
- grepFlags = `-rn${ci}${ctx} --max-count=${max}`;
3707
- }
3708
- const rgCmd = `rg ${rgFlags}${globFlag} '${escaped}' ${searchPath}`;
3709
- const grepCmd = `grep ${grepFlags} '${escaped}' ${searchPath} --include='*.ts' --include='*.tsx' --include='*.js' --include='*.json' --include='*.md'`;
3710
- return new Promise((resolve4) => {
3711
- exec(rgCmd, { maxBuffer: 512 * 1024 }, (err, stdout) => {
3712
- if (stdout?.trim()) {
3713
- resolve4(formatResults(stdout, max, mode));
3714
- return;
3704
+ rgFlags = ["-n", "--no-heading", ...ci, ...ctx, `--max-count=${max}`];
3705
+ grepFlags = ["-rn", ...ci, ...ctx, `--max-count=${max}`];
3706
+ }
3707
+ const rgArgs = [
3708
+ ...rgFlags,
3709
+ ...input.glob ? ["--glob", input.glob] : [],
3710
+ "--",
3711
+ input.pattern,
3712
+ searchPath
3713
+ ];
3714
+ const grepArgs = [
3715
+ ...grepFlags,
3716
+ "--exclude-dir=node_modules",
3717
+ "--exclude-dir=.git",
3718
+ "--include=*.ts",
3719
+ "--include=*.tsx",
3720
+ "--include=*.js",
3721
+ "--include=*.json",
3722
+ "--include=*.md",
3723
+ "--",
3724
+ input.pattern,
3725
+ searchPath
3726
+ ];
3727
+ const run = (cmd, cmdArgs) => new Promise((resolve4) => {
3728
+ const child = execFile(
3729
+ cmd,
3730
+ cmdArgs,
3731
+ { maxBuffer: 512 * 1024, timeout: 3e4 },
3732
+ (err, stdout) => {
3733
+ resolve4({
3734
+ stdout: stdout ?? "",
3735
+ timedOut: err?.killed === true
3736
+ });
3715
3737
  }
3716
- exec(grepCmd, { maxBuffer: 512 * 1024 }, (_err, grepStdout) => {
3717
- if (grepStdout?.trim()) {
3718
- resolve4(formatResults(grepStdout, max, mode));
3719
- } else {
3720
- resolve4("No matches found.");
3721
- }
3722
- });
3723
- });
3738
+ );
3739
+ child.stdin?.end();
3724
3740
  });
3741
+ const rg = await run("rg", rgArgs);
3742
+ if (rg.stdout.trim()) {
3743
+ return formatResults(rg.stdout, max, mode);
3744
+ }
3745
+ if (rg.timedOut) {
3746
+ return `Error: search timed out after 30s in ${searchPath} \u2014 narrow the path or pattern.`;
3747
+ }
3748
+ const grep = await run("grep", grepArgs);
3749
+ if (grep.stdout.trim()) {
3750
+ return formatResults(grep.stdout, max, mode);
3751
+ }
3752
+ if (grep.timedOut) {
3753
+ return `Error: search timed out after 30s in ${searchPath} \u2014 narrow the path or pattern.`;
3754
+ }
3755
+ return "No matches found.";
3725
3756
  }
3726
3757
  };
3727
3758
  }
@@ -4360,6 +4391,13 @@ var init_analyzeImage = __esm({
4360
4391
  // src/tools/_helpers/screenshot.ts
4361
4392
  function buildScreenshotAnalysisPrompt(opts) {
4362
4393
  let p = opts?.prompt || SCREENSHOT_ANALYSIS_PROMPT;
4394
+ if (opts?.additionalQuestions) {
4395
+ p += `
4396
+
4397
+ After the analysis above, also answer the following specific questions about the screenshot:
4398
+
4399
+ ${opts.additionalQuestions}`;
4400
+ }
4363
4401
  if (opts?.styleMap) {
4364
4402
  p += `
4365
4403
 
@@ -5620,7 +5658,8 @@ async function runBrowserAutomation(task, context, opts) {
5620
5658
  step: {
5621
5659
  imageUrl: s.result.url,
5622
5660
  prompt: buildScreenshotAnalysisPrompt({
5623
- styleMap: s.result.styleMap
5661
+ styleMap: s.result.styleMap,
5662
+ additionalQuestions: opts?.analysisPrompt
5624
5663
  }),
5625
5664
  visionModelOverride: visionOverride
5626
5665
  }
@@ -5635,7 +5674,13 @@ async function runBrowserAutomation(task, context, opts) {
5635
5674
  if (i >= analyses.length) {
5636
5675
  return;
5637
5676
  }
5638
- step.result.analysis = analyses[i]?.output?.analysis || analyses[i]?.output || "";
5677
+ const analysis = analyses[i]?.output?.analysis || analyses[i]?.output || "";
5678
+ step.result.analysis = analysis;
5679
+ const kind = step.command === "screenshotFullPage" ? "fullPage" : "viewport";
5680
+ const harvested = lastCapture[kind];
5681
+ if (harvested && typeof analysis === "string" && analysis && harvested.url === step.result.url) {
5682
+ harvested.analysis = analysis;
5683
+ }
5639
5684
  });
5640
5685
  } catch {
5641
5686
  log9.debug("Failed to parse batch analysis result", {
@@ -5655,7 +5700,7 @@ async function runBrowserAutomation(task, context, opts) {
5655
5700
  const preferred = opts?.capture === "viewport" ? lastCapture.viewport ?? lastCapture.fullPage : lastCapture.fullPage ?? lastCapture.viewport;
5656
5701
  return {
5657
5702
  text: result.text,
5658
- ...preferred?.url ? { screenshot: { url: preferred.url, styleMap: preferred.styleMap } } : {}
5703
+ ...preferred?.url ? { screenshot: preferred } : {}
5659
5704
  };
5660
5705
  } finally {
5661
5706
  release();
@@ -5728,15 +5773,25 @@ async function executeScreenshot(input, onLog, context) {
5728
5773
  const shotKind = fullPage ? "full-page" : "viewport";
5729
5774
  const task = input.path ? `Navigate to "${input.path}", then: ${input.instructions}. After completing these steps, take a ${shotKind} screenshot.` : `${input.instructions}. After completing these steps, take a ${shotKind} screenshot.`;
5730
5775
  const result = await runBrowserAutomation(task, context, {
5731
- capture: fullPage ? "fullPage" : "viewport"
5776
+ capture: fullPage ? "fullPage" : "viewport",
5777
+ analysisPrompt: input.prompt
5732
5778
  });
5733
5779
  if (!result.screenshot) {
5734
5780
  return result.text;
5735
5781
  }
5782
+ const { url, styleMap, analysis } = result.screenshot;
5783
+ if (analysis) {
5784
+ onLog?.(JSON.stringify({ url, analysis }));
5785
+ return JSON.stringify({
5786
+ url,
5787
+ analysis,
5788
+ ...styleMap ? { styleMap } : {}
5789
+ });
5790
+ }
5736
5791
  return await streamScreenshotAnalysis({
5737
- image: result.screenshot.url,
5792
+ image: url,
5738
5793
  prompt: input.prompt,
5739
- styleMap: result.screenshot.styleMap,
5794
+ styleMap,
5740
5795
  onLog,
5741
5796
  model,
5742
5797
  apiConfig: context?.apiConfig
@@ -5782,7 +5837,7 @@ var init_screenshot2 = __esm({
5782
5837
  },
5783
5838
  prompt: {
5784
5839
  type: "string",
5785
- description: "Optional question about the screenshot. If omitted, returns a general description of what's visible."
5840
+ description: "Optional specific questions about the screenshot, answered alongside the general description of what's visible. If omitted, returns just the general description."
5786
5841
  },
5787
5842
  imageUrl: {
5788
5843
  type: "string",
@@ -11172,7 +11227,10 @@ var init_headless = __esm({
11172
11227
  //////////////////////////////////////////////////////////////////////////////
11173
11228
  handleClear() {
11174
11229
  clearSession(this.state);
11175
- return {};
11230
+ return {
11231
+ modelSurfaces: getEffectiveModelSurfaces(),
11232
+ allowedModelsByType: ALLOWED_MODELS_BY_TYPE
11233
+ };
11176
11234
  }
11177
11235
  /** Change per-agent model picks without clearing history. Takes effect on
11178
11236
  * the next turn — the model is resolved live, per LLM call, from
@@ -120,6 +120,8 @@ analytics.track('vendor_submitted', { vendorType: 'restaurant' });
120
120
  analytics.track('checkout_completed', { itemCount: 3, total: 47.99 });
121
121
  ```
122
122
 
123
+ - **Apps can also READ their own analytics from backend methods** — the agent SDK's `analytics` namespace (lifetime per-page metrics, live visitor count, traffic sources, event stats), so an admin view can show real traffic next to the app's own data. Consult `askMindStudioSdk` for the query API when building one.
124
+
123
125
  Analytics is **cookie-banner-free by design**: per-app scoping, IP discarded after geo lookup, country-level only, query strings server-scrubbed except for a UTM whitelist (`utm_*`, `ref`, `source`, `gclid`, `fbclid`, `msclkid`), no fingerprinting, no third-party scripts. If a user asks about GDPR cookie consent for analytics, you can explain why it is not needed.
124
126
 
125
127
  Disabling telemetry is a per-app dashboard setting (platform toggle, not code). Point users there if they ask.
@@ -1,3 +1,3 @@
1
1
  # Jewels
2
2
 
3
- A "jewel" is an optional AI companion for a single app method (`foo.jewel.ts` beside `foo.ts`): it proposes the method call a human would otherwise make, the method's manifest `autonomy` setting decides what happens to each proposal: recorded silently (`shadow`), queued for human review (`approve`), or committed (`auto`), and every proposal is then graded against what really happened. Jewels are never part of an initial build, and most apps will never need them. By default, build AI features as plain method code - if the user wants to add jewels later on, conversion is easy and additive. Before doing any work with or thinking around jewels, load the `jewels` skill.
3
+ A "jewel" is an optional AI companion for a single app method (`foo.jewel.ts` beside `foo.ts`): it proposes the method call a human would otherwise make, the method's manifest `autonomy` setting decides what happens to each proposal: recorded silently (`shadow`), queued for human review (`approve`), or committed (`auto`), and every proposal is then graded against what really happened. Jewels are never part of an initial build, and most apps will never need them. By default, build AI features as plain method code - if the user wants to add jewels later on, conversion is easy. Before doing any work with or thinking around jewels, load the `jewels` skill.
@@ -167,14 +167,55 @@ const results = await Vendors
167
167
  // Aggregates — all return Query objects (batchable)
168
168
  const count = await Vendors.count();
169
169
  const any = await Vendors.some(v => v.status === 'pending');
170
- const cheapest = await Vendors.min(v => v.totalCents);
171
- const grouped = await Vendors.groupBy(v => v.status);
170
+ const cheapest = await Vendors.min(v => v.totalCents); // full ROW with min value
171
+ const grouped = await Vendors.groupBy(v => v.status); // Map of FULL rows — fetches everything
172
172
 
173
173
  // These two return Promises directly (not batchable)
174
174
  const all = await Vendors.every(v => v.status !== 'rejected');
175
175
  const empty = await Vendors.isEmpty();
176
176
  ```
177
177
 
178
+ ### Aggregation
179
+
180
+ `count()`, `sum()`, `avg()`, `countDistinct()`, and `aggregate()` compile to SQL aggregates (`COUNT`/`TOTAL`/`AVG`/`GROUP BY`) — no rows are fetched, so they stay cheap at any table size. Use them for every summary over a table that can grow; never page a whole table into memory to compute totals.
181
+
182
+ ```typescript
183
+ // Scalar aggregates — accessors, like sortBy/min/max
184
+ const revenue = await Orders.filter(o => o.status === 'paid').sum(o => o.amountCents);
185
+ const avgScore = await Answers.avg(a => a.score); // number | null (null on empty set)
186
+ const respondents = await Answers.countDistinct(a => a.responseId);
187
+
188
+ // Grouped aggregation — STRING column names, one plain object per group
189
+ const top = await Answers
190
+ .filter((a, $) => a.surveyId === $.surveyId, { surveyId }) // bindings: lifts closure var so filter compiles to SQL
191
+ .aggregate({
192
+ by: ['questionId', 'dimension'],
193
+ select: {
194
+ n: { count: true },
195
+ total: { sum: 'score' },
196
+ avgScore: { avg: 'score' },
197
+ respondents: { countDistinct: 'responseId' },
198
+ },
199
+ orderBy: 'total', desc: true, limit: 20,
200
+ });
201
+ // Array<{ questionId, dimension, n: number, total: number, avgScore: number | null, respondents: number }>
202
+ ```
203
+
204
+ Select terms: `{ count: true }`, `{ sum: 'col' }`, `{ avg: 'col' }`, `{ min: 'col' }`, `{ max: 'col' }`, `{ countDistinct: 'col' }`. Omit `by` for several aggregates over the whole filtered set (returns a single object). Empty set: `count`/`countDistinct` → 0, `sum` → 0, `avg`/`min`/`max` → null. `groupBy()` fetches full rows — reach for it only when you need the rows themselves, not a summary.
205
+
206
+ ### Raw SQL Escape Hatch
207
+
208
+ `db.sql()` runs read-only raw SQL against the app's own database — for joins, subqueries, and window functions the typed API can't express. Last resort: prefer the typed API (including `aggregate()`) whenever it can express the query.
209
+
210
+ ```typescript
211
+ const rows = await db.sql<{ questionId: string; n: number }>(
212
+ 'SELECT questionId, COUNT(*) AS n FROM answers WHERE surveyId = ? GROUP BY questionId',
213
+ [surveyId],
214
+ );
215
+ ```
216
+
217
+ `SELECT`/`WITH` only — writes throw; use Table methods for writes. Positional `?` bind params. Lazy and batchable via `db.batch()`. Raw rows come back close to how SQLite stores them and may not exactly match the typed API's representations.
218
+
178
219
  ### Updating Records
179
220
 
180
221
  ```typescript
@@ -342,7 +383,7 @@ const [_, newOrder, pending] = await db.batch(
342
383
 
343
384
  **Always batch instead of sequential awaits.** A loop with `await Table.update()` inside makes N separate HTTP calls. Mapping to mutations and passing them to `db.batch()` makes one.
344
385
 
345
- **Note:** Almost all read methods return batchable `Query` objects — including `get()`, `findOne()`, `count()`, `some()`, `min()`, `max()`, and `groupBy()`. The exceptions are `every()` and `isEmpty()`, which return Promises directly and cannot be batched.
386
+ **Note:** Almost all read methods return batchable `Query` objects — including `get()`, `findOne()`, `count()`, `sum()`, `avg()`, `countDistinct()`, `aggregate()`, `some()`, `min()`, `max()`, and `groupBy()` — and `db.sql()` results batch too. The exceptions are `every()` and `isEmpty()`, which return Promises directly and cannot be batched.
346
387
 
347
388
  ## Migrations
348
389
 
@@ -30,7 +30,7 @@ One jewel per judgment. If a jewel would need to take two actions, the methods a
30
30
 
31
31
  Never include a jewel in an initial build, and don't bring them up while an app is young. Most users will never want one; a few will care a lot. The default posture:
32
32
 
33
- - **Build AI features plainly.** A drafting feature is a method that calls a model and returns text, with no jewel machinery anywhere. Converting it to a jewel later is additive (a `.jewel.ts` beside the method, an `autonomy` line in the manifest); nothing about the plain version is thrown away.
33
+ - **Build AI features plainly.** A drafting feature is a method that calls a model and returns text, with no jewel machinery anywhere. Converting it later is cheap. At shadow it's purely additive (a `.jewel.ts` beside the method, an `autonomy` line in the manifest). At approve, the jewel takes over as the sole producer: it proposes what the plain feature used to write, surfaced in the same place the human already saw it, and the plain generation is retired — one generator, always. A jewel graded against human edits of some other AI's draft is measuring model against model.
34
34
  - **Jewels enter when the user asks** about automating a judgment the app already handles, or picks an automation item off the roadmap. Once an app is solid and a judgment verb sees real use, a benefit-phrased automation item may belong on the roadmap; that is the only proactive channel.
35
35
  - **When the user engages, have the conversation first**: what it does in their terms, what the levels mean, what evidence looks like. Then start with exactly one verb.
36
36
 
@@ -77,7 +77,7 @@ interface JewelPairRecord {
77
77
 
78
78
  The executor never throws: a shadow run must never break anything. Your code failing inside `subject` or `propose` becomes the record's `error`; `grade` failing softens to verdict `skip`.
79
79
 
80
- Shadowing runs only on the deployed app. Dev traffic never fires jewels; dev invocations are synthetic and would pollute the pair ledger.
80
+ Shadowing runs only on the deployed app dev invocations are synthetic and would pollute the pair ledger. The arrival flow is different: `jewels.propose` on approve/auto methods runs for real in dev sessions (the jewel executes from local source, queue items are scoped to the dev session, auto commits apply against the dev database) so the app's hot path is testable end-to-end, but none of it is recorded — no pairs, no grading, no training data.
81
81
 
82
82
  ## Usage
83
83
 
@@ -203,7 +203,7 @@ grade: async ({ proposed, actual }) => {
203
203
 
204
204
  Four levels: `manual` (no jewel ever; a policy statement), `shadow` (runs silently on every human invocation, pairs recorded, nothing visible), `approve` (jewel drafts, a human accepts, edits, or rejects; the edit is the richest training signal there is), `auto` (the jewel acts under its own identity).
205
205
 
206
- **Auto is the only level you earn with evidence. Shadow and approve are both safe starting points; pick by what the product is.** When the proposals themselves are the product (drafts for review), start at approve; it's often the permanent home. When the evidence is the product (an auto-bound verb), start at shadow. Raising to auto is a reviewed manifest diff justified by agreement numbers.
206
+ **Auto is the only level you earn with evidence. Shadow and approve are both safe starting points; pick by what the product is.** When the proposals themselves are the product (drafts for review), start at approve; it's often the permanent home. When the evidence is the product (an auto-bound verb), start at shadow. The levels differ in what exists, not in presentation: at approve the human sees and edits the proposal before it takes effect; at shadow nothing ever surfaces. Raising to auto is a reviewed manifest diff justified by agreement numbers.
207
207
 
208
208
  **Choose approve by the verb's risk shape, not by default.** Reversible state-machine verbs (routing, classification; a mistake is a two-click correction) go shadow → auto with a `sampleRate` canary and skip approve, because a review queue on a high-agreement classifier adds work without adding safety. Approve belongs on irreversible or outward-facing verbs (send, publish, charge, refund): `sampleRate` is population-level risk control, and those verbs need per-instance gating, which is what approve is.
209
209
 
@@ -229,7 +229,7 @@ from the platform's small menu; `qwen3.5-4b` is the default, with `qwen3.5-9b` a
229
229
  `gpt-oss-20b` as larger alternatives, and the default is the right choice unless a
230
230
  report shows it falling short), `windowDays` (how many days of ledger history to train
231
231
  on; default all history), `epochs` (1-10, default 3), `rank` (LoRA rank, 4-64, default
232
- 16), `learningRate` (default 5e-5). It requires `jewel`, and it rides the release:
232
+ 16), `learningRate` (default 1e-4). It requires `jewel`, and it rides the release:
233
233
  changing a knob is a commit + deploy before the next training run.
234
234
 
235
235
  Once a method has accumulated graded pairs, train from the prod CLI:
@@ -238,14 +238,20 @@ a run id and the dataset report; a run takes minutes to tens of minutes, so neve
238
238
  `--wait` (that flag is for humans at a terminal — it would block your whole loop).
239
239
  Start the run, tell the user it's training, keep working on other things, and check in
240
240
  with `mindstudio-prod jewels run <runId>` between tasks — the `progress` field shows
241
- the live phase and training percent, and `status` goes `complete` or `failed` with the
241
+ the live phase and training percent, the run's `log` narrates the whole story as
242
+ timestamped status events (queued, GPU acquired, training, grading — loss points are
243
+ compacted to a count in CLI output), and `status` goes `complete` or `failed` with the
242
244
  full report. The dataset report says whether the ledger is trainable (pairs without an
243
245
  attached `trace` don't count), and a run produces a downloadable LoRA adapter plus a
244
246
  held-out agreement report: how often the trained model matched your team's decisions
245
247
  on pairs it never saw. The adapter and report land in the app's own file store
246
248
  (`models/` in the Files dashboard), so the user can download them there. The
247
- agreement number is `report.grading.agreement` scored by the jewel's own grade
248
- function, the same grader as the pairs dashboard; there is no other agreement field.
249
+ agreement number is `report.selected.agreement` when present, else
250
+ `report.grading.agreement` scored by the jewel's own grade function, the same grader
251
+ as the pairs dashboard. A run trains multiple checkpoint candidates (per-epoch
252
+ snapshots, plus a DPO pass over the team's corrections when enough have accumulated)
253
+ and the platform promotes whichever scores highest on that grader — `report.selected`
254
+ names the serving checkpoint; this is automatic, never something you configure.
249
255
  A completed run without a `grading` block just hasn't been graded yet
250
256
  (`mindstudio-prod jewels grade <runId>` fills it). A completed run registers the
251
257
  app's tuned model as a real model id — `tuned/{appId}/{methodId}`, one stable id per
@@ -253,9 +259,19 @@ method that retraining advances in place — and the latest complete run per met
253
259
  automatically served on the platform's GPU pool, so that id works like any other
254
260
  model the moment training finishes. There is no special invoke tool: to sanity-check
255
261
  or demo a tuned model, make an ordinary generate-text call with that model id (the
256
- pool serves it with the exact template posture it was trained under). Promotion (the
257
- app actually switching a jewel onto its tuned model) is still a deliberate later
258
- step; do not wire a tuned model into app code unprompted.
262
+ pool serves it with the exact template posture it was trained under). **Promotion**
263
+ (switching a jewel onto its own tuned model) is a one-line change: swap the `model`
264
+ id inside the jewel's existing `runTask` call to the tuned line the git commit is
265
+ the promotion record, and reverting it is the rollback. Tool-using jewels promote
266
+ too: the tuned model was trained on the jewel's full transcripts, tool calls
267
+ included, and the training report's `toolEval` block shows how faithfully it makes
268
+ the teacher's tool decisions (qwen bases only — gpt-oss can't drive tools yet, and
269
+ the platform rejects that combination with a clear error). One hard rule: never
270
+ restructure the propose path to a generate-text call to do it — that silently loses
271
+ `outputSchema` validation AND the task `traceId`, so the jewel's new pairs stop
272
+ being trainable and the learning loop dies. `runTask` with the tuned id keeps both.
273
+ Promotion is still a deliberate step; do not wire a tuned model into app code
274
+ unprompted.
259
275
 
260
276
  ## Arrival Triggers (`mindstudio.jewels.propose`)
261
277
 
@@ -288,7 +304,7 @@ Rules that matter:
288
304
 
289
305
  ## Native Approval Flows (`jewels.queue`)
290
306
 
291
- For `approve`-mode methods, the review inbox belongs in the app, next to the work. Three pieces:
307
+ For `approve`-mode methods, the queue is data, not a screen: proposals surface wherever the human already makes this decision. When the app has an editor for the decision, the pending proposal pre-fills it and the human's normal confirm action resolves it; a dedicated inbox is only for decisions with no existing surface. Three pieces:
292
308
 
293
309
  ```typescript
294
310
  // 1. Backend list method, gated with the APP'S reviewer role.
@@ -297,7 +313,7 @@ export async function listPendingDrafts() {
297
313
  return mindstudio.jewels.queue.list({ methodId: 'send-message' });
298
314
  }
299
315
 
300
- // 2. Frontend inbox UI renders items: subject, proposed input, reasoning.
316
+ // 2. Frontend surfaces items where the decision already lives (pre-fill the existing editor); render subject, proposed input, reasoning.
301
317
 
302
318
  // 3. Backend resolve method: approve applies, dismiss records.
303
319
  export async function reviewDraft(input: {
@@ -82,7 +82,7 @@ You have access to the `mindstudio` CLI, which exposes every SDK action as a com
82
82
  ### Production App Management
83
83
  You have access to `mindstudio-prod`, a CLI for managing the user's production app. Use it via your bash tool. All output is JSON. Run `mindstudio-prod --help` or `mindstudio-prod <command> --help` to discover usage and available options.
84
84
 
85
- Available commands: `requests` (server logs, errors, latency), `crashes` (frontend browser errors), `analytics` (traffic, referrers, live counters), `releases`, `diagnostics` (Lighthouse audit), `domains`, `users` (list, set roles), `db` (query production sql), `data` (live db operations like lift-from-dev), `methods` (list, invoke), `secrets`, `files` (CDN files), `datasources` (document corpora), `prerender` (crawler snapshots), `voice` (phone numbers, call logs, voice policy), `issues` (externally-reported bugs).
85
+ Available commands: `requests` (server logs, errors, latency), `crashes` (frontend browser errors), `analytics` (traffic queries — lifetime metrics, sources, live counters), `releases`, `diagnostics` (Lighthouse audit), `domains`, `users` (list, set roles), `db` (query production sql), `data` (live db operations like lift-from-dev), `methods` (list, invoke), `secrets`, `files` (CDN files), `datasources` (document corpora), `prerender` (crawler snapshots), `voice` (phone numbers, call logs, voice policy), `issues` (externally-reported bugs).
86
86
 
87
87
  Two rules: buying a `voice` phone number bills $1/month — never buy without the user's explicit confirmation. `issues` is for externally-reported bugs only — read from it and resolve items when the user asks; never use it to track work you are doing with the user.
88
88
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mindstudio-ai/remy",
3
- "version": "0.1.289",
3
+ "version": "0.1.290",
4
4
  "description": "Remy coding agent",
5
5
  "repository": {
6
6
  "type": "git",