@remixhq/claude-plugin 0.1.22 → 0.1.23

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,
@@ -9003,6 +9064,8 @@ function buildBaseState() {
9003
9064
  branchName: null,
9004
9065
  localCommitHash: null,
9005
9066
  currentSnapshotHash: null,
9067
+ currentServerRevisionId: null,
9068
+ currentServerTreeHash: null,
9006
9069
  currentServerHeadHash: null,
9007
9070
  currentServerHeadCommitId: null,
9008
9071
  worktreeClean: false,
@@ -9036,6 +9099,8 @@ function buildBaseState() {
9036
9099
  baseline: {
9037
9100
  lastSnapshotId: null,
9038
9101
  lastSnapshotHash: null,
9102
+ lastServerRevisionId: null,
9103
+ lastServerTreeHash: null,
9039
9104
  lastServerHeadHash: null,
9040
9105
  lastSeenLocalCommitHash: null
9041
9106
  }
@@ -9162,6 +9227,8 @@ async function collabDetectRepoState(params) {
9162
9227
  summarizeAsyncJobs({ repoRoot, branchName: binding.branchName ?? null })
9163
9228
  ]);
9164
9229
  const appHead = unwrapResponseObject(headResp, "app head");
9230
+ detected.currentServerRevisionId = appHead.headRevisionId ?? null;
9231
+ detected.currentServerTreeHash = appHead.treeHash ?? null;
9165
9232
  detected.currentServerHeadHash = appHead.headCommitHash;
9166
9233
  detected.currentServerHeadCommitId = appHead.headCommitId;
9167
9234
  detected.currentSnapshotHash = inspection.snapshotHash;
@@ -9170,6 +9237,8 @@ async function collabDetectRepoState(params) {
9170
9237
  detected.baseline = {
9171
9238
  lastSnapshotId: baseline?.lastSnapshotId ?? null,
9172
9239
  lastSnapshotHash: baseline?.lastSnapshotHash ?? null,
9240
+ lastServerRevisionId: baseline?.lastServerRevisionId ?? null,
9241
+ lastServerTreeHash: baseline?.lastServerTreeHash ?? null,
9173
9242
  lastServerHeadHash: baseline?.lastServerHeadHash ?? null,
9174
9243
  lastSeenLocalCommitHash: baseline?.lastSeenLocalCommitHash ?? null
9175
9244
  };
@@ -9179,6 +9248,7 @@ async function collabDetectRepoState(params) {
9179
9248
  const bootstrapResp = await params.api.getAppDelta(binding.currentAppId, {
9180
9249
  baseHeadHash: localCommitHash,
9181
9250
  targetHeadHash: appHead.headCommitHash,
9251
+ targetRevisionId: appHead.headRevisionId,
9182
9252
  repoFingerprint: binding.repoFingerprint ?? void 0,
9183
9253
  remoteUrl: binding.remoteUrl ?? void 0,
9184
9254
  defaultBranch: binding.defaultBranch ?? void 0
@@ -9201,7 +9271,7 @@ async function collabDetectRepoState(params) {
9201
9271
  }
9202
9272
  }
9203
9273
  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.";
9274
+ 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
9275
  return detected;
9206
9276
  }
9207
9277
  const localHeadMovedSinceBaseline = Boolean(baseline.lastSeenLocalCommitHash) && localCommitHash !== baseline.lastSeenLocalCommitHash;
@@ -9220,7 +9290,30 @@ async function collabDetectRepoState(params) {
9220
9290
  return detected;
9221
9291
  }
9222
9292
  const localChanged = inspection.snapshotHash !== baseline.lastSnapshotHash;
9223
- const serverChanged = appHead.headCommitHash !== baseline.lastServerHeadHash;
9293
+ const serverHeadChanged = appHead.headCommitHash !== baseline.lastServerHeadHash;
9294
+ const revisionChanged = Boolean(
9295
+ baseline.lastServerRevisionId && (appHead.headRevisionId ?? null) !== baseline.lastServerRevisionId
9296
+ );
9297
+ const equivalentRevisionDrift = revisionChanged && !serverHeadChanged;
9298
+ if (equivalentRevisionDrift) {
9299
+ await writeLocalBaseline({
9300
+ repoRoot,
9301
+ repoFingerprint: binding.repoFingerprint,
9302
+ laneId: binding.laneId,
9303
+ currentAppId: binding.currentAppId,
9304
+ branchName: binding.branchName,
9305
+ lastSnapshotId: baseline.lastSnapshotId,
9306
+ lastSnapshotHash: baseline.lastSnapshotHash,
9307
+ lastServerRevisionId: appHead.headRevisionId ?? null,
9308
+ lastServerTreeHash: appHead.treeHash ?? baseline.lastServerTreeHash ?? null,
9309
+ lastServerHeadHash: appHead.headCommitHash,
9310
+ lastSeenLocalCommitHash: baseline.lastSeenLocalCommitHash
9311
+ });
9312
+ detected.baseline.lastServerRevisionId = appHead.headRevisionId ?? null;
9313
+ detected.baseline.lastServerTreeHash = appHead.treeHash ?? baseline.lastServerTreeHash ?? null;
9314
+ detected.baseline.lastServerHeadHash = appHead.headCommitHash;
9315
+ }
9316
+ const serverChanged = serverHeadChanged;
9224
9317
  if (!localChanged && !serverChanged) {
9225
9318
  detected.repoState = "idle";
9226
9319
  return detected;
@@ -9644,6 +9737,7 @@ function buildWorkspaceMetadata(params) {
9644
9737
  recordingMode: "boundary_delta",
9645
9738
  baselineSnapshotId: params.baselineSnapshotId,
9646
9739
  currentSnapshotId: params.currentSnapshotId,
9740
+ baselineServerRevisionId: params.baselineServerRevisionId ?? null,
9647
9741
  baselineServerHeadHash: params.baselineServerHeadHash,
9648
9742
  currentSnapshotHash: params.currentSnapshotHash,
9649
9743
  localCommitHash: params.localCommitHash,
@@ -9722,12 +9816,12 @@ async function processClaimedPendingFinalizeJobInner(params) {
9722
9816
  throw buildFinalizeCliError({
9723
9817
  message: "Local baseline is missing for this queued finalize job.",
9724
9818
  exitCode: 2,
9725
- hint: "Run `remix collab re-anchor` to anchor the repository again.",
9819
+ hint: "Run `remix collab init` to seed this checkout's revision baseline.",
9726
9820
  disposition: "terminal",
9727
9821
  reason: "baseline_missing"
9728
9822
  });
9729
9823
  }
9730
- const baselineDrifted = baseline.lastSnapshotId !== job.baselineSnapshotId || baseline.lastServerHeadHash !== job.baselineServerHeadHash;
9824
+ const baselineDrifted = baseline.lastSnapshotId !== job.baselineSnapshotId || (job.baselineServerRevisionId ? baseline.lastServerRevisionId !== job.baselineServerRevisionId : false) || baseline.lastServerHeadHash !== job.baselineServerHeadHash;
9731
9825
  const appHead = unwrapResponseObject(appHeadResp, "app head");
9732
9826
  const remoteUrl = readMetadataString(job, "remoteUrl");
9733
9827
  const defaultBranch = readMetadataString(job, "defaultBranch");
@@ -9750,12 +9844,13 @@ async function processClaimedPendingFinalizeJobInner(params) {
9750
9844
  throw buildFinalizeCliError({
9751
9845
  message: "Finalize queue baseline drifted before this job was processed.",
9752
9846
  exitCode: 1,
9753
- hint: "Process queued finalize jobs in capture order, or re-anchor the repository before retrying.",
9847
+ hint: "Process queued finalize jobs in capture order, or run `remix collab init` to refresh the revision baseline before retrying.",
9754
9848
  disposition: "terminal",
9755
9849
  reason: "baseline_drifted"
9756
9850
  });
9757
9851
  }
9758
- if (appHead.headCommitHash !== job.baselineServerHeadHash) {
9852
+ const serverStillAtBaseline = job.baselineServerRevisionId ? appHead.headRevisionId === job.baselineServerRevisionId : appHead.headCommitHash === job.baselineServerHeadHash;
9853
+ if (!serverStillAtBaseline) {
9759
9854
  throw buildFinalizeCliError({
9760
9855
  message: "Server lane changed before a no-diff turn could be recorded.",
9761
9856
  exitCode: 2,
@@ -9777,6 +9872,7 @@ async function processClaimedPendingFinalizeJobInner(params) {
9777
9872
  defaultBranch,
9778
9873
  baselineSnapshotId: job.baselineSnapshotId,
9779
9874
  currentSnapshotId: job.currentSnapshotId,
9875
+ baselineServerRevisionId: job.baselineServerRevisionId,
9780
9876
  baselineServerHeadHash: job.baselineServerHeadHash,
9781
9877
  currentSnapshotHash: snapshot.snapshotHash,
9782
9878
  localCommitHash: snapshot.localCommitHash,
@@ -9797,6 +9893,8 @@ async function processClaimedPendingFinalizeJobInner(params) {
9797
9893
  branchName: job.branchName,
9798
9894
  lastSnapshotId: snapshot.id,
9799
9895
  lastSnapshotHash: snapshot.snapshotHash,
9896
+ lastServerRevisionId: appHead.headRevisionId ?? null,
9897
+ lastServerTreeHash: appHead.treeHash ?? null,
9800
9898
  lastServerHeadHash: appHead.headCommitHash,
9801
9899
  lastSeenLocalCommitHash: snapshot.localCommitHash
9802
9900
  });
@@ -9817,14 +9915,14 @@ async function processClaimedPendingFinalizeJobInner(params) {
9817
9915
  };
9818
9916
  }
9819
9917
  const localBaselineAdvanced = baseline.lastSnapshotId !== job.baselineSnapshotId;
9820
- const serverHeadAdvanced = appHead.headCommitHash !== job.baselineServerHeadHash;
9918
+ const serverHeadAdvanced = job.baselineServerRevisionId ? appHead.headRevisionId !== job.baselineServerRevisionId : appHead.headCommitHash !== job.baselineServerHeadHash;
9821
9919
  if (baselineDrifted) {
9822
9920
  const consistentAdvance = localBaselineAdvanced && serverHeadAdvanced;
9823
9921
  if (!consistentAdvance) {
9824
9922
  throw buildFinalizeCliError({
9825
9923
  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
9924
  exitCode: 1,
9827
- hint: "Run `remix collab status` to inspect, then `remix collab re-anchor` only if the lane has no valid baseline.",
9925
+ hint: "Run `remix collab status` to inspect, then sync or reconcile before retrying.",
9828
9926
  disposition: "terminal",
9829
9927
  reason: "baseline_drifted"
9830
9928
  });
@@ -9832,6 +9930,7 @@ async function processClaimedPendingFinalizeJobInner(params) {
9832
9930
  }
9833
9931
  let submissionDiff = diffResult.diff;
9834
9932
  let submissionBaseHeadHash = job.baselineServerHeadHash;
9933
+ let submissionBaseRevisionId = job.baselineServerRevisionId;
9835
9934
  let replayedFromBaseHash = null;
9836
9935
  if (!submissionBaseHeadHash) {
9837
9936
  throw buildFinalizeCliError({
@@ -9849,7 +9948,9 @@ async function processClaimedPendingFinalizeJobInner(params) {
9849
9948
  assistantResponse: job.assistantResponse,
9850
9949
  diff: diffResult.diff,
9851
9950
  baseCommitHash: submissionBaseHeadHash,
9951
+ baseRevisionId: job.baselineServerRevisionId,
9852
9952
  targetHeadCommitHash: appHead.headCommitHash,
9953
+ targetRevisionId: appHead.headRevisionId,
9853
9954
  expectedPaths: diffResult.changedPaths,
9854
9955
  actor,
9855
9956
  workspaceMetadata: buildWorkspaceMetadata({
@@ -9859,6 +9960,7 @@ async function processClaimedPendingFinalizeJobInner(params) {
9859
9960
  defaultBranch,
9860
9961
  baselineSnapshotId: job.baselineSnapshotId,
9861
9962
  currentSnapshotId: job.currentSnapshotId,
9963
+ baselineServerRevisionId: job.baselineServerRevisionId,
9862
9964
  baselineServerHeadHash: job.baselineServerHeadHash,
9863
9965
  currentSnapshotHash: snapshot.snapshotHash,
9864
9966
  localCommitHash: snapshot.localCommitHash,
@@ -9884,6 +9986,7 @@ async function processClaimedPendingFinalizeJobInner(params) {
9884
9986
  submissionDiff = replayDiff.diff;
9885
9987
  replayedFromBaseHash = submissionBaseHeadHash;
9886
9988
  submissionBaseHeadHash = appHead.headCommitHash;
9989
+ submissionBaseRevisionId = appHead.headRevisionId;
9887
9990
  } catch (error) {
9888
9991
  if (error instanceof RemixError && error.finalizeDisposition === void 0) {
9889
9992
  const detail = error.hint ? `${error.message} (${error.hint})` : error.message;
@@ -9905,6 +10008,7 @@ async function processClaimedPendingFinalizeJobInner(params) {
9905
10008
  assistantResponse: job.assistantResponse,
9906
10009
  diff: submissionDiff,
9907
10010
  baseCommitHash: submissionBaseHeadHash,
10011
+ baseRevisionId: submissionBaseRevisionId,
9908
10012
  headCommitHash: submissionBaseHeadHash,
9909
10013
  changedFilesCount: diffResult.stats.changedFilesCount,
9910
10014
  insertions: diffResult.stats.insertions,
@@ -9917,6 +10021,7 @@ async function processClaimedPendingFinalizeJobInner(params) {
9917
10021
  defaultBranch,
9918
10022
  baselineSnapshotId: job.baselineSnapshotId,
9919
10023
  currentSnapshotId: job.currentSnapshotId,
10024
+ baselineServerRevisionId: job.baselineServerRevisionId,
9920
10025
  baselineServerHeadHash: job.baselineServerHeadHash,
9921
10026
  currentSnapshotHash: snapshot.snapshotHash,
9922
10027
  localCommitHash: snapshot.localCommitHash,
@@ -9938,11 +10043,28 @@ async function processClaimedPendingFinalizeJobInner(params) {
9938
10043
  throw buildFinalizeCliError({
9939
10044
  message: "Backend returned a succeeded change step without a head commit hash.",
9940
10045
  exitCode: 1,
9941
- hint: "This is a backend invariant violation; retry will not help. Re-anchor and try again.",
10046
+ hint: "This is a backend invariant violation; retry will not help. Run `remix collab status` before trying again.",
9942
10047
  disposition: "terminal",
9943
10048
  reason: "missing_head_commit_hash"
9944
10049
  });
9945
10050
  }
10051
+ let nextServerRevisionId = typeof changeStep.resultRevisionId === "string" ? changeStep.resultRevisionId.trim() : "";
10052
+ let nextServerTreeHash = null;
10053
+ if (!nextServerRevisionId) {
10054
+ const freshHeadResp = await params.api.getAppHead(job.currentAppId);
10055
+ const freshHead = unwrapResponseObject(freshHeadResp, "app head");
10056
+ if (freshHead.headCommitHash !== nextServerHeadHash || !freshHead.headRevisionId) {
10057
+ throw buildFinalizeCliError({
10058
+ message: "Backend returned a succeeded change step without a matching result revision.",
10059
+ exitCode: 1,
10060
+ 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`.",
10061
+ disposition: "terminal",
10062
+ reason: "missing_result_revision_id"
10063
+ });
10064
+ }
10065
+ nextServerRevisionId = freshHead.headRevisionId;
10066
+ nextServerTreeHash = freshHead.treeHash ?? null;
10067
+ }
9946
10068
  await writeLocalBaseline({
9947
10069
  repoRoot: job.repoRoot,
9948
10070
  repoFingerprint: job.repoFingerprint,
@@ -9951,6 +10073,8 @@ async function processClaimedPendingFinalizeJobInner(params) {
9951
10073
  branchName: job.branchName,
9952
10074
  lastSnapshotId: snapshot.id,
9953
10075
  lastSnapshotHash: snapshot.snapshotHash,
10076
+ lastServerRevisionId: nextServerRevisionId,
10077
+ lastServerTreeHash: nextServerTreeHash,
9954
10078
  lastServerHeadHash: nextServerHeadHash,
9955
10079
  lastSeenLocalCommitHash: snapshot.localCommitHash
9956
10080
  });
@@ -9982,6 +10106,7 @@ async function enqueueCapturedFinalizeTurn(params) {
9982
10106
  prompt: params.prompt,
9983
10107
  assistantResponse: params.assistantResponse,
9984
10108
  baselineSnapshotId: params.baselineSnapshotId,
10109
+ baselineServerRevisionId: params.baselineServerRevisionId ?? null,
9985
10110
  baselineServerHeadHash: params.baselineServerHeadHash,
9986
10111
  currentSnapshotId: params.currentSnapshotId,
9987
10112
  idempotencyKey: params.idempotencyKey,
@@ -10080,17 +10205,6 @@ async function collabFinalizeTurn(params) {
10080
10205
  });
10081
10206
  }
10082
10207
  }
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
10208
  const detected = await collabDetectRepoState({
10095
10209
  api: params.api,
10096
10210
  cwd: repoRoot,
@@ -10131,9 +10245,16 @@ async function collabFinalizeTurn(params) {
10131
10245
  hint: detected.hint
10132
10246
  });
10133
10247
  }
10248
+ if (detected.repoState === "both_changed") {
10249
+ throw new RemixError("Local and server changes must be reconciled before finalizing this turn.", {
10250
+ code: "reconcile_required",
10251
+ exitCode: 2,
10252
+ hint: detected.hint || "Run `remix collab reconcile --dry-run` to inspect recovery options before retrying."
10253
+ });
10254
+ }
10134
10255
  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",
10256
+ throw new RemixError("The local checkout is missing a Remix revision baseline for this lane.", {
10257
+ code: "baseline_missing",
10137
10258
  exitCode: 2,
10138
10259
  hint: detected.hint
10139
10260
  });
@@ -10145,8 +10266,9 @@ async function collabFinalizeTurn(params) {
10145
10266
  });
10146
10267
  if (!baseline) {
10147
10268
  throw new RemixError("Local Remix baseline is missing for this lane.", {
10269
+ code: "baseline_missing",
10148
10270
  exitCode: 2,
10149
- hint: "Run `remix collab re-anchor` to create a fresh baseline."
10271
+ hint: "Run `remix collab init` or sync this lane to create a fresh revision baseline."
10150
10272
  });
10151
10273
  }
10152
10274
  const snapshot = await captureLocalSnapshot({
@@ -10157,10 +10279,11 @@ async function collabFinalizeTurn(params) {
10157
10279
  });
10158
10280
  const mode = snapshot.snapshotHash === baseline.lastSnapshotHash ? "no_diff_turn" : "changed_turn";
10159
10281
  const idempotencyKey = params.idempotencyKey?.trim() || buildDeterministicIdempotencyKey({
10160
- kind: "collab_finalize_turn_boundary_v1",
10282
+ kind: "collab_finalize_turn_boundary_v2",
10161
10283
  appId: binding.currentAppId,
10162
10284
  laneId: binding.laneId,
10163
10285
  baselineSnapshotId: baseline.lastSnapshotId,
10286
+ baselineServerRevisionId: baseline.lastServerRevisionId,
10164
10287
  baselineServerHeadHash: baseline.lastServerHeadHash,
10165
10288
  currentSnapshotId: snapshot.id,
10166
10289
  currentSnapshotHash: snapshot.snapshotHash,
@@ -10180,6 +10303,7 @@ async function collabFinalizeTurn(params) {
10180
10303
  prompt,
10181
10304
  assistantResponse,
10182
10305
  baselineSnapshotId: baseline.lastSnapshotId,
10306
+ baselineServerRevisionId: baseline.lastServerRevisionId,
10183
10307
  baselineServerHeadHash: baseline.lastServerHeadHash,
10184
10308
  currentSnapshotId: snapshot.id,
10185
10309
  idempotencyKey,
@@ -10226,13 +10350,538 @@ var FINALIZE_PREFLIGHT_FAILURE_CODES = [
10226
10350
  // Server has commits we don't. Fix: `remix collab sync` (safe to
10227
10351
  // auto-run for fast-forward; non-FF refused by the command itself).
10228
10352
  "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"
10353
+ // Both local and server changed. Fix: inspect and apply reconcile.
10354
+ "reconcile_required",
10355
+ // Local revision baseline is missing. Fix: `remix collab init` or sync.
10356
+ "baseline_missing"
10232
10357
  ];
10233
10358
  var CODE_SET = new Set(FINALIZE_PREFLIGHT_FAILURE_CODES);
10359
+ function isFinalizePreflightFailureCode(value) {
10360
+ return typeof value === "string" && CODE_SET.has(value);
10361
+ }
10362
+
10363
+ // src/auto-fix-dispatcher.ts
10364
+ var import_node_child_process6 = require("child_process");
10365
+ var import_node_fs6 = require("fs");
10366
+ var import_node_path10 = __toESM(require("path"), 1);
10367
+
10368
+ // src/finalize-failure-marker.ts
10369
+ var import_promises19 = __toESM(require("fs/promises"), 1);
10370
+ var import_node_path7 = __toESM(require("path"), 1);
10371
+ var FINALIZE_FAILURE_MARKER_REL = import_node_path7.default.join(".remix", ".last-finalize-failure.json");
10372
+ function markerPath(repoRoot) {
10373
+ return import_node_path7.default.join(repoRoot, FINALIZE_FAILURE_MARKER_REL);
10374
+ }
10375
+ async function readFinalizeFailureMarker(repoRoot) {
10376
+ const raw = await import_promises19.default.readFile(markerPath(repoRoot), "utf8").catch(() => null);
10377
+ if (!raw) return null;
10378
+ try {
10379
+ const parsed = JSON.parse(raw);
10380
+ if (parsed.schemaVersion !== 1) return null;
10381
+ if (typeof parsed.repoRoot !== "string" || typeof parsed.failedAt !== "string") return null;
10382
+ if (typeof parsed.message !== "string") return null;
10383
+ return parsed;
10384
+ } catch {
10385
+ return null;
10386
+ }
10387
+ }
10388
+ async function writeFinalizeFailureMarker(marker) {
10389
+ const filePath = markerPath(marker.repoRoot);
10390
+ await import_promises19.default.mkdir(import_node_path7.default.dirname(filePath), { recursive: true });
10391
+ const tmpPath = `${filePath}.tmp-${process.pid}-${Date.now()}`;
10392
+ await import_promises19.default.writeFile(tmpPath, JSON.stringify(marker, null, 2), "utf8");
10393
+ await import_promises19.default.rename(tmpPath, filePath);
10394
+ }
10395
+ function buildFreshFailureMarker(params) {
10396
+ return {
10397
+ schemaVersion: 1,
10398
+ failedAt: (/* @__PURE__ */ new Date()).toISOString(),
10399
+ repoRoot: params.repoRoot,
10400
+ preflightCode: params.preflightCode,
10401
+ message: params.message,
10402
+ hint: params.hint,
10403
+ recommendedCommand: params.recommendedCommand,
10404
+ autoFix: {
10405
+ status: "not_attempted",
10406
+ command: null,
10407
+ pid: null,
10408
+ logPath: null,
10409
+ attemptedAt: null,
10410
+ failureMessage: null
10411
+ }
10412
+ };
10413
+ }
10414
+
10415
+ // src/hook-diagnostics.ts
10416
+ var import_node_crypto2 = require("crypto");
10417
+ var import_promises21 = __toESM(require("fs/promises"), 1);
10418
+ var import_node_os6 = __toESM(require("os"), 1);
10419
+ var import_node_path9 = __toESM(require("path"), 1);
10420
+
10421
+ // src/hook-state.ts
10422
+ var import_promises20 = __toESM(require("fs/promises"), 1);
10423
+ var import_node_os5 = __toESM(require("os"), 1);
10424
+ var import_node_path8 = __toESM(require("path"), 1);
10425
+ var import_node_crypto = require("crypto");
10426
+ function stateRoot2() {
10427
+ const configured = process.env.REMIX_CLAUDE_PLUGIN_HOOK_STATE_ROOT?.trim();
10428
+ return configured || import_node_path8.default.join(import_node_os5.default.tmpdir(), "remix-claude-plugin-hooks");
10429
+ }
10430
+ function statePath(sessionId) {
10431
+ return import_node_path8.default.join(stateRoot2(), `${sessionId}.json`);
10432
+ }
10433
+ function stateLockPath(sessionId) {
10434
+ return import_node_path8.default.join(stateRoot2(), `${sessionId}.lock`);
10435
+ }
10436
+ function stateLockMetaPath(sessionId) {
10437
+ return import_node_path8.default.join(stateLockPath(sessionId), "owner.json");
10438
+ }
10439
+ async function writeJsonAtomic2(filePath, value) {
10440
+ await import_promises20.default.mkdir(import_node_path8.default.dirname(filePath), { recursive: true });
10441
+ const tmpPath = `${filePath}.tmp-${Date.now()}-${Math.random().toString(16).slice(2)}`;
10442
+ await import_promises20.default.writeFile(tmpPath, JSON.stringify(value, null, 2) + "\n", "utf8");
10443
+ await import_promises20.default.rename(tmpPath, filePath);
10444
+ }
10445
+ var STATE_LOCK_WAIT_MS = 2e3;
10446
+ var STATE_LOCK_POLL_MS = 25;
10447
+ var STATE_LOCK_STALE_MS = 3e4;
10448
+ var STATE_LOCK_HEARTBEAT_MS = 5e3;
10449
+ async function sleep2(ms) {
10450
+ await new Promise((resolve) => setTimeout(resolve, ms));
10451
+ }
10452
+ async function readStateLockMetadata(sessionId) {
10453
+ const raw = await import_promises20.default.readFile(stateLockMetaPath(sessionId), "utf8").catch(() => null);
10454
+ if (!raw) return null;
10455
+ try {
10456
+ const parsed = JSON.parse(raw);
10457
+ if (typeof parsed.ownerId !== "string" || typeof parsed.pid !== "number" || typeof parsed.createdAt !== "string" || typeof parsed.heartbeatAt !== "string") {
10458
+ return null;
10459
+ }
10460
+ return {
10461
+ ownerId: parsed.ownerId,
10462
+ pid: parsed.pid,
10463
+ createdAt: parsed.createdAt,
10464
+ heartbeatAt: parsed.heartbeatAt
10465
+ };
10466
+ } catch {
10467
+ return null;
10468
+ }
10469
+ }
10470
+ async function writeStateLockMetadata(sessionId, metadata) {
10471
+ await writeJsonAtomic2(stateLockMetaPath(sessionId), metadata);
10472
+ }
10473
+ async function tryRemoveStaleStateLock(sessionId) {
10474
+ const lockPath = stateLockPath(sessionId);
10475
+ const metadata = await readStateLockMetadata(sessionId);
10476
+ const staleByHeartbeat = metadata && Date.now() - new Date(metadata.heartbeatAt).getTime() > STATE_LOCK_STALE_MS;
10477
+ if (staleByHeartbeat) {
10478
+ await import_promises20.default.rm(lockPath, { recursive: true, force: true }).catch(() => void 0);
10479
+ return true;
10480
+ }
10481
+ if (!metadata) {
10482
+ const lockStat = await import_promises20.default.stat(lockPath).catch(() => null);
10483
+ if (lockStat && Date.now() - lockStat.mtimeMs > STATE_LOCK_STALE_MS) {
10484
+ await import_promises20.default.rm(lockPath, { recursive: true, force: true }).catch(() => void 0);
10485
+ return true;
10486
+ }
10487
+ }
10488
+ return false;
10489
+ }
10490
+ async function acquireStateLock(sessionId) {
10491
+ const lockPath = stateLockPath(sessionId);
10492
+ const deadline = Date.now() + STATE_LOCK_WAIT_MS;
10493
+ await import_promises20.default.mkdir(stateRoot2(), { recursive: true });
10494
+ while (true) {
10495
+ try {
10496
+ await import_promises20.default.mkdir(lockPath);
10497
+ const ownerId = (0, import_node_crypto.randomUUID)();
10498
+ const createdAt = (/* @__PURE__ */ new Date()).toISOString();
10499
+ const metadata = {
10500
+ ownerId,
10501
+ pid: process.pid,
10502
+ createdAt,
10503
+ heartbeatAt: createdAt
10504
+ };
10505
+ await writeStateLockMetadata(sessionId, metadata);
10506
+ let released = false;
10507
+ const heartbeat = setInterval(() => {
10508
+ if (released) return;
10509
+ void writeStateLockMetadata(sessionId, {
10510
+ ...metadata,
10511
+ heartbeatAt: (/* @__PURE__ */ new Date()).toISOString()
10512
+ }).catch(() => void 0);
10513
+ }, STATE_LOCK_HEARTBEAT_MS);
10514
+ heartbeat.unref?.();
10515
+ return async () => {
10516
+ if (released) return;
10517
+ released = true;
10518
+ clearInterval(heartbeat);
10519
+ const currentMetadata = await readStateLockMetadata(sessionId);
10520
+ if (currentMetadata?.ownerId === ownerId) {
10521
+ await import_promises20.default.rm(lockPath, { recursive: true, force: true }).catch(() => void 0);
10522
+ }
10523
+ };
10524
+ } catch (error) {
10525
+ const code = error && typeof error === "object" && "code" in error ? error.code : null;
10526
+ if (code !== "EEXIST") {
10527
+ throw error;
10528
+ }
10529
+ if (await tryRemoveStaleStateLock(sessionId)) {
10530
+ continue;
10531
+ }
10532
+ if (Date.now() >= deadline) {
10533
+ throw new Error(`Timed out acquiring hook state lock for session ${sessionId}.`);
10534
+ }
10535
+ await sleep2(STATE_LOCK_POLL_MS);
10536
+ }
10537
+ }
10538
+ }
10539
+ async function withStateLock(sessionId, fn) {
10540
+ const release = await acquireStateLock(sessionId);
10541
+ try {
10542
+ return await fn();
10543
+ } finally {
10544
+ await release();
10545
+ }
10546
+ }
10547
+ async function savePendingTurnState(state) {
10548
+ await writeJsonAtomic2(statePath(state.sessionId), state);
10549
+ }
10550
+ async function createPendingTurnState(params) {
10551
+ return withStateLock(params.sessionId, async () => {
10552
+ const state = {
10553
+ sessionId: params.sessionId,
10554
+ turnId: (0, import_node_crypto.randomUUID)(),
10555
+ prompt: params.prompt,
10556
+ initialCwd: params.initialCwd?.trim() || null,
10557
+ intent: params.intent,
10558
+ submittedAt: (/* @__PURE__ */ new Date()).toISOString(),
10559
+ consultedMemory: false,
10560
+ touchedRepos: {},
10561
+ turnFailureMessage: null,
10562
+ turnFailureHint: null,
10563
+ turnFailedAt: null
10564
+ };
10565
+ await savePendingTurnState(state);
10566
+ return state;
10567
+ });
10568
+ }
10569
+
10570
+ // package.json
10571
+ var package_default = {
10572
+ name: "@remixhq/claude-plugin",
10573
+ version: "0.1.23",
10574
+ description: "Claude Code plugin for Remix collaboration workflows",
10575
+ homepage: "https://github.com/RemixDotOne/remix-claude-plugin",
10576
+ license: "MIT",
10577
+ repository: {
10578
+ type: "git",
10579
+ url: "https://github.com/RemixDotOne/remix-claude-plugin.git"
10580
+ },
10581
+ type: "module",
10582
+ engines: {
10583
+ node: ">=20"
10584
+ },
10585
+ publishConfig: {
10586
+ access: "public"
10587
+ },
10588
+ files: [
10589
+ "dist",
10590
+ ".claude-plugin/plugin.json",
10591
+ ".mcp.json",
10592
+ "skills",
10593
+ "hooks",
10594
+ "agents"
10595
+ ],
10596
+ exports: {
10597
+ ".": {
10598
+ types: "./dist/index.d.ts",
10599
+ import: "./dist/index.js"
10600
+ }
10601
+ },
10602
+ scripts: {
10603
+ build: "tsup",
10604
+ 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);"`,
10605
+ dev: "tsx src/mcp-server.ts",
10606
+ typecheck: "tsc -p tsconfig.json --noEmit",
10607
+ test: "node --import tsx --test 'src/**/*.test.ts'",
10608
+ prepack: "npm run build"
10609
+ },
10610
+ dependencies: {
10611
+ "@remixhq/core": "^0.1.18",
10612
+ "@remixhq/mcp": "^0.1.18"
10613
+ },
10614
+ devDependencies: {
10615
+ "@types/node": "^25.4.0",
10616
+ tsup: "^8.5.1",
10617
+ tsx: "^4.21.0",
10618
+ typescript: "^5.9.3"
10619
+ }
10620
+ };
10621
+
10622
+ // src/metadata.ts
10623
+ var pluginMetadata = {
10624
+ name: package_default.name,
10625
+ version: package_default.version,
10626
+ description: package_default.description,
10627
+ pluginId: "remix",
10628
+ agentName: "remix-collab"
10629
+ };
10234
10630
 
10235
- // node_modules/@remixhq/core/dist/chunk-US5SM7ZC.js
10631
+ // src/hook-diagnostics.ts
10632
+ var MAX_LOG_BYTES = 512 * 1024;
10633
+ function resolveClaudeRoot() {
10634
+ const configured = process.env.CLAUDE_CONFIG_DIR?.trim();
10635
+ return configured || import_node_path9.default.join(import_node_os6.default.homedir(), ".claude");
10636
+ }
10637
+ function resolvePluginDataDirName() {
10638
+ return `${pluginMetadata.pluginId}-${pluginMetadata.pluginId}`;
10639
+ }
10640
+ function getHookDiagnosticsDirPath() {
10641
+ const configured = process.env.REMIX_CLAUDE_PLUGIN_HOOK_DIAGNOSTICS_DIR?.trim();
10642
+ return configured || import_node_path9.default.join(resolveClaudeRoot(), "plugins", "data", resolvePluginDataDirName());
10643
+ }
10644
+ function getHookDiagnosticsLogPath() {
10645
+ return import_node_path9.default.join(getHookDiagnosticsDirPath(), "hooks.ndjson");
10646
+ }
10647
+ function toFieldValue(value) {
10648
+ if (value === null) return null;
10649
+ if (typeof value === "string") return value;
10650
+ if (typeof value === "number" && Number.isFinite(value)) return value;
10651
+ if (typeof value === "boolean") return value;
10652
+ return void 0;
10653
+ }
10654
+ function normalizeFields(fields) {
10655
+ if (!fields) return {};
10656
+ const normalizedEntries = Object.entries(fields).map(([key, value]) => {
10657
+ const normalized = toFieldValue(value);
10658
+ return normalized === void 0 ? null : [key, normalized];
10659
+ }).filter((entry) => entry !== null);
10660
+ return Object.fromEntries(normalizedEntries);
10661
+ }
10662
+ async function rotateLogIfNeeded(logPath) {
10663
+ const stat = await import_promises21.default.stat(logPath).catch(() => null);
10664
+ if (!stat || stat.size < MAX_LOG_BYTES) {
10665
+ return;
10666
+ }
10667
+ const rotatedPath = `${logPath}.1`;
10668
+ await import_promises21.default.rm(rotatedPath, { force: true }).catch(() => void 0);
10669
+ await import_promises21.default.rename(logPath, rotatedPath).catch(() => void 0);
10670
+ }
10671
+ function summarizeText(value) {
10672
+ if (typeof value !== "string" || !value.trim()) {
10673
+ return {
10674
+ present: false,
10675
+ length: 0,
10676
+ sha256Prefix: null
10677
+ };
10678
+ }
10679
+ const trimmed = value.trim();
10680
+ return {
10681
+ present: true,
10682
+ length: trimmed.length,
10683
+ sha256Prefix: (0, import_node_crypto2.createHash)("sha256").update(trimmed).digest("hex").slice(0, 12)
10684
+ };
10685
+ }
10686
+ async function appendHookDiagnosticsEvent(params) {
10687
+ try {
10688
+ const logPath = getHookDiagnosticsLogPath();
10689
+ await import_promises21.default.mkdir(import_node_path9.default.dirname(logPath), { recursive: true });
10690
+ await rotateLogIfNeeded(logPath);
10691
+ const event = {
10692
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
10693
+ hook: params.hook,
10694
+ pluginVersion: pluginMetadata.version,
10695
+ pid: process.pid,
10696
+ sessionId: params.sessionId?.trim() || null,
10697
+ turnId: params.turnId?.trim() || null,
10698
+ stage: params.stage.trim(),
10699
+ result: params.result,
10700
+ reason: params.reason?.trim() || null,
10701
+ toolName: params.toolName?.trim() || null,
10702
+ repoRoot: params.repoRoot?.trim() || null,
10703
+ message: params.message?.trim() || null,
10704
+ fields: normalizeFields(params.fields)
10705
+ };
10706
+ await import_promises21.default.appendFile(logPath, `${JSON.stringify(event)}
10707
+ `, "utf8");
10708
+ } catch {
10709
+ }
10710
+ }
10711
+
10712
+ // src/auto-fix-dispatcher.ts
10713
+ var AUTO_FIX_COMMAND = {
10714
+ // Already auto-spawned by hook-user-prompt's branch-init path, but we
10715
+ // include it here too so a finalize-time failure (e.g. binding got
10716
+ // deleted between init and the next finalize) also self-heals.
10717
+ branch_binding_missing: ["collab", "init"],
10718
+ // Local revision baseline is missing. Init seeds the branch/lane baseline
10719
+ // without requiring the user to know about the recording internals.
10720
+ baseline_missing: ["collab", "init"],
10721
+ // Server moved ahead. `collab sync` is fast-forward-safe by default;
10722
+ // it refuses non-FF on its own, so we don't need to gate here.
10723
+ pull_required: ["collab", "sync"]
10724
+ };
10725
+ var RECOMMENDED_USER_COMMAND = {
10726
+ not_bound: "remix collab init",
10727
+ branch_binding_missing: "remix collab init",
10728
+ family_ambiguous: "remix collab status",
10729
+ metadata_conflict: "remix collab status",
10730
+ branch_mismatch: "remix collab status",
10731
+ missing_head: "remix collab status",
10732
+ remote_error: "remix collab status",
10733
+ pull_required: "remix collab sync",
10734
+ baseline_missing: "remix collab init"
10735
+ };
10736
+ var SPAWN_LOCK_REL = (cmdSlug) => import_node_path10.default.join(".remix", `.${cmdSlug}-spawning`);
10737
+ var SPAWN_LOG_REL = (cmdSlug) => import_node_path10.default.join(".remix", `${cmdSlug}.log`);
10738
+ var SPAWN_THROTTLE_MS = 5 * 60 * 1e3;
10739
+ function commandSlug(args) {
10740
+ return args.join("-").replace(/[^a-zA-Z0-9_-]/g, "_");
10741
+ }
10742
+ function spawnFixDetached(repoRoot, args) {
10743
+ const slug = commandSlug(args);
10744
+ const command = `remix ${args.join(" ")}`;
10745
+ const remixDir = import_node_path10.default.join(repoRoot, ".remix");
10746
+ const lockPath = import_node_path10.default.join(repoRoot, SPAWN_LOCK_REL(slug));
10747
+ const logPath = import_node_path10.default.join(repoRoot, SPAWN_LOG_REL(slug));
10748
+ try {
10749
+ if ((0, import_node_fs6.existsSync)(lockPath)) {
10750
+ const ageMs = Date.now() - (0, import_node_fs6.statSync)(lockPath).mtimeMs;
10751
+ if (ageMs < SPAWN_THROTTLE_MS) {
10752
+ return { kind: "spawn_throttled", command, reason: "spawn_lock_held" };
10753
+ }
10754
+ }
10755
+ } catch {
10756
+ }
10757
+ try {
10758
+ (0, import_node_fs6.mkdirSync)(remixDir, { recursive: true });
10759
+ } catch {
10760
+ }
10761
+ let out;
10762
+ let err;
10763
+ try {
10764
+ out = (0, import_node_fs6.openSync)(logPath, "a");
10765
+ err = (0, import_node_fs6.openSync)(logPath, "a");
10766
+ } catch (logErr) {
10767
+ return {
10768
+ kind: "spawn_failed",
10769
+ command,
10770
+ reason: "log_open_failed",
10771
+ message: logErr instanceof Error ? logErr.message : String(logErr)
10772
+ };
10773
+ }
10774
+ try {
10775
+ const child = (0, import_node_child_process6.spawn)("remix", [...args], {
10776
+ cwd: repoRoot,
10777
+ detached: true,
10778
+ stdio: ["ignore", out, err],
10779
+ env: { ...process.env, REMIX_AUTO_FIX_SPAWN: "1" }
10780
+ });
10781
+ child.unref();
10782
+ try {
10783
+ (0, import_node_fs6.writeFileSync)(lockPath, String(child.pid ?? ""), "utf8");
10784
+ (0, import_node_fs6.utimesSync)(lockPath, /* @__PURE__ */ new Date(), /* @__PURE__ */ new Date());
10785
+ } catch {
10786
+ }
10787
+ return { kind: "spawned", command, pid: child.pid, logPath };
10788
+ } catch (spawnErr) {
10789
+ return {
10790
+ kind: "spawn_failed",
10791
+ command,
10792
+ reason: "spawn_failed",
10793
+ message: spawnErr instanceof Error ? spawnErr.message : String(spawnErr)
10794
+ };
10795
+ }
10796
+ }
10797
+ async function dispatchFinalizeFailure(input) {
10798
+ const recommendedCommand = input.preflightCode ? RECOMMENDED_USER_COMMAND[input.preflightCode] ?? null : null;
10799
+ const marker = buildFreshFailureMarker({
10800
+ repoRoot: input.repoRoot,
10801
+ preflightCode: input.preflightCode,
10802
+ message: input.message,
10803
+ hint: input.hint,
10804
+ recommendedCommand
10805
+ });
10806
+ let outcome;
10807
+ const autoFixArgs = input.preflightCode ? AUTO_FIX_COMMAND[input.preflightCode] : void 0;
10808
+ if (!autoFixArgs) {
10809
+ outcome = {
10810
+ kind: "warn_only",
10811
+ reason: input.preflightCode ? "no_auto_fix_for_code" : "unknown_code"
10812
+ };
10813
+ } else {
10814
+ outcome = spawnFixDetached(input.repoRoot, autoFixArgs);
10815
+ marker.autoFix = mergeOutcomeIntoMarker(marker.autoFix, outcome);
10816
+ }
10817
+ try {
10818
+ await writeFinalizeFailureMarker(marker);
10819
+ } catch (writeErr) {
10820
+ await appendHookDiagnosticsEvent({
10821
+ hook: input.hook,
10822
+ sessionId: input.sessionId,
10823
+ turnId: input.turnId ?? void 0,
10824
+ stage: "finalize_failure_marker_write_failed",
10825
+ result: "error",
10826
+ reason: "exception",
10827
+ repoRoot: input.repoRoot,
10828
+ message: writeErr instanceof Error ? writeErr.message : String(writeErr)
10829
+ });
10830
+ }
10831
+ await appendHookDiagnosticsEvent({
10832
+ hook: input.hook,
10833
+ sessionId: input.sessionId,
10834
+ turnId: input.turnId ?? void 0,
10835
+ stage: "auto_fix_dispatched",
10836
+ result: outcome.kind === "spawned" ? "success" : outcome.kind === "warn_only" ? "info" : "error",
10837
+ reason: outcome.kind,
10838
+ repoRoot: input.repoRoot,
10839
+ fields: {
10840
+ preflightCode: input.preflightCode,
10841
+ command: "command" in outcome ? outcome.command : null,
10842
+ pid: outcome.kind === "spawned" ? outcome.pid ?? null : null,
10843
+ logPath: outcome.kind === "spawned" ? outcome.logPath : null,
10844
+ recommendedCommand
10845
+ },
10846
+ message: outcome.kind === "spawn_failed" ? outcome.message : null
10847
+ });
10848
+ return outcome;
10849
+ }
10850
+ function mergeOutcomeIntoMarker(existing, outcome) {
10851
+ if (outcome.kind === "spawned") {
10852
+ return {
10853
+ status: "in_progress",
10854
+ command: outcome.command,
10855
+ pid: outcome.pid ?? null,
10856
+ logPath: outcome.logPath,
10857
+ attemptedAt: (/* @__PURE__ */ new Date()).toISOString(),
10858
+ failureMessage: null
10859
+ };
10860
+ }
10861
+ if (outcome.kind === "spawn_throttled") {
10862
+ return {
10863
+ status: "in_progress",
10864
+ command: outcome.command,
10865
+ pid: existing.pid,
10866
+ logPath: existing.logPath,
10867
+ attemptedAt: existing.attemptedAt,
10868
+ failureMessage: null
10869
+ };
10870
+ }
10871
+ if (outcome.kind === "spawn_failed") {
10872
+ return {
10873
+ status: "spawn_failed",
10874
+ command: outcome.command,
10875
+ pid: null,
10876
+ logPath: null,
10877
+ attemptedAt: (/* @__PURE__ */ new Date()).toISOString(),
10878
+ failureMessage: outcome.message
10879
+ };
10880
+ }
10881
+ return existing;
10882
+ }
10883
+
10884
+ // node_modules/@remixhq/core/dist/chunk-RCNOSZP6.js
10236
10885
  async function readJsonSafe(res) {
10237
10886
  const ct = res.headers.get("content-type") ?? "";
10238
10887
  if (!ct.toLowerCase().includes("application/json")) return null;
@@ -10245,8 +10894,13 @@ async function readJsonSafe(res) {
10245
10894
  function createApiClient(config, opts) {
10246
10895
  const apiKey = (opts?.apiKey ?? "").trim();
10247
10896
  const tokenProvider = opts?.tokenProvider;
10897
+ const defaultTimeoutMs = typeof opts?.defaultRequestTimeoutMs === "number" && opts.defaultRequestTimeoutMs > 0 ? opts.defaultRequestTimeoutMs : null;
10248
10898
  const CLIENT_KEY_HEADER = "x-comerge-api-key";
10249
- async function request(path16, init) {
10899
+ function makeTimeoutSignal(timeoutMs) {
10900
+ const ms = typeof timeoutMs === "number" && timeoutMs > 0 ? timeoutMs : defaultTimeoutMs;
10901
+ return ms != null ? AbortSignal.timeout(ms) : void 0;
10902
+ }
10903
+ async function request(path17, init, opts2) {
10250
10904
  if (!tokenProvider) {
10251
10905
  throw new RemixError("API client is missing a token provider.", {
10252
10906
  exitCode: 1,
@@ -10254,9 +10908,10 @@ function createApiClient(config, opts) {
10254
10908
  });
10255
10909
  }
10256
10910
  const auth = await tokenProvider();
10257
- const url = new URL(path16, config.apiUrl).toString();
10911
+ const url = new URL(path17, config.apiUrl).toString();
10258
10912
  const doFetch = async (bearer) => fetch(url, {
10259
10913
  ...init,
10914
+ signal: makeTimeoutSignal(opts2?.timeoutMs),
10260
10915
  headers: {
10261
10916
  Accept: "application/json",
10262
10917
  "Content-Type": "application/json",
@@ -10273,12 +10928,16 @@ function createApiClient(config, opts) {
10273
10928
  if (!res.ok) {
10274
10929
  const body = await readJsonSafe(res);
10275
10930
  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 });
10931
+ throw new RemixError(msg, {
10932
+ exitCode: 1,
10933
+ hint: body ? JSON.stringify(body, null, 2) : null,
10934
+ statusCode: res.status
10935
+ });
10277
10936
  }
10278
10937
  const json = await readJsonSafe(res);
10279
10938
  return json ?? null;
10280
10939
  }
10281
- async function requestBinary(path16, init) {
10940
+ async function requestBinary(path17, init, opts2) {
10282
10941
  if (!tokenProvider) {
10283
10942
  throw new RemixError("API client is missing a token provider.", {
10284
10943
  exitCode: 1,
@@ -10286,9 +10945,10 @@ function createApiClient(config, opts) {
10286
10945
  });
10287
10946
  }
10288
10947
  const auth = await tokenProvider();
10289
- const url = new URL(path16, config.apiUrl).toString();
10948
+ const url = new URL(path17, config.apiUrl).toString();
10290
10949
  const doFetch = async (bearer) => fetch(url, {
10291
10950
  ...init,
10951
+ signal: makeTimeoutSignal(opts2?.timeoutMs),
10292
10952
  headers: {
10293
10953
  Accept: "*/*",
10294
10954
  ...init?.headers ?? {},
@@ -10304,7 +10964,11 @@ function createApiClient(config, opts) {
10304
10964
  if (!res.ok) {
10305
10965
  const body = await readJsonSafe(res);
10306
10966
  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 });
10967
+ throw new RemixError(msg, {
10968
+ exitCode: 1,
10969
+ hint: body ? JSON.stringify(body, null, 2) : null,
10970
+ statusCode: res.status
10971
+ });
10308
10972
  }
10309
10973
  const contentDisposition = res.headers.get("content-disposition") ?? "";
10310
10974
  const fileNameMatch = contentDisposition.match(/filename=\"([^\"]+)\"/i);
@@ -11136,8 +11800,8 @@ function getErrorMap() {
11136
11800
 
11137
11801
  // node_modules/zod/v3/helpers/parseUtil.js
11138
11802
  var makeIssue = (params) => {
11139
- const { data, path: path16, errorMaps, issueData } = params;
11140
- const fullPath = [...path16, ...issueData.path || []];
11803
+ const { data, path: path17, errorMaps, issueData } = params;
11804
+ const fullPath = [...path17, ...issueData.path || []];
11141
11805
  const fullIssue = {
11142
11806
  ...issueData,
11143
11807
  path: fullPath
@@ -11253,11 +11917,11 @@ var errorUtil;
11253
11917
 
11254
11918
  // node_modules/zod/v3/types.js
11255
11919
  var ParseInputLazyPath = class {
11256
- constructor(parent, value, path16, key) {
11920
+ constructor(parent, value, path17, key) {
11257
11921
  this._cachedPath = [];
11258
11922
  this.parent = parent;
11259
11923
  this.data = value;
11260
- this._path = path16;
11924
+ this._path = path17;
11261
11925
  this._key = key;
11262
11926
  }
11263
11927
  get path() {
@@ -14699,8 +15363,8 @@ var coerce = {
14699
15363
  };
14700
15364
  var NEVER = INVALID;
14701
15365
 
14702
- // node_modules/@remixhq/core/dist/chunk-P6JHXOV4.js
14703
- var import_promises19 = __toESM(require("fs/promises"), 1);
15366
+ // node_modules/@remixhq/core/dist/chunk-XETDXVGM.js
15367
+ var import_promises22 = __toESM(require("fs/promises"), 1);
14704
15368
  var import_os3 = __toESM(require("os"), 1);
14705
15369
  var import_path7 = __toESM(require("path"), 1);
14706
15370
 
@@ -15108,7 +15772,7 @@ var PostgrestError = class extends Error {
15108
15772
  };
15109
15773
  }
15110
15774
  };
15111
- function sleep2(ms, signal) {
15775
+ function sleep3(ms, signal) {
15112
15776
  return new Promise((resolve) => {
15113
15777
  if (signal === null || signal === void 0 ? void 0 : signal.aborted) {
15114
15778
  resolve();
@@ -15304,7 +15968,7 @@ var PostgrestBuilder = class {
15304
15968
  if (_this.retryEnabled && attemptCount < DEFAULT_MAX_RETRIES) {
15305
15969
  const delay = getRetryDelay(attemptCount);
15306
15970
  attemptCount++;
15307
- await sleep2(delay, _this.signal);
15971
+ await sleep3(delay, _this.signal);
15308
15972
  continue;
15309
15973
  }
15310
15974
  throw fetchError;
@@ -15315,7 +15979,7 @@ var PostgrestBuilder = class {
15315
15979
  const delay = retryAfterHeader !== null ? Math.max(0, parseInt(retryAfterHeader, 10) || 0) * 1e3 : getRetryDelay(attemptCount);
15316
15980
  await res$1.text();
15317
15981
  attemptCount++;
15318
- await sleep2(delay, _this.signal);
15982
+ await sleep3(delay, _this.signal);
15319
15983
  continue;
15320
15984
  }
15321
15985
  return await _this.processResponse(res$1);
@@ -23805,8 +24469,8 @@ var IcebergError = class extends Error {
23805
24469
  return this.status === 419;
23806
24470
  }
23807
24471
  };
23808
- function buildUrl(baseUrl, path16, query) {
23809
- const url = new URL(path16, baseUrl);
24472
+ function buildUrl(baseUrl, path17, query) {
24473
+ const url = new URL(path17, baseUrl);
23810
24474
  if (query) {
23811
24475
  for (const [key, value] of Object.entries(query)) {
23812
24476
  if (value !== void 0) {
@@ -23836,12 +24500,12 @@ function createFetchClient(options) {
23836
24500
  return {
23837
24501
  async request({
23838
24502
  method,
23839
- path: path16,
24503
+ path: path17,
23840
24504
  query,
23841
24505
  body,
23842
24506
  headers
23843
24507
  }) {
23844
- const url = buildUrl(options.baseUrl, path16, query);
24508
+ const url = buildUrl(options.baseUrl, path17, query);
23845
24509
  const authHeaders = await buildAuthHeaders(options.auth);
23846
24510
  const res = await fetchFn(url, {
23847
24511
  method,
@@ -24679,7 +25343,7 @@ var StorageFileApi = class extends BaseApiClient {
24679
25343
  * @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
25344
  * @param fileBody The body of the file to be stored in the bucket.
24681
25345
  */
24682
- async uploadOrUpdate(method, path16, fileBody, fileOptions) {
25346
+ async uploadOrUpdate(method, path17, fileBody, fileOptions) {
24683
25347
  var _this = this;
24684
25348
  return _this.handleOperation(async () => {
24685
25349
  let body;
@@ -24703,7 +25367,7 @@ var StorageFileApi = class extends BaseApiClient {
24703
25367
  if ((typeof ReadableStream !== "undefined" && body instanceof ReadableStream || body && typeof body === "object" && "pipe" in body && typeof body.pipe === "function") && !options.duplex) options.duplex = "half";
24704
25368
  }
24705
25369
  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);
25370
+ const cleanPath = _this._removeEmptyFolders(path17);
24707
25371
  const _path = _this._getFinalPath(cleanPath);
24708
25372
  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
25373
  return {
@@ -24764,8 +25428,8 @@ var StorageFileApi = class extends BaseApiClient {
24764
25428
  * - Refer to the [Storage guide](/docs/guides/storage/security/access-control) on how access control works
24765
25429
  * - 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
25430
  */
24767
- async upload(path16, fileBody, fileOptions) {
24768
- return this.uploadOrUpdate("POST", path16, fileBody, fileOptions);
25431
+ async upload(path17, fileBody, fileOptions) {
25432
+ return this.uploadOrUpdate("POST", path17, fileBody, fileOptions);
24769
25433
  }
24770
25434
  /**
24771
25435
  * Upload a file with a token generated from `createSignedUploadUrl`.
@@ -24804,9 +25468,9 @@ var StorageFileApi = class extends BaseApiClient {
24804
25468
  * - `objects` table permissions: none
24805
25469
  * - Refer to the [Storage guide](/docs/guides/storage/security/access-control) on how access control works
24806
25470
  */
24807
- async uploadToSignedUrl(path16, token, fileBody, fileOptions) {
25471
+ async uploadToSignedUrl(path17, token, fileBody, fileOptions) {
24808
25472
  var _this3 = this;
24809
- const cleanPath = _this3._removeEmptyFolders(path16);
25473
+ const cleanPath = _this3._removeEmptyFolders(path17);
24810
25474
  const _path = _this3._getFinalPath(cleanPath);
24811
25475
  const url = new URL(_this3.url + `/object/upload/sign/${_path}`);
24812
25476
  url.searchParams.set("token", token);
@@ -24868,10 +25532,10 @@ var StorageFileApi = class extends BaseApiClient {
24868
25532
  * - `objects` table permissions: `insert`
24869
25533
  * - Refer to the [Storage guide](/docs/guides/storage/security/access-control) on how access control works
24870
25534
  */
24871
- async createSignedUploadUrl(path16, options) {
25535
+ async createSignedUploadUrl(path17, options) {
24872
25536
  var _this4 = this;
24873
25537
  return _this4.handleOperation(async () => {
24874
- let _path = _this4._getFinalPath(path16);
25538
+ let _path = _this4._getFinalPath(path17);
24875
25539
  const headers = _objectSpread22({}, _this4.headers);
24876
25540
  if (options === null || options === void 0 ? void 0 : options.upsert) headers["x-upsert"] = "true";
24877
25541
  const data = await post(_this4.fetch, `${_this4.url}/object/upload/sign/${_path}`, {}, { headers });
@@ -24880,7 +25544,7 @@ var StorageFileApi = class extends BaseApiClient {
24880
25544
  if (!token) throw new StorageError("No token returned by API");
24881
25545
  return {
24882
25546
  signedUrl: url.toString(),
24883
- path: path16,
25547
+ path: path17,
24884
25548
  token
24885
25549
  };
24886
25550
  });
@@ -24936,8 +25600,8 @@ var StorageFileApi = class extends BaseApiClient {
24936
25600
  * - Refer to the [Storage guide](/docs/guides/storage/security/access-control) on how access control works
24937
25601
  * - 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
25602
  */
24939
- async update(path16, fileBody, fileOptions) {
24940
- return this.uploadOrUpdate("PUT", path16, fileBody, fileOptions);
25603
+ async update(path17, fileBody, fileOptions) {
25604
+ return this.uploadOrUpdate("PUT", path17, fileBody, fileOptions);
24941
25605
  }
24942
25606
  /**
24943
25607
  * Moves an existing file to a new path in the same bucket.
@@ -25085,10 +25749,10 @@ var StorageFileApi = class extends BaseApiClient {
25085
25749
  * - `objects` table permissions: `select`
25086
25750
  * - Refer to the [Storage guide](/docs/guides/storage/security/access-control) on how access control works
25087
25751
  */
25088
- async createSignedUrl(path16, expiresIn, options) {
25752
+ async createSignedUrl(path17, expiresIn, options) {
25089
25753
  var _this8 = this;
25090
25754
  return _this8.handleOperation(async () => {
25091
- let _path = _this8._getFinalPath(path16);
25755
+ let _path = _this8._getFinalPath(path17);
25092
25756
  const hasTransform = typeof (options === null || options === void 0 ? void 0 : options.transform) === "object" && options.transform !== null && Object.keys(options.transform).length > 0;
25093
25757
  let data = await post(_this8.fetch, `${_this8.url}/object/sign/${_path}`, _objectSpread22({ expiresIn }, hasTransform ? { transform: options.transform } : {}), { headers: _this8.headers });
25094
25758
  const query = new URLSearchParams();
@@ -25222,13 +25886,13 @@ var StorageFileApi = class extends BaseApiClient {
25222
25886
  * - `objects` table permissions: `select`
25223
25887
  * - Refer to the [Storage guide](/docs/guides/storage/security/access-control) on how access control works
25224
25888
  */
25225
- download(path16, options, parameters) {
25889
+ download(path17, options, parameters) {
25226
25890
  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
25891
  const query = new URLSearchParams();
25228
25892
  if (options === null || options === void 0 ? void 0 : options.transform) this.applyTransformOptsToQuery(query, options.transform);
25229
25893
  if ((options === null || options === void 0 ? void 0 : options.cacheNonce) != null) query.set("cacheNonce", String(options.cacheNonce));
25230
25894
  const queryString = query.toString();
25231
- const _path = this._getFinalPath(path16);
25895
+ const _path = this._getFinalPath(path17);
25232
25896
  const downloadFn = () => get(this.fetch, `${this.url}/${renderPath}/${_path}${queryString ? `?${queryString}` : ""}`, {
25233
25897
  headers: this.headers,
25234
25898
  noResolveJson: true
@@ -25258,9 +25922,9 @@ var StorageFileApi = class extends BaseApiClient {
25258
25922
  * }
25259
25923
  * ```
25260
25924
  */
25261
- async info(path16) {
25925
+ async info(path17) {
25262
25926
  var _this10 = this;
25263
- const _path = _this10._getFinalPath(path16);
25927
+ const _path = _this10._getFinalPath(path17);
25264
25928
  return _this10.handleOperation(async () => {
25265
25929
  return recursiveToCamel(await get(_this10.fetch, `${_this10.url}/object/info/${_path}`, { headers: _this10.headers }));
25266
25930
  });
@@ -25280,9 +25944,9 @@ var StorageFileApi = class extends BaseApiClient {
25280
25944
  * .exists('folder/avatar1.png')
25281
25945
  * ```
25282
25946
  */
25283
- async exists(path16) {
25947
+ async exists(path17) {
25284
25948
  var _this11 = this;
25285
- const _path = _this11._getFinalPath(path16);
25949
+ const _path = _this11._getFinalPath(path17);
25286
25950
  try {
25287
25951
  await head(_this11.fetch, `${_this11.url}/object/${_path}`, { headers: _this11.headers });
25288
25952
  return {
@@ -25360,8 +26024,8 @@ var StorageFileApi = class extends BaseApiClient {
25360
26024
  * - `objects` table permissions: none
25361
26025
  * - Refer to the [Storage guide](/docs/guides/storage/security/access-control) on how access control works
25362
26026
  */
25363
- getPublicUrl(path16, options) {
25364
- const _path = this._getFinalPath(path16);
26027
+ getPublicUrl(path17, options) {
26028
+ const _path = this._getFinalPath(path17);
25365
26029
  const query = new URLSearchParams();
25366
26030
  if (options === null || options === void 0 ? void 0 : options.download) query.set("download", options.download === true ? "" : options.download);
25367
26031
  if (options === null || options === void 0 ? void 0 : options.transform) this.applyTransformOptsToQuery(query, options.transform);
@@ -25498,10 +26162,10 @@ var StorageFileApi = class extends BaseApiClient {
25498
26162
  * - `objects` table permissions: `select`
25499
26163
  * - Refer to the [Storage guide](/docs/guides/storage/security/access-control) on how access control works
25500
26164
  */
25501
- async list(path16, options, parameters) {
26165
+ async list(path17, options, parameters) {
25502
26166
  var _this13 = this;
25503
26167
  return _this13.handleOperation(async () => {
25504
- const body = _objectSpread22(_objectSpread22(_objectSpread22({}, DEFAULT_SEARCH_OPTIONS), options), {}, { prefix: path16 || "" });
26168
+ const body = _objectSpread22(_objectSpread22(_objectSpread22({}, DEFAULT_SEARCH_OPTIONS), options), {}, { prefix: path17 || "" });
25505
26169
  return await post(_this13.fetch, `${_this13.url}/object/list/${_this13.bucketId}`, body, { headers: _this13.headers }, parameters);
25506
26170
  });
25507
26171
  }
@@ -25565,11 +26229,11 @@ var StorageFileApi = class extends BaseApiClient {
25565
26229
  if (typeof Buffer !== "undefined") return Buffer.from(data).toString("base64");
25566
26230
  return btoa(data);
25567
26231
  }
25568
- _getFinalPath(path16) {
25569
- return `${this.bucketId}/${path16.replace(/^\/+/, "")}`;
26232
+ _getFinalPath(path17) {
26233
+ return `${this.bucketId}/${path17.replace(/^\/+/, "")}`;
25570
26234
  }
25571
- _removeEmptyFolders(path16) {
25572
- return path16.replace(/^\/|\/$/g, "").replace(/\/+/g, "/");
26235
+ _removeEmptyFolders(path17) {
26236
+ return path17.replace(/^\/|\/$/g, "").replace(/\/+/g, "/");
25573
26237
  }
25574
26238
  /** Modifies the `query`, appending values the from `transform` */
25575
26239
  applyTransformOptsToQuery(query, transform) {
@@ -27312,7 +27976,7 @@ function decodeJWT(token) {
27312
27976
  };
27313
27977
  return data;
27314
27978
  }
27315
- async function sleep3(time) {
27979
+ async function sleep4(time) {
27316
27980
  return await new Promise((accept) => {
27317
27981
  setTimeout(() => accept(null), time);
27318
27982
  });
@@ -33105,7 +33769,7 @@ var GoTrueClient = class _GoTrueClient {
33105
33769
  const startedAt = Date.now();
33106
33770
  return await retryable(async (attempt) => {
33107
33771
  if (attempt > 0) {
33108
- await sleep3(200 * Math.pow(2, attempt - 1));
33772
+ await sleep4(200 * Math.pow(2, attempt - 1));
33109
33773
  }
33110
33774
  this._debug(debugName, "refreshing attempt", attempt);
33111
33775
  return await _request(this.fetch, "POST", `${this.url}/token?grant_type=refresh_token`, {
@@ -34644,7 +35308,7 @@ function shouldShowDeprecationWarning() {
34644
35308
  }
34645
35309
  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
35310
 
34647
- // node_modules/@remixhq/core/dist/chunk-P6JHXOV4.js
35311
+ // node_modules/@remixhq/core/dist/chunk-XETDXVGM.js
34648
35312
  var storedSessionSchema = external_exports.object({
34649
35313
  access_token: external_exports.string().min(1),
34650
35314
  refresh_token: external_exports.string().min(1),
@@ -34677,24 +35341,24 @@ async function maybeLoadKeytar() {
34677
35341
  }
34678
35342
  async function ensurePathPermissions(filePath) {
34679
35343
  const dir = import_path7.default.dirname(filePath);
34680
- await import_promises19.default.mkdir(dir, { recursive: true });
35344
+ await import_promises22.default.mkdir(dir, { recursive: true });
34681
35345
  try {
34682
- await import_promises19.default.chmod(dir, 448);
35346
+ await import_promises22.default.chmod(dir, 448);
34683
35347
  } catch {
34684
35348
  }
34685
35349
  try {
34686
- await import_promises19.default.chmod(filePath, 384);
35350
+ await import_promises22.default.chmod(filePath, 384);
34687
35351
  } catch {
34688
35352
  }
34689
35353
  }
34690
- async function writeJsonAtomic2(filePath, value) {
34691
- await import_promises19.default.mkdir(import_path7.default.dirname(filePath), { recursive: true });
35354
+ async function writeJsonAtomic3(filePath, value) {
35355
+ await import_promises22.default.mkdir(import_path7.default.dirname(filePath), { recursive: true });
34692
35356
  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);
35357
+ await import_promises22.default.writeFile(tmpPath, JSON.stringify(value, null, 2) + "\n", "utf8");
35358
+ await import_promises22.default.rename(tmpPath, filePath);
34695
35359
  }
34696
35360
  async function writeSessionFileFallback(filePath, session) {
34697
- await writeJsonAtomic2(filePath, session);
35361
+ await writeJsonAtomic3(filePath, session);
34698
35362
  await ensurePathPermissions(filePath);
34699
35363
  }
34700
35364
  function createLocalSessionStore(params) {
@@ -34714,7 +35378,7 @@ function createLocalSessionStore(params) {
34714
35378
  }
34715
35379
  }
34716
35380
  async function readFile() {
34717
- const raw = await import_promises19.default.readFile(filePath, "utf8").catch(() => null);
35381
+ const raw = await import_promises22.default.readFile(filePath, "utf8").catch(() => null);
34718
35382
  if (!raw) return null;
34719
35383
  try {
34720
35384
  const parsed = storedSessionSchema.safeParse(JSON.parse(raw));
@@ -34858,7 +35522,7 @@ function createSupabaseAuthHelpers(config) {
34858
35522
  };
34859
35523
  }
34860
35524
 
34861
- // node_modules/@remixhq/core/dist/chunk-VM3CGCNX.js
35525
+ // node_modules/@remixhq/core/dist/chunk-XCZRNB35.js
34862
35526
  var DEFAULT_API_URL = "https://api.remix.one";
34863
35527
  var DEFAULT_SUPABASE_URL = "https://xtfxwbckjpfmqubnsusu.supabase.co";
34864
35528
  var DEFAULT_SUPABASE_ANON_KEY = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Inh0Znh3YmNranBmbXF1Ym5zdXN1Iiwicm9sZSI6ImFub24iLCJpYXQiOjE3NjA2MDEyMzAsImV4cCI6MjA3NjE3NzIzMH0.dzWGAWrK4CvrmHVHzf8w7JlUZohdap0ZPnLZnABMV8s";
@@ -34896,6 +35560,7 @@ async function resolveConfig(_opts) {
34896
35560
  }
34897
35561
 
34898
35562
  // src/hook-auth.ts
35563
+ var HOOK_API_REQUEST_TIMEOUT_MS = 6e4;
34899
35564
  async function createHookCollabApiClient() {
34900
35565
  const config = await resolveConfig();
34901
35566
  const sessionStore = createLocalSessionStore();
@@ -34908,307 +35573,11 @@ async function createHookCollabApiClient() {
34908
35573
  }
34909
35574
  });
34910
35575
  return createApiClient(config, {
34911
- tokenProvider
35576
+ tokenProvider,
35577
+ defaultRequestTimeoutMs: HOOK_API_REQUEST_TIMEOUT_MS
34912
35578
  });
34913
35579
  }
34914
35580
 
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
35581
  // src/deferred-turn-drainer.ts
35213
35582
  var collabFinalizeTurn2 = collabFinalizeTurn;
35214
35583
  var drainPendingFinalizeQueue2 = drainPendingFinalizeQueue;
@@ -35218,6 +35587,16 @@ var HOOK_ACTOR = {
35218
35587
  version: pluginMetadata.version,
35219
35588
  provider: "anthropic"
35220
35589
  };
35590
+ function getDrainerErrorDetails(error) {
35591
+ if (error instanceof Error) {
35592
+ const hint = typeof error.hint === "string" ? String(error.hint) : null;
35593
+ const codeRaw = error.code;
35594
+ const preflightCode = isFinalizePreflightFailureCode(codeRaw) ? codeRaw : null;
35595
+ return { message: error.message || "Deferred turn recording failed.", hint, preflightCode };
35596
+ }
35597
+ const message = typeof error === "string" && error.trim() ? error.trim() : "Deferred turn recording failed.";
35598
+ return { message, hint: null, preflightCode: null };
35599
+ }
35221
35600
  var DEFERRED_TURN_DRAIN_POLL_INTERVAL_MS = 3e3;
35222
35601
  var DEFERRED_TURN_DRAIN_MAX_WAIT_MS = 15 * 60 * 1e3;
35223
35602
  var DEFERRED_TURN_DRAIN_LOCK_HEARTBEAT_MS = 3e4;
@@ -35236,10 +35615,10 @@ function repoLockFileName(repoRoot) {
35236
35615
  return `.drainer-${hash}.lock`;
35237
35616
  }
35238
35617
  function repoLockPath(repoRoot) {
35239
- return import_node_path9.default.join(getDeferredTurnDirPath(), repoLockFileName(repoRoot));
35618
+ return import_node_path11.default.join(getDeferredTurnDirPath(), repoLockFileName(repoRoot));
35240
35619
  }
35241
35620
  async function readDrainLockMetadata(lockPath) {
35242
- const raw = await import_promises22.default.readFile(lockPath, "utf8").catch(() => null);
35621
+ const raw = await import_promises23.default.readFile(lockPath, "utf8").catch(() => null);
35243
35622
  if (!raw) return null;
35244
35623
  try {
35245
35624
  const parsed = JSON.parse(raw);
@@ -35253,15 +35632,15 @@ async function readDrainLockMetadata(lockPath) {
35253
35632
  }
35254
35633
  async function writeDrainLockMetadata(lockPath, metadata) {
35255
35634
  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);
35635
+ await import_promises23.default.writeFile(tmpPath, JSON.stringify(metadata), "utf8");
35636
+ await import_promises23.default.rename(tmpPath, lockPath);
35258
35637
  }
35259
35638
  async function tryAcquireDrainLock(repoRoot) {
35260
35639
  const lockPath = repoLockPath(repoRoot);
35261
- await import_promises22.default.mkdir(import_node_path9.default.dirname(lockPath), { recursive: true });
35640
+ await import_promises23.default.mkdir(import_node_path11.default.dirname(lockPath), { recursive: true });
35262
35641
  const existingMeta = await readDrainLockMetadata(lockPath);
35263
35642
  if (existingMeta) {
35264
- const lockStat = await import_promises22.default.stat(lockPath).catch(() => null);
35643
+ const lockStat = await import_promises23.default.stat(lockPath).catch(() => null);
35265
35644
  const ageMs = lockStat ? Date.now() - lockStat.mtimeMs : Number.POSITIVE_INFINITY;
35266
35645
  const fresh = ageMs <= DEFERRED_TURN_DRAIN_LOCK_STALE_MS;
35267
35646
  const alive = isPidAlive(existingMeta.pid);
@@ -35279,11 +35658,11 @@ async function tryAcquireDrainLock(repoRoot) {
35279
35658
  async function releaseDrainLock(lockPath) {
35280
35659
  const meta = await readDrainLockMetadata(lockPath);
35281
35660
  if (meta && meta.pid !== process.pid) return;
35282
- await import_promises22.default.rm(lockPath, { force: true }).catch(() => void 0);
35661
+ await import_promises23.default.rm(lockPath, { force: true }).catch(() => void 0);
35283
35662
  }
35284
35663
  async function heartbeatDrainLock(lockPath) {
35285
35664
  const now = /* @__PURE__ */ new Date();
35286
- await import_promises22.default.utimes(lockPath, now, now).catch(() => void 0);
35665
+ await import_promises23.default.utimes(lockPath, now, now).catch(() => void 0);
35287
35666
  }
35288
35667
  async function sleep5(ms) {
35289
35668
  await new Promise((resolve) => setTimeout(resolve, ms));
@@ -35383,6 +35762,7 @@ async function runStandaloneDeferredTurnDrainer(repoRoot) {
35383
35762
  let api = null;
35384
35763
  let recordedTotal = 0;
35385
35764
  let failedTotal = 0;
35765
+ let droppedTotal = 0;
35386
35766
  let exitReason = "queue_empty";
35387
35767
  try {
35388
35768
  while (true) {
@@ -35413,7 +35793,49 @@ async function runStandaloneDeferredTurnDrainer(repoRoot) {
35413
35793
  const bindingState = await readCollabBindingState(repoRoot).catch(() => null);
35414
35794
  const currentBranch = bindingState?.currentBranch ?? null;
35415
35795
  const isCurrentBranchBound = bindingState?.binding != null;
35416
- const attemptable = entries.filter(
35796
+ const currentAppId = bindingState?.binding?.currentAppId ?? null;
35797
+ const currentProjectId = bindingState?.binding?.projectId ?? bindingState?.projectId ?? null;
35798
+ let droppedThisPass = 0;
35799
+ const liveEntries = [];
35800
+ for (const entry of entries) {
35801
+ const appIdMismatch = entry.record.appIdAtDefer != null && currentAppId != null && entry.record.appIdAtDefer !== currentAppId;
35802
+ const projectIdMismatch = entry.record.projectIdAtDefer != null && currentProjectId != null && entry.record.projectIdAtDefer !== currentProjectId;
35803
+ if (appIdMismatch || projectIdMismatch) {
35804
+ await deleteDeferredTurnFile(entry.filePath);
35805
+ droppedThisPass += 1;
35806
+ await appendHookDiagnosticsEvent({
35807
+ hook: "deferredTurnDrainer",
35808
+ sessionId: sessionMarker,
35809
+ stage: "deferred_turn_dropped",
35810
+ result: "info",
35811
+ reason: appIdMismatch ? "app_id_mismatch" : "project_id_mismatch",
35812
+ repoRoot,
35813
+ fields: {
35814
+ deferredTurnId: entry.record.turnId,
35815
+ deferredSessionId: entry.record.sessionId,
35816
+ appIdAtDefer: entry.record.appIdAtDefer,
35817
+ projectIdAtDefer: entry.record.projectIdAtDefer,
35818
+ currentAppId,
35819
+ currentProjectId
35820
+ }
35821
+ });
35822
+ continue;
35823
+ }
35824
+ liveEntries.push(entry);
35825
+ }
35826
+ if (droppedThisPass > 0) {
35827
+ droppedTotal += droppedThisPass;
35828
+ }
35829
+ if (liveEntries.length === 0) {
35830
+ const remaining = await listDeferredTurnsForRepo(repoRoot).catch(() => []);
35831
+ if (remaining.length === 0) {
35832
+ exitReason = "queue_empty";
35833
+ break;
35834
+ }
35835
+ await sleep5(DEFERRED_TURN_DRAIN_POLL_INTERVAL_MS);
35836
+ continue;
35837
+ }
35838
+ const attemptable = liveEntries.filter(
35417
35839
  (e) => isCurrentBranchBound && (!e.record.branchAtDefer || e.record.branchAtDefer === currentBranch)
35418
35840
  );
35419
35841
  if (attemptable.length === 0) {
@@ -35462,6 +35884,8 @@ async function runStandaloneDeferredTurnDrainer(repoRoot) {
35462
35884
  } else {
35463
35885
  failedThisPass += 1;
35464
35886
  failedTotal += 1;
35887
+ const outcome = await recordDeferredTurnFailedAttempt(entry.filePath).catch(() => null);
35888
+ const promoted = outcome?.promoted === true;
35465
35889
  await appendHookDiagnosticsEvent({
35466
35890
  hook: "deferredTurnDrainer",
35467
35891
  sessionId: sessionMarker,
@@ -35472,9 +35896,43 @@ async function runStandaloneDeferredTurnDrainer(repoRoot) {
35472
35896
  message: result.error instanceof Error ? result.error.message : String(result.error ?? ""),
35473
35897
  fields: {
35474
35898
  deferredTurnId: entry.record.turnId,
35475
- deferredSessionId: entry.record.sessionId
35899
+ deferredSessionId: entry.record.sessionId,
35900
+ attemptCount: outcome?.promoted === false ? outcome.newAttemptCount : outcome?.promoted === true ? outcome.finalAttemptCount : null,
35901
+ promoted
35476
35902
  }
35477
35903
  });
35904
+ if (promoted) {
35905
+ const errorDetails = getDrainerErrorDetails(result.error);
35906
+ await dispatchFinalizeFailure({
35907
+ // The dispatcher only knows about the two real Claude hook
35908
+ // entrypoints. The standalone drainer is logically a
35909
+ // post-Stop background process and the marker we're about
35910
+ // to write is consumed by the next prompt's UserPromptSubmit
35911
+ // hook, so attributing the failure to "Stop" matches what
35912
+ // the user will see.
35913
+ hook: "Stop",
35914
+ sessionId: sessionMarker,
35915
+ turnId: entry.record.turnId,
35916
+ repoRoot,
35917
+ preflightCode: errorDetails.preflightCode,
35918
+ message: `Deferred turn could not be recorded after ${outcome?.finalAttemptCount ?? "max"} attempts: ${errorDetails.message}`,
35919
+ hint: errorDetails.hint
35920
+ }).catch(async (dispatchErr) => {
35921
+ await appendHookDiagnosticsEvent({
35922
+ hook: "deferredTurnDrainer",
35923
+ sessionId: sessionMarker,
35924
+ stage: "deferred_turn_promotion_dispatch_failed",
35925
+ result: "error",
35926
+ reason: "exception",
35927
+ repoRoot,
35928
+ message: dispatchErr instanceof Error ? dispatchErr.message : String(dispatchErr),
35929
+ fields: {
35930
+ deferredTurnId: entry.record.turnId,
35931
+ deferredSessionId: entry.record.sessionId
35932
+ }
35933
+ });
35934
+ });
35935
+ }
35478
35936
  }
35479
35937
  }
35480
35938
  if (recordedThisPass > 0) {
@@ -35527,6 +35985,7 @@ async function runStandaloneDeferredTurnDrainer(repoRoot) {
35527
35985
  fields: {
35528
35986
  recordedTotal,
35529
35987
  failedTotal,
35988
+ droppedTotal,
35530
35989
  elapsedMs: Date.now() - startedAt
35531
35990
  }
35532
35991
  });
@@ -35555,34 +36014,13 @@ async function maybeRunDeferredTurnDrainerFromArgv() {
35555
36014
  return true;
35556
36015
  }
35557
36016
 
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
36017
  // src/spawn-helpers.ts
35580
- var import_node_child_process6 = require("child_process");
36018
+ var import_node_child_process7 = require("child_process");
35581
36019
  function spawnDeferredTurnDrainer(repoRoot) {
35582
36020
  const entrypoint = process.argv[1];
35583
36021
  if (!entrypoint) return;
35584
36022
  if (!repoRoot) return;
35585
- const child = (0, import_node_child_process6.spawn)(
36023
+ const child = (0, import_node_child_process7.spawn)(
35586
36024
  process.execPath,
35587
36025
  [...process.execArgv, entrypoint, "--drain-deferred-turns", repoRoot],
35588
36026
  {
@@ -35596,7 +36034,7 @@ function spawnDeferredTurnDrainer(repoRoot) {
35596
36034
 
35597
36035
  // src/hook-utils.ts
35598
36036
  var import_promises24 = __toESM(require("fs/promises"), 1);
35599
- var import_node_path11 = __toESM(require("path"), 1);
36037
+ var import_node_path12 = __toESM(require("path"), 1);
35600
36038
  async function readJsonStdin() {
35601
36039
  const chunks = [];
35602
36040
  for await (const chunk of process.stdin) {
@@ -35622,16 +36060,16 @@ function extractString(input, keys) {
35622
36060
  }
35623
36061
  async function findBoundRepo(startPath) {
35624
36062
  if (!startPath) return null;
35625
- let current = import_node_path11.default.resolve(startPath);
36063
+ let current = import_node_path12.default.resolve(startPath);
35626
36064
  let stats = await import_promises24.default.stat(current).catch(() => null);
35627
36065
  if (stats?.isFile()) {
35628
- current = import_node_path11.default.dirname(current);
36066
+ current = import_node_path12.default.dirname(current);
35629
36067
  }
35630
36068
  while (true) {
35631
- const bindingPath = import_node_path11.default.join(current, ".remix", "config.json");
36069
+ const bindingPath = import_node_path12.default.join(current, ".remix", "config.json");
35632
36070
  const bindingStats = await import_promises24.default.stat(bindingPath).catch(() => null);
35633
36071
  if (bindingStats?.isFile()) return current;
35634
- const parent = import_node_path11.default.dirname(current);
36072
+ const parent = import_node_path12.default.dirname(current);
35635
36073
  if (parent === current) return null;
35636
36074
  current = parent;
35637
36075
  }
@@ -35655,8 +36093,8 @@ function buildRuntimeStatusOverride() {
35655
36093
  "Use `remix_collab_drain_finalize_queue` only for explicit recovery flows, such as status reporting `await_finalize` before a merge-related operation."
35656
36094
  ].join("\n");
35657
36095
  }
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");
36096
+ var COLLAB_INIT_LOG_REL = import_node_path13.default.join(".remix", "collab-init.log");
36097
+ var COLLAB_INIT_SPAWN_LOCK_REL = import_node_path13.default.join(".remix", ".collab-init-spawning");
35660
36098
  var COLLAB_INIT_SPAWN_LOCK_STALE_MS = 90 * 1e3;
35661
36099
  function isPidAlive2(pid) {
35662
36100
  if (!Number.isFinite(pid) || pid <= 0) return false;
@@ -35669,7 +36107,7 @@ function isPidAlive2(pid) {
35669
36107
  }
35670
36108
  function readLockPid(spawnLockPath) {
35671
36109
  try {
35672
- const raw = (0, import_node_fs6.readFileSync)(spawnLockPath, "utf8").trim();
36110
+ const raw = (0, import_node_fs7.readFileSync)(spawnLockPath, "utf8").trim();
35673
36111
  if (!raw) return null;
35674
36112
  const pid = Number.parseInt(raw, 10);
35675
36113
  return Number.isFinite(pid) && pid > 0 ? pid : null;
@@ -35678,20 +36116,20 @@ function readLockPid(spawnLockPath) {
35678
36116
  }
35679
36117
  }
35680
36118
  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);
36119
+ const remixDir = import_node_path13.default.join(repoRoot, ".remix");
36120
+ const spawnLockPath = import_node_path13.default.join(repoRoot, COLLAB_INIT_SPAWN_LOCK_REL);
36121
+ const logPath = import_node_path13.default.join(repoRoot, COLLAB_INIT_LOG_REL);
35684
36122
  try {
35685
- if ((0, import_node_fs6.existsSync)(spawnLockPath)) {
36123
+ if ((0, import_node_fs7.existsSync)(spawnLockPath)) {
35686
36124
  const lockPid = readLockPid(spawnLockPath);
35687
36125
  const lockAlive = lockPid !== null && isPidAlive2(lockPid);
35688
- const ageMs = Date.now() - (0, import_node_fs6.statSync)(spawnLockPath).mtimeMs;
36126
+ const ageMs = Date.now() - (0, import_node_fs7.statSync)(spawnLockPath).mtimeMs;
35689
36127
  if (lockAlive && ageMs < COLLAB_INIT_SPAWN_LOCK_STALE_MS) {
35690
36128
  return { spawned: false, reason: "spawn_lock_held" };
35691
36129
  }
35692
36130
  if (!lockAlive) {
35693
36131
  try {
35694
- (0, import_node_fs6.unlinkSync)(spawnLockPath);
36132
+ (0, import_node_fs7.unlinkSync)(spawnLockPath);
35695
36133
  } catch {
35696
36134
  }
35697
36135
  }
@@ -35699,14 +36137,14 @@ function maybeAutoSpawnBranchInit(repoRoot) {
35699
36137
  } catch {
35700
36138
  }
35701
36139
  try {
35702
- (0, import_node_fs6.mkdirSync)(remixDir, { recursive: true });
36140
+ (0, import_node_fs7.mkdirSync)(remixDir, { recursive: true });
35703
36141
  } catch {
35704
36142
  }
35705
36143
  let out;
35706
36144
  let err;
35707
36145
  try {
35708
- out = (0, import_node_fs6.openSync)(logPath, "a");
35709
- err = (0, import_node_fs6.openSync)(logPath, "a");
36146
+ out = (0, import_node_fs7.openSync)(logPath, "a");
36147
+ err = (0, import_node_fs7.openSync)(logPath, "a");
35710
36148
  } catch (logErr) {
35711
36149
  return {
35712
36150
  spawned: false,
@@ -35715,7 +36153,7 @@ function maybeAutoSpawnBranchInit(repoRoot) {
35715
36153
  };
35716
36154
  }
35717
36155
  try {
35718
- const child = (0, import_node_child_process7.spawn)("remix", ["collab", "init"], {
36156
+ const child = (0, import_node_child_process8.spawn)("remix", ["collab", "init"], {
35719
36157
  cwd: repoRoot,
35720
36158
  detached: true,
35721
36159
  stdio: ["ignore", out, err],
@@ -35723,8 +36161,8 @@ function maybeAutoSpawnBranchInit(repoRoot) {
35723
36161
  });
35724
36162
  child.unref();
35725
36163
  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());
36164
+ (0, import_node_fs7.writeFileSync)(spawnLockPath, String(child.pid ?? ""), "utf8");
36165
+ (0, import_node_fs7.utimesSync)(spawnLockPath, /* @__PURE__ */ new Date(), /* @__PURE__ */ new Date());
35728
36166
  } catch {
35729
36167
  }
35730
36168
  return { spawned: true, pid: child.pid, logPath };
@@ -35742,7 +36180,7 @@ function buildBranchInitContextMessage(branch, repoRoot, logPath) {
35742
36180
  "[Remix recovery in progress]:",
35743
36181
  `Remix is initializing recording for ${branchLabel} in ${repoRoot} in the background.`,
35744
36182
  "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.",
36183
+ "Do NOT call any Remix MCP tool to initialize, repair, or sync this branch \u2014 the plugin is handling it automatically.",
35746
36184
  `Init log: ${logPath}`
35747
36185
  ].join("\n");
35748
36186
  }