@mindstudio-ai/remy 0.1.331 → 0.1.333

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
@@ -172,6 +172,10 @@ var init_loopGuard = __esm({
172
172
  });
173
173
 
174
174
  // src/api.ts
175
+ function sandboxSessionHeader() {
176
+ const sessionId = process.env.MINDSTUDIO_SESSION_ID;
177
+ return sessionId ? { "x-sandbox-session": sessionId } : {};
178
+ }
175
179
  async function* streamChat(params) {
176
180
  const { baseUrl: baseUrl2, apiKey, signal, requestId, model, ...rest } = params;
177
181
  const url = `${baseUrl2}/_internal/v2/agent/remy/chat`;
@@ -191,7 +195,8 @@ async function* streamChat(params) {
191
195
  method: "POST",
192
196
  headers: {
193
197
  "Content-Type": "application/json",
194
- Authorization: `Bearer ${apiKey}`
198
+ Authorization: `Bearer ${apiKey}`,
199
+ ...sandboxSessionHeader()
195
200
  },
196
201
  body: JSON.stringify(requestBody),
197
202
  signal
@@ -446,7 +451,8 @@ async function generateBackgroundAck(params) {
446
451
  method: "POST",
447
452
  headers: {
448
453
  "Content-Type": "application/json",
449
- Authorization: `Bearer ${params.apiConfig.apiKey}`
454
+ Authorization: `Bearer ${params.apiConfig.apiKey}`,
455
+ ...sandboxSessionHeader()
450
456
  },
451
457
  body: JSON.stringify({
452
458
  appId: params.apiConfig.appId,
@@ -1647,6 +1653,33 @@ ${xml}
1647
1653
  </background_results>`;
1648
1654
  return automatedMessage("background_results", body);
1649
1655
  }
1656
+ function buildWorkspaceStatusMessage(status) {
1657
+ const lines = [
1658
+ `Default branch tip: ${status.upstream}`,
1659
+ `Checked at: ${(/* @__PURE__ */ new Date()).toISOString()}`,
1660
+ `Behind by: ${status.behind === null ? "unknown (could not count \u2014 treat the list below as what is missing)" : `${status.behind} commit(s)`}`,
1661
+ `Unpushed commits here: ${status.ahead === null ? "unknown" : status.ahead}`,
1662
+ `Uncommitted changes here: ${status.dirty === "unknown" ? "could not tell \u2014 check with `git status` before relying on it" : status.dirty ? "yes" : "no"}`
1663
+ ];
1664
+ if (status.incoming.length > 0) {
1665
+ lines.push(
1666
+ "",
1667
+ status.incomingTruncated ? `Landed on the default branch since, newest first (first ${status.incoming.length}; there are more):` : "Landed on the default branch since, newest first:",
1668
+ ...status.incoming.map((line) => `- ${escapeForEnvelope(line)}`)
1669
+ );
1670
+ }
1671
+ const body = `<workspace_status>
1672
+ Automated note about this workspace, checked when Remy started \u2014 the timestamp below says when, and it may have been a while. This block is not from the user; anything outside it is. Nobody has seen it, and nobody can see it; it is context for you.
1673
+
1674
+ This copy of the app does not have everything on the branch production builds from.
1675
+
1676
+ ${lines.join("\n")}
1677
+ </workspace_status>`;
1678
+ return automatedMessage("workspace_status", body);
1679
+ }
1680
+ function escapeForEnvelope(text) {
1681
+ return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/@@automated/g, "@ @automated");
1682
+ }
1650
1683
  function mergeBackgroundResultsMessages(messages) {
1651
1684
  const results = [];
1652
1685
  const toolRe = /<tool_result id="([^"]+)" name="([^"]+)">\n([\s\S]*?)\n<\/tool_result>/g;
@@ -1930,6 +1963,9 @@ function serializeForSummary(messages) {
1930
1963
  }
1931
1964
  continue;
1932
1965
  }
1966
+ if (msg.role === "user" && (msg.hidden || typeof msg.content === "string" && isAutomatedMessage(msg.content))) {
1967
+ continue;
1968
+ }
1933
1969
  if (typeof msg.content === "string") {
1934
1970
  if (msg.content.trim()) {
1935
1971
  lines.push(`[${msg.role}]: ${msg.content}`);
@@ -4989,7 +5025,11 @@ function startStatusWatcher(config) {
4989
5025
  method: "POST",
4990
5026
  headers: {
4991
5027
  "Content-Type": "application/json",
4992
- Authorization: `Bearer ${apiConfig.apiKey}`
5028
+ Authorization: `Bearer ${apiConfig.apiKey}`,
5029
+ // Also a liveness signal for this box, and the most frequent one there is — this ticks
5030
+ // every few seconds for as long as the agent is working, which is exactly the window in
5031
+ // which the user may have backgrounded the tab and stopped its own keepalive.
5032
+ ...sandboxSessionHeader()
4993
5033
  },
4994
5034
  body: JSON.stringify({ appId: apiConfig.appId, context }),
4995
5035
  signal
@@ -5041,11 +5081,13 @@ var INTERNAL_PAYLOAD_MARKERS;
5041
5081
  var init_statusWatcher = __esm({
5042
5082
  "src/statusWatcher.ts"() {
5043
5083
  "use strict";
5084
+ init_api();
5044
5085
  INTERNAL_PAYLOAD_MARKERS = [
5045
5086
  "[USER CANCELLED]",
5046
5087
  "[INTERRUPTED]",
5047
5088
  "[INTERRUPTED - PARTIAL OUTPUT RETRIEVED]",
5048
5089
  "<background_results>",
5090
+ "<workspace_status>",
5049
5091
  "<tool_result"
5050
5092
  ];
5051
5093
  }
@@ -5895,7 +5937,7 @@ TypeScript running in a sandboxed environment. Any npm package can be installed.
5895
5937
  - Managed SQLite database with typed schemas and automatic migrations. Define a TypeScript interface, push, and the platform handles diffing and migrating.
5896
5938
  - Built-in app-managed auth. Opt-in via manifest \u2014 developer builds login UI, platform handles verification codes (email-code, sms-code) and cookie sessions. API key auth for programmatic access. No OAuth, no social login (no Apple, Google, Facebook, or GitHub sign-in). Backend methods use auth.requireRole() for access control.
5897
5939
  - Encrypted secrets with separate dev/prod values, injected as process.env. For third-party service credentials not covered by the SDK.
5898
- - Git-native deployment. Push to default branch to deploy.
5940
+ - Git-native deployment. Each person with edit access works in their own copy of the app, and everyone publishes to the same default branch, which is what deploys. Pushing any other branch builds a private preview instead. Rollback is a git revert.
5899
5941
 
5900
5942
  ## MindStudio SDK
5901
5943
 
@@ -9436,7 +9478,10 @@ var init_resolve = __esm({
9436
9478
  "use strict";
9437
9479
  init_assets();
9438
9480
  init_sentinel();
9439
- NON_ACTION_SENTINELS = /* @__PURE__ */ new Set(["background_results"]);
9481
+ NON_ACTION_SENTINELS = /* @__PURE__ */ new Set([
9482
+ "background_results",
9483
+ "workspace_status"
9484
+ ]);
9440
9485
  }
9441
9486
  });
9442
9487
 
@@ -10112,7 +10157,9 @@ async function runTurn(params) {
10112
10157
  for (const entry of keptEntries) {
10113
10158
  appendEntry(entry);
10114
10159
  }
10115
- const isFirstMessage = state.messages.filter((m) => m.role === "user").length === 1;
10160
+ const isFirstMessage = state.messages.filter(
10161
+ (m) => m.role === "user" && !m.hidden && !(typeof m.content === "string" && isAutomatedMessage(m.content))
10162
+ ).length === 1;
10116
10163
  const STATUS_EXCLUDED_TOOLS = /* @__PURE__ */ new Set([
10117
10164
  "markBuildComplete",
10118
10165
  "setProjectMetadata",
@@ -11311,7 +11358,24 @@ function loadPassiveResults() {
11311
11358
  }
11312
11359
  return [];
11313
11360
  }
11314
- function writeStats(stats, queue, passiveResults, suggestCompactAt) {
11361
+ function emptyWorkspaceNotice() {
11362
+ return { pendingNote: null, lastNotedUpstream: null };
11363
+ }
11364
+ function loadWorkspaceNotice() {
11365
+ try {
11366
+ const stats = JSON.parse(readFileSync2(STATS_FILE, "utf-8"));
11367
+ const notice = stats.workspaceNotice;
11368
+ if (notice && typeof notice === "object") {
11369
+ return {
11370
+ pendingNote: typeof notice.pendingNote === "string" ? notice.pendingNote : null,
11371
+ lastNotedUpstream: typeof notice.lastNotedUpstream === "string" ? notice.lastNotedUpstream : null
11372
+ };
11373
+ }
11374
+ } catch {
11375
+ }
11376
+ return emptyWorkspaceNotice();
11377
+ }
11378
+ function writeStats(stats, queue, passiveResults, suggestCompactAt, workspaceNotice) {
11315
11379
  try {
11316
11380
  writeFileAtomicSync(
11317
11381
  STATS_FILE,
@@ -11319,7 +11383,8 @@ function writeStats(stats, queue, passiveResults, suggestCompactAt) {
11319
11383
  ...stats,
11320
11384
  suggestCompactAt,
11321
11385
  queue,
11322
- passiveResults
11386
+ passiveResults,
11387
+ workspaceNotice
11323
11388
  })
11324
11389
  );
11325
11390
  } catch {
@@ -11334,6 +11399,134 @@ var init_stats = __esm({
11334
11399
  }
11335
11400
  });
11336
11401
 
11402
+ // src/git/upstreamStatus.ts
11403
+ import { execFile as execFile2 } from "child_process";
11404
+ function git(args2, timeout = GIT_TIMEOUT_MS) {
11405
+ return new Promise((resolve4) => {
11406
+ const child = execFile2(
11407
+ "git",
11408
+ args2,
11409
+ {
11410
+ cwd: PROJECT_ROOT,
11411
+ encoding: "utf-8",
11412
+ timeout,
11413
+ maxBuffer: MAX_BUFFER_BYTES,
11414
+ // No controlling tty in a box, but an inherited one anywhere else would
11415
+ // let a credential prompt hold the whole timeout open.
11416
+ env: { ...process.env, GIT_TERMINAL_PROMPT: "0" },
11417
+ // execFile's `timeout` sends SIGTERM, which a wedged `git-remote-https`
11418
+ // can ignore. Escalate so the boot path cannot be held by one.
11419
+ killSignal: "SIGKILL"
11420
+ },
11421
+ (err, stdout, stderr) => {
11422
+ resolve4({
11423
+ ok: !err,
11424
+ stdout: (stdout ?? "").trim(),
11425
+ // Kept so a failure can say WHY. For a probe whose entire failure
11426
+ // model is silence, this is the only line that makes it debuggable.
11427
+ error: err ? (stderr || "").trim() || err.message : null
11428
+ });
11429
+ }
11430
+ );
11431
+ child.on("error", () => {
11432
+ });
11433
+ });
11434
+ }
11435
+ async function readUpstreamStatus() {
11436
+ const inRepo = await git(["rev-parse", "--git-dir"]);
11437
+ if (!inRepo.ok) {
11438
+ return null;
11439
+ }
11440
+ const fetched = await git(
11441
+ ["fetch", "origin", DEFAULT_BRANCH],
11442
+ FETCH_TIMEOUT_MS
11443
+ );
11444
+ if (!fetched.ok) {
11445
+ log17.info(
11446
+ `fetch failed, falling back to the origin/${DEFAULT_BRANCH} on disk: ${fetched.error}`
11447
+ );
11448
+ }
11449
+ const [upstreamRef, headRef] = await Promise.all([
11450
+ git(["rev-parse", `origin/${DEFAULT_BRANCH}`]),
11451
+ git(["rev-parse", "HEAD"])
11452
+ ]);
11453
+ if (!upstreamRef.ok || !headRef.ok) {
11454
+ return null;
11455
+ }
11456
+ const upstream = upstreamRef.stdout;
11457
+ if (upstream === headRef.stdout) {
11458
+ return null;
11459
+ }
11460
+ const incomingResult = await git([
11461
+ "log",
11462
+ `--max-count=${MAX_INCOMING + 1}`,
11463
+ "--format=%an: %s",
11464
+ `HEAD..origin/${DEFAULT_BRANCH}`
11465
+ ]);
11466
+ const incomingLines = incomingResult.ok ? incomingResult.stdout.split("\n").filter((line) => line.length > 0) : [];
11467
+ if (incomingResult.ok && incomingLines.length === 0) {
11468
+ return null;
11469
+ }
11470
+ let behind = null;
11471
+ let ahead = null;
11472
+ const counts = await git([
11473
+ "rev-list",
11474
+ "--left-right",
11475
+ "--count",
11476
+ `origin/${DEFAULT_BRANCH}...HEAD`
11477
+ ]);
11478
+ if (counts.ok) {
11479
+ const [left, right] = counts.stdout.split(/\s+/);
11480
+ behind = Number(left);
11481
+ ahead = Number(right);
11482
+ if (!Number.isFinite(behind) || behind <= 0) {
11483
+ return null;
11484
+ }
11485
+ if (!Number.isFinite(ahead)) {
11486
+ ahead = null;
11487
+ }
11488
+ } else if (!incomingResult.ok) {
11489
+ const contained = await git([
11490
+ "merge-base",
11491
+ "--is-ancestor",
11492
+ `origin/${DEFAULT_BRANCH}`,
11493
+ "HEAD"
11494
+ ]);
11495
+ if (contained.ok) {
11496
+ return null;
11497
+ }
11498
+ log17.info(
11499
+ `behind by an unknown amount: rev-list and log both failed (${counts.error})`
11500
+ );
11501
+ }
11502
+ const status = await git(["status", "--porcelain"]);
11503
+ if (!status.ok) {
11504
+ log17.info(`could not read the working tree state: ${status.error}`);
11505
+ }
11506
+ return {
11507
+ upstream,
11508
+ behind,
11509
+ ahead,
11510
+ dirty: status.ok ? status.stdout.length > 0 : "unknown",
11511
+ incoming: incomingLines.slice(0, MAX_INCOMING),
11512
+ incomingTruncated: incomingLines.length > MAX_INCOMING
11513
+ };
11514
+ }
11515
+ var log17, DEFAULT_BRANCH, MAX_INCOMING, FETCH_TIMEOUT_MS, GIT_TIMEOUT_MS, MAX_BUFFER_BYTES;
11516
+ var init_upstreamStatus = __esm({
11517
+ "src/git/upstreamStatus.ts"() {
11518
+ "use strict";
11519
+ init_projectRoot();
11520
+ init_logger();
11521
+ log17 = createLogger("upstream");
11522
+ DEFAULT_BRANCH = "main";
11523
+ MAX_INCOMING = 20;
11524
+ FETCH_TIMEOUT_MS = 3e4;
11525
+ GIT_TIMEOUT_MS = 1e4;
11526
+ MAX_BUFFER_BYTES = 32 * 1024 * 1024;
11527
+ }
11528
+ });
11529
+
11337
11530
  // src/headless/messageQueue.ts
11338
11531
  function holdRestoredUserItems(items) {
11339
11532
  return items.map(
@@ -11550,7 +11743,7 @@ var headless_exports = {};
11550
11743
  __export(headless_exports, {
11551
11744
  HeadlessSession: () => HeadlessSession
11552
11745
  });
11553
- var log17, EXTERNAL_TOOL_TIMEOUT_MS, LONG_RUNNING_TOOLS, LONG_RUNNING_TOOL_TIMEOUT_MS, USER_FACING_TOOLS, HeadlessSession;
11746
+ var log18, EXTERNAL_TOOL_TIMEOUT_MS, LONG_RUNNING_TOOLS, LONG_RUNNING_TOOL_TIMEOUT_MS, USER_FACING_TOOLS, HeadlessSession;
11554
11747
  var init_headless = __esm({
11555
11748
  "src/headless/index.ts"() {
11556
11749
  "use strict";
@@ -11568,11 +11761,12 @@ var init_headless = __esm({
11568
11761
  init_attachments();
11569
11762
  init_planFile();
11570
11763
  init_stats();
11764
+ init_upstreamStatus();
11571
11765
  init_tools10();
11572
11766
  init_messageQueue();
11573
11767
  init_resolve();
11574
11768
  init_sentinel();
11575
- log17 = createLogger("headless");
11769
+ log18 = createLogger("headless");
11576
11770
  EXTERNAL_TOOL_TIMEOUT_MS = 3e5;
11577
11771
  LONG_RUNNING_TOOLS = /* @__PURE__ */ new Set(["runMethod", "testJewel"]);
11578
11772
  LONG_RUNNING_TOOL_TIMEOUT_MS = 18e5;
@@ -11636,6 +11830,15 @@ var init_headless = __esm({
11636
11830
  * Persisted to .remy-stats.json alongside the queue.
11637
11831
  */
11638
11832
  passivePen = [];
11833
+ /**
11834
+ * The workspace-behind note, and the upstream tip it was raised for.
11835
+ *
11836
+ * Rides the same sweep as the passive pen and for the same reason: a
11837
+ * workspace that is behind is worth knowing about before the next piece of
11838
+ * work, and worth nothing at all if nobody is working — so it must never
11839
+ * initiate a turn of its own.
11840
+ */
11841
+ workspaceNotice = emptyWorkspaceNotice();
11639
11842
  // External tool bridge
11640
11843
  pendingTools = /* @__PURE__ */ new Map();
11641
11844
  earlyResults = /* @__PURE__ */ new Map();
@@ -11682,6 +11885,8 @@ var init_headless = __esm({
11682
11885
  }
11683
11886
  );
11684
11887
  this.passivePen = loadPassiveResults();
11888
+ this.workspaceNotice = loadWorkspaceNotice();
11889
+ void this.checkUpstream();
11685
11890
  this.persistStats();
11686
11891
  if (resumed) {
11687
11892
  this.emit("session_restored", {
@@ -11805,7 +12010,7 @@ var init_headless = __esm({
11805
12010
  try {
11806
12011
  this.handleCancel("shutdown");
11807
12012
  } catch (err) {
11808
- log17.warn("Shutdown cancel failed", { error: err?.message });
12013
+ log18.warn("Shutdown cancel failed", { error: err?.message });
11809
12014
  }
11810
12015
  this.emit("stopping");
11811
12016
  this.emit("stopped");
@@ -11821,7 +12026,7 @@ var init_headless = __esm({
11821
12026
  }
11822
12027
  const line = JSON.stringify(payload) + "\n";
11823
12028
  if (event === "history") {
11824
- log17.info("Wrote history event to stdout", {
12029
+ log18.info("Wrote history event to stdout", {
11825
12030
  requestId,
11826
12031
  bytes: line.length
11827
12032
  });
@@ -11868,9 +12073,45 @@ var init_headless = __esm({
11868
12073
  this.sessionStats,
11869
12074
  this.queue.snapshot(),
11870
12075
  this.passivePen,
11871
- suggestCompactAt
12076
+ suggestCompactAt,
12077
+ this.workspaceNotice
11872
12078
  );
11873
12079
  }
12080
+ /**
12081
+ * Park a note if this workspace is missing work that is already in production.
12082
+ *
12083
+ * Once per upstream tip rather than once per boot. Remy restarts for reasons
12084
+ * that have nothing to do with the repo — a pod recycle, a crash, a new
12085
+ * session — and re-raising the same note on each of those turns a useful
12086
+ * signal into nagging. A colleague publishing again moves the tip, which
12087
+ * re-arms it; bringing the workspace current means there is nothing to raise.
12088
+ *
12089
+ * Never throws: this is called unawaited from the boot path, so an unhandled
12090
+ * rejection here would be a crash at the least recoverable moment.
12091
+ */
12092
+ async checkUpstream() {
12093
+ try {
12094
+ const status = await readUpstreamStatus();
12095
+ if (!status || status.upstream === this.workspaceNotice.lastNotedUpstream) {
12096
+ return;
12097
+ }
12098
+ this.workspaceNotice = {
12099
+ pendingNote: buildWorkspaceStatusMessage(status),
12100
+ lastNotedUpstream: status.upstream
12101
+ };
12102
+ this.persistStats();
12103
+ log18.info("workspace behind upstream; note parked for the next turn", {
12104
+ // The upstream sha is the dedupe key, so it is what makes "why did I
12105
+ // not get a note" answerable from the log alone.
12106
+ upstream: status.upstream,
12107
+ behind: status.behind,
12108
+ ahead: status.ahead,
12109
+ dirty: status.dirty
12110
+ });
12111
+ } catch (err) {
12112
+ log18.info(`upstream check failed: ${String(err)}`);
12113
+ }
12114
+ }
11874
12115
  //////////////////////////////////////////////////////////////////////////////
11875
12116
  // Background completions (tool-block mutation; message delivery via queue)
11876
12117
  //////////////////////////////////////////////////////////////////////////////
@@ -11917,7 +12158,7 @@ var init_headless = __esm({
11917
12158
  if (this.sessionStats.lastContextSize <= threshold) {
11918
12159
  return;
11919
12160
  }
11920
- log17.info("Forced compaction gate triggered", {
12161
+ log18.info("Forced compaction gate triggered", {
11921
12162
  contextSize: this.sessionStats.lastContextSize,
11922
12163
  threshold,
11923
12164
  model: parentModel,
@@ -11937,7 +12178,7 @@ var init_headless = __esm({
11937
12178
  onBackgroundComplete = (toolCallId, name, result, subAgentMessages) => {
11938
12179
  const notify = getToolByName(name)?.backgroundNotify ?? "wake";
11939
12180
  this.pendingBlockUpdates.push({ toolCallId, result, subAgentMessages });
11940
- log17.info("Background complete", {
12181
+ log18.info("Background complete", {
11941
12182
  toolCallId,
11942
12183
  name,
11943
12184
  notify,
@@ -12219,7 +12460,7 @@ var init_headless = __esm({
12219
12460
  const { documents, images } = await persistAttachments(attachments);
12220
12461
  return buildUploadHeader(documents, images) || void 0;
12221
12462
  } catch (err) {
12222
- log17.warn("Attachment persistence failed", { error: err.message });
12463
+ log18.warn("Attachment persistence failed", { error: err.message });
12223
12464
  return void 0;
12224
12465
  }
12225
12466
  }
@@ -12279,7 +12520,7 @@ var init_headless = __esm({
12279
12520
  }
12280
12521
  if (batch.length === 0) {
12281
12522
  if (landings > 0) {
12282
- log17.info("promptUser store landings passed through", { landings });
12523
+ log18.info("promptUser store landings passed through", { landings });
12283
12524
  }
12284
12525
  return raw;
12285
12526
  }
@@ -12287,7 +12528,7 @@ var init_headless = __esm({
12287
12528
  try {
12288
12529
  results = await persistAttachmentList(batch);
12289
12530
  } catch (err) {
12290
- log17.warn("promptUser upload persistence failed", {
12531
+ log18.warn("promptUser upload persistence failed", {
12291
12532
  error: err.message
12292
12533
  });
12293
12534
  results = batch.map(() => null);
@@ -12299,7 +12540,7 @@ var init_headless = __esm({
12299
12540
  return r.localPath;
12300
12541
  }
12301
12542
  const att = batch[cursor + i];
12302
- log17.warn("promptUser upload not persisted; falling back to url", {
12543
+ log18.warn("promptUser upload not persisted; falling back to url", {
12303
12544
  filename: att.filename
12304
12545
  });
12305
12546
  return att.url;
@@ -12307,7 +12548,7 @@ var init_headless = __esm({
12307
12548
  cursor += slot.count;
12308
12549
  answers[slot.id] = slot.isArray ? paths : paths[0];
12309
12550
  }
12310
- log17.info("promptUser uploads persisted", { count: batch.length });
12551
+ log18.info("promptUser uploads persisted", { count: batch.length });
12311
12552
  return JSON.stringify(answers);
12312
12553
  }
12313
12554
  /**
@@ -12330,7 +12571,7 @@ var init_headless = __esm({
12330
12571
  async runSingleTurn(parsed, requestId, fromChain = false, queued = false) {
12331
12572
  const attachments = parsed.attachments;
12332
12573
  if (attachments?.length) {
12333
- log17.info("Message has attachments", {
12574
+ log18.info("Message has attachments", {
12334
12575
  count: attachments.length,
12335
12576
  urls: attachments.map((a) => a.url)
12336
12577
  });
@@ -12478,6 +12719,18 @@ var init_headless = __esm({
12478
12719
  hidden: true
12479
12720
  });
12480
12721
  }
12722
+ const hasUserWords = entries.some(
12723
+ (entry) => !entry.hidden && !isAutomatedMessage(entry.text)
12724
+ );
12725
+ if (this.workspaceNotice.pendingNote && hasUserWords) {
12726
+ const note = this.workspaceNotice.pendingNote;
12727
+ this.workspaceNotice = {
12728
+ ...this.workspaceNotice,
12729
+ pendingNote: null
12730
+ };
12731
+ this.persistStats();
12732
+ entries.unshift({ text: note, hidden: true });
12733
+ }
12481
12734
  const consumedRids = [];
12482
12735
  const takeSteering = async () => {
12483
12736
  const items = this.queue.removeWhere(
@@ -12530,7 +12783,7 @@ var init_headless = __esm({
12530
12783
  error: "Turn ended unexpectedly"
12531
12784
  });
12532
12785
  }
12533
- log17.info("Turn complete", {
12786
+ log18.info("Turn complete", {
12534
12787
  requestId,
12535
12788
  durationMs: Date.now() - this.turnStart
12536
12789
  });
@@ -12542,7 +12795,7 @@ var init_headless = __esm({
12542
12795
  error: err.message
12543
12796
  });
12544
12797
  }
12545
- log17.warn("Command failed", {
12798
+ log18.warn("Command failed", {
12546
12799
  action: "message",
12547
12800
  requestId,
12548
12801
  error: err.message
@@ -12849,7 +13102,7 @@ var init_headless = __esm({
12849
13102
  try {
12850
13103
  parsed = JSON.parse(line);
12851
13104
  } catch (err) {
12852
- log17.warn("Invalid JSON on stdin", {
13105
+ log18.warn("Invalid JSON on stdin", {
12853
13106
  error: err.message,
12854
13107
  lineLength: line.length,
12855
13108
  preview: line.slice(0, 200)
@@ -12858,7 +13111,7 @@ var init_headless = __esm({
12858
13111
  return;
12859
13112
  }
12860
13113
  const { action, requestId } = parsed;
12861
- log17.info("Command received", { action, requestId });
13114
+ log18.info("Command received", { action, requestId });
12862
13115
  if (action === "tool_result" && parsed.id) {
12863
13116
  const id = parsed.id;
12864
13117
  const result = parsed.result ?? "";
@@ -12867,7 +13120,7 @@ var init_headless = __esm({
12867
13120
  this.pendingTools.delete(id);
12868
13121
  pending2.resolve(result);
12869
13122
  } else if (!this.running) {
12870
- log17.info("Late tool_result while idle, dismissing", { id });
13123
+ log18.info("Late tool_result while idle, dismissing", { id });
12871
13124
  this.emit("completed", { success: true }, requestId);
12872
13125
  } else {
12873
13126
  this.earlyResults.set(id, result);
@@ -12880,7 +13133,7 @@ var init_headless = __esm({
12880
13133
  ...typeof parsed.before === "number" ? { before: parsed.before } : {},
12881
13134
  ...typeof parsed.limit === "number" ? { limit: parsed.limit } : {}
12882
13135
  });
12883
- log17.info("History response", {
13136
+ log18.info("History response", {
12884
13137
  requestId,
12885
13138
  startIndex: page.startIndex,
12886
13139
  endIndex: page.endIndex,
@@ -25,8 +25,17 @@ The dev database is disposable. Experiment freely — there's no risk of breakin
25
25
 
26
26
  `console.log`, `console.warn`, and `console.error` in methods are captured and displayed in the terminal. They don't affect the method's return value. Every method execution is logged with full input, output, duration, and error info.
27
27
 
28
+ ## Working Alongside Other People
29
+
30
+ Most apps are single-owner, but sometimes an app can have several people with edit access, and each of them gets their own workspace: a separate copy of the code, its own dev database, its own conversation with you. Nothing one person does is visible to anyone else until it's pushed, and you can't see into the other workspaces. Everyone's copy tracks the same branch - the one production builds from - and publishing is what advances it. If this copy is missing work the default branch already has, it's likely someone else published while this copy was idle
31
+
32
+ Whenever any of this reaches the user, talk about the work and the people, not the plumbing. What someone shipped and what it means for what they're about to do is useful. Commit counts, branch names, and phrases like "clean tree" or "fast-forward" are not — almost nobody holds an accurate model of git, including people who use it daily, and reciting repository state at someone is not the same as telling them what happened.
33
+
28
34
  ## What Happens on Deploy
29
35
 
36
+ Publishing pushes `main` — the `publishing` skill has the flow. This section is what the platform
37
+ does once that branch moves:
38
+
30
39
  ```bash
31
40
  git push origin main
32
41
  ```
@@ -113,10 +113,12 @@ A spec starts with YAML frontmatter followed by freeform Markdown. There's no ma
113
113
  - `name` (required) — display name for the spec file
114
114
  - `description` (optional) — short summary of what this file covers
115
115
  - `type` (optional) — defaults to `spec`. Other values: `design/color` (color palette definition), `design/typography` (font and type style definition), `roadmap` (feature roadmap item). The frontend renders these types with specialized editors.
116
- - `status` (roadmap only) — `done`, `in-progress`, or `not-started`
117
- - `requires` (roadmap only) — array of slugs for prerequisite roadmap items. Empty array means available now.
116
+ - `status` (roadmap only) — `done`, `in-progress`, or `not-started`. `in-progress` means being built right now, so it is transient: at most one item holds it, and it becomes `done` when the build lands. There is no `blocked` value — blocking is derived from `requires`, and authoring it separately creates a second source of truth that drifts.
117
+ - `requires` (roadmap only) — inline array of **filenames** for prerequisite roadmap items, e.g. `["core-collections.md", "agent-chat.md"]`. Empty array means available now. Filenames, not slugs, so the entries match the names `index.json` uses.
118
118
  - `effort` (roadmap only) — `quick`, `small`, `medium`, or `large`
119
119
 
120
+ **Don't quote frontmatter values** unless the value contains a `:` and would otherwise be ambiguous. Quotes are stripped when the file is read, so `name: Share & Export` and `name: "Share & Export"` are the same value — the unquoted form is what everything else uses.
121
+
120
122
  ```markdown
121
123
  ---
122
124
  name: Expense Tracker
@@ -210,6 +212,16 @@ Share haikus as styled image cards on social media or download as prints.
210
212
  The card system generates images using the brand's typography and color
211
213
  palette, creating shareable assets that feel native to the app's identity.
212
214
 
215
+ ## What it looks like
216
+
217
+ - Card composer — pick a haiku, pick a frame
218
+ - Share sheet — one tap to any social app
219
+ - Print export — a high-resolution file for a real print
220
+
221
+ ## Key details
222
+
223
+ Cards always carry the app's own type and color, never a generic template.
224
+
213
225
  ~~~
214
226
  Use generateImage to create styled cards. Card template
215
227
  applies brand typography and colors from the spec. Export as PNG via
@@ -217,10 +229,22 @@ CDN transform at 2x resolution. Social sharing via Web Share API with
217
229
  clipboard fallback for unsupported browsers.
218
230
  ~~~
219
231
 
232
+ ## Still to build
233
+
234
+ A frame gallery — seasonal and custom frames — and scheduled sharing.
235
+
220
236
  ## History
221
237
 
222
- - **2026-03-22** — Built card generation using generateImage.
223
- Added share button to haiku detail view.
238
+ - **2026-03-22** — Built the composer, share sheet and print export.
239
+ Frame gallery and scheduling deferred.
224
240
  ```
225
241
 
226
- Unbuilt item — same shape with `status: not-started`, a body describing the intended feature, and no History section (History is appended when it's built).
242
+ Unbuilt item — same shape with `status: not-started`, a body describing the intended feature, and no History or Still to build section (both are written when it's built).
243
+
244
+ Three body sections on roadmap items are read by the frontend, so their shape matters:
245
+
246
+ - **`## What it looks like`** — one bullet per user-visible surface the item delivers. The roadmap sizes an item from how many surfaces it has, so every item needs this section and every bullet should name one concrete surface.
247
+ - **`## Still to build`** — a sentence or two naming what this item deliberately deferred. Only present when there is something. This is how a `done` item says it landed partially, so name real scope, not vague polish.
248
+ - **`## History`** — one `- **YYYY-MM-DD** — …` entry per build, newest last. Keep entries to a sentence or two about what shipped and what was deferred; they are read in a sidebar, not as a changelog essay.
249
+
250
+ Write all three as ordinary Markdown — plain `-` bullets and prose. Don't use task-list checkboxes (`- [x]`); the spec editor doesn't round-trip them, so they are erased the first time anyone edits the file.
@@ -92,7 +92,7 @@ const { vendor } = await api.approveVendor({ vendorId: '...' });
92
92
  - **Built-in auth.** Opt-in via manifest. Developer builds login UI, platform handles verification codes (email/SMS), cookie sessions, and role enforcement. Backend methods use `auth.requireRole('admin')` for access control.
93
93
  - **Multiple interfaces, one codebase.** Web, API, Cron, Webhook, Email, MCP, Agent, Voice — all invoke the same methods. Methods don't know which interface called them.
94
94
  - **Sandboxed execution.** Each method invocation runs in its own isolated execution context with npm packages pre-installed.
95
- - **Git-native deployment.** Push to default branch to deploy. Push to feature branch for preview. Rollback is a git revert.
95
+ - **Git-native deployment.** Push to default branch to deploy. Push to feature branch for preview. Rollback is a git revert. Each person with edit access works in their own copy of the app, and everyone publishes to the same default branch.
96
96
  - **Secrets.** Encrypted environment variables with separate dev/prod values. Injected as `process.env` in methods. For third-party service credentials not covered by the SDK.
97
97
 
98
98
  ## Minimum Viable App