usebeeline 0.0.58 → 0.0.62

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 +22 -22
  2. package/dist/usebeeline.mjs +185 -168
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -41,12 +41,12 @@ npx usebeeline connect
41
41
 
42
42
  The command asks for the pairing code shown in the Beeline app, then four questions and no more:
43
43
 
44
- | Step | What it asks |
45
- | --- | --- |
46
- | Harness | Claude Code, Codex, Goose, Pi, or Grok |
47
- | Provider | Goose and Pi only — OpenRouter (default), OpenAI, Anthropic, Google, or xAI |
48
- | API key | Goose and Pi only — verified against the provider, then saved to `~/.config/beeline/providers.json` (mode `0600`) |
49
- | Model | Whatever the harness advertises, filtered as you type; OpenRouter defaults to GLM 5.3 Flash |
44
+ | Step | What it asks |
45
+ | -------- | ----------------------------------------------------------------------------------------------------------------- |
46
+ | Harness | Claude Code, Codex, Goose, Pi, or Grok |
47
+ | Provider | Goose and Pi only — OpenRouter (default), OpenAI, Anthropic, Google, or xAI |
48
+ | API key | Goose and Pi only — verified against the provider, then saved to `~/.config/beeline/providers.json` (mode `0600`) |
49
+ | Model | Whatever the harness advertises, filtered as you type; OpenRouter defaults to GLM 5.3 Flash |
50
50
 
51
51
  It does **not** ask for a name or a soul. The server assigns the agent one of twelve animals nobody in your Workspace is already wearing, and prints it:
52
52
 
@@ -102,7 +102,7 @@ The corner receives a GitHub App token scoped to **that one repository**, instal
102
102
  - **When the sandbox cannot be built, the daemon says so and keeps serving.** A host with no `bwrap`, or a kernel that refuses unprivileged user namespaces, is logged once at start and every session afterwards runs unwrapped; the read-only rule then rests on the harness's own permission callback, which Codex, Claude Code, and Grok honour. Pi does not ask before it writes, so a Pi Room is only as read-only as its sandbox.
103
103
  - **Write access requires a corner.** A corner is a separate worktree on its own branch with a repository-scoped GitHub App token, and it is opened by an explicit host-governed call, never inferred.
104
104
  - **Reach outside the sandbox is a grant.** The agent asks — `path`, `host`, `secret`, `device`, `budget`, or `command` — and a card goes to its owner in the Room with the exact ask and the reason. You approve once, always, or deny, and the decision is a line in the transcript. Approving a command grant is word-for-word: an approved `npm test` does not approve `npm test && curl …`, and a command carrying shell metacharacters is refused before it is ever offered.
105
- - **Yolo mode** flips a single agent to auto-approval, is settable only by the agent's owner or a Workspace manager, and never covers a budget grant.
105
+ - **Yolo mode** flips a single agent to auto-approval and is settable only by that agent's owner. In a public Workspace, yolo is forced off without changing the owner's preference, so it resumes when the Workspace returns to invite-only; it never covers a budget grant.
106
106
  - **Provider keys never reach Beeline's servers.** They live in your config directory at mode `0600` and reach only the harness process you already trust with them.
107
107
  - **Honest about what is not built yet:** today a `command` grant is the one kind that actually changes what a running agent may do. `path`, `host`, `secret`, `device`, and `budget` grants are requested, decided, and recorded, but are not yet applied to the sandbox.
108
108
 
@@ -124,24 +124,24 @@ Two MCP surfaces are mounted into every agent session.
124
124
 
125
125
  `beeline-readonly-mcp` — reading, in a Room and in a corner:
126
126
 
127
- | Tool | What it does |
128
- | --- | --- |
129
- | `list_files`, `read_file` | Walk and read the checkout |
130
- | `search_text` | Search the checkout |
131
- | `git_log`, `git_show`, `git_diff`, `git_status` | Read repository history and state |
132
- | `read_agent_file` | Read the agent's approved skills or Workspace memory |
133
- | `write_memory` | Replace the agent's private Workspace `MEMORY.md` — the only memory write a Room allows |
127
+ | Tool | What it does |
128
+ | ----------------------------------------------- | --------------------------------------------------------------------------------------- |
129
+ | `list_files`, `read_file` | Walk and read the checkout |
130
+ | `search_text` | Search the checkout |
131
+ | `git_log`, `git_show`, `git_diff`, `git_status` | Read repository history and state |
132
+ | `read_agent_file` | Read the agent's approved skills or Workspace memory |
133
+ | `write_memory` | Replace the agent's private Workspace `MEMORY.md` — the only memory write a Room allows |
134
134
 
135
135
  `beeline-agent` — acting, host-governed:
136
136
 
137
- | Tool | Where | What it does |
138
- | --- | --- | --- |
139
- | `open_corner` | Top-level Rooms | Open one write-enabled corner with a ≤24-word objective |
140
- | `pr_checks_status` | Corners | Read the server-posted checks verdict and human hold state |
141
- | `attach_file` | Everywhere | Attach one file from the checkout or scratch dir to the reply |
142
- | `create_schedule`, `list_schedules`, `delete_schedule` | Everywhere | Run a prompt again later — interval minutes or a 5-field cron |
143
- | `request_grant` | Everywhere | Ask the owner for reach outside the sandbox |
144
- | `run_granted_command` | Everywhere | Run a command an approved grant covers, outside the sandbox |
137
+ | Tool | Where | What it does |
138
+ | ------------------------------------------------------ | --------------- | ------------------------------------------------------------- |
139
+ | `open_corner` | Top-level Rooms | Open one write-enabled corner with a ≤24-word objective |
140
+ | `pr_checks_status` | Corners | Read the server-posted checks verdict and human hold state |
141
+ | `attach_file` | Everywhere | Attach one file from the checkout or scratch dir to the reply |
142
+ | `create_schedule`, `list_schedules`, `delete_schedule` | Everywhere | Run a prompt again later — interval minutes or a 5-field cron |
143
+ | `request_grant` | Everywhere | Ask the owner for reach outside the sandbox |
144
+ | `run_granted_command` | Everywhere | Run a command an approved grant covers, outside the sandbox |
145
145
 
146
146
  ## The app
147
147
 
@@ -8126,6 +8126,7 @@ async function syncAgentModelCatalog(input) {
8126
8126
 
8127
8127
  // apps/body/dist/daemon-api-client.js
8128
8128
  import { resolve as resolve7 } from "node:path";
8129
+ import { randomUUID } from "node:crypto";
8129
8130
 
8130
8131
  // node_modules/ws/wrapper.mjs
8131
8132
  var import_stream = __toESM(require_stream(), 1);
@@ -16940,12 +16941,11 @@ var DaemonApiClient = class {
16940
16941
  agentId;
16941
16942
  fetchImpl;
16942
16943
  webSocketFactory;
16944
+ presenceLifecycleId = randomUUID();
16943
16945
  liveSocket;
16944
16946
  liveReconnect;
16945
16947
  liveReconnectDelayMs = 1e3;
16946
16948
  liveRooms = /* @__PURE__ */ new Map();
16947
- parityPending = /* @__PURE__ */ new Map();
16948
- parityMisses = /* @__PURE__ */ new Map();
16949
16949
  constructor(baseUrl, daemonToken, agentId, fetchImpl = fetch, webSocketFactory = (url, protocols) => new wrapper_default(url, protocols)) {
16950
16950
  this.baseUrl = baseUrl;
16951
16951
  this.daemonToken = daemonToken;
@@ -16957,18 +16957,21 @@ var DaemonApiClient = class {
16957
16957
  connection() {
16958
16958
  return { baseUrl: this.baseUrl, daemonToken: this.daemonToken, agentId: this.agentId };
16959
16959
  }
16960
- /** Add one Room to this agent's shared live socket. Polling remains active in Phase 2. */
16961
- liveSubscribe(roomId, cursor3, onItems) {
16960
+ /** Add one Room to this agent's shared live socket. */
16961
+ liveSubscribe(roomId, cursor3, onItems, onState, presence) {
16962
16962
  const existing = this.liveRooms.get(roomId);
16963
16963
  if (existing) {
16964
16964
  existing.cursor = cursor3 ?? existing.cursor;
16965
16965
  existing.onItems = onItems ?? existing.onItems;
16966
+ existing.onState = onState ?? existing.onState;
16967
+ existing.presence = presence ?? existing.presence;
16966
16968
  } else {
16967
16969
  this.liveRooms.set(roomId, {
16968
16970
  ...cursor3 ? { cursor: cursor3 } : {},
16969
16971
  pushedIds: /* @__PURE__ */ new Set(),
16970
- parityMissIds: /* @__PURE__ */ new Set(),
16971
- ...onItems ? { onItems } : {}
16972
+ ...onItems ? { onItems } : {},
16973
+ ...onState ? { onState } : {},
16974
+ ...presence ? { presence } : {}
16972
16975
  });
16973
16976
  }
16974
16977
  this.ensureLiveSocket();
@@ -16976,11 +16979,6 @@ var DaemonApiClient = class {
16976
16979
  this.sendLiveSubscription(roomId);
16977
16980
  return () => {
16978
16981
  this.liveRooms.delete(roomId);
16979
- const pending = this.parityPending.get(roomId);
16980
- if (pending)
16981
- for (const timer of pending.values())
16982
- clearTimeout(timer);
16983
- this.parityPending.delete(roomId);
16984
16982
  if (this.liveSocket?.readyState === wrapper_default.OPEN) {
16985
16983
  this.liveSocket.send(JSON.stringify({ type: "unsubscribe", roomId }));
16986
16984
  }
@@ -16996,42 +16994,6 @@ var DaemonApiClient = class {
16996
16994
  if (room && cursor3)
16997
16995
  room.cursor = cursor3;
16998
16996
  }
16999
- /** Count a poll delivery only if push still has not produced its id after a grace window. */
17000
- notePolled(roomId, items) {
17001
- const room = this.liveRooms.get(roomId);
17002
- if (!room)
17003
- return;
17004
- const now2 = Date.now();
17005
- if (room.parityReportedAt === void 0 || now2 - room.parityReportedAt >= 6e4) {
17006
- room.parityReportedAt = now2;
17007
- console.info(`[thin-core] push parity room=${roomId} poll_only=${room.parityMissIds.size}`);
17008
- }
17009
- let pending = this.parityPending.get(roomId);
17010
- if (!pending) {
17011
- pending = /* @__PURE__ */ new Map();
17012
- this.parityPending.set(roomId, pending);
17013
- }
17014
- for (const item of items) {
17015
- if (room.pushedIds.has(item.id) || pending.has(item.id))
17016
- continue;
17017
- const timer = setTimeout(() => {
17018
- pending.delete(item.id);
17019
- if (room.pushedIds.has(item.id) || room.parityMissIds.has(item.id))
17020
- return;
17021
- room.parityMissIds.add(item.id);
17022
- while (room.parityMissIds.size > 1e4)
17023
- room.parityMissIds.delete(room.parityMissIds.values().next().value);
17024
- const count = room.parityMissIds.size;
17025
- this.parityMisses.set(roomId, count);
17026
- console.warn(`[thin-core] push parity miss room=${roomId} count=${count}`);
17027
- }, 5e3);
17028
- timer.unref?.();
17029
- pending.set(item.id, timer);
17030
- }
17031
- }
17032
- pushParityMissCount(roomId) {
17033
- return this.parityMisses.get(roomId) ?? 0;
17034
- }
17035
16997
  async execute(name, input) {
17036
16998
  const candidate = input;
17037
16999
  if (typeof candidate.agentId === "string" && candidate.agentId !== this.agentId) {
@@ -17052,7 +17014,9 @@ var DaemonApiClient = class {
17052
17014
  ensureLiveSocket() {
17053
17015
  if (this.liveSocket || !this.liveRooms.size)
17054
17016
  return;
17055
- const socket = this.webSocketFactory(this.baseUrl.replace(/^http/, "ws") + "/v1/phone/live", [`bearer.${this.daemonToken}`]);
17017
+ const socket = this.webSocketFactory(this.baseUrl.replace(/^http/, "ws") + "/v1/phone/live", [
17018
+ `bearer.${this.daemonToken}`
17019
+ ]);
17056
17020
  this.liveSocket = socket;
17057
17021
  socket.onopen = () => {
17058
17022
  this.liveReconnectDelayMs = 1e3;
@@ -17069,6 +17033,14 @@ var DaemonApiClient = class {
17069
17033
  if (!value || typeof value !== "object")
17070
17034
  return;
17071
17035
  const event = value;
17036
+ if (event.type === "subscribed" && typeof event.roomId === "string") {
17037
+ const capabilities = event.capabilities;
17038
+ this.liveRooms.get(event.roomId)?.onState?.(true, {
17039
+ pushIntake: capabilities?.pushIntake === true,
17040
+ connectionPresence: capabilities?.connectionPresence === true
17041
+ });
17042
+ return;
17043
+ }
17072
17044
  if (event.type !== "inbox" || typeof event.roomId !== "string" || !Array.isArray(event.items))
17073
17045
  return;
17074
17046
  const room = this.liveRooms.get(event.roomId);
@@ -17083,14 +17055,6 @@ var DaemonApiClient = class {
17083
17055
  if (!room.pushedIds.has(id))
17084
17056
  items.push(candidate);
17085
17057
  room.pushedIds.add(id);
17086
- if (room.parityMissIds.delete(id)) {
17087
- this.parityMisses.set(event.roomId, room.parityMissIds.size);
17088
- console.info(`[thin-core] push parity room=${event.roomId} poll_only=${room.parityMissIds.size}`);
17089
- }
17090
- const pending = this.parityPending.get(event.roomId)?.get(id);
17091
- if (pending)
17092
- clearTimeout(pending);
17093
- this.parityPending.get(event.roomId)?.delete(id);
17094
17058
  }
17095
17059
  }
17096
17060
  while (room.pushedIds.size > 1e4)
@@ -17102,6 +17066,8 @@ var DaemonApiClient = class {
17102
17066
  if (this.liveSocket !== socket)
17103
17067
  return;
17104
17068
  this.liveSocket = void 0;
17069
+ for (const room of this.liveRooms.values())
17070
+ room.onState?.(false);
17105
17071
  if (!this.liveRooms.size)
17106
17072
  return;
17107
17073
  const delay = this.liveReconnectDelayMs;
@@ -17119,7 +17085,9 @@ var DaemonApiClient = class {
17119
17085
  this.liveSocket.send(JSON.stringify({
17120
17086
  type: "subscribe",
17121
17087
  roomId,
17122
- ...room.cursor ? { cursor: room.cursor } : {}
17088
+ lifecycleId: this.presenceLifecycleId,
17089
+ ...room.cursor ? { cursor: room.cursor } : {},
17090
+ ...room.presence
17123
17091
  }));
17124
17092
  }
17125
17093
  };
@@ -18018,7 +17986,7 @@ import { promisify as promisify3 } from "node:util";
18018
17986
 
18019
17987
  // apps/body/dist/agent-home.js
18020
17988
  import { existsSync as existsSync3, readFileSync as readFileSync4 } from "node:fs";
18021
- import { createHash as createHash3, randomUUID } from "node:crypto";
17989
+ import { createHash as createHash3, randomUUID as randomUUID2 } from "node:crypto";
18022
17990
  import { chmod as chmod2, copyFile, lstat, mkdir as mkdir4, readFile as readFile6, readdir as readdir2, realpath, rename as rename2, rm as rm2, symlink, unlink, writeFile as writeFile5 } from "node:fs/promises";
18023
17991
  import { homedir as homedir5 } from "node:os";
18024
17992
  import { basename as basename3, dirname as dirname5, join as join3, relative as relative2, resolve as resolve13, sep } from "node:path";
@@ -18940,7 +18908,7 @@ async function provisionManagedSkillsDir(target, managedSkills, sharedSkills, op
18940
18908
  const plan = await planManagedSkills(managedSkills, sharedSkills, optionalShares);
18941
18909
  if (await materializedSkillManifest(target) === plan.manifest)
18942
18910
  return;
18943
- const staged = resolve13(parent, `.skills.${process.pid}.${randomUUID()}.tmp`);
18911
+ const staged = resolve13(parent, `.skills.${process.pid}.${randomUUID2()}.tmp`);
18944
18912
  await mkdir4(staged, { mode: 448 });
18945
18913
  try {
18946
18914
  for (const entry of plan.entries) {
@@ -19192,7 +19160,7 @@ async function writeIsolatedHarnessFile(path, content) {
19192
19160
  if (!parentStats.isDirectory() || parentStats.isSymbolicLink()) {
19193
19161
  throw new Error(`isolated harness parent is not a real directory: ${parent}`);
19194
19162
  }
19195
- const temporary = resolve13(parent, `.${basename3(path)}.${process.pid}.${randomUUID()}.tmp`);
19163
+ const temporary = resolve13(parent, `.${basename3(path)}.${process.pid}.${randomUUID2()}.tmp`);
19196
19164
  try {
19197
19165
  await writeFile5(temporary, content, { mode: 384, flag: "wx" });
19198
19166
  await chmod2(temporary, 384);
@@ -20807,6 +20775,14 @@ function cornerClosePollMs(random = Math.random) {
20807
20775
  var MonolithCornerTurnLoop = class {
20808
20776
  options;
20809
20777
  agent;
20778
+ reconciliationRequested = true;
20779
+ wakeIntake;
20780
+ /** Called by the daemon's one slow workspace reconciliation sweep. */
20781
+ requestReconciliation() {
20782
+ this.reconciliationRequested = true;
20783
+ this.wakeIntake?.();
20784
+ this.wakeIntake = void 0;
20785
+ }
20810
20786
  client;
20811
20787
  sessionId;
20812
20788
  /** The configuration the live session baked in; a change invalidates it. */
@@ -20951,11 +20927,9 @@ var MonolithCornerTurnLoop = class {
20951
20927
  })
20952
20928
  }) : {};
20953
20929
  const command = this.options.config.agentCommand ?? this.options.config.agentBinary;
20954
- let githubEnv = {
20955
- GH_TOKEN: this.options.githubToken,
20956
- GITHUB_TOKEN: this.options.githubToken
20957
- };
20958
- if (this.options.config.runtimeConfigPath && this.options.config.agentHomeRoot) {
20930
+ const repository = this.options.repository;
20931
+ let githubEnv = repository ? { GH_TOKEN: repository.githubToken, GITHUB_TOKEN: repository.githubToken } : {};
20932
+ if (repository && this.options.config.runtimeConfigPath && this.options.config.agentHomeRoot) {
20959
20933
  const gitBinary = (await execFileAsync3("which", ["git"])).stdout.trim();
20960
20934
  const ghBinary = await execFileAsync3("which", ["gh"]).then((result) => result.stdout.trim()).catch(() => void 0);
20961
20935
  githubEnv = await installCornerGitHubWrappers({
@@ -20994,7 +20968,7 @@ var MonolithCornerTurnLoop = class {
20994
20968
  mode: "edit",
20995
20969
  cwd: this.options.worktreePath,
20996
20970
  worktreePath: this.options.worktreePath,
20997
- gitCommonDir: this.options.gitCommonDir,
20971
+ ...repository ? { gitCommonDir: repository.gitCommonDir } : {},
20998
20972
  protectedPaths: [this.options.runtime.supervisorRoot],
20999
20973
  harnessStateDirs: stateDirs,
21000
20974
  harnessHomeStateDirs: homeStateDirs,
@@ -21017,19 +20991,21 @@ var MonolithCornerTurnLoop = class {
21017
20991
  this.client = (this.options.createAcpClient ?? ((value) => new AcpClient(value)))(clientOptions);
21018
20992
  await this.client.start();
21019
20993
  const servers = [
21020
- {
21021
- name: "buzz-dev-mcp",
21022
- command: this.options.config.mcpBinary,
21023
- args: [],
21024
- // ACP hosts launch stdio MCP servers with an explicit, sanitized env.
21025
- // This token is minted for this exact corner and is also the credential
21026
- // helper's password source, so its shell commands need the same scope as
21027
- // the corner harness without inheriting any host credentials.
21028
- env: [
21029
- { name: "GH_TOKEN", value: this.options.githubToken },
21030
- { name: "GITHUB_TOKEN", value: this.options.githubToken }
21031
- ]
21032
- },
20994
+ ...repository ? [
20995
+ {
20996
+ name: "buzz-dev-mcp",
20997
+ command: this.options.config.mcpBinary,
20998
+ args: [],
20999
+ // ACP hosts launch stdio MCP servers with an explicit, sanitized env.
21000
+ // This token is minted for this exact corner and is also the credential
21001
+ // helper's password source, so its shell commands need the same scope as
21002
+ // the corner harness without inheriting any host credentials.
21003
+ env: [
21004
+ { name: "GH_TOKEN", value: repository.githubToken },
21005
+ { name: "GITHUB_TOKEN", value: repository.githubToken }
21006
+ ]
21007
+ }
21008
+ ] : [],
21033
21009
  beelineAgentMcpServer(this.options.config, this.options.api, {
21034
21010
  roomId: this.options.parentRoomId,
21035
21011
  workspaceId: this.options.workspaceId,
@@ -21060,17 +21036,23 @@ var MonolithCornerTurnLoop = class {
21060
21036
  systemPrompt: [
21061
21037
  identityInstructions,
21062
21038
  personaInstructions,
21063
- `You are in an isolated git worktree on ${this.options.featureBranch}, targeting ${this.options.targetBranch}.`,
21064
- "Work normally with the full coding tools. Commit and push only this feature branch. Use gh to open its pull request.",
21065
- `This corner is shared: any of its member agents may be addressed in it and work on ${this.options.featureBranch}. Run git pull --rebase origin ${this.options.featureBranch} before you push, and never force-push it.`,
21066
- "PR-opening turn rule: as soon as a pull request exists, print its full GitHub URL as your final response and end the turn immediately. Do not call pr_checks_status in that same turn and do not wait for checks inside it. Then stay idle until a later corner fact or human message starts another turn.",
21067
- 'Never merge because local tests pass or because gh reports passing checks. On a later turn triggered by a server-posted checks-passed note, call beeline-agent pr_checks_status. Merge only when it returns checks="passed", held=false, and approvalPending=false.',
21068
- "Merge the PR yourself only after the checks-passed event shows every check green; if any check failed or is still running, say exactly which and stop - never merge red.",
21069
- "If any human in this corner says hold or do not merge, do not merge until a later human explicitly resumes it.",
21070
- "Do not tag the user when a corner turn finishes: the server posts the merge summary card and its push already cover completion. Tag a human only mid-turn, and only when you need a decision or input.",
21071
- 'GitHub check and merge notes are server lines already in the corner: never restate them (no "checks passed", "CI is green", "PR ready for review"). On a checks turn, say nothing unless you act - a merge or a pushed fix - and then one short line about that.',
21072
- "A human approval in the app asks the server to merge. When approval is pending, wait for the server close request instead of racing it with gh. If checks passed, no hold exists, and no approval is pending, merge the pull request yourself with gh.",
21073
- "Never push directly to the target branch. Never merge a different pull request."
21039
+ ...repository ? [
21040
+ `You are in an isolated git worktree on ${repository.featureBranch}, targeting ${repository.targetBranch}.`,
21041
+ "Work normally with the full coding tools. Commit and push only this feature branch. Use gh to open its pull request.",
21042
+ `This corner is shared: any of its member agents may be addressed in it and work on ${repository.featureBranch}. Run git pull --rebase origin ${repository.featureBranch} before you push, and never force-push it.`,
21043
+ "PR-opening turn rule: as soon as a pull request exists, print its full GitHub URL as your final response and end the turn immediately. Do not call pr_checks_status in that same turn and do not wait for checks inside it. Then stay idle until a later corner fact or human message starts another turn.",
21044
+ 'Never merge because local tests pass or because gh reports passing checks. On a later turn triggered by a server-posted checks-passed note, call beeline-agent pr_checks_status. Merge only when it returns checks="passed", held=false, and approvalPending=false.',
21045
+ "Merge the PR yourself only after the checks-passed event shows every check green; if any check failed or is still running, say exactly which and stop - never merge red.",
21046
+ "If any human in this corner says hold or do not merge, do not merge until a later human explicitly resumes it.",
21047
+ "Do not tag the user when a corner turn finishes: the server posts the merge summary card and its push already cover completion. Tag a human only mid-turn, and only when you need a decision or input.",
21048
+ 'GitHub check and merge notes are server lines already in the corner: never restate them (no "checks passed", "CI is green", "PR ready for review"). On a checks turn, say nothing unless you act - a merge or a pushed fix - and then one short line about that.',
21049
+ "A human approval in the app asks the server to merge. When approval is pending, wait for the server close request instead of racing it with gh. If checks passed, no hold exists, and no approval is pending, merge the pull request yourself with gh.",
21050
+ "Never push directly to the target branch. Never merge a different pull request."
21051
+ ] : [
21052
+ "This is a chat-only corner with no repository or GitHub workflow.",
21053
+ "Work in this corner's writable workspace. Use write_scratch_file or ordinary tools to create files, then attach_file to send them back to the corner.",
21054
+ "Do not initialize a repository, create a branch, push, open a pull request, or wait for GitHub checks."
21055
+ ]
21074
21056
  ].filter(Boolean).join("\n\n")
21075
21057
  });
21076
21058
  this.sessionId = opened.sessionId;
@@ -21203,7 +21185,7 @@ ${this.options.objective}`,
21203
21185
  ${trigger}`,
21204
21186
  ...attachmentPromptLines(attachments, delivered, this.acceptsImages())
21205
21187
  ].join("\n"),
21206
- "Continue the objective. Obey the PR checks and human hold rules in your session instructions.",
21188
+ this.options.repository ? "Continue the objective. Obey the PR checks and human hold rules in your session instructions." : "Continue the objective. Attach completed files before calling close_corner.",
21207
21189
  MAINTAIN_ASSIGNED_IDENTITY_DIRECTIVE
21208
21190
  ].filter(Boolean).join("\n\n");
21209
21191
  const stream = new AgentTurnStream({
@@ -21353,10 +21335,13 @@ ${trigger}`,
21353
21335
  * fails with that sentence and the server inscribes it in the corner.
21354
21336
  */
21355
21337
  async syncBranch() {
21356
- const token = await this.options.api.execute("getRoomGitHubToken", { roomId: this.options.parentRoomId }).then((granted) => granted.token).catch(() => this.options.githubToken);
21338
+ const repository = this.options.repository;
21339
+ if (!repository)
21340
+ return;
21341
+ const token = await this.options.api.execute("getRoomGitHubToken", { roomId: this.options.parentRoomId }).then((granted) => granted.token).catch(() => repository.githubToken);
21357
21342
  await syncCornerBranch({
21358
21343
  worktreePath: this.options.worktreePath,
21359
- featureBranch: this.options.featureBranch,
21344
+ featureBranch: repository.featureBranch,
21360
21345
  env: { ...process.env, GH_TOKEN: token, GITHUB_TOKEN: token, GIT_TERMINAL_PROMPT: "0" }
21361
21346
  });
21362
21347
  }
@@ -21384,15 +21369,23 @@ ${trigger}`,
21384
21369
  const processedInboxIds = /* @__PURE__ */ new Set();
21385
21370
  const pushedInbox = [];
21386
21371
  let pendingPushedCursor;
21387
- let wakeForPush;
21372
+ let liveConnected = false;
21388
21373
  const rewindSupported = Array.isArray(activation.rewindIds);
21389
21374
  for (const id of activation.rewindIds ?? [])
21390
21375
  processedInboxIds.add(id);
21391
21376
  const stopLive = api.liveSubscribe?.(cornerId, cursor3, (items, pushedCursor) => {
21392
21377
  pushedInbox.push(...items);
21393
21378
  pendingPushedCursor = laterInboxCursor(pendingPushedCursor, pushedCursor);
21394
- wakeForPush?.();
21395
- wakeForPush = void 0;
21379
+ this.wakeIntake?.();
21380
+ this.wakeIntake = void 0;
21381
+ }, (connected, capabilities) => {
21382
+ liveConnected = connected && capabilities?.pushIntake === true;
21383
+ this.wakeIntake?.();
21384
+ this.wakeIntake = void 0;
21385
+ }, {
21386
+ ...this.options.config.daemonReleaseVersion ? { releaseVersion: this.options.config.daemonReleaseVersion } : {},
21387
+ ...this.options.config.daemonSourceSha ? { sourceSha: this.options.config.daemonSourceSha } : {},
21388
+ available: !this.options.config.modelUnavailable
21396
21389
  });
21397
21390
  const history = await api.execute("getRoomConversation", { roomId: cornerId, limit: 200 });
21398
21391
  await this.roster().catch(() => void 0);
@@ -21407,14 +21400,14 @@ ${trigger}`,
21407
21400
  let pollWithoutWait = false;
21408
21401
  while (!signal?.aborted) {
21409
21402
  try {
21410
- const pollNow = pushedInbox.length === 0;
21403
+ const pollNow = pushedInbox.length > 0 || !liveConnected || this.reconciliationRequested;
21411
21404
  const inbox = !pollNow ? { items: [], cursor: void 0, closeRequested: false } : await api.execute("getCornerCloseRequests", {
21412
21405
  cornerId,
21413
21406
  ...cursor3 ? { after: cursor3 } : {},
21414
21407
  ...rewindSupported ? { rewind: true } : {}
21415
21408
  });
21416
21409
  if (pollNow) {
21417
- api.notePolled?.(cornerId, inbox.items.filter((item) => !processedInboxIds.has(item.id)));
21410
+ this.reconciliationRequested = false;
21418
21411
  }
21419
21412
  if (inbox.closeRequested) {
21420
21413
  await this.options.onCloseRequested();
@@ -21473,10 +21466,9 @@ This answers your grant request; resume the paused work. If approved and it is a
21473
21466
  if (pollNow)
21474
21467
  this.options.onPoll();
21475
21468
  await Promise.race([
21476
- wait(pollWithoutWait ? 0 : this.options.pollMs ?? cornerClosePollMs(), signal),
21477
- waitForWake(api, cornerId, signal),
21469
+ wait(pollWithoutWait ? 0 : liveConnected ? 2147483647 : this.options.pollMs ?? cornerClosePollMs(), signal),
21478
21470
  pushedInbox.length ? Promise.resolve() : new Promise((resolve30) => {
21479
- wakeForPush = resolve30;
21471
+ this.wakeIntake = resolve30;
21480
21472
  })
21481
21473
  ]);
21482
21474
  pollWithoutWait = false;
@@ -21490,27 +21482,12 @@ This answers your grant request; resume the paused work. If approved and it is a
21490
21482
  }
21491
21483
  } finally {
21492
21484
  stopLive?.();
21485
+ this.wakeIntake = void 0;
21493
21486
  this.options.grantRunner?.unregister(cornerId);
21494
21487
  await this.options.scheduler.suspend(cornerId);
21495
21488
  }
21496
21489
  }
21497
21490
  };
21498
- async function waitForWake(api, cornerId, signal) {
21499
- if (signal?.aborted)
21500
- return;
21501
- try {
21502
- await api.execute("waitForCornerWake", { cornerId });
21503
- } catch {
21504
- await never(signal);
21505
- }
21506
- }
21507
- async function never(signal) {
21508
- if (signal?.aborted)
21509
- return;
21510
- await new Promise((resolveNever) => {
21511
- signal?.addEventListener("abort", () => resolveNever(), { once: true });
21512
- });
21513
- }
21514
21491
  async function wait(ms, signal) {
21515
21492
  if (signal?.aborted)
21516
21493
  return;
@@ -21658,6 +21635,14 @@ function agentReplyMentionIds(text2, roster, authorId) {
21658
21635
  var MonolithRoomTurnLoop = class {
21659
21636
  options;
21660
21637
  agent;
21638
+ reconciliationRequested = true;
21639
+ wakeIntake;
21640
+ /** Called by the daemon's one slow workspace reconciliation sweep. */
21641
+ requestReconciliation() {
21642
+ this.reconciliationRequested = true;
21643
+ this.wakeIntake?.();
21644
+ this.wakeIntake = void 0;
21645
+ }
21661
21646
  client;
21662
21647
  sessionId;
21663
21648
  /** The configuration the live session baked in; a change invalidates it. */
@@ -22251,25 +22236,11 @@ var MonolithRoomTurnLoop = class {
22251
22236
  }
22252
22237
  async run() {
22253
22238
  const { api, roomId, signal } = this.options;
22254
- const status = this.options.config.modelUnavailable ? "offline" : "online";
22255
- const postPresence = async (presence) => {
22256
- await api.execute("postAgentPresence", {
22257
- agentId: this.agent.publicKey,
22258
- roomId,
22259
- status: presence,
22260
- ...this.options.config.daemonReleaseVersion ? { releaseVersion: this.options.config.daemonReleaseVersion } : {},
22261
- ...this.options.config.daemonSourceSha ? { sourceSha: this.options.config.daemonSourceSha } : {}
22262
- });
22263
- this.options.health.presence(presence);
22264
- };
22265
- await postPresence(status);
22266
- const heartbeat = setInterval(() => void postPresence(status).catch((error) => console.error(`[thin-core] monolith Room ${roomId} presence heartbeat failed:`, error)), 3e4);
22267
- heartbeat.unref?.();
22268
22239
  let cursor3;
22269
22240
  const processedInboxIds = /* @__PURE__ */ new Set();
22270
22241
  const pushedInbox = [];
22271
22242
  let pendingPushedCursor;
22272
- let wakeForPush;
22243
+ let liveConnected = false;
22273
22244
  let stopLive;
22274
22245
  try {
22275
22246
  const activation = await api.execute("getRoomInbox", { roomId, startAtLatest: true });
@@ -22280,15 +22251,24 @@ var MonolithRoomTurnLoop = class {
22280
22251
  stopLive = api.liveSubscribe?.(roomId, cursor3, (items, pushedCursor) => {
22281
22252
  pushedInbox.push(...items);
22282
22253
  pendingPushedCursor = laterInboxCursor(pendingPushedCursor, pushedCursor);
22283
- wakeForPush?.();
22284
- wakeForPush = void 0;
22254
+ this.wakeIntake?.();
22255
+ this.wakeIntake = void 0;
22256
+ }, (connected, capabilities) => {
22257
+ liveConnected = connected && capabilities?.pushIntake === true;
22258
+ this.options.health.presence(connected && !this.options.config.modelUnavailable ? "online" : "offline");
22259
+ this.wakeIntake?.();
22260
+ this.wakeIntake = void 0;
22261
+ }, {
22262
+ ...this.options.config.daemonReleaseVersion ? { releaseVersion: this.options.config.daemonReleaseVersion } : {},
22263
+ ...this.options.config.daemonSourceSha ? { sourceSha: this.options.config.daemonSourceSha } : {},
22264
+ available: !this.options.config.modelUnavailable
22285
22265
  });
22286
22266
  while (!signal?.aborted) {
22287
22267
  try {
22288
22268
  if (!this.activeTurn && this.queuedTurns.length) {
22289
22269
  this.startPrompt(this.queuedTurns.shift());
22290
22270
  }
22291
- const pollNow = pushedInbox.length === 0;
22271
+ const pollNow = pushedInbox.length === 0 && (!liveConnected || this.reconciliationRequested);
22292
22272
  const inbox = !pollNow ? { items: [], cursor: void 0 } : await api.execute("getRoomInbox", {
22293
22273
  roomId,
22294
22274
  ...cursor3 ? { after: cursor3 } : {},
@@ -22296,7 +22276,7 @@ var MonolithRoomTurnLoop = class {
22296
22276
  limit: 200
22297
22277
  });
22298
22278
  if (pollNow) {
22299
- api.notePolled?.(roomId, inbox.items.filter((item) => !processedInboxIds.has(item.id)));
22279
+ this.reconciliationRequested = false;
22300
22280
  }
22301
22281
  const delivered = [...pushedInbox.splice(0), ...inbox.items];
22302
22282
  for (const item of delivered) {
@@ -22331,9 +22311,9 @@ var MonolithRoomTurnLoop = class {
22331
22311
  this.options.health.poll();
22332
22312
  if (!pushedInbox.length) {
22333
22313
  await Promise.race([
22334
- wait2(this.options.pollMs ?? 1e3, signal),
22314
+ wait2(liveConnected ? 2147483647 : this.options.pollMs ?? 1e3, signal),
22335
22315
  new Promise((resolve30) => {
22336
- wakeForPush = resolve30;
22316
+ this.wakeIntake = resolve30;
22337
22317
  })
22338
22318
  ]);
22339
22319
  }
@@ -22347,13 +22327,12 @@ var MonolithRoomTurnLoop = class {
22347
22327
  }
22348
22328
  } finally {
22349
22329
  stopLive?.();
22350
- clearInterval(heartbeat);
22330
+ this.wakeIntake = void 0;
22351
22331
  this.options.grantRunner?.unregister(roomId);
22352
22332
  if (this.activeTurn?.phase === "prompting" && this.client && this.sessionId) {
22353
22333
  this.client.sessionCancel(this.sessionId);
22354
22334
  }
22355
22335
  await this.activeTurn?.promise;
22356
- await postPresence("offline").catch((error) => console.error(`[thin-core] monolith Room ${roomId} offline presence failed:`, error));
22357
22336
  await this.options.scheduler.suspend(roomId);
22358
22337
  }
22359
22338
  }
@@ -22844,7 +22823,14 @@ async function materializeCornerWorktree(input) {
22844
22823
  "credential.https://github.com.helper",
22845
22824
  "!f() { echo username=x-access-token; echo password=$GH_TOKEN; }; f"
22846
22825
  ]);
22847
- await execFileAsync4("git", ["-C", path, "config", "--worktree", "user.name", input.committer.name]);
22826
+ await execFileAsync4("git", [
22827
+ "-C",
22828
+ path,
22829
+ "config",
22830
+ "--worktree",
22831
+ "user.name",
22832
+ input.committer.name
22833
+ ]);
22848
22834
  await execFileAsync4("git", [
22849
22835
  "-C",
22850
22836
  path,
@@ -22873,6 +22859,13 @@ async function mapWithConcurrency(values, limit, visit) {
22873
22859
  }));
22874
22860
  }
22875
22861
  var execFileAsync4 = promisify4(execFile5);
22862
+ async function removeCornerScratchWorkspace(input) {
22863
+ const expected = resolve19(input.roomRoot, "scratch");
22864
+ if (resolve19(input.scratchPath) !== expected) {
22865
+ throw new Error(`refusing to remove scratch outside corner ${input.cornerId}`);
22866
+ }
22867
+ await rm4(expected, { recursive: true, force: true });
22868
+ }
22876
22869
  var RoomRuntimeCoordinator = class {
22877
22870
  configPath;
22878
22871
  baseConfig;
@@ -23037,6 +23030,8 @@ var RoomRuntimeCoordinator = class {
23037
23030
  await running.promise.catch(() => void 0);
23038
23031
  if (running.worktree)
23039
23032
  await this.reapCornerWorktree(running.worktree);
23033
+ else if (running.scratch)
23034
+ await this.reapCornerScratch(running.scratch);
23040
23035
  }
23041
23036
  await mapWithConcurrency(desiredTopRooms, ROOM_JOIN_CONCURRENCY, async (roomId) => {
23042
23037
  if (!this.running.has(roomId))
@@ -23046,6 +23041,8 @@ var RoomRuntimeCoordinator = class {
23046
23041
  if (!this.running.has(corner.cornerId))
23047
23042
  await this.startCorner(corner);
23048
23043
  });
23044
+ for (const running of this.running.values())
23045
+ running.body.requestReconciliation();
23049
23046
  return "member";
23050
23047
  }
23051
23048
  roomRecord(roomId) {
@@ -23069,8 +23066,7 @@ var RoomRuntimeCoordinator = class {
23069
23066
  return void 0;
23070
23067
  }
23071
23068
  }
23072
- roomConfig(roomId) {
23073
- const workspaceRoot = this.roomRoot(roomId);
23069
+ roomConfig(roomId, workspaceRoot = this.roomRoot(roomId)) {
23074
23070
  const agentHomeRoot = this.roomAgentHomeRoot(workspaceRoot, true);
23075
23071
  return {
23076
23072
  ...this.baseConfig,
@@ -23174,7 +23170,7 @@ var RoomRuntimeCoordinator = class {
23174
23170
  return;
23175
23171
  this.startingCorners.add(corner.cornerId);
23176
23172
  try {
23177
- const [restore, repository, conversation, granted] = await Promise.all([
23173
+ const [restore, repository, conversation] = await Promise.all([
23178
23174
  this.options.daemonApi.execute("getCornerRestoreState", { cornerId: corner.cornerId }),
23179
23175
  this.options.daemonApi.execute("getRoomRepositoryState", {
23180
23176
  roomId: corner.parentRoomId
@@ -23186,28 +23182,35 @@ var RoomRuntimeCoordinator = class {
23186
23182
  roomId: corner.cornerId,
23187
23183
  limit: 200,
23188
23184
  window: "earliest"
23189
- }),
23190
- this.options.daemonApi.execute("getRoomGitHubToken", {
23191
- roomId: corner.parentRoomId
23192
23185
  })
23193
23186
  ]);
23194
- if (repository.resolution !== "repository" || !repository.remote || !repository.key) {
23195
- throw new Error("corner parent Room has no verified repository binding");
23187
+ if (repository.resolution === "unverified") {
23188
+ throw new Error("corner parent Room repository state is not verified yet");
23189
+ }
23190
+ if (repository.resolution === "repository" && (!repository.remote || !repository.key)) {
23191
+ throw new Error("corner parent Room has an incomplete repository binding");
23196
23192
  }
23197
23193
  const objective = conversation.items.find((item) => item.type === "message")?.body.trim();
23198
23194
  if (!objective)
23199
23195
  throw new Error("corner has no durable objective post");
23200
- const targetBranch = repository.targetBranch || "main";
23201
- const featureBranch = restore.featureBranch ?? `feature/corner-${corner.cornerId.replaceAll("-", "").slice(0, 12)}`;
23202
- const worktree = await this.materializeCornerWorktree({
23196
+ const repositoryBacked = repository.resolution === "repository";
23197
+ const targetBranch = repositoryBacked ? repository.targetBranch || "main" : void 0;
23198
+ const featureBranch = repositoryBacked ? restore.featureBranch ?? `feature/corner-${corner.cornerId.replaceAll("-", "").slice(0, 12)}` : void 0;
23199
+ const granted = repositoryBacked ? await this.options.daemonApi.execute("getRoomGitHubToken", {
23200
+ roomId: corner.parentRoomId
23201
+ }) : void 0;
23202
+ const worktree = repositoryBacked ? await this.materializeCornerWorktree({
23203
23203
  cornerId: corner.cornerId,
23204
23204
  remote: repository.remote,
23205
23205
  targetBranch,
23206
23206
  featureBranch,
23207
23207
  token: granted.token
23208
- });
23208
+ }) : void 0;
23209
+ const workspacePath = worktree?.path ?? resolve19(this.roomRoot(corner.cornerId), "scratch");
23210
+ if (!worktree)
23211
+ await mkdir11(workspacePath, { recursive: true, mode: 448 });
23209
23212
  const isOpener = !corner.openedBy || corner.openedBy === this.agent.publicKey;
23210
- if (shouldPostInitialCornerWorkingState(restore, isOpener)) {
23213
+ if (worktree && shouldPostInitialCornerWorkingState(restore, isOpener)) {
23211
23214
  await this.options.daemonApi.execute("postCornerRemoteState", {
23212
23215
  cornerId: corner.cornerId,
23213
23216
  branch: featureBranch,
@@ -23226,23 +23229,27 @@ var RoomRuntimeCoordinator = class {
23226
23229
  workspaceId: this.runtime.communityId,
23227
23230
  ...corner.openedBy ? { openedBy: corner.openedBy } : {},
23228
23231
  objective,
23229
- featureBranch,
23230
- targetBranch,
23231
- worktreePath: worktree.path,
23232
- gitCommonDir: worktree.gitCommonDir,
23233
- githubToken: granted.token,
23232
+ worktreePath: workspacePath,
23233
+ ...worktree ? {
23234
+ repository: {
23235
+ featureBranch,
23236
+ targetBranch,
23237
+ gitCommonDir: worktree.gitCommonDir,
23238
+ githubToken: granted.token
23239
+ }
23240
+ } : {},
23234
23241
  runtime: this.runtime,
23235
- config: this.roomConfig(corner.cornerId),
23242
+ config: this.roomConfig(corner.cornerId, worktree ? void 0 : workspacePath),
23236
23243
  api: this.options.daemonApi,
23237
23244
  scheduler: this.scheduler,
23238
23245
  signal: controller.signal,
23239
23246
  onPoll: () => this.notePoll(corner.cornerId),
23240
23247
  onFailure: (retryInMs) => this.noteFailure(corner.cornerId, retryInMs),
23241
- onCloseRequested: () => this.reapCornerWorktree({
23248
+ onCloseRequested: () => worktree ? this.reapCornerWorktree({
23242
23249
  ...worktree,
23243
23250
  cornerId: corner.cornerId,
23244
23251
  branch: featureBranch
23245
- })
23252
+ }) : this.reapCornerScratch({ path: workspacePath, cornerId: corner.cornerId })
23246
23253
  });
23247
23254
  const promise = loop.run().catch((error) => {
23248
23255
  if (!controller.signal.aborted) {
@@ -23260,14 +23267,17 @@ var RoomRuntimeCoordinator = class {
23260
23267
  lastPollAt: startedAt,
23261
23268
  backoffUntil: 0,
23262
23269
  recovering: false,
23263
- worktree: {
23264
- ...worktree,
23265
- cornerId: corner.cornerId,
23266
- branch: featureBranch
23267
- }
23270
+ ...worktree ? {
23271
+ worktree: {
23272
+ ...worktree,
23273
+ cornerId: corner.cornerId,
23274
+ branch: featureBranch
23275
+ }
23276
+ } : {},
23277
+ ...!worktree ? { scratch: { path: workspacePath, cornerId: corner.cornerId } } : {}
23268
23278
  });
23269
23279
  this.reportedCornerStartFailures.delete(corner.cornerId);
23270
- console.log(`[thin-core] serving corner ${corner.cornerId} on ${featureBranch} at ${worktree.path}`);
23280
+ console.log(worktree ? `[thin-core] serving corner ${corner.cornerId} on ${featureBranch} at ${workspacePath}` : `[thin-core] serving chat-only corner ${corner.cornerId} at ${workspacePath}`);
23271
23281
  } catch (error) {
23272
23282
  console.error(`[thin-core] failed to start corner ${corner.cornerId}:`, error);
23273
23283
  await this.reportCornerStartFailure(corner.cornerId, error);
@@ -23334,6 +23344,13 @@ var RoomRuntimeCoordinator = class {
23334
23344
  checks: "unknown"
23335
23345
  });
23336
23346
  }
23347
+ async reapCornerScratch(scratch) {
23348
+ await removeCornerScratchWorkspace({
23349
+ cornerId: scratch.cornerId,
23350
+ roomRoot: this.roomRoot(scratch.cornerId),
23351
+ scratchPath: scratch.path
23352
+ });
23353
+ }
23337
23354
  notePoll(roomId) {
23338
23355
  const room = this.running.get(roomId);
23339
23356
  if (!room)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "usebeeline",
3
- "version": "0.0.58",
3
+ "version": "0.0.62",
4
4
  "description": "Connect an AI coding agent to Beeline with one command.",
5
5
  "homepage": "https://usebeeline.app",
6
6
  "bugs": {