@remixhq/claude-plugin 0.1.22 → 0.1.24

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.
@@ -38,7 +38,7 @@ var require_windows = __commonJS({
38
38
  module2.exports = isexe;
39
39
  isexe.sync = sync;
40
40
  var fs11 = require("fs");
41
- function checkPathExt(path16, options) {
41
+ function checkPathExt(path17, 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 && path16.substr(-p.length).toLowerCase() === p) {
52
+ if (p && path17.substr(-p.length).toLowerCase() === p) {
53
53
  return true;
54
54
  }
55
55
  }
56
56
  return false;
57
57
  }
58
- function checkStat(stat, path16, options) {
58
+ function checkStat(stat, path17, options) {
59
59
  if (!stat.isSymbolicLink() && !stat.isFile()) {
60
60
  return false;
61
61
  }
62
- return checkPathExt(path16, options);
62
+ return checkPathExt(path17, options);
63
63
  }
64
- function isexe(path16, options, cb) {
65
- fs11.stat(path16, function(er, stat) {
66
- cb(er, er ? false : checkStat(stat, path16, options));
64
+ function isexe(path17, options, cb) {
65
+ fs11.stat(path17, function(er, stat) {
66
+ cb(er, er ? false : checkStat(stat, path17, options));
67
67
  });
68
68
  }
69
- function sync(path16, options) {
70
- return checkStat(fs11.statSync(path16), path16, options);
69
+ function sync(path17, options) {
70
+ return checkStat(fs11.statSync(path17), path17, options);
71
71
  }
72
72
  }
73
73
  });
@@ -79,13 +79,13 @@ var require_mode = __commonJS({
79
79
  module2.exports = isexe;
80
80
  isexe.sync = sync;
81
81
  var fs11 = require("fs");
82
- function isexe(path16, options, cb) {
83
- fs11.stat(path16, function(er, stat) {
82
+ function isexe(path17, options, cb) {
83
+ fs11.stat(path17, function(er, stat) {
84
84
  cb(er, er ? false : checkStat(stat, options));
85
85
  });
86
86
  }
87
- function sync(path16, options) {
88
- return checkStat(fs11.statSync(path16), options);
87
+ function sync(path17, options) {
88
+ return checkStat(fs11.statSync(path17), options);
89
89
  }
90
90
  function checkStat(stat, options) {
91
91
  return stat.isFile() && checkMode(stat, options);
@@ -119,7 +119,7 @@ var require_isexe = __commonJS({
119
119
  }
120
120
  module2.exports = isexe;
121
121
  isexe.sync = sync;
122
- function isexe(path16, options, cb) {
122
+ function isexe(path17, 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(path16, options || {}, function(er, is) {
132
+ isexe(path17, 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(path16, options || {}, function(er, is) {
141
+ core(path17, 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(path16, options) {
151
+ function sync(path17, options) {
152
152
  try {
153
- return core.sync(path16, options || {});
153
+ return core.sync(path17, 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 path16 = require("path");
170
+ var path17 = 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 = path16.join(pathPart, cmd);
208
+ const pCmd = path17.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 = path16.join(pathPart, cmd);
235
+ const pCmd = path17.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 path16 = require("path");
283
+ var path17 = 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 ? path16.delimiter : void 0
301
+ pathExt: withoutPathExt ? path17.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 = path16.resolve(hasCustomCwd ? parsed.options.cwd : "", resolved);
310
+ resolved = path17.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 [path16, argument] = match[0].replace(/#! ?/, "").split(" ");
365
- const binary = path16.split("/").pop();
364
+ const [path17, argument] = match[0].replace(/#! ?/, "").split(" ");
365
+ const binary = path17.split("/").pop();
366
366
  if (binary === "env") {
367
367
  return argument;
368
368
  }
@@ -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 path16 = require("path");
400
+ var path17 = 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 = path16.normalize(parsed.command);
425
+ parsed.command = path17.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 spawn4(command, args, options) {
515
+ function spawn5(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 = spawn4;
528
- module2.exports.spawn = spawn4;
527
+ module2.exports = spawn5;
528
+ module2.exports.spawn = spawn5;
529
529
  module2.exports.sync = spawnSync2;
530
530
  module2.exports._parse = parse;
531
531
  module2.exports._enoent = enoent;
@@ -538,21 +538,27 @@ __export(hook_user_prompt_exports, {
538
538
  runHookUserPrompt: () => runHookUserPrompt
539
539
  });
540
540
  module.exports = __toCommonJS(hook_user_prompt_exports);
541
- var import_node_child_process7 = require("child_process");
542
- var import_node_fs6 = require("fs");
543
- var import_node_path12 = __toESM(require("path"), 1);
541
+ var import_node_child_process8 = require("child_process");
542
+ var import_node_fs7 = require("fs");
543
+ var import_node_path13 = __toESM(require("path"), 1);
544
544
 
545
- // node_modules/@remixhq/core/dist/chunk-YZ34ICNN.js
545
+ // node_modules/@remixhq/core/dist/chunk-7XJGOKEO.js
546
546
  var RemixError = class extends Error {
547
547
  code;
548
548
  exitCode;
549
549
  hint;
550
+ // HTTP status code when this error originates from an API response.
551
+ // null for non-HTTP errors (validation, local IO, programming bugs).
552
+ // Callers use this to distinguish transient (5xx) from permanent (4xx)
553
+ // API failures without resorting to error-message string matching.
554
+ statusCode;
550
555
  constructor(message, opts) {
551
556
  super(message);
552
557
  this.name = "RemixError";
553
558
  this.code = opts?.code ?? null;
554
559
  this.exitCode = opts?.exitCode ?? 1;
555
560
  this.hint = opts?.hint ?? null;
561
+ this.statusCode = opts?.statusCode ?? null;
556
562
  }
557
563
  };
558
564
 
@@ -4937,13 +4943,13 @@ var logOutputSync = ({ serializedResult, fdNumber, state, verboseInfo, encoding,
4937
4943
  }
4938
4944
  };
4939
4945
  var writeToFiles = (serializedResult, stdioItems, outputFiles) => {
4940
- for (const { path: path16, append } of stdioItems.filter(({ type }) => FILE_TYPES.has(type))) {
4941
- const pathString = typeof path16 === "string" ? path16 : path16.toString();
4946
+ for (const { path: path17, append } of stdioItems.filter(({ type }) => FILE_TYPES.has(type))) {
4947
+ const pathString = typeof path17 === "string" ? path17 : path17.toString();
4942
4948
  if (append || outputFiles.has(pathString)) {
4943
- (0, import_node_fs4.appendFileSync)(path16, serializedResult);
4949
+ (0, import_node_fs4.appendFileSync)(path17, serializedResult);
4944
4950
  } else {
4945
4951
  outputFiles.add(pathString);
4946
- (0, import_node_fs4.writeFileSync)(path16, serializedResult);
4952
+ (0, import_node_fs4.writeFileSync)(path17, serializedResult);
4947
4953
  }
4948
4954
  }
4949
4955
  };
@@ -7331,7 +7337,7 @@ var {
7331
7337
  getCancelSignal: getCancelSignal2
7332
7338
  } = getIpcExport();
7333
7339
 
7334
- // node_modules/@remixhq/core/dist/chunk-WT6VRLXU.js
7340
+ // node_modules/@remixhq/core/dist/chunk-S4ECO35X.js
7335
7341
  async function runGit(args, cwd) {
7336
7342
  const res = await execa("git", args, { cwd, stderr: "ignore" });
7337
7343
  return String(res.stdout || "").trim();
@@ -7386,7 +7392,7 @@ function summarizeUnifiedDiff(diff) {
7386
7392
  return { changedFilesCount, insertions, deletions };
7387
7393
  }
7388
7394
 
7389
- // node_modules/@remixhq/core/dist/chunk-YCFLOHJV.js
7395
+ // node_modules/@remixhq/core/dist/chunk-DBVN42RF.js
7390
7396
  var import_promises12 = __toESM(require("fs/promises"), 1);
7391
7397
  var import_path = __toESM(require("path"), 1);
7392
7398
  var import_promises13 = __toESM(require("fs/promises"), 1);
@@ -7770,6 +7776,7 @@ var import_promises14 = __toESM(require("fs/promises"), 1);
7770
7776
  var import_node_os4 = __toESM(require("os"), 1);
7771
7777
  var import_node_path6 = __toESM(require("path"), 1);
7772
7778
  var DEFERRED_TURN_SCHEMA_VERSION = 1;
7779
+ var DEFERRED_TURN_MAX_ATTEMPTS = 10;
7773
7780
  var DEFERRED_TURN_TTL_MS = 24 * 60 * 60 * 1e3;
7774
7781
  var DEFERRED_TURN_DIR = "deferred-turns";
7775
7782
  function stateRoot() {
@@ -7779,6 +7786,28 @@ function stateRoot() {
7779
7786
  function getDeferredTurnDirPath() {
7780
7787
  return import_node_path6.default.join(stateRoot(), DEFERRED_TURN_DIR);
7781
7788
  }
7789
+ function deferredTurnFileName(sessionId, turnId) {
7790
+ const safe = (s) => s.replace(/[^A-Za-z0-9_-]/g, "_");
7791
+ return `${safe(sessionId)}-${safe(turnId)}.json`;
7792
+ }
7793
+ function getDeferredTurnFilePath(sessionId, turnId) {
7794
+ return import_node_path6.default.join(getDeferredTurnDirPath(), deferredTurnFileName(sessionId, turnId));
7795
+ }
7796
+ async function writeDeferredTurn(record) {
7797
+ if (record.schemaVersion !== DEFERRED_TURN_SCHEMA_VERSION) {
7798
+ throw new Error(`writeDeferredTurn: unsupported schemaVersion ${record.schemaVersion}`);
7799
+ }
7800
+ if (!record.prompt.trim() || !record.assistantResponse.trim()) {
7801
+ throw new Error("writeDeferredTurn: prompt and assistantResponse must be non-empty");
7802
+ }
7803
+ const dir = getDeferredTurnDirPath();
7804
+ await import_promises14.default.mkdir(dir, { recursive: true });
7805
+ const filePath = getDeferredTurnFilePath(record.sessionId, record.turnId);
7806
+ const tmpPath = `${filePath}.tmp-${process.pid}-${Date.now()}`;
7807
+ await import_promises14.default.writeFile(tmpPath, JSON.stringify(record), "utf8");
7808
+ await import_promises14.default.rename(tmpPath, filePath);
7809
+ return filePath;
7810
+ }
7782
7811
  async function readDeferredTurnFile(filePath) {
7783
7812
  const raw = await import_promises14.default.readFile(filePath, "utf8").catch(() => null);
7784
7813
  if (!raw) return null;
@@ -7791,7 +7820,7 @@ async function readDeferredTurnFile(filePath) {
7791
7820
  if (!parsed || typeof parsed !== "object") return null;
7792
7821
  const record = parsed;
7793
7822
  if (record.schemaVersion !== DEFERRED_TURN_SCHEMA_VERSION) return null;
7794
- if (typeof record.sessionId !== "string" || typeof record.turnId !== "string" || typeof record.repoRoot !== "string" || typeof record.prompt !== "string" || typeof record.assistantResponse !== "string" || typeof record.submittedAt !== "string" || typeof record.deferredAt !== "string" || record.reason !== "current_branch_unbound" && record.reason !== "recovery_in_progress") {
7823
+ if (typeof record.sessionId !== "string" || typeof record.turnId !== "string" || typeof record.repoRoot !== "string" || typeof record.prompt !== "string" || typeof record.assistantResponse !== "string" || typeof record.submittedAt !== "string" || typeof record.deferredAt !== "string" || record.reason !== "current_branch_unbound" && record.reason !== "recovery_in_progress" && record.reason !== "transient_recording_failure") {
7795
7824
  return null;
7796
7825
  }
7797
7826
  return {
@@ -7804,7 +7833,17 @@ async function readDeferredTurnFile(filePath) {
7804
7833
  submittedAt: record.submittedAt,
7805
7834
  deferredAt: record.deferredAt,
7806
7835
  reason: record.reason,
7807
- branchAtDefer: typeof record.branchAtDefer === "string" || record.branchAtDefer === null ? record.branchAtDefer : null
7836
+ branchAtDefer: typeof record.branchAtDefer === "string" || record.branchAtDefer === null ? record.branchAtDefer : null,
7837
+ // Additive fields: pre-appId-aware records on disk won't have these
7838
+ // keys at all. Coerce missing/invalid to `null` (drainer treats
7839
+ // null as "legacy, drain as today" — see drainer for the policy).
7840
+ appIdAtDefer: typeof record.appIdAtDefer === "string" ? record.appIdAtDefer : null,
7841
+ projectIdAtDefer: typeof record.projectIdAtDefer === "string" ? record.projectIdAtDefer : null,
7842
+ // Pre-attemptCount records coerce to 0 — they've never been
7843
+ // counted against the cap, so giving them the cap's full budget
7844
+ // is correct (we'd rather over-retry a legacy record than drop it
7845
+ // unexpectedly). Negative or non-finite values also coerce to 0.
7846
+ attemptCount: typeof record.attemptCount === "number" && Number.isFinite(record.attemptCount) && record.attemptCount >= 0 ? Math.floor(record.attemptCount) : 0
7808
7847
  };
7809
7848
  }
7810
7849
  async function listDeferredTurnsForRepo(repoRoot) {
@@ -7856,10 +7895,24 @@ async function pruneStaleDeferredTurns(maxAgeMs = DEFERRED_TURN_TTL_MS) {
7856
7895
  }
7857
7896
  return pruned;
7858
7897
  }
7898
+ async function recordDeferredTurnFailedAttempt(filePath) {
7899
+ const current = await readDeferredTurnFile(filePath);
7900
+ if (!current) {
7901
+ return { promoted: true, finalAttemptCount: DEFERRED_TURN_MAX_ATTEMPTS };
7902
+ }
7903
+ const newAttemptCount = current.attemptCount + 1;
7904
+ if (newAttemptCount >= DEFERRED_TURN_MAX_ATTEMPTS) {
7905
+ await deleteDeferredTurnFile(filePath);
7906
+ return { promoted: true, finalAttemptCount: newAttemptCount };
7907
+ }
7908
+ const next = { ...current, attemptCount: newAttemptCount };
7909
+ await writeDeferredTurn(next);
7910
+ return { promoted: false, newAttemptCount };
7911
+ }
7859
7912
 
7860
7913
  // src/deferred-turn-drainer.ts
7861
- var import_promises22 = __toESM(require("fs/promises"), 1);
7862
- var import_node_path9 = __toESM(require("path"), 1);
7914
+ var import_promises23 = __toESM(require("fs/promises"), 1);
7915
+ var import_node_path11 = __toESM(require("path"), 1);
7863
7916
  var import_node_crypto3 = require("crypto");
7864
7917
 
7865
7918
  // node_modules/@remixhq/core/dist/collab.js
@@ -7885,6 +7938,8 @@ function buildAppDeltaCacheKey(appId, payload) {
7885
7938
  appId,
7886
7939
  payload.baseHeadHash,
7887
7940
  payload.targetHeadHash ?? "",
7941
+ payload.baseRevisionId ?? "",
7942
+ payload.targetRevisionId ?? "",
7888
7943
  payload.localSnapshotHash ?? "",
7889
7944
  payload.repoFingerprint ?? "",
7890
7945
  payload.remoteUrl ?? "",
@@ -8131,11 +8186,11 @@ async function readLocalBaseline(params) {
8131
8186
  const raw = await import_promises16.default.readFile(getBaselinePath(params), "utf8");
8132
8187
  const parsed = JSON.parse(raw);
8133
8188
  if (!parsed || typeof parsed !== "object") return null;
8134
- if (parsed.schemaVersion !== 1 || typeof parsed.key !== "string" || typeof parsed.repoRoot !== "string") {
8189
+ if (![1, 2].includes(Number(parsed.schemaVersion)) || typeof parsed.key !== "string" || typeof parsed.repoRoot !== "string") {
8135
8190
  return null;
8136
8191
  }
8137
8192
  return {
8138
- schemaVersion: 1,
8193
+ schemaVersion: Number(parsed.schemaVersion) === 2 ? 2 : 1,
8139
8194
  key: parsed.key,
8140
8195
  repoRoot: parsed.repoRoot,
8141
8196
  repoFingerprint: parsed.repoFingerprint ?? null,
@@ -8144,6 +8199,8 @@ async function readLocalBaseline(params) {
8144
8199
  branchName: parsed.branchName ?? null,
8145
8200
  lastSnapshotId: parsed.lastSnapshotId ?? null,
8146
8201
  lastSnapshotHash: parsed.lastSnapshotHash ?? null,
8202
+ lastServerRevisionId: parsed.lastServerRevisionId ?? null,
8203
+ lastServerTreeHash: parsed.lastServerTreeHash ?? null,
8147
8204
  lastServerHeadHash: parsed.lastServerHeadHash ?? null,
8148
8205
  lastSeenLocalCommitHash: parsed.lastSeenLocalCommitHash ?? null,
8149
8206
  updatedAt: String(parsed.updatedAt ?? "")
@@ -8155,7 +8212,7 @@ async function readLocalBaseline(params) {
8155
8212
  async function writeLocalBaseline(baseline) {
8156
8213
  const key = buildLaneStateKey(baseline);
8157
8214
  const normalized = {
8158
- schemaVersion: 1,
8215
+ schemaVersion: 2,
8159
8216
  key,
8160
8217
  repoRoot: baseline.repoRoot,
8161
8218
  repoFingerprint: baseline.repoFingerprint ?? null,
@@ -8164,6 +8221,8 @@ async function writeLocalBaseline(baseline) {
8164
8221
  branchName: baseline.branchName ?? null,
8165
8222
  lastSnapshotId: baseline.lastSnapshotId ?? null,
8166
8223
  lastSnapshotHash: baseline.lastSnapshotHash ?? null,
8224
+ lastServerRevisionId: baseline.lastServerRevisionId ?? null,
8225
+ lastServerTreeHash: baseline.lastServerTreeHash ?? null,
8167
8226
  lastServerHeadHash: baseline.lastServerHeadHash ?? null,
8168
8227
  lastSeenLocalCommitHash: baseline.lastSeenLocalCommitHash ?? null,
8169
8228
  updatedAt: baseline.updatedAt ?? (/* @__PURE__ */ new Date()).toISOString()
@@ -8468,6 +8527,7 @@ function normalizeJob2(input) {
8468
8527
  prompt: input.prompt,
8469
8528
  assistantResponse: input.assistantResponse,
8470
8529
  baselineSnapshotId: input.baselineSnapshotId ?? null,
8530
+ baselineServerRevisionId: input.baselineServerRevisionId ?? null,
8471
8531
  baselineServerHeadHash: input.baselineServerHeadHash ?? null,
8472
8532
  currentSnapshotId: input.currentSnapshotId,
8473
8533
  capturedAt: input.capturedAt ?? now,
@@ -8502,6 +8562,7 @@ async function readPendingFinalizeJob(jobId) {
8502
8562
  prompt: String(parsed.prompt ?? ""),
8503
8563
  assistantResponse: String(parsed.assistantResponse ?? ""),
8504
8564
  baselineSnapshotId: parsed.baselineSnapshotId ?? null,
8565
+ baselineServerRevisionId: parsed.baselineServerRevisionId ?? null,
8505
8566
  baselineServerHeadHash: parsed.baselineServerHeadHash ?? null,
8506
8567
  currentSnapshotId: String(parsed.currentSnapshotId ?? ""),
8507
8568
  capturedAt: parsed.capturedAt,
@@ -8766,6 +8827,15 @@ function shouldRequireRemoteLaneForCurrentBranch(params) {
8766
8827
  if (params.currentBranch === defaultBranch) return false;
8767
8828
  return !params.binding.laneId || params.binding.currentAppId === params.binding.upstreamAppId;
8768
8829
  }
8830
+ function resolveLaneLookupProjectId(params) {
8831
+ const currentBranch = normalizeBranchName2(params.currentBranch);
8832
+ const defaultBranch = normalizeBranchName2(params.defaultBranch);
8833
+ const localProjectId = params.localBinding.projectId ?? null;
8834
+ if (currentBranch && currentBranch !== defaultBranch && localProjectId) {
8835
+ return localProjectId;
8836
+ }
8837
+ return params.explicitRootProjectId ?? (params.requireRemoteLane ? void 0 : localProjectId ?? params.fallbackProjectId ?? void 0);
8838
+ }
8769
8839
  async function persistResolvedLane(repoRoot, binding) {
8770
8840
  await writeCollabBinding(repoRoot, {
8771
8841
  projectId: binding.projectId,
@@ -8844,7 +8914,14 @@ async function resolveActiveLaneBindingUncached(params, state) {
8844
8914
  };
8845
8915
  }
8846
8916
  const laneResp2 = await params.api.resolveProjectLaneBinding({
8847
- projectId: state.explicitRootBinding?.projectId ?? (requireRemoteLane ? void 0 : localBinding.projectId ?? state.projectId ?? void 0),
8917
+ projectId: resolveLaneLookupProjectId({
8918
+ explicitRootProjectId: state.explicitRootBinding?.projectId,
8919
+ localBinding,
8920
+ currentBranch,
8921
+ defaultBranch: state.defaultBranch,
8922
+ requireRemoteLane,
8923
+ fallbackProjectId: state.projectId
8924
+ }),
8848
8925
  repoFingerprint: state.repoFingerprint ?? void 0,
8849
8926
  remoteUrl: state.remoteUrl ?? void 0,
8850
8927
  defaultBranch: state.defaultBranch ?? void 0,
@@ -9003,6 +9080,8 @@ function buildBaseState() {
9003
9080
  branchName: null,
9004
9081
  localCommitHash: null,
9005
9082
  currentSnapshotHash: null,
9083
+ currentServerRevisionId: null,
9084
+ currentServerTreeHash: null,
9006
9085
  currentServerHeadHash: null,
9007
9086
  currentServerHeadCommitId: null,
9008
9087
  worktreeClean: false,
@@ -9036,6 +9115,8 @@ function buildBaseState() {
9036
9115
  baseline: {
9037
9116
  lastSnapshotId: null,
9038
9117
  lastSnapshotHash: null,
9118
+ lastServerRevisionId: null,
9119
+ lastServerTreeHash: null,
9039
9120
  lastServerHeadHash: null,
9040
9121
  lastSeenLocalCommitHash: null
9041
9122
  }
@@ -9162,6 +9243,8 @@ async function collabDetectRepoState(params) {
9162
9243
  summarizeAsyncJobs({ repoRoot, branchName: binding.branchName ?? null })
9163
9244
  ]);
9164
9245
  const appHead = unwrapResponseObject(headResp, "app head");
9246
+ detected.currentServerRevisionId = appHead.headRevisionId ?? null;
9247
+ detected.currentServerTreeHash = appHead.treeHash ?? null;
9165
9248
  detected.currentServerHeadHash = appHead.headCommitHash;
9166
9249
  detected.currentServerHeadCommitId = appHead.headCommitId;
9167
9250
  detected.currentSnapshotHash = inspection.snapshotHash;
@@ -9170,6 +9253,8 @@ async function collabDetectRepoState(params) {
9170
9253
  detected.baseline = {
9171
9254
  lastSnapshotId: baseline?.lastSnapshotId ?? null,
9172
9255
  lastSnapshotHash: baseline?.lastSnapshotHash ?? null,
9256
+ lastServerRevisionId: baseline?.lastServerRevisionId ?? null,
9257
+ lastServerTreeHash: baseline?.lastServerTreeHash ?? null,
9173
9258
  lastServerHeadHash: baseline?.lastServerHeadHash ?? null,
9174
9259
  lastSeenLocalCommitHash: baseline?.lastSeenLocalCommitHash ?? null
9175
9260
  };
@@ -9179,6 +9264,7 @@ async function collabDetectRepoState(params) {
9179
9264
  const bootstrapResp = await params.api.getAppDelta(binding.currentAppId, {
9180
9265
  baseHeadHash: localCommitHash,
9181
9266
  targetHeadHash: appHead.headCommitHash,
9267
+ targetRevisionId: appHead.headRevisionId,
9182
9268
  repoFingerprint: binding.repoFingerprint ?? void 0,
9183
9269
  remoteUrl: binding.remoteUrl ?? void 0,
9184
9270
  defaultBranch: binding.defaultBranch ?? void 0
@@ -9201,7 +9287,7 @@ async function collabDetectRepoState(params) {
9201
9287
  }
9202
9288
  }
9203
9289
  detected.repoState = "external_local_base_changed";
9204
- detected.hint = "No local Remix baseline exists for this lane yet. Run `remix collab re-anchor` to anchor this checkout.";
9290
+ detected.hint = "No local Remix revision baseline exists for this lane yet. Run `remix collab init` or sync this lane to seed the baseline.";
9205
9291
  return detected;
9206
9292
  }
9207
9293
  const localHeadMovedSinceBaseline = Boolean(baseline.lastSeenLocalCommitHash) && localCommitHash !== baseline.lastSeenLocalCommitHash;
@@ -9220,7 +9306,30 @@ async function collabDetectRepoState(params) {
9220
9306
  return detected;
9221
9307
  }
9222
9308
  const localChanged = inspection.snapshotHash !== baseline.lastSnapshotHash;
9223
- const serverChanged = appHead.headCommitHash !== baseline.lastServerHeadHash;
9309
+ const serverHeadChanged = appHead.headCommitHash !== baseline.lastServerHeadHash;
9310
+ const revisionChanged = Boolean(
9311
+ baseline.lastServerRevisionId && (appHead.headRevisionId ?? null) !== baseline.lastServerRevisionId
9312
+ );
9313
+ const equivalentRevisionDrift = revisionChanged && !serverHeadChanged;
9314
+ if (equivalentRevisionDrift) {
9315
+ await writeLocalBaseline({
9316
+ repoRoot,
9317
+ repoFingerprint: binding.repoFingerprint,
9318
+ laneId: binding.laneId,
9319
+ currentAppId: binding.currentAppId,
9320
+ branchName: binding.branchName,
9321
+ lastSnapshotId: baseline.lastSnapshotId,
9322
+ lastSnapshotHash: baseline.lastSnapshotHash,
9323
+ lastServerRevisionId: appHead.headRevisionId ?? null,
9324
+ lastServerTreeHash: appHead.treeHash ?? baseline.lastServerTreeHash ?? null,
9325
+ lastServerHeadHash: appHead.headCommitHash,
9326
+ lastSeenLocalCommitHash: baseline.lastSeenLocalCommitHash
9327
+ });
9328
+ detected.baseline.lastServerRevisionId = appHead.headRevisionId ?? null;
9329
+ detected.baseline.lastServerTreeHash = appHead.treeHash ?? baseline.lastServerTreeHash ?? null;
9330
+ detected.baseline.lastServerHeadHash = appHead.headCommitHash;
9331
+ }
9332
+ const serverChanged = serverHeadChanged;
9224
9333
  if (!localChanged && !serverChanged) {
9225
9334
  detected.repoState = "idle";
9226
9335
  return detected;
@@ -9644,6 +9753,7 @@ function buildWorkspaceMetadata(params) {
9644
9753
  recordingMode: "boundary_delta",
9645
9754
  baselineSnapshotId: params.baselineSnapshotId,
9646
9755
  currentSnapshotId: params.currentSnapshotId,
9756
+ baselineServerRevisionId: params.baselineServerRevisionId ?? null,
9647
9757
  baselineServerHeadHash: params.baselineServerHeadHash,
9648
9758
  currentSnapshotHash: params.currentSnapshotHash,
9649
9759
  localCommitHash: params.localCommitHash,
@@ -9662,6 +9772,59 @@ function buildWorkspaceMetadata(params) {
9662
9772
  }
9663
9773
  return metadata;
9664
9774
  }
9775
+ async function findExistingChangeStepByIdempotency(params) {
9776
+ const idempotencyKey = params.idempotencyKey?.trim();
9777
+ if (!idempotencyKey) return null;
9778
+ const resp = await params.api.listChangeSteps(params.appId, { limit: 1, idempotencyKey });
9779
+ const responseObject = unwrapResponseObject(
9780
+ resp,
9781
+ "change step list"
9782
+ );
9783
+ const steps = Array.isArray(responseObject) ? responseObject : Array.isArray(responseObject.items) ? responseObject.items : [];
9784
+ return steps.find((step) => step.idempotencyKey === idempotencyKey) ?? null;
9785
+ }
9786
+ async function writeBaselineFromSucceededChangeStep(params) {
9787
+ const nextServerHeadHash = typeof params.changeStep.headCommitHash === "string" ? params.changeStep.headCommitHash.trim() : "";
9788
+ if (!nextServerHeadHash) {
9789
+ throw buildFinalizeCliError({
9790
+ message: "Backend returned a succeeded change step without a head commit hash.",
9791
+ exitCode: 1,
9792
+ hint: "This is a backend invariant violation; retry will not help. Run `remix collab status` before trying again.",
9793
+ disposition: "terminal",
9794
+ reason: "missing_head_commit_hash"
9795
+ });
9796
+ }
9797
+ let nextServerRevisionId = typeof params.changeStep.resultRevisionId === "string" ? params.changeStep.resultRevisionId.trim() : "";
9798
+ let nextServerTreeHash = null;
9799
+ if (!nextServerRevisionId) {
9800
+ const freshHeadResp = await params.api.getAppHead(params.job.currentAppId);
9801
+ const freshHead = unwrapResponseObject(freshHeadResp, "app head");
9802
+ if (freshHead.headCommitHash !== nextServerHeadHash || !freshHead.headRevisionId) {
9803
+ throw buildFinalizeCliError({
9804
+ message: "Backend returned a succeeded change step without a matching result revision.",
9805
+ exitCode: 1,
9806
+ hint: "The local baseline was not advanced because the post-step revision could not be verified. Restart the backend/CLI and retry after checking `remix collab status`.",
9807
+ disposition: "terminal",
9808
+ reason: "missing_result_revision_id"
9809
+ });
9810
+ }
9811
+ nextServerRevisionId = freshHead.headRevisionId;
9812
+ nextServerTreeHash = freshHead.treeHash ?? null;
9813
+ }
9814
+ await writeLocalBaseline({
9815
+ repoRoot: params.job.repoRoot,
9816
+ repoFingerprint: params.job.repoFingerprint,
9817
+ laneId: params.job.laneId,
9818
+ currentAppId: params.job.currentAppId,
9819
+ branchName: params.job.branchName,
9820
+ lastSnapshotId: params.snapshot.id,
9821
+ lastSnapshotHash: params.snapshot.snapshotHash,
9822
+ lastServerRevisionId: nextServerRevisionId,
9823
+ lastServerTreeHash: nextServerTreeHash,
9824
+ lastServerHeadHash: nextServerHeadHash,
9825
+ lastSeenLocalCommitHash: params.snapshot.localCommitHash
9826
+ });
9827
+ }
9665
9828
  async function harvestPreTurnEvents(repoRoot, fromCommit, toCommit) {
9666
9829
  if (!toCommit) return null;
9667
9830
  try {
@@ -9722,12 +9885,12 @@ async function processClaimedPendingFinalizeJobInner(params) {
9722
9885
  throw buildFinalizeCliError({
9723
9886
  message: "Local baseline is missing for this queued finalize job.",
9724
9887
  exitCode: 2,
9725
- hint: "Run `remix collab re-anchor` to anchor the repository again.",
9888
+ hint: "Run `remix collab init` to seed this checkout's revision baseline.",
9726
9889
  disposition: "terminal",
9727
9890
  reason: "baseline_missing"
9728
9891
  });
9729
9892
  }
9730
- const baselineDrifted = baseline.lastSnapshotId !== job.baselineSnapshotId || baseline.lastServerHeadHash !== job.baselineServerHeadHash;
9893
+ const baselineDrifted = baseline.lastSnapshotId !== job.baselineSnapshotId || (job.baselineServerRevisionId ? baseline.lastServerRevisionId !== job.baselineServerRevisionId : false) || baseline.lastServerHeadHash !== job.baselineServerHeadHash;
9731
9894
  const appHead = unwrapResponseObject(appHeadResp, "app head");
9732
9895
  const remoteUrl = readMetadataString(job, "remoteUrl");
9733
9896
  const defaultBranch = readMetadataString(job, "defaultBranch");
@@ -9750,12 +9913,13 @@ async function processClaimedPendingFinalizeJobInner(params) {
9750
9913
  throw buildFinalizeCliError({
9751
9914
  message: "Finalize queue baseline drifted before this job was processed.",
9752
9915
  exitCode: 1,
9753
- hint: "Process queued finalize jobs in capture order, or re-anchor the repository before retrying.",
9916
+ hint: "Process queued finalize jobs in capture order, or run `remix collab init` to refresh the revision baseline before retrying.",
9754
9917
  disposition: "terminal",
9755
9918
  reason: "baseline_drifted"
9756
9919
  });
9757
9920
  }
9758
- if (appHead.headCommitHash !== job.baselineServerHeadHash) {
9921
+ const serverStillAtBaseline = job.baselineServerRevisionId ? appHead.headRevisionId === job.baselineServerRevisionId : appHead.headCommitHash === job.baselineServerHeadHash;
9922
+ if (!serverStillAtBaseline) {
9759
9923
  throw buildFinalizeCliError({
9760
9924
  message: "Server lane changed before a no-diff turn could be recorded.",
9761
9925
  exitCode: 2,
@@ -9777,6 +9941,7 @@ async function processClaimedPendingFinalizeJobInner(params) {
9777
9941
  defaultBranch,
9778
9942
  baselineSnapshotId: job.baselineSnapshotId,
9779
9943
  currentSnapshotId: job.currentSnapshotId,
9944
+ baselineServerRevisionId: job.baselineServerRevisionId,
9780
9945
  baselineServerHeadHash: job.baselineServerHeadHash,
9781
9946
  currentSnapshotHash: snapshot.snapshotHash,
9782
9947
  localCommitHash: snapshot.localCommitHash,
@@ -9797,6 +9962,8 @@ async function processClaimedPendingFinalizeJobInner(params) {
9797
9962
  branchName: job.branchName,
9798
9963
  lastSnapshotId: snapshot.id,
9799
9964
  lastSnapshotHash: snapshot.snapshotHash,
9965
+ lastServerRevisionId: appHead.headRevisionId ?? null,
9966
+ lastServerTreeHash: appHead.treeHash ?? null,
9800
9967
  lastServerHeadHash: appHead.headCommitHash,
9801
9968
  lastSeenLocalCommitHash: snapshot.localCommitHash
9802
9969
  });
@@ -9817,14 +9984,14 @@ async function processClaimedPendingFinalizeJobInner(params) {
9817
9984
  };
9818
9985
  }
9819
9986
  const localBaselineAdvanced = baseline.lastSnapshotId !== job.baselineSnapshotId;
9820
- const serverHeadAdvanced = appHead.headCommitHash !== job.baselineServerHeadHash;
9987
+ const serverHeadAdvanced = job.baselineServerRevisionId ? appHead.headRevisionId !== job.baselineServerRevisionId : appHead.headCommitHash !== job.baselineServerHeadHash;
9821
9988
  if (baselineDrifted) {
9822
9989
  const consistentAdvance = localBaselineAdvanced && serverHeadAdvanced;
9823
9990
  if (!consistentAdvance) {
9824
9991
  throw buildFinalizeCliError({
9825
9992
  message: `Finalize queue baseline advanced inconsistently before this job was processed (localBaselineAdvanced=${localBaselineAdvanced}, serverHeadAdvanced=${serverHeadAdvanced}, jobBaselineSnapshotId=${job.baselineSnapshotId ?? "null"}, liveBaselineSnapshotId=${baseline.lastSnapshotId ?? "null"}, jobBaselineServerHeadHash=${job.baselineServerHeadHash ?? "null"}, liveBaselineServerHeadHash=${baseline.lastServerHeadHash ?? "null"}, currentAppHeadHash=${appHead.headCommitHash}). This indicates local Remix state diverged from the backend in a way that should not be reachable in normal operation; please report this as a bug.`,
9826
9993
  exitCode: 1,
9827
- hint: "Run `remix collab status` to inspect, then `remix collab re-anchor` only if the lane has no valid baseline.",
9994
+ hint: "Run `remix collab status` to inspect, then sync or reconcile before retrying.",
9828
9995
  disposition: "terminal",
9829
9996
  reason: "baseline_drifted"
9830
9997
  });
@@ -9832,6 +9999,7 @@ async function processClaimedPendingFinalizeJobInner(params) {
9832
9999
  }
9833
10000
  let submissionDiff = diffResult.diff;
9834
10001
  let submissionBaseHeadHash = job.baselineServerHeadHash;
10002
+ let submissionBaseRevisionId = job.baselineServerRevisionId;
9835
10003
  let replayedFromBaseHash = null;
9836
10004
  if (!submissionBaseHeadHash) {
9837
10005
  throw buildFinalizeCliError({
@@ -9842,6 +10010,34 @@ async function processClaimedPendingFinalizeJobInner(params) {
9842
10010
  });
9843
10011
  }
9844
10012
  const replayNeeded = appHead.headCommitHash !== submissionBaseHeadHash || baselineDrifted;
10013
+ if (replayNeeded) {
10014
+ const existingChangeStep = await findExistingChangeStepByIdempotency({
10015
+ api: params.api,
10016
+ appId: job.currentAppId,
10017
+ idempotencyKey: job.idempotencyKey
10018
+ });
10019
+ if (existingChangeStep) {
10020
+ const changeStep2 = existingChangeStep.status === "succeeded" ? existingChangeStep : await pollChangeStep(params.api, job.currentAppId, existingChangeStep.id);
10021
+ invalidateAppHeadCache(job.currentAppId);
10022
+ invalidateAppDeltaCacheForApp(job.currentAppId);
10023
+ await writeBaselineFromSucceededChangeStep({ api: params.api, job, snapshot, changeStep: changeStep2 });
10024
+ await updatePendingFinalizeJob(job.id, {
10025
+ status: "completed",
10026
+ metadata: { changeStepId: String(changeStep2.id ?? "") }
10027
+ });
10028
+ return {
10029
+ mode: "changed_turn",
10030
+ idempotencyKey: job.idempotencyKey ?? "",
10031
+ queued: false,
10032
+ jobId: job.id,
10033
+ repoState,
10034
+ changeStep: changeStep2,
10035
+ collabTurn: null,
10036
+ autoSync: null,
10037
+ warnings: []
10038
+ };
10039
+ }
10040
+ }
9845
10041
  if (replayNeeded) {
9846
10042
  try {
9847
10043
  const replayResp = await params.api.startChangeStepReplay(job.currentAppId, {
@@ -9849,7 +10045,9 @@ async function processClaimedPendingFinalizeJobInner(params) {
9849
10045
  assistantResponse: job.assistantResponse,
9850
10046
  diff: diffResult.diff,
9851
10047
  baseCommitHash: submissionBaseHeadHash,
10048
+ baseRevisionId: job.baselineServerRevisionId,
9852
10049
  targetHeadCommitHash: appHead.headCommitHash,
10050
+ targetRevisionId: appHead.headRevisionId,
9853
10051
  expectedPaths: diffResult.changedPaths,
9854
10052
  actor,
9855
10053
  workspaceMetadata: buildWorkspaceMetadata({
@@ -9859,6 +10057,7 @@ async function processClaimedPendingFinalizeJobInner(params) {
9859
10057
  defaultBranch,
9860
10058
  baselineSnapshotId: job.baselineSnapshotId,
9861
10059
  currentSnapshotId: job.currentSnapshotId,
10060
+ baselineServerRevisionId: job.baselineServerRevisionId,
9862
10061
  baselineServerHeadHash: job.baselineServerHeadHash,
9863
10062
  currentSnapshotHash: snapshot.snapshotHash,
9864
10063
  localCommitHash: snapshot.localCommitHash,
@@ -9884,6 +10083,7 @@ async function processClaimedPendingFinalizeJobInner(params) {
9884
10083
  submissionDiff = replayDiff.diff;
9885
10084
  replayedFromBaseHash = submissionBaseHeadHash;
9886
10085
  submissionBaseHeadHash = appHead.headCommitHash;
10086
+ submissionBaseRevisionId = appHead.headRevisionId;
9887
10087
  } catch (error) {
9888
10088
  if (error instanceof RemixError && error.finalizeDisposition === void 0) {
9889
10089
  const detail = error.hint ? `${error.message} (${error.hint})` : error.message;
@@ -9905,6 +10105,7 @@ async function processClaimedPendingFinalizeJobInner(params) {
9905
10105
  assistantResponse: job.assistantResponse,
9906
10106
  diff: submissionDiff,
9907
10107
  baseCommitHash: submissionBaseHeadHash,
10108
+ baseRevisionId: submissionBaseRevisionId,
9908
10109
  headCommitHash: submissionBaseHeadHash,
9909
10110
  changedFilesCount: diffResult.stats.changedFilesCount,
9910
10111
  insertions: diffResult.stats.insertions,
@@ -9917,6 +10118,7 @@ async function processClaimedPendingFinalizeJobInner(params) {
9917
10118
  defaultBranch,
9918
10119
  baselineSnapshotId: job.baselineSnapshotId,
9919
10120
  currentSnapshotId: job.currentSnapshotId,
10121
+ baselineServerRevisionId: job.baselineServerRevisionId,
9920
10122
  baselineServerHeadHash: job.baselineServerHeadHash,
9921
10123
  currentSnapshotHash: snapshot.snapshotHash,
9922
10124
  localCommitHash: snapshot.localCommitHash,
@@ -9933,27 +10135,7 @@ async function processClaimedPendingFinalizeJobInner(params) {
9933
10135
  const changeStep = await pollChangeStep(params.api, job.currentAppId, String(createdStep.id));
9934
10136
  invalidateAppHeadCache(job.currentAppId);
9935
10137
  invalidateAppDeltaCacheForApp(job.currentAppId);
9936
- const nextServerHeadHash = typeof changeStep.headCommitHash === "string" ? changeStep.headCommitHash.trim() : "";
9937
- if (!nextServerHeadHash) {
9938
- throw buildFinalizeCliError({
9939
- message: "Backend returned a succeeded change step without a head commit hash.",
9940
- exitCode: 1,
9941
- hint: "This is a backend invariant violation; retry will not help. Re-anchor and try again.",
9942
- disposition: "terminal",
9943
- reason: "missing_head_commit_hash"
9944
- });
9945
- }
9946
- await writeLocalBaseline({
9947
- repoRoot: job.repoRoot,
9948
- repoFingerprint: job.repoFingerprint,
9949
- laneId: job.laneId,
9950
- currentAppId: job.currentAppId,
9951
- branchName: job.branchName,
9952
- lastSnapshotId: snapshot.id,
9953
- lastSnapshotHash: snapshot.snapshotHash,
9954
- lastServerHeadHash: nextServerHeadHash,
9955
- lastSeenLocalCommitHash: snapshot.localCommitHash
9956
- });
10138
+ await writeBaselineFromSucceededChangeStep({ api: params.api, job, snapshot, changeStep });
9957
10139
  await updatePendingFinalizeJob(job.id, {
9958
10140
  status: "completed",
9959
10141
  metadata: { changeStepId: String(changeStep.id ?? "") }
@@ -9982,6 +10164,7 @@ async function enqueueCapturedFinalizeTurn(params) {
9982
10164
  prompt: params.prompt,
9983
10165
  assistantResponse: params.assistantResponse,
9984
10166
  baselineSnapshotId: params.baselineSnapshotId,
10167
+ baselineServerRevisionId: params.baselineServerRevisionId ?? null,
9985
10168
  baselineServerHeadHash: params.baselineServerHeadHash,
9986
10169
  currentSnapshotId: params.currentSnapshotId,
9987
10170
  idempotencyKey: params.idempotencyKey,
@@ -10080,17 +10263,6 @@ async function collabFinalizeTurn(params) {
10080
10263
  });
10081
10264
  }
10082
10265
  }
10083
- const pendingReAnchor = await findPendingAsyncJob({
10084
- repoRoot,
10085
- branchName: binding.branchName ?? null,
10086
- kind: "re_anchor"
10087
- });
10088
- if (pendingReAnchor) {
10089
- throw new RemixError("Cannot finalize a turn while a re-anchor is still processing.", {
10090
- exitCode: 2,
10091
- hint: `Re-anchor job ${pendingReAnchor.id} is still in the background queue. Run \`remix collab status\` to check progress.`
10092
- });
10093
- }
10094
10266
  const detected = await collabDetectRepoState({
10095
10267
  api: params.api,
10096
10268
  cwd: repoRoot,
@@ -10131,9 +10303,16 @@ async function collabFinalizeTurn(params) {
10131
10303
  hint: detected.hint
10132
10304
  });
10133
10305
  }
10306
+ if (detected.repoState === "both_changed") {
10307
+ throw new RemixError("Local and server changes must be reconciled before finalizing this turn.", {
10308
+ code: "reconcile_required",
10309
+ exitCode: 2,
10310
+ hint: detected.hint || "Run `remix collab reconcile --dry-run` to inspect recovery options before retrying."
10311
+ });
10312
+ }
10134
10313
  if (detected.repoState === "external_local_base_changed") {
10135
- throw new RemixError("The local checkout must be re-anchored before finalizing this turn.", {
10136
- code: "re_anchor_required",
10314
+ throw new RemixError("The local checkout is missing a Remix revision baseline for this lane.", {
10315
+ code: "baseline_missing",
10137
10316
  exitCode: 2,
10138
10317
  hint: detected.hint
10139
10318
  });
@@ -10145,8 +10324,9 @@ async function collabFinalizeTurn(params) {
10145
10324
  });
10146
10325
  if (!baseline) {
10147
10326
  throw new RemixError("Local Remix baseline is missing for this lane.", {
10327
+ code: "baseline_missing",
10148
10328
  exitCode: 2,
10149
- hint: "Run `remix collab re-anchor` to create a fresh baseline."
10329
+ hint: "Run `remix collab init` or sync this lane to create a fresh revision baseline."
10150
10330
  });
10151
10331
  }
10152
10332
  const snapshot = await captureLocalSnapshot({
@@ -10157,10 +10337,11 @@ async function collabFinalizeTurn(params) {
10157
10337
  });
10158
10338
  const mode = snapshot.snapshotHash === baseline.lastSnapshotHash ? "no_diff_turn" : "changed_turn";
10159
10339
  const idempotencyKey = params.idempotencyKey?.trim() || buildDeterministicIdempotencyKey({
10160
- kind: "collab_finalize_turn_boundary_v1",
10340
+ kind: "collab_finalize_turn_boundary_v2",
10161
10341
  appId: binding.currentAppId,
10162
10342
  laneId: binding.laneId,
10163
10343
  baselineSnapshotId: baseline.lastSnapshotId,
10344
+ baselineServerRevisionId: baseline.lastServerRevisionId,
10164
10345
  baselineServerHeadHash: baseline.lastServerHeadHash,
10165
10346
  currentSnapshotId: snapshot.id,
10166
10347
  currentSnapshotHash: snapshot.snapshotHash,
@@ -10180,6 +10361,7 @@ async function collabFinalizeTurn(params) {
10180
10361
  prompt,
10181
10362
  assistantResponse,
10182
10363
  baselineSnapshotId: baseline.lastSnapshotId,
10364
+ baselineServerRevisionId: baseline.lastServerRevisionId,
10183
10365
  baselineServerHeadHash: baseline.lastServerHeadHash,
10184
10366
  currentSnapshotId: snapshot.id,
10185
10367
  idempotencyKey,
@@ -10226,13 +10408,538 @@ var FINALIZE_PREFLIGHT_FAILURE_CODES = [
10226
10408
  // Server has commits we don't. Fix: `remix collab sync` (safe to
10227
10409
  // auto-run for fast-forward; non-FF refused by the command itself).
10228
10410
  "pull_required",
10229
- // Local base hash doesn't match the recorded baseline (force-push,
10230
- // hard reset, rebase). Fix: `remix collab re-anchor`.
10231
- "re_anchor_required"
10411
+ // Both local and server changed. Fix: inspect and apply reconcile.
10412
+ "reconcile_required",
10413
+ // Local revision baseline is missing. Fix: `remix collab init` or sync.
10414
+ "baseline_missing"
10232
10415
  ];
10233
10416
  var CODE_SET = new Set(FINALIZE_PREFLIGHT_FAILURE_CODES);
10417
+ function isFinalizePreflightFailureCode(value) {
10418
+ return typeof value === "string" && CODE_SET.has(value);
10419
+ }
10420
+
10421
+ // src/auto-fix-dispatcher.ts
10422
+ var import_node_child_process6 = require("child_process");
10423
+ var import_node_fs6 = require("fs");
10424
+ var import_node_path10 = __toESM(require("path"), 1);
10425
+
10426
+ // src/finalize-failure-marker.ts
10427
+ var import_promises19 = __toESM(require("fs/promises"), 1);
10428
+ var import_node_path7 = __toESM(require("path"), 1);
10429
+ var FINALIZE_FAILURE_MARKER_REL = import_node_path7.default.join(".remix", ".last-finalize-failure.json");
10430
+ function markerPath(repoRoot) {
10431
+ return import_node_path7.default.join(repoRoot, FINALIZE_FAILURE_MARKER_REL);
10432
+ }
10433
+ async function readFinalizeFailureMarker(repoRoot) {
10434
+ const raw = await import_promises19.default.readFile(markerPath(repoRoot), "utf8").catch(() => null);
10435
+ if (!raw) return null;
10436
+ try {
10437
+ const parsed = JSON.parse(raw);
10438
+ if (parsed.schemaVersion !== 1) return null;
10439
+ if (typeof parsed.repoRoot !== "string" || typeof parsed.failedAt !== "string") return null;
10440
+ if (typeof parsed.message !== "string") return null;
10441
+ return parsed;
10442
+ } catch {
10443
+ return null;
10444
+ }
10445
+ }
10446
+ async function writeFinalizeFailureMarker(marker) {
10447
+ const filePath = markerPath(marker.repoRoot);
10448
+ await import_promises19.default.mkdir(import_node_path7.default.dirname(filePath), { recursive: true });
10449
+ const tmpPath = `${filePath}.tmp-${process.pid}-${Date.now()}`;
10450
+ await import_promises19.default.writeFile(tmpPath, JSON.stringify(marker, null, 2), "utf8");
10451
+ await import_promises19.default.rename(tmpPath, filePath);
10452
+ }
10453
+ function buildFreshFailureMarker(params) {
10454
+ return {
10455
+ schemaVersion: 1,
10456
+ failedAt: (/* @__PURE__ */ new Date()).toISOString(),
10457
+ repoRoot: params.repoRoot,
10458
+ preflightCode: params.preflightCode,
10459
+ message: params.message,
10460
+ hint: params.hint,
10461
+ recommendedCommand: params.recommendedCommand,
10462
+ autoFix: {
10463
+ status: "not_attempted",
10464
+ command: null,
10465
+ pid: null,
10466
+ logPath: null,
10467
+ attemptedAt: null,
10468
+ failureMessage: null
10469
+ }
10470
+ };
10471
+ }
10472
+
10473
+ // src/hook-diagnostics.ts
10474
+ var import_node_crypto2 = require("crypto");
10475
+ var import_promises21 = __toESM(require("fs/promises"), 1);
10476
+ var import_node_os6 = __toESM(require("os"), 1);
10477
+ var import_node_path9 = __toESM(require("path"), 1);
10478
+
10479
+ // src/hook-state.ts
10480
+ var import_promises20 = __toESM(require("fs/promises"), 1);
10481
+ var import_node_os5 = __toESM(require("os"), 1);
10482
+ var import_node_path8 = __toESM(require("path"), 1);
10483
+ var import_node_crypto = require("crypto");
10484
+ function stateRoot2() {
10485
+ const configured = process.env.REMIX_CLAUDE_PLUGIN_HOOK_STATE_ROOT?.trim();
10486
+ return configured || import_node_path8.default.join(import_node_os5.default.tmpdir(), "remix-claude-plugin-hooks");
10487
+ }
10488
+ function statePath(sessionId) {
10489
+ return import_node_path8.default.join(stateRoot2(), `${sessionId}.json`);
10490
+ }
10491
+ function stateLockPath(sessionId) {
10492
+ return import_node_path8.default.join(stateRoot2(), `${sessionId}.lock`);
10493
+ }
10494
+ function stateLockMetaPath(sessionId) {
10495
+ return import_node_path8.default.join(stateLockPath(sessionId), "owner.json");
10496
+ }
10497
+ async function writeJsonAtomic2(filePath, value) {
10498
+ await import_promises20.default.mkdir(import_node_path8.default.dirname(filePath), { recursive: true });
10499
+ const tmpPath = `${filePath}.tmp-${Date.now()}-${Math.random().toString(16).slice(2)}`;
10500
+ await import_promises20.default.writeFile(tmpPath, JSON.stringify(value, null, 2) + "\n", "utf8");
10501
+ await import_promises20.default.rename(tmpPath, filePath);
10502
+ }
10503
+ var STATE_LOCK_WAIT_MS = 2e3;
10504
+ var STATE_LOCK_POLL_MS = 25;
10505
+ var STATE_LOCK_STALE_MS = 3e4;
10506
+ var STATE_LOCK_HEARTBEAT_MS = 5e3;
10507
+ async function sleep2(ms) {
10508
+ await new Promise((resolve) => setTimeout(resolve, ms));
10509
+ }
10510
+ async function readStateLockMetadata(sessionId) {
10511
+ const raw = await import_promises20.default.readFile(stateLockMetaPath(sessionId), "utf8").catch(() => null);
10512
+ if (!raw) return null;
10513
+ try {
10514
+ const parsed = JSON.parse(raw);
10515
+ if (typeof parsed.ownerId !== "string" || typeof parsed.pid !== "number" || typeof parsed.createdAt !== "string" || typeof parsed.heartbeatAt !== "string") {
10516
+ return null;
10517
+ }
10518
+ return {
10519
+ ownerId: parsed.ownerId,
10520
+ pid: parsed.pid,
10521
+ createdAt: parsed.createdAt,
10522
+ heartbeatAt: parsed.heartbeatAt
10523
+ };
10524
+ } catch {
10525
+ return null;
10526
+ }
10527
+ }
10528
+ async function writeStateLockMetadata(sessionId, metadata) {
10529
+ await writeJsonAtomic2(stateLockMetaPath(sessionId), metadata);
10530
+ }
10531
+ async function tryRemoveStaleStateLock(sessionId) {
10532
+ const lockPath = stateLockPath(sessionId);
10533
+ const metadata = await readStateLockMetadata(sessionId);
10534
+ const staleByHeartbeat = metadata && Date.now() - new Date(metadata.heartbeatAt).getTime() > STATE_LOCK_STALE_MS;
10535
+ if (staleByHeartbeat) {
10536
+ await import_promises20.default.rm(lockPath, { recursive: true, force: true }).catch(() => void 0);
10537
+ return true;
10538
+ }
10539
+ if (!metadata) {
10540
+ const lockStat = await import_promises20.default.stat(lockPath).catch(() => null);
10541
+ if (lockStat && Date.now() - lockStat.mtimeMs > STATE_LOCK_STALE_MS) {
10542
+ await import_promises20.default.rm(lockPath, { recursive: true, force: true }).catch(() => void 0);
10543
+ return true;
10544
+ }
10545
+ }
10546
+ return false;
10547
+ }
10548
+ async function acquireStateLock(sessionId) {
10549
+ const lockPath = stateLockPath(sessionId);
10550
+ const deadline = Date.now() + STATE_LOCK_WAIT_MS;
10551
+ await import_promises20.default.mkdir(stateRoot2(), { recursive: true });
10552
+ while (true) {
10553
+ try {
10554
+ await import_promises20.default.mkdir(lockPath);
10555
+ const ownerId = (0, import_node_crypto.randomUUID)();
10556
+ const createdAt = (/* @__PURE__ */ new Date()).toISOString();
10557
+ const metadata = {
10558
+ ownerId,
10559
+ pid: process.pid,
10560
+ createdAt,
10561
+ heartbeatAt: createdAt
10562
+ };
10563
+ await writeStateLockMetadata(sessionId, metadata);
10564
+ let released = false;
10565
+ const heartbeat = setInterval(() => {
10566
+ if (released) return;
10567
+ void writeStateLockMetadata(sessionId, {
10568
+ ...metadata,
10569
+ heartbeatAt: (/* @__PURE__ */ new Date()).toISOString()
10570
+ }).catch(() => void 0);
10571
+ }, STATE_LOCK_HEARTBEAT_MS);
10572
+ heartbeat.unref?.();
10573
+ return async () => {
10574
+ if (released) return;
10575
+ released = true;
10576
+ clearInterval(heartbeat);
10577
+ const currentMetadata = await readStateLockMetadata(sessionId);
10578
+ if (currentMetadata?.ownerId === ownerId) {
10579
+ await import_promises20.default.rm(lockPath, { recursive: true, force: true }).catch(() => void 0);
10580
+ }
10581
+ };
10582
+ } catch (error) {
10583
+ const code = error && typeof error === "object" && "code" in error ? error.code : null;
10584
+ if (code !== "EEXIST") {
10585
+ throw error;
10586
+ }
10587
+ if (await tryRemoveStaleStateLock(sessionId)) {
10588
+ continue;
10589
+ }
10590
+ if (Date.now() >= deadline) {
10591
+ throw new Error(`Timed out acquiring hook state lock for session ${sessionId}.`);
10592
+ }
10593
+ await sleep2(STATE_LOCK_POLL_MS);
10594
+ }
10595
+ }
10596
+ }
10597
+ async function withStateLock(sessionId, fn) {
10598
+ const release = await acquireStateLock(sessionId);
10599
+ try {
10600
+ return await fn();
10601
+ } finally {
10602
+ await release();
10603
+ }
10604
+ }
10605
+ async function savePendingTurnState(state) {
10606
+ await writeJsonAtomic2(statePath(state.sessionId), state);
10607
+ }
10608
+ async function createPendingTurnState(params) {
10609
+ return withStateLock(params.sessionId, async () => {
10610
+ const state = {
10611
+ sessionId: params.sessionId,
10612
+ turnId: (0, import_node_crypto.randomUUID)(),
10613
+ prompt: params.prompt,
10614
+ initialCwd: params.initialCwd?.trim() || null,
10615
+ intent: params.intent,
10616
+ submittedAt: (/* @__PURE__ */ new Date()).toISOString(),
10617
+ consultedMemory: false,
10618
+ touchedRepos: {},
10619
+ turnFailureMessage: null,
10620
+ turnFailureHint: null,
10621
+ turnFailedAt: null
10622
+ };
10623
+ await savePendingTurnState(state);
10624
+ return state;
10625
+ });
10626
+ }
10627
+
10628
+ // package.json
10629
+ var package_default = {
10630
+ name: "@remixhq/claude-plugin",
10631
+ version: "0.1.24",
10632
+ description: "Claude Code plugin for Remix collaboration workflows",
10633
+ homepage: "https://github.com/RemixDotOne/remix-claude-plugin",
10634
+ license: "MIT",
10635
+ repository: {
10636
+ type: "git",
10637
+ url: "https://github.com/RemixDotOne/remix-claude-plugin.git"
10638
+ },
10639
+ type: "module",
10640
+ engines: {
10641
+ node: ">=20"
10642
+ },
10643
+ publishConfig: {
10644
+ access: "public"
10645
+ },
10646
+ files: [
10647
+ "dist",
10648
+ ".claude-plugin/plugin.json",
10649
+ ".mcp.json",
10650
+ "skills",
10651
+ "hooks",
10652
+ "agents"
10653
+ ],
10654
+ exports: {
10655
+ ".": {
10656
+ types: "./dist/index.d.ts",
10657
+ import: "./dist/index.js"
10658
+ }
10659
+ },
10660
+ scripts: {
10661
+ build: "tsup",
10662
+ postbuild: `node -e "const fs=require('node:fs'); for (const p of ['dist/mcp-server.cjs','dist/hook-pre-git.cjs','dist/hook-user-prompt.cjs','dist/hook-post-collab.cjs','dist/hook-stop-collab.cjs']) fs.chmodSync(p, 0o755);"`,
10663
+ dev: "tsx src/mcp-server.ts",
10664
+ typecheck: "tsc -p tsconfig.json --noEmit",
10665
+ test: "node --import tsx --test 'src/**/*.test.ts'",
10666
+ prepack: "npm run build"
10667
+ },
10668
+ dependencies: {
10669
+ "@remixhq/core": "^0.1.19",
10670
+ "@remixhq/mcp": "^0.1.19"
10671
+ },
10672
+ devDependencies: {
10673
+ "@types/node": "^25.4.0",
10674
+ tsup: "^8.5.1",
10675
+ tsx: "^4.21.0",
10676
+ typescript: "^5.9.3"
10677
+ }
10678
+ };
10679
+
10680
+ // src/metadata.ts
10681
+ var pluginMetadata = {
10682
+ name: package_default.name,
10683
+ version: package_default.version,
10684
+ description: package_default.description,
10685
+ pluginId: "remix",
10686
+ agentName: "remix-collab"
10687
+ };
10688
+
10689
+ // src/hook-diagnostics.ts
10690
+ var MAX_LOG_BYTES = 512 * 1024;
10691
+ function resolveClaudeRoot() {
10692
+ const configured = process.env.CLAUDE_CONFIG_DIR?.trim();
10693
+ return configured || import_node_path9.default.join(import_node_os6.default.homedir(), ".claude");
10694
+ }
10695
+ function resolvePluginDataDirName() {
10696
+ return `${pluginMetadata.pluginId}-${pluginMetadata.pluginId}`;
10697
+ }
10698
+ function getHookDiagnosticsDirPath() {
10699
+ const configured = process.env.REMIX_CLAUDE_PLUGIN_HOOK_DIAGNOSTICS_DIR?.trim();
10700
+ return configured || import_node_path9.default.join(resolveClaudeRoot(), "plugins", "data", resolvePluginDataDirName());
10701
+ }
10702
+ function getHookDiagnosticsLogPath() {
10703
+ return import_node_path9.default.join(getHookDiagnosticsDirPath(), "hooks.ndjson");
10704
+ }
10705
+ function toFieldValue(value) {
10706
+ if (value === null) return null;
10707
+ if (typeof value === "string") return value;
10708
+ if (typeof value === "number" && Number.isFinite(value)) return value;
10709
+ if (typeof value === "boolean") return value;
10710
+ return void 0;
10711
+ }
10712
+ function normalizeFields(fields) {
10713
+ if (!fields) return {};
10714
+ const normalizedEntries = Object.entries(fields).map(([key, value]) => {
10715
+ const normalized = toFieldValue(value);
10716
+ return normalized === void 0 ? null : [key, normalized];
10717
+ }).filter((entry) => entry !== null);
10718
+ return Object.fromEntries(normalizedEntries);
10719
+ }
10720
+ async function rotateLogIfNeeded(logPath) {
10721
+ const stat = await import_promises21.default.stat(logPath).catch(() => null);
10722
+ if (!stat || stat.size < MAX_LOG_BYTES) {
10723
+ return;
10724
+ }
10725
+ const rotatedPath = `${logPath}.1`;
10726
+ await import_promises21.default.rm(rotatedPath, { force: true }).catch(() => void 0);
10727
+ await import_promises21.default.rename(logPath, rotatedPath).catch(() => void 0);
10728
+ }
10729
+ function summarizeText(value) {
10730
+ if (typeof value !== "string" || !value.trim()) {
10731
+ return {
10732
+ present: false,
10733
+ length: 0,
10734
+ sha256Prefix: null
10735
+ };
10736
+ }
10737
+ const trimmed = value.trim();
10738
+ return {
10739
+ present: true,
10740
+ length: trimmed.length,
10741
+ sha256Prefix: (0, import_node_crypto2.createHash)("sha256").update(trimmed).digest("hex").slice(0, 12)
10742
+ };
10743
+ }
10744
+ async function appendHookDiagnosticsEvent(params) {
10745
+ try {
10746
+ const logPath = getHookDiagnosticsLogPath();
10747
+ await import_promises21.default.mkdir(import_node_path9.default.dirname(logPath), { recursive: true });
10748
+ await rotateLogIfNeeded(logPath);
10749
+ const event = {
10750
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
10751
+ hook: params.hook,
10752
+ pluginVersion: pluginMetadata.version,
10753
+ pid: process.pid,
10754
+ sessionId: params.sessionId?.trim() || null,
10755
+ turnId: params.turnId?.trim() || null,
10756
+ stage: params.stage.trim(),
10757
+ result: params.result,
10758
+ reason: params.reason?.trim() || null,
10759
+ toolName: params.toolName?.trim() || null,
10760
+ repoRoot: params.repoRoot?.trim() || null,
10761
+ message: params.message?.trim() || null,
10762
+ fields: normalizeFields(params.fields)
10763
+ };
10764
+ await import_promises21.default.appendFile(logPath, `${JSON.stringify(event)}
10765
+ `, "utf8");
10766
+ } catch {
10767
+ }
10768
+ }
10769
+
10770
+ // src/auto-fix-dispatcher.ts
10771
+ var AUTO_FIX_COMMAND = {
10772
+ // Already auto-spawned by hook-user-prompt's branch-init path, but we
10773
+ // include it here too so a finalize-time failure (e.g. binding got
10774
+ // deleted between init and the next finalize) also self-heals.
10775
+ branch_binding_missing: ["collab", "init"],
10776
+ // Local revision baseline is missing. Init seeds the branch/lane baseline
10777
+ // without requiring the user to know about the recording internals.
10778
+ baseline_missing: ["collab", "init"],
10779
+ // Server moved ahead. `collab sync` is fast-forward-safe by default;
10780
+ // it refuses non-FF on its own, so we don't need to gate here.
10781
+ pull_required: ["collab", "sync"]
10782
+ };
10783
+ var RECOMMENDED_USER_COMMAND = {
10784
+ not_bound: "remix collab init",
10785
+ branch_binding_missing: "remix collab init",
10786
+ family_ambiguous: "remix collab status",
10787
+ metadata_conflict: "remix collab status",
10788
+ branch_mismatch: "remix collab status",
10789
+ missing_head: "remix collab status",
10790
+ remote_error: "remix collab status",
10791
+ pull_required: "remix collab sync",
10792
+ baseline_missing: "remix collab init"
10793
+ };
10794
+ var SPAWN_LOCK_REL = (cmdSlug) => import_node_path10.default.join(".remix", `.${cmdSlug}-spawning`);
10795
+ var SPAWN_LOG_REL = (cmdSlug) => import_node_path10.default.join(".remix", `${cmdSlug}.log`);
10796
+ var SPAWN_THROTTLE_MS = 5 * 60 * 1e3;
10797
+ function commandSlug(args) {
10798
+ return args.join("-").replace(/[^a-zA-Z0-9_-]/g, "_");
10799
+ }
10800
+ function spawnFixDetached(repoRoot, args) {
10801
+ const slug = commandSlug(args);
10802
+ const command = `remix ${args.join(" ")}`;
10803
+ const remixDir = import_node_path10.default.join(repoRoot, ".remix");
10804
+ const lockPath = import_node_path10.default.join(repoRoot, SPAWN_LOCK_REL(slug));
10805
+ const logPath = import_node_path10.default.join(repoRoot, SPAWN_LOG_REL(slug));
10806
+ try {
10807
+ if ((0, import_node_fs6.existsSync)(lockPath)) {
10808
+ const ageMs = Date.now() - (0, import_node_fs6.statSync)(lockPath).mtimeMs;
10809
+ if (ageMs < SPAWN_THROTTLE_MS) {
10810
+ return { kind: "spawn_throttled", command, reason: "spawn_lock_held" };
10811
+ }
10812
+ }
10813
+ } catch {
10814
+ }
10815
+ try {
10816
+ (0, import_node_fs6.mkdirSync)(remixDir, { recursive: true });
10817
+ } catch {
10818
+ }
10819
+ let out;
10820
+ let err;
10821
+ try {
10822
+ out = (0, import_node_fs6.openSync)(logPath, "a");
10823
+ err = (0, import_node_fs6.openSync)(logPath, "a");
10824
+ } catch (logErr) {
10825
+ return {
10826
+ kind: "spawn_failed",
10827
+ command,
10828
+ reason: "log_open_failed",
10829
+ message: logErr instanceof Error ? logErr.message : String(logErr)
10830
+ };
10831
+ }
10832
+ try {
10833
+ const child = (0, import_node_child_process6.spawn)("remix", [...args], {
10834
+ cwd: repoRoot,
10835
+ detached: true,
10836
+ stdio: ["ignore", out, err],
10837
+ env: { ...process.env, REMIX_AUTO_FIX_SPAWN: "1" }
10838
+ });
10839
+ child.unref();
10840
+ try {
10841
+ (0, import_node_fs6.writeFileSync)(lockPath, String(child.pid ?? ""), "utf8");
10842
+ (0, import_node_fs6.utimesSync)(lockPath, /* @__PURE__ */ new Date(), /* @__PURE__ */ new Date());
10843
+ } catch {
10844
+ }
10845
+ return { kind: "spawned", command, pid: child.pid, logPath };
10846
+ } catch (spawnErr) {
10847
+ return {
10848
+ kind: "spawn_failed",
10849
+ command,
10850
+ reason: "spawn_failed",
10851
+ message: spawnErr instanceof Error ? spawnErr.message : String(spawnErr)
10852
+ };
10853
+ }
10854
+ }
10855
+ async function dispatchFinalizeFailure(input) {
10856
+ const recommendedCommand = input.preflightCode ? RECOMMENDED_USER_COMMAND[input.preflightCode] ?? null : null;
10857
+ const marker = buildFreshFailureMarker({
10858
+ repoRoot: input.repoRoot,
10859
+ preflightCode: input.preflightCode,
10860
+ message: input.message,
10861
+ hint: input.hint,
10862
+ recommendedCommand
10863
+ });
10864
+ let outcome;
10865
+ const autoFixArgs = input.preflightCode ? AUTO_FIX_COMMAND[input.preflightCode] : void 0;
10866
+ if (!autoFixArgs) {
10867
+ outcome = {
10868
+ kind: "warn_only",
10869
+ reason: input.preflightCode ? "no_auto_fix_for_code" : "unknown_code"
10870
+ };
10871
+ } else {
10872
+ outcome = spawnFixDetached(input.repoRoot, autoFixArgs);
10873
+ marker.autoFix = mergeOutcomeIntoMarker(marker.autoFix, outcome);
10874
+ }
10875
+ try {
10876
+ await writeFinalizeFailureMarker(marker);
10877
+ } catch (writeErr) {
10878
+ await appendHookDiagnosticsEvent({
10879
+ hook: input.hook,
10880
+ sessionId: input.sessionId,
10881
+ turnId: input.turnId ?? void 0,
10882
+ stage: "finalize_failure_marker_write_failed",
10883
+ result: "error",
10884
+ reason: "exception",
10885
+ repoRoot: input.repoRoot,
10886
+ message: writeErr instanceof Error ? writeErr.message : String(writeErr)
10887
+ });
10888
+ }
10889
+ await appendHookDiagnosticsEvent({
10890
+ hook: input.hook,
10891
+ sessionId: input.sessionId,
10892
+ turnId: input.turnId ?? void 0,
10893
+ stage: "auto_fix_dispatched",
10894
+ result: outcome.kind === "spawned" ? "success" : outcome.kind === "warn_only" ? "info" : "error",
10895
+ reason: outcome.kind,
10896
+ repoRoot: input.repoRoot,
10897
+ fields: {
10898
+ preflightCode: input.preflightCode,
10899
+ command: "command" in outcome ? outcome.command : null,
10900
+ pid: outcome.kind === "spawned" ? outcome.pid ?? null : null,
10901
+ logPath: outcome.kind === "spawned" ? outcome.logPath : null,
10902
+ recommendedCommand
10903
+ },
10904
+ message: outcome.kind === "spawn_failed" ? outcome.message : null
10905
+ });
10906
+ return outcome;
10907
+ }
10908
+ function mergeOutcomeIntoMarker(existing, outcome) {
10909
+ if (outcome.kind === "spawned") {
10910
+ return {
10911
+ status: "in_progress",
10912
+ command: outcome.command,
10913
+ pid: outcome.pid ?? null,
10914
+ logPath: outcome.logPath,
10915
+ attemptedAt: (/* @__PURE__ */ new Date()).toISOString(),
10916
+ failureMessage: null
10917
+ };
10918
+ }
10919
+ if (outcome.kind === "spawn_throttled") {
10920
+ return {
10921
+ status: "in_progress",
10922
+ command: outcome.command,
10923
+ pid: existing.pid,
10924
+ logPath: existing.logPath,
10925
+ attemptedAt: existing.attemptedAt,
10926
+ failureMessage: null
10927
+ };
10928
+ }
10929
+ if (outcome.kind === "spawn_failed") {
10930
+ return {
10931
+ status: "spawn_failed",
10932
+ command: outcome.command,
10933
+ pid: null,
10934
+ logPath: null,
10935
+ attemptedAt: (/* @__PURE__ */ new Date()).toISOString(),
10936
+ failureMessage: outcome.message
10937
+ };
10938
+ }
10939
+ return existing;
10940
+ }
10234
10941
 
10235
- // node_modules/@remixhq/core/dist/chunk-US5SM7ZC.js
10942
+ // node_modules/@remixhq/core/dist/chunk-C2FOZ3O7.js
10236
10943
  async function readJsonSafe(res) {
10237
10944
  const ct = res.headers.get("content-type") ?? "";
10238
10945
  if (!ct.toLowerCase().includes("application/json")) return null;
@@ -10245,8 +10952,13 @@ async function readJsonSafe(res) {
10245
10952
  function createApiClient(config, opts) {
10246
10953
  const apiKey = (opts?.apiKey ?? "").trim();
10247
10954
  const tokenProvider = opts?.tokenProvider;
10955
+ const defaultTimeoutMs = typeof opts?.defaultRequestTimeoutMs === "number" && opts.defaultRequestTimeoutMs > 0 ? opts.defaultRequestTimeoutMs : null;
10248
10956
  const CLIENT_KEY_HEADER = "x-comerge-api-key";
10249
- async function request(path16, init) {
10957
+ function makeTimeoutSignal(timeoutMs) {
10958
+ const ms = typeof timeoutMs === "number" && timeoutMs > 0 ? timeoutMs : defaultTimeoutMs;
10959
+ return ms != null ? AbortSignal.timeout(ms) : void 0;
10960
+ }
10961
+ async function request(path17, init, opts2) {
10250
10962
  if (!tokenProvider) {
10251
10963
  throw new RemixError("API client is missing a token provider.", {
10252
10964
  exitCode: 1,
@@ -10254,9 +10966,10 @@ function createApiClient(config, opts) {
10254
10966
  });
10255
10967
  }
10256
10968
  const auth = await tokenProvider();
10257
- const url = new URL(path16, config.apiUrl).toString();
10969
+ const url = new URL(path17, config.apiUrl).toString();
10258
10970
  const doFetch = async (bearer) => fetch(url, {
10259
10971
  ...init,
10972
+ signal: makeTimeoutSignal(opts2?.timeoutMs),
10260
10973
  headers: {
10261
10974
  Accept: "application/json",
10262
10975
  "Content-Type": "application/json",
@@ -10273,12 +10986,16 @@ function createApiClient(config, opts) {
10273
10986
  if (!res.ok) {
10274
10987
  const body = await readJsonSafe(res);
10275
10988
  const msg = (body && typeof body === "object" && body && "message" in body && typeof body.message === "string" ? body.message : null) ?? `Request failed (status ${res.status})`;
10276
- throw new RemixError(msg, { exitCode: 1, hint: body ? JSON.stringify(body, null, 2) : null });
10989
+ throw new RemixError(msg, {
10990
+ exitCode: 1,
10991
+ hint: body ? JSON.stringify(body, null, 2) : null,
10992
+ statusCode: res.status
10993
+ });
10277
10994
  }
10278
10995
  const json = await readJsonSafe(res);
10279
10996
  return json ?? null;
10280
10997
  }
10281
- async function requestBinary(path16, init) {
10998
+ async function requestBinary(path17, init, opts2) {
10282
10999
  if (!tokenProvider) {
10283
11000
  throw new RemixError("API client is missing a token provider.", {
10284
11001
  exitCode: 1,
@@ -10286,9 +11003,10 @@ function createApiClient(config, opts) {
10286
11003
  });
10287
11004
  }
10288
11005
  const auth = await tokenProvider();
10289
- const url = new URL(path16, config.apiUrl).toString();
11006
+ const url = new URL(path17, config.apiUrl).toString();
10290
11007
  const doFetch = async (bearer) => fetch(url, {
10291
11008
  ...init,
11009
+ signal: makeTimeoutSignal(opts2?.timeoutMs),
10292
11010
  headers: {
10293
11011
  Accept: "*/*",
10294
11012
  ...init?.headers ?? {},
@@ -10304,7 +11022,11 @@ function createApiClient(config, opts) {
10304
11022
  if (!res.ok) {
10305
11023
  const body = await readJsonSafe(res);
10306
11024
  const msg = (body && typeof body === "object" && body && "message" in body && typeof body.message === "string" ? body.message : null) ?? `Request failed (status ${res.status})`;
10307
- throw new RemixError(msg, { exitCode: 1, hint: body ? JSON.stringify(body, null, 2) : null });
11025
+ throw new RemixError(msg, {
11026
+ exitCode: 1,
11027
+ hint: body ? JSON.stringify(body, null, 2) : null,
11028
+ statusCode: res.status
11029
+ });
10308
11030
  }
10309
11031
  const contentDisposition = res.headers.get("content-disposition") ?? "";
10310
11032
  const fileNameMatch = contentDisposition.match(/filename=\"([^\"]+)\"/i);
@@ -10411,6 +11133,14 @@ function createApiClient(config, opts) {
10411
11133
  method: "POST",
10412
11134
  body: JSON.stringify(payload)
10413
11135
  }),
11136
+ listChangeSteps: (appId, params) => {
11137
+ const qs = new URLSearchParams();
11138
+ if (params?.limit !== void 0) qs.set("limit", String(params.limit));
11139
+ if (params?.offset !== void 0) qs.set("offset", String(params.offset));
11140
+ if (params?.idempotencyKey) qs.set("idempotencyKey", params.idempotencyKey);
11141
+ const suffix = qs.toString() ? `?${qs.toString()}` : "";
11142
+ return request(`/v1/apps/${encodeURIComponent(appId)}/change-steps${suffix}`, { method: "GET" });
11143
+ },
10414
11144
  createCollabTurn: (appId, payload) => request(`/v1/apps/${encodeURIComponent(appId)}/collab-turns`, {
10415
11145
  method: "POST",
10416
11146
  body: JSON.stringify(payload)
@@ -11136,8 +11866,8 @@ function getErrorMap() {
11136
11866
 
11137
11867
  // node_modules/zod/v3/helpers/parseUtil.js
11138
11868
  var makeIssue = (params) => {
11139
- const { data, path: path16, errorMaps, issueData } = params;
11140
- const fullPath = [...path16, ...issueData.path || []];
11869
+ const { data, path: path17, errorMaps, issueData } = params;
11870
+ const fullPath = [...path17, ...issueData.path || []];
11141
11871
  const fullIssue = {
11142
11872
  ...issueData,
11143
11873
  path: fullPath
@@ -11253,11 +11983,11 @@ var errorUtil;
11253
11983
 
11254
11984
  // node_modules/zod/v3/types.js
11255
11985
  var ParseInputLazyPath = class {
11256
- constructor(parent, value, path16, key) {
11986
+ constructor(parent, value, path17, key) {
11257
11987
  this._cachedPath = [];
11258
11988
  this.parent = parent;
11259
11989
  this.data = value;
11260
- this._path = path16;
11990
+ this._path = path17;
11261
11991
  this._key = key;
11262
11992
  }
11263
11993
  get path() {
@@ -14699,8 +15429,8 @@ var coerce = {
14699
15429
  };
14700
15430
  var NEVER = INVALID;
14701
15431
 
14702
- // node_modules/@remixhq/core/dist/chunk-P6JHXOV4.js
14703
- var import_promises19 = __toESM(require("fs/promises"), 1);
15432
+ // node_modules/@remixhq/core/dist/chunk-XETDXVGM.js
15433
+ var import_promises22 = __toESM(require("fs/promises"), 1);
14704
15434
  var import_os3 = __toESM(require("os"), 1);
14705
15435
  var import_path7 = __toESM(require("path"), 1);
14706
15436
 
@@ -15108,7 +15838,7 @@ var PostgrestError = class extends Error {
15108
15838
  };
15109
15839
  }
15110
15840
  };
15111
- function sleep2(ms, signal) {
15841
+ function sleep3(ms, signal) {
15112
15842
  return new Promise((resolve) => {
15113
15843
  if (signal === null || signal === void 0 ? void 0 : signal.aborted) {
15114
15844
  resolve();
@@ -15304,7 +16034,7 @@ var PostgrestBuilder = class {
15304
16034
  if (_this.retryEnabled && attemptCount < DEFAULT_MAX_RETRIES) {
15305
16035
  const delay = getRetryDelay(attemptCount);
15306
16036
  attemptCount++;
15307
- await sleep2(delay, _this.signal);
16037
+ await sleep3(delay, _this.signal);
15308
16038
  continue;
15309
16039
  }
15310
16040
  throw fetchError;
@@ -15315,7 +16045,7 @@ var PostgrestBuilder = class {
15315
16045
  const delay = retryAfterHeader !== null ? Math.max(0, parseInt(retryAfterHeader, 10) || 0) * 1e3 : getRetryDelay(attemptCount);
15316
16046
  await res$1.text();
15317
16047
  attemptCount++;
15318
- await sleep2(delay, _this.signal);
16048
+ await sleep3(delay, _this.signal);
15319
16049
  continue;
15320
16050
  }
15321
16051
  return await _this.processResponse(res$1);
@@ -23805,8 +24535,8 @@ var IcebergError = class extends Error {
23805
24535
  return this.status === 419;
23806
24536
  }
23807
24537
  };
23808
- function buildUrl(baseUrl, path16, query) {
23809
- const url = new URL(path16, baseUrl);
24538
+ function buildUrl(baseUrl, path17, query) {
24539
+ const url = new URL(path17, baseUrl);
23810
24540
  if (query) {
23811
24541
  for (const [key, value] of Object.entries(query)) {
23812
24542
  if (value !== void 0) {
@@ -23836,12 +24566,12 @@ function createFetchClient(options) {
23836
24566
  return {
23837
24567
  async request({
23838
24568
  method,
23839
- path: path16,
24569
+ path: path17,
23840
24570
  query,
23841
24571
  body,
23842
24572
  headers
23843
24573
  }) {
23844
- const url = buildUrl(options.baseUrl, path16, query);
24574
+ const url = buildUrl(options.baseUrl, path17, query);
23845
24575
  const authHeaders = await buildAuthHeaders(options.auth);
23846
24576
  const res = await fetchFn(url, {
23847
24577
  method,
@@ -24679,7 +25409,7 @@ var StorageFileApi = class extends BaseApiClient {
24679
25409
  * @param path The relative file path. Should be of the format `folder/subfolder/filename.png`. The bucket must already exist before attempting to upload.
24680
25410
  * @param fileBody The body of the file to be stored in the bucket.
24681
25411
  */
24682
- async uploadOrUpdate(method, path16, fileBody, fileOptions) {
25412
+ async uploadOrUpdate(method, path17, fileBody, fileOptions) {
24683
25413
  var _this = this;
24684
25414
  return _this.handleOperation(async () => {
24685
25415
  let body;
@@ -24703,7 +25433,7 @@ var StorageFileApi = class extends BaseApiClient {
24703
25433
  if ((typeof ReadableStream !== "undefined" && body instanceof ReadableStream || body && typeof body === "object" && "pipe" in body && typeof body.pipe === "function") && !options.duplex) options.duplex = "half";
24704
25434
  }
24705
25435
  if (fileOptions === null || fileOptions === void 0 ? void 0 : fileOptions.headers) for (const [key, value] of Object.entries(fileOptions.headers)) headers = setHeader(headers, key, value);
24706
- const cleanPath = _this._removeEmptyFolders(path16);
25436
+ const cleanPath = _this._removeEmptyFolders(path17);
24707
25437
  const _path = _this._getFinalPath(cleanPath);
24708
25438
  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 } : {}));
24709
25439
  return {
@@ -24764,8 +25494,8 @@ var StorageFileApi = class extends BaseApiClient {
24764
25494
  * - Refer to the [Storage guide](/docs/guides/storage/security/access-control) on how access control works
24765
25495
  * - 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.
24766
25496
  */
24767
- async upload(path16, fileBody, fileOptions) {
24768
- return this.uploadOrUpdate("POST", path16, fileBody, fileOptions);
25497
+ async upload(path17, fileBody, fileOptions) {
25498
+ return this.uploadOrUpdate("POST", path17, fileBody, fileOptions);
24769
25499
  }
24770
25500
  /**
24771
25501
  * Upload a file with a token generated from `createSignedUploadUrl`.
@@ -24804,9 +25534,9 @@ var StorageFileApi = class extends BaseApiClient {
24804
25534
  * - `objects` table permissions: none
24805
25535
  * - Refer to the [Storage guide](/docs/guides/storage/security/access-control) on how access control works
24806
25536
  */
24807
- async uploadToSignedUrl(path16, token, fileBody, fileOptions) {
25537
+ async uploadToSignedUrl(path17, token, fileBody, fileOptions) {
24808
25538
  var _this3 = this;
24809
- const cleanPath = _this3._removeEmptyFolders(path16);
25539
+ const cleanPath = _this3._removeEmptyFolders(path17);
24810
25540
  const _path = _this3._getFinalPath(cleanPath);
24811
25541
  const url = new URL(_this3.url + `/object/upload/sign/${_path}`);
24812
25542
  url.searchParams.set("token", token);
@@ -24868,10 +25598,10 @@ var StorageFileApi = class extends BaseApiClient {
24868
25598
  * - `objects` table permissions: `insert`
24869
25599
  * - Refer to the [Storage guide](/docs/guides/storage/security/access-control) on how access control works
24870
25600
  */
24871
- async createSignedUploadUrl(path16, options) {
25601
+ async createSignedUploadUrl(path17, options) {
24872
25602
  var _this4 = this;
24873
25603
  return _this4.handleOperation(async () => {
24874
- let _path = _this4._getFinalPath(path16);
25604
+ let _path = _this4._getFinalPath(path17);
24875
25605
  const headers = _objectSpread22({}, _this4.headers);
24876
25606
  if (options === null || options === void 0 ? void 0 : options.upsert) headers["x-upsert"] = "true";
24877
25607
  const data = await post(_this4.fetch, `${_this4.url}/object/upload/sign/${_path}`, {}, { headers });
@@ -24880,7 +25610,7 @@ var StorageFileApi = class extends BaseApiClient {
24880
25610
  if (!token) throw new StorageError("No token returned by API");
24881
25611
  return {
24882
25612
  signedUrl: url.toString(),
24883
- path: path16,
25613
+ path: path17,
24884
25614
  token
24885
25615
  };
24886
25616
  });
@@ -24936,8 +25666,8 @@ var StorageFileApi = class extends BaseApiClient {
24936
25666
  * - Refer to the [Storage guide](/docs/guides/storage/security/access-control) on how access control works
24937
25667
  * - 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.
24938
25668
  */
24939
- async update(path16, fileBody, fileOptions) {
24940
- return this.uploadOrUpdate("PUT", path16, fileBody, fileOptions);
25669
+ async update(path17, fileBody, fileOptions) {
25670
+ return this.uploadOrUpdate("PUT", path17, fileBody, fileOptions);
24941
25671
  }
24942
25672
  /**
24943
25673
  * Moves an existing file to a new path in the same bucket.
@@ -25085,10 +25815,10 @@ var StorageFileApi = class extends BaseApiClient {
25085
25815
  * - `objects` table permissions: `select`
25086
25816
  * - Refer to the [Storage guide](/docs/guides/storage/security/access-control) on how access control works
25087
25817
  */
25088
- async createSignedUrl(path16, expiresIn, options) {
25818
+ async createSignedUrl(path17, expiresIn, options) {
25089
25819
  var _this8 = this;
25090
25820
  return _this8.handleOperation(async () => {
25091
- let _path = _this8._getFinalPath(path16);
25821
+ let _path = _this8._getFinalPath(path17);
25092
25822
  const hasTransform = typeof (options === null || options === void 0 ? void 0 : options.transform) === "object" && options.transform !== null && Object.keys(options.transform).length > 0;
25093
25823
  let data = await post(_this8.fetch, `${_this8.url}/object/sign/${_path}`, _objectSpread22({ expiresIn }, hasTransform ? { transform: options.transform } : {}), { headers: _this8.headers });
25094
25824
  const query = new URLSearchParams();
@@ -25222,13 +25952,13 @@ var StorageFileApi = class extends BaseApiClient {
25222
25952
  * - `objects` table permissions: `select`
25223
25953
  * - Refer to the [Storage guide](/docs/guides/storage/security/access-control) on how access control works
25224
25954
  */
25225
- download(path16, options, parameters) {
25955
+ download(path17, options, parameters) {
25226
25956
  const renderPath = typeof (options === null || options === void 0 ? void 0 : options.transform) === "object" && options.transform !== null && Object.keys(options.transform).length > 0 ? "render/image/authenticated" : "object";
25227
25957
  const query = new URLSearchParams();
25228
25958
  if (options === null || options === void 0 ? void 0 : options.transform) this.applyTransformOptsToQuery(query, options.transform);
25229
25959
  if ((options === null || options === void 0 ? void 0 : options.cacheNonce) != null) query.set("cacheNonce", String(options.cacheNonce));
25230
25960
  const queryString = query.toString();
25231
- const _path = this._getFinalPath(path16);
25961
+ const _path = this._getFinalPath(path17);
25232
25962
  const downloadFn = () => get(this.fetch, `${this.url}/${renderPath}/${_path}${queryString ? `?${queryString}` : ""}`, {
25233
25963
  headers: this.headers,
25234
25964
  noResolveJson: true
@@ -25258,9 +25988,9 @@ var StorageFileApi = class extends BaseApiClient {
25258
25988
  * }
25259
25989
  * ```
25260
25990
  */
25261
- async info(path16) {
25991
+ async info(path17) {
25262
25992
  var _this10 = this;
25263
- const _path = _this10._getFinalPath(path16);
25993
+ const _path = _this10._getFinalPath(path17);
25264
25994
  return _this10.handleOperation(async () => {
25265
25995
  return recursiveToCamel(await get(_this10.fetch, `${_this10.url}/object/info/${_path}`, { headers: _this10.headers }));
25266
25996
  });
@@ -25280,9 +26010,9 @@ var StorageFileApi = class extends BaseApiClient {
25280
26010
  * .exists('folder/avatar1.png')
25281
26011
  * ```
25282
26012
  */
25283
- async exists(path16) {
26013
+ async exists(path17) {
25284
26014
  var _this11 = this;
25285
- const _path = _this11._getFinalPath(path16);
26015
+ const _path = _this11._getFinalPath(path17);
25286
26016
  try {
25287
26017
  await head(_this11.fetch, `${_this11.url}/object/${_path}`, { headers: _this11.headers });
25288
26018
  return {
@@ -25360,8 +26090,8 @@ var StorageFileApi = class extends BaseApiClient {
25360
26090
  * - `objects` table permissions: none
25361
26091
  * - Refer to the [Storage guide](/docs/guides/storage/security/access-control) on how access control works
25362
26092
  */
25363
- getPublicUrl(path16, options) {
25364
- const _path = this._getFinalPath(path16);
26093
+ getPublicUrl(path17, options) {
26094
+ const _path = this._getFinalPath(path17);
25365
26095
  const query = new URLSearchParams();
25366
26096
  if (options === null || options === void 0 ? void 0 : options.download) query.set("download", options.download === true ? "" : options.download);
25367
26097
  if (options === null || options === void 0 ? void 0 : options.transform) this.applyTransformOptsToQuery(query, options.transform);
@@ -25498,10 +26228,10 @@ var StorageFileApi = class extends BaseApiClient {
25498
26228
  * - `objects` table permissions: `select`
25499
26229
  * - Refer to the [Storage guide](/docs/guides/storage/security/access-control) on how access control works
25500
26230
  */
25501
- async list(path16, options, parameters) {
26231
+ async list(path17, options, parameters) {
25502
26232
  var _this13 = this;
25503
26233
  return _this13.handleOperation(async () => {
25504
- const body = _objectSpread22(_objectSpread22(_objectSpread22({}, DEFAULT_SEARCH_OPTIONS), options), {}, { prefix: path16 || "" });
26234
+ const body = _objectSpread22(_objectSpread22(_objectSpread22({}, DEFAULT_SEARCH_OPTIONS), options), {}, { prefix: path17 || "" });
25505
26235
  return await post(_this13.fetch, `${_this13.url}/object/list/${_this13.bucketId}`, body, { headers: _this13.headers }, parameters);
25506
26236
  });
25507
26237
  }
@@ -25565,11 +26295,11 @@ var StorageFileApi = class extends BaseApiClient {
25565
26295
  if (typeof Buffer !== "undefined") return Buffer.from(data).toString("base64");
25566
26296
  return btoa(data);
25567
26297
  }
25568
- _getFinalPath(path16) {
25569
- return `${this.bucketId}/${path16.replace(/^\/+/, "")}`;
26298
+ _getFinalPath(path17) {
26299
+ return `${this.bucketId}/${path17.replace(/^\/+/, "")}`;
25570
26300
  }
25571
- _removeEmptyFolders(path16) {
25572
- return path16.replace(/^\/|\/$/g, "").replace(/\/+/g, "/");
26301
+ _removeEmptyFolders(path17) {
26302
+ return path17.replace(/^\/|\/$/g, "").replace(/\/+/g, "/");
25573
26303
  }
25574
26304
  /** Modifies the `query`, appending values the from `transform` */
25575
26305
  applyTransformOptsToQuery(query, transform) {
@@ -27312,7 +28042,7 @@ function decodeJWT(token) {
27312
28042
  };
27313
28043
  return data;
27314
28044
  }
27315
- async function sleep3(time) {
28045
+ async function sleep4(time) {
27316
28046
  return await new Promise((accept) => {
27317
28047
  setTimeout(() => accept(null), time);
27318
28048
  });
@@ -33105,7 +33835,7 @@ var GoTrueClient = class _GoTrueClient {
33105
33835
  const startedAt = Date.now();
33106
33836
  return await retryable(async (attempt) => {
33107
33837
  if (attempt > 0) {
33108
- await sleep3(200 * Math.pow(2, attempt - 1));
33838
+ await sleep4(200 * Math.pow(2, attempt - 1));
33109
33839
  }
33110
33840
  this._debug(debugName, "refreshing attempt", attempt);
33111
33841
  return await _request(this.fetch, "POST", `${this.url}/token?grant_type=refresh_token`, {
@@ -34644,7 +35374,7 @@ function shouldShowDeprecationWarning() {
34644
35374
  }
34645
35375
  if (shouldShowDeprecationWarning()) console.warn("\u26A0\uFE0F Node.js 18 and below are deprecated and will no longer be supported in future versions of @supabase/supabase-js. Please upgrade to Node.js 20 or later. For more information, visit: https://github.com/orgs/supabase/discussions/37217");
34646
35376
 
34647
- // node_modules/@remixhq/core/dist/chunk-P6JHXOV4.js
35377
+ // node_modules/@remixhq/core/dist/chunk-XETDXVGM.js
34648
35378
  var storedSessionSchema = external_exports.object({
34649
35379
  access_token: external_exports.string().min(1),
34650
35380
  refresh_token: external_exports.string().min(1),
@@ -34677,24 +35407,24 @@ async function maybeLoadKeytar() {
34677
35407
  }
34678
35408
  async function ensurePathPermissions(filePath) {
34679
35409
  const dir = import_path7.default.dirname(filePath);
34680
- await import_promises19.default.mkdir(dir, { recursive: true });
35410
+ await import_promises22.default.mkdir(dir, { recursive: true });
34681
35411
  try {
34682
- await import_promises19.default.chmod(dir, 448);
35412
+ await import_promises22.default.chmod(dir, 448);
34683
35413
  } catch {
34684
35414
  }
34685
35415
  try {
34686
- await import_promises19.default.chmod(filePath, 384);
35416
+ await import_promises22.default.chmod(filePath, 384);
34687
35417
  } catch {
34688
35418
  }
34689
35419
  }
34690
- async function writeJsonAtomic2(filePath, value) {
34691
- await import_promises19.default.mkdir(import_path7.default.dirname(filePath), { recursive: true });
35420
+ async function writeJsonAtomic3(filePath, value) {
35421
+ await import_promises22.default.mkdir(import_path7.default.dirname(filePath), { recursive: true });
34692
35422
  const tmpPath = `${filePath}.tmp-${Date.now()}-${Math.random().toString(16).slice(2)}`;
34693
- await import_promises19.default.writeFile(tmpPath, JSON.stringify(value, null, 2) + "\n", "utf8");
34694
- await import_promises19.default.rename(tmpPath, filePath);
35423
+ await import_promises22.default.writeFile(tmpPath, JSON.stringify(value, null, 2) + "\n", "utf8");
35424
+ await import_promises22.default.rename(tmpPath, filePath);
34695
35425
  }
34696
35426
  async function writeSessionFileFallback(filePath, session) {
34697
- await writeJsonAtomic2(filePath, session);
35427
+ await writeJsonAtomic3(filePath, session);
34698
35428
  await ensurePathPermissions(filePath);
34699
35429
  }
34700
35430
  function createLocalSessionStore(params) {
@@ -34714,7 +35444,7 @@ function createLocalSessionStore(params) {
34714
35444
  }
34715
35445
  }
34716
35446
  async function readFile() {
34717
- const raw = await import_promises19.default.readFile(filePath, "utf8").catch(() => null);
35447
+ const raw = await import_promises22.default.readFile(filePath, "utf8").catch(() => null);
34718
35448
  if (!raw) return null;
34719
35449
  try {
34720
35450
  const parsed = storedSessionSchema.safeParse(JSON.parse(raw));
@@ -34858,7 +35588,7 @@ function createSupabaseAuthHelpers(config) {
34858
35588
  };
34859
35589
  }
34860
35590
 
34861
- // node_modules/@remixhq/core/dist/chunk-VM3CGCNX.js
35591
+ // node_modules/@remixhq/core/dist/chunk-XCZRNB35.js
34862
35592
  var DEFAULT_API_URL = "https://api.remix.one";
34863
35593
  var DEFAULT_SUPABASE_URL = "https://xtfxwbckjpfmqubnsusu.supabase.co";
34864
35594
  var DEFAULT_SUPABASE_ANON_KEY = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Inh0Znh3YmNranBmbXF1Ym5zdXN1Iiwicm9sZSI6ImFub24iLCJpYXQiOjE3NjA2MDEyMzAsImV4cCI6MjA3NjE3NzIzMH0.dzWGAWrK4CvrmHVHzf8w7JlUZohdap0ZPnLZnABMV8s";
@@ -34896,6 +35626,7 @@ async function resolveConfig(_opts) {
34896
35626
  }
34897
35627
 
34898
35628
  // src/hook-auth.ts
35629
+ var HOOK_API_REQUEST_TIMEOUT_MS = 6e4;
34899
35630
  async function createHookCollabApiClient() {
34900
35631
  const config = await resolveConfig();
34901
35632
  const sessionStore = createLocalSessionStore();
@@ -34908,307 +35639,11 @@ async function createHookCollabApiClient() {
34908
35639
  }
34909
35640
  });
34910
35641
  return createApiClient(config, {
34911
- tokenProvider
35642
+ tokenProvider,
35643
+ defaultRequestTimeoutMs: HOOK_API_REQUEST_TIMEOUT_MS
34912
35644
  });
34913
35645
  }
34914
35646
 
34915
- // src/hook-diagnostics.ts
34916
- var import_node_crypto2 = require("crypto");
34917
- var import_promises21 = __toESM(require("fs/promises"), 1);
34918
- var import_node_os6 = __toESM(require("os"), 1);
34919
- var import_node_path8 = __toESM(require("path"), 1);
34920
-
34921
- // src/hook-state.ts
34922
- var import_promises20 = __toESM(require("fs/promises"), 1);
34923
- var import_node_os5 = __toESM(require("os"), 1);
34924
- var import_node_path7 = __toESM(require("path"), 1);
34925
- var import_node_crypto = require("crypto");
34926
- function stateRoot2() {
34927
- const configured = process.env.REMIX_CLAUDE_PLUGIN_HOOK_STATE_ROOT?.trim();
34928
- return configured || import_node_path7.default.join(import_node_os5.default.tmpdir(), "remix-claude-plugin-hooks");
34929
- }
34930
- function statePath(sessionId) {
34931
- return import_node_path7.default.join(stateRoot2(), `${sessionId}.json`);
34932
- }
34933
- function stateLockPath(sessionId) {
34934
- return import_node_path7.default.join(stateRoot2(), `${sessionId}.lock`);
34935
- }
34936
- function stateLockMetaPath(sessionId) {
34937
- return import_node_path7.default.join(stateLockPath(sessionId), "owner.json");
34938
- }
34939
- async function writeJsonAtomic3(filePath, value) {
34940
- await import_promises20.default.mkdir(import_node_path7.default.dirname(filePath), { recursive: true });
34941
- const tmpPath = `${filePath}.tmp-${Date.now()}-${Math.random().toString(16).slice(2)}`;
34942
- await import_promises20.default.writeFile(tmpPath, JSON.stringify(value, null, 2) + "\n", "utf8");
34943
- await import_promises20.default.rename(tmpPath, filePath);
34944
- }
34945
- var STATE_LOCK_WAIT_MS = 2e3;
34946
- var STATE_LOCK_POLL_MS = 25;
34947
- var STATE_LOCK_STALE_MS = 3e4;
34948
- var STATE_LOCK_HEARTBEAT_MS = 5e3;
34949
- async function sleep4(ms) {
34950
- await new Promise((resolve) => setTimeout(resolve, ms));
34951
- }
34952
- async function readStateLockMetadata(sessionId) {
34953
- const raw = await import_promises20.default.readFile(stateLockMetaPath(sessionId), "utf8").catch(() => null);
34954
- if (!raw) return null;
34955
- try {
34956
- const parsed = JSON.parse(raw);
34957
- if (typeof parsed.ownerId !== "string" || typeof parsed.pid !== "number" || typeof parsed.createdAt !== "string" || typeof parsed.heartbeatAt !== "string") {
34958
- return null;
34959
- }
34960
- return {
34961
- ownerId: parsed.ownerId,
34962
- pid: parsed.pid,
34963
- createdAt: parsed.createdAt,
34964
- heartbeatAt: parsed.heartbeatAt
34965
- };
34966
- } catch {
34967
- return null;
34968
- }
34969
- }
34970
- async function writeStateLockMetadata(sessionId, metadata) {
34971
- await writeJsonAtomic3(stateLockMetaPath(sessionId), metadata);
34972
- }
34973
- async function tryRemoveStaleStateLock(sessionId) {
34974
- const lockPath = stateLockPath(sessionId);
34975
- const metadata = await readStateLockMetadata(sessionId);
34976
- const staleByHeartbeat = metadata && Date.now() - new Date(metadata.heartbeatAt).getTime() > STATE_LOCK_STALE_MS;
34977
- if (staleByHeartbeat) {
34978
- await import_promises20.default.rm(lockPath, { recursive: true, force: true }).catch(() => void 0);
34979
- return true;
34980
- }
34981
- if (!metadata) {
34982
- const lockStat = await import_promises20.default.stat(lockPath).catch(() => null);
34983
- if (lockStat && Date.now() - lockStat.mtimeMs > STATE_LOCK_STALE_MS) {
34984
- await import_promises20.default.rm(lockPath, { recursive: true, force: true }).catch(() => void 0);
34985
- return true;
34986
- }
34987
- }
34988
- return false;
34989
- }
34990
- async function acquireStateLock(sessionId) {
34991
- const lockPath = stateLockPath(sessionId);
34992
- const deadline = Date.now() + STATE_LOCK_WAIT_MS;
34993
- await import_promises20.default.mkdir(stateRoot2(), { recursive: true });
34994
- while (true) {
34995
- try {
34996
- await import_promises20.default.mkdir(lockPath);
34997
- const ownerId = (0, import_node_crypto.randomUUID)();
34998
- const createdAt = (/* @__PURE__ */ new Date()).toISOString();
34999
- const metadata = {
35000
- ownerId,
35001
- pid: process.pid,
35002
- createdAt,
35003
- heartbeatAt: createdAt
35004
- };
35005
- await writeStateLockMetadata(sessionId, metadata);
35006
- let released = false;
35007
- const heartbeat = setInterval(() => {
35008
- if (released) return;
35009
- void writeStateLockMetadata(sessionId, {
35010
- ...metadata,
35011
- heartbeatAt: (/* @__PURE__ */ new Date()).toISOString()
35012
- }).catch(() => void 0);
35013
- }, STATE_LOCK_HEARTBEAT_MS);
35014
- heartbeat.unref?.();
35015
- return async () => {
35016
- if (released) return;
35017
- released = true;
35018
- clearInterval(heartbeat);
35019
- const currentMetadata = await readStateLockMetadata(sessionId);
35020
- if (currentMetadata?.ownerId === ownerId) {
35021
- await import_promises20.default.rm(lockPath, { recursive: true, force: true }).catch(() => void 0);
35022
- }
35023
- };
35024
- } catch (error) {
35025
- const code = error && typeof error === "object" && "code" in error ? error.code : null;
35026
- if (code !== "EEXIST") {
35027
- throw error;
35028
- }
35029
- if (await tryRemoveStaleStateLock(sessionId)) {
35030
- continue;
35031
- }
35032
- if (Date.now() >= deadline) {
35033
- throw new Error(`Timed out acquiring hook state lock for session ${sessionId}.`);
35034
- }
35035
- await sleep4(STATE_LOCK_POLL_MS);
35036
- }
35037
- }
35038
- }
35039
- async function withStateLock(sessionId, fn) {
35040
- const release = await acquireStateLock(sessionId);
35041
- try {
35042
- return await fn();
35043
- } finally {
35044
- await release();
35045
- }
35046
- }
35047
- async function savePendingTurnState(state) {
35048
- await writeJsonAtomic3(statePath(state.sessionId), state);
35049
- }
35050
- async function createPendingTurnState(params) {
35051
- return withStateLock(params.sessionId, async () => {
35052
- const state = {
35053
- sessionId: params.sessionId,
35054
- turnId: (0, import_node_crypto.randomUUID)(),
35055
- prompt: params.prompt,
35056
- initialCwd: params.initialCwd?.trim() || null,
35057
- intent: params.intent,
35058
- submittedAt: (/* @__PURE__ */ new Date()).toISOString(),
35059
- consultedMemory: false,
35060
- touchedRepos: {},
35061
- turnFailureMessage: null,
35062
- turnFailureHint: null,
35063
- turnFailedAt: null
35064
- };
35065
- await savePendingTurnState(state);
35066
- return state;
35067
- });
35068
- }
35069
-
35070
- // package.json
35071
- var package_default = {
35072
- name: "@remixhq/claude-plugin",
35073
- version: "0.1.22",
35074
- description: "Claude Code plugin for Remix collaboration workflows",
35075
- homepage: "https://github.com/RemixDotOne/remix-claude-plugin",
35076
- license: "MIT",
35077
- repository: {
35078
- type: "git",
35079
- url: "https://github.com/RemixDotOne/remix-claude-plugin.git"
35080
- },
35081
- type: "module",
35082
- engines: {
35083
- node: ">=20"
35084
- },
35085
- publishConfig: {
35086
- access: "public"
35087
- },
35088
- files: [
35089
- "dist",
35090
- ".claude-plugin/plugin.json",
35091
- ".mcp.json",
35092
- "skills",
35093
- "hooks",
35094
- "agents"
35095
- ],
35096
- exports: {
35097
- ".": {
35098
- types: "./dist/index.d.ts",
35099
- import: "./dist/index.js"
35100
- }
35101
- },
35102
- scripts: {
35103
- build: "tsup",
35104
- postbuild: `node -e "const fs=require('node:fs'); for (const p of ['dist/mcp-server.cjs','dist/hook-pre-git.cjs','dist/hook-user-prompt.cjs','dist/hook-post-collab.cjs','dist/hook-stop-collab.cjs']) fs.chmodSync(p, 0o755);"`,
35105
- dev: "tsx src/mcp-server.ts",
35106
- typecheck: "tsc -p tsconfig.json --noEmit",
35107
- test: "node --import tsx --test 'src/**/*.test.ts'",
35108
- prepack: "npm run build"
35109
- },
35110
- dependencies: {
35111
- "@remixhq/core": "^0.1.17",
35112
- "@remixhq/mcp": "^0.1.17"
35113
- },
35114
- devDependencies: {
35115
- "@types/node": "^25.4.0",
35116
- tsup: "^8.5.1",
35117
- tsx: "^4.21.0",
35118
- typescript: "^5.9.3"
35119
- }
35120
- };
35121
-
35122
- // src/metadata.ts
35123
- var pluginMetadata = {
35124
- name: package_default.name,
35125
- version: package_default.version,
35126
- description: package_default.description,
35127
- pluginId: "remix",
35128
- agentName: "remix-collab"
35129
- };
35130
-
35131
- // src/hook-diagnostics.ts
35132
- var MAX_LOG_BYTES = 512 * 1024;
35133
- function resolveClaudeRoot() {
35134
- const configured = process.env.CLAUDE_CONFIG_DIR?.trim();
35135
- return configured || import_node_path8.default.join(import_node_os6.default.homedir(), ".claude");
35136
- }
35137
- function resolvePluginDataDirName() {
35138
- return `${pluginMetadata.pluginId}-${pluginMetadata.pluginId}`;
35139
- }
35140
- function getHookDiagnosticsDirPath() {
35141
- const configured = process.env.REMIX_CLAUDE_PLUGIN_HOOK_DIAGNOSTICS_DIR?.trim();
35142
- return configured || import_node_path8.default.join(resolveClaudeRoot(), "plugins", "data", resolvePluginDataDirName());
35143
- }
35144
- function getHookDiagnosticsLogPath() {
35145
- return import_node_path8.default.join(getHookDiagnosticsDirPath(), "hooks.ndjson");
35146
- }
35147
- function toFieldValue(value) {
35148
- if (value === null) return null;
35149
- if (typeof value === "string") return value;
35150
- if (typeof value === "number" && Number.isFinite(value)) return value;
35151
- if (typeof value === "boolean") return value;
35152
- return void 0;
35153
- }
35154
- function normalizeFields(fields) {
35155
- if (!fields) return {};
35156
- const normalizedEntries = Object.entries(fields).map(([key, value]) => {
35157
- const normalized = toFieldValue(value);
35158
- return normalized === void 0 ? null : [key, normalized];
35159
- }).filter((entry) => entry !== null);
35160
- return Object.fromEntries(normalizedEntries);
35161
- }
35162
- async function rotateLogIfNeeded(logPath) {
35163
- const stat = await import_promises21.default.stat(logPath).catch(() => null);
35164
- if (!stat || stat.size < MAX_LOG_BYTES) {
35165
- return;
35166
- }
35167
- const rotatedPath = `${logPath}.1`;
35168
- await import_promises21.default.rm(rotatedPath, { force: true }).catch(() => void 0);
35169
- await import_promises21.default.rename(logPath, rotatedPath).catch(() => void 0);
35170
- }
35171
- function summarizeText(value) {
35172
- if (typeof value !== "string" || !value.trim()) {
35173
- return {
35174
- present: false,
35175
- length: 0,
35176
- sha256Prefix: null
35177
- };
35178
- }
35179
- const trimmed = value.trim();
35180
- return {
35181
- present: true,
35182
- length: trimmed.length,
35183
- sha256Prefix: (0, import_node_crypto2.createHash)("sha256").update(trimmed).digest("hex").slice(0, 12)
35184
- };
35185
- }
35186
- async function appendHookDiagnosticsEvent(params) {
35187
- try {
35188
- const logPath = getHookDiagnosticsLogPath();
35189
- await import_promises21.default.mkdir(import_node_path8.default.dirname(logPath), { recursive: true });
35190
- await rotateLogIfNeeded(logPath);
35191
- const event = {
35192
- ts: (/* @__PURE__ */ new Date()).toISOString(),
35193
- hook: params.hook,
35194
- pluginVersion: pluginMetadata.version,
35195
- pid: process.pid,
35196
- sessionId: params.sessionId?.trim() || null,
35197
- turnId: params.turnId?.trim() || null,
35198
- stage: params.stage.trim(),
35199
- result: params.result,
35200
- reason: params.reason?.trim() || null,
35201
- toolName: params.toolName?.trim() || null,
35202
- repoRoot: params.repoRoot?.trim() || null,
35203
- message: params.message?.trim() || null,
35204
- fields: normalizeFields(params.fields)
35205
- };
35206
- await import_promises21.default.appendFile(logPath, `${JSON.stringify(event)}
35207
- `, "utf8");
35208
- } catch {
35209
- }
35210
- }
35211
-
35212
35647
  // src/deferred-turn-drainer.ts
35213
35648
  var collabFinalizeTurn2 = collabFinalizeTurn;
35214
35649
  var drainPendingFinalizeQueue2 = drainPendingFinalizeQueue;
@@ -35218,6 +35653,16 @@ var HOOK_ACTOR = {
35218
35653
  version: pluginMetadata.version,
35219
35654
  provider: "anthropic"
35220
35655
  };
35656
+ function getDrainerErrorDetails(error) {
35657
+ if (error instanceof Error) {
35658
+ const hint = typeof error.hint === "string" ? String(error.hint) : null;
35659
+ const codeRaw = error.code;
35660
+ const preflightCode = isFinalizePreflightFailureCode(codeRaw) ? codeRaw : null;
35661
+ return { message: error.message || "Deferred turn recording failed.", hint, preflightCode };
35662
+ }
35663
+ const message = typeof error === "string" && error.trim() ? error.trim() : "Deferred turn recording failed.";
35664
+ return { message, hint: null, preflightCode: null };
35665
+ }
35221
35666
  var DEFERRED_TURN_DRAIN_POLL_INTERVAL_MS = 3e3;
35222
35667
  var DEFERRED_TURN_DRAIN_MAX_WAIT_MS = 15 * 60 * 1e3;
35223
35668
  var DEFERRED_TURN_DRAIN_LOCK_HEARTBEAT_MS = 3e4;
@@ -35236,10 +35681,10 @@ function repoLockFileName(repoRoot) {
35236
35681
  return `.drainer-${hash}.lock`;
35237
35682
  }
35238
35683
  function repoLockPath(repoRoot) {
35239
- return import_node_path9.default.join(getDeferredTurnDirPath(), repoLockFileName(repoRoot));
35684
+ return import_node_path11.default.join(getDeferredTurnDirPath(), repoLockFileName(repoRoot));
35240
35685
  }
35241
35686
  async function readDrainLockMetadata(lockPath) {
35242
- const raw = await import_promises22.default.readFile(lockPath, "utf8").catch(() => null);
35687
+ const raw = await import_promises23.default.readFile(lockPath, "utf8").catch(() => null);
35243
35688
  if (!raw) return null;
35244
35689
  try {
35245
35690
  const parsed = JSON.parse(raw);
@@ -35253,15 +35698,15 @@ async function readDrainLockMetadata(lockPath) {
35253
35698
  }
35254
35699
  async function writeDrainLockMetadata(lockPath, metadata) {
35255
35700
  const tmpPath = `${lockPath}.tmp-${process.pid}-${Date.now()}`;
35256
- await import_promises22.default.writeFile(tmpPath, JSON.stringify(metadata), "utf8");
35257
- await import_promises22.default.rename(tmpPath, lockPath);
35701
+ await import_promises23.default.writeFile(tmpPath, JSON.stringify(metadata), "utf8");
35702
+ await import_promises23.default.rename(tmpPath, lockPath);
35258
35703
  }
35259
35704
  async function tryAcquireDrainLock(repoRoot) {
35260
35705
  const lockPath = repoLockPath(repoRoot);
35261
- await import_promises22.default.mkdir(import_node_path9.default.dirname(lockPath), { recursive: true });
35706
+ await import_promises23.default.mkdir(import_node_path11.default.dirname(lockPath), { recursive: true });
35262
35707
  const existingMeta = await readDrainLockMetadata(lockPath);
35263
35708
  if (existingMeta) {
35264
- const lockStat = await import_promises22.default.stat(lockPath).catch(() => null);
35709
+ const lockStat = await import_promises23.default.stat(lockPath).catch(() => null);
35265
35710
  const ageMs = lockStat ? Date.now() - lockStat.mtimeMs : Number.POSITIVE_INFINITY;
35266
35711
  const fresh = ageMs <= DEFERRED_TURN_DRAIN_LOCK_STALE_MS;
35267
35712
  const alive = isPidAlive(existingMeta.pid);
@@ -35279,11 +35724,11 @@ async function tryAcquireDrainLock(repoRoot) {
35279
35724
  async function releaseDrainLock(lockPath) {
35280
35725
  const meta = await readDrainLockMetadata(lockPath);
35281
35726
  if (meta && meta.pid !== process.pid) return;
35282
- await import_promises22.default.rm(lockPath, { force: true }).catch(() => void 0);
35727
+ await import_promises23.default.rm(lockPath, { force: true }).catch(() => void 0);
35283
35728
  }
35284
35729
  async function heartbeatDrainLock(lockPath) {
35285
35730
  const now = /* @__PURE__ */ new Date();
35286
- await import_promises22.default.utimes(lockPath, now, now).catch(() => void 0);
35731
+ await import_promises23.default.utimes(lockPath, now, now).catch(() => void 0);
35287
35732
  }
35288
35733
  async function sleep5(ms) {
35289
35734
  await new Promise((resolve) => setTimeout(resolve, ms));
@@ -35383,6 +35828,7 @@ async function runStandaloneDeferredTurnDrainer(repoRoot) {
35383
35828
  let api = null;
35384
35829
  let recordedTotal = 0;
35385
35830
  let failedTotal = 0;
35831
+ let droppedTotal = 0;
35386
35832
  let exitReason = "queue_empty";
35387
35833
  try {
35388
35834
  while (true) {
@@ -35413,7 +35859,49 @@ async function runStandaloneDeferredTurnDrainer(repoRoot) {
35413
35859
  const bindingState = await readCollabBindingState(repoRoot).catch(() => null);
35414
35860
  const currentBranch = bindingState?.currentBranch ?? null;
35415
35861
  const isCurrentBranchBound = bindingState?.binding != null;
35416
- const attemptable = entries.filter(
35862
+ const currentAppId = bindingState?.binding?.currentAppId ?? null;
35863
+ const currentProjectId = bindingState?.binding?.projectId ?? bindingState?.projectId ?? null;
35864
+ let droppedThisPass = 0;
35865
+ const liveEntries = [];
35866
+ for (const entry of entries) {
35867
+ const appIdMismatch = entry.record.appIdAtDefer != null && currentAppId != null && entry.record.appIdAtDefer !== currentAppId;
35868
+ const projectIdMismatch = entry.record.projectIdAtDefer != null && currentProjectId != null && entry.record.projectIdAtDefer !== currentProjectId;
35869
+ if (appIdMismatch || projectIdMismatch) {
35870
+ await deleteDeferredTurnFile(entry.filePath);
35871
+ droppedThisPass += 1;
35872
+ await appendHookDiagnosticsEvent({
35873
+ hook: "deferredTurnDrainer",
35874
+ sessionId: sessionMarker,
35875
+ stage: "deferred_turn_dropped",
35876
+ result: "info",
35877
+ reason: appIdMismatch ? "app_id_mismatch" : "project_id_mismatch",
35878
+ repoRoot,
35879
+ fields: {
35880
+ deferredTurnId: entry.record.turnId,
35881
+ deferredSessionId: entry.record.sessionId,
35882
+ appIdAtDefer: entry.record.appIdAtDefer,
35883
+ projectIdAtDefer: entry.record.projectIdAtDefer,
35884
+ currentAppId,
35885
+ currentProjectId
35886
+ }
35887
+ });
35888
+ continue;
35889
+ }
35890
+ liveEntries.push(entry);
35891
+ }
35892
+ if (droppedThisPass > 0) {
35893
+ droppedTotal += droppedThisPass;
35894
+ }
35895
+ if (liveEntries.length === 0) {
35896
+ const remaining = await listDeferredTurnsForRepo(repoRoot).catch(() => []);
35897
+ if (remaining.length === 0) {
35898
+ exitReason = "queue_empty";
35899
+ break;
35900
+ }
35901
+ await sleep5(DEFERRED_TURN_DRAIN_POLL_INTERVAL_MS);
35902
+ continue;
35903
+ }
35904
+ const attemptable = liveEntries.filter(
35417
35905
  (e) => isCurrentBranchBound && (!e.record.branchAtDefer || e.record.branchAtDefer === currentBranch)
35418
35906
  );
35419
35907
  if (attemptable.length === 0) {
@@ -35462,6 +35950,8 @@ async function runStandaloneDeferredTurnDrainer(repoRoot) {
35462
35950
  } else {
35463
35951
  failedThisPass += 1;
35464
35952
  failedTotal += 1;
35953
+ const outcome = await recordDeferredTurnFailedAttempt(entry.filePath).catch(() => null);
35954
+ const promoted = outcome?.promoted === true;
35465
35955
  await appendHookDiagnosticsEvent({
35466
35956
  hook: "deferredTurnDrainer",
35467
35957
  sessionId: sessionMarker,
@@ -35472,9 +35962,43 @@ async function runStandaloneDeferredTurnDrainer(repoRoot) {
35472
35962
  message: result.error instanceof Error ? result.error.message : String(result.error ?? ""),
35473
35963
  fields: {
35474
35964
  deferredTurnId: entry.record.turnId,
35475
- deferredSessionId: entry.record.sessionId
35965
+ deferredSessionId: entry.record.sessionId,
35966
+ attemptCount: outcome?.promoted === false ? outcome.newAttemptCount : outcome?.promoted === true ? outcome.finalAttemptCount : null,
35967
+ promoted
35476
35968
  }
35477
35969
  });
35970
+ if (promoted) {
35971
+ const errorDetails = getDrainerErrorDetails(result.error);
35972
+ await dispatchFinalizeFailure({
35973
+ // The dispatcher only knows about the two real Claude hook
35974
+ // entrypoints. The standalone drainer is logically a
35975
+ // post-Stop background process and the marker we're about
35976
+ // to write is consumed by the next prompt's UserPromptSubmit
35977
+ // hook, so attributing the failure to "Stop" matches what
35978
+ // the user will see.
35979
+ hook: "Stop",
35980
+ sessionId: sessionMarker,
35981
+ turnId: entry.record.turnId,
35982
+ repoRoot,
35983
+ preflightCode: errorDetails.preflightCode,
35984
+ message: `Deferred turn could not be recorded after ${outcome?.finalAttemptCount ?? "max"} attempts: ${errorDetails.message}`,
35985
+ hint: errorDetails.hint
35986
+ }).catch(async (dispatchErr) => {
35987
+ await appendHookDiagnosticsEvent({
35988
+ hook: "deferredTurnDrainer",
35989
+ sessionId: sessionMarker,
35990
+ stage: "deferred_turn_promotion_dispatch_failed",
35991
+ result: "error",
35992
+ reason: "exception",
35993
+ repoRoot,
35994
+ message: dispatchErr instanceof Error ? dispatchErr.message : String(dispatchErr),
35995
+ fields: {
35996
+ deferredTurnId: entry.record.turnId,
35997
+ deferredSessionId: entry.record.sessionId
35998
+ }
35999
+ });
36000
+ });
36001
+ }
35478
36002
  }
35479
36003
  }
35480
36004
  if (recordedThisPass > 0) {
@@ -35527,6 +36051,7 @@ async function runStandaloneDeferredTurnDrainer(repoRoot) {
35527
36051
  fields: {
35528
36052
  recordedTotal,
35529
36053
  failedTotal,
36054
+ droppedTotal,
35530
36055
  elapsedMs: Date.now() - startedAt
35531
36056
  }
35532
36057
  });
@@ -35555,34 +36080,13 @@ async function maybeRunDeferredTurnDrainerFromArgv() {
35555
36080
  return true;
35556
36081
  }
35557
36082
 
35558
- // src/finalize-failure-marker.ts
35559
- var import_promises23 = __toESM(require("fs/promises"), 1);
35560
- var import_node_path10 = __toESM(require("path"), 1);
35561
- var FINALIZE_FAILURE_MARKER_REL = import_node_path10.default.join(".remix", ".last-finalize-failure.json");
35562
- function markerPath(repoRoot) {
35563
- return import_node_path10.default.join(repoRoot, FINALIZE_FAILURE_MARKER_REL);
35564
- }
35565
- async function readFinalizeFailureMarker(repoRoot) {
35566
- const raw = await import_promises23.default.readFile(markerPath(repoRoot), "utf8").catch(() => null);
35567
- if (!raw) return null;
35568
- try {
35569
- const parsed = JSON.parse(raw);
35570
- if (parsed.schemaVersion !== 1) return null;
35571
- if (typeof parsed.repoRoot !== "string" || typeof parsed.failedAt !== "string") return null;
35572
- if (typeof parsed.message !== "string") return null;
35573
- return parsed;
35574
- } catch {
35575
- return null;
35576
- }
35577
- }
35578
-
35579
36083
  // src/spawn-helpers.ts
35580
- var import_node_child_process6 = require("child_process");
36084
+ var import_node_child_process7 = require("child_process");
35581
36085
  function spawnDeferredTurnDrainer(repoRoot) {
35582
36086
  const entrypoint = process.argv[1];
35583
36087
  if (!entrypoint) return;
35584
36088
  if (!repoRoot) return;
35585
- const child = (0, import_node_child_process6.spawn)(
36089
+ const child = (0, import_node_child_process7.spawn)(
35586
36090
  process.execPath,
35587
36091
  [...process.execArgv, entrypoint, "--drain-deferred-turns", repoRoot],
35588
36092
  {
@@ -35596,7 +36100,7 @@ function spawnDeferredTurnDrainer(repoRoot) {
35596
36100
 
35597
36101
  // src/hook-utils.ts
35598
36102
  var import_promises24 = __toESM(require("fs/promises"), 1);
35599
- var import_node_path11 = __toESM(require("path"), 1);
36103
+ var import_node_path12 = __toESM(require("path"), 1);
35600
36104
  async function readJsonStdin() {
35601
36105
  const chunks = [];
35602
36106
  for await (const chunk of process.stdin) {
@@ -35622,16 +36126,16 @@ function extractString(input, keys) {
35622
36126
  }
35623
36127
  async function findBoundRepo(startPath) {
35624
36128
  if (!startPath) return null;
35625
- let current = import_node_path11.default.resolve(startPath);
36129
+ let current = import_node_path12.default.resolve(startPath);
35626
36130
  let stats = await import_promises24.default.stat(current).catch(() => null);
35627
36131
  if (stats?.isFile()) {
35628
- current = import_node_path11.default.dirname(current);
36132
+ current = import_node_path12.default.dirname(current);
35629
36133
  }
35630
36134
  while (true) {
35631
- const bindingPath = import_node_path11.default.join(current, ".remix", "config.json");
36135
+ const bindingPath = import_node_path12.default.join(current, ".remix", "config.json");
35632
36136
  const bindingStats = await import_promises24.default.stat(bindingPath).catch(() => null);
35633
36137
  if (bindingStats?.isFile()) return current;
35634
- const parent = import_node_path11.default.dirname(current);
36138
+ const parent = import_node_path12.default.dirname(current);
35635
36139
  if (parent === current) return null;
35636
36140
  current = parent;
35637
36141
  }
@@ -35655,8 +36159,8 @@ function buildRuntimeStatusOverride() {
35655
36159
  "Use `remix_collab_drain_finalize_queue` only for explicit recovery flows, such as status reporting `await_finalize` before a merge-related operation."
35656
36160
  ].join("\n");
35657
36161
  }
35658
- var COLLAB_INIT_LOG_REL = import_node_path12.default.join(".remix", "collab-init.log");
35659
- var COLLAB_INIT_SPAWN_LOCK_REL = import_node_path12.default.join(".remix", ".collab-init-spawning");
36162
+ var COLLAB_INIT_LOG_REL = import_node_path13.default.join(".remix", "collab-init.log");
36163
+ var COLLAB_INIT_SPAWN_LOCK_REL = import_node_path13.default.join(".remix", ".collab-init-spawning");
35660
36164
  var COLLAB_INIT_SPAWN_LOCK_STALE_MS = 90 * 1e3;
35661
36165
  function isPidAlive2(pid) {
35662
36166
  if (!Number.isFinite(pid) || pid <= 0) return false;
@@ -35667,31 +36171,44 @@ function isPidAlive2(pid) {
35667
36171
  return false;
35668
36172
  }
35669
36173
  }
35670
- function readLockPid(spawnLockPath) {
36174
+ function readSpawnLock(spawnLockPath) {
35671
36175
  try {
35672
- const raw = (0, import_node_fs6.readFileSync)(spawnLockPath, "utf8").trim();
36176
+ const raw = (0, import_node_fs7.readFileSync)(spawnLockPath, "utf8").trim();
35673
36177
  if (!raw) return null;
36178
+ if (raw.startsWith("{")) {
36179
+ const parsed = JSON.parse(raw);
36180
+ const pid2 = Number(parsed?.pid ?? 0);
36181
+ return {
36182
+ pid: Number.isFinite(pid2) && pid2 > 0 ? pid2 : null,
36183
+ branchName: typeof parsed?.branchName === "string" && parsed.branchName.trim() ? parsed.branchName : null
36184
+ };
36185
+ }
35674
36186
  const pid = Number.parseInt(raw, 10);
35675
- return Number.isFinite(pid) && pid > 0 ? pid : null;
36187
+ return {
36188
+ pid: Number.isFinite(pid) && pid > 0 ? pid : null,
36189
+ branchName: null
36190
+ };
35676
36191
  } catch {
35677
36192
  return null;
35678
36193
  }
35679
36194
  }
35680
- function maybeAutoSpawnBranchInit(repoRoot) {
35681
- const remixDir = import_node_path12.default.join(repoRoot, ".remix");
35682
- const spawnLockPath = import_node_path12.default.join(repoRoot, COLLAB_INIT_SPAWN_LOCK_REL);
35683
- const logPath = import_node_path12.default.join(repoRoot, COLLAB_INIT_LOG_REL);
36195
+ function maybeAutoSpawnBranchInit(repoRoot, branchName) {
36196
+ const remixDir = import_node_path13.default.join(repoRoot, ".remix");
36197
+ const spawnLockPath = import_node_path13.default.join(repoRoot, COLLAB_INIT_SPAWN_LOCK_REL);
36198
+ const logPath = import_node_path13.default.join(repoRoot, COLLAB_INIT_LOG_REL);
35684
36199
  try {
35685
- if ((0, import_node_fs6.existsSync)(spawnLockPath)) {
35686
- const lockPid = readLockPid(spawnLockPath);
36200
+ if ((0, import_node_fs7.existsSync)(spawnLockPath)) {
36201
+ const lock = readSpawnLock(spawnLockPath);
36202
+ const lockPid = lock?.pid ?? null;
35687
36203
  const lockAlive = lockPid !== null && isPidAlive2(lockPid);
35688
- const ageMs = Date.now() - (0, import_node_fs6.statSync)(spawnLockPath).mtimeMs;
35689
- if (lockAlive && ageMs < COLLAB_INIT_SPAWN_LOCK_STALE_MS) {
36204
+ const sameBranch = !lock?.branchName || !branchName || lock.branchName === branchName;
36205
+ const ageMs = Date.now() - (0, import_node_fs7.statSync)(spawnLockPath).mtimeMs;
36206
+ if (lockAlive && sameBranch && ageMs < COLLAB_INIT_SPAWN_LOCK_STALE_MS) {
35690
36207
  return { spawned: false, reason: "spawn_lock_held" };
35691
36208
  }
35692
36209
  if (!lockAlive) {
35693
36210
  try {
35694
- (0, import_node_fs6.unlinkSync)(spawnLockPath);
36211
+ (0, import_node_fs7.unlinkSync)(spawnLockPath);
35695
36212
  } catch {
35696
36213
  }
35697
36214
  }
@@ -35699,14 +36216,14 @@ function maybeAutoSpawnBranchInit(repoRoot) {
35699
36216
  } catch {
35700
36217
  }
35701
36218
  try {
35702
- (0, import_node_fs6.mkdirSync)(remixDir, { recursive: true });
36219
+ (0, import_node_fs7.mkdirSync)(remixDir, { recursive: true });
35703
36220
  } catch {
35704
36221
  }
35705
36222
  let out;
35706
36223
  let err;
35707
36224
  try {
35708
- out = (0, import_node_fs6.openSync)(logPath, "a");
35709
- err = (0, import_node_fs6.openSync)(logPath, "a");
36225
+ out = (0, import_node_fs7.openSync)(logPath, "a");
36226
+ err = (0, import_node_fs7.openSync)(logPath, "a");
35710
36227
  } catch (logErr) {
35711
36228
  return {
35712
36229
  spawned: false,
@@ -35715,7 +36232,14 @@ function maybeAutoSpawnBranchInit(repoRoot) {
35715
36232
  };
35716
36233
  }
35717
36234
  try {
35718
- const child = (0, import_node_child_process7.spawn)("remix", ["collab", "init"], {
36235
+ (0, import_node_fs7.appendFileSync)(
36236
+ logPath,
36237
+ `
36238
+ [${(/* @__PURE__ */ new Date()).toISOString()}] auto-spawning remix collab init for branch=${branchName ?? "(unknown)"} repo=${repoRoot}
36239
+ `,
36240
+ "utf8"
36241
+ );
36242
+ const child = (0, import_node_child_process8.spawn)("remix", ["collab", "init"], {
35719
36243
  cwd: repoRoot,
35720
36244
  detached: true,
35721
36245
  stdio: ["ignore", out, err],
@@ -35723,8 +36247,17 @@ function maybeAutoSpawnBranchInit(repoRoot) {
35723
36247
  });
35724
36248
  child.unref();
35725
36249
  try {
35726
- (0, import_node_fs6.writeFileSync)(spawnLockPath, String(child.pid ?? ""), "utf8");
35727
- (0, import_node_fs6.utimesSync)(spawnLockPath, /* @__PURE__ */ new Date(), /* @__PURE__ */ new Date());
36250
+ const lock = {
36251
+ schemaVersion: 1,
36252
+ pid: child.pid ?? null,
36253
+ branchName,
36254
+ repoRoot,
36255
+ startedAt: (/* @__PURE__ */ new Date()).toISOString(),
36256
+ command: "remix collab init"
36257
+ };
36258
+ (0, import_node_fs7.writeFileSync)(spawnLockPath, `${JSON.stringify(lock, null, 2)}
36259
+ `, "utf8");
36260
+ (0, import_node_fs7.utimesSync)(spawnLockPath, /* @__PURE__ */ new Date(), /* @__PURE__ */ new Date());
35728
36261
  } catch {
35729
36262
  }
35730
36263
  return { spawned: true, pid: child.pid, logPath };
@@ -35742,7 +36275,7 @@ function buildBranchInitContextMessage(branch, repoRoot, logPath) {
35742
36275
  "[Remix recovery in progress]:",
35743
36276
  `Remix is initializing recording for ${branchLabel} in ${repoRoot} in the background.`,
35744
36277
  "This turn may be recorded retroactively once init finishes (it may appear in the timeline with a small delay).",
35745
- "Do NOT call any Remix MCP tool to initialize, re-anchor, or sync this branch \u2014 the plugin is handling it automatically.",
36278
+ "Do NOT call any Remix MCP tool to initialize, repair, or sync this branch \u2014 the plugin is handling it automatically.",
35746
36279
  `Init log: ${logPath}`
35747
36280
  ].join("\n");
35748
36281
  }
@@ -35942,7 +36475,7 @@ async function runHookUserPrompt(payload) {
35942
36475
  }
35943
36476
  if (isCurrentBranchUnbound) {
35944
36477
  const currentBranch = bindingState?.currentBranch ?? null;
35945
- const outcome = maybeAutoSpawnBranchInit(boundRepo);
36478
+ const outcome = maybeAutoSpawnBranchInit(boundRepo, currentBranch);
35946
36479
  if (outcome.spawned) {
35947
36480
  advisorySections.push(buildBranchInitContextMessage(currentBranch, boundRepo, outcome.logPath));
35948
36481
  await appendHookDiagnosticsEvent({