@remixhq/claude-plugin 0.1.17 → 0.1.18

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.
@@ -37,8 +37,8 @@ var require_windows = __commonJS({
37
37
  "use strict";
38
38
  module2.exports = isexe;
39
39
  isexe.sync = sync;
40
- var fs9 = require("fs");
41
- function checkPathExt(path13, options) {
40
+ var fs8 = require("fs");
41
+ function checkPathExt(path12, options) {
42
42
  var pathext = options.pathExt !== void 0 ? options.pathExt : process.env.PATHEXT;
43
43
  if (!pathext) {
44
44
  return true;
@@ -49,25 +49,25 @@ var require_windows = __commonJS({
49
49
  }
50
50
  for (var i2 = 0; i2 < pathext.length; i2++) {
51
51
  var p = pathext[i2].toLowerCase();
52
- if (p && path13.substr(-p.length).toLowerCase() === p) {
52
+ if (p && path12.substr(-p.length).toLowerCase() === p) {
53
53
  return true;
54
54
  }
55
55
  }
56
56
  return false;
57
57
  }
58
- function checkStat(stat, path13, options) {
58
+ function checkStat(stat, path12, options) {
59
59
  if (!stat.isSymbolicLink() && !stat.isFile()) {
60
60
  return false;
61
61
  }
62
- return checkPathExt(path13, options);
62
+ return checkPathExt(path12, options);
63
63
  }
64
- function isexe(path13, options, cb) {
65
- fs9.stat(path13, function(er, stat) {
66
- cb(er, er ? false : checkStat(stat, path13, options));
64
+ function isexe(path12, options, cb) {
65
+ fs8.stat(path12, function(er, stat) {
66
+ cb(er, er ? false : checkStat(stat, path12, options));
67
67
  });
68
68
  }
69
- function sync(path13, options) {
70
- return checkStat(fs9.statSync(path13), path13, options);
69
+ function sync(path12, options) {
70
+ return checkStat(fs8.statSync(path12), path12, options);
71
71
  }
72
72
  }
73
73
  });
@@ -78,14 +78,14 @@ var require_mode = __commonJS({
78
78
  "use strict";
79
79
  module2.exports = isexe;
80
80
  isexe.sync = sync;
81
- var fs9 = require("fs");
82
- function isexe(path13, options, cb) {
83
- fs9.stat(path13, function(er, stat) {
81
+ var fs8 = require("fs");
82
+ function isexe(path12, options, cb) {
83
+ fs8.stat(path12, function(er, stat) {
84
84
  cb(er, er ? false : checkStat(stat, options));
85
85
  });
86
86
  }
87
- function sync(path13, options) {
88
- return checkStat(fs9.statSync(path13), options);
87
+ function sync(path12, options) {
88
+ return checkStat(fs8.statSync(path12), options);
89
89
  }
90
90
  function checkStat(stat, options) {
91
91
  return stat.isFile() && checkMode(stat, options);
@@ -110,7 +110,7 @@ var require_mode = __commonJS({
110
110
  var require_isexe = __commonJS({
111
111
  "node_modules/isexe/index.js"(exports2, module2) {
112
112
  "use strict";
113
- var fs9 = require("fs");
113
+ var fs8 = require("fs");
114
114
  var core;
115
115
  if (process.platform === "win32" || global.TESTING_WINDOWS) {
116
116
  core = require_windows();
@@ -119,7 +119,7 @@ var require_isexe = __commonJS({
119
119
  }
120
120
  module2.exports = isexe;
121
121
  isexe.sync = sync;
122
- function isexe(path13, options, cb) {
122
+ function isexe(path12, options, cb) {
123
123
  if (typeof options === "function") {
124
124
  cb = options;
125
125
  options = {};
@@ -129,7 +129,7 @@ var require_isexe = __commonJS({
129
129
  throw new TypeError("callback not provided");
130
130
  }
131
131
  return new Promise(function(resolve, reject) {
132
- isexe(path13, options || {}, function(er, is) {
132
+ isexe(path12, options || {}, function(er, is) {
133
133
  if (er) {
134
134
  reject(er);
135
135
  } else {
@@ -138,7 +138,7 @@ var require_isexe = __commonJS({
138
138
  });
139
139
  });
140
140
  }
141
- core(path13, options || {}, function(er, is) {
141
+ core(path12, options || {}, function(er, is) {
142
142
  if (er) {
143
143
  if (er.code === "EACCES" || options && options.ignoreErrors) {
144
144
  er = null;
@@ -148,9 +148,9 @@ var require_isexe = __commonJS({
148
148
  cb(er, is);
149
149
  });
150
150
  }
151
- function sync(path13, options) {
151
+ function sync(path12, options) {
152
152
  try {
153
- return core.sync(path13, options || {});
153
+ return core.sync(path12, options || {});
154
154
  } catch (er) {
155
155
  if (options && options.ignoreErrors || er.code === "EACCES") {
156
156
  return false;
@@ -167,7 +167,7 @@ var require_which = __commonJS({
167
167
  "node_modules/which/which.js"(exports2, module2) {
168
168
  "use strict";
169
169
  var isWindows = process.platform === "win32" || process.env.OSTYPE === "cygwin" || process.env.OSTYPE === "msys";
170
- var path13 = require("path");
170
+ var path12 = require("path");
171
171
  var COLON = isWindows ? ";" : ":";
172
172
  var isexe = require_isexe();
173
173
  var getNotFoundError = (cmd) => Object.assign(new Error(`not found: ${cmd}`), { code: "ENOENT" });
@@ -205,7 +205,7 @@ var require_which = __commonJS({
205
205
  return opt.all && found.length ? resolve(found) : reject(getNotFoundError(cmd));
206
206
  const ppRaw = pathEnv[i2];
207
207
  const pathPart = /^".*"$/.test(ppRaw) ? ppRaw.slice(1, -1) : ppRaw;
208
- const pCmd = path13.join(pathPart, cmd);
208
+ const pCmd = path12.join(pathPart, cmd);
209
209
  const p = !pathPart && /^\.[\\\/]/.test(cmd) ? cmd.slice(0, 2) + pCmd : pCmd;
210
210
  resolve(subStep(p, i2, 0));
211
211
  });
@@ -232,7 +232,7 @@ var require_which = __commonJS({
232
232
  for (let i2 = 0; i2 < pathEnv.length; i2++) {
233
233
  const ppRaw = pathEnv[i2];
234
234
  const pathPart = /^".*"$/.test(ppRaw) ? ppRaw.slice(1, -1) : ppRaw;
235
- const pCmd = path13.join(pathPart, cmd);
235
+ const pCmd = path12.join(pathPart, cmd);
236
236
  const p = !pathPart && /^\.[\\\/]/.test(cmd) ? cmd.slice(0, 2) + pCmd : pCmd;
237
237
  for (let j = 0; j < pathExt.length; j++) {
238
238
  const cur = p + pathExt[j];
@@ -280,7 +280,7 @@ var require_path_key = __commonJS({
280
280
  var require_resolveCommand = __commonJS({
281
281
  "node_modules/cross-spawn/lib/util/resolveCommand.js"(exports2, module2) {
282
282
  "use strict";
283
- var path13 = require("path");
283
+ var path12 = require("path");
284
284
  var which = require_which();
285
285
  var getPathKey = require_path_key();
286
286
  function resolveCommandAttempt(parsed, withoutPathExt) {
@@ -298,7 +298,7 @@ var require_resolveCommand = __commonJS({
298
298
  try {
299
299
  resolved = which.sync(parsed.command, {
300
300
  path: env[getPathKey({ env })],
301
- pathExt: withoutPathExt ? path13.delimiter : void 0
301
+ pathExt: withoutPathExt ? path12.delimiter : void 0
302
302
  });
303
303
  } catch (e) {
304
304
  } finally {
@@ -307,7 +307,7 @@ var require_resolveCommand = __commonJS({
307
307
  }
308
308
  }
309
309
  if (resolved) {
310
- resolved = path13.resolve(hasCustomCwd ? parsed.options.cwd : "", resolved);
310
+ resolved = path12.resolve(hasCustomCwd ? parsed.options.cwd : "", resolved);
311
311
  }
312
312
  return resolved;
313
313
  }
@@ -361,8 +361,8 @@ var require_shebang_command = __commonJS({
361
361
  if (!match) {
362
362
  return null;
363
363
  }
364
- const [path13, argument] = match[0].replace(/#! ?/, "").split(" ");
365
- const binary = path13.split("/").pop();
364
+ const [path12, argument] = match[0].replace(/#! ?/, "").split(" ");
365
+ const binary = path12.split("/").pop();
366
366
  if (binary === "env") {
367
367
  return argument;
368
368
  }
@@ -375,16 +375,16 @@ var require_shebang_command = __commonJS({
375
375
  var require_readShebang = __commonJS({
376
376
  "node_modules/cross-spawn/lib/util/readShebang.js"(exports2, module2) {
377
377
  "use strict";
378
- var fs9 = require("fs");
378
+ var fs8 = require("fs");
379
379
  var shebangCommand = require_shebang_command();
380
380
  function readShebang(command) {
381
381
  const size = 150;
382
382
  const buffer = Buffer.alloc(size);
383
383
  let fd;
384
384
  try {
385
- fd = fs9.openSync(command, "r");
386
- fs9.readSync(fd, buffer, 0, size, 0);
387
- fs9.closeSync(fd);
385
+ fd = fs8.openSync(command, "r");
386
+ fs8.readSync(fd, buffer, 0, size, 0);
387
+ fs8.closeSync(fd);
388
388
  } catch (e) {
389
389
  }
390
390
  return shebangCommand(buffer.toString());
@@ -397,7 +397,7 @@ var require_readShebang = __commonJS({
397
397
  var require_parse = __commonJS({
398
398
  "node_modules/cross-spawn/lib/parse.js"(exports2, module2) {
399
399
  "use strict";
400
- var path13 = require("path");
400
+ var path12 = require("path");
401
401
  var resolveCommand = require_resolveCommand();
402
402
  var escape = require_escape();
403
403
  var readShebang = require_readShebang();
@@ -422,7 +422,7 @@ var require_parse = __commonJS({
422
422
  const needsShell = !isExecutableRegExp.test(commandFile);
423
423
  if (parsed.options.forceShell || needsShell) {
424
424
  const needsDoubleEscapeMetaChars = isCmdShimRegExp.test(commandFile);
425
- parsed.command = path13.normalize(parsed.command);
425
+ parsed.command = path12.normalize(parsed.command);
426
426
  parsed.command = escape.command(parsed.command);
427
427
  parsed.args = parsed.args.map((arg) => escape.argument(arg, needsDoubleEscapeMetaChars));
428
428
  const shellCommand = [parsed.command].concat(parsed.args).join(" ");
@@ -512,7 +512,7 @@ var require_cross_spawn = __commonJS({
512
512
  var cp = require("child_process");
513
513
  var parse = require_parse();
514
514
  var enoent = require_enoent();
515
- function spawn2(command, args, options) {
515
+ function spawn3(command, args, options) {
516
516
  const parsed = parse(command, args, options);
517
517
  const spawned = cp.spawn(parsed.command, parsed.args, parsed.options);
518
518
  enoent.hookChildProcess(spawned, parsed);
@@ -524,8 +524,8 @@ var require_cross_spawn = __commonJS({
524
524
  result.error = result.error || enoent.verifyENOENTSync(result.status, parsed);
525
525
  return result;
526
526
  }
527
- module2.exports = spawn2;
528
- module2.exports.spawn = spawn2;
527
+ module2.exports = spawn3;
528
+ module2.exports.spawn = spawn3;
529
529
  module2.exports.sync = spawnSync2;
530
530
  module2.exports._parse = parse;
531
531
  module2.exports._enoent = enoent;
@@ -538,15 +538,7 @@ __export(hook_stop_collab_exports, {
538
538
  runHookStopCollab: () => runHookStopCollab
539
539
  });
540
540
  module.exports = __toCommonJS(hook_stop_collab_exports);
541
-
542
- // node_modules/@remixhq/core/dist/chunk-GC2MOT3U.js
543
- var REMIX_ERROR_CODES = {
544
- REPO_LOCK_HELD: "REPO_LOCK_HELD",
545
- REPO_LOCK_TIMEOUT: "REPO_LOCK_TIMEOUT",
546
- REPO_LOCK_STALE_RECOVERED: "REPO_LOCK_STALE_RECOVERED",
547
- REPO_STATE_CHANGED_DURING_OPERATION: "REPO_STATE_CHANGED_DURING_OPERATION",
548
- PREFERRED_BRANCH_MISMATCH: "PREFERRED_BRANCH_MISMATCH"
549
- };
541
+ var import_node_child_process6 = require("child_process");
550
542
 
551
543
  // node_modules/@remixhq/core/dist/chunk-YZ34ICNN.js
552
544
  var RemixError = class extends Error {
@@ -562,12 +554,6 @@ var RemixError = class extends Error {
562
554
  }
563
555
  };
564
556
 
565
- // node_modules/@remixhq/core/dist/chunk-RREREIGW.js
566
- var import_promises12 = __toESM(require("fs/promises"), 1);
567
- var import_crypto = require("crypto");
568
- var import_os = __toESM(require("os"), 1);
569
- var import_path = __toESM(require("path"), 1);
570
-
571
557
  // node_modules/is-plain-obj/index.js
572
558
  function isPlainObject(value) {
573
559
  if (typeof value !== "object" || value === null) {
@@ -4949,13 +4935,13 @@ var logOutputSync = ({ serializedResult, fdNumber, state, verboseInfo, encoding,
4949
4935
  }
4950
4936
  };
4951
4937
  var writeToFiles = (serializedResult, stdioItems, outputFiles) => {
4952
- for (const { path: path13, append } of stdioItems.filter(({ type }) => FILE_TYPES.has(type))) {
4953
- const pathString = typeof path13 === "string" ? path13 : path13.toString();
4938
+ for (const { path: path12, append } of stdioItems.filter(({ type }) => FILE_TYPES.has(type))) {
4939
+ const pathString = typeof path12 === "string" ? path12 : path12.toString();
4954
4940
  if (append || outputFiles.has(pathString)) {
4955
- (0, import_node_fs4.appendFileSync)(path13, serializedResult);
4941
+ (0, import_node_fs4.appendFileSync)(path12, serializedResult);
4956
4942
  } else {
4957
4943
  outputFiles.add(pathString);
4958
- (0, import_node_fs4.writeFileSync)(path13, serializedResult);
4944
+ (0, import_node_fs4.writeFileSync)(path12, serializedResult);
4959
4945
  }
4960
4946
  }
4961
4947
  };
@@ -7343,131 +7329,11 @@ var {
7343
7329
  getCancelSignal: getCancelSignal2
7344
7330
  } = getIpcExport();
7345
7331
 
7346
- // node_modules/@remixhq/core/dist/chunk-RREREIGW.js
7332
+ // node_modules/@remixhq/core/dist/chunk-WT6VRLXU.js
7347
7333
  async function runGit(args, cwd) {
7348
7334
  const res = await execa("git", args, { cwd, stderr: "ignore" });
7349
7335
  return String(res.stdout || "").trim();
7350
7336
  }
7351
- async function runGitWithEnv(args, cwd, env) {
7352
- const res = await execa("git", args, { cwd, env, stderr: "ignore" });
7353
- return String(res.stdout || "").trim();
7354
- }
7355
- async function runGitRawWithEnv(args, cwd, env) {
7356
- const res = await execa("git", args, { cwd, env, stderr: "ignore", stripFinalNewline: false });
7357
- return String(res.stdout || "");
7358
- }
7359
- async function runGitDetailed(args, cwd) {
7360
- const res = await execa("git", args, { cwd, reject: false });
7361
- return {
7362
- exitCode: res.exitCode ?? 1,
7363
- stdout: String(res.stdout || ""),
7364
- stderr: String(res.stderr || "")
7365
- };
7366
- }
7367
- function normalizeRepoRelativePath(value) {
7368
- const normalized = import_path.default.posix.normalize(value.replace(/\\/g, "/").trim());
7369
- if (!normalized || normalized === "." || normalized === ".." || normalized.startsWith("../") || import_path.default.posix.isAbsolute(normalized)) {
7370
- throw new RemixError("Git returned an invalid repository-relative path.", {
7371
- exitCode: 1,
7372
- hint: `Path: ${value}`
7373
- });
7374
- }
7375
- return normalized;
7376
- }
7377
- function resolveRepoRelativePath(repoRoot, relativePath) {
7378
- return import_path.default.resolve(repoRoot, ...relativePath.split("/"));
7379
- }
7380
- function isWithinRepoRoot(repoRoot, targetPath) {
7381
- const relative = import_path.default.relative(repoRoot, targetPath);
7382
- return relative === "" || !relative.startsWith("..") && !import_path.default.isAbsolute(relative);
7383
- }
7384
- function sha256Hex(value) {
7385
- return (0, import_crypto.createHash)("sha256").update(value).digest("hex");
7386
- }
7387
- function resolveGitReportedPath(cwd, reportedPath) {
7388
- const value = reportedPath.trim();
7389
- if (!value) {
7390
- throw new RemixError("Git returned an empty internal path.", { exitCode: 1 });
7391
- }
7392
- return import_path.default.isAbsolute(value) ? value : import_path.default.resolve(cwd, value);
7393
- }
7394
- function parseGitNameStatusZ(stdout) {
7395
- const tokens = stdout.split("\0");
7396
- const entries = [];
7397
- for (let i2 = 0; i2 < tokens.length; i2 += 1) {
7398
- const rawToken = tokens[i2];
7399
- if (!rawToken) continue;
7400
- let status = rawToken;
7401
- let inlinePath = null;
7402
- const tabIdx = rawToken.indexOf(" ");
7403
- if (tabIdx >= 0) {
7404
- status = rawToken.slice(0, tabIdx);
7405
- inlinePath = rawToken.slice(tabIdx + 1) || null;
7406
- }
7407
- const kind = status[0]?.toUpperCase() ?? "";
7408
- if (kind === "R" || kind === "C") {
7409
- const oldPath = inlinePath ?? tokens[++i2] ?? "";
7410
- const newPath = tokens[++i2] ?? "";
7411
- if (!newPath) continue;
7412
- entries.push({
7413
- status,
7414
- path: normalizeRepoRelativePath(newPath),
7415
- oldPath: oldPath ? normalizeRepoRelativePath(oldPath) : null
7416
- });
7417
- continue;
7418
- }
7419
- const nextPath = inlinePath ?? tokens[++i2] ?? "";
7420
- if (!nextPath) continue;
7421
- entries.push({
7422
- status,
7423
- path: normalizeRepoRelativePath(nextPath),
7424
- oldPath: null
7425
- });
7426
- }
7427
- return entries;
7428
- }
7429
- function buildStagePlan(entries) {
7430
- const updatePaths = /* @__PURE__ */ new Set();
7431
- const addPaths = /* @__PURE__ */ new Set();
7432
- for (const entry of entries) {
7433
- const kind = entry.status[0]?.toUpperCase() ?? "";
7434
- if (kind === "A" || kind === "C") {
7435
- addPaths.add(entry.path);
7436
- continue;
7437
- }
7438
- if (kind === "R") {
7439
- if (entry.oldPath) updatePaths.add(entry.oldPath);
7440
- addPaths.add(entry.path);
7441
- continue;
7442
- }
7443
- updatePaths.add(entry.path);
7444
- }
7445
- const expectedPaths = /* @__PURE__ */ new Set([...updatePaths, ...addPaths]);
7446
- return {
7447
- updatePaths: Array.from(updatePaths).sort(),
7448
- addPaths: Array.from(addPaths).sort(),
7449
- expectedPaths: Array.from(expectedPaths).sort()
7450
- };
7451
- }
7452
- function classifyGitApplyFailure(detail) {
7453
- const normalized = String(detail ?? "").toLowerCase();
7454
- if (normalized.includes("corrupt patch") || normalized.includes("patch with only garbage") || normalized.includes("unrecognized input") || normalized.includes("malformed patch") || normalized.includes("patch fragment without header")) {
7455
- return "malformed_patch";
7456
- }
7457
- if (normalized.includes("patch failed") || normalized.includes("does not apply") || normalized.includes("merge conflict") || normalized.includes("conflict")) {
7458
- return "apply_conflict";
7459
- }
7460
- return "unknown";
7461
- }
7462
- async function pruneEmptyParentDirectories(repoRoot, filePath) {
7463
- let current = import_path.default.dirname(filePath);
7464
- while (current !== repoRoot && isWithinRepoRoot(repoRoot, current)) {
7465
- const entries = await import_promises12.default.readdir(current).catch(() => null);
7466
- if (!entries || entries.length > 0) return;
7467
- await import_promises12.default.rmdir(current).catch(() => void 0);
7468
- current = import_path.default.dirname(current);
7469
- }
7470
- }
7471
7337
  async function findGitRoot(startDir) {
7472
7338
  try {
7473
7339
  const root = await runGit(["rev-parse", "--show-toplevel"], startDir);
@@ -7480,17 +7346,6 @@ async function findGitRoot(startDir) {
7480
7346
  });
7481
7347
  }
7482
7348
  }
7483
- async function getGitCommonDir(cwd) {
7484
- try {
7485
- const commonDir = await runGit(["rev-parse", "--git-common-dir"], cwd);
7486
- return resolveGitReportedPath(cwd, commonDir);
7487
- } catch {
7488
- throw new RemixError("Failed to resolve the git common directory.", {
7489
- exitCode: 1,
7490
- hint: "Ensure the current working directory is inside a valid git repository."
7491
- });
7492
- }
7493
- }
7494
7349
  async function getCurrentBranch(cwd) {
7495
7350
  try {
7496
7351
  const branch = await runGit(["branch", "--show-current"], cwd);
@@ -7499,121 +7354,6 @@ async function getCurrentBranch(cwd) {
7499
7354
  return null;
7500
7355
  }
7501
7356
  }
7502
- async function listUntrackedFiles(cwd) {
7503
- try {
7504
- const out = await runGit(["ls-files", "--others", "--exclude-standard"], cwd);
7505
- return out.split("\n").map((line) => line.trim()).filter(Boolean);
7506
- } catch {
7507
- return [];
7508
- }
7509
- }
7510
- async function getWorkspaceDiff(cwd) {
7511
- const snapshot = await getWorkspaceSnapshot(cwd);
7512
- return {
7513
- diff: snapshot.diff,
7514
- includedUntrackedPaths: snapshot.includedUntrackedPaths
7515
- };
7516
- }
7517
- async function getWorkspaceSnapshot(cwd) {
7518
- const headCommitHash = await getHeadCommitHash(cwd);
7519
- if (!headCommitHash) {
7520
- throw new RemixError("Failed to resolve local HEAD commit.", { exitCode: 1 });
7521
- }
7522
- const includedUntrackedPaths = Array.from(new Set((await listUntrackedFiles(cwd)).map((entry) => normalizeRepoRelativePath(entry))));
7523
- const tempDir = await import_promises12.default.mkdtemp(import_path.default.join(import_os.default.tmpdir(), "remix-index-"));
7524
- const tempIndexPath = import_path.default.join(tempDir, "index");
7525
- const env = { ...process.env, GIT_INDEX_FILE: tempIndexPath };
7526
- try {
7527
- try {
7528
- await runGitWithEnv(["read-tree", "HEAD"], cwd, env);
7529
- await runGitWithEnv(["add", "-A", "--", "."], cwd, env);
7530
- const diff = await runGitRawWithEnv(["diff", "--binary", "--no-ext-diff", "--cached", "HEAD"], cwd, env);
7531
- const nameStatus = await runGitRawWithEnv(["diff", "--name-status", "--find-renames", "-z", "--cached", "HEAD"], cwd, env);
7532
- const pathEntries = parseGitNameStatusZ(nameStatus);
7533
- return {
7534
- diff,
7535
- diffSha256: sha256Hex(diff),
7536
- includedUntrackedPaths,
7537
- pathEntries,
7538
- stagePlan: buildStagePlan(pathEntries)
7539
- };
7540
- } catch {
7541
- throw new RemixError("Failed to generate workspace diff.", {
7542
- exitCode: 1,
7543
- hint: "Git could not snapshot the current workspace into an isolated index."
7544
- });
7545
- }
7546
- } finally {
7547
- await import_promises12.default.rm(tempDir, { recursive: true, force: true }).catch(() => void 0);
7548
- }
7549
- }
7550
- async function writeTempUnifiedDiffBackup(diff, prefix = "remix-add-backup") {
7551
- const safePrefix = prefix.replace(/[^a-zA-Z0-9._-]+/g, "-") || "remix-add-backup";
7552
- const tmpDir = await import_promises12.default.mkdtemp(import_path.default.join(import_os.default.tmpdir(), `${safePrefix}-`));
7553
- const backupPath = import_path.default.join(tmpDir, "submitted.diff");
7554
- await import_promises12.default.writeFile(backupPath, diff, "utf8");
7555
- return { backupPath, diffSha256: sha256Hex(diff) };
7556
- }
7557
- async function validateUnifiedDiff(cwd, diff) {
7558
- const { backupPath } = await writeTempUnifiedDiffBackup(diff, "remix-validate-diff");
7559
- try {
7560
- const applyRes = await runGitDetailed(["apply", "--check", "--index", "--3way", backupPath], cwd);
7561
- if (applyRes.exitCode === 0) {
7562
- return { ok: true };
7563
- }
7564
- const detail = [applyRes.stderr.trim(), applyRes.stdout.trim()].filter(Boolean).join("\n\n") || null;
7565
- return {
7566
- ok: false,
7567
- kind: classifyGitApplyFailure(detail),
7568
- detail
7569
- };
7570
- } finally {
7571
- await import_promises12.default.rm(import_path.default.dirname(backupPath), { recursive: true, force: true }).catch(() => void 0);
7572
- }
7573
- }
7574
- async function preserveWorkspaceChanges(cwd, prefix = "remix-add-preserve") {
7575
- const baseHeadCommitHash = await getHeadCommitHash(cwd);
7576
- if (!baseHeadCommitHash) {
7577
- throw new RemixError("Failed to resolve local HEAD before preserving workspace changes.", { exitCode: 1 });
7578
- }
7579
- const snapshot = await getWorkspaceSnapshot(cwd);
7580
- const { backupPath, diffSha256 } = await writeTempUnifiedDiffBackup(snapshot.diff, prefix);
7581
- return {
7582
- baseHeadCommitHash,
7583
- preservedDiffPath: backupPath,
7584
- preservedDiffSha256: diffSha256,
7585
- includedUntrackedPaths: snapshot.includedUntrackedPaths,
7586
- stagePlan: snapshot.stagePlan
7587
- };
7588
- }
7589
- async function reapplyPreservedWorkspaceChanges(cwd, preserved, operation = "`remix collab add`") {
7590
- const patchFilePath = preserved.preservedDiffPath;
7591
- const tempPatchHash = await import_promises12.default.readFile(patchFilePath, "utf8").then((content) => sha256Hex(content)).catch(() => null);
7592
- if (!tempPatchHash) {
7593
- return { status: "failed", detail: "Preserved diff artifact is missing." };
7594
- }
7595
- if (tempPatchHash !== preserved.preservedDiffSha256) {
7596
- return { status: "failed", detail: "Preserved diff artifact failed integrity verification." };
7597
- }
7598
- const applyRes = await runGitDetailed(["apply", "--index", "--3way", patchFilePath], cwd);
7599
- if (applyRes.exitCode !== 0) {
7600
- await runGitDetailed(["reset", "--hard", "HEAD"], cwd).catch(() => void 0);
7601
- await runGitDetailed(["clean", "-fd"], cwd).catch(() => void 0);
7602
- const detail = [applyRes.stderr.trim(), applyRes.stdout.trim()].filter(Boolean).join("\n\n") || null;
7603
- return { status: "conflict", detail };
7604
- }
7605
- const unstageRes = await runGitDetailed(["reset", "--mixed", "HEAD", "--", "."], cwd);
7606
- if (unstageRes.exitCode !== 0) {
7607
- await runGitDetailed(["reset", "--hard", "HEAD"], cwd).catch(() => void 0);
7608
- await runGitDetailed(["clean", "-fd"], cwd).catch(() => void 0);
7609
- const detail = [unstageRes.stderr.trim(), unstageRes.stdout.trim()].filter(Boolean).join("\n\n") || null;
7610
- return {
7611
- status: "failed",
7612
- detail: detail || `Git could not restore the working tree state while running ${operation}.`
7613
- };
7614
- }
7615
- return { status: "clean" };
7616
- }
7617
7357
  async function getHeadCommitHash(cwd) {
7618
7358
  try {
7619
7359
  const hash = await runGit(["rev-parse", "HEAD"], cwd);
@@ -7631,157 +7371,6 @@ async function getWorktreeStatus(cwd) {
7631
7371
  return { isClean: false, entries: [] };
7632
7372
  }
7633
7373
  }
7634
- async function captureRepoSnapshot(cwd, options) {
7635
- const [branch, headCommitHash, worktreeStatus] = await Promise.all([
7636
- getCurrentBranch(cwd),
7637
- getHeadCommitHash(cwd),
7638
- getWorktreeStatus(cwd)
7639
- ]);
7640
- const workspaceDiffSha256 = options?.includeWorkspaceDiffHash ? (await getWorkspaceSnapshot(cwd)).diffSha256 : null;
7641
- return {
7642
- branch,
7643
- headCommitHash,
7644
- statusEntries: worktreeStatus.entries,
7645
- statusSha256: sha256Hex(worktreeStatus.entries.join("\n")),
7646
- workspaceDiffSha256
7647
- };
7648
- }
7649
- async function assertRepoSnapshotUnchanged(cwd, snapshot, params) {
7650
- const current = await captureRepoSnapshot(cwd, {
7651
- includeWorkspaceDiffHash: snapshot.workspaceDiffSha256 !== null
7652
- });
7653
- const changes = [];
7654
- if (current.branch !== snapshot.branch) {
7655
- changes.push(`Branch changed: ${snapshot.branch ?? "(detached)"} -> ${current.branch ?? "(detached)"}`);
7656
- }
7657
- if (current.headCommitHash !== snapshot.headCommitHash) {
7658
- changes.push(`HEAD changed: ${snapshot.headCommitHash ?? "(missing)"} -> ${current.headCommitHash ?? "(missing)"}`);
7659
- }
7660
- if (current.statusSha256 !== snapshot.statusSha256) {
7661
- changes.push("Working tree status changed.");
7662
- }
7663
- if (snapshot.workspaceDiffSha256 !== null && current.workspaceDiffSha256 !== snapshot.workspaceDiffSha256) {
7664
- changes.push("Workspace diff changed.");
7665
- }
7666
- if (changes.length === 0) return;
7667
- const operation = params?.operation?.trim() || "this Remix operation";
7668
- const hint = [
7669
- `Operation: ${operation}`,
7670
- ...changes,
7671
- params?.recoveryHint?.trim() || "Review the local changes, then rerun the operation."
7672
- ].filter(Boolean).join("\n");
7673
- throw new RemixError("Repository state changed during the operation.", {
7674
- code: REMIX_ERROR_CODES.REPO_STATE_CHANGED_DURING_OPERATION,
7675
- exitCode: 2,
7676
- hint
7677
- });
7678
- }
7679
- async function ensureCleanWorktree(cwd, operation = "`remix collab sync`") {
7680
- const status = await getWorktreeStatus(cwd);
7681
- if (status.isClean) return;
7682
- const preview = status.entries.slice(0, 10).join("\n");
7683
- const suffix = status.entries.length > 10 ? `
7684
- ...and ${status.entries.length - 10} more` : "";
7685
- throw new RemixError(`Working tree must be clean before running ${operation}.`, {
7686
- exitCode: 2,
7687
- hint: `Commit, stash, or discard local changes first.
7688
-
7689
- ${preview}${suffix}`
7690
- });
7691
- }
7692
- async function discardTrackedChanges(cwd, operation = "`remix collab add`") {
7693
- const res = await runGitDetailed(["reset", "--hard", "HEAD"], cwd);
7694
- if (res.exitCode !== 0) {
7695
- const detail = [res.stderr.trim(), res.stdout.trim()].filter(Boolean).join("\n\n");
7696
- throw new RemixError(`Failed to discard local tracked changes while running ${operation}.`, {
7697
- exitCode: 1,
7698
- hint: detail || "Git could not reset tracked changes back to HEAD."
7699
- });
7700
- }
7701
- const hash = await getHeadCommitHash(cwd);
7702
- if (!hash) {
7703
- throw new RemixError("Failed to resolve local HEAD after discarding tracked changes.", { exitCode: 1 });
7704
- }
7705
- return hash;
7706
- }
7707
- async function discardCapturedUntrackedChanges(repoRoot, capturedPaths) {
7708
- const normalizedCapturedPaths = Array.from(new Set(capturedPaths.map((entry) => normalizeRepoRelativePath(entry))));
7709
- if (normalizedCapturedPaths.length === 0) {
7710
- return { removedPaths: [] };
7711
- }
7712
- const currentUntracked = new Set((await listUntrackedFiles(repoRoot)).map((entry) => normalizeRepoRelativePath(entry)));
7713
- const removedPaths = [];
7714
- for (const relativePath of normalizedCapturedPaths) {
7715
- if (!currentUntracked.has(relativePath)) continue;
7716
- const absolutePath = resolveRepoRelativePath(repoRoot, relativePath);
7717
- if (!isWithinRepoRoot(repoRoot, absolutePath)) {
7718
- throw new RemixError("Refusing to delete a path outside the repository root.", {
7719
- exitCode: 1,
7720
- hint: `Path: ${relativePath}`
7721
- });
7722
- }
7723
- await import_promises12.default.rm(absolutePath, { recursive: true, force: true }).catch((err) => {
7724
- throw new RemixError("Failed to remove a captured untracked path before syncing.", {
7725
- exitCode: 1,
7726
- hint: err instanceof Error ? `${relativePath}
7727
-
7728
- ${err.message}` : relativePath
7729
- });
7730
- });
7731
- removedPaths.push(relativePath);
7732
- await pruneEmptyParentDirectories(repoRoot, absolutePath);
7733
- }
7734
- return { removedPaths };
7735
- }
7736
- async function requireCurrentBranch(cwd) {
7737
- const branch = await getCurrentBranch(cwd);
7738
- if (!branch) {
7739
- throw new RemixError("`remix collab sync` requires a checked out local branch.", {
7740
- exitCode: 2,
7741
- hint: "Checkout a branch before syncing."
7742
- });
7743
- }
7744
- return branch;
7745
- }
7746
- async function importGitBundle(cwd, bundlePath, bundleRef) {
7747
- const verifyRes = await runGitDetailed(["bundle", "verify", bundlePath], cwd);
7748
- if (verifyRes.exitCode !== 0) {
7749
- const detail = [verifyRes.stderr.trim(), verifyRes.stdout.trim()].filter(Boolean).join("\n\n");
7750
- throw new RemixError("Failed to verify sync bundle.", {
7751
- exitCode: 1,
7752
- hint: detail || "Git bundle verification failed."
7753
- });
7754
- }
7755
- const fetchRes = await runGitDetailed(["fetch", "--quiet", bundlePath, bundleRef], cwd);
7756
- if (fetchRes.exitCode !== 0) {
7757
- const detail = [fetchRes.stderr.trim(), fetchRes.stdout.trim()].filter(Boolean).join("\n\n");
7758
- throw new RemixError("Failed to import sync bundle.", {
7759
- exitCode: 1,
7760
- hint: detail || "Git could not fetch objects from the sync bundle."
7761
- });
7762
- }
7763
- }
7764
- async function ensureCommitExists(cwd, commitHash) {
7765
- const res = await runGitDetailed(["cat-file", "-e", `${commitHash}^{commit}`], cwd);
7766
- if (res.exitCode === 0) return;
7767
- throw new RemixError("Expected target commit is missing after bundle import.", {
7768
- exitCode: 1,
7769
- hint: `Commit ${commitHash} is not available in the local repository.`
7770
- });
7771
- }
7772
- async function fastForwardToCommit(cwd, commitHash) {
7773
- const res = await runGitDetailed(["merge", "--ff-only", commitHash], cwd);
7774
- if (res.exitCode !== 0) {
7775
- const detail = [res.stderr.trim(), res.stdout.trim()].filter(Boolean).join("\n\n");
7776
- throw new RemixError("Failed to fast-forward local branch.", {
7777
- exitCode: 1,
7778
- hint: detail || "Git could not fast-forward to the target commit."
7779
- });
7780
- }
7781
- const hash = await getHeadCommitHash(cwd);
7782
- if (!hash) throw new RemixError("Failed to resolve local HEAD after fast-forward sync.", { exitCode: 1 });
7783
- return hash;
7784
- }
7785
7374
  function summarizeUnifiedDiff(diff) {
7786
7375
  const lines = diff.split("\n");
7787
7376
  let changedFilesCount = 0;
@@ -7795,21 +7384,21 @@ function summarizeUnifiedDiff(diff) {
7795
7384
  return { changedFilesCount, insertions, deletions };
7796
7385
  }
7797
7386
 
7798
- // node_modules/@remixhq/core/dist/chunk-IXWQWFYT.js
7387
+ // node_modules/@remixhq/core/dist/chunk-YCFLOHJV.js
7388
+ var import_promises12 = __toESM(require("fs/promises"), 1);
7389
+ var import_path = __toESM(require("path"), 1);
7799
7390
  var import_promises13 = __toESM(require("fs/promises"), 1);
7800
7391
  var import_path2 = __toESM(require("path"), 1);
7801
- var import_promises14 = __toESM(require("fs/promises"), 1);
7802
- var import_path3 = __toESM(require("path"), 1);
7803
7392
  async function writeJsonAtomic(filePath, value) {
7804
- const dir = import_path3.default.dirname(filePath);
7805
- await import_promises14.default.mkdir(dir, { recursive: true });
7393
+ const dir = import_path2.default.dirname(filePath);
7394
+ await import_promises13.default.mkdir(dir, { recursive: true });
7806
7395
  const tmp = `${filePath}.tmp-${Date.now()}`;
7807
- await import_promises14.default.writeFile(tmp, `${JSON.stringify(value, null, 2)}
7396
+ await import_promises13.default.writeFile(tmp, `${JSON.stringify(value, null, 2)}
7808
7397
  `, "utf8");
7809
- await import_promises14.default.rename(tmp, filePath);
7398
+ await import_promises13.default.rename(tmp, filePath);
7810
7399
  }
7811
7400
  function getCollabBindingPath(repoRoot) {
7812
- return import_path2.default.join(repoRoot, ".remix", "config.json");
7401
+ return import_path.default.join(repoRoot, ".remix", "config.json");
7813
7402
  }
7814
7403
  function buildBindingFileV3(params) {
7815
7404
  return {
@@ -7872,7 +7461,7 @@ async function readCollabBindingState(repoRoot, options) {
7872
7461
  try {
7873
7462
  const persist = options?.persist === true;
7874
7463
  const filePath = getCollabBindingPath(repoRoot);
7875
- const raw = await import_promises13.default.readFile(filePath, "utf8");
7464
+ const raw = await import_promises12.default.readFile(filePath, "utf8");
7876
7465
  const parsed = JSON.parse(raw);
7877
7466
  if (!parsed || typeof parsed !== "object") return null;
7878
7467
  const currentBranch = normalizeBranchName(await getCurrentBranch(repoRoot).catch(() => null));
@@ -8072,15 +7661,19 @@ async function writeCollabBinding(repoRoot, binding) {
8072
7661
  }
8073
7662
 
8074
7663
  // node_modules/@remixhq/core/dist/collab.js
8075
- var import_promises15 = __toESM(require("fs/promises"), 1);
7664
+ var import_promises14 = __toESM(require("fs/promises"), 1);
7665
+ var import_path3 = __toESM(require("path"), 1);
7666
+ var import_crypto = require("crypto");
7667
+ var import_os = __toESM(require("os"), 1);
8076
7668
  var import_path4 = __toESM(require("path"), 1);
8077
7669
  var import_crypto2 = require("crypto");
8078
- var import_promises16 = __toESM(require("fs/promises"), 1);
7670
+ var import_promises15 = __toESM(require("fs/promises"), 1);
8079
7671
  var import_os2 = __toESM(require("os"), 1);
8080
7672
  var import_path5 = __toESM(require("path"), 1);
8081
- var import_promises17 = __toESM(require("fs/promises"), 1);
8082
- var import_os3 = __toESM(require("os"), 1);
7673
+ var import_crypto3 = require("crypto");
7674
+ var import_promises16 = __toESM(require("fs/promises"), 1);
8083
7675
  var import_path6 = __toESM(require("path"), 1);
7676
+ var import_crypto4 = require("crypto");
8084
7677
  function describeBranch(value) {
8085
7678
  const normalized = String(value ?? "").trim();
8086
7679
  return normalized || "(detached)";
@@ -8099,18 +7692,562 @@ function buildBranchMismatchHint(params) {
8099
7692
  `Switch to ${describeBranch(params.branchName)} or rerun with ${overrideFlag} if this is intentional.`
8100
7693
  ].join("\n");
8101
7694
  }
8102
- function assertBoundBranchMatch(params) {
8103
- if (params.allowBranchMismatch) return;
8104
- if (isBoundBranchMatch(params.currentBranch, params.branchName)) return;
8105
- throw new RemixError(`Current branch does not match this checkout's bound Remix branch while running ${params.operation}.`, {
8106
- code: REMIX_ERROR_CODES.PREFERRED_BRANCH_MISMATCH,
8107
- exitCode: 2,
8108
- hint: buildBranchMismatchHint({
8109
- currentBranch: params.currentBranch,
8110
- branchName: params.branchName,
8111
- overrideFlag: params.overrideFlag
8112
- })
7695
+ function sha256Hex(value) {
7696
+ return (0, import_crypto.createHash)("sha256").update(value).digest("hex");
7697
+ }
7698
+ function getCollabStateRoot() {
7699
+ const configured = process.env.REMIX_COLLAB_STATE_ROOT?.trim();
7700
+ return configured || import_path4.default.join(import_os.default.homedir(), ".remix", "collab-state");
7701
+ }
7702
+ function buildLaneStateKey(params) {
7703
+ const fingerprint = params.repoFingerprint?.trim();
7704
+ const laneId = params.laneId?.trim();
7705
+ const repoRoot = params.repoRoot?.trim();
7706
+ const stableSource = repoRoot || "unknown-repo-root";
7707
+ const fingerprintSource = fingerprint || "unknown-repo-fingerprint";
7708
+ const laneSource = laneId || "unknown-lane";
7709
+ return sha256Hex(`${stableSource}::${fingerprintSource}::${laneSource}`);
7710
+ }
7711
+ function getSnapshotsRoot() {
7712
+ return import_path4.default.join(getCollabStateRoot(), "snapshots");
7713
+ }
7714
+ function getSnapshotRecordsRoot() {
7715
+ return import_path4.default.join(getSnapshotsRoot(), "records");
7716
+ }
7717
+ function getSnapshotBlobsRoot() {
7718
+ return import_path4.default.join(getSnapshotsRoot(), "blobs");
7719
+ }
7720
+ function getBaselinesRoot() {
7721
+ return import_path4.default.join(getCollabStateRoot(), "baselines");
7722
+ }
7723
+ function getFinalizeQueueRoot() {
7724
+ return import_path4.default.join(getCollabStateRoot(), "finalize-queue");
7725
+ }
7726
+ function getBaselinePath(params) {
7727
+ return import_path3.default.join(getBaselinesRoot(), `${buildLaneStateKey(params)}.json`);
7728
+ }
7729
+ async function readLocalBaseline(params) {
7730
+ try {
7731
+ const raw = await import_promises14.default.readFile(getBaselinePath(params), "utf8");
7732
+ const parsed = JSON.parse(raw);
7733
+ if (!parsed || typeof parsed !== "object") return null;
7734
+ if (parsed.schemaVersion !== 1 || typeof parsed.key !== "string" || typeof parsed.repoRoot !== "string") {
7735
+ return null;
7736
+ }
7737
+ return {
7738
+ schemaVersion: 1,
7739
+ key: parsed.key,
7740
+ repoRoot: parsed.repoRoot,
7741
+ repoFingerprint: parsed.repoFingerprint ?? null,
7742
+ laneId: parsed.laneId ?? null,
7743
+ currentAppId: String(parsed.currentAppId ?? ""),
7744
+ branchName: parsed.branchName ?? null,
7745
+ lastSnapshotId: parsed.lastSnapshotId ?? null,
7746
+ lastSnapshotHash: parsed.lastSnapshotHash ?? null,
7747
+ lastServerHeadHash: parsed.lastServerHeadHash ?? null,
7748
+ lastSeenLocalCommitHash: parsed.lastSeenLocalCommitHash ?? null,
7749
+ updatedAt: String(parsed.updatedAt ?? "")
7750
+ };
7751
+ } catch {
7752
+ return null;
7753
+ }
7754
+ }
7755
+ async function writeLocalBaseline(baseline) {
7756
+ const key = buildLaneStateKey(baseline);
7757
+ const normalized = {
7758
+ schemaVersion: 1,
7759
+ key,
7760
+ repoRoot: baseline.repoRoot,
7761
+ repoFingerprint: baseline.repoFingerprint ?? null,
7762
+ laneId: baseline.laneId ?? null,
7763
+ currentAppId: baseline.currentAppId,
7764
+ branchName: baseline.branchName ?? null,
7765
+ lastSnapshotId: baseline.lastSnapshotId ?? null,
7766
+ lastSnapshotHash: baseline.lastSnapshotHash ?? null,
7767
+ lastServerHeadHash: baseline.lastServerHeadHash ?? null,
7768
+ lastSeenLocalCommitHash: baseline.lastSeenLocalCommitHash ?? null,
7769
+ updatedAt: baseline.updatedAt ?? (/* @__PURE__ */ new Date()).toISOString()
7770
+ };
7771
+ await writeJsonAtomic(getBaselinePath(baseline), normalized);
7772
+ return normalized;
7773
+ }
7774
+ function sha256Hex2(value) {
7775
+ return (0, import_crypto2.createHash)("sha256").update(value).digest("hex");
7776
+ }
7777
+ function getSnapshotRecordPath(snapshotId) {
7778
+ return import_path5.default.join(getSnapshotRecordsRoot(), `${snapshotId}.json`);
7779
+ }
7780
+ function getBlobPath(blobHash) {
7781
+ return import_path5.default.join(getSnapshotBlobsRoot(), blobHash.slice(0, 2), blobHash);
7782
+ }
7783
+ async function runGitZ(args, cwd) {
7784
+ const res = await execa("git", args, {
7785
+ cwd,
7786
+ stderr: "ignore",
7787
+ stripFinalNewline: false
8113
7788
  });
7789
+ return String(res.stdout || "");
7790
+ }
7791
+ async function listWorkspaceFiles(repoRoot) {
7792
+ const raw = await runGitZ(["ls-files", "-z", "--cached", "--others", "--exclude-standard", "--deduplicate"], repoRoot);
7793
+ const seen = /* @__PURE__ */ new Set();
7794
+ const result = [];
7795
+ for (const entry of raw.split("\0")) {
7796
+ const relativePath = entry.trim();
7797
+ if (!relativePath || seen.has(relativePath)) continue;
7798
+ const absolutePath = import_path5.default.join(repoRoot, relativePath);
7799
+ try {
7800
+ const stat = await import_promises15.default.lstat(absolutePath);
7801
+ if (stat.isFile() || stat.isSymbolicLink()) {
7802
+ seen.add(relativePath);
7803
+ result.push(relativePath);
7804
+ }
7805
+ } catch {
7806
+ }
7807
+ }
7808
+ return result.sort((a2, b) => a2.localeCompare(b));
7809
+ }
7810
+ async function persistBlob(blobHash, content) {
7811
+ const blobPath = getBlobPath(blobHash);
7812
+ try {
7813
+ await import_promises15.default.access(blobPath);
7814
+ } catch {
7815
+ await import_promises15.default.mkdir(import_path5.default.dirname(blobPath), { recursive: true });
7816
+ if (typeof content === "string") {
7817
+ await import_promises15.default.writeFile(blobPath, content, "utf8");
7818
+ } else {
7819
+ await import_promises15.default.writeFile(blobPath, content);
7820
+ }
7821
+ }
7822
+ }
7823
+ function buildSnapshotHash(files) {
7824
+ const manifest = files.map((file) => `${file.path} ${file.mode} ${file.blobHash} ${file.size}`).join("\n");
7825
+ return sha256Hex2(manifest);
7826
+ }
7827
+ async function inspectLocalSnapshot(params) {
7828
+ const repoRoot = params.repoRoot;
7829
+ const files = await listWorkspaceFiles(repoRoot);
7830
+ const manifest = [];
7831
+ for (const relativePath of files) {
7832
+ const absolutePath = import_path5.default.join(repoRoot, relativePath);
7833
+ const stat = await import_promises15.default.lstat(absolutePath);
7834
+ if (stat.isSymbolicLink()) {
7835
+ const linkTarget = await import_promises15.default.readlink(absolutePath);
7836
+ const blobHash2 = sha256Hex2(`symlink:${linkTarget}`);
7837
+ if (params.persistBlobs !== false) {
7838
+ await persistBlob(blobHash2, linkTarget);
7839
+ }
7840
+ manifest.push({
7841
+ path: relativePath,
7842
+ mode: "symlink",
7843
+ blobHash: blobHash2,
7844
+ size: Buffer.byteLength(linkTarget)
7845
+ });
7846
+ continue;
7847
+ }
7848
+ const content = await import_promises15.default.readFile(absolutePath);
7849
+ const blobHash = sha256Hex2(content);
7850
+ if (params.persistBlobs !== false) {
7851
+ await persistBlob(blobHash, content);
7852
+ }
7853
+ manifest.push({
7854
+ path: relativePath,
7855
+ mode: stat.mode & 73 ? "executable" : "file",
7856
+ blobHash,
7857
+ size: stat.size
7858
+ });
7859
+ }
7860
+ const normalizedManifest = manifest.sort((a2, b) => a2.path.localeCompare(b.path));
7861
+ return {
7862
+ repoRoot,
7863
+ repoFingerprint: params.repoFingerprint ?? null,
7864
+ laneId: params.laneId ?? null,
7865
+ branchName: params.branchName ?? await getCurrentBranch(repoRoot).catch(() => null),
7866
+ localCommitHash: await getHeadCommitHash(repoRoot).catch(() => null),
7867
+ snapshotHash: buildSnapshotHash(normalizedManifest),
7868
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
7869
+ files: normalizedManifest
7870
+ };
7871
+ }
7872
+ async function captureLocalSnapshot(params) {
7873
+ const inspection = await inspectLocalSnapshot({ ...params, persistBlobs: true });
7874
+ const snapshot = {
7875
+ schemaVersion: 1,
7876
+ id: (0, import_crypto2.randomUUID)(),
7877
+ ...inspection
7878
+ };
7879
+ await writeJsonAtomic(getSnapshotRecordPath(snapshot.id), snapshot);
7880
+ return snapshot;
7881
+ }
7882
+ async function readLocalSnapshot(snapshotId) {
7883
+ if (!snapshotId) return null;
7884
+ try {
7885
+ const raw = await import_promises15.default.readFile(getSnapshotRecordPath(snapshotId), "utf8");
7886
+ const parsed = JSON.parse(raw);
7887
+ if (!parsed || parsed.schemaVersion !== 1) return null;
7888
+ return parsed;
7889
+ } catch {
7890
+ return null;
7891
+ }
7892
+ }
7893
+ async function materializeLocalSnapshot(snapshotId, targetDir) {
7894
+ const snapshot = await readLocalSnapshot(snapshotId);
7895
+ await import_promises15.default.mkdir(targetDir, { recursive: true });
7896
+ if (!snapshot) return;
7897
+ for (const entry of snapshot.files) {
7898
+ const destination = import_path5.default.join(targetDir, entry.path);
7899
+ await import_promises15.default.mkdir(import_path5.default.dirname(destination), { recursive: true });
7900
+ const blobPath = getBlobPath(entry.blobHash);
7901
+ if (entry.mode === "symlink") {
7902
+ const linkTarget = await import_promises15.default.readFile(blobPath, "utf8");
7903
+ await import_promises15.default.symlink(linkTarget, destination);
7904
+ continue;
7905
+ }
7906
+ await import_promises15.default.copyFile(blobPath, destination);
7907
+ if (entry.mode === "executable") {
7908
+ await import_promises15.default.chmod(destination, 493);
7909
+ }
7910
+ }
7911
+ }
7912
+ async function clearDirectoryExceptGit(targetDir) {
7913
+ const entries = await import_promises15.default.readdir(targetDir, { withFileTypes: true });
7914
+ for (const entry of entries) {
7915
+ if (entry.name === ".git") continue;
7916
+ await import_promises15.default.rm(import_path5.default.join(targetDir, entry.name), { recursive: true, force: true });
7917
+ }
7918
+ }
7919
+ async function diffLocalSnapshots(params) {
7920
+ const tempRoot = await import_promises15.default.mkdtemp(import_path5.default.join(import_os2.default.tmpdir(), "remix-snapshot-diff-"));
7921
+ const repoDir = import_path5.default.join(tempRoot, "repo");
7922
+ await import_promises15.default.mkdir(repoDir, { recursive: true });
7923
+ try {
7924
+ await materializeLocalSnapshot(params.baseSnapshotId, repoDir);
7925
+ await execa("git", ["init"], { cwd: repoDir, stderr: "ignore" });
7926
+ await execa("git", ["add", "-A"], { cwd: repoDir, stderr: "ignore" });
7927
+ await execa(
7928
+ "git",
7929
+ ["-c", "user.name=Remix", "-c", "user.email=remix@local", "commit", "--allow-empty", "-m", "baseline snapshot"],
7930
+ { cwd: repoDir, stderr: "ignore" }
7931
+ );
7932
+ await clearDirectoryExceptGit(repoDir);
7933
+ await materializeLocalSnapshot(params.targetSnapshotId, repoDir);
7934
+ await execa("git", ["add", "-A"], { cwd: repoDir, stderr: "ignore" });
7935
+ const diffRes = await execa("git", ["diff", "--binary", "--no-ext-diff", "--cached", "HEAD"], {
7936
+ cwd: repoDir,
7937
+ reject: false,
7938
+ stderr: "ignore",
7939
+ stripFinalNewline: false
7940
+ });
7941
+ const pathsRes = await execa("git", ["diff", "--name-only", "--cached", "HEAD", "-z"], {
7942
+ cwd: repoDir,
7943
+ reject: false,
7944
+ stderr: "ignore",
7945
+ stripFinalNewline: false
7946
+ });
7947
+ const diff = String(diffRes.stdout || "");
7948
+ const changedPaths = String(pathsRes.stdout || "").split("\0").map((value) => value.trim()).filter(Boolean);
7949
+ return {
7950
+ baseSnapshotId: params.baseSnapshotId,
7951
+ targetSnapshotId: params.targetSnapshotId,
7952
+ diff,
7953
+ diffSha256: diff ? sha256Hex2(diff) : null,
7954
+ changedPaths,
7955
+ stats: summarizeUnifiedDiff(diff)
7956
+ };
7957
+ } finally {
7958
+ await import_promises15.default.rm(tempRoot, { recursive: true, force: true });
7959
+ }
7960
+ }
7961
+ var FINALIZE_JOB_LOCK_STALE_MS = 10 * 60 * 1e3;
7962
+ var TERMINAL_FINALIZE_JOB_RETENTION_MS = 24 * 60 * 60 * 1e3;
7963
+ function getJobPath(id) {
7964
+ return import_path6.default.join(getFinalizeQueueRoot(), `${id}.json`);
7965
+ }
7966
+ function getJobLockPath(id) {
7967
+ return import_path6.default.join(getFinalizeQueueRoot(), `${id}.lock`);
7968
+ }
7969
+ function isPastDue(isoTimestamp) {
7970
+ if (!isoTimestamp) return true;
7971
+ const parsed = Date.parse(isoTimestamp);
7972
+ return Number.isFinite(parsed) && parsed <= Date.now();
7973
+ }
7974
+ function isStaleAttempt(job) {
7975
+ if (job.status !== "processing") return false;
7976
+ if (!job.lastAttemptAt) return true;
7977
+ const parsed = Date.parse(job.lastAttemptAt);
7978
+ if (!Number.isFinite(parsed)) return true;
7979
+ return Date.now() - parsed >= FINALIZE_JOB_LOCK_STALE_MS;
7980
+ }
7981
+ function readMetadataDisposition(job) {
7982
+ const value = job.metadata.failureDisposition;
7983
+ return value === "retryable" || value === "terminal" ? value : null;
7984
+ }
7985
+ function isTerminalFailure(job) {
7986
+ return job.status === "failed" && readMetadataDisposition(job) === "terminal";
7987
+ }
7988
+ function isTerminalFailureExpired(job) {
7989
+ if (!isTerminalFailure(job)) return false;
7990
+ const updatedAtMs = Date.parse(job.updatedAt);
7991
+ if (!Number.isFinite(updatedAtMs)) return false;
7992
+ return Date.now() - updatedAtMs >= TERMINAL_FINALIZE_JOB_RETENTION_MS;
7993
+ }
7994
+ function matchesJobScope(job, scope) {
7995
+ if (job.repoRoot !== scope.repoRoot) return false;
7996
+ if (scope.currentAppId && job.currentAppId !== scope.currentAppId) return false;
7997
+ if (scope.laneId && job.laneId !== scope.laneId) return false;
7998
+ if (scope.repoFingerprint && job.repoFingerprint && job.repoFingerprint !== scope.repoFingerprint) return false;
7999
+ return true;
8000
+ }
8001
+ function createEmptyPendingFinalizeQueueSummary() {
8002
+ return {
8003
+ state: "idle",
8004
+ activeJobCount: 0,
8005
+ queuedJobCount: 0,
8006
+ processingJobCount: 0,
8007
+ retryScheduledJobCount: 0,
8008
+ failedJobCount: 0,
8009
+ oldestCapturedAt: null,
8010
+ newestCapturedAt: null,
8011
+ nextRetryAt: null,
8012
+ latestError: null
8013
+ };
8014
+ }
8015
+ async function acquireJobLock(jobId) {
8016
+ const lockPath = getJobLockPath(jobId);
8017
+ try {
8018
+ await import_promises16.default.mkdir(lockPath);
8019
+ return true;
8020
+ } catch (error) {
8021
+ if (error?.code !== "EEXIST") {
8022
+ throw error;
8023
+ }
8024
+ }
8025
+ try {
8026
+ const stat = await import_promises16.default.stat(lockPath);
8027
+ if (Date.now() - stat.mtimeMs < FINALIZE_JOB_LOCK_STALE_MS) {
8028
+ return false;
8029
+ }
8030
+ await import_promises16.default.rm(lockPath, { recursive: true, force: true });
8031
+ } catch (error) {
8032
+ if (error?.code !== "ENOENT") {
8033
+ throw error;
8034
+ }
8035
+ }
8036
+ try {
8037
+ await import_promises16.default.mkdir(lockPath);
8038
+ return true;
8039
+ } catch (error) {
8040
+ if (error?.code === "EEXIST") {
8041
+ return false;
8042
+ }
8043
+ throw error;
8044
+ }
8045
+ }
8046
+ function normalizeJob(input) {
8047
+ const now = (/* @__PURE__ */ new Date()).toISOString();
8048
+ return {
8049
+ schemaVersion: 1,
8050
+ id: input.id ?? (0, import_crypto3.randomUUID)(),
8051
+ status: input.status,
8052
+ repoRoot: input.repoRoot,
8053
+ repoFingerprint: input.repoFingerprint ?? null,
8054
+ currentAppId: input.currentAppId,
8055
+ laneId: input.laneId ?? null,
8056
+ threadId: input.threadId ?? null,
8057
+ branchName: input.branchName ?? null,
8058
+ prompt: input.prompt,
8059
+ assistantResponse: input.assistantResponse,
8060
+ baselineSnapshotId: input.baselineSnapshotId ?? null,
8061
+ baselineServerHeadHash: input.baselineServerHeadHash ?? null,
8062
+ currentSnapshotId: input.currentSnapshotId,
8063
+ capturedAt: input.capturedAt ?? now,
8064
+ updatedAt: input.updatedAt ?? now,
8065
+ idempotencyKey: input.idempotencyKey ?? null,
8066
+ error: input.error ?? null,
8067
+ retryCount: Number.isFinite(input.retryCount) ? Math.max(0, Number(input.retryCount)) : 0,
8068
+ lastAttemptAt: input.lastAttemptAt ?? null,
8069
+ nextRetryAt: input.nextRetryAt ?? null,
8070
+ metadata: input.metadata ?? {}
8071
+ };
8072
+ }
8073
+ async function enqueuePendingFinalizeJob(input) {
8074
+ const job = normalizeJob(input);
8075
+ await writeJsonAtomic(getJobPath(job.id), job);
8076
+ return job;
8077
+ }
8078
+ async function readPendingFinalizeJob(jobId) {
8079
+ try {
8080
+ const raw = await import_promises16.default.readFile(getJobPath(jobId), "utf8");
8081
+ const parsed = JSON.parse(raw);
8082
+ if (!parsed || parsed.schemaVersion !== 1 || typeof parsed.id !== "string") return null;
8083
+ return normalizeJob({
8084
+ id: parsed.id,
8085
+ status: parsed.status ?? "queued",
8086
+ repoRoot: String(parsed.repoRoot ?? ""),
8087
+ repoFingerprint: parsed.repoFingerprint ?? null,
8088
+ currentAppId: String(parsed.currentAppId ?? ""),
8089
+ laneId: parsed.laneId ?? null,
8090
+ threadId: parsed.threadId ?? null,
8091
+ branchName: parsed.branchName ?? null,
8092
+ prompt: String(parsed.prompt ?? ""),
8093
+ assistantResponse: String(parsed.assistantResponse ?? ""),
8094
+ baselineSnapshotId: parsed.baselineSnapshotId ?? null,
8095
+ baselineServerHeadHash: parsed.baselineServerHeadHash ?? null,
8096
+ currentSnapshotId: String(parsed.currentSnapshotId ?? ""),
8097
+ capturedAt: parsed.capturedAt,
8098
+ updatedAt: parsed.updatedAt,
8099
+ idempotencyKey: parsed.idempotencyKey ?? null,
8100
+ error: parsed.error ?? null,
8101
+ retryCount: parsed.retryCount ?? 0,
8102
+ lastAttemptAt: parsed.lastAttemptAt ?? null,
8103
+ nextRetryAt: parsed.nextRetryAt ?? null,
8104
+ metadata: parsed.metadata ?? {}
8105
+ });
8106
+ } catch {
8107
+ return null;
8108
+ }
8109
+ }
8110
+ async function listPendingFinalizeJobs() {
8111
+ try {
8112
+ const entries = await import_promises16.default.readdir(getFinalizeQueueRoot(), { withFileTypes: true });
8113
+ const jobs = await Promise.all(
8114
+ entries.filter((entry) => entry.isFile() && entry.name.endsWith(".json")).map((entry) => readPendingFinalizeJob(entry.name.replace(/\.json$/, "")))
8115
+ );
8116
+ return jobs.filter((job) => Boolean(job)).sort((a2, b) => a2.capturedAt.localeCompare(b.capturedAt));
8117
+ } catch (error) {
8118
+ if (error?.code === "ENOENT") {
8119
+ return [];
8120
+ }
8121
+ throw error;
8122
+ }
8123
+ }
8124
+ async function prunePendingFinalizeJobs() {
8125
+ const jobs = await listPendingFinalizeJobs();
8126
+ await Promise.all(
8127
+ jobs.filter((job) => job.status === "completed" || isTerminalFailureExpired(job)).map((job) => removePendingFinalizeJob(job.id))
8128
+ );
8129
+ }
8130
+ async function summarizePendingFinalizeJobs(scope) {
8131
+ const jobs = (await listPendingFinalizeJobs()).filter((job) => matchesJobScope(job, scope));
8132
+ const summary = createEmptyPendingFinalizeQueueSummary();
8133
+ const relevantJobs = jobs.filter((job) => job.status !== "completed");
8134
+ if (relevantJobs.length === 0) return summary;
8135
+ summary.oldestCapturedAt = relevantJobs[0]?.capturedAt ?? null;
8136
+ summary.newestCapturedAt = relevantJobs[relevantJobs.length - 1]?.capturedAt ?? null;
8137
+ for (const job of relevantJobs) {
8138
+ if (job.error) {
8139
+ summary.latestError = job.error;
8140
+ }
8141
+ if (job.nextRetryAt && (!summary.nextRetryAt || job.nextRetryAt < summary.nextRetryAt)) {
8142
+ summary.nextRetryAt = job.nextRetryAt;
8143
+ }
8144
+ if (job.status === "processing") {
8145
+ summary.processingJobCount += 1;
8146
+ continue;
8147
+ }
8148
+ if (job.status === "failed") {
8149
+ summary.failedJobCount += 1;
8150
+ continue;
8151
+ }
8152
+ if (!isPastDue(job.nextRetryAt)) {
8153
+ summary.retryScheduledJobCount += 1;
8154
+ continue;
8155
+ }
8156
+ summary.queuedJobCount += 1;
8157
+ }
8158
+ summary.activeJobCount = summary.queuedJobCount + summary.processingJobCount + summary.retryScheduledJobCount;
8159
+ if (summary.processingJobCount > 0) {
8160
+ summary.state = "processing";
8161
+ } else if (summary.queuedJobCount > 0) {
8162
+ summary.state = "queued";
8163
+ } else if (summary.retryScheduledJobCount > 0) {
8164
+ summary.state = "retry_scheduled";
8165
+ } else if (summary.failedJobCount > 0) {
8166
+ summary.state = "failed";
8167
+ }
8168
+ return summary;
8169
+ }
8170
+ async function updatePendingFinalizeJob(jobId, update) {
8171
+ const existing = await readPendingFinalizeJob(jobId);
8172
+ if (!existing) return null;
8173
+ const next = {
8174
+ ...existing,
8175
+ ...update,
8176
+ schemaVersion: 1,
8177
+ id: existing.id,
8178
+ capturedAt: existing.capturedAt,
8179
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
8180
+ metadata: update.metadata ? { ...existing.metadata, ...update.metadata } : existing.metadata
8181
+ };
8182
+ await writeJsonAtomic(getJobPath(jobId), next);
8183
+ return next;
8184
+ }
8185
+ async function claimPendingFinalizeJob(jobId) {
8186
+ const lockPath = getJobLockPath(jobId);
8187
+ const lockAcquired = await acquireJobLock(jobId);
8188
+ if (!lockAcquired) return null;
8189
+ let released = false;
8190
+ const release = async () => {
8191
+ if (released) return;
8192
+ released = true;
8193
+ await import_promises16.default.rm(lockPath, { recursive: true, force: true }).catch(() => void 0);
8194
+ };
8195
+ try {
8196
+ let existing = await readPendingFinalizeJob(jobId);
8197
+ if (!existing) {
8198
+ await release();
8199
+ return null;
8200
+ }
8201
+ if (isStaleAttempt(existing)) {
8202
+ const recovered = await updatePendingFinalizeJob(jobId, {
8203
+ status: "queued",
8204
+ error: existing.error ?? "Recovered a stale finalize processing lease.",
8205
+ nextRetryAt: null
8206
+ });
8207
+ existing = recovered ?? existing;
8208
+ }
8209
+ if (existing.status === "failed") {
8210
+ if (isTerminalFailure(existing)) {
8211
+ await release();
8212
+ return null;
8213
+ }
8214
+ const recovered = await updatePendingFinalizeJob(jobId, {
8215
+ status: "queued",
8216
+ nextRetryAt: existing.nextRetryAt ?? null
8217
+ });
8218
+ existing = recovered ?? existing;
8219
+ }
8220
+ if (existing.status !== "queued" || !isPastDue(existing.nextRetryAt)) {
8221
+ await release();
8222
+ return null;
8223
+ }
8224
+ const now = (/* @__PURE__ */ new Date()).toISOString();
8225
+ const claimed = await updatePendingFinalizeJob(jobId, {
8226
+ status: "processing",
8227
+ error: null,
8228
+ retryCount: existing.retryCount + 1,
8229
+ lastAttemptAt: now,
8230
+ nextRetryAt: null
8231
+ });
8232
+ if (!claimed) {
8233
+ await release();
8234
+ return null;
8235
+ }
8236
+ return { job: claimed, release };
8237
+ } catch (error) {
8238
+ await release();
8239
+ throw error;
8240
+ }
8241
+ }
8242
+ async function removePendingFinalizeJob(jobId) {
8243
+ try {
8244
+ await import_promises16.default.unlink(getJobPath(jobId));
8245
+ } catch (error) {
8246
+ if (error?.code !== "ENOENT") {
8247
+ throw error;
8248
+ }
8249
+ }
8250
+ await import_promises16.default.rm(getJobLockPath(jobId), { recursive: true, force: true }).catch(() => void 0);
8114
8251
  }
8115
8252
  function unwrapResponseObject(resp, label) {
8116
8253
  const obj = resp?.responseObject;
@@ -8124,7 +8261,7 @@ function sleep(ms) {
8124
8261
  return new Promise((resolve) => setTimeout(resolve, ms));
8125
8262
  }
8126
8263
  function buildDeterministicIdempotencyKey(parts) {
8127
- return (0, import_crypto2.createHash)("sha256").update(JSON.stringify(parts)).digest("hex");
8264
+ return (0, import_crypto4.createHash)("sha256").update(JSON.stringify(parts)).digest("hex");
8128
8265
  }
8129
8266
  function formatCliErrorDetail(err) {
8130
8267
  if (err instanceof RemixError) {
@@ -8135,25 +8272,6 @@ function formatCliErrorDetail(err) {
8135
8272
  }
8136
8273
  return typeof err === "string" && err.trim() ? err.trim() : null;
8137
8274
  }
8138
- async function pollAppReady(api, appId) {
8139
- const started = Date.now();
8140
- let delay = 2e3;
8141
- while (Date.now() - started < 20 * 60 * 1e3) {
8142
- const appResp = await api.getApp(appId);
8143
- const app = unwrapResponseObject(appResp, "app");
8144
- const status = typeof app.status === "string" ? app.status : "";
8145
- if (status === "ready") return app;
8146
- if (status === "error") {
8147
- throw new RemixError("App is in error state.", {
8148
- exitCode: 1,
8149
- hint: typeof app.statusError === "string" ? app.statusError : null
8150
- });
8151
- }
8152
- await sleep(delay);
8153
- delay = Math.min(1e4, Math.floor(delay * 1.4));
8154
- }
8155
- throw new RemixError("Timed out waiting for app to become ready.", { exitCode: 1 });
8156
- }
8157
8275
  async function pollChangeStep(api, appId, changeStepId) {
8158
8276
  const started = Date.now();
8159
8277
  let delay = 1500;
@@ -8428,638 +8546,567 @@ async function ensureActiveLaneBinding(params) {
8428
8546
  hint: `Run \`remix collab init\` on branch ${resolved.currentBranch} before running ${params.operation ?? "this command"}.`
8429
8547
  });
8430
8548
  }
8431
- async function collabRecordingPreflight(params) {
8549
+ function buildBaseState() {
8550
+ return {
8551
+ status: "ready",
8552
+ repoState: null,
8553
+ repoRoot: null,
8554
+ binding: null,
8555
+ currentBranch: null,
8556
+ branchName: null,
8557
+ localCommitHash: null,
8558
+ currentSnapshotHash: null,
8559
+ currentServerHeadHash: null,
8560
+ currentServerHeadCommitId: null,
8561
+ worktreeClean: false,
8562
+ pendingFinalize: {
8563
+ state: "idle",
8564
+ activeJobCount: 0,
8565
+ queuedJobCount: 0,
8566
+ processingJobCount: 0,
8567
+ retryScheduledJobCount: 0,
8568
+ failedJobCount: 0,
8569
+ oldestCapturedAt: null,
8570
+ newestCapturedAt: null,
8571
+ nextRetryAt: null,
8572
+ latestError: null
8573
+ },
8574
+ warnings: [],
8575
+ hint: null,
8576
+ metadataWarnings: [],
8577
+ baseline: {
8578
+ lastSnapshotId: null,
8579
+ lastSnapshotHash: null,
8580
+ lastServerHeadHash: null,
8581
+ lastSeenLocalCommitHash: null
8582
+ }
8583
+ };
8584
+ }
8585
+ async function collabDetectRepoState(params) {
8586
+ const detected = buildBaseState();
8432
8587
  let repoRoot;
8433
8588
  try {
8434
8589
  repoRoot = await findGitRoot(params.cwd);
8435
8590
  } catch (error) {
8436
- const message = error instanceof Error ? error.message : "Not inside a git repository.";
8437
- return {
8438
- status: "not_git_repo",
8439
- repoRoot: null,
8440
- appId: null,
8441
- currentBranch: null,
8442
- branchName: null,
8443
- headCommitHash: null,
8444
- worktreeClean: false,
8445
- syncStatus: null,
8446
- syncTargetCommitHash: null,
8447
- syncTargetCommitId: null,
8448
- reconcileTargetHeadCommitHash: null,
8449
- reconcileTargetHeadCommitId: null,
8450
- warnings: [],
8451
- hint: message
8452
- };
8591
+ detected.status = "not_git_repo";
8592
+ detected.hint = formatCliErrorDetail(error) ?? "Not inside a git repository.";
8593
+ return detected;
8453
8594
  }
8454
- const bindingResolution = await resolveActiveLaneBinding({ repoRoot, api: params.api });
8595
+ detected.repoRoot = repoRoot;
8596
+ const bindingResolution = await resolveActiveLaneBinding({ repoRoot, api: params.api ?? void 0 });
8455
8597
  if (bindingResolution.status === "not_bound") {
8456
- return {
8457
- status: "not_bound",
8458
- repoRoot,
8459
- appId: null,
8460
- currentBranch: null,
8461
- branchName: null,
8462
- headCommitHash: null,
8463
- worktreeClean: false,
8464
- syncStatus: null,
8465
- syncTargetCommitHash: null,
8466
- syncTargetCommitId: null,
8467
- reconcileTargetHeadCommitHash: null,
8468
- reconcileTargetHeadCommitId: null,
8469
- warnings: [],
8470
- hint: "Run `remix collab init` first."
8471
- };
8598
+ detected.status = "not_bound";
8599
+ detected.repoState = "binding_problem";
8600
+ detected.hint = "Run `remix collab init` first.";
8601
+ return detected;
8472
8602
  }
8473
8603
  if (bindingResolution.status === "missing_branch_binding") {
8474
- return {
8475
- status: "branch_binding_missing",
8476
- repoRoot,
8477
- appId: null,
8478
- currentBranch: bindingResolution.currentBranch,
8479
- branchName: bindingResolution.currentBranch,
8480
- headCommitHash: null,
8481
- worktreeClean: false,
8482
- syncStatus: null,
8483
- syncTargetCommitHash: null,
8484
- syncTargetCommitId: null,
8485
- reconcileTargetHeadCommitHash: null,
8486
- reconcileTargetHeadCommitId: null,
8487
- warnings: [],
8488
- hint: `Current branch ${bindingResolution.currentBranch ?? "(detached)"} is not yet bound to a Remix lane.`
8489
- };
8604
+ detected.status = "branch_binding_missing";
8605
+ detected.repoState = "binding_problem";
8606
+ detected.currentBranch = bindingResolution.currentBranch;
8607
+ detected.branchName = bindingResolution.currentBranch;
8608
+ detected.hint = `Current branch ${bindingResolution.currentBranch ?? "(detached)"} is not yet bound to a Remix lane.`;
8609
+ return detected;
8490
8610
  }
8491
8611
  if (bindingResolution.status === "ambiguous_family_selection") {
8492
- return {
8493
- status: "family_ambiguous",
8494
- repoRoot,
8495
- appId: null,
8496
- currentBranch: bindingResolution.currentBranch,
8497
- branchName: bindingResolution.currentBranch,
8498
- headCommitHash: null,
8499
- worktreeClean: false,
8500
- syncStatus: null,
8501
- syncTargetCommitHash: null,
8502
- syncTargetCommitId: null,
8503
- reconcileTargetHeadCommitHash: null,
8504
- reconcileTargetHeadCommitId: null,
8505
- warnings: [],
8506
- hint: "Multiple canonical Remix families match this repository. Continue from a checkout already bound to the intended family, or run `remix collab init --force-new` to create a new canonical family."
8507
- };
8612
+ detected.status = "family_ambiguous";
8613
+ detected.repoState = "binding_problem";
8614
+ detected.currentBranch = bindingResolution.currentBranch;
8615
+ detected.branchName = bindingResolution.currentBranch;
8616
+ detected.hint = "Multiple canonical Remix families match this repository. Continue from a checkout already bound to the intended family, or run `remix collab init --force-new`.";
8617
+ return detected;
8508
8618
  }
8509
8619
  if (bindingResolution.status === "binding_conflict") {
8510
- return {
8511
- status: "metadata_conflict",
8512
- repoRoot,
8513
- appId: bindingResolution.binding.currentAppId,
8514
- currentBranch: bindingResolution.currentBranch,
8515
- branchName: bindingResolution.binding.branchName,
8516
- headCommitHash: null,
8517
- worktreeClean: false,
8518
- syncStatus: null,
8519
- syncTargetCommitHash: null,
8520
- syncTargetCommitId: null,
8521
- reconcileTargetHeadCommitHash: null,
8522
- reconcileTargetHeadCommitId: null,
8523
- warnings: [],
8524
- hint: `Local binding for ${bindingResolution.currentBranch ?? "(detached)"} points to app ${bindingResolution.binding.currentAppId}, but the server resolved lane ${bindingResolution.resolvedLane.laneId ?? "(unknown)"} / app ${bindingResolution.resolvedLane.currentAppId ?? "(unknown)"}. Repair the branch binding before recording work.`
8525
- };
8620
+ detected.status = "metadata_conflict";
8621
+ detected.repoState = "metadata_conflict";
8622
+ detected.binding = bindingResolution.binding;
8623
+ detected.currentBranch = bindingResolution.currentBranch;
8624
+ detected.branchName = bindingResolution.binding.branchName;
8625
+ detected.hint = `Local binding for ${bindingResolution.currentBranch ?? "(detached)"} points to app ${bindingResolution.binding.currentAppId}, but the server resolved lane ${bindingResolution.resolvedLane.laneId ?? "(unknown)"} / app ${bindingResolution.resolvedLane.currentAppId ?? "(unknown)"}.`;
8626
+ return detected;
8526
8627
  }
8527
8628
  const binding = bindingResolution.binding;
8528
- const [currentBranch, headCommitHash, worktreeStatus] = await Promise.all([
8629
+ detected.binding = binding;
8630
+ const [currentBranch, localCommitHash, worktreeStatus] = await Promise.all([
8529
8631
  getCurrentBranch(repoRoot),
8530
8632
  getHeadCommitHash(repoRoot),
8531
8633
  getWorktreeStatus(repoRoot)
8532
8634
  ]);
8533
- const branchName = binding.branchName ?? null;
8534
- if (!headCommitHash) {
8535
- return {
8536
- status: "missing_head",
8537
- repoRoot,
8538
- appId: binding.currentAppId,
8539
- currentBranch,
8540
- branchName,
8541
- headCommitHash: null,
8542
- worktreeClean: worktreeStatus.isClean,
8543
- syncStatus: null,
8544
- syncTargetCommitHash: null,
8545
- syncTargetCommitId: null,
8546
- reconcileTargetHeadCommitHash: null,
8547
- reconcileTargetHeadCommitId: null,
8548
- warnings: [],
8549
- hint: "Failed to resolve local HEAD commit."
8550
- };
8551
- }
8552
- if (!params.allowBranchMismatch && !isBoundBranchMatch(currentBranch, branchName)) {
8553
- return {
8554
- status: "branch_mismatch",
8555
- repoRoot,
8556
- appId: binding.currentAppId,
8557
- currentBranch,
8558
- branchName,
8559
- headCommitHash,
8560
- worktreeClean: worktreeStatus.isClean,
8561
- syncStatus: null,
8562
- syncTargetCommitHash: null,
8563
- syncTargetCommitId: null,
8564
- reconcileTargetHeadCommitHash: null,
8565
- reconcileTargetHeadCommitId: null,
8566
- warnings: [],
8567
- hint: buildBranchMismatchHint({
8568
- currentBranch,
8569
- branchName
8635
+ detected.currentBranch = currentBranch;
8636
+ detected.branchName = binding.branchName ?? null;
8637
+ detected.localCommitHash = localCommitHash;
8638
+ detected.worktreeClean = worktreeStatus.isClean;
8639
+ if (!localCommitHash) {
8640
+ detected.status = "missing_head";
8641
+ detected.repoState = "binding_problem";
8642
+ detected.hint = "Failed to resolve local HEAD commit.";
8643
+ return detected;
8644
+ }
8645
+ if (!params.allowBranchMismatch && !isBoundBranchMatch(currentBranch, binding.branchName ?? null)) {
8646
+ detected.status = "branch_mismatch";
8647
+ detected.repoState = "binding_problem";
8648
+ detected.hint = buildBranchMismatchHint({ currentBranch, branchName: binding.branchName ?? null });
8649
+ return detected;
8650
+ }
8651
+ if (!params.api) {
8652
+ const [inspection, pendingFinalize] = await Promise.all([
8653
+ inspectLocalSnapshot({
8654
+ repoRoot,
8655
+ repoFingerprint: binding.repoFingerprint,
8656
+ laneId: binding.laneId,
8657
+ branchName: binding.branchName,
8658
+ persistBlobs: false
8659
+ }),
8660
+ summarizePendingFinalizeJobs({
8661
+ repoRoot,
8662
+ repoFingerprint: binding.repoFingerprint,
8663
+ currentAppId: binding.currentAppId,
8664
+ laneId: binding.laneId
8570
8665
  })
8571
- };
8572
- }
8573
- const syncResp = await params.api.syncLocalApp(binding.currentAppId, {
8574
- baseCommitHash: headCommitHash,
8575
- repoFingerprint: binding.repoFingerprint ?? void 0,
8576
- remoteUrl: binding.remoteUrl ?? void 0,
8577
- defaultBranch: binding.defaultBranch ?? void 0,
8578
- dryRun: true
8579
- });
8580
- const sync = unwrapResponseObject(syncResp, "sync result");
8581
- if (sync.status === "conflict_risk") {
8582
- return {
8583
- status: "metadata_conflict",
8584
- repoRoot,
8585
- appId: binding.currentAppId,
8586
- currentBranch,
8587
- branchName,
8588
- headCommitHash,
8589
- worktreeClean: worktreeStatus.isClean,
8590
- syncStatus: sync.status,
8591
- syncTargetCommitHash: sync.targetCommitHash,
8592
- syncTargetCommitId: sync.targetCommitId,
8593
- reconcileTargetHeadCommitHash: null,
8594
- reconcileTargetHeadCommitId: null,
8595
- warnings: sync.warnings,
8596
- hint: sync.warnings.join("\n") || "Run the command from the correct bound repository."
8597
- };
8598
- }
8599
- if (sync.status === "up_to_date" || sync.status === "ready_to_fast_forward") {
8600
- return {
8601
- status: sync.status,
8602
- repoRoot,
8603
- appId: binding.currentAppId,
8604
- currentBranch,
8605
- branchName,
8606
- headCommitHash,
8607
- worktreeClean: worktreeStatus.isClean,
8608
- syncStatus: sync.status,
8609
- syncTargetCommitHash: sync.targetCommitHash,
8610
- syncTargetCommitId: sync.targetCommitId,
8611
- reconcileTargetHeadCommitHash: null,
8612
- reconcileTargetHeadCommitId: null,
8613
- warnings: sync.warnings,
8614
- hint: null
8615
- };
8616
- }
8617
- const reconcileResp = await params.api.preflightAppReconcile(binding.currentAppId, {
8618
- localHeadCommitHash: headCommitHash,
8619
- repoFingerprint: binding.repoFingerprint ?? void 0,
8620
- remoteUrl: binding.remoteUrl ?? void 0,
8621
- defaultBranch: binding.defaultBranch ?? void 0
8622
- });
8623
- const reconcile = unwrapResponseObject(reconcileResp, "reconcile preflight");
8624
- if (reconcile.status === "metadata_conflict") {
8625
- return {
8626
- status: "metadata_conflict",
8627
- repoRoot,
8628
- appId: binding.currentAppId,
8629
- currentBranch,
8630
- branchName,
8631
- headCommitHash,
8632
- worktreeClean: worktreeStatus.isClean,
8633
- syncStatus: sync.status,
8634
- syncTargetCommitHash: sync.targetCommitHash,
8635
- syncTargetCommitId: sync.targetCommitId,
8636
- reconcileTargetHeadCommitHash: reconcile.targetHeadCommitHash,
8637
- reconcileTargetHeadCommitId: reconcile.targetHeadCommitId,
8638
- warnings: reconcile.warnings,
8639
- hint: reconcile.warnings.join("\n") || "Run the command from the correct bound repository."
8640
- };
8641
- }
8642
- if (reconcile.status === "up_to_date") {
8643
- return {
8644
- status: "up_to_date",
8645
- repoRoot,
8646
- appId: binding.currentAppId,
8647
- currentBranch,
8648
- branchName,
8649
- headCommitHash,
8650
- worktreeClean: worktreeStatus.isClean,
8651
- syncStatus: sync.status,
8652
- syncTargetCommitHash: sync.targetCommitHash,
8653
- syncTargetCommitId: sync.targetCommitId,
8654
- reconcileTargetHeadCommitHash: reconcile.targetHeadCommitHash,
8655
- reconcileTargetHeadCommitId: reconcile.targetHeadCommitId,
8656
- warnings: reconcile.warnings,
8657
- hint: null
8658
- };
8666
+ ]);
8667
+ detected.currentSnapshotHash = inspection.snapshotHash;
8668
+ detected.pendingFinalize = pendingFinalize;
8669
+ return detected;
8659
8670
  }
8660
- return {
8661
- status: "reconcile_required",
8662
- repoRoot,
8663
- appId: binding.currentAppId,
8664
- currentBranch,
8665
- branchName,
8666
- headCommitHash,
8667
- worktreeClean: worktreeStatus.isClean,
8668
- syncStatus: sync.status,
8669
- syncTargetCommitHash: sync.targetCommitHash,
8670
- syncTargetCommitId: sync.targetCommitId,
8671
- reconcileTargetHeadCommitHash: reconcile.targetHeadCommitHash,
8672
- reconcileTargetHeadCommitId: reconcile.targetHeadCommitId,
8673
- warnings: reconcile.warnings,
8674
- hint: reconcile.warnings.join("\n") || "Run `remix collab reconcile` first because the local history is no longer fast-forward compatible with the app."
8675
- };
8676
- }
8677
- var DEFAULT_ACQUIRE_TIMEOUT_MS = 15e3;
8678
- var DEFAULT_STALE_MS = 45e3;
8679
- var DEFAULT_HEARTBEAT_MS = 5e3;
8680
- var RETRY_DELAY_MS = 250;
8681
- var heldLocks = /* @__PURE__ */ new Map();
8682
- function sleep2(ms) {
8683
- return new Promise((resolve) => setTimeout(resolve, ms));
8684
- }
8685
- function createOwner(params) {
8686
- const now = (/* @__PURE__ */ new Date()).toISOString();
8687
- return {
8688
- operation: params.operation,
8689
- repoRoot: params.repoRoot,
8690
- pid: process.pid,
8691
- hostname: import_os2.default.hostname(),
8692
- startedAt: now,
8693
- heartbeatAt: now,
8694
- version: process.version,
8695
- requestId: params.requestId?.trim() || null
8696
- };
8697
- }
8698
- async function writeOwnerMetadata(ownerPath, owner) {
8699
- await import_promises16.default.writeFile(ownerPath, `${JSON.stringify(owner, null, 2)}
8700
- `, "utf8");
8701
- }
8702
- async function readOwnerMetadata(ownerPath) {
8703
8671
  try {
8704
- const raw = await import_promises16.default.readFile(ownerPath, "utf8");
8705
- const parsed = JSON.parse(raw);
8706
- if (!parsed || typeof parsed !== "object") return null;
8707
- if (!parsed.operation || !parsed.repoRoot || typeof parsed.pid !== "number" || !parsed.startedAt || !parsed.heartbeatAt) {
8708
- return null;
8709
- }
8710
- return {
8711
- operation: parsed.operation,
8712
- repoRoot: parsed.repoRoot,
8713
- pid: parsed.pid,
8714
- hostname: typeof parsed.hostname === "string" ? parsed.hostname : "unknown",
8715
- startedAt: parsed.startedAt,
8716
- heartbeatAt: parsed.heartbeatAt,
8717
- version: typeof parsed.version === "string" ? parsed.version : "unknown",
8718
- requestId: typeof parsed.requestId === "string" ? parsed.requestId : null
8672
+ const [headResp, inspection, baseline, pendingFinalize] = await Promise.all([
8673
+ params.api.getAppHead(binding.currentAppId),
8674
+ inspectLocalSnapshot({
8675
+ repoRoot,
8676
+ repoFingerprint: binding.repoFingerprint,
8677
+ laneId: binding.laneId,
8678
+ branchName: binding.branchName,
8679
+ persistBlobs: false
8680
+ }),
8681
+ readLocalBaseline({
8682
+ repoFingerprint: binding.repoFingerprint,
8683
+ laneId: binding.laneId,
8684
+ repoRoot
8685
+ }),
8686
+ summarizePendingFinalizeJobs({
8687
+ repoRoot,
8688
+ repoFingerprint: binding.repoFingerprint,
8689
+ currentAppId: binding.currentAppId,
8690
+ laneId: binding.laneId
8691
+ })
8692
+ ]);
8693
+ const appHead = unwrapResponseObject(headResp, "app head");
8694
+ detected.currentServerHeadHash = appHead.headCommitHash;
8695
+ detected.currentServerHeadCommitId = appHead.headCommitId;
8696
+ detected.currentSnapshotHash = inspection.snapshotHash;
8697
+ detected.pendingFinalize = pendingFinalize;
8698
+ detected.baseline = {
8699
+ lastSnapshotId: baseline?.lastSnapshotId ?? null,
8700
+ lastSnapshotHash: baseline?.lastSnapshotHash ?? null,
8701
+ lastServerHeadHash: baseline?.lastServerHeadHash ?? null,
8702
+ lastSeenLocalCommitHash: baseline?.lastSeenLocalCommitHash ?? null
8719
8703
  };
8720
- } catch {
8721
- return null;
8722
- }
8723
- }
8724
- async function isProcessAlive(owner) {
8725
- if (!owner) return null;
8726
- if (owner.hostname !== import_os2.default.hostname()) return null;
8727
- try {
8728
- process.kill(owner.pid, 0);
8729
- return true;
8704
+ if (!baseline?.lastSnapshotHash || !baseline.lastServerHeadHash) {
8705
+ if (detected.worktreeClean && localCommitHash && localCommitHash !== appHead.headCommitHash) {
8706
+ try {
8707
+ const bootstrapResp = await params.api.getAppDelta(binding.currentAppId, {
8708
+ baseHeadHash: localCommitHash,
8709
+ targetHeadHash: appHead.headCommitHash,
8710
+ repoFingerprint: binding.repoFingerprint ?? void 0,
8711
+ remoteUrl: binding.remoteUrl ?? void 0,
8712
+ defaultBranch: binding.defaultBranch ?? void 0
8713
+ });
8714
+ const bootstrapDelta = unwrapResponseObject(bootstrapResp, "app delta");
8715
+ detected.metadataWarnings = Array.from(/* @__PURE__ */ new Set([...detected.metadataWarnings, ...bootstrapDelta.warnings]));
8716
+ detected.warnings.push(...bootstrapDelta.warnings);
8717
+ if (bootstrapDelta.status === "conflict_risk") {
8718
+ detected.status = "metadata_conflict";
8719
+ detected.repoState = "metadata_conflict";
8720
+ detected.hint = bootstrapDelta.warnings.join("\n") || "Run the command from the correct bound repository.";
8721
+ return detected;
8722
+ }
8723
+ if (bootstrapDelta.status === "delta_ready" || bootstrapDelta.status === "up_to_date") {
8724
+ detected.repoState = "server_only_changed";
8725
+ detected.hint = "This checkout has not stored a local Remix baseline yet, but its current Git HEAD is already known to Remix. Pull the server delta locally to create the first baseline for this checkout.";
8726
+ return detected;
8727
+ }
8728
+ } catch {
8729
+ }
8730
+ }
8731
+ detected.repoState = "external_local_base_changed";
8732
+ detected.hint = "No local Remix baseline exists for this lane yet. Run `remix collab re-anchor` to anchor this checkout.";
8733
+ return detected;
8734
+ }
8735
+ const localHeadMovedSinceBaseline = Boolean(baseline.lastSeenLocalCommitHash) && localCommitHash !== baseline.lastSeenLocalCommitHash;
8736
+ if (localHeadMovedSinceBaseline) {
8737
+ detected.warnings.push(
8738
+ "Local Git HEAD changed since the last Remix baseline. Remix will use the current workspace snapshot to detect divergence."
8739
+ );
8740
+ }
8741
+ const metadataBaseHeadHash = baseline.lastServerHeadHash || appHead.headCommitHash;
8742
+ const metadataResp = await params.api.getAppDelta(binding.currentAppId, {
8743
+ baseHeadHash: metadataBaseHeadHash,
8744
+ targetHeadHash: metadataBaseHeadHash,
8745
+ repoFingerprint: binding.repoFingerprint ?? void 0,
8746
+ remoteUrl: binding.remoteUrl ?? void 0,
8747
+ defaultBranch: binding.defaultBranch ?? void 0
8748
+ });
8749
+ const metadataCheck = unwrapResponseObject(metadataResp, "app delta metadata");
8750
+ detected.metadataWarnings = metadataCheck.warnings;
8751
+ detected.warnings.push(...metadataCheck.warnings);
8752
+ if (metadataCheck.status === "conflict_risk") {
8753
+ detected.status = "metadata_conflict";
8754
+ detected.repoState = "metadata_conflict";
8755
+ detected.hint = metadataCheck.warnings.join("\n") || "Run the command from the correct bound repository.";
8756
+ return detected;
8757
+ }
8758
+ const localChanged = inspection.snapshotHash !== baseline.lastSnapshotHash;
8759
+ const serverChanged = appHead.headCommitHash !== baseline.lastServerHeadHash;
8760
+ if (!localChanged && !serverChanged) {
8761
+ detected.repoState = "idle";
8762
+ return detected;
8763
+ }
8764
+ if (localChanged && !serverChanged) {
8765
+ detected.repoState = "local_only_changed";
8766
+ return detected;
8767
+ }
8768
+ if (!localChanged && serverChanged) {
8769
+ detected.repoState = "server_only_changed";
8770
+ detected.hint = "The server lane advanced since the last agreed baseline. Pull the server delta locally before continuing.";
8771
+ return detected;
8772
+ }
8773
+ detected.repoState = "both_changed";
8774
+ detected.hint = "Both the local workspace and the server lane changed since the last agreed baseline. Replay or reconcile is required before normal recording continues.";
8775
+ return detected;
8730
8776
  } catch (error) {
8731
- if (error?.code === "EPERM") return true;
8732
- if (error?.code === "ESRCH") return false;
8733
- return null;
8777
+ detected.status = "remote_error";
8778
+ detected.hint = formatCliErrorDetail(error) ?? "Failed to detect the current Remix repo state.";
8779
+ return detected;
8734
8780
  }
8735
8781
  }
8736
- async function getLastKnownUpdateMs(lockDir, ownerPath, owner) {
8737
- const heartbeatMs = owner ? Date.parse(owner.heartbeatAt) : Number.NaN;
8738
- if (Number.isFinite(heartbeatMs)) return heartbeatMs;
8739
- const startedMs = owner ? Date.parse(owner.startedAt) : Number.NaN;
8740
- if (Number.isFinite(startedMs)) return startedMs;
8741
- const stat = await import_promises16.default.stat(ownerPath).catch(() => null);
8742
- if (stat) return stat.mtimeMs;
8743
- const dirStat = await import_promises16.default.stat(lockDir).catch(() => null);
8744
- if (dirStat) return dirStat.mtimeMs;
8745
- return 0;
8782
+ var FINALIZE_RETRY_BASE_DELAY_MS = 15e3;
8783
+ var FINALIZE_RETRY_MAX_DELAY_MS = 5 * 60 * 1e3;
8784
+ function readMetadataString(job, key) {
8785
+ const value = job.metadata[key];
8786
+ return typeof value === "string" && value.trim() ? value.trim() : null;
8746
8787
  }
8747
- async function ensureLockDir(lockDir) {
8748
- await import_promises16.default.mkdir(import_path5.default.dirname(lockDir), { recursive: true });
8788
+ function readMetadataActor(job) {
8789
+ const actor = job.metadata.actor;
8790
+ return actor && typeof actor === "object" ? actor : void 0;
8749
8791
  }
8750
- async function tryAcquireLock(lockDir, ownerPath, owner) {
8751
- try {
8752
- await ensureLockDir(lockDir);
8753
- await import_promises16.default.mkdir(lockDir);
8754
- try {
8755
- await writeOwnerMetadata(ownerPath, owner);
8756
- } catch (error) {
8757
- await import_promises16.default.rm(lockDir, { recursive: true, force: true }).catch(() => void 0);
8758
- throw error;
8759
- }
8760
- return true;
8761
- } catch (error) {
8762
- if (error?.code === "EEXIST") return false;
8763
- throw error;
8764
- }
8792
+ function buildNextRetryAt(retryCount) {
8793
+ const exponent = Math.max(0, retryCount - 1);
8794
+ const delayMs = Math.min(FINALIZE_RETRY_BASE_DELAY_MS * 2 ** exponent, FINALIZE_RETRY_MAX_DELAY_MS);
8795
+ return new Date(Date.now() + delayMs).toISOString();
8765
8796
  }
8766
- function formatLockHint(params) {
8767
- const lines = [
8768
- params.observedHeldLock ? `Observed lock state: ${REMIX_ERROR_CODES.REPO_LOCK_HELD}.` : null,
8769
- params.owner ? `Active operation: ${params.owner.operation}` : "Active operation: unknown",
8770
- params.owner ? `Repo root: ${params.owner.repoRoot}` : null,
8771
- params.owner ? `Owner: pid=${params.owner.pid} host=${params.owner.hostname}` : null,
8772
- params.owner ? `Started at: ${params.owner.startedAt}` : null,
8773
- params.owner ? `Heartbeat at: ${params.owner.heartbeatAt}` : null,
8774
- `Waited ${params.waitedMs}ms for the repo mutation lock.`,
8775
- `Stale lock threshold: ${params.staleMs}ms.`,
8776
- "Retry after the active operation finishes. If the process crashed, wait for stale lock recovery or remove the stale lock manually if necessary."
8777
- ];
8778
- return lines.filter(Boolean).join("\n");
8797
+ function buildFinalizeCliError(params) {
8798
+ const error = new RemixError(params.message, {
8799
+ exitCode: params.exitCode,
8800
+ hint: params.hint
8801
+ });
8802
+ error.finalizeDisposition = params.disposition;
8803
+ error.finalizeReason = params.reason;
8804
+ return error;
8779
8805
  }
8780
- function formatOwnerSummary(owner) {
8781
- if (!owner) {
8782
- return "unknown owner";
8783
- }
8784
- return `operation=${owner.operation} pid=${owner.pid} host=${owner.hostname} startedAt=${owner.startedAt} heartbeatAt=${owner.heartbeatAt}`;
8806
+ function classifyFinalizeError(error) {
8807
+ const tagged = error;
8808
+ return {
8809
+ disposition: tagged.finalizeDisposition ?? "retryable",
8810
+ reason: tagged.finalizeReason ?? "unknown",
8811
+ message: error instanceof Error ? error.message : String(error)
8812
+ };
8785
8813
  }
8786
- function buildStaleRecoveryNotice(owner) {
8814
+ function buildWorkspaceMetadata(params) {
8787
8815
  return {
8788
- code: REMIX_ERROR_CODES.REPO_LOCK_STALE_RECOVERED,
8789
- owner,
8790
- message: `[${REMIX_ERROR_CODES.REPO_LOCK_STALE_RECOVERED}] Recovered a stale Remix repo mutation lock (${formatOwnerSummary(owner)}).`
8816
+ branch: params.branchName,
8817
+ repoRoot: params.repoRoot,
8818
+ remoteUrl: params.remoteUrl,
8819
+ defaultBranch: params.defaultBranch,
8820
+ recordingMode: "boundary_delta",
8821
+ baselineSnapshotId: params.baselineSnapshotId,
8822
+ currentSnapshotId: params.currentSnapshotId,
8823
+ baselineServerHeadHash: params.baselineServerHeadHash,
8824
+ currentSnapshotHash: params.currentSnapshotHash,
8825
+ localCommitHash: params.localCommitHash,
8826
+ repoStateAtCapture: params.repoState,
8827
+ replayedFromBaseHash: params.replayedFromBaseHash ?? null
8791
8828
  };
8792
8829
  }
8793
- async function acquirePhysicalLock(lockDir, ownerPath, owner, options) {
8794
- const startedAt = Date.now();
8795
- const notices = [];
8796
- let observedHeldLock = false;
8797
- while (Date.now() - startedAt < options.acquireTimeoutMs) {
8798
- if (await tryAcquireLock(lockDir, ownerPath, owner)) return notices;
8799
- const currentOwner2 = await readOwnerMetadata(ownerPath);
8800
- observedHeldLock = true;
8801
- const lastUpdateMs = await getLastKnownUpdateMs(lockDir, ownerPath, currentOwner2);
8802
- const ageMs = Math.max(0, Date.now() - lastUpdateMs);
8803
- const alive = await isProcessAlive(currentOwner2);
8804
- if (ageMs >= options.staleMs && alive !== true) {
8805
- notices.push(buildStaleRecoveryNotice(currentOwner2));
8806
- await import_promises16.default.rm(lockDir, { recursive: true, force: true }).catch(() => void 0);
8807
- continue;
8830
+ async function processClaimedPendingFinalizeJob(params) {
8831
+ const job = params.job;
8832
+ try {
8833
+ const [snapshot, baseline, appHeadResp] = await Promise.all([
8834
+ readLocalSnapshot(job.currentSnapshotId),
8835
+ readLocalBaseline({
8836
+ repoFingerprint: job.repoFingerprint,
8837
+ laneId: job.laneId,
8838
+ repoRoot: job.repoRoot
8839
+ }),
8840
+ params.api.getAppHead(job.currentAppId)
8841
+ ]);
8842
+ if (!snapshot) {
8843
+ throw buildFinalizeCliError({
8844
+ message: "Captured snapshot is missing from the local snapshot store.",
8845
+ exitCode: 1,
8846
+ disposition: "terminal",
8847
+ reason: "snapshot_missing"
8848
+ });
8808
8849
  }
8809
- await sleep2(RETRY_DELAY_MS);
8810
- }
8811
- const currentOwner = await readOwnerMetadata(ownerPath);
8812
- throw new RemixError("Repository is busy with another Remix mutation.", {
8813
- code: REMIX_ERROR_CODES.REPO_LOCK_TIMEOUT,
8814
- exitCode: 2,
8815
- hint: formatLockHint({
8816
- owner: currentOwner,
8817
- waitedMs: Date.now() - startedAt,
8818
- staleMs: options.staleMs,
8819
- observedHeldLock
8820
- })
8821
- });
8822
- }
8823
- function startHeartbeat(lockDir, ownerPath, owner, heartbeatMs) {
8824
- return setInterval(() => {
8825
- const nextOwner = {
8826
- ...owner,
8827
- heartbeatAt: (/* @__PURE__ */ new Date()).toISOString()
8828
- };
8829
- owner.heartbeatAt = nextOwner.heartbeatAt;
8830
- void writeOwnerMetadata(ownerPath, nextOwner).catch(() => void 0);
8831
- void import_promises16.default.utimes(lockDir, /* @__PURE__ */ new Date(), /* @__PURE__ */ new Date()).catch(() => void 0);
8832
- }, heartbeatMs);
8833
- }
8834
- async function releaseReentrantLock(lockDir) {
8835
- const held = heldLocks.get(lockDir);
8836
- if (!held) return;
8837
- held.count -= 1;
8838
- if (held.count > 0) return;
8839
- clearInterval(held.heartbeatTimer);
8840
- heldLocks.delete(lockDir);
8841
- await import_promises16.default.rm(lockDir, { recursive: true, force: true }).catch(() => void 0);
8842
- }
8843
- async function withRepoMutationLock(options, fn) {
8844
- const repoRoot = await findGitRoot(options.cwd);
8845
- const gitCommonDir = await getGitCommonDir(repoRoot);
8846
- const lockDir = import_path5.default.join(gitCommonDir, "remix", "locks", "repo-mutation.lock");
8847
- const owner = createOwner({
8848
- operation: options.operation,
8849
- repoRoot,
8850
- requestId: options.requestId
8851
- });
8852
- const heartbeatMs = options.heartbeatMs ?? DEFAULT_HEARTBEAT_MS;
8853
- const acquireTimeoutMs = options.acquireTimeoutMs ?? DEFAULT_ACQUIRE_TIMEOUT_MS;
8854
- const staleMs = options.staleMs ?? DEFAULT_STALE_MS;
8855
- const existing = heldLocks.get(lockDir);
8856
- let notices = [];
8857
- if (!existing) {
8858
- notices = await acquirePhysicalLock(lockDir, import_path5.default.join(lockDir, "owner.json"), owner, {
8859
- acquireTimeoutMs,
8860
- staleMs
8861
- });
8862
- const ownerPath = import_path5.default.join(lockDir, "owner.json");
8863
- heldLocks.set(lockDir, {
8864
- count: 1,
8865
- lockDir,
8866
- ownerPath,
8867
- owner,
8868
- heartbeatTimer: startHeartbeat(lockDir, ownerPath, owner, heartbeatMs)
8850
+ if (!baseline) {
8851
+ throw buildFinalizeCliError({
8852
+ message: "Local baseline is missing for this queued finalize job.",
8853
+ exitCode: 2,
8854
+ hint: "Run `remix collab re-anchor` to anchor the repository again.",
8855
+ disposition: "terminal",
8856
+ reason: "baseline_missing"
8857
+ });
8858
+ }
8859
+ if (baseline.lastSnapshotId !== job.baselineSnapshotId || baseline.lastServerHeadHash !== job.baselineServerHeadHash) {
8860
+ throw buildFinalizeCliError({
8861
+ message: "Finalize queue baseline drifted before this job was processed.",
8862
+ exitCode: 1,
8863
+ hint: "Process queued finalize jobs in capture order, or re-anchor the repository before retrying.",
8864
+ disposition: "terminal",
8865
+ reason: "baseline_drifted"
8866
+ });
8867
+ }
8868
+ const appHead = unwrapResponseObject(appHeadResp, "app head");
8869
+ const remoteUrl = readMetadataString(job, "remoteUrl");
8870
+ const defaultBranch = readMetadataString(job, "defaultBranch");
8871
+ const repoState = readMetadataString(job, "repoState");
8872
+ const actor = readMetadataActor(job);
8873
+ const diffResult = await diffLocalSnapshots({
8874
+ baseSnapshotId: job.baselineSnapshotId,
8875
+ targetSnapshotId: job.currentSnapshotId
8876
+ });
8877
+ if (!diffResult.diff.trim()) {
8878
+ if (appHead.headCommitHash !== job.baselineServerHeadHash) {
8879
+ throw buildFinalizeCliError({
8880
+ message: "Server lane changed before a no-diff turn could be recorded.",
8881
+ exitCode: 2,
8882
+ hint: "Pull the server changes locally before recording another no-diff turn.",
8883
+ disposition: "terminal",
8884
+ reason: "server_lane_changed"
8885
+ });
8886
+ }
8887
+ const collabTurnResp = await params.api.createCollabTurn(job.currentAppId, {
8888
+ threadId: job.threadId ?? void 0,
8889
+ collabLaneId: job.laneId ?? void 0,
8890
+ prompt: job.prompt,
8891
+ assistantResponse: job.assistantResponse,
8892
+ actor,
8893
+ workspaceMetadata: buildWorkspaceMetadata({
8894
+ repoRoot: job.repoRoot,
8895
+ branchName: job.branchName,
8896
+ remoteUrl,
8897
+ defaultBranch,
8898
+ baselineSnapshotId: job.baselineSnapshotId,
8899
+ currentSnapshotId: job.currentSnapshotId,
8900
+ baselineServerHeadHash: job.baselineServerHeadHash,
8901
+ currentSnapshotHash: snapshot.snapshotHash,
8902
+ localCommitHash: snapshot.localCommitHash,
8903
+ repoState
8904
+ }),
8905
+ idempotencyKey: job.idempotencyKey ?? void 0
8906
+ });
8907
+ const collabTurn = unwrapResponseObject(collabTurnResp, "collab turn");
8908
+ await writeLocalBaseline({
8909
+ repoRoot: job.repoRoot,
8910
+ repoFingerprint: job.repoFingerprint,
8911
+ laneId: job.laneId,
8912
+ currentAppId: job.currentAppId,
8913
+ branchName: job.branchName,
8914
+ lastSnapshotId: snapshot.id,
8915
+ lastSnapshotHash: snapshot.snapshotHash,
8916
+ lastServerHeadHash: appHead.headCommitHash,
8917
+ lastSeenLocalCommitHash: snapshot.localCommitHash
8918
+ });
8919
+ await updatePendingFinalizeJob(job.id, {
8920
+ status: "completed",
8921
+ metadata: { collabTurnId: collabTurn.id }
8922
+ });
8923
+ return {
8924
+ mode: "no_diff_turn",
8925
+ idempotencyKey: job.idempotencyKey ?? "",
8926
+ queued: false,
8927
+ jobId: job.id,
8928
+ repoState,
8929
+ changeStep: null,
8930
+ collabTurn,
8931
+ autoSync: null,
8932
+ warnings: []
8933
+ };
8934
+ }
8935
+ let submissionDiff = diffResult.diff;
8936
+ let submissionBaseHeadHash = job.baselineServerHeadHash;
8937
+ let replayedFromBaseHash = null;
8938
+ if (!submissionBaseHeadHash) {
8939
+ throw buildFinalizeCliError({
8940
+ message: "Baseline server head is missing for this finalize job.",
8941
+ exitCode: 1,
8942
+ disposition: "terminal",
8943
+ reason: "baseline_server_head_missing"
8944
+ });
8945
+ }
8946
+ if (appHead.headCommitHash !== submissionBaseHeadHash) {
8947
+ const replayResp = await params.api.startChangeStepReplay(job.currentAppId, {
8948
+ prompt: job.prompt,
8949
+ assistantResponse: job.assistantResponse,
8950
+ diff: diffResult.diff,
8951
+ baseCommitHash: submissionBaseHeadHash,
8952
+ targetHeadCommitHash: appHead.headCommitHash,
8953
+ expectedPaths: diffResult.changedPaths,
8954
+ actor,
8955
+ workspaceMetadata: buildWorkspaceMetadata({
8956
+ repoRoot: job.repoRoot,
8957
+ branchName: job.branchName,
8958
+ remoteUrl,
8959
+ defaultBranch,
8960
+ baselineSnapshotId: job.baselineSnapshotId,
8961
+ currentSnapshotId: job.currentSnapshotId,
8962
+ baselineServerHeadHash: job.baselineServerHeadHash,
8963
+ currentSnapshotHash: snapshot.snapshotHash,
8964
+ localCommitHash: snapshot.localCommitHash,
8965
+ repoState
8966
+ }),
8967
+ idempotencyKey: buildDeterministicIdempotencyKey({
8968
+ kind: "collab_finalize_turn_replay_v1",
8969
+ appId: job.currentAppId,
8970
+ baseCommitHash: submissionBaseHeadHash,
8971
+ targetHeadCommitHash: appHead.headCommitHash,
8972
+ currentSnapshotId: job.currentSnapshotId,
8973
+ diffSha256: diffResult.diffSha256
8974
+ })
8975
+ });
8976
+ const replayStart = unwrapResponseObject(replayResp, "change step replay");
8977
+ const replay = await pollChangeStepReplay(params.api, job.currentAppId, String(replayStart.id));
8978
+ const replayDiffResp = await params.api.getChangeStepReplayDiff(job.currentAppId, replay.id);
8979
+ const replayDiff = unwrapResponseObject(replayDiffResp, "change step replay diff");
8980
+ submissionDiff = replayDiff.diff;
8981
+ replayedFromBaseHash = submissionBaseHeadHash;
8982
+ submissionBaseHeadHash = appHead.headCommitHash;
8983
+ }
8984
+ const changeStepResp = await params.api.createChangeStep(job.currentAppId, {
8985
+ threadId: job.threadId ?? void 0,
8986
+ collabLaneId: job.laneId ?? void 0,
8987
+ prompt: job.prompt,
8988
+ assistantResponse: job.assistantResponse,
8989
+ diff: submissionDiff,
8990
+ baseCommitHash: submissionBaseHeadHash,
8991
+ headCommitHash: submissionBaseHeadHash,
8992
+ changedFilesCount: diffResult.stats.changedFilesCount,
8993
+ insertions: diffResult.stats.insertions,
8994
+ deletions: diffResult.stats.deletions,
8995
+ actor,
8996
+ workspaceMetadata: buildWorkspaceMetadata({
8997
+ repoRoot: job.repoRoot,
8998
+ branchName: job.branchName,
8999
+ remoteUrl,
9000
+ defaultBranch,
9001
+ baselineSnapshotId: job.baselineSnapshotId,
9002
+ currentSnapshotId: job.currentSnapshotId,
9003
+ baselineServerHeadHash: job.baselineServerHeadHash,
9004
+ currentSnapshotHash: snapshot.snapshotHash,
9005
+ localCommitHash: snapshot.localCommitHash,
9006
+ repoState,
9007
+ replayedFromBaseHash
9008
+ }),
9009
+ idempotencyKey: job.idempotencyKey ?? void 0
9010
+ });
9011
+ const createdStep = unwrapResponseObject(changeStepResp, "change step");
9012
+ const changeStep = await pollChangeStep(params.api, job.currentAppId, String(createdStep.id));
9013
+ const nextHeadResp = await params.api.getAppHead(job.currentAppId);
9014
+ const nextHead = unwrapResponseObject(nextHeadResp, "app head");
9015
+ await writeLocalBaseline({
9016
+ repoRoot: job.repoRoot,
9017
+ repoFingerprint: job.repoFingerprint,
9018
+ laneId: job.laneId,
9019
+ currentAppId: job.currentAppId,
9020
+ branchName: job.branchName,
9021
+ lastSnapshotId: snapshot.id,
9022
+ lastSnapshotHash: snapshot.snapshotHash,
9023
+ lastServerHeadHash: nextHead.headCommitHash,
9024
+ lastSeenLocalCommitHash: snapshot.localCommitHash
9025
+ });
9026
+ await updatePendingFinalizeJob(job.id, {
9027
+ status: "completed",
9028
+ metadata: { changeStepId: String(changeStep.id ?? "") }
8869
9029
  });
8870
- } else {
8871
- existing.count += 1;
8872
- }
8873
- try {
8874
- return await fn({
8875
- repoRoot,
8876
- gitCommonDir,
8877
- lockDir,
8878
- notices,
8879
- warnings: notices.map((notice) => notice.message)
9030
+ return {
9031
+ mode: "changed_turn",
9032
+ idempotencyKey: job.idempotencyKey ?? "",
9033
+ queued: false,
9034
+ jobId: job.id,
9035
+ repoState,
9036
+ changeStep,
9037
+ collabTurn: null,
9038
+ autoSync: null,
9039
+ warnings: []
9040
+ };
9041
+ } catch (error) {
9042
+ const classified = classifyFinalizeError(error);
9043
+ await updatePendingFinalizeJob(job.id, {
9044
+ status: classified.disposition === "terminal" ? "failed" : "queued",
9045
+ error: classified.message,
9046
+ nextRetryAt: classified.disposition === "terminal" ? null : buildNextRetryAt(job.retryCount),
9047
+ metadata: {
9048
+ failureDisposition: classified.disposition,
9049
+ failureReason: classified.reason
9050
+ }
8880
9051
  });
9052
+ throw error;
8881
9053
  } finally {
8882
- await releaseReentrantLock(lockDir);
9054
+ await params.release();
8883
9055
  }
8884
9056
  }
8885
- async function collabSync(params) {
8886
- const repoRoot = await findGitRoot(params.cwd);
8887
- const binding = await ensureActiveLaneBinding({
8888
- repoRoot,
8889
- api: params.api,
8890
- operation: "`remix collab sync`"
8891
- });
8892
- if (!binding) {
8893
- throw new RemixError("Repository is not bound to Remix.", {
8894
- exitCode: 2,
8895
- hint: "Run `remix collab init` first."
8896
- });
8897
- }
8898
- await ensureCleanWorktree(repoRoot);
8899
- const branch = await requireCurrentBranch(repoRoot);
8900
- assertBoundBranchMatch({
8901
- currentBranch: branch,
8902
- branchName: binding.branchName,
8903
- allowBranchMismatch: params.allowBranchMismatch,
8904
- operation: "`remix collab sync`"
8905
- });
8906
- const headCommitHash = await getHeadCommitHash(repoRoot);
8907
- const repoSnapshot = await captureRepoSnapshot(repoRoot);
8908
- if (!headCommitHash) {
8909
- throw new RemixError("Failed to resolve local HEAD commit.", { exitCode: 1 });
8910
- }
8911
- const resp = await params.api.syncLocalApp(binding.currentAppId, {
8912
- baseCommitHash: headCommitHash,
8913
- repoFingerprint: binding.repoFingerprint ?? void 0,
8914
- remoteUrl: binding.remoteUrl ?? void 0,
8915
- defaultBranch: binding.defaultBranch ?? void 0,
8916
- dryRun: params.dryRun
9057
+ async function enqueueCapturedFinalizeTurn(params) {
9058
+ return enqueuePendingFinalizeJob({
9059
+ status: "queued",
9060
+ repoRoot: params.repoRoot,
9061
+ repoFingerprint: params.repoFingerprint,
9062
+ currentAppId: params.currentAppId,
9063
+ laneId: params.laneId,
9064
+ threadId: params.threadId,
9065
+ branchName: params.branchName,
9066
+ prompt: params.prompt,
9067
+ assistantResponse: params.assistantResponse,
9068
+ baselineSnapshotId: params.baselineSnapshotId,
9069
+ baselineServerHeadHash: params.baselineServerHeadHash,
9070
+ currentSnapshotId: params.currentSnapshotId,
9071
+ idempotencyKey: params.idempotencyKey,
9072
+ error: null,
9073
+ retryCount: 0,
9074
+ lastAttemptAt: null,
9075
+ nextRetryAt: null,
9076
+ metadata: params.metadata ?? {}
8917
9077
  });
8918
- const sync = unwrapResponseObject(resp, "sync result");
8919
- if (sync.status === "conflict_risk") {
8920
- throw new RemixError("Local repository metadata conflicts with the bound Remix app.", {
8921
- exitCode: 2,
8922
- hint: sync.warnings.join("\n") || "Run the command from the correct bound repository."
8923
- });
8924
- }
8925
- if (sync.status === "base_unknown") {
8926
- throw new RemixError("Local repository cannot be fast-forward synced.", {
8927
- exitCode: 2,
8928
- hint: "Your local HEAD is not on the app sandbox history. Reconcile the repository manually before syncing."
8929
- });
8930
- }
8931
- if (sync.status === "up_to_date") {
8932
- return {
8933
- status: sync.status,
8934
- branch,
8935
- repoRoot,
8936
- baseCommitHash: sync.baseCommitHash,
8937
- targetCommitHash: sync.targetCommitHash,
8938
- targetCommitId: sync.targetCommitId,
8939
- stats: sync.stats,
8940
- localCommitHash: headCommitHash,
8941
- applied: false,
8942
- dryRun: params.dryRun
8943
- };
8944
- }
8945
- const previewResult = {
8946
- status: sync.status,
8947
- branch,
8948
- repoRoot,
8949
- baseCommitHash: sync.baseCommitHash,
8950
- targetCommitHash: sync.targetCommitHash,
8951
- targetCommitId: sync.targetCommitId,
8952
- stats: sync.stats,
8953
- bundleRef: sync.bundleRef,
8954
- bundleSizeBytes: sync.bundleSizeBytes,
8955
- localCommitHash: headCommitHash,
8956
- applied: false,
8957
- dryRun: params.dryRun
8958
- };
8959
- if (params.dryRun) {
8960
- return previewResult;
8961
- }
8962
- if (!sync.bundleBase64 || !sync.bundleRef) {
8963
- throw new RemixError("Sync bundle payload is missing.", { exitCode: 1 });
8964
- }
8965
- const bundleBase64 = sync.bundleBase64;
8966
- const bundleRef = sync.bundleRef;
8967
- return withRepoMutationLock(
8968
- {
8969
- cwd: repoRoot,
8970
- operation: "collabSync"
8971
- },
8972
- async ({ repoRoot: lockedRepoRoot, warnings }) => {
8973
- await assertRepoSnapshotUnchanged(lockedRepoRoot, repoSnapshot, {
8974
- operation: "`remix collab sync`",
8975
- recoveryHint: "The repository changed after sync was prepared. Review the local changes and rerun `remix collab sync`."
8976
- });
8977
- await ensureCleanWorktree(lockedRepoRoot);
8978
- const lockedBranch = await requireCurrentBranch(lockedRepoRoot);
8979
- assertBoundBranchMatch({
8980
- currentBranch: lockedBranch,
8981
- branchName: binding.branchName,
8982
- allowBranchMismatch: params.allowBranchMismatch,
8983
- operation: "`remix collab sync`"
9078
+ }
9079
+ async function drainPendingFinalizeQueue(params) {
9080
+ await prunePendingFinalizeJobs();
9081
+ const jobs = await listPendingFinalizeJobs();
9082
+ const results = [];
9083
+ for (const job of jobs) {
9084
+ const claimed = await claimPendingFinalizeJob(job.id);
9085
+ if (!claimed) continue;
9086
+ try {
9087
+ const result = await processClaimedPendingFinalizeJob({
9088
+ api: params.api,
9089
+ job: claimed.job,
9090
+ release: claimed.release
8984
9091
  });
8985
- const tempDir = await import_promises17.default.mkdtemp(import_path6.default.join(import_os3.default.tmpdir(), "remix-sync-"));
8986
- const bundlePath = import_path6.default.join(tempDir, "sync-local.bundle");
8987
- try {
8988
- await import_promises17.default.writeFile(bundlePath, Buffer.from(bundleBase64, "base64"));
8989
- await importGitBundle(lockedRepoRoot, bundlePath, bundleRef);
8990
- await ensureCommitExists(lockedRepoRoot, sync.targetCommitHash);
8991
- const localCommitHash = await fastForwardToCommit(lockedRepoRoot, sync.targetCommitHash);
8992
- return {
8993
- ...previewResult,
8994
- localCommitHash,
8995
- applied: true,
8996
- dryRun: false,
8997
- ...warnings.length > 0 ? { warnings } : {}
8998
- };
8999
- } finally {
9000
- await import_promises17.default.rm(tempDir, { recursive: true, force: true });
9001
- }
9092
+ results.push(result);
9093
+ await removePendingFinalizeJob(job.id);
9094
+ } catch {
9002
9095
  }
9003
- );
9004
- }
9005
- function assertSupportedRecordingPreflight(preflight) {
9006
- if (preflight.status === "not_bound") {
9007
- throw new RemixError("Repository is not bound to Remix.", {
9008
- exitCode: 2,
9009
- hint: preflight.hint
9010
- });
9011
- }
9012
- if (preflight.status === "branch_binding_missing") {
9013
- throw new RemixError("Current branch is not yet bound to a Remix lane.", {
9014
- exitCode: 2,
9015
- hint: preflight.hint
9016
- });
9017
- }
9018
- if (preflight.status === "family_ambiguous") {
9019
- throw new RemixError("Multiple canonical Remix families match this repository.", {
9020
- exitCode: 2,
9021
- hint: preflight.hint
9022
- });
9023
- }
9024
- if (preflight.status === "not_git_repo") {
9025
- throw new RemixError(preflight.hint || "Not inside a git repository.", {
9026
- exitCode: 2,
9027
- hint: preflight.hint
9028
- });
9029
- }
9030
- if (preflight.status === "missing_head") {
9031
- throw new RemixError("Failed to resolve local HEAD commit.", {
9032
- exitCode: 1,
9033
- hint: preflight.hint
9034
- });
9035
- }
9036
- if (preflight.status === "branch_mismatch") {
9037
- assertBoundBranchMatch({
9038
- currentBranch: preflight.currentBranch,
9039
- branchName: preflight.branchName,
9040
- allowBranchMismatch: false,
9041
- operation: "`remix collab add`"
9042
- });
9043
- }
9044
- if (preflight.status === "metadata_conflict") {
9045
- throw new RemixError("Local repository metadata conflicts with the bound Remix app.", {
9046
- exitCode: 2,
9047
- hint: preflight.hint
9048
- });
9049
- }
9050
- if (preflight.status === "reconcile_required") {
9051
- throw new RemixError("Local repository cannot be fast-forward synced.", {
9052
- exitCode: 2,
9053
- hint: preflight.hint
9054
- });
9055
9096
  }
9097
+ return results;
9056
9098
  }
9057
- async function collabAdd(params) {
9099
+ function collectWarnings(value) {
9100
+ if (!Array.isArray(value)) return [];
9101
+ return value.filter((entry) => typeof entry === "string" && entry.trim().length > 0);
9102
+ }
9103
+ var FINALIZE_QUEUED_WARNING = "Queued only: the local Remix turn was captured, but no remote change step or collab turn exists yet. Drain or await finalize before merge-related flows.";
9104
+ async function collabFinalizeTurn(params) {
9058
9105
  const repoRoot = await findGitRoot(params.cwd);
9059
9106
  const binding = await ensureActiveLaneBinding({
9060
9107
  repoRoot,
9061
9108
  api: params.api,
9062
- operation: "`remix collab add`"
9109
+ operation: "`remix collab finalize-turn`"
9063
9110
  });
9064
9111
  if (!binding) {
9065
9112
  throw new RemixError("Repository is not bound to Remix.", {
@@ -9068,385 +9115,114 @@ async function collabAdd(params) {
9068
9115
  });
9069
9116
  }
9070
9117
  const prompt = params.prompt.trim();
9118
+ const assistantResponse = params.assistantResponse.trim();
9071
9119
  if (!prompt) throw new RemixError("Prompt is required.", { exitCode: 2 });
9072
- const assistantResponse = params.assistantResponse?.trim() || null;
9073
- const attachWarnings = (value, warnings) => warnings.length > 0 ? { ...value, warnings } : value;
9074
- const diffSource = params.diffSource ?? (params.diff ? "external" : "worktree");
9075
- const autoSyncEnabled = params.sync !== false;
9076
- const run = async (lockWarnings = []) => {
9077
- const preflight = await collabRecordingPreflight({
9078
- api: params.api,
9079
- cwd: repoRoot,
9080
- allowBranchMismatch: params.allowBranchMismatch
9081
- });
9082
- assertSupportedRecordingPreflight(preflight);
9083
- const branch = preflight.currentBranch;
9084
- assertBoundBranchMatch({
9085
- currentBranch: branch,
9086
- branchName: binding.branchName,
9087
- allowBranchMismatch: params.allowBranchMismatch,
9088
- operation: "`remix collab add`"
9089
- });
9090
- let headCommitHash = preflight.headCommitHash;
9091
- if (!headCommitHash) {
9092
- throw new RemixError("Failed to resolve local HEAD commit.", { exitCode: 1 });
9093
- }
9094
- const worktreeStatus = await getWorktreeStatus(repoRoot);
9095
- if (preflight.status === "ready_to_fast_forward") {
9096
- if (!autoSyncEnabled) {
9097
- throw new RemixError("Local repository is stale and `collab add` sync automation is disabled.", {
9098
- exitCode: 2,
9099
- hint: "Run `remix collab sync` first, or rerun without disabling sync automation."
9100
- });
9101
- }
9102
- if (!worktreeStatus.isClean && diffSource !== "worktree") {
9103
- throw new RemixError("Automatic stale-work replay requires the current worktree diff.", {
9104
- exitCode: 2,
9105
- hint: "Use `remix collab add` without an external diff while the local repo is dirty, or clean the repo before submitting an external diff."
9106
- });
9107
- }
9108
- if (worktreeStatus.isClean) {
9109
- await collabSync({
9110
- api: params.api,
9111
- cwd: repoRoot,
9112
- dryRun: false,
9113
- allowBranchMismatch: params.allowBranchMismatch
9114
- });
9115
- headCommitHash = await getHeadCommitHash(repoRoot);
9116
- if (!headCommitHash) {
9117
- throw new RemixError("Failed to resolve local HEAD after syncing.", { exitCode: 1 });
9118
- }
9119
- } else {
9120
- const staleWorkSnapshot = await captureRepoSnapshot(repoRoot, { includeWorkspaceDiffHash: true });
9121
- const preserved = await preserveWorkspaceChanges(repoRoot, "remix-add-preserve");
9122
- try {
9123
- await assertRepoSnapshotUnchanged(repoRoot, staleWorkSnapshot, {
9124
- operation: "`remix collab add` stale-work pre-sync",
9125
- recoveryHint: "The worktree changed while local changes were being preserved. Review the local changes and rerun `remix collab add`."
9126
- });
9127
- await discardTrackedChanges(repoRoot, "`remix collab add`");
9128
- await discardCapturedUntrackedChanges(repoRoot, preserved.includedUntrackedPaths);
9129
- await collabSync({
9130
- api: params.api,
9131
- cwd: repoRoot,
9132
- dryRun: false,
9133
- allowBranchMismatch: params.allowBranchMismatch
9134
- });
9135
- } catch (err) {
9136
- const detail = formatCliErrorDetail(err);
9137
- const hint = [
9138
- detail,
9139
- `The preserved local diff is available at: ${preserved.preservedDiffPath}`
9140
- ].filter(Boolean).join("\n\n");
9141
- throw new RemixError("Failed to sync the stale repository before submitting the change step.", {
9142
- exitCode: err instanceof RemixError ? err.exitCode : 1,
9143
- hint
9144
- });
9145
- }
9146
- headCommitHash = await getHeadCommitHash(repoRoot);
9147
- if (!headCommitHash) {
9148
- throw new RemixError("Failed to resolve local HEAD after syncing.", { exitCode: 1 });
9149
- }
9150
- const deterministicReapply = await reapplyPreservedWorkspaceChanges(repoRoot, preserved);
9151
- if (deterministicReapply.status === "failed") {
9152
- const hint = [
9153
- deterministicReapply.detail,
9154
- `The preserved local diff is available at: ${preserved.preservedDiffPath}`
9155
- ].filter(Boolean).join("\n\n");
9156
- throw new RemixError("Failed to restore preserved local changes after syncing.", {
9157
- exitCode: 1,
9158
- hint
9159
- });
9160
- }
9161
- if (deterministicReapply.status === "conflict") {
9162
- try {
9163
- const replayResp = await params.api.startChangeStepReplay(binding.currentAppId, {
9164
- prompt,
9165
- assistantResponse: assistantResponse ?? void 0,
9166
- diff: await import_promises15.default.readFile(preserved.preservedDiffPath, "utf8"),
9167
- baseCommitHash: preserved.baseHeadCommitHash,
9168
- targetHeadCommitHash: headCommitHash,
9169
- expectedPaths: preserved.stagePlan.expectedPaths,
9170
- actor: params.actor,
9171
- workspaceMetadata: {
9172
- branch,
9173
- repoRoot,
9174
- remoteUrl: binding.remoteUrl,
9175
- defaultBranch: binding.defaultBranch
9176
- },
9177
- idempotencyKey: buildDeterministicIdempotencyKey({
9178
- appId: binding.currentAppId,
9179
- baseCommitHash: preserved.baseHeadCommitHash,
9180
- targetHeadCommitHash: headCommitHash,
9181
- prompt,
9182
- assistantResponse,
9183
- preservedDiffSha256: preserved.preservedDiffSha256
9184
- })
9185
- });
9186
- const startedReplay = unwrapResponseObject(replayResp, "change step replay");
9187
- const replay = await pollChangeStepReplay(params.api, binding.currentAppId, String(startedReplay.id));
9188
- const replayDiffResp = await params.api.getChangeStepReplayDiff(binding.currentAppId, String(replay.id));
9189
- const replayDiff = unwrapResponseObject(replayDiffResp, "change step replay diff");
9190
- const { backupPath: backupPath2, diffSha256 } = await writeTempUnifiedDiffBackup(replayDiff.diff, "remix-add-ai-replay");
9191
- const replayApply = await reapplyPreservedWorkspaceChanges(repoRoot, {
9192
- baseHeadCommitHash: headCommitHash,
9193
- preservedDiffPath: backupPath2,
9194
- preservedDiffSha256: diffSha256,
9195
- includedUntrackedPaths: [],
9196
- stagePlan: preserved.stagePlan
9197
- });
9198
- if (replayApply.status !== "clean") {
9199
- const hint = [
9200
- replayApply.detail,
9201
- `The preserved local diff is available at: ${preserved.preservedDiffPath}`,
9202
- `The AI-replayed diff is available at: ${backupPath2}`
9203
- ].filter(Boolean).join("\n\n");
9204
- throw new RemixError("AI-assisted stale-work replay produced a diff that could not be applied locally.", {
9205
- exitCode: 1,
9206
- hint
9207
- });
9208
- }
9209
- } catch (err) {
9210
- const detail = formatCliErrorDetail(err);
9211
- const hint = [
9212
- detail,
9213
- `The preserved local diff is available at: ${preserved.preservedDiffPath}`,
9214
- "Resolve the local conflict manually if needed, then rerun `remix collab add`."
9215
- ].filter(Boolean).join("\n\n");
9216
- throw new RemixError("AI-assisted stale-work replay could not complete safely.", {
9217
- exitCode: err instanceof RemixError ? err.exitCode : 1,
9218
- hint
9219
- });
9220
- }
9221
- }
9222
- }
9223
- }
9224
- const workspaceSnapshot = diffSource === "external" ? null : await getWorkspaceSnapshot(repoRoot);
9225
- const submissionSnapshot = diffSource === "worktree" ? await captureRepoSnapshot(repoRoot, { includeWorkspaceDiffHash: true }) : null;
9226
- const diff = params.diff ?? workspaceSnapshot?.diff ?? "";
9227
- if (!diff.trim()) {
9228
- throw new RemixError("Diff is empty.", {
9229
- exitCode: 2,
9230
- hint: "Make changes first, or pass `--diff-file`/`--diff-stdin`."
9231
- });
9232
- }
9233
- if (diffSource === "external") {
9234
- const validation = await validateUnifiedDiff(repoRoot, diff);
9235
- if (!validation.ok) {
9236
- const actionHint = validation.kind === "malformed_patch" ? "The provided external diff is malformed. Recreate it with `git diff --binary --no-ext-diff`, avoid hand-editing patch hunks, and ensure the patch ends with a trailing newline." : validation.kind === "apply_conflict" ? "The external diff is valid patch syntax, but it does not apply cleanly to the current local HEAD. Sync or update the repo and regenerate the diff against the latest base." : "Git could not validate the provided external diff against the current repository state.";
9237
- const hint = [validation.detail, actionHint].filter(Boolean).join("\n\n");
9238
- throw new RemixError("External diff validation failed.", {
9239
- exitCode: validation.kind === "malformed_patch" ? 2 : 1,
9240
- hint
9241
- });
9242
- }
9243
- }
9244
- headCommitHash = await getHeadCommitHash(repoRoot);
9245
- if (!headCommitHash) {
9246
- throw new RemixError("Failed to resolve local HEAD before creating the change step.", { exitCode: 1 });
9247
- }
9248
- const stats = summarizeUnifiedDiff(diff);
9249
- const idempotencyKey = params.idempotencyKey?.trim() || buildDeterministicIdempotencyKey({
9250
- appId: binding.currentAppId,
9251
- upstreamAppId: binding.upstreamAppId,
9252
- headCommitHash,
9253
- prompt,
9254
- assistantResponse,
9255
- diff
9256
- });
9257
- const resp = await params.api.createChangeStep(binding.currentAppId, {
9258
- threadId: binding.threadId ?? void 0,
9259
- collabLaneId: binding.laneId ?? void 0,
9260
- prompt,
9261
- assistantResponse: assistantResponse ?? void 0,
9262
- diff,
9263
- baseCommitHash: headCommitHash,
9264
- headCommitHash,
9265
- changedFilesCount: stats.changedFilesCount,
9266
- insertions: stats.insertions,
9267
- deletions: stats.deletions,
9268
- actor: params.actor,
9269
- workspaceMetadata: {
9270
- branch,
9271
- repoRoot,
9272
- remoteUrl: binding.remoteUrl,
9273
- defaultBranch: binding.defaultBranch
9274
- },
9275
- idempotencyKey
9276
- });
9277
- const created = unwrapResponseObject(resp, "change step");
9278
- const step = await pollChangeStep(params.api, binding.currentAppId, String(created.id));
9279
- const canAutoSyncLocally = autoSyncEnabled && diffSource === "worktree";
9280
- if (!autoSyncEnabled || !canAutoSyncLocally) {
9281
- return attachWarnings(step, lockWarnings);
9282
- }
9283
- const { backupPath } = await writeTempUnifiedDiffBackup(diff, "remix-add");
9284
- try {
9285
- await pollAppReady(params.api, binding.currentAppId);
9286
- if (submissionSnapshot) {
9287
- await assertRepoSnapshotUnchanged(repoRoot, submissionSnapshot, {
9288
- operation: "`remix collab add` auto-sync",
9289
- recoveryHint: "The repository changed after the change step was submitted. Review the local changes, inspect the preserved diff if needed, and rerun `remix collab sync` manually."
9290
- });
9291
- }
9292
- await discardTrackedChanges(repoRoot, "`remix collab add`");
9293
- await discardCapturedUntrackedChanges(repoRoot, workspaceSnapshot?.includedUntrackedPaths ?? []);
9294
- await collabSync({
9295
- api: params.api,
9296
- cwd: repoRoot,
9297
- dryRun: false,
9298
- allowBranchMismatch: params.allowBranchMismatch
9299
- });
9300
- await import_promises15.default.rm(import_path4.default.dirname(backupPath), { recursive: true, force: true }).catch(() => void 0);
9301
- } catch (err) {
9302
- const detail = formatCliErrorDetail(err);
9303
- const hint = [
9304
- detail,
9305
- `The submitted diff backup was preserved at: ${backupPath}`,
9306
- "The change step already succeeded remotely. Inspect or reapply that diff manually if needed, then run `remix collab sync`."
9307
- ].filter(Boolean).join("\n\n");
9308
- throw new RemixError("Change step succeeded remotely, but automatic local sync failed.", {
9309
- exitCode: err instanceof RemixError ? err.exitCode : 1,
9310
- hint
9311
- });
9312
- }
9313
- return attachWarnings(step, lockWarnings);
9314
- };
9315
- if (diffSource === "worktree") {
9316
- return withRepoMutationLock(
9317
- {
9318
- cwd: repoRoot,
9319
- operation: "collabAdd"
9320
- },
9321
- async ({ warnings }) => run(warnings)
9322
- );
9323
- }
9324
- return run();
9325
- }
9326
- function assertSupportedRecordingPreflight2(preflight) {
9327
- if (preflight.status === "not_bound") {
9328
- throw new RemixError("Repository is not bound to Remix.", {
9120
+ if (!assistantResponse) throw new RemixError("Assistant response is required.", { exitCode: 2 });
9121
+ if (params.diff?.trim()) {
9122
+ throw new RemixError("External diff submission is no longer supported for `finalize_turn`.", {
9329
9123
  exitCode: 2,
9330
- hint: preflight.hint
9124
+ hint: "Finalize turns now capture the real workspace boundary from the local snapshot store."
9331
9125
  });
9332
9126
  }
9333
- if (preflight.status === "branch_binding_missing") {
9334
- throw new RemixError("Current branch is not yet bound to a Remix lane.", {
9335
- exitCode: 2,
9336
- hint: preflight.hint
9337
- });
9127
+ const detected = await collabDetectRepoState({
9128
+ api: params.api,
9129
+ cwd: repoRoot,
9130
+ allowBranchMismatch: params.allowBranchMismatch
9131
+ });
9132
+ if (detected.status === "not_bound") {
9133
+ throw new RemixError("Repository is not bound to Remix.", { exitCode: 2, hint: detected.hint });
9338
9134
  }
9339
- if (preflight.status === "family_ambiguous") {
9340
- throw new RemixError("Multiple canonical Remix families match this repository.", {
9341
- exitCode: 2,
9342
- hint: preflight.hint
9343
- });
9135
+ if (detected.status === "branch_binding_missing" || detected.status === "family_ambiguous") {
9136
+ throw new RemixError(detected.hint || "Current branch is not ready for Remix recording.", { exitCode: 2, hint: detected.hint });
9344
9137
  }
9345
- if (preflight.status === "not_git_repo") {
9346
- throw new RemixError(preflight.hint || "Not inside a git repository.", {
9138
+ if (detected.status === "metadata_conflict" || detected.status === "branch_mismatch") {
9139
+ throw new RemixError("Repository must be realigned before finalizing the turn.", {
9347
9140
  exitCode: 2,
9348
- hint: preflight.hint
9141
+ hint: detected.hint
9349
9142
  });
9350
9143
  }
9351
- if (preflight.status === "missing_head") {
9352
- throw new RemixError("Failed to resolve local HEAD commit.", {
9144
+ if (detected.status === "missing_head" || detected.status === "remote_error") {
9145
+ throw new RemixError(detected.hint || "Failed to determine the current repo state.", {
9353
9146
  exitCode: 1,
9354
- hint: preflight.hint
9355
- });
9356
- }
9357
- if (preflight.status === "branch_mismatch") {
9358
- assertBoundBranchMatch({
9359
- currentBranch: preflight.currentBranch,
9360
- branchName: preflight.branchName,
9361
- allowBranchMismatch: false,
9362
- operation: "`remix collab record-turn`"
9147
+ hint: detected.hint
9363
9148
  });
9364
9149
  }
9365
- if (preflight.status === "metadata_conflict") {
9366
- throw new RemixError("Local repository metadata conflicts with the bound Remix app.", {
9150
+ if (detected.repoState === "server_only_changed") {
9151
+ throw new RemixError("Server changes must be pulled locally before finalizing this turn.", {
9367
9152
  exitCode: 2,
9368
- hint: preflight.hint
9153
+ hint: detected.hint
9369
9154
  });
9370
9155
  }
9371
- if (preflight.status === "reconcile_required") {
9372
- throw new RemixError("Local repository cannot be fast-forward synced.", {
9156
+ if (detected.repoState === "external_local_base_changed") {
9157
+ throw new RemixError("The local checkout must be re-anchored before finalizing this turn.", {
9373
9158
  exitCode: 2,
9374
- hint: preflight.hint
9159
+ hint: detected.hint
9375
9160
  });
9376
9161
  }
9377
- }
9378
- async function collabRecordTurn(params) {
9379
- const repoRoot = await findGitRoot(params.cwd);
9380
- const binding = await ensureActiveLaneBinding({
9381
- repoRoot,
9382
- api: params.api,
9383
- operation: "`remix collab record-turn`"
9384
- });
9385
- if (!binding) {
9386
- throw new RemixError("Repository is not bound to Remix.", {
9387
- exitCode: 2,
9388
- hint: "Run `remix collab init` first."
9389
- });
9390
- }
9391
- const prompt = params.prompt.trim();
9392
- const assistantResponse = params.assistantResponse.trim();
9393
- if (!prompt) throw new RemixError("Prompt is required.", { exitCode: 2 });
9394
- if (!assistantResponse) throw new RemixError("Assistant response is required.", { exitCode: 2 });
9395
- const preflight = await collabRecordingPreflight({
9396
- api: params.api,
9397
- cwd: repoRoot,
9398
- allowBranchMismatch: params.allowBranchMismatch
9162
+ const baseline = await readLocalBaseline({
9163
+ repoFingerprint: binding.repoFingerprint,
9164
+ laneId: binding.laneId,
9165
+ repoRoot
9399
9166
  });
9400
- assertSupportedRecordingPreflight2(preflight);
9401
- if (!preflight.worktreeClean) {
9402
- throw new RemixError("Cannot record a no-diff turn while the worktree has local changes.", {
9167
+ if (!baseline) {
9168
+ throw new RemixError("Local Remix baseline is missing for this lane.", {
9403
9169
  exitCode: 2,
9404
- hint: "Record the pending code changes as a Remix change step with `remix collab add`, or clean the worktree before retrying `remix collab record-turn`."
9170
+ hint: "Run `remix collab re-anchor` to create a fresh baseline."
9405
9171
  });
9406
9172
  }
9407
- if (preflight.status === "ready_to_fast_forward") {
9408
- await collabSync({
9409
- api: params.api,
9410
- cwd: repoRoot,
9411
- dryRun: false,
9412
- allowBranchMismatch: params.allowBranchMismatch
9413
- });
9414
- }
9415
- const branch = await getCurrentBranch(repoRoot);
9416
- assertBoundBranchMatch({
9417
- currentBranch: branch,
9418
- branchName: binding.branchName,
9419
- allowBranchMismatch: params.allowBranchMismatch,
9420
- operation: "`remix collab record-turn`"
9173
+ const snapshot = await captureLocalSnapshot({
9174
+ repoRoot,
9175
+ repoFingerprint: binding.repoFingerprint,
9176
+ laneId: binding.laneId,
9177
+ branchName: binding.branchName
9421
9178
  });
9422
- const headCommitHash = await getHeadCommitHash(repoRoot);
9179
+ const mode = snapshot.snapshotHash === baseline.lastSnapshotHash ? "no_diff_turn" : "changed_turn";
9423
9180
  const idempotencyKey = params.idempotencyKey?.trim() || buildDeterministicIdempotencyKey({
9181
+ kind: "collab_finalize_turn_boundary_v1",
9424
9182
  appId: binding.currentAppId,
9425
- upstreamAppId: binding.upstreamAppId,
9426
- headCommitHash,
9183
+ laneId: binding.laneId,
9184
+ baselineSnapshotId: baseline.lastSnapshotId,
9185
+ baselineServerHeadHash: baseline.lastServerHeadHash,
9186
+ currentSnapshotId: snapshot.id,
9187
+ currentSnapshotHash: snapshot.snapshotHash,
9188
+ repoState: detected.repoState,
9427
9189
  prompt,
9428
9190
  assistantResponse
9429
9191
  });
9430
- const resp = await params.api.createCollabTurn(binding.currentAppId, {
9431
- threadId: binding.threadId ?? void 0,
9432
- collabLaneId: binding.laneId ?? void 0,
9192
+ const job = await enqueueCapturedFinalizeTurn({
9193
+ repoRoot,
9194
+ repoFingerprint: binding.repoFingerprint,
9195
+ currentAppId: binding.currentAppId,
9196
+ laneId: binding.laneId,
9197
+ threadId: binding.threadId,
9198
+ branchName: binding.branchName,
9433
9199
  prompt,
9434
9200
  assistantResponse,
9435
- actor: params.actor,
9436
- workspaceMetadata: {
9437
- branch,
9438
- repoRoot,
9201
+ baselineSnapshotId: baseline.lastSnapshotId,
9202
+ baselineServerHeadHash: baseline.lastServerHeadHash,
9203
+ currentSnapshotId: snapshot.id,
9204
+ idempotencyKey,
9205
+ metadata: {
9439
9206
  remoteUrl: binding.remoteUrl,
9440
9207
  defaultBranch: binding.defaultBranch,
9441
- headCommitHash
9442
- },
9443
- idempotencyKey
9208
+ actor: params.actor ?? null,
9209
+ repoState: detected.repoState
9210
+ }
9444
9211
  });
9445
- const turn = unwrapResponseObject(resp, "collab turn");
9446
- return turn;
9212
+ return {
9213
+ mode,
9214
+ idempotencyKey,
9215
+ queued: true,
9216
+ jobId: job.id,
9217
+ repoState: detected.repoState,
9218
+ changeStep: null,
9219
+ collabTurn: null,
9220
+ autoSync: null,
9221
+ warnings: [FINALIZE_QUEUED_WARNING, ...collectWarnings(detected.warnings)]
9222
+ };
9447
9223
  }
9448
9224
 
9449
- // node_modules/@remixhq/core/dist/chunk-BNKPTE2U.js
9225
+ // node_modules/@remixhq/core/dist/chunk-R7FVSCQW.js
9450
9226
  async function readJsonSafe(res) {
9451
9227
  const ct = res.headers.get("content-type") ?? "";
9452
9228
  if (!ct.toLowerCase().includes("application/json")) return null;
@@ -9460,7 +9236,7 @@ function createApiClient(config, opts) {
9460
9236
  const apiKey = (opts?.apiKey ?? "").trim();
9461
9237
  const tokenProvider = opts?.tokenProvider;
9462
9238
  const CLIENT_KEY_HEADER = "x-comerge-api-key";
9463
- async function request(path13, init) {
9239
+ async function request(path12, init) {
9464
9240
  if (!tokenProvider) {
9465
9241
  throw new RemixError("API client is missing a token provider.", {
9466
9242
  exitCode: 1,
@@ -9468,7 +9244,7 @@ function createApiClient(config, opts) {
9468
9244
  });
9469
9245
  }
9470
9246
  const auth = await tokenProvider();
9471
- const url = new URL(path13, config.apiUrl).toString();
9247
+ const url = new URL(path12, config.apiUrl).toString();
9472
9248
  const doFetch = async (bearer) => fetch(url, {
9473
9249
  ...init,
9474
9250
  headers: {
@@ -9492,7 +9268,7 @@ function createApiClient(config, opts) {
9492
9268
  const json = await readJsonSafe(res);
9493
9269
  return json ?? null;
9494
9270
  }
9495
- async function requestBinary(path13, init) {
9271
+ async function requestBinary(path12, init) {
9496
9272
  if (!tokenProvider) {
9497
9273
  throw new RemixError("API client is missing a token provider.", {
9498
9274
  exitCode: 1,
@@ -9500,7 +9276,7 @@ function createApiClient(config, opts) {
9500
9276
  });
9501
9277
  }
9502
9278
  const auth = await tokenProvider();
9503
- const url = new URL(path13, config.apiUrl).toString();
9279
+ const url = new URL(path12, config.apiUrl).toString();
9504
9280
  const doFetch = async (bearer) => fetch(url, {
9505
9281
  ...init,
9506
9282
  headers: {
@@ -9599,6 +9375,15 @@ function createApiClient(config, opts) {
9599
9375
  const suffix = qs.toString() ? `?${qs.toString()}` : "";
9600
9376
  return request(`/v1/apps/${encodeURIComponent(appId)}/edit-queue${suffix}`, { method: "GET" });
9601
9377
  },
9378
+ listAppJobQueue: (appId, params) => {
9379
+ const qs = new URLSearchParams();
9380
+ if (typeof params?.limit === "number") qs.set("limit", String(params.limit));
9381
+ if (typeof params?.offset === "number") qs.set("offset", String(params.offset));
9382
+ for (const kind of params?.kind ?? []) qs.append("kind", kind);
9383
+ for (const status of params?.status ?? []) qs.append("status", status);
9384
+ const suffix = qs.toString() ? `?${qs.toString()}` : "";
9385
+ return request(`/v1/apps/${encodeURIComponent(appId)}/job-queue${suffix}`, { method: "GET" });
9386
+ },
9602
9387
  getMergeRequest: (mrId) => request(`/v1/merge-requests/${encodeURIComponent(mrId)}`, { method: "GET" }),
9603
9388
  presignImportUpload: (payload) => request("/v1/apps/import/upload/presign", { method: "POST", body: JSON.stringify(payload) }),
9604
9389
  importFromUpload: (payload) => request("/v1/apps/import/upload", { method: "POST", body: JSON.stringify(payload) }),
@@ -9606,6 +9391,11 @@ function createApiClient(config, opts) {
9606
9391
  importFromUploadFirstParty: (payload) => request("/v1/apps/import/upload/first-party", { method: "POST", body: JSON.stringify(payload) }),
9607
9392
  importFromGithubFirstParty: (payload) => request("/v1/apps/import/github/first-party", { method: "POST", body: JSON.stringify(payload) }),
9608
9393
  forkApp: (appId, payload) => request(`/v1/apps/${encodeURIComponent(appId)}/fork`, { method: "POST", body: JSON.stringify(payload ?? {}) }),
9394
+ getAppHead: (appId) => request(`/v1/apps/${encodeURIComponent(appId)}/head`, { method: "GET" }),
9395
+ getAppDelta: (appId, payload) => request(`/v1/apps/${encodeURIComponent(appId)}/delta`, {
9396
+ method: "POST",
9397
+ body: JSON.stringify(payload)
9398
+ }),
9609
9399
  downloadAppBundle: (appId) => requestBinary(`/v1/apps/${encodeURIComponent(appId)}/download.bundle`, { method: "GET" }),
9610
9400
  createChangeStep: (appId, payload) => request(`/v1/apps/${encodeURIComponent(appId)}/change-steps`, {
9611
9401
  method: "POST",
@@ -10318,8 +10108,8 @@ function getErrorMap() {
10318
10108
 
10319
10109
  // node_modules/zod/v3/helpers/parseUtil.js
10320
10110
  var makeIssue = (params) => {
10321
- const { data, path: path13, errorMaps, issueData } = params;
10322
- const fullPath = [...path13, ...issueData.path || []];
10111
+ const { data, path: path12, errorMaps, issueData } = params;
10112
+ const fullPath = [...path12, ...issueData.path || []];
10323
10113
  const fullIssue = {
10324
10114
  ...issueData,
10325
10115
  path: fullPath
@@ -10435,11 +10225,11 @@ var errorUtil;
10435
10225
 
10436
10226
  // node_modules/zod/v3/types.js
10437
10227
  var ParseInputLazyPath = class {
10438
- constructor(parent, value, path13, key) {
10228
+ constructor(parent, value, path12, key) {
10439
10229
  this._cachedPath = [];
10440
10230
  this.parent = parent;
10441
10231
  this.data = value;
10442
- this._path = path13;
10232
+ this._path = path12;
10443
10233
  this._key = key;
10444
10234
  }
10445
10235
  get path() {
@@ -13882,8 +13672,8 @@ var coerce = {
13882
13672
  var NEVER = INVALID;
13883
13673
 
13884
13674
  // node_modules/@remixhq/core/dist/chunk-EVWDYCBL.js
13885
- var import_promises18 = __toESM(require("fs/promises"), 1);
13886
- var import_os4 = __toESM(require("os"), 1);
13675
+ var import_promises17 = __toESM(require("fs/promises"), 1);
13676
+ var import_os3 = __toESM(require("os"), 1);
13887
13677
  var import_path7 = __toESM(require("path"), 1);
13888
13678
 
13889
13679
  // node_modules/tslib/tslib.es6.mjs
@@ -22772,8 +22562,8 @@ var IcebergError = class extends Error {
22772
22562
  return this.status === 419;
22773
22563
  }
22774
22564
  };
22775
- function buildUrl(baseUrl, path13, query) {
22776
- const url = new URL(path13, baseUrl);
22565
+ function buildUrl(baseUrl, path12, query) {
22566
+ const url = new URL(path12, baseUrl);
22777
22567
  if (query) {
22778
22568
  for (const [key, value] of Object.entries(query)) {
22779
22569
  if (value !== void 0) {
@@ -22803,12 +22593,12 @@ function createFetchClient(options) {
22803
22593
  return {
22804
22594
  async request({
22805
22595
  method,
22806
- path: path13,
22596
+ path: path12,
22807
22597
  query,
22808
22598
  body,
22809
22599
  headers
22810
22600
  }) {
22811
- const url = buildUrl(options.baseUrl, path13, query);
22601
+ const url = buildUrl(options.baseUrl, path12, query);
22812
22602
  const authHeaders = await buildAuthHeaders(options.auth);
22813
22603
  const res = await fetchFn(url, {
22814
22604
  method,
@@ -23627,7 +23417,7 @@ var StorageFileApi = class extends BaseApiClient {
23627
23417
  * @param path The relative file path. Should be of the format `folder/subfolder/filename.png`. The bucket must already exist before attempting to upload.
23628
23418
  * @param fileBody The body of the file to be stored in the bucket.
23629
23419
  */
23630
- async uploadOrUpdate(method, path13, fileBody, fileOptions) {
23420
+ async uploadOrUpdate(method, path12, fileBody, fileOptions) {
23631
23421
  var _this = this;
23632
23422
  return _this.handleOperation(async () => {
23633
23423
  let body;
@@ -23651,7 +23441,7 @@ var StorageFileApi = class extends BaseApiClient {
23651
23441
  if ((typeof ReadableStream !== "undefined" && body instanceof ReadableStream || body && typeof body === "object" && "pipe" in body && typeof body.pipe === "function") && !options.duplex) options.duplex = "half";
23652
23442
  }
23653
23443
  if (fileOptions === null || fileOptions === void 0 ? void 0 : fileOptions.headers) headers = _objectSpread22(_objectSpread22({}, headers), fileOptions.headers);
23654
- const cleanPath = _this._removeEmptyFolders(path13);
23444
+ const cleanPath = _this._removeEmptyFolders(path12);
23655
23445
  const _path = _this._getFinalPath(cleanPath);
23656
23446
  const data = await (method == "PUT" ? put : post)(_this.fetch, `${_this.url}/object/${_path}`, body, _objectSpread22({ headers }, (options === null || options === void 0 ? void 0 : options.duplex) ? { duplex: options.duplex } : {}));
23657
23447
  return {
@@ -23712,8 +23502,8 @@ var StorageFileApi = class extends BaseApiClient {
23712
23502
  * - Refer to the [Storage guide](/docs/guides/storage/security/access-control) on how access control works
23713
23503
  * - For React Native, using either `Blob`, `File` or `FormData` does not work as intended. Upload file using `ArrayBuffer` from base64 file data instead, see example below.
23714
23504
  */
23715
- async upload(path13, fileBody, fileOptions) {
23716
- return this.uploadOrUpdate("POST", path13, fileBody, fileOptions);
23505
+ async upload(path12, fileBody, fileOptions) {
23506
+ return this.uploadOrUpdate("POST", path12, fileBody, fileOptions);
23717
23507
  }
23718
23508
  /**
23719
23509
  * Upload a file with a token generated from `createSignedUploadUrl`.
@@ -23752,9 +23542,9 @@ var StorageFileApi = class extends BaseApiClient {
23752
23542
  * - `objects` table permissions: none
23753
23543
  * - Refer to the [Storage guide](/docs/guides/storage/security/access-control) on how access control works
23754
23544
  */
23755
- async uploadToSignedUrl(path13, token, fileBody, fileOptions) {
23545
+ async uploadToSignedUrl(path12, token, fileBody, fileOptions) {
23756
23546
  var _this3 = this;
23757
- const cleanPath = _this3._removeEmptyFolders(path13);
23547
+ const cleanPath = _this3._removeEmptyFolders(path12);
23758
23548
  const _path = _this3._getFinalPath(cleanPath);
23759
23549
  const url = new URL(_this3.url + `/object/upload/sign/${_path}`);
23760
23550
  url.searchParams.set("token", token);
@@ -23816,10 +23606,10 @@ var StorageFileApi = class extends BaseApiClient {
23816
23606
  * - `objects` table permissions: `insert`
23817
23607
  * - Refer to the [Storage guide](/docs/guides/storage/security/access-control) on how access control works
23818
23608
  */
23819
- async createSignedUploadUrl(path13, options) {
23609
+ async createSignedUploadUrl(path12, options) {
23820
23610
  var _this4 = this;
23821
23611
  return _this4.handleOperation(async () => {
23822
- let _path = _this4._getFinalPath(path13);
23612
+ let _path = _this4._getFinalPath(path12);
23823
23613
  const headers = _objectSpread22({}, _this4.headers);
23824
23614
  if (options === null || options === void 0 ? void 0 : options.upsert) headers["x-upsert"] = "true";
23825
23615
  const data = await post(_this4.fetch, `${_this4.url}/object/upload/sign/${_path}`, {}, { headers });
@@ -23828,7 +23618,7 @@ var StorageFileApi = class extends BaseApiClient {
23828
23618
  if (!token) throw new StorageError("No token returned by API");
23829
23619
  return {
23830
23620
  signedUrl: url.toString(),
23831
- path: path13,
23621
+ path: path12,
23832
23622
  token
23833
23623
  };
23834
23624
  });
@@ -23884,8 +23674,8 @@ var StorageFileApi = class extends BaseApiClient {
23884
23674
  * - Refer to the [Storage guide](/docs/guides/storage/security/access-control) on how access control works
23885
23675
  * - For React Native, using either `Blob`, `File` or `FormData` does not work as intended. Update file using `ArrayBuffer` from base64 file data instead, see example below.
23886
23676
  */
23887
- async update(path13, fileBody, fileOptions) {
23888
- return this.uploadOrUpdate("PUT", path13, fileBody, fileOptions);
23677
+ async update(path12, fileBody, fileOptions) {
23678
+ return this.uploadOrUpdate("PUT", path12, fileBody, fileOptions);
23889
23679
  }
23890
23680
  /**
23891
23681
  * Moves an existing file to a new path in the same bucket.
@@ -24032,10 +23822,10 @@ var StorageFileApi = class extends BaseApiClient {
24032
23822
  * - `objects` table permissions: `select`
24033
23823
  * - Refer to the [Storage guide](/docs/guides/storage/security/access-control) on how access control works
24034
23824
  */
24035
- async createSignedUrl(path13, expiresIn, options) {
23825
+ async createSignedUrl(path12, expiresIn, options) {
24036
23826
  var _this8 = this;
24037
23827
  return _this8.handleOperation(async () => {
24038
- let _path = _this8._getFinalPath(path13);
23828
+ let _path = _this8._getFinalPath(path12);
24039
23829
  const hasTransform = typeof (options === null || options === void 0 ? void 0 : options.transform) === "object" && options.transform !== null && Object.keys(options.transform).length > 0;
24040
23830
  let data = await post(_this8.fetch, `${_this8.url}/object/sign/${_path}`, _objectSpread22({ expiresIn }, hasTransform ? { transform: options.transform } : {}), { headers: _this8.headers });
24041
23831
  const downloadQueryParam = (options === null || options === void 0 ? void 0 : options.download) ? `&download=${options.download === true ? "" : options.download}` : "";
@@ -24162,11 +23952,11 @@ var StorageFileApi = class extends BaseApiClient {
24162
23952
  * - `objects` table permissions: `select`
24163
23953
  * - Refer to the [Storage guide](/docs/guides/storage/security/access-control) on how access control works
24164
23954
  */
24165
- download(path13, options, parameters) {
23955
+ download(path12, options, parameters) {
24166
23956
  const renderPath = typeof (options === null || options === void 0 ? void 0 : options.transform) !== "undefined" ? "render/image/authenticated" : "object";
24167
23957
  const transformationQuery = this.transformOptsToQueryString((options === null || options === void 0 ? void 0 : options.transform) || {});
24168
23958
  const queryString = transformationQuery ? `?${transformationQuery}` : "";
24169
- const _path = this._getFinalPath(path13);
23959
+ const _path = this._getFinalPath(path12);
24170
23960
  const downloadFn = () => get(this.fetch, `${this.url}/${renderPath}/${_path}${queryString}`, {
24171
23961
  headers: this.headers,
24172
23962
  noResolveJson: true
@@ -24196,9 +23986,9 @@ var StorageFileApi = class extends BaseApiClient {
24196
23986
  * }
24197
23987
  * ```
24198
23988
  */
24199
- async info(path13) {
23989
+ async info(path12) {
24200
23990
  var _this10 = this;
24201
- const _path = _this10._getFinalPath(path13);
23991
+ const _path = _this10._getFinalPath(path12);
24202
23992
  return _this10.handleOperation(async () => {
24203
23993
  return recursiveToCamel(await get(_this10.fetch, `${_this10.url}/object/info/${_path}`, { headers: _this10.headers }));
24204
23994
  });
@@ -24218,9 +24008,9 @@ var StorageFileApi = class extends BaseApiClient {
24218
24008
  * .exists('folder/avatar1.png')
24219
24009
  * ```
24220
24010
  */
24221
- async exists(path13) {
24011
+ async exists(path12) {
24222
24012
  var _this11 = this;
24223
- const _path = _this11._getFinalPath(path13);
24013
+ const _path = _this11._getFinalPath(path12);
24224
24014
  try {
24225
24015
  await head(_this11.fetch, `${_this11.url}/object/${_path}`, { headers: _this11.headers });
24226
24016
  return {
@@ -24297,8 +24087,8 @@ var StorageFileApi = class extends BaseApiClient {
24297
24087
  * - `objects` table permissions: none
24298
24088
  * - Refer to the [Storage guide](/docs/guides/storage/security/access-control) on how access control works
24299
24089
  */
24300
- getPublicUrl(path13, options) {
24301
- const _path = this._getFinalPath(path13);
24090
+ getPublicUrl(path12, options) {
24091
+ const _path = this._getFinalPath(path12);
24302
24092
  const _queryString = [];
24303
24093
  const downloadQueryParam = (options === null || options === void 0 ? void 0 : options.download) ? `download=${options.download === true ? "" : options.download}` : "";
24304
24094
  if (downloadQueryParam !== "") _queryString.push(downloadQueryParam);
@@ -24437,10 +24227,10 @@ var StorageFileApi = class extends BaseApiClient {
24437
24227
  * - `objects` table permissions: `select`
24438
24228
  * - Refer to the [Storage guide](/docs/guides/storage/security/access-control) on how access control works
24439
24229
  */
24440
- async list(path13, options, parameters) {
24230
+ async list(path12, options, parameters) {
24441
24231
  var _this13 = this;
24442
24232
  return _this13.handleOperation(async () => {
24443
- const body = _objectSpread22(_objectSpread22(_objectSpread22({}, DEFAULT_SEARCH_OPTIONS), options), {}, { prefix: path13 || "" });
24233
+ const body = _objectSpread22(_objectSpread22(_objectSpread22({}, DEFAULT_SEARCH_OPTIONS), options), {}, { prefix: path12 || "" });
24444
24234
  return await post(_this13.fetch, `${_this13.url}/object/list/${_this13.bucketId}`, body, { headers: _this13.headers }, parameters);
24445
24235
  });
24446
24236
  }
@@ -24504,11 +24294,11 @@ var StorageFileApi = class extends BaseApiClient {
24504
24294
  if (typeof Buffer !== "undefined") return Buffer.from(data).toString("base64");
24505
24295
  return btoa(data);
24506
24296
  }
24507
- _getFinalPath(path13) {
24508
- return `${this.bucketId}/${path13.replace(/^\/+/, "")}`;
24297
+ _getFinalPath(path12) {
24298
+ return `${this.bucketId}/${path12.replace(/^\/+/, "")}`;
24509
24299
  }
24510
- _removeEmptyFolders(path13) {
24511
- return path13.replace(/^\/|\/$/g, "").replace(/\/+/g, "/");
24300
+ _removeEmptyFolders(path12) {
24301
+ return path12.replace(/^\/|\/$/g, "").replace(/\/+/g, "/");
24512
24302
  }
24513
24303
  transformOptsToQueryString(transform) {
24514
24304
  const params = [];
@@ -26222,7 +26012,7 @@ function decodeJWT(token) {
26222
26012
  };
26223
26013
  return data;
26224
26014
  }
26225
- async function sleep3(time) {
26015
+ async function sleep2(time) {
26226
26016
  return await new Promise((accept) => {
26227
26017
  setTimeout(() => accept(null), time);
26228
26018
  });
@@ -31991,7 +31781,7 @@ var GoTrueClient = class _GoTrueClient {
31991
31781
  const startedAt = Date.now();
31992
31782
  return await retryable(async (attempt) => {
31993
31783
  if (attempt > 0) {
31994
- await sleep3(200 * Math.pow(2, attempt - 1));
31784
+ await sleep2(200 * Math.pow(2, attempt - 1));
31995
31785
  }
31996
31786
  this._debug(debugName, "refreshing attempt", attempt);
31997
31787
  return await _request(this.fetch, "POST", `${this.url}/token?grant_type=refresh_token`, {
@@ -33543,7 +33333,7 @@ var storedSessionSchema = external_exports.object({
33543
33333
  function xdgConfigHome() {
33544
33334
  const value = process.env.XDG_CONFIG_HOME;
33545
33335
  if (typeof value === "string" && value.trim()) return value;
33546
- return import_path7.default.join(import_os4.default.homedir(), ".config");
33336
+ return import_path7.default.join(import_os3.default.homedir(), ".config");
33547
33337
  }
33548
33338
  async function maybeLoadKeytar() {
33549
33339
  try {
@@ -33562,21 +33352,21 @@ async function maybeLoadKeytar() {
33562
33352
  }
33563
33353
  async function ensurePathPermissions(filePath) {
33564
33354
  const dir = import_path7.default.dirname(filePath);
33565
- await import_promises18.default.mkdir(dir, { recursive: true });
33355
+ await import_promises17.default.mkdir(dir, { recursive: true });
33566
33356
  try {
33567
- await import_promises18.default.chmod(dir, 448);
33357
+ await import_promises17.default.chmod(dir, 448);
33568
33358
  } catch {
33569
33359
  }
33570
33360
  try {
33571
- await import_promises18.default.chmod(filePath, 384);
33361
+ await import_promises17.default.chmod(filePath, 384);
33572
33362
  } catch {
33573
33363
  }
33574
33364
  }
33575
33365
  async function writeJsonAtomic2(filePath, value) {
33576
- await import_promises18.default.mkdir(import_path7.default.dirname(filePath), { recursive: true });
33366
+ await import_promises17.default.mkdir(import_path7.default.dirname(filePath), { recursive: true });
33577
33367
  const tmpPath = `${filePath}.tmp-${Date.now()}-${Math.random().toString(16).slice(2)}`;
33578
- await import_promises18.default.writeFile(tmpPath, JSON.stringify(value, null, 2) + "\n", "utf8");
33579
- await import_promises18.default.rename(tmpPath, filePath);
33368
+ await import_promises17.default.writeFile(tmpPath, JSON.stringify(value, null, 2) + "\n", "utf8");
33369
+ await import_promises17.default.rename(tmpPath, filePath);
33580
33370
  }
33581
33371
  async function writeSessionFileFallback(filePath, session) {
33582
33372
  await writeJsonAtomic2(filePath, session);
@@ -33599,7 +33389,7 @@ function createLocalSessionStore(params) {
33599
33389
  return null;
33600
33390
  }
33601
33391
  }
33602
- const raw = await import_promises18.default.readFile(filePath, "utf8").catch(() => null);
33392
+ const raw = await import_promises17.default.readFile(filePath, "utf8").catch(() => null);
33603
33393
  if (!raw) return null;
33604
33394
  try {
33605
33395
  const parsed = storedSessionSchema.safeParse(JSON.parse(raw));
@@ -33786,12 +33576,12 @@ async function createHookCollabApiClient() {
33786
33576
 
33787
33577
  // src/hook-diagnostics.ts
33788
33578
  var import_node_crypto2 = require("crypto");
33789
- var import_promises20 = __toESM(require("fs/promises"), 1);
33579
+ var import_promises19 = __toESM(require("fs/promises"), 1);
33790
33580
  var import_node_os5 = __toESM(require("os"), 1);
33791
33581
  var import_node_path7 = __toESM(require("path"), 1);
33792
33582
 
33793
33583
  // src/hook-state.ts
33794
- var import_promises19 = __toESM(require("fs/promises"), 1);
33584
+ var import_promises18 = __toESM(require("fs/promises"), 1);
33795
33585
  var import_node_os4 = __toESM(require("os"), 1);
33796
33586
  var import_node_path6 = __toESM(require("path"), 1);
33797
33587
  var import_node_crypto = require("crypto");
@@ -33809,20 +33599,20 @@ function stateLockMetaPath(sessionId) {
33809
33599
  return import_node_path6.default.join(stateLockPath(sessionId), "owner.json");
33810
33600
  }
33811
33601
  async function writeJsonAtomic3(filePath, value) {
33812
- await import_promises19.default.mkdir(import_node_path6.default.dirname(filePath), { recursive: true });
33602
+ await import_promises18.default.mkdir(import_node_path6.default.dirname(filePath), { recursive: true });
33813
33603
  const tmpPath = `${filePath}.tmp-${Date.now()}-${Math.random().toString(16).slice(2)}`;
33814
- await import_promises19.default.writeFile(tmpPath, JSON.stringify(value, null, 2) + "\n", "utf8");
33815
- await import_promises19.default.rename(tmpPath, filePath);
33604
+ await import_promises18.default.writeFile(tmpPath, JSON.stringify(value, null, 2) + "\n", "utf8");
33605
+ await import_promises18.default.rename(tmpPath, filePath);
33816
33606
  }
33817
33607
  var STATE_LOCK_WAIT_MS = 2e3;
33818
33608
  var STATE_LOCK_POLL_MS = 25;
33819
33609
  var STATE_LOCK_STALE_MS = 3e4;
33820
33610
  var STATE_LOCK_HEARTBEAT_MS = 5e3;
33821
- async function sleep4(ms) {
33611
+ async function sleep3(ms) {
33822
33612
  await new Promise((resolve) => setTimeout(resolve, ms));
33823
33613
  }
33824
33614
  async function readStateLockMetadata(sessionId) {
33825
- const raw = await import_promises19.default.readFile(stateLockMetaPath(sessionId), "utf8").catch(() => null);
33615
+ const raw = await import_promises18.default.readFile(stateLockMetaPath(sessionId), "utf8").catch(() => null);
33826
33616
  if (!raw) return null;
33827
33617
  try {
33828
33618
  const parsed = JSON.parse(raw);
@@ -33847,13 +33637,13 @@ async function tryRemoveStaleStateLock(sessionId) {
33847
33637
  const metadata = await readStateLockMetadata(sessionId);
33848
33638
  const staleByHeartbeat = metadata && Date.now() - new Date(metadata.heartbeatAt).getTime() > STATE_LOCK_STALE_MS;
33849
33639
  if (staleByHeartbeat) {
33850
- await import_promises19.default.rm(lockPath, { recursive: true, force: true }).catch(() => void 0);
33640
+ await import_promises18.default.rm(lockPath, { recursive: true, force: true }).catch(() => void 0);
33851
33641
  return true;
33852
33642
  }
33853
33643
  if (!metadata) {
33854
- const lockStat = await import_promises19.default.stat(lockPath).catch(() => null);
33644
+ const lockStat = await import_promises18.default.stat(lockPath).catch(() => null);
33855
33645
  if (lockStat && Date.now() - lockStat.mtimeMs > STATE_LOCK_STALE_MS) {
33856
- await import_promises19.default.rm(lockPath, { recursive: true, force: true }).catch(() => void 0);
33646
+ await import_promises18.default.rm(lockPath, { recursive: true, force: true }).catch(() => void 0);
33857
33647
  return true;
33858
33648
  }
33859
33649
  }
@@ -33862,10 +33652,10 @@ async function tryRemoveStaleStateLock(sessionId) {
33862
33652
  async function acquireStateLock(sessionId) {
33863
33653
  const lockPath = stateLockPath(sessionId);
33864
33654
  const deadline = Date.now() + STATE_LOCK_WAIT_MS;
33865
- await import_promises19.default.mkdir(stateRoot(), { recursive: true });
33655
+ await import_promises18.default.mkdir(stateRoot(), { recursive: true });
33866
33656
  while (true) {
33867
33657
  try {
33868
- await import_promises19.default.mkdir(lockPath);
33658
+ await import_promises18.default.mkdir(lockPath);
33869
33659
  const ownerId = (0, import_node_crypto.randomUUID)();
33870
33660
  const createdAt = (/* @__PURE__ */ new Date()).toISOString();
33871
33661
  const metadata = {
@@ -33890,7 +33680,7 @@ async function acquireStateLock(sessionId) {
33890
33680
  clearInterval(heartbeat);
33891
33681
  const currentMetadata = await readStateLockMetadata(sessionId);
33892
33682
  if (currentMetadata?.ownerId === ownerId) {
33893
- await import_promises19.default.rm(lockPath, { recursive: true, force: true }).catch(() => void 0);
33683
+ await import_promises18.default.rm(lockPath, { recursive: true, force: true }).catch(() => void 0);
33894
33684
  }
33895
33685
  };
33896
33686
  } catch (error) {
@@ -33904,7 +33694,7 @@ async function acquireStateLock(sessionId) {
33904
33694
  if (Date.now() >= deadline) {
33905
33695
  throw new Error(`Timed out acquiring hook state lock for session ${sessionId}.`);
33906
33696
  }
33907
- await sleep4(STATE_LOCK_POLL_MS);
33697
+ await sleep3(STATE_LOCK_POLL_MS);
33908
33698
  }
33909
33699
  }
33910
33700
  }
@@ -33930,6 +33720,12 @@ function normalizeStringArray(value) {
33930
33720
  )
33931
33721
  );
33932
33722
  }
33723
+ function normalizeManualRecordingScope(value) {
33724
+ if (value === "full_turn") {
33725
+ return "full_turn";
33726
+ }
33727
+ return null;
33728
+ }
33933
33729
  function normalizeTouchedRepo(value, repoRoot) {
33934
33730
  if (!value || typeof value !== "object") return null;
33935
33731
  const parsed = value;
@@ -33948,7 +33744,7 @@ function normalizeTouchedRepo(value, repoRoot) {
33948
33744
  manuallyRecorded: Boolean(parsed.manuallyRecorded),
33949
33745
  manuallyRecordedAt: normalizeString(parsed.manuallyRecordedAt),
33950
33746
  manuallyRecordedByTool: normalizeString(parsed.manuallyRecordedByTool),
33951
- manualRecordingScope: parsed.manualRecordingScope === "change_step" || parsed.manualRecordingScope === "full_turn" ? parsed.manualRecordingScope : null,
33747
+ manualRecordingScope: normalizeManualRecordingScope(parsed.manualRecordingScope),
33952
33748
  manualRemoteChangeRecordedAt: normalizeString(parsed.manualRemoteChangeRecordedAt),
33953
33749
  stopAttempted: Boolean(parsed.stopAttempted),
33954
33750
  stopRecorded: Boolean(parsed.stopRecorded),
@@ -34002,7 +33798,7 @@ async function updatePendingTurnState(sessionId, updater) {
34002
33798
  });
34003
33799
  }
34004
33800
  async function loadPendingTurnState(sessionId) {
34005
- const raw = await import_promises19.default.readFile(statePath(sessionId), "utf8").catch(() => null);
33801
+ const raw = await import_promises18.default.readFile(statePath(sessionId), "utf8").catch(() => null);
34006
33802
  if (!raw) return null;
34007
33803
  try {
34008
33804
  const parsed = JSON.parse(raw);
@@ -34104,14 +33900,14 @@ async function listTouchedRepos(sessionId) {
34104
33900
  }
34105
33901
  async function clearPendingTurnState(sessionId) {
34106
33902
  await withStateLock(sessionId, async () => {
34107
- await import_promises19.default.rm(statePath(sessionId), { force: true }).catch(() => void 0);
33903
+ await import_promises18.default.rm(statePath(sessionId), { force: true }).catch(() => void 0);
34108
33904
  });
34109
33905
  }
34110
33906
 
34111
33907
  // package.json
34112
33908
  var package_default = {
34113
33909
  name: "@remixhq/claude-plugin",
34114
- version: "0.1.17",
33910
+ version: "0.1.18",
34115
33911
  description: "Claude Code plugin for Remix collaboration workflows",
34116
33912
  homepage: "https://github.com/RemixDotOne/remix-claude-plugin",
34117
33913
  license: "MIT",
@@ -34142,8 +33938,8 @@ var package_default = {
34142
33938
  prepack: "npm run build"
34143
33939
  },
34144
33940
  dependencies: {
34145
- "@remixhq/core": "^0.1.12",
34146
- "@remixhq/mcp": "^0.1.12"
33941
+ "@remixhq/core": "^0.1.13",
33942
+ "@remixhq/mcp": "^0.1.13"
34147
33943
  },
34148
33944
  devDependencies: {
34149
33945
  "@types/node": "^25.4.0",
@@ -34194,13 +33990,13 @@ function normalizeFields(fields) {
34194
33990
  return Object.fromEntries(normalizedEntries);
34195
33991
  }
34196
33992
  async function rotateLogIfNeeded(logPath) {
34197
- const stat = await import_promises20.default.stat(logPath).catch(() => null);
33993
+ const stat = await import_promises19.default.stat(logPath).catch(() => null);
34198
33994
  if (!stat || stat.size < MAX_LOG_BYTES) {
34199
33995
  return;
34200
33996
  }
34201
33997
  const rotatedPath = `${logPath}.1`;
34202
- await import_promises20.default.rm(rotatedPath, { force: true }).catch(() => void 0);
34203
- await import_promises20.default.rename(logPath, rotatedPath).catch(() => void 0);
33998
+ await import_promises19.default.rm(rotatedPath, { force: true }).catch(() => void 0);
33999
+ await import_promises19.default.rename(logPath, rotatedPath).catch(() => void 0);
34204
34000
  }
34205
34001
  function summarizeText(value) {
34206
34002
  if (typeof value !== "string" || !value.trim()) {
@@ -34220,7 +34016,7 @@ function summarizeText(value) {
34220
34016
  async function appendHookDiagnosticsEvent(params) {
34221
34017
  try {
34222
34018
  const logPath = getHookDiagnosticsLogPath();
34223
- await import_promises20.default.mkdir(import_node_path7.default.dirname(logPath), { recursive: true });
34019
+ await import_promises19.default.mkdir(import_node_path7.default.dirname(logPath), { recursive: true });
34224
34020
  await rotateLogIfNeeded(logPath);
34225
34021
  const event = {
34226
34022
  ts: (/* @__PURE__ */ new Date()).toISOString(),
@@ -34237,14 +34033,14 @@ async function appendHookDiagnosticsEvent(params) {
34237
34033
  message: params.message?.trim() || null,
34238
34034
  fields: normalizeFields(params.fields)
34239
34035
  };
34240
- await import_promises20.default.appendFile(logPath, `${JSON.stringify(event)}
34036
+ await import_promises19.default.appendFile(logPath, `${JSON.stringify(event)}
34241
34037
  `, "utf8");
34242
34038
  } catch {
34243
34039
  }
34244
34040
  }
34245
34041
 
34246
34042
  // src/hook-utils.ts
34247
- var import_promises21 = __toESM(require("fs/promises"), 1);
34043
+ var import_promises20 = __toESM(require("fs/promises"), 1);
34248
34044
  var import_node_path8 = __toESM(require("path"), 1);
34249
34045
  async function readJsonStdin() {
34250
34046
  const chunks = [];
@@ -34308,13 +34104,13 @@ function extractBoolean(input, keys) {
34308
34104
  async function findBoundRepo(startPath) {
34309
34105
  if (!startPath) return null;
34310
34106
  let current = import_node_path8.default.resolve(startPath);
34311
- let stats = await import_promises21.default.stat(current).catch(() => null);
34107
+ let stats = await import_promises20.default.stat(current).catch(() => null);
34312
34108
  if (stats?.isFile()) {
34313
34109
  current = import_node_path8.default.dirname(current);
34314
34110
  }
34315
34111
  while (true) {
34316
34112
  const bindingPath = import_node_path8.default.join(current, ".remix", "config.json");
34317
- const bindingStats = await import_promises21.default.stat(bindingPath).catch(() => null);
34113
+ const bindingStats = await import_promises20.default.stat(bindingPath).catch(() => null);
34318
34114
  if (bindingStats?.isFile()) return current;
34319
34115
  const parent = import_node_path8.default.dirname(current);
34320
34116
  if (parent === current) return null;
@@ -34341,6 +34137,7 @@ var HOOK_ACTOR = {
34341
34137
  version: pluginMetadata.version,
34342
34138
  provider: "anthropic"
34343
34139
  };
34140
+ var collabFinalizeTurn2 = collabFinalizeTurn;
34344
34141
  function getErrorDetails(error) {
34345
34142
  if (error instanceof Error) {
34346
34143
  const hint = typeof error.hint === "string" ? String(error.hint) : null;
@@ -34352,58 +34149,13 @@ function getErrorDetails(error) {
34352
34149
  const message = typeof error === "string" && error.trim() ? error.trim() : "Fallback Remix turn recording failed.";
34353
34150
  return { message, hint: null };
34354
34151
  }
34355
- function getRecordingBlockedMessage(status, repoRoot) {
34356
- if (status.status === "branch_binding_missing") {
34357
- return {
34358
- message: "Fallback Remix turn recording was blocked because the current branch does not have a Remix lane binding yet.",
34359
- hint: status.hint || `Run \`remix_collab_status\` for ${repoRoot}, then initialize or provision the current branch lane before recording work.`
34360
- };
34361
- }
34362
- if (status.status === "family_ambiguous") {
34363
- return {
34364
- message: "Fallback Remix turn recording was blocked because multiple canonical Remix families match this repository and the current checkout does not identify which family to use.",
34365
- hint: status.hint || `Continue from a checkout already bound to the intended family, or run \`remix_collab_init\` with forceNew=true for ${repoRoot} to create a new canonical family.`
34366
- };
34367
- }
34368
- switch (status.status) {
34369
- case "not_git_repo":
34370
- return {
34371
- message: "Fallback Remix turn recording failed because the repository is no longer inside a git repository.",
34372
- hint: status.hint || `Repo root: ${repoRoot}`
34373
- };
34374
- case "not_bound":
34375
- return {
34376
- message: "Fallback Remix turn recording failed because the repository is no longer bound to Remix.",
34377
- hint: status.hint || `Repo root: ${repoRoot}`
34378
- };
34379
- case "missing_head":
34380
- return {
34381
- message: "Fallback Remix turn recording failed because the repository HEAD could not be resolved.",
34382
- hint: status.hint || `Repo root: ${repoRoot}`
34383
- };
34384
- case "branch_mismatch":
34385
- return {
34386
- message: "Fallback Remix turn recording was blocked because the current checkout branch does not match the branch expected by the bound Remix lane.",
34387
- hint: status.hint || `Run \`remix_collab_status\` for ${repoRoot}, then switch back to the expected branch or refresh this branch's binding before recording or syncing.`
34388
- };
34389
- case "metadata_conflict":
34390
- return {
34391
- message: "Fallback Remix turn recording was blocked because local repository metadata conflicts with the bound Remix app.",
34392
- hint: status.hint || `Repo root: ${repoRoot}`
34393
- };
34394
- case "reconcile_required":
34395
- return {
34396
- message: "Fallback Remix turn recording was blocked because the repository must be reconciled before recording can continue safely.",
34397
- hint: status.hint || `Repo root: ${repoRoot}`
34398
- };
34399
- default:
34400
- return null;
34401
- }
34402
- }
34403
34152
  function buildRepoIdempotencyKey(turnId, repo) {
34404
34153
  const repoToken = repo.currentAppId?.trim() || repo.repoRoot;
34405
34154
  return `${turnId}:${repoToken}:finalize_turn`;
34406
34155
  }
34156
+ function isLegacyManualRecordingTool(toolName) {
34157
+ return /remix_collab_(add|add_change_step|record_turn|record_no_diff_turn)$/i.test(toolName ?? "");
34158
+ }
34407
34159
  function shouldSkipStopRecording(repo) {
34408
34160
  if (repo.stopRecorded) {
34409
34161
  return true;
@@ -34411,7 +34163,8 @@ function shouldSkipStopRecording(repo) {
34411
34163
  if (!repo.manuallyRecorded) {
34412
34164
  return false;
34413
34165
  }
34414
- if (repo.manualRecordingScope !== "full_turn") {
34166
+ const alreadyRecordedByCompatibleFlow = repo.manualRecordingScope === "full_turn" || isLegacyManualRecordingTool(repo.manuallyRecordedByTool);
34167
+ if (!alreadyRecordedByCompatibleFlow) {
34415
34168
  return false;
34416
34169
  }
34417
34170
  if (!repo.manuallyRecordedAt) {
@@ -34422,12 +34175,6 @@ function shouldSkipStopRecording(repo) {
34422
34175
  }
34423
34176
  return new Date(repo.lastObservedWriteAt).getTime() <= new Date(repo.manuallyRecordedAt).getTime();
34424
34177
  }
34425
- function hasNewObservedWriteSince(timestamp, repo) {
34426
- if (!repo.lastObservedWriteAt) {
34427
- return false;
34428
- }
34429
- return new Date(repo.lastObservedWriteAt).getTime() > new Date(timestamp).getTime();
34430
- }
34431
34178
  function createFallbackTouchedRepo(params) {
34432
34179
  const now = (/* @__PURE__ */ new Date()).toISOString();
34433
34180
  return {
@@ -34486,113 +34233,9 @@ async function recordTouchedRepo(params) {
34486
34233
  reason: "repo_not_bound",
34487
34234
  repoRoot: repo.repoRoot
34488
34235
  });
34489
- return false;
34490
- }
34491
- const workspaceDiff = await getWorkspaceDiff(repo.repoRoot);
34492
- if (workspaceDiff.diff.trim()) {
34493
- await appendHookDiagnosticsEvent({
34494
- hook,
34495
- sessionId,
34496
- turnId,
34497
- stage: "workspace_diff_checked",
34498
- result: "info",
34499
- repoRoot: repo.repoRoot,
34500
- fields: {
34501
- hasWorkspaceDiff: true,
34502
- diffLength: workspaceDiff.diff.length
34503
- }
34504
- });
34505
- if (repo.manualRemoteChangeRecordedAt && !hasNewObservedWriteSince(repo.manualRemoteChangeRecordedAt, repo)) {
34506
- await markTouchedRepoStopRecorded(sessionId, repo.repoRoot, { mode: "changed_turn" });
34507
- await appendHookDiagnosticsEvent({
34508
- hook,
34509
- sessionId,
34510
- turnId,
34511
- stage: "recording_skipped",
34512
- result: "success",
34513
- reason: "manual_recording_already_covers_diff",
34514
- repoRoot: repo.repoRoot
34515
- });
34516
- return true;
34517
- }
34518
- const recordingPreflight2 = await collabRecordingPreflight({
34519
- api,
34520
- cwd: repo.repoRoot
34521
- });
34522
- const blocked2 = getRecordingBlockedMessage(recordingPreflight2, repo.repoRoot);
34523
- if (blocked2) {
34524
- await markTouchedRepoRecordingFailure(sessionId, repo.repoRoot, blocked2);
34525
- await appendHookDiagnosticsEvent({
34526
- hook,
34527
- sessionId,
34528
- turnId,
34529
- stage: "recording_preflight",
34530
- result: "error",
34531
- reason: recordingPreflight2.status,
34532
- repoRoot: repo.repoRoot,
34533
- message: blocked2.message,
34534
- fields: {
34535
- hint: blocked2.hint
34536
- }
34537
- });
34538
- return false;
34539
- }
34540
- await collabAdd({
34541
- api,
34542
- cwd: repo.repoRoot,
34543
- prompt,
34544
- assistantResponse,
34545
- diffSource: "worktree",
34546
- idempotencyKey: buildRepoIdempotencyKey(turnId, repo),
34547
- actor: HOOK_ACTOR
34548
- });
34549
- await markTouchedRepoStopRecorded(sessionId, repo.repoRoot, { mode: "changed_turn" });
34550
- await appendHookDiagnosticsEvent({
34551
- hook,
34552
- sessionId,
34553
- turnId,
34554
- stage: "recording_completed",
34555
- result: "success",
34556
- reason: "changed_turn_recorded",
34557
- repoRoot: repo.repoRoot
34558
- });
34559
- return true;
34560
- }
34561
- await appendHookDiagnosticsEvent({
34562
- hook,
34563
- sessionId,
34564
- turnId,
34565
- stage: "workspace_diff_checked",
34566
- result: "info",
34567
- repoRoot: repo.repoRoot,
34568
- fields: {
34569
- hasWorkspaceDiff: false,
34570
- diffLength: 0
34571
- }
34572
- });
34573
- const recordingPreflight = await collabRecordingPreflight({
34574
- api,
34575
- cwd: repo.repoRoot
34576
- });
34577
- const blocked = getRecordingBlockedMessage(recordingPreflight, repo.repoRoot);
34578
- if (blocked) {
34579
- await markTouchedRepoRecordingFailure(sessionId, repo.repoRoot, blocked);
34580
- await appendHookDiagnosticsEvent({
34581
- hook,
34582
- sessionId,
34583
- turnId,
34584
- stage: "recording_preflight",
34585
- result: "error",
34586
- reason: recordingPreflight.status,
34587
- repoRoot: repo.repoRoot,
34588
- message: blocked.message,
34589
- fields: {
34590
- hint: blocked.hint
34591
- }
34592
- });
34593
- return false;
34236
+ return { recorded: false, queued: false };
34594
34237
  }
34595
- await collabRecordTurn({
34238
+ const result = await collabFinalizeTurn2({
34596
34239
  api,
34597
34240
  cwd: repo.repoRoot,
34598
34241
  prompt,
@@ -34600,17 +34243,17 @@ async function recordTouchedRepo(params) {
34600
34243
  idempotencyKey: buildRepoIdempotencyKey(turnId, repo),
34601
34244
  actor: HOOK_ACTOR
34602
34245
  });
34603
- await markTouchedRepoStopRecorded(sessionId, repo.repoRoot, { mode: "no_diff_turn" });
34246
+ await markTouchedRepoStopRecorded(sessionId, repo.repoRoot, { mode: result.mode });
34604
34247
  await appendHookDiagnosticsEvent({
34605
34248
  hook,
34606
34249
  sessionId,
34607
34250
  turnId,
34608
34251
  stage: "recording_completed",
34609
34252
  result: "success",
34610
- reason: "no_diff_turn_recorded",
34253
+ reason: result.mode,
34611
34254
  repoRoot: repo.repoRoot
34612
34255
  });
34613
- return true;
34256
+ return { recorded: true, queued: result.queued === true };
34614
34257
  } catch (error) {
34615
34258
  const details = getErrorDetails(error);
34616
34259
  await markTouchedRepoRecordingFailure(sessionId, repo.repoRoot, details);
@@ -34627,9 +34270,19 @@ async function recordTouchedRepo(params) {
34627
34270
  hint: details.hint
34628
34271
  }
34629
34272
  });
34630
- return false;
34273
+ return { recorded: false, queued: false };
34631
34274
  }
34632
34275
  }
34276
+ function spawnFinalizeQueueDrainer() {
34277
+ const entrypoint = process.argv[1];
34278
+ if (!entrypoint) return;
34279
+ const child = (0, import_node_child_process6.spawn)(process.execPath, [...process.execArgv, entrypoint, "--drain-finalize-queue"], {
34280
+ detached: true,
34281
+ stdio: "ignore",
34282
+ env: process.env
34283
+ });
34284
+ child.unref();
34285
+ }
34633
34286
  async function runHookStopCollab(payload) {
34634
34287
  const hook = "Stop";
34635
34288
  if (extractBoolean(payload, ["stop_hook_active"])) {
@@ -34776,8 +34429,11 @@ async function runHookStopCollab(payload) {
34776
34429
  }
34777
34430
  });
34778
34431
  let hadFailure = false;
34432
+ let queuedFinalizeWork = false;
34779
34433
  for (const repo of touchedRepos) {
34780
34434
  if (shouldSkipStopRecording(repo)) {
34435
+ const backupDrainQueued = repo.manuallyRecordedByTool === "remix_collab_finalize_turn" && repo.manualRecordingScope === "full_turn";
34436
+ queuedFinalizeWork = queuedFinalizeWork || backupDrainQueued;
34781
34437
  await appendHookDiagnosticsEvent({
34782
34438
  hook,
34783
34439
  sessionId,
@@ -34788,12 +34444,13 @@ async function runHookStopCollab(payload) {
34788
34444
  repoRoot: repo.repoRoot,
34789
34445
  fields: {
34790
34446
  manuallyRecorded: repo.manuallyRecorded,
34791
- stopRecorded: repo.stopRecorded
34447
+ stopRecorded: repo.stopRecorded,
34448
+ backupDrainQueued
34792
34449
  }
34793
34450
  });
34794
34451
  continue;
34795
34452
  }
34796
- const recorded = await recordTouchedRepo({
34453
+ const recording = await recordTouchedRepo({
34797
34454
  hook,
34798
34455
  sessionId,
34799
34456
  turnId: state.turnId,
@@ -34802,10 +34459,14 @@ async function runHookStopCollab(payload) {
34802
34459
  assistantResponse,
34803
34460
  api
34804
34461
  });
34805
- if (!recorded) {
34462
+ queuedFinalizeWork = queuedFinalizeWork || recording.queued;
34463
+ if (!recording.recorded) {
34806
34464
  hadFailure = true;
34807
34465
  }
34808
34466
  }
34467
+ if (queuedFinalizeWork) {
34468
+ spawnFinalizeQueueDrainer();
34469
+ }
34809
34470
  if (!hadFailure) {
34810
34471
  await clearPendingTurnState(sessionId);
34811
34472
  await appendHookDiagnosticsEvent({
@@ -34844,6 +34505,14 @@ async function runHookStopCollab(payload) {
34844
34505
  }
34845
34506
  }
34846
34507
  async function main() {
34508
+ if (process.argv.includes("--drain-finalize-queue")) {
34509
+ const api = await createHookCollabApiClient();
34510
+ const drainPendingFinalizeQueue2 = drainPendingFinalizeQueue;
34511
+ if (typeof drainPendingFinalizeQueue2 === "function") {
34512
+ await drainPendingFinalizeQueue2({ api });
34513
+ }
34514
+ return;
34515
+ }
34847
34516
  const payload = await readJsonStdin();
34848
34517
  await runHookStopCollab(payload);
34849
34518
  }