@remixhq/claude-plugin 0.1.21 → 0.1.22

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -37,8 +37,8 @@ var require_windows = __commonJS({
37
37
  "use strict";
38
38
  module2.exports = isexe;
39
39
  isexe.sync = sync;
40
- var fs10 = require("fs");
41
- function checkPathExt(path13, options) {
40
+ var fs13 = require("fs");
41
+ function checkPathExt(path16, options) {
42
42
  var pathext = options.pathExt !== void 0 ? options.pathExt : process.env.PATHEXT;
43
43
  if (!pathext) {
44
44
  return true;
@@ -49,25 +49,25 @@ var require_windows = __commonJS({
49
49
  }
50
50
  for (var i2 = 0; i2 < pathext.length; i2++) {
51
51
  var p = pathext[i2].toLowerCase();
52
- if (p && path13.substr(-p.length).toLowerCase() === p) {
52
+ if (p && path16.substr(-p.length).toLowerCase() === p) {
53
53
  return true;
54
54
  }
55
55
  }
56
56
  return false;
57
57
  }
58
- function checkStat(stat, path13, options) {
58
+ function checkStat(stat, path16, options) {
59
59
  if (!stat.isSymbolicLink() && !stat.isFile()) {
60
60
  return false;
61
61
  }
62
- return checkPathExt(path13, options);
62
+ return checkPathExt(path16, options);
63
63
  }
64
- function isexe(path13, options, cb) {
65
- fs10.stat(path13, function(er, stat) {
66
- cb(er, er ? false : checkStat(stat, path13, options));
64
+ function isexe(path16, options, cb) {
65
+ fs13.stat(path16, function(er, stat) {
66
+ cb(er, er ? false : checkStat(stat, path16, options));
67
67
  });
68
68
  }
69
- function sync(path13, options) {
70
- return checkStat(fs10.statSync(path13), path13, options);
69
+ function sync(path16, options) {
70
+ return checkStat(fs13.statSync(path16), path16, options);
71
71
  }
72
72
  }
73
73
  });
@@ -78,14 +78,14 @@ var require_mode = __commonJS({
78
78
  "use strict";
79
79
  module2.exports = isexe;
80
80
  isexe.sync = sync;
81
- var fs10 = require("fs");
82
- function isexe(path13, options, cb) {
83
- fs10.stat(path13, function(er, stat) {
81
+ var fs13 = require("fs");
82
+ function isexe(path16, options, cb) {
83
+ fs13.stat(path16, function(er, stat) {
84
84
  cb(er, er ? false : checkStat(stat, options));
85
85
  });
86
86
  }
87
- function sync(path13, options) {
88
- return checkStat(fs10.statSync(path13), options);
87
+ function sync(path16, options) {
88
+ return checkStat(fs13.statSync(path16), options);
89
89
  }
90
90
  function checkStat(stat, options) {
91
91
  return stat.isFile() && checkMode(stat, options);
@@ -110,7 +110,7 @@ var require_mode = __commonJS({
110
110
  var require_isexe = __commonJS({
111
111
  "node_modules/isexe/index.js"(exports2, module2) {
112
112
  "use strict";
113
- var fs10 = require("fs");
113
+ var fs13 = require("fs");
114
114
  var core;
115
115
  if (process.platform === "win32" || global.TESTING_WINDOWS) {
116
116
  core = require_windows();
@@ -119,7 +119,7 @@ var require_isexe = __commonJS({
119
119
  }
120
120
  module2.exports = isexe;
121
121
  isexe.sync = sync;
122
- function isexe(path13, options, cb) {
122
+ function isexe(path16, options, cb) {
123
123
  if (typeof options === "function") {
124
124
  cb = options;
125
125
  options = {};
@@ -129,7 +129,7 @@ var require_isexe = __commonJS({
129
129
  throw new TypeError("callback not provided");
130
130
  }
131
131
  return new Promise(function(resolve, reject) {
132
- isexe(path13, options || {}, function(er, is) {
132
+ isexe(path16, options || {}, function(er, is) {
133
133
  if (er) {
134
134
  reject(er);
135
135
  } else {
@@ -138,7 +138,7 @@ var require_isexe = __commonJS({
138
138
  });
139
139
  });
140
140
  }
141
- core(path13, options || {}, function(er, is) {
141
+ core(path16, options || {}, function(er, is) {
142
142
  if (er) {
143
143
  if (er.code === "EACCES" || options && options.ignoreErrors) {
144
144
  er = null;
@@ -148,9 +148,9 @@ var require_isexe = __commonJS({
148
148
  cb(er, is);
149
149
  });
150
150
  }
151
- function sync(path13, options) {
151
+ function sync(path16, options) {
152
152
  try {
153
- return core.sync(path13, options || {});
153
+ return core.sync(path16, options || {});
154
154
  } catch (er) {
155
155
  if (options && options.ignoreErrors || er.code === "EACCES") {
156
156
  return false;
@@ -167,7 +167,7 @@ var require_which = __commonJS({
167
167
  "node_modules/which/which.js"(exports2, module2) {
168
168
  "use strict";
169
169
  var isWindows = process.platform === "win32" || process.env.OSTYPE === "cygwin" || process.env.OSTYPE === "msys";
170
- var path13 = require("path");
170
+ var path16 = require("path");
171
171
  var COLON = isWindows ? ";" : ":";
172
172
  var isexe = require_isexe();
173
173
  var getNotFoundError = (cmd) => Object.assign(new Error(`not found: ${cmd}`), { code: "ENOENT" });
@@ -205,7 +205,7 @@ var require_which = __commonJS({
205
205
  return opt.all && found.length ? resolve(found) : reject(getNotFoundError(cmd));
206
206
  const ppRaw = pathEnv[i2];
207
207
  const pathPart = /^".*"$/.test(ppRaw) ? ppRaw.slice(1, -1) : ppRaw;
208
- const pCmd = path13.join(pathPart, cmd);
208
+ const pCmd = path16.join(pathPart, cmd);
209
209
  const p = !pathPart && /^\.[\\\/]/.test(cmd) ? cmd.slice(0, 2) + pCmd : pCmd;
210
210
  resolve(subStep(p, i2, 0));
211
211
  });
@@ -232,7 +232,7 @@ var require_which = __commonJS({
232
232
  for (let i2 = 0; i2 < pathEnv.length; i2++) {
233
233
  const ppRaw = pathEnv[i2];
234
234
  const pathPart = /^".*"$/.test(ppRaw) ? ppRaw.slice(1, -1) : ppRaw;
235
- const pCmd = path13.join(pathPart, cmd);
235
+ const pCmd = path16.join(pathPart, cmd);
236
236
  const p = !pathPart && /^\.[\\\/]/.test(cmd) ? cmd.slice(0, 2) + pCmd : pCmd;
237
237
  for (let j = 0; j < pathExt.length; j++) {
238
238
  const cur = p + pathExt[j];
@@ -280,7 +280,7 @@ var require_path_key = __commonJS({
280
280
  var require_resolveCommand = __commonJS({
281
281
  "node_modules/cross-spawn/lib/util/resolveCommand.js"(exports2, module2) {
282
282
  "use strict";
283
- var path13 = require("path");
283
+ var path16 = require("path");
284
284
  var which = require_which();
285
285
  var getPathKey = require_path_key();
286
286
  function resolveCommandAttempt(parsed, withoutPathExt) {
@@ -298,7 +298,7 @@ var require_resolveCommand = __commonJS({
298
298
  try {
299
299
  resolved = which.sync(parsed.command, {
300
300
  path: env[getPathKey({ env })],
301
- pathExt: withoutPathExt ? path13.delimiter : void 0
301
+ pathExt: withoutPathExt ? path16.delimiter : void 0
302
302
  });
303
303
  } catch (e) {
304
304
  } finally {
@@ -307,7 +307,7 @@ var require_resolveCommand = __commonJS({
307
307
  }
308
308
  }
309
309
  if (resolved) {
310
- resolved = path13.resolve(hasCustomCwd ? parsed.options.cwd : "", resolved);
310
+ resolved = path16.resolve(hasCustomCwd ? parsed.options.cwd : "", resolved);
311
311
  }
312
312
  return resolved;
313
313
  }
@@ -361,8 +361,8 @@ var require_shebang_command = __commonJS({
361
361
  if (!match) {
362
362
  return null;
363
363
  }
364
- const [path13, argument] = match[0].replace(/#! ?/, "").split(" ");
365
- const binary = path13.split("/").pop();
364
+ const [path16, argument] = match[0].replace(/#! ?/, "").split(" ");
365
+ const binary = path16.split("/").pop();
366
366
  if (binary === "env") {
367
367
  return argument;
368
368
  }
@@ -375,16 +375,16 @@ var require_shebang_command = __commonJS({
375
375
  var require_readShebang = __commonJS({
376
376
  "node_modules/cross-spawn/lib/util/readShebang.js"(exports2, module2) {
377
377
  "use strict";
378
- var fs10 = require("fs");
378
+ var fs13 = require("fs");
379
379
  var shebangCommand = require_shebang_command();
380
380
  function readShebang(command) {
381
381
  const size = 150;
382
382
  const buffer = Buffer.alloc(size);
383
383
  let fd;
384
384
  try {
385
- fd = fs10.openSync(command, "r");
386
- fs10.readSync(fd, buffer, 0, size, 0);
387
- fs10.closeSync(fd);
385
+ fd = fs13.openSync(command, "r");
386
+ fs13.readSync(fd, buffer, 0, size, 0);
387
+ fs13.closeSync(fd);
388
388
  } catch (e) {
389
389
  }
390
390
  return shebangCommand(buffer.toString());
@@ -397,7 +397,7 @@ var require_readShebang = __commonJS({
397
397
  var require_parse = __commonJS({
398
398
  "node_modules/cross-spawn/lib/parse.js"(exports2, module2) {
399
399
  "use strict";
400
- var path13 = require("path");
400
+ var path16 = require("path");
401
401
  var resolveCommand = require_resolveCommand();
402
402
  var escape = require_escape();
403
403
  var readShebang = require_readShebang();
@@ -422,7 +422,7 @@ var require_parse = __commonJS({
422
422
  const needsShell = !isExecutableRegExp.test(commandFile);
423
423
  if (parsed.options.forceShell || needsShell) {
424
424
  const needsDoubleEscapeMetaChars = isCmdShimRegExp.test(commandFile);
425
- parsed.command = path13.normalize(parsed.command);
425
+ parsed.command = path16.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 spawn3(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 = spawn3;
528
- module2.exports.spawn = spawn3;
527
+ module2.exports = spawn5;
528
+ module2.exports.spawn = spawn5;
529
529
  module2.exports.sync = spawnSync3;
530
530
  module2.exports._parse = parse;
531
531
  module2.exports._enoent = enoent;
@@ -538,9 +538,7 @@ __export(hook_stop_collab_exports, {
538
538
  runHookStopCollab: () => runHookStopCollab
539
539
  });
540
540
  module.exports = __toCommonJS(hook_stop_collab_exports);
541
- var import_node_child_process7 = require("child_process");
542
- var import_node_fs7 = require("fs");
543
- var import_node_path10 = __toESM(require("path"), 1);
541
+ var import_node_child_process9 = require("child_process");
544
542
 
545
543
  // node_modules/@remixhq/core/dist/chunk-YZ34ICNN.js
546
544
  var RemixError = class extends Error {
@@ -4937,13 +4935,13 @@ var logOutputSync = ({ serializedResult, fdNumber, state, verboseInfo, encoding,
4937
4935
  }
4938
4936
  };
4939
4937
  var writeToFiles = (serializedResult, stdioItems, outputFiles) => {
4940
- for (const { path: path13, append } of stdioItems.filter(({ type }) => FILE_TYPES.has(type))) {
4941
- const pathString = typeof path13 === "string" ? path13 : path13.toString();
4938
+ for (const { path: path16, append } of stdioItems.filter(({ type }) => FILE_TYPES.has(type))) {
4939
+ const pathString = typeof path16 === "string" ? path16 : path16.toString();
4942
4940
  if (append || outputFiles.has(pathString)) {
4943
- (0, import_node_fs4.appendFileSync)(path13, serializedResult);
4941
+ (0, import_node_fs4.appendFileSync)(path16, serializedResult);
4944
4942
  } else {
4945
4943
  outputFiles.add(pathString);
4946
- (0, import_node_fs4.writeFileSync)(path13, serializedResult);
4944
+ (0, import_node_fs4.writeFileSync)(path16, serializedResult);
4947
4945
  }
4948
4946
  }
4949
4947
  };
@@ -10031,202 +10029,1059 @@ var FINALIZE_PREFLIGHT_FAILURE_CODES = [
10031
10029
  "re_anchor_required"
10032
10030
  ];
10033
10031
  var CODE_SET = new Set(FINALIZE_PREFLIGHT_FAILURE_CODES);
10032
+ function isFinalizePreflightFailureCode(value) {
10033
+ return typeof value === "string" && CODE_SET.has(value);
10034
+ }
10034
10035
 
10035
- // node_modules/@remixhq/core/dist/chunk-US5SM7ZC.js
10036
- async function readJsonSafe(res) {
10037
- const ct = res.headers.get("content-type") ?? "";
10038
- if (!ct.toLowerCase().includes("application/json")) return null;
10036
+ // src/auto-fix-dispatcher.ts
10037
+ var import_node_child_process6 = require("child_process");
10038
+ var import_node_fs6 = require("fs");
10039
+ var import_node_path9 = __toESM(require("path"), 1);
10040
+
10041
+ // src/finalize-failure-marker.ts
10042
+ var import_promises18 = __toESM(require("fs/promises"), 1);
10043
+ var import_node_path6 = __toESM(require("path"), 1);
10044
+ var FINALIZE_FAILURE_MARKER_REL = import_node_path6.default.join(".remix", ".last-finalize-failure.json");
10045
+ function markerPath(repoRoot) {
10046
+ return import_node_path6.default.join(repoRoot, FINALIZE_FAILURE_MARKER_REL);
10047
+ }
10048
+ async function writeFinalizeFailureMarker(marker) {
10049
+ const filePath = markerPath(marker.repoRoot);
10050
+ await import_promises18.default.mkdir(import_node_path6.default.dirname(filePath), { recursive: true });
10051
+ const tmpPath = `${filePath}.tmp-${process.pid}-${Date.now()}`;
10052
+ await import_promises18.default.writeFile(tmpPath, JSON.stringify(marker, null, 2), "utf8");
10053
+ await import_promises18.default.rename(tmpPath, filePath);
10054
+ }
10055
+ async function clearFinalizeFailureMarker(repoRoot) {
10056
+ await import_promises18.default.rm(markerPath(repoRoot), { force: true }).catch(() => void 0);
10057
+ }
10058
+ function buildFreshFailureMarker(params) {
10059
+ return {
10060
+ schemaVersion: 1,
10061
+ failedAt: (/* @__PURE__ */ new Date()).toISOString(),
10062
+ repoRoot: params.repoRoot,
10063
+ preflightCode: params.preflightCode,
10064
+ message: params.message,
10065
+ hint: params.hint,
10066
+ recommendedCommand: params.recommendedCommand,
10067
+ autoFix: {
10068
+ status: "not_attempted",
10069
+ command: null,
10070
+ pid: null,
10071
+ logPath: null,
10072
+ attemptedAt: null,
10073
+ failureMessage: null
10074
+ }
10075
+ };
10076
+ }
10077
+
10078
+ // src/hook-diagnostics.ts
10079
+ var import_node_crypto2 = require("crypto");
10080
+ var import_promises20 = __toESM(require("fs/promises"), 1);
10081
+ var import_node_os5 = __toESM(require("os"), 1);
10082
+ var import_node_path8 = __toESM(require("path"), 1);
10083
+
10084
+ // src/hook-state.ts
10085
+ var import_promises19 = __toESM(require("fs/promises"), 1);
10086
+ var import_node_os4 = __toESM(require("os"), 1);
10087
+ var import_node_path7 = __toESM(require("path"), 1);
10088
+ var import_node_crypto = require("crypto");
10089
+ function stateRoot() {
10090
+ const configured = process.env.REMIX_CLAUDE_PLUGIN_HOOK_STATE_ROOT?.trim();
10091
+ return configured || import_node_path7.default.join(import_node_os4.default.tmpdir(), "remix-claude-plugin-hooks");
10092
+ }
10093
+ function statePath(sessionId) {
10094
+ return import_node_path7.default.join(stateRoot(), `${sessionId}.json`);
10095
+ }
10096
+ function stateLockPath(sessionId) {
10097
+ return import_node_path7.default.join(stateRoot(), `${sessionId}.lock`);
10098
+ }
10099
+ function stateLockMetaPath(sessionId) {
10100
+ return import_node_path7.default.join(stateLockPath(sessionId), "owner.json");
10101
+ }
10102
+ async function writeJsonAtomic2(filePath, value) {
10103
+ await import_promises19.default.mkdir(import_node_path7.default.dirname(filePath), { recursive: true });
10104
+ const tmpPath = `${filePath}.tmp-${Date.now()}-${Math.random().toString(16).slice(2)}`;
10105
+ await import_promises19.default.writeFile(tmpPath, JSON.stringify(value, null, 2) + "\n", "utf8");
10106
+ await import_promises19.default.rename(tmpPath, filePath);
10107
+ }
10108
+ var STATE_LOCK_WAIT_MS = 2e3;
10109
+ var STATE_LOCK_POLL_MS = 25;
10110
+ var STATE_LOCK_STALE_MS = 3e4;
10111
+ var STATE_LOCK_HEARTBEAT_MS = 5e3;
10112
+ async function sleep2(ms) {
10113
+ await new Promise((resolve) => setTimeout(resolve, ms));
10114
+ }
10115
+ async function readStateLockMetadata(sessionId) {
10116
+ const raw = await import_promises19.default.readFile(stateLockMetaPath(sessionId), "utf8").catch(() => null);
10117
+ if (!raw) return null;
10039
10118
  try {
10040
- return await res.json();
10119
+ const parsed = JSON.parse(raw);
10120
+ if (typeof parsed.ownerId !== "string" || typeof parsed.pid !== "number" || typeof parsed.createdAt !== "string" || typeof parsed.heartbeatAt !== "string") {
10121
+ return null;
10122
+ }
10123
+ return {
10124
+ ownerId: parsed.ownerId,
10125
+ pid: parsed.pid,
10126
+ createdAt: parsed.createdAt,
10127
+ heartbeatAt: parsed.heartbeatAt
10128
+ };
10041
10129
  } catch {
10042
10130
  return null;
10043
10131
  }
10044
10132
  }
10045
- function createApiClient(config, opts) {
10046
- const apiKey = (opts?.apiKey ?? "").trim();
10047
- const tokenProvider = opts?.tokenProvider;
10048
- const CLIENT_KEY_HEADER = "x-comerge-api-key";
10049
- async function request(path13, init) {
10050
- if (!tokenProvider) {
10051
- throw new RemixError("API client is missing a token provider.", {
10052
- exitCode: 1,
10053
- hint: "Configure auth before creating the Remix API client."
10054
- });
10133
+ async function writeStateLockMetadata(sessionId, metadata) {
10134
+ await writeJsonAtomic2(stateLockMetaPath(sessionId), metadata);
10135
+ }
10136
+ async function tryRemoveStaleStateLock(sessionId) {
10137
+ const lockPath = stateLockPath(sessionId);
10138
+ const metadata = await readStateLockMetadata(sessionId);
10139
+ const staleByHeartbeat = metadata && Date.now() - new Date(metadata.heartbeatAt).getTime() > STATE_LOCK_STALE_MS;
10140
+ if (staleByHeartbeat) {
10141
+ await import_promises19.default.rm(lockPath, { recursive: true, force: true }).catch(() => void 0);
10142
+ return true;
10143
+ }
10144
+ if (!metadata) {
10145
+ const lockStat = await import_promises19.default.stat(lockPath).catch(() => null);
10146
+ if (lockStat && Date.now() - lockStat.mtimeMs > STATE_LOCK_STALE_MS) {
10147
+ await import_promises19.default.rm(lockPath, { recursive: true, force: true }).catch(() => void 0);
10148
+ return true;
10055
10149
  }
10056
- const auth = await tokenProvider();
10057
- const url = new URL(path13, config.apiUrl).toString();
10058
- const doFetch = async (bearer) => fetch(url, {
10059
- ...init,
10060
- headers: {
10061
- Accept: "application/json",
10062
- "Content-Type": "application/json",
10063
- ...init?.headers ?? {},
10064
- Authorization: `Bearer ${bearer}`,
10065
- ...apiKey ? { [CLIENT_KEY_HEADER]: apiKey } : {}
10150
+ }
10151
+ return false;
10152
+ }
10153
+ async function acquireStateLock(sessionId) {
10154
+ const lockPath = stateLockPath(sessionId);
10155
+ const deadline = Date.now() + STATE_LOCK_WAIT_MS;
10156
+ await import_promises19.default.mkdir(stateRoot(), { recursive: true });
10157
+ while (true) {
10158
+ try {
10159
+ await import_promises19.default.mkdir(lockPath);
10160
+ const ownerId = (0, import_node_crypto.randomUUID)();
10161
+ const createdAt = (/* @__PURE__ */ new Date()).toISOString();
10162
+ const metadata = {
10163
+ ownerId,
10164
+ pid: process.pid,
10165
+ createdAt,
10166
+ heartbeatAt: createdAt
10167
+ };
10168
+ await writeStateLockMetadata(sessionId, metadata);
10169
+ let released = false;
10170
+ const heartbeat = setInterval(() => {
10171
+ if (released) return;
10172
+ void writeStateLockMetadata(sessionId, {
10173
+ ...metadata,
10174
+ heartbeatAt: (/* @__PURE__ */ new Date()).toISOString()
10175
+ }).catch(() => void 0);
10176
+ }, STATE_LOCK_HEARTBEAT_MS);
10177
+ heartbeat.unref?.();
10178
+ return async () => {
10179
+ if (released) return;
10180
+ released = true;
10181
+ clearInterval(heartbeat);
10182
+ const currentMetadata = await readStateLockMetadata(sessionId);
10183
+ if (currentMetadata?.ownerId === ownerId) {
10184
+ await import_promises19.default.rm(lockPath, { recursive: true, force: true }).catch(() => void 0);
10185
+ }
10186
+ };
10187
+ } catch (error) {
10188
+ const code = error && typeof error === "object" && "code" in error ? error.code : null;
10189
+ if (code !== "EEXIST") {
10190
+ throw error;
10066
10191
  }
10067
- });
10068
- let res = await doFetch(auth.token);
10069
- if (res.status === 401 && !auth.fromEnv && auth.session?.refresh_token) {
10070
- const refreshed = await tokenProvider({ forceRefresh: true });
10071
- res = await doFetch(refreshed.token);
10072
- }
10073
- if (!res.ok) {
10074
- const body = await readJsonSafe(res);
10075
- const msg = (body && typeof body === "object" && body && "message" in body && typeof body.message === "string" ? body.message : null) ?? `Request failed (status ${res.status})`;
10076
- throw new RemixError(msg, { exitCode: 1, hint: body ? JSON.stringify(body, null, 2) : null });
10192
+ if (await tryRemoveStaleStateLock(sessionId)) {
10193
+ continue;
10194
+ }
10195
+ if (Date.now() >= deadline) {
10196
+ throw new Error(`Timed out acquiring hook state lock for session ${sessionId}.`);
10197
+ }
10198
+ await sleep2(STATE_LOCK_POLL_MS);
10077
10199
  }
10078
- const json = await readJsonSafe(res);
10079
- return json ?? null;
10080
10200
  }
10081
- async function requestBinary(path13, init) {
10082
- if (!tokenProvider) {
10083
- throw new RemixError("API client is missing a token provider.", {
10084
- exitCode: 1,
10085
- hint: "Configure auth before creating the Remix API client."
10086
- });
10201
+ }
10202
+ async function withStateLock(sessionId, fn) {
10203
+ const release = await acquireStateLock(sessionId);
10204
+ try {
10205
+ return await fn();
10206
+ } finally {
10207
+ await release();
10208
+ }
10209
+ }
10210
+ function normalizeIntent(value) {
10211
+ return value === "memory_first" || value === "collab_state" || value === "git_facts" ? value : "neutral";
10212
+ }
10213
+ function normalizeString(value) {
10214
+ return typeof value === "string" && value.trim() ? value.trim() : null;
10215
+ }
10216
+ function normalizeStringArray(value) {
10217
+ if (!Array.isArray(value)) return [];
10218
+ return Array.from(
10219
+ new Set(
10220
+ value.filter((entry) => typeof entry === "string" && entry.trim().length > 0).map((entry) => entry.trim())
10221
+ )
10222
+ );
10223
+ }
10224
+ function normalizeManualRecordingScope(value) {
10225
+ if (value === "full_turn") {
10226
+ return "full_turn";
10227
+ }
10228
+ return null;
10229
+ }
10230
+ function normalizeTouchedRepo(value, repoRoot) {
10231
+ if (!value || typeof value !== "object") return null;
10232
+ const parsed = value;
10233
+ const normalizedRepoRoot = normalizeString(parsed.repoRoot) ?? repoRoot.trim();
10234
+ if (!normalizedRepoRoot) return null;
10235
+ return {
10236
+ repoRoot: normalizedRepoRoot,
10237
+ projectId: normalizeString(parsed.projectId),
10238
+ currentAppId: normalizeString(parsed.currentAppId),
10239
+ upstreamAppId: normalizeString(parsed.upstreamAppId),
10240
+ firstTouchedAt: normalizeString(parsed.firstTouchedAt) ?? (/* @__PURE__ */ new Date()).toISOString(),
10241
+ lastTouchedAt: normalizeString(parsed.lastTouchedAt) ?? (/* @__PURE__ */ new Date()).toISOString(),
10242
+ lastObservedWriteAt: normalizeString(parsed.lastObservedWriteAt),
10243
+ touchedBy: normalizeStringArray(parsed.touchedBy),
10244
+ hasObservedWrite: Boolean(parsed.hasObservedWrite),
10245
+ manuallyRecorded: Boolean(parsed.manuallyRecorded),
10246
+ manuallyRecordedAt: normalizeString(parsed.manuallyRecordedAt),
10247
+ manuallyRecordedByTool: normalizeString(parsed.manuallyRecordedByTool),
10248
+ manualRecordingScope: normalizeManualRecordingScope(parsed.manualRecordingScope),
10249
+ manualRemoteChangeRecordedAt: normalizeString(parsed.manualRemoteChangeRecordedAt),
10250
+ stopAttempted: Boolean(parsed.stopAttempted),
10251
+ stopRecorded: Boolean(parsed.stopRecorded),
10252
+ stopRecordedAt: normalizeString(parsed.stopRecordedAt),
10253
+ stopRecordedMode: parsed.stopRecordedMode === "changed_turn" || parsed.stopRecordedMode === "no_diff_turn" ? parsed.stopRecordedMode : null,
10254
+ recordingFailureMessage: normalizeString(parsed.recordingFailureMessage),
10255
+ recordingFailureHint: normalizeString(parsed.recordingFailureHint),
10256
+ recordingFailedAt: normalizeString(parsed.recordingFailedAt)
10257
+ };
10258
+ }
10259
+ function normalizeTouchedRepos(value) {
10260
+ if (!value || typeof value !== "object") return {};
10261
+ const entries = Object.entries(value).map(([repoRoot, repo]) => normalizeTouchedRepo(repo, repoRoot)).filter((repo) => repo !== null).sort((a2, b) => a2.repoRoot.localeCompare(b.repoRoot));
10262
+ return Object.fromEntries(entries.map((repo) => [repo.repoRoot, repo]));
10263
+ }
10264
+ function createTouchedRepo(params) {
10265
+ const now = (/* @__PURE__ */ new Date()).toISOString();
10266
+ const touchedBy = params.touchedBy?.trim() ? [params.touchedBy.trim()] : [];
10267
+ return {
10268
+ repoRoot: params.repoRoot,
10269
+ projectId: normalizeString(params.projectId),
10270
+ currentAppId: normalizeString(params.currentAppId),
10271
+ upstreamAppId: normalizeString(params.upstreamAppId),
10272
+ firstTouchedAt: now,
10273
+ lastTouchedAt: now,
10274
+ lastObservedWriteAt: params.hasObservedWrite ? now : null,
10275
+ touchedBy,
10276
+ hasObservedWrite: Boolean(params.hasObservedWrite),
10277
+ manuallyRecorded: false,
10278
+ manuallyRecordedAt: null,
10279
+ manuallyRecordedByTool: null,
10280
+ manualRecordingScope: null,
10281
+ manualRemoteChangeRecordedAt: null,
10282
+ stopAttempted: false,
10283
+ stopRecorded: false,
10284
+ stopRecordedAt: null,
10285
+ stopRecordedMode: null,
10286
+ recordingFailureMessage: null,
10287
+ recordingFailureHint: null,
10288
+ recordingFailedAt: null
10289
+ };
10290
+ }
10291
+ async function updatePendingTurnState(sessionId, updater) {
10292
+ return withStateLock(sessionId, async () => {
10293
+ const existing = await loadPendingTurnState(sessionId);
10294
+ if (!existing) return null;
10295
+ const result = updater(existing);
10296
+ if (result === false) return existing;
10297
+ await savePendingTurnState(existing);
10298
+ return existing;
10299
+ });
10300
+ }
10301
+ async function loadPendingTurnState(sessionId) {
10302
+ const raw = await import_promises19.default.readFile(statePath(sessionId), "utf8").catch(() => null);
10303
+ if (!raw) return null;
10304
+ try {
10305
+ const parsed = JSON.parse(raw);
10306
+ if (!parsed || typeof parsed !== "object") return null;
10307
+ if (typeof parsed.sessionId !== "string" || typeof parsed.turnId !== "string" || typeof parsed.prompt !== "string") {
10308
+ return null;
10087
10309
  }
10088
- const auth = await tokenProvider();
10089
- const url = new URL(path13, config.apiUrl).toString();
10090
- const doFetch = async (bearer) => fetch(url, {
10091
- ...init,
10092
- headers: {
10093
- Accept: "*/*",
10094
- ...init?.headers ?? {},
10095
- Authorization: `Bearer ${bearer}`,
10096
- ...apiKey ? { [CLIENT_KEY_HEADER]: apiKey } : {}
10097
- }
10310
+ return {
10311
+ sessionId: parsed.sessionId,
10312
+ turnId: parsed.turnId,
10313
+ prompt: parsed.prompt,
10314
+ initialCwd: normalizeString(parsed.initialCwd),
10315
+ intent: normalizeIntent(parsed.intent),
10316
+ submittedAt: typeof parsed.submittedAt === "string" ? parsed.submittedAt : (/* @__PURE__ */ new Date()).toISOString(),
10317
+ consultedMemory: Boolean(parsed.consultedMemory),
10318
+ touchedRepos: normalizeTouchedRepos(parsed.touchedRepos),
10319
+ turnFailureMessage: normalizeString(parsed.turnFailureMessage),
10320
+ turnFailureHint: normalizeString(parsed.turnFailureHint),
10321
+ turnFailedAt: normalizeString(parsed.turnFailedAt)
10322
+ };
10323
+ } catch {
10324
+ return null;
10325
+ }
10326
+ }
10327
+ async function savePendingTurnState(state) {
10328
+ await writeJsonAtomic2(statePath(state.sessionId), state);
10329
+ }
10330
+ async function upsertTouchedRepo(sessionId, params) {
10331
+ const normalizedRepoRoot = params.repoRoot.trim();
10332
+ if (!normalizedRepoRoot) return null;
10333
+ const state = await updatePendingTurnState(sessionId, (existing) => {
10334
+ const current = existing.touchedRepos[normalizedRepoRoot] ?? createTouchedRepo({
10335
+ repoRoot: normalizedRepoRoot,
10336
+ projectId: params.projectId,
10337
+ currentAppId: params.currentAppId,
10338
+ upstreamAppId: params.upstreamAppId,
10339
+ touchedBy: params.touchedBy,
10340
+ hasObservedWrite: params.hasObservedWrite
10098
10341
  });
10099
- let res = await doFetch(auth.token);
10100
- if (res.status === 401 && !auth.fromEnv && auth.session?.refresh_token) {
10101
- const refreshed = await tokenProvider({ forceRefresh: true });
10102
- res = await doFetch(refreshed.token);
10342
+ current.projectId = normalizeString(params.projectId) ?? current.projectId;
10343
+ current.currentAppId = normalizeString(params.currentAppId) ?? current.currentAppId;
10344
+ current.upstreamAppId = normalizeString(params.upstreamAppId) ?? current.upstreamAppId;
10345
+ current.lastTouchedAt = (/* @__PURE__ */ new Date()).toISOString();
10346
+ if (params.touchedBy?.trim() && !current.touchedBy.includes(params.touchedBy.trim())) {
10347
+ current.touchedBy = [...current.touchedBy, params.touchedBy.trim()].sort((a2, b) => a2.localeCompare(b));
10103
10348
  }
10104
- if (!res.ok) {
10105
- const body = await readJsonSafe(res);
10106
- const msg = (body && typeof body === "object" && body && "message" in body && typeof body.message === "string" ? body.message : null) ?? `Request failed (status ${res.status})`;
10107
- throw new RemixError(msg, { exitCode: 1, hint: body ? JSON.stringify(body, null, 2) : null });
10349
+ if (params.hasObservedWrite) {
10350
+ current.hasObservedWrite = true;
10351
+ current.lastObservedWriteAt = (/* @__PURE__ */ new Date()).toISOString();
10108
10352
  }
10109
- const contentDisposition = res.headers.get("content-disposition") ?? "";
10110
- const fileNameMatch = contentDisposition.match(/filename=\"([^\"]+)\"/i);
10111
- return {
10112
- data: Buffer.from(await res.arrayBuffer()),
10113
- fileName: fileNameMatch?.[1] ?? null,
10114
- contentType: res.headers.get("content-type")
10115
- };
10353
+ existing.touchedRepos[normalizedRepoRoot] = current;
10354
+ });
10355
+ return state?.touchedRepos[normalizedRepoRoot] ?? null;
10356
+ }
10357
+ async function markTouchedRepoStopAttempted(sessionId, repoRoot) {
10358
+ await updatePendingTurnState(sessionId, (existing) => {
10359
+ const current = existing.touchedRepos[repoRoot];
10360
+ if (!current) return false;
10361
+ current.stopAttempted = true;
10362
+ current.lastTouchedAt = (/* @__PURE__ */ new Date()).toISOString();
10363
+ });
10364
+ }
10365
+ async function markTouchedRepoStopRecorded(sessionId, repoRoot, params) {
10366
+ await updatePendingTurnState(sessionId, (existing) => {
10367
+ const current = existing.touchedRepos[repoRoot];
10368
+ if (!current) return false;
10369
+ current.stopAttempted = true;
10370
+ current.stopRecorded = true;
10371
+ current.stopRecordedAt = (/* @__PURE__ */ new Date()).toISOString();
10372
+ current.stopRecordedMode = params.mode;
10373
+ current.recordingFailureMessage = null;
10374
+ current.recordingFailureHint = null;
10375
+ current.recordingFailedAt = null;
10376
+ current.lastTouchedAt = (/* @__PURE__ */ new Date()).toISOString();
10377
+ });
10378
+ }
10379
+ async function markTouchedRepoRecordingFailure(sessionId, repoRoot, params) {
10380
+ await updatePendingTurnState(sessionId, (existing) => {
10381
+ const current = existing.touchedRepos[repoRoot];
10382
+ if (!current) return false;
10383
+ current.stopAttempted = true;
10384
+ current.recordingFailureMessage = params.message.trim();
10385
+ current.recordingFailureHint = params.hint?.trim() || null;
10386
+ current.recordingFailedAt = (/* @__PURE__ */ new Date()).toISOString();
10387
+ current.lastTouchedAt = (/* @__PURE__ */ new Date()).toISOString();
10388
+ });
10389
+ }
10390
+ function lastFinalizedPath(sessionId) {
10391
+ return import_node_path7.default.join(stateRoot(), `${sessionId}.last-finalized.json`);
10392
+ }
10393
+ async function markLastFinalizedTurn(sessionId, turnId, prompt) {
10394
+ const record = {
10395
+ sessionId,
10396
+ turnId,
10397
+ prompt,
10398
+ finalizedAt: (/* @__PURE__ */ new Date()).toISOString()
10399
+ };
10400
+ await writeJsonAtomic2(lastFinalizedPath(sessionId), record);
10401
+ }
10402
+ async function loadLastFinalizedTurn(sessionId) {
10403
+ const raw = await import_promises19.default.readFile(lastFinalizedPath(sessionId), "utf8").catch(() => null);
10404
+ if (!raw) return null;
10405
+ try {
10406
+ const parsed = JSON.parse(raw);
10407
+ if (typeof parsed.sessionId === "string" && typeof parsed.turnId === "string" && typeof parsed.prompt === "string" && typeof parsed.finalizedAt === "string") {
10408
+ return {
10409
+ sessionId: parsed.sessionId,
10410
+ turnId: parsed.turnId,
10411
+ prompt: parsed.prompt,
10412
+ finalizedAt: parsed.finalizedAt
10413
+ };
10414
+ }
10415
+ return null;
10416
+ } catch {
10417
+ return null;
10116
10418
  }
10117
- return {
10118
- getMe: () => request("/v1/me", { method: "GET" }),
10119
- listOrganizations: () => request("/v1/organizations", { method: "GET" }),
10120
- getOrganization: (orgId) => request(`/v1/organizations/${encodeURIComponent(orgId)}`, { method: "GET" }),
10121
- listProjects: (params) => {
10122
- const qs = new URLSearchParams();
10123
- if (params?.organizationId) qs.set("organizationId", params.organizationId);
10124
- if (params?.clientAppId) qs.set("clientAppId", params.clientAppId);
10125
- const suffix = qs.toString() ? `?${qs.toString()}` : "";
10126
- return request(`/v1/projects${suffix}`, { method: "GET" });
10127
- },
10128
- getProject: (projectId) => request(`/v1/projects/${encodeURIComponent(projectId)}`, { method: "GET" }),
10129
- resolveProjectBinding: (params) => {
10130
- const qs = new URLSearchParams();
10131
- if (params.repoFingerprint) qs.set("repoFingerprint", params.repoFingerprint);
10132
- if (params.remoteUrl) qs.set("remoteUrl", params.remoteUrl);
10133
- if (params.branchName) qs.set("branchName", params.branchName);
10134
- return request(`/v1/projects/bindings/resolve?${qs.toString()}`, { method: "GET" });
10135
- },
10136
- resolveProjectLaneBinding: (params) => {
10137
- const qs = new URLSearchParams();
10138
- if (params.projectId) qs.set("projectId", params.projectId);
10139
- if (params.repoFingerprint) qs.set("repoFingerprint", params.repoFingerprint);
10140
- if (params.remoteUrl) qs.set("remoteUrl", params.remoteUrl);
10141
- if (params.defaultBranch) qs.set("defaultBranch", params.defaultBranch);
10142
- qs.set("branchName", params.branchName);
10143
- return request(`/v1/projects/bindings/resolve-lane?${qs.toString()}`, { method: "GET" });
10144
- },
10145
- ensureProjectLaneBinding: (payload) => request("/v1/projects/bindings/ensure-lane", { method: "POST", body: JSON.stringify(payload) }),
10146
- bootstrapFreshProjectLane: (payload) => request("/v1/projects/bindings/bootstrap-fresh-lane", { method: "POST", body: JSON.stringify(payload) }),
10147
- autoEnableDeveloper: () => request("/v1/developer/auto-enable", { method: "POST" }),
10148
- listClientApps: (params) => {
10149
- const qs = params?.orgId ? `?orgId=${encodeURIComponent(params.orgId)}` : "";
10150
- return request(`/v1/developer/client-apps${qs}`, { method: "GET" });
10151
- },
10152
- createClientApp: (payload) => request("/v1/developer/client-apps", { method: "POST", body: JSON.stringify(payload) }),
10153
- createClientAppKey: (clientAppId, payload) => request(`/v1/developer/client-apps/${encodeURIComponent(clientAppId)}/keys`, {
10154
- method: "POST",
10155
- body: JSON.stringify(payload ?? {})
10156
- }),
10157
- listApps: (params) => {
10158
- const qs = new URLSearchParams();
10159
- if (params?.projectId) qs.set("projectId", params.projectId);
10160
- if (params?.organizationId) qs.set("organizationId", params.organizationId);
10161
- if (params?.ownership) qs.set("ownership", params.ownership);
10162
- if (params?.accessScope) qs.set("accessScope", params.accessScope);
10163
- if (params?.createdBy) qs.set("createdBy", params.createdBy);
10164
- if (params?.forked) qs.set("forked", params.forked);
10165
- if (typeof params?.limit === "number") qs.set("limit", String(params.limit));
10166
- if (typeof params?.offset === "number") qs.set("offset", String(params.offset));
10167
- const suffix = qs.toString() ? `?${qs.toString()}` : "";
10168
- return request(`/v1/apps${suffix}`, { method: "GET" });
10169
- },
10170
- getApp: (appId) => request(`/v1/apps/${encodeURIComponent(appId)}`, { method: "GET" }),
10171
- getAppContext: (appId) => request(`/v1/apps/${encodeURIComponent(appId)}/context`, { method: "GET" }),
10172
- getAppOverview: (appId) => request(`/v1/apps/${encodeURIComponent(appId)}/overview`, { method: "GET" }),
10173
- listAppTimeline: (appId, params) => {
10174
- const qs = new URLSearchParams();
10175
- if (typeof params?.limit === "number") qs.set("limit", String(params.limit));
10176
- if (params?.cursor) qs.set("cursor", params.cursor);
10177
- const suffix = qs.toString() ? `?${qs.toString()}` : "";
10178
- return request(`/v1/apps/${encodeURIComponent(appId)}/timeline${suffix}`, { method: "GET" });
10179
- },
10180
- getAppTimelineEvent: (appId, eventId) => request(`/v1/apps/${encodeURIComponent(appId)}/timeline/${encodeURIComponent(eventId)}`, { method: "GET" }),
10181
- listAppEditQueue: (appId, params) => {
10182
- const qs = new URLSearchParams();
10183
- if (typeof params?.limit === "number") qs.set("limit", String(params.limit));
10184
- if (typeof params?.offset === "number") qs.set("offset", String(params.offset));
10185
- const suffix = qs.toString() ? `?${qs.toString()}` : "";
10186
- return request(`/v1/apps/${encodeURIComponent(appId)}/edit-queue${suffix}`, { method: "GET" });
10187
- },
10188
- listAppJobQueue: (appId, params) => {
10189
- const qs = new URLSearchParams();
10190
- if (typeof params?.limit === "number") qs.set("limit", String(params.limit));
10191
- if (typeof params?.offset === "number") qs.set("offset", String(params.offset));
10192
- for (const kind of params?.kind ?? []) qs.append("kind", kind);
10193
- for (const status of params?.status ?? []) qs.append("status", status);
10194
- const suffix = qs.toString() ? `?${qs.toString()}` : "";
10195
- return request(`/v1/apps/${encodeURIComponent(appId)}/job-queue${suffix}`, { method: "GET" });
10196
- },
10197
- getMergeRequest: (mrId) => request(`/v1/merge-requests/${encodeURIComponent(mrId)}`, { method: "GET" }),
10198
- presignImportUpload: (payload) => request("/v1/apps/import/upload/presign", { method: "POST", body: JSON.stringify(payload) }),
10199
- importFromUpload: (payload) => request("/v1/apps/import/upload", { method: "POST", body: JSON.stringify(payload) }),
10200
- presignImportUploadFirstParty: (payload) => request("/v1/apps/import/upload/presign/first-party", { method: "POST", body: JSON.stringify(payload) }),
10201
- importFromUploadFirstParty: (payload) => request("/v1/apps/import/upload/first-party", { method: "POST", body: JSON.stringify(payload) }),
10202
- importFromGithubFirstParty: (payload) => request("/v1/apps/import/github/first-party", { method: "POST", body: JSON.stringify(payload) }),
10203
- forkApp: (appId, payload) => request(`/v1/apps/${encodeURIComponent(appId)}/fork`, { method: "POST", body: JSON.stringify(payload ?? {}) }),
10204
- getAppHead: (appId) => request(`/v1/apps/${encodeURIComponent(appId)}/head`, { method: "GET" }),
10205
- getAppDelta: (appId, payload) => request(`/v1/apps/${encodeURIComponent(appId)}/delta`, {
10206
- method: "POST",
10207
- body: JSON.stringify(payload)
10208
- }),
10209
- downloadAppBundle: (appId) => requestBinary(`/v1/apps/${encodeURIComponent(appId)}/download.bundle`, { method: "GET" }),
10210
- createChangeStep: (appId, payload) => request(`/v1/apps/${encodeURIComponent(appId)}/change-steps`, {
10211
- method: "POST",
10212
- body: JSON.stringify(payload)
10213
- }),
10214
- createCollabTurn: (appId, payload) => request(`/v1/apps/${encodeURIComponent(appId)}/collab-turns`, {
10215
- method: "POST",
10216
- body: JSON.stringify(payload)
10217
- }),
10218
- attachCollabTurnUsage: (appId, payload) => request(`/v1/apps/${encodeURIComponent(appId)}/collab-turns/attach-usage`, {
10219
- method: "POST",
10220
- body: JSON.stringify(payload)
10221
- }),
10222
- listCollabTurns: (appId, params) => {
10223
- const qs = new URLSearchParams();
10224
- if (params?.limit !== void 0) qs.set("limit", String(params.limit));
10225
- if (params?.offset !== void 0) qs.set("offset", String(params.offset));
10226
- if (params?.changeStepId) qs.set("changeStepId", params.changeStepId);
10227
- if (params?.threadId) qs.set("threadId", params.threadId);
10228
- if (params?.collabLaneId) qs.set("collabLaneId", params.collabLaneId);
10229
- if (params?.createdAfter) qs.set("createdAfter", params.createdAfter);
10419
+ }
10420
+ async function markPendingTurnFailure(sessionId, params) {
10421
+ await updatePendingTurnState(sessionId, (existing) => {
10422
+ existing.turnFailureMessage = params.message.trim();
10423
+ existing.turnFailureHint = params.hint?.trim() || null;
10424
+ existing.turnFailedAt = (/* @__PURE__ */ new Date()).toISOString();
10425
+ });
10426
+ }
10427
+ async function listTouchedRepos(sessionId) {
10428
+ const existing = await loadPendingTurnState(sessionId);
10429
+ if (!existing) return [];
10430
+ return Object.values(existing.touchedRepos).sort((a2, b) => a2.repoRoot.localeCompare(b.repoRoot));
10431
+ }
10432
+ async function clearPendingTurnState(sessionId) {
10433
+ await withStateLock(sessionId, async () => {
10434
+ await import_promises19.default.rm(statePath(sessionId), { force: true }).catch(() => void 0);
10435
+ });
10436
+ }
10437
+
10438
+ // package.json
10439
+ var package_default = {
10440
+ name: "@remixhq/claude-plugin",
10441
+ version: "0.1.22",
10442
+ description: "Claude Code plugin for Remix collaboration workflows",
10443
+ homepage: "https://github.com/RemixDotOne/remix-claude-plugin",
10444
+ license: "MIT",
10445
+ repository: {
10446
+ type: "git",
10447
+ url: "https://github.com/RemixDotOne/remix-claude-plugin.git"
10448
+ },
10449
+ type: "module",
10450
+ engines: {
10451
+ node: ">=20"
10452
+ },
10453
+ publishConfig: {
10454
+ access: "public"
10455
+ },
10456
+ files: [
10457
+ "dist",
10458
+ ".claude-plugin/plugin.json",
10459
+ ".mcp.json",
10460
+ "skills",
10461
+ "hooks",
10462
+ "agents"
10463
+ ],
10464
+ exports: {
10465
+ ".": {
10466
+ types: "./dist/index.d.ts",
10467
+ import: "./dist/index.js"
10468
+ }
10469
+ },
10470
+ scripts: {
10471
+ build: "tsup",
10472
+ 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);"`,
10473
+ dev: "tsx src/mcp-server.ts",
10474
+ typecheck: "tsc -p tsconfig.json --noEmit",
10475
+ test: "node --import tsx --test 'src/**/*.test.ts'",
10476
+ prepack: "npm run build"
10477
+ },
10478
+ dependencies: {
10479
+ "@remixhq/core": "^0.1.17",
10480
+ "@remixhq/mcp": "^0.1.17"
10481
+ },
10482
+ devDependencies: {
10483
+ "@types/node": "^25.4.0",
10484
+ tsup: "^8.5.1",
10485
+ tsx: "^4.21.0",
10486
+ typescript: "^5.9.3"
10487
+ }
10488
+ };
10489
+
10490
+ // src/metadata.ts
10491
+ var pluginMetadata = {
10492
+ name: package_default.name,
10493
+ version: package_default.version,
10494
+ description: package_default.description,
10495
+ pluginId: "remix",
10496
+ agentName: "remix-collab"
10497
+ };
10498
+
10499
+ // src/hook-diagnostics.ts
10500
+ var MAX_LOG_BYTES = 512 * 1024;
10501
+ function resolveClaudeRoot() {
10502
+ const configured = process.env.CLAUDE_CONFIG_DIR?.trim();
10503
+ return configured || import_node_path8.default.join(import_node_os5.default.homedir(), ".claude");
10504
+ }
10505
+ function resolvePluginDataDirName() {
10506
+ return `${pluginMetadata.pluginId}-${pluginMetadata.pluginId}`;
10507
+ }
10508
+ function getHookDiagnosticsDirPath() {
10509
+ const configured = process.env.REMIX_CLAUDE_PLUGIN_HOOK_DIAGNOSTICS_DIR?.trim();
10510
+ return configured || import_node_path8.default.join(resolveClaudeRoot(), "plugins", "data", resolvePluginDataDirName());
10511
+ }
10512
+ function getHookDiagnosticsLogPath() {
10513
+ return import_node_path8.default.join(getHookDiagnosticsDirPath(), "hooks.ndjson");
10514
+ }
10515
+ function toFieldValue(value) {
10516
+ if (value === null) return null;
10517
+ if (typeof value === "string") return value;
10518
+ if (typeof value === "number" && Number.isFinite(value)) return value;
10519
+ if (typeof value === "boolean") return value;
10520
+ return void 0;
10521
+ }
10522
+ function normalizeFields(fields) {
10523
+ if (!fields) return {};
10524
+ const normalizedEntries = Object.entries(fields).map(([key, value]) => {
10525
+ const normalized = toFieldValue(value);
10526
+ return normalized === void 0 ? null : [key, normalized];
10527
+ }).filter((entry) => entry !== null);
10528
+ return Object.fromEntries(normalizedEntries);
10529
+ }
10530
+ async function rotateLogIfNeeded(logPath) {
10531
+ const stat = await import_promises20.default.stat(logPath).catch(() => null);
10532
+ if (!stat || stat.size < MAX_LOG_BYTES) {
10533
+ return;
10534
+ }
10535
+ const rotatedPath = `${logPath}.1`;
10536
+ await import_promises20.default.rm(rotatedPath, { force: true }).catch(() => void 0);
10537
+ await import_promises20.default.rename(logPath, rotatedPath).catch(() => void 0);
10538
+ }
10539
+ function summarizeText(value) {
10540
+ if (typeof value !== "string" || !value.trim()) {
10541
+ return {
10542
+ present: false,
10543
+ length: 0,
10544
+ sha256Prefix: null
10545
+ };
10546
+ }
10547
+ const trimmed = value.trim();
10548
+ return {
10549
+ present: true,
10550
+ length: trimmed.length,
10551
+ sha256Prefix: (0, import_node_crypto2.createHash)("sha256").update(trimmed).digest("hex").slice(0, 12)
10552
+ };
10553
+ }
10554
+ async function appendHookDiagnosticsEvent(params) {
10555
+ try {
10556
+ const logPath = getHookDiagnosticsLogPath();
10557
+ await import_promises20.default.mkdir(import_node_path8.default.dirname(logPath), { recursive: true });
10558
+ await rotateLogIfNeeded(logPath);
10559
+ const event = {
10560
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
10561
+ hook: params.hook,
10562
+ pluginVersion: pluginMetadata.version,
10563
+ pid: process.pid,
10564
+ sessionId: params.sessionId?.trim() || null,
10565
+ turnId: params.turnId?.trim() || null,
10566
+ stage: params.stage.trim(),
10567
+ result: params.result,
10568
+ reason: params.reason?.trim() || null,
10569
+ toolName: params.toolName?.trim() || null,
10570
+ repoRoot: params.repoRoot?.trim() || null,
10571
+ message: params.message?.trim() || null,
10572
+ fields: normalizeFields(params.fields)
10573
+ };
10574
+ await import_promises20.default.appendFile(logPath, `${JSON.stringify(event)}
10575
+ `, "utf8");
10576
+ } catch {
10577
+ }
10578
+ }
10579
+
10580
+ // src/auto-fix-dispatcher.ts
10581
+ var AUTO_FIX_COMMAND = {
10582
+ // Already auto-spawned by hook-user-prompt's branch-init path, but we
10583
+ // include it here too so a finalize-time failure (e.g. binding got
10584
+ // deleted between init and the next finalize) also self-heals.
10585
+ branch_binding_missing: ["collab", "init"],
10586
+ // External base diverged. Re-anchor declares the new local state as
10587
+ // truth. Risky if the user hard-reset to the wrong commit, but the
10588
+ // worst case is "Remix forgets the previous anchor" — recorded turns
10589
+ // are preserved on the server, so this is recoverable.
10590
+ re_anchor_required: ["collab", "re-anchor"],
10591
+ // Server moved ahead. `collab sync` is fast-forward-safe by default;
10592
+ // it refuses non-FF on its own, so we don't need to gate here.
10593
+ pull_required: ["collab", "sync"]
10594
+ };
10595
+ function isAutoFixableFinalizeFailureCode(code) {
10596
+ return Boolean(code && AUTO_FIX_COMMAND[code]);
10597
+ }
10598
+ var RECOMMENDED_USER_COMMAND = {
10599
+ not_bound: "remix collab init",
10600
+ branch_binding_missing: "remix collab init",
10601
+ family_ambiguous: "remix collab status",
10602
+ metadata_conflict: "remix collab status",
10603
+ branch_mismatch: "remix collab status",
10604
+ missing_head: "remix collab status",
10605
+ remote_error: "remix collab status",
10606
+ pull_required: "remix collab sync",
10607
+ re_anchor_required: "remix collab re-anchor"
10608
+ };
10609
+ var SPAWN_LOCK_REL = (cmdSlug) => import_node_path9.default.join(".remix", `.${cmdSlug}-spawning`);
10610
+ var SPAWN_LOG_REL = (cmdSlug) => import_node_path9.default.join(".remix", `${cmdSlug}.log`);
10611
+ var SPAWN_THROTTLE_MS = 5 * 60 * 1e3;
10612
+ function commandSlug(args) {
10613
+ return args.join("-").replace(/[^a-zA-Z0-9_-]/g, "_");
10614
+ }
10615
+ function spawnFixDetached(repoRoot, args) {
10616
+ const slug = commandSlug(args);
10617
+ const command = `remix ${args.join(" ")}`;
10618
+ const remixDir = import_node_path9.default.join(repoRoot, ".remix");
10619
+ const lockPath = import_node_path9.default.join(repoRoot, SPAWN_LOCK_REL(slug));
10620
+ const logPath = import_node_path9.default.join(repoRoot, SPAWN_LOG_REL(slug));
10621
+ try {
10622
+ if ((0, import_node_fs6.existsSync)(lockPath)) {
10623
+ const ageMs = Date.now() - (0, import_node_fs6.statSync)(lockPath).mtimeMs;
10624
+ if (ageMs < SPAWN_THROTTLE_MS) {
10625
+ return { kind: "spawn_throttled", command, reason: "spawn_lock_held" };
10626
+ }
10627
+ }
10628
+ } catch {
10629
+ }
10630
+ try {
10631
+ (0, import_node_fs6.mkdirSync)(remixDir, { recursive: true });
10632
+ } catch {
10633
+ }
10634
+ let out;
10635
+ let err;
10636
+ try {
10637
+ out = (0, import_node_fs6.openSync)(logPath, "a");
10638
+ err = (0, import_node_fs6.openSync)(logPath, "a");
10639
+ } catch (logErr) {
10640
+ return {
10641
+ kind: "spawn_failed",
10642
+ command,
10643
+ reason: "log_open_failed",
10644
+ message: logErr instanceof Error ? logErr.message : String(logErr)
10645
+ };
10646
+ }
10647
+ try {
10648
+ const child = (0, import_node_child_process6.spawn)("remix", [...args], {
10649
+ cwd: repoRoot,
10650
+ detached: true,
10651
+ stdio: ["ignore", out, err],
10652
+ env: { ...process.env, REMIX_AUTO_FIX_SPAWN: "1" }
10653
+ });
10654
+ child.unref();
10655
+ try {
10656
+ (0, import_node_fs6.writeFileSync)(lockPath, String(child.pid ?? ""), "utf8");
10657
+ (0, import_node_fs6.utimesSync)(lockPath, /* @__PURE__ */ new Date(), /* @__PURE__ */ new Date());
10658
+ } catch {
10659
+ }
10660
+ return { kind: "spawned", command, pid: child.pid, logPath };
10661
+ } catch (spawnErr) {
10662
+ return {
10663
+ kind: "spawn_failed",
10664
+ command,
10665
+ reason: "spawn_failed",
10666
+ message: spawnErr instanceof Error ? spawnErr.message : String(spawnErr)
10667
+ };
10668
+ }
10669
+ }
10670
+ async function dispatchFinalizeFailure(input) {
10671
+ const recommendedCommand = input.preflightCode ? RECOMMENDED_USER_COMMAND[input.preflightCode] ?? null : null;
10672
+ const marker = buildFreshFailureMarker({
10673
+ repoRoot: input.repoRoot,
10674
+ preflightCode: input.preflightCode,
10675
+ message: input.message,
10676
+ hint: input.hint,
10677
+ recommendedCommand
10678
+ });
10679
+ let outcome;
10680
+ const autoFixArgs = input.preflightCode ? AUTO_FIX_COMMAND[input.preflightCode] : void 0;
10681
+ if (!autoFixArgs) {
10682
+ outcome = {
10683
+ kind: "warn_only",
10684
+ reason: input.preflightCode ? "no_auto_fix_for_code" : "unknown_code"
10685
+ };
10686
+ } else {
10687
+ outcome = spawnFixDetached(input.repoRoot, autoFixArgs);
10688
+ marker.autoFix = mergeOutcomeIntoMarker(marker.autoFix, outcome);
10689
+ }
10690
+ try {
10691
+ await writeFinalizeFailureMarker(marker);
10692
+ } catch (writeErr) {
10693
+ await appendHookDiagnosticsEvent({
10694
+ hook: input.hook,
10695
+ sessionId: input.sessionId,
10696
+ turnId: input.turnId ?? void 0,
10697
+ stage: "finalize_failure_marker_write_failed",
10698
+ result: "error",
10699
+ reason: "exception",
10700
+ repoRoot: input.repoRoot,
10701
+ message: writeErr instanceof Error ? writeErr.message : String(writeErr)
10702
+ });
10703
+ }
10704
+ await appendHookDiagnosticsEvent({
10705
+ hook: input.hook,
10706
+ sessionId: input.sessionId,
10707
+ turnId: input.turnId ?? void 0,
10708
+ stage: "auto_fix_dispatched",
10709
+ result: outcome.kind === "spawned" ? "success" : outcome.kind === "warn_only" ? "info" : "error",
10710
+ reason: outcome.kind,
10711
+ repoRoot: input.repoRoot,
10712
+ fields: {
10713
+ preflightCode: input.preflightCode,
10714
+ command: "command" in outcome ? outcome.command : null,
10715
+ pid: outcome.kind === "spawned" ? outcome.pid ?? null : null,
10716
+ logPath: outcome.kind === "spawned" ? outcome.logPath : null,
10717
+ recommendedCommand
10718
+ },
10719
+ message: outcome.kind === "spawn_failed" ? outcome.message : null
10720
+ });
10721
+ return outcome;
10722
+ }
10723
+ function mergeOutcomeIntoMarker(existing, outcome) {
10724
+ if (outcome.kind === "spawned") {
10725
+ return {
10726
+ status: "in_progress",
10727
+ command: outcome.command,
10728
+ pid: outcome.pid ?? null,
10729
+ logPath: outcome.logPath,
10730
+ attemptedAt: (/* @__PURE__ */ new Date()).toISOString(),
10731
+ failureMessage: null
10732
+ };
10733
+ }
10734
+ if (outcome.kind === "spawn_throttled") {
10735
+ return {
10736
+ status: "in_progress",
10737
+ command: outcome.command,
10738
+ pid: existing.pid,
10739
+ logPath: existing.logPath,
10740
+ attemptedAt: existing.attemptedAt,
10741
+ failureMessage: null
10742
+ };
10743
+ }
10744
+ if (outcome.kind === "spawn_failed") {
10745
+ return {
10746
+ status: "spawn_failed",
10747
+ command: outcome.command,
10748
+ pid: null,
10749
+ logPath: null,
10750
+ attemptedAt: (/* @__PURE__ */ new Date()).toISOString(),
10751
+ failureMessage: outcome.message
10752
+ };
10753
+ }
10754
+ return existing;
10755
+ }
10756
+
10757
+ // src/deferred-turn-queue.ts
10758
+ var import_promises21 = __toESM(require("fs/promises"), 1);
10759
+ var import_node_os6 = __toESM(require("os"), 1);
10760
+ var import_node_path10 = __toESM(require("path"), 1);
10761
+ var DEFERRED_TURN_SCHEMA_VERSION = 1;
10762
+ var DEFERRED_TURN_TTL_MS = 24 * 60 * 60 * 1e3;
10763
+ var DEFERRED_TURN_DIR = "deferred-turns";
10764
+ function stateRoot2() {
10765
+ const configured = process.env.REMIX_CLAUDE_PLUGIN_HOOK_STATE_ROOT?.trim();
10766
+ return configured || import_node_path10.default.join(import_node_os6.default.tmpdir(), "remix-claude-plugin-hooks");
10767
+ }
10768
+ function getDeferredTurnDirPath() {
10769
+ return import_node_path10.default.join(stateRoot2(), DEFERRED_TURN_DIR);
10770
+ }
10771
+ function deferredTurnFileName(sessionId, turnId) {
10772
+ const safe = (s) => s.replace(/[^A-Za-z0-9_-]/g, "_");
10773
+ return `${safe(sessionId)}-${safe(turnId)}.json`;
10774
+ }
10775
+ function getDeferredTurnFilePath(sessionId, turnId) {
10776
+ return import_node_path10.default.join(getDeferredTurnDirPath(), deferredTurnFileName(sessionId, turnId));
10777
+ }
10778
+ async function writeDeferredTurn(record) {
10779
+ if (record.schemaVersion !== DEFERRED_TURN_SCHEMA_VERSION) {
10780
+ throw new Error(`writeDeferredTurn: unsupported schemaVersion ${record.schemaVersion}`);
10781
+ }
10782
+ if (!record.prompt.trim() || !record.assistantResponse.trim()) {
10783
+ throw new Error("writeDeferredTurn: prompt and assistantResponse must be non-empty");
10784
+ }
10785
+ const dir = getDeferredTurnDirPath();
10786
+ await import_promises21.default.mkdir(dir, { recursive: true });
10787
+ const filePath = getDeferredTurnFilePath(record.sessionId, record.turnId);
10788
+ const tmpPath = `${filePath}.tmp-${process.pid}-${Date.now()}`;
10789
+ await import_promises21.default.writeFile(tmpPath, JSON.stringify(record), "utf8");
10790
+ await import_promises21.default.rename(tmpPath, filePath);
10791
+ return filePath;
10792
+ }
10793
+ async function readDeferredTurnFile(filePath) {
10794
+ const raw = await import_promises21.default.readFile(filePath, "utf8").catch(() => null);
10795
+ if (!raw) return null;
10796
+ let parsed;
10797
+ try {
10798
+ parsed = JSON.parse(raw);
10799
+ } catch {
10800
+ return null;
10801
+ }
10802
+ if (!parsed || typeof parsed !== "object") return null;
10803
+ const record = parsed;
10804
+ if (record.schemaVersion !== DEFERRED_TURN_SCHEMA_VERSION) return null;
10805
+ 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") {
10806
+ return null;
10807
+ }
10808
+ return {
10809
+ schemaVersion: DEFERRED_TURN_SCHEMA_VERSION,
10810
+ sessionId: record.sessionId,
10811
+ turnId: record.turnId,
10812
+ repoRoot: record.repoRoot,
10813
+ prompt: record.prompt,
10814
+ assistantResponse: record.assistantResponse,
10815
+ submittedAt: record.submittedAt,
10816
+ deferredAt: record.deferredAt,
10817
+ reason: record.reason,
10818
+ branchAtDefer: typeof record.branchAtDefer === "string" || record.branchAtDefer === null ? record.branchAtDefer : null
10819
+ };
10820
+ }
10821
+ async function listDeferredTurnsForRepo(repoRoot) {
10822
+ const dir = getDeferredTurnDirPath();
10823
+ const dirEntries = await import_promises21.default.readdir(dir, { withFileTypes: true }).catch(() => []);
10824
+ const entries = [];
10825
+ for (const entry of dirEntries) {
10826
+ if (!entry.isFile() || !entry.name.endsWith(".json")) continue;
10827
+ const filePath = import_node_path10.default.join(dir, entry.name);
10828
+ const record = await readDeferredTurnFile(filePath);
10829
+ if (!record) continue;
10830
+ if (record.repoRoot !== repoRoot) continue;
10831
+ entries.push({ filePath, record });
10832
+ }
10833
+ entries.sort((a2, b) => {
10834
+ const aMs = Date.parse(a2.record.submittedAt);
10835
+ const bMs = Date.parse(b.record.submittedAt);
10836
+ if (Number.isFinite(aMs) && Number.isFinite(bMs)) return aMs - bMs;
10837
+ return 0;
10838
+ });
10839
+ return entries;
10840
+ }
10841
+ async function deleteDeferredTurnFile(filePath) {
10842
+ await import_promises21.default.rm(filePath, { force: true }).catch(() => void 0);
10843
+ }
10844
+ async function pruneStaleDeferredTurns(maxAgeMs = DEFERRED_TURN_TTL_MS) {
10845
+ const dir = getDeferredTurnDirPath();
10846
+ const dirEntries = await import_promises21.default.readdir(dir, { withFileTypes: true }).catch(() => []);
10847
+ const pruned = [];
10848
+ const now = Date.now();
10849
+ for (const entry of dirEntries) {
10850
+ if (!entry.isFile() || !entry.name.endsWith(".json")) continue;
10851
+ const filePath = import_node_path10.default.join(dir, entry.name);
10852
+ const record = await readDeferredTurnFile(filePath);
10853
+ if (!record) {
10854
+ const stat = await import_promises21.default.stat(filePath).catch(() => null);
10855
+ if (stat && now - stat.mtimeMs > maxAgeMs) {
10856
+ await deleteDeferredTurnFile(filePath);
10857
+ pruned.push(filePath);
10858
+ }
10859
+ continue;
10860
+ }
10861
+ const deferredAtMs = Date.parse(record.deferredAt);
10862
+ if (!Number.isFinite(deferredAtMs)) continue;
10863
+ if (now - deferredAtMs > maxAgeMs) {
10864
+ await deleteDeferredTurnFile(filePath);
10865
+ pruned.push(filePath);
10866
+ }
10867
+ }
10868
+ return pruned;
10869
+ }
10870
+ function buildDeferredTurnRecord(params) {
10871
+ return {
10872
+ schemaVersion: DEFERRED_TURN_SCHEMA_VERSION,
10873
+ sessionId: params.sessionId,
10874
+ turnId: params.turnId,
10875
+ repoRoot: params.repoRoot,
10876
+ prompt: params.prompt,
10877
+ assistantResponse: params.assistantResponse,
10878
+ submittedAt: params.submittedAt,
10879
+ deferredAt: (/* @__PURE__ */ new Date()).toISOString(),
10880
+ reason: params.reason ?? "current_branch_unbound",
10881
+ branchAtDefer: params.branchAtDefer
10882
+ };
10883
+ }
10884
+
10885
+ // src/deferred-turn-drainer.ts
10886
+ var import_promises23 = __toESM(require("fs/promises"), 1);
10887
+ var import_node_path11 = __toESM(require("path"), 1);
10888
+ var import_node_crypto3 = require("crypto");
10889
+
10890
+ // node_modules/@remixhq/core/dist/chunk-US5SM7ZC.js
10891
+ async function readJsonSafe(res) {
10892
+ const ct = res.headers.get("content-type") ?? "";
10893
+ if (!ct.toLowerCase().includes("application/json")) return null;
10894
+ try {
10895
+ return await res.json();
10896
+ } catch {
10897
+ return null;
10898
+ }
10899
+ }
10900
+ function createApiClient(config, opts) {
10901
+ const apiKey = (opts?.apiKey ?? "").trim();
10902
+ const tokenProvider = opts?.tokenProvider;
10903
+ const CLIENT_KEY_HEADER = "x-comerge-api-key";
10904
+ async function request(path16, init) {
10905
+ if (!tokenProvider) {
10906
+ throw new RemixError("API client is missing a token provider.", {
10907
+ exitCode: 1,
10908
+ hint: "Configure auth before creating the Remix API client."
10909
+ });
10910
+ }
10911
+ const auth = await tokenProvider();
10912
+ const url = new URL(path16, config.apiUrl).toString();
10913
+ const doFetch = async (bearer) => fetch(url, {
10914
+ ...init,
10915
+ headers: {
10916
+ Accept: "application/json",
10917
+ "Content-Type": "application/json",
10918
+ ...init?.headers ?? {},
10919
+ Authorization: `Bearer ${bearer}`,
10920
+ ...apiKey ? { [CLIENT_KEY_HEADER]: apiKey } : {}
10921
+ }
10922
+ });
10923
+ let res = await doFetch(auth.token);
10924
+ if (res.status === 401 && !auth.fromEnv && auth.session?.refresh_token) {
10925
+ const refreshed = await tokenProvider({ forceRefresh: true });
10926
+ res = await doFetch(refreshed.token);
10927
+ }
10928
+ if (!res.ok) {
10929
+ const body = await readJsonSafe(res);
10930
+ const msg = (body && typeof body === "object" && body && "message" in body && typeof body.message === "string" ? body.message : null) ?? `Request failed (status ${res.status})`;
10931
+ throw new RemixError(msg, { exitCode: 1, hint: body ? JSON.stringify(body, null, 2) : null });
10932
+ }
10933
+ const json = await readJsonSafe(res);
10934
+ return json ?? null;
10935
+ }
10936
+ async function requestBinary(path16, init) {
10937
+ if (!tokenProvider) {
10938
+ throw new RemixError("API client is missing a token provider.", {
10939
+ exitCode: 1,
10940
+ hint: "Configure auth before creating the Remix API client."
10941
+ });
10942
+ }
10943
+ const auth = await tokenProvider();
10944
+ const url = new URL(path16, config.apiUrl).toString();
10945
+ const doFetch = async (bearer) => fetch(url, {
10946
+ ...init,
10947
+ headers: {
10948
+ Accept: "*/*",
10949
+ ...init?.headers ?? {},
10950
+ Authorization: `Bearer ${bearer}`,
10951
+ ...apiKey ? { [CLIENT_KEY_HEADER]: apiKey } : {}
10952
+ }
10953
+ });
10954
+ let res = await doFetch(auth.token);
10955
+ if (res.status === 401 && !auth.fromEnv && auth.session?.refresh_token) {
10956
+ const refreshed = await tokenProvider({ forceRefresh: true });
10957
+ res = await doFetch(refreshed.token);
10958
+ }
10959
+ if (!res.ok) {
10960
+ const body = await readJsonSafe(res);
10961
+ const msg = (body && typeof body === "object" && body && "message" in body && typeof body.message === "string" ? body.message : null) ?? `Request failed (status ${res.status})`;
10962
+ throw new RemixError(msg, { exitCode: 1, hint: body ? JSON.stringify(body, null, 2) : null });
10963
+ }
10964
+ const contentDisposition = res.headers.get("content-disposition") ?? "";
10965
+ const fileNameMatch = contentDisposition.match(/filename=\"([^\"]+)\"/i);
10966
+ return {
10967
+ data: Buffer.from(await res.arrayBuffer()),
10968
+ fileName: fileNameMatch?.[1] ?? null,
10969
+ contentType: res.headers.get("content-type")
10970
+ };
10971
+ }
10972
+ return {
10973
+ getMe: () => request("/v1/me", { method: "GET" }),
10974
+ listOrganizations: () => request("/v1/organizations", { method: "GET" }),
10975
+ getOrganization: (orgId) => request(`/v1/organizations/${encodeURIComponent(orgId)}`, { method: "GET" }),
10976
+ listProjects: (params) => {
10977
+ const qs = new URLSearchParams();
10978
+ if (params?.organizationId) qs.set("organizationId", params.organizationId);
10979
+ if (params?.clientAppId) qs.set("clientAppId", params.clientAppId);
10980
+ const suffix = qs.toString() ? `?${qs.toString()}` : "";
10981
+ return request(`/v1/projects${suffix}`, { method: "GET" });
10982
+ },
10983
+ getProject: (projectId) => request(`/v1/projects/${encodeURIComponent(projectId)}`, { method: "GET" }),
10984
+ resolveProjectBinding: (params) => {
10985
+ const qs = new URLSearchParams();
10986
+ if (params.repoFingerprint) qs.set("repoFingerprint", params.repoFingerprint);
10987
+ if (params.remoteUrl) qs.set("remoteUrl", params.remoteUrl);
10988
+ if (params.branchName) qs.set("branchName", params.branchName);
10989
+ return request(`/v1/projects/bindings/resolve?${qs.toString()}`, { method: "GET" });
10990
+ },
10991
+ resolveProjectLaneBinding: (params) => {
10992
+ const qs = new URLSearchParams();
10993
+ if (params.projectId) qs.set("projectId", params.projectId);
10994
+ if (params.repoFingerprint) qs.set("repoFingerprint", params.repoFingerprint);
10995
+ if (params.remoteUrl) qs.set("remoteUrl", params.remoteUrl);
10996
+ if (params.defaultBranch) qs.set("defaultBranch", params.defaultBranch);
10997
+ qs.set("branchName", params.branchName);
10998
+ return request(`/v1/projects/bindings/resolve-lane?${qs.toString()}`, { method: "GET" });
10999
+ },
11000
+ ensureProjectLaneBinding: (payload) => request("/v1/projects/bindings/ensure-lane", { method: "POST", body: JSON.stringify(payload) }),
11001
+ bootstrapFreshProjectLane: (payload) => request("/v1/projects/bindings/bootstrap-fresh-lane", { method: "POST", body: JSON.stringify(payload) }),
11002
+ autoEnableDeveloper: () => request("/v1/developer/auto-enable", { method: "POST" }),
11003
+ listClientApps: (params) => {
11004
+ const qs = params?.orgId ? `?orgId=${encodeURIComponent(params.orgId)}` : "";
11005
+ return request(`/v1/developer/client-apps${qs}`, { method: "GET" });
11006
+ },
11007
+ createClientApp: (payload) => request("/v1/developer/client-apps", { method: "POST", body: JSON.stringify(payload) }),
11008
+ createClientAppKey: (clientAppId, payload) => request(`/v1/developer/client-apps/${encodeURIComponent(clientAppId)}/keys`, {
11009
+ method: "POST",
11010
+ body: JSON.stringify(payload ?? {})
11011
+ }),
11012
+ listApps: (params) => {
11013
+ const qs = new URLSearchParams();
11014
+ if (params?.projectId) qs.set("projectId", params.projectId);
11015
+ if (params?.organizationId) qs.set("organizationId", params.organizationId);
11016
+ if (params?.ownership) qs.set("ownership", params.ownership);
11017
+ if (params?.accessScope) qs.set("accessScope", params.accessScope);
11018
+ if (params?.createdBy) qs.set("createdBy", params.createdBy);
11019
+ if (params?.forked) qs.set("forked", params.forked);
11020
+ if (typeof params?.limit === "number") qs.set("limit", String(params.limit));
11021
+ if (typeof params?.offset === "number") qs.set("offset", String(params.offset));
11022
+ const suffix = qs.toString() ? `?${qs.toString()}` : "";
11023
+ return request(`/v1/apps${suffix}`, { method: "GET" });
11024
+ },
11025
+ getApp: (appId) => request(`/v1/apps/${encodeURIComponent(appId)}`, { method: "GET" }),
11026
+ getAppContext: (appId) => request(`/v1/apps/${encodeURIComponent(appId)}/context`, { method: "GET" }),
11027
+ getAppOverview: (appId) => request(`/v1/apps/${encodeURIComponent(appId)}/overview`, { method: "GET" }),
11028
+ listAppTimeline: (appId, params) => {
11029
+ const qs = new URLSearchParams();
11030
+ if (typeof params?.limit === "number") qs.set("limit", String(params.limit));
11031
+ if (params?.cursor) qs.set("cursor", params.cursor);
11032
+ const suffix = qs.toString() ? `?${qs.toString()}` : "";
11033
+ return request(`/v1/apps/${encodeURIComponent(appId)}/timeline${suffix}`, { method: "GET" });
11034
+ },
11035
+ getAppTimelineEvent: (appId, eventId) => request(`/v1/apps/${encodeURIComponent(appId)}/timeline/${encodeURIComponent(eventId)}`, { method: "GET" }),
11036
+ listAppEditQueue: (appId, params) => {
11037
+ const qs = new URLSearchParams();
11038
+ if (typeof params?.limit === "number") qs.set("limit", String(params.limit));
11039
+ if (typeof params?.offset === "number") qs.set("offset", String(params.offset));
11040
+ const suffix = qs.toString() ? `?${qs.toString()}` : "";
11041
+ return request(`/v1/apps/${encodeURIComponent(appId)}/edit-queue${suffix}`, { method: "GET" });
11042
+ },
11043
+ listAppJobQueue: (appId, params) => {
11044
+ const qs = new URLSearchParams();
11045
+ if (typeof params?.limit === "number") qs.set("limit", String(params.limit));
11046
+ if (typeof params?.offset === "number") qs.set("offset", String(params.offset));
11047
+ for (const kind of params?.kind ?? []) qs.append("kind", kind);
11048
+ for (const status of params?.status ?? []) qs.append("status", status);
11049
+ const suffix = qs.toString() ? `?${qs.toString()}` : "";
11050
+ return request(`/v1/apps/${encodeURIComponent(appId)}/job-queue${suffix}`, { method: "GET" });
11051
+ },
11052
+ getMergeRequest: (mrId) => request(`/v1/merge-requests/${encodeURIComponent(mrId)}`, { method: "GET" }),
11053
+ presignImportUpload: (payload) => request("/v1/apps/import/upload/presign", { method: "POST", body: JSON.stringify(payload) }),
11054
+ importFromUpload: (payload) => request("/v1/apps/import/upload", { method: "POST", body: JSON.stringify(payload) }),
11055
+ presignImportUploadFirstParty: (payload) => request("/v1/apps/import/upload/presign/first-party", { method: "POST", body: JSON.stringify(payload) }),
11056
+ importFromUploadFirstParty: (payload) => request("/v1/apps/import/upload/first-party", { method: "POST", body: JSON.stringify(payload) }),
11057
+ importFromGithubFirstParty: (payload) => request("/v1/apps/import/github/first-party", { method: "POST", body: JSON.stringify(payload) }),
11058
+ forkApp: (appId, payload) => request(`/v1/apps/${encodeURIComponent(appId)}/fork`, { method: "POST", body: JSON.stringify(payload ?? {}) }),
11059
+ getAppHead: (appId) => request(`/v1/apps/${encodeURIComponent(appId)}/head`, { method: "GET" }),
11060
+ getAppDelta: (appId, payload) => request(`/v1/apps/${encodeURIComponent(appId)}/delta`, {
11061
+ method: "POST",
11062
+ body: JSON.stringify(payload)
11063
+ }),
11064
+ downloadAppBundle: (appId) => requestBinary(`/v1/apps/${encodeURIComponent(appId)}/download.bundle`, { method: "GET" }),
11065
+ createChangeStep: (appId, payload) => request(`/v1/apps/${encodeURIComponent(appId)}/change-steps`, {
11066
+ method: "POST",
11067
+ body: JSON.stringify(payload)
11068
+ }),
11069
+ createCollabTurn: (appId, payload) => request(`/v1/apps/${encodeURIComponent(appId)}/collab-turns`, {
11070
+ method: "POST",
11071
+ body: JSON.stringify(payload)
11072
+ }),
11073
+ attachCollabTurnUsage: (appId, payload) => request(`/v1/apps/${encodeURIComponent(appId)}/collab-turns/attach-usage`, {
11074
+ method: "POST",
11075
+ body: JSON.stringify(payload)
11076
+ }),
11077
+ listCollabTurns: (appId, params) => {
11078
+ const qs = new URLSearchParams();
11079
+ if (params?.limit !== void 0) qs.set("limit", String(params.limit));
11080
+ if (params?.offset !== void 0) qs.set("offset", String(params.offset));
11081
+ if (params?.changeStepId) qs.set("changeStepId", params.changeStepId);
11082
+ if (params?.threadId) qs.set("threadId", params.threadId);
11083
+ if (params?.collabLaneId) qs.set("collabLaneId", params.collabLaneId);
11084
+ if (params?.createdAfter) qs.set("createdAfter", params.createdAfter);
10230
11085
  if (params?.createdBefore) qs.set("createdBefore", params.createdBefore);
10231
11086
  const suffix = qs.toString() ? `?${qs.toString()}` : "";
10232
11087
  return request(`/v1/apps/${encodeURIComponent(appId)}/collab-turns${suffix}`, { method: "GET" });
@@ -10936,8 +11791,8 @@ function getErrorMap() {
10936
11791
 
10937
11792
  // node_modules/zod/v3/helpers/parseUtil.js
10938
11793
  var makeIssue = (params) => {
10939
- const { data, path: path13, errorMaps, issueData } = params;
10940
- const fullPath = [...path13, ...issueData.path || []];
11794
+ const { data, path: path16, errorMaps, issueData } = params;
11795
+ const fullPath = [...path16, ...issueData.path || []];
10941
11796
  const fullIssue = {
10942
11797
  ...issueData,
10943
11798
  path: fullPath
@@ -11053,11 +11908,11 @@ var errorUtil;
11053
11908
 
11054
11909
  // node_modules/zod/v3/types.js
11055
11910
  var ParseInputLazyPath = class {
11056
- constructor(parent, value, path13, key) {
11911
+ constructor(parent, value, path16, key) {
11057
11912
  this._cachedPath = [];
11058
11913
  this.parent = parent;
11059
11914
  this.data = value;
11060
- this._path = path13;
11915
+ this._path = path16;
11061
11916
  this._key = key;
11062
11917
  }
11063
11918
  get path() {
@@ -14500,7 +15355,7 @@ var coerce = {
14500
15355
  var NEVER = INVALID;
14501
15356
 
14502
15357
  // node_modules/@remixhq/core/dist/chunk-P6JHXOV4.js
14503
- var import_promises18 = __toESM(require("fs/promises"), 1);
15358
+ var import_promises22 = __toESM(require("fs/promises"), 1);
14504
15359
  var import_os3 = __toESM(require("os"), 1);
14505
15360
  var import_path7 = __toESM(require("path"), 1);
14506
15361
 
@@ -14908,7 +15763,7 @@ var PostgrestError = class extends Error {
14908
15763
  };
14909
15764
  }
14910
15765
  };
14911
- function sleep2(ms, signal) {
15766
+ function sleep3(ms, signal) {
14912
15767
  return new Promise((resolve) => {
14913
15768
  if (signal === null || signal === void 0 ? void 0 : signal.aborted) {
14914
15769
  resolve();
@@ -15104,7 +15959,7 @@ var PostgrestBuilder = class {
15104
15959
  if (_this.retryEnabled && attemptCount < DEFAULT_MAX_RETRIES) {
15105
15960
  const delay = getRetryDelay(attemptCount);
15106
15961
  attemptCount++;
15107
- await sleep2(delay, _this.signal);
15962
+ await sleep3(delay, _this.signal);
15108
15963
  continue;
15109
15964
  }
15110
15965
  throw fetchError;
@@ -15115,7 +15970,7 @@ var PostgrestBuilder = class {
15115
15970
  const delay = retryAfterHeader !== null ? Math.max(0, parseInt(retryAfterHeader, 10) || 0) * 1e3 : getRetryDelay(attemptCount);
15116
15971
  await res$1.text();
15117
15972
  attemptCount++;
15118
- await sleep2(delay, _this.signal);
15973
+ await sleep3(delay, _this.signal);
15119
15974
  continue;
15120
15975
  }
15121
15976
  return await _this.processResponse(res$1);
@@ -23605,8 +24460,8 @@ var IcebergError = class extends Error {
23605
24460
  return this.status === 419;
23606
24461
  }
23607
24462
  };
23608
- function buildUrl(baseUrl, path13, query) {
23609
- const url = new URL(path13, baseUrl);
24463
+ function buildUrl(baseUrl, path16, query) {
24464
+ const url = new URL(path16, baseUrl);
23610
24465
  if (query) {
23611
24466
  for (const [key, value] of Object.entries(query)) {
23612
24467
  if (value !== void 0) {
@@ -23636,12 +24491,12 @@ function createFetchClient(options) {
23636
24491
  return {
23637
24492
  async request({
23638
24493
  method,
23639
- path: path13,
24494
+ path: path16,
23640
24495
  query,
23641
24496
  body,
23642
24497
  headers
23643
24498
  }) {
23644
- const url = buildUrl(options.baseUrl, path13, query);
24499
+ const url = buildUrl(options.baseUrl, path16, query);
23645
24500
  const authHeaders = await buildAuthHeaders(options.auth);
23646
24501
  const res = await fetchFn(url, {
23647
24502
  method,
@@ -24479,7 +25334,7 @@ var StorageFileApi = class extends BaseApiClient {
24479
25334
  * @param path The relative file path. Should be of the format `folder/subfolder/filename.png`. The bucket must already exist before attempting to upload.
24480
25335
  * @param fileBody The body of the file to be stored in the bucket.
24481
25336
  */
24482
- async uploadOrUpdate(method, path13, fileBody, fileOptions) {
25337
+ async uploadOrUpdate(method, path16, fileBody, fileOptions) {
24483
25338
  var _this = this;
24484
25339
  return _this.handleOperation(async () => {
24485
25340
  let body;
@@ -24503,7 +25358,7 @@ var StorageFileApi = class extends BaseApiClient {
24503
25358
  if ((typeof ReadableStream !== "undefined" && body instanceof ReadableStream || body && typeof body === "object" && "pipe" in body && typeof body.pipe === "function") && !options.duplex) options.duplex = "half";
24504
25359
  }
24505
25360
  if (fileOptions === null || fileOptions === void 0 ? void 0 : fileOptions.headers) for (const [key, value] of Object.entries(fileOptions.headers)) headers = setHeader(headers, key, value);
24506
- const cleanPath = _this._removeEmptyFolders(path13);
25361
+ const cleanPath = _this._removeEmptyFolders(path16);
24507
25362
  const _path = _this._getFinalPath(cleanPath);
24508
25363
  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 } : {}));
24509
25364
  return {
@@ -24564,8 +25419,8 @@ var StorageFileApi = class extends BaseApiClient {
24564
25419
  * - Refer to the [Storage guide](/docs/guides/storage/security/access-control) on how access control works
24565
25420
  * - 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.
24566
25421
  */
24567
- async upload(path13, fileBody, fileOptions) {
24568
- return this.uploadOrUpdate("POST", path13, fileBody, fileOptions);
25422
+ async upload(path16, fileBody, fileOptions) {
25423
+ return this.uploadOrUpdate("POST", path16, fileBody, fileOptions);
24569
25424
  }
24570
25425
  /**
24571
25426
  * Upload a file with a token generated from `createSignedUploadUrl`.
@@ -24604,9 +25459,9 @@ var StorageFileApi = class extends BaseApiClient {
24604
25459
  * - `objects` table permissions: none
24605
25460
  * - Refer to the [Storage guide](/docs/guides/storage/security/access-control) on how access control works
24606
25461
  */
24607
- async uploadToSignedUrl(path13, token, fileBody, fileOptions) {
25462
+ async uploadToSignedUrl(path16, token, fileBody, fileOptions) {
24608
25463
  var _this3 = this;
24609
- const cleanPath = _this3._removeEmptyFolders(path13);
25464
+ const cleanPath = _this3._removeEmptyFolders(path16);
24610
25465
  const _path = _this3._getFinalPath(cleanPath);
24611
25466
  const url = new URL(_this3.url + `/object/upload/sign/${_path}`);
24612
25467
  url.searchParams.set("token", token);
@@ -24668,10 +25523,10 @@ var StorageFileApi = class extends BaseApiClient {
24668
25523
  * - `objects` table permissions: `insert`
24669
25524
  * - Refer to the [Storage guide](/docs/guides/storage/security/access-control) on how access control works
24670
25525
  */
24671
- async createSignedUploadUrl(path13, options) {
25526
+ async createSignedUploadUrl(path16, options) {
24672
25527
  var _this4 = this;
24673
25528
  return _this4.handleOperation(async () => {
24674
- let _path = _this4._getFinalPath(path13);
25529
+ let _path = _this4._getFinalPath(path16);
24675
25530
  const headers = _objectSpread22({}, _this4.headers);
24676
25531
  if (options === null || options === void 0 ? void 0 : options.upsert) headers["x-upsert"] = "true";
24677
25532
  const data = await post(_this4.fetch, `${_this4.url}/object/upload/sign/${_path}`, {}, { headers });
@@ -24680,7 +25535,7 @@ var StorageFileApi = class extends BaseApiClient {
24680
25535
  if (!token) throw new StorageError("No token returned by API");
24681
25536
  return {
24682
25537
  signedUrl: url.toString(),
24683
- path: path13,
25538
+ path: path16,
24684
25539
  token
24685
25540
  };
24686
25541
  });
@@ -24736,8 +25591,8 @@ var StorageFileApi = class extends BaseApiClient {
24736
25591
  * - Refer to the [Storage guide](/docs/guides/storage/security/access-control) on how access control works
24737
25592
  * - 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.
24738
25593
  */
24739
- async update(path13, fileBody, fileOptions) {
24740
- return this.uploadOrUpdate("PUT", path13, fileBody, fileOptions);
25594
+ async update(path16, fileBody, fileOptions) {
25595
+ return this.uploadOrUpdate("PUT", path16, fileBody, fileOptions);
24741
25596
  }
24742
25597
  /**
24743
25598
  * Moves an existing file to a new path in the same bucket.
@@ -24885,10 +25740,10 @@ var StorageFileApi = class extends BaseApiClient {
24885
25740
  * - `objects` table permissions: `select`
24886
25741
  * - Refer to the [Storage guide](/docs/guides/storage/security/access-control) on how access control works
24887
25742
  */
24888
- async createSignedUrl(path13, expiresIn, options) {
25743
+ async createSignedUrl(path16, expiresIn, options) {
24889
25744
  var _this8 = this;
24890
25745
  return _this8.handleOperation(async () => {
24891
- let _path = _this8._getFinalPath(path13);
25746
+ let _path = _this8._getFinalPath(path16);
24892
25747
  const hasTransform = typeof (options === null || options === void 0 ? void 0 : options.transform) === "object" && options.transform !== null && Object.keys(options.transform).length > 0;
24893
25748
  let data = await post(_this8.fetch, `${_this8.url}/object/sign/${_path}`, _objectSpread22({ expiresIn }, hasTransform ? { transform: options.transform } : {}), { headers: _this8.headers });
24894
25749
  const query = new URLSearchParams();
@@ -25022,13 +25877,13 @@ var StorageFileApi = class extends BaseApiClient {
25022
25877
  * - `objects` table permissions: `select`
25023
25878
  * - Refer to the [Storage guide](/docs/guides/storage/security/access-control) on how access control works
25024
25879
  */
25025
- download(path13, options, parameters) {
25880
+ download(path16, options, parameters) {
25026
25881
  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";
25027
25882
  const query = new URLSearchParams();
25028
25883
  if (options === null || options === void 0 ? void 0 : options.transform) this.applyTransformOptsToQuery(query, options.transform);
25029
25884
  if ((options === null || options === void 0 ? void 0 : options.cacheNonce) != null) query.set("cacheNonce", String(options.cacheNonce));
25030
25885
  const queryString = query.toString();
25031
- const _path = this._getFinalPath(path13);
25886
+ const _path = this._getFinalPath(path16);
25032
25887
  const downloadFn = () => get(this.fetch, `${this.url}/${renderPath}/${_path}${queryString ? `?${queryString}` : ""}`, {
25033
25888
  headers: this.headers,
25034
25889
  noResolveJson: true
@@ -25058,9 +25913,9 @@ var StorageFileApi = class extends BaseApiClient {
25058
25913
  * }
25059
25914
  * ```
25060
25915
  */
25061
- async info(path13) {
25916
+ async info(path16) {
25062
25917
  var _this10 = this;
25063
- const _path = _this10._getFinalPath(path13);
25918
+ const _path = _this10._getFinalPath(path16);
25064
25919
  return _this10.handleOperation(async () => {
25065
25920
  return recursiveToCamel(await get(_this10.fetch, `${_this10.url}/object/info/${_path}`, { headers: _this10.headers }));
25066
25921
  });
@@ -25080,9 +25935,9 @@ var StorageFileApi = class extends BaseApiClient {
25080
25935
  * .exists('folder/avatar1.png')
25081
25936
  * ```
25082
25937
  */
25083
- async exists(path13) {
25938
+ async exists(path16) {
25084
25939
  var _this11 = this;
25085
- const _path = _this11._getFinalPath(path13);
25940
+ const _path = _this11._getFinalPath(path16);
25086
25941
  try {
25087
25942
  await head(_this11.fetch, `${_this11.url}/object/${_path}`, { headers: _this11.headers });
25088
25943
  return {
@@ -25160,8 +26015,8 @@ var StorageFileApi = class extends BaseApiClient {
25160
26015
  * - `objects` table permissions: none
25161
26016
  * - Refer to the [Storage guide](/docs/guides/storage/security/access-control) on how access control works
25162
26017
  */
25163
- getPublicUrl(path13, options) {
25164
- const _path = this._getFinalPath(path13);
26018
+ getPublicUrl(path16, options) {
26019
+ const _path = this._getFinalPath(path16);
25165
26020
  const query = new URLSearchParams();
25166
26021
  if (options === null || options === void 0 ? void 0 : options.download) query.set("download", options.download === true ? "" : options.download);
25167
26022
  if (options === null || options === void 0 ? void 0 : options.transform) this.applyTransformOptsToQuery(query, options.transform);
@@ -25298,10 +26153,10 @@ var StorageFileApi = class extends BaseApiClient {
25298
26153
  * - `objects` table permissions: `select`
25299
26154
  * - Refer to the [Storage guide](/docs/guides/storage/security/access-control) on how access control works
25300
26155
  */
25301
- async list(path13, options, parameters) {
26156
+ async list(path16, options, parameters) {
25302
26157
  var _this13 = this;
25303
26158
  return _this13.handleOperation(async () => {
25304
- const body = _objectSpread22(_objectSpread22(_objectSpread22({}, DEFAULT_SEARCH_OPTIONS), options), {}, { prefix: path13 || "" });
26159
+ const body = _objectSpread22(_objectSpread22(_objectSpread22({}, DEFAULT_SEARCH_OPTIONS), options), {}, { prefix: path16 || "" });
25305
26160
  return await post(_this13.fetch, `${_this13.url}/object/list/${_this13.bucketId}`, body, { headers: _this13.headers }, parameters);
25306
26161
  });
25307
26162
  }
@@ -25365,11 +26220,11 @@ var StorageFileApi = class extends BaseApiClient {
25365
26220
  if (typeof Buffer !== "undefined") return Buffer.from(data).toString("base64");
25366
26221
  return btoa(data);
25367
26222
  }
25368
- _getFinalPath(path13) {
25369
- return `${this.bucketId}/${path13.replace(/^\/+/, "")}`;
26223
+ _getFinalPath(path16) {
26224
+ return `${this.bucketId}/${path16.replace(/^\/+/, "")}`;
25370
26225
  }
25371
- _removeEmptyFolders(path13) {
25372
- return path13.replace(/^\/|\/$/g, "").replace(/\/+/g, "/");
26226
+ _removeEmptyFolders(path16) {
26227
+ return path16.replace(/^\/|\/$/g, "").replace(/\/+/g, "/");
25373
26228
  }
25374
26229
  /** Modifies the `query`, appending values the from `transform` */
25375
26230
  applyTransformOptsToQuery(query, transform) {
@@ -27112,7 +27967,7 @@ function decodeJWT(token) {
27112
27967
  };
27113
27968
  return data;
27114
27969
  }
27115
- async function sleep3(time) {
27970
+ async function sleep4(time) {
27116
27971
  return await new Promise((accept) => {
27117
27972
  setTimeout(() => accept(null), time);
27118
27973
  });
@@ -32905,7 +33760,7 @@ var GoTrueClient = class _GoTrueClient {
32905
33760
  const startedAt = Date.now();
32906
33761
  return await retryable(async (attempt) => {
32907
33762
  if (attempt > 0) {
32908
- await sleep3(200 * Math.pow(2, attempt - 1));
33763
+ await sleep4(200 * Math.pow(2, attempt - 1));
32909
33764
  }
32910
33765
  this._debug(debugName, "refreshing attempt", attempt);
32911
33766
  return await _request(this.fetch, "POST", `${this.url}/token?grant_type=refresh_token`, {
@@ -34477,24 +35332,24 @@ async function maybeLoadKeytar() {
34477
35332
  }
34478
35333
  async function ensurePathPermissions(filePath) {
34479
35334
  const dir = import_path7.default.dirname(filePath);
34480
- await import_promises18.default.mkdir(dir, { recursive: true });
35335
+ await import_promises22.default.mkdir(dir, { recursive: true });
34481
35336
  try {
34482
- await import_promises18.default.chmod(dir, 448);
35337
+ await import_promises22.default.chmod(dir, 448);
34483
35338
  } catch {
34484
35339
  }
34485
35340
  try {
34486
- await import_promises18.default.chmod(filePath, 384);
35341
+ await import_promises22.default.chmod(filePath, 384);
34487
35342
  } catch {
34488
35343
  }
34489
35344
  }
34490
- async function writeJsonAtomic2(filePath, value) {
34491
- await import_promises18.default.mkdir(import_path7.default.dirname(filePath), { recursive: true });
35345
+ async function writeJsonAtomic3(filePath, value) {
35346
+ await import_promises22.default.mkdir(import_path7.default.dirname(filePath), { recursive: true });
34492
35347
  const tmpPath = `${filePath}.tmp-${Date.now()}-${Math.random().toString(16).slice(2)}`;
34493
- await import_promises18.default.writeFile(tmpPath, JSON.stringify(value, null, 2) + "\n", "utf8");
34494
- await import_promises18.default.rename(tmpPath, filePath);
35348
+ await import_promises22.default.writeFile(tmpPath, JSON.stringify(value, null, 2) + "\n", "utf8");
35349
+ await import_promises22.default.rename(tmpPath, filePath);
34495
35350
  }
34496
35351
  async function writeSessionFileFallback(filePath, session) {
34497
- await writeJsonAtomic2(filePath, session);
35352
+ await writeJsonAtomic3(filePath, session);
34498
35353
  await ensurePathPermissions(filePath);
34499
35354
  }
34500
35355
  function createLocalSessionStore(params) {
@@ -34514,7 +35369,7 @@ function createLocalSessionStore(params) {
34514
35369
  }
34515
35370
  }
34516
35371
  async function readFile() {
34517
- const raw = await import_promises18.default.readFile(filePath, "utf8").catch(() => null);
35372
+ const raw = await import_promises22.default.readFile(filePath, "utf8").catch(() => null);
34518
35373
  if (!raw) return null;
34519
35374
  try {
34520
35375
  const parsed = storedSessionSchema.safeParse(JSON.parse(raw));
@@ -34712,514 +35567,376 @@ async function createHookCollabApiClient() {
34712
35567
  });
34713
35568
  }
34714
35569
 
34715
- // src/hook-diagnostics.ts
34716
- var import_node_crypto2 = require("crypto");
34717
- var import_promises20 = __toESM(require("fs/promises"), 1);
34718
- var import_node_os5 = __toESM(require("os"), 1);
34719
- var import_node_path7 = __toESM(require("path"), 1);
34720
-
34721
- // src/hook-state.ts
34722
- var import_promises19 = __toESM(require("fs/promises"), 1);
34723
- var import_node_os4 = __toESM(require("os"), 1);
34724
- var import_node_path6 = __toESM(require("path"), 1);
34725
- var import_node_crypto = require("crypto");
34726
- function stateRoot() {
34727
- const configured = process.env.REMIX_CLAUDE_PLUGIN_HOOK_STATE_ROOT?.trim();
34728
- return configured || import_node_path6.default.join(import_node_os4.default.tmpdir(), "remix-claude-plugin-hooks");
34729
- }
34730
- function statePath(sessionId) {
34731
- return import_node_path6.default.join(stateRoot(), `${sessionId}.json`);
34732
- }
34733
- function stateLockPath(sessionId) {
34734
- return import_node_path6.default.join(stateRoot(), `${sessionId}.lock`);
34735
- }
34736
- function stateLockMetaPath(sessionId) {
34737
- return import_node_path6.default.join(stateLockPath(sessionId), "owner.json");
35570
+ // src/deferred-turn-drainer.ts
35571
+ var collabFinalizeTurn2 = collabFinalizeTurn;
35572
+ var drainPendingFinalizeQueue2 = drainPendingFinalizeQueue;
35573
+ var HOOK_ACTOR = {
35574
+ type: "agent",
35575
+ name: "claude-code",
35576
+ version: pluginMetadata.version,
35577
+ provider: "anthropic"
35578
+ };
35579
+ var DEFERRED_TURN_DRAIN_POLL_INTERVAL_MS = 3e3;
35580
+ var DEFERRED_TURN_DRAIN_MAX_WAIT_MS = 15 * 60 * 1e3;
35581
+ var DEFERRED_TURN_DRAIN_LOCK_HEARTBEAT_MS = 3e4;
35582
+ var DEFERRED_TURN_DRAIN_LOCK_STALE_MS = 9e4;
35583
+ function isPidAlive(pid) {
35584
+ if (!Number.isFinite(pid) || pid <= 0) return false;
35585
+ try {
35586
+ process.kill(pid, 0);
35587
+ return true;
35588
+ } catch {
35589
+ return false;
35590
+ }
34738
35591
  }
34739
- async function writeJsonAtomic3(filePath, value) {
34740
- await import_promises19.default.mkdir(import_node_path6.default.dirname(filePath), { recursive: true });
34741
- const tmpPath = `${filePath}.tmp-${Date.now()}-${Math.random().toString(16).slice(2)}`;
34742
- await import_promises19.default.writeFile(tmpPath, JSON.stringify(value, null, 2) + "\n", "utf8");
34743
- await import_promises19.default.rename(tmpPath, filePath);
35592
+ function repoLockFileName(repoRoot) {
35593
+ const hash = (0, import_node_crypto3.createHash)("sha256").update(repoRoot).digest("hex").slice(0, 16);
35594
+ return `.drainer-${hash}.lock`;
34744
35595
  }
34745
- var STATE_LOCK_WAIT_MS = 2e3;
34746
- var STATE_LOCK_POLL_MS = 25;
34747
- var STATE_LOCK_STALE_MS = 3e4;
34748
- var STATE_LOCK_HEARTBEAT_MS = 5e3;
34749
- async function sleep4(ms) {
34750
- await new Promise((resolve) => setTimeout(resolve, ms));
35596
+ function repoLockPath(repoRoot) {
35597
+ return import_node_path11.default.join(getDeferredTurnDirPath(), repoLockFileName(repoRoot));
34751
35598
  }
34752
- async function readStateLockMetadata(sessionId) {
34753
- const raw = await import_promises19.default.readFile(stateLockMetaPath(sessionId), "utf8").catch(() => null);
35599
+ async function readDrainLockMetadata(lockPath) {
35600
+ const raw = await import_promises23.default.readFile(lockPath, "utf8").catch(() => null);
34754
35601
  if (!raw) return null;
34755
35602
  try {
34756
35603
  const parsed = JSON.parse(raw);
34757
- if (typeof parsed.ownerId !== "string" || typeof parsed.pid !== "number" || typeof parsed.createdAt !== "string" || typeof parsed.heartbeatAt !== "string") {
35604
+ if (typeof parsed.pid !== "number" || typeof parsed.repoRoot !== "string" || typeof parsed.startedAt !== "string") {
34758
35605
  return null;
34759
35606
  }
34760
- return {
34761
- ownerId: parsed.ownerId,
34762
- pid: parsed.pid,
34763
- createdAt: parsed.createdAt,
34764
- heartbeatAt: parsed.heartbeatAt
34765
- };
35607
+ return { pid: parsed.pid, repoRoot: parsed.repoRoot, startedAt: parsed.startedAt };
34766
35608
  } catch {
34767
35609
  return null;
34768
35610
  }
34769
35611
  }
34770
- async function writeStateLockMetadata(sessionId, metadata) {
34771
- await writeJsonAtomic3(stateLockMetaPath(sessionId), metadata);
35612
+ async function writeDrainLockMetadata(lockPath, metadata) {
35613
+ const tmpPath = `${lockPath}.tmp-${process.pid}-${Date.now()}`;
35614
+ await import_promises23.default.writeFile(tmpPath, JSON.stringify(metadata), "utf8");
35615
+ await import_promises23.default.rename(tmpPath, lockPath);
34772
35616
  }
34773
- async function tryRemoveStaleStateLock(sessionId) {
34774
- const lockPath = stateLockPath(sessionId);
34775
- const metadata = await readStateLockMetadata(sessionId);
34776
- const staleByHeartbeat = metadata && Date.now() - new Date(metadata.heartbeatAt).getTime() > STATE_LOCK_STALE_MS;
34777
- if (staleByHeartbeat) {
34778
- await import_promises19.default.rm(lockPath, { recursive: true, force: true }).catch(() => void 0);
34779
- return true;
34780
- }
34781
- if (!metadata) {
34782
- const lockStat = await import_promises19.default.stat(lockPath).catch(() => null);
34783
- if (lockStat && Date.now() - lockStat.mtimeMs > STATE_LOCK_STALE_MS) {
34784
- await import_promises19.default.rm(lockPath, { recursive: true, force: true }).catch(() => void 0);
34785
- return true;
34786
- }
34787
- }
34788
- return false;
34789
- }
34790
- async function acquireStateLock(sessionId) {
34791
- const lockPath = stateLockPath(sessionId);
34792
- const deadline = Date.now() + STATE_LOCK_WAIT_MS;
34793
- await import_promises19.default.mkdir(stateRoot(), { recursive: true });
34794
- while (true) {
34795
- try {
34796
- await import_promises19.default.mkdir(lockPath);
34797
- const ownerId = (0, import_node_crypto.randomUUID)();
34798
- const createdAt = (/* @__PURE__ */ new Date()).toISOString();
34799
- const metadata = {
34800
- ownerId,
34801
- pid: process.pid,
34802
- createdAt,
34803
- heartbeatAt: createdAt
34804
- };
34805
- await writeStateLockMetadata(sessionId, metadata);
34806
- let released = false;
34807
- const heartbeat = setInterval(() => {
34808
- if (released) return;
34809
- void writeStateLockMetadata(sessionId, {
34810
- ...metadata,
34811
- heartbeatAt: (/* @__PURE__ */ new Date()).toISOString()
34812
- }).catch(() => void 0);
34813
- }, STATE_LOCK_HEARTBEAT_MS);
34814
- heartbeat.unref?.();
34815
- return async () => {
34816
- if (released) return;
34817
- released = true;
34818
- clearInterval(heartbeat);
34819
- const currentMetadata = await readStateLockMetadata(sessionId);
34820
- if (currentMetadata?.ownerId === ownerId) {
34821
- await import_promises19.default.rm(lockPath, { recursive: true, force: true }).catch(() => void 0);
34822
- }
34823
- };
34824
- } catch (error) {
34825
- const code = error && typeof error === "object" && "code" in error ? error.code : null;
34826
- if (code !== "EEXIST") {
34827
- throw error;
34828
- }
34829
- if (await tryRemoveStaleStateLock(sessionId)) {
34830
- continue;
34831
- }
34832
- if (Date.now() >= deadline) {
34833
- throw new Error(`Timed out acquiring hook state lock for session ${sessionId}.`);
34834
- }
34835
- await sleep4(STATE_LOCK_POLL_MS);
35617
+ async function tryAcquireDrainLock(repoRoot) {
35618
+ const lockPath = repoLockPath(repoRoot);
35619
+ await import_promises23.default.mkdir(import_node_path11.default.dirname(lockPath), { recursive: true });
35620
+ const existingMeta = await readDrainLockMetadata(lockPath);
35621
+ if (existingMeta) {
35622
+ const lockStat = await import_promises23.default.stat(lockPath).catch(() => null);
35623
+ const ageMs = lockStat ? Date.now() - lockStat.mtimeMs : Number.POSITIVE_INFINITY;
35624
+ const fresh = ageMs <= DEFERRED_TURN_DRAIN_LOCK_STALE_MS;
35625
+ const alive = isPidAlive(existingMeta.pid);
35626
+ if (fresh && alive) {
35627
+ return { acquired: false, lockPath };
34836
35628
  }
34837
35629
  }
35630
+ await writeDrainLockMetadata(lockPath, {
35631
+ pid: process.pid,
35632
+ repoRoot,
35633
+ startedAt: (/* @__PURE__ */ new Date()).toISOString()
35634
+ });
35635
+ return { acquired: true, lockPath };
34838
35636
  }
34839
- async function withStateLock(sessionId, fn) {
34840
- const release = await acquireStateLock(sessionId);
34841
- try {
34842
- return await fn();
34843
- } finally {
34844
- await release();
34845
- }
35637
+ async function releaseDrainLock(lockPath) {
35638
+ const meta = await readDrainLockMetadata(lockPath);
35639
+ if (meta && meta.pid !== process.pid) return;
35640
+ await import_promises23.default.rm(lockPath, { force: true }).catch(() => void 0);
34846
35641
  }
34847
- function normalizeIntent(value) {
34848
- return value === "memory_first" || value === "collab_state" || value === "git_facts" ? value : "neutral";
35642
+ async function heartbeatDrainLock(lockPath) {
35643
+ const now = /* @__PURE__ */ new Date();
35644
+ await import_promises23.default.utimes(lockPath, now, now).catch(() => void 0);
34849
35645
  }
34850
- function normalizeString(value) {
34851
- return typeof value === "string" && value.trim() ? value.trim() : null;
35646
+ async function sleep5(ms) {
35647
+ await new Promise((resolve) => setTimeout(resolve, ms));
34852
35648
  }
34853
- function normalizeStringArray(value) {
34854
- if (!Array.isArray(value)) return [];
34855
- return Array.from(
34856
- new Set(
34857
- value.filter((entry) => typeof entry === "string" && entry.trim().length > 0).map((entry) => entry.trim())
34858
- )
34859
- );
35649
+ function buildIdempotencyKey(turnId, repoRoot) {
35650
+ return `${turnId}:${repoRoot}:finalize_turn`;
34860
35651
  }
34861
- function normalizeManualRecordingScope(value) {
34862
- if (value === "full_turn") {
34863
- return "full_turn";
35652
+ async function pushPendingFinalizeQueueToServer(params) {
35653
+ if (typeof drainPendingFinalizeQueue2 !== "function") {
35654
+ await appendHookDiagnosticsEvent({
35655
+ hook: "deferredTurnDrainer",
35656
+ sessionId: params.sessionMarker,
35657
+ stage: "finalize_queue_push_skipped",
35658
+ result: "info",
35659
+ reason: "drain_pending_finalize_queue_unavailable",
35660
+ repoRoot: params.repoRoot
35661
+ });
35662
+ return;
34864
35663
  }
34865
- return null;
34866
- }
34867
- function normalizeTouchedRepo(value, repoRoot) {
34868
- if (!value || typeof value !== "object") return null;
34869
- const parsed = value;
34870
- const normalizedRepoRoot = normalizeString(parsed.repoRoot) ?? repoRoot.trim();
34871
- if (!normalizedRepoRoot) return null;
34872
- return {
34873
- repoRoot: normalizedRepoRoot,
34874
- projectId: normalizeString(parsed.projectId),
34875
- currentAppId: normalizeString(parsed.currentAppId),
34876
- upstreamAppId: normalizeString(parsed.upstreamAppId),
34877
- firstTouchedAt: normalizeString(parsed.firstTouchedAt) ?? (/* @__PURE__ */ new Date()).toISOString(),
34878
- lastTouchedAt: normalizeString(parsed.lastTouchedAt) ?? (/* @__PURE__ */ new Date()).toISOString(),
34879
- lastObservedWriteAt: normalizeString(parsed.lastObservedWriteAt),
34880
- touchedBy: normalizeStringArray(parsed.touchedBy),
34881
- hasObservedWrite: Boolean(parsed.hasObservedWrite),
34882
- manuallyRecorded: Boolean(parsed.manuallyRecorded),
34883
- manuallyRecordedAt: normalizeString(parsed.manuallyRecordedAt),
34884
- manuallyRecordedByTool: normalizeString(parsed.manuallyRecordedByTool),
34885
- manualRecordingScope: normalizeManualRecordingScope(parsed.manualRecordingScope),
34886
- manualRemoteChangeRecordedAt: normalizeString(parsed.manualRemoteChangeRecordedAt),
34887
- stopAttempted: Boolean(parsed.stopAttempted),
34888
- stopRecorded: Boolean(parsed.stopRecorded),
34889
- stopRecordedAt: normalizeString(parsed.stopRecordedAt),
34890
- stopRecordedMode: parsed.stopRecordedMode === "changed_turn" || parsed.stopRecordedMode === "no_diff_turn" ? parsed.stopRecordedMode : null,
34891
- recordingFailureMessage: normalizeString(parsed.recordingFailureMessage),
34892
- recordingFailureHint: normalizeString(parsed.recordingFailureHint),
34893
- recordingFailedAt: normalizeString(parsed.recordingFailedAt)
34894
- };
34895
- }
34896
- function normalizeTouchedRepos(value) {
34897
- if (!value || typeof value !== "object") return {};
34898
- const entries = Object.entries(value).map(([repoRoot, repo]) => normalizeTouchedRepo(repo, repoRoot)).filter((repo) => repo !== null).sort((a2, b) => a2.repoRoot.localeCompare(b.repoRoot));
34899
- return Object.fromEntries(entries.map((repo) => [repo.repoRoot, repo]));
34900
- }
34901
- function createTouchedRepo(params) {
34902
- const now = (/* @__PURE__ */ new Date()).toISOString();
34903
- const touchedBy = params.touchedBy?.trim() ? [params.touchedBy.trim()] : [];
34904
- return {
34905
- repoRoot: params.repoRoot,
34906
- projectId: normalizeString(params.projectId),
34907
- currentAppId: normalizeString(params.currentAppId),
34908
- upstreamAppId: normalizeString(params.upstreamAppId),
34909
- firstTouchedAt: now,
34910
- lastTouchedAt: now,
34911
- lastObservedWriteAt: params.hasObservedWrite ? now : null,
34912
- touchedBy,
34913
- hasObservedWrite: Boolean(params.hasObservedWrite),
34914
- manuallyRecorded: false,
34915
- manuallyRecordedAt: null,
34916
- manuallyRecordedByTool: null,
34917
- manualRecordingScope: null,
34918
- manualRemoteChangeRecordedAt: null,
34919
- stopAttempted: false,
34920
- stopRecorded: false,
34921
- stopRecordedAt: null,
34922
- stopRecordedMode: null,
34923
- recordingFailureMessage: null,
34924
- recordingFailureHint: null,
34925
- recordingFailedAt: null
34926
- };
34927
- }
34928
- async function updatePendingTurnState(sessionId, updater) {
34929
- return withStateLock(sessionId, async () => {
34930
- const existing = await loadPendingTurnState(sessionId);
34931
- if (!existing) return null;
34932
- const result = updater(existing);
34933
- if (result === false) return existing;
34934
- await savePendingTurnState(existing);
34935
- return existing;
34936
- });
34937
- }
34938
- async function loadPendingTurnState(sessionId) {
34939
- const raw = await import_promises19.default.readFile(statePath(sessionId), "utf8").catch(() => null);
34940
- if (!raw) return null;
34941
35664
  try {
34942
- const parsed = JSON.parse(raw);
34943
- if (!parsed || typeof parsed !== "object") return null;
34944
- if (typeof parsed.sessionId !== "string" || typeof parsed.turnId !== "string" || typeof parsed.prompt !== "string") {
34945
- return null;
34946
- }
34947
- return {
34948
- sessionId: parsed.sessionId,
34949
- turnId: parsed.turnId,
34950
- prompt: parsed.prompt,
34951
- initialCwd: normalizeString(parsed.initialCwd),
34952
- intent: normalizeIntent(parsed.intent),
34953
- submittedAt: typeof parsed.submittedAt === "string" ? parsed.submittedAt : (/* @__PURE__ */ new Date()).toISOString(),
34954
- consultedMemory: Boolean(parsed.consultedMemory),
34955
- touchedRepos: normalizeTouchedRepos(parsed.touchedRepos),
34956
- turnFailureMessage: normalizeString(parsed.turnFailureMessage),
34957
- turnFailureHint: normalizeString(parsed.turnFailureHint),
34958
- turnFailedAt: normalizeString(parsed.turnFailedAt)
34959
- };
34960
- } catch {
34961
- return null;
35665
+ await drainPendingFinalizeQueue2({ api: params.api });
35666
+ await appendHookDiagnosticsEvent({
35667
+ hook: "deferredTurnDrainer",
35668
+ sessionId: params.sessionMarker,
35669
+ stage: "finalize_queue_pushed",
35670
+ result: "success",
35671
+ repoRoot: params.repoRoot
35672
+ });
35673
+ } catch (err) {
35674
+ await appendHookDiagnosticsEvent({
35675
+ hook: "deferredTurnDrainer",
35676
+ sessionId: params.sessionMarker,
35677
+ stage: "finalize_queue_push_failed",
35678
+ result: "error",
35679
+ reason: "exception",
35680
+ repoRoot: params.repoRoot,
35681
+ message: err instanceof Error ? err.message : String(err)
35682
+ });
34962
35683
  }
34963
35684
  }
34964
- async function savePendingTurnState(state) {
34965
- await writeJsonAtomic3(statePath(state.sessionId), state);
35685
+ async function recordOneDeferredTurn(params) {
35686
+ const { entry, api } = params;
35687
+ const { record, filePath } = entry;
35688
+ try {
35689
+ await collabFinalizeTurn2({
35690
+ api,
35691
+ cwd: record.repoRoot,
35692
+ prompt: record.prompt,
35693
+ assistantResponse: record.assistantResponse,
35694
+ idempotencyKey: buildIdempotencyKey(record.turnId, record.repoRoot),
35695
+ actor: HOOK_ACTOR,
35696
+ turnUsage: null,
35697
+ // The deferred queue can hold a turn for a long time (until the next
35698
+ // `remix collab init` lands a binding), so the server's ingestion
35699
+ // timestamp would otherwise be hours/days off from the real prompt
35700
+ // time. Forward the original submit time so the dashboard timeline
35701
+ // sorts this turn into its true position relative to siblings.
35702
+ promptedAt: record.submittedAt
35703
+ });
35704
+ await deleteDeferredTurnFile(filePath);
35705
+ return { recorded: true };
35706
+ } catch (error) {
35707
+ return { recorded: false, error };
35708
+ }
34966
35709
  }
34967
- async function upsertTouchedRepo(sessionId, params) {
34968
- const normalizedRepoRoot = params.repoRoot.trim();
34969
- if (!normalizedRepoRoot) return null;
34970
- const state = await updatePendingTurnState(sessionId, (existing) => {
34971
- const current = existing.touchedRepos[normalizedRepoRoot] ?? createTouchedRepo({
34972
- repoRoot: normalizedRepoRoot,
34973
- projectId: params.projectId,
34974
- currentAppId: params.currentAppId,
34975
- upstreamAppId: params.upstreamAppId,
34976
- touchedBy: params.touchedBy,
34977
- hasObservedWrite: params.hasObservedWrite
35710
+ async function runStandaloneDeferredTurnDrainer(repoRoot) {
35711
+ const startedAt = Date.now();
35712
+ const sessionMarker = `drainer-${process.pid}-${Math.random().toString(36).slice(2, 10)}`;
35713
+ const acquireResult = await tryAcquireDrainLock(repoRoot);
35714
+ if (!acquireResult.acquired) {
35715
+ await appendHookDiagnosticsEvent({
35716
+ hook: "deferredTurnDrainer",
35717
+ sessionId: sessionMarker,
35718
+ stage: "lock_skipped",
35719
+ result: "skip",
35720
+ reason: "another_drainer_active",
35721
+ repoRoot
34978
35722
  });
34979
- current.projectId = normalizeString(params.projectId) ?? current.projectId;
34980
- current.currentAppId = normalizeString(params.currentAppId) ?? current.currentAppId;
34981
- current.upstreamAppId = normalizeString(params.upstreamAppId) ?? current.upstreamAppId;
34982
- current.lastTouchedAt = (/* @__PURE__ */ new Date()).toISOString();
34983
- if (params.touchedBy?.trim() && !current.touchedBy.includes(params.touchedBy.trim())) {
34984
- current.touchedBy = [...current.touchedBy, params.touchedBy.trim()].sort((a2, b) => a2.localeCompare(b));
34985
- }
34986
- if (params.hasObservedWrite) {
34987
- current.hasObservedWrite = true;
34988
- current.lastObservedWriteAt = (/* @__PURE__ */ new Date()).toISOString();
35723
+ return;
35724
+ }
35725
+ await appendHookDiagnosticsEvent({
35726
+ hook: "deferredTurnDrainer",
35727
+ sessionId: sessionMarker,
35728
+ stage: "drainer_started",
35729
+ result: "info",
35730
+ repoRoot,
35731
+ fields: {
35732
+ pid: process.pid,
35733
+ maxWaitMs: DEFERRED_TURN_DRAIN_MAX_WAIT_MS,
35734
+ pollIntervalMs: DEFERRED_TURN_DRAIN_POLL_INTERVAL_MS
34989
35735
  }
34990
- existing.touchedRepos[normalizedRepoRoot] = current;
34991
- });
34992
- return state?.touchedRepos[normalizedRepoRoot] ?? null;
34993
- }
34994
- async function markTouchedRepoStopAttempted(sessionId, repoRoot) {
34995
- await updatePendingTurnState(sessionId, (existing) => {
34996
- const current = existing.touchedRepos[repoRoot];
34997
- if (!current) return false;
34998
- current.stopAttempted = true;
34999
- current.lastTouchedAt = (/* @__PURE__ */ new Date()).toISOString();
35000
35736
  });
35001
- }
35002
- async function markTouchedRepoStopRecorded(sessionId, repoRoot, params) {
35003
- await updatePendingTurnState(sessionId, (existing) => {
35004
- const current = existing.touchedRepos[repoRoot];
35005
- if (!current) return false;
35006
- current.stopAttempted = true;
35007
- current.stopRecorded = true;
35008
- current.stopRecordedAt = (/* @__PURE__ */ new Date()).toISOString();
35009
- current.stopRecordedMode = params.mode;
35010
- current.recordingFailureMessage = null;
35011
- current.recordingFailureHint = null;
35012
- current.recordingFailedAt = null;
35013
- current.lastTouchedAt = (/* @__PURE__ */ new Date()).toISOString();
35014
- });
35015
- }
35016
- async function markTouchedRepoRecordingFailure(sessionId, repoRoot, params) {
35017
- await updatePendingTurnState(sessionId, (existing) => {
35018
- const current = existing.touchedRepos[repoRoot];
35019
- if (!current) return false;
35020
- current.stopAttempted = true;
35021
- current.recordingFailureMessage = params.message.trim();
35022
- current.recordingFailureHint = params.hint?.trim() || null;
35023
- current.recordingFailedAt = (/* @__PURE__ */ new Date()).toISOString();
35024
- current.lastTouchedAt = (/* @__PURE__ */ new Date()).toISOString();
35025
- });
35026
- }
35027
- function lastFinalizedPath(sessionId) {
35028
- return import_node_path6.default.join(stateRoot(), `${sessionId}.last-finalized.json`);
35029
- }
35030
- async function markLastFinalizedTurn(sessionId, turnId, prompt) {
35031
- const record = {
35032
- sessionId,
35033
- turnId,
35034
- prompt,
35035
- finalizedAt: (/* @__PURE__ */ new Date()).toISOString()
35036
- };
35037
- await writeJsonAtomic3(lastFinalizedPath(sessionId), record);
35038
- }
35039
- async function loadLastFinalizedTurn(sessionId) {
35040
- const raw = await import_promises19.default.readFile(lastFinalizedPath(sessionId), "utf8").catch(() => null);
35041
- if (!raw) return null;
35737
+ const heartbeat = setInterval(() => {
35738
+ void heartbeatDrainLock(acquireResult.lockPath).catch(() => void 0);
35739
+ }, DEFERRED_TURN_DRAIN_LOCK_HEARTBEAT_MS);
35740
+ heartbeat.unref?.();
35741
+ let api = null;
35742
+ let recordedTotal = 0;
35743
+ let failedTotal = 0;
35744
+ let exitReason = "queue_empty";
35042
35745
  try {
35043
- const parsed = JSON.parse(raw);
35044
- if (typeof parsed.sessionId === "string" && typeof parsed.turnId === "string" && typeof parsed.prompt === "string" && typeof parsed.finalizedAt === "string") {
35045
- return {
35046
- sessionId: parsed.sessionId,
35047
- turnId: parsed.turnId,
35048
- prompt: parsed.prompt,
35049
- finalizedAt: parsed.finalizedAt
35050
- };
35746
+ while (true) {
35747
+ if (Date.now() - startedAt > DEFERRED_TURN_DRAIN_MAX_WAIT_MS) {
35748
+ exitReason = "timeout";
35749
+ break;
35750
+ }
35751
+ let entries = [];
35752
+ try {
35753
+ entries = await listDeferredTurnsForRepo(repoRoot);
35754
+ } catch (listErr) {
35755
+ await appendHookDiagnosticsEvent({
35756
+ hook: "deferredTurnDrainer",
35757
+ sessionId: sessionMarker,
35758
+ stage: "list_failed",
35759
+ result: "error",
35760
+ reason: "exception",
35761
+ repoRoot,
35762
+ message: listErr instanceof Error ? listErr.message : String(listErr)
35763
+ });
35764
+ await sleep5(DEFERRED_TURN_DRAIN_POLL_INTERVAL_MS);
35765
+ continue;
35766
+ }
35767
+ if (entries.length === 0) {
35768
+ exitReason = "queue_empty";
35769
+ break;
35770
+ }
35771
+ const bindingState = await readCollabBindingState(repoRoot).catch(() => null);
35772
+ const currentBranch = bindingState?.currentBranch ?? null;
35773
+ const isCurrentBranchBound = bindingState?.binding != null;
35774
+ const attemptable = entries.filter(
35775
+ (e) => isCurrentBranchBound && (!e.record.branchAtDefer || e.record.branchAtDefer === currentBranch)
35776
+ );
35777
+ if (attemptable.length === 0) {
35778
+ await sleep5(DEFERRED_TURN_DRAIN_POLL_INTERVAL_MS);
35779
+ continue;
35780
+ }
35781
+ if (!api) {
35782
+ try {
35783
+ api = await createHookCollabApiClient();
35784
+ } catch (apiErr) {
35785
+ await appendHookDiagnosticsEvent({
35786
+ hook: "deferredTurnDrainer",
35787
+ sessionId: sessionMarker,
35788
+ stage: "api_client_failed",
35789
+ result: "error",
35790
+ reason: "exception",
35791
+ repoRoot,
35792
+ message: apiErr instanceof Error ? apiErr.message : String(apiErr)
35793
+ });
35794
+ exitReason = "api_init_failed";
35795
+ break;
35796
+ }
35797
+ }
35798
+ let recordedThisPass = 0;
35799
+ let failedThisPass = 0;
35800
+ for (const entry of attemptable) {
35801
+ const result = await recordOneDeferredTurn({ entry, api });
35802
+ if (result.recorded) {
35803
+ recordedThisPass += 1;
35804
+ recordedTotal += 1;
35805
+ await appendHookDiagnosticsEvent({
35806
+ hook: "deferredTurnDrainer",
35807
+ sessionId: sessionMarker,
35808
+ stage: "deferred_turn_recorded",
35809
+ result: "success",
35810
+ repoRoot,
35811
+ fields: {
35812
+ deferredTurnId: entry.record.turnId,
35813
+ deferredSessionId: entry.record.sessionId,
35814
+ deferredAt: entry.record.deferredAt,
35815
+ submittedAt: entry.record.submittedAt,
35816
+ recordingDelayMs: Math.max(0, Date.now() - Date.parse(entry.record.deferredAt)),
35817
+ recoveredBy: "standalone_drainer"
35818
+ }
35819
+ });
35820
+ } else {
35821
+ failedThisPass += 1;
35822
+ failedTotal += 1;
35823
+ await appendHookDiagnosticsEvent({
35824
+ hook: "deferredTurnDrainer",
35825
+ sessionId: sessionMarker,
35826
+ stage: "deferred_turn_record_failed",
35827
+ result: "error",
35828
+ reason: "exception",
35829
+ repoRoot,
35830
+ message: result.error instanceof Error ? result.error.message : String(result.error ?? ""),
35831
+ fields: {
35832
+ deferredTurnId: entry.record.turnId,
35833
+ deferredSessionId: entry.record.sessionId
35834
+ }
35835
+ });
35836
+ }
35837
+ }
35838
+ if (recordedThisPass > 0) {
35839
+ await pushPendingFinalizeQueueToServer({
35840
+ sessionMarker,
35841
+ repoRoot,
35842
+ api
35843
+ });
35844
+ }
35845
+ if (recordedThisPass > 0 && failedThisPass === 0) {
35846
+ const remaining = await listDeferredTurnsForRepo(repoRoot).catch(() => []);
35847
+ if (remaining.length === 0) {
35848
+ exitReason = "queue_empty";
35849
+ break;
35850
+ }
35851
+ }
35852
+ await sleep5(DEFERRED_TURN_DRAIN_POLL_INTERVAL_MS);
35051
35853
  }
35052
- return null;
35053
- } catch {
35054
- return null;
35055
- }
35056
- }
35057
- async function markPendingTurnFailure(sessionId, params) {
35058
- await updatePendingTurnState(sessionId, (existing) => {
35059
- existing.turnFailureMessage = params.message.trim();
35060
- existing.turnFailureHint = params.hint?.trim() || null;
35061
- existing.turnFailedAt = (/* @__PURE__ */ new Date()).toISOString();
35062
- });
35063
- }
35064
- async function listTouchedRepos(sessionId) {
35065
- const existing = await loadPendingTurnState(sessionId);
35066
- if (!existing) return [];
35067
- return Object.values(existing.touchedRepos).sort((a2, b) => a2.repoRoot.localeCompare(b.repoRoot));
35068
- }
35069
- async function clearPendingTurnState(sessionId) {
35070
- await withStateLock(sessionId, async () => {
35071
- await import_promises19.default.rm(statePath(sessionId), { force: true }).catch(() => void 0);
35072
- });
35073
- }
35074
-
35075
- // package.json
35076
- var package_default = {
35077
- name: "@remixhq/claude-plugin",
35078
- version: "0.1.21",
35079
- description: "Claude Code plugin for Remix collaboration workflows",
35080
- homepage: "https://github.com/RemixDotOne/remix-claude-plugin",
35081
- license: "MIT",
35082
- repository: {
35083
- type: "git",
35084
- url: "https://github.com/RemixDotOne/remix-claude-plugin.git"
35085
- },
35086
- type: "module",
35087
- engines: {
35088
- node: ">=20"
35089
- },
35090
- publishConfig: {
35091
- access: "public"
35092
- },
35093
- files: [
35094
- "dist",
35095
- ".claude-plugin/plugin.json",
35096
- ".mcp.json",
35097
- "skills",
35098
- "hooks",
35099
- "agents"
35100
- ],
35101
- exports: {
35102
- ".": {
35103
- types: "./dist/index.d.ts",
35104
- import: "./dist/index.js"
35854
+ if (recordedTotal > 0 && api) {
35855
+ await pushPendingFinalizeQueueToServer({
35856
+ sessionMarker,
35857
+ repoRoot,
35858
+ api
35859
+ });
35105
35860
  }
35106
- },
35107
- scripts: {
35108
- build: "tsup",
35109
- 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);"`,
35110
- dev: "tsx src/mcp-server.ts",
35111
- typecheck: "tsc -p tsconfig.json --noEmit",
35112
- test: "node --import tsx --test src/**/*.test.ts",
35113
- prepack: "npm run build"
35114
- },
35115
- dependencies: {
35116
- "@remixhq/core": "^0.1.15",
35117
- "@remixhq/mcp": "^0.1.16"
35118
- },
35119
- devDependencies: {
35120
- "@types/node": "^25.4.0",
35121
- tsup: "^8.5.1",
35122
- tsx: "^4.21.0",
35123
- typescript: "^5.9.3"
35861
+ try {
35862
+ const pruned = await pruneStaleDeferredTurns();
35863
+ if (pruned.length > 0) {
35864
+ await appendHookDiagnosticsEvent({
35865
+ hook: "deferredTurnDrainer",
35866
+ sessionId: sessionMarker,
35867
+ stage: "ttl_pruned",
35868
+ result: "info",
35869
+ repoRoot,
35870
+ fields: { prunedCount: pruned.length }
35871
+ });
35872
+ }
35873
+ } catch {
35874
+ }
35875
+ } finally {
35876
+ clearInterval(heartbeat);
35877
+ await releaseDrainLock(acquireResult.lockPath);
35878
+ await appendHookDiagnosticsEvent({
35879
+ hook: "deferredTurnDrainer",
35880
+ sessionId: sessionMarker,
35881
+ stage: "drainer_completed",
35882
+ result: exitReason === "queue_empty" ? "success" : "info",
35883
+ reason: exitReason,
35884
+ repoRoot,
35885
+ fields: {
35886
+ recordedTotal,
35887
+ failedTotal,
35888
+ elapsedMs: Date.now() - startedAt
35889
+ }
35890
+ });
35124
35891
  }
35125
- };
35126
-
35127
- // src/metadata.ts
35128
- var pluginMetadata = {
35129
- name: package_default.name,
35130
- version: package_default.version,
35131
- description: package_default.description,
35132
- pluginId: "remix",
35133
- agentName: "remix-collab"
35134
- };
35135
-
35136
- // src/hook-diagnostics.ts
35137
- var MAX_LOG_BYTES = 512 * 1024;
35138
- function resolveClaudeRoot() {
35139
- const configured = process.env.CLAUDE_CONFIG_DIR?.trim();
35140
- return configured || import_node_path7.default.join(import_node_os5.default.homedir(), ".claude");
35141
- }
35142
- function resolvePluginDataDirName() {
35143
- return `${pluginMetadata.pluginId}-${pluginMetadata.pluginId}`;
35144
- }
35145
- function getHookDiagnosticsDirPath() {
35146
- const configured = process.env.REMIX_CLAUDE_PLUGIN_HOOK_DIAGNOSTICS_DIR?.trim();
35147
- return configured || import_node_path7.default.join(resolveClaudeRoot(), "plugins", "data", resolvePluginDataDirName());
35148
- }
35149
- function getHookDiagnosticsLogPath() {
35150
- return import_node_path7.default.join(getHookDiagnosticsDirPath(), "hooks.ndjson");
35151
- }
35152
- function toFieldValue(value) {
35153
- if (value === null) return null;
35154
- if (typeof value === "string") return value;
35155
- if (typeof value === "number" && Number.isFinite(value)) return value;
35156
- if (typeof value === "boolean") return value;
35157
- return void 0;
35158
35892
  }
35159
- function normalizeFields(fields) {
35160
- if (!fields) return {};
35161
- const normalizedEntries = Object.entries(fields).map(([key, value]) => {
35162
- const normalized = toFieldValue(value);
35163
- return normalized === void 0 ? null : [key, normalized];
35164
- }).filter((entry) => entry !== null);
35165
- return Object.fromEntries(normalizedEntries);
35166
- }
35167
- async function rotateLogIfNeeded(logPath) {
35168
- const stat = await import_promises20.default.stat(logPath).catch(() => null);
35169
- if (!stat || stat.size < MAX_LOG_BYTES) {
35170
- return;
35893
+ function parseDeferredTurnDrainerArgv(argv) {
35894
+ for (let i2 = 0; i2 < argv.length; i2 += 1) {
35895
+ const arg = argv[i2];
35896
+ if (!arg) continue;
35897
+ if (arg === "--drain-deferred-turns") {
35898
+ const next = argv[i2 + 1];
35899
+ if (next && !next.startsWith("--")) return next;
35900
+ return null;
35901
+ }
35902
+ if (arg.startsWith("--drain-deferred-turns=")) {
35903
+ const value = arg.slice("--drain-deferred-turns=".length);
35904
+ return value || null;
35905
+ }
35171
35906
  }
35172
- const rotatedPath = `${logPath}.1`;
35173
- await import_promises20.default.rm(rotatedPath, { force: true }).catch(() => void 0);
35174
- await import_promises20.default.rename(logPath, rotatedPath).catch(() => void 0);
35907
+ return null;
35175
35908
  }
35176
- function summarizeText(value) {
35177
- if (typeof value !== "string" || !value.trim()) {
35178
- return {
35179
- present: false,
35180
- length: 0,
35181
- sha256Prefix: null
35182
- };
35183
- }
35184
- const trimmed = value.trim();
35185
- return {
35186
- present: true,
35187
- length: trimmed.length,
35188
- sha256Prefix: (0, import_node_crypto2.createHash)("sha256").update(trimmed).digest("hex").slice(0, 12)
35189
- };
35909
+ async function maybeRunDeferredTurnDrainerFromArgv() {
35910
+ const repoRoot = parseDeferredTurnDrainerArgv(process.argv);
35911
+ if (!repoRoot) return false;
35912
+ await runStandaloneDeferredTurnDrainer(repoRoot);
35913
+ return true;
35190
35914
  }
35191
- async function appendHookDiagnosticsEvent(params) {
35192
- try {
35193
- const logPath = getHookDiagnosticsLogPath();
35194
- await import_promises20.default.mkdir(import_node_path7.default.dirname(logPath), { recursive: true });
35195
- await rotateLogIfNeeded(logPath);
35196
- const event = {
35197
- ts: (/* @__PURE__ */ new Date()).toISOString(),
35198
- hook: params.hook,
35199
- pluginVersion: pluginMetadata.version,
35200
- pid: process.pid,
35201
- sessionId: params.sessionId?.trim() || null,
35202
- turnId: params.turnId?.trim() || null,
35203
- stage: params.stage.trim(),
35204
- result: params.result,
35205
- reason: params.reason?.trim() || null,
35206
- toolName: params.toolName?.trim() || null,
35207
- repoRoot: params.repoRoot?.trim() || null,
35208
- message: params.message?.trim() || null,
35209
- fields: normalizeFields(params.fields)
35210
- };
35211
- await import_promises20.default.appendFile(logPath, `${JSON.stringify(event)}
35212
- `, "utf8");
35213
- } catch {
35214
- }
35915
+
35916
+ // src/spawn-helpers.ts
35917
+ var import_node_child_process7 = require("child_process");
35918
+ function spawnDeferredTurnDrainer(repoRoot) {
35919
+ const entrypoint = process.argv[1];
35920
+ if (!entrypoint) return;
35921
+ if (!repoRoot) return;
35922
+ const child = (0, import_node_child_process7.spawn)(
35923
+ process.execPath,
35924
+ [...process.execArgv, entrypoint, "--drain-deferred-turns", repoRoot],
35925
+ {
35926
+ detached: true,
35927
+ stdio: "ignore",
35928
+ env: process.env
35929
+ }
35930
+ );
35931
+ child.unref();
35215
35932
  }
35216
35933
 
35217
35934
  // node_modules/@remixhq/core/dist/history.js
35218
- var import_promises21 = __toESM(require("fs/promises"), 1);
35935
+ var import_promises24 = __toESM(require("fs/promises"), 1);
35219
35936
  async function readAndParseTranscript(transcriptPath) {
35220
35937
  let raw;
35221
35938
  try {
35222
- raw = await import_promises21.default.readFile(transcriptPath, "utf8");
35939
+ raw = await import_promises24.default.readFile(transcriptPath, "utf8");
35223
35940
  } catch (err) {
35224
35941
  const code = err && typeof err === "object" && "code" in err ? err.code : null;
35225
35942
  if (code === "ENOENT") {
@@ -35662,10 +36379,10 @@ function harvestClaudeCodeUsage(input) {
35662
36379
  }
35663
36380
 
35664
36381
  // src/usage/claudeCodeSession.ts
35665
- var import_node_child_process6 = require("child_process");
35666
- var import_node_fs6 = require("fs");
35667
- var import_node_os6 = require("os");
35668
- var import_node_path8 = require("path");
36382
+ var import_node_child_process8 = require("child_process");
36383
+ var import_node_fs7 = require("fs");
36384
+ var import_node_os7 = require("os");
36385
+ var import_node_path12 = require("path");
35669
36386
  var CACHE_SCHEMA_VERSION = 1;
35670
36387
  var SUCCESS_TTL_MS = 60 * 60 * 1e3;
35671
36388
  var FAILURE_TTL_MS = 5 * 60 * 1e3;
@@ -35675,7 +36392,7 @@ var spawnerImpl = defaultSpawnClaudeAuthStatus;
35675
36392
  function defaultSpawnClaudeAuthStatus(timeoutMs) {
35676
36393
  let result;
35677
36394
  try {
35678
- result = (0, import_node_child_process6.spawnSync)("claude", ["auth", "status", "--json"], {
36395
+ result = (0, import_node_child_process8.spawnSync)("claude", ["auth", "status", "--json"], {
35679
36396
  stdio: ["ignore", "pipe", "pipe"],
35680
36397
  timeout: timeoutMs,
35681
36398
  env: process.env
@@ -35694,10 +36411,10 @@ function defaultSpawnClaudeAuthStatus(timeoutMs) {
35694
36411
  }
35695
36412
  function getCollabStateRoot2() {
35696
36413
  const configured = process.env.REMIX_COLLAB_STATE_ROOT?.trim();
35697
- return configured || (0, import_node_path8.join)((0, import_node_os6.homedir)(), ".remix", "collab-state");
36414
+ return configured || (0, import_node_path12.join)((0, import_node_os7.homedir)(), ".remix", "collab-state");
35698
36415
  }
35699
36416
  function getAuthCachePath() {
35700
- return (0, import_node_path8.join)(getCollabStateRoot2(), "claude-auth-cache.json");
36417
+ return (0, import_node_path12.join)(getCollabStateRoot2(), "claude-auth-cache.json");
35701
36418
  }
35702
36419
  function getSpawnTimeoutMs() {
35703
36420
  const raw = process.env.REMIX_CLAUDE_AUTH_TIMEOUT_MS?.trim();
@@ -35709,7 +36426,7 @@ function getSpawnTimeoutMs() {
35709
36426
  function readAuthCache() {
35710
36427
  let raw;
35711
36428
  try {
35712
- raw = (0, import_node_fs6.readFileSync)(getAuthCachePath(), "utf8");
36429
+ raw = (0, import_node_fs7.readFileSync)(getAuthCachePath(), "utf8");
35713
36430
  } catch {
35714
36431
  return null;
35715
36432
  }
@@ -35738,10 +36455,10 @@ function isCacheFresh(record) {
35738
36455
  function writeAuthCache(record) {
35739
36456
  const cachePath = getAuthCachePath();
35740
36457
  try {
35741
- (0, import_node_fs6.mkdirSync)((0, import_node_path8.dirname)(cachePath), { recursive: true });
36458
+ (0, import_node_fs7.mkdirSync)((0, import_node_path12.dirname)(cachePath), { recursive: true });
35742
36459
  const tmpPath = `${cachePath}.${process.pid}.${Date.now()}.tmp`;
35743
- (0, import_node_fs6.writeFileSync)(tmpPath, JSON.stringify(record), "utf8");
35744
- (0, import_node_fs6.renameSync)(tmpPath, cachePath);
36460
+ (0, import_node_fs7.writeFileSync)(tmpPath, JSON.stringify(record), "utf8");
36461
+ (0, import_node_fs7.renameSync)(tmpPath, cachePath);
35745
36462
  } catch {
35746
36463
  }
35747
36464
  }
@@ -35795,8 +36512,8 @@ function resolveClaudeCodeSession(hookPayload) {
35795
36512
  }
35796
36513
 
35797
36514
  // src/hook-utils.ts
35798
- var import_promises22 = __toESM(require("fs/promises"), 1);
35799
- var import_node_path9 = __toESM(require("path"), 1);
36515
+ var import_promises25 = __toESM(require("fs/promises"), 1);
36516
+ var import_node_path13 = __toESM(require("path"), 1);
35800
36517
  async function readJsonStdin() {
35801
36518
  const chunks = [];
35802
36519
  for await (const chunk of process.stdin) {
@@ -35858,16 +36575,16 @@ function extractBoolean(input, keys) {
35858
36575
  }
35859
36576
  async function findBoundRepo(startPath) {
35860
36577
  if (!startPath) return null;
35861
- let current = import_node_path9.default.resolve(startPath);
35862
- let stats = await import_promises22.default.stat(current).catch(() => null);
36578
+ let current = import_node_path13.default.resolve(startPath);
36579
+ let stats = await import_promises25.default.stat(current).catch(() => null);
35863
36580
  if (stats?.isFile()) {
35864
- current = import_node_path9.default.dirname(current);
36581
+ current = import_node_path13.default.dirname(current);
35865
36582
  }
35866
36583
  while (true) {
35867
- const bindingPath = import_node_path9.default.join(current, ".remix", "config.json");
35868
- const bindingStats = await import_promises22.default.stat(bindingPath).catch(() => null);
36584
+ const bindingPath = import_node_path13.default.join(current, ".remix", "config.json");
36585
+ const bindingStats = await import_promises25.default.stat(bindingPath).catch(() => null);
35869
36586
  if (bindingStats?.isFile()) return current;
35870
- const parent = import_node_path9.default.dirname(current);
36587
+ const parent = import_node_path13.default.dirname(current);
35871
36588
  if (parent === current) return null;
35872
36589
  current = parent;
35873
36590
  }
@@ -35886,23 +36603,26 @@ async function resolveBoundRepoSummary(startPath) {
35886
36603
  }
35887
36604
 
35888
36605
  // src/hook-stop-collab.ts
35889
- var HOOK_ACTOR = {
36606
+ var HOOK_ACTOR2 = {
35890
36607
  type: "agent",
35891
36608
  name: "claude-code",
35892
36609
  version: pluginMetadata.version,
35893
36610
  provider: "anthropic"
35894
36611
  };
35895
- var collabFinalizeTurn2 = collabFinalizeTurn;
36612
+ var collabFinalizeTurn3 = collabFinalizeTurn;
35896
36613
  function getErrorDetails(error) {
35897
36614
  if (error instanceof Error) {
35898
36615
  const hint = typeof error.hint === "string" ? String(error.hint) : null;
36616
+ const codeRaw = error.code;
36617
+ const preflightCode = isFinalizePreflightFailureCode(codeRaw) ? codeRaw : null;
35899
36618
  return {
35900
36619
  message: error.message || "Fallback Remix turn recording failed.",
35901
- hint
36620
+ hint,
36621
+ preflightCode
35902
36622
  };
35903
36623
  }
35904
36624
  const message = typeof error === "string" && error.trim() ? error.trim() : "Fallback Remix turn recording failed.";
35905
- return { message, hint: null };
36625
+ return { message, hint: null, preflightCode: null };
35906
36626
  }
35907
36627
  function buildRepoIdempotencyKey(turnId, repo) {
35908
36628
  const repoToken = repo.currentAppId?.trim() || repo.repoRoot;
@@ -35957,7 +36677,7 @@ function createFallbackTouchedRepo(params) {
35957
36677
  };
35958
36678
  }
35959
36679
  var TRANSCRIPT_FLUSH_RETRY_DELAYS_MS = [50, 100, 200];
35960
- function sleep5(ms) {
36680
+ function sleep6(ms) {
35961
36681
  return new Promise((resolve) => setTimeout(resolve, ms));
35962
36682
  }
35963
36683
  async function harvestTurnUsage(params) {
@@ -36002,7 +36722,7 @@ async function harvestTurnUsage(params) {
36002
36722
  while (!currentResult.ok && currentResult.reason === "no_messages_for_turn") {
36003
36723
  if (retriesUsed >= TRANSCRIPT_FLUSH_RETRY_DELAYS_MS.length) break;
36004
36724
  const delayMs = TRANSCRIPT_FLUSH_RETRY_DELAYS_MS[retriesUsed];
36005
- await sleep5(delayMs);
36725
+ await sleep6(delayMs);
36006
36726
  retriesUsed += 1;
36007
36727
  totalBackoffMs += delayMs;
36008
36728
  const reparsed = await readAndParseTranscript(transcriptPath);
@@ -36207,9 +36927,18 @@ async function recordTouchedRepo(params) {
36207
36927
  try {
36208
36928
  const binding = await readCollabBinding(repo.repoRoot).catch(() => null);
36209
36929
  if (!binding) {
36210
- await markTouchedRepoRecordingFailure(sessionId, repo.repoRoot, {
36930
+ const failure = {
36931
+ repoRoot: repo.repoRoot,
36211
36932
  message: "Fallback Remix turn recording failed because the repository is no longer bound to Remix.",
36212
- hint: `Repo root: ${repo.repoRoot}`
36933
+ hint: `Repo root: ${repo.repoRoot}`,
36934
+ // Equivalent to the not_bound preflight code — the binding
36935
+ // disappeared between touch-time and finalize-time. Reusing the
36936
+ // code lets the dispatcher route this through the same recovery.
36937
+ preflightCode: "not_bound"
36938
+ };
36939
+ await markTouchedRepoRecordingFailure(sessionId, repo.repoRoot, {
36940
+ message: failure.message,
36941
+ hint: failure.hint
36213
36942
  });
36214
36943
  await appendHookDiagnosticsEvent({
36215
36944
  hook,
@@ -36220,19 +36949,20 @@ async function recordTouchedRepo(params) {
36220
36949
  reason: "repo_not_bound",
36221
36950
  repoRoot: repo.repoRoot
36222
36951
  });
36223
- return { recorded: false, queued: false };
36952
+ return { recorded: false, queued: false, failure };
36224
36953
  }
36225
- const result = await collabFinalizeTurn2({
36954
+ const result = await collabFinalizeTurn3({
36226
36955
  api,
36227
36956
  cwd: repo.repoRoot,
36228
36957
  prompt,
36229
36958
  assistantResponse,
36230
36959
  idempotencyKey: buildRepoIdempotencyKey(turnId, repo),
36231
- actor: HOOK_ACTOR,
36960
+ actor: HOOK_ACTOR2,
36232
36961
  turnUsage,
36233
36962
  promptedAt: promptedAt ?? null
36234
36963
  });
36235
36964
  await markTouchedRepoStopRecorded(sessionId, repo.repoRoot, { mode: result.mode });
36965
+ await clearFinalizeFailureMarker(repo.repoRoot).catch(() => void 0);
36236
36966
  await appendHookDiagnosticsEvent({
36237
36967
  hook,
36238
36968
  sessionId,
@@ -36242,10 +36972,17 @@ async function recordTouchedRepo(params) {
36242
36972
  reason: result.mode,
36243
36973
  repoRoot: repo.repoRoot
36244
36974
  });
36245
- return { recorded: true, queued: result.queued === true };
36975
+ return {
36976
+ recorded: true,
36977
+ queued: result.queued === true,
36978
+ failure: null
36979
+ };
36246
36980
  } catch (error) {
36247
36981
  const details = getErrorDetails(error);
36248
- await markTouchedRepoRecordingFailure(sessionId, repo.repoRoot, details);
36982
+ await markTouchedRepoRecordingFailure(sessionId, repo.repoRoot, {
36983
+ message: details.message,
36984
+ hint: details.hint
36985
+ });
36249
36986
  await appendHookDiagnosticsEvent({
36250
36987
  hook,
36251
36988
  sessionId,
@@ -36256,68 +36993,246 @@ async function recordTouchedRepo(params) {
36256
36993
  repoRoot: repo.repoRoot,
36257
36994
  message: details.message,
36258
36995
  fields: {
36259
- hint: details.hint
36996
+ hint: details.hint,
36997
+ preflightCode: details.preflightCode
36260
36998
  }
36261
36999
  });
36262
- return { recorded: false, queued: false };
37000
+ return {
37001
+ recorded: false,
37002
+ queued: false,
37003
+ failure: {
37004
+ repoRoot: repo.repoRoot,
37005
+ message: details.message,
37006
+ hint: details.hint,
37007
+ preflightCode: details.preflightCode
37008
+ }
37009
+ };
36263
37010
  }
36264
37011
  }
36265
37012
  function spawnFinalizeQueueDrainer() {
36266
37013
  const entrypoint = process.argv[1];
36267
37014
  if (!entrypoint) return;
36268
- const child = (0, import_node_child_process7.spawn)(process.execPath, [...process.execArgv, entrypoint, "--drain-finalize-queue"], {
37015
+ const child = (0, import_node_child_process9.spawn)(process.execPath, [...process.execArgv, entrypoint, "--drain-finalize-queue"], {
36269
37016
  detached: true,
36270
37017
  stdio: "ignore",
36271
37018
  env: process.env
36272
37019
  });
36273
37020
  child.unref();
36274
37021
  }
36275
- var HISTORY_IMPORT_MARKER_REL = import_node_path10.default.join(".remix", ".history-imported");
36276
- var HISTORY_IMPORT_LOG_REL = import_node_path10.default.join(".remix", "history-import.log");
36277
- function maybeAutoSpawnHistoryImportFromStopHook(repoRoot) {
37022
+ var DEFERRED_TURN_PRUNE_LOG_LIMIT = 5;
37023
+ async function drainDeferredTurnsForRepo(params) {
37024
+ const { hook, sessionId, triggerTurnId, repoRoot, api } = params;
37025
+ let entries = [];
36278
37026
  try {
36279
- if ((0, import_node_fs7.existsSync)(import_node_path10.default.join(repoRoot, HISTORY_IMPORT_MARKER_REL))) {
36280
- return { spawned: false, reason: "marker_present" };
37027
+ entries = await listDeferredTurnsForRepo(repoRoot);
37028
+ } catch (listErr) {
37029
+ await appendHookDiagnosticsEvent({
37030
+ hook,
37031
+ sessionId,
37032
+ turnId: triggerTurnId,
37033
+ stage: "deferred_turn_list_failed",
37034
+ result: "error",
37035
+ reason: "exception",
37036
+ repoRoot,
37037
+ message: listErr instanceof Error ? listErr.message : String(listErr)
37038
+ });
37039
+ return;
37040
+ }
37041
+ if (entries.length === 0) return;
37042
+ const bindingState = await readCollabBindingState(repoRoot).catch(() => null);
37043
+ const currentBranch = bindingState?.currentBranch ?? null;
37044
+ const isCurrentBranchBound = bindingState?.binding != null;
37045
+ await appendHookDiagnosticsEvent({
37046
+ hook,
37047
+ sessionId,
37048
+ turnId: triggerTurnId,
37049
+ stage: "deferred_turn_drain_started",
37050
+ result: "info",
37051
+ repoRoot,
37052
+ fields: {
37053
+ candidateCount: entries.length,
37054
+ currentBranch,
37055
+ currentBranchBound: isCurrentBranchBound
37056
+ }
37057
+ });
37058
+ let recordedCount = 0;
37059
+ let skippedCount = 0;
37060
+ let failedCount = 0;
37061
+ for (const entry of entries) {
37062
+ const { record, filePath } = entry;
37063
+ if (!isCurrentBranchBound || record.branchAtDefer && record.branchAtDefer !== currentBranch) {
37064
+ skippedCount += 1;
37065
+ await appendHookDiagnosticsEvent({
37066
+ hook,
37067
+ sessionId,
37068
+ turnId: triggerTurnId,
37069
+ stage: "deferred_turn_skipped",
37070
+ result: "info",
37071
+ reason: "branch_mismatch",
37072
+ repoRoot,
37073
+ fields: {
37074
+ deferredTurnId: record.turnId,
37075
+ deferredSessionId: record.sessionId,
37076
+ branchAtDefer: record.branchAtDefer,
37077
+ currentBranch
37078
+ }
37079
+ });
37080
+ continue;
37081
+ }
37082
+ try {
37083
+ const idempotencyKey = `${record.turnId}:${repoRoot}:finalize_turn`;
37084
+ await collabFinalizeTurn3({
37085
+ api,
37086
+ cwd: repoRoot,
37087
+ prompt: record.prompt,
37088
+ assistantResponse: record.assistantResponse,
37089
+ idempotencyKey,
37090
+ actor: HOOK_ACTOR2,
37091
+ turnUsage: null,
37092
+ promptedAt: record.submittedAt
37093
+ });
37094
+ await deleteDeferredTurnFile(filePath);
37095
+ recordedCount += 1;
37096
+ await appendHookDiagnosticsEvent({
37097
+ hook,
37098
+ sessionId,
37099
+ turnId: triggerTurnId,
37100
+ stage: "deferred_turn_recorded",
37101
+ result: "success",
37102
+ repoRoot,
37103
+ fields: {
37104
+ deferredTurnId: record.turnId,
37105
+ deferredSessionId: record.sessionId,
37106
+ deferredAt: record.deferredAt,
37107
+ submittedAt: record.submittedAt,
37108
+ recordingDelayMs: Math.max(0, Date.now() - Date.parse(record.deferredAt))
37109
+ }
37110
+ });
37111
+ } catch (recordErr) {
37112
+ failedCount += 1;
37113
+ await appendHookDiagnosticsEvent({
37114
+ hook,
37115
+ sessionId,
37116
+ turnId: triggerTurnId,
37117
+ stage: "deferred_turn_record_failed",
37118
+ result: "error",
37119
+ reason: "exception",
37120
+ repoRoot,
37121
+ message: recordErr instanceof Error ? recordErr.message : String(recordErr),
37122
+ fields: {
37123
+ deferredTurnId: record.turnId,
37124
+ deferredSessionId: record.sessionId
37125
+ }
37126
+ });
36281
37127
  }
36282
- } catch (markerErr) {
36283
- return {
36284
- spawned: false,
36285
- reason: "marker_check_failed",
36286
- message: markerErr instanceof Error ? markerErr.message : String(markerErr)
36287
- };
36288
37128
  }
36289
- const remixDir = import_node_path10.default.join(repoRoot, ".remix");
36290
37129
  try {
36291
- (0, import_node_fs7.mkdirSync)(remixDir, { recursive: true });
37130
+ const pruned = await pruneStaleDeferredTurns();
37131
+ for (const prunedPath of pruned.slice(0, DEFERRED_TURN_PRUNE_LOG_LIMIT)) {
37132
+ await appendHookDiagnosticsEvent({
37133
+ hook,
37134
+ sessionId,
37135
+ turnId: triggerTurnId,
37136
+ stage: "deferred_turn_pruned",
37137
+ result: "info",
37138
+ reason: "stale",
37139
+ repoRoot,
37140
+ fields: { prunedFilePath: prunedPath }
37141
+ });
37142
+ }
37143
+ if (pruned.length > DEFERRED_TURN_PRUNE_LOG_LIMIT) {
37144
+ await appendHookDiagnosticsEvent({
37145
+ hook,
37146
+ sessionId,
37147
+ turnId: triggerTurnId,
37148
+ stage: "deferred_turn_pruned",
37149
+ result: "info",
37150
+ reason: "stale_truncated",
37151
+ repoRoot,
37152
+ fields: {
37153
+ totalPruned: pruned.length,
37154
+ loggedPruned: DEFERRED_TURN_PRUNE_LOG_LIMIT
37155
+ }
37156
+ });
37157
+ }
36292
37158
  } catch {
36293
37159
  }
36294
- const logPath = import_node_path10.default.join(repoRoot, HISTORY_IMPORT_LOG_REL);
36295
- let out;
36296
- let err;
36297
- try {
36298
- out = (0, import_node_fs7.openSync)(logPath, "a");
36299
- err = (0, import_node_fs7.openSync)(logPath, "a");
36300
- } catch (logErr) {
36301
- return {
36302
- spawned: false,
36303
- reason: "log_open_failed",
36304
- message: logErr instanceof Error ? logErr.message : String(logErr)
36305
- };
36306
- }
37160
+ await appendHookDiagnosticsEvent({
37161
+ hook,
37162
+ sessionId,
37163
+ turnId: triggerTurnId,
37164
+ stage: "deferred_turn_drain_completed",
37165
+ result: "info",
37166
+ repoRoot,
37167
+ fields: {
37168
+ recordedCount,
37169
+ skippedCount,
37170
+ failedCount
37171
+ }
37172
+ });
37173
+ }
37174
+ async function deferTurnForRecoveryInProgress(params) {
37175
+ const { hook, sessionId, turnId, repoRoot, prompt, assistantResponse, submittedAt, preflightCode } = params;
37176
+ const bindingState = await readCollabBindingState(repoRoot).catch(() => null);
37177
+ const branchAtDefer = bindingState?.currentBranch ?? null;
36307
37178
  try {
36308
- const child = (0, import_node_child_process7.spawn)("remix", ["history", "import", "--repo", repoRoot, "--include-prompt-text"], {
36309
- detached: true,
36310
- stdio: ["ignore", out, err],
36311
- env: { ...process.env, REMIX_HISTORY_AUTO_SPAWN: "1" }
37179
+ const deferredFilePath = await writeDeferredTurn(
37180
+ buildDeferredTurnRecord({
37181
+ sessionId,
37182
+ turnId,
37183
+ repoRoot,
37184
+ prompt,
37185
+ assistantResponse,
37186
+ submittedAt,
37187
+ branchAtDefer,
37188
+ reason: "recovery_in_progress"
37189
+ })
37190
+ );
37191
+ await appendHookDiagnosticsEvent({
37192
+ hook,
37193
+ sessionId,
37194
+ turnId,
37195
+ stage: "turn_deferred",
37196
+ result: "success",
37197
+ reason: "recovery_in_progress",
37198
+ repoRoot,
37199
+ fields: {
37200
+ deferredFilePath,
37201
+ promptLength: prompt.length,
37202
+ assistantResponseLength: assistantResponse.length,
37203
+ branchAtDefer,
37204
+ preflightCode
37205
+ }
36312
37206
  });
36313
- child.unref();
36314
- return { spawned: true, pid: child.pid, logPath };
36315
- } catch (spawnErr) {
36316
- return {
36317
- spawned: false,
36318
- reason: "spawn_failed",
36319
- message: spawnErr instanceof Error ? spawnErr.message : String(spawnErr)
36320
- };
37207
+ spawnDeferredTurnDrainer(repoRoot);
37208
+ await appendHookDiagnosticsEvent({
37209
+ hook,
37210
+ sessionId,
37211
+ turnId,
37212
+ stage: "deferred_turn_drainer_spawned",
37213
+ result: "info",
37214
+ repoRoot,
37215
+ fields: {
37216
+ triggeredBy: "recovery_in_progress",
37217
+ preflightCode
37218
+ }
37219
+ });
37220
+ return deferredFilePath;
37221
+ } catch (deferErr) {
37222
+ await appendHookDiagnosticsEvent({
37223
+ hook,
37224
+ sessionId,
37225
+ turnId,
37226
+ stage: "deferred_turn_write_failed",
37227
+ result: "error",
37228
+ reason: "exception",
37229
+ repoRoot,
37230
+ message: deferErr instanceof Error ? deferErr.message : String(deferErr),
37231
+ fields: {
37232
+ preflightCode
37233
+ }
37234
+ });
37235
+ return null;
36321
37236
  }
36322
37237
  }
36323
37238
  async function runHookStopCollab(payload) {
@@ -36395,6 +37310,35 @@ async function runHookStopCollab(payload) {
36395
37310
  unboundBranchKnownCount = knownBoundBranches.length;
36396
37311
  }
36397
37312
  }
37313
+ const promptTextForDefer = state.prompt.trim();
37314
+ const assistantResponseForDefer = (extractAssistantResponse(payload) || "").trim();
37315
+ let deferredFilePath = null;
37316
+ if (skipReason === "current_branch_unbound" && unboundBranchRepoRoot && promptTextForDefer && assistantResponseForDefer) {
37317
+ try {
37318
+ deferredFilePath = await writeDeferredTurn(
37319
+ buildDeferredTurnRecord({
37320
+ sessionId,
37321
+ turnId: state.turnId,
37322
+ repoRoot: unboundBranchRepoRoot,
37323
+ prompt: promptTextForDefer,
37324
+ assistantResponse: assistantResponseForDefer,
37325
+ submittedAt: state.submittedAt,
37326
+ branchAtDefer: unboundBranchName
37327
+ })
37328
+ );
37329
+ } catch (deferErr) {
37330
+ await appendHookDiagnosticsEvent({
37331
+ hook,
37332
+ sessionId,
37333
+ turnId: state.turnId,
37334
+ stage: "deferred_turn_write_failed",
37335
+ result: "error",
37336
+ reason: "exception",
37337
+ repoRoot: unboundBranchRepoRoot,
37338
+ message: deferErr instanceof Error ? deferErr.message : String(deferErr)
37339
+ });
37340
+ }
37341
+ }
36398
37342
  await clearPendingTurnState(sessionId);
36399
37343
  await appendHookDiagnosticsEvent({
36400
37344
  hook,
@@ -36406,16 +37350,44 @@ async function runHookStopCollab(payload) {
36406
37350
  repoRoot: unboundBranchRepoRoot,
36407
37351
  fields: skipReason === "current_branch_unbound" ? {
36408
37352
  currentBranch: unboundBranchName,
36409
- knownBoundBranchCount: unboundBranchKnownCount
37353
+ knownBoundBranchCount: unboundBranchKnownCount,
37354
+ deferredForRetry: deferredFilePath !== null
36410
37355
  } : {}
36411
37356
  });
37357
+ if (deferredFilePath && unboundBranchRepoRoot) {
37358
+ await appendHookDiagnosticsEvent({
37359
+ hook,
37360
+ sessionId,
37361
+ turnId: state.turnId,
37362
+ stage: "turn_deferred",
37363
+ result: "success",
37364
+ reason: "current_branch_unbound",
37365
+ repoRoot: unboundBranchRepoRoot,
37366
+ fields: {
37367
+ deferredFilePath,
37368
+ promptLength: promptTextForDefer.length,
37369
+ assistantResponseLength: assistantResponseForDefer.length,
37370
+ branchAtDefer: unboundBranchName
37371
+ }
37372
+ });
37373
+ spawnDeferredTurnDrainer(unboundBranchRepoRoot);
37374
+ await appendHookDiagnosticsEvent({
37375
+ hook,
37376
+ sessionId,
37377
+ turnId: state.turnId,
37378
+ stage: "deferred_turn_drainer_spawned",
37379
+ result: "info",
37380
+ repoRoot: unboundBranchRepoRoot,
37381
+ fields: { triggeredBy: "defer" }
37382
+ });
37383
+ }
36412
37384
  await appendHookDiagnosticsEvent({
36413
37385
  hook,
36414
37386
  sessionId,
36415
37387
  turnId: state.turnId,
36416
37388
  stage: "state_cleanup",
36417
37389
  result: "success",
36418
- reason: "cleared_without_bound_repo"
37390
+ reason: deferredFilePath ? "cleared_after_defer" : "cleared_without_bound_repo"
36419
37391
  });
36420
37392
  return;
36421
37393
  }
@@ -36491,10 +37463,20 @@ async function runHookStopCollab(payload) {
36491
37463
  state: { turnId: state.turnId, prompt, submittedAt: state.submittedAt },
36492
37464
  payload
36493
37465
  });
37466
+ for (const repo of touchedRepos) {
37467
+ await drainDeferredTurnsForRepo({
37468
+ hook,
37469
+ sessionId,
37470
+ triggerTurnId: state.turnId,
37471
+ repoRoot: repo.repoRoot,
37472
+ api
37473
+ }).catch(() => void 0);
37474
+ }
36494
37475
  let hadFailure = false;
36495
37476
  let queuedFinalizeWork = false;
36496
37477
  let anyRecorded = false;
36497
37478
  let anyTurnExists = false;
37479
+ const failures = [];
36498
37480
  for (const repo of touchedRepos) {
36499
37481
  if (shouldSkipStopRecording(repo)) {
36500
37482
  const legacyMcpFinalizeQueued = repo.manuallyRecordedByTool === "remix_collab_finalize_turn" && repo.manualRecordingScope === "full_turn";
@@ -36541,31 +37523,11 @@ async function runHookStopCollab(payload) {
36541
37523
  if (recording.recorded) {
36542
37524
  anyRecorded = true;
36543
37525
  anyTurnExists = true;
36544
- const autoSpawn = maybeAutoSpawnHistoryImportFromStopHook(repo.repoRoot);
36545
- if (autoSpawn.spawned) {
36546
- await appendHookDiagnosticsEvent({
36547
- hook,
36548
- sessionId,
36549
- turnId: state.turnId,
36550
- stage: "history_import_auto_spawned_from_stop",
36551
- result: "success",
36552
- repoRoot: repo.repoRoot,
36553
- fields: { pid: autoSpawn.pid ?? null, logPath: autoSpawn.logPath }
36554
- });
36555
- } else if (autoSpawn.reason !== "marker_present") {
36556
- await appendHookDiagnosticsEvent({
36557
- hook,
36558
- sessionId,
36559
- turnId: state.turnId,
36560
- stage: "history_import_auto_spawn_skipped",
36561
- result: "info",
36562
- reason: autoSpawn.reason,
36563
- repoRoot: repo.repoRoot,
36564
- message: autoSpawn.message ?? null
36565
- });
36566
- }
36567
37526
  } else {
36568
37527
  hadFailure = true;
37528
+ if (recording.failure) {
37529
+ failures.push(recording.failure);
37530
+ }
36569
37531
  }
36570
37532
  }
36571
37533
  if (anyRecorded || anyTurnExists) {
@@ -36574,7 +37536,48 @@ async function runHookStopCollab(payload) {
36574
37536
  if (queuedFinalizeWork) {
36575
37537
  spawnFinalizeQueueDrainer();
36576
37538
  }
36577
- if (!hadFailure) {
37539
+ let deferredFailureCount = 0;
37540
+ let dispatchFailureCount = 0;
37541
+ for (const failure of failures) {
37542
+ const outcome = await dispatchFinalizeFailure({
37543
+ hook: "Stop",
37544
+ sessionId,
37545
+ turnId: state.turnId,
37546
+ repoRoot: failure.repoRoot,
37547
+ preflightCode: failure.preflightCode,
37548
+ message: failure.message,
37549
+ hint: failure.hint
37550
+ }).catch((dispatchErr) => {
37551
+ dispatchFailureCount += 1;
37552
+ return appendHookDiagnosticsEvent({
37553
+ hook,
37554
+ sessionId,
37555
+ turnId: state.turnId,
37556
+ stage: "auto_fix_dispatch_failed",
37557
+ result: "error",
37558
+ reason: "exception",
37559
+ repoRoot: failure.repoRoot,
37560
+ message: dispatchErr instanceof Error ? dispatchErr.message : String(dispatchErr)
37561
+ }).then(() => null);
37562
+ });
37563
+ if (outcome && isAutoFixableFinalizeFailureCode(failure.preflightCode) && (outcome.kind === "spawned" || outcome.kind === "spawn_throttled")) {
37564
+ const deferredFilePath = await deferTurnForRecoveryInProgress({
37565
+ hook,
37566
+ sessionId,
37567
+ turnId: state.turnId,
37568
+ repoRoot: failure.repoRoot,
37569
+ prompt,
37570
+ assistantResponse,
37571
+ submittedAt: state.submittedAt,
37572
+ preflightCode: failure.preflightCode
37573
+ });
37574
+ if (deferredFilePath) {
37575
+ deferredFailureCount += 1;
37576
+ }
37577
+ }
37578
+ }
37579
+ const allFailuresDeferred = failures.length > 0 && deferredFailureCount === failures.length && dispatchFailureCount === 0;
37580
+ if (!hadFailure || allFailuresDeferred) {
36578
37581
  await clearPendingTurnState(sessionId);
36579
37582
  await appendHookDiagnosticsEvent({
36580
37583
  hook,
@@ -36582,7 +37585,10 @@ async function runHookStopCollab(payload) {
36582
37585
  turnId: state.turnId,
36583
37586
  stage: "state_cleanup",
36584
37587
  result: "success",
36585
- reason: "cleared_after_success"
37588
+ reason: allFailuresDeferred ? "cleared_after_recovery_defer" : "cleared_after_success",
37589
+ fields: allFailuresDeferred ? {
37590
+ deferredFailureCount
37591
+ } : void 0
36586
37592
  });
36587
37593
  return;
36588
37594
  }
@@ -36596,7 +37602,7 @@ async function runHookStopCollab(payload) {
36596
37602
  });
36597
37603
  } catch (error) {
36598
37604
  const details = getErrorDetails(error);
36599
- await markPendingTurnFailure(sessionId, details);
37605
+ await markPendingTurnFailure(sessionId, { message: details.message, hint: details.hint });
36600
37606
  await appendHookDiagnosticsEvent({
36601
37607
  hook,
36602
37608
  sessionId,
@@ -36606,17 +37612,21 @@ async function runHookStopCollab(payload) {
36606
37612
  reason: "exception",
36607
37613
  message: details.message,
36608
37614
  fields: {
36609
- hint: details.hint
37615
+ hint: details.hint,
37616
+ preflightCode: details.preflightCode
36610
37617
  }
36611
37618
  });
36612
37619
  }
36613
37620
  }
36614
37621
  async function main() {
37622
+ if (await maybeRunDeferredTurnDrainerFromArgv()) {
37623
+ return;
37624
+ }
36615
37625
  if (process.argv.includes("--drain-finalize-queue")) {
36616
37626
  const api = await createHookCollabApiClient();
36617
- const drainPendingFinalizeQueue2 = drainPendingFinalizeQueue;
36618
- if (typeof drainPendingFinalizeQueue2 === "function") {
36619
- await drainPendingFinalizeQueue2({ api });
37627
+ const drainPendingFinalizeQueue3 = drainPendingFinalizeQueue;
37628
+ if (typeof drainPendingFinalizeQueue3 === "function") {
37629
+ await drainPendingFinalizeQueue3({ api });
36620
37630
  }
36621
37631
  return;
36622
37632
  }