@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/README.md +3 -4
- package/dist/headless.d.ts +22 -0
- package/dist/headless.js +269 -26
- package/dist/index.js +280 -27
- package/dist/prompt/compiled/dev-and-deploy.md +9 -0
- package/dist/prompt/compiled/msfm.md +29 -5
- package/dist/prompt/compiled/platform.md +1 -1
- package/dist/prompt/skills/dataSources.md +11 -2
- package/dist/prompt/skills/publishing.md +60 -6
- package/dist/prompt/static/instructions.md +4 -3
- package/dist/prompt/static/spec-maintenance.md +1 -1
- package/dist/subagents/productVision/prompt.md +27 -15
- package/package.json +1 -1
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, "&").replace(/</g, "<").replace(/>/g, ">").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.
|
|
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([
|
|
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(
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
223
|
-
|
|
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 (
|
|
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
|