usebeeline 0.1.18 → 0.1.20

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.
Files changed (3) hide show
  1. package/README.md +4 -3
  2. package/dist/usebeeline.mjs +1599 -1513
  3. package/package.json +1 -1
@@ -3025,7 +3025,7 @@ var require_websocket = __commonJS({
3025
3025
  var http = __require("http");
3026
3026
  var net2 = __require("net");
3027
3027
  var tls = __require("tls");
3028
- var { randomBytes: randomBytes10, createHash: createHash16 } = __require("crypto");
3028
+ var { randomBytes: randomBytes10, createHash: createHash15 } = __require("crypto");
3029
3029
  var { Duplex, Readable } = __require("stream");
3030
3030
  var { URL: URL2 } = __require("url");
3031
3031
  var PerMessageDeflate2 = require_permessage_deflate();
@@ -3693,7 +3693,7 @@ var require_websocket = __commonJS({
3693
3693
  abortHandshake(websocket, socket, "Invalid Upgrade header");
3694
3694
  return;
3695
3695
  }
3696
- const digest = createHash16("sha1").update(key + GUID).digest("base64");
3696
+ const digest = createHash15("sha1").update(key + GUID).digest("base64");
3697
3697
  if (res.headers["sec-websocket-accept"] !== digest) {
3698
3698
  abortHandshake(websocket, socket, "Invalid Sec-WebSocket-Accept header");
3699
3699
  return;
@@ -4062,7 +4062,7 @@ var require_websocket_server = __commonJS({
4062
4062
  var EventEmitter2 = __require("events");
4063
4063
  var http = __require("http");
4064
4064
  var { Duplex } = __require("stream");
4065
- var { createHash: createHash16 } = __require("crypto");
4065
+ var { createHash: createHash15 } = __require("crypto");
4066
4066
  var extension2 = require_extension();
4067
4067
  var PerMessageDeflate2 = require_permessage_deflate();
4068
4068
  var subprotocol2 = require_subprotocol();
@@ -4369,7 +4369,7 @@ var require_websocket_server = __commonJS({
4369
4369
  );
4370
4370
  }
4371
4371
  if (this._state > RUNNING) return abortHandshake(socket, 503);
4372
- const digest = createHash16("sha1").update(key + GUID).digest("base64");
4372
+ const digest = createHash15("sha1").update(key + GUID).digest("base64");
4373
4373
  const headers = [
4374
4374
  "HTTP/1.1 101 Switching Protocols",
4375
4375
  "Upgrade: websocket",
@@ -16517,7 +16517,9 @@ async function revokeGrants(mcp, ref) {
16517
16517
  // apps/body/dist/squire-mcp-client.js
16518
16518
  init_squire_host();
16519
16519
  var INITIALIZE_TIMEOUT_MS = 3e4;
16520
- var CALL_TIMEOUT_MS = 12e4;
16520
+ var SQUIRE_LONGEST_CALL_MS = 18e4;
16521
+ var CALL_TIMEOUT_MARGIN_MS = 3e4;
16522
+ var CALL_TIMEOUT_MS = SQUIRE_LONGEST_CALL_MS + CALL_TIMEOUT_MARGIN_MS;
16521
16523
  var StdioSquireMcpClient = class {
16522
16524
  options;
16523
16525
  child;
@@ -16535,7 +16537,7 @@ var StdioSquireMcpClient = class {
16535
16537
  /** One Squire MCP tool call; resolves with the tool's parsed result. */
16536
16538
  async call(tool, args = {}) {
16537
16539
  await this.ensureSession();
16538
- const response = await this.request("tools/call", { name: tool, arguments: args });
16540
+ const response = await this.request("tools/call", { name: tool, arguments: args }, tool);
16539
16541
  if (response.isError) {
16540
16542
  const text3 = response.content?.find((entry) => entry.type === "text")?.text ?? "tool error";
16541
16543
  throw new Error(`${tool} failed: ${text3}`);
@@ -16634,16 +16636,17 @@ var StdioSquireMcpClient = class {
16634
16636
  this.child?.stdin.write(`${JSON.stringify({ jsonrpc: "2.0", method, ...params ? { params } : {} })}
16635
16637
  `);
16636
16638
  }
16637
- request(method, params) {
16639
+ request(method, params, toolName2) {
16638
16640
  const id = this.nextId++;
16639
16641
  const child = this.child;
16640
16642
  if (!child)
16641
16643
  return Promise.reject(new Error("Squire MCP session is not running"));
16642
16644
  return new Promise((resolve45, reject) => {
16645
+ const timeoutMs = method === "initialize" ? INITIALIZE_TIMEOUT_MS : CALL_TIMEOUT_MS;
16643
16646
  const timer = setTimeout(() => {
16644
16647
  this.pending.delete(id);
16645
- reject(new Error(`${method} timed out`));
16646
- }, method === "initialize" ? INITIALIZE_TIMEOUT_MS : CALL_TIMEOUT_MS);
16648
+ reject(new Error(`${toolName2 ?? method} timed out after ${timeoutMs}ms`));
16649
+ }, timeoutMs);
16647
16650
  this.pending.set(id, { resolve: resolve45, reject, timer });
16648
16651
  child.stdin.write(`${JSON.stringify({ jsonrpc: "2.0", id, method, params })}
16649
16652
  `);
@@ -16730,26 +16733,26 @@ function staleLeaseResult(value) {
16730
16733
  return false;
16731
16734
  }
16732
16735
  }
16733
- function resultStatus(value, depth = 0) {
16736
+ function resultString(value, key, depth = 0) {
16734
16737
  if (depth > 8 || !value)
16735
16738
  return void 0;
16736
16739
  if (typeof value === "string") {
16737
16740
  if (value.length > 1e5 || !value.trimStart().startsWith("{"))
16738
16741
  return void 0;
16739
16742
  try {
16740
- return resultStatus(JSON.parse(value), depth + 1);
16743
+ return resultString(JSON.parse(value), key, depth + 1);
16741
16744
  } catch {
16742
16745
  return void 0;
16743
16746
  }
16744
16747
  }
16745
16748
  if (Array.isArray(value))
16746
- return value.map((item2) => resultStatus(item2, depth + 1)).find(Boolean);
16749
+ return value.map((item2) => resultString(item2, key, depth + 1)).find(Boolean);
16747
16750
  if (typeof value !== "object")
16748
16751
  return void 0;
16749
16752
  const item = value;
16750
- if (typeof item.status === "string")
16751
- return item.status;
16752
- return Object.values(item).map((entry) => resultStatus(entry, depth + 1)).find(Boolean);
16753
+ if (typeof item[key] === "string")
16754
+ return item[key];
16755
+ return Object.values(item).map((entry) => resultString(entry, key, depth + 1)).find(Boolean);
16753
16756
  }
16754
16757
  var SquireTaskRelay = class {
16755
16758
  agentId;
@@ -17040,21 +17043,29 @@ var SquireTaskRelay = class {
17040
17043
  if (approval) {
17041
17044
  const ids = requestedIds.length ? requestedIds : [...task.sessions];
17042
17045
  const approvalKey = createHash("sha256").update(approval.approvalId ?? approval.approvalUrl).digest("hex");
17043
- const status = resultStatus(result);
17046
+ const status = resultString(result, "status");
17044
17047
  const pending = status === "approval_pending" || status === "pending_approval";
17045
- if (pending && ids.length) {
17046
- task.pendingApprovals.set(approvalKey, {
17048
+ if (pending && (ids.length || approval.approvalId)) {
17049
+ clearTimeout(task.pendingApprovals.get(approvalKey)?.expiry);
17050
+ const entry = {
17047
17051
  requestId: active.requestId,
17048
17052
  sessionIds: new Set(ids),
17049
17053
  tool: approval.tool,
17050
17054
  title: approval.title,
17051
17055
  detail: approval.detail
17052
- });
17056
+ };
17057
+ const expiresAt = Date.parse(resultString(result, "expires_at") ?? "");
17058
+ if (approval.approvalId && Number.isFinite(expiresAt)) {
17059
+ entry.expiry = setTimeout(() => this.expireApproval(task, approvalKey, entry, approval.approvalId), Math.min(Math.max(expiresAt - Date.now(), 0), 2 ** 31 - 1));
17060
+ entry.expiry.unref?.();
17061
+ }
17062
+ task.pendingApprovals.set(approvalKey, entry);
17053
17063
  this.log("approval-pending", task, {
17054
17064
  tool: safeToolName ?? null,
17055
17065
  sessionId: redactedSessionId(ids[0])
17056
17066
  });
17057
17067
  } else if (!pending) {
17068
+ clearTimeout(task.pendingApprovals.get(approvalKey)?.expiry);
17058
17069
  task.pendingApprovals.delete(approvalKey);
17059
17070
  }
17060
17071
  }
@@ -17083,12 +17094,35 @@ var SquireTaskRelay = class {
17083
17094
  }
17084
17095
  releasePendingApprovals(task, sessionIdsToRelease) {
17085
17096
  for (const [id, approval] of task.pendingApprovals) {
17097
+ if (!approval.sessionIds.size)
17098
+ continue;
17086
17099
  for (const sessionId of sessionIdsToRelease)
17087
17100
  approval.sessionIds.delete(sessionId);
17088
- if (!approval.sessionIds.size)
17101
+ if (!approval.sessionIds.size) {
17102
+ clearTimeout(approval.expiry);
17089
17103
  task.pendingApprovals.delete(id);
17104
+ }
17090
17105
  }
17091
17106
  }
17107
+ /**
17108
+ * Squire stops watching an approval at its `expires_at` and sends nothing,
17109
+ * so the relay wakes the asking turn itself with `expired`.
17110
+ */
17111
+ expireApproval(task, approvalKey, entry, approvalId) {
17112
+ if (task.pendingApprovals.get(approvalKey) !== entry)
17113
+ return;
17114
+ task.pendingApprovals.delete(approvalKey);
17115
+ this.onApprovalDecided({
17116
+ requestId: entry.requestId,
17117
+ approvalId,
17118
+ status: "expired",
17119
+ tool: entry.tool,
17120
+ title: entry.title,
17121
+ detail: entry.detail
17122
+ });
17123
+ if (!this.active && !this.approvalRequestId && !task.pendingApprovals.size)
17124
+ this.scheduleIdle();
17125
+ }
17092
17126
  /**
17093
17127
  * Squire's own unsolicited decision on an approval this task is still
17094
17128
  * holding a connection open for. Matched back to the pending entry by the
@@ -17108,6 +17142,7 @@ var SquireTaskRelay = class {
17108
17142
  const pending = task.pendingApprovals.get(approvalKey);
17109
17143
  if (!pending)
17110
17144
  return;
17145
+ clearTimeout(pending.expiry);
17111
17146
  task.pendingApprovals.delete(approvalKey);
17112
17147
  this.onApprovalDecided({
17113
17148
  requestId: pending.requestId,
@@ -17990,7 +18025,7 @@ var SERVER_EVENT_KIND_DETAIL = {
17990
18025
  "check-failed": "fires when a corner's pull request checks turn red",
17991
18026
  "merged": "fires when a corner's pull request merges",
17992
18027
  "grant-decided": "carries the grant id and status and resumes the turn that asked for the grant",
17993
- "squire-approval-decided": "carries what was approved or denied and the Squire approval id, and resumes the turn that asked",
18028
+ "squire-approval-decided": "carries what was approved, denied, or left to expire and the Squire approval id, and resumes the turn that asked",
17994
18029
  "connector-offer-decided": "fires when a human accepts or declines an offered connector, or a reconnect ceremony finishes, and resumes the turn that offered it",
17995
18030
  "turn-cancelled": "fires when a running turn is stopped",
17996
18031
  "choice-answered": "fires when a human answers a posted choice, starting a new turn for the asking agent",
@@ -28830,7 +28865,6 @@ var DaemonApiClient = class {
28830
28865
  connectorAssignmentListener;
28831
28866
  agentSignInListener;
28832
28867
  cornerCompleteListener;
28833
- cornerRestartListener;
28834
28868
  helperReleaseListener;
28835
28869
  forceUpdateListener;
28836
28870
  helperIdentity = {
@@ -28958,10 +28992,6 @@ var DaemonApiClient = class {
28958
28992
  setCornerCompleteListener(listener) {
28959
28993
  this.cornerCompleteListener = listener;
28960
28994
  }
28961
- /** A no-code corner entered the code lane and must rebuild its local runtime. */
28962
- setCornerRestartListener(listener) {
28963
- this.cornerRestartListener = listener;
28964
- }
28965
28995
  setHelperReleaseListener(listener) {
28966
28996
  this.helperReleaseListener = listener;
28967
28997
  }
@@ -29058,10 +29088,6 @@ var DaemonApiClient = class {
29058
29088
  this.cornerCompleteListener?.(event.roomId);
29059
29089
  return;
29060
29090
  }
29061
- if (event.type === "corner-restart" && typeof event.roomId === "string") {
29062
- this.cornerRestartListener?.(event.roomId);
29063
- return;
29064
- }
29065
29091
  if (event.type === "connector-assignment") {
29066
29092
  this.connectorAssignmentListener?.();
29067
29093
  return;
@@ -29558,7 +29584,7 @@ init_self_update_manifest();
29558
29584
  // apps/body/dist/managed-update.js
29559
29585
  init_self_update();
29560
29586
  import { spawn as spawn12 } from "node:child_process";
29561
- import { createHash as createHash9 } from "node:crypto";
29587
+ import { createHash as createHash8 } from "node:crypto";
29562
29588
  import { mkdir as mkdir14, rm as rm8, stat as stat6, writeFile as writeFile11 } from "node:fs/promises";
29563
29589
  import { dirname as dirname15, resolve as resolve22 } from "node:path";
29564
29590
  init_launchd();
@@ -31616,7 +31642,7 @@ function stringify(obj, { maxDepth = 1e3, numbersAsFloat = false } = {}) {
31616
31642
  }
31617
31643
 
31618
31644
  // apps/body/dist/agent-home.js
31619
- import { createHash as createHash8, randomUUID as randomUUID6 } from "node:crypto";
31645
+ import { createHash as createHash7, randomUUID as randomUUID6 } from "node:crypto";
31620
31646
  import { chmod as chmod5, copyFile, lstat as lstat3, mkdir as mkdir11, readFile as readFile16, readlink, readdir as readdir4, realpath, rename as rename7, rm as rm6, symlink as symlink2, unlink as unlink5, writeFile as writeFile9 } from "node:fs/promises";
31621
31647
  import { homedir as homedir11 } from "node:os";
31622
31648
  import { basename as basename3, dirname as dirname12, join as join12, relative, resolve as resolve16, sep } from "node:path";
@@ -31901,9 +31927,18 @@ var CREDENTIAL_DIRECTORIES = /* @__PURE__ */ new Set([
31901
31927
  ".docker",
31902
31928
  "keyrings",
31903
31929
  "gh",
31904
- "trusty-squire",
31905
- "beeline"
31930
+ "trusty-squire"
31906
31931
  ]);
31932
+ function isBeelineKeyStore(segments) {
31933
+ return segments.some((segment, index) => {
31934
+ if (segment.toLowerCase() !== "beeline" || index === 0)
31935
+ return false;
31936
+ return segments[index - 1].toLowerCase() === ".config";
31937
+ });
31938
+ }
31939
+ function isAgentRuntimeFile(segments) {
31940
+ return segments.some((segment, index) => segment.toLowerCase() === "beeline" && segments[index + 1]?.toLowerCase() === "agents" && index + 3 === segments.length - 1);
31941
+ }
31907
31942
  var CREDENTIAL_EXTENSIONS = /\.(pem|p12|pfx|key|jks|keystore|kdbx|asc|gpg|ppk|crt|pkcs12)$/i;
31908
31943
  var CREDENTIAL_WORD = /(^|[-_.])(secret|secrets|credential|credentials|password|passwd|token|apikey|api_key|keystore|privatekey)([-_.]|$)/i;
31909
31944
  var ENV_FILE = /(^|[.\-_])env(\.|$)|^\.env/i;
@@ -31935,7 +31970,12 @@ function namesCredential(word) {
31935
31970
  return true;
31936
31971
  if (base.endsWith(".env") || ENV_FILE.test(base))
31937
31972
  return true;
31938
- return segments.slice(0, -1).some((segment) => CREDENTIAL_DIRECTORIES.has(segment.toLowerCase()));
31973
+ if (segments.slice(0, -1).some((segment) => CREDENTIAL_DIRECTORIES.has(segment.toLowerCase()))) {
31974
+ return true;
31975
+ }
31976
+ if (isBeelineKeyStore(segments))
31977
+ return true;
31978
+ return isAgentRuntimeFile(segments);
31939
31979
  }
31940
31980
  function namesCredentialFile(argv) {
31941
31981
  return argv.slice(1).some(namesCredential);
@@ -32221,1062 +32261,416 @@ var MESSAGE_SEARCH_FILLER_WORDS = [
32221
32261
  ];
32222
32262
  var FILLER_WORDS = new Set(MESSAGE_SEARCH_FILLER_WORDS);
32223
32263
 
32224
- // packages/api-contract/dist/system-identity.js
32225
- import { createHash as createHash7 } from "node:crypto";
32226
- var SYSTEM_IDENTITY_ID = createHash7("sha256").update("beeline:system-identity").digest("hex");
32227
- var DELETED_ACCOUNT_IDENTITY_ID = createHash7("sha256").update("beeline:deleted-account").digest("hex");
32228
-
32229
- // apps/body/dist/transcript-budget.js
32230
- function budgetTranscript(lines, modelContextTokens, otherPromptBytes) {
32231
- const context = modelContextTokens && modelContextTokens > 0 ? modelContextTokens : 32768;
32232
- const reserveTokens = Math.max(4096, Math.ceil(context * 0.3));
32233
- const available = Math.max(0, context - Math.ceil(otherPromptBytes / 2) - reserveTokens);
32234
- const budget = Math.max(0, Math.min(24e3, Math.floor(context * 0.25), available * 2) - 128);
32235
- if (!budget)
32236
- return [];
32237
- const selected = [];
32238
- let used = 0;
32239
- const rowLimit = Math.min(4096, Math.floor(budget / 2));
32240
- for (let index = lines.length - 1; index >= 0; index -= 1) {
32241
- const row = boundMessageText(lines[index], rowLimit);
32242
- const bytes = Buffer.byteLength(row) + 1;
32243
- if (used + bytes > budget)
32244
- break;
32245
- selected.push(row);
32246
- used += bytes;
32247
- }
32248
- selected.reverse();
32249
- if (selected.length < lines.length) {
32250
- selected.unshift(`[${lines.length - selected.length} older messages omitted from this prompt]`);
32264
+ // apps/body/dist/beeline-skill.js
32265
+ var USING_BEELINE_SKILL_NAME = "using-beeline";
32266
+ var BEELINE_TRIAGE_SKILL_NAME = "beeline-triage";
32267
+ var BEELINE_REVIEW_SKILL_NAME = "beeline-review";
32268
+ var BEELINE_SPEC_SKILL_NAME = "beeline-spec";
32269
+ var BEELINE_ROOM_MECHANICS = [
32270
+ "Files and photos on the message you are answering are downloaded for you: read them at the local path named in the prompt (photos may also arrive inline). A file on an earlier message shows only as a \u{1F4CE} marker with an id; call beeline-agent download_attachment with that message id and attachment id, then read the returned path. Never fetch a media URL; if the download fails, tell the requester you could not open the file.",
32271
+ "To create a file you can send, call beeline-agent write_scratch_file with a relative path and content - text by default, or base64 for bytes you computed; it returns a path in your writable session home. To send a file, call beeline-agent post_artifact with a path inside your checkout or anywhere in your writable session home (wherever a file you or your harness generated actually landed, including one you just wrote), or with html/bytes content directly; it is uploaded and attached to your reply, and title and mime default from the file when you post by path. write_scratch_file produces the file, not a picture.",
32272
+ "To run something later or repeatedly, call beeline-agent create_schedule (interval in minutes or a 5-field cron, optional maxRuns); list_schedules / update_schedule / delete_schedule manage them, including schedules other agents created for themselves.",
32273
+ `To react to a message in this Room, call beeline-agent react_to_message with its message id and one supported emoji (${MESSAGE_REACTION_EMOJIS.join(" ")}).`,
32274
+ `To react to things that HAPPEN in this Room rather than only to what is said to you, call beeline-agent subscribe_events with the kinds you want; each one then wakes you for a turn. Available kinds: ${eventKindCatalogLines().join("; ")}. Subscriptions are per Room and cover every way the event lands: joining a Room you subscribed to wakes you, and so does a person arriving in the Workspace when that arrival projects into this Room - subscribe to joined in an onboarding Room and every newcomer wakes you, exactly like a greeter. It replaces your list, so send every kind you want - list_event_subscriptions shows the current one. You do this yourself: nobody has to configure it for you. A corner merging wakes nobody in its parent Room by default. If you still have work to do after a corner merges (a hand-off, a release step, a reply that has to describe landed code), subscribe to merged in the parent Room, from a turn there, before the merge. Each merge in that Room then wakes you and names the corner and pull request: act only on the corner you are waiting for, post nothing for any other merge, and take merged back out of your list once nothing is left to wait for. Never post just to acknowledge a merge: the merge card already announces it. A grant, connector offer, posted choice or poll, or Squire approval (payment, credential, sign-in, or vouch) you asked for wakes you back with the answer on its own - there is nothing to subscribe to for it, and asking is refused.`,
32275
+ "If you need reach outside the sandbox, call beeline-agent request_grant. If you already know discrete options, call beeline-agent ask_choice (one human, optional) or open_poll (every human in this Room, required deadline). A poll is refused below two electors and above fifty, and in a DM. A plurality is a fact, never permission to deploy, delete, merge, or spend. Open-ended asks stay tagged prose. Never put Always / Once / No on a preference.",
32276
+ "To state something that happened so the Room and other agents can act on it, call beeline-agent emit_event with your own agent:<slug> kind, one sentence, and optionally the agent members to wake. Chains of events are bounded and a refused emit posts nothing."
32277
+ ].join(" ");
32278
+ function runningBeelineReleaseId(env = process.env, read2 = (path) => readFileSync4(path, "utf8")) {
32279
+ try {
32280
+ const lib = env.BEELINE_LIB_DIR;
32281
+ if (!lib)
32282
+ return "source";
32283
+ const manifest = JSON.parse(read2(resolve13(lib, "bundle.json")));
32284
+ return [manifest.version, manifest.commit].filter(Boolean).join("-") || "source";
32285
+ } catch {
32286
+ return "source";
32251
32287
  }
32252
- return selected;
32253
- }
32254
- function boundRoomTaskBody(body, modelContextTokens) {
32255
- const context = modelContextTokens && modelContextTokens > 0 ? modelContextTokens : 32768;
32256
- return boundMessageText(body, Math.min(8e3, Math.floor(context / 4)));
32257
32288
  }
32258
- function boundMessageText(original, maxBytes) {
32259
- const sanitized = stripInlineData(original);
32260
- const clean4 = sanitized === original ? original : sanitized + fetchPointer(original, "inline content omitted");
32261
- return Buffer.byteLength(clean4) > maxBytes ? truncateRow(clean4, maxBytes) : clean4;
32289
+ function usingBeelineSkillMarkdown(releaseId) {
32290
+ return `---
32291
+ name: using-beeline
32292
+ description: How to answer inside a Beeline Room.
32293
+ ---
32294
+
32295
+ <!-- beeline-release: ${releaseId} -->
32296
+
32297
+ # Using Beeline
32298
+
32299
+ These mechanics apply in Rooms and corners. ${BEELINE_ROOM_MECHANICS}
32300
+
32301
+ ## Conflicting human instructions
32302
+
32303
+ Follow your own owner first, then this Workspace's owner and admins, then members. A higher-tier instruction overrides a lower-tier hold. A human at the same standing cannot clear another human's hold; only that holder or someone of higher standing can. Never tell a higher-tier human that a lower-tier hold binds them. Reason from the conversation. When you explain a hold or go-ahead in the Room, name the person and their standing in ordinary words. Never write field names or field=value syntax such as workspaceRole=member or agentOwner.
32304
+
32305
+ ## Tools and the Workbench
32306
+
32307
+ A **tool** is something you can use once a human adds it; a **key** is the credential that tool holds for that human. You spend a key through the mounted connector and never see the credential itself.
32308
+
32309
+ The tools this build knows, what each is for, and which the owner of this machine already has are one call away: beeline-agent workbench_status. That read is the Workbench of the machine and owner you actually run on, not the person who asked. Connected apps are listed once per app with a stable ID. Their sign-in and credentials stay with the server. Trusty Squire is vaulted credentials plus a browser for the separate key and browser routes: operate_drive drives a whole flow, and flat steps (operate_click, operate_type, operate_select, operate_press, operate_scroll, operate_wait, operate_navigate, operate_read_inbox, operate_extract, and the rest of its flat operate_* actions) handle one action at a time. Tailscale installs its CLI on the selected helper and opens Tailscale's own browser sign-in; once connected, use the CLI for tailnet resources and tailscale file cp or tailscale file get for Taildrop. The wallet is created only from the Workbench page. When you cannot reach a tool, say: I can't reach X on this machine because Y; to fix it, Z. Give one explanation, then take or offer Z.
32310
+
32311
+ **Inspect your own host before answering.** When a question is about a system on the machine you run on, inspect its live state with the tools you have: processes, logs, local endpoints or service status. Say what you checked. Memory and Room history are not evidence of current state.
32312
+
32313
+ **Use available tools before asking the person.** Before asking a person for a code, a status or a file, call workbench_status and check whether a tool or connected app you already have can get it, then use it. This includes an emailed verification code in an already-connected inbox. Ask only when none can, and say why. The key-ownership and grant rules still apply.
32314
+
32315
+ **Offer the tool at the moment you need it.** When the work in front of you needs a tool the person does not have, do not send them to a settings page and do not stop at naming it. Call workbench_status first - a tool they already have is used, not offered. In a Room or DM, call offer_connector with the connectorType and one short reason: a card appears there, spoken by you, addressed to the person you are answering, with one action. Connector offers are unavailable in corners; use a Room or DM for those. Your turn pauses on the card - say in prose what you are waiting for and end the turn; you are woken when it is added, and then you carry on.
32316
+
32317
+ **Research before you offer, and say so.** Never offer a tool you cannot describe. If someone asks you to install or add something you do not already know - by name, by purpose, or by what it will hold - say plainly that you are looking it up first, find out what it is and what it does with credentials, and state what you learned in your reply BEFORE the card appears. A person reading "Add X?" must already have read, in your own words, what X is. Refusing to act blind is part of being trusted with keys.
32318
+
32319
+ An offer is setup, never authority: it does not replace a grant, write permission, target-branch confirmation, or the merge gate, and it never needs a raw credential in chat. You never pair a tool yourself and never ask anyone for a key value; once a tool is added, provisioning happens inside it and receipts reach the person through the tool's own status message.
32320
+
32321
+ Keys belong to the human who provisioned them. Your owner's keys serve whoever asks; another person's keys need that person's private scoped approval before you use them for anyone else, and your owner cannot authorize them. Use the connector without exposing or sharing raw keys. The Workbench page remains the place a person manages tools and keys by hand (Settings \u2192 Workbench); you point there to MANAGE what exists, not to add what you need.
32322
+
32323
+ ## Where you run
32324
+
32325
+ Each agent runs on one owner's machine, with that owner's keys, hosts, and network. Agents in one Room can differ in what they reach. Memory is shared across all machines.
32326
+
32327
+ - Check your own capabilities with workbench_status and your tools. Never infer another agent's.
32328
+ - A failure saved from another machine is not proof here. Try it first.
32329
+ - If you cannot do X here, say "I can't do X on this machine because Y", then hand off to an agent that memory shows has done X, or ask the requester who has access.
32330
+
32331
+ ## Memory
32332
+
32333
+ You keep memory yourself. No background reviewer exists: what you do not keep current, nothing does.
32334
+
32335
+ | Store | Holds | Write with |
32336
+ | --- | --- | --- |
32337
+ | \`workspace-core\` procedure | Capabilities per machine; the Squire credential checklist | save_skill, whole text |
32338
+ | Profile facts | Facts about the person who asked | save_memory |
32339
+ | Workspace facts | Facts about anyone or anything else | save_memory |
32340
+ | Procedures | Steps for a task that repeats | save_skill |
32341
+ | Workflows | Roles and steps for a run across agents | save_workflow; run with start_workflow |
32342
+ | History | Every message in the Workspace | Nothing; read with search_history |
32343
+
32344
+ - **Look up.** At task start, call load_workspace_skill for \`workspace-core\`. Before you say "can't" or "not saved", run search_memory, list_credentials and workbench_status.
32345
+ - **Use.** Call report_memory_used for what your answer used. When a saved item fails, fix it in the same turn.
32346
+ - **Save.** Put each fact in one store only. Never save "X is impossible". Save the specific failure and the machine: "On Niglet's host, the highroll Fly token returns 403."
32347
+ - **Prune.** Rewrite \`workspace-core\` whole and keep it at most 2,200 characters. An agent-saved Workspace fact that nobody uses expires in the weekly pass. Replace a procedure or workflow by saving its slug again; remove it with delete_skill.
32348
+
32349
+ ## Connect an app
32350
+
32351
+ When a request needs an app or service, call beeline-agent \`workbench_status\` first. For a connected app via composio, use its ID with \`list_app_tools\`, then call \`execute_app_tool\` with the selected tool slug and arguments. For an app via squire-api, call its API with \`use_credential\` (service = the app key); via squire-browser, use the \`operate_*\` tools; via registry-mcp, use its mounted MCP tools. The server executes against that person\u2019s account and decides permission on every call. If tool discovery reports an error, follow its cause; \`needs_connection\` means the account must be reconnected, and \`needs_permission\` means wait for the connected person's decision. Do not switch paths after denial or provider unavailability. No app token, provider key, or sign-in URL belongs in chat or a model prompt.
32352
+
32353
+ For an app that is not connected, call \`connect_app\` with its name, one short reason, and a request-specific \`continuation\`: one sentence stating what you will do right after sign-in. Do not include a link, credential, account identifier, or private data in that sentence. The server chooses the route and returns \`next\`. In a corner, call it from that corner so the sign-in card and connection wake return there. A managed OAuth app presents a sign-in card to its owner; end the turn and wait for the connection wake. If the provider reports managed OAuth unsupported, follow the server's Squire route and its instructions. A provider outage is unavailable, not permission to change routes. After the person connects, read Workbench again and use the app ID. Every use is authorized as \`app:<key>\` and recorded once.
32354
+
32355
+ ## Showing a mock
32356
+
32357
+ When a design decision needs eyes, show it instead of describing it. Build ONE self-contained HTML page and post it with beeline-agent post_artifact (mime "text/html", pass the document as html). Everything is inline: a single <style> element for all CSS and data: URLs for any image - no script, no external dependencies, no network references. The validator refuses every <script>, <link>, <iframe>, <object>, <embed>, <form>, inline event handler, and http(s) URL, so a page that reaches for the network never posts. Use the Obsidian Refined tokens: grayscale surfaces, one brass accent #d7af5f, 3px radii, IBM Plex Sans/Mono type. Lay the user stories out as frames - one bordered, labelled block per story, so each story can be judged on its own. After posting, ask for feedback here in the corner: the artifact is the thing people react to, not your prose.
32358
+
32359
+ ## Showing a photograph
32360
+
32361
+ When the mock needs a real product photo, do not draw an SVG stand-in. Call beeline-agent fetch_image with the photo's http(s) URL. The daemon downloads it (30 seconds, 25 MB) into your writable session home and returns the path, mime, and size. Read those bytes, base64-encode them, and put them in the HTML as a data: URL (\`<img src="data:image/jpeg;base64,\u2026">\`). Then post_artifact as usual. The validator still refuses every http(s) image reference \u2014 the photograph has to be inline. The artifact is a snapshot: it never fetches the network when someone opens it.
32362
+ `;
32262
32363
  }
32263
- function stripInlineData(text2) {
32264
- return text2.replace(/data:[^\s"'<>)]*/gi, "[inline data omitted]").replace(/\b[A-Za-z0-9+/]{512,}={0,2}\b/g, (candidate) => /[A-Z]/.test(candidate) && /[a-z]/.test(candidate) && /[0-9+/]/.test(candidate) ? "[inline binary data omitted]" : candidate);
32364
+ function beelineReviewSkillMarkdown(releaseId) {
32365
+ return `---
32366
+ name: beeline-review
32367
+ description: Review a corner pull request against the brief's outline, the approval quote, and the Beeline merge gate.
32368
+ ---
32369
+
32370
+ <!-- beeline-release: ${releaseId} -->
32371
+
32372
+ # Beeline pull-request review
32373
+
32374
+ Follow these steps in order. Do not skip or reorder them.
32375
+
32376
+ ## 1. Isolate the revision
32377
+
32378
+ - Run \`gh pr view N --json headRefOid,files\` and record \`headRefOid\`.
32379
+ - Run \`gh pr diff N\`.
32380
+ - Review the objective, files, and complete PR diff before making another working copy.
32381
+ - Check out that exact head only when an empirical command must read or run the tree. Never use the author's worktree.
32382
+ - When a checkout is needed, create a temporary detached worktree, install an EXIT trap that removes it with \`git worktree remove --force\` and deletes its temporary directory, and run every command there. Cleanup is mandatory on PASS, FAIL, and command error.
32383
+ - For a deterministic one-shot bootstrap use \`npm run review:exact-head -- <sha> [--typecheck <pkg> \u2026] [--] <pkg>:<glob> \u2026\`: it checks that exact head out, runs \`npm ci\`, builds the workspace package exports, runs the given typechecks and targeted tests with non-interactive PASS/FAIL summaries, and removes the worktree itself (pass \`--keep\` to keep it and print the removal command).
32384
+ - Review and test only the recorded revision. If the head moves, clean up the scratch worktree and start over.
32385
+
32386
+ ## 2. P0 - OUTLINE FULFILLED, DEMONSTRATED
32387
+
32388
+ Read the server-assigned brief and its current revision, including every required file. Quote the approval with its message ID and approver before considering the short objective. The spec's outline (user stories, acceptance criteria, non-goals, risks) is the scope, and the approval quote is the human's own words: it wins any conflict with the spec. The short objective is navigation-only text and the PR description cannot add, remove, or narrow scope. A revision with no recorded approval is reviewed against its spec alone. If a required file is unavailable, or the revision changes during review, refuse approval and describe what is missing. A requirement may become out of scope only through a new human-authorized brief revision; reviewer discretion, implementation difficulty, and silence never remove it. A passing narrow test does not prove a broader requirement. Review a deliberate human choice as written, even if it is unusual.
32389
+
32390
+ Before general correctness, produce an outline ledger in spec order: list every \`## User stories\` and \`## Acceptance criteria\` line exactly once as met, unmet, unverified, or out of scope, followed by concrete evidence (for a criterion, the named test run against the PR head) - no stories to list is a FAIL, sent back to the author to write the outline; confirm nothing under \`## Non-goals\` was done; confirm every \`## Risks\` line was addressed or explicitly accepted with a stated reason. For a frontend change, open the rendered screens and compare them against the attached mock, noting every visible deviation. Any missing, unmet, unverified story or acceptance criterion, done non-goal, unaddressed risk, or unreviewed mock mismatch blocks PASS. Out of scope is valid only when the current revision itself records the human-authorized removal.
32391
+
32392
+ Before judging the implementation, independently repeat the two judgment legs from request triage:
32393
+
32394
+ - **Work warranted:** For a bug, reproduce the reported behavior on the target branch. For another request, establish the unmet user need from the request and current product. Search current code, history, issues, and open or recently merged pull requests for work that already resolves or supersedes it. Treat title similarity only as a candidate, not proof of duplication. FAIL confirmed duplicate or obsolete work.
32395
+ - **Desirable:** Check repository-owned goals, invariants, architecture, and established product behavior. Require a concrete user benefit, the smallest coherent solution, and no unapproved scope. FAIL a confirmed conflict or an unsupported product judgment; put merely plausible concerns below as non-blocking findings.
32396
+
32397
+ - Quote the approval text, with its message ID, exactly as stored. Do not quote the short objective as product truth.
32398
+ - Derive the end-user story from the spec and the approval quote in one sentence: \`a user who does X sees Y\`.
32399
+ - Make Y happen against the built PR head: run the app or affected service and perform X.
32400
+ - If no interactive surface is reachable, run the narrowest test or script that exercises the exact user path and prints the observable Y.
32401
+ - Record the command and the observed Y.
32402
+ - A unit test of an inner function, a log line, \`the code looks right\`, or any other proxy does not count.
32403
+ - If the user-visible Y cannot be produced, FAIL now. Nothing below can rescue the review.
32404
+ - For a bug, if a \`Reproduction <id>\` was recorded, quote it, re-run that exact user path on the PR head, and record that the wrong result is gone. FAIL if that proof does not name the identifier, even when other tests pass. If none was obtained, require the proof to say so plainly and show the regression instead; do not fail the review for a missing identifier.
32405
+ - State whether the diff meets every user story and the approval quote without unapproved scope.
32406
+
32407
+ ## 3. Empirical pass second
32408
+
32409
+ Check the brief revision and code head for base synchronization, tests, documentation, lint and types, publication, and CI. A required check that was skipped, failed, or not verified blocks PASS. An empty rollup alone is not proof of passing checks. Use pr_checks_status for the exact current head: checks="passed" with checkCount=0 is the server-confirmed no-checks path and satisfies the CI prerequisite for review. Do not refuse review because no literal green check exists, and do not require mergeAllowed=true before reviewing: the missing reviewer PASS itself keeps that gate closed. Pending, failed, or unknown checks still block approval. Reuse valid author evidence; run targeted independent checks where needed. A screenshot or rendered app is needed for visual claims, and server-boundary behavior for authorization claims.
32410
+
32411
+ - Run the repository typecheck and tests touched by the diff.
32412
+ - If the spec or the approval quote names a user path, exercise that path.
32413
+ - Record every command and exit code.
32414
+ - A review with no executed command is invalid and must FAIL.
32415
+
32416
+ ### Design rules
32417
+
32418
+ When the diff touches UI and the repository has a DESIGN.md, check every changed style against its rules: colours from theme tokens only, the type roles, the spacing scale, the allowed radii, elevation, the shared components, and touch targets.
32419
+
32420
+ - Run the repository's design lints. In Beeline, from \`apps/mobile\`: \`npx vitest run sources/buzz/calm-lint sources/buzz/design-lint\`.
32421
+ - FAIL when a design baseline count grows, or when an allowlist entry is added without naming the design decision behind it.
32422
+ - Require a screenshot or rendered proof of every visual change in each theme the app ships (in Beeline, Obsidian and Bone).
32423
+ - A design-rule violation is an engineering finding and blocks PASS.
32424
+
32425
+ ## 4. Adversarial pass
32426
+
32427
+ - For every changed function, name one concrete input or sequence that breaks it.
32428
+ - If none is found, write \`none found\` for that function.
32429
+
32430
+ ## 5. Verify before reporting
32431
+
32432
+ - Confirm every finding by reading the exact line or by running a command.
32433
+ - Put unconfirmed concerns under plausible findings. They never block.
32434
+
32435
+ ## 6. Bloat guard
32436
+
32437
+ - Compare net lines with the spec, its user stories and non-goals, and the approval quote.
32438
+ - FAIL backwards-compatibility shims, dual paths, feature flags, or abstractions with one caller.
32439
+ - FAIL machinery the spec and the approval quote did not ask for.
32440
+
32441
+ ## 7. Security and data
32442
+
32443
+ - Check credentials, authorization boundaries, and destructive migrations.
32444
+
32445
+ ## 8. Gate and verdict
32446
+
32447
+ - Review the exact green head named in your reviewer instruction; a server-confirmed zero-check passed head counts as green. If the head moved, do not approve it.
32448
+ - Re-read the assigned brief revision before the verdict. A repair changes the head and invalidates affected evidence; a requirement correction invalidates the relevant verdict even if code did not change. Keep product-completeness findings (a missing or contradicted user story, done non-goal, unaddressed risk, mock mismatch, or approval quote) separate from engineering findings (correctness, security, maintainability, tests). Give every confirmed finding a stable ID that survives rereview, the affected outline line or \`engineering\`, location, severity, evidence, and repair disposition. Reuse the same ID until that finding is resolved. Mechanical repairs return to the author; ask a human only for a genuinely unresolved product choice. Do not run a nested validation pipeline or push the author's branch.
32449
+ - Always use this exact verdict shape:
32450
+
32451
+ \`approval quoted with its message ID:\`
32452
+ \`user story:\`
32453
+ \`work warranted evidence:\`
32454
+ \`desirability evidence:\`
32455
+ \`reproduction id (or none obtained):\`
32456
+ \`proof of that reproduction (or none obtained + regression):\`
32457
+ \`how Y was demonstrated (or FAIL):\`
32458
+ \`commands run + results:\`
32459
+ \`brief revision:\`
32460
+ \`outline ledger (every user story, acceptance criterion, non-goal and risk + status + evidence; mock-vs-screen notes for frontend work):\`
32461
+ \`product-completeness findings (block):\`
32462
+ \`engineering findings (block):\`
32463
+ \`plausible findings (do not block):\`
32464
+ \`net lines:\`
32465
+ \`decision: PASS|FAIL\`
32466
+
32467
+ Then take exactly one action:
32468
+
32469
+ - FAIL: reply \`@author\` with the confirmed findings and the reviewed head SHA to fix.
32470
+ - PASS: call \`approve_merge\` with the reviewed head SHA, then reply \`approved <reviewed sha>\` without tagging the author. Do not tell the author to merge.
32471
+ - A human's question or proposal in the corner discussion with no answer is a blocking finding: FAIL and name it.
32472
+ - Approving is your last step as reviewer. Your PASS is the yes that wakes the author, who merges that exact head with \`merge_corner\` once checks are green and no hold stands. You never merge it yourself.
32473
+ `;
32265
32474
  }
32266
- function truncateRow(row, maxBytes) {
32267
- const pointer = fetchPointer(row, "message truncated");
32268
- const contentBytes = Math.max(0, maxBytes - Buffer.byteLength(pointer));
32269
- let prefix = Buffer.from(row).subarray(0, contentBytes).toString("utf8");
32270
- prefix = prefix.replace(/\uFFFD$/, "");
32271
- return prefix + pointer;
32475
+ function beelineSpecSkillMarkdown(releaseId) {
32476
+ return `---
32477
+ name: beeline-spec
32478
+ description: Prepare or revise a durable corner assignment from Room decisions before repository work. Do not use for ordinary conversation or unrelated artifact delivery.
32479
+ ---
32480
+
32481
+ <!-- beeline-release: ${releaseId} -->
32482
+
32483
+ # Durable corner brief
32484
+
32485
+ Create an assignment a fresh session can execute without the parent transcript. A brief has three parts:
32486
+
32487
+ - \`spec\`: one Markdown doc you write, with these headings:
32488
+ - \`## Intent\`: the human's own words, quoted exactly, each with its Room message ID. Never substitute a summary.
32489
+ - \`## User stories\`: what this enables, one per line as "a person who does X sees Y". Only stories traceable to the quoted Intent; never import a plan, a teammate's suggestion, or your own idea as a story. Keep unaffected lines as they are across revisions.
32490
+ - \`## Acceptance criteria\`: the requester's own rules from \`## Intent\`, quoted verbatim and numbered, each naming the test (existing or to be written) that fails if the rule is broken anywhere. Criteria are the invariant; stories are examples of it. Write "None beyond the stories." when the request states no rule.
32491
+ - \`## Non-goals\`: explicit exclusions.
32492
+ - \`## Risks\`: what could go wrong, and for whom.
32493
+ - \`## References\`: each reference or mock with what it is and whose call it is; the mock posted here (DESIGN.md, else existing app styles) is the reviewer's acceptance reference, not any other optional or agent-generated mock.
32494
+ Add implementation guidance under further headings when it helps.
32495
+ - files (\`attachments\`): the Room files the worker needs, each with its purpose and whether it is required.
32496
+ - \`approval\`: one human Room message ID. The server quotes that message's exact text and author; the quote wins any conflict with the spec.
32497
+
32498
+ Do not infer approval from silence, assent to different prose, or the mere existence of a plan. Keep agent recommendations labelled as recommendations. Do not promote a plausible UX detail, algorithm, failure message, or extra test into a user story merely because it sounds helpful.
32499
+
32500
+ ## Compact path for a settled small fix
32501
+
32502
+ Use this when the requested behavior, touched surface, exclusions, and proof are already obvious. Inspect the narrow current behavior, write a short spec (quoted intent, observable user stories, non-goals, risks, references), and use the initiating command as the approval. Always compose the full outline and post it to the Room before calling open_corner, even when the initiating command already settles scope and stands as the approval: posting is not optional merely because approval is inferred, since the human must be able to see what you are about to build. \u201CChange the message action label from Remove to Delete; leave confirmation and accessibility wording unchanged\u201D needs the visible-label user story and both exclusions, not another confirmation question.
32503
+
32504
+ ## Complex-work planning loop
32505
+
32506
+ Use this default-on loop when work crosses components, changes architecture or authorization, needs a visual choice, has multiple user stories, or still contains a material unknown.
32507
+
32508
+ ### 1. Scope and current state
32509
+
32510
+ - Collect every relevant human message, including corrections; later corrections supersede only what they explicitly change. Quote their exact text and IDs under \`## Intent\`.
32511
+ - Inspect current code, data contracts, tests, recent related work, repository invariants, and existing behavior. State what exists, what is missing, and what is deliberately out of scope.
32512
+ - Separate facts, human decisions, agent recommendations, and unresolved choices.
32513
+
32514
+ ### 2. User stories and product boundary
32515
+
32516
+ - Write the actor/action/observable-result stories.
32517
+ - Map each story to its own observable line and explicit non-goals.
32518
+ - If one unresolved choice would materially change behavior, scope, irreversible effects, or the intended result, present only that choice or the affected brief slice to the human. Use the answering message as the \`approval\`. Never treat no answer as approval.
32519
+
32520
+ ### 3. Architecture and data flow
32521
+
32522
+ - Trace inputs, authorities, state transitions, persistence, outputs, and trust boundaries.
32523
+ - Name the existing components to reuse and the smallest coherent changes.
32524
+ - Describe migration and compatibility behavior only where the human intent or existing data requires it.
32525
+
32526
+ ### 4. Failure modes and test map
32527
+
32528
+ - Enumerate missing/stale data, retries, concurrency, partial failure, authorization failure, rollback, and boundary-specific hazards that apply.
32529
+ - Map every user story to its proof boundary: unit, integration, device/browser, server authorization, migration, or live harness. No story may be left without planned evidence.
32530
+
32531
+ ### 5. Mocks and references
32532
+
32533
+ - When a visual or interaction decision needs eyes, create and post one self-contained mock using the using-beeline mock procedure.
32534
+ - A mock/file posted with \`post_artifact\` in this planning turn is added automatically to the brief attachment manifest; do not manually copy its object ID. Still list it under \`## References\` with whose call it is. Existing human-posted files must be named explicitly in attachments.
32535
+
32536
+ ### 6. Implementation tasks
32537
+
32538
+ - Synthesize ordered, independently verifiable tasks. For each task name the affected boundary, user stories, expected files/components, failure handling, and proof.
32539
+ - Keep the spec actionable, not a transcript summary.
32540
+
32541
+ ### 7. Bounded adversarial second read (default on)
32542
+
32543
+ - Spend one pass, bounded to the drafted scope, trying to disprove completeness: find a missing story, authority mismatch, unhandled failure, user story without proof, mock ambiguity, accidental scope expansion, or task-order hazard.
32544
+ - Return findings to the planning agent and repair the draft. Escalate only a genuinely material product choice to the human; engineering choices and mechanical gaps are resolved in the plan.
32545
+ - End after one adversarial pass unless its repair exposes one new material product choice. Do not recursively review the review.
32546
+
32547
+ ## Dispatch and revision
32548
+
32549
+ Existing authorization permits dispatch when the initiating command already settles the exact material scope; that command is the approval, but the outline is still posted to the Room first so the human can see it before open_corner runs. Drafting a brief does not create approval. When the scope was not already explicit, post the spec and ask the corner's opener to approve it, then use their reply as the \`approval\`. Honor an explicit request to review first.
32550
+
32551
+ Pass the brief as \`open_corner.brief\`. Read the current revision before revising and use \`revise_corner_brief\` with the complete replacement spec, the approving message, and a concise change description. Preserve unaffected intent quotes, user stories, and references. The latest revision governs implementation and review; chat prose alone never changes scope.
32552
+ `;
32272
32553
  }
32273
- function fetchPointer(row, reason) {
32274
- const id = /^\[message id: ([^\]\n]+)\]/.exec(row)?.[1];
32275
- return id ? `
32276
- [${reason}; call get_room_message with messageId "${id}" and offset 0 for the full text]` : `
32277
- [${reason}; read the source conversation for the full text]`;
32554
+ function beelineTriageSkillMarkdown(releaseId) {
32555
+ return `---
32556
+ name: beeline-triage
32557
+ description: Clarify and assess a user request before opening a Beeline corner, then bind a bugfix to its recorded reproduction. Use before open_corner and while implementing a bug in a corner.
32558
+ ---
32559
+
32560
+ <!-- beeline-release: ${releaseId} -->
32561
+
32562
+ # Beeline request triage
32563
+
32564
+ Run these checks before opening a corner.
32565
+
32566
+ ## 1. Is it clear?
32567
+
32568
+ - Rewrite the request as a concrete outcome and the user stories it enables.
32569
+ - State material exclusions (non-goals) and the risks needed to prevent unrequested work or surprise.
32570
+ - Keep the corner objective complete and within 24 words.
32571
+ - First ask whether the request has any material ambiguity - one that would change scope, behavior, or risk - and resolve it with the human before doing anything else; never resolve it by adopting a teammate's plan, a prior document, or your own assumption.
32572
+ - Once it's resolved, say plainly if the request itself is suboptimal and give your own recommendation - you are expected to disagree, not just permitted to.
32573
+ - On a judgment call you're still not sure of, tag a Room colleague for an adversarial second opinion and ask them to reply tagging you back; skip this only when you are the only agent in the Room.
32574
+
32575
+ ## 2. Is work warranted?
32576
+
32577
+ - For a bug, try to reproduce the exact user-visible behavior and record the evidence.
32578
+ - For another request, identify the unmet user need and the current behavior that does not meet it.
32579
+ - Search current code, history, issues, and open or recently merged pull requests for released or unreleased work that resolves or supersedes the request.
32580
+ - Treat similar titles as candidates. Confirm behavior and scope before calling work duplicate.
32581
+ - If the need cannot be established, reproduction fails, or other work may obviate it, warn; do not block.
32582
+
32583
+ ## 3. Is it desirable?
32584
+
32585
+ - Compare the request with repository-owned goals, invariants, architecture, and established product behavior.
32586
+ - Look for concrete user benefit, the smallest coherent solution, and ongoing maintenance cost.
32587
+ - Do not invent product strategy. If the evidence is absent or conflicting, warn; do not block.
32588
+
32589
+ ## Output
32590
+
32591
+ When dispatching work, pass the complete brief to open_corner under existing authorization, after posting the outline as beeline-spec requires. Add applicable warnings to the brief:
32592
+
32593
+ \`Triage warning \u2014 warranted: <evidence-backed reason>\`
32594
+ \`Triage warning \u2014 desirable: <evidence-backed reason>\`
32595
+
32596
+ When a bug reproduction succeeds, also emit:
32597
+
32598
+ \`Reproduction <id>: <user path> \u2192 <observable wrong result>\`
32599
+
32600
+ Do not emit a warning merely because evidence is incomplete when the repository offers no practical way to obtain it. Never describe a warning as approval or rejection. Warnings inform the user and implementer; they do not block work.
32601
+
32602
+ ## Bugfix execution
32603
+
32604
+ Once a corner is open for a reported bug, follow these steps in order. They are instruction, not a server gate. They do not condition the fix on reproduction. Warranted-work and desirability warnings still do not block.
32605
+
32606
+ ### 1. Attempt to reproduce
32607
+
32608
+ - Attempt to reproduce the bug as triage isolated it, using every tool the host offers: emulator, Playwright, browser, test runner.
32609
+ - Record what was tried and what was observed.
32610
+ - If a reproduction is obtained, record the exact user path and the observable wrong result under \`Reproduction <id>\`. Reuse the identifier triage emitted when it recorded one.
32611
+ - If reproduction fails, warn and continue exactly as triage already does. Never stop. Never condition the fix on reproduction.
32612
+
32613
+ ### 2. Narrow fix
32614
+
32615
+ - Narrow the fix to the reported behavior.
32616
+ - When a reproduction exists, change only what removes that recorded reproduction.
32617
+ - Nearby improvements stay out of scope.
32618
+
32619
+ ### 3. Proof matching triage
32620
+
32621
+ - When a reproduction exists, re-run it, cite the same identifier, and show it now passing, plus the regression that would fail if the bug returned.
32622
+ - When none was obtained, state that plainly and show the regression instead.
32623
+ - Name the identifier in the pull request when one exists so the reviewer can check that proof, not merely that some test exists.
32624
+ `;
32278
32625
  }
32279
32626
 
32280
- // apps/body/dist/prompt-assembly.js
32281
- function renderReplyContext(message) {
32282
- if (!message.replyToMessageId)
32283
- return "";
32284
- const author = message.replyToAuthorName ?? message.replyToAuthorId ?? "unknown author";
32285
- return [
32286
- `Reply to message ${message.replyToMessageId} by ${JSON.stringify(author)}:`,
32287
- ...message.replyToExcerpt !== void 0 ? [`> ${JSON.stringify(boundReplyExcerpt(message.replyToExcerpt))}`] : []
32288
- ].join("\n");
32627
+ // apps/body/dist/external-mcp-capabilities.js
32628
+ var SQUIRE_MCP_VERSION = "latest";
32629
+ var SQUIRE_MCP_PACKAGE = `@trusty-squire/mcp@${SQUIRE_MCP_VERSION}`;
32630
+ function isTrustySquireMcpLaunch(command, args = []) {
32631
+ return [command, ...args].some((value) => {
32632
+ const normalized = value.trim().replace(/\\/g, "/").toLowerCase();
32633
+ return /(^|[\s/="'\[,])@trusty-squire\/mcp(?:@[^\s/"'\],}]+)?(?=$|[\s/"'\],}])/i.test(normalized) || /(^|\/)trusty-squire(?:-mcp)?(?:\.[a-z0-9]+)?$/i.test(normalized);
32634
+ });
32289
32635
  }
32290
- var PROMPT_SURFACES = [
32291
- "room",
32292
- "dm",
32293
- "code-corner",
32294
- "review-corner",
32295
- "no-code-corner"
32296
- ];
32297
- var CORNERS = [
32298
- "code-corner",
32299
- "review-corner",
32300
- "no-code-corner"
32636
+ var SQUIRE_GOVERNED_TOOLS = [
32637
+ "use_credential",
32638
+ "grant_app_access",
32639
+ "revoke_app_access"
32301
32640
  ];
32302
- var EVERYWHERE = PROMPT_SURFACES;
32303
- var VALIDATION_STAGE_OWNERSHIP = `Validation stages (merge effect: none for every stage):
32304
- Stage | Recorder | Server checks
32305
- --- | --- | ---
32306
- intent, base, tests, docs, lint_types, publication, final_authorization | implementer | current revision/head
32307
- review | configured reviewer records the review stage | current revision/head and reviewer identity
32308
- ci | implementer | current revision/head; passed requires passing checks
32309
- The server merge gate is the authority: pr_checks_status reports mergeAllowed true for the current head; the server then merges that head. final_authorization grants nothing; do not wait for mergeAllowed.`;
32310
- function cornerHasRepositoryWork(brief) {
32311
- if (!brief)
32312
- return true;
32313
- if (brief.repositoryWork !== void 0)
32314
- return brief.repositoryWork;
32315
- const nonGoalsSections = [
32316
- ...brief.spec.matchAll(/^## (?:Non-goals|Not doing)\s*\n([\s\S]*?)(?=^## |$(?![\s\S]))/gim)
32317
- ].map((match) => match[1] ?? "");
32318
- const prohibition = /^(?:repository edits?|repository changes?|pull requests?|(?:edit|modify|change) (?:the )?(?:repository|repo|code)|push(?:es)?|commit(?:s)?)[.!;]*$/i;
32319
- for (const nonGoals of nonGoalsSections) {
32320
- for (const line of nonGoals.split("\n")) {
32321
- const bullet = /^\s*[-*]\s+/.test(line);
32322
- const clause = line.trim().replace(/^[-*]\s+/, "");
32323
- const negative = /^(?:no|never|do not|forbid(?:s|den)?)\s+/i;
32324
- if (!bullet && !negative.test(clause))
32325
- continue;
32326
- const items = clause.replace(negative, "").split(/,\s*|\s+(?:and|or)\s+/i);
32327
- if (items.some((item) => prohibition.test(item.trim())))
32328
- return false;
32329
- }
32641
+ var SQUIRE_GOVERNED_TOOL_SET = new Set(SQUIRE_GOVERNED_TOOLS);
32642
+
32643
+ // apps/body/dist/host-mcp-route.js
32644
+ import { join as join11, resolve as resolve14 } from "node:path";
32645
+ var import_yaml = __toESM(require_dist(), 1);
32646
+ init_squire_host();
32647
+ function grantedMcpServerNames(grants) {
32648
+ const names = /* @__PURE__ */ new Set();
32649
+ for (const grant of grants ?? []) {
32650
+ if (grant.kind !== "mcp")
32651
+ continue;
32652
+ const name = grant.target.trim();
32653
+ if (name)
32654
+ names.add(name);
32330
32655
  }
32331
- const assigned = brief.spec.match(/^## Assigned files\s*\n([\s\S]*?)(?=^## |$(?![\s\S]))/mi)?.[1];
32332
- if (assigned !== void 0 && (!assigned.trim() || /^(?:\s*[-*]?\s*)(?:\(?none\)?|no assigned files)\b/i.test(assigned)))
32333
- return false;
32334
- return true;
32335
- }
32336
- var CORNER_RUNTIME_AUTHOR_CONTRACT = `The current brief's spec \u2014 stories, criteria, non-goals, risks \u2014 defines scope and done. The human approval quote wins any conflict. The short objective is navigation-only, never scope.
32337
- Meet every story; respect every non-goal and risk.
32338
- Before any code or build action, the brief must hold this ask's outline; opening and prompting inside it is not that outline. When it has none or the ask changed, read the revision and write it with revise_corner_brief first; chat never revises it.
32339
- Follow the beeline-triage skill's bugfix execution contract when the spec or its approval quote reports a defect.
32340
- Reproduce as triage isolated it with available emulator, Playwright, browser and test runner. Record attempts and observations. If a reproduction is obtained, record it under Reproduction <id>, reusing triage's identifier when it recorded one. If reproduction fails, warn and continue; never stop and never condition the fix on reproduction.
32341
- Fix only the spec and approval quote. With a reproduction, change only what removes it and meets its user stories.
32342
- Deliver files with post_artifact; answer plain chat questions directly.
32343
- No unrequested features, flags, compatibility shims or refactors.`;
32344
- var CORNER_AUTHOR_CONTRACT = `The current brief's spec \u2014 stories, criteria, non-goals, risks \u2014 defines scope and done. The human approval quote wins any conflict. The short objective is navigation-only, never scope.
32345
- Meet every story; respect every non-goal and risk. Use its file manifest. Use record_validation_stage for this revision and head. Do not call a missing stage passed.
32346
- ${VALIDATION_STAGE_OWNERSHIP}
32347
- Before any code or build action, the brief must hold this ask's outline; opening and prompting inside it is not that outline. When it has none or the ask changed, read the revision and write it with revise_corner_brief first; chat never revises it.
32348
- Follow the beeline-triage skill's bugfix execution contract when the spec or its approval quote reports a defect.
32349
- Reproduce as triage isolated it with available emulator, Playwright, browser and test runner. Record attempts and observations. If a reproduction is obtained, record it under Reproduction <id>, reusing triage's identifier when it recorded one. If reproduction fails, warn and continue; never stop and never condition the fix on reproduction.
32350
- Fix only the spec and approval quote. With a reproduction, change only what removes it and meets its user stories.
32351
- Only when the brief calls for repository changes, follow the PR procedure below. Otherwise do not commit, push or open a PR; deliver with post_artifact.
32352
- Before the PR, demonstrate Y: run the built app or affected service and perform X. If no interactive surface is reachable, run the narrowest test or script exercising the exact user path and printing Y. An inner-function unit test, log line or code read is not a demonstration.
32353
- The PR body MUST have ## Reproduced and ## Demonstrated. Cite Reproduction <id> in both, with steps, the wrong result and the now-passing result. In ## Demonstrated, when none was obtained, state that plainly ("not obtained") and show the regression; for feature work say "not a defect report".
32354
- No unrequested features, flags, compatibility shims or refactors.`;
32355
- var CORNER_REVIEWER_SESSION_INSTRUCTION = "You are this Room's configured reviewer. The active turn prompt names the latest stable green PR head. Review and approve only that exact head; if no stable green head is named, end the turn without a verdict. Never merge yourself.";
32356
- var CORNER_REVIEWER_UNSTABLE_HEAD_INSTRUCTION = "There is no stable green PR head for this active reviewer turn. Do not review or call approve_merge. End this turn without a verdict; the next green transition will wake you.";
32357
- var CORNER_DELIVERY_NUDGE = "Only when the brief calls for repository changes, inspect the repository state and finish delivering the work unless a human hold stands: commit and push the intended changes and open the pull request if one does not exist. Decide yourself whether any remaining dirty work belongs to the brief; do not discard it merely to make the worktree clean. The pull request body must carry ## Reproduced and ## Demonstrated; if they are missing, add them before ending the turn. Otherwise do not commit, push or open a PR; deliver with post_artifact.";
32358
- var CORNER_YOLO_MERGE_NUDGE = 'Yolo is on. Call pr_checks_status now. If checks="failed", fix the failure and push. If checks="unknown", reply in this corner with the tool reason and stop instead of retrying. Otherwise stop: the server merges this pull request itself once mergeAllowed=true. Never merge it yourself.';
32359
- var UPGRADE_INTENT_RULE = "Call this on your own judgment when the work in this corner needs the repository; nobody has to ask. Call it while answering a human message in this corner, then write the brief in the restarted code session.";
32360
- var CORNER_PLACEHOLDER_BRIEF_RULE = "This brief is the placeholder the upgrade wrote from the request. Read the code, write the real spec with revise_corner_brief, then post it in this corner and ask the corner's opener to approve it before editing. This is best effort: if the opener is away or the request was already explicit, use your judgment and proceed. When the opener replies, record their reply as the approval in the next revision.";
32361
- function isPlaceholderCornerBrief(brief) {
32362
- return brief.revision === 1 && brief.authorId === SYSTEM_IDENTITY_ID;
32656
+ return [...names];
32363
32657
  }
32364
- var handle = (value) => value.replace(/^@/, "");
32365
- function cornerMergeInstruction(yoloMode, reviewerHandle) {
32366
- if (!reviewerHandle)
32367
- return "Once the pull request exists, reply with its full URL and end the turn; do not check or wait for CI. This Room has no configured reviewer, so the merge gate never opens for you: never merge; a person merges it.";
32368
- const reviewer = `the reviewer (${handle(reviewerHandle)})`;
32369
- return yoloMode ? `Once the pull request exists, reply with its full URL and end the turn; do not tag ${reviewer}, check, or wait for CI. Green checks wake ${reviewer}; after its PASS the server merges the pull request itself. Never merge it yourself. You are woken only to fix failing checks, requested changes, or a merge GitHub refused; push the fix, and the new head needs green checks and a fresh PASS.` : `Once the pull request exists, reply with its full URL and end the turn; do not tag ${reviewer}, check, or wait for CI. Yolo is off, so the merge gate stays closed for you: never merge; a person merges it after ${reviewer} approves.`;
32658
+ function ungatedHostServers(hostServers, granted) {
32659
+ const allowed = new Set(granted);
32660
+ return hostServers.filter((name) => !allowed.has(name));
32370
32661
  }
32371
- function cornerReviewerInstruction(input) {
32372
- if (!input.isReviewer || input.openedByAgent)
32373
- return void 0;
32374
- const author = input.authorHandle ? handle(input.authorHandle) : "author";
32375
- const number = input.pullRequestNumber ?? "N";
32376
- const headSha = input.headSha ?? "<head sha>";
32377
- return `Checks are green on PR #${number} at ${headSha}${input.briefRevision ? ` with assigned brief revision ${input.briefRevision}` : ""}. Review it now with the beeline-review skill against that exact head and current assigned brief. FAIL: call record_validation_stage with stage "review", status "failed", and headSha ${headSha}, then reply \`@${author}\` with the confirmed findings and the reviewed head ${headSha} to fix. PASS: call the approve_merge tool for ${headSha}${input.briefRevision ? ` with briefRevision=${input.briefRevision}` : ""}, then reply \`approved ${headSha}\` without tagging ${author}: the server merges it, and a tag would wake the author for nothing. Never merge yourself or tell the author to merge. Never say you are holding or waiting for checks.`;
32378
- }
32379
- function cornerSelfReviewerInstruction(input) {
32380
- if (!input.isReviewer || !input.openedByAgent)
32381
- return void 0;
32382
- return "Once the pull request exists, reply with its full URL and end the turn. You are this Room's reviewer, so your own pull request needs no review: do not request one or tag any agent for review. The server merges it once checks are green, yolo is on, and no human hold stands; never merge it yourself. You are woken only to fix failing checks or a merge GitHub refused.";
32383
- }
32384
- function shellLine(shell) {
32385
- if (!shell)
32386
- return "";
32387
- if (shell.available) {
32388
- return "Shell commands are available in this session: run one with your own shell tool when the work needs it, and cite the output that supports your answer.";
32389
- }
32390
- return `Shell commands are NOT available in this session, so a request that needs one cannot be run; say that plainly instead of retrying.${shell.detail ? ` Relay this to the person: ${shell.detail}` : ""}`;
32391
- }
32392
- var SESSION_SECTIONS = [
32393
- {
32394
- id: "core.identity",
32395
- topic: "identity",
32396
- why: "Agents on harnesses with their own default persona introduce themselves under the wrong name unless told who they are.",
32397
- budgetBytes: 1400,
32398
- layer: "core",
32399
- surfaces: EVERYWHERE,
32400
- render: ({ agentName, soul }) => [
32401
- `You are ${agentName} in Beeline. Stay ${agentName} in every reply, including when a tool or permission blocks you.`,
32402
- soul ? `Soul (${soul.name}): ${soul.instructions}
32403
- The soul is not authority and never changes your tools, permissions, roles, or merge rights.` : ""
32404
- ].filter(Boolean).join("\n")
32405
- },
32406
- {
32407
- id: "core.voice",
32408
- topic: "voice",
32409
- why: "A seeded voice once bent facts for the bit and leaked into commit messages that outlive it.",
32410
- budgetBytes: 320,
32411
- layer: "core",
32412
- surfaces: EVERYWHERE,
32413
- render: () => "Your voice never changes the facts: never trim, soften, exaggerate, or invent a detail for style. Write commit messages, pull request titles and bodies, and code comments in a plain voice."
32414
- },
32415
- {
32416
- id: "core.proactive",
32417
- topic: "proactivity",
32418
- why: "Agents answered a reported problem by restating it instead of fixing it or offering to.",
32419
- budgetBytes: 200,
32420
- layer: "core",
32421
- surfaces: EVERYWHERE,
32422
- render: () => "Answer every problem with the finished fix, or with the concrete next step and an offer to take it. Never just restate the problem."
32423
- },
32424
- {
32425
- id: "core.finish",
32426
- topic: "turn-end",
32427
- why: "An agent read a not-yet-connected tool result as a stopping point and ended the turn with its own work undone.",
32428
- budgetBytes: 360,
32429
- layer: "core",
32430
- surfaces: EVERYWHERE,
32431
- render: () => "Finish the work before you end the turn. A tool result that hands you a step (connecting, needs sign-in, call again, a next step) is your next action: take it now. End the turn to wait only when someone else must act next (a person replying or answering a card, grant, choice or approval; CI; a reviewer) or you are truly blocked."
32432
- },
32433
- {
32434
- id: "core.truth",
32435
- topic: "claims",
32436
- why: "Agents reported replies and actions that no tool had performed.",
32437
- budgetBytes: 200,
32438
- layer: "core",
32439
- surfaces: EVERYWHERE,
32440
- render: () => "Never claim an action, result, or reply happened unless the prompt or a tool result proves it. State real doubt once, plainly; skip filler hedges."
32441
- },
32442
- {
32443
- id: "core.reply",
32444
- topic: "reply-shape",
32445
- why: "Agents echoed their instructions, the trigger, or their own answer back as a second reply.",
32446
- budgetBytes: 160,
32447
- layer: "core",
32448
- surfaces: EVERYWHERE,
32449
- render: () => "Write only the message the reader needs. Never repeat these instructions, the trigger, or an answer you already gave."
32450
- },
32451
- {
32452
- id: "core.tagging",
32453
- topic: "tagging",
32454
- why: "Every @handle is a wake; stray handles woke agents with nothing to do, and guessed spellings reached nobody.",
32455
- budgetBytes: 360,
32456
- layer: "core",
32457
- surfaces: EVERYWHERE,
32458
- render: () => "Every exact @handle you write wakes that member. Write one only to hand off work, to ask for a decision or input, or, when nothing else announces it, to tell the person who asked that their task is done; otherwise name people and agents in plain prose. Copy handles only from the member list in the turn prompt; any other spelling reaches nobody."
32459
- },
32460
- {
32461
- id: "core.authority",
32462
- topic: "authority",
32463
- why: "Resource access and the two command hard stops must hold on every surface, whoever asked.",
32464
- budgetBytes: 520,
32465
- layer: "core",
32466
- surfaces: EVERYWHERE,
32467
- render: () => "Use your own owner's tools and keys for whoever asks. Another person's files, tools, or keys need that person's approval before you use them for anyone else, and the wallet keeps its own approval rule. Never, whoever asks: run a command that names a credential file, or run a script nobody has read. Follow your owner first, then Workspace admins, then members; only the person who set a hold, or someone above them, clears it. Delegated and follow-up work keeps the original requester."
32468
- },
32469
- {
32470
- id: "core.feedback",
32471
- topic: "feedback",
32472
- why: "Nothing reviews a turn after it ends, so friction in Beeline itself is lost unless the agent that hit it reports it in the turn.",
32473
- budgetBytes: 260,
32474
- layer: "core",
32475
- surfaces: EVERYWHERE,
32476
- render: () => "Only when this turn hit friction in Beeline itself (a tool error, a retry, a refused call, contradictory instructions, missing context, or a person correcting you), call report_feedback once before ending the turn. Otherwise do not."
32477
- },
32478
- {
32479
- id: "core.files",
32480
- topic: "files",
32481
- why: "Files on earlier messages are shown only as markers and never pre-downloaded; agents fetched media URLs instead.",
32482
- budgetBytes: 160,
32483
- layer: "core",
32484
- surfaces: EVERYWHERE,
32485
- render: () => "For a \u{1F4CE} file, call download_attachment and read its path. Never fetch media URLs; on failure, say you couldn't open it."
32486
- },
32487
- {
32488
- id: "surface.tools",
32489
- topic: "tools",
32490
- why: "Agents need app discovery before use in every turn; connector offers remain in Rooms and DMs.",
32491
- budgetBytes: 240,
32492
- layer: "surface",
32493
- surfaces: EVERYWHERE,
32494
- render: ({ surface }) => surface === "room" || surface === "dm" ? "When you need a tool you do not have, call workbench_status; offer_connector the tool it lists, otherwise call connect_app for the app." : "In corners, workbench_status discovers apps; connect_app starts sign-in here. Connector offers stay in Rooms and DMs."
32495
- },
32496
- // --- Room ----------------------------------------------------------------
32497
- {
32498
- id: "room.place",
32499
- topic: "place",
32500
- why: "The agent must know it cannot write here, where the checkout is, and whether it has a shell.",
32501
- budgetBytes: 700,
32502
- layer: "surface",
32503
- surfaces: ["room"],
32504
- render: ({ repository, shell }) => [
32505
- `This is a Beeline Room. The repository filesystem is read-only here.${repository ? ` The Room is bound to ${repository.name} (branch ${repository.branch}); the read-only checkout is at the session root.` : ""} Web search is available.`,
32506
- shellLine(shell),
32507
- shell?.available === false ? "Read code with CodeGraph when available, then beeline-readonly-mcp search_text and read_file." : "If a shell command is refused, say so plainly rather than retrying, and read code with CodeGraph when available, then beeline-readonly-mcp search_text and read_file."
32508
- ].filter(Boolean).join(" ")
32509
- },
32510
- {
32511
- id: "room.corners",
32512
- topic: "repository-work",
32513
- why: "Repository work must start through the host-governed corner, with a brief as its authority.",
32514
- budgetBytes: 1100,
32515
- layer: "surface",
32516
- surfaces: ["room"],
32517
- render: () => [
32518
- "Repository changes happen only in a corner. In a Room, the finished fix for a code problem is the exact change plus an offer to open a corner for it; open one when a person asks for the change.",
32519
- "Before opening one, consult beeline-triage and beeline-spec, then call open_corner with a name of at most three words joined by hyphens (no whitespace), an objective of at most 24 words, and the brief (a spec plus the approving message). The objective only titles the work; the brief is its authority. When open_corner succeeds the server posts the corner card: do not restate it.",
32520
- "For corners you belong to, use inspect_corner for status, steer_corner to pass Room input down, and ask_corner for one answer. Never post into a corner without a Room command.",
32521
- "A corner ends when its pull request merges; since a person may merge minutes after it opens, anything still needed must already live in a different corner before that pull request opens, not after. A request covering several items is one corner per item, opened as work reaches each one, never one corner returned to for a later item."
32522
- ].join(" ")
32523
- },
32524
- {
32525
- id: "room.replying",
32526
- topic: "room-replies",
32527
- why: "Several agents share a Room; replies with nothing to act on, and echoed nudges, are noise.",
32528
- budgetBytes: 520,
32529
- layer: "surface",
32530
- surfaces: ["room"],
32531
- render: () => "If nothing is actionable for you, do not reply. If the current task is only a nudge to respond, answer the most recent unanswered human message instead of echoing the nudge. A poll plurality is a fact, never permission to deploy, delete, merge, or spend. Consult the using-beeline skill for Room mechanics (files, artifacts, schedules, reactions, events, mocks)."
32532
- },
32533
- // --- DM ------------------------------------------------------------------
32534
- {
32535
- id: "dm.place",
32536
- topic: "place",
32537
- why: "A DM has one person, no repository work, and no corners; the agent must not tag or try to open one.",
32538
- budgetBytes: 700,
32539
- layer: "surface",
32540
- surfaces: ["dm"],
32541
- render: ({ shell }) => [
32542
- "This is a private direct message with one person: everything they send is for you, so reply without tagging. It has no repository work and no corners, and the filesystem is read-only. Web search is available.",
32543
- shellLine(shell)
32544
- ].filter(Boolean).join(" ")
32545
- },
32546
- {
32547
- id: "dm.choices",
32548
- topic: "choices",
32549
- why: "A pick in a DM is a preference, never authority, and polls need an electorate.",
32550
- budgetBytes: 300,
32551
- layer: "surface",
32552
- surfaces: ["dm"],
32553
- render: () => "ask_choice offers options; a pick is a preference, never sandbox, spend, or merge authority. Consult the using-beeline skill for files, artifacts, and reactions."
32554
- },
32555
- // --- Corners -------------------------------------------------------------
32556
- {
32557
- id: "corner.steering",
32558
- topic: "corner-steering",
32559
- why: "Active corner turns can steer member siblings without borrowing parent Room authority.",
32560
- budgetBytes: 160,
32561
- layer: "surface",
32562
- surfaces: ["code-corner", "no-code-corner", "review-corner"],
32563
- render: () => "Use steer_corner during a turn to send input to a member sibling under this parent. Membership alone grants no turn."
32564
- },
32565
- {
32566
- id: "corner.handover",
32567
- topic: "corner-handover",
32568
- why: "An agent asked to hand the corner on opened a duplicate sibling corner instead of tagging the other agent.",
32569
- budgetBytes: 360,
32570
- layer: "surface",
32571
- surfaces: ["code-corner", "no-code-corner"],
32572
- render: () => "When a person asks you to hand this corner to another agent, tag in your reply only an agent that person named by handle or display name: it moves the implementer role here. A tag for any other reason does not move the role. Do not open a second corner for a hand-over."
32573
- },
32574
- {
32575
- id: "corner.worktree",
32576
- topic: "place",
32577
- why: "The agent must know its branch and target; only a code-corner author is told to push and open the pull request.",
32578
- budgetBytes: 900,
32579
- layer: "surface",
32580
- surfaces: ["code-corner", "review-corner"],
32581
- render: ({ worktree, surface, githubCli, brief }) => [
32582
- `You are in an isolated git worktree on ${worktree?.featureBranch ?? "the feature branch"}, targeting ${worktree?.targetBranch ?? "the target branch"}.`,
32583
- surface !== "code-corner" || !cornerHasRepositoryWork(brief) ? "" : githubCli === "rest" ? `Only when the brief calls for repository changes: commit and push only ${worktree?.featureBranch}; never force-push or write to ${worktree?.targetBranch}. Before pushing, run \`git ls-remote --exit-code origin ${worktree?.featureBranch}\`. Exit code 0: rebase on origin/${worktree?.featureBranch}; resolve conflicts autonomously, realigning to that remote branch and redoing the work if needed, then rerun affected tests. Exit code 2: the branch is absent; skip rebase for the first push. Any other non-zero exit is a lookup failure: stop and retry; do not skip rebase or push. Open it with \`gh pr create\` and read it with \`gh pr view\`; this host has no gh, so the Beeline launcher answers both over the GitHub REST API with the app token.` : `Only when the brief calls for repository changes: commit and push only ${worktree?.featureBranch}; never force-push or write to ${worktree?.targetBranch}. Before pushing, run \`git ls-remote --exit-code origin ${worktree?.featureBranch}\`. Exit code 0: rebase on origin/${worktree?.featureBranch}; resolve conflicts autonomously, realigning to that remote branch and redoing the work if needed, then rerun affected tests. Exit code 2: the branch is absent; skip rebase for the first push. Any other non-zero exit is a lookup failure: stop and retry; do not skip rebase or push. Open the pull request with gh.`
32584
- ].filter(Boolean).join("\n")
32585
- },
32586
- {
32587
- id: "corner.android",
32588
- topic: "android",
32589
- why: "An agent under its isolated $HOME concluded the host had no emulator, and agents on the shared adb server drove each other's emulators.",
32590
- budgetBytes: 380,
32591
- layer: "surface",
32592
- surfaces: ["code-corner"],
32593
- when: ({ android }) => Boolean(android),
32594
- render: ({ android }) => `Android: adb and emulator are on PATH; the SDK is $ANDROID_HOME, AVDs are in $ANDROID_AVD_HOME, and /dev/kvm works. Your adb server sees only your own emulator. Boot it with \`emulator -avd <name> -port ${android?.emulatorPort} -read-only -no-window -no-snapshot -gpu swiftshader_indirect\`; it reaches host services at 10.0.2.2.`
32595
- },
32596
- {
32597
- id: "corner.prepare",
32598
- topic: "corner-prepare",
32599
- why: "Only a checkout that defines the command can run it; another repository fails on its first step.",
32600
- budgetBytes: 60,
32601
- layer: "surface",
32602
- surfaces: ["code-corner"],
32603
- when: ({ brief, cornerPrepareScript }) => cornerHasRepositoryWork(brief) && cornerPrepareScript === true,
32604
- render: () => "Run npm run corner:prepare first."
32605
- },
32606
- {
32607
- id: "corner.contract",
32608
- topic: "author-contract",
32609
- why: "The reviewer enforces this contract; an author that never saw it fails review.",
32610
- budgetBytes: 2600,
32611
- layer: "surface",
32612
- surfaces: ["code-corner"],
32613
- render: ({ brief }) => cornerHasRepositoryWork(brief) ? CORNER_AUTHOR_CONTRACT : CORNER_RUNTIME_AUTHOR_CONTRACT
32614
- },
32615
- {
32616
- id: "corner.merge",
32617
- topic: "merge",
32618
- why: 'Exactly one rule may say who merges and when; two rules here once said both "merge" and "never merge".',
32619
- budgetBytes: 520,
32620
- layer: "surface",
32621
- surfaces: ["code-corner"],
32622
- when: ({ brief }) => cornerHasRepositoryWork(brief),
32623
- render: ({ yoloMode, reviewerHandle, selfReviewer }) => (selfReviewer ? cornerSelfReviewerInstruction({ isReviewer: true, openedByAgent: true }) : cornerMergeInstruction(Boolean(yoloMode), reviewerHandle)) + " Never merge any other pull request."
32624
- },
32625
- {
32626
- id: "corner.checks",
32627
- topic: "checks-turns",
32628
- why: "Server check notes were restated as replies, and agents scheduled polls of a gate that wakes them itself.",
32629
- budgetBytes: 900,
32630
- layer: "surface",
32631
- surfaces: ["code-corner"],
32632
- when: ({ brief }) => cornerHasRepositoryWork(brief),
32633
- render: () => "When asked whether the reviewer was woken, call pr_checks_status and report reviewerWake; do not invent a cause. Never schedule polls of pr_checks_status or the merge gate; the server wakes you when it changes."
32634
- },
32635
- {
32636
- id: "corner.review",
32637
- topic: "merge",
32638
- why: "The reviewer approves one exact green head and never merges.",
32639
- budgetBytes: 400,
32640
- layer: "surface",
32641
- surfaces: ["review-corner"],
32642
- render: () => `${CORNER_REVIEWER_SESSION_INSTRUCTION} Report findings to the author; never edit or push the author's branch.`
32643
- },
32644
- {
32645
- id: "corner.no-code",
32646
- topic: "place",
32647
- why: "A no-code corner has no repository; the agent delivers files, not commits.",
32648
- budgetBytes: 1e3,
32649
- layer: "surface",
32650
- surfaces: ["no-code-corner"],
32651
- render: ({ agentMayUpgradeCorner }) => [
32652
- "This is a no-code corner with no repository checkout and no GitHub workflow. Work in this corner's writable workspace: create files with write_scratch_file or your own tools, then send them with post_artifact.",
32653
- agentMayUpgradeCorner ? `Do not initialize a repository, create a branch, commit, push, open a pull request, or wait for GitHub checks. The one exception is beeline-agent upgrade_corner_to_code. ${UPGRADE_INTENT_RULE} That one-way upgrade restarts this same corner with a feature branch and writable checkout, keeps its discussion, and re-delivers that same request in the code session, so end this turn immediately once it succeeds and do not edit this workspace.` : "Do not initialize a repository, create a branch, commit, push, open a pull request, or wait for GitHub checks."
32654
- ].join("\n")
32655
- },
32656
- {
32657
- id: "corner.no-code-reply",
32658
- topic: "reply-target",
32659
- why: "Every turn was told to end with a one-line summary of files it posted, so answers were followed by a restatement of themselves.",
32660
- budgetBytes: 260,
32661
- layer: "surface",
32662
- surfaces: ["no-code-corner"],
32663
- render: ({ requesterHandle }) => `${requesterHandle ? `Tag @${handle(requesterHandle)} once, when the deliverable is posted or you need their input. ` : ""}When you posted files, name them in one line; never restate your answer. Only a human closes this corner.`
32664
- }
32665
- ];
32666
- function applicable(sections, context) {
32667
- return sections.filter((section) => section.surfaces.includes(context.surface) && (section.when?.(context) ?? true)).map((section) => ({ section, text: section.render(context).trim() })).filter((entry) => entry.text);
32668
- }
32669
- function sessionSections(context) {
32670
- return applicable(SESSION_SECTIONS, context);
32671
- }
32672
- function assembleSessionPrompt(context) {
32673
- const entries = sessionSections(context);
32674
- const systemPrompt = entries.map((entry) => entry.text).join("\n\n");
32675
- return {
32676
- systemPrompt,
32677
- turnPrefix: harnessHonorsSessionSystemPrompt(context.agentCommand) ? "" : systemPrompt,
32678
- report: entries.map(({ section, text: text2 }) => ({
32679
- id: section.id,
32680
- bytes: Buffer.byteLength(text2)
32681
- }))
32682
- };
32683
- }
32684
- function renderAssignedCornerBrief(brief) {
32685
- const approval = brief.approval ? `Approved by ${brief.approval.approverName}, message ${brief.approval.sourceMessageId}: ${brief.approval.text}` : "This revision predates recorded approvals.";
32686
- return `Brief revision ${brief.revision}:
32687
- ${brief.spec}
32688
-
32689
- ${approval}`;
32690
- }
32691
- function roomMentionDirectory(roster, selfId) {
32692
- const rows = [];
32693
- let selfHandle;
32694
- for (const member of roster.members) {
32695
- if (member.identityId === selfId) {
32696
- selfHandle = member.handle?.trim().replace(/^@/, "");
32697
- continue;
32698
- }
32699
- const memberHandle = member.handle?.trim().replace(/^@/, "");
32700
- if (!memberHandle)
32701
- continue;
32702
- const name = member.name?.trim() ?? "";
32703
- const kind = member.kind === "agent" ? "agent" : "person";
32704
- rows.push(`- @${memberHandle}${name && name !== memberHandle ? ` \u2014 ${name}` : ""} (${kind})`);
32705
- }
32706
- if (selfHandle)
32707
- rows.push(`You are @${selfHandle}, a member of this Room; you are not listed above.`);
32708
- if (!rows.length)
32709
- return "";
32710
- return ["Members (exact tag spellings):", ...rows].join("\n");
32711
- }
32712
- var ROOMS = ["room", "dm"];
32713
- var TURN_SECTIONS = [
32714
- {
32715
- id: "turn.session",
32716
- topic: "session-delivery",
32717
- why: "Codex, Pi, and Grok drop the ACP session prompt; without this they never saw the corner rules.",
32718
- budgetBytes: 16e3,
32719
- layer: "turn",
32720
- surfaces: EVERYWHERE,
32721
- render: ({ sessionPrefix }) => sessionPrefix ?? ""
32722
- },
32723
- {
32724
- id: "turn.checks",
32725
- topic: "checks-turns",
32726
- why: "Only CI or merge-gate wakes suppress ordinary reporting; user schedules must deliver their task results.",
32727
- budgetBytes: 350,
32728
- layer: "turn",
32729
- surfaces: ["code-corner"],
32730
- when: ({ task }) => task.commandReason !== "schedule" && (["corner_check", "corner_merge_refused", "corner_merge_conflict"].includes(task.commandReason ?? "") || ["check-passed", "check-failed"].includes(task.outsideEvent?.kind ?? "")),
32731
- render: () => 'This is a checks turn about CI or the merge gate. On it, say nothing unless you push a fix or report checks="unknown", and then use one short line. Never restate server check or merge notes.'
32732
- },
32733
- {
32734
- id: "turn.objective",
32735
- topic: "objective",
32736
- why: "The objective locates the work but must never be mistaken for its authority.",
32737
- budgetBytes: 600,
32738
- layer: "turn",
32739
- surfaces: CORNERS,
32740
- render: ({ objective }) => objective ? `Corner objective (navigation only, not product authority): ${objective}` : ""
32741
- },
32742
- {
32743
- id: "turn.rename",
32744
- topic: "corner-rename",
32745
- why: "A generated corner name says nothing about the work; only the server knows the name is still generated, so the ask rides only on those turns.",
32746
- budgetBytes: 600,
32747
- layer: "turn",
32748
- surfaces: CORNERS,
32749
- render: ({ generatedTitle, brief }) => generatedTitle ? `This human-opened corner still has its generated name, "${generatedTitle}". If the newest human message states the work, call rename_corner once before you reply with a name of at most three words joined by hyphens (no whitespace) and a short objective${brief ? "" : ", plus a brief grounded in that message with brief.approval.sourceMessageId set to the human message id"}. A bare mention does not state the work; leave the name until a message does.` : ""
32750
- },
32751
- {
32752
- id: "turn.brief",
32753
- topic: "brief",
32754
- why: "The brief is the product authority for corner work; a corner without one says so once.",
32755
- budgetBytes: 24e3,
32756
- layer: "turn",
32757
- surfaces: CORNERS,
32758
- render: ({ brief, surface }) => brief ? `${renderAssignedCornerBrief(brief.brief)}${cornerHasRepositoryWork(brief.brief) ? `
32759
-
32760
- Assigned files:
32761
- ${brief.fileLines.join("\n") || "(none)"}` : brief.fileLines.length ? `
32762
-
32763
- Brief attachments:
32764
- ${brief.fileLines.join("\n")}` : ""}${brief.missingRequiredFile ? "\nRequired assignment files are unavailable. Pause work that depends on them and report the missing file precisely." : ""}${surface === "code-corner" && isPlaceholderCornerBrief(brief.brief) ? `
32765
-
32766
- ${CORNER_PLACEHOLDER_BRIEF_RULE}` : ""}` : "No assigned brief: the human messages in this corner are the authority; skip steps that need a brief revision."
32767
- },
32768
- {
32769
- id: "turn.checkout",
32770
- topic: "checkout",
32771
- why: "A Room checkout is refreshed per turn; the agent must know which commit it reads.",
32772
- budgetBytes: 200,
32773
- layer: "turn",
32774
- surfaces: ROOMS,
32775
- render: ({ checkout }) => checkout ? `Repository checkout: origin/${checkout.branch} at commit ${checkout.commit}, refreshed for this turn.` : ""
32776
- },
32777
- {
32778
- id: "turn.transcript",
32779
- topic: "transcript",
32780
- why: "The conversation so far, with rows this session already saw left out.",
32781
- budgetBytes: 2e5,
32782
- layer: "turn",
32783
- surfaces: EVERYWHERE,
32784
- render: ({ transcript, surface }) => {
32785
- if (!transcript?.lines.length)
32786
- return "";
32787
- const place = ROOMS.includes(surface) ? "Room" : "corner";
32788
- return `${transcript.sinceLastTurn ? `New in the ${place} since your last turn:` : `${place === "Room" ? "Room conversation" : "Corner transcript"}:`}
32789
- ${transcript.lines.join("\n")}`;
32790
- }
32791
- },
32792
- {
32793
- id: "turn.resume",
32794
- topic: "resume",
32795
- why: "A card answer resumes a paused turn; the agent must know which question was answered.",
32796
- budgetBytes: 600,
32797
- layer: "turn",
32798
- surfaces: ROOMS,
32799
- render: ({ resume }) => resume ?? ""
32800
- },
32801
- {
32802
- id: "turn.members",
32803
- topic: "members",
32804
- why: "Handle spellings change; only the live roster is a safe source.",
32805
- budgetBytes: 4e3,
32806
- layer: "turn",
32807
- surfaces: EVERYWHERE,
32808
- render: ({ members }) => members ?? ""
32809
- },
32810
- {
32811
- id: "turn.memory",
32812
- topic: "memory",
32813
- why: "Keyword-matched institutional memory, compiled and capped by the server.",
32814
- budgetBytes: 1e3,
32815
- layer: "turn",
32816
- surfaces: EVERYWHERE,
32817
- render: ({ memory }) => memory ?? ""
32818
- },
32819
- {
32820
- id: "turn.corners",
32821
- topic: "corners",
32822
- why: "Room and corner agents need exact member corner ids for supported relay actions.",
32823
- budgetBytes: 8e3,
32824
- layer: "turn",
32825
- surfaces: ["room", "code-corner", "no-code-corner", "review-corner"],
32826
- render: ({ surface, corners, closedCorners }) => [
32827
- corners?.length ? `Current corners you belong to (use the exact cornerId with ${surface === "room" ? "inspect_corner, steer_corner, or ask_corner" : "steer_corner for a sibling under this parent Room"}):
32828
- ${JSON.stringify(corners)}` : "",
32829
- surface === "room" && closedCorners?.length ? `Corners you belong to closed in the last 24 hours (merge commit is unavailable when absent):
32830
- ${closedCorners.map((corner) => `- ${corner.name ?? corner.cornerId} (${corner.cornerId}): PR ${corner.pullRequestNumber ? `#${corner.pullRequestNumber}` : "unavailable"}, merge commit ${corner.mergeCommitSha ?? "unavailable"}`).join("\n")}` : ""
32831
- ].filter(Boolean).join("\n\n")
32832
- },
32833
- {
32834
- id: "turn.reviewer-target",
32835
- topic: "reviewer-target",
32836
- why: "The reviewer must review the head that is green now, not one named in an older message.",
32837
- budgetBytes: 1200,
32838
- layer: "turn",
32839
- surfaces: ["review-corner"],
32840
- render: ({ reviewerTarget }) => reviewerTarget ?? ""
32841
- },
32842
- {
32843
- id: "turn.task",
32844
- topic: "task",
32845
- why: "The message this turn answers, said once and last.",
32846
- budgetBytes: 64e3,
32847
- layer: "turn",
32848
- surfaces: EVERYWHERE,
32849
- render: ({ task, surface }) => ROOMS.includes(surface) ? [
32850
- `Current task from ${task.fromName ?? "a Room member"}:`,
32851
- task.cornerAskId ? `Corner ask id: ${task.cornerAskId}. Use get_corner_ask to retrieve its status and answer.` : "",
32852
- task.body,
32853
- renderReplyContext(task.reply ?? {})
32854
- ].filter(Boolean).join("\n\n") : [
32855
- ...task.reactionTargetId ? [`Reaction target message id: ${task.reactionTargetId}`] : [],
32856
- `Newest message:
32857
- ${task.body}`,
32858
- ...task.reply?.replyToMessageId ? [renderReplyContext(task.reply)] : [],
32859
- ...task.attachmentLines ?? []
32860
- ].join("\n")
32861
- }
32862
- ];
32863
- function assembleTurnPrompt(context) {
32864
- if (context.task.outsideEvent?.kind?.startsWith("webhook:")) {
32865
- context = { ...context, task: { ...context.task, body: quoteOutsideData(context.task.outsideEvent.kind, context.task.outsideEvent.payload) } };
32866
- }
32867
- const boundedContext = ROOMS.includes(context.surface) ? {
32868
- ...context,
32869
- task: {
32870
- ...context.task,
32871
- body: boundRoomTaskBody(context.task.body, context.modelContextTokens)
32872
- }
32873
- } : context;
32874
- const withoutTranscript = applicable(TURN_SECTIONS, {
32875
- ...boundedContext,
32876
- transcript: void 0
32877
- });
32878
- const otherBytes = withoutTranscript.reduce((sum, entry) => sum + Buffer.byteLength(entry.text) + 2, 0);
32879
- const lines = budgetTranscript(boundedContext.transcript?.lines ?? [], boundedContext.modelContextTokens, otherBytes);
32880
- const entries = applicable(TURN_SECTIONS, {
32881
- ...boundedContext,
32882
- transcript: boundedContext.transcript ? { ...boundedContext.transcript, lines } : void 0
32883
- });
32884
- return {
32885
- text: entries.map((entry) => entry.text).join("\n\n"),
32886
- report: entries.map(({ section, text: text2 }) => ({
32887
- id: section.id,
32888
- bytes: Buffer.byteLength(text2)
32889
- }))
32890
- };
32891
- }
32892
-
32893
- // apps/body/dist/beeline-skill.js
32894
- var USING_BEELINE_SKILL_NAME = "using-beeline";
32895
- var BEELINE_TRIAGE_SKILL_NAME = "beeline-triage";
32896
- var BEELINE_REVIEW_SKILL_NAME = "beeline-review";
32897
- var BEELINE_SPEC_SKILL_NAME = "beeline-spec";
32898
- var BEELINE_ROOM_MECHANICS = [
32899
- "Files and photos on the message you are answering are downloaded for you: read them at the local path named in the prompt (photos may also arrive inline). A file on an earlier message shows only as a \u{1F4CE} marker with an id; call beeline-agent download_attachment with that message id and attachment id, then read the returned path. Never fetch a media URL; if the download fails, tell the requester you could not open the file.",
32900
- "To create a file you can send, call beeline-agent write_scratch_file with a relative path and content - text by default, or base64 for bytes you computed; it returns a path in your writable session home. To send a file, call beeline-agent post_artifact with a path inside your checkout or anywhere in your writable session home (wherever a file you or your harness generated actually landed, including one you just wrote), or with html/bytes content directly; it is uploaded and attached to your reply, and title and mime default from the file when you post by path. write_scratch_file produces the file, not a picture.",
32901
- "To run something later or repeatedly, call beeline-agent create_schedule (interval in minutes or a 5-field cron, optional maxRuns); list_schedules / update_schedule / delete_schedule manage them, including schedules other agents created for themselves.",
32902
- `To react to a message in this Room, call beeline-agent react_to_message with its message id and one supported emoji (${MESSAGE_REACTION_EMOJIS.join(" ")}).`,
32903
- `To react to things that HAPPEN in this Room rather than only to what is said to you, call beeline-agent subscribe_events with the kinds you want; each one then wakes you for a turn. Available kinds: ${eventKindCatalogLines().join("; ")}. Subscriptions are per Room and cover every way the event lands: joining a Room you subscribed to wakes you, and so does a person arriving in the Workspace when that arrival projects into this Room - subscribe to joined in an onboarding Room and every newcomer wakes you, exactly like a greeter. It replaces your list, so send every kind you want - list_event_subscriptions shows the current one. You do this yourself: nobody has to configure it for you. A corner merging wakes nobody in its parent Room by default. If you still have work to do after a corner merges (a hand-off, a release step, a reply that has to describe landed code), subscribe to merged in the parent Room, from a turn there, before the merge. Each merge in that Room then wakes you and names the corner and pull request: act only on the corner you are waiting for, post nothing for any other merge, and take merged back out of your list once nothing is left to wait for. Never post just to acknowledge a merge: the merge card already announces it. A grant, connector offer, posted choice or poll, or Squire approval (payment, credential, sign-in, or vouch) you asked for wakes you back with the answer on its own - there is nothing to subscribe to for it, and asking is refused.`,
32904
- "If you need reach outside the sandbox, call beeline-agent request_grant. If you already know discrete options, call beeline-agent ask_choice (one human, optional) or open_poll (every human in this Room, required deadline). A poll is refused below two electors and above fifty, and in a DM. A plurality is a fact, never permission to deploy, delete, merge, or spend. Open-ended asks stay tagged prose. Never put Always / Once / No on a preference.",
32905
- "To state something that happened so the Room and other agents can act on it, call beeline-agent emit_event with your own agent:<slug> kind, one sentence, and optionally the agent members to wake. Chains of events are bounded and a refused emit posts nothing."
32906
- ].join(" ");
32907
- function runningBeelineReleaseId(env = process.env, read2 = (path) => readFileSync4(path, "utf8")) {
32908
- try {
32909
- const lib = env.BEELINE_LIB_DIR;
32910
- if (!lib)
32911
- return "source";
32912
- const manifest = JSON.parse(read2(resolve13(lib, "bundle.json")));
32913
- return [manifest.version, manifest.commit].filter(Boolean).join("-") || "source";
32914
- } catch {
32915
- return "source";
32916
- }
32917
- }
32918
- function usingBeelineSkillMarkdown(releaseId) {
32919
- return `---
32920
- name: using-beeline
32921
- description: How to answer inside a Beeline Room.
32922
- ---
32923
-
32924
- <!-- beeline-release: ${releaseId} -->
32925
-
32926
- # Using Beeline
32927
-
32928
- These mechanics apply in Rooms and corners. ${BEELINE_ROOM_MECHANICS}
32929
-
32930
- ## Conflicting human instructions
32931
-
32932
- Follow your own owner first, then this Workspace's owner and admins, then members. A higher-tier instruction overrides a lower-tier hold. A human at the same standing cannot clear another human's hold; only that holder or someone of higher standing can. Never tell a higher-tier human that a lower-tier hold binds them. Reason from the conversation. When you explain a hold or go-ahead in the Room, name the person and their standing in ordinary words. Never write field names or field=value syntax such as workspaceRole=member or agentOwner.
32933
-
32934
- ## Tools and the Workbench
32935
-
32936
- A **tool** is something you can use once a human adds it; a **key** is the credential that tool holds for that human. You spend a key through the mounted connector and never see the credential itself.
32937
-
32938
- The tools this build knows, what each is for, and which the owner of this machine already has are one call away: beeline-agent workbench_status. That read is the Workbench of the machine and owner you actually run on, not the person who asked. Connected apps are listed once per app with a stable ID. Their sign-in and credentials stay with the server. Trusty Squire is vaulted credentials plus a browser for the separate key and browser routes. Drive a multi-step browser flow such as checkout or signup with operate_drive rather than one operate_act call per step. Tailscale installs its CLI on the selected helper and opens Tailscale's own browser sign-in; once connected, use the CLI for tailnet resources and tailscale file cp or tailscale file get for Taildrop. The wallet is created only from the Workbench page. When you cannot reach a tool, say: I can't reach X on this machine because Y; to fix it, Z. Give one explanation, then take or offer Z.
32939
-
32940
- **Inspect your own host before answering.** When a question is about a system on the machine you run on, inspect its live state with the tools you have: processes, logs, local endpoints or service status. Say what you checked. Memory and Room history are not evidence of current state.
32941
-
32942
- **Use available tools before asking the person.** Before asking a person for a code, a status or a file, call workbench_status and check whether a tool or connected app you already have can get it, then use it. This includes an emailed verification code in an already-connected inbox. Ask only when none can, and say why. The key-ownership and grant rules still apply.
32943
-
32944
- **Offer the tool at the moment you need it.** When the work in front of you needs a tool the person does not have, do not send them to a settings page and do not stop at naming it. Call workbench_status first - a tool they already have is used, not offered. In a Room or DM, call offer_connector with the connectorType and one short reason: a card appears there, spoken by you, addressed to the person you are answering, with one action. Connector offers are unavailable in corners; use a Room or DM for those. Your turn pauses on the card - say in prose what you are waiting for and end the turn; you are woken when it is added, and then you carry on.
32945
-
32946
- **Research before you offer, and say so.** Never offer a tool you cannot describe. If someone asks you to install or add something you do not already know - by name, by purpose, or by what it will hold - say plainly that you are looking it up first, find out what it is and what it does with credentials, and state what you learned in your reply BEFORE the card appears. A person reading "Add X?" must already have read, in your own words, what X is. Refusing to act blind is part of being trusted with keys.
32947
-
32948
- An offer is setup, never authority: it does not replace a grant, write permission, target-branch confirmation, or the merge gate, and it never needs a raw credential in chat. You never pair a tool yourself and never ask anyone for a key value; once a tool is added, provisioning happens inside it and receipts reach the person through the tool's own status message.
32949
-
32950
- Keys belong to the human who provisioned them. Your owner's keys serve whoever asks; another person's keys need that person's private scoped approval before you use them for anyone else, and your owner cannot authorize them. Use the connector without exposing or sharing raw keys. The Workbench page remains the place a person manages tools and keys by hand (Settings \u2192 Workbench); you point there to MANAGE what exists, not to add what you need.
32951
-
32952
- ## Connect an app
32953
-
32954
- When a request needs an app or service, call beeline-agent \`workbench_status\` first. For a connected app, use its ID with \`list_app_tools\`, then call \`execute_app_tool\` with the selected tool slug and arguments. The server executes against that person\u2019s account and decides permission on every call. If tool discovery reports an error, follow its cause; \`needs_connection\` means the account must be reconnected, and \`needs_permission\` means wait for the connected person's decision. Do not switch paths after denial or provider unavailability. No app token, provider key, or sign-in URL belongs in chat or a model prompt.
32955
-
32956
- For an app that is not connected, call \`connect_app\` with its name, one short reason, and a request-specific \`continuation\`: one sentence stating what you will do right after sign-in. Do not include a link, credential, account identifier, or private data in that sentence. The server chooses the route and returns \`next\`. In a corner, call it from that corner so the sign-in card and connection wake return there. A managed OAuth app presents a sign-in card to its owner; end the turn and wait for the connection wake. If the provider reports managed OAuth unsupported, follow the server's Squire route and its instructions. A provider outage is unavailable, not permission to change routes. After the person connects, read Workbench again and use the app ID. Every use is authorized as \`app:<key>\` and recorded once.
32957
-
32958
- ## Showing a mock
32959
-
32960
- When a design decision needs eyes, show it instead of describing it. Build ONE self-contained HTML page and post it with beeline-agent post_artifact (mime "text/html", pass the document as html). Everything is inline: a single <style> element for all CSS and data: URLs for any image - no script, no external dependencies, no network references. The validator refuses every <script>, <link>, <iframe>, <object>, <embed>, <form>, inline event handler, and http(s) URL, so a page that reaches for the network never posts. Use the Obsidian Refined tokens: grayscale surfaces, one brass accent #d7af5f, 3px radii, IBM Plex Sans/Mono type. Lay the user stories out as frames - one bordered, labelled block per story, so each story can be judged on its own. After posting, ask for feedback here in the corner: the artifact is the thing people react to, not your prose.
32961
-
32962
- ## Showing a photograph
32963
-
32964
- When the mock needs a real product photo, do not draw an SVG stand-in. Call beeline-agent fetch_image with the photo's http(s) URL. The daemon downloads it (30 seconds, 25 MB) into your writable session home and returns the path, mime, and size. Read those bytes, base64-encode them, and put them in the HTML as a data: URL (\`<img src="data:image/jpeg;base64,\u2026">\`). Then post_artifact as usual. The validator still refuses every http(s) image reference \u2014 the photograph has to be inline. The artifact is a snapshot: it never fetches the network when someone opens it.
32965
- `;
32966
- }
32967
- function beelineReviewSkillMarkdown(releaseId) {
32968
- return `---
32969
- name: beeline-review
32970
- description: Review a corner pull request against the brief's outline, the approval quote, and the Beeline merge gate.
32971
- ---
32972
-
32973
- <!-- beeline-release: ${releaseId} -->
32974
-
32975
- # Beeline pull-request review
32976
-
32977
- Follow these steps in order. Do not skip or reorder them.
32978
-
32979
- ## 1. Isolate the revision
32980
-
32981
- - Run \`gh pr view N --json headRefOid,files\` and record \`headRefOid\`.
32982
- - Run \`gh pr diff N\`.
32983
- - Review the objective, files, and complete PR diff before making another working copy.
32984
- - Check out that exact head only when an empirical command must read or run the tree. Never use the author's worktree.
32985
- - When a checkout is needed, create a temporary detached worktree, install an EXIT trap that removes it with \`git worktree remove --force\` and deletes its temporary directory, and run every command there. Cleanup is mandatory on PASS, FAIL, and command error.
32986
- - For a deterministic one-shot bootstrap use \`npm run review:exact-head -- <sha> [--typecheck <pkg> \u2026] [--] <pkg>:<glob> \u2026\`: it checks that exact head out, runs \`npm ci\`, builds the workspace package exports, runs the given typechecks and targeted tests with non-interactive PASS/FAIL summaries, and removes the worktree itself (pass \`--keep\` to keep it and print the removal command).
32987
- - Review and test only the recorded revision. If the head moves, clean up the scratch worktree and start over.
32988
-
32989
- ## 2. P0 - OUTLINE FULFILLED, DEMONSTRATED
32990
-
32991
- Read the server-assigned brief and its current revision, including every required file. Quote the approval with its message ID and approver before considering the short objective. The spec's outline (user stories, acceptance criteria, non-goals, risks) is the scope, and the approval quote is the human's own words: it wins any conflict with the spec. The short objective is navigation-only text and the PR description cannot add, remove, or narrow scope. A revision with no recorded approval is reviewed against its spec alone. If a required file is unavailable, or the revision changes during review, refuse approval and describe what is missing. A requirement may become out of scope only through a new human-authorized brief revision; reviewer discretion, implementation difficulty, and silence never remove it. A passing narrow test does not prove a broader requirement. Review a deliberate human choice as written, even if it is unusual.
32992
-
32993
- Before general correctness, produce an outline ledger in spec order: list every \`## User stories\` and \`## Acceptance criteria\` line exactly once as met, unmet, unverified, or out of scope, followed by concrete evidence (for a criterion, the named test run against the PR head) - no stories to list is a FAIL, sent back to the author to write the outline; confirm nothing under \`## Non-goals\` was done; confirm every \`## Risks\` line was addressed or explicitly accepted with a stated reason. For a frontend change, open the rendered screens and compare them against the attached mock, noting every visible deviation. Any missing, unmet, unverified story or acceptance criterion, done non-goal, unaddressed risk, or unreviewed mock mismatch blocks PASS. Out of scope is valid only when the current revision itself records the human-authorized removal.
32994
-
32995
- Before judging the implementation, independently repeat the two judgment legs from request triage:
32996
-
32997
- - **Work warranted:** For a bug, reproduce the reported behavior on the target branch. For another request, establish the unmet user need from the request and current product. Search current code, history, issues, and open or recently merged pull requests for work that already resolves or supersedes it. Treat title similarity only as a candidate, not proof of duplication. FAIL confirmed duplicate or obsolete work.
32998
- - **Desirable:** Check repository-owned goals, invariants, architecture, and established product behavior. Require a concrete user benefit, the smallest coherent solution, and no unapproved scope. FAIL a confirmed conflict or an unsupported product judgment; put merely plausible concerns below as non-blocking findings.
32999
-
33000
- - Quote the approval text, with its message ID, exactly as stored. Do not quote the short objective as product truth.
33001
- - Derive the end-user story from the spec and the approval quote in one sentence: \`a user who does X sees Y\`.
33002
- - Make Y happen against the built PR head: run the app or affected service and perform X.
33003
- - If no interactive surface is reachable, run the narrowest test or script that exercises the exact user path and prints the observable Y.
33004
- - Record the command and the observed Y.
33005
- - A unit test of an inner function, a log line, \`the code looks right\`, or any other proxy does not count.
33006
- - If the user-visible Y cannot be produced, FAIL now. Nothing below can rescue the review.
33007
- - For a bug, if a \`Reproduction <id>\` was recorded, quote it, re-run that exact user path on the PR head, and record that the wrong result is gone. FAIL if that proof does not name the identifier, even when other tests pass. If none was obtained, require the proof to say so plainly and show the regression instead; do not fail the review for a missing identifier.
33008
- - State whether the diff meets every user story and the approval quote without unapproved scope.
33009
-
33010
- ## 3. Empirical pass second
33011
-
33012
- Build one visible validation record for the brief revision and code head. Assess intent, base synchronization, independent review, tests, documentation, lint and types, publication, CI, and final Beeline authorization. Each applicable stage is pending, running, passed, failed, skipped, or not applicable, with a reason for the last two. A required skipped, failed, or unverified stage blocks PASS. An empty rollup alone is not proof of passing checks. Use pr_checks_status for the exact current head: checks="passed" with checkCount=0 is the server-confirmed no-checks path and satisfies the CI prerequisite for review. Do not refuse review because no literal green check exists, and do not require mergeAllowed=true before reviewing: the missing reviewer PASS itself keeps that gate closed. Pending, failed, or unknown checks still block approval. Reuse valid author evidence; run targeted independent checks where needed. A screenshot or rendered app is needed for visual claims, and server-boundary behavior for authorization claims.
33013
- Use record_validation_stage for each assessed stage, naming the current brief revision and exact PR head.
33014
- ${VALIDATION_STAGE_OWNERSHIP}
33015
- The record informs the verdict but never replaces approve_merge or pr_checks_status.
33016
-
33017
- - Run the repository typecheck and tests touched by the diff.
33018
- - If the spec or the approval quote names a user path, exercise that path.
33019
- - Record every command and exit code.
33020
- - A review with no executed command is invalid and must FAIL.
33021
-
33022
- ### Design rules
33023
-
33024
- When the diff touches UI and the repository has a DESIGN.md, check every changed style against its rules: colours from theme tokens only, the type roles, the spacing scale, the allowed radii, elevation, the shared components, and touch targets.
33025
-
33026
- - Run the repository's design lints. In Beeline, from \`apps/mobile\`: \`npx vitest run sources/buzz/calm-lint sources/buzz/design-lint\`.
33027
- - FAIL when a design baseline count grows, or when an allowlist entry is added without naming the design decision behind it.
33028
- - Require a screenshot or rendered proof of every visual change in each theme the app ships (in Beeline, Obsidian and Bone).
33029
- - A design-rule violation is an engineering finding and blocks PASS.
33030
-
33031
- ## 4. Adversarial pass
33032
-
33033
- - For every changed function, name one concrete input or sequence that breaks it.
33034
- - If none is found, write \`none found\` for that function.
33035
-
33036
- ## 5. Verify before reporting
33037
-
33038
- - Confirm every finding by reading the exact line or by running a command.
33039
- - Put unconfirmed concerns under plausible findings. They never block.
33040
-
33041
- ## 6. Bloat guard
33042
-
33043
- - Compare net lines with the spec, its user stories and non-goals, and the approval quote.
33044
- - FAIL backwards-compatibility shims, dual paths, feature flags, or abstractions with one caller.
33045
- - FAIL machinery the spec and the approval quote did not ask for.
33046
-
33047
- ## 7. Security and data
33048
-
33049
- - Check credentials, authorization boundaries, and destructive migrations.
33050
-
33051
- ## 8. Gate and verdict
33052
-
33053
- - Review the exact green head named in your reviewer instruction; a server-confirmed zero-check passed head counts as green. If the head moved, do not approve it.
33054
- - Re-read the assigned brief revision before the verdict. A repair changes the head and invalidates affected evidence; a requirement correction invalidates the relevant verdict even if code did not change. Keep product-completeness findings (a missing or contradicted user story, done non-goal, unaddressed risk, mock mismatch, or approval quote) separate from engineering findings (correctness, security, maintainability, tests). Give every confirmed finding a stable ID that survives rereview, the affected outline line or \`engineering\`, location, severity, evidence, and repair disposition. Reuse the same ID until that finding is resolved. Mechanical repairs return to the author; ask a human only for a genuinely unresolved product choice. Do not run a nested validation pipeline or push the author's branch.
33055
- - Always use this exact verdict shape:
33056
-
33057
- \`approval quoted with its message ID:\`
33058
- \`user story:\`
33059
- \`work warranted evidence:\`
33060
- \`desirability evidence:\`
33061
- \`reproduction id (or none obtained):\`
33062
- \`proof of that reproduction (or none obtained + regression):\`
33063
- \`how Y was demonstrated (or FAIL):\`
33064
- \`commands run + results:\`
33065
- \`brief revision:\`
33066
- \`outline ledger (every user story, acceptance criterion, non-goal and risk + status + evidence; mock-vs-screen notes for frontend work):\`
33067
- \`validation stages and evidence:\`
33068
- \`product-completeness findings (block):\`
33069
- \`engineering findings (block):\`
33070
- \`plausible findings (do not block):\`
33071
- \`net lines:\`
33072
- \`decision: PASS|FAIL\`
33073
-
33074
- Then take exactly one action:
33075
-
33076
- - FAIL: call \`record_validation_stage\` with stage \`review\`, status \`failed\`, and the reviewed head SHA, then reply \`@author\` with the confirmed findings and that reviewed head SHA to fix. The recorded review stage is how an implementer who has already fixed the findings can tell whether the review is stale.
33077
- - PASS: call \`approve_merge\` with the reviewed head SHA and assigned briefRevision (omit the revision only for a legacy corner without a brief), then reply \`approved <reviewed sha>\` without tagging the author. Do not tell the author to merge.
33078
- - Approving is your last step as reviewer. The server squash-merges that exact head once checks are green, the worker's yolo is on, and no human hold stands. Neither you nor the author merges it.
33079
- `;
33080
- }
33081
- function beelineSpecSkillMarkdown(releaseId) {
33082
- return `---
33083
- name: beeline-spec
33084
- description: Prepare or revise a durable corner assignment from Room decisions before repository work. Do not use for ordinary conversation or unrelated artifact delivery.
33085
- ---
33086
-
33087
- <!-- beeline-release: ${releaseId} -->
33088
-
33089
- # Durable corner brief
33090
-
33091
- Create an assignment a fresh session can execute without the parent transcript. A brief has three parts:
33092
-
33093
- - \`spec\`: one Markdown doc you write, with these headings:
33094
- - \`## Intent\`: the human's own words, quoted exactly, each with its Room message ID. Never substitute a summary.
33095
- - \`## User stories\`: what this enables, one per line as "a person who does X sees Y". Only stories traceable to the quoted Intent; never import a plan, a teammate's suggestion, or your own idea as a story. Keep unaffected lines as they are across revisions.
33096
- - \`## Acceptance criteria\`: the requester's own rules from \`## Intent\`, quoted verbatim and numbered, each naming the test (existing or to be written) that fails if the rule is broken anywhere. Criteria are the invariant; stories are examples of it. Write "None beyond the stories." when the request states no rule.
33097
- - \`## Non-goals\`: explicit exclusions.
33098
- - \`## Risks\`: what could go wrong, and for whom.
33099
- - \`## References\`: each reference or mock with what it is and whose call it is; the mock posted here (DESIGN.md, else existing app styles) is the reviewer's acceptance reference, not any other optional or agent-generated mock.
33100
- Add implementation guidance under further headings when it helps.
33101
- - files (\`attachments\`): the Room files the worker needs, each with its purpose and whether it is required.
33102
- - \`approval\`: one human Room message ID. The server quotes that message's exact text and author; the quote wins any conflict with the spec.
33103
-
33104
- Do not infer approval from silence, assent to different prose, or the mere existence of a plan. Keep agent recommendations labelled as recommendations. Do not promote a plausible UX detail, algorithm, failure message, or extra test into a user story merely because it sounds helpful.
33105
-
33106
- ## Compact path for a settled small fix
33107
-
33108
- Use this when the requested behavior, touched surface, exclusions, and proof are already obvious. Inspect the narrow current behavior, write a short spec (quoted intent, observable user stories, non-goals, risks, references), and use the initiating command as the approval. Always compose the full outline and post it to the Room before calling open_corner, even when the initiating command already settles scope and stands as the approval: posting is not optional merely because approval is inferred, since the human must be able to see what you are about to build. \u201CChange the message action label from Remove to Delete; leave confirmation and accessibility wording unchanged\u201D needs the visible-label user story and both exclusions, not another confirmation question.
33109
-
33110
- ## Complex-work planning loop
33111
-
33112
- Use this default-on loop when work crosses components, changes architecture or authorization, needs a visual choice, has multiple user stories, or still contains a material unknown.
33113
-
33114
- ### 1. Scope and current state
33115
-
33116
- - Collect every relevant human message, including corrections; later corrections supersede only what they explicitly change. Quote their exact text and IDs under \`## Intent\`.
33117
- - Inspect current code, data contracts, tests, recent related work, repository invariants, and existing behavior. State what exists, what is missing, and what is deliberately out of scope.
33118
- - Separate facts, human decisions, agent recommendations, and unresolved choices.
33119
-
33120
- ### 2. User stories and product boundary
33121
-
33122
- - Write the actor/action/observable-result stories.
33123
- - Map each story to its own observable line and explicit non-goals.
33124
- - If one unresolved choice would materially change behavior, scope, irreversible effects, or the intended result, present only that choice or the affected brief slice to the human. Use the answering message as the \`approval\`. Never treat no answer as approval.
33125
-
33126
- ### 3. Architecture and data flow
33127
-
33128
- - Trace inputs, authorities, state transitions, persistence, outputs, and trust boundaries.
33129
- - Name the existing components to reuse and the smallest coherent changes.
33130
- - Describe migration and compatibility behavior only where the human intent or existing data requires it.
33131
-
33132
- ### 4. Failure modes and test map
33133
-
33134
- - Enumerate missing/stale data, retries, concurrency, partial failure, authorization failure, rollback, and boundary-specific hazards that apply.
33135
- - Map every user story to its proof boundary: unit, integration, device/browser, server authorization, migration, or live harness. No story may be left without planned evidence.
33136
-
33137
- ### 5. Mocks and references
33138
-
33139
- - When a visual or interaction decision needs eyes, create and post one self-contained mock using the using-beeline mock procedure.
33140
- - A mock/file posted with \`post_artifact\` in this planning turn is added automatically to the brief attachment manifest; do not manually copy its object ID. Still list it under \`## References\` with whose call it is. Existing human-posted files must be named explicitly in attachments.
33141
-
33142
- ### 6. Implementation tasks
33143
-
33144
- - Synthesize ordered, independently verifiable tasks. For each task name the affected boundary, user stories, expected files/components, failure handling, and proof.
33145
- - Keep the spec actionable, not a transcript summary.
33146
-
33147
- ### 7. Bounded adversarial second read (default on)
33148
-
33149
- - Spend one pass, bounded to the drafted scope, trying to disprove completeness: find a missing story, authority mismatch, unhandled failure, user story without proof, mock ambiguity, accidental scope expansion, or task-order hazard.
33150
- - Return findings to the planning agent and repair the draft. Escalate only a genuinely material product choice to the human; engineering choices and mechanical gaps are resolved in the plan.
33151
- - End after one adversarial pass unless its repair exposes one new material product choice. Do not recursively review the review.
33152
-
33153
- ## Dispatch and revision
33154
-
33155
- Existing authorization permits dispatch when the initiating command already settles the exact material scope; that command is the approval, but the outline is still posted to the Room first so the human can see it before open_corner runs. Drafting a brief does not create approval. When the scope was not already explicit, post the spec and ask the corner's opener to approve it, then use their reply as the \`approval\`. Honor an explicit request to review first.
33156
-
33157
- Pass the brief as \`open_corner.brief\`. Read the current revision before revising and use \`revise_corner_brief\` with the complete replacement spec, the approving message, and a concise change description. Preserve unaffected intent quotes, user stories, and references. The latest revision governs implementation and review; chat prose alone never changes scope.
33158
- `;
33159
- }
33160
- function beelineTriageSkillMarkdown(releaseId) {
33161
- return `---
33162
- name: beeline-triage
33163
- description: Clarify and assess a user request before opening a Beeline corner, then bind a bugfix to its recorded reproduction. Use before open_corner and while implementing a bug in a corner.
33164
- ---
33165
-
33166
- <!-- beeline-release: ${releaseId} -->
33167
-
33168
- # Beeline request triage
33169
-
33170
- Run these checks before opening a corner.
33171
-
33172
- ## 1. Is it clear?
33173
-
33174
- - Rewrite the request as a concrete outcome and the user stories it enables.
33175
- - State material exclusions (non-goals) and the risks needed to prevent unrequested work or surprise.
33176
- - Keep the corner objective complete and within 24 words.
33177
- - First ask whether the request has any material ambiguity - one that would change scope, behavior, or risk - and resolve it with the human before doing anything else; never resolve it by adopting a teammate's plan, a prior document, or your own assumption.
33178
- - Once it's resolved, say plainly if the request itself is suboptimal and give your own recommendation - you are expected to disagree, not just permitted to.
33179
- - On a judgment call you're still not sure of, tag a Room colleague for an adversarial second opinion and ask them to reply tagging you back; skip this only when you are the only agent in the Room.
33180
-
33181
- ## 2. Is work warranted?
33182
-
33183
- - For a bug, try to reproduce the exact user-visible behavior and record the evidence.
33184
- - For another request, identify the unmet user need and the current behavior that does not meet it.
33185
- - Search current code, history, issues, and open or recently merged pull requests for released or unreleased work that resolves or supersedes the request.
33186
- - Treat similar titles as candidates. Confirm behavior and scope before calling work duplicate.
33187
- - If the need cannot be established, reproduction fails, or other work may obviate it, warn; do not block.
33188
-
33189
- ## 3. Is it desirable?
33190
-
33191
- - Compare the request with repository-owned goals, invariants, architecture, and established product behavior.
33192
- - Look for concrete user benefit, the smallest coherent solution, and ongoing maintenance cost.
33193
- - Do not invent product strategy. If the evidence is absent or conflicting, warn; do not block.
33194
-
33195
- ## Output
33196
-
33197
- When dispatching work, pass the complete brief to open_corner under existing authorization, after posting the outline as beeline-spec requires. Add applicable warnings to the brief:
33198
-
33199
- \`Triage warning \u2014 warranted: <evidence-backed reason>\`
33200
- \`Triage warning \u2014 desirable: <evidence-backed reason>\`
33201
-
33202
- When a bug reproduction succeeds, also emit:
33203
-
33204
- \`Reproduction <id>: <user path> \u2192 <observable wrong result>\`
33205
-
33206
- Do not emit a warning merely because evidence is incomplete when the repository offers no practical way to obtain it. Never describe a warning as approval or rejection. Warnings inform the user and implementer; they do not block work.
33207
-
33208
- ## Bugfix execution
33209
-
33210
- Once a corner is open for a reported bug, follow these steps in order. They are instruction, not a server gate. They do not condition the fix on reproduction. Warranted-work and desirability warnings still do not block.
33211
-
33212
- ### 1. Attempt to reproduce
33213
-
33214
- - Attempt to reproduce the bug as triage isolated it, using every tool the host offers: emulator, Playwright, browser, test runner.
33215
- - Record what was tried and what was observed.
33216
- - If a reproduction is obtained, record the exact user path and the observable wrong result under \`Reproduction <id>\`. Reuse the identifier triage emitted when it recorded one.
33217
- - If reproduction fails, warn and continue exactly as triage already does. Never stop. Never condition the fix on reproduction.
33218
-
33219
- ### 2. Narrow fix
33220
-
33221
- - Narrow the fix to the reported behavior.
33222
- - When a reproduction exists, change only what removes that recorded reproduction.
33223
- - Nearby improvements stay out of scope.
33224
-
33225
- ### 3. Proof matching triage
33226
-
33227
- - When a reproduction exists, re-run it, cite the same identifier, and show it now passing, plus the regression that would fail if the bug returned.
33228
- - When none was obtained, state that plainly and show the regression instead.
33229
- - Name the identifier in the pull request when one exists so the reviewer can check that proof, not merely that some test exists.
33230
- `;
33231
- }
33232
-
33233
- // apps/body/dist/external-mcp-capabilities.js
33234
- var SQUIRE_MCP_VERSION = "latest";
33235
- var SQUIRE_MCP_PACKAGE = `@trusty-squire/mcp@${SQUIRE_MCP_VERSION}`;
33236
- function isTrustySquireMcpLaunch(command, args = []) {
33237
- return [command, ...args].some((value) => {
33238
- const normalized = value.trim().replace(/\\/g, "/").toLowerCase();
33239
- return /(^|[\s/="'\[,])@trusty-squire\/mcp(?:@[^\s/"'\],}]+)?(?=$|[\s/"'\],}])/i.test(normalized) || /(^|\/)trusty-squire(?:-mcp)?(?:\.[a-z0-9]+)?$/i.test(normalized);
33240
- });
33241
- }
33242
- var SQUIRE_GOVERNED_TOOLS = [
33243
- "use_credential",
33244
- "grant_app_access",
33245
- "revoke_app_access"
33246
- ];
33247
- var SQUIRE_GOVERNED_TOOL_SET = new Set(SQUIRE_GOVERNED_TOOLS);
33248
-
33249
- // apps/body/dist/host-mcp-route.js
33250
- import { join as join11, resolve as resolve14 } from "node:path";
33251
- var import_yaml = __toESM(require_dist(), 1);
33252
- init_squire_host();
33253
- function grantedMcpServerNames(grants) {
33254
- const names = /* @__PURE__ */ new Set();
33255
- for (const grant of grants ?? []) {
33256
- if (grant.kind !== "mcp")
33257
- continue;
33258
- const name = grant.target.trim();
33259
- if (name)
33260
- names.add(name);
33261
- }
33262
- return [...names];
33263
- }
33264
- function ungatedHostServers(hostServers, granted) {
33265
- const allowed = new Set(granted);
33266
- return hostServers.filter((name) => !allowed.has(name));
33267
- }
33268
- async function claimGrantedHostRoutes(result) {
33269
- if (!result || typeof result !== "object" || !("grants" in result))
33270
- return [];
33271
- const grants = result.grants;
33272
- if (!Array.isArray(grants))
33273
- return [];
33274
- return grantedMcpServerNames(grants.flatMap((entry) => {
33275
- if (!entry || typeof entry !== "object")
33276
- return [];
33277
- const grant = entry;
33278
- return grant.kind === "mcp" && typeof grant.target === "string" && (grant.status === "once" || grant.status === "approved") ? [{ kind: "mcp", target: grant.target }] : [];
33279
- }));
32662
+ async function claimGrantedHostRoutes(result) {
32663
+ if (!result || typeof result !== "object" || !("grants" in result))
32664
+ return [];
32665
+ const grants = result.grants;
32666
+ if (!Array.isArray(grants))
32667
+ return [];
32668
+ return grantedMcpServerNames(grants.flatMap((entry) => {
32669
+ if (!entry || typeof entry !== "object")
32670
+ return [];
32671
+ const grant = entry;
32672
+ return grant.kind === "mcp" && typeof grant.target === "string" && (grant.status === "once" || grant.status === "approved") ? [{ kind: "mcp", target: grant.target }] : [];
32673
+ }));
33280
32674
  }
33281
32675
  function recordValue(value) {
33282
32676
  return value && typeof value === "object" && !Array.isArray(value) ? value : void 0;
@@ -34852,7 +34246,7 @@ async function materializedSkillManifest(target) {
34852
34246
  return lines.sort().join("\n");
34853
34247
  }
34854
34248
  function sha2563(value) {
34855
- return createHash8("sha256").update(value).digest("hex");
34249
+ return createHash7("sha256").update(value).digest("hex");
34856
34250
  }
34857
34251
  async function resolveSharedSkillSources(operatorHome, names) {
34858
34252
  if (names.length > 0)
@@ -35032,7 +34426,7 @@ async function writeIsolatedHarnessFile(path, content) {
35032
34426
  }
35033
34427
  }
35034
34428
  function roomSessionTmpDir(root, base = "/tmp") {
35035
- const hash = createHash8("sha256").update(resolve16(root)).digest("hex").slice(0, 12);
34429
+ const hash = createHash7("sha256").update(resolve16(root)).digest("hex").slice(0, 12);
35036
34430
  return join12(base, `beeline-${hash}`);
35037
34431
  }
35038
34432
  function roomAgentHomeEnv(root) {
@@ -36509,7 +35903,7 @@ var DEFAULT_UPDATE_INTERVAL_MS = 3e4;
36509
35903
  var UPDATE_DRAIN_DEADLINE_MS = UPDATE_CONVERGENCE_SLO_MS - 15e4;
36510
35904
  var MAX_HOST_RESTART_STAGGER_MS = 15e3;
36511
35905
  function managedRestartStaggerMs(agentId, desiredRelease, drainDeadlineAt, now2 = Date.now()) {
36512
- const digest = createHash9("sha256").update(agentId).update("\0").update(desiredRelease).digest();
35906
+ const digest = createHash8("sha256").update(agentId).update("\0").update(desiredRelease).digest();
36513
35907
  const spread = digest.readUInt32BE(0) % (MAX_HOST_RESTART_STAGGER_MS + 1);
36514
35908
  return Math.min(spread, Math.max(0, drainDeadlineAt - now2));
36515
35909
  }
@@ -37398,7 +36792,7 @@ async function runStartCommand(args, interactiveUi, dependencyOverrides = {}) {
37398
36792
 
37399
36793
  // apps/body/dist/connect-command.js
37400
36794
  import { spawn as spawn13 } from "node:child_process";
37401
- import { createHash as createHash10, randomUUID as randomUUID7 } from "node:crypto";
36795
+ import { createHash as createHash9, randomUUID as randomUUID7 } from "node:crypto";
37402
36796
  import { chmod as chmod7, mkdir as mkdir16, readFile as readFile20, unlink as unlink7, writeFile as writeFile13 } from "node:fs/promises";
37403
36797
  import { homedir as homedir15, hostname } from "node:os";
37404
36798
  import { dirname as dirname19, resolve as resolve26 } from "node:path";
@@ -38468,7 +37862,7 @@ async function readMachineId(env = process.env) {
38468
37862
  await writeFile13(machineIdPath, `${machineId}
38469
37863
  `, { mode: 384 });
38470
37864
  } catch {
38471
- machineId = createHash10("sha256").update(machineName).digest("hex").slice(0, 36);
37865
+ machineId = createHash9("sha256").update(machineName).digest("hex").slice(0, 36);
38472
37866
  }
38473
37867
  }
38474
37868
  return { machineId, machineName };
@@ -38477,7 +37871,7 @@ function requestConnectGrant(baseUrl, pairingCode, selection, fetchImpl, machine
38477
37871
  const normalizedPairingCode = normalizeAgentPairingCode(pairingCode);
38478
37872
  if (!normalizedPairingCode)
38479
37873
  throw new Error("invalid pairing code");
38480
- const avatarSeed = createHash10("sha256").update(normalizedPairingCode.toUpperCase()).digest("hex").slice(0, 32);
37874
+ const avatarSeed = createHash9("sha256").update(normalizedPairingCode.toUpperCase()).digest("hex").slice(0, 32);
38481
37875
  return jsonRequest(`${baseUrl}/auth/agent/connect`, {
38482
37876
  pairing_code: normalizedPairingCode,
38483
37877
  harness: selection.harness,
@@ -38948,13 +38342,13 @@ async function applyRuntimeModelPreflight(config, agent, selection, validate = v
38948
38342
  }
38949
38343
 
38950
38344
  // apps/body/dist/model-catalog-sync.js
38951
- import { createHash as createHash11 } from "node:crypto";
38345
+ import { createHash as createHash10 } from "node:crypto";
38952
38346
  import { readFile as readFile21, writeFile as writeFile14 } from "node:fs/promises";
38953
38347
  import { resolve as resolve27 } from "node:path";
38954
38348
  var MODEL_CATALOG_PROBE_TIMEOUT_MS = 3e4;
38955
38349
  var MODEL_CATALOG_HASH_FILE = "model-catalog.sha256";
38956
38350
  function modelCatalogHash(options, selection, startupUnavailable) {
38957
- return createHash11("sha256").update(JSON.stringify({
38351
+ return createHash10("sha256").update(JSON.stringify({
38958
38352
  options,
38959
38353
  selection: selection ?? null,
38960
38354
  startupUnavailable: startupUnavailable ?? null
@@ -39585,7 +38979,7 @@ function describe2(error) {
39585
38979
 
39586
38980
  // apps/body/dist/room-runtime.js
39587
38981
  import { execFile as execFile13 } from "node:child_process";
39588
- import { createHash as createHash15 } from "node:crypto";
38982
+ import { createHash as createHash14 } from "node:crypto";
39589
38983
  import { existsSync as existsSync10, mkdirSync as mkdirSync3 } from "node:fs";
39590
38984
  import { mkdir as mkdir27, readdir as readdir9, realpath as realpath2, rm as rm15 } from "node:fs/promises";
39591
38985
  import { dirname as dirname27, relative as relative3, resolve as resolve38, sep as sep2 } from "node:path";
@@ -39593,7 +38987,7 @@ import { promisify as promisify8 } from "node:util";
39593
38987
 
39594
38988
  // apps/body/dist/grant-runner.js
39595
38989
  import { execFile as execFile8 } from "node:child_process";
39596
- import { createHash as createHash12, randomBytes as randomBytes9, randomUUID as randomUUID9 } from "node:crypto";
38990
+ import { createHash as createHash11, randomBytes as randomBytes9, randomUUID as randomUUID9 } from "node:crypto";
39597
38991
  import { chmod as chmod8, mkdir as mkdir17, readFile as readFile22, readdir as readdir6, rename as rename9, rm as rm11, rmdir, stat as stat7, writeFile as writeFile15 } from "node:fs/promises";
39598
38992
  import { createServer as createServer3 } from "node:http";
39599
38993
  import { dirname as dirname21, resolve as resolve28 } from "node:path";
@@ -39858,7 +39252,7 @@ ${ROOM_WRITE_REFUSED_NOTE}` : outcome.output, secrets), cap);
39858
39252
  if (!script || script.path !== argument.path) {
39859
39253
  throw new Error(`${argument.path} is a script no human has read: this grant was approved without it. Ask with request_grant kind=command so the card carries what the command runs.`);
39860
39254
  }
39861
- if (createHash12("sha256").update(bytes).digest("hex") !== script.sha256) {
39255
+ if (createHash11("sha256").update(bytes).digest("hex") !== script.sha256) {
39862
39256
  throw new Error(`${argument.path} changed after it was approved, so the approval no longer covers it. Ask with request_grant kind=command again so a human reads what runs now.`);
39863
39257
  }
39864
39258
  }
@@ -40448,7 +39842,7 @@ async function runServerCommandIntake(options) {
40448
39842
 
40449
39843
  // apps/body/dist/monolith-corner-turn.js
40450
39844
  import { execFile as execFile12 } from "node:child_process";
40451
- import { createHash as createHash14 } from "node:crypto";
39845
+ import { createHash as createHash13 } from "node:crypto";
40452
39846
  import { existsSync as existsSync9 } from "node:fs";
40453
39847
  import { mkdir as mkdir26, readFile as readFile30, writeFile as writeFile20 } from "node:fs/promises";
40454
39848
  import { homedir as homedir18, hostname as hostname3 } from "node:os";
@@ -41594,329 +40988,1006 @@ function toolIdentityCandidates(toolCall) {
41594
40988
  add(record5?.[key]);
41595
40989
  return candidates;
41596
40990
  }
41597
- function normalizedServerName(name) {
41598
- return name.toLowerCase().replace(/[^a-z0-9]+/g, "_");
40991
+ function normalizedServerName(name) {
40992
+ return name.toLowerCase().replace(/[^a-z0-9]+/g, "_");
40993
+ }
40994
+ function resolveMountedMcpToolCall(request, mountedServers) {
40995
+ const toolCall = request.toolCall;
40996
+ if (shellPayload(toolCall))
40997
+ return void 0;
40998
+ const record5 = permissionRecord(toolCall?.rawInput);
40999
+ if (dispatchedPayloadIsShell(record5))
41000
+ return void 0;
41001
+ const candidates = toolIdentityCandidates(toolCall);
41002
+ for (const server of mountedServers) {
41003
+ const spellings = /* @__PURE__ */ new Set([server.toLowerCase(), normalizedServerName(server)]);
41004
+ for (const candidate of candidates) {
41005
+ const lowered = candidate.toLowerCase();
41006
+ for (const spelling of spellings) {
41007
+ for (const separator of TOOL_NAME_SEPARATORS) {
41008
+ const prefix = `${spelling}${separator}`;
41009
+ if (!lowered.startsWith(prefix))
41010
+ continue;
41011
+ const tool = candidate.slice(prefix.length).trim();
41012
+ if (TOOL_NAME_PATTERN.test(tool))
41013
+ return { server, tool };
41014
+ }
41015
+ }
41016
+ }
41017
+ }
41018
+ return void 0;
41019
+ }
41020
+ function isReadOnlyMcpPermissionRequest(request) {
41021
+ const toolCall = request.toolCall;
41022
+ const title = toolCall?.title?.trim() ?? "";
41023
+ const rawInput = toolCall?.rawInput;
41024
+ const mcpCall = Boolean(rawInput) && typeof rawInput === "object" && !Array.isArray(rawInput) ? rawInput : void 0;
41025
+ if (mcpCall?.server === READ_ONLY_MCP_SERVER_NAME && typeof mcpCall.tool === "string" && READ_ONLY_TOOL_SET.has(mcpCall.tool)) {
41026
+ return true;
41027
+ }
41028
+ if (shellPayload(toolCall))
41029
+ return false;
41030
+ if (READ_ONLY_PERMISSION_TITLES.has(title))
41031
+ return true;
41032
+ if (READ_ONLY_SERVER_TITLE_PREFIXES.some((prefix) => title.startsWith(prefix))) {
41033
+ for (const tool of READ_ONLY_TOOL_NAMES) {
41034
+ if (title === tool || title.endsWith(`(${tool})`) || TOOL_NAME_SEPARATORS.some((separator) => title.endsWith(`${separator}${tool}`))) {
41035
+ return true;
41036
+ }
41037
+ }
41038
+ }
41039
+ return false;
41040
+ }
41041
+ function isMountedMcpToolPermissionRequest(request, mountedServers = ROOM_MOUNTED_MCP_SERVERS) {
41042
+ const toolCall = request.toolCall;
41043
+ const rawInput = toolCall?.rawInput;
41044
+ if (dispatchedPayloadIsShell(permissionRecord(rawInput)))
41045
+ return false;
41046
+ if (rawInput && typeof rawInput === "object" && !Array.isArray(rawInput)) {
41047
+ const call = rawInput;
41048
+ if (typeof call.server === "string" && typeof call.tool === "string")
41049
+ return true;
41050
+ }
41051
+ if (shellPayload(toolCall))
41052
+ return false;
41053
+ const title = toolCall?.title?.trim() ?? "";
41054
+ if (/^mcp__[^_].*__[^_]/.test(title))
41055
+ return true;
41056
+ if (/^mcp\.[^.]+\.[^.]/.test(title))
41057
+ return true;
41058
+ if (resolveMountedMcpToolCall(request, mountedServers))
41059
+ return true;
41060
+ return isReadOnlyMcpPermissionRequest(request) || isBeelineAgentMcpPermissionRequest(request);
41061
+ }
41062
+ var AGENT_SURFACE_TOOL_NAMES = [
41063
+ "get_avatar",
41064
+ "set_avatar",
41065
+ "open_corner",
41066
+ "pr_checks_status",
41067
+ "post_artifact",
41068
+ "write_scratch_file",
41069
+ "fetch_image",
41070
+ "connect_app"
41071
+ ];
41072
+ function isHostMcpPermissionRequest(request, hostServers = CODE_OWNED_HOST_MCP_NAMES) {
41073
+ const rawInput = request.toolCall?.rawInput;
41074
+ if (rawInput && typeof rawInput === "object" && !Array.isArray(rawInput)) {
41075
+ const server = rawInput.server;
41076
+ if (typeof server === "string" && isHostMcpIdentity(server, hostServers))
41077
+ return true;
41078
+ }
41079
+ return toolIdentityCandidates(request.toolCall).some((candidate) => isHostMcpIdentity(candidate, hostServers));
41080
+ }
41081
+ function isBeelineAgentMcpPermissionRequest(request) {
41082
+ const toolCall = request.toolCall;
41083
+ const title = toolCall?.title?.trim() ?? "";
41084
+ const rawInput = toolCall?.rawInput;
41085
+ if (rawInput && typeof rawInput === "object" && !Array.isArray(rawInput)) {
41086
+ const call = rawInput;
41087
+ if (call.server === BEELINE_AGENT_MCP_SERVER_NAME && typeof call.tool === "string" && AGENT_SURFACE_TOOL_NAMES.includes(call.tool)) {
41088
+ return true;
41089
+ }
41090
+ }
41091
+ if (shellPayload(toolCall))
41092
+ return false;
41093
+ const normalized = BEELINE_AGENT_MCP_SERVER_NAME.replaceAll("-", "_");
41094
+ return AGENT_SURFACE_TOOL_NAMES.flatMap((tool) => [
41095
+ `mcp__${BEELINE_AGENT_MCP_SERVER_NAME}__${tool}`,
41096
+ `mcp__${normalized}__${tool}`,
41097
+ `mcp.${BEELINE_AGENT_MCP_SERVER_NAME}.${tool}`
41098
+ ]).includes(title);
41099
+ }
41100
+ function isMcpShapedPermissionRequest(request, hostServers = CODE_OWNED_HOST_MCP_NAMES) {
41101
+ if (isHostMcpPermissionRequest(request, hostServers))
41102
+ return true;
41103
+ const record5 = permissionRecord(request.toolCall?.rawInput);
41104
+ if (typeof record5?.server === "string")
41105
+ return true;
41106
+ if (DISPATCHER_TOOL_NAME_KEYS.some((key) => typeof record5?.[key] === "string"))
41107
+ return true;
41108
+ const title = request.toolCall?.title?.trim() ?? "";
41109
+ return /^mcp__[^_].*__[^_]/.test(title) || /^mcp\.[^.]+\.[^.]/.test(title);
41110
+ }
41111
+
41112
+ // apps/body/dist/room-session.js
41113
+ var ReadOnlyToolsUnavailableError = class extends Error {
41114
+ name = "ReadOnlyToolsUnavailableError";
41115
+ };
41116
+ function beelineAgentMcpServer(config, api, context) {
41117
+ if (!config.readonlyMcpCommand) {
41118
+ throw new ReadOnlyToolsUnavailableError("agent tools unavailable: the Beeline MCP command is required");
41119
+ }
41120
+ const connection = api.connection();
41121
+ return {
41122
+ name: BEELINE_AGENT_MCP_SERVER_NAME,
41123
+ command: config.readonlyMcpCommand,
41124
+ args: [...config.readonlyMcpArgs ?? []],
41125
+ env: [
41126
+ { name: "BEELINE_MCP_SURFACE", value: "agent" },
41127
+ ...context.turnContextPath ? [{ name: "BEELINE_TURN_CONTEXT_FILE", value: context.turnContextPath }] : [],
41128
+ ...context.directMessage ? [{ name: "BEELINE_AGENT_DM", value: "1" }] : [],
41129
+ ...context.squireRelay ? [
41130
+ { name: "BEELINE_SQUIRE_RELAY_URL", value: context.squireRelay.url },
41131
+ { name: "BEELINE_SQUIRE_RELAY_TOKEN", value: context.squireRelay.token }
41132
+ ] : [],
41133
+ { name: "BEELINE_DAEMON_BASE_URL", value: connection.baseUrl },
41134
+ { name: "BEELINE_DAEMON_TOKEN", value: connection.daemonToken },
41135
+ { name: "BEELINE_HELPER_VERSION", value: connection.helperVersion },
41136
+ { name: "BEELINE_DAEMON_AGENT_ID", value: connection.agentId },
41137
+ { name: "BEELINE_OPERATOR_HOME", value: config.operatorHome ?? homedir16() },
41138
+ { name: "BEELINE_DAEMON_ROOM_ID", value: context.roomId },
41139
+ { name: "BEELINE_DAEMON_WORKSPACE_ID", value: context.workspaceId },
41140
+ ...context.cornerId ? [{ name: "BEELINE_DAEMON_CORNER_ID", value: context.cornerId }] : [],
41141
+ ...context.agentMayCloseCorner ? [{ name: "BEELINE_CORNER_AGENT_CLOSE", value: "1" }] : [],
41142
+ ...context.reviewer ? [{ name: "BEELINE_CORNER_REVIEWER", value: "1" }] : [],
41143
+ ...context.cornerId && context.repositoryBacked ? [{ name: "BEELINE_CORNER_REPOSITORY", value: "1" }] : [],
41144
+ ...context.attachRoot ? [{ name: "BEELINE_ATTACH_ROOT", value: context.attachRoot }] : [],
41145
+ ...context.attachScratchRoot ? [{ name: "BEELINE_ATTACH_SCRATCH_ROOT", value: context.attachScratchRoot }] : [],
41146
+ ...context.grantRunner ? [
41147
+ { name: "BEELINE_GRANT_RUNNER_URL", value: context.grantRunner.url },
41148
+ { name: "BEELINE_GRANT_RUNNER_TOKEN", value: context.grantRunner.token }
41149
+ ] : []
41150
+ ]
41151
+ };
41152
+ }
41153
+ function readOnlyMcpServer(config, cwd, agentMemoryDir) {
41154
+ if (!config.readonlyMcpCommand) {
41155
+ throw new ReadOnlyToolsUnavailableError("read-only tools unavailable: beeline-readonly-mcp is required for Room sessions");
41156
+ }
41157
+ const skillDir = agentSkillDir(config.agentKind);
41158
+ return {
41159
+ name: READ_ONLY_MCP_SERVER_NAME,
41160
+ command: config.readonlyMcpCommand,
41161
+ args: [...config.readonlyMcpArgs ?? []],
41162
+ env: [
41163
+ { name: "BEELINE_READONLY_ROOT", value: resolve32(cwd) },
41164
+ ...config.agentHomeRoot ? [
41165
+ {
41166
+ name: "BEELINE_READONLY_AGENT_SKILLS_ROOT",
41167
+ value: resolve32(config.agentHomeRoot, skillDir, "skills")
41168
+ }
41169
+ ] : [],
41170
+ ...agentMemoryDir ? [{ name: "BEELINE_READONLY_AGENT_MEMORY_ROOT", value: resolve32(agentMemoryDir) }] : []
41171
+ ]
41172
+ };
41173
+ }
41174
+
41175
+ // apps/body/dist/institutional-context.js
41176
+ import { performance as performance2 } from "node:perf_hooks";
41177
+ var INSTITUTIONAL_CONTEXT_TIMEOUT_MS = 500;
41178
+ var INSTITUTIONAL_MEMORY_LIVE_FLAG = "BEELINE_INSTITUTIONAL_MEMORY_ENABLED";
41179
+ function institutionalMemoryFlagEnabled(env, flag) {
41180
+ return env[flag] !== "false";
41181
+ }
41182
+ var EMPTY_INSTITUTIONAL_CONTEXT = {
41183
+ snapshotRevision: 0,
41184
+ text: "",
41185
+ itemIds: [],
41186
+ totalBytes: 0,
41187
+ omitted: {}
41188
+ };
41189
+ var INSTITUTIONAL_CONTEXT_MISSED_TEXT = "institutional memory did not load this turn; use search_memory before concluding anything is not stored.";
41190
+ function startInstitutionalContextFetch(api, roomId, enabled = institutionalMemoryFlagEnabled(process.env, INSTITUTIONAL_MEMORY_LIVE_FLAG), now2 = () => performance2.now()) {
41191
+ const startedAt = now2();
41192
+ const promise = enabled ? api.execute("getInstitutionalContext", { roomId }) : Promise.resolve(EMPTY_INSTITUTIONAL_CONTEXT);
41193
+ promise.catch(() => {
41194
+ });
41195
+ return { promise, startedAt };
41196
+ }
41197
+ async function awaitInstitutionalContext(fetch2, log2 = console.warn, budgetMs = INSTITUTIONAL_CONTEXT_TIMEOUT_MS, now2 = () => performance2.now()) {
41198
+ const remainingMs = budgetMs - (now2() - fetch2.startedAt);
41199
+ let timer;
41200
+ try {
41201
+ const snapshot = await Promise.race([
41202
+ fetch2.promise,
41203
+ new Promise((_, reject) => {
41204
+ timer = setTimeout(() => reject(new Error("institutional context timed out")), Math.max(0, remainingMs));
41205
+ timer.unref?.();
41206
+ })
41207
+ ]);
41208
+ return { ...snapshot, outcome: snapshot.text !== "" ? "served" : "empty" };
41209
+ } catch (error) {
41210
+ log2(`institutional context unavailable: ${error instanceof Error ? error.message : String(error)}`);
41211
+ return {
41212
+ ...EMPTY_INSTITUTIONAL_CONTEXT,
41213
+ text: INSTITUTIONAL_CONTEXT_MISSED_TEXT,
41214
+ outcome: "timed-out"
41215
+ };
41216
+ } finally {
41217
+ if (timer)
41218
+ clearTimeout(timer);
41219
+ }
41220
+ }
41221
+
41222
+ // apps/body/dist/codegraph.js
41223
+ import { execFile as execFile11 } from "node:child_process";
41224
+ import { access as access3, appendFile, mkdir as mkdir21, readFile as readFile28 } from "node:fs/promises";
41225
+ import { dirname as dirname23, isAbsolute as isAbsolute3, resolve as resolve33 } from "node:path";
41226
+ import { promisify as promisify6 } from "node:util";
41227
+ var execFileAsync6 = promisify6(execFile11);
41228
+ var CODEGRAPH_MCP_SERVER_NAME = "codegraph";
41229
+ var CODEGRAPH_INDEX_PATH = ".codegraph/codegraph.db";
41230
+ var CODEGRAPH_PREPARE_TIMEOUT_MS = 12e4;
41231
+ function codegraphIndexDirectory(cwd) {
41232
+ return resolve33(cwd, dirname23(CODEGRAPH_INDEX_PATH));
41233
+ }
41234
+ function codegraphMcpServer(config, cwd, options) {
41235
+ if (!config.codegraphCommand)
41236
+ return void 0;
41237
+ return {
41238
+ name: CODEGRAPH_MCP_SERVER_NAME,
41239
+ command: config.codegraphCommand,
41240
+ args: ["serve", "--mcp", "--path", resolve33(cwd), ...options.readonly ? ["--no-watch"] : []],
41241
+ env: [
41242
+ { name: "CODEGRAPH_TELEMETRY", value: "0" },
41243
+ // One MCP child per harness session is easier to contain than a daemon
41244
+ // that can outlive the Room/corner whose index and permissions it used.
41245
+ { name: "CODEGRAPH_NO_DAEMON", value: "1" }
41246
+ ]
41247
+ };
41248
+ }
41249
+ function codegraphFingerprintServers(config, servers, mounted = Boolean(config.codegraphCommand)) {
41250
+ return config.codegraphCommand && mounted ? [...servers, CODEGRAPH_MCP_SERVER_NAME] : [...servers];
41599
41251
  }
41600
- function resolveMountedMcpToolCall(request, mountedServers) {
41601
- const toolCall = request.toolCall;
41602
- if (shellPayload(toolCall))
41603
- return void 0;
41604
- const record5 = permissionRecord(toolCall?.rawInput);
41605
- if (dispatchedPayloadIsShell(record5))
41606
- return void 0;
41607
- const candidates = toolIdentityCandidates(toolCall);
41608
- for (const server of mountedServers) {
41609
- const spellings = /* @__PURE__ */ new Set([server.toLowerCase(), normalizedServerName(server)]);
41610
- for (const candidate of candidates) {
41611
- const lowered = candidate.toLowerCase();
41612
- for (const spelling of spellings) {
41613
- for (const separator of TOOL_NAME_SEPARATORS) {
41614
- const prefix = `${spelling}${separator}`;
41615
- if (!lowered.startsWith(prefix))
41616
- continue;
41617
- const tool = candidate.slice(prefix.length).trim();
41618
- if (TOOL_NAME_PATTERN.test(tool))
41619
- return { server, tool };
41620
- }
41621
- }
41622
- }
41252
+ async function isGitWorktree(cwd) {
41253
+ try {
41254
+ const { stdout: stdout6 } = await execFileAsync6("git", ["rev-parse", "--show-toplevel"], {
41255
+ cwd,
41256
+ timeout: 1e4
41257
+ });
41258
+ return resolve33(stdout6.trim()) === resolve33(cwd);
41259
+ } catch {
41260
+ return false;
41623
41261
  }
41624
- return void 0;
41625
41262
  }
41626
- function isReadOnlyMcpPermissionRequest(request) {
41627
- const toolCall = request.toolCall;
41628
- const title = toolCall?.title?.trim() ?? "";
41629
- const rawInput = toolCall?.rawInput;
41630
- const mcpCall = Boolean(rawInput) && typeof rawInput === "object" && !Array.isArray(rawInput) ? rawInput : void 0;
41631
- if (mcpCall?.server === READ_ONLY_MCP_SERVER_NAME && typeof mcpCall.tool === "string" && READ_ONLY_TOOL_SET.has(mcpCall.tool)) {
41632
- return true;
41263
+ async function excludeIndexFromGitStatus(cwd) {
41264
+ try {
41265
+ const { stdout: stdout6 } = await execFileAsync6("git", ["rev-parse", "--git-path", "info/exclude"], {
41266
+ cwd,
41267
+ timeout: 1e4
41268
+ });
41269
+ const gitPath = stdout6.trim();
41270
+ if (!gitPath)
41271
+ return;
41272
+ const path = isAbsolute3(gitPath) ? gitPath : resolve33(cwd, gitPath);
41273
+ await mkdir21(dirname23(path), { recursive: true });
41274
+ const existing = await readFile28(path, "utf8").catch(() => "");
41275
+ if (existing.split("\n").includes(".codegraph/"))
41276
+ return;
41277
+ await appendFile(path, `${existing && !existing.endsWith("\n") ? "\n" : ""}.codegraph/
41278
+ `);
41279
+ } catch {
41633
41280
  }
41634
- if (shellPayload(toolCall))
41281
+ }
41282
+ async function prepareCodegraphIndex(config, cwd) {
41283
+ const command = config.codegraphCommand;
41284
+ if (!command || !await isGitWorktree(cwd))
41635
41285
  return false;
41636
- if (READ_ONLY_PERMISSION_TITLES.has(title))
41286
+ await excludeIndexFromGitStatus(cwd);
41287
+ const indexPath = resolve33(cwd, CODEGRAPH_INDEX_PATH);
41288
+ const indexed = await access3(indexPath).then(() => true, () => false);
41289
+ const args = indexed ? ["sync", "--quiet", cwd] : ["init", "--yes", cwd];
41290
+ try {
41291
+ await execFileAsync6(command, args, {
41292
+ cwd,
41293
+ env: { ...process.env, CODEGRAPH_TELEMETRY: "0", CODEGRAPH_NO_DAEMON: "1" },
41294
+ timeout: CODEGRAPH_PREPARE_TIMEOUT_MS,
41295
+ maxBuffer: 4 * 1024 * 1024
41296
+ });
41637
41297
  return true;
41638
- if (READ_ONLY_SERVER_TITLE_PREFIXES.some((prefix) => title.startsWith(prefix))) {
41639
- for (const tool of READ_ONLY_TOOL_NAMES) {
41640
- if (title === tool || title.endsWith(`(${tool})`) || TOOL_NAME_SEPARATORS.some((separator) => title.endsWith(`${separator}${tool}`))) {
41641
- return true;
41642
- }
41643
- }
41298
+ } catch (error) {
41299
+ console.warn(`[body] CodeGraph ${args[0]} failed for ${cwd}; continuing with Beeline read tools:`, error);
41300
+ return false;
41644
41301
  }
41645
- return false;
41646
41302
  }
41647
- function isMountedMcpToolPermissionRequest(request, mountedServers = ROOM_MOUNTED_MCP_SERVERS) {
41648
- const toolCall = request.toolCall;
41649
- const rawInput = toolCall?.rawInput;
41650
- if (dispatchedPayloadIsShell(permissionRecord(rawInput)))
41303
+
41304
+ // apps/body/dist/monolith-room-turn.js
41305
+ import { mkdir as mkdir23, writeFile as writeFile18 } from "node:fs/promises";
41306
+ import { homedir as homedir17, hostname as hostname2 } from "node:os";
41307
+ import { dirname as dirname24, join as join18 } from "node:path";
41308
+
41309
+ // packages/api-contract/dist/scheduled-prompts.js
41310
+ var SCHEDULE_SCHEDULER_NAME = "Beeline Scheduler";
41311
+ var SCHEDULE_RAN_VERB = "ran a schedule for";
41312
+
41313
+ // apps/body/dist/transcript-budget.js
41314
+ function budgetTranscript(lines, modelContextTokens, otherPromptBytes) {
41315
+ const context = modelContextTokens && modelContextTokens > 0 ? modelContextTokens : 32768;
41316
+ const reserveTokens = Math.max(4096, Math.ceil(context * 0.3));
41317
+ const available = Math.max(0, context - Math.ceil(otherPromptBytes / 2) - reserveTokens);
41318
+ const budget = Math.max(0, Math.min(24e3, Math.floor(context * 0.25), available * 2) - 128);
41319
+ if (!budget)
41320
+ return [];
41321
+ const selected = [];
41322
+ let used = 0;
41323
+ const rowLimit = Math.min(4096, Math.floor(budget / 2));
41324
+ for (let index = lines.length - 1; index >= 0; index -= 1) {
41325
+ const row = boundMessageText(lines[index], rowLimit);
41326
+ const bytes = Buffer.byteLength(row) + 1;
41327
+ if (used + bytes > budget)
41328
+ break;
41329
+ selected.push(row);
41330
+ used += bytes;
41331
+ }
41332
+ selected.reverse();
41333
+ if (selected.length < lines.length) {
41334
+ selected.unshift(`[${lines.length - selected.length} older messages omitted from this prompt]`);
41335
+ }
41336
+ return selected;
41337
+ }
41338
+ function boundRoomTaskBody(body, modelContextTokens) {
41339
+ const context = modelContextTokens && modelContextTokens > 0 ? modelContextTokens : 32768;
41340
+ return boundMessageText(body, Math.min(8e3, Math.floor(context / 4)));
41341
+ }
41342
+ function boundMessageText(original, maxBytes) {
41343
+ const sanitized = stripInlineData(original);
41344
+ const clean4 = sanitized === original ? original : sanitized + fetchPointer(original, "inline content omitted");
41345
+ return Buffer.byteLength(clean4) > maxBytes ? truncateRow(clean4, maxBytes) : clean4;
41346
+ }
41347
+ function stripInlineData(text2) {
41348
+ return text2.replace(/data:[^\s"'<>)]*/gi, "[inline data omitted]").replace(/\b[A-Za-z0-9+/]{512,}={0,2}\b/g, (candidate) => /[A-Z]/.test(candidate) && /[a-z]/.test(candidate) && /[0-9+/]/.test(candidate) ? "[inline binary data omitted]" : candidate);
41349
+ }
41350
+ function truncateRow(row, maxBytes) {
41351
+ const pointer = fetchPointer(row, "message truncated");
41352
+ const contentBytes = Math.max(0, maxBytes - Buffer.byteLength(pointer));
41353
+ let prefix = Buffer.from(row).subarray(0, contentBytes).toString("utf8");
41354
+ prefix = prefix.replace(/\uFFFD$/, "");
41355
+ return prefix + pointer;
41356
+ }
41357
+ function fetchPointer(row, reason) {
41358
+ const id = /^\[message id: ([^\]\n]+)\]/.exec(row)?.[1];
41359
+ return id ? `
41360
+ [${reason}; call get_room_message with messageId "${id}" and offset 0 for the full text]` : `
41361
+ [${reason}; read the source conversation for the full text]`;
41362
+ }
41363
+
41364
+ // apps/body/dist/prompt-assembly.js
41365
+ function renderReplyContext(message) {
41366
+ if (!message.replyToMessageId)
41367
+ return "";
41368
+ const author = message.replyToAuthorName ?? message.replyToAuthorId ?? "unknown author";
41369
+ return [
41370
+ `Reply to message ${message.replyToMessageId} by ${JSON.stringify(author)}:`,
41371
+ ...message.replyToExcerpt !== void 0 ? [`> ${JSON.stringify(boundReplyExcerpt(message.replyToExcerpt))}`] : []
41372
+ ].join("\n");
41373
+ }
41374
+ var PROMPT_SURFACES = [
41375
+ "room",
41376
+ "dm",
41377
+ "code-corner",
41378
+ "repo-less-corner"
41379
+ ];
41380
+ var CORNERS = ["code-corner", "repo-less-corner"];
41381
+ var EVERYWHERE = PROMPT_SURFACES;
41382
+ function cornerHasRepositoryWork(brief) {
41383
+ if (!brief)
41651
41384
  return false;
41652
- if (rawInput && typeof rawInput === "object" && !Array.isArray(rawInput)) {
41653
- const call = rawInput;
41654
- if (typeof call.server === "string" && typeof call.tool === "string")
41655
- return true;
41385
+ if (brief.repositoryWork !== void 0)
41386
+ return brief.repositoryWork;
41387
+ const nonGoalsSections = [
41388
+ ...brief.spec.matchAll(/^## (?:Non-goals|Not doing)\s*\n([\s\S]*?)(?=^## |$(?![\s\S]))/gim)
41389
+ ].map((match) => match[1] ?? "");
41390
+ const prohibition = /^(?:repository edits?|repository changes?|pull requests?|(?:edit|modify|change) (?:the )?(?:repository|repo|code)|push(?:es)?|commit(?:s)?)[.!;]*$/i;
41391
+ for (const nonGoals of nonGoalsSections) {
41392
+ for (const line of nonGoals.split("\n")) {
41393
+ const bullet = /^\s*[-*]\s+/.test(line);
41394
+ const clause = line.trim().replace(/^[-*]\s+/, "");
41395
+ const negative = /^(?:no|never|do not|forbid(?:s|den)?)\s+/i;
41396
+ if (!bullet && !negative.test(clause))
41397
+ continue;
41398
+ const items = clause.replace(negative, "").split(/,\s*|\s+(?:and|or)\s+/i);
41399
+ if (items.some((item) => prohibition.test(item.trim())))
41400
+ return false;
41401
+ }
41656
41402
  }
41657
- if (shellPayload(toolCall))
41403
+ const assigned = brief.spec.match(/^## Assigned files\s*\n([\s\S]*?)(?=^## |$(?![\s\S]))/mi)?.[1];
41404
+ if (assigned !== void 0 && (!assigned.trim() || /^(?:\s*[-*]?\s*)(?:\(?none\)?|no assigned files)\b/i.test(assigned)))
41658
41405
  return false;
41659
- const title = toolCall?.title?.trim() ?? "";
41660
- if (/^mcp__[^_].*__[^_]/.test(title))
41661
- return true;
41662
- if (/^mcp\.[^.]+\.[^.]/.test(title))
41663
- return true;
41664
- if (resolveMountedMcpToolCall(request, mountedServers))
41665
- return true;
41666
- return isReadOnlyMcpPermissionRequest(request) || isBeelineAgentMcpPermissionRequest(request);
41406
+ return true;
41667
41407
  }
41668
- var AGENT_SURFACE_TOOL_NAMES = [
41669
- "get_avatar",
41670
- "set_avatar",
41671
- "open_corner",
41672
- "pr_checks_status",
41673
- "post_artifact",
41674
- "write_scratch_file",
41675
- "fetch_image",
41676
- "connect_app"
41677
- ];
41678
- function isHostMcpPermissionRequest(request, hostServers = CODE_OWNED_HOST_MCP_NAMES) {
41679
- const rawInput = request.toolCall?.rawInput;
41680
- if (rawInput && typeof rawInput === "object" && !Array.isArray(rawInput)) {
41681
- const server = rawInput.server;
41682
- if (typeof server === "string" && isHostMcpIdentity(server, hostServers))
41683
- return true;
41408
+ var CORNER_RUNTIME_AUTHOR_CONTRACT = `The current brief's spec \u2014 stories, criteria, non-goals, risks \u2014 defines scope and done. The human approval quote wins any conflict. The short objective is navigation-only, never scope.
41409
+ Meet every story; respect every non-goal and risk.
41410
+ Before any code or build action, the brief must hold this ask's outline; opening and prompting inside it is not that outline. When it has none or the ask changed, read the revision and write it with revise_corner_brief first; chat never revises it.
41411
+ Follow the beeline-triage skill's bugfix execution contract when the spec or its approval quote reports a defect.
41412
+ Reproduce as triage isolated it with available emulator, Playwright, browser and test runner. Record attempts and observations. If a reproduction is obtained, record it under Reproduction <id>, reusing triage's identifier when it recorded one. If reproduction fails, warn and continue; never stop and never condition the fix on reproduction.
41413
+ Fix only the spec and approval quote. With a reproduction, change only what removes it and meets its user stories.
41414
+ Deliver files with post_artifact; answer plain chat questions directly.
41415
+ No unrequested features, flags, compatibility shims or refactors.`;
41416
+ var CORNER_AUTHOR_CONTRACT = `The current brief's spec \u2014 stories, criteria, non-goals, risks \u2014 defines scope and done. The human approval quote wins any conflict. The short objective is navigation-only, never scope.
41417
+ Meet every story; respect every non-goal and risk. Use its file manifest.
41418
+ Before any code or build action, the brief must hold this ask's outline; opening and prompting inside it is not that outline. When it has none or the ask changed, read the revision and write it with revise_corner_brief first; chat never revises it.
41419
+ Follow the beeline-triage skill's bugfix execution contract when the spec or its approval quote reports a defect.
41420
+ Reproduce as triage isolated it with available emulator, Playwright, browser and test runner. Record attempts and observations. If a reproduction is obtained, record it under Reproduction <id>, reusing triage's identifier when it recorded one. If reproduction fails, warn and continue; never stop and never condition the fix on reproduction.
41421
+ Fix only the spec and approval quote. With a reproduction, change only what removes it and meets its user stories.
41422
+ Only when the brief calls for repository changes, follow the PR procedure below. Otherwise do not commit, push or open a PR; deliver with post_artifact.
41423
+ Before the PR, demonstrate Y: run the built app or affected service and perform X. If no interactive surface is reachable, run the narrowest test or script exercising the exact user path and printing Y. An inner-function unit test, log line or code read is not a demonstration.
41424
+ The PR body MUST have ## Reproduced and ## Demonstrated. Cite Reproduction <id> in both, with steps, the wrong result and the now-passing result. In ## Demonstrated, when none was obtained, state that plainly ("not obtained") and show the regression; for feature work say "not a defect report".
41425
+ No unrequested features, flags, compatibility shims or refactors.`;
41426
+ var CORNER_REVIEWER_SESSION_INSTRUCTION = "You are this Room's configured reviewer. A review turn prompt names the latest stable green PR head. Review and approve only that exact head; if no stable green head is named, end the turn without a verdict. Never merge a pull request you review.";
41427
+ var CORNER_REVIEWER_UNSTABLE_HEAD_INSTRUCTION = "There is no stable green PR head for this active reviewer turn. Do not review or call approve_merge. End this turn without a verdict; the next green transition will wake you.";
41428
+ var CORNER_DELIVERY_NUDGE = "Only when you write code in this corner and the brief calls for repository changes, inspect the repository state and finish delivering the work unless a human hold stands: commit and push the intended changes and open the pull request if one does not exist. Decide yourself whether any remaining dirty work belongs to the brief; do not discard it merely to make the worktree clean. The pull request body must carry ## Reproduced and ## Demonstrated; if they are missing, add them before ending the turn. Otherwise do not commit, push or open a PR; deliver with post_artifact.";
41429
+ var CORNER_LAND_RULE = "Then, if a human's question or proposal in the corner has no answer, name it and do not merge; otherwise call merge_corner.";
41430
+ var CORNER_CHECKS_NUDGE = `Call pr_checks_status now. If checks="failed" and you write code in this corner, fix the failure and push. If checks="unknown", reply in this corner with the tool reason and stop instead of retrying. Otherwise stop: a reviewer's PASS wakes you to merge with merge_corner. Never merge it any other way.`;
41431
+ var handle = (value) => value.replace(/^@/, "");
41432
+ function cornerMergeInstruction(reviewerHandle) {
41433
+ if (!reviewerHandle)
41434
+ return "Once the pull request exists, reply with its full URL and end the turn; do not check or wait for CI. This Room has no configured reviewer, so it merges only on a person's yes: ask a Workspace owner or admin to approve the merge, then record their yes with order_corner_merge. You are woken to fix failing checks or a merge GitHub refused.";
41435
+ const reviewer = `the reviewer (${handle(reviewerHandle)})`;
41436
+ return `Once the pull request exists, reply with its full URL and end the turn; do not tag ${reviewer}, check, or wait for CI. Green checks wake ${reviewer}; its PASS wakes you to merge. ${CORNER_LAND_RULE} You are also woken to fix failing checks, requested changes, or a refused merge; a fixed head needs green checks and a fresh yes.`;
41437
+ }
41438
+ function cornerReviewerInstruction(input) {
41439
+ if (!input.isReviewer)
41440
+ return void 0;
41441
+ const author = input.authorHandle ? handle(input.authorHandle) : void 0;
41442
+ const number = input.pullRequestNumber ?? "N";
41443
+ const headSha = input.headSha ?? "<head sha>";
41444
+ return `Checks are green on PR #${number} at ${headSha}${input.briefRevision ? ` with assigned brief revision ${input.briefRevision}` : ""}. Review it now with the beeline-review skill against that exact head and current assigned brief. FAIL: ${author ? `reply \`@${author}\`` : "tag the agent that wrote this head"} with the confirmed findings and the reviewed head ${headSha} to fix. PASS: call the approve_merge tool for ${headSha}, then reply \`approved ${headSha}\` without tagging ${author ?? "the author"}: your PASS wakes the author to merge, so a tag adds nothing. A human's question or proposal in the corner discussion with no answer is a FAIL finding; name it. Never merge yourself or tell the author to merge. Never say you are holding or waiting for checks.`;
41445
+ }
41446
+ var CORNER_REVIEWER_AUTHOR_MERGE = "You are this Room's reviewer, so no agent can review your own code; a person is your reviewer. Once the pull request exists, reply with its full URL, tag the person who assigned you this work, and ask them to approve the merge; only a Workspace owner or admin's yes counts. Record their yes with order_corner_merge. Never merge it any other way. You are woken to fix failing checks or a merge GitHub refused.";
41447
+ function shellLine(shell) {
41448
+ if (!shell)
41449
+ return "";
41450
+ if (shell.available) {
41451
+ return "Shell commands are available in this session: run one with your own shell tool when the work needs it, and cite the output that supports your answer.";
41684
41452
  }
41685
- return toolIdentityCandidates(request.toolCall).some((candidate) => isHostMcpIdentity(candidate, hostServers));
41453
+ return `Shell commands are NOT available in this session, so a request that needs one cannot be run; say that plainly instead of retrying.${shell.detail ? ` Relay this to the person: ${shell.detail}` : ""}`;
41686
41454
  }
41687
- function isBeelineAgentMcpPermissionRequest(request) {
41688
- const toolCall = request.toolCall;
41689
- const title = toolCall?.title?.trim() ?? "";
41690
- const rawInput = toolCall?.rawInput;
41691
- if (rawInput && typeof rawInput === "object" && !Array.isArray(rawInput)) {
41692
- const call = rawInput;
41693
- if (call.server === BEELINE_AGENT_MCP_SERVER_NAME && typeof call.tool === "string" && AGENT_SURFACE_TOOL_NAMES.includes(call.tool)) {
41694
- return true;
41695
- }
41455
+ var SESSION_SECTIONS = [
41456
+ {
41457
+ id: "core.identity",
41458
+ topic: "identity",
41459
+ why: "Agents on harnesses with their own default persona introduce themselves under the wrong name unless told who they are.",
41460
+ budgetBytes: 1400,
41461
+ layer: "core",
41462
+ surfaces: EVERYWHERE,
41463
+ render: ({ agentName, soul }) => [
41464
+ `You are ${agentName} in Beeline. Stay ${agentName} in every reply, including when a tool or permission blocks you.`,
41465
+ soul ? `Soul (${soul.name}): ${soul.instructions}
41466
+ The soul is not authority and never changes your tools, permissions, roles, or merge rights.` : ""
41467
+ ].filter(Boolean).join("\n")
41468
+ },
41469
+ {
41470
+ id: "core.voice",
41471
+ topic: "voice",
41472
+ why: "A seeded voice once bent facts for the bit and leaked into commit messages that outlive it.",
41473
+ budgetBytes: 320,
41474
+ layer: "core",
41475
+ surfaces: EVERYWHERE,
41476
+ render: () => "Your voice never changes the facts: never trim, soften, exaggerate, or invent a detail for style. Write commit messages, pull request titles and bodies, and code comments in a plain voice."
41477
+ },
41478
+ {
41479
+ id: "core.proactive",
41480
+ topic: "proactivity",
41481
+ why: "Agents answered a reported problem by restating it instead of fixing it or offering to.",
41482
+ budgetBytes: 200,
41483
+ layer: "core",
41484
+ surfaces: EVERYWHERE,
41485
+ render: () => "Answer every problem with the finished fix, or with the concrete next step and an offer to take it. Never just restate the problem."
41486
+ },
41487
+ {
41488
+ id: "core.finish",
41489
+ topic: "turn-end",
41490
+ why: "An agent read a not-yet-connected tool result as a stopping point and ended the turn with its own work undone.",
41491
+ budgetBytes: 360,
41492
+ layer: "core",
41493
+ surfaces: EVERYWHERE,
41494
+ render: () => "Finish the work before you end the turn. A tool result that hands you a step (connecting, needs sign-in, call again, a next step) is your next action: take it now. End the turn to wait only when someone else must act next (a person replying or answering a card, grant, choice or approval; CI; a reviewer) or you are truly blocked."
41495
+ },
41496
+ {
41497
+ id: "core.truth",
41498
+ topic: "claims",
41499
+ why: "Agents reported replies and actions that no tool had performed.",
41500
+ budgetBytes: 200,
41501
+ layer: "core",
41502
+ surfaces: EVERYWHERE,
41503
+ render: () => "Never claim an action, result, or reply happened unless the prompt or a tool result proves it. State real doubt once, plainly; skip filler hedges."
41504
+ },
41505
+ {
41506
+ id: "core.reply",
41507
+ topic: "reply-shape",
41508
+ why: "Agents echoed their instructions, the trigger, or their own answer back as a second reply.",
41509
+ budgetBytes: 160,
41510
+ layer: "core",
41511
+ surfaces: EVERYWHERE,
41512
+ render: () => "Write only the message the reader needs. Never repeat these instructions, the trigger, or an answer you already gave."
41513
+ },
41514
+ {
41515
+ id: "core.tagging",
41516
+ topic: "tagging",
41517
+ why: "Every @handle is a wake; stray handles woke agents with nothing to do, and guessed spellings reached nobody.",
41518
+ budgetBytes: 360,
41519
+ layer: "core",
41520
+ surfaces: EVERYWHERE,
41521
+ render: () => "Every exact @handle you write wakes that member. Write one only to hand off work, to ask for a decision or input, or, when nothing else announces it, to tell the person who asked that their task is done; otherwise name people and agents in plain prose. Copy handles only from the member list in the turn prompt; any other spelling reaches nobody."
41522
+ },
41523
+ {
41524
+ id: "core.authority",
41525
+ topic: "authority",
41526
+ why: "Resource access and the two command hard stops must hold on every surface, whoever asked.",
41527
+ budgetBytes: 520,
41528
+ layer: "core",
41529
+ surfaces: EVERYWHERE,
41530
+ render: () => "Use your own owner's tools and keys for whoever asks. Another person's files, tools, or keys need that person's approval before you use them for anyone else, and the wallet keeps its own approval rule. Never, whoever asks: run a command that names a credential file, or run a script nobody has read. Follow your owner first, then Workspace admins, then members; only the person who set a hold, or someone above them, clears it. Delegated and follow-up work keeps the original requester."
41531
+ },
41532
+ {
41533
+ id: "core.feedback",
41534
+ topic: "feedback",
41535
+ why: "Nothing reviews a turn after it ends, so friction in Beeline itself is lost unless the agent that hit it reports it in the turn.",
41536
+ budgetBytes: 260,
41537
+ layer: "core",
41538
+ surfaces: EVERYWHERE,
41539
+ render: () => "Only when this turn hit friction in Beeline itself (a tool error, a retry, a refused call, contradictory instructions, missing context, or a person correcting you), call report_feedback once before ending the turn. Otherwise do not."
41540
+ },
41541
+ {
41542
+ id: "core.files",
41543
+ topic: "files",
41544
+ why: "Files on earlier messages are shown only as markers and never pre-downloaded; agents fetched media URLs instead.",
41545
+ budgetBytes: 160,
41546
+ layer: "core",
41547
+ surfaces: EVERYWHERE,
41548
+ render: () => "For a \u{1F4CE} file, call download_attachment and read its path. Never fetch media URLs; on failure, say you couldn't open it."
41549
+ },
41550
+ {
41551
+ id: "surface.tools",
41552
+ topic: "tools",
41553
+ why: "Agents need app discovery before use in every turn; connector offers remain in Rooms and DMs.",
41554
+ budgetBytes: 240,
41555
+ layer: "surface",
41556
+ surfaces: EVERYWHERE,
41557
+ render: ({ surface }) => surface === "room" || surface === "dm" ? "When you need a tool you do not have, call workbench_status; offer_connector the tool it lists, otherwise call connect_app for the app." : "In corners, workbench_status discovers apps; connect_app starts sign-in here. Connector offers stay in Rooms and DMs."
41558
+ },
41559
+ {
41560
+ id: "surface.squire-drive",
41561
+ topic: "squire-browser",
41562
+ why: "Agents rarely called operate_drive and instead hand-drove checkouts/signups one flat step at a time, including after a drive handback (ts-drive-adoption-scout).",
41563
+ budgetBytes: 420,
41564
+ layer: "surface",
41565
+ surfaces: EVERYWHERE,
41566
+ render: () => "For a multi-step browser goal (signup, checkout, provisioning), start with Trusty Squire operate_drive {url, goal, facts} rather than individual steps. On a low_confidence or needs_value handback, answer it from the offered options or take one flat step (operate_click, operate_type, operate_select, \u2026), then resume operate_drive on that same session instead of finishing the task by hand."
41567
+ },
41568
+ // --- Room ----------------------------------------------------------------
41569
+ {
41570
+ id: "room.place",
41571
+ topic: "place",
41572
+ why: "The agent must know it cannot write here, where the checkout is, and whether it has a shell.",
41573
+ budgetBytes: 700,
41574
+ layer: "surface",
41575
+ surfaces: ["room"],
41576
+ render: ({ repository, shell }) => [
41577
+ `This is a Beeline Room. The repository filesystem is read-only here.${repository ? ` The Room is bound to ${repository.name} (branch ${repository.branch}); the read-only checkout is at the session root.` : ""} Web search is available.`,
41578
+ shellLine(shell),
41579
+ shell?.available === false ? "Read code with CodeGraph when available, then beeline-readonly-mcp search_text and read_file." : "If a shell command is refused, say so plainly rather than retrying, and read code with CodeGraph when available, then beeline-readonly-mcp search_text and read_file."
41580
+ ].filter(Boolean).join(" ")
41581
+ },
41582
+ {
41583
+ id: "room.corners",
41584
+ topic: "repository-work",
41585
+ why: "Repository work must start through the host-governed corner, with a brief as its authority.",
41586
+ budgetBytes: 1100,
41587
+ layer: "surface",
41588
+ surfaces: ["room"],
41589
+ render: () => [
41590
+ "Repository changes happen only in a corner. In a Room, the finished fix for a code problem is the exact change plus an offer to open a corner for it; open one when a person asks for the change.",
41591
+ "Before opening one, consult beeline-triage and beeline-spec, then call open_corner with a name of at most three words joined by hyphens (no whitespace), an objective of at most 24 words, and the brief (a spec plus the approving message). The objective only titles the work; the brief is its authority. When open_corner succeeds the server posts the corner card: do not restate it.",
41592
+ "For corners you belong to, use inspect_corner for status, steer_corner to pass Room input down, and ask_corner for one answer. Never post into a corner without a Room command.",
41593
+ "A corner ends when its pull request merges; since a person may merge minutes after it opens, anything still needed must already live in a different corner before that pull request opens, not after. A request covering several items is one corner per item, opened as work reaches each one, never one corner returned to for a later item."
41594
+ ].join(" ")
41595
+ },
41596
+ {
41597
+ id: "room.replying",
41598
+ topic: "room-replies",
41599
+ why: "Several agents share a Room; replies with nothing to act on, and echoed nudges, are noise.",
41600
+ budgetBytes: 520,
41601
+ layer: "surface",
41602
+ surfaces: ["room"],
41603
+ render: () => "If nothing is actionable for you, do not reply. If the current task is only a nudge to respond, answer the most recent unanswered human message instead of echoing the nudge. A poll plurality is a fact, never permission to deploy, delete, merge, or spend. Consult the using-beeline skill for Room mechanics (files, artifacts, schedules, reactions, events, mocks)."
41604
+ },
41605
+ // --- DM ------------------------------------------------------------------
41606
+ {
41607
+ id: "dm.place",
41608
+ topic: "place",
41609
+ why: "A DM has one person, no repository work, and no corners; the agent must not tag or try to open one.",
41610
+ budgetBytes: 700,
41611
+ layer: "surface",
41612
+ surfaces: ["dm"],
41613
+ render: ({ shell }) => [
41614
+ "This is a private direct message with one person: everything they send is for you, so reply without tagging. It has no repository work and no corners, and the filesystem is read-only. Web search is available.",
41615
+ shellLine(shell)
41616
+ ].filter(Boolean).join(" ")
41617
+ },
41618
+ {
41619
+ id: "dm.choices",
41620
+ topic: "choices",
41621
+ why: "A pick in a DM is a preference, never authority, and polls need an electorate.",
41622
+ budgetBytes: 300,
41623
+ layer: "surface",
41624
+ surfaces: ["dm"],
41625
+ render: () => "ask_choice offers options; a pick is a preference, never sandbox, spend, or merge authority. Consult the using-beeline skill for files, artifacts, and reactions."
41626
+ },
41627
+ // --- Corners -------------------------------------------------------------
41628
+ {
41629
+ id: "corner.steering",
41630
+ topic: "corner-steering",
41631
+ why: "Active corner turns can steer member siblings without borrowing parent Room authority.",
41632
+ budgetBytes: 160,
41633
+ layer: "surface",
41634
+ surfaces: ["code-corner", "repo-less-corner"],
41635
+ render: () => "Use steer_corner during a turn to send input to a member sibling under this parent. Membership alone grants no turn."
41636
+ },
41637
+ {
41638
+ id: "corner.handover",
41639
+ topic: "corner-handover",
41640
+ why: "An agent asked to hand the corner on opened a duplicate sibling corner instead of tagging the other agent.",
41641
+ budgetBytes: 360,
41642
+ layer: "surface",
41643
+ surfaces: ["code-corner", "repo-less-corner"],
41644
+ render: () => "When a person asks you to hand this corner to another agent, tag in your reply only an agent that person named by handle or display name: it moves the implementer role here. A tag for any other reason does not move the role. Do not open a second corner for a hand-over."
41645
+ },
41646
+ {
41647
+ id: "corner.worktree",
41648
+ topic: "place",
41649
+ why: "Every agent in a code corner must know its branch and target, whether or not it writes code.",
41650
+ budgetBytes: 200,
41651
+ layer: "surface",
41652
+ surfaces: ["code-corner"],
41653
+ render: ({ worktree }) => `You are in an isolated git worktree on ${worktree?.featureBranch ?? "the feature branch"}, targeting ${worktree?.targetBranch ?? "the target branch"}.`
41654
+ },
41655
+ {
41656
+ id: "corner.android",
41657
+ topic: "android",
41658
+ why: "An agent under its isolated $HOME concluded the host had no emulator, and agents on the shared adb server drove each other's emulators.",
41659
+ budgetBytes: 380,
41660
+ layer: "surface",
41661
+ surfaces: ["code-corner"],
41662
+ when: ({ android }) => Boolean(android),
41663
+ render: ({ android }) => `Android: adb and emulator are on PATH; the SDK is $ANDROID_HOME, AVDs are in $ANDROID_AVD_HOME, and /dev/kvm works. Your adb server sees only your own emulator. Boot it with \`emulator -avd <name> -port ${android?.emulatorPort} -read-only -no-window -no-snapshot -gpu swiftshader_indirect\`; it reaches host services at 10.0.2.2.`
41664
+ },
41665
+ {
41666
+ id: "corner.implementer",
41667
+ topic: "implementer-condition",
41668
+ why: "A reviewer that opened its corner was given push steps and pushed a fix (PR #2224); only the agent knows whether it was told to write code, so the author rules are conditional.",
41669
+ budgetBytes: 400,
41670
+ layer: "surface",
41671
+ surfaces: ["code-corner"],
41672
+ render: ({ reviewer }) => `Implementer rules: the rules below${reviewer ? ", up to the reviewer rules," : ""} apply only when the brief or a person in this corner told you to write code or do this corner's work. A hand-over of the work to another agent ends that. Otherwise do not commit, push, or open a pull request.`
41673
+ },
41674
+ {
41675
+ id: "corner.prepare",
41676
+ topic: "corner-prepare",
41677
+ why: "Only a checkout that defines the command can run it; another repository fails on its first step.",
41678
+ budgetBytes: 60,
41679
+ layer: "surface",
41680
+ surfaces: ["code-corner"],
41681
+ when: ({ brief, cornerPrepareScript }) => cornerHasRepositoryWork(brief) && cornerPrepareScript === true,
41682
+ render: () => "Run npm run corner:prepare first."
41683
+ },
41684
+ {
41685
+ id: "corner.push",
41686
+ topic: "push",
41687
+ why: "The author must push only its own branch and rebase before each push, or it overwrites or loses work.",
41688
+ budgetBytes: 800,
41689
+ layer: "surface",
41690
+ surfaces: ["code-corner"],
41691
+ when: ({ brief }) => cornerHasRepositoryWork(brief),
41692
+ render: ({ worktree, githubCli }) => githubCli === "rest" ? `Only when the brief calls for repository changes: commit and push only ${worktree?.featureBranch}; never force-push or write to ${worktree?.targetBranch}. Before pushing, run \`git ls-remote --exit-code origin ${worktree?.featureBranch}\`. Exit code 0: rebase on origin/${worktree?.featureBranch}; resolve conflicts autonomously, realigning to that remote branch and redoing the work if needed, then rerun affected tests. Exit code 2: the branch is absent; skip rebase for the first push. Any other non-zero exit is a lookup failure: stop and retry; do not skip rebase or push. Open it with \`gh pr create\` and read it with \`gh pr view\`; this host has no gh, so the Beeline launcher answers both over the GitHub REST API with the app token.` : `Only when the brief calls for repository changes: commit and push only ${worktree?.featureBranch}; never force-push or write to ${worktree?.targetBranch}. Before pushing, run \`git ls-remote --exit-code origin ${worktree?.featureBranch}\`. Exit code 0: rebase on origin/${worktree?.featureBranch}; resolve conflicts autonomously, realigning to that remote branch and redoing the work if needed, then rerun affected tests. Exit code 2: the branch is absent; skip rebase for the first push. Any other non-zero exit is a lookup failure: stop and retry; do not skip rebase or push. Open the pull request with gh.`
41693
+ },
41694
+ {
41695
+ id: "corner.contract",
41696
+ topic: "author-contract",
41697
+ why: "The reviewer enforces this contract; an author that never saw it fails review.",
41698
+ budgetBytes: 2600,
41699
+ layer: "surface",
41700
+ surfaces: ["code-corner"],
41701
+ render: ({ brief }) => cornerHasRepositoryWork(brief) ? CORNER_AUTHOR_CONTRACT : CORNER_RUNTIME_AUTHOR_CONTRACT
41702
+ },
41703
+ {
41704
+ id: "corner.checks",
41705
+ topic: "checks-turns",
41706
+ why: "Server check notes were restated as replies, and agents scheduled polls of a gate that wakes them itself.",
41707
+ budgetBytes: 900,
41708
+ layer: "surface",
41709
+ surfaces: ["code-corner"],
41710
+ when: ({ brief }) => cornerHasRepositoryWork(brief),
41711
+ render: () => "When asked whether the reviewer was woken, call pr_checks_status and report reviewerWake; do not invent a cause. Never schedule polls of pr_checks_status or the merge gate; the server wakes you when it changes."
41712
+ },
41713
+ {
41714
+ id: "corner.merge",
41715
+ topic: "merge",
41716
+ why: 'Exactly one rule may say who merges and when; two rules here once said both "merge" and "never merge".',
41717
+ budgetBytes: 520,
41718
+ layer: "surface",
41719
+ surfaces: ["code-corner"],
41720
+ when: ({ brief }) => cornerHasRepositoryWork(brief),
41721
+ render: ({ reviewerHandle, reviewer }) => (reviewer ? CORNER_REVIEWER_AUTHOR_MERGE : cornerMergeInstruction(reviewerHandle)) + " Never merge any other pull request."
41722
+ },
41723
+ {
41724
+ id: "corner.review",
41725
+ topic: "review",
41726
+ why: "The reviewer approves one exact green head, never merges it, and returns defects instead of pushing them.",
41727
+ budgetBytes: 450,
41728
+ layer: "surface",
41729
+ surfaces: ["code-corner"],
41730
+ when: ({ reviewer }) => reviewer === true,
41731
+ render: () => `Reviewer rules: ${CORNER_REVIEWER_SESSION_INSTRUCTION} Report findings to the author; never edit or push another agent's branch.`
41732
+ },
41733
+ {
41734
+ id: "corner.repo-less",
41735
+ topic: "place",
41736
+ why: "A repo-less corner has no repository; the agent delivers files, not commits.",
41737
+ budgetBytes: 1e3,
41738
+ layer: "surface",
41739
+ surfaces: ["repo-less-corner"],
41740
+ render: () => [
41741
+ "This is a repo-less corner with no repository checkout and no GitHub workflow. Work in this corner's writable workspace: create files with write_scratch_file or your own tools, then send them with post_artifact.",
41742
+ "Do not initialize a repository, create a branch, commit, push, open a pull request, or wait for GitHub checks."
41743
+ ].join("\n")
41744
+ },
41745
+ {
41746
+ id: "corner.repo-less-reply",
41747
+ topic: "reply-target",
41748
+ why: "Every turn was told to end with a one-line summary of files it posted, so answers were followed by a restatement of themselves.",
41749
+ budgetBytes: 260,
41750
+ layer: "surface",
41751
+ surfaces: ["repo-less-corner"],
41752
+ render: ({ requesterHandle }) => `${requesterHandle ? `Tag @${handle(requesterHandle)} once, when the deliverable is posted or you need their input. ` : ""}When you posted files, name them in one line; never restate your answer. Only a human closes this corner.`
41696
41753
  }
41697
- if (shellPayload(toolCall))
41698
- return false;
41699
- const normalized = BEELINE_AGENT_MCP_SERVER_NAME.replaceAll("-", "_");
41700
- return AGENT_SURFACE_TOOL_NAMES.flatMap((tool) => [
41701
- `mcp__${BEELINE_AGENT_MCP_SERVER_NAME}__${tool}`,
41702
- `mcp__${normalized}__${tool}`,
41703
- `mcp.${BEELINE_AGENT_MCP_SERVER_NAME}.${tool}`
41704
- ]).includes(title);
41705
- }
41706
- function isMcpShapedPermissionRequest(request, hostServers = CODE_OWNED_HOST_MCP_NAMES) {
41707
- if (isHostMcpPermissionRequest(request, hostServers))
41708
- return true;
41709
- const record5 = permissionRecord(request.toolCall?.rawInput);
41710
- if (typeof record5?.server === "string")
41711
- return true;
41712
- if (DISPATCHER_TOOL_NAME_KEYS.some((key) => typeof record5?.[key] === "string"))
41713
- return true;
41714
- const title = request.toolCall?.title?.trim() ?? "";
41715
- return /^mcp__[^_].*__[^_]/.test(title) || /^mcp\.[^.]+\.[^.]/.test(title);
41754
+ ];
41755
+ function applicable(sections, context) {
41756
+ return sections.filter((section) => section.surfaces.includes(context.surface) && (section.when?.(context) ?? true)).map((section) => ({ section, text: section.render(context).trim() })).filter((entry) => entry.text);
41716
41757
  }
41717
-
41718
- // apps/body/dist/room-session.js
41719
- var ReadOnlyToolsUnavailableError = class extends Error {
41720
- name = "ReadOnlyToolsUnavailableError";
41721
- };
41722
- function beelineAgentMcpServer(config, api, context) {
41723
- if (!config.readonlyMcpCommand) {
41724
- throw new ReadOnlyToolsUnavailableError("agent tools unavailable: the Beeline MCP command is required");
41725
- }
41726
- const connection = api.connection();
41727
- return {
41728
- name: BEELINE_AGENT_MCP_SERVER_NAME,
41729
- command: config.readonlyMcpCommand,
41730
- args: [...config.readonlyMcpArgs ?? []],
41731
- env: [
41732
- { name: "BEELINE_MCP_SURFACE", value: "agent" },
41733
- ...context.turnContextPath ? [{ name: "BEELINE_TURN_CONTEXT_FILE", value: context.turnContextPath }] : [],
41734
- ...context.directMessage ? [{ name: "BEELINE_AGENT_DM", value: "1" }] : [],
41735
- ...context.squireRelay ? [
41736
- { name: "BEELINE_SQUIRE_RELAY_URL", value: context.squireRelay.url },
41737
- { name: "BEELINE_SQUIRE_RELAY_TOKEN", value: context.squireRelay.token }
41738
- ] : [],
41739
- { name: "BEELINE_DAEMON_BASE_URL", value: connection.baseUrl },
41740
- { name: "BEELINE_DAEMON_TOKEN", value: connection.daemonToken },
41741
- { name: "BEELINE_HELPER_VERSION", value: connection.helperVersion },
41742
- { name: "BEELINE_DAEMON_AGENT_ID", value: connection.agentId },
41743
- { name: "BEELINE_OPERATOR_HOME", value: config.operatorHome ?? homedir16() },
41744
- { name: "BEELINE_DAEMON_ROOM_ID", value: context.roomId },
41745
- { name: "BEELINE_DAEMON_WORKSPACE_ID", value: context.workspaceId },
41746
- ...context.cornerId ? [{ name: "BEELINE_DAEMON_CORNER_ID", value: context.cornerId }] : [],
41747
- ...context.agentMayCloseCorner ? [{ name: "BEELINE_CORNER_AGENT_CLOSE", value: "1" }] : [],
41748
- ...context.agentMayUpgradeCorner ? [{ name: "BEELINE_CORNER_CAN_UPGRADE", value: "1" }] : [],
41749
- ...context.reviewer ? [{ name: "BEELINE_CORNER_REVIEWER", value: "1" }] : [],
41750
- ...context.cornerId && context.lane ? [{ name: "BEELINE_CORNER_LANE", value: context.lane }] : [],
41751
- ...context.attachRoot ? [{ name: "BEELINE_ATTACH_ROOT", value: context.attachRoot }] : [],
41752
- ...context.attachScratchRoot ? [{ name: "BEELINE_ATTACH_SCRATCH_ROOT", value: context.attachScratchRoot }] : [],
41753
- ...context.grantRunner ? [
41754
- { name: "BEELINE_GRANT_RUNNER_URL", value: context.grantRunner.url },
41755
- { name: "BEELINE_GRANT_RUNNER_TOKEN", value: context.grantRunner.token }
41756
- ] : []
41757
- ]
41758
- };
41758
+ function sessionSections(context) {
41759
+ return applicable(SESSION_SECTIONS, context);
41759
41760
  }
41760
- function readOnlyMcpServer(config, cwd, agentMemoryDir) {
41761
- if (!config.readonlyMcpCommand) {
41762
- throw new ReadOnlyToolsUnavailableError("read-only tools unavailable: beeline-readonly-mcp is required for Room sessions");
41763
- }
41764
- const skillDir = agentSkillDir(config.agentKind);
41761
+ function assembleSessionPrompt(context) {
41762
+ const entries = sessionSections(context);
41763
+ const systemPrompt = entries.map((entry) => entry.text).join("\n\n");
41765
41764
  return {
41766
- name: READ_ONLY_MCP_SERVER_NAME,
41767
- command: config.readonlyMcpCommand,
41768
- args: [...config.readonlyMcpArgs ?? []],
41769
- env: [
41770
- { name: "BEELINE_READONLY_ROOT", value: resolve32(cwd) },
41771
- ...config.agentHomeRoot ? [
41772
- {
41773
- name: "BEELINE_READONLY_AGENT_SKILLS_ROOT",
41774
- value: resolve32(config.agentHomeRoot, skillDir, "skills")
41775
- }
41776
- ] : [],
41777
- ...agentMemoryDir ? [{ name: "BEELINE_READONLY_AGENT_MEMORY_ROOT", value: resolve32(agentMemoryDir) }] : []
41778
- ]
41765
+ systemPrompt,
41766
+ turnPrefix: harnessHonorsSessionSystemPrompt(context.agentCommand) ? "" : systemPrompt,
41767
+ report: entries.map(({ section, text: text2 }) => ({
41768
+ id: section.id,
41769
+ bytes: Buffer.byteLength(text2)
41770
+ }))
41779
41771
  };
41780
41772
  }
41773
+ function renderAssignedCornerBrief(brief) {
41774
+ const approval = brief.approval ? `Approved by ${brief.approval.approverName}, message ${brief.approval.sourceMessageId}: ${brief.approval.text}` : "This revision predates recorded approvals.";
41775
+ return `Brief revision ${brief.revision}:
41776
+ ${brief.spec}
41781
41777
 
41782
- // apps/body/dist/institutional-context.js
41783
- import { performance as performance2 } from "node:perf_hooks";
41784
- var INSTITUTIONAL_CONTEXT_TIMEOUT_MS = 500;
41785
- var INSTITUTIONAL_MEMORY_LIVE_FLAG = "BEELINE_INSTITUTIONAL_MEMORY_ENABLED";
41786
- function institutionalMemoryFlagEnabled(env, flag) {
41787
- return env[flag] !== "false";
41788
- }
41789
- var EMPTY_INSTITUTIONAL_CONTEXT = {
41790
- snapshotRevision: 0,
41791
- text: "",
41792
- itemIds: [],
41793
- totalBytes: 0,
41794
- omitted: {}
41795
- };
41796
- var INSTITUTIONAL_CONTEXT_MISSED_TEXT = "institutional memory did not load this turn; use search_memory before concluding anything is not stored.";
41797
- function startInstitutionalContextFetch(api, roomId, enabled = institutionalMemoryFlagEnabled(process.env, INSTITUTIONAL_MEMORY_LIVE_FLAG), now2 = () => performance2.now()) {
41798
- const startedAt = now2();
41799
- const promise = enabled ? api.execute("getInstitutionalContext", { roomId }) : Promise.resolve(EMPTY_INSTITUTIONAL_CONTEXT);
41800
- promise.catch(() => {
41801
- });
41802
- return { promise, startedAt };
41778
+ ${approval}`;
41803
41779
  }
41804
- async function awaitInstitutionalContext(fetch2, log2 = console.warn, budgetMs = INSTITUTIONAL_CONTEXT_TIMEOUT_MS, now2 = () => performance2.now()) {
41805
- const remainingMs = budgetMs - (now2() - fetch2.startedAt);
41806
- let timer;
41807
- try {
41808
- const snapshot = await Promise.race([
41809
- fetch2.promise,
41810
- new Promise((_, reject) => {
41811
- timer = setTimeout(() => reject(new Error("institutional context timed out")), Math.max(0, remainingMs));
41812
- timer.unref?.();
41813
- })
41814
- ]);
41815
- return { ...snapshot, outcome: snapshot.text !== "" ? "served" : "empty" };
41816
- } catch (error) {
41817
- log2(`institutional context unavailable: ${error instanceof Error ? error.message : String(error)}`);
41818
- return {
41819
- ...EMPTY_INSTITUTIONAL_CONTEXT,
41820
- text: INSTITUTIONAL_CONTEXT_MISSED_TEXT,
41821
- outcome: "timed-out"
41822
- };
41823
- } finally {
41824
- if (timer)
41825
- clearTimeout(timer);
41780
+ function roomMentionDirectory(roster, selfId) {
41781
+ const rows = [];
41782
+ let selfHandle;
41783
+ for (const member of roster.members) {
41784
+ if (member.identityId === selfId) {
41785
+ selfHandle = member.handle?.trim().replace(/^@/, "");
41786
+ continue;
41787
+ }
41788
+ const memberHandle = member.handle?.trim().replace(/^@/, "");
41789
+ if (!memberHandle)
41790
+ continue;
41791
+ const name = member.name?.trim() ?? "";
41792
+ const kind = member.kind === "agent" ? "agent" : "person";
41793
+ rows.push(`- @${memberHandle}${name && name !== memberHandle ? ` \u2014 ${name}` : ""} (${kind})`);
41826
41794
  }
41795
+ if (selfHandle)
41796
+ rows.push(`You are @${selfHandle}, a member of this Room; you are not listed above.`);
41797
+ if (!rows.length)
41798
+ return "";
41799
+ return ["Members (exact tag spellings):", ...rows].join("\n");
41827
41800
  }
41801
+ var ROOMS = ["room", "dm"];
41802
+ var TURN_SECTIONS = [
41803
+ {
41804
+ id: "turn.session",
41805
+ topic: "session-delivery",
41806
+ why: "Codex, Pi, and Grok drop the ACP session prompt; without this they never saw the corner rules.",
41807
+ budgetBytes: 16e3,
41808
+ layer: "turn",
41809
+ surfaces: EVERYWHERE,
41810
+ render: ({ sessionPrefix }) => sessionPrefix ?? ""
41811
+ },
41812
+ {
41813
+ id: "turn.checks",
41814
+ topic: "checks-turns",
41815
+ why: "Only CI or merge-gate wakes suppress ordinary reporting; user schedules must deliver their task results.",
41816
+ budgetBytes: 350,
41817
+ layer: "turn",
41818
+ surfaces: ["code-corner"],
41819
+ // A reviewer's green-checks wake is a review turn, not a fix turn.
41820
+ when: ({ task, reviewerTarget }) => !reviewerTarget && task.commandReason !== "schedule" && (["corner_check", "corner_merge_refused", "corner_merge_conflict"].includes(task.commandReason ?? "") || ["check-passed", "check-failed"].includes(task.outsideEvent?.kind ?? "")),
41821
+ render: () => 'This is a checks turn about CI or the merge gate. On it, say nothing unless you push a fix or report checks="unknown", and then use one short line. Never restate server check or merge notes.'
41822
+ },
41823
+ {
41824
+ id: "turn.land",
41825
+ topic: "land-turn",
41826
+ why: "A reviewer's PASS hands the merge to the implementer, who must not merge over an unanswered human question.",
41827
+ budgetBytes: 300,
41828
+ layer: "turn",
41829
+ surfaces: ["code-corner"],
41830
+ when: ({ task }) => task.commandReason === "corner_land",
41831
+ render: () => `The reviewer approved this pull request's current head. ${CORNER_LAND_RULE}`
41832
+ },
41833
+ {
41834
+ id: "turn.objective",
41835
+ topic: "objective",
41836
+ why: "The objective locates the work but must never be mistaken for its authority.",
41837
+ budgetBytes: 600,
41838
+ layer: "turn",
41839
+ surfaces: CORNERS,
41840
+ render: ({ objective }) => objective ? `Corner objective (navigation only, not product authority): ${objective}` : ""
41841
+ },
41842
+ {
41843
+ id: "turn.rename",
41844
+ topic: "corner-rename",
41845
+ why: "A generated corner name says nothing about the work; only the server knows the name is still generated, so the ask rides only on those turns.",
41846
+ budgetBytes: 600,
41847
+ layer: "turn",
41848
+ surfaces: CORNERS,
41849
+ render: ({ generatedTitle, brief }) => generatedTitle ? `This human-opened corner still has its generated name, "${generatedTitle}". If the newest human message states the work, call rename_corner once before you reply with a name of at most three words joined by hyphens (no whitespace) and a short objective${brief ? "" : ", plus a brief grounded in that message with brief.approval.sourceMessageId set to the human message id"}. A bare mention does not state the work; leave the name until a message does.` : ""
41850
+ },
41851
+ {
41852
+ id: "turn.brief",
41853
+ topic: "brief",
41854
+ why: "The brief is the product authority for corner work; a corner without one says so once.",
41855
+ budgetBytes: 24e3,
41856
+ layer: "turn",
41857
+ surfaces: CORNERS,
41858
+ render: ({ brief, surface, generatedTitle }) => brief ? `${renderAssignedCornerBrief(brief.brief)}${cornerHasRepositoryWork(brief.brief) ? `
41828
41859
 
41829
- // apps/body/dist/codegraph.js
41830
- import { execFile as execFile11 } from "node:child_process";
41831
- import { access as access3, appendFile, mkdir as mkdir21, readFile as readFile28 } from "node:fs/promises";
41832
- import { dirname as dirname23, isAbsolute as isAbsolute3, resolve as resolve33 } from "node:path";
41833
- import { promisify as promisify6 } from "node:util";
41834
- var execFileAsync6 = promisify6(execFile11);
41835
- var CODEGRAPH_MCP_SERVER_NAME = "codegraph";
41836
- var CODEGRAPH_INDEX_PATH = ".codegraph/codegraph.db";
41837
- var CODEGRAPH_PREPARE_TIMEOUT_MS = 12e4;
41838
- function codegraphIndexDirectory(cwd) {
41839
- return resolve33(cwd, dirname23(CODEGRAPH_INDEX_PATH));
41840
- }
41841
- function codegraphMcpServer(config, cwd, options) {
41842
- if (!config.codegraphCommand)
41843
- return void 0;
41844
- return {
41845
- name: CODEGRAPH_MCP_SERVER_NAME,
41846
- command: config.codegraphCommand,
41847
- args: ["serve", "--mcp", "--path", resolve33(cwd), ...options.readonly ? ["--no-watch"] : []],
41848
- env: [
41849
- { name: "CODEGRAPH_TELEMETRY", value: "0" },
41850
- // One MCP child per harness session is easier to contain than a daemon
41851
- // that can outlive the Room/corner whose index and permissions it used.
41852
- { name: "CODEGRAPH_NO_DAEMON", value: "1" }
41853
- ]
41854
- };
41855
- }
41856
- function codegraphFingerprintServers(config, servers, mounted = Boolean(config.codegraphCommand)) {
41857
- return config.codegraphCommand && mounted ? [...servers, CODEGRAPH_MCP_SERVER_NAME] : [...servers];
41858
- }
41859
- async function isGitWorktree(cwd) {
41860
- try {
41861
- const { stdout: stdout6 } = await execFileAsync6("git", ["rev-parse", "--show-toplevel"], {
41862
- cwd,
41863
- timeout: 1e4
41864
- });
41865
- return resolve33(stdout6.trim()) === resolve33(cwd);
41866
- } catch {
41867
- return false;
41868
- }
41869
- }
41870
- async function excludeIndexFromGitStatus(cwd) {
41871
- try {
41872
- const { stdout: stdout6 } = await execFileAsync6("git", ["rev-parse", "--git-path", "info/exclude"], {
41873
- cwd,
41874
- timeout: 1e4
41875
- });
41876
- const gitPath = stdout6.trim();
41877
- if (!gitPath)
41878
- return;
41879
- const path = isAbsolute3(gitPath) ? gitPath : resolve33(cwd, gitPath);
41880
- await mkdir21(dirname23(path), { recursive: true });
41881
- const existing = await readFile28(path, "utf8").catch(() => "");
41882
- if (existing.split("\n").includes(".codegraph/"))
41883
- return;
41884
- await appendFile(path, `${existing && !existing.endsWith("\n") ? "\n" : ""}.codegraph/
41885
- `);
41886
- } catch {
41860
+ Assigned files:
41861
+ ${brief.fileLines.join("\n") || "(none)"}` : brief.fileLines.length ? `
41862
+
41863
+ Brief attachments:
41864
+ ${brief.fileLines.join("\n")}` : ""}${brief.missingRequiredFile ? "\nRequired assignment files are unavailable. Pause work that depends on them and report the missing file precisely." : ""}` : surface === "code-corner" ? `No assigned brief yet. Before any code, write the first brief from the newest human request with ${generatedTitle ? "rename_corner" : "revise_corner_brief at expectedRevision 0"}.` : "No assigned brief: the human messages in this corner are the authority; skip steps that need a brief revision."
41865
+ },
41866
+ {
41867
+ id: "turn.checkout",
41868
+ topic: "checkout",
41869
+ why: "A Room checkout is refreshed per turn; the agent must know which commit it reads.",
41870
+ budgetBytes: 200,
41871
+ layer: "turn",
41872
+ surfaces: ROOMS,
41873
+ render: ({ checkout }) => checkout ? `Repository checkout: origin/${checkout.branch} at commit ${checkout.commit}, refreshed for this turn.` : ""
41874
+ },
41875
+ {
41876
+ id: "turn.transcript",
41877
+ topic: "transcript",
41878
+ why: "The conversation so far, with rows this session already saw left out.",
41879
+ budgetBytes: 2e5,
41880
+ layer: "turn",
41881
+ surfaces: EVERYWHERE,
41882
+ render: ({ transcript, surface }) => {
41883
+ if (!transcript?.lines.length)
41884
+ return "";
41885
+ const place = ROOMS.includes(surface) ? "Room" : "corner";
41886
+ return `${transcript.sinceLastTurn ? `New in the ${place} since your last turn:` : `${place === "Room" ? "Room conversation" : "Corner transcript"}:`}
41887
+ ${transcript.lines.join("\n")}`;
41888
+ }
41889
+ },
41890
+ {
41891
+ id: "turn.resume",
41892
+ topic: "resume",
41893
+ why: "A card answer resumes a paused turn; the agent must know which question was answered.",
41894
+ budgetBytes: 600,
41895
+ layer: "turn",
41896
+ surfaces: ROOMS,
41897
+ render: ({ resume }) => resume ?? ""
41898
+ },
41899
+ {
41900
+ id: "turn.members",
41901
+ topic: "members",
41902
+ why: "Handle spellings change; only the live roster is a safe source.",
41903
+ budgetBytes: 4e3,
41904
+ layer: "turn",
41905
+ surfaces: EVERYWHERE,
41906
+ render: ({ members }) => members ?? ""
41907
+ },
41908
+ {
41909
+ id: "turn.memory",
41910
+ topic: "memory",
41911
+ why: "Keyword-matched institutional memory, compiled and capped by the server.",
41912
+ budgetBytes: 1e3,
41913
+ layer: "turn",
41914
+ surfaces: EVERYWHERE,
41915
+ render: ({ memory }) => memory ?? ""
41916
+ },
41917
+ {
41918
+ id: "turn.corners",
41919
+ topic: "corners",
41920
+ why: "Room and corner agents need exact member corner ids for supported relay actions.",
41921
+ budgetBytes: 8e3,
41922
+ layer: "turn",
41923
+ surfaces: ["room", "code-corner", "repo-less-corner"],
41924
+ render: ({ surface, corners, closedCorners }) => [
41925
+ corners?.length ? `Current corners you belong to (use the exact cornerId with ${surface === "room" ? "inspect_corner, steer_corner, or ask_corner" : "steer_corner for a sibling under this parent Room"}):
41926
+ ${JSON.stringify(corners)}` : "",
41927
+ surface === "room" && closedCorners?.length ? `Corners you belong to closed in the last 24 hours (merge commit is unavailable when absent):
41928
+ ${closedCorners.map((corner) => `- ${corner.name ?? corner.cornerId} (${corner.cornerId}): PR ${corner.pullRequestNumber ? `#${corner.pullRequestNumber}` : "unavailable"}, merge commit ${corner.mergeCommitSha ?? "unavailable"}`).join("\n")}` : ""
41929
+ ].filter(Boolean).join("\n\n")
41930
+ },
41931
+ {
41932
+ id: "turn.reviewer-target",
41933
+ topic: "reviewer-target",
41934
+ why: "The reviewer must review the head that is green now, not one named in an older message.",
41935
+ budgetBytes: 1200,
41936
+ layer: "turn",
41937
+ surfaces: ["code-corner"],
41938
+ render: ({ reviewerTarget }) => reviewerTarget ?? ""
41939
+ },
41940
+ {
41941
+ id: "turn.task",
41942
+ topic: "task",
41943
+ why: "The message this turn answers, said once and last.",
41944
+ budgetBytes: 64e3,
41945
+ layer: "turn",
41946
+ surfaces: EVERYWHERE,
41947
+ render: ({ task, surface }) => ROOMS.includes(surface) ? [
41948
+ `Current task from ${task.fromName ?? "a Room member"}:`,
41949
+ task.cornerAskId ? `Corner ask id: ${task.cornerAskId}. Use get_corner_ask to retrieve its status and answer.` : "",
41950
+ task.body,
41951
+ renderReplyContext(task.reply ?? {})
41952
+ ].filter(Boolean).join("\n\n") : [
41953
+ ...task.reactionTargetId ? [`Reaction target message id: ${task.reactionTargetId}`] : [],
41954
+ `Newest message:
41955
+ ${task.body}`,
41956
+ ...task.reply?.replyToMessageId ? [renderReplyContext(task.reply)] : [],
41957
+ ...task.attachmentLines ?? []
41958
+ ].join("\n")
41887
41959
  }
41888
- }
41889
- async function prepareCodegraphIndex(config, cwd) {
41890
- const command = config.codegraphCommand;
41891
- if (!command || !await isGitWorktree(cwd))
41892
- return false;
41893
- await excludeIndexFromGitStatus(cwd);
41894
- const indexPath = resolve33(cwd, CODEGRAPH_INDEX_PATH);
41895
- const indexed = await access3(indexPath).then(() => true, () => false);
41896
- const args = indexed ? ["sync", "--quiet", cwd] : ["init", "--yes", cwd];
41897
- try {
41898
- await execFileAsync6(command, args, {
41899
- cwd,
41900
- env: { ...process.env, CODEGRAPH_TELEMETRY: "0", CODEGRAPH_NO_DAEMON: "1" },
41901
- timeout: CODEGRAPH_PREPARE_TIMEOUT_MS,
41902
- maxBuffer: 4 * 1024 * 1024
41903
- });
41904
- return true;
41905
- } catch (error) {
41906
- console.warn(`[body] CodeGraph ${args[0]} failed for ${cwd}; continuing with Beeline read tools:`, error);
41907
- return false;
41960
+ ];
41961
+ function assembleTurnPrompt(context) {
41962
+ if (context.task.outsideEvent?.kind?.startsWith("webhook:")) {
41963
+ context = { ...context, task: { ...context.task, body: quoteOutsideData(context.task.outsideEvent.kind, context.task.outsideEvent.payload) } };
41908
41964
  }
41965
+ const boundedContext = ROOMS.includes(context.surface) ? {
41966
+ ...context,
41967
+ task: {
41968
+ ...context.task,
41969
+ body: boundRoomTaskBody(context.task.body, context.modelContextTokens)
41970
+ }
41971
+ } : context;
41972
+ const withoutTranscript = applicable(TURN_SECTIONS, {
41973
+ ...boundedContext,
41974
+ transcript: void 0
41975
+ });
41976
+ const otherBytes = withoutTranscript.reduce((sum, entry) => sum + Buffer.byteLength(entry.text) + 2, 0);
41977
+ const lines = budgetTranscript(boundedContext.transcript?.lines ?? [], boundedContext.modelContextTokens, otherBytes);
41978
+ const entries = applicable(TURN_SECTIONS, {
41979
+ ...boundedContext,
41980
+ transcript: boundedContext.transcript ? { ...boundedContext.transcript, lines } : void 0
41981
+ });
41982
+ return {
41983
+ text: entries.map((entry) => entry.text).join("\n\n"),
41984
+ report: entries.map(({ section, text: text2 }) => ({
41985
+ id: section.id,
41986
+ bytes: Buffer.byteLength(text2)
41987
+ }))
41988
+ };
41909
41989
  }
41910
41990
 
41911
- // apps/body/dist/monolith-room-turn.js
41912
- import { mkdir as mkdir23, writeFile as writeFile18 } from "node:fs/promises";
41913
- import { homedir as homedir17, hostname as hostname2 } from "node:os";
41914
- import { dirname as dirname24, join as join18 } from "node:path";
41915
-
41916
- // packages/api-contract/dist/scheduled-prompts.js
41917
- var SCHEDULE_SCHEDULER_NAME = "Beeline Scheduler";
41918
- var SCHEDULE_RAN_VERB = "ran a schedule for";
41919
-
41920
41991
  // apps/body/dist/turn-trace.js
41921
41992
  import { appendFile as appendFile2, mkdir as mkdir22, readdir as readdir7, rm as rm13 } from "node:fs/promises";
41922
41993
  import { resolve as resolve34 } from "node:path";
@@ -42375,7 +42446,8 @@ function resumePrompt(item) {
42375
42446
  `This is Trusty Squire's answer to the approval you were waiting on: ${item.body}.`,
42376
42447
  "Your paused work resumes now.",
42377
42448
  "If it was approved, continue exactly where you left off with Trusty Squire; do not ask the person to approve again, and do not restart the task.",
42378
- "If it was denied, stop that Trusty Squire action and say plainly what you cannot do."
42449
+ "If it was denied, stop that Trusty Squire action and say plainly what you cannot do.",
42450
+ "If it got no answer, the approval expired; say so plainly, and ask again only if the person still wants it."
42379
42451
  ].join(" ");
42380
42452
  }
42381
42453
  if (item.systemEvent?.kind === "connector-offer-decided") {
@@ -43695,14 +43767,17 @@ function cornerGitHubCommandRefusal(launcher, argv, featureBranch, targetBranch,
43695
43767
  return { refused: true };
43696
43768
  const separator = raw.indexOf(":");
43697
43769
  const source = separator >= 0 ? raw.slice(0, separator) : raw;
43698
- const destination = separator >= 0 ? raw.slice(separator + 1) : source;
43770
+ const explicitDestination = separator >= 0 ? raw.slice(separator + 1) : void 0;
43699
43771
  if (!source || source === ":" || /(?:^|\/)refs\/tags\//.test(source))
43700
43772
  return { refused: true };
43701
- const resolved = resolvePushBranch(source === "HEAD" ? void 0 : source);
43773
+ const resolved = resolvePushBranch(source);
43702
43774
  if (resolved.tag)
43703
43775
  return { refused: true };
43704
- const branch = destination ? branchName(destination) : resolved.branch ? branchName(resolved.branch) : void 0;
43705
- return { branch, refused: destination.startsWith("refs/tags/") };
43776
+ const branch = explicitDestination ? branchName(explicitDestination) : resolved.branch ? branchName(resolved.branch) : void 0;
43777
+ return {
43778
+ branch,
43779
+ refused: explicitDestination !== void 0 && explicitDestination.startsWith("refs/tags/")
43780
+ };
43706
43781
  }) : [resolvePushBranch()];
43707
43782
  if (destinations.some((destination) => "refused" in destination && destination.refused === true || !destination.branch || branchName(destination.branch) !== featureBranch || branchName(destination.branch) === targetBranch))
43708
43783
  return refusal;
@@ -43731,6 +43806,12 @@ const gitContext = (() => {
43731
43806
  })();
43732
43807
  function git(args, options) { return spawnSync(config.gitCommand, gitContext.concat(args), options); }
43733
43808
  function resolvePushBranch(source) {
43809
+ if (source === 'HEAD') {
43810
+ // git push origin HEAD targets the branch checked out now; read it
43811
+ // directly and fail closed in a detached HEAD.
43812
+ const current = git(['symbolic-ref', '--short', 'HEAD'], { encoding: 'utf8' });
43813
+ return current.status === 0 ? { branch: current.stdout.trim() } : {};
43814
+ }
43734
43815
  if (source) {
43735
43816
  const tag = git(['show-ref', '--verify', '--quiet', 'refs/tags/' + source], { stdio: 'ignore' }).status === 0;
43736
43817
  return { branch: source, tag };
@@ -44028,7 +44109,7 @@ if (!refusal) {
44028
44109
  }
44029
44110
 
44030
44111
  // apps/body/dist/warm-node-modules.js
44031
- import { createHash as createHash13, randomUUID as randomUUID11 } from "node:crypto";
44112
+ import { createHash as createHash12, randomUUID as randomUUID11 } from "node:crypto";
44032
44113
  import { constants as fsConstants3 } from "node:fs";
44033
44114
  import { chmod as chmod10, copyFile as copyFile2, link, lstat as lstat4, mkdir as mkdir25, readdir as readdir8, readFile as readFile29, readlink as readlink2, rename as rename10, rm as rm14, stat as stat8, symlink as symlink3, utimes } from "node:fs/promises";
44034
44115
  import { dirname as dirname25, join as join19, resolve as resolve36 } from "node:path";
@@ -44073,7 +44154,7 @@ async function readWarmPlan(worktreePath) {
44073
44154
  const unsafe = trees.find((tree) => !isContainedTreePath(tree));
44074
44155
  if (unsafe)
44075
44156
  return { failure: "unsafe-lockfile", detail: unsafe };
44076
- const key = createHash13("sha256").update(`${STORE_FORMAT}\0${process.platform}\0${process.arch}\0${process.versions.modules}\0`).update(lockfile).digest("hex").slice(0, 32);
44157
+ const key = createHash12("sha256").update(`${STORE_FORMAT}\0${process.platform}\0${process.arch}\0${process.versions.modules}\0`).update(lockfile).digest("hex").slice(0, 32);
44077
44158
  return { key, trees };
44078
44159
  }
44079
44160
  function nodeModulesTreePaths(packages) {
@@ -44326,9 +44407,13 @@ var execFileAsync7 = promisify7(execFile12);
44326
44407
  var TOOL_ARGUMENT_MAX_BYTES = 1200;
44327
44408
  var TOOL_OUTPUT_MAX_BYTES = 3200;
44328
44409
  var TOOL_PATH_LIMIT = 12;
44410
+ var GITHUB_OUTAGE_WAIT_MS = 12e4;
44329
44411
  function isCornerChecksTurn(trigger, restates) {
44330
44412
  return Boolean(restates) || /\b(?:passed|failed) a check\b/i.test(trigger);
44331
44413
  }
44414
+ function isCornerReviewTurn(reviewer, trigger) {
44415
+ return reviewer && /\bpassed a check\b/i.test(trigger);
44416
+ }
44332
44417
  async function cornerUndeliveredRepositoryState(worktreePath, featureBranch, targetBranch) {
44333
44418
  try {
44334
44419
  const [{ stdout: status }, ahead] = await Promise.all([
@@ -44462,7 +44547,7 @@ function filePaths(value, worktreePath) {
44462
44547
  visit(value);
44463
44548
  return [...paths];
44464
44549
  }
44465
- function resultStatus2(call) {
44550
+ function resultStatus(call) {
44466
44551
  const content = record4(call.content) ?? (typeof call.content === "string" ? (() => {
44467
44552
  try {
44468
44553
  return record4(JSON.parse(call.content));
@@ -44487,11 +44572,8 @@ function toolArguments(call) {
44487
44572
  const input = serialized(call.rawInput);
44488
44573
  return input ? { input: clampBytes(redactToolDetail(input), TOOL_ARGUMENT_MAX_BYTES) } : {};
44489
44574
  }
44490
- function spentAuthorityOnLaneUpgrade(call) {
44491
- return /(?:^|[._:/-])upgrade_corner_to_code$/i.test(call.title ?? "") && isCompletedToolCall(call);
44492
- }
44493
44575
  function toolCallKey(call, index) {
44494
- return call.id ? `id-${createHash14("sha256").update(call.id).digest("hex")}` : `tool-${index}`;
44576
+ return call.id ? `id-${createHash13("sha256").update(call.id).digest("hex")}` : `tool-${index}`;
44495
44577
  }
44496
44578
  function toolCallSettled(call) {
44497
44579
  if (call.resultReceived)
@@ -44523,7 +44605,7 @@ async function cornerToolActivity(call, worktreePath, requestedBy) {
44523
44605
  kind: "tool",
44524
44606
  title: title.slice(0, 240),
44525
44607
  operation: operation.slice(0, 80),
44526
- status: resultStatus2(call),
44608
+ status: resultStatus(call),
44527
44609
  ...argumentsSummary,
44528
44610
  ...output ? { output } : {},
44529
44611
  ...requestedBy ? { requestedBy } : {},
@@ -44573,9 +44655,10 @@ var MonolithCornerTurnLoop = class {
44573
44655
  modelTakesImages;
44574
44656
  /** The one provider re-pinned after an empty completion, until the session ends. */
44575
44657
  pinnedProviderOverride;
44576
- /** The worker's yolo mode baked into the current session. */
44577
- yoloMode = false;
44578
- /** Identity-only reviewer context; the exact PR head is refreshed inside each active turn. */
44658
+ /**
44659
+ * Identity-only reviewer context, set when this agent holds the Room's
44660
+ * reviewer post; the exact PR head is refreshed inside each review turn.
44661
+ */
44579
44662
  reviewerInstructionInput;
44580
44663
  /** The role-specific second-chance instruction for this session. */
44581
44664
  /** Repository state already given a delivery reminder, until that state changes. */
@@ -44861,17 +44944,13 @@ var MonolithCornerTurnLoop = class {
44861
44944
  ]);
44862
44945
  const { hostRoutes: grantedHostRoutes, devices: grantedDevices } = grants;
44863
44946
  const self = roster.members.find((member) => member.identityId === this.agent.publicKey);
44864
- this.yoloMode = configuration.yoloMode;
44865
44947
  const opener = this.options.openedBy ? roster.members.find((member) => member.identityId === this.options.openedBy) : void 0;
44866
- const reviewerInput = {
44867
- isReviewer: configuration.isReviewer === true,
44868
- authorHandle: opener?.handle,
44869
- openedByAgent: !this.options.openedBy || this.options.openedBy === this.agent.publicKey
44870
- };
44871
- const reviewerInstruction = cornerReviewerInstruction(reviewerInput) ? CORNER_REVIEWER_SESSION_INSTRUCTION : void 0;
44872
- this.reviewerInstructionInput = reviewerInstruction ? reviewerInput : void 0;
44873
- const selfReviewerInstruction = cornerSelfReviewerInstruction(reviewerInput);
44874
- this.sessionSurface = !this.options.repository ? "no-code-corner" : reviewerInstruction ? "review-corner" : "code-corner";
44948
+ const reviewer = Boolean(this.options.repository) && configuration.isReviewer === true;
44949
+ this.reviewerInstructionInput = reviewer ? {
44950
+ isReviewer: true,
44951
+ ...opener?.handle && opener.identityId !== this.agent.publicKey ? { authorHandle: opener.handle } : {}
44952
+ } : void 0;
44953
+ this.sessionSurface = this.options.repository ? "code-corner" : "repo-less-corner";
44875
44954
  this.repositoryWork = cornerHasRepositoryWork(restored.brief);
44876
44955
  const cornerPrepareScript = await hasCornerPrepareScript(this.options.worktreePath);
44877
44956
  this.sessionPromptContext = {
@@ -44879,8 +44958,7 @@ var MonolithCornerTurnLoop = class {
44879
44958
  surface: this.sessionSurface,
44880
44959
  agentName: self?.name ?? this.agent.name,
44881
44960
  ...configuration.reviewerHandle ? { reviewerHandle: configuration.reviewerHandle } : {},
44882
- selfReviewer: Boolean(selfReviewerInstruction),
44883
- yoloMode: configuration.yoloMode,
44961
+ reviewer,
44884
44962
  cornerPrepareScript
44885
44963
  };
44886
44964
  await mkdir26(this.options.worktreePath, { recursive: true });
@@ -44910,7 +44988,7 @@ var MonolithCornerTurnLoop = class {
44910
44988
  root: this.options.config.agentHomeRoot,
44911
44989
  squireScope,
44912
44990
  sharedSkills: this.options.config.sharedSkills ?? [],
44913
- isReviewer: Boolean(reviewerInstruction),
44991
+ isReviewer: reviewer,
44914
44992
  grantedHostRoutes: mountedHostRoutes,
44915
44993
  extraHostRoutes: registryHostDeclarations,
44916
44994
  resourceAuthFile,
@@ -45096,9 +45174,8 @@ var MonolithCornerTurnLoop = class {
45096
45174
  workspaceId: this.options.workspaceId,
45097
45175
  cornerId: this.options.cornerId,
45098
45176
  agentMayCloseCorner: Boolean(repository),
45099
- agentMayUpgradeCorner: this.options.agentMayUpgradeCorner,
45100
- reviewer: Boolean(reviewerInstruction),
45101
- lane: this.options.lane ?? (repository ? "code" : "no_code"),
45177
+ reviewer,
45178
+ repositoryBacked: Boolean(repository),
45102
45179
  attachRoot: this.options.worktreePath,
45103
45180
  // The whole per-session overlay, not an enumerated subset: see
45104
45181
  // `monolith-room-turn.ts`'s matching comment.
@@ -45136,7 +45213,6 @@ var MonolithCornerTurnLoop = class {
45136
45213
  }
45137
45214
  } : {},
45138
45215
  ...this.options.requesterHandle ? { requesterHandle: this.options.requesterHandle } : {},
45139
- ...this.options.agentMayUpgradeCorner ? { agentMayUpgradeCorner: true } : {},
45140
45216
  ...agentEnv.BEELINE_ANDROID_EMULATOR_PORT ? { android: { emulatorPort: agentEnv.BEELINE_ANDROID_EMULATOR_PORT } } : {}
45141
45217
  });
45142
45218
  this.turnSessionPrefix = session.turnPrefix;
@@ -45247,24 +45323,6 @@ var MonolithCornerTurnLoop = class {
45247
45323
  servingProviders() {
45248
45324
  return this.pinnedProviderOverride ? [this.pinnedProviderOverride] : this.pinnedProviders;
45249
45325
  }
45250
- /**
45251
- * Did this corner's lane already move under the running turn?
45252
- *
45253
- * The upgrade is a server fact, and only the server can say it happened: a
45254
- * harness that reports no tool calls at all (`cursor-acp-bridge.ts` emits
45255
- * message chunks only) would otherwise finish its turn believing it still
45256
- * owns write authority the upgrade has already spent. One bounded read, and
45257
- * only where an upgrade is possible — the tool is mounted nowhere else.
45258
- */
45259
- async laneUpgradeCommitted() {
45260
- if (this.options.lane !== "no_code" || !this.options.agentMayUpgradeCorner)
45261
- return false;
45262
- const state = await this.options.api.execute("getCornerRestoreState", { cornerId: this.options.cornerId }).catch((error) => {
45263
- console.error(`[thin-core] corner ${this.options.cornerId} lane read failed:`, error);
45264
- return void 0;
45265
- });
45266
- return Boolean(state?.lane && state.lane !== "no_code");
45267
- }
45268
45326
  /** Why a turn carried no answer text, or undefined when it did. */
45269
45327
  async explainEmpty(result) {
45270
45328
  if (durableReplyText(result.agentText))
@@ -45358,7 +45416,6 @@ var MonolithCornerTurnLoop = class {
45358
45416
  await trace.finish(outcome, reason);
45359
45417
  };
45360
45418
  let deliberateNoReply = false;
45361
- let laneUpgraded = false;
45362
45419
  try {
45363
45420
  await withTurnReceiptHeartbeat(api, {
45364
45421
  agentId: this.agent.publicKey,
@@ -45377,6 +45434,7 @@ var MonolithCornerTurnLoop = class {
45377
45434
  this.turnMetrics = {};
45378
45435
  this.turnUsage = new TurnUsageAccumulator();
45379
45436
  await this.syncBranch();
45437
+ const reviewTurn = isCornerReviewTurn(Boolean(this.reviewerInstructionInput), trigger);
45380
45438
  const [conversation, roster, restored, activeReviewerInstruction, institutionalContext, memberCorners] = await trace.measure("context-fetch", () => Promise.all([
45381
45439
  api.execute("getRoomConversation", {
45382
45440
  roomId: cornerId,
@@ -45385,7 +45443,7 @@ var MonolithCornerTurnLoop = class {
45385
45443
  }),
45386
45444
  this.roster(),
45387
45445
  api.execute("getCornerRestoreState", { cornerId }),
45388
- this.activeReviewerInstruction(),
45446
+ reviewTurn ? this.activeReviewerInstruction() : void 0,
45389
45447
  awaitInstitutionalContext(institutionalContextFetch, (message) => console.warn(`[thin-core] corner ${cornerId}: ${message}`)),
45390
45448
  api.execute("listRoomCorners", { roomId: this.options.parentRoomId })
45391
45449
  ]));
@@ -45410,7 +45468,7 @@ var MonolithCornerTurnLoop = class {
45410
45468
  const briefDelivered = this.attachmentDir && briefAttachments.length ? await deliverAttachments(briefAttachments, join20(this.attachmentDir, requestId.replace(/[^\w-]/g, "_"), "brief", String(assignedBrief.revision)), this.options.fetchImpl) : [];
45411
45469
  briefFileLines = await Promise.all((assignedBrief?.attachments ?? []).map(async (file, index) => {
45412
45470
  const entry = briefDelivered.find((item) => item.attachment === briefAttachments[index]);
45413
- const verified = entry?.path && createHash14("sha256").update(await readFile30(entry.path)).digest("hex") === file.sha256;
45471
+ const verified = entry?.path && createHash13("sha256").update(await readFile30(entry.path)).digest("hex") === file.sha256;
45414
45472
  return `- ${file.title} (${file.purpose}; ${file.required ? "required" : "optional"}; sha256 ${file.sha256}): ${verified ? entry.path : "UNAVAILABLE OR CONTENT MISMATCH"}`;
45415
45473
  }));
45416
45474
  missingRequiredBriefFile = (assignedBrief?.attachments ?? []).some((file, index) => file.required && briefFileLines[index]?.includes("UNAVAILABLE OR CONTENT MISMATCH"));
@@ -45526,8 +45584,6 @@ var MonolithCornerTurnLoop = class {
45526
45584
  let activityAttempt = 0;
45527
45585
  const publishToolCalls = (calls, settledOnly, exceptKey) => {
45528
45586
  calls.forEach((call, index) => {
45529
- if (spentAuthorityOnLaneUpgrade(call))
45530
- return;
45531
45587
  const key = `${activityAttempt}:${toolCallKey(call, index)}`;
45532
45588
  if (settledOnly && !observedToolCalls.has(key)) {
45533
45589
  observedToolCalls.add(key);
@@ -45678,12 +45734,6 @@ ${continuation}` : assembled;
45678
45734
  };
45679
45735
  let result = await runPrompt();
45680
45736
  trace.promptSettled();
45681
- laneUpgraded = await this.laneUpgradeCommitted();
45682
- if (laneUpgraded) {
45683
- console.log(`[thin-core] corner ${cornerId} turn ${requestId} upgraded to the code lane`);
45684
- await stream.retract();
45685
- return;
45686
- }
45687
45737
  const grantedDevice = approvedDeviceGrant(result.toolCalls);
45688
45738
  if (grantedDevice) {
45689
45739
  await flushToolCalls(result.toolCalls, "");
@@ -45724,19 +45774,19 @@ ${continuation}` : assembled;
45724
45774
  let refreshedReviewerInstruction;
45725
45775
  let reviewerSecondPass = false;
45726
45776
  let reviewerTargetChanged = false;
45727
- if (!explained && checksTurn && this.reviewerInstructionInput) {
45777
+ if (!explained && reviewTurn) {
45728
45778
  await this.syncBranch();
45729
45779
  refreshedReviewerInstruction = await this.activeReviewerInstruction();
45730
45780
  reviewerTargetChanged = refreshedReviewerInstruction !== activeReviewerInstruction;
45731
- reviewerSecondPass = reviewerTargetChanged || !result.toolCalls.some((call) => /(?:^|[._:/-])(?:approve_merge|record_validation_stage)$/i.test(call.title ?? "") && isCompletedToolCall(call));
45781
+ reviewerSecondPass = reviewerTargetChanged || !result.toolCalls.some((call) => /(?:^|[._:/-])approve_merge$/i.test(call.title ?? "") && isCompletedToolCall(call));
45732
45782
  }
45733
45783
  let replyBeforeNudge = "";
45734
- if (!explained && (needsDeliveryNudge || checksTurn && (this.reviewerInstructionInput ? reviewerSecondPass : repositoryWork && !repositoryHeld && this.yoloMode && !result.toolCalls.some((call) => /(?:^|[._:/-])pr_checks_status$/i.test(call.title ?? "") && isCompletedToolCall(call))))) {
45784
+ if (!explained && (needsDeliveryNudge || checksTurn && (reviewTurn ? reviewerSecondPass : repositoryWork && !repositoryHeld && !result.toolCalls.some((call) => /(?:^|[._:/-])pr_checks_status$/i.test(call.title ?? "") && isCompletedToolCall(call))))) {
45735
45785
  if (needsDeliveryNudge)
45736
45786
  this.lastDeliveryNudgeState = deliveryState;
45737
45787
  await flushToolCalls(result.toolCalls, "");
45738
45788
  replyBeforeNudge = durableReplyText(stream.remainderOf(result.agentText));
45739
- result = await runPrompt(this.reviewerInstructionInput ? refreshedReviewerInstruction ?? CORNER_REVIEWER_SESSION_INSTRUCTION : checksTurn ? CORNER_YOLO_MERGE_NUDGE : CORNER_DELIVERY_NUDGE);
45789
+ result = await runPrompt(reviewTurn ? refreshedReviewerInstruction ?? CORNER_REVIEWER_SESSION_INSTRUCTION : checksTurn ? CORNER_CHECKS_NUDGE : CORNER_DELIVERY_NUDGE);
45740
45790
  trace.promptSettled();
45741
45791
  explained = await this.explainEmpty(result);
45742
45792
  }
@@ -45782,10 +45832,6 @@ ${continuation}` : assembled;
45782
45832
  console.warn(`[thin-core] corner ${cornerId} turn ${requestId} dropped: the server revoked its output authority`);
45783
45833
  this.stopTurn(requestId);
45784
45834
  });
45785
- if (laneUpgraded) {
45786
- void finishTrace("complete").catch(() => void 0);
45787
- return;
45788
- }
45789
45835
  startTraceStats();
45790
45836
  await api.execute("postAgentTurnReceipt", {
45791
45837
  agentId: this.agent.publicKey,
@@ -45804,12 +45850,6 @@ ${continuation}` : assembled;
45804
45850
  void finishTrace("cancelled").catch(() => void 0);
45805
45851
  return;
45806
45852
  }
45807
- laneUpgraded ||= await this.laneUpgradeCommitted();
45808
- if (laneUpgraded) {
45809
- console.log(`[thin-core] corner ${cornerId} turn ${requestId} ended after its lane upgrade:`, error);
45810
- void finishTrace("complete").catch(() => void 0);
45811
- return;
45812
- }
45813
45853
  const reason = distillTurnFailureReason(error);
45814
45854
  startTraceStats();
45815
45855
  await api.execute("postAgentTurnReceipt", {
@@ -45888,16 +45928,19 @@ ${continuation}` : assembled;
45888
45928
  if (!repository)
45889
45929
  return;
45890
45930
  let token;
45891
- for (let attempt = 0; attempt < 3; attempt += 1) {
45931
+ const outageDeadline = Date.now() + GITHUB_OUTAGE_WAIT_MS;
45932
+ for (let attempt = 0; ; attempt += 1) {
45892
45933
  try {
45893
45934
  token = (await this.options.api.execute("getRoomGitHubToken", {
45894
45935
  roomId: this.options.parentRoomId
45895
45936
  })).token;
45896
45937
  break;
45897
45938
  } catch (error) {
45898
- if (attempt === 2)
45939
+ const outage = error instanceof DaemonApiError && error.status === 502;
45940
+ const delay = outage ? Math.min(2e3 * 2 ** attempt, 3e4) : 250 * (attempt + 1);
45941
+ if ((outage ? Date.now() + delay > outageDeadline : attempt === 2) || this.forcedStop || this.currentTurn && this.stoppedTurns.has(this.currentTurn.requestId))
45899
45942
  throw error;
45900
- await new Promise((resolve45) => setTimeout(resolve45, 250 * (attempt + 1)));
45943
+ await new Promise((resolve45) => setTimeout(resolve45, delay));
45901
45944
  }
45902
45945
  }
45903
45946
  if (!token)
@@ -45975,10 +46018,6 @@ ${continuation}` : assembled;
45975
46018
  return false;
45976
46019
  this.reconcileCloseRequested = false;
45977
46020
  const state = await api.execute("getCornerRestoreState", { cornerId });
45978
- if (this.options.lane === "no_code" && state.lane !== "no_code") {
45979
- this.options.onLaneChanged?.();
45980
- return true;
45981
- }
45982
46021
  if (!state.closeRequested)
45983
46022
  return false;
45984
46023
  await this.harvest;
@@ -46508,7 +46547,7 @@ function cornerStartConfigKey(repository, objective) {
46508
46547
  }
46509
46548
  function cornerRepositoryCacheDir(supervisorRoot, remote) {
46510
46549
  const normalizedRemote = roomCheckoutRemote(remote);
46511
- const repositoryHash = createHash15("sha256").update(normalizedRemote).digest("hex").slice(0, 24);
46550
+ const repositoryHash = createHash14("sha256").update(normalizedRemote).digest("hex").slice(0, 24);
46512
46551
  return resolve38(supervisorRoot, "beeline", "repositories", `${repositoryHash}.git`);
46513
46552
  }
46514
46553
  function isStandingCornerStartFault(error) {
@@ -46683,6 +46722,7 @@ async function removeCornerWorktreeAndBranches(worktree, options = {}) {
46683
46722
  if (head === localRef) {
46684
46723
  throw new Error(`refusing to delete repository HEAD ${localRef}`);
46685
46724
  }
46725
+ await assertLocalBranchPublished(worktree);
46686
46726
  }
46687
46727
  if (!preserveRemoteBranch) {
46688
46728
  await deleteExactRemoteBranch(worktree.gitCommonDir, remoteRef, worktree.token);
@@ -46703,26 +46743,38 @@ async function assertCornerWorktreePublished(worktree) {
46703
46743
  const originalHead = (await execFileAsync8("git", ["-C", worktree.path, "rev-parse", "--verify", "HEAD"])).stdout.trim();
46704
46744
  if (await status())
46705
46745
  throw new Error(`corner ${worktree.cornerId} has unpushed working-tree work`);
46706
- let published = await originContains(worktree.gitCommonDir, originalHead);
46746
+ await assertPublishedOnOrigin(worktree.gitCommonDir, originalHead, worktree.cornerId, worktree.token);
46747
+ const settledHead = (await execFileAsync8("git", ["-C", worktree.path, "rev-parse", "--verify", "HEAD"])).stdout.trim();
46748
+ if (settledHead !== originalHead || await status()) {
46749
+ throw new Error(`corner ${worktree.cornerId} changed during cleanup`);
46750
+ }
46751
+ }
46752
+ async function assertLocalBranchPublished(worktree) {
46753
+ const head = (await execFileAsync8("git", [
46754
+ `--git-dir=${worktree.gitCommonDir}`,
46755
+ "rev-parse",
46756
+ "--verify",
46757
+ `refs/heads/${worktree.branch}`
46758
+ ])).stdout.trim();
46759
+ await assertPublishedOnOrigin(worktree.gitCommonDir, head, worktree.cornerId, worktree.token);
46760
+ }
46761
+ async function assertPublishedOnOrigin(gitCommonDir, head, cornerId, token) {
46762
+ let published = await originContains(gitCommonDir, head);
46707
46763
  if (!published) {
46708
46764
  await execFileAsync8("git", [
46709
- `--git-dir=${worktree.gitCommonDir}`,
46765
+ `--git-dir=${gitCommonDir}`,
46710
46766
  "fetch",
46711
46767
  "--prune",
46712
46768
  "origin",
46713
46769
  "+refs/heads/*:refs/remotes/origin/*"
46714
- ], { env: githubGitEnv(worktree.token), maxBuffer: 4 * 1024 * 1024 });
46715
- published = await originContains(worktree.gitCommonDir, originalHead);
46770
+ ], { env: githubGitEnv(token), maxBuffer: 4 * 1024 * 1024 });
46771
+ published = await originContains(gitCommonDir, head);
46716
46772
  }
46717
46773
  if (!published)
46718
- throw new Error(`corner ${worktree.cornerId} has unpushed commits`);
46719
- const settledHead = (await execFileAsync8("git", ["-C", worktree.path, "rev-parse", "--verify", "HEAD"])).stdout.trim();
46720
- if (settledHead !== originalHead || await status()) {
46721
- throw new Error(`corner ${worktree.cornerId} changed during cleanup`);
46722
- }
46774
+ throw new Error(`corner ${cornerId} has unpushed commits`);
46723
46775
  }
46724
- function hasUnpushedCommits(error) {
46725
- return error instanceof Error && / has unpushed commits$/.test(error.message);
46776
+ function hasUnpublishedWork(error) {
46777
+ return error instanceof Error && / has unpushed (?:commits|working-tree work)$/.test(error.message);
46726
46778
  }
46727
46779
  async function originContains(gitCommonDir, head) {
46728
46780
  const result = await execFileAsync8("git", [
@@ -46829,8 +46881,11 @@ var RoomRuntimeCoordinator = class {
46829
46881
  this.connectorUsage = new ConnectorUsageRecorder();
46830
46882
  this.options.daemonApi.setRoomsChangedListener?.((event) => {
46831
46883
  if (event?.repositoryChanged) {
46832
- if (event.roomId)
46884
+ if (event.roomId) {
46833
46885
  this.invalidateParentRepository(event.roomId);
46886
+ void this.refreshRoomCorners(event.roomId);
46887
+ return;
46888
+ }
46834
46889
  this.wakeDiscovery();
46835
46890
  return;
46836
46891
  }
@@ -46844,9 +46899,6 @@ var RoomRuntimeCoordinator = class {
46844
46899
  this.options.daemonApi.setCornerCompleteListener?.((roomId) => {
46845
46900
  void this.applyCornerComplete(roomId).catch((error) => console.error("[thin-core] live corner-complete apply failed", error));
46846
46901
  });
46847
- this.options.daemonApi.setCornerRestartListener?.((roomId) => {
46848
- void this.applyCornerRestart(roomId).catch((error) => console.error("[thin-core] live corner-restart apply failed", error));
46849
- });
46850
46902
  this.options.daemonApi.setConfigChangedListener?.(() => {
46851
46903
  void this.scheduler.suspendIdle().catch((error) => console.error("[body] config-change session restart failed", error));
46852
46904
  void Promise.resolve(this.options.onConfigChanged?.()).catch((error) => console.error("[body] config-change catalog refresh failed", error));
@@ -47164,27 +47216,14 @@ var RoomRuntimeCoordinator = class {
47164
47216
  async applyCornerComplete(cornerId) {
47165
47217
  this.running.get(cornerId)?.body.requestClose?.();
47166
47218
  }
47167
- /** Retire the scratch runtime. Ordinary desired-state reconciliation starts
47168
- * the same corner id again and re-reads its now-code lane. */
47169
- async applyCornerRestart(cornerId) {
47170
- if (this.stopped)
47171
- return;
47172
- const running = this.running.get(cornerId);
47173
- if (!running) {
47174
- this.wakeDiscovery();
47175
- return;
47176
- }
47177
- await this.stopRunning(cornerId, running, true);
47178
- this.wakeDiscovery();
47179
- }
47180
- async stopRunning(channelId, running, preserveScratch = false) {
47219
+ async stopRunning(channelId, running) {
47181
47220
  this.deferredRepositoryRestarts.delete(channelId);
47182
47221
  running.controller.abort();
47183
47222
  await running.promise.catch(() => void 0);
47184
47223
  try {
47185
47224
  if (running.worktree)
47186
47225
  await this.reapCornerWorktree(running.worktree);
47187
- else if (running.scratch && !preserveScratch)
47226
+ else if (running.scratch)
47188
47227
  await this.reapCornerScratch(running.scratch);
47189
47228
  } catch (error) {
47190
47229
  console.error(`[thin-core] corner ${channelId} cleanup failed; will retry:`, error);
@@ -47299,7 +47338,7 @@ var RoomRuntimeCoordinator = class {
47299
47338
  return this.roomRoot(roomId);
47300
47339
  const remote = roomCheckoutRemote(repository.remote);
47301
47340
  const targetBranch = repository.targetBranch || "main";
47302
- const checkoutId = createHash15("sha256").update(`${roomId}\0${remote}\0${targetBranch}`).digest("hex").slice(0, 24);
47341
+ const checkoutId = createHash14("sha256").update(`${roomId}\0${remote}\0${targetBranch}`).digest("hex").slice(0, 24);
47303
47342
  const path = resolve38(this.runtime.supervisorRoot, "beeline", "room-checkouts", checkoutId);
47304
47343
  await mkdir27(dirname27(path), { recursive: true, mode: 448 });
47305
47344
  const token = remote.startsWith("https://github.com/") ? await this.parentRepositoryToken(roomId) : void 0;
@@ -47378,12 +47417,9 @@ var RoomRuntimeCoordinator = class {
47378
47417
  if (repository.resolution === "repository" && (!repository.remote || !repository.key)) {
47379
47418
  throw new Error("corner parent Room has an incomplete repository binding");
47380
47419
  }
47381
- if (!objective)
47420
+ if (!objective && restore.kind !== "human")
47382
47421
  throw new Error("corner has no authoritative objective fact");
47383
- const repositoryBacked = repository.resolution === "repository" && restore.lane !== "no_code";
47384
- const scratchPath = resolve38(this.roomRoot(corner.cornerId), "scratch");
47385
- if (repositoryBacked && existsSync10(scratchPath) && !restore.featureBranch)
47386
- throw new Error("promoted corner has no server-assigned feature branch");
47422
+ const repositoryBacked = repository.resolution === "repository";
47387
47423
  const targetBranch = repositoryBacked ? repository.targetBranch || "main" : void 0;
47388
47424
  const featureBranch = repositoryBacked ? restore.featureBranch ?? `feature/corner-${corner.cornerId.replaceAll("-", "").slice(0, 12)}` : void 0;
47389
47425
  const granted = repositoryBacked ? await this.cornerRepositoryToken(corner.parentRoomId) : void 0;
@@ -47394,8 +47430,6 @@ var RoomRuntimeCoordinator = class {
47394
47430
  featureBranch,
47395
47431
  token: granted.token
47396
47432
  }) : void 0;
47397
- if (worktree && existsSync10(scratchPath))
47398
- await this.reapCornerScratch({ path: scratchPath, cornerId: corner.cornerId });
47399
47433
  const workspacePath = worktree?.path ?? resolve38(this.roomRoot(corner.cornerId), "scratch");
47400
47434
  if (!worktree)
47401
47435
  await mkdir27(workspacePath, { recursive: true, mode: 448 });
@@ -47419,8 +47453,6 @@ var RoomRuntimeCoordinator = class {
47419
47453
  ...corner.openedBy ? { openedBy: corner.openedBy } : {},
47420
47454
  objective,
47421
47455
  worktreePath: workspacePath,
47422
- lane: restore.lane,
47423
- agentMayUpgradeCorner: repository.resolution === "repository" && restore.lane === "no_code",
47424
47456
  ...restore.requesterHandle ? { requesterHandle: restore.requesterHandle } : {},
47425
47457
  ...worktree ? {
47426
47458
  repository: {
@@ -47445,7 +47477,6 @@ var RoomRuntimeCoordinator = class {
47445
47477
  parentRoomId: corner.parentRoomId,
47446
47478
  token: ""
47447
47479
  }) : this.reapCornerScratch({ path: workspacePath, cornerId: corner.cornerId }),
47448
- onLaneChanged: () => void this.applyCornerRestart(corner.cornerId).catch((error) => console.error("[thin-core] corner lane-change restart failed", error)),
47449
47480
  onRestartRequested: () => this.requestLifecycleRestart(),
47450
47481
  canStartTurn: () => !this.restartRequested,
47451
47482
  supervisor: this.supervisor
@@ -47482,7 +47513,7 @@ var RoomRuntimeCoordinator = class {
47482
47513
  });
47483
47514
  this.standingCornerStartFaults.delete(corner.cornerId);
47484
47515
  this.reportedCornerStartFailures.delete(corner.cornerId);
47485
- console.log(worktree ? `[thin-core] serving corner ${corner.cornerId} on ${featureBranch} at ${workspacePath}` : `[thin-core] serving no-code corner ${corner.cornerId} at ${workspacePath}`);
47516
+ console.log(worktree ? `[thin-core] serving corner ${corner.cornerId} on ${featureBranch} at ${workspacePath}` : `[thin-core] serving repo-less corner ${corner.cornerId} at ${workspacePath}`);
47486
47517
  } catch (error) {
47487
47518
  this.surfaceHealth.degraded(corner.cornerId, "corner failed to start");
47488
47519
  console.error(`[thin-core] failed to start corner ${corner.cornerId}:`, error);
@@ -47559,6 +47590,61 @@ var RoomRuntimeCoordinator = class {
47559
47590
  this.tokenCache.delete(roomId);
47560
47591
  this.tokenFlights.delete(roomId);
47561
47592
  }
47593
+ /**
47594
+ * A repository-change event names its own Room, so only that Room's
47595
+ * corners need a fresh listing; re-listing every Room's corners for one
47596
+ * Room's push would repeat that read for every other Room on each push.
47597
+ * Only new or newly-unarchived corners are started here - removals and
47598
+ * archived-corner cleanup stay on the regular reconcile pass. The named
47599
+ * Room itself is restarted the same way a full reconcile would, so a
47600
+ * running Room picks up the change (a new GitHub installation, a token) too.
47601
+ */
47602
+ async refreshRoomCorners(roomId) {
47603
+ if (this.stopped)
47604
+ return;
47605
+ try {
47606
+ await this.restartRunningRoomForRepositoryChange(roomId);
47607
+ if (this.stopped)
47608
+ return;
47609
+ const result = await this.options.daemonApi.execute("listRoomCorners", { roomId });
47610
+ for (const corner of result.corners) {
47611
+ this.monolithCornerParents.set(corner.cornerId, roomId);
47612
+ if (corner.archived)
47613
+ continue;
47614
+ this.surfaceHealth.discover(corner.cornerId, "corner");
47615
+ this.roomRemovalConfirmations.delete(corner.cornerId);
47616
+ if (this.running.has(corner.cornerId) || this.startingCorners.has(corner.cornerId))
47617
+ continue;
47618
+ await this.watchCorner({
47619
+ cornerId: corner.cornerId,
47620
+ parentRoomId: roomId,
47621
+ ...corner.createdBy ? { openedBy: corner.createdBy } : {}
47622
+ });
47623
+ }
47624
+ } catch (error) {
47625
+ console.error(`[thin-core] scoped corner refresh failed for Room ${roomId}:`, error);
47626
+ this.wakeDiscovery();
47627
+ }
47628
+ }
47629
+ /**
47630
+ * Same stop/restart (or, mid-turn, defer-to-`notePoll`) a full reconcile
47631
+ * applies to a Room whose `repositoryRevision` changed, for the one Room a
47632
+ * live repository-change event names. A no-op when that id names nothing
47633
+ * currently running - e.g. a Room whose corners changed but is not itself
47634
+ * being served, or a corner id, which this event never actually names.
47635
+ */
47636
+ async restartRunningRoomForRepositoryChange(roomId) {
47637
+ const running = this.running.get(roomId);
47638
+ if (!running)
47639
+ return;
47640
+ if (running.body.isBusy()) {
47641
+ this.deferredRepositoryRestarts.add(roomId);
47642
+ return;
47643
+ }
47644
+ await this.stopRunning(roomId, running);
47645
+ if (!this.stopped)
47646
+ await this.startRoom(roomId);
47647
+ }
47562
47648
  async parentRepositoryState(roomId) {
47563
47649
  const cached = this.repositoryStateCache.get(roomId);
47564
47650
  if (cached && cached.until > this.now())
@@ -47675,9 +47761,9 @@ var RoomRuntimeCoordinator = class {
47675
47761
  try {
47676
47762
  await this.reapCornerWorktree(worktree);
47677
47763
  } catch (error) {
47678
- if (hasUnpushedCommits(error)) {
47764
+ if (hasUnpublishedWork(error)) {
47679
47765
  this.archiveCleanupAbandoned.add(cornerId);
47680
- console.error(`[thin-core] corner ${cornerId} branch cleanup abandoned; unpushed commits kept:`, error);
47766
+ console.error(`[thin-core] corner ${cornerId} branch cleanup abandoned; unpublished work kept:`, error);
47681
47767
  continue;
47682
47768
  }
47683
47769
  this.deferArchiveCleanup(cornerId);