usebeeline 0.0.84 → 0.0.91
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/usebeeline.mjs +185 -109
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -137,7 +137,7 @@ Two MCP surfaces are mounted into every agent session.
|
|
|
137
137
|
| Tool | Where | What it does |
|
|
138
138
|
| ------------------------------------------------------ | --------------- | ------------------------------------------------------------- |
|
|
139
139
|
| `open_corner` | Top-level Rooms | Open one write-enabled corner with a ≤24-word objective |
|
|
140
|
-
| `pr_checks_status` | Corners | Read
|
|
140
|
+
| `pr_checks_status` | Corners | Read checks, human hold, and PR/head-bound merge approval |
|
|
141
141
|
| `attach_file` | Everywhere | Attach one file from the checkout or scratch dir to the reply |
|
|
142
142
|
| `create_schedule`, `list_schedules`, `delete_schedule` | Everywhere | Run a prompt again later — interval minutes or a 5-field cron |
|
|
143
143
|
| `request_grant` | Everywhere | Ask the owner for reach outside the sandbox |
|
package/dist/usebeeline.mjs
CHANGED
|
@@ -19777,7 +19777,8 @@ function sessionConfigFingerprint(input) {
|
|
|
19777
19777
|
input.effort ?? "",
|
|
19778
19778
|
input.soul?.name ?? "",
|
|
19779
19779
|
input.soul?.instructions ?? "",
|
|
19780
|
-
input.agentName ?? ""
|
|
19780
|
+
input.agentName ?? "",
|
|
19781
|
+
input.yoloMode ?? false
|
|
19781
19782
|
]);
|
|
19782
19783
|
}
|
|
19783
19784
|
|
|
@@ -19968,9 +19969,6 @@ export default async function (pi) {
|
|
|
19968
19969
|
import { execFile as execFile3 } from "node:child_process";
|
|
19969
19970
|
import { promisify as promisify2 } from "node:util";
|
|
19970
19971
|
var execFileAsync2 = promisify2(execFile3);
|
|
19971
|
-
var CornerBranchDivergedError = class extends Error {
|
|
19972
|
-
name = "CornerBranchDivergedError";
|
|
19973
|
-
};
|
|
19974
19972
|
async function syncCornerBranch(input) {
|
|
19975
19973
|
const git = input.git ?? (async (args) => (await execFileAsync2("git", ["-C", input.worktreePath, ...args], {
|
|
19976
19974
|
...input.env ? { env: input.env } : {},
|
|
@@ -19996,22 +19994,15 @@ async function syncCornerBranch(input) {
|
|
|
19996
19994
|
try {
|
|
19997
19995
|
await git(behind ? ["merge", "--ff-only", remote] : ["rebase", remote]);
|
|
19998
19996
|
return behind ? "fast-forwarded" : "rebased";
|
|
19999
|
-
} catch
|
|
19997
|
+
} catch {
|
|
20000
19998
|
await git(["rebase", "--abort"]).catch(() => void 0);
|
|
20001
|
-
|
|
19999
|
+
await git(["reset", "--hard", remote]);
|
|
20000
|
+
return "realigned";
|
|
20002
20001
|
}
|
|
20003
20002
|
}
|
|
20004
20003
|
async function contains(git, head, ancestor) {
|
|
20005
20004
|
return git(["merge-base", "--is-ancestor", ancestor, head]).then(() => true, () => false);
|
|
20006
20005
|
}
|
|
20007
|
-
function divergedReason(featureBranch, error) {
|
|
20008
|
-
const detail = String(error?.stderr ?? "").split("\n").map((line) => line.trim()).find((line) => /conflict|could not|error:/i.test(line));
|
|
20009
|
-
return [
|
|
20010
|
-
`could not rebase onto origin/${featureBranch}: another agent pushed to this corner's branch`,
|
|
20011
|
-
detail ? ` (${detail})` : "",
|
|
20012
|
-
". Nothing was pushed; the branch needs a person."
|
|
20013
|
-
].join("").slice(0, 200);
|
|
20014
|
-
}
|
|
20015
20006
|
|
|
20016
20007
|
// apps/body/dist/room-session.js
|
|
20017
20008
|
import { resolve as resolve15 } from "node:path";
|
|
@@ -20845,12 +20836,11 @@ function roomMentionDirectory(roster, selfId) {
|
|
|
20845
20836
|
if (member.identityId === selfId)
|
|
20846
20837
|
continue;
|
|
20847
20838
|
const handle = member.handle?.trim().replace(/^@/, "");
|
|
20848
|
-
|
|
20849
|
-
const alias = handle || name;
|
|
20850
|
-
if (!alias)
|
|
20839
|
+
if (!handle)
|
|
20851
20840
|
continue;
|
|
20841
|
+
const name = member.name?.trim() ?? "";
|
|
20852
20842
|
const kind = member.kind === "agent" ? "agent" : "person";
|
|
20853
|
-
rows.push(`- @${
|
|
20843
|
+
rows.push(`- @${handle}${name && name !== handle ? ` \u2014 ${name}` : ""} (${kind})`);
|
|
20854
20844
|
}
|
|
20855
20845
|
if (!rows.length)
|
|
20856
20846
|
return "";
|
|
@@ -20861,47 +20851,6 @@ function roomMentionDirectory(roster, selfId) {
|
|
|
20861
20851
|
"Write a tag exactly as spelled here. An @name spelled any other way is plain text: it reaches nobody, and nobody is told it was meant for them. Never invent a handle, shorten one, or copy an @name out of the conversation \u2014 old messages carry spellings that no longer exist."
|
|
20862
20852
|
].join("\n");
|
|
20863
20853
|
}
|
|
20864
|
-
function escapeRegExp(value) {
|
|
20865
|
-
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
20866
|
-
}
|
|
20867
|
-
function agentReplyMentionIds(text2, roster, authorId) {
|
|
20868
|
-
const aliases = /* @__PURE__ */ new Map();
|
|
20869
|
-
for (const member of roster.members) {
|
|
20870
|
-
if (member.identityId === authorId)
|
|
20871
|
-
continue;
|
|
20872
|
-
const rawAliases = member.kind === "agent" ? [member.handle] : [member.name, member.handle, member.soul?.name];
|
|
20873
|
-
for (const raw of rawAliases) {
|
|
20874
|
-
const display = raw?.trim().replace(/^@/, "");
|
|
20875
|
-
if (!display)
|
|
20876
|
-
continue;
|
|
20877
|
-
const key = display.toLocaleLowerCase();
|
|
20878
|
-
const entry = aliases.get(key) ?? { display, ids: /* @__PURE__ */ new Set() };
|
|
20879
|
-
entry.ids.add(member.identityId);
|
|
20880
|
-
aliases.set(key, entry);
|
|
20881
|
-
}
|
|
20882
|
-
}
|
|
20883
|
-
for (const member of roster.members) {
|
|
20884
|
-
if (member.identityId === authorId || member.kind === "agent" || !member.handle)
|
|
20885
|
-
continue;
|
|
20886
|
-
const handle = member.handle.trim().replace(/^@/, "").toLocaleLowerCase();
|
|
20887
|
-
const canonical = aliases.get(handle);
|
|
20888
|
-
const legacy = `a_${handle}`;
|
|
20889
|
-
if (canonical && !aliases.has(legacy))
|
|
20890
|
-
aliases.set(legacy, { ...canonical, display: legacy });
|
|
20891
|
-
}
|
|
20892
|
-
const mentioned = [];
|
|
20893
|
-
for (const { display, ids } of [...aliases.values()].sort((left, right) => right.display.length - left.display.length)) {
|
|
20894
|
-
if (ids.size !== 1)
|
|
20895
|
-
continue;
|
|
20896
|
-
const pattern = new RegExp(`(^|[\\s([{])@${escapeRegExp(display)}(?=$|[\\s.,!?;:)\\]}])`, "iu");
|
|
20897
|
-
if (!pattern.test(text2))
|
|
20898
|
-
continue;
|
|
20899
|
-
const identityId = [...ids][0];
|
|
20900
|
-
if (!mentioned.includes(identityId))
|
|
20901
|
-
mentioned.push(identityId);
|
|
20902
|
-
}
|
|
20903
|
-
return mentioned;
|
|
20904
|
-
}
|
|
20905
20854
|
var MonolithRoomTurnLoop = class {
|
|
20906
20855
|
options;
|
|
20907
20856
|
commandContext;
|
|
@@ -21487,10 +21436,8 @@ var MonolithRoomTurnLoop = class {
|
|
|
21487
21436
|
if (openCornerCall && !isFailedToolCall(openCornerCall)) {
|
|
21488
21437
|
reply = stripCornerOpenEcho(reply);
|
|
21489
21438
|
}
|
|
21490
|
-
const mentionIds = reply ? agentReplyMentionIds(reply, roster, this.agent.publicKey) : [];
|
|
21491
21439
|
await trace.measure("publish", () => stream.settle(reply, reply ? {
|
|
21492
|
-
triggerMessageId: item.id
|
|
21493
|
-
mentionIds
|
|
21440
|
+
triggerMessageId: item.id
|
|
21494
21441
|
} : {}));
|
|
21495
21442
|
}, { priority: "interactive", roomKey: this.options.roomId });
|
|
21496
21443
|
}, (error) => console.error(`[thin-core] monolith Room ${this.options.roomId} receipt heartbeat failed:`, error));
|
|
@@ -21571,6 +21518,9 @@ var execFileAsync3 = promisify3(execFile4);
|
|
|
21571
21518
|
var TOOL_ARGUMENT_MAX_BYTES = 1200;
|
|
21572
21519
|
var TOOL_OUTPUT_MAX_BYTES = 3200;
|
|
21573
21520
|
var TOOL_PATH_LIMIT = 12;
|
|
21521
|
+
function cornerMergeInstruction(yoloMode) {
|
|
21522
|
+
return yoloMode ? "Yolo is on: when the gate passes, merge this pull request with gh." : "Yolo is off: never merge; wait for explicit human approval in the app.";
|
|
21523
|
+
}
|
|
21574
21524
|
function oneLine(value) {
|
|
21575
21525
|
return value.replace(/\s+/g, " ").trim();
|
|
21576
21526
|
}
|
|
@@ -21831,7 +21781,8 @@ var MonolithCornerTurnLoop = class {
|
|
|
21831
21781
|
model: configuration.model ?? this.options.config.modelSelection?.model,
|
|
21832
21782
|
effort: configuration.effort ?? this.options.config.modelSelection?.effort,
|
|
21833
21783
|
soul: configuration.soul ?? self?.soul,
|
|
21834
|
-
agentName: self?.name ?? this.agent.name
|
|
21784
|
+
agentName: self?.name ?? this.agent.name,
|
|
21785
|
+
yoloMode: configuration.yoloMode
|
|
21835
21786
|
});
|
|
21836
21787
|
}
|
|
21837
21788
|
async activate(trace) {
|
|
@@ -21850,7 +21801,8 @@ var MonolithCornerTurnLoop = class {
|
|
|
21850
21801
|
model: configuration.model ?? this.options.config.modelSelection?.model,
|
|
21851
21802
|
effort: configuration.effort ?? this.options.config.modelSelection?.effort,
|
|
21852
21803
|
soul: configuration.soul ?? self?.soul,
|
|
21853
|
-
agentName: self?.name ?? this.agent.name
|
|
21804
|
+
agentName: self?.name ?? this.agent.name,
|
|
21805
|
+
yoloMode: configuration.yoloMode
|
|
21854
21806
|
});
|
|
21855
21807
|
await mkdir11(this.options.worktreePath, { recursive: true });
|
|
21856
21808
|
const selection = configuration.model || configuration.effort ? { model: configuration.model, effort: configuration.effort } : this.options.config.modelSelection;
|
|
@@ -21981,16 +21933,11 @@ var MonolithCornerTurnLoop = class {
|
|
|
21981
21933
|
personaInstructions,
|
|
21982
21934
|
...repository ? [
|
|
21983
21935
|
`You are in an isolated git worktree on ${repository.featureBranch}, targeting ${repository.targetBranch}.`,
|
|
21984
|
-
|
|
21985
|
-
|
|
21986
|
-
|
|
21987
|
-
'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.',
|
|
21988
|
-
"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.",
|
|
21989
|
-
"If any human in this corner says hold or do not merge, do not merge until a later human explicitly resumes it.",
|
|
21936
|
+
`Commit and push only ${repository.featureBranch}; never force-push or write to ${repository.targetBranch}. Before pushing, rebase on origin/${repository.featureBranch}; resolve conflicts autonomously, realigning to that remote branch and redoing the objective if needed, then rerun affected tests. Open the pull request with gh.`,
|
|
21937
|
+
'Once the pull request exists, reply only with its full URL and end the turn; do not check or wait for CI. On a later checks turn, call pr_checks_status. Merge only when checks="passed", held=false, and approvalPending=false; only a later explicit human resume clears a hold.',
|
|
21938
|
+
cornerMergeInstruction(configuration.yoloMode),
|
|
21990
21939
|
"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.",
|
|
21991
|
-
|
|
21992
|
-
"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.",
|
|
21993
|
-
"Never push directly to the target branch. Never merge a different pull request."
|
|
21940
|
+
"Never restate server check or merge notes. On a checks turn, say nothing unless you merge or push a fix, then use one short line. When approval is pending, wait for the server close request. Never merge another pull request."
|
|
21994
21941
|
] : [
|
|
21995
21942
|
"This is a chat-only corner with no repository or GitHub workflow.",
|
|
21996
21943
|
"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.",
|
|
@@ -22276,10 +22223,8 @@ ${trigger}`,
|
|
|
22276
22223
|
console.warn(`[thin-core] corner ${cornerId} turn ${requestId}: ${explained.reason}`);
|
|
22277
22224
|
}
|
|
22278
22225
|
const durableReply = spoken(reply);
|
|
22279
|
-
const mentionIds = durableReply ? agentReplyMentionIds(durableReply, roster, this.agent.publicKey) : [];
|
|
22280
22226
|
await trace.measure("publish", () => stream.settle(durableReply, durableReply ? {
|
|
22281
|
-
...requestedById ? { triggerMessageId: requestId } : {}
|
|
22282
|
-
mentionIds
|
|
22227
|
+
...requestedById ? { triggerMessageId: requestId } : {}
|
|
22283
22228
|
} : {}));
|
|
22284
22229
|
}, { priority: "interactive", roomKey: cornerId });
|
|
22285
22230
|
}, (error) => console.error(`[thin-core] corner ${cornerId} receipt heartbeat failed:`, error));
|
|
@@ -22320,8 +22265,8 @@ ${trigger}`,
|
|
|
22320
22265
|
* The branch is the shared artifact: another member agent may have pushed to
|
|
22321
22266
|
* it since this helper last looked, and a first touch of a corner this
|
|
22322
22267
|
* helper did not open starts from whatever `room-runtime.ts` restored. A
|
|
22323
|
-
*
|
|
22324
|
-
*
|
|
22268
|
+
* conflicting local commits are realigned to that remote branch so the
|
|
22269
|
+
* fixed objective can be attempted again without clobbering a sibling push.
|
|
22325
22270
|
*/
|
|
22326
22271
|
async syncBranch() {
|
|
22327
22272
|
const repository = this.options.repository;
|
|
@@ -22781,6 +22726,7 @@ var ROOM_JOIN_CONCURRENCY = 4;
|
|
|
22781
22726
|
var DEFAULT_ROOM_WATCHDOG_STALE_MS = 9e4;
|
|
22782
22727
|
var DEFAULT_RECONCILE_HEARTBEAT_MS = 6e4;
|
|
22783
22728
|
var DEFAULT_DRAIN_DEADLINE_MS = 30 * 6e4;
|
|
22729
|
+
var CORNER_BRANCH_DELETE_ATTEMPTS = 3;
|
|
22784
22730
|
function shouldPostInitialCornerWorkingState(restore, isOpener = true) {
|
|
22785
22731
|
return isOpener && !restore.featureBranch && !restore.lifecycle?.branch && !restore.lifecycle?.pr;
|
|
22786
22732
|
}
|
|
@@ -22873,6 +22819,52 @@ async function mapWithConcurrency(values, limit, visit) {
|
|
|
22873
22819
|
}
|
|
22874
22820
|
}));
|
|
22875
22821
|
}
|
|
22822
|
+
async function removeCornerWorktreeAndBranches(worktree) {
|
|
22823
|
+
const localRef = `refs/heads/${worktree.branch}`;
|
|
22824
|
+
const remoteRef = `refs/heads/${worktree.branch}`;
|
|
22825
|
+
const worktreeExists = existsSync4(worktree.path);
|
|
22826
|
+
const localExists = await gitRefExists(worktree.gitCommonDir, localRef);
|
|
22827
|
+
if (worktreeExists) {
|
|
22828
|
+
const checkedOut = await execFileAsync4("git", [
|
|
22829
|
+
"-C",
|
|
22830
|
+
worktree.path,
|
|
22831
|
+
"symbolic-ref",
|
|
22832
|
+
"--quiet",
|
|
22833
|
+
"HEAD"
|
|
22834
|
+
]);
|
|
22835
|
+
if (checkedOut.stdout.trim() !== localRef) {
|
|
22836
|
+
throw new Error(`corner worktree branch mismatch: expected ${localRef}`);
|
|
22837
|
+
}
|
|
22838
|
+
await execFileAsync4("git", [
|
|
22839
|
+
`--git-dir=${worktree.gitCommonDir}`,
|
|
22840
|
+
"worktree",
|
|
22841
|
+
"remove",
|
|
22842
|
+
"--force",
|
|
22843
|
+
worktree.path
|
|
22844
|
+
]);
|
|
22845
|
+
} else if (!localExists) {
|
|
22846
|
+
await deleteExactRemoteBranch(worktree.gitCommonDir, remoteRef, worktree.token, {
|
|
22847
|
+
requireAbsent: true
|
|
22848
|
+
});
|
|
22849
|
+
return;
|
|
22850
|
+
} else {
|
|
22851
|
+
const head = await repositoryHeadRef(worktree.gitCommonDir);
|
|
22852
|
+
if (head === localRef) {
|
|
22853
|
+
throw new Error(`refusing to delete repository HEAD ${localRef}`);
|
|
22854
|
+
}
|
|
22855
|
+
}
|
|
22856
|
+
await deleteExactRemoteBranch(worktree.gitCommonDir, remoteRef, worktree.token);
|
|
22857
|
+
if (await gitRefExists(worktree.gitCommonDir, localRef)) {
|
|
22858
|
+
await execFileAsync4("git", [
|
|
22859
|
+
`--git-dir=${worktree.gitCommonDir}`,
|
|
22860
|
+
"branch",
|
|
22861
|
+
"--delete",
|
|
22862
|
+
"--force",
|
|
22863
|
+
"--",
|
|
22864
|
+
worktree.branch
|
|
22865
|
+
]);
|
|
22866
|
+
}
|
|
22867
|
+
}
|
|
22876
22868
|
var execFileAsync4 = promisify4(execFile5);
|
|
22877
22869
|
async function removeCornerScratchWorkspace(input) {
|
|
22878
22870
|
const expected = resolve19(input.roomRoot, "scratch");
|
|
@@ -22887,6 +22879,8 @@ var RoomRuntimeCoordinator = class {
|
|
|
22887
22879
|
options;
|
|
22888
22880
|
runtime;
|
|
22889
22881
|
running = /* @__PURE__ */ new Map();
|
|
22882
|
+
/** Close/reconcile leftovers retried until local and remote refs are gone. */
|
|
22883
|
+
pendingCornerReaps = /* @__PURE__ */ new Map();
|
|
22890
22884
|
startingCorners = /* @__PURE__ */ new Set();
|
|
22891
22885
|
/** Corners whose start failure has already been said out loud, once each. */
|
|
22892
22886
|
reportedCornerStartFailures = /* @__PURE__ */ new Set();
|
|
@@ -23029,6 +23023,7 @@ var RoomRuntimeCoordinator = class {
|
|
|
23029
23023
|
});
|
|
23030
23024
|
for (const channelId of desired)
|
|
23031
23025
|
this.roomRemovalConfirmations.delete(channelId);
|
|
23026
|
+
await this.retryPendingCornerReaps(desired);
|
|
23032
23027
|
for (const [channelId, running] of [...this.running]) {
|
|
23033
23028
|
if (desired.has(channelId))
|
|
23034
23029
|
continue;
|
|
@@ -23040,10 +23035,15 @@ var RoomRuntimeCoordinator = class {
|
|
|
23040
23035
|
}
|
|
23041
23036
|
running.controller.abort();
|
|
23042
23037
|
await running.promise.catch(() => void 0);
|
|
23043
|
-
|
|
23044
|
-
|
|
23045
|
-
|
|
23046
|
-
|
|
23038
|
+
try {
|
|
23039
|
+
if (running.worktree)
|
|
23040
|
+
await this.reapCornerWorktree(running.worktree);
|
|
23041
|
+
else if (running.scratch)
|
|
23042
|
+
await this.reapCornerScratch(running.scratch);
|
|
23043
|
+
} catch (error) {
|
|
23044
|
+
console.error(`[thin-core] corner ${channelId} cleanup failed; will retry:`, error);
|
|
23045
|
+
this.confirmationPending = true;
|
|
23046
|
+
}
|
|
23047
23047
|
}
|
|
23048
23048
|
await mapWithConcurrency(desiredTopRooms, ROOM_JOIN_CONCURRENCY, async (roomId) => {
|
|
23049
23049
|
if (!this.running.has(roomId))
|
|
@@ -23182,18 +23182,10 @@ var RoomRuntimeCoordinator = class {
|
|
|
23182
23182
|
return;
|
|
23183
23183
|
this.startingCorners.add(corner.cornerId);
|
|
23184
23184
|
try {
|
|
23185
|
-
const [restore, repository
|
|
23185
|
+
const [restore, repository] = await Promise.all([
|
|
23186
23186
|
this.options.daemonApi.execute("getCornerRestoreState", { cornerId: corner.cornerId }),
|
|
23187
23187
|
this.options.daemonApi.execute("getRoomRepositoryState", {
|
|
23188
23188
|
roomId: corner.parentRoomId
|
|
23189
|
-
}),
|
|
23190
|
-
// Startup recovers the objective from the corner's FIRST durable
|
|
23191
|
-
// message, so this is the one conversation read that wants the oldest
|
|
23192
|
-
// end of the Room. Every other read defaults to the newest page.
|
|
23193
|
-
this.options.daemonApi.execute("getRoomConversation", {
|
|
23194
|
-
roomId: corner.cornerId,
|
|
23195
|
-
limit: 200,
|
|
23196
|
-
window: "earliest"
|
|
23197
23189
|
})
|
|
23198
23190
|
]);
|
|
23199
23191
|
if (repository.resolution === "unverified") {
|
|
@@ -23202,9 +23194,9 @@ var RoomRuntimeCoordinator = class {
|
|
|
23202
23194
|
if (repository.resolution === "repository" && (!repository.remote || !repository.key)) {
|
|
23203
23195
|
throw new Error("corner parent Room has an incomplete repository binding");
|
|
23204
23196
|
}
|
|
23205
|
-
const objective =
|
|
23197
|
+
const objective = restore.objective.trim();
|
|
23206
23198
|
if (!objective)
|
|
23207
|
-
throw new Error("corner has no
|
|
23199
|
+
throw new Error("corner has no authoritative objective fact");
|
|
23208
23200
|
const repositoryBacked = repository.resolution === "repository";
|
|
23209
23201
|
const targetBranch = repositoryBacked ? repository.targetBranch || "main" : void 0;
|
|
23210
23202
|
const featureBranch = repositoryBacked ? restore.featureBranch ?? `feature/corner-${corner.cornerId.replaceAll("-", "").slice(0, 12)}` : void 0;
|
|
@@ -23260,7 +23252,9 @@ var RoomRuntimeCoordinator = class {
|
|
|
23260
23252
|
onCloseRequested: () => worktree ? this.reapCornerWorktree({
|
|
23261
23253
|
...worktree,
|
|
23262
23254
|
cornerId: corner.cornerId,
|
|
23263
|
-
branch: featureBranch
|
|
23255
|
+
branch: featureBranch,
|
|
23256
|
+
parentRoomId: corner.parentRoomId,
|
|
23257
|
+
token: ""
|
|
23264
23258
|
}) : this.reapCornerScratch({ path: workspacePath, cornerId: corner.cornerId })
|
|
23265
23259
|
});
|
|
23266
23260
|
const promise = loop.run().catch((error) => {
|
|
@@ -23283,7 +23277,9 @@ var RoomRuntimeCoordinator = class {
|
|
|
23283
23277
|
worktree: {
|
|
23284
23278
|
...worktree,
|
|
23285
23279
|
cornerId: corner.cornerId,
|
|
23286
|
-
branch: featureBranch
|
|
23280
|
+
branch: featureBranch,
|
|
23281
|
+
parentRoomId: corner.parentRoomId,
|
|
23282
|
+
token: ""
|
|
23287
23283
|
}
|
|
23288
23284
|
} : {},
|
|
23289
23285
|
...!worktree ? { scratch: { path: workspacePath, cornerId: corner.cornerId } } : {}
|
|
@@ -23339,22 +23335,40 @@ var RoomRuntimeCoordinator = class {
|
|
|
23339
23335
|
committer: { name: this.agent.name, publicKey: this.agent.publicKey }
|
|
23340
23336
|
});
|
|
23341
23337
|
}
|
|
23338
|
+
async retryPendingCornerReaps(desired) {
|
|
23339
|
+
for (const [cornerId, worktree] of [...this.pendingCornerReaps]) {
|
|
23340
|
+
if (desired.has(cornerId)) {
|
|
23341
|
+
this.pendingCornerReaps.delete(cornerId);
|
|
23342
|
+
continue;
|
|
23343
|
+
}
|
|
23344
|
+
try {
|
|
23345
|
+
await this.reapCornerWorktree(worktree);
|
|
23346
|
+
} catch (error) {
|
|
23347
|
+
console.error(`[thin-core] corner ${cornerId} branch cleanup retry failed:`, error);
|
|
23348
|
+
this.confirmationPending = true;
|
|
23349
|
+
}
|
|
23350
|
+
}
|
|
23351
|
+
}
|
|
23342
23352
|
async reapCornerWorktree(worktree) {
|
|
23343
|
-
|
|
23344
|
-
|
|
23345
|
-
|
|
23346
|
-
|
|
23347
|
-
|
|
23348
|
-
|
|
23349
|
-
|
|
23350
|
-
|
|
23353
|
+
try {
|
|
23354
|
+
const parentRoomId = worktree.parentRoomId;
|
|
23355
|
+
if (!parentRoomId) {
|
|
23356
|
+
throw new Error(`corner ${worktree.cornerId} has no parent Room for GitHub token`);
|
|
23357
|
+
}
|
|
23358
|
+
const token = (await this.options.daemonApi.execute("getRoomGitHubToken", { roomId: parentRoomId })).token;
|
|
23359
|
+
await removeCornerWorktreeAndBranches({ ...worktree, token });
|
|
23360
|
+
await this.options.daemonApi.execute("postCornerRemoteState", {
|
|
23361
|
+
cornerId: worktree.cornerId,
|
|
23362
|
+
branch: worktree.branch,
|
|
23363
|
+
state: "gone",
|
|
23364
|
+
checks: "unknown"
|
|
23365
|
+
});
|
|
23366
|
+
this.pendingCornerReaps.delete(worktree.cornerId);
|
|
23367
|
+
} catch (error) {
|
|
23368
|
+
this.pendingCornerReaps.set(worktree.cornerId, { ...worktree, token: "" });
|
|
23369
|
+
this.confirmationPending = true;
|
|
23370
|
+
throw error;
|
|
23351
23371
|
}
|
|
23352
|
-
await this.options.daemonApi.execute("postCornerRemoteState", {
|
|
23353
|
-
cornerId: worktree.cornerId,
|
|
23354
|
-
branch: worktree.branch,
|
|
23355
|
-
state: "gone",
|
|
23356
|
-
checks: "unknown"
|
|
23357
|
-
});
|
|
23358
23372
|
}
|
|
23359
23373
|
async reapCornerScratch(scratch) {
|
|
23360
23374
|
await removeCornerScratchWorkspace({
|
|
@@ -23437,6 +23451,68 @@ function githubGitEnv(token) {
|
|
|
23437
23451
|
GIT_TERMINAL_PROMPT: "0"
|
|
23438
23452
|
};
|
|
23439
23453
|
}
|
|
23454
|
+
async function gitRefExists(gitCommonDir, ref) {
|
|
23455
|
+
try {
|
|
23456
|
+
await execFileAsync4("git", [`--git-dir=${gitCommonDir}`, "show-ref", "--verify", "--quiet", ref]);
|
|
23457
|
+
return true;
|
|
23458
|
+
} catch (error) {
|
|
23459
|
+
if (error.code === 1)
|
|
23460
|
+
return false;
|
|
23461
|
+
throw error;
|
|
23462
|
+
}
|
|
23463
|
+
}
|
|
23464
|
+
async function repositoryHeadRef(gitCommonDir) {
|
|
23465
|
+
try {
|
|
23466
|
+
const head = await execFileAsync4("git", [
|
|
23467
|
+
`--git-dir=${gitCommonDir}`,
|
|
23468
|
+
"symbolic-ref",
|
|
23469
|
+
"--quiet",
|
|
23470
|
+
"HEAD"
|
|
23471
|
+
]);
|
|
23472
|
+
return head.stdout.trim() || void 0;
|
|
23473
|
+
} catch {
|
|
23474
|
+
return void 0;
|
|
23475
|
+
}
|
|
23476
|
+
}
|
|
23477
|
+
function gitStderr(error) {
|
|
23478
|
+
return typeof error === "object" && error && "stderr" in error ? String(error.stderr ?? "") : "";
|
|
23479
|
+
}
|
|
23480
|
+
function isRemoteRefMissing(error) {
|
|
23481
|
+
return /remote ref does not exist|remote reference does not exist/i.test(`${error instanceof Error ? error.message : String(error)}
|
|
23482
|
+
${gitStderr(error)}`);
|
|
23483
|
+
}
|
|
23484
|
+
async function deleteExactRemoteBranch(gitCommonDir, remoteRef, token, options = {}) {
|
|
23485
|
+
const authEnv = githubGitEnv(token);
|
|
23486
|
+
const listRemote = () => execFileAsync4("git", [`--git-dir=${gitCommonDir}`, "ls-remote", "--heads", "origin", remoteRef], {
|
|
23487
|
+
env: authEnv,
|
|
23488
|
+
maxBuffer: 4 * 1024 * 1024
|
|
23489
|
+
});
|
|
23490
|
+
if (options.requireAbsent) {
|
|
23491
|
+
const remote = await listRemote();
|
|
23492
|
+
if (remote.stdout.trim()) {
|
|
23493
|
+
throw new Error(`cannot delete ${remoteRef} without the local ref that proves this corner owns it`);
|
|
23494
|
+
}
|
|
23495
|
+
return;
|
|
23496
|
+
}
|
|
23497
|
+
let remoteError;
|
|
23498
|
+
for (let attempt = 1; attempt <= CORNER_BRANCH_DELETE_ATTEMPTS; attempt += 1) {
|
|
23499
|
+
try {
|
|
23500
|
+
const remote = await listRemote();
|
|
23501
|
+
if (remote.stdout.trim()) {
|
|
23502
|
+
await execFileAsync4("git", [`--git-dir=${gitCommonDir}`, "push", "origin", `:${remoteRef}`], {
|
|
23503
|
+
env: authEnv,
|
|
23504
|
+
maxBuffer: 4 * 1024 * 1024
|
|
23505
|
+
});
|
|
23506
|
+
}
|
|
23507
|
+
return;
|
|
23508
|
+
} catch (error) {
|
|
23509
|
+
if (isRemoteRefMissing(error))
|
|
23510
|
+
return;
|
|
23511
|
+
remoteError = error;
|
|
23512
|
+
}
|
|
23513
|
+
}
|
|
23514
|
+
throw remoteError;
|
|
23515
|
+
}
|
|
23440
23516
|
|
|
23441
23517
|
// apps/body/dist/thin-core.js
|
|
23442
23518
|
async function waitForNextTick(ms, signal) {
|