@synkro-sh/cli 1.7.88 → 1.7.90

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/bootstrap.js CHANGED
@@ -147,7 +147,7 @@ function getIdentity() {
147
147
  if (cached2) return cached2;
148
148
  let cliVersion = "0.0.0";
149
149
  try {
150
- cliVersion = "1.7.88";
150
+ cliVersion = "1.7.90";
151
151
  } catch {
152
152
  }
153
153
  const creds = loadCredentialsIdentity();
@@ -283,6 +283,100 @@ var init_emit = __esm({
283
283
  }
284
284
  });
285
285
 
286
+ // cli/telemetry/httpSql.ts
287
+ import { readFileSync as readFileSync3 } from "fs";
288
+ import { homedir as homedir4 } from "os";
289
+ import { join as join4 } from "path";
290
+ function quoteIdentifier(value) {
291
+ if (!/^[a-z_][a-z0-9_]*$/i.test(value)) throw new Error("invalid SQL identifier");
292
+ return `"${value}"`;
293
+ }
294
+ function shiftPlaceholders(text, offset) {
295
+ return text.replace(/\$(\d+)/g, (_, raw) => `$${Number(raw) + offset}`);
296
+ }
297
+ function bulkFragment(rows, columns) {
298
+ if (!rows.length || !columns.length) throw new Error("empty SQL helper");
299
+ const params = [];
300
+ const tuples = rows.map((row) => {
301
+ const placeholders = columns.map((column) => {
302
+ let value = row[column];
303
+ let cast = "";
304
+ if (column === "context" && typeof value === "object" && value !== null) {
305
+ value = JSON.stringify(value);
306
+ cast = "::jsonb";
307
+ }
308
+ params.push(value ?? null);
309
+ return `$${params.length}${cast}`;
310
+ });
311
+ return `(${placeholders.join(", ")})`;
312
+ });
313
+ return {
314
+ [FRAGMENT]: true,
315
+ text: `(${columns.map(quoteIdentifier).join(", ")}) VALUES ${tuples.join(", ")}`,
316
+ params
317
+ };
318
+ }
319
+ function createTelemetryHttpSql(options = {}) {
320
+ const port = options.port ?? Number.parseInt(process.env.SYNKRO_HOST_MCP_PORT || "18931", 10);
321
+ const endpoint = `http://127.0.0.1:${port}/api/local/telemetry/query`;
322
+ const token = options.token ?? readFileSync3(join4(homedir4(), ".synkro", ".mcp-jwt"), "utf-8").trim();
323
+ const fetchImpl = options.fetchImpl ?? fetch;
324
+ const query = async (text, params = []) => {
325
+ const response = await fetchImpl(endpoint, {
326
+ method: "POST",
327
+ headers: {
328
+ Authorization: `Bearer ${token}`,
329
+ "Content-Type": "application/json"
330
+ },
331
+ body: JSON.stringify({ text, params }),
332
+ signal: AbortSignal.timeout(3e4)
333
+ });
334
+ const payload = await response.json().catch(() => ({}));
335
+ if (!response.ok) throw new Error(payload.error || `local telemetry query failed (${response.status})`);
336
+ const rows = Array.isArray(payload.rows) ? payload.rows : [];
337
+ Object.defineProperty(rows, "count", {
338
+ value: Number(payload.count ?? rows.length),
339
+ enumerable: false,
340
+ configurable: true
341
+ });
342
+ return rows;
343
+ };
344
+ const sql = function(first, ...values) {
345
+ if (Array.isArray(first) && "raw" in first) {
346
+ const strings = first;
347
+ let text = strings[0];
348
+ const params = [];
349
+ for (let i = 0; i < values.length; i++) {
350
+ const value = values[i];
351
+ if (value && typeof value === "object" && value[FRAGMENT]) {
352
+ text += shiftPlaceholders(value.text, params.length);
353
+ params.push(...value.params);
354
+ } else {
355
+ params.push(value);
356
+ text += `$${params.length}`;
357
+ }
358
+ text += strings[i + 1];
359
+ }
360
+ return query(text, params);
361
+ }
362
+ if (Array.isArray(first)) {
363
+ return bulkFragment(first, values);
364
+ }
365
+ throw new Error("unsupported SQL invocation");
366
+ };
367
+ sql.unsafe = query;
368
+ sql.end = async () => {
369
+ };
370
+ return sql;
371
+ }
372
+ var FRAGMENT;
373
+ var init_httpSql = __esm({
374
+ "cli/telemetry/httpSql.ts"() {
375
+ "use strict";
376
+ FRAGMENT = /* @__PURE__ */ Symbol("synkro-http-sql-fragment");
377
+ }
378
+ });
379
+
286
380
  // cli/telemetry/db.ts
287
381
  var db_exports = {};
288
382
  __export(db_exports, {
@@ -290,24 +384,11 @@ __export(db_exports, {
290
384
  connectDb: () => connectDb,
291
385
  resetDbForTests: () => resetDbForTests
292
386
  });
293
- import postgres from "postgres";
294
387
  async function connectDb() {
295
388
  if (cached3) return cached3;
296
389
  if (initFailedAt && Date.now() - initFailedAt < INIT_RETRY_MS) return null;
297
390
  try {
298
- const sql = postgres({
299
- host: PGLITE_HOST,
300
- port: PGLITE_PORT,
301
- username: PGLITE_USER,
302
- database: PGLITE_DB,
303
- // max:1 mirrors synkro-server — PGlite is single-threaded; multiple
304
- // sockets just queue at the multiplexer.
305
- max: 1,
306
- connect_timeout: 2,
307
- idle_timeout: 30,
308
- onnotice: () => {
309
- }
310
- });
391
+ const sql = createTelemetryHttpSql();
311
392
  await sql`SELECT 1`;
312
393
  cached3 = sql;
313
394
  initFailedAt = 0;
@@ -317,7 +398,7 @@ async function connectDb() {
317
398
  initFailedAt = Date.now();
318
399
  if (process.env.SYNKRO_TELEMETRY_DEBUG === "1") {
319
400
  const msg = err instanceof Error ? err.message : String(err);
320
- process.stderr.write(`[synkro] telemetry pglite unreachable at ${PGLITE_HOST}:${PGLITE_PORT}: ${msg}
401
+ process.stderr.write(`[synkro] local telemetry API unreachable: ${msg}
321
402
  `);
322
403
  }
323
404
  return null;
@@ -354,17 +435,11 @@ function resetDbForTests() {
354
435
  initFailedAt = 0;
355
436
  migrated = false;
356
437
  }
357
- var PGLITE_HOST, PGLITE_PORT, PGLITE_USER, PGLITE_DB, SCHEMA_VERSION, cached3, migrated, INIT_RETRY_MS, initFailedAt, MIGRATION_V1;
438
+ var SCHEMA_VERSION, cached3, migrated, INIT_RETRY_MS, initFailedAt, MIGRATION_V1;
358
439
  var init_db = __esm({
359
440
  "cli/telemetry/db.ts"() {
360
441
  "use strict";
361
- PGLITE_HOST = process.env.SYNKRO_PGLITE_HOST || "127.0.0.1";
362
- PGLITE_PORT = parseInt(
363
- process.env.SYNKRO_PGLITE_PORT || process.env.SYNKRO_HOST_PGLITE_PORT || "15433",
364
- 10
365
- );
366
- PGLITE_USER = process.env.SYNKRO_PGLITE_USER || "postgres";
367
- PGLITE_DB = process.env.SYNKRO_PGLITE_DB || "postgres";
442
+ init_httpSql();
368
443
  SCHEMA_VERSION = 1;
369
444
  cached3 = null;
370
445
  migrated = false;
@@ -438,9 +513,9 @@ var init_db = __esm({
438
513
  });
439
514
 
440
515
  // cli/telemetry/drain.ts
441
- import { openSync, closeSync, fstatSync, readFileSync as readFileSync3, writeFileSync as writeFileSync2, appendFileSync as appendFileSync2, unlinkSync, mkdirSync as mkdirSync3, renameSync as renameSync2, existsSync as existsSync4 } from "fs";
442
- import { homedir as homedir4 } from "os";
443
- import { join as join4 } from "path";
516
+ import { openSync, closeSync, fstatSync, readFileSync as readFileSync4, writeFileSync as writeFileSync2, appendFileSync as appendFileSync2, unlinkSync, mkdirSync as mkdirSync3, renameSync as renameSync2, existsSync as existsSync4 } from "fs";
517
+ import { homedir as homedir5 } from "os";
518
+ import { join as join5 } from "path";
444
519
  function ensureDir3() {
445
520
  if (!existsSync4(SYNKRO_DIR3)) mkdirSync3(SYNKRO_DIR3, { recursive: true, mode: 448 });
446
521
  }
@@ -494,7 +569,7 @@ function parseQueueFile(path) {
494
569
  if (!existsSync4(path)) return { rows: [], malformed: 0 };
495
570
  let raw;
496
571
  try {
497
- raw = readFileSync3(path, "utf-8");
572
+ raw = readFileSync4(path, "utf-8");
498
573
  } catch {
499
574
  return { rows: [], malformed: 0 };
500
575
  }
@@ -534,7 +609,7 @@ function recoverProcessing() {
534
609
  ensureDir3();
535
610
  let body;
536
611
  try {
537
- body = readFileSync3(PROCESSING_PATH, "utf-8");
612
+ body = readFileSync4(PROCESSING_PATH, "utf-8");
538
613
  } catch {
539
614
  return;
540
615
  }
@@ -663,39 +738,41 @@ async function insertEvents(sql, rows, mirroredIds) {
663
738
  return inserted;
664
739
  }
665
740
  async function drainToPglite() {
666
- const { rows } = readQueue();
667
- if (rows.length === 0) return { ok: true, ingested: 0, pending: 0, reason: "no_pending" };
741
+ const queued = readQueue().rows;
742
+ if (queued.length === 0) return { ok: true, ingested: 0, pending: 0, reason: "no_pending" };
743
+ const rows = queued.length > MIRROR_MAX_PER_DRAIN ? queued.slice(-MIRROR_MAX_PER_DRAIN) : queued;
668
744
  const sql = await connectDb();
669
- if (!sql) return { ok: false, ingested: 0, pending: rows.length, reason: "db_unavailable" };
745
+ if (!sql) return { ok: false, ingested: 0, pending: queued.length, reason: "db_unavailable" };
670
746
  try {
671
747
  const ingested = await insertEvents(sql, rows);
672
748
  patchMetaCache({ last_drained_at: (/* @__PURE__ */ new Date()).toISOString() });
673
- return { ok: true, ingested, pending: rows.length };
749
+ return { ok: true, ingested, pending: queued.length };
674
750
  } catch (err) {
675
751
  const msg = err instanceof Error ? err.message : String(err);
676
- return { ok: false, ingested: 0, pending: rows.length, reason: "error", error: msg };
752
+ return { ok: false, ingested: 0, pending: queued.length, reason: "error", error: msg };
677
753
  }
678
754
  }
679
- var SYNKRO_DIR3, QUEUE_LOCK, PROCESSING_PATH, CHUNK, STALE_LOCK_MS, MAX_QUEUE_ROWS;
755
+ var SYNKRO_DIR3, QUEUE_LOCK, PROCESSING_PATH, CHUNK, MIRROR_MAX_PER_DRAIN, STALE_LOCK_MS, MAX_QUEUE_ROWS;
680
756
  var init_drain = __esm({
681
757
  "cli/telemetry/drain.ts"() {
682
758
  "use strict";
683
759
  init_db();
684
760
  init_emit();
685
761
  init_metaCache();
686
- SYNKRO_DIR3 = join4(homedir4(), ".synkro");
687
- QUEUE_LOCK = join4(SYNKRO_DIR3, ".telemetry-queue.lock");
762
+ SYNKRO_DIR3 = join5(homedir5(), ".synkro");
763
+ QUEUE_LOCK = join5(SYNKRO_DIR3, ".telemetry-queue.lock");
688
764
  PROCESSING_PATH = `${PENDING_PATH}.processing`;
689
765
  CHUNK = 500;
766
+ MIRROR_MAX_PER_DRAIN = 5e3;
690
767
  STALE_LOCK_MS = 6e4;
691
768
  MAX_QUEUE_ROWS = 5e4;
692
769
  }
693
770
  });
694
771
 
695
772
  // cli/telemetry/optout.ts
696
- import { chmodSync, existsSync as existsSync5, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "fs";
697
- import { homedir as homedir5 } from "os";
698
- import { join as join5 } from "path";
773
+ import { chmodSync, existsSync as existsSync5, readFileSync as readFileSync5, writeFileSync as writeFileSync3 } from "fs";
774
+ import { homedir as homedir6 } from "os";
775
+ import { join as join6 } from "path";
699
776
  function sanitize(raw, maxLen = 256) {
700
777
  return raw.replace(/[^\x20-\x7E]/g, "").slice(0, maxLen);
701
778
  }
@@ -708,7 +785,7 @@ function writeConfigEnvFlag(key, value) {
708
785
  let content = "";
709
786
  if (existsSync5(CONFIG_PATH2)) {
710
787
  try {
711
- content = readFileSync4(CONFIG_PATH2, "utf-8");
788
+ content = readFileSync5(CONFIG_PATH2, "utf-8");
712
789
  } catch {
713
790
  content = "";
714
791
  }
@@ -778,8 +855,8 @@ var init_optout = __esm({
778
855
  "use strict";
779
856
  init_emit();
780
857
  init_metaCache();
781
- SYNKRO_DIR4 = join5(homedir5(), ".synkro");
782
- CONFIG_PATH2 = join5(SYNKRO_DIR4, "config.env");
858
+ SYNKRO_DIR4 = join6(homedir6(), ".synkro");
859
+ CONFIG_PATH2 = join6(SYNKRO_DIR4, "config.env");
783
860
  KEY_ENABLED = "SYNKRO_TELEMETRY_ENABLED";
784
861
  KEY_REMOTE = "SYNKRO_TELEMETRY_REMOTE_FLUSH";
785
862
  }
@@ -828,15 +905,15 @@ __export(flush_exports, {
828
905
  flushDetached: () => flushDetached
829
906
  });
830
907
  import { spawn } from "child_process";
831
- import { existsSync as existsSync6, readFileSync as readFileSync5 } from "fs";
832
- import { homedir as homedir6 } from "os";
833
- import { join as join6 } from "path";
908
+ import { existsSync as existsSync6, readFileSync as readFileSync6 } from "fs";
909
+ import { homedir as homedir7 } from "os";
910
+ import { join as join7 } from "path";
834
911
  function readConfigEnv() {
835
912
  if (!existsSync6(CONFIG_PATH3)) return {};
836
913
  const out = {};
837
914
  let raw;
838
915
  try {
839
- raw = readFileSync5(CONFIG_PATH3, "utf-8");
916
+ raw = readFileSync6(CONFIG_PATH3, "utf-8");
840
917
  } catch {
841
918
  return {};
842
919
  }
@@ -877,14 +954,14 @@ function resolveGatewayUrl() {
877
954
  function loadJwt() {
878
955
  if ((process.env.SYNKRO_STORAGE_MODE || "") === "cloud") {
879
956
  try {
880
- const mcp = readFileSync5(join6(SYNKRO_DIR5, ".mcp-jwt"), "utf-8").trim();
957
+ const mcp = readFileSync6(join7(SYNKRO_DIR5, ".mcp-jwt"), "utf-8").trim();
881
958
  if (mcp) return mcp;
882
959
  } catch {
883
960
  }
884
961
  }
885
962
  try {
886
963
  if (!existsSync6(CREDS_PATH2)) return null;
887
- const creds = JSON.parse(readFileSync5(CREDS_PATH2, "utf-8"));
964
+ const creds = JSON.parse(readFileSync6(CREDS_PATH2, "utf-8"));
888
965
  return creds.access_token || null;
889
966
  } catch {
890
967
  return null;
@@ -1039,20 +1116,20 @@ var init_flush = __esm({
1039
1116
  init_optout();
1040
1117
  init_metaCache();
1041
1118
  init_wire();
1042
- SYNKRO_DIR5 = join6(homedir6(), ".synkro");
1043
- CONFIG_PATH3 = join6(SYNKRO_DIR5, "config.env");
1044
- CREDS_PATH2 = process.env.SYNKRO_CREDENTIALS_PATH || join6(SYNKRO_DIR5, "credentials.json");
1119
+ SYNKRO_DIR5 = join7(homedir7(), ".synkro");
1120
+ CONFIG_PATH3 = join7(SYNKRO_DIR5, "config.env");
1121
+ CREDS_PATH2 = process.env.SYNKRO_CREDENTIALS_PATH || join7(SYNKRO_DIR5, "credentials.json");
1045
1122
  THROTTLE_MS = 6e4;
1046
1123
  DEFAULT_GATEWAY = "https://api.synkro.sh";
1047
1124
  }
1048
1125
  });
1049
1126
 
1050
1127
  // cli/telemetry/stats.ts
1051
- import { existsSync as existsSync7, readFileSync as readFileSync6, statSync, writeFileSync as writeFileSync4 } from "fs";
1128
+ import { existsSync as existsSync7, readFileSync as readFileSync7, statSync, writeFileSync as writeFileSync4 } from "fs";
1052
1129
  function countPending() {
1053
1130
  if (!existsSync7(PENDING_PATH)) return 0;
1054
1131
  try {
1055
- const raw = readFileSync6(PENDING_PATH, "utf-8");
1132
+ const raw = readFileSync7(PENDING_PATH, "utf-8");
1056
1133
  if (!raw) return 0;
1057
1134
  let n = 0;
1058
1135
  for (const line of raw.split("\n")) if (line.trim()) n++;
@@ -1174,13 +1251,13 @@ var init_stats = __esm({
1174
1251
  });
1175
1252
 
1176
1253
  // cli/telemetry/purge.ts
1177
- import { existsSync as existsSync8, readFileSync as readFileSync7, unlinkSync as unlinkSync2 } from "fs";
1254
+ import { existsSync as existsSync8, readFileSync as readFileSync8, unlinkSync as unlinkSync2 } from "fs";
1178
1255
  import { randomUUID as randomUUID3 } from "crypto";
1179
1256
  function unlinkPendingCount() {
1180
1257
  if (!existsSync8(PENDING_PATH)) return 0;
1181
1258
  let n = 0;
1182
1259
  try {
1183
- const raw = readFileSync7(PENDING_PATH, "utf-8");
1260
+ const raw = readFileSync8(PENDING_PATH, "utf-8");
1184
1261
  for (const line of raw.split("\n")) if (line.trim()) n++;
1185
1262
  } catch {
1186
1263
  }
@@ -1345,8 +1422,8 @@ var init_telemetry = __esm({
1345
1422
 
1346
1423
  // cli/installer/agentDetect.ts
1347
1424
  import { existsSync as existsSync9 } from "fs";
1348
- import { homedir as homedir7 } from "os";
1349
- import { join as join7 } from "path";
1425
+ import { homedir as homedir8 } from "os";
1426
+ import { join as join8 } from "path";
1350
1427
  import { execFileSync as execFileSync2, execSync } from "child_process";
1351
1428
  function which(cmd3) {
1352
1429
  try {
@@ -1367,21 +1444,21 @@ function getVersion(cmd3) {
1367
1444
  }
1368
1445
  function detectAgents() {
1369
1446
  const agents = [];
1370
- const home = homedir7();
1447
+ const home = homedir8();
1371
1448
  const claudeBinary = which("claude");
1372
- const claudeConfigDir = join7(home, ".claude");
1449
+ const claudeConfigDir = join8(home, ".claude");
1373
1450
  if (claudeBinary) {
1374
1451
  agents.push({
1375
1452
  kind: "claude_code",
1376
1453
  name: "Claude Code",
1377
1454
  binaryPath: claudeBinary,
1378
1455
  configDir: claudeConfigDir,
1379
- settingsPath: join7(claudeConfigDir, "settings.json"),
1456
+ settingsPath: join8(claudeConfigDir, "settings.json"),
1380
1457
  version: getVersion("claude")
1381
1458
  });
1382
1459
  }
1383
1460
  const cursorBinary = which("cursor");
1384
- const cursorConfigDir = join7(home, ".cursor");
1461
+ const cursorConfigDir = join8(home, ".cursor");
1385
1462
  const cursorApp = process.platform === "darwin" && existsSync9("/Applications/Cursor.app");
1386
1463
  if (cursorBinary || cursorApp || existsSync9(cursorConfigDir)) {
1387
1464
  let version;
@@ -1399,19 +1476,19 @@ function detectAgents() {
1399
1476
  name: "Cursor",
1400
1477
  binaryPath: cursorBinary,
1401
1478
  configDir: cursorConfigDir,
1402
- settingsPath: join7(cursorConfigDir, "hooks.json"),
1479
+ settingsPath: join8(cursorConfigDir, "hooks.json"),
1403
1480
  version
1404
1481
  });
1405
1482
  }
1406
1483
  const codexBinary = which("codex");
1407
- const codexConfigDir = process.env.CODEX_HOME || join7(home, ".codex");
1484
+ const codexConfigDir = process.env.CODEX_HOME || join8(home, ".codex");
1408
1485
  if (codexBinary || existsSync9(codexConfigDir)) {
1409
1486
  agents.push({
1410
1487
  kind: "codex",
1411
1488
  name: "Codex",
1412
1489
  binaryPath: codexBinary,
1413
1490
  configDir: codexConfigDir,
1414
- settingsPath: join7(codexConfigDir, "hooks.json"),
1491
+ settingsPath: join8(codexConfigDir, "hooks.json"),
1415
1492
  version: codexBinary ? getVersion("codex") : void 0
1416
1493
  });
1417
1494
  }
@@ -1425,16 +1502,16 @@ var init_agentDetect = __esm({
1425
1502
 
1426
1503
  // cli/installer/platform.ts
1427
1504
  import { existsSync as existsSync10 } from "fs";
1428
- import { homedir as homedir8 } from "os";
1429
- import { join as join8 } from "path";
1505
+ import { homedir as homedir9 } from "os";
1506
+ import { join as join9 } from "path";
1430
1507
  import { spawnSync } from "child_process";
1431
1508
  function resolveBunBin() {
1432
1509
  const finder = IS_WINDOWS ? "where" : "which";
1433
1510
  const r = spawnSync(finder, ["bun"], { encoding: "utf-8", timeout: 5e3 });
1434
1511
  const resolved = (r.stdout || "").split(/\r?\n/).map((s) => s.trim()).find(Boolean);
1435
1512
  if (resolved) return resolved;
1436
- const home = process.env.USERPROFILE || homedir8();
1437
- const candidates = IS_WINDOWS ? [join8(home, ".bun", "bin", "bun.exe"), "C:\\Program Files\\bun\\bin\\bun.exe"] : ["/opt/homebrew/bin/bun", "/usr/local/bin/bun", join8(home, ".bun", "bin", "bun")];
1513
+ const home = process.env.USERPROFILE || homedir9();
1514
+ const candidates = IS_WINDOWS ? [join9(home, ".bun", "bin", "bun.exe"), "C:\\Program Files\\bun\\bin\\bun.exe"] : ["/opt/homebrew/bin/bun", "/usr/local/bin/bun", join9(home, ".bun", "bin", "bun")];
1438
1515
  for (const p of candidates) if (existsSync10(p)) return p;
1439
1516
  return IS_WINDOWS ? "bun.exe" : "bun";
1440
1517
  }
@@ -1458,12 +1535,12 @@ var init_platform = __esm({
1458
1535
  });
1459
1536
 
1460
1537
  // cli/installer/ccHookConfig.ts
1461
- import { existsSync as existsSync11, readFileSync as readFileSync8, writeFileSync as writeFileSync6, renameSync as renameSync3, mkdirSync as mkdirSync4 } from "fs";
1538
+ import { existsSync as existsSync11, readFileSync as readFileSync9, writeFileSync as writeFileSync6, renameSync as renameSync3, mkdirSync as mkdirSync4 } from "fs";
1462
1539
  import { dirname } from "path";
1463
1540
  function readSettings(path) {
1464
1541
  if (!existsSync11(path)) return {};
1465
1542
  try {
1466
- const raw = readFileSync8(path, "utf-8");
1543
+ const raw = readFileSync9(path, "utf-8");
1467
1544
  return JSON.parse(raw);
1468
1545
  } catch (err) {
1469
1546
  throw new Error(`Failed to parse ${path}: ${err.message}`);
@@ -1609,7 +1686,7 @@ function installCCHooks(settingsPath, config) {
1609
1686
  }
1610
1687
  if (config.taskActivateIntentScriptPath) {
1611
1688
  settings.hooks.PreToolUse.push({
1612
- matcher: "mcp__synkro-guardrails__activate_standard",
1689
+ matcher: "mcp__synkro[-_]guardrails__activate_standard",
1613
1690
  hooks: [
1614
1691
  {
1615
1692
  type: "command",
@@ -1723,9 +1800,9 @@ var init_ccHookConfig = __esm({
1723
1800
  });
1724
1801
 
1725
1802
  // cli/installer/cursorHookConfig.ts
1726
- import { readFileSync as readFileSync9, writeFileSync as writeFileSync7, renameSync as renameSync4, mkdirSync as mkdirSync5 } from "fs";
1803
+ import { readFileSync as readFileSync10, writeFileSync as writeFileSync7, renameSync as renameSync4, mkdirSync as mkdirSync5 } from "fs";
1727
1804
  import { dirname as dirname2, resolve, normalize } from "path";
1728
- import { homedir as homedir9 } from "os";
1805
+ import { homedir as homedir10 } from "os";
1729
1806
  function shellQuote(s) {
1730
1807
  return "'" + s.replace(/'/g, "'\\''") + "'";
1731
1808
  }
@@ -1745,7 +1822,7 @@ function validateHooksPath(path) {
1745
1822
  function readHooksFile(rawPath) {
1746
1823
  const safePath = validateHooksPath(rawPath);
1747
1824
  try {
1748
- const raw = readFileSync9(safePath, "utf-8");
1825
+ const raw = readFileSync10(safePath, "utf-8");
1749
1826
  return JSON.parse(raw);
1750
1827
  } catch (err) {
1751
1828
  if (err?.code === "ENOENT") return { version: 1, hooks: {} };
@@ -1927,8 +2004,8 @@ var init_cursorHookConfig = __esm({
1927
2004
  init_platform();
1928
2005
  SYNKRO_MARKER2 = "__synkro_managed__";
1929
2006
  ALLOWED_PARENT_DIRS = [
1930
- resolve(homedir9(), ".cursor"),
1931
- resolve(homedir9(), ".config", "cursor")
2007
+ resolve(homedir10(), ".cursor"),
2008
+ resolve(homedir10(), ".config", "cursor")
1932
2009
  ];
1933
2010
  ALL_EVENTS = [
1934
2011
  "sessionStart",
@@ -1948,9 +2025,9 @@ var init_cursorHookConfig = __esm({
1948
2025
  });
1949
2026
 
1950
2027
  // cli/installer/codexHookConfig.ts
1951
- import { existsSync as existsSync12, readFileSync as readFileSync10, writeFileSync as writeFileSync8, renameSync as renameSync5, mkdirSync as mkdirSync6 } from "fs";
2028
+ import { existsSync as existsSync12, readFileSync as readFileSync11, writeFileSync as writeFileSync8, renameSync as renameSync5, mkdirSync as mkdirSync6 } from "fs";
1952
2029
  import { dirname as dirname3, resolve as resolve2, normalize as normalize2 } from "path";
1953
- import { homedir as homedir10 } from "os";
2030
+ import { homedir as homedir11 } from "os";
1954
2031
  function shellQuote2(s) {
1955
2032
  return "'" + s.replace(/'/g, "'\\''") + "'";
1956
2033
  }
@@ -1967,8 +2044,8 @@ function codexCmd(scriptPath) {
1967
2044
  function validateHooksPath2(path) {
1968
2045
  const resolved = resolve2(normalize2(path));
1969
2046
  const allowedParentDirs = [
1970
- resolve2(homedir10(), ".codex"),
1971
- resolve2(homedir10(), ".config", "codex"),
2047
+ resolve2(homedir11(), ".codex"),
2048
+ resolve2(homedir11(), ".config", "codex"),
1972
2049
  ...process.env.CODEX_HOME ? [resolve2(process.env.CODEX_HOME)] : []
1973
2050
  ];
1974
2051
  if (!allowedParentDirs.some((dir) => resolved.startsWith(dir + "/") || resolved === dir)) {
@@ -1980,7 +2057,7 @@ function readHooksFile2(rawPath) {
1980
2057
  const safePath = validateHooksPath2(rawPath);
1981
2058
  if (!existsSync12(safePath)) return { hooks: {} };
1982
2059
  try {
1983
- return JSON.parse(readFileSync10(safePath, "utf-8"));
2060
+ return JSON.parse(readFileSync11(safePath, "utf-8"));
1984
2061
  } catch (err) {
1985
2062
  throw new Error(`Failed to parse ${safePath}: ${err.message}`);
1986
2063
  }
@@ -2026,15 +2103,25 @@ function installCodexHooks(hooksJsonPath, config) {
2026
2103
  cmd(config.cvePrecheckScriptPath, 15),
2027
2104
  ...config.skillJudgeScriptPath ? [cmd(config.skillJudgeScriptPath, 50)] : []
2028
2105
  ], M_EDIT);
2106
+ push(h, "PostToolUse", [
2107
+ cmd(config.editFollowupScriptPath, 30, "Recording edit")
2108
+ ], M_EDIT);
2029
2109
  push(h, "PreToolUse", [cmd(config.agentJudgeScriptPath, 50)], M_AGENT);
2030
2110
  if (config.mcpGateScriptPath) push(h, "PreToolUse", [cmd(config.mcpGateScriptPath, 50)], M_MCP);
2031
2111
  if (config.taskActivateIntentScriptPath) push(h, "PreToolUse", [cmd(config.taskActivateIntentScriptPath, 5)], M_ACTIVATE);
2112
+ if (config.taskActivateIntentScriptPath) push(h, "PermissionRequest", [cmd(config.taskActivateIntentScriptPath, 5)], M_ACTIVATE);
2032
2113
  push(h, "PostToolUse", [cmd(config.bashFollowupScriptPath, 10)], M_BASH);
2033
2114
  push(h, "UserPromptSubmit", [cmd(config.userPromptSubmitScriptPath, 5)]);
2034
2115
  if (config.promptRouteScriptPath) push(h, "UserPromptSubmit", [cmd(config.promptRouteScriptPath, 5)]);
2035
2116
  push(h, "SessionStart", [cmd(config.sessionStartScriptPath, 5)]);
2117
+ push(h, "SubagentStart", [cmd(config.subagentStartScriptPath, 5)]);
2118
+ if (!config.skipTranscriptSync) push(h, "SubagentStop", [cmd(config.subagentStopScriptPath, 5)]);
2036
2119
  push(h, "SessionEnd", [cmd(config.stopSummaryScriptPath, 3)]);
2037
- if (!config.skipTranscriptSync) push(h, "Stop", [cmd(config.transcriptSyncScriptPath, 3)]);
2120
+ push(h, "Stop", [
2121
+ cmd(config.cweStopScriptPath, 50, "Checking completed edits for weaknesses"),
2122
+ cmd(config.cveStopScriptPath, 50, "Checking completed edits for vulnerabilities"),
2123
+ ...!config.skipTranscriptSync ? [cmd(config.transcriptSyncScriptPath, 3)] : []
2124
+ ]);
2038
2125
  writeHooksFileAtomic2(hooksJsonPath, file);
2039
2126
  }
2040
2127
  function uninstallCodexHooks(hooksJsonPath) {
@@ -2054,30 +2141,31 @@ function uninstallCodexHooks(hooksJsonPath) {
2054
2141
  writeHooksFileAtomic2(hooksJsonPath, file);
2055
2142
  return true;
2056
2143
  }
2057
- var SYNKRO_MARKER3, M_BASH, M_EDIT, M_AGENT, M_MCP, M_ACTIVATE, ALL_EVENTS2;
2144
+ var SYNKRO_MARKER3, M_BASH, CODEX_EDIT_MATCHER, M_EDIT, M_AGENT, M_MCP, M_ACTIVATE, ALL_EVENTS2;
2058
2145
  var init_codexHookConfig = __esm({
2059
2146
  "cli/installer/codexHookConfig.ts"() {
2060
2147
  "use strict";
2061
2148
  init_platform();
2062
2149
  SYNKRO_MARKER3 = "__synkro_managed__";
2063
2150
  M_BASH = "Bash";
2064
- M_EDIT = "apply_patch|Edit|Write";
2151
+ CODEX_EDIT_MATCHER = "^(?:apply_patch|ApplyPatch|Edit|Write|functions[._]apply_patch)$";
2152
+ M_EDIT = CODEX_EDIT_MATCHER;
2065
2153
  M_AGENT = "Agent";
2066
2154
  M_MCP = "mcp__.*";
2067
- M_ACTIVATE = "mcp__synkro-guardrails__activate_standard";
2068
- ALL_EVENTS2 = ["PreToolUse", "PostToolUse", "UserPromptSubmit", "SessionStart", "SessionEnd", "Stop"];
2155
+ M_ACTIVATE = "mcp__synkro[-_]guardrails__activate_standard";
2156
+ ALL_EVENTS2 = ["PreToolUse", "PermissionRequest", "PostToolUse", "UserPromptSubmit", "SessionStart", "SessionEnd", "SubagentStart", "SubagentStop", "Stop"];
2069
2157
  }
2070
2158
  });
2071
2159
 
2072
2160
  // cli/installer/mcpConfig.ts
2073
- import { existsSync as existsSync13, readFileSync as readFileSync11, writeFileSync as writeFileSync9, renameSync as renameSync6, mkdirSync as mkdirSync7 } from "fs";
2074
- import { homedir as homedir11 } from "os";
2075
- import { dirname as dirname4, join as join9 } from "path";
2161
+ import { existsSync as existsSync13, readFileSync as readFileSync12, writeFileSync as writeFileSync9, renameSync as renameSync6, mkdirSync as mkdirSync7 } from "fs";
2162
+ import { homedir as homedir12 } from "os";
2163
+ import { dirname as dirname4, join as join10 } from "path";
2076
2164
  import { randomBytes } from "crypto";
2077
2165
  function readClaudeJson() {
2078
2166
  if (!existsSync13(CC_CONFIG_PATH)) return {};
2079
2167
  try {
2080
- const raw = readFileSync11(CC_CONFIG_PATH, "utf-8");
2168
+ const raw = readFileSync12(CC_CONFIG_PATH, "utf-8");
2081
2169
  return JSON.parse(raw);
2082
2170
  } catch (err) {
2083
2171
  throw new Error(`Failed to parse ${CC_CONFIG_PATH}: ${err.message}`);
@@ -2096,7 +2184,7 @@ function installMcpConfig(opts) {
2096
2184
  if (entry?.[SYNKRO_MARKER4] === true) delete config.mcpServers[name];
2097
2185
  }
2098
2186
  if (opts.local) {
2099
- const proxyScript = join9(homedir11(), ".synkro", "hooks", "mcp-stdio-proxy.ts");
2187
+ const proxyScript = join10(homedir12(), ".synkro", "hooks", "mcp-stdio-proxy.ts");
2100
2188
  config.mcpServers[SYNKRO_SERVER_NAME] = {
2101
2189
  type: "stdio",
2102
2190
  command: "bun",
@@ -2135,7 +2223,7 @@ function uninstallMcpConfig() {
2135
2223
  function readCursorMcpJson() {
2136
2224
  if (!existsSync13(CURSOR_MCP_PATH)) return {};
2137
2225
  try {
2138
- const raw = readFileSync11(CURSOR_MCP_PATH, "utf-8");
2226
+ const raw = readFileSync12(CURSOR_MCP_PATH, "utf-8");
2139
2227
  return JSON.parse(raw);
2140
2228
  } catch (err) {
2141
2229
  throw new Error(`Failed to parse ${CURSOR_MCP_PATH}: ${err.message}`);
@@ -2156,10 +2244,10 @@ function installCursorMcpConfig(opts) {
2156
2244
  if (opts.local) {
2157
2245
  const port = process.env.SYNKRO_MCP_PORT || "18931";
2158
2246
  const url2 = `http://127.0.0.1:${port}/`;
2159
- const jwtPath = join9(homedir11(), ".synkro", ".mcp-jwt");
2247
+ const jwtPath = join10(homedir12(), ".synkro", ".mcp-jwt");
2160
2248
  let jwt2 = "";
2161
2249
  try {
2162
- jwt2 = readFileSync11(jwtPath, "utf-8").trim();
2250
+ jwt2 = readFileSync12(jwtPath, "utf-8").trim();
2163
2251
  } catch {
2164
2252
  }
2165
2253
  config.mcpServers[SYNKRO_SERVER_NAME] = {
@@ -2201,7 +2289,7 @@ function codexConfigPath(path) {
2201
2289
  function readCodexToml(path) {
2202
2290
  if (!existsSync13(path)) return "";
2203
2291
  try {
2204
- return readFileSync11(path, "utf-8");
2292
+ return readFileSync12(path, "utf-8");
2205
2293
  } catch (err) {
2206
2294
  throw new Error(`Failed to read ${path}: ${err.message}`);
2207
2295
  }
@@ -2290,7 +2378,7 @@ function resolveBunBin2() {
2290
2378
  function readDesktopJson() {
2291
2379
  if (!existsSync13(CLAUDE_DESKTOP_CONFIG_PATH)) return {};
2292
2380
  try {
2293
- return JSON.parse(readFileSync11(CLAUDE_DESKTOP_CONFIG_PATH, "utf-8"));
2381
+ return JSON.parse(readFileSync12(CLAUDE_DESKTOP_CONFIG_PATH, "utf-8"));
2294
2382
  } catch (err) {
2295
2383
  throw new Error(`Failed to parse ${CLAUDE_DESKTOP_CONFIG_PATH}: ${err.message}`);
2296
2384
  }
@@ -2347,31 +2435,31 @@ var init_mcpConfig = __esm({
2347
2435
  init_platform();
2348
2436
  SYNKRO_MARKER4 = "__synkro_managed__";
2349
2437
  SYNKRO_SERVER_NAME = "synkro-guardrails";
2350
- CC_CONFIG_PATH = join9(homedir11(), ".claude.json");
2351
- CURSOR_MCP_PATH = join9(homedir11(), ".cursor", "mcp.json");
2352
- CODEX_CONFIG_PATH = join9(process.env.CODEX_HOME || join9(homedir11(), ".codex"), "config.toml");
2438
+ CC_CONFIG_PATH = join10(homedir12(), ".claude.json");
2439
+ CURSOR_MCP_PATH = join10(homedir12(), ".cursor", "mcp.json");
2440
+ CODEX_CONFIG_PATH = join10(process.env.CODEX_HOME || join10(homedir12(), ".codex"), "config.toml");
2353
2441
  CODEX_MCP_BEGIN = "# >>> synkro managed MCP: synkro-guardrails";
2354
2442
  CODEX_MCP_END = "# <<< synkro managed MCP: synkro-guardrails";
2355
2443
  CODEX_MCP_SECTION = 'mcp_servers."synkro-guardrails"';
2356
- CLAUDE_DESKTOP_CONFIG_PATH = join9(
2357
- homedir11(),
2444
+ CLAUDE_DESKTOP_CONFIG_PATH = join10(
2445
+ homedir12(),
2358
2446
  "Library",
2359
2447
  "Application Support",
2360
2448
  "Claude",
2361
2449
  "claude_desktop_config.json"
2362
2450
  );
2363
- MCP_STDIO_PROXY_PATH = join9(homedir11(), ".synkro", "hooks", "mcp-stdio-proxy.ts");
2451
+ MCP_STDIO_PROXY_PATH = join10(homedir12(), ".synkro", "hooks", "mcp-stdio-proxy.ts");
2364
2452
  }
2365
2453
  });
2366
2454
 
2367
2455
  // cli/installer/synkroCommand.ts
2368
- import { existsSync as existsSync14, mkdirSync as mkdirSync8, readFileSync as readFileSync12, writeFileSync as writeFileSync10, unlinkSync as unlinkSync4 } from "fs";
2369
- import { homedir as homedir12 } from "os";
2370
- import { join as join10 } from "path";
2456
+ import { existsSync as existsSync14, mkdirSync as mkdirSync8, readFileSync as readFileSync13, writeFileSync as writeFileSync10, unlinkSync as unlinkSync4 } from "fs";
2457
+ import { homedir as homedir13 } from "os";
2458
+ import { join as join11 } from "path";
2371
2459
  function installSynkroCommand() {
2372
2460
  try {
2373
2461
  if (existsSync14(COMMAND_PATH)) {
2374
- const current = readFileSync12(COMMAND_PATH, "utf-8");
2462
+ const current = readFileSync13(COMMAND_PATH, "utf-8");
2375
2463
  if (!current.includes(MARKER)) return null;
2376
2464
  }
2377
2465
  mkdirSync8(COMMANDS_DIR, { recursive: true });
@@ -2384,7 +2472,7 @@ function installSynkroCommand() {
2384
2472
  function uninstallSynkroCommand() {
2385
2473
  try {
2386
2474
  if (!existsSync14(COMMAND_PATH)) return false;
2387
- const current = readFileSync12(COMMAND_PATH, "utf-8");
2475
+ const current = readFileSync13(COMMAND_PATH, "utf-8");
2388
2476
  if (!current.includes(MARKER)) return false;
2389
2477
  unlinkSync4(COMMAND_PATH);
2390
2478
  return true;
@@ -2397,8 +2485,8 @@ var init_synkroCommand = __esm({
2397
2485
  "cli/installer/synkroCommand.ts"() {
2398
2486
  "use strict";
2399
2487
  MARKER = "__synkro_managed__";
2400
- COMMANDS_DIR = join10(homedir12(), ".claude", "commands");
2401
- COMMAND_PATH = join10(COMMANDS_DIR, "synkro.md");
2488
+ COMMANDS_DIR = join11(homedir13(), ".claude", "commands");
2489
+ COMMAND_PATH = join11(COMMANDS_DIR, "synkro.md");
2402
2490
  COMMAND_BODY = `<!-- ${MARKER} -->
2403
2491
  ---
2404
2492
  description: Set up Synkro \u2014 create a starter rule set, review suggestions, and watch a live block
@@ -2456,14 +2544,14 @@ var init_skillParser = __esm({
2456
2544
  function stubHook(surface, optsLiteral) {
2457
2545
  return "#!/usr/bin/env bun\nimport { runStub } from './_synkro-stub-common.ts';\nrunStub(" + JSON.stringify(surface) + ", " + optsLiteral + ");\n";
2458
2546
  }
2459
- var STUB_COMMON_TS, STUB_EDIT_PRECHECK_TS, STUB_CWE_PRECHECK_TS, STUB_CVE_PRECHECK_TS, STUB_BASH_JUDGE_TS, STUB_SKILL_JUDGE_TS, STUB_INSTALL_SCAN_TS, STUB_AGENT_JUDGE_TS, STUB_MCP_GATE_TS, STUB_PLAN_JUDGE_TS, STUB_STOP_SUMMARY_TS, STUB_SESSION_START_TS, STUB_TRANSCRIPT_SYNC_TS, STUB_USER_PROMPT_SUBMIT_TS, STUB_BASH_FOLLOWUP_TS, STUB_PROMPT_ROUTE_TS, STUB_TASK_ACTIVATE_INTENT_TS, STUB_CURSOR_BASH_JUDGE_TS, STUB_CURSOR_SKILL_JUDGE_TS, STUB_CURSOR_EDIT_CAPTURE_TS, STUB_CURSOR_AGENT_CAPTURE_TS;
2547
+ var STUB_COMMON_TS, STUB_EDIT_PRECHECK_TS, STUB_EDIT_FOLLOWUP_TS, STUB_CWE_PRECHECK_TS, STUB_CVE_PRECHECK_TS, STUB_BASH_JUDGE_TS, STUB_SKILL_JUDGE_TS, STUB_INSTALL_SCAN_TS, STUB_AGENT_JUDGE_TS, STUB_MCP_GATE_TS, STUB_PLAN_JUDGE_TS, STUB_STOP_SUMMARY_TS, STUB_SESSION_START_TS, STUB_TRANSCRIPT_SYNC_TS, STUB_CODEX_CWE_STOP_TS, STUB_CODEX_CVE_STOP_TS, STUB_SUBAGENT_START_TS, STUB_SUBAGENT_STOP_TS, STUB_USER_PROMPT_SUBMIT_TS, STUB_BASH_FOLLOWUP_TS, STUB_PROMPT_ROUTE_TS, STUB_TASK_ACTIVATE_INTENT_TS, STUB_CURSOR_BASH_JUDGE_TS, STUB_CURSOR_SKILL_JUDGE_TS, STUB_CURSOR_EDIT_CAPTURE_TS, STUB_CURSOR_AGENT_CAPTURE_TS;
2460
2548
  var init_hookScriptsTs = __esm({
2461
2549
  "cli/installer/hookScriptsTs.ts"() {
2462
2550
  "use strict";
2463
- STUB_COMMON_TS = String.raw`import { existsSync, readFileSync, mkdirSync, writeFileSync, appendFileSync, statSync, openSync, readSync, closeSync } from 'node:fs';
2551
+ STUB_COMMON_TS = String.raw`import { existsSync, readFileSync, readdirSync, mkdirSync, writeFileSync, appendFileSync, statSync, realpathSync, openSync, readSync, closeSync } from 'node:fs';
2464
2552
  import { execSync } from 'node:child_process';
2465
2553
  import { homedir } from 'node:os';
2466
- import { join, resolve, isAbsolute } from 'node:path';
2554
+ import { basename, join, resolve, relative, isAbsolute } from 'node:path';
2467
2555
  import { randomUUID, createHash } from 'node:crypto';
2468
2556
 
2469
2557
  const HOME = homedir();
@@ -2622,13 +2710,17 @@ async function cloudMcpGate(payload: any, harness: string): Promise<string> {
2622
2710
  try {
2623
2711
  const toolName = String(payload.tool_name || payload.tool || '');
2624
2712
  const parts = toolName.split('__');
2625
- const server = (parts.length >= 3 && parts[0] === 'mcp') ? parts[1] : '';
2713
+ const input = payload.tool_input && typeof payload.tool_input === 'object' ? payload.tool_input : {};
2714
+ const server = (parts.length >= 3 && parts[0] === 'mcp')
2715
+ ? parts[1]
2716
+ : String(payload.server_name || payload.serverName || payload.server || payload.mcp_server
2717
+ || input.server_name || input.serverName || input.server || input.mcp_server || '');
2626
2718
  if (!server) return failOpen(harness);
2627
2719
  const base = (cfgVal('SYNKRO_GATEWAY_URL') || 'https://api.synkro.sh').replace(/\/+$/, '');
2628
2720
  const resp = await fetch(base + '/api/mcp/gate', {
2629
2721
  method: 'POST',
2630
2722
  headers: { 'Content-Type': 'application/json', Authorization: 'Bearer ' + loadMcpJwt() },
2631
- body: JSON.stringify({ server: server, tool: parts.slice(2).join('__') }),
2723
+ body: JSON.stringify({ server: server, tool: parts.slice(2).join('__') || toolName, harness: harness }),
2632
2724
  signal: AbortSignal.timeout(5000),
2633
2725
  });
2634
2726
  if (!resp.ok) return failOpen(harness);
@@ -2664,6 +2756,28 @@ function failOpen(harness: string): string {
2664
2756
  return harness === 'cursor' ? '{"permission":"allow"}' : '{}';
2665
2757
  }
2666
2758
 
2759
+ /** Convert an existing security PreToolUse verdict into Codex's Stop contract. */
2760
+ export function codexStopResponse(responseText: string): string {
2761
+ try {
2762
+ const parsed = JSON.parse(responseText || '{}');
2763
+ const specific = parsed?.hookSpecificOutput;
2764
+ if (specific?.permissionDecision !== 'deny') return '{}';
2765
+ const reason = String(
2766
+ specific.permissionDecisionReason
2767
+ || specific.additionalContext
2768
+ || parsed.systemMessage
2769
+ || 'A completed edit contains a CWE weakness. Fix it before stopping.',
2770
+ );
2771
+ return JSON.stringify({
2772
+ decision: 'block',
2773
+ reason,
2774
+ ...(parsed.systemMessage ? { systemMessage: parsed.systemMessage } : {}),
2775
+ });
2776
+ } catch {
2777
+ return '{}';
2778
+ }
2779
+ }
2780
+
2667
2781
  function out(s: string): void { try { process.stdout.write(s + '\n'); } catch {} }
2668
2782
 
2669
2783
  function gitRoot(cwd: string): string {
@@ -2725,48 +2839,327 @@ function gatherBaseContent(fp: string, ti: any): string | undefined {
2725
2839
  return '\n'.repeat(linesBefore) + full.slice(start, end);
2726
2840
  }
2727
2841
 
2842
+ function gatherPreEditContentFromPost(fp: string, toolName: string, ti: any, operation: string): string | undefined {
2843
+ if (operation === 'add') return '';
2844
+ if (operation === 'delete') {
2845
+ const patch = String(ti?.patch || '');
2846
+ const removed: string[] = [];
2847
+ for (const line of patch.split('\n')) {
2848
+ if (line.startsWith('*** ') || line.startsWith('@@')) continue;
2849
+ if (line.startsWith('-')) removed.push(line.slice(1));
2850
+ }
2851
+ return removed.length ? removed.join('\n') : undefined;
2852
+ }
2853
+ let full = '';
2854
+ try { full = readFileSync(fp, 'utf-8'); } catch { return undefined; }
2855
+ if (toolName === 'Edit') {
2856
+ const oldString = String(ti?.old_string || '');
2857
+ const newString = String(ti?.new_string || '');
2858
+ if (newString && full.includes(newString)) return full.replace(newString, oldString);
2859
+ return undefined;
2860
+ }
2861
+ if (toolName === 'MultiEdit' && Array.isArray(ti?.edits)) {
2862
+ let before = full;
2863
+ for (const edit of [...ti.edits].reverse()) {
2864
+ const oldString = String(edit?.old_string || '');
2865
+ const newString = String(edit?.new_string || '');
2866
+ if (!newString || !before.includes(newString)) return undefined;
2867
+ before = before.replace(newString, oldString);
2868
+ }
2869
+ return before;
2870
+ }
2871
+ return full;
2872
+ }
2873
+
2728
2874
  // Codex edits arrive as the apply_patch tool with the patch text in
2729
2875
  // tool_input.command (lines: "*** Update/Add/Delete File: PATH", then hunks where
2730
2876
  // "+" adds, "-" removes, " " is context). The edit hooks expect the CC shape
2731
- // {file_path, old_string, new_string}; translate once so filePathFromToolInput,
2732
- // editAnchorString, and gatherBaseContent all work unchanged. A patch spanning
2733
- // more than one file or hunk cannot be reconstructed faithfully as one Edit, so
2734
- // mark it for a clear deny-and-retry response instead of grading partial content.
2877
+ // {file_path, old_string, new_string}; translate every file and hunk into an
2878
+ // explicit edit batch so one apply_patch call never drops evidence.
2735
2879
  export function normalizeCodexApplyPatch(payload: any): void {
2736
2880
  try {
2737
- if (!/apply_patch/i.test(String((payload && payload.tool_name) || ''))) return;
2881
+ if (!/^(?:apply_?patch|functions[._]apply_?patch)$/i.test(String((payload && payload.tool_name) || ''))) return;
2738
2882
  const ti = payload.tool_input;
2739
2883
  const patch = (ti && typeof ti === 'object')
2740
2884
  ? String(ti.command || ti.patch || ti.input || '')
2741
2885
  : (typeof ti === 'string' ? ti : '');
2742
2886
  if (!patch) return;
2743
- const files: string[] = [];
2744
- let hunkCount = 0;
2745
- const oldLines: string[] = [];
2746
- const newLines: string[] = [];
2887
+ type Hunk = { oldLines: string[]; newLines: string[] };
2888
+ type PatchFile = {
2889
+ operation: 'Add' | 'Update' | 'Delete';
2890
+ path: string;
2891
+ moveTarget: string;
2892
+ hunks: Hunk[];
2893
+ fragment: string[];
2894
+ };
2895
+ const files: PatchFile[] = [];
2896
+ let current: PatchFile | null = null;
2897
+ let hunk: Hunk | null = null;
2898
+ const flushHunk = () => {
2899
+ if (current && hunk && (hunk.oldLines.length || hunk.newLines.length)) current.hunks.push(hunk);
2900
+ hunk = null;
2901
+ };
2902
+ const flushFile = () => {
2903
+ flushHunk();
2904
+ if (current) files.push(current);
2905
+ current = null;
2906
+ };
2747
2907
  for (const ln of patch.split('\n')) {
2748
2908
  const mFile = ln.match(/^\*\*\*\s+(?:Update|Add|Delete)\s+File:\s+(.+?)\s*$/);
2749
- if (mFile) { files.push(mFile[1].trim()); continue; }
2750
- if (ln.indexOf('@@') === 0) { hunkCount++; continue; }
2909
+ if (mFile) {
2910
+ flushFile();
2911
+ const operation = ln.match(/^\*\*\*\s+(Update|Add|Delete)\s+File:/)?.[1] as 'Add' | 'Update' | 'Delete';
2912
+ current = {
2913
+ operation,
2914
+ path: mFile[1].trim(),
2915
+ moveTarget: '',
2916
+ hunks: [],
2917
+ fragment: [ln],
2918
+ };
2919
+ continue;
2920
+ }
2921
+ if (!current) continue;
2922
+ current.fragment.push(ln);
2923
+ const mMove = ln.match(/^\*\*\*\s+Move to:\s+(.+?)\s*$/);
2924
+ if (mMove) { current.moveTarget = mMove[1].trim(); continue; }
2925
+ if (ln.indexOf('@@') === 0) {
2926
+ flushHunk();
2927
+ hunk = { oldLines: [], newLines: [] };
2928
+ const anchor = ln.slice(2).trim();
2929
+ if (anchor) { hunk.oldLines.push(anchor); hunk.newLines.push(anchor); }
2930
+ continue;
2931
+ }
2751
2932
  if (ln.indexOf('*** ') === 0) continue;
2933
+ hunk = hunk || { oldLines: [], newLines: [] };
2752
2934
  const c = ln.charAt(0);
2753
- if (c === '+') newLines.push(ln.slice(1));
2754
- else if (c === '-') oldLines.push(ln.slice(1));
2755
- else if (c === ' ') { oldLines.push(ln.slice(1)); newLines.push(ln.slice(1)); }
2935
+ if (c === '+') hunk.newLines.push(ln.slice(1));
2936
+ else if (c === '-') hunk.oldLines.push(ln.slice(1));
2937
+ else if (c === ' ') { hunk.oldLines.push(ln.slice(1)); hunk.newLines.push(ln.slice(1)); }
2756
2938
  }
2757
- if (files.length !== 1 || hunkCount > 1) {
2939
+ flushFile();
2940
+ if (!files.length) {
2758
2941
  payload.__synkro_codex_patch_error =
2759
- 'Synkro needs Codex edits split into one file and one contiguous hunk. Retry this apply_patch as separate edits.';
2942
+ 'Synkro could not recover a file edit from this apply_patch payload.';
2760
2943
  return;
2761
2944
  }
2762
- if (!files[0]) return;
2763
2945
  const cwd = typeof payload.cwd === 'string' ? payload.cwd : '';
2764
- const filePath = !isAbsolute(files[0]) && cwd ? resolve(cwd, files[0]) : files[0];
2765
- payload.tool_name = 'Edit';
2766
- payload.tool_input = { file_path: filePath, old_string: oldLines.join('\n'), new_string: newLines.join('\n') };
2946
+ const normalized = files.map((file) => {
2947
+ const filePath = !isAbsolute(file.path) && cwd ? resolve(cwd, file.path) : file.path;
2948
+ const patchFragment = file.fragment.join('\n');
2949
+ if (file.operation === 'Add') {
2950
+ return {
2951
+ tool_name: 'Write',
2952
+ tool_input: {
2953
+ file_path: filePath,
2954
+ content: file.hunks.flatMap((item) => item.newLines).join('\n'),
2955
+ patch: patchFragment,
2956
+ },
2957
+ __synkro_codex_operation: 'add',
2958
+ };
2959
+ }
2960
+ if (file.operation === 'Delete') {
2961
+ return {
2962
+ tool_name: 'Write',
2963
+ tool_input: { file_path: filePath, content: '', patch: patchFragment },
2964
+ __synkro_codex_operation: 'delete',
2965
+ };
2966
+ }
2967
+ const edits = file.hunks.map((item) => ({
2968
+ old_string: item.oldLines.join('\n'),
2969
+ new_string: item.newLines.join('\n'),
2970
+ }));
2971
+ return {
2972
+ tool_name: edits.length > 1 ? 'MultiEdit' : 'Edit',
2973
+ tool_input: edits.length > 1
2974
+ ? { file_path: filePath, edits, patch: patchFragment }
2975
+ : { file_path: filePath, ...(edits[0] || { old_string: '', new_string: '' }), patch: patchFragment },
2976
+ __synkro_codex_operation: file.moveTarget ? 'rename' : 'modify',
2977
+ ...(file.moveTarget ? { __synkro_codex_move_target: file.moveTarget } : {}),
2978
+ };
2979
+ });
2980
+ payload.__synkro_codex_edits = normalized;
2981
+ Object.assign(payload, normalized[0]);
2767
2982
  } catch { /* leave payload untouched on any parse error */ }
2768
2983
  }
2769
2984
 
2985
+ type RecoveredCodexEdit = { payload: Record<string, any>; baseContent?: string };
2986
+
2987
+ function completedCodexToolRecord(entry: any): any {
2988
+ if (!entry || typeof entry !== 'object') return null;
2989
+ if (entry.type === 'response_item' && entry.payload && typeof entry.payload === 'object') {
2990
+ return entry.payload;
2991
+ }
2992
+ return entry.payload && typeof entry.payload === 'object' ? entry.payload : entry;
2993
+ }
2994
+
2995
+ function completedCodexToolInput(record: any): any {
2996
+ const raw = record?.input ?? record?.arguments ?? record?.command;
2997
+ if (raw && typeof raw === 'object') return raw;
2998
+ if (typeof raw !== 'string') return {};
2999
+ try {
3000
+ const parsed = JSON.parse(raw);
3001
+ if (parsed && typeof parsed === 'object') return parsed;
3002
+ if (typeof parsed === 'string') return { command: parsed };
3003
+ } catch {}
3004
+ return { command: raw };
3005
+ }
3006
+
3007
+ function containedCodexEditPath(repoRoot: string, filePath: string): string {
3008
+ if (!repoRoot || !filePath) return '';
3009
+ try {
3010
+ const lexicalRoot = resolve(repoRoot);
3011
+ const realRoot = realpathSync(lexicalRoot);
3012
+ const lexical = isAbsolute(filePath) ? resolve(filePath) : resolve(lexicalRoot, filePath);
3013
+ const isContained = (rel: string): boolean =>
3014
+ !!rel && rel !== '..' && !rel.startsWith('..' + '/') && !isAbsolute(rel);
3015
+ // Accept an absolute path expressed through either the lexical repo alias
3016
+ // (/var on macOS, a symlinked checkout) or its physical path, but always
3017
+ // continue from the physical root so new and existing files share one
3018
+ // canonical provenance path.
3019
+ let rel = relative(lexicalRoot, lexical);
3020
+ if (!isContained(rel)) {
3021
+ rel = relative(realRoot, lexical);
3022
+ if (!isContained(rel)) return '';
3023
+ }
3024
+ let candidate = resolve(realRoot, rel);
3025
+ // realpathSync requires the leaf to exist. Walk to the nearest existing
3026
+ // ancestor, resolve symlinks there, then append the missing suffix. This
3027
+ // both canonicalizes new files and rejects a new leaf beneath an in-repo
3028
+ // symlink that actually escapes the repository.
3029
+ const missing: string[] = [];
3030
+ let ancestor = candidate;
3031
+ while (!existsSync(ancestor)) {
3032
+ const parent = resolve(ancestor, '..');
3033
+ if (parent === ancestor) return '';
3034
+ missing.unshift(basename(ancestor));
3035
+ ancestor = parent;
3036
+ }
3037
+ candidate = resolve(realpathSync(ancestor), ...missing);
3038
+ const realRel = relative(realRoot, candidate);
3039
+ if (!realRel || realRel.startsWith('..') || isAbsolute(realRel)) return '';
3040
+ return existsSync(candidate) ? realpathSync(candidate) : candidate;
3041
+ } catch {
3042
+ return '';
3043
+ }
3044
+ }
3045
+
3046
+ /**
3047
+ * Recover completed Codex apply_patch calls from a bounded transcript tail.
3048
+ * A tool call is evidence only after a matching tool output exists. Calls are
3049
+ * expanded one event per file, restricted to the repo, and keyed by the stable
3050
+ * Codex call id so replay is idempotent server-side.
3051
+ */
3052
+ export function extractCompletedCodexPatchEdits(
3053
+ transcript: string,
3054
+ repoRoot: string,
3055
+ ): RecoveredCodexEdit[] {
3056
+ if (!transcript || !repoRoot) return [];
3057
+ const calls = new Map<string, { record: any; timestamp: string }>();
3058
+ const completed = new Set<string>();
3059
+ for (const line of transcript.split('\n')) {
3060
+ if (!line.trim()) continue;
3061
+ let entry: any;
3062
+ try { entry = JSON.parse(line); } catch { continue; }
3063
+ const record = completedCodexToolRecord(entry);
3064
+ const type = String(record?.type || '');
3065
+ const callId = String(record?.call_id || record?.tool_call_id || record?.id || '');
3066
+ if (!callId) continue;
3067
+ if (type === 'custom_tool_call_output' || type === 'function_call_output') {
3068
+ completed.add(callId);
3069
+ continue;
3070
+ }
3071
+ if (type !== 'custom_tool_call' && type !== 'function_call') continue;
3072
+ const name = String(record?.name || record?.tool_name || '');
3073
+ if (!/^(?:apply_?patch|functions[._]apply_?patch)$/i.test(name)) continue;
3074
+ calls.set(callId, {
3075
+ record,
3076
+ timestamp: String(entry?.timestamp || record?.timestamp || ''),
3077
+ });
3078
+ }
3079
+
3080
+ const results: RecoveredCodexEdit[] = [];
3081
+ const seen = new Set<string>();
3082
+ const candidates = [...calls.entries()].filter(([callId]) => completed.has(callId)).slice(-100);
3083
+ for (const [callId, call] of candidates) {
3084
+ const toolInput = completedCodexToolInput(call.record);
3085
+ const patch = String(toolInput?.command || toolInput?.patch || toolInput?.input || '');
3086
+ if (!patch || patch.length > 100000) continue;
3087
+ const raw: any = {
3088
+ cwd: repoRoot,
3089
+ tool_name: String(call.record?.name || call.record?.tool_name || 'apply_patch'),
3090
+ tool_input: toolInput,
3091
+ tool_call_id: callId,
3092
+ _ts: call.timestamp || undefined,
3093
+ };
3094
+ normalizeCodexApplyPatch(raw);
3095
+ const edits = Array.isArray(raw.__synkro_codex_edits) ? raw.__synkro_codex_edits : [];
3096
+ for (const edit of edits) {
3097
+ if (results.length >= 50) return results;
3098
+ const fp = containedCodexEditPath(repoRoot, filePathFromToolInput(edit?.tool_input));
3099
+ if (!fp) continue;
3100
+ const dedupeKey = callId + '\u0000' + fp;
3101
+ if (seen.has(dedupeKey)) continue;
3102
+ seen.add(dedupeKey);
3103
+ const payload: any = {
3104
+ cwd: repoRoot,
3105
+ tool_name: edit.tool_name,
3106
+ tool_input: { ...(edit.tool_input || {}), file_path: fp },
3107
+ tool_call_id: callId,
3108
+ _ts: call.timestamp || undefined,
3109
+ __synkro_codex_operation: edit.__synkro_codex_operation,
3110
+ __synkro_codex_move_target: edit.__synkro_codex_move_target,
3111
+ __synkro_edit_source: 'transcript',
3112
+ };
3113
+ const baseContent = gatherPreEditContentFromPost(
3114
+ fp,
3115
+ String(payload.tool_name || ''),
3116
+ payload.tool_input,
3117
+ String(payload.__synkro_codex_operation || ''),
3118
+ );
3119
+ if (baseContent === undefined) continue;
3120
+ results.push({
3121
+ payload,
3122
+ baseContent,
3123
+ });
3124
+ }
3125
+ }
3126
+ return results;
3127
+ }
3128
+
3129
+ /**
3130
+ * Recover only the latest completed apply_patch for each file in the current
3131
+ * Codex turn. Stop hooks may fire repeatedly while an agent self-corrects, so
3132
+ * scanning the latest on-disk state avoids re-blocking an earlier vulnerable
3133
+ * patch after a later patch fixed it.
3134
+ */
3135
+ export function extractCompletedCodexTurnPatchEdits(
3136
+ transcript: string,
3137
+ repoRoot: string,
3138
+ ): RecoveredCodexEdit[] {
3139
+ if (!transcript || !repoRoot) return [];
3140
+ const lines = transcript.split('\n');
3141
+ let latestUserLine = -1;
3142
+ for (let index = 0; index < lines.length; index++) {
3143
+ if (!lines[index].trim()) continue;
3144
+ let entry: any;
3145
+ try { entry = JSON.parse(lines[index]); } catch { continue; }
3146
+ const canonicalUser = entry?.type === 'event_msg'
3147
+ && entry?.payload?.type === 'user_message';
3148
+ const compatibilityUser = entry?.type === 'response_item'
3149
+ && entry?.payload?.type === 'message'
3150
+ && entry?.payload?.role === 'user';
3151
+ if (canonicalUser || compatibilityUser) latestUserLine = index;
3152
+ }
3153
+ const turnTranscript = lines.slice(latestUserLine + 1).join('\n');
3154
+ const recovered = extractCompletedCodexPatchEdits(turnTranscript, repoRoot);
3155
+ const latestByFile = new Map<string, RecoveredCodexEdit>();
3156
+ for (const item of recovered) {
3157
+ const filePath = filePathFromToolInput(item.payload?.tool_input);
3158
+ if (filePath) latestByFile.set(filePath, item);
3159
+ }
3160
+ return [...latestByFile.values()];
3161
+ }
3162
+
2770
3163
  interface StubOpts {
2771
3164
  needsFile?: boolean;
2772
3165
  needsTranscript?: boolean;
@@ -2776,6 +3169,119 @@ interface StubOpts {
2776
3169
  needsPlan?: boolean;
2777
3170
  telemetry?: boolean;
2778
3171
  harness?: string;
3172
+ postEdit?: boolean;
3173
+ /** Recover only this Codex turn's latest completed patch per file. */
3174
+ codexTurnEdits?: boolean;
3175
+ /** Translate a security PreToolUse deny into Codex's Stop block response. */
3176
+ stopContract?: boolean;
3177
+ /** Codex SubagentStart/SubagentStop report the parent as session_id and
3178
+ * the child as agent_id. Remap them to the ordinary child-session envelope. */
3179
+ subagent?: boolean;
3180
+ }
3181
+
3182
+ function isContainedFile(root: string, candidate: string): boolean {
3183
+ try {
3184
+ const rel = relative(root, candidate);
3185
+ return !!rel && !rel.startsWith('..') && !isAbsolute(rel) && statSync(candidate).isFile();
3186
+ } catch { return false; }
3187
+ }
3188
+
3189
+ function safeCodexSessionId(value: string): boolean {
3190
+ if (!value || value.length > 200) return false;
3191
+ for (const ch of value) {
3192
+ const code = ch.charCodeAt(0);
3193
+ const alphaNumeric = (code >= 48 && code <= 57)
3194
+ || (code >= 65 && code <= 90)
3195
+ || (code >= 97 && code <= 122);
3196
+ if (!alphaNumeric && ch !== '-' && ch !== '_') return false;
3197
+ }
3198
+ return true;
3199
+ }
3200
+
3201
+ function readJsonlHeader(path: string): string {
3202
+ let fd = -1;
3203
+ try {
3204
+ fd = openSync(path, 'r');
3205
+ const buf = Buffer.alloc(16384);
3206
+ const count = readSync(fd, buf, 0, buf.length, 0);
3207
+ const head = buf.subarray(0, count).toString('utf-8');
3208
+ const newline = head.indexOf('\n');
3209
+ return newline >= 0 ? head.slice(0, newline) : head;
3210
+ } catch { return ''; }
3211
+ finally { if (fd >= 0) { try { closeSync(fd); } catch {} } }
3212
+ }
3213
+
3214
+ /**
3215
+ * Codex hook payloads identify the rollout by session_id but do not always send
3216
+ * transcript_path. Resolve that ID only within CODEX_HOME/sessions (or the
3217
+ * default ~/.codex/sessions), then cache the exact contained file so subsequent
3218
+ * tool hooks do not recursively scan the rollout tree.
3219
+ */
3220
+ export function resolveHookTranscriptPath(payload: any, harness: string, sessionId: string): string {
3221
+ const explicit = [
3222
+ payload?.transcript_path,
3223
+ payload?.rollout_path,
3224
+ payload?.session_path,
3225
+ ].find((value) => typeof value === 'string' && value);
3226
+ if (typeof explicit === 'string') return explicit;
3227
+ if (harness !== 'codex' || !safeCodexSessionId(sessionId)) return '';
3228
+
3229
+ const homeRoots = [
3230
+ process.env.CODEX_HOME ? resolve(process.env.CODEX_HOME) : '',
3231
+ resolve(join(HOME, '.codex')),
3232
+ ].filter((value, index, all) => !!value && all.indexOf(value) === index);
3233
+ const sessionRoots = homeRoots.map((root) => join(root, 'sessions')).filter((root) => existsSync(root));
3234
+ if (!sessionRoots.length) return '';
3235
+
3236
+ const cachePath = join(
3237
+ HOME,
3238
+ '.synkro',
3239
+ 'codex-transcript-' + createHash('sha256').update(sessionId).digest('hex').slice(0, 16),
3240
+ );
3241
+ try {
3242
+ const cached = resolve(readFileSync(cachePath, 'utf-8').trim());
3243
+ if (sessionRoots.some((root) => isContainedFile(root, cached))) return cached;
3244
+ } catch {}
3245
+
3246
+ const candidates: string[] = [];
3247
+ for (const root of sessionRoots) {
3248
+ let entries: string[] = [];
3249
+ try { entries = readdirSync(root, { recursive: true, encoding: 'utf-8' }) as string[]; } catch {}
3250
+ for (const entry of entries) {
3251
+ if (!entry.endsWith('.jsonl')) continue;
3252
+ const candidate = resolve(join(root, entry));
3253
+ if (isContainedFile(root, candidate)) candidates.push(candidate);
3254
+ }
3255
+ }
3256
+
3257
+ let found = candidates.find((candidate) => candidate.includes(sessionId)) || '';
3258
+ if (!found) {
3259
+ const recent = candidates
3260
+ .map((candidate) => {
3261
+ try { return { candidate, mtime: statSync(candidate).mtimeMs }; }
3262
+ catch { return { candidate, mtime: 0 }; }
3263
+ })
3264
+ .sort((a, b) => b.mtime - a.mtime)
3265
+ .slice(0, 100);
3266
+ for (const item of recent) {
3267
+ try {
3268
+ const first = JSON.parse(readJsonlHeader(item.candidate));
3269
+ const candidateId = String(first?.payload?.session_id || first?.payload?.id || '');
3270
+ if (first?.type === 'session_meta' && candidateId === sessionId) {
3271
+ found = item.candidate;
3272
+ break;
3273
+ }
3274
+ } catch {}
3275
+ }
3276
+ }
3277
+
3278
+ if (found) {
3279
+ try {
3280
+ mkdirSync(join(HOME, '.synkro'), { recursive: true });
3281
+ writeFileSync(cachePath, found, { encoding: 'utf-8', mode: 0o600 });
3282
+ } catch {}
3283
+ }
3284
+ return found;
2779
3285
  }
2780
3286
 
2781
3287
  export async function runStub(surface: string, opts: StubOpts = {}): Promise<void> {
@@ -2792,6 +3298,17 @@ export async function runStub(surface: string, opts: StubOpts = {}): Promise<voi
2792
3298
  // Codex: rewrite apply_patch tool_input into the CC edit shape before any
2793
3299
  // surface reads it (edit/cwe/cve). No-op for CC/Cursor and non-edit tools.
2794
3300
  if (harness === 'codex') normalizeCodexApplyPatch(payload);
3301
+ if (harness === 'codex' && opts.subagent) {
3302
+ const parentSessionId = String(payload?.session_id || '');
3303
+ const childSessionId = String(payload?.agent_id || '');
3304
+ if (parentSessionId && childSessionId) {
3305
+ payload.parent_session_id = parentSessionId;
3306
+ payload.session_id = childSessionId;
3307
+ if (!payload.transcript_path && payload.agent_transcript_path) {
3308
+ payload.transcript_path = payload.agent_transcript_path;
3309
+ }
3310
+ }
3311
+ }
2795
3312
  telemPayload = payload;
2796
3313
  telemCwd = (typeof payload?.cwd === 'string' ? payload.cwd : '') || '';
2797
3314
  telemSessionId = String(payload?.session_id || payload?.conversation_id || '');
@@ -2862,14 +3379,40 @@ export async function runStub(surface: string, opts: StubOpts = {}): Promise<voi
2862
3379
  const envelope: any = { payload, harness, cwd: root || cwd, sessionId, synkroFileText };
2863
3380
 
2864
3381
  if (opts.needsFile) {
2865
- const fp = filePathFromToolInput(payload.tool_input || {});
2866
- if (fp && existsSync(fp)) {
2867
- const bc = gatherBaseContent(fp, payload.tool_input || {});
2868
- if (bc !== undefined) envelope.baseContent = bc;
3382
+ const normalizedEdits = harness === 'codex' && Array.isArray(payload.__synkro_codex_edits)
3383
+ ? payload.__synkro_codex_edits
3384
+ : [{ tool_name: payload.tool_name, tool_input: payload.tool_input }];
3385
+ const editBatch = normalizedEdits.map((edit: any) => {
3386
+ const childPayload = {
3387
+ ...payload,
3388
+ tool_name: edit.tool_name,
3389
+ tool_input: edit.tool_input,
3390
+ __synkro_codex_operation: edit.__synkro_codex_operation,
3391
+ __synkro_codex_move_target: edit.__synkro_codex_move_target,
3392
+ };
3393
+ delete childPayload.__synkro_codex_edits;
3394
+ const fp = filePathFromToolInput(childPayload.tool_input || {});
3395
+ const canRead = fp && (existsSync(fp) || (opts.postEdit && childPayload.__synkro_codex_operation === 'add'));
3396
+ const bc = canRead
3397
+ ? (opts.postEdit
3398
+ ? gatherPreEditContentFromPost(
3399
+ fp,
3400
+ String(childPayload.tool_name || ''),
3401
+ childPayload.tool_input || {},
3402
+ String(childPayload.__synkro_codex_operation || ''),
3403
+ )
3404
+ : gatherBaseContent(fp, childPayload.tool_input || {}))
3405
+ : undefined;
3406
+ return { payload: childPayload, ...(bc !== undefined ? { baseContent: bc } : {}) };
3407
+ });
3408
+ if (editBatch.length) {
3409
+ envelope.editBatch = editBatch;
3410
+ envelope.payload = editBatch[0].payload;
3411
+ if (editBatch[0].baseContent !== undefined) envelope.baseContent = editBatch[0].baseContent;
2869
3412
  }
2870
3413
  }
2871
3414
  if (opts.needsTranscript) {
2872
- const tp = typeof payload.transcript_path === 'string' ? payload.transcript_path : '';
3415
+ const tp = resolveHookTranscriptPath(payload, harness, sessionId);
2873
3416
  if (tp && existsSync(tp)) {
2874
3417
  try {
2875
3418
  const t = readFileSync(tp, 'utf-8');
@@ -2881,9 +3424,34 @@ export async function runStub(surface: string, opts: StubOpts = {}): Promise<voi
2881
3424
  // by ts, so we no longer depend on the whole file for absolute message indices.
2882
3425
  const cap = opts.fullTranscript ? 400000 : 200000;
2883
3426
  envelope.transcript = t.length <= cap ? t : t.slice(t.length - cap);
3427
+ if (harness === 'codex') {
3428
+ const recovered = opts.codexTurnEdits
3429
+ ? extractCompletedCodexTurnPatchEdits(envelope.transcript, root || cwd)
3430
+ : extractCompletedCodexPatchEdits(envelope.transcript, root || cwd);
3431
+ if (recovered.length) {
3432
+ envelope.transcriptEdits = recovered;
3433
+ if (opts.codexTurnEdits) {
3434
+ envelope.editBatch = recovered.map((item) => ({
3435
+ ...item,
3436
+ payload: {
3437
+ ...item.payload,
3438
+ __synkro_post_edit_enforcement: true,
3439
+ },
3440
+ }));
3441
+ envelope.payload = envelope.editBatch[0].payload;
3442
+ envelope.baseContent = envelope.editBatch[0].baseContent;
3443
+ }
3444
+ }
3445
+ }
2884
3446
  } catch {}
2885
3447
  }
2886
3448
  }
3449
+ // No completed apply_patch in this turn means there is nothing for the
3450
+ // fallback to scan. Stay silent and avoid touching the server/database.
3451
+ if (opts.codexTurnEdits && (!Array.isArray(envelope.editBatch) || envelope.editBatch.length === 0)) {
3452
+ out(failOpen(harness));
3453
+ return;
3454
+ }
2887
3455
  if (opts.needsPlan) {
2888
3456
  const ti = payload.tool_input || {};
2889
3457
  envelope.planText = String(ti.plan || ti.content || payload.plan || '');
@@ -2896,7 +3464,8 @@ export async function runStub(surface: string, opts: StubOpts = {}): Promise<voi
2896
3464
  // prompt-submit is the opposite: it's INTERACTIVE (blocks the user's prompt from
2897
3465
  // being sent) under a short 5s hook timeout, so it gets ONE short attempt (below)
2898
3466
  // and a tight per-attempt budget that fits inside 5s — never the 6s×3 telemetry path.
2899
- const timeoutMs = surface === 'bash-followup' ? 32000
3467
+ const timeoutMs = surface === 'cwe-precheck' ? 48000
3468
+ : surface === 'bash-followup' ? 32000
2900
3469
  : surface === 'prompt-submit' ? 3500
2901
3470
  : (opts.telemetry ? 6000 : 28000);
2902
3471
  // Cloud has no local container to reach. Post the SAME envelope to the org's grader
@@ -2937,7 +3506,10 @@ export async function runStub(surface: string, opts: StubOpts = {}): Promise<voi
2937
3506
  } catch (e) { /* connection refused / timeout → retry below */ }
2938
3507
  if (i < attempts - 1) await new Promise((r) => setTimeout(r, 500 * (i + 1)));
2939
3508
  }
2940
- const responseText = text || failOpen(harness);
3509
+ const rawResponseText = text || failOpen(harness);
3510
+ const responseText = opts.stopContract && harness === 'codex'
3511
+ ? codexStopResponse(rawResponseText)
3512
+ : rawResponseText;
2941
3513
  out(responseText);
2942
3514
  emitStubTelemetry(surface, harness, telemPayload, responseText, Date.now() - startedAt, telemCwd, telemSessionId);
2943
3515
  } catch (err) {
@@ -3061,8 +3633,16 @@ function toolInputSummary(toolName: string, toolInput: any, includeCommand: bool
3061
3633
  // cc_model is derived by the caller (bounded tail read) and passed in; this stays
3062
3634
  // allocation-only so the dormant hot path never touches the transcript.
3063
3635
  function telemPayloadOpts(payload: any, harness: string, cwd: string, sessionId: string, ccModel?: string): any {
3636
+ const normalizedHarness = String(harness || '').trim().toLowerCase().replace(/[\\s-]+/g, '_');
3637
+ const agentKind = normalizedHarness === 'cursor'
3638
+ ? 'cursor'
3639
+ : normalizedHarness === 'codex'
3640
+ ? 'codex'
3641
+ : normalizedHarness === 'cc' || normalizedHarness === 'claude_code'
3642
+ ? 'claude_code'
3643
+ : undefined;
3064
3644
  return {
3065
- agent_kind: harness === 'cursor' ? 'cursor' : harness === 'codex' ? 'codex' : 'claude_code',
3645
+ agent_kind: agentKind,
3066
3646
  cc_session_id: sessionId || undefined,
3067
3647
  cc_tool_use_id: payload?.tool_use_id ? String(payload.tool_use_id) : undefined,
3068
3648
  cc_model: ccModel || undefined,
@@ -3164,6 +3744,7 @@ function emitStubTelemetry(
3164
3744
  }
3165
3745
  `;
3166
3746
  STUB_EDIT_PRECHECK_TS = stubHook("edit-precheck", "{ needsFile: true, needsTranscript: true }");
3747
+ STUB_EDIT_FOLLOWUP_TS = stubHook("edit-followup", "{ needsFile: true, needsTranscript: true, postEdit: true }");
3167
3748
  STUB_CWE_PRECHECK_TS = stubHook("cwe-precheck", "{ needsFile: true, needsTranscript: true }");
3168
3749
  STUB_CVE_PRECHECK_TS = stubHook("cve-precheck", "{ needsFile: true, needsTranscript: true }");
3169
3750
  STUB_BASH_JUDGE_TS = stubHook("bash-judge", "{ needsTranscript: true }");
@@ -3175,6 +3756,10 @@ function emitStubTelemetry(
3175
3756
  STUB_STOP_SUMMARY_TS = stubHook("stop-summary", "{ needsTranscript: true, fullTranscript: true }");
3176
3757
  STUB_SESSION_START_TS = stubHook("session-start", "{ telemetry: true }");
3177
3758
  STUB_TRANSCRIPT_SYNC_TS = stubHook("transcript-sync", "{ needsTranscript: true, fullTranscript: true, telemetry: true }");
3759
+ STUB_CODEX_CWE_STOP_TS = stubHook("cwe-precheck", "{ needsTranscript: true, fullTranscript: true, codexTurnEdits: true, stopContract: true }");
3760
+ STUB_CODEX_CVE_STOP_TS = stubHook("cve-precheck", "{ needsTranscript: true, fullTranscript: true, codexTurnEdits: true, stopContract: true }");
3761
+ STUB_SUBAGENT_START_TS = stubHook("subagent-start", "{ telemetry: true, subagent: true }");
3762
+ STUB_SUBAGENT_STOP_TS = stubHook("subagent-stop", "{ needsTranscript: true, fullTranscript: true, telemetry: true, subagent: true }");
3178
3763
  STUB_USER_PROMPT_SUBMIT_TS = stubHook("prompt-submit", "{ telemetry: true }");
3179
3764
  STUB_BASH_FOLLOWUP_TS = stubHook("bash-followup", "{ telemetry: true }");
3180
3765
  STUB_PROMPT_ROUTE_TS = `#!/usr/bin/env bun
@@ -3307,7 +3892,7 @@ try {
3307
3892
  const resp = await fetch('http://127.0.0.1:' + (process.env.SYNKRO_GRADER_HOST_PORT || '18929') + '/submit', {
3308
3893
  method: 'POST',
3309
3894
  headers: { 'Content-Type': 'application/json', Authorization: 'Bearer ' + mcpJwt() },
3310
- body: JSON.stringify({ role: 'route-classify', payload: t.slice(0, 1000), content: 'x', agent_kind: 'claude_code', hedge: false }),
3895
+ body: JSON.stringify({ role: 'route-classify', payload: t.slice(0, 1000), content: 'x', hedge: false }),
3311
3896
  signal: AbortSignal.timeout(1000),
3312
3897
  });
3313
3898
  if (resp.ok) {
@@ -3346,8 +3931,14 @@ const PORT = process.env.SYNKRO_MCP_PORT || '18931';
3346
3931
  function mcpJwt(): string { try { return readFileSync(join(homedir(), '.synkro', '.mcp-jwt'), 'utf-8').trim(); } catch { return ''; } }
3347
3932
  const chunks: Buffer[] = [];
3348
3933
  for await (const chunk of process.stdin) chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
3934
+ let originalToolInput: Record<string, unknown> = {};
3935
+ let hookEventName = 'PreToolUse';
3349
3936
  try {
3350
3937
  const payload = JSON.parse(Buffer.concat(chunks).toString('utf-8') || '{}');
3938
+ hookEventName = String(payload.hook_event_name || payload.hookEventName || 'PreToolUse');
3939
+ if (payload.tool_input && typeof payload.tool_input === 'object' && !Array.isArray(payload.tool_input)) {
3940
+ originalToolInput = payload.tool_input;
3941
+ }
3351
3942
  await fetch('http://127.0.0.1:' + PORT + '/api/local/task-activate-intent', {
3352
3943
  method: 'POST',
3353
3944
  headers: { 'Content-Type': 'application/json', Authorization: 'Bearer ' + mcpJwt() },
@@ -3355,7 +3946,29 @@ try {
3355
3946
  signal: AbortSignal.timeout(5000),
3356
3947
  });
3357
3948
  } catch {}
3358
- process.stdout.write('{}\\n');
3949
+ // PreToolUse enforcement and Codex's native approval are separate lifecycle
3950
+ // stages. The policy gate already allowed this exact activate_standard call;
3951
+ // approve its subsequent PermissionRequest so headless approval_policy=never
3952
+ // can execute it. This script is registered only for the activation matcher,
3953
+ // never as a blanket MCP approval hook.
3954
+ if (process.env.SYNKRO_HOOK_FORMAT === 'codex' && hookEventName === 'PermissionRequest') {
3955
+ process.stdout.write(JSON.stringify({
3956
+ hookSpecificOutput: {
3957
+ hookEventName: 'PermissionRequest',
3958
+ decision: { behavior: 'allow' },
3959
+ },
3960
+ }) + '\\n');
3961
+ } else if (process.env.SYNKRO_HOOK_FORMAT === 'codex') {
3962
+ process.stdout.write(JSON.stringify({
3963
+ hookSpecificOutput: {
3964
+ hookEventName: 'PreToolUse',
3965
+ permissionDecision: 'allow',
3966
+ updatedInput: originalToolInput,
3967
+ },
3968
+ }) + '\\n');
3969
+ } else {
3970
+ process.stdout.write('{}\\n');
3971
+ }
3359
3972
  `;
3360
3973
  STUB_CURSOR_BASH_JUDGE_TS = stubHook("bash-judge", "{ needsTranscript: true, harness: 'cursor' }");
3361
3974
  STUB_CURSOR_SKILL_JUDGE_TS = stubHook("skill-judge", "{ needsFile: true, needsTranscript: true, harness: 'cursor' }");
@@ -3382,9 +3995,9 @@ __export(stub_exports, {
3382
3995
  saveCredentials: () => saveCredentials
3383
3996
  });
3384
3997
  import { createServer } from "http";
3385
- import { writeFileSync as writeFileSync11, readFileSync as readFileSync13, existsSync as existsSync16, mkdirSync as mkdirSync9, unlinkSync as unlinkSync5 } from "fs";
3386
- import { homedir as homedir13, platform as platform2 } from "os";
3387
- import { join as join11, dirname as dirname5 } from "path";
3998
+ import { writeFileSync as writeFileSync11, readFileSync as readFileSync14, existsSync as existsSync16, mkdirSync as mkdirSync9, unlinkSync as unlinkSync5 } from "fs";
3999
+ import { homedir as homedir14, platform as platform2 } from "os";
4000
+ import { join as join12, dirname as dirname5 } from "path";
3388
4001
  import { execFile } from "child_process";
3389
4002
  import jwt from "jsonwebtoken";
3390
4003
  function openBrowser(url) {
@@ -3423,7 +4036,7 @@ function loadCredentials() {
3423
4036
  return null;
3424
4037
  }
3425
4038
  try {
3426
- const content = readFileSync13(AUTH_FILE, "utf8");
4039
+ const content = readFileSync14(AUTH_FILE, "utf8");
3427
4040
  return JSON.parse(content);
3428
4041
  } catch (error) {
3429
4042
  return null;
@@ -3711,7 +4324,7 @@ var init_stub = __esm({
3711
4324
  PORT = 8100;
3712
4325
  RAW_WEB_AUTH_URL = process.env.SYNKRO_WEB_AUTH_URL;
3713
4326
  SYNKRO_WEB_AUTH_URL = RAW_WEB_AUTH_URL && /^https?:\/\//.test(RAW_WEB_AUTH_URL) ? RAW_WEB_AUTH_URL : "https://app.synkro.sh";
3714
- AUTH_FILE = process.env.SYNKRO_AUTH_FILE || join11(homedir13(), ".synkro", "credentials.json");
4327
+ AUTH_FILE = process.env.SYNKRO_AUTH_FILE || join12(homedir14(), ".synkro", "credentials.json");
3715
4328
  RAW_API_URL = process.env.SYNKRO_CRUD_URL || process.env.SYNKRO_API_URL;
3716
4329
  SYNKRO_API_URL = RAW_API_URL && /^https?:\/\//.test(RAW_API_URL) ? RAW_API_URL : "https://api.synkro.sh";
3717
4330
  ERROR_HTML = `
@@ -3948,9 +4561,9 @@ __export(claudeDesktopTap_exports, {
3948
4561
  runClaudeDesktopTap: () => runClaudeDesktopTap
3949
4562
  });
3950
4563
  import { spawn as spawn2 } from "child_process";
3951
- import { writeFileSync as writeFileSync12, mkdtempSync, mkdirSync as mkdirSync10, readFileSync as readFileSync14, existsSync as existsSync17 } from "fs";
3952
- import { join as join12 } from "path";
3953
- import { homedir as homedir14 } from "os";
4564
+ import { writeFileSync as writeFileSync12, mkdtempSync, mkdirSync as mkdirSync10, readFileSync as readFileSync15, existsSync as existsSync17 } from "fs";
4565
+ import { join as join13 } from "path";
4566
+ import { homedir as homedir15 } from "os";
3954
4567
  function claudeDesktopInstalled() {
3955
4568
  return process.platform === "darwin" && existsSync17("/Applications/Claude.app/Contents/MacOS/Claude");
3956
4569
  }
@@ -4066,7 +4679,7 @@ async function runClaudeDesktopTap(opts = {}) {
4066
4679
  }
4067
4680
  let token = "";
4068
4681
  try {
4069
- token = readFileSync14(JWT_PATH, "utf-8").trim();
4682
+ token = readFileSync15(JWT_PATH, "utf-8").trim();
4070
4683
  } catch {
4071
4684
  }
4072
4685
  if (!token) {
@@ -4087,13 +4700,13 @@ async function runClaudeDesktopTap(opts = {}) {
4087
4700
  } catch (err) {
4088
4701
  console.log(` \u26A0 Could not register Claude Desktop MCP: ${err.message}`);
4089
4702
  }
4090
- const cdRoot = join12(homedir14(), ".synkro", "cd-sessions");
4703
+ const cdRoot = join13(homedir15(), ".synkro", "cd-sessions");
4091
4704
  mkdirSync10(cdRoot, { recursive: true });
4092
- const sessionDir = mkdtempSync(join12(cdRoot, "synkro-cd-"));
4093
- writeFileSync12(join12(sessionDir, "tap.py"), ADDON_PY, "utf-8");
4094
- writeFileSync12(join12(sessionDir, "mcp_proxy.py"), MCP_PROXY_PY, { mode: 493 });
4095
- writeFileSync12(join12(sessionDir, "mcp_patch.py"), MCP_PATCH_PY, "utf-8");
4096
- const runnerPath = join12(sessionDir, "run.sh");
4705
+ const sessionDir = mkdtempSync(join13(cdRoot, "synkro-cd-"));
4706
+ writeFileSync12(join13(sessionDir, "tap.py"), ADDON_PY, "utf-8");
4707
+ writeFileSync12(join13(sessionDir, "mcp_proxy.py"), MCP_PROXY_PY, { mode: 493 });
4708
+ writeFileSync12(join13(sessionDir, "mcp_patch.py"), MCP_PATCH_PY, "utf-8");
4709
+ const runnerPath = join13(sessionDir, "run.sh");
4097
4710
  writeFileSync12(runnerPath, buildRunner(sessionDir), { mode: 493 });
4098
4711
  await new Promise((resolve6) => {
4099
4712
  const child = spawn2("bash", [runnerPath], {
@@ -4127,7 +4740,7 @@ var init_claudeDesktopTap = __esm({
4127
4740
  TURN_VERDICTS_URL = "http://127.0.0.1:18931/api/local/turn-verdicts";
4128
4741
  TURN_VERDICT_URL = "http://127.0.0.1:18931/api/local/turn-verdict";
4129
4742
  MCP_EVENT_URL = "http://127.0.0.1:18931/api/local/mcp/event";
4130
- JWT_PATH = join12(homedir14(), ".synkro", ".mcp-jwt");
4743
+ JWT_PATH = join13(homedir15(), ".synkro", ".mcp-jwt");
4131
4744
  ADDON_PY = `import os, json, threading, urllib.request, urllib.error, re, base64, time, hashlib
4132
4745
  from mitmproxy import http
4133
4746
 
@@ -4264,6 +4877,7 @@ def _emit_mcp_event(server, tool, ev_type, eid, convo_id=""):
4264
4877
  "tool_name": (str(tool)[:120] if tool else None),
4265
4878
  "decision": ("allowed" if ev_type == "tool_call" else None),
4266
4879
  "repo": "claude-desktop",
4880
+ "harness": "claude_desktop",
4267
4881
  }).encode("utf-8")
4268
4882
  _post_json(MCP_EVENT_URL, payload, 5)
4269
4883
  except Exception:
@@ -5243,9 +5857,9 @@ __export(macKeychain_exports, {
5243
5857
  writeCursorApiKey: () => writeCursorApiKey,
5244
5858
  writeRefreshAgent: () => writeRefreshAgent
5245
5859
  });
5246
- import { existsSync as existsSync18, mkdirSync as mkdirSync11, writeFileSync as writeFileSync13, chmodSync as chmodSync2, readFileSync as readFileSync15 } from "fs";
5247
- import { homedir as homedir15, platform as platform3 } from "os";
5248
- import { join as join13 } from "path";
5860
+ import { existsSync as existsSync18, mkdirSync as mkdirSync11, writeFileSync as writeFileSync13, chmodSync as chmodSync2, readFileSync as readFileSync16 } from "fs";
5861
+ import { homedir as homedir16, platform as platform3 } from "os";
5862
+ import { join as join14 } from "path";
5249
5863
  import { spawnSync as spawnSync2 } from "child_process";
5250
5864
  function needsKeychainBridge() {
5251
5865
  return platform3() === "darwin";
@@ -5268,7 +5882,7 @@ function exportKeychainCreds() {
5268
5882
  let changed = true;
5269
5883
  try {
5270
5884
  if (existsSync18(CLAUDE_CREDS_FILE)) {
5271
- changed = readFileSync15(CLAUDE_CREDS_FILE, "utf-8") !== blob;
5885
+ changed = readFileSync16(CLAUDE_CREDS_FILE, "utf-8") !== blob;
5272
5886
  }
5273
5887
  } catch {
5274
5888
  }
@@ -5278,7 +5892,7 @@ function exportKeychainCreds() {
5278
5892
  }
5279
5893
  function cursorApiKeyConfigured() {
5280
5894
  try {
5281
- return existsSync18(CURSOR_API_KEY_FILE) && readFileSync15(CURSOR_API_KEY_FILE, "utf-8").trim().length > 0;
5895
+ return existsSync18(CURSOR_API_KEY_FILE) && readFileSync16(CURSOR_API_KEY_FILE, "utf-8").trim().length > 0;
5282
5896
  } catch {
5283
5897
  return false;
5284
5898
  }
@@ -5294,7 +5908,7 @@ function writeCursorApiKey(key) {
5294
5908
  async function validateCursorApiKey() {
5295
5909
  let key;
5296
5910
  try {
5297
- key = readFileSync15(CURSOR_API_KEY_FILE, "utf-8").trim();
5911
+ key = readFileSync16(CURSOR_API_KEY_FILE, "utf-8").trim();
5298
5912
  } catch {
5299
5913
  return null;
5300
5914
  }
@@ -5315,7 +5929,7 @@ async function validateCursorApiKey() {
5315
5929
  function credsAreStale() {
5316
5930
  if (!existsSync18(CLAUDE_CREDS_FILE)) return true;
5317
5931
  try {
5318
- const raw = readFileSync15(CLAUDE_CREDS_FILE, "utf-8");
5932
+ const raw = readFileSync16(CLAUDE_CREDS_FILE, "utf-8");
5319
5933
  const exp = JSON.parse(raw)?.claudeAiOauth?.expiresAt ?? 0;
5320
5934
  if (!exp) return true;
5321
5935
  return Date.now() >= exp - REFRESH_EXPIRY_BUFFER_SECONDS * 1e3;
@@ -5330,7 +5944,7 @@ function writeRefreshAgent(synkroBinPath) {
5330
5944
  if (platform3() !== "darwin") {
5331
5945
  throw new KeychainExportError("writeRefreshAgent is darwin-only");
5332
5946
  }
5333
- mkdirSync11(join13(homedir15(), "Library", "LaunchAgents"), { recursive: true });
5947
+ mkdirSync11(join14(homedir16(), "Library", "LaunchAgents"), { recursive: true });
5334
5948
  mkdirSync11(SYNKRO_DIR6, { recursive: true });
5335
5949
  const script = `#!/bin/bash
5336
5950
  # Generated by synkro (writeRefreshAgent). Expiry-aware Claude creds refresher.
@@ -5427,7 +6041,7 @@ function refreshCreds() {
5427
6041
  }
5428
6042
  function readExportedCreds() {
5429
6043
  try {
5430
- return readFileSync15(CLAUDE_CREDS_FILE, "utf-8");
6044
+ return readFileSync16(CLAUDE_CREDS_FILE, "utf-8");
5431
6045
  } catch {
5432
6046
  return null;
5433
6047
  }
@@ -5436,16 +6050,16 @@ var SYNKRO_DIR6, CLAUDE_CREDS_DIR, CLAUDE_CREDS_FILE, CURSOR_CREDS_DIR, CURSOR_A
5436
6050
  var init_macKeychain = __esm({
5437
6051
  "cli/local-cc/macKeychain.ts"() {
5438
6052
  "use strict";
5439
- SYNKRO_DIR6 = join13(homedir15(), ".synkro");
5440
- CLAUDE_CREDS_DIR = join13(SYNKRO_DIR6, "claude-creds");
5441
- CLAUDE_CREDS_FILE = join13(CLAUDE_CREDS_DIR, ".credentials.json");
5442
- CURSOR_CREDS_DIR = join13(SYNKRO_DIR6, "cursor-creds");
5443
- CURSOR_API_KEY_FILE = join13(CURSOR_CREDS_DIR, "api-key");
6053
+ SYNKRO_DIR6 = join14(homedir16(), ".synkro");
6054
+ CLAUDE_CREDS_DIR = join14(SYNKRO_DIR6, "claude-creds");
6055
+ CLAUDE_CREDS_FILE = join14(CLAUDE_CREDS_DIR, ".credentials.json");
6056
+ CURSOR_CREDS_DIR = join14(SYNKRO_DIR6, "cursor-creds");
6057
+ CURSOR_API_KEY_FILE = join14(CURSOR_CREDS_DIR, "api-key");
5444
6058
  KEYCHAIN_SERVICE = "Claude Code-credentials";
5445
6059
  LAUNCHD_LABEL = "com.synkro.cli.claude-creds-refresh";
5446
- LAUNCHD_PLIST = join13(homedir15(), "Library", "LaunchAgents", `${LAUNCHD_LABEL}.plist`);
5447
- REFRESH_SCRIPT = join13(SYNKRO_DIR6, "claude-creds-refresh-loop.sh");
5448
- REFRESH_LOG = join13(SYNKRO_DIR6, "claude-creds-refresh.log");
6060
+ LAUNCHD_PLIST = join14(homedir16(), "Library", "LaunchAgents", `${LAUNCHD_LABEL}.plist`);
6061
+ REFRESH_SCRIPT = join14(SYNKRO_DIR6, "claude-creds-refresh-loop.sh");
6062
+ REFRESH_LOG = join14(SYNKRO_DIR6, "claude-creds-refresh.log");
5449
6063
  REFRESH_EXPIRY_BUFFER_SECONDS = 120;
5450
6064
  MIN_REFRESH_INTERVAL_SECONDS = 30;
5451
6065
  MAX_REFRESH_INTERVAL_SECONDS = 5 * 60;
@@ -5464,8 +6078,11 @@ var init_macKeychain = __esm({
5464
6078
  var dockerInstall_exports = {};
5465
6079
  __export(dockerInstall_exports, {
5466
6080
  DockerInstallError: () => DockerInstallError,
6081
+ HOST_PGLITE_PORT: () => HOST_PGLITE_PORT,
6082
+ PGLITE_PASSWORD_PATH: () => PGLITE_PASSWORD_PATH,
5467
6083
  SYNKRO_DIR: () => SYNKRO_DIR7,
5468
6084
  assertDockerAvailable: () => assertDockerAvailable,
6085
+ createPgliteScramVerifier: () => createPgliteScramVerifier,
5469
6086
  dockerInstall: () => dockerInstall,
5470
6087
  dockerRemove: () => dockerRemove,
5471
6088
  dockerSafeRestart: () => dockerSafeRestart,
@@ -5474,21 +6091,71 @@ __export(dockerInstall_exports, {
5474
6091
  dockerStatus: () => dockerStatus,
5475
6092
  dockerStop: () => dockerStop,
5476
6093
  dockerUpdate: () => dockerUpdate,
6094
+ ensurePgliteProxyCredentials: () => ensurePgliteProxyCredentials,
5477
6095
  imageTag: () => imageTag,
5478
6096
  normalizeProvider: () => normalizeProvider,
5479
6097
  poolLabel: () => poolLabel,
5480
6098
  readContainerConfig: () => readContainerConfig,
5481
6099
  resolveConductorProvider: () => resolveConductorProvider,
6100
+ resolveContainerName: () => resolveContainerName,
5482
6101
  resolveGraderPool: () => resolveGraderPool,
5483
6102
  resolveWorkerConfig: () => resolveWorkerConfig,
5484
6103
  splitWorkers: () => splitWorkers,
5485
6104
  waitForContainerReady: () => waitForContainerReady,
5486
6105
  waitForWorkersReady: () => waitForWorkersReady
5487
6106
  });
5488
- import { copyFileSync, existsSync as existsSync19, mkdirSync as mkdirSync12, readFileSync as readFileSync16, readdirSync as readdirSync2 } from "fs";
5489
- import { homedir as homedir16 } from "os";
5490
- import { join as join14 } from "path";
6107
+ import {
6108
+ chmodSync as chmodSync3,
6109
+ copyFileSync,
6110
+ existsSync as existsSync19,
6111
+ mkdirSync as mkdirSync12,
6112
+ readFileSync as readFileSync17,
6113
+ readdirSync as readdirSync2,
6114
+ renameSync as renameSync7,
6115
+ writeFileSync as writeFileSync14
6116
+ } from "fs";
6117
+ import { createHash as createHash2, createHmac, pbkdf2Sync, randomBytes as randomBytes2 } from "crypto";
6118
+ import { homedir as homedir17 } from "os";
6119
+ import { join as join15 } from "path";
5491
6120
  import { execSync as execSync3, spawnSync as spawnSync3 } from "child_process";
6121
+ function resolveContainerName(raw = process.env.SYNKRO_CONTAINER_NAME) {
6122
+ const value = String(raw || "").trim();
6123
+ if (!value) return "synkro-server";
6124
+ if (!/^[A-Za-z0-9][A-Za-z0-9_.-]{0,127}$/.test(value)) {
6125
+ throw new Error("SYNKRO_CONTAINER_NAME must be a valid Docker container name (1-128 letters, digits, _, ., or -)");
6126
+ }
6127
+ return value;
6128
+ }
6129
+ function createPgliteScramVerifier(password, salt = randomBytes2(16)) {
6130
+ const iterations = 4096;
6131
+ const saltedPassword = pbkdf2Sync(password, salt, iterations, 32, "sha256");
6132
+ const clientKey = createHmac("sha256", saltedPassword).update("Client Key").digest();
6133
+ const storedKey = createHash2("sha256").update(clientKey).digest();
6134
+ const serverKey = createHmac("sha256", saltedPassword).update("Server Key").digest();
6135
+ return `SCRAM-SHA-256$${iterations}:${salt.toString("base64")}$${storedKey.toString("base64")}:${serverKey.toString("base64")}`;
6136
+ }
6137
+ function ensurePgliteProxyCredentials() {
6138
+ const hasPassword = existsSync19(PGLITE_PASSWORD_PATH) && readFileSync17(PGLITE_PASSWORD_PATH, "utf-8").trim().length > 0;
6139
+ const hasUserlist = existsSync19(PGLITE_USERLIST_PATH) && readFileSync17(PGLITE_USERLIST_PATH, "utf-8").trim().length > 0;
6140
+ if (hasPassword && hasUserlist) {
6141
+ chmodSync3(PGLITE_PASSWORD_PATH, 384);
6142
+ chmodSync3(PGLITE_USERLIST_PATH, 384);
6143
+ return;
6144
+ }
6145
+ const password = randomBytes2(24).toString("base64url");
6146
+ const verifier = createPgliteScramVerifier(password);
6147
+ const suffix = `${process.pid}.${Date.now()}.tmp`;
6148
+ const passwordTmp = `${PGLITE_PASSWORD_PATH}.${suffix}`;
6149
+ const userlistTmp = `${PGLITE_USERLIST_PATH}.${suffix}`;
6150
+ writeFileSync14(passwordTmp, `${password}
6151
+ `, { mode: 384 });
6152
+ writeFileSync14(userlistTmp, `"synkro" "${verifier}"
6153
+ `, { mode: 384 });
6154
+ renameSync7(passwordTmp, PGLITE_PASSWORD_PATH);
6155
+ renameSync7(userlistTmp, PGLITE_USERLIST_PATH);
6156
+ chmodSync3(PGLITE_PASSWORD_PATH, 384);
6157
+ chmodSync3(PGLITE_USERLIST_PATH, 384);
6158
+ }
5492
6159
  function resolveConductorProvider(pool, counts) {
5493
6160
  if (pool !== "auto") return pool;
5494
6161
  const ranked = [
@@ -5597,9 +6264,9 @@ function readSynkroFileConfig() {
5597
6264
  try {
5598
6265
  const root = execSync3("git rev-parse --show-toplevel 2>/dev/null", { encoding: "utf-8", timeout: 3e3, stdio: ["pipe", "pipe", "pipe"] }).trim();
5599
6266
  if (!root) return { pool: "auto", warnings: [] };
5600
- const fp = join14(root, "synkro.toml");
6267
+ const fp = join15(root, "synkro.toml");
5601
6268
  if (!existsSync19(fp)) return { pool: "auto", warnings: [] };
5602
- const parsed = parseSynkroToml(readFileSync16(fp, "utf-8"));
6269
+ const parsed = parseSynkroToml(readFileSync17(fp, "utf-8"));
5603
6270
  return resolveGraderPool(parsed);
5604
6271
  } catch {
5605
6272
  }
@@ -5682,7 +6349,7 @@ function assertDockerAvailable() {
5682
6349
  }
5683
6350
  function claudeCredsHostDir() {
5684
6351
  if (needsKeychainBridge()) return CLAUDE_CREDS_DIR;
5685
- return join14(homedir16(), ".claude");
6352
+ return join15(homedir17(), ".claude");
5686
6353
  }
5687
6354
  function resolveSynkroBin() {
5688
6355
  const which2 = spawnSync3("which", ["synkro"], { encoding: "utf-8", timeout: 5e3 });
@@ -5741,16 +6408,17 @@ async function dockerInstall(opts = {}) {
5741
6408
  const usesClaude = claudeWorkers > 0 || conductorProvider === "claude_code";
5742
6409
  const usesCursor = cursorWorkers > 0 || conductorProvider === "cursor";
5743
6410
  const usesCodex = codexWorkers > 0 || conductorProvider === "codex";
5744
- const codexHomeDir = opts.codexHomeDir ?? join14(SYNKRO_DIR7, "codex-local-session");
5745
- if (usesCodex && !existsSync19(join14(codexHomeDir, "auth.json"))) {
6411
+ const codexHomeDir = opts.codexHomeDir ?? join15(SYNKRO_DIR7, "codex-local-session");
6412
+ if (usesCodex && !existsSync19(join15(codexHomeDir, "auth.json"))) {
5746
6413
  throw new DockerInstallError(
5747
6414
  "Codex grader credentials are missing. Re-run `synkro install` to authorize an isolated Codex session."
5748
6415
  );
5749
6416
  }
5750
6417
  mkdirSync12(PGDATA_PATH, { recursive: true });
5751
6418
  mkdirSync12(BACKUP_DIR, { recursive: true });
6419
+ ensurePgliteProxyCredentials();
5752
6420
  mkdirSync12(CLAUDE_HOST_STATE_DIR, { recursive: true });
5753
- const hostClaudeJson = join14(homedir16(), ".claude.json");
6421
+ const hostClaudeJson = join15(homedir17(), ".claude.json");
5754
6422
  if (existsSync19(hostClaudeJson)) {
5755
6423
  copyFileSync(hostClaudeJson, CLAUDE_HOST_STATE_FILE);
5756
6424
  }
@@ -5783,7 +6451,7 @@ async function dockerInstall(opts = {}) {
5783
6451
  console.warn(` Plist written to ${plist} \u2014 load manually with launchctl bootstrap when ready.`);
5784
6452
  }
5785
6453
  } else {
5786
- mkdirSync12(join14(homedir16(), ".claude"), { recursive: true });
6454
+ mkdirSync12(join15(homedir17(), ".claude"), { recursive: true });
5787
6455
  }
5788
6456
  const imageExistsLocally = () => spawnSync3("docker", ["image", "inspect", image], { stdio: "ignore", timeout: 3e4 }).status === 0;
5789
6457
  const skipPull = process.env.SYNKRO_SKIP_PULL === "1" || process.env.SYNKRO_SKIP_PULL === "true";
@@ -5822,7 +6490,7 @@ async function dockerInstall(opts = {}) {
5822
6490
  "-p",
5823
6491
  `127.0.0.1:${HOST_CWE_PORT}:8930`,
5824
6492
  "-p",
5825
- `127.0.0.1:${HOST_PGLITE_PORT}:5433`,
6493
+ `127.0.0.1:${HOST_PGLITE_PORT}:5434`,
5826
6494
  "-v",
5827
6495
  `${PGDATA_PATH}:/data/pgdata`,
5828
6496
  "-v",
@@ -5836,7 +6504,7 @@ async function dockerInstall(opts = {}) {
5836
6504
  "-v",
5837
6505
  `${credsDir}:/home/synkro/.claude:rw`,
5838
6506
  "-v",
5839
- `${join14(homedir16(), ".claude")}:/data/claude-host:ro`,
6507
+ `${join15(homedir17(), ".claude")}:/data/claude-host:ro`,
5840
6508
  "-v",
5841
6509
  `${CLAUDE_HOST_STATE_DIR}:/data/claude-host-state:ro`,
5842
6510
  // Cursor creds — mounted RW so the in-container refresher can rotate the
@@ -5856,6 +6524,8 @@ async function dockerInstall(opts = {}) {
5856
6524
  // Pass through the batch-size lever if the operator set it. Defaults
5857
6525
  // inside the container to 5; clamped to [1, 20] by synkro-server.ts.
5858
6526
  ...process.env.SYNKRO_MAX_BATCH_SIZE ? ["-e", `SYNKRO_MAX_BATCH_SIZE=${process.env.SYNKRO_MAX_BATCH_SIZE}`] : [],
6527
+ // Full verifier prompt/response tracing is explicit opt-in because it contains source code.
6528
+ ...process.env.SYNKRO_VERIFY_TRACE === "1" ? ["-e", "SYNKRO_VERIFY_TRACE=1"] : [],
5859
6529
  // Cursor grading model — tunable like SYNKRO_MAX_BATCH_SIZE.
5860
6530
  ...process.env.SYNKRO_CURSOR_MODEL ? ["-e", `SYNKRO_CURSOR_MODEL=${process.env.SYNKRO_CURSOR_MODEL}`] : [],
5861
6531
  // Fix-poll kill switch. Default ON in the image; a benchmark/headless run
@@ -5887,7 +6557,14 @@ async function dockerInstall(opts = {}) {
5887
6557
  if (run.status !== 0) {
5888
6558
  throw new DockerInstallError(`docker run failed (image ${image})`);
5889
6559
  }
5890
- return { image, hostMcpPort: HOST_MCP_PORT, hostGraderPort: HOST_GRADER_PORT, hostCwePort: HOST_CWE_PORT, hostPglitePort: HOST_PGLITE_PORT };
6560
+ return {
6561
+ image,
6562
+ hostMcpPort: HOST_MCP_PORT,
6563
+ hostGraderPort: HOST_GRADER_PORT,
6564
+ hostCwePort: HOST_CWE_PORT,
6565
+ hostPglitePort: HOST_PGLITE_PORT,
6566
+ pglitePasswordPath: PGLITE_PASSWORD_PATH
6567
+ };
5891
6568
  }
5892
6569
  async function waitForContainerReady(timeoutMs = 6e4) {
5893
6570
  const start = Date.now();
@@ -5942,7 +6619,7 @@ function dockerStatus() {
5942
6619
  return {
5943
6620
  running: true,
5944
6621
  image: imageTag(),
5945
- healthz: `http://127.0.0.1:${HOST_MCP_PORT}/`
6622
+ healthz: `http://127.0.0.1:${HOST_MCP_PORT}/health`
5946
6623
  };
5947
6624
  }
5948
6625
  function readContainerConfig() {
@@ -6089,23 +6766,34 @@ function checkPgdata() {
6089
6766
  if (!hasPgControl) return { healthy: false, details: "pg_control/global directory missing" };
6090
6767
  return { healthy: true, details: `${entries.length} entries, WAL present, no stale PID` };
6091
6768
  }
6092
- var SYNKRO_DIR7, MCP_JWT_PATH, PGDATA_PATH, CLAUDE_HOST_STATE_DIR, CLAUDE_HOST_STATE_FILE, HOST_MCP_PORT, HOST_GRADER_PORT, HOST_CWE_PORT, HOST_PGLITE_PORT, CONTAINER_NAME, DEFAULT_IMAGE, DockerInstallError, BACKUP_DIR;
6769
+ var SYNKRO_DIR7, MCP_JWT_PATH, PGDATA_PATH, PGLITE_PASSWORD_PATH, PGLITE_USERLIST_PATH, CLAUDE_HOST_STATE_DIR, CLAUDE_HOST_STATE_FILE, HOST_MCP_PORT, HOST_GRADER_PORT, HOST_CWE_PORT, HOST_PGLITE_PORT, CONTAINER_NAME, defaultImageVersion, DEFAULT_IMAGE, DockerInstallError, BACKUP_DIR;
6093
6770
  var init_dockerInstall = __esm({
6094
6771
  "cli/local-cc/dockerInstall.ts"() {
6095
6772
  "use strict";
6096
6773
  init_agentDetect();
6097
6774
  init_macKeychain();
6098
- SYNKRO_DIR7 = join14(homedir16(), ".synkro");
6099
- MCP_JWT_PATH = join14(SYNKRO_DIR7, ".mcp-jwt");
6100
- PGDATA_PATH = join14(SYNKRO_DIR7, "pgdata");
6101
- CLAUDE_HOST_STATE_DIR = join14(SYNKRO_DIR7, "claude-host-state");
6102
- CLAUDE_HOST_STATE_FILE = join14(CLAUDE_HOST_STATE_DIR, ".claude.json");
6775
+ SYNKRO_DIR7 = join15(homedir17(), ".synkro");
6776
+ MCP_JWT_PATH = join15(SYNKRO_DIR7, ".mcp-jwt");
6777
+ PGDATA_PATH = join15(SYNKRO_DIR7, "pgdata");
6778
+ PGLITE_PASSWORD_PATH = join15(SYNKRO_DIR7, ".pglite-password");
6779
+ PGLITE_USERLIST_PATH = join15(SYNKRO_DIR7, ".pglite-userlist");
6780
+ CLAUDE_HOST_STATE_DIR = join15(SYNKRO_DIR7, "claude-host-state");
6781
+ CLAUDE_HOST_STATE_FILE = join15(CLAUDE_HOST_STATE_DIR, ".claude.json");
6103
6782
  HOST_MCP_PORT = parseInt(process.env.SYNKRO_HOST_MCP_PORT || "18931", 10);
6104
6783
  HOST_GRADER_PORT = parseInt(process.env.SYNKRO_HOST_GRADER_PORT || "18929", 10);
6105
6784
  HOST_CWE_PORT = parseInt(process.env.SYNKRO_HOST_CWE_PORT || "18930", 10);
6106
6785
  HOST_PGLITE_PORT = parseInt(process.env.SYNKRO_HOST_PGLITE_PORT || "15433", 10);
6107
- CONTAINER_NAME = "synkro-server";
6108
- DEFAULT_IMAGE = "ghcr.io/synkro-sh/synkro-server:latest";
6786
+ CONTAINER_NAME = resolveContainerName();
6787
+ defaultImageVersion = () => {
6788
+ if (true) return "1.7.90";
6789
+ try {
6790
+ const pkg = JSON.parse(readFileSync17(new URL("../../package.json", import.meta.url), "utf8"));
6791
+ if (pkg.version) return pkg.version;
6792
+ } catch {
6793
+ }
6794
+ return process.env.npm_package_version || "0.0.0-dev";
6795
+ };
6796
+ DEFAULT_IMAGE = `ghcr.io/synkro-sh/synkro-server:${defaultImageVersion()}`;
6109
6797
  DockerInstallError = class extends Error {
6110
6798
  constructor(message, cause) {
6111
6799
  super(message);
@@ -6114,17 +6802,17 @@ var init_dockerInstall = __esm({
6114
6802
  }
6115
6803
  cause;
6116
6804
  };
6117
- BACKUP_DIR = join14(SYNKRO_DIR7, "pgdata-backups");
6805
+ BACKUP_DIR = join15(SYNKRO_DIR7, "pgdata-backups");
6118
6806
  }
6119
6807
  });
6120
6808
 
6121
6809
  // cli/local-cc/setupToken.ts
6122
6810
  import { spawn as nodeSpawn } from "child_process";
6123
- import { readFileSync as readFileSync17, unlinkSync as unlinkSync6 } from "fs";
6124
- import { homedir as homedir17, platform as platform4 } from "os";
6125
- import { join as join15 } from "path";
6811
+ import { readFileSync as readFileSync18, unlinkSync as unlinkSync6 } from "fs";
6812
+ import { homedir as homedir18, platform as platform4 } from "os";
6813
+ import { join as join16 } from "path";
6126
6814
  function captureClaudeSetupToken() {
6127
- const tmpFile = join15(SYNKRO_DIR8, `token-capture-${Date.now()}.raw`);
6815
+ const tmpFile = join16(SYNKRO_DIR8, `token-capture-${Date.now()}.raw`);
6128
6816
  const isMac = platform4() === "darwin";
6129
6817
  const bin = "script";
6130
6818
  const args2 = isMac ? ["-q", tmpFile, "claude", "setup-token"] : ["-qec", "claude setup-token", tmpFile];
@@ -6138,7 +6826,7 @@ function captureClaudeSetupToken() {
6138
6826
  proc.on("close", (code) => {
6139
6827
  let raw = "";
6140
6828
  try {
6141
- raw = readFileSync17(tmpFile, "utf-8");
6829
+ raw = readFileSync18(tmpFile, "utf-8");
6142
6830
  } catch (e) {
6143
6831
  reject(new Error(`Could not read script output file: ${e.message}`));
6144
6832
  return;
@@ -6204,15 +6892,15 @@ var SYNKRO_DIR8;
6204
6892
  var init_setupToken = __esm({
6205
6893
  "cli/local-cc/setupToken.ts"() {
6206
6894
  "use strict";
6207
- SYNKRO_DIR8 = join15(homedir17(), ".synkro");
6895
+ SYNKRO_DIR8 = join16(homedir18(), ".synkro");
6208
6896
  }
6209
6897
  });
6210
6898
 
6211
6899
  // cli/local-cc/codexCloudSetup.ts
6212
6900
  import { spawn as nodeSpawn2, spawnSync as spawnSync4 } from "child_process";
6213
- import { readFileSync as readFileSync18, writeFileSync as writeFileSync14, mkdirSync as mkdirSync13, rmSync, existsSync as existsSync20 } from "fs";
6214
- import { homedir as homedir18 } from "os";
6215
- import { join as join16 } from "path";
6901
+ import { readFileSync as readFileSync19, writeFileSync as writeFileSync15, mkdirSync as mkdirSync13, rmSync, existsSync as existsSync20 } from "fs";
6902
+ import { homedir as homedir19 } from "os";
6903
+ import { join as join17 } from "path";
6216
6904
  function findCodexBinary() {
6217
6905
  if (process.env.SYNKRO_CODEX_BIN) return process.env.SYNKRO_CODEX_BIN;
6218
6906
  const r = spawnSync4("which", ["codex"], { encoding: "utf-8" });
@@ -6221,7 +6909,7 @@ function findCodexBinary() {
6221
6909
  }
6222
6910
  function runCodexLogin(codexBin, codexHome) {
6223
6911
  mkdirSync13(codexHome, { recursive: true, mode: 448 });
6224
- writeFileSync14(join16(codexHome, "config.toml"), 'cli_auth_credentials_store = "file"\n', { mode: 384 });
6912
+ writeFileSync15(join17(codexHome, "config.toml"), 'cli_auth_credentials_store = "file"\n', { mode: 384 });
6225
6913
  return new Promise((resolve6, reject) => {
6226
6914
  const proc = nodeSpawn2(codexBin, ["login"], {
6227
6915
  stdio: "inherit",
@@ -6253,13 +6941,13 @@ async function setupCodexCloud(gatewayUrl, bearerToken, onStatus) {
6253
6941
  } catch (e) {
6254
6942
  return { ok: false, error: `Codex login failed: ${e.message}` };
6255
6943
  }
6256
- const authPath = join16(CODEX_CLOUD_HOME, "auth.json");
6944
+ const authPath = join17(CODEX_CLOUD_HOME, "auth.json");
6257
6945
  if (!existsSync20(authPath)) {
6258
6946
  return { ok: false, error: "codex login completed but no auth.json was written \u2014 did the browser approval finish?" };
6259
6947
  }
6260
6948
  let auth;
6261
6949
  try {
6262
- auth = JSON.parse(readFileSync18(authPath, "utf-8"));
6950
+ auth = JSON.parse(readFileSync19(authPath, "utf-8"));
6263
6951
  } catch (e) {
6264
6952
  return { ok: false, error: `could not read codex auth.json: ${e.message}` };
6265
6953
  }
@@ -6293,7 +6981,7 @@ async function setupCodexLocal(onStatus) {
6293
6981
  if (!codexBin) {
6294
6982
  return { ok: false, error: "Codex CLI not found on PATH. Install Codex, then re-run `synkro install`." };
6295
6983
  }
6296
- const authPath = join16(CODEX_LOCAL_HOME, "auth.json");
6984
+ const authPath = join17(CODEX_LOCAL_HOME, "auth.json");
6297
6985
  if (!existsSync20(authPath)) {
6298
6986
  onStatus?.("Opening your browser to authorize an isolated Codex session for the local grader\u2026");
6299
6987
  try {
@@ -6303,7 +6991,7 @@ async function setupCodexLocal(onStatus) {
6303
6991
  }
6304
6992
  }
6305
6993
  try {
6306
- const auth = JSON.parse(readFileSync18(authPath, "utf-8"));
6994
+ const auth = JSON.parse(readFileSync19(authPath, "utf-8"));
6307
6995
  if (!auth.tokens?.refresh_token) throw new Error("auth.json has no refresh token");
6308
6996
  } catch (e) {
6309
6997
  return { ok: false, error: `Codex local grader auth is invalid: ${e.message}` };
@@ -6315,9 +7003,9 @@ var SYNKRO_DIR9, CODEX_CLOUD_HOME, CODEX_LOCAL_HOME;
6315
7003
  var init_codexCloudSetup = __esm({
6316
7004
  "cli/local-cc/codexCloudSetup.ts"() {
6317
7005
  "use strict";
6318
- SYNKRO_DIR9 = join16(homedir18(), ".synkro");
6319
- CODEX_CLOUD_HOME = join16(SYNKRO_DIR9, "codex-cloud-session");
6320
- CODEX_LOCAL_HOME = join16(SYNKRO_DIR9, "codex-local-session");
7006
+ SYNKRO_DIR9 = join17(homedir19(), ".synkro");
7007
+ CODEX_CLOUD_HOME = join17(SYNKRO_DIR9, "codex-cloud-session");
7008
+ CODEX_LOCAL_HOME = join17(SYNKRO_DIR9, "codex-local-session");
6321
7009
  }
6322
7010
  });
6323
7011
 
@@ -6339,21 +7027,21 @@ __export(ptyShim_exports, {
6339
7027
  import {
6340
7028
  existsSync as existsSync21,
6341
7029
  mkdirSync as mkdirSync14,
6342
- writeFileSync as writeFileSync15,
6343
- chmodSync as chmodSync3,
6344
- readFileSync as readFileSync19,
7030
+ writeFileSync as writeFileSync16,
7031
+ chmodSync as chmodSync4,
7032
+ readFileSync as readFileSync20,
6345
7033
  rmSync as rmSync2,
6346
7034
  realpathSync,
6347
7035
  symlinkSync,
6348
7036
  lstatSync,
6349
7037
  readdirSync as readdirSync3
6350
7038
  } from "fs";
6351
- import { homedir as homedir19 } from "os";
6352
- import { join as join17 } from "path";
7039
+ import { homedir as homedir20 } from "os";
7040
+ import { join as join18 } from "path";
6353
7041
  import { spawnSync as spawnSync5, spawn as spawn3 } from "child_process";
6354
7042
  function rcFiles() {
6355
- const h = homedir19();
6356
- return [join17(h, ".zshrc"), join17(h, ".bashrc"), join17(h, ".bash_profile")];
7043
+ const h = homedir20();
7044
+ return [join18(h, ".zshrc"), join18(h, ".bashrc"), join18(h, ".bash_profile")];
6357
7045
  }
6358
7046
  function resolveRealClaude() {
6359
7047
  const r = spawnSync5("bash", ["-lc", `PATH="$(printf '%s' "$PATH" | sed "s#$HOME/.synkro/bin:##g")" command -v claude`], { encoding: "utf-8" });
@@ -6365,7 +7053,7 @@ function resolveRealClaude() {
6365
7053
  return p;
6366
7054
  }
6367
7055
  }
6368
- for (const c of [join17(homedir19(), ".local", "bin", "claude"), "/usr/local/bin/claude", "/opt/homebrew/bin/claude"]) {
7056
+ for (const c of [join18(homedir20(), ".local", "bin", "claude"), "/usr/local/bin/claude", "/opt/homebrew/bin/claude"]) {
6369
7057
  if (existsSync21(c)) {
6370
7058
  try {
6371
7059
  return realpathSync(c);
@@ -6380,7 +7068,7 @@ function findClaudeLink() {
6380
7068
  const r = spawnSync5("bash", ["-lc", `PATH="$(printf '%s' "$PATH" | sed "s#$HOME/.synkro/bin:##g")" command -v claude`], { encoding: "utf-8" });
6381
7069
  let linkPath = (r.stdout || "").trim();
6382
7070
  if (!linkPath) {
6383
- const c = join17(homedir19(), ".local", "bin", "claude");
7071
+ const c = join18(homedir20(), ".local", "bin", "claude");
6384
7072
  if (existsSync21(c)) linkPath = c;
6385
7073
  else return null;
6386
7074
  }
@@ -6394,7 +7082,7 @@ function findClaudeLink() {
6394
7082
  }
6395
7083
  function isOurShim(path) {
6396
7084
  try {
6397
- return readFileSync19(path, "utf-8").slice(0, 300).includes("Synkro pty shim");
7085
+ return readFileSync20(path, "utf-8").slice(0, 300).includes("Synkro pty shim");
6398
7086
  } catch {
6399
7087
  return false;
6400
7088
  }
@@ -6407,7 +7095,7 @@ function shadowClaude() {
6407
7095
  }
6408
7096
  const { linkPath, realTarget } = found;
6409
7097
  if (isOurShim(linkPath)) {
6410
- console.log(` \xB7 ${linkPath.replace(homedir19(), "~")} already shadowed`);
7098
+ console.log(` \xB7 ${linkPath.replace(homedir20(), "~")} already shadowed`);
6411
7099
  return;
6412
7100
  }
6413
7101
  let wasSymlink = false;
@@ -6417,14 +7105,14 @@ function shadowClaude() {
6417
7105
  }
6418
7106
  const state = { linkPath, realTarget, wasSymlink };
6419
7107
  try {
6420
- writeFileSync15(SHADOW_STATE_FILE, JSON.stringify(state), "utf-8");
7108
+ writeFileSync16(SHADOW_STATE_FILE, JSON.stringify(state), "utf-8");
6421
7109
  } catch {
6422
7110
  }
6423
7111
  try {
6424
7112
  rmSync2(linkPath, { force: true });
6425
- writeFileSync15(linkPath, SHIM_SOURCE.replace("__BAKED_CLAUDE__", realTarget), "utf-8");
6426
- chmodSync3(linkPath, 493);
6427
- console.log(` \u2713 shadowed ${linkPath.replace(homedir19(), "~")} \u2192 shim (real: ${realTarget.replace(homedir19(), "~")})`);
7113
+ writeFileSync16(linkPath, SHIM_SOURCE.replace("__BAKED_CLAUDE__", realTarget), "utf-8");
7114
+ chmodSync4(linkPath, 493);
7115
+ console.log(` \u2713 shadowed ${linkPath.replace(homedir20(), "~")} \u2192 shim (real: ${realTarget.replace(homedir20(), "~")})`);
6428
7116
  } catch (e) {
6429
7117
  console.warn(` \u26A0 could not shadow ${linkPath}: ${e.message}`);
6430
7118
  }
@@ -6432,7 +7120,7 @@ function shadowClaude() {
6432
7120
  function unshadowClaude() {
6433
7121
  let state;
6434
7122
  try {
6435
- state = JSON.parse(readFileSync19(SHADOW_STATE_FILE, "utf-8"));
7123
+ state = JSON.parse(readFileSync20(SHADOW_STATE_FILE, "utf-8"));
6436
7124
  } catch {
6437
7125
  return;
6438
7126
  }
@@ -6441,7 +7129,7 @@ function unshadowClaude() {
6441
7129
  if (existsSync21(state.linkPath) && !isOurShim(state.linkPath)) return;
6442
7130
  rmSync2(state.linkPath, { force: true });
6443
7131
  symlinkSync(state.realTarget, state.linkPath);
6444
- console.log(`\u2713 restored ${state.linkPath.replace(homedir19(), "~")} \u2192 ${state.realTarget.replace(homedir19(), "~")}`);
7132
+ console.log(`\u2713 restored ${state.linkPath.replace(homedir20(), "~")} \u2192 ${state.realTarget.replace(homedir20(), "~")}`);
6445
7133
  } catch (e) {
6446
7134
  console.warn(` \u26A0 could not restore claude: ${e.message} \u2014 run: ln -sf ${state.realTarget} ${state.linkPath}`);
6447
7135
  }
@@ -6452,13 +7140,13 @@ function addPathBlock() {
6452
7140
  if (!existsSync21(rc) && rc.endsWith(".bash_profile")) continue;
6453
7141
  let body = "";
6454
7142
  try {
6455
- body = readFileSync19(rc, "utf-8");
7143
+ body = readFileSync20(rc, "utf-8");
6456
7144
  } catch {
6457
7145
  }
6458
7146
  const cleaned = stripBlock(body);
6459
7147
  const next = cleaned.replace(/\n*$/, "") + (cleaned ? "\n\n" : "") + RC_BLOCK + "\n";
6460
7148
  try {
6461
- writeFileSync15(rc, next, "utf-8");
7149
+ writeFileSync16(rc, next, "utf-8");
6462
7150
  touched.push(rc);
6463
7151
  } catch {
6464
7152
  }
@@ -6477,12 +7165,12 @@ function installPtyShim() {
6477
7165
  mkdirSync14(SHIM_BIN_DIR, { recursive: true });
6478
7166
  mkdirSync14(PTY_STATE_DIR, { recursive: true });
6479
7167
  const real = resolveRealClaude();
6480
- writeFileSync15(SHIM_PATH, SHIM_SOURCE.replace("__BAKED_CLAUDE__", real), "utf-8");
6481
- chmodSync3(SHIM_PATH, 493);
7168
+ writeFileSync16(SHIM_PATH, SHIM_SOURCE.replace("__BAKED_CLAUDE__", real), "utf-8");
7169
+ chmodSync4(SHIM_PATH, 493);
6482
7170
  const touched = addPathBlock();
6483
7171
  console.log(` \u2713 pty routing shim installed (real claude: ${real})`);
6484
7172
  shadowClaude();
6485
- if (touched.length) console.log(` added ~/.synkro/bin to PATH in: ${touched.map((t) => t.replace(homedir19(), "~")).join(", ")}`);
7173
+ if (touched.length) console.log(` added ~/.synkro/bin to PATH in: ${touched.map((t) => t.replace(homedir20(), "~")).join(", ")}`);
6486
7174
  }
6487
7175
  function uninstallPtyShim() {
6488
7176
  try {
@@ -6500,10 +7188,10 @@ function uninstallPtyShim() {
6500
7188
  for (const rc of rcFiles()) {
6501
7189
  if (!existsSync21(rc)) continue;
6502
7190
  try {
6503
- const body = readFileSync19(rc, "utf-8");
7191
+ const body = readFileSync20(rc, "utf-8");
6504
7192
  if (body.includes(RC_BEGIN)) {
6505
- writeFileSync15(rc, stripBlock(body).replace(/\n{3,}/g, "\n\n"), "utf-8");
6506
- cleaned.push(rc.replace(homedir19(), "~"));
7193
+ writeFileSync16(rc, stripBlock(body).replace(/\n{3,}/g, "\n\n"), "utf-8");
7194
+ cleaned.push(rc.replace(homedir20(), "~"));
6507
7195
  }
6508
7196
  } catch {
6509
7197
  }
@@ -6535,7 +7223,7 @@ function listSessions(opts = {}) {
6535
7223
  const out = [];
6536
7224
  for (const f of files) {
6537
7225
  try {
6538
- const r = JSON.parse(readFileSync19(join17(SESSIONS_DIR, f), "utf-8"));
7226
+ const r = JSON.parse(readFileSync20(join18(SESSIONS_DIR, f), "utf-8"));
6539
7227
  if (!r || !r.session_id) continue;
6540
7228
  if (liveOnly) {
6541
7229
  const s = r.tmux_session;
@@ -6564,7 +7252,7 @@ function resolveTargetSession(override) {
6564
7252
  const own = currentTmuxSession();
6565
7253
  const fromFile = (() => {
6566
7254
  try {
6567
- return readFileSync19(ACTIVE_SESSION_FILE, "utf-8").trim();
7255
+ return readFileSync20(ACTIVE_SESSION_FILE, "utf-8").trim();
6568
7256
  } catch {
6569
7257
  return "";
6570
7258
  }
@@ -6590,7 +7278,7 @@ function injectModel(model, sessionOverride) {
6590
7278
  sendKeys("Escape");
6591
7279
  sendKeys("-l", `/model ${model}`);
6592
7280
  sendKeys("Enter");
6593
- const pidFile = join17(PTY_STATE_DIR, `poll-${session}.pid`);
7281
+ const pidFile = join18(PTY_STATE_DIR, `poll-${session}.pid`);
6594
7282
  try {
6595
7283
  mkdirSync14(PTY_STATE_DIR, { recursive: true });
6596
7284
  } catch {
@@ -6604,13 +7292,13 @@ var SYNKRO_DIR10, SHIM_BIN_DIR, SHIM_PATH, PTY_STATE_DIR, ACTIVE_SESSION_FILE, S
6604
7292
  var init_ptyShim = __esm({
6605
7293
  "cli/local-cc/ptyShim.ts"() {
6606
7294
  "use strict";
6607
- SYNKRO_DIR10 = join17(homedir19(), ".synkro");
6608
- SHIM_BIN_DIR = join17(SYNKRO_DIR10, "bin");
6609
- SHIM_PATH = join17(SHIM_BIN_DIR, "claude");
6610
- PTY_STATE_DIR = join17(SYNKRO_DIR10, "pty");
6611
- ACTIVE_SESSION_FILE = join17(PTY_STATE_DIR, "active");
6612
- SHADOW_STATE_FILE = join17(PTY_STATE_DIR, "shadow.json");
6613
- SESSIONS_DIR = join17(PTY_STATE_DIR, "sessions");
7295
+ SYNKRO_DIR10 = join18(homedir20(), ".synkro");
7296
+ SHIM_BIN_DIR = join18(SYNKRO_DIR10, "bin");
7297
+ SHIM_PATH = join18(SHIM_BIN_DIR, "claude");
7298
+ PTY_STATE_DIR = join18(SYNKRO_DIR10, "pty");
7299
+ ACTIVE_SESSION_FILE = join18(PTY_STATE_DIR, "active");
7300
+ SHADOW_STATE_FILE = join18(PTY_STATE_DIR, "shadow.json");
7301
+ SESSIONS_DIR = join18(PTY_STATE_DIR, "sessions");
6614
7302
  SHIM_SESSION_PREFIX = "synkro-cc-";
6615
7303
  RC_BEGIN = "# >>> synkro pty shim (managed \u2014 do not edit) >>>";
6616
7304
  RC_END = "# <<< synkro pty shim <<<";
@@ -6681,6 +7369,189 @@ var init_graderSmoke = __esm({
6681
7369
  }
6682
7370
  });
6683
7371
 
7372
+ // cli/scanning/codexTranscriptUsage.ts
7373
+ function rawCounters(value) {
7374
+ return {
7375
+ input: finiteCounter(value?.input_tokens),
7376
+ output: finiteCounter(value?.output_tokens),
7377
+ cacheCreation: finiteCounter(value?.cache_write_input_tokens),
7378
+ cacheRead: finiteCounter(value?.cached_input_tokens)
7379
+ };
7380
+ }
7381
+ function counterDelta(current, previous) {
7382
+ return current >= previous ? current - previous : current;
7383
+ }
7384
+ function normalizedUsage(raw) {
7385
+ return {
7386
+ // Codex input_tokens includes cached/cache-write details. Store only the
7387
+ // uncached remainder in input_tokens so totals and pricing never count the
7388
+ // same prompt tokens twice.
7389
+ input_tokens: Math.max(0, raw.input - raw.cacheCreation - raw.cacheRead),
7390
+ output_tokens: raw.output,
7391
+ cache_creation_input_tokens: raw.cacheCreation,
7392
+ cache_read_input_tokens: raw.cacheRead
7393
+ };
7394
+ }
7395
+ function addUsage(a, b) {
7396
+ return {
7397
+ input_tokens: (a?.input_tokens || 0) + b.input_tokens,
7398
+ output_tokens: (a?.output_tokens || 0) + b.output_tokens,
7399
+ cache_creation_input_tokens: (a?.cache_creation_input_tokens || 0) + b.cache_creation_input_tokens,
7400
+ cache_read_input_tokens: (a?.cache_read_input_tokens || 0) + b.cache_read_input_tokens
7401
+ };
7402
+ }
7403
+ function parseCodexTranscriptUsage(transcript, options = {}) {
7404
+ if (!transcript) return null;
7405
+ const lines = transcript.split("\n");
7406
+ const hasCanonicalAssistant = lines.some((line) => {
7407
+ try {
7408
+ const entry = JSON.parse(line);
7409
+ return entry?.type === "event_msg" && entry?.payload?.type === "agent_message";
7410
+ } catch {
7411
+ return false;
7412
+ }
7413
+ });
7414
+ const turnsByLine = /* @__PURE__ */ new Map();
7415
+ let model = "";
7416
+ let lastAssistantLine = -1;
7417
+ let previous = { input: 0, output: 0, cacheCreation: 0, cacheRead: 0 };
7418
+ let latest = null;
7419
+ let sawSnapshot = false;
7420
+ for (let i = 0; i < lines.length; i++) {
7421
+ const line = lines[i].trim();
7422
+ if (!line) continue;
7423
+ let entry;
7424
+ try {
7425
+ entry = JSON.parse(line);
7426
+ } catch {
7427
+ continue;
7428
+ }
7429
+ if (entry?.type === "turn_context" && typeof entry?.payload?.model === "string") {
7430
+ model = entry.payload.model;
7431
+ continue;
7432
+ }
7433
+ if (!hasCanonicalAssistant && entry?.type === "response_item" && entry?.payload?.type === "message" && entry?.payload?.role === "assistant") {
7434
+ lastAssistantLine = i;
7435
+ continue;
7436
+ }
7437
+ if (entry?.type !== "event_msg") continue;
7438
+ if (entry?.payload?.type === "agent_message") {
7439
+ lastAssistantLine = i;
7440
+ continue;
7441
+ }
7442
+ if (entry?.payload?.type !== "token_count") continue;
7443
+ const eventModel = entry?.payload?.model ?? entry?.model;
7444
+ if (typeof eventModel === "string" && eventModel) model = eventModel;
7445
+ const total = entry?.payload?.info?.total_token_usage;
7446
+ if (!total || typeof total !== "object") continue;
7447
+ const current = rawCounters(total);
7448
+ latest = current;
7449
+ if (options.partial && !sawSnapshot) {
7450
+ previous = current;
7451
+ sawSnapshot = true;
7452
+ continue;
7453
+ }
7454
+ sawSnapshot = true;
7455
+ const delta = normalizedUsage({
7456
+ input: counterDelta(current.input, previous.input),
7457
+ output: counterDelta(current.output, previous.output),
7458
+ cacheCreation: counterDelta(current.cacheCreation, previous.cacheCreation),
7459
+ cacheRead: counterDelta(current.cacheRead, previous.cacheRead)
7460
+ });
7461
+ previous = current;
7462
+ if (lastAssistantLine >= 0) {
7463
+ const prior = turnsByLine.get(lastAssistantLine);
7464
+ turnsByLine.set(lastAssistantLine, {
7465
+ lineIndex: lastAssistantLine,
7466
+ model,
7467
+ usage: addUsage(prior?.usage, delta)
7468
+ });
7469
+ }
7470
+ }
7471
+ if (model) {
7472
+ for (const turn of turnsByLine.values()) {
7473
+ if (!turn.model) turn.model = model;
7474
+ }
7475
+ }
7476
+ if (!latest) return null;
7477
+ return { model, total: normalizedUsage(latest), turnsByLine };
7478
+ }
7479
+ var finiteCounter;
7480
+ var init_codexTranscriptUsage = __esm({
7481
+ "cli/scanning/codexTranscriptUsage.ts"() {
7482
+ "use strict";
7483
+ finiteCounter = (value) => {
7484
+ const n = Number(value);
7485
+ return Number.isFinite(n) && n > 0 ? Math.floor(n) : 0;
7486
+ };
7487
+ }
7488
+ });
7489
+
7490
+ // cli/scanning/codexTranscriptMessages.ts
7491
+ import { createHash as createHash3 } from "crypto";
7492
+ function textContent(content) {
7493
+ if (typeof content === "string") return content.trim();
7494
+ if (!Array.isArray(content)) return "";
7495
+ return content.map((block) => {
7496
+ if (typeof block === "string") return block;
7497
+ return block?.type === "text" || block?.type === "input_text" || block?.type === "output_text" ? String(block.text || "") : "";
7498
+ }).join(" ").trim();
7499
+ }
7500
+ function isCodexConversationNoise(content) {
7501
+ const value = content.trimStart();
7502
+ return value.startsWith("[synkro:") || value.startsWith("claude-code: {") || value.startsWith("RAW claude-code agent:") || value.startsWith("Guard: (R") || value.startsWith("<developer") || value.startsWith("<system") || value.startsWith("<permissions instructions>") || value.startsWith("<app-context>") || value.startsWith("<environment_context>") || value.startsWith("<recommended_plugins>");
7503
+ }
7504
+ function parseCodexConversationMessages(transcript) {
7505
+ if (!transcript) return [];
7506
+ const entries = [];
7507
+ for (const [lineIndex, line] of transcript.split("\n").entries()) {
7508
+ const value = line.trim();
7509
+ if (!value) continue;
7510
+ try {
7511
+ entries.push({ lineIndex, entry: JSON.parse(value) });
7512
+ } catch {
7513
+ }
7514
+ }
7515
+ const hasCanonicalMessages = entries.some(({ entry }) => entry?.type === "event_msg" && (entry?.payload?.type === "user_message" || entry?.payload?.type === "agent_message"));
7516
+ const messages = [];
7517
+ for (const { lineIndex, entry } of entries) {
7518
+ let role = null;
7519
+ let content;
7520
+ let itemId = "";
7521
+ const eventType = entry?.type === "event_msg" ? entry?.payload?.type : "";
7522
+ if (eventType === "user_message" || eventType === "agent_message") {
7523
+ role = eventType === "user_message" ? "user" : "assistant";
7524
+ content = entry?.payload?.message;
7525
+ } else if (!hasCanonicalMessages && entry?.type === "response_item" && entry?.payload?.type === "message") {
7526
+ const responseRole = entry?.payload?.role;
7527
+ if (responseRole !== "user" && responseRole !== "assistant") continue;
7528
+ role = responseRole;
7529
+ content = entry?.payload?.content;
7530
+ itemId = typeof entry?.payload?.id === "string" ? entry.payload.id : "";
7531
+ } else {
7532
+ continue;
7533
+ }
7534
+ if (!role) continue;
7535
+ const text = textContent(content);
7536
+ if (!text || isCodexConversationNoise(text)) continue;
7537
+ const timestamp = typeof entry?.timestamp === "string" && entry.timestamp ? entry.timestamp : void 0;
7538
+ const uuid = typeof entry?.uuid === "string" && entry.uuid ? entry.uuid : itemId || createHash3("sha256").update((timestamp || "") + "\0" + role + "\0" + text).digest("hex").slice(0, 32);
7539
+ messages.push({
7540
+ lineIndex,
7541
+ role,
7542
+ content: text.slice(0, 8e3),
7543
+ uuid,
7544
+ ...timestamp ? { timestamp } : {}
7545
+ });
7546
+ }
7547
+ return messages;
7548
+ }
7549
+ var init_codexTranscriptMessages = __esm({
7550
+ "cli/scanning/codexTranscriptMessages.ts"() {
7551
+ "use strict";
7552
+ }
7553
+ });
7554
+
6684
7555
  // cli/commands/install.ts
6685
7556
  var install_exports = {};
6686
7557
  __export(install_exports, {
@@ -6696,12 +7567,12 @@ __export(install_exports, {
6696
7567
  syncSkillFiles: () => syncSkillFiles,
6697
7568
  writeHookScripts: () => writeHookScripts
6698
7569
  });
6699
- import { existsSync as existsSync22, mkdirSync as mkdirSync15, writeFileSync as writeFileSync16, chmodSync as chmodSync4, readFileSync as readFileSync20, readdirSync as readdirSync4, unlinkSync as unlinkSync7, statSync as statSync2 } from "fs";
6700
- import { homedir as homedir20 } from "os";
6701
- import { join as join18, isAbsolute, resolve as resolve4 } from "path";
7570
+ import { existsSync as existsSync22, mkdirSync as mkdirSync15, writeFileSync as writeFileSync17, chmodSync as chmodSync5, readFileSync as readFileSync21, readdirSync as readdirSync4, unlinkSync as unlinkSync7, statSync as statSync2 } from "fs";
7571
+ import { homedir as homedir21 } from "os";
7572
+ import { join as join19, isAbsolute, resolve as resolve4 } from "path";
6702
7573
  import { execSync as execSync4, spawn as spawn4 } from "child_process";
6703
7574
  import { createInterface as createInterface2 } from "readline";
6704
- import { createHash as createHash2 } from "crypto";
7575
+ import { createHash as createHash4 } from "crypto";
6705
7576
  function resolvePersistedHookMode() {
6706
7577
  return "stub";
6707
7578
  }
@@ -6829,34 +7700,39 @@ function ensureSynkroDir() {
6829
7700
  mkdirSync15(HOOKS_DIR, { recursive: true });
6830
7701
  mkdirSync15(BIN_DIR, { recursive: true });
6831
7702
  mkdirSync15(OFFSETS_DIR, { recursive: true });
6832
- mkdirSync15(join18(SYNKRO_DIR11, "sessions"), { recursive: true });
7703
+ mkdirSync15(join19(SYNKRO_DIR11, "sessions"), { recursive: true });
6833
7704
  }
6834
7705
  function writeHookScripts() {
6835
- const installExtractCorePath = join18(HOOKS_DIR, "installExtractCore.ts");
6836
- const bashScriptPath = join18(HOOKS_DIR, "cc-bash-judge.ts");
6837
- const skillJudgeScriptPath = join18(HOOKS_DIR, "cc-skill-judge.ts");
6838
- const cursorSkillJudgePath = join18(HOOKS_DIR, "cursor-skill-judge.ts");
6839
- const bashFollowupScriptPath = join18(HOOKS_DIR, "cc-bash-followup.ts");
6840
- const editPrecheckScriptPath = join18(HOOKS_DIR, "cc-edit-precheck.ts");
6841
- const cwePrecheckScriptPath = join18(HOOKS_DIR, "cc-cwe-precheck.ts");
6842
- const cvePrecheckScriptPath = join18(HOOKS_DIR, "cc-cve-precheck.ts");
6843
- const planJudgeScriptPath = join18(HOOKS_DIR, "cc-plan-judge.ts");
6844
- const agentJudgeScriptPath = join18(HOOKS_DIR, "cc-agent-judge.ts");
6845
- const stopSummaryScriptPath = join18(HOOKS_DIR, "cc-stop-summary.ts");
6846
- const sessionStartScriptPath = join18(HOOKS_DIR, "cc-session-start.ts");
6847
- const transcriptSyncScriptPath = join18(HOOKS_DIR, "cc-transcript-sync.ts");
6848
- const userPromptSubmitScriptPath = join18(HOOKS_DIR, "cc-user-prompt-submit.ts");
6849
- const promptRouteScriptPath = join18(HOOKS_DIR, "cc-prompt-route.ts");
6850
- const commonScriptPath = join18(HOOKS_DIR, "_synkro-common.ts");
6851
- const commonBashScriptPath = join18(HOOKS_DIR, "_synkro-common.sh");
6852
- const installScanScriptPath = join18(HOOKS_DIR, "cc-install-scan.ts");
6853
- const cursorBashJudgePath = join18(HOOKS_DIR, "cursor-bash-judge.ts");
6854
- const cursorEditCapturePath = join18(HOOKS_DIR, "cursor-edit-capture.ts");
6855
- const cursorAgentCapturePath = join18(HOOKS_DIR, "cursor-agent-capture.ts");
6856
- const mcpStdioProxyPath = join18(HOOKS_DIR, "mcp-stdio-proxy.ts");
6857
- const taskActivateIntentScriptPath = join18(HOOKS_DIR, "cc-task-activate-intent.ts");
6858
- const mcpGateScriptPath = join18(HOOKS_DIR, "cc-mcp-gate.ts");
6859
- const stubCommonPath = join18(HOOKS_DIR, "_synkro-stub-common.ts");
7706
+ const installExtractCorePath = join19(HOOKS_DIR, "installExtractCore.ts");
7707
+ const bashScriptPath = join19(HOOKS_DIR, "cc-bash-judge.ts");
7708
+ const skillJudgeScriptPath = join19(HOOKS_DIR, "cc-skill-judge.ts");
7709
+ const cursorSkillJudgePath = join19(HOOKS_DIR, "cursor-skill-judge.ts");
7710
+ const bashFollowupScriptPath = join19(HOOKS_DIR, "cc-bash-followup.ts");
7711
+ const editPrecheckScriptPath = join19(HOOKS_DIR, "cc-edit-precheck.ts");
7712
+ const editFollowupScriptPath = join19(HOOKS_DIR, "cc-edit-followup.ts");
7713
+ const cwePrecheckScriptPath = join19(HOOKS_DIR, "cc-cwe-precheck.ts");
7714
+ const cvePrecheckScriptPath = join19(HOOKS_DIR, "cc-cve-precheck.ts");
7715
+ const planJudgeScriptPath = join19(HOOKS_DIR, "cc-plan-judge.ts");
7716
+ const agentJudgeScriptPath = join19(HOOKS_DIR, "cc-agent-judge.ts");
7717
+ const stopSummaryScriptPath = join19(HOOKS_DIR, "cc-stop-summary.ts");
7718
+ const cweStopScriptPath = join19(HOOKS_DIR, "codex-cwe-stop.ts");
7719
+ const cveStopScriptPath = join19(HOOKS_DIR, "codex-cve-stop.ts");
7720
+ const sessionStartScriptPath = join19(HOOKS_DIR, "cc-session-start.ts");
7721
+ const transcriptSyncScriptPath = join19(HOOKS_DIR, "cc-transcript-sync.ts");
7722
+ const subagentStartScriptPath = join19(HOOKS_DIR, "codex-subagent-start.ts");
7723
+ const subagentStopScriptPath = join19(HOOKS_DIR, "codex-subagent-stop.ts");
7724
+ const userPromptSubmitScriptPath = join19(HOOKS_DIR, "cc-user-prompt-submit.ts");
7725
+ const promptRouteScriptPath = join19(HOOKS_DIR, "cc-prompt-route.ts");
7726
+ const commonScriptPath = join19(HOOKS_DIR, "_synkro-common.ts");
7727
+ const commonBashScriptPath = join19(HOOKS_DIR, "_synkro-common.sh");
7728
+ const installScanScriptPath = join19(HOOKS_DIR, "cc-install-scan.ts");
7729
+ const cursorBashJudgePath = join19(HOOKS_DIR, "cursor-bash-judge.ts");
7730
+ const cursorEditCapturePath = join19(HOOKS_DIR, "cursor-edit-capture.ts");
7731
+ const cursorAgentCapturePath = join19(HOOKS_DIR, "cursor-agent-capture.ts");
7732
+ const mcpStdioProxyPath = join19(HOOKS_DIR, "mcp-stdio-proxy.ts");
7733
+ const taskActivateIntentScriptPath = join19(HOOKS_DIR, "cc-task-activate-intent.ts");
7734
+ const mcpGateScriptPath = join19(HOOKS_DIR, "cc-mcp-gate.ts");
7735
+ const stubCommonPath = join19(HOOKS_DIR, "_synkro-stub-common.ts");
6860
7736
  const stubFiles = [
6861
7737
  [stubCommonPath, STUB_COMMON_TS],
6862
7738
  [bashScriptPath, STUB_BASH_JUDGE_TS],
@@ -6864,13 +7740,18 @@ function writeHookScripts() {
6864
7740
  [cursorSkillJudgePath, STUB_CURSOR_SKILL_JUDGE_TS],
6865
7741
  [bashFollowupScriptPath, STUB_BASH_FOLLOWUP_TS],
6866
7742
  [editPrecheckScriptPath, STUB_EDIT_PRECHECK_TS],
7743
+ [editFollowupScriptPath, STUB_EDIT_FOLLOWUP_TS],
6867
7744
  [cwePrecheckScriptPath, STUB_CWE_PRECHECK_TS],
6868
7745
  [cvePrecheckScriptPath, STUB_CVE_PRECHECK_TS],
6869
7746
  [planJudgeScriptPath, STUB_PLAN_JUDGE_TS],
6870
7747
  [agentJudgeScriptPath, STUB_AGENT_JUDGE_TS],
6871
7748
  [stopSummaryScriptPath, STUB_STOP_SUMMARY_TS],
7749
+ [cweStopScriptPath, STUB_CODEX_CWE_STOP_TS],
7750
+ [cveStopScriptPath, STUB_CODEX_CVE_STOP_TS],
6872
7751
  [sessionStartScriptPath, STUB_SESSION_START_TS],
6873
7752
  [transcriptSyncScriptPath, STUB_TRANSCRIPT_SYNC_TS],
7753
+ [subagentStartScriptPath, STUB_SUBAGENT_START_TS],
7754
+ [subagentStopScriptPath, STUB_SUBAGENT_STOP_TS],
6874
7755
  [userPromptSubmitScriptPath, STUB_USER_PROMPT_SUBMIT_TS],
6875
7756
  [promptRouteScriptPath, STUB_PROMPT_ROUTE_TS],
6876
7757
  [installScanScriptPath, STUB_INSTALL_SCAN_TS],
@@ -6881,14 +7762,14 @@ function writeHookScripts() {
6881
7762
  [cursorAgentCapturePath, STUB_CURSOR_AGENT_CAPTURE_TS]
6882
7763
  ];
6883
7764
  for (const [p, content] of stubFiles) {
6884
- writeFileSync16(p, content, "utf-8");
6885
- chmodSync4(p, 493);
7765
+ writeFileSync17(p, content, "utf-8");
7766
+ chmodSync5(p, 493);
6886
7767
  }
6887
- writeFileSync16(mcpStdioProxyPath, MCP_STDIO_PROXY_SRC, "utf-8");
6888
- chmodSync4(mcpStdioProxyPath, 493);
7768
+ writeFileSync17(mcpStdioProxyPath, MCP_STDIO_PROXY_SRC, "utf-8");
7769
+ chmodSync5(mcpStdioProxyPath, 493);
6889
7770
  for (const stale of ["_synkro-common.ts", "_synkro-common.sh", "installExtractCore.ts"]) {
6890
7771
  try {
6891
- unlinkSync7(join18(HOOKS_DIR, stale));
7772
+ unlinkSync7(join19(HOOKS_DIR, stale));
6892
7773
  } catch {
6893
7774
  }
6894
7775
  }
@@ -6898,13 +7779,18 @@ function writeHookScripts() {
6898
7779
  cursorSkillJudgeScript: cursorSkillJudgePath,
6899
7780
  bashFollowupScript: bashFollowupScriptPath,
6900
7781
  editPrecheckScript: editPrecheckScriptPath,
7782
+ editFollowupScript: editFollowupScriptPath,
6901
7783
  cwePrecheckScript: cwePrecheckScriptPath,
6902
7784
  cvePrecheckScript: cvePrecheckScriptPath,
6903
7785
  planJudgeScript: planJudgeScriptPath,
6904
7786
  agentJudgeScript: agentJudgeScriptPath,
6905
7787
  stopSummaryScript: stopSummaryScriptPath,
7788
+ cweStopScript: cweStopScriptPath,
7789
+ cveStopScript: cveStopScriptPath,
6906
7790
  sessionStartScript: sessionStartScriptPath,
6907
7791
  transcriptSyncScript: transcriptSyncScriptPath,
7792
+ subagentStartScript: subagentStartScriptPath,
7793
+ subagentStopScript: subagentStopScriptPath,
6908
7794
  userPromptSubmitScript: userPromptSubmitScriptPath,
6909
7795
  promptRouteScript: promptRouteScriptPath,
6910
7796
  installScanScript: installScanScriptPath,
@@ -6928,7 +7814,7 @@ function resolveSynkroBundle() {
6928
7814
  return null;
6929
7815
  }
6930
7816
  function writeConfigEnv(opts) {
6931
- const credsPath = join18(SYNKRO_DIR11, "credentials.json");
7817
+ const credsPath = join19(SYNKRO_DIR11, "credentials.json");
6932
7818
  const safeGateway = sanitizeConfigValue(opts.gatewayUrl);
6933
7819
  const safeUserId = sanitizeConfigValue(opts.userId);
6934
7820
  const safeOrgId = sanitizeConfigValue(opts.orgId);
@@ -6944,7 +7830,7 @@ function writeConfigEnv(opts) {
6944
7830
  `SYNKRO_CREDENTIALS_PATH=${shellQuoteSingle2(credsPath)}`,
6945
7831
  `SYNKRO_TIER=${shellQuoteSingle2(safeTier)}`,
6946
7832
  `SYNKRO_INFERENCE=${shellQuoteSingle2(safeInference)}`,
6947
- `SYNKRO_VERSION=${shellQuoteSingle2("1.7.88")}`
7833
+ `SYNKRO_VERSION=${shellQuoteSingle2("1.7.90")}`
6948
7834
  ];
6949
7835
  if (safeSynkroBin) lines.push(`SYNKRO_CLI_BIN=${shellQuoteSingle2(safeSynkroBin)}`);
6950
7836
  if (safeUserId) lines.push(`SYNKRO_USER_ID=${shellQuoteSingle2(safeUserId)}`);
@@ -6964,12 +7850,12 @@ function writeConfigEnv(opts) {
6964
7850
  lines.push(`SYNKRO_DEPLOY_LOCATION=${shellQuoteSingle2(sanitizeConfigValue(opts.deployLocation ?? "local", 16))}`);
6965
7851
  lines.push(`SYNKRO_HOOK_MODE=${shellQuoteSingle2(sanitizeConfigValue(opts.hookMode ?? "stub", 8))}`);
6966
7852
  lines.push("");
6967
- writeFileSync16(CONFIG_PATH4, lines.join("\n"), "utf-8");
6968
- chmodSync4(CONFIG_PATH4, 384);
7853
+ writeFileSync17(CONFIG_PATH4, lines.join("\n"), "utf-8");
7854
+ chmodSync5(CONFIG_PATH4, 384);
6969
7855
  }
6970
7856
  function persistedTranscriptConsent(source) {
6971
7857
  try {
6972
- const env = readFileSync20(CONFIG_PATH4, "utf-8");
7858
+ const env = readFileSync21(CONFIG_PATH4, "utf-8");
6973
7859
  const specific = env.match(new RegExp(`^SYNKRO_TRANSCRIPT_CONSENT_${source}='(yes|no)'`, "m"));
6974
7860
  if (specific) return specific[1] === "yes";
6975
7861
  if (source !== "CODEX") {
@@ -6992,7 +7878,7 @@ async function getOrMintCloudToken(gatewayUrl) {
6992
7878
  assertGatewayAllowed(gatewayUrl);
6993
7879
  let stored = "";
6994
7880
  try {
6995
- stored = readFileSync20(CLOUD_JWT_PATH, "utf-8").trim();
7881
+ stored = readFileSync21(CLOUD_JWT_PATH, "utf-8").trim();
6996
7882
  } catch {
6997
7883
  }
6998
7884
  if (stored && !jwtExpired(stored)) return stored;
@@ -7008,7 +7894,7 @@ async function getOrMintCloudToken(gatewayUrl) {
7008
7894
  throw new Error(`cloud-token mint failed (${resp.status}): ${t.slice(0, 200)}`);
7009
7895
  }
7010
7896
  const { token } = await resp.json();
7011
- writeFileSync16(CLOUD_JWT_PATH, token + "\n", { mode: 384 });
7897
+ writeFileSync17(CLOUD_JWT_PATH, token + "\n", { mode: 384 });
7012
7898
  return token;
7013
7899
  }
7014
7900
  async function provisionCloudContainer(opts) {
@@ -7092,7 +7978,7 @@ async function provisionCloudContainer(opts) {
7092
7978
  let cursorApiKey = "";
7093
7979
  if (cursorWorkers > 0 || selectedKind === "cursor") {
7094
7980
  try {
7095
- cursorApiKey = readFileSync20(join18(SYNKRO_DIR11, "cursor-creds", "api-key"), "utf-8").trim();
7981
+ cursorApiKey = readFileSync21(join19(SYNKRO_DIR11, "cursor-creds", "api-key"), "utf-8").trim();
7096
7982
  } catch {
7097
7983
  }
7098
7984
  }
@@ -7278,7 +8164,7 @@ async function verifyCloudGrader(jwt2, requestedKind) {
7278
8164
  function readPersistedDeployLocation() {
7279
8165
  try {
7280
8166
  if (existsSync22(CONFIG_PATH4)) {
7281
- const m = readFileSync20(CONFIG_PATH4, "utf-8").match(/^SYNKRO_DEPLOY_LOCATION='([^']*)'/m);
8167
+ const m = readFileSync21(CONFIG_PATH4, "utf-8").match(/^SYNKRO_DEPLOY_LOCATION='([^']*)'/m);
7282
8168
  if (m?.[1] === "cloud") return "cloud";
7283
8169
  }
7284
8170
  } catch {
@@ -7287,7 +8173,7 @@ function readPersistedDeployLocation() {
7287
8173
  }
7288
8174
  function updateConfigEnvLocation(location) {
7289
8175
  if (!existsSync22(CONFIG_PATH4)) return;
7290
- let env = readFileSync20(CONFIG_PATH4, "utf-8");
8176
+ let env = readFileSync21(CONFIG_PATH4, "utf-8");
7291
8177
  const set = (k, v) => {
7292
8178
  const re = new RegExp(`^${k}=.*$`, "m");
7293
8179
  const line = `${k}='${v}'`;
@@ -7295,14 +8181,14 @@ function updateConfigEnvLocation(location) {
7295
8181
  };
7296
8182
  set("SYNKRO_DEPLOY_LOCATION", location);
7297
8183
  set("SYNKRO_STORAGE_MODE", location === "cloud" ? "cloud" : "local");
7298
- writeFileSync16(CONFIG_PATH4, env, "utf-8");
7299
- chmodSync4(CONFIG_PATH4, 384);
8184
+ writeFileSync17(CONFIG_PATH4, env, "utf-8");
8185
+ chmodSync5(CONFIG_PATH4, 384);
7300
8186
  }
7301
8187
  async function applyMcpConfig(opts) {
7302
8188
  if (!opts.hasClaudeCode && !opts.hasCursor && !opts.hasCodex) return;
7303
8189
  let mcpJwt2 = "";
7304
8190
  try {
7305
- mcpJwt2 = readFileSync20(join18(SYNKRO_DIR11, ".mcp-jwt"), "utf-8").trim();
8191
+ mcpJwt2 = readFileSync21(join19(SYNKRO_DIR11, ".mcp-jwt"), "utf-8").trim();
7306
8192
  } catch {
7307
8193
  }
7308
8194
  if (!mcpJwt2) {
@@ -7433,7 +8319,7 @@ function resolveDeploymentMode() {
7433
8319
  if (envOverride === "bare-host" || envOverride === "docker") return envOverride;
7434
8320
  try {
7435
8321
  if (existsSync22(CONFIG_PATH4)) {
7436
- const m = readFileSync20(CONFIG_PATH4, "utf-8").match(/^SYNKRO_DEPLOYMENT_MODE='([^']*)'/m);
8322
+ const m = readFileSync21(CONFIG_PATH4, "utf-8").match(/^SYNKRO_DEPLOYMENT_MODE='([^']*)'/m);
7437
8323
  const val = m?.[1]?.toLowerCase();
7438
8324
  if (val === "bare-host" || val === "docker") return val;
7439
8325
  }
@@ -7460,16 +8346,16 @@ function collectLocalMetadata(includeClaudeCode = true) {
7460
8346
  meta.cc_version = execSync4("claude --version", { encoding: "utf-8", timeout: 5e3 }).trim().split("\n")[0];
7461
8347
  } catch {
7462
8348
  }
7463
- const claudeDir = join18(homedir20(), ".claude");
8349
+ const claudeDir = join19(homedir21(), ".claude");
7464
8350
  try {
7465
- const settings = JSON.parse(readFileSync20(join18(claudeDir, "settings.json"), "utf-8"));
8351
+ const settings = JSON.parse(readFileSync21(join19(claudeDir, "settings.json"), "utf-8"));
7466
8352
  const plugins = Object.keys(settings.enabledPlugins ?? {}).filter((k) => settings.enabledPlugins[k]);
7467
8353
  if (plugins.length) meta.enabled_plugins = plugins;
7468
8354
  if (settings.permissions?.defaultMode) meta.permissions_mode = settings.permissions.defaultMode;
7469
8355
  } catch {
7470
8356
  }
7471
8357
  try {
7472
- const mcpCache = JSON.parse(readFileSync20(join18(claudeDir, "mcp-needs-auth-cache.json"), "utf-8"));
8358
+ const mcpCache = JSON.parse(readFileSync21(join19(claudeDir, "mcp-needs-auth-cache.json"), "utf-8"));
7473
8359
  const mcpNames = Object.keys(mcpCache);
7474
8360
  if (mcpNames.length) meta.mcp_servers = mcpNames;
7475
8361
  } catch {
@@ -7481,10 +8367,10 @@ function collectLocalMetadata(includeClaudeCode = true) {
7481
8367
  } catch {
7482
8368
  }
7483
8369
  try {
7484
- const sessionsDir = join18(claudeDir, "sessions");
8370
+ const sessionsDir = join19(claudeDir, "sessions");
7485
8371
  const files = readdirSync4(sessionsDir).filter((f) => f.endsWith(".json")).slice(-5);
7486
8372
  for (const f of files) {
7487
- const s = JSON.parse(readFileSync20(join18(sessionsDir, f), "utf-8"));
8373
+ const s = JSON.parse(readFileSync21(join19(sessionsDir, f), "utf-8"));
7488
8374
  if (s.version) {
7489
8375
  meta.cc_version = meta.cc_version || s.version;
7490
8376
  break;
@@ -7686,7 +8572,7 @@ async function installCommand(opts = {}) {
7686
8572
  await setTelemetryState({ enabled: true, remoteFlushEnabled: telemetryConsent });
7687
8573
  emit("install", {
7688
8574
  phase: "started",
7689
- cli_version_to: "1.7.88",
8575
+ cli_version_to: "1.7.90",
7690
8576
  agents_detected: agents.map((a) => a.kind),
7691
8577
  with_github: false,
7692
8578
  with_local_cc: false,
@@ -7698,9 +8584,9 @@ async function installCommand(opts = {}) {
7698
8584
  const scripts = writeHookScripts();
7699
8585
  console.log("Wrote hook scripts to ~/.synkro/hooks/\n");
7700
8586
  for (const mode of ["edit", "bash"]) {
7701
- const pidFile = join18(SYNKRO_DIR11, "daemon", mode, "daemon.pid");
8587
+ const pidFile = join19(SYNKRO_DIR11, "daemon", mode, "daemon.pid");
7702
8588
  try {
7703
- const pid = parseInt(readFileSync20(pidFile, "utf-8").trim(), 10);
8589
+ const pid = parseInt(readFileSync21(pidFile, "utf-8").trim(), 10);
7704
8590
  if (pid > 0) {
7705
8591
  process.kill(pid, "SIGTERM");
7706
8592
  console.log(`Stopped stale ${mode} grader daemon (pid ${pid})`);
@@ -7763,12 +8649,17 @@ async function installCommand(opts = {}) {
7763
8649
  skillJudgeScriptPath: scripts.skillJudgeScript,
7764
8650
  bashFollowupScriptPath: scripts.bashFollowupScript,
7765
8651
  editPrecheckScriptPath: scripts.editPrecheckScript,
8652
+ editFollowupScriptPath: scripts.editFollowupScript,
7766
8653
  cwePrecheckScriptPath: scripts.cwePrecheckScript,
7767
8654
  cvePrecheckScriptPath: scripts.cvePrecheckScript,
7768
8655
  agentJudgeScriptPath: scripts.agentJudgeScript,
7769
8656
  stopSummaryScriptPath: scripts.stopSummaryScript,
8657
+ cweStopScriptPath: scripts.cweStopScript,
8658
+ cveStopScriptPath: scripts.cveStopScript,
7770
8659
  sessionStartScriptPath: scripts.sessionStartScript,
7771
8660
  transcriptSyncScriptPath: scripts.transcriptSyncScript,
8661
+ subagentStartScriptPath: scripts.subagentStartScript,
8662
+ subagentStopScriptPath: scripts.subagentStopScript,
7772
8663
  userPromptSubmitScriptPath: scripts.userPromptSubmitScript,
7773
8664
  promptRouteScriptPath: scripts.promptRouteScript,
7774
8665
  installScanScriptPath: scripts.installScanScript,
@@ -7818,7 +8709,7 @@ async function installCommand(opts = {}) {
7818
8709
  if (mintResp.ok) {
7819
8710
  const minted = await mintResp.json();
7820
8711
  mcpJwt2 = minted.token;
7821
- writeFileSync16(join18(SYNKRO_DIR11, ".mcp-jwt"), mcpJwt2 + "\n", { mode: 384 });
8712
+ writeFileSync17(join19(SYNKRO_DIR11, ".mcp-jwt"), mcpJwt2 + "\n", { mode: 384 });
7822
8713
  } else {
7823
8714
  console.warn(" \u26A0 Could not mint MCP token \u2014 local server will reject requests until re-installed.");
7824
8715
  }
@@ -7846,7 +8737,7 @@ async function installCommand(opts = {}) {
7846
8737
  throw new Error(`mcp-token mint failed (${mintResp.status}): ${errText.slice(0, 200)}`);
7847
8738
  }
7848
8739
  const minted = await mintResp.json();
7849
- writeFileSync16(join18(SYNKRO_DIR11, ".mcp-jwt"), minted.token + "\n", { mode: 384 });
8740
+ writeFileSync17(join19(SYNKRO_DIR11, ".mcp-jwt"), minted.token + "\n", { mode: 384 });
7850
8741
  const mcp = installMcpConfig({ gatewayUrl, bearerToken: minted.token });
7851
8742
  console.log(`Registered Synkro guardrails MCP server in ${mcp.path}`);
7852
8743
  console.log(` url: ${mcp.url}`);
@@ -7868,7 +8759,7 @@ async function installCommand(opts = {}) {
7868
8759
  if (hasCursor && !opts.noMcp) {
7869
8760
  try {
7870
8761
  if (useLocalMcp) {
7871
- const jwtPath = join18(SYNKRO_DIR11, ".mcp-jwt");
8762
+ const jwtPath = join19(SYNKRO_DIR11, ".mcp-jwt");
7872
8763
  if (!existsSync22(jwtPath)) {
7873
8764
  const mintResp = await fetch(`${gatewayUrl}/api/v1/cli/mcp-token`, {
7874
8765
  method: "POST",
@@ -7877,7 +8768,7 @@ async function installCommand(opts = {}) {
7877
8768
  });
7878
8769
  if (mintResp.ok) {
7879
8770
  const minted = await mintResp.json();
7880
- writeFileSync16(jwtPath, minted.token + "\n", { mode: 384 });
8771
+ writeFileSync17(jwtPath, minted.token + "\n", { mode: 384 });
7881
8772
  }
7882
8773
  }
7883
8774
  const mcp = installCursorMcpConfig({ gatewayUrl, bearerToken: "", local: true });
@@ -7897,7 +8788,7 @@ async function installCommand(opts = {}) {
7897
8788
  throw new Error(`mcp-token mint failed (${mintResp.status}): ${errText.slice(0, 200)}`);
7898
8789
  }
7899
8790
  const minted = await mintResp.json();
7900
- writeFileSync16(join18(SYNKRO_DIR11, ".mcp-jwt"), minted.token + "\n", { mode: 384 });
8791
+ writeFileSync17(join19(SYNKRO_DIR11, ".mcp-jwt"), minted.token + "\n", { mode: 384 });
7901
8792
  const mcp = installCursorMcpConfig({ gatewayUrl, bearerToken: minted.token });
7902
8793
  console.log(`Registered Synkro guardrails MCP server in ${mcp.path}`);
7903
8794
  console.log(` url: ${mcp.url}`);
@@ -7910,10 +8801,10 @@ async function installCommand(opts = {}) {
7910
8801
  }
7911
8802
  if (hasCodex && !opts.noMcp) {
7912
8803
  try {
7913
- const jwtPath = join18(SYNKRO_DIR11, ".mcp-jwt");
8804
+ const jwtPath = join19(SYNKRO_DIR11, ".mcp-jwt");
7914
8805
  let mcpJwt2 = "";
7915
8806
  try {
7916
- mcpJwt2 = readFileSync20(jwtPath, "utf-8").trim();
8807
+ mcpJwt2 = readFileSync21(jwtPath, "utf-8").trim();
7917
8808
  } catch {
7918
8809
  }
7919
8810
  if (!mcpJwt2) {
@@ -7931,7 +8822,7 @@ async function installCommand(opts = {}) {
7931
8822
  }
7932
8823
  const minted = await mintResp.json();
7933
8824
  mcpJwt2 = minted.token;
7934
- writeFileSync16(jwtPath, mcpJwt2 + "\n", { mode: 384 });
8825
+ writeFileSync17(jwtPath, mcpJwt2 + "\n", { mode: 384 });
7935
8826
  }
7936
8827
  const mcp = installCodexMcpConfig({
7937
8828
  gatewayUrl,
@@ -8043,14 +8934,15 @@ async function installCommand(opts = {}) {
8043
8934
  }
8044
8935
  console.log(` worker pool: ${claudeWorkers} claude + ${cursorWorkers} cursor + ${codexWorkers} codex`);
8045
8936
  const connectedRepo = detectGitRepo2() || void 0;
8046
- const { image, hostMcpPort, hostGraderPort, hostCwePort, hostPglitePort } = await dockerInstall({ claudeWorkers, cursorWorkers, codexWorkers, conductorProvider, codexHomeDir, connectedRepo });
8937
+ const { image, hostMcpPort, hostGraderPort, hostCwePort, hostPglitePort, pglitePasswordPath } = await dockerInstall({ claudeWorkers, cursorWorkers, codexWorkers, conductorProvider, codexHomeDir, connectedRepo });
8047
8938
  console.log(` \u2713 pulled ${image}`);
8048
- console.log(` container started \u2014 MCP=${hostMcpPort} general=${hostGraderPort} CWE=${hostCwePort} pglite=${hostPglitePort}`);
8939
+ console.log(` container started \u2014 MCP=${hostMcpPort} general=${hostGraderPort} CWE=${hostCwePort}`);
8940
+ console.log(` PGLite: postgresql://synkro@127.0.0.1:${hostPglitePort}/postgres (password: ${pglitePasswordPath})`);
8049
8941
  console.log(" waiting for container to be ready...");
8050
8942
  const ready = await waitForContainerReady(6e4);
8051
8943
  if (ready) {
8052
8944
  console.log(" \u2713 container ready");
8053
- const mcpJwt2 = readFileSync20(join18(SYNKRO_DIR11, ".mcp-jwt"), "utf-8").trim();
8945
+ const mcpJwt2 = readFileSync21(join19(SYNKRO_DIR11, ".mcp-jwt"), "utf-8").trim();
8054
8946
  try {
8055
8947
  const ingestResp = await fetch(`http://127.0.0.1:${hostMcpPort}/api/ingest`, {
8056
8948
  method: "POST",
@@ -8093,7 +8985,7 @@ async function installCommand(opts = {}) {
8093
8985
  try {
8094
8986
  let mcpToken = "";
8095
8987
  try {
8096
- mcpToken = readFileSync20(join18(SYNKRO_DIR11, ".mcp-jwt"), "utf-8").trim();
8988
+ mcpToken = readFileSync21(join19(SYNKRO_DIR11, ".mcp-jwt"), "utf-8").trim();
8097
8989
  } catch {
8098
8990
  }
8099
8991
  if (mcpToken) {
@@ -8261,8 +9153,8 @@ function writeSynkroFileIfMissing(opts) {
8261
9153
  try {
8262
9154
  const root = execSync4("git rev-parse --show-toplevel", { encoding: "utf-8", timeout: 3e3, stdio: ["pipe", "pipe", "pipe"] }).trim();
8263
9155
  if (!root) return;
8264
- if (root === homedir20()) return;
8265
- const fp = join18(root, "synkro.toml");
9156
+ if (root === homedir21()) return;
9157
+ const fp = join19(root, "synkro.toml");
8266
9158
  let hasFile = false;
8267
9159
  try {
8268
9160
  hasFile = statSync2(fp).isFile();
@@ -8294,7 +9186,7 @@ function writeSynkroFileIfMissing(opts) {
8294
9186
  "cve = true",
8295
9187
  ""
8296
9188
  ].join("\n");
8297
- writeFileSync16(fp, toml, "utf-8");
9189
+ writeFileSync17(fp, toml, "utf-8");
8298
9190
  console.log(` synkro.toml: wrote ${fp} (pool=${pool}, mode=${mode})`);
8299
9191
  } catch {
8300
9192
  }
@@ -8302,12 +9194,12 @@ function writeSynkroFileIfMissing(opts) {
8302
9194
  function updateSynkroTomlLocation(location) {
8303
9195
  try {
8304
9196
  const root = execSync4("git rev-parse --show-toplevel", { encoding: "utf-8", timeout: 3e3, stdio: ["pipe", "pipe", "pipe"] }).trim();
8305
- if (!root || root === homedir20()) return;
8306
- const fp = join18(root, "synkro.toml");
9197
+ if (!root || root === homedir21()) return;
9198
+ const fp = join19(root, "synkro.toml");
8307
9199
  let txt = "";
8308
9200
  try {
8309
9201
  if (!statSync2(fp).isFile()) return;
8310
- txt = readFileSync20(fp, "utf-8");
9202
+ txt = readFileSync21(fp, "utf-8");
8311
9203
  } catch {
8312
9204
  return;
8313
9205
  }
@@ -8315,7 +9207,7 @@ function updateSynkroTomlLocation(location) {
8315
9207
  if (!re.test(txt)) return;
8316
9208
  const next = txt.replace(re, `$1"${location}"`);
8317
9209
  if (next !== txt) {
8318
- writeFileSync16(fp, next, "utf-8");
9210
+ writeFileSync17(fp, next, "utf-8");
8319
9211
  console.log(` synkro.toml: [grader] location = "${location}"`);
8320
9212
  }
8321
9213
  } catch {
@@ -8325,9 +9217,9 @@ function readFullSynkroFile() {
8325
9217
  try {
8326
9218
  const root = execSync4("git rev-parse --show-toplevel", { encoding: "utf-8", timeout: 3e3, stdio: ["pipe", "pipe", "pipe"] }).trim();
8327
9219
  if (!root) return null;
8328
- const fp = join18(root, "synkro.toml");
9220
+ const fp = join19(root, "synkro.toml");
8329
9221
  if (!existsSync22(fp)) return null;
8330
- const parsed = parseSynkroToml2(readFileSync20(fp, "utf-8"));
9222
+ const parsed = parseSynkroToml2(readFileSync21(fp, "utf-8"));
8331
9223
  const valid = ["claude-code", "cursor", "codex"];
8332
9224
  const harness = Array.isArray(parsed.harness) ? parsed.harness.filter((h) => valid.includes(h)) : ["claude-code", "cursor"];
8333
9225
  const resolved = resolveGraderPool(parsed);
@@ -8367,7 +9259,7 @@ function reconcileHarness() {
8367
9259
  console.log(`synkro.toml: harness=[${sf.harness.join(", ")}] pool=${poolLabel(sf.grader.pool)} mode=${sf.grader.mode}`);
8368
9260
  const scripts = writeHookScripts();
8369
9261
  console.log("Wrote hook scripts to ~/.synkro/hooks/");
8370
- const ccSettings = join18(homedir20(), ".claude", "settings.json");
9262
+ const ccSettings = join19(homedir21(), ".claude", "settings.json");
8371
9263
  if (wantCC) {
8372
9264
  installCCHooks(ccSettings, {
8373
9265
  bashJudgeScriptPath: scripts.bashScript,
@@ -8390,7 +9282,7 @@ function reconcileHarness() {
8390
9282
  });
8391
9283
  console.log(" \u2713 Claude Code hooks registered");
8392
9284
  try {
8393
- const mcpJwt2 = readFileSync20(join18(SYNKRO_DIR11, ".mcp-jwt"), "utf-8").trim();
9285
+ const mcpJwt2 = readFileSync21(join19(SYNKRO_DIR11, ".mcp-jwt"), "utf-8").trim();
8394
9286
  if (mcpJwt2) {
8395
9287
  installMcpConfig({ gatewayUrl: "", bearerToken: mcpJwt2, local: true });
8396
9288
  console.log(" \u2713 Claude Code MCP registered");
@@ -8402,7 +9294,7 @@ function reconcileHarness() {
8402
9294
  if (uninstallMcpConfig()) console.log(" \u2717 Claude Code MCP removed");
8403
9295
  if (uninstallClaudeDesktopMcpConfig()) console.log(" \u2717 Claude Desktop MCP removed");
8404
9296
  }
8405
- const cursorHooks = join18(homedir20(), ".cursor", "hooks.json");
9297
+ const cursorHooks = join19(homedir21(), ".cursor", "hooks.json");
8406
9298
  if (wantCursor) {
8407
9299
  installCursorHooks(cursorHooks, {
8408
9300
  bashJudgeScriptPath: scripts.cursorBashJudgeScript,
@@ -8433,19 +9325,24 @@ function reconcileHarness() {
8433
9325
  if (uninstallCursorHooks(cursorHooks)) console.log(" \u2717 Cursor hooks removed");
8434
9326
  if (uninstallCursorMcpConfig()) console.log(" \u2717 Cursor MCP removed");
8435
9327
  }
8436
- const codexHooks = join18(process.env.CODEX_HOME || join18(homedir20(), ".codex"), "hooks.json");
9328
+ const codexHooks = join19(process.env.CODEX_HOME || join19(homedir21(), ".codex"), "hooks.json");
8437
9329
  if (wantCodex) {
8438
9330
  installCodexHooks(codexHooks, {
8439
9331
  bashJudgeScriptPath: scripts.bashScript,
8440
9332
  skillJudgeScriptPath: scripts.skillJudgeScript,
8441
9333
  bashFollowupScriptPath: scripts.bashFollowupScript,
8442
9334
  editPrecheckScriptPath: scripts.editPrecheckScript,
9335
+ editFollowupScriptPath: scripts.editFollowupScript,
8443
9336
  cwePrecheckScriptPath: scripts.cwePrecheckScript,
8444
9337
  cvePrecheckScriptPath: scripts.cvePrecheckScript,
8445
9338
  agentJudgeScriptPath: scripts.agentJudgeScript,
8446
9339
  stopSummaryScriptPath: scripts.stopSummaryScript,
9340
+ cweStopScriptPath: scripts.cweStopScript,
9341
+ cveStopScriptPath: scripts.cveStopScript,
8447
9342
  sessionStartScriptPath: scripts.sessionStartScript,
8448
9343
  transcriptSyncScriptPath: scripts.transcriptSyncScript,
9344
+ subagentStartScriptPath: scripts.subagentStartScript,
9345
+ subagentStopScriptPath: scripts.subagentStopScript,
8449
9346
  userPromptSubmitScriptPath: scripts.userPromptSubmitScript,
8450
9347
  promptRouteScriptPath: scripts.promptRouteScript,
8451
9348
  installScanScriptPath: scripts.installScanScript,
@@ -8525,7 +9422,7 @@ async function syncSkillFiles() {
8525
9422
  if (resolved.length === 0) return;
8526
9423
  const mcpPort = process.env.SYNKRO_MCP_PORT || "18931";
8527
9424
  const tasks = resolved.map((fp) => {
8528
- const content = readFileSync20(fp, "utf-8");
9425
+ const content = readFileSync21(fp, "utf-8");
8529
9426
  const source = `skill:${fp.split("/").pop()}`;
8530
9427
  if (!content.trim()) {
8531
9428
  console.log(` \u2298 skill ${source}: empty file, skipped`);
@@ -8550,11 +9447,11 @@ function discoverSkillFiles(repoRoot2, excludeHashes, ingestedHashes, ingestedNa
8550
9447
  } catch {
8551
9448
  }
8552
9449
  };
8553
- add(join18(homedir20(), ".claude", "skills"));
8554
- add(join18(homedir20(), ".agents", "skills"));
9450
+ add(join19(homedir21(), ".claude", "skills"));
9451
+ add(join19(homedir21(), ".agents", "skills"));
8555
9452
  if (repoRoot2) {
8556
- add(join18(repoRoot2, ".claude", "skills"));
8557
- add(join18(repoRoot2, ".agents", "skills"));
9453
+ add(join19(repoRoot2, ".claude", "skills"));
9454
+ add(join19(repoRoot2, ".agents", "skills"));
8558
9455
  }
8559
9456
  const out = [];
8560
9457
  const seen = /* @__PURE__ */ new Set();
@@ -8565,12 +9462,12 @@ function discoverSkillFiles(repoRoot2, excludeHashes, ingestedHashes, ingestedNa
8565
9462
  try {
8566
9463
  const st = statSync2(file);
8567
9464
  if (!st.isFile() || st.size > 2e5) return;
8568
- content = readFileSync20(file, "utf-8");
9465
+ content = readFileSync21(file, "utf-8");
8569
9466
  } catch {
8570
9467
  return;
8571
9468
  }
8572
9469
  if (!content.trim()) return;
8573
- const hash = createHash2("sha256").update(content).digest("hex");
9470
+ const hash = createHash4("sha256").update(content).digest("hex");
8574
9471
  if (seen.has(hash) || excludeHashes.has(hash)) return;
8575
9472
  seen.add(hash);
8576
9473
  const ingested = ingestedHashes.has(hash) || ingestedNames.has(normSkillName(name));
@@ -8585,7 +9482,7 @@ function discoverSkillFiles(repoRoot2, excludeHashes, ingestedHashes, ingestedNa
8585
9482
  }
8586
9483
  for (const entry of entries) {
8587
9484
  if (entry.startsWith(".")) continue;
8588
- const full = join18(root, entry);
9485
+ const full = join19(root, entry);
8589
9486
  let st;
8590
9487
  try {
8591
9488
  st = statSync2(full);
@@ -8593,7 +9490,7 @@ function discoverSkillFiles(repoRoot2, excludeHashes, ingestedHashes, ingestedNa
8593
9490
  continue;
8594
9491
  }
8595
9492
  if (st.isDirectory()) {
8596
- const skillMd = join18(full, "SKILL.md");
9493
+ const skillMd = join19(full, "SKILL.md");
8597
9494
  if (existsSync22(skillMd)) consider(skillMd, entry);
8598
9495
  } else if (/\.mdx?$/i.test(entry) && entry.toUpperCase() !== "README.MD") {
8599
9496
  consider(full, entry.replace(/\.mdx?$/i, ""));
@@ -8603,7 +9500,7 @@ function discoverSkillFiles(repoRoot2, excludeHashes, ingestedHashes, ingestedNa
8603
9500
  return out;
8604
9501
  }
8605
9502
  function discoverySetHash(found) {
8606
- return createHash2("sha256").update(found.map((f) => f.hash).sort().join(",")).digest("hex");
9503
+ return createHash4("sha256").update(found.map((f) => f.hash).sort().join(",")).digest("hex");
8607
9504
  }
8608
9505
  async function promptSkillDiscovery(found) {
8609
9506
  if (!process.stdin.isTTY || found.length === 0) return [];
@@ -8643,7 +9540,7 @@ async function discoverAndIngestSkills() {
8643
9540
  if (sf?.skills?.length) {
8644
9541
  for (const fp of resolveSkillPaths(sf.skills, sf._repoRoot)) {
8645
9542
  try {
8646
- excludeHashes.add(createHash2("sha256").update(readFileSync20(fp, "utf-8")).digest("hex"));
9543
+ excludeHashes.add(createHash4("sha256").update(readFileSync21(fp, "utf-8")).digest("hex"));
8647
9544
  } catch {
8648
9545
  }
8649
9546
  }
@@ -8674,13 +9571,13 @@ async function discoverAndIngestSkills() {
8674
9571
  const setHash = discoverySetHash(selectable);
8675
9572
  let prev = "";
8676
9573
  try {
8677
- prev = readFileSync20(SKILLS_DISCOVERED_PATH, "utf-8").trim();
9574
+ prev = readFileSync21(SKILLS_DISCOVERED_PATH, "utf-8").trim();
8678
9575
  } catch {
8679
9576
  }
8680
9577
  if (prev === setHash) return;
8681
9578
  const picks = await promptSkillDiscovery(found);
8682
9579
  try {
8683
- writeFileSync16(SKILLS_DISCOVERED_PATH, setHash);
9580
+ writeFileSync17(SKILLS_DISCOVERED_PATH, setHash);
8684
9581
  } catch {
8685
9582
  }
8686
9583
  if (picks.length === 0) {
@@ -8719,9 +9616,9 @@ function ensureReachabilityGitHook() {
8719
9616
  const root = run("git rev-parse --show-toplevel");
8720
9617
  if (!root) return null;
8721
9618
  let hooksDir = run("git config --get core.hooksPath");
8722
- hooksDir = hooksDir ? isAbsolute(hooksDir) ? hooksDir : join18(root, hooksDir) : join18(root, ".git", "hooks");
9619
+ hooksDir = hooksDir ? isAbsolute(hooksDir) ? hooksDir : join19(root, hooksDir) : join19(root, ".git", "hooks");
8723
9620
  if (!existsSync22(hooksDir)) mkdirSync15(hooksDir, { recursive: true });
8724
- const hookPath = join18(hooksDir, "post-commit");
9621
+ const hookPath = join19(hooksDir, "post-commit");
8725
9622
  const resolvedBin = resolveSynkroBinPath();
8726
9623
  const invoke = resolvedBin ? `"${resolvedBin}" reachability-scan --quiet` : "true";
8727
9624
  const START = "# >>> synkro reachability (managed) >>>";
@@ -8735,23 +9632,23 @@ function ensureReachabilityGitHook() {
8735
9632
  ].join("\n");
8736
9633
  const esc = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
8737
9634
  if (!existsSync22(hookPath)) {
8738
- writeFileSync16(hookPath, "#!/bin/sh\n" + block + "\n", { mode: 493 });
9635
+ writeFileSync17(hookPath, "#!/bin/sh\n" + block + "\n", { mode: 493 });
8739
9636
  return "installed";
8740
9637
  }
8741
- let cur = readFileSync20(hookPath, "utf-8");
9638
+ let cur = readFileSync21(hookPath, "utf-8");
8742
9639
  if (cur.includes(START)) {
8743
9640
  cur = cur.replace(new RegExp(esc(START) + "[\\s\\S]*?" + esc(END), "m"), block);
8744
- writeFileSync16(hookPath, cur);
9641
+ writeFileSync17(hookPath, cur);
8745
9642
  try {
8746
- chmodSync4(hookPath, 493);
9643
+ chmodSync5(hookPath, 493);
8747
9644
  } catch {
8748
9645
  }
8749
9646
  return "updated";
8750
9647
  }
8751
9648
  const sep = cur.endsWith("\n") ? "" : "\n";
8752
- writeFileSync16(hookPath, cur + sep + "\n" + block + "\n");
9649
+ writeFileSync17(hookPath, cur + sep + "\n" + block + "\n");
8753
9650
  try {
8754
- chmodSync4(hookPath, 493);
9651
+ chmodSync5(hookPath, 493);
8755
9652
  } catch {
8756
9653
  }
8757
9654
  return "updated";
@@ -8777,7 +9674,7 @@ function detectGitRepo2() {
8777
9674
  function getClaudeProjectsFolder() {
8778
9675
  const cwd = process.cwd();
8779
9676
  const sanitized = "-" + cwd.replace(/\//g, "-");
8780
- const projectsDir = join18(homedir20(), ".claude", "projects", sanitized);
9677
+ const projectsDir = join19(homedir21(), ".claude", "projects", sanitized);
8781
9678
  return existsSync22(projectsDir) ? projectsDir : null;
8782
9679
  }
8783
9680
  function extractSessionInsights(projectsDir) {
@@ -8785,9 +9682,9 @@ function extractSessionInsights(projectsDir) {
8785
9682
  const files = readdirSync4(projectsDir).filter((f) => f.endsWith(".jsonl"));
8786
9683
  for (const file of files) {
8787
9684
  const sessionId = file.replace(".jsonl", "");
8788
- const filePath = join18(projectsDir, file);
9685
+ const filePath = join19(projectsDir, file);
8789
9686
  try {
8790
- const content = readFileSync20(filePath, "utf-8");
9687
+ const content = readFileSync21(filePath, "utf-8");
8791
9688
  const lines = content.split("\n").filter(Boolean);
8792
9689
  for (let i = 0; i < lines.length; i++) {
8793
9690
  try {
@@ -8863,7 +9760,7 @@ function extractTextContent(content) {
8863
9760
  return "";
8864
9761
  }
8865
9762
  function getCodexTranscriptFiles(repo) {
8866
- const sessionsDir = join18(process.env.CODEX_HOME || join18(homedir20(), ".codex"), "sessions");
9763
+ const sessionsDir = join19(process.env.CODEX_HOME || join19(homedir21(), ".codex"), "sessions");
8867
9764
  if (!existsSync22(sessionsDir)) return [];
8868
9765
  let relative = [];
8869
9766
  try {
@@ -8871,9 +9768,9 @@ function getCodexTranscriptFiles(repo) {
8871
9768
  } catch {
8872
9769
  return [];
8873
9770
  }
8874
- return relative.filter((p) => p.endsWith(".jsonl")).map((p) => join18(sessionsDir, p)).filter((filePath) => {
9771
+ return relative.filter((p) => p.endsWith(".jsonl")).map((p) => join19(sessionsDir, p)).filter((filePath) => {
8875
9772
  try {
8876
- const first = readFileSync20(filePath, "utf-8").split("\n", 1)[0];
9773
+ const first = readFileSync21(filePath, "utf-8").split("\n", 1)[0];
8877
9774
  const meta = JSON.parse(first);
8878
9775
  const cwd = typeof meta?.payload?.cwd === "string" ? resolve4(meta.payload.cwd) : "";
8879
9776
  const root = resolve4(repo);
@@ -8883,38 +9780,45 @@ function getCodexTranscriptFiles(repo) {
8883
9780
  }
8884
9781
  });
8885
9782
  }
9783
+ function isJsonSyntaxError(error) {
9784
+ return error instanceof SyntaxError;
9785
+ }
8886
9786
  function parseCodexTranscriptFile(filePath) {
8887
- const lines = readFileSync20(filePath, "utf-8").split("\n").filter(Boolean);
9787
+ const transcript = readFileSync21(filePath, "utf-8");
9788
+ const lines = transcript.split("\n");
9789
+ const transcriptUsage = parseCodexTranscriptUsage(transcript);
8888
9790
  let sessionId = "";
8889
9791
  let model = "";
8890
- const messages = [];
8891
- for (let i = 0; i < lines.length; i++) {
9792
+ for (const line of lines) {
8892
9793
  try {
8893
- const entry = JSON.parse(lines[i]);
9794
+ const entry = JSON.parse(line);
8894
9795
  if (entry.type === "session_meta") {
8895
9796
  sessionId = String(entry.payload?.session_id || entry.payload?.id || sessionId);
8896
- continue;
8897
- }
8898
- if (entry.type === "turn_context" && typeof entry.payload?.model === "string") {
9797
+ } else if (entry.type === "turn_context" && typeof entry.payload?.model === "string") {
8899
9798
  model = entry.payload.model;
8900
- continue;
8901
9799
  }
8902
- if (entry.type !== "event_msg") continue;
8903
- const eventType = entry.payload?.type;
8904
- if (eventType !== "user_message" && eventType !== "agent_message") continue;
8905
- const text = String(entry.payload?.message || "").slice(0, 8e3);
8906
- if (!text) continue;
8907
- const type = eventType === "user_message" ? "user" : "assistant";
8908
- messages.push({
8909
- message_index: i,
8910
- type,
8911
- content: text,
8912
- ...type === "assistant" && model ? { model } : {}
8913
- });
8914
- } catch {
9800
+ } catch (error) {
9801
+ if (!isJsonSyntaxError(error)) throw error;
8915
9802
  }
8916
9803
  }
8917
- return { sessionId, messages };
9804
+ const messages = parseCodexConversationMessages(transcript).map((message) => {
9805
+ const turnUsage = message.role === "assistant" ? transcriptUsage?.turnsByLine.get(message.lineIndex) : void 0;
9806
+ return {
9807
+ message_index: message.lineIndex,
9808
+ uuid: message.uuid,
9809
+ type: message.role,
9810
+ content: message.content,
9811
+ ...message.role === "assistant" && (turnUsage?.model || model) ? { model: turnUsage?.model || model } : {},
9812
+ ...turnUsage ? { usage: turnUsage.usage } : {},
9813
+ ...message.timestamp ? { timestamp: message.timestamp } : {}
9814
+ };
9815
+ });
9816
+ return {
9817
+ sessionId,
9818
+ messages,
9819
+ model: transcriptUsage?.model || model,
9820
+ usage: transcriptUsage?.total
9821
+ };
8918
9822
  }
8919
9823
  async function syncCodexTranscriptsLocal(mcpPort, mcpToken, repo) {
8920
9824
  const files = getCodexTranscriptFiles(repo);
@@ -8931,7 +9835,15 @@ async function syncCodexTranscriptsLocal(mcpPort, mcpToken, repo) {
8931
9835
  const resp = await fetch(`http://127.0.0.1:${mcpPort}/api/conversation-sync`, {
8932
9836
  method: "POST",
8933
9837
  headers: { "Content-Type": "application/json", Authorization: `Bearer ${mcpToken}` },
8934
- body: JSON.stringify({ session_id: parsed.sessionId, repo, messages }),
9838
+ body: JSON.stringify({
9839
+ session_id: parsed.sessionId,
9840
+ repo,
9841
+ messages,
9842
+ session_usage: parsed.usage,
9843
+ model: parsed.model,
9844
+ harness: "codex",
9845
+ usage_cumulative: true
9846
+ }),
8935
9847
  signal: AbortSignal.timeout(15e3)
8936
9848
  });
8937
9849
  if (resp.ok) {
@@ -8939,7 +9851,7 @@ async function syncCodexTranscriptsLocal(mcpPort, mcpToken, repo) {
8939
9851
  totalSessions++;
8940
9852
  totalMessages += result.ingested ?? messages.length;
8941
9853
  }
8942
- writeFileSync16(join18(OFFSETS_DIR, parsed.sessionId), String(readFileSync20(files[i], "utf-8").split("\n").filter(Boolean).length), "utf-8");
9854
+ writeFileSync17(join19(OFFSETS_DIR, parsed.sessionId), String(readFileSync21(files[i], "utf-8").split("\n").filter(Boolean).length), "utf-8");
8943
9855
  } catch {
8944
9856
  }
8945
9857
  if ((i + 1) % 10 === 0 || i === files.length - 1) {
@@ -8953,14 +9865,14 @@ function cursorProjectSlug(workspaceRoot) {
8953
9865
  return workspaceRoot.replace(/^[/]+/, "").replace(/[^a-zA-Z0-9]+/g, "-").replace(/^-+|-+$/g, "");
8954
9866
  }
8955
9867
  function getCursorTranscriptsDir() {
8956
- const dir = join18(homedir20(), ".cursor", "projects", cursorProjectSlug(process.cwd()), "agent-transcripts");
9868
+ const dir = join19(homedir21(), ".cursor", "projects", cursorProjectSlug(process.cwd()), "agent-transcripts");
8957
9869
  return existsSync22(dir) ? dir : null;
8958
9870
  }
8959
9871
  function isSafeConvId(id) {
8960
9872
  return /^[A-Za-z0-9_-]+$/.test(id);
8961
9873
  }
8962
9874
  function parseCursorTranscriptFile(filePath) {
8963
- const content = readFileSync20(filePath, "utf-8");
9875
+ const content = readFileSync21(filePath, "utf-8");
8964
9876
  const lines = content.split("\n").filter(Boolean);
8965
9877
  const messages = [];
8966
9878
  for (let i = 0; i < lines.length; i++) {
@@ -8992,7 +9904,7 @@ async function syncCursorTranscriptsLocal(mcpPort, mcpToken, repo) {
8992
9904
  for (let i = 0; i < convDirs.length; i++) {
8993
9905
  const convId = convDirs[i];
8994
9906
  if (!isSafeConvId(convId)) continue;
8995
- const filePath = join18(dir, convId, `${convId}.jsonl`);
9907
+ const filePath = join19(dir, convId, `${convId}.jsonl`);
8996
9908
  if (!existsSync22(filePath)) continue;
8997
9909
  try {
8998
9910
  const all = parseCursorTranscriptFile(filePath);
@@ -9015,8 +9927,8 @@ async function syncCursorTranscriptsLocal(mcpPort, mcpToken, repo) {
9015
9927
  process.stdout.write(`\r Progress: ${i + 1}/${convDirs.length} sessions (${totalMessages} messages embedded) `);
9016
9928
  }
9017
9929
  try {
9018
- const lc = readFileSync20(filePath, "utf-8").split("\n").filter(Boolean).length;
9019
- writeFileSync16(join18(OFFSETS_DIR, convId), String(lc), "utf-8");
9930
+ const lc = readFileSync21(filePath, "utf-8").split("\n").filter(Boolean).length;
9931
+ writeFileSync17(join19(OFFSETS_DIR, convId), String(lc), "utf-8");
9020
9932
  } catch {
9021
9933
  }
9022
9934
  }
@@ -9024,7 +9936,7 @@ async function syncCursorTranscriptsLocal(mcpPort, mcpToken, repo) {
9024
9936
  return { sessions: totalSessions, messages: totalMessages };
9025
9937
  }
9026
9938
  function parseTranscriptFile(filePath) {
9027
- const content = readFileSync20(filePath, "utf-8");
9939
+ const content = readFileSync21(filePath, "utf-8");
9028
9940
  const lines = content.split("\n").filter(Boolean);
9029
9941
  const messages = [];
9030
9942
  for (let i = 0; i < lines.length; i++) {
@@ -9072,7 +9984,7 @@ async function syncTranscriptsLocal(mcpPort, mcpToken, repo) {
9072
9984
  for (let i = 0; i < files.length; i++) {
9073
9985
  const file = files[i];
9074
9986
  const sessionId = file.replace(".jsonl", "");
9075
- const filePath = join18(projectsDir, file);
9987
+ const filePath = join19(projectsDir, file);
9076
9988
  try {
9077
9989
  const allMessages = parseTranscriptFile(filePath);
9078
9990
  const messages = allMessages.length > 500 ? allMessages.slice(-500) : allMessages;
@@ -9094,9 +10006,9 @@ async function syncTranscriptsLocal(mcpPort, mcpToken, repo) {
9094
10006
  process.stdout.write(`\r Progress: ${i + 1}/${files.length} sessions (${totalMessages} messages embedded) `);
9095
10007
  }
9096
10008
  try {
9097
- const content = readFileSync20(join18(projectsDir, file), "utf-8");
10009
+ const content = readFileSync21(join19(projectsDir, file), "utf-8");
9098
10010
  const lineCount = content.split("\n").filter(Boolean).length;
9099
- writeFileSync16(join18(OFFSETS_DIR, sessionId), String(lineCount), "utf-8");
10011
+ writeFileSync17(join19(OFFSETS_DIR, sessionId), String(lineCount), "utf-8");
9100
10012
  } catch {
9101
10013
  }
9102
10014
  }
@@ -9117,7 +10029,7 @@ async function syncTranscriptsBulk(gatewayUrl, token, repo) {
9117
10029
  const sessions = [];
9118
10030
  for (const file of batch) {
9119
10031
  const sessionId = file.replace(".jsonl", "");
9120
- const filePath = join18(projectsDir, file);
10032
+ const filePath = join19(projectsDir, file);
9121
10033
  try {
9122
10034
  const allMessages = parseTranscriptFile(filePath);
9123
10035
  const messages = allMessages.length > maxMessagesPerSession ? allMessages.slice(-maxMessagesPerSession) : allMessages;
@@ -9146,11 +10058,11 @@ async function syncTranscriptsBulk(gatewayUrl, token, repo) {
9146
10058
  }
9147
10059
  for (const file of batch) {
9148
10060
  const sessionId = file.replace(".jsonl", "");
9149
- const filePath = join18(projectsDir, file);
10061
+ const filePath = join19(projectsDir, file);
9150
10062
  try {
9151
- const content = readFileSync20(filePath, "utf-8");
10063
+ const content = readFileSync21(filePath, "utf-8");
9152
10064
  const lineCount = content.split("\n").filter(Boolean).length;
9153
- writeFileSync16(join18(OFFSETS_DIR, sessionId), String(lineCount), "utf-8");
10065
+ writeFileSync17(join19(OFFSETS_DIR, sessionId), String(lineCount), "utf-8");
9154
10066
  } catch {
9155
10067
  }
9156
10068
  }
@@ -9168,7 +10080,16 @@ async function syncCodexTranscriptsBulk(gatewayUrl, token, repo) {
9168
10080
  try {
9169
10081
  const parsed = parseCodexTranscriptFile(filePath);
9170
10082
  const messages = parsed.messages.length > 500 ? parsed.messages.slice(-500) : parsed.messages;
9171
- if (parsed.sessionId && messages.length > 0) sessions.push({ cc_session_id: parsed.sessionId, messages });
10083
+ if (parsed.sessionId && messages.length > 0) {
10084
+ sessions.push({
10085
+ cc_session_id: parsed.sessionId,
10086
+ messages,
10087
+ model: parsed.model,
10088
+ session_usage: parsed.usage,
10089
+ harness: "codex",
10090
+ usage_cumulative: true
10091
+ });
10092
+ }
9172
10093
  } catch {
9173
10094
  }
9174
10095
  }
@@ -9190,7 +10111,7 @@ async function syncCodexTranscriptsBulk(gatewayUrl, token, repo) {
9190
10111
  try {
9191
10112
  const parsed = parseCodexTranscriptFile(filePath);
9192
10113
  if (parsed.sessionId) {
9193
- writeFileSync16(join18(OFFSETS_DIR, parsed.sessionId), String(readFileSync20(filePath, "utf-8").split("\n").filter(Boolean).length), "utf-8");
10114
+ writeFileSync17(join19(OFFSETS_DIR, parsed.sessionId), String(readFileSync21(filePath, "utf-8").split("\n").filter(Boolean).length), "utf-8");
9194
10115
  }
9195
10116
  } catch {
9196
10117
  }
@@ -9221,10 +10142,12 @@ var init_install = __esm({
9221
10142
  init_codexCloudSetup();
9222
10143
  init_ptyShim();
9223
10144
  init_graderSmoke();
9224
- SYNKRO_DIR11 = join18(homedir20(), ".synkro");
9225
- HOOKS_DIR = join18(SYNKRO_DIR11, "hooks");
9226
- BIN_DIR = join18(SYNKRO_DIR11, "bin");
9227
- CONFIG_PATH4 = join18(SYNKRO_DIR11, "config.env");
10145
+ init_codexTranscriptUsage();
10146
+ init_codexTranscriptMessages();
10147
+ SYNKRO_DIR11 = join19(homedir21(), ".synkro");
10148
+ HOOKS_DIR = join19(SYNKRO_DIR11, "hooks");
10149
+ BIN_DIR = join19(SYNKRO_DIR11, "bin");
10150
+ CONFIG_PATH4 = join19(SYNKRO_DIR11, "config.env");
9228
10151
  MCP_STDIO_PROXY_SRC = `#!/usr/bin/env bun
9229
10152
  import { readFileSync } from 'node:fs';
9230
10153
  import { homedir } from 'node:os';
@@ -9335,23 +10258,23 @@ rl.on('line', async (line) => {
9335
10258
  }
9336
10259
  });
9337
10260
  `;
9338
- OFFSETS_DIR = join18(SYNKRO_DIR11, ".transcript-offsets");
9339
- CLOUD_JWT_PATH = join18(SYNKRO_DIR11, ".cloud-jwt");
9340
- SKILLS_DISCOVERED_PATH = join18(SYNKRO_DIR11, ".skills-discovered");
10261
+ OFFSETS_DIR = join19(SYNKRO_DIR11, ".transcript-offsets");
10262
+ CLOUD_JWT_PATH = join19(SYNKRO_DIR11, ".cloud-jwt");
10263
+ SKILLS_DISCOVERED_PATH = join19(SYNKRO_DIR11, ".skills-discovered");
9341
10264
  }
9342
10265
  });
9343
10266
 
9344
10267
  // cli/local-cc/install.ts
9345
- import { existsSync as existsSync23, mkdirSync as mkdirSync16, writeFileSync as writeFileSync17, readFileSync as readFileSync21, chmodSync as chmodSync5, copyFileSync as copyFileSync2, renameSync as renameSync7, unlinkSync as unlinkSync8, openSync as openSync2, fsyncSync, closeSync as closeSync2 } from "fs";
9346
- import { join as join19 } from "path";
9347
- import { homedir as homedir21 } from "os";
10268
+ import { existsSync as existsSync23, mkdirSync as mkdirSync16, writeFileSync as writeFileSync18, readFileSync as readFileSync22, chmodSync as chmodSync6, copyFileSync as copyFileSync2, renameSync as renameSync8, unlinkSync as unlinkSync8, openSync as openSync2, fsyncSync, closeSync as closeSync2 } from "fs";
10269
+ import { join as join20 } from "path";
10270
+ import { homedir as homedir22 } from "os";
9348
10271
  import { spawnSync as spawnSync7 } from "child_process";
9349
10272
  function writePluginFiles() {
9350
10273
  for (const c of CHANNELS) {
9351
10274
  mkdirSync16(c.sessionDir, { recursive: true });
9352
10275
  mkdirSync16(c.pluginSettingsDir, { recursive: true });
9353
- writeFileSync17(c.pluginPkgPath, PLUGIN_PACKAGE_JSON, "utf-8");
9354
- writeFileSync17(
10276
+ writeFileSync18(c.pluginPkgPath, PLUGIN_PACKAGE_JSON, "utf-8");
10277
+ writeFileSync18(
9355
10278
  c.pluginSettingsPath,
9356
10279
  JSON.stringify({
9357
10280
  fastMode: true,
@@ -9366,8 +10289,8 @@ function writePluginFiles() {
9366
10289
  }, null, 2) + "\n",
9367
10290
  "utf-8"
9368
10291
  );
9369
- writeFileSync17(c.runScriptPath, c.runScriptSource, "utf-8");
9370
- chmodSync5(c.runScriptPath, 493);
10292
+ writeFileSync18(c.runScriptPath, c.runScriptSource, "utf-8");
10293
+ chmodSync6(c.runScriptPath, 493);
9371
10294
  }
9372
10295
  }
9373
10296
  function runBunInstall() {
@@ -9388,7 +10311,7 @@ function safelyMutateClaudeJson(mutator) {
9388
10311
  if (!existsSync23(CLAUDE_JSON_PATH)) {
9389
10312
  return;
9390
10313
  }
9391
- const originalText = readFileSync21(CLAUDE_JSON_PATH, "utf-8");
10314
+ const originalText = readFileSync22(CLAUDE_JSON_PATH, "utf-8");
9392
10315
  let parsed;
9393
10316
  try {
9394
10317
  parsed = JSON.parse(originalText);
@@ -9420,14 +10343,14 @@ function safelyMutateClaudeJson(mutator) {
9420
10343
  copyFileSync2(CLAUDE_JSON_PATH, CLAUDE_JSON_BACKUP_PATH);
9421
10344
  const tmpPath = `${CLAUDE_JSON_PATH}.synkro-tmp.${process.pid}`;
9422
10345
  try {
9423
- writeFileSync17(tmpPath, newText, "utf-8");
10346
+ writeFileSync18(tmpPath, newText, "utf-8");
9424
10347
  const fd = openSync2(tmpPath, "r");
9425
10348
  try {
9426
10349
  fsyncSync(fd);
9427
10350
  } finally {
9428
10351
  closeSync2(fd);
9429
10352
  }
9430
- renameSync7(tmpPath, CLAUDE_JSON_PATH);
10353
+ renameSync8(tmpPath, CLAUDE_JSON_PATH);
9431
10354
  } catch (err) {
9432
10355
  try {
9433
10356
  unlinkSync8(tmpPath);
@@ -9453,7 +10376,7 @@ function writeProjectMcpJson() {
9453
10376
  }
9454
10377
  }
9455
10378
  };
9456
- writeFileSync17(c.projectMcpPath, JSON.stringify(mcp, null, 2) + "\n", "utf-8");
10379
+ writeFileSync18(c.projectMcpPath, JSON.stringify(mcp, null, 2) + "\n", "utf-8");
9457
10380
  }
9458
10381
  }
9459
10382
  function patchClaudeJson() {
@@ -9530,42 +10453,42 @@ var CLAUDE_JSON_BACKUP_PATH, SESSION_DIR, PLUGIN_PATH, PLUGIN_PKG_PATH, PLUGIN_S
9530
10453
  var init_install2 = __esm({
9531
10454
  "cli/local-cc/install.ts"() {
9532
10455
  "use strict";
9533
- CLAUDE_JSON_BACKUP_PATH = join19(homedir21(), ".claude.json.synkro-bak");
9534
- SESSION_DIR = join19(homedir21(), ".synkro", "cc_sessions");
9535
- PLUGIN_PATH = join19(SESSION_DIR, "synkro-channel.ts");
9536
- PLUGIN_PKG_PATH = join19(SESSION_DIR, "package.json");
9537
- PLUGIN_SETTINGS_DIR = join19(SESSION_DIR, ".claude");
9538
- PLUGIN_SETTINGS_PATH = join19(PLUGIN_SETTINGS_DIR, "settings.json");
9539
- PROJECT_MCP_PATH = join19(SESSION_DIR, ".mcp.json");
9540
- CLAUDE_JSON_PATH = join19(homedir21(), ".claude.json");
9541
- RUN_SCRIPT_PATH = join19(SESSION_DIR, "run-claude.sh");
10456
+ CLAUDE_JSON_BACKUP_PATH = join20(homedir22(), ".claude.json.synkro-bak");
10457
+ SESSION_DIR = join20(homedir22(), ".synkro", "cc_sessions");
10458
+ PLUGIN_PATH = join20(SESSION_DIR, "synkro-channel.ts");
10459
+ PLUGIN_PKG_PATH = join20(SESSION_DIR, "package.json");
10460
+ PLUGIN_SETTINGS_DIR = join20(SESSION_DIR, ".claude");
10461
+ PLUGIN_SETTINGS_PATH = join20(PLUGIN_SETTINGS_DIR, "settings.json");
10462
+ PROJECT_MCP_PATH = join20(SESSION_DIR, ".mcp.json");
10463
+ CLAUDE_JSON_PATH = join20(homedir22(), ".claude.json");
10464
+ RUN_SCRIPT_PATH = join20(SESSION_DIR, "run-claude.sh");
9542
10465
  TMUX_SESSION_NAME = "synkro-local-cc";
9543
10466
  CHANNEL_1_PORT = 8941;
9544
- SESSION_DIR_2 = join19(homedir21(), ".synkro", "cc_sessions_2");
9545
- PLUGIN_PATH_2 = join19(SESSION_DIR_2, "synkro-channel.ts");
9546
- PLUGIN_PKG_PATH_2 = join19(SESSION_DIR_2, "package.json");
9547
- PLUGIN_SETTINGS_DIR_2 = join19(SESSION_DIR_2, ".claude");
9548
- PLUGIN_SETTINGS_PATH_2 = join19(PLUGIN_SETTINGS_DIR_2, "settings.json");
9549
- PROJECT_MCP_PATH_2 = join19(SESSION_DIR_2, ".mcp.json");
9550
- RUN_SCRIPT_PATH_2 = join19(SESSION_DIR_2, "run-claude.sh");
10467
+ SESSION_DIR_2 = join20(homedir22(), ".synkro", "cc_sessions_2");
10468
+ PLUGIN_PATH_2 = join20(SESSION_DIR_2, "synkro-channel.ts");
10469
+ PLUGIN_PKG_PATH_2 = join20(SESSION_DIR_2, "package.json");
10470
+ PLUGIN_SETTINGS_DIR_2 = join20(SESSION_DIR_2, ".claude");
10471
+ PLUGIN_SETTINGS_PATH_2 = join20(PLUGIN_SETTINGS_DIR_2, "settings.json");
10472
+ PROJECT_MCP_PATH_2 = join20(SESSION_DIR_2, ".mcp.json");
10473
+ RUN_SCRIPT_PATH_2 = join20(SESSION_DIR_2, "run-claude.sh");
9551
10474
  TMUX_SESSION_NAME_2 = "synkro-local-cc-2";
9552
10475
  CHANNEL_2_PORT = 8951;
9553
- SESSION_DIR_3 = join19(homedir21(), ".synkro", "cc_sessions_3");
9554
- PLUGIN_PATH_3 = join19(SESSION_DIR_3, "synkro-channel.ts");
9555
- PLUGIN_PKG_PATH_3 = join19(SESSION_DIR_3, "package.json");
9556
- PLUGIN_SETTINGS_DIR_3 = join19(SESSION_DIR_3, ".claude");
9557
- PLUGIN_SETTINGS_PATH_3 = join19(PLUGIN_SETTINGS_DIR_3, "settings.json");
9558
- PROJECT_MCP_PATH_3 = join19(SESSION_DIR_3, ".mcp.json");
9559
- RUN_SCRIPT_PATH_3 = join19(SESSION_DIR_3, "run-claude.sh");
10476
+ SESSION_DIR_3 = join20(homedir22(), ".synkro", "cc_sessions_3");
10477
+ PLUGIN_PATH_3 = join20(SESSION_DIR_3, "synkro-channel.ts");
10478
+ PLUGIN_PKG_PATH_3 = join20(SESSION_DIR_3, "package.json");
10479
+ PLUGIN_SETTINGS_DIR_3 = join20(SESSION_DIR_3, ".claude");
10480
+ PLUGIN_SETTINGS_PATH_3 = join20(PLUGIN_SETTINGS_DIR_3, "settings.json");
10481
+ PROJECT_MCP_PATH_3 = join20(SESSION_DIR_3, ".mcp.json");
10482
+ RUN_SCRIPT_PATH_3 = join20(SESSION_DIR_3, "run-claude.sh");
9560
10483
  TMUX_SESSION_NAME_3 = "synkro-local-cc-3";
9561
10484
  CHANNEL_3_PORT = 8942;
9562
- SESSION_DIR_4 = join19(homedir21(), ".synkro", "cc_sessions_4");
9563
- PLUGIN_PATH_4 = join19(SESSION_DIR_4, "synkro-channel.ts");
9564
- PLUGIN_PKG_PATH_4 = join19(SESSION_DIR_4, "package.json");
9565
- PLUGIN_SETTINGS_DIR_4 = join19(SESSION_DIR_4, ".claude");
9566
- PLUGIN_SETTINGS_PATH_4 = join19(PLUGIN_SETTINGS_DIR_4, "settings.json");
9567
- PROJECT_MCP_PATH_4 = join19(SESSION_DIR_4, ".mcp.json");
9568
- RUN_SCRIPT_PATH_4 = join19(SESSION_DIR_4, "run-claude.sh");
10485
+ SESSION_DIR_4 = join20(homedir22(), ".synkro", "cc_sessions_4");
10486
+ PLUGIN_PATH_4 = join20(SESSION_DIR_4, "synkro-channel.ts");
10487
+ PLUGIN_PKG_PATH_4 = join20(SESSION_DIR_4, "package.json");
10488
+ PLUGIN_SETTINGS_DIR_4 = join20(SESSION_DIR_4, ".claude");
10489
+ PLUGIN_SETTINGS_PATH_4 = join20(PLUGIN_SETTINGS_DIR_4, "settings.json");
10490
+ PROJECT_MCP_PATH_4 = join20(SESSION_DIR_4, ".mcp.json");
10491
+ RUN_SCRIPT_PATH_4 = join20(SESSION_DIR_4, "run-claude.sh");
9569
10492
  TMUX_SESSION_NAME_4 = "synkro-local-cc-4";
9570
10493
  CHANNEL_4_PORT = 8952;
9571
10494
  RUN_SCRIPT_SOURCE = `#!/usr/bin/env bash
@@ -9840,8 +10763,8 @@ __export(disconnect_exports, {
9840
10763
  disconnectCommand: () => disconnectCommand
9841
10764
  });
9842
10765
  import { existsSync as existsSync24, rmSync as rmSync3, readdirSync as readdirSync5 } from "fs";
9843
- import { homedir as homedir22 } from "os";
9844
- import { join as join20 } from "path";
10766
+ import { homedir as homedir23 } from "os";
10767
+ import { join as join21 } from "path";
9845
10768
  import { spawnSync as spawnSync8 } from "child_process";
9846
10769
  import { createInterface as createInterface3 } from "readline";
9847
10770
  async function tearDownLocalCC() {
@@ -9956,13 +10879,13 @@ async function disconnectCommand(args2 = [], opts = {}) {
9956
10879
  console.log(`\u2713 wiped ${SYNKRO_DIR12} entirely \u2014 including all scan data and backups`);
9957
10880
  } else {
9958
10881
  const keep = /* @__PURE__ */ new Set([
9959
- join20(SYNKRO_DIR12, "pgdata"),
9960
- join20(SYNKRO_DIR12, "pgdata-backups"),
9961
- join20(SYNKRO_DIR12, ".transcript-offsets")
10882
+ join21(SYNKRO_DIR12, "pgdata"),
10883
+ join21(SYNKRO_DIR12, "pgdata-backups"),
10884
+ join21(SYNKRO_DIR12, ".transcript-offsets")
9962
10885
  ]);
9963
10886
  const preserved = [];
9964
10887
  for (const entry of readdirSync5(SYNKRO_DIR12)) {
9965
- const full = join20(SYNKRO_DIR12, entry);
10888
+ const full = join21(SYNKRO_DIR12, entry);
9966
10889
  if (keep.has(full)) {
9967
10890
  preserved.push(entry);
9968
10891
  continue;
@@ -10000,14 +10923,14 @@ var init_disconnect = __esm({
10000
10923
  init_dockerInstall();
10001
10924
  init_macKeychain();
10002
10925
  init_telemetry();
10003
- SYNKRO_DIR12 = join20(homedir22(), ".synkro");
10926
+ SYNKRO_DIR12 = join21(homedir23(), ".synkro");
10004
10927
  }
10005
10928
  });
10006
10929
 
10007
10930
  // cli/local-cc/turnLog.ts
10008
- import { appendFileSync as appendFileSync3, existsSync as existsSync25, mkdirSync as mkdirSync17, openSync as openSync3, readFileSync as readFileSync22, readSync, closeSync as closeSync3, statSync as statSync3, watchFile, unwatchFile } from "fs";
10009
- import { dirname as dirname7, join as join21 } from "path";
10010
- import { homedir as homedir23 } from "os";
10931
+ import { appendFileSync as appendFileSync3, existsSync as existsSync25, mkdirSync as mkdirSync17, openSync as openSync3, readFileSync as readFileSync23, readSync, closeSync as closeSync3, statSync as statSync3, watchFile, unwatchFile } from "fs";
10932
+ import { dirname as dirname7, join as join22 } from "path";
10933
+ import { homedir as homedir24 } from "os";
10011
10934
  function truncate(s, max = PREVIEW_MAX) {
10012
10935
  if (s.length <= max) return s;
10013
10936
  return s.slice(0, max) + "\u2026 [+" + (s.length - max) + " chars]";
@@ -10047,7 +10970,7 @@ function readRecentTurns(n = 20) {
10047
10970
  try {
10048
10971
  const size = statSync3(TURN_LOG_PATH).size;
10049
10972
  if (size === 0) return [];
10050
- const text = readFileSync22(TURN_LOG_PATH, "utf-8");
10973
+ const text = readFileSync23(TURN_LOG_PATH, "utf-8");
10051
10974
  const lines = text.split("\n").filter(Boolean);
10052
10975
  const lastN = lines.slice(-n).reverse();
10053
10976
  return lastN.map((line) => {
@@ -10126,7 +11049,7 @@ var TURN_LOG_PATH, PREVIEW_MAX;
10126
11049
  var init_turnLog = __esm({
10127
11050
  "cli/local-cc/turnLog.ts"() {
10128
11051
  "use strict";
10129
- TURN_LOG_PATH = join21(homedir23(), ".synkro", "cc_sessions", "turns.log");
11052
+ TURN_LOG_PATH = join22(homedir24(), ".synkro", "cc_sessions", "turns.log");
10130
11053
  PREVIEW_MAX = 400;
10131
11054
  }
10132
11055
  });
@@ -10280,8 +11203,8 @@ __export(scanPr_exports, {
10280
11203
  scanPrCommand: () => scanPrCommand
10281
11204
  });
10282
11205
  import { execSync as execSync5, spawn as spawn5 } from "child_process";
10283
- import { readFileSync as readFileSync23, existsSync as existsSync26 } from "fs";
10284
- import { join as join22 } from "path";
11206
+ import { readFileSync as readFileSync24, existsSync as existsSync26 } from "fs";
11207
+ import { join as join23 } from "path";
10285
11208
  function parseMatchSpec(condition) {
10286
11209
  if (!condition.startsWith("match_spec:")) return null;
10287
11210
  try {
@@ -10760,10 +11683,10 @@ function shouldFail(findings, threshold) {
10760
11683
  return findings.some((f) => order.indexOf(f.severity) >= thresholdIdx);
10761
11684
  }
10762
11685
  function readRepoDeps() {
10763
- const pkgPath = join22(process.cwd(), "package.json");
11686
+ const pkgPath = join23(process.cwd(), "package.json");
10764
11687
  if (!existsSync26(pkgPath)) return {};
10765
11688
  try {
10766
- const pkg = JSON.parse(readFileSync23(pkgPath, "utf-8"));
11689
+ const pkg = JSON.parse(readFileSync24(pkgPath, "utf-8"));
10767
11690
  return { ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} };
10768
11691
  } catch {
10769
11692
  return {};
@@ -11007,15 +11930,15 @@ var routeDecide_exports = {};
11007
11930
  __export(routeDecide_exports, {
11008
11931
  routeDecide: () => routeDecide
11009
11932
  });
11010
- import { readFileSync as readFileSync24, writeFileSync as writeFileSync18 } from "fs";
11011
- import { homedir as homedir24 } from "os";
11012
- import { join as join23 } from "path";
11933
+ import { readFileSync as readFileSync25, writeFileSync as writeFileSync19 } from "fs";
11934
+ import { homedir as homedir25 } from "os";
11935
+ import { join as join24 } from "path";
11013
11936
  function safeSid(sid) {
11014
11937
  return sid.replace(/[^A-Za-z0-9_-]/g, "_");
11015
11938
  }
11016
11939
  function loadMcpJwt() {
11017
11940
  try {
11018
- return readFileSync24(join23(SYNKRO_DIR13, ".mcp-jwt"), "utf-8").trim();
11941
+ return readFileSync25(join24(SYNKRO_DIR13, ".mcp-jwt"), "utf-8").trim();
11019
11942
  } catch {
11020
11943
  return "";
11021
11944
  }
@@ -11025,7 +11948,7 @@ async function routeDecide(sessionId) {
11025
11948
  const sid = safeSid(sessionId);
11026
11949
  let prompt = "";
11027
11950
  try {
11028
- const rec = JSON.parse(readFileSync24(join23(SESSIONS_DIR2, sid + ".json"), "utf-8"));
11951
+ const rec = JSON.parse(readFileSync25(join24(SESSIONS_DIR2, sid + ".json"), "utf-8"));
11029
11952
  prompt = String(rec.last_prompt || "").trim();
11030
11953
  } catch {
11031
11954
  return;
@@ -11036,7 +11959,7 @@ async function routeDecide(sessionId) {
11036
11959
  const resp = await fetch(`http://127.0.0.1:${GRADER_HOST_PORT}/submit`, {
11037
11960
  method: "POST",
11038
11961
  headers: { "Content-Type": "application/json", Authorization: "Bearer " + loadMcpJwt() },
11039
- body: JSON.stringify({ role: "route-classify", payload: prompt, content: prompt, agent_kind: "claude_code", hedge: false }),
11962
+ body: JSON.stringify({ role: "route-classify", payload: prompt, content: prompt, hedge: false }),
11040
11963
  // First call may spin up the route lane (a haiku worker boot), so allow headroom.
11041
11964
  signal: AbortSignal.timeout(12e4)
11042
11965
  });
@@ -11048,19 +11971,19 @@ async function routeDecide(sessionId) {
11048
11971
  return;
11049
11972
  }
11050
11973
  if (!model || !VALID_MODELS.has(model)) return;
11051
- const lastFile = join23(PTY_DIR, "route-last-" + sid);
11974
+ const lastFile = join24(PTY_DIR, "route-last-" + sid);
11052
11975
  let last = "";
11053
11976
  try {
11054
- last = readFileSync24(lastFile, "utf-8").trim();
11977
+ last = readFileSync25(lastFile, "utf-8").trim();
11055
11978
  } catch {
11056
11979
  }
11057
11980
  if (model === last) return;
11058
11981
  try {
11059
- writeFileSync18(lastFile, model);
11982
+ writeFileSync19(lastFile, model);
11060
11983
  } catch {
11061
11984
  }
11062
11985
  try {
11063
- writeFileSync18(join23(PTY_DIR, "route-" + sid), model);
11986
+ writeFileSync19(join24(PTY_DIR, "route-" + sid), model);
11064
11987
  } catch {
11065
11988
  }
11066
11989
  }
@@ -11068,9 +11991,9 @@ var SYNKRO_DIR13, PTY_DIR, SESSIONS_DIR2, VALID_MODELS, GRADER_HOST_PORT;
11068
11991
  var init_routeDecide = __esm({
11069
11992
  "cli/local-cc/routeDecide.ts"() {
11070
11993
  "use strict";
11071
- SYNKRO_DIR13 = join23(homedir24(), ".synkro");
11072
- PTY_DIR = join23(SYNKRO_DIR13, "pty");
11073
- SESSIONS_DIR2 = join23(PTY_DIR, "sessions");
11994
+ SYNKRO_DIR13 = join24(homedir25(), ".synkro");
11995
+ PTY_DIR = join24(SYNKRO_DIR13, "pty");
11996
+ SESSIONS_DIR2 = join24(PTY_DIR, "sessions");
11074
11997
  VALID_MODELS = /* @__PURE__ */ new Set(["claude-haiku-4-5", "claude-sonnet-5", "claude-opus-4-8"]);
11075
11998
  GRADER_HOST_PORT = process.env.SYNKRO_GRADER_HOST_PORT || "18929";
11076
11999
  }
@@ -11081,9 +12004,9 @@ var routeOrchestrate_exports = {};
11081
12004
  __export(routeOrchestrate_exports, {
11082
12005
  routeAndResubmit: () => routeAndResubmit
11083
12006
  });
11084
- import { readFileSync as readFileSync25, writeFileSync as writeFileSync19 } from "fs";
11085
- import { homedir as homedir25 } from "os";
11086
- import { join as join24 } from "path";
12007
+ import { readFileSync as readFileSync26, writeFileSync as writeFileSync20 } from "fs";
12008
+ import { homedir as homedir26 } from "os";
12009
+ import { join as join25 } from "path";
11087
12010
  import { spawnSync as spawnSync9 } from "child_process";
11088
12011
  function safeSid2(sid) {
11089
12012
  return sid.replace(/[^A-Za-z0-9_-]/g, "_");
@@ -11093,7 +12016,7 @@ function safeSession(s) {
11093
12016
  }
11094
12017
  function loadMcpJwt2() {
11095
12018
  try {
11096
- return readFileSync25(join24(SYNKRO_DIR14, ".mcp-jwt"), "utf-8").trim();
12019
+ return readFileSync26(join25(SYNKRO_DIR14, ".mcp-jwt"), "utf-8").trim();
11097
12020
  } catch {
11098
12021
  return "";
11099
12022
  }
@@ -11103,7 +12026,7 @@ async function classifyTask(prompt) {
11103
12026
  const resp = await fetch(`http://127.0.0.1:${GRADER_HOST_PORT2}/submit`, {
11104
12027
  method: "POST",
11105
12028
  headers: { "Content-Type": "application/json", Authorization: "Bearer " + loadMcpJwt2() },
11106
- body: JSON.stringify({ role: "route-classify", payload: prompt, content: prompt, agent_kind: "claude_code", hedge: false }),
12029
+ body: JSON.stringify({ role: "route-classify", payload: prompt, content: prompt, hedge: false }),
11107
12030
  signal: AbortSignal.timeout(6e4)
11108
12031
  });
11109
12032
  if (!resp.ok) return null;
@@ -11119,7 +12042,7 @@ async function classifyTask(prompt) {
11119
12042
  }
11120
12043
  function lastRoutedModel(sid) {
11121
12044
  try {
11122
- const v = readFileSync25(join24(PTY_DIR2, "route-last-" + sid), "utf-8").trim();
12045
+ const v = readFileSync26(join25(PTY_DIR2, "route-last-" + sid), "utf-8").trim();
11123
12046
  return VALID_MODELS2.has(v) ? v : "";
11124
12047
  } catch {
11125
12048
  return "";
@@ -11129,12 +12052,12 @@ function resolveSession(sid, tmuxSession) {
11129
12052
  const candidates = [];
11130
12053
  if (tmuxSession) candidates.push(tmuxSession);
11131
12054
  try {
11132
- const rec = JSON.parse(readFileSync25(join24(SESSIONS_DIR3, safeSid2(sid) + ".json"), "utf-8"));
12055
+ const rec = JSON.parse(readFileSync26(join25(SESSIONS_DIR3, safeSid2(sid) + ".json"), "utf-8"));
11133
12056
  if (rec.tmux_session) candidates.push(rec.tmux_session);
11134
12057
  } catch {
11135
12058
  }
11136
12059
  try {
11137
- candidates.push(readFileSync25(ACTIVE_SESSION_FILE2, "utf-8").trim());
12060
+ candidates.push(readFileSync26(ACTIVE_SESSION_FILE2, "utf-8").trim());
11138
12061
  } catch {
11139
12062
  }
11140
12063
  for (const c of candidates) if (c && safeSession(c)) return c;
@@ -11169,12 +12092,12 @@ async function routeAndResubmit(sessionId, task, tmuxSession, forceModel) {
11169
12092
  }
11170
12093
  await wait(500);
11171
12094
  try {
11172
- writeFileSync19(join24(PTY_DIR2, "route-last-" + sid), picked);
12095
+ writeFileSync20(join25(PTY_DIR2, "route-last-" + sid), picked);
11173
12096
  } catch {
11174
12097
  }
11175
12098
  }
11176
12099
  try {
11177
- writeFileSync19(join24(PTY_DIR2, "route-guard-" + sid), "1");
12100
+ writeFileSync20(join25(PTY_DIR2, "route-guard-" + sid), "1");
11178
12101
  } catch {
11179
12102
  }
11180
12103
  sk("C-u");
@@ -11188,10 +12111,10 @@ var SYNKRO_DIR14, PTY_DIR2, SESSIONS_DIR3, ACTIVE_SESSION_FILE2, VALID_MODELS2,
11188
12111
  var init_routeOrchestrate = __esm({
11189
12112
  "cli/local-cc/routeOrchestrate.ts"() {
11190
12113
  "use strict";
11191
- SYNKRO_DIR14 = join24(homedir25(), ".synkro");
11192
- PTY_DIR2 = join24(SYNKRO_DIR14, "pty");
11193
- SESSIONS_DIR3 = join24(PTY_DIR2, "sessions");
11194
- ACTIVE_SESSION_FILE2 = join24(PTY_DIR2, "active");
12114
+ SYNKRO_DIR14 = join25(homedir26(), ".synkro");
12115
+ PTY_DIR2 = join25(SYNKRO_DIR14, "pty");
12116
+ SESSIONS_DIR3 = join25(PTY_DIR2, "sessions");
12117
+ ACTIVE_SESSION_FILE2 = join25(PTY_DIR2, "active");
11195
12118
  VALID_MODELS2 = /* @__PURE__ */ new Set(["claude-haiku-4-5", "claude-sonnet-5", "claude-opus-4-8"]);
11196
12119
  GRADER_HOST_PORT2 = process.env.SYNKRO_GRADER_HOST_PORT || "18929";
11197
12120
  wait = (ms) => new Promise((r) => setTimeout(r, ms));
@@ -11203,14 +12126,14 @@ var routingToggle_exports = {};
11203
12126
  __export(routingToggle_exports, {
11204
12127
  routingCommand: () => routingCommand
11205
12128
  });
11206
- import { writeFileSync as writeFileSync20, unlinkSync as unlinkSync9, existsSync as existsSync27, readdirSync as readdirSync6 } from "fs";
11207
- import { homedir as homedir26 } from "os";
11208
- import { join as join25 } from "path";
12129
+ import { writeFileSync as writeFileSync21, unlinkSync as unlinkSync9, existsSync as existsSync27, readdirSync as readdirSync6 } from "fs";
12130
+ import { homedir as homedir27 } from "os";
12131
+ import { join as join26 } from "path";
11209
12132
  function safeSid3(sid) {
11210
12133
  return sid.replace(/[^A-Za-z0-9_-]/g, "_");
11211
12134
  }
11212
12135
  function markerFor(session) {
11213
- return session ? join25(PTY_DIR3, "routing-on-" + safeSid3(session)) : join25(PTY_DIR3, "routing-on");
12136
+ return session ? join26(PTY_DIR3, "routing-on-" + safeSid3(session)) : join26(PTY_DIR3, "routing-on");
11214
12137
  }
11215
12138
  function routingCommand(args2) {
11216
12139
  const sub = (args2[0] || "status").trim();
@@ -11219,7 +12142,7 @@ function routingCommand(args2) {
11219
12142
  if (si >= 0 && args2[si + 1]) session = args2[si + 1].trim();
11220
12143
  if (sub === "on") {
11221
12144
  try {
11222
- writeFileSync20(markerFor(session), "1");
12145
+ writeFileSync21(markerFor(session), "1");
11223
12146
  } catch (e) {
11224
12147
  console.error("routing on failed:", String(e));
11225
12148
  return;
@@ -11242,7 +12165,7 @@ function routingCommand(args2) {
11242
12165
  for (const f of safeList()) {
11243
12166
  if (f === "routing-on" || f.startsWith("routing-on-")) {
11244
12167
  try {
11245
- unlinkSync9(join25(PTY_DIR3, f));
12168
+ unlinkSync9(join26(PTY_DIR3, f));
11246
12169
  removed++;
11247
12170
  } catch {
11248
12171
  }
@@ -11274,14 +12197,14 @@ var PTY_DIR3;
11274
12197
  var init_routingToggle = __esm({
11275
12198
  "cli/local-cc/routingToggle.ts"() {
11276
12199
  "use strict";
11277
- PTY_DIR3 = join25(homedir26(), ".synkro", "pty");
12200
+ PTY_DIR3 = join26(homedir27(), ".synkro", "pty");
11278
12201
  }
11279
12202
  });
11280
12203
 
11281
12204
  // cli/local-cc/pueue.ts
11282
12205
  import { execFileSync as execFileSync4, spawnSync as spawnSync10, spawn as spawn6 } from "child_process";
11283
- import { homedir as homedir27 } from "os";
11284
- import { join as join26 } from "path";
12206
+ import { homedir as homedir28 } from "os";
12207
+ import { join as join27 } from "path";
11285
12208
  import { connect as connect2 } from "net";
11286
12209
  function pueueAvailable() {
11287
12210
  const r = spawnSync10("pueue", ["--version"], { encoding: "utf-8" });
@@ -11347,7 +12270,7 @@ function startTask(opts = {}) {
11347
12270
  spawnSync10("pueue", ["remove", String(existing.id)], { encoding: "utf-8" });
11348
12271
  existing = findTask(ch);
11349
12272
  }
11350
- const runScript = join26(cwd, "run-claude.sh");
12273
+ const runScript = join27(cwd, "run-claude.sh");
11351
12274
  const args2 = [
11352
12275
  "add",
11353
12276
  "--label",
@@ -11477,12 +12400,12 @@ var init_pueue = __esm({
11477
12400
  "use strict";
11478
12401
  TASK_LABEL = "synkro-local-cc";
11479
12402
  TMUX_SESSION = "synkro-local-cc";
11480
- SESSION_DIR2 = join26(homedir27(), ".synkro", "cc_sessions");
12403
+ SESSION_DIR2 = join27(homedir28(), ".synkro", "cc_sessions");
11481
12404
  TASK_LABEL_2 = "synkro-local-cc-2";
11482
12405
  TMUX_SESSION_2 = "synkro-local-cc-2";
11483
- SESSION_DIR_22 = join26(homedir27(), ".synkro", "cc_sessions_2");
11484
- SESSION_DIR_32 = join26(homedir27(), ".synkro", "cc_sessions_3");
11485
- SESSION_DIR_42 = join26(homedir27(), ".synkro", "cc_sessions_4");
12406
+ SESSION_DIR_22 = join27(homedir28(), ".synkro", "cc_sessions_2");
12407
+ SESSION_DIR_32 = join27(homedir28(), ".synkro", "cc_sessions_3");
12408
+ SESSION_DIR_42 = join27(homedir28(), ".synkro", "cc_sessions_4");
11486
12409
  PueueError = class extends Error {
11487
12410
  constructor(message, cause) {
11488
12411
  super(message);
@@ -11497,13 +12420,13 @@ var init_pueue = __esm({
11497
12420
  });
11498
12421
 
11499
12422
  // cli/local-cc/settings.ts
11500
- import { existsSync as existsSync28, readFileSync as readFileSync26 } from "fs";
11501
- import { homedir as homedir28 } from "os";
11502
- import { join as join27 } from "path";
12423
+ import { existsSync as existsSync28, readFileSync as readFileSync27 } from "fs";
12424
+ import { homedir as homedir29 } from "os";
12425
+ import { join as join28 } from "path";
11503
12426
  function isLocalCCEnabled() {
11504
12427
  if (!existsSync28(CONFIG_PATH5)) return false;
11505
12428
  try {
11506
- const content = readFileSync26(CONFIG_PATH5, "utf-8");
12429
+ const content = readFileSync27(CONFIG_PATH5, "utf-8");
11507
12430
  const match = content.match(/^SYNKRO_LOCAL_INFERENCE='([^']*)'/m);
11508
12431
  return match?.[1] === "yes";
11509
12432
  } catch {
@@ -11514,7 +12437,7 @@ var CONFIG_PATH5;
11514
12437
  var init_settings = __esm({
11515
12438
  "cli/local-cc/settings.ts"() {
11516
12439
  "use strict";
11517
- CONFIG_PATH5 = join27(homedir28(), ".synkro", "config.env");
12440
+ CONFIG_PATH5 = join28(homedir29(), ".synkro", "config.env");
11518
12441
  }
11519
12442
  });
11520
12443
 
@@ -11524,10 +12447,10 @@ __export(localCc_exports, {
11524
12447
  localCcCommand: () => localCcCommand
11525
12448
  });
11526
12449
  import { spawnSync as spawnSync11 } from "child_process";
11527
- import { homedir as homedir29 } from "os";
11528
- import { join as join28 } from "path";
12450
+ import { homedir as homedir30 } from "os";
12451
+ import { join as join29 } from "path";
11529
12452
  import { readFileSync as fsReadFileSync, existsSync as fsExistsSync } from "fs";
11530
- import { existsSync as existsSync29, readFileSync as readFileSync27, writeFileSync as writeFileSync21 } from "fs";
12453
+ import { existsSync as existsSync29, readFileSync as readFileSync28, writeFileSync as writeFileSync22 } from "fs";
11531
12454
  function deploymentMode() {
11532
12455
  const env = (process.env.SYNKRO_DEPLOYMENT_MODE || "").toLowerCase();
11533
12456
  if (env === "docker") return "docker";
@@ -11634,14 +12557,14 @@ TROUBLESHOOTING
11634
12557
  }
11635
12558
  function readGatewayUrl() {
11636
12559
  if (existsSync29(CONFIG_PATH6)) {
11637
- const m = readFileSync27(CONFIG_PATH6, "utf-8").match(/^SYNKRO_GATEWAY_URL='([^']*)'/m);
12560
+ const m = readFileSync28(CONFIG_PATH6, "utf-8").match(/^SYNKRO_GATEWAY_URL='([^']*)'/m);
11638
12561
  if (m) return m[1];
11639
12562
  }
11640
12563
  return "https://api.synkro.sh";
11641
12564
  }
11642
12565
  function updateLocalInferenceFlag(enabled) {
11643
12566
  if (!existsSync29(CONFIG_PATH6)) return;
11644
- let content = readFileSync27(CONFIG_PATH6, "utf-8");
12567
+ let content = readFileSync28(CONFIG_PATH6, "utf-8");
11645
12568
  const flag = enabled ? "yes" : "no";
11646
12569
  if (content.includes("SYNKRO_LOCAL_INFERENCE=")) {
11647
12570
  content = content.replace(/^SYNKRO_LOCAL_INFERENCE='[^']*'/m, `SYNKRO_LOCAL_INFERENCE='${flag}'`);
@@ -11650,7 +12573,7 @@ function updateLocalInferenceFlag(enabled) {
11650
12573
  SYNKRO_LOCAL_INFERENCE='${flag}'
11651
12574
  `;
11652
12575
  }
11653
- writeFileSync21(CONFIG_PATH6, content, "utf-8");
12576
+ writeFileSync22(CONFIG_PATH6, content, "utf-8");
11654
12577
  }
11655
12578
  async function setServerGradingProvider(provider) {
11656
12579
  await ensureValidToken();
@@ -11679,7 +12602,7 @@ async function cmdStatus() {
11679
12602
  } else {
11680
12603
  console.log(`synkro-server container: running (${status.image})`);
11681
12604
  try {
11682
- const r = await fetch(`${status.healthz}healthz`, { signal: AbortSignal.timeout(3e3) });
12605
+ const r = await fetch(status.healthz, { signal: AbortSignal.timeout(3e3) });
11683
12606
  console.log(`Health probe: ${r.ok ? "ok" : `HTTP ${r.status}`}`);
11684
12607
  } catch (err) {
11685
12608
  console.log(`Health probe: ${err.message}`);
@@ -12084,8 +13007,8 @@ var init_localCc = __esm({
12084
13007
  init_install();
12085
13008
  init_client2();
12086
13009
  init_stub();
12087
- SYNKRO_CONFIG_PATH = join28(homedir29(), ".synkro", "config.env");
12088
- CONFIG_PATH6 = join28(homedir29(), ".synkro", "config.env");
13010
+ SYNKRO_CONFIG_PATH = join29(homedir30(), ".synkro", "config.env");
13011
+ CONFIG_PATH6 = join29(homedir30(), ".synkro", "config.env");
12089
13012
  }
12090
13013
  });
12091
13014
 
@@ -12094,14 +13017,14 @@ var import_exports = {};
12094
13017
  __export(import_exports, {
12095
13018
  importCommand: () => importCommand
12096
13019
  });
12097
- import { existsSync as existsSync30, readFileSync as readFileSync28, readdirSync as readdirSync7 } from "fs";
12098
- import { homedir as homedir30 } from "os";
12099
- import { join as join29 } from "path";
13020
+ import { existsSync as existsSync30, readFileSync as readFileSync29, readdirSync as readdirSync7 } from "fs";
13021
+ import { homedir as homedir31 } from "os";
13022
+ import { join as join30 } from "path";
12100
13023
  import { execSync as execSync6 } from "child_process";
12101
13024
  import { createInterface as createInterface4 } from "readline";
12102
13025
  function readMcpJwt() {
12103
13026
  try {
12104
- return readFileSync28(join29(homedir30(), ".synkro", ".mcp-jwt"), "utf-8").trim();
13027
+ return readFileSync29(join30(homedir31(), ".synkro", ".mcp-jwt"), "utf-8").trim();
12105
13028
  } catch {
12106
13029
  return "";
12107
13030
  }
@@ -12109,7 +13032,7 @@ function readMcpJwt() {
12109
13032
  function readConfigEnv2() {
12110
13033
  const out = {};
12111
13034
  try {
12112
- for (const line of readFileSync28(CONFIG_PATH7, "utf-8").split("\n")) {
13035
+ for (const line of readFileSync29(CONFIG_PATH7, "utf-8").split("\n")) {
12113
13036
  const t = line.trim();
12114
13037
  if (!t || t.startsWith("#")) continue;
12115
13038
  const eq = t.indexOf("=");
@@ -12121,7 +13044,7 @@ function readConfigEnv2() {
12121
13044
  }
12122
13045
  function projectsFolder() {
12123
13046
  const sanitized = process.cwd().replace(/\//g, "-");
12124
- const dir = join29(homedir30(), ".claude", "projects", sanitized);
13047
+ const dir = join30(homedir31(), ".claude", "projects", sanitized);
12125
13048
  return existsSync30(dir) ? dir : null;
12126
13049
  }
12127
13050
  function repoName() {
@@ -12161,7 +13084,7 @@ function extractToolResultText(content, e) {
12161
13084
  return t;
12162
13085
  }
12163
13086
  function parseSession(filePath, sessionId) {
12164
- const lines = readFileSync28(filePath, "utf-8").split("\n").filter(Boolean);
13087
+ const lines = readFileSync29(filePath, "utf-8").split("\n").filter(Boolean);
12165
13088
  const messages = [];
12166
13089
  const actions = [];
12167
13090
  let step = 0;
@@ -12241,7 +13164,7 @@ async function importCommand() {
12241
13164
  return;
12242
13165
  }
12243
13166
  }
12244
- const sessions = files.map((f) => parseSession(join29(dir, f), f.replace(".jsonl", ""))).filter((s) => s.messages.length > 0);
13167
+ const sessions = files.map((f) => parseSession(join30(dir, f), f.replace(".jsonl", ""))).filter((s) => s.messages.length > 0);
12245
13168
  const totalMsgs = sessions.reduce((n, s) => n + s.messages.length, 0);
12246
13169
  let ok = 0, fail = 0;
12247
13170
  if (isCloud) {
@@ -12316,7 +13239,7 @@ var init_import = __esm({
12316
13239
  "cli/commands/import.ts"() {
12317
13240
  "use strict";
12318
13241
  init_stub();
12319
- CONFIG_PATH7 = join29(homedir30(), ".synkro", "config.env");
13242
+ CONFIG_PATH7 = join30(homedir31(), ".synkro", "config.env");
12320
13243
  }
12321
13244
  });
12322
13245
 
@@ -12358,10 +13281,10 @@ var init_packVerify = __esm({
12358
13281
  });
12359
13282
 
12360
13283
  // cli/installer/lockfile.ts
12361
- import { existsSync as existsSync31, readFileSync as readFileSync29, writeFileSync as writeFileSync22 } from "fs";
12362
- import { join as join30 } from "path";
13284
+ import { existsSync as existsSync31, readFileSync as readFileSync30, writeFileSync as writeFileSync23 } from "fs";
13285
+ import { join as join31 } from "path";
12363
13286
  function lockPath(repoRoot2) {
12364
- return join30(repoRoot2, LOCK_FILE);
13287
+ return join31(repoRoot2, LOCK_FILE);
12365
13288
  }
12366
13289
  function writeLockfile(repoRoot2, entries) {
12367
13290
  const sorted = [...entries].sort((a, b) => a.ref.localeCompare(b.ref));
@@ -12379,7 +13302,7 @@ function writeLockfile(repoRoot2, entries) {
12379
13302
  ""
12380
13303
  ])
12381
13304
  ].join("\n");
12382
- writeFileSync22(lockPath(repoRoot2), body, "utf-8");
13305
+ writeFileSync23(lockPath(repoRoot2), body, "utf-8");
12383
13306
  }
12384
13307
  var LOCK_FILE;
12385
13308
  var init_lockfile = __esm({
@@ -12394,9 +13317,9 @@ var sync_exports = {};
12394
13317
  __export(sync_exports, {
12395
13318
  syncCommand: () => syncCommand
12396
13319
  });
12397
- import { existsSync as existsSync32, mkdirSync as mkdirSync18, readdirSync as readdirSync8, rmSync as rmSync4, writeFileSync as writeFileSync23 } from "fs";
12398
- import { homedir as homedir31 } from "os";
12399
- import { join as join31 } from "path";
13320
+ import { existsSync as existsSync32, mkdirSync as mkdirSync18, readdirSync as readdirSync8, rmSync as rmSync4, writeFileSync as writeFileSync24 } from "fs";
13321
+ import { homedir as homedir32 } from "os";
13322
+ import { join as join32 } from "path";
12400
13323
  function cacheKey(ref, version) {
12401
13324
  return ref.replace(/\//g, "__").replace(/[^\w.@-]/g, "_") + "@" + version + ".json";
12402
13325
  }
@@ -12427,7 +13350,7 @@ async function syncCommand(_args = []) {
12427
13350
  }
12428
13351
  const gateway = (process.env.SYNKRO_GATEWAY_URL || "https://api.synkro.sh").replace(/\/$/, "");
12429
13352
  const cloud = process.env.SYNKRO_DEPLOY_LOCATION === "cloud";
12430
- const cacheDir = join31(homedir31(), ".synkro", "cache", "packs");
13353
+ const cacheDir = join32(homedir32(), ".synkro", "cache", "packs");
12431
13354
  if (!cloud) mkdirSync18(cacheDir, { recursive: true });
12432
13355
  console.log(`Syncing ${refs.length} standard(s) from the registry\u2026`);
12433
13356
  const lock = [];
@@ -12456,7 +13379,7 @@ async function syncCommand(_args = []) {
12456
13379
  if (!cloud) {
12457
13380
  const fname = cacheKey(ref, data.version);
12458
13381
  keptCacheFiles.add(fname);
12459
- writeFileSync23(join31(cacheDir, fname), JSON.stringify({
13382
+ writeFileSync24(join32(cacheDir, fname), JSON.stringify({
12460
13383
  ref,
12461
13384
  version: data.version,
12462
13385
  digest: data.digest,
@@ -12472,7 +13395,7 @@ async function syncCommand(_args = []) {
12472
13395
  for (const f of readdirSync8(cacheDir)) {
12473
13396
  if (f.endsWith(".json") && !keptCacheFiles.has(f)) {
12474
13397
  try {
12475
- rmSync4(join31(cacheDir, f));
13398
+ rmSync4(join32(cacheDir, f));
12476
13399
  } catch {
12477
13400
  }
12478
13401
  }
@@ -12501,13 +13424,13 @@ var whoami_exports = {};
12501
13424
  __export(whoami_exports, {
12502
13425
  whoamiCommand: () => whoamiCommand
12503
13426
  });
12504
- import { readFileSync as readFileSync30, existsSync as existsSync33 } from "fs";
12505
- import { join as join32 } from "path";
12506
- import { homedir as homedir32 } from "os";
13427
+ import { readFileSync as readFileSync31, existsSync as existsSync33 } from "fs";
13428
+ import { join as join33 } from "path";
13429
+ import { homedir as homedir33 } from "os";
12507
13430
  function readConfigEnv3() {
12508
13431
  if (!existsSync33(CONFIG_PATH8)) return {};
12509
13432
  const out = {};
12510
- for (const line of readFileSync30(CONFIG_PATH8, "utf-8").split("\n")) {
13433
+ for (const line of readFileSync31(CONFIG_PATH8, "utf-8").split("\n")) {
12511
13434
  const t = line.trim();
12512
13435
  if (!t || t.startsWith("#")) continue;
12513
13436
  const eq = t.indexOf("=");
@@ -12518,7 +13441,7 @@ function readConfigEnv3() {
12518
13441
  function jwtStatus() {
12519
13442
  try {
12520
13443
  if (!existsSync33(JWT_PATH2)) return { status: "none" };
12521
- const jwt2 = readFileSync30(JWT_PATH2, "utf-8").trim();
13444
+ const jwt2 = readFileSync31(JWT_PATH2, "utf-8").trim();
12522
13445
  if (!jwt2) return { status: "none" };
12523
13446
  const payload = jwt2.split(".")[1];
12524
13447
  if (!payload) return { status: "valid" };
@@ -12580,9 +13503,9 @@ var SYNKRO_DIR15, CONFIG_PATH8, JWT_PATH2, GRADING_LABEL;
12580
13503
  var init_whoami = __esm({
12581
13504
  "cli/commands/whoami.ts"() {
12582
13505
  "use strict";
12583
- SYNKRO_DIR15 = join32(homedir32(), ".synkro");
12584
- CONFIG_PATH8 = join32(SYNKRO_DIR15, "config.env");
12585
- JWT_PATH2 = join32(SYNKRO_DIR15, ".mcp-jwt");
13506
+ SYNKRO_DIR15 = join33(homedir33(), ".synkro");
13507
+ CONFIG_PATH8 = join33(SYNKRO_DIR15, "config.env");
13508
+ JWT_PATH2 = join33(SYNKRO_DIR15, ".mcp-jwt");
12586
13509
  GRADING_LABEL = {
12587
13510
  local: "on-device worker pool",
12588
13511
  cloud: "Synkro Cloud worker pool",
@@ -12627,12 +13550,12 @@ __export(linear_exports, {
12627
13550
  formatLinks: () => formatLinks,
12628
13551
  linearCommand: () => linearCommand
12629
13552
  });
12630
- import { readFileSync as readFileSync31 } from "fs";
12631
- import { homedir as homedir33 } from "os";
12632
- import { join as join33 } from "path";
13553
+ import { readFileSync as readFileSync32 } from "fs";
13554
+ import { homedir as homedir34 } from "os";
13555
+ import { join as join34 } from "path";
12633
13556
  function mcpJwt() {
12634
13557
  try {
12635
- return readFileSync31(join33(SYNKRO_DIR16, ".mcp-jwt"), "utf-8").trim();
13558
+ return readFileSync32(join34(SYNKRO_DIR16, ".mcp-jwt"), "utf-8").trim();
12636
13559
  } catch {
12637
13560
  return "";
12638
13561
  }
@@ -12671,7 +13594,7 @@ var SYNKRO_DIR16, PORT2, BASE;
12671
13594
  var init_linear = __esm({
12672
13595
  "cli/commands/linear.ts"() {
12673
13596
  "use strict";
12674
- SYNKRO_DIR16 = join33(homedir33(), ".synkro");
13597
+ SYNKRO_DIR16 = join34(homedir34(), ".synkro");
12675
13598
  PORT2 = process.env.SYNKRO_MCP_PORT || "18931";
12676
13599
  BASE = `http://127.0.0.1:${PORT2}`;
12677
13600
  }
@@ -12679,7 +13602,7 @@ var init_linear = __esm({
12679
13602
 
12680
13603
  // cli/scanning/cveReachability.ts
12681
13604
  import { parse } from "@babel/parser";
12682
- import { readFileSync as readFileSync32 } from "fs";
13605
+ import { readFileSync as readFileSync33 } from "fs";
12683
13606
  function walk(node, visit) {
12684
13607
  if (!node || typeof node.type !== "string") return;
12685
13608
  visit(node);
@@ -12821,9 +13744,9 @@ var init_cveReachability = __esm({
12821
13744
 
12822
13745
  // cli/reachability/reachabilityScan.ts
12823
13746
  import { spawnSync as spawnSync12, execFileSync as execFileSync5 } from "child_process";
12824
- import { readFileSync as readFileSync33, writeFileSync as writeFileSync24, existsSync as existsSync34, readdirSync as readdirSync9 } from "fs";
12825
- import { join as join34 } from "path";
12826
- import { homedir as homedir34 } from "os";
13747
+ import { readFileSync as readFileSync34, writeFileSync as writeFileSync25, existsSync as existsSync34, readdirSync as readdirSync9 } from "fs";
13748
+ import { join as join35 } from "path";
13749
+ import { homedir as homedir35 } from "os";
12827
13750
  import { createRequire } from "module";
12828
13751
  function walkSourceFiles(repoRoot2, maxFiles = 4e3, maxBytes = 5e5) {
12829
13752
  const SKIP = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "build", "coverage", ".next", ".turbo", "out", ".cache", ".synkro", ".claude", "vendor", "__tests__", "test-results"]);
@@ -12840,7 +13763,7 @@ function walkSourceFiles(repoRoot2, maxFiles = 4e3, maxBytes = 5e5) {
12840
13763
  }
12841
13764
  for (const e of ents) {
12842
13765
  if (files.length >= maxFiles) break;
12843
- const full = join34(dir, e.name);
13766
+ const full = join35(dir, e.name);
12844
13767
  if (e.isDirectory()) {
12845
13768
  if (!SKIP.has(e.name) && !e.name.startsWith(".")) stack.push(full);
12846
13769
  continue;
@@ -12848,7 +13771,7 @@ function walkSourceFiles(repoRoot2, maxFiles = 4e3, maxBytes = 5e5) {
12848
13771
  if (!EXT.test(e.name) || e.name.endsWith(".d.ts")) continue;
12849
13772
  const rel = full.startsWith(repoRoot2 + "/") ? full.slice(repoRoot2.length + 1) : full;
12850
13773
  try {
12851
- const content = readFileSync33(full, "utf8");
13774
+ const content = readFileSync34(full, "utf8");
12852
13775
  if (content.length <= maxBytes) files.push({ path: rel, content });
12853
13776
  } catch {
12854
13777
  }
@@ -12867,12 +13790,12 @@ function cleanVersion(spec) {
12867
13790
  function gatherManifestVersions(repoRoot2) {
12868
13791
  const out = {};
12869
13792
  const dirs = [repoRoot2];
12870
- const pkgsDir = join34(repoRoot2, "packages");
13793
+ const pkgsDir = join35(repoRoot2, "packages");
12871
13794
  if (existsSync34(pkgsDir)) {
12872
13795
  try {
12873
13796
  for (const d of readdirSync9(pkgsDir)) {
12874
- const pd = join34(pkgsDir, d);
12875
- if (existsSync34(join34(pd, "package.json"))) dirs.push(pd);
13797
+ const pd = join35(pkgsDir, d);
13798
+ if (existsSync34(join35(pd, "package.json"))) dirs.push(pd);
12876
13799
  }
12877
13800
  } catch {
12878
13801
  }
@@ -12881,7 +13804,7 @@ function gatherManifestVersions(repoRoot2) {
12881
13804
  for (const dir of dirs) {
12882
13805
  let pkg;
12883
13806
  try {
12884
- pkg = JSON.parse(readFileSync33(join34(dir, "package.json"), "utf8"));
13807
+ pkg = JSON.parse(readFileSync34(join35(dir, "package.json"), "utf8"));
12885
13808
  } catch {
12886
13809
  continue;
12887
13810
  }
@@ -12901,28 +13824,28 @@ function findJelly(repoRoot2) {
12901
13824
  try {
12902
13825
  const pkgJson = require2.resolve("@cs-au-dk/jelly/package.json");
12903
13826
  const dir = pkgJson.slice(0, pkgJson.length - "package.json".length);
12904
- const pkg = JSON.parse(readFileSync33(pkgJson, "utf8"));
13827
+ const pkg = JSON.parse(readFileSync34(pkgJson, "utf8"));
12905
13828
  const bin = typeof pkg.bin === "string" ? pkg.bin : pkg.bin && (pkg.bin.jelly || pkg.bin[Object.keys(pkg.bin)[0]]);
12906
13829
  if (bin) {
12907
- const p = join34(dir, bin);
13830
+ const p = join35(dir, bin);
12908
13831
  if (existsSync34(p)) return p;
12909
13832
  }
12910
13833
  } catch {
12911
13834
  }
12912
13835
  for (const base of [repoRoot2, process.cwd()]) {
12913
- const b = join34(base, "node_modules", ".bin", "jelly");
13836
+ const b = join35(base, "node_modules", ".bin", "jelly");
12914
13837
  if (existsSync34(b)) return b;
12915
13838
  }
12916
13839
  return null;
12917
13840
  }
12918
13841
  function findEntries(repoRoot2) {
12919
13842
  const dirs = [repoRoot2];
12920
- const pkgsDir = join34(repoRoot2, "packages");
13843
+ const pkgsDir = join35(repoRoot2, "packages");
12921
13844
  if (existsSync34(pkgsDir)) {
12922
13845
  try {
12923
13846
  for (const d of readdirSync9(pkgsDir)) {
12924
- const pd = join34(pkgsDir, d);
12925
- if (existsSync34(join34(pd, "package.json"))) dirs.push(pd);
13847
+ const pd = join35(pkgsDir, d);
13848
+ if (existsSync34(join35(pd, "package.json"))) dirs.push(pd);
12926
13849
  }
12927
13850
  } catch {
12928
13851
  }
@@ -12930,11 +13853,11 @@ function findEntries(repoRoot2) {
12930
13853
  const entries = [];
12931
13854
  for (const dir of dirs) {
12932
13855
  try {
12933
- const pkg = JSON.parse(readFileSync33(join34(dir, "package.json"), "utf8"));
13856
+ const pkg = JSON.parse(readFileSync34(join35(dir, "package.json"), "utf8"));
12934
13857
  const cands = [pkg.source, pkg.module, pkg.main, "src/index.ts", "src/index.js", "src/main.ts", "src/server.ts", "index.ts", "index.js"];
12935
13858
  for (const c of cands) {
12936
13859
  if (typeof c !== "string") continue;
12937
- const f = join34(dir, c);
13860
+ const f = join35(dir, c);
12938
13861
  if (existsSync34(f)) {
12939
13862
  entries.push(f);
12940
13863
  break;
@@ -12970,7 +13893,7 @@ function runReachabilityScan(repoRoot2, opts = {}) {
12970
13893
  const commit = currentCommit(repoRoot2);
12971
13894
  if (!opts.force && commit && existsSync34(REACHABILITY_PATH)) {
12972
13895
  try {
12973
- const prev = JSON.parse(readFileSync33(REACHABILITY_PATH, "utf8"));
13896
+ const prev = JSON.parse(readFileSync34(REACHABILITY_PATH, "utf8"));
12974
13897
  if (prev.commit === commit) return { ok: true, cached: true, packages: Object.keys(prev.packages || {}).length };
12975
13898
  } catch {
12976
13899
  }
@@ -13059,7 +13982,7 @@ function runReachabilityScan(repoRoot2, opts = {}) {
13059
13982
  if (Object.keys(packages).length === 0) return { ok: false, reason: "no package usage found (no jelly output, no AST imports)" };
13060
13983
  const file = { generatedAt: (/* @__PURE__ */ new Date()).toISOString(), commit, tool, packages, versions: gatherManifestVersions(repoRoot2) };
13061
13984
  try {
13062
- writeFileSync24(REACHABILITY_PATH, JSON.stringify(file, null, 2));
13985
+ writeFileSync25(REACHABILITY_PATH, JSON.stringify(file, null, 2));
13063
13986
  } catch (e) {
13064
13987
  return { ok: false, reason: "write failed: " + String(e.message || e) };
13065
13988
  }
@@ -13071,7 +13994,7 @@ var init_reachabilityScan = __esm({
13071
13994
  "use strict";
13072
13995
  init_cveReachability();
13073
13996
  require2 = createRequire(import.meta.url);
13074
- REACHABILITY_PATH = join34(homedir34(), ".synkro", "reachability.json");
13997
+ REACHABILITY_PATH = join35(homedir35(), ".synkro", "reachability.json");
13075
13998
  }
13076
13999
  });
13077
14000
 
@@ -13080,15 +14003,15 @@ var reachabilityScan_exports = {};
13080
14003
  __export(reachabilityScan_exports, {
13081
14004
  reachabilityScanCommand: () => reachabilityScanCommand
13082
14005
  });
13083
- import { readFileSync as readFileSync34, existsSync as existsSync35 } from "fs";
13084
- import { join as join35 } from "path";
13085
- import { homedir as homedir35 } from "os";
14006
+ import { readFileSync as readFileSync35, existsSync as existsSync35 } from "fs";
14007
+ import { join as join36 } from "path";
14008
+ import { homedir as homedir36 } from "os";
13086
14009
  import { execFileSync as execFileSync6 } from "child_process";
13087
14010
  function readConfigEnv4() {
13088
- const p = join35(SYNKRO_DIR17, "config.env");
14011
+ const p = join36(SYNKRO_DIR17, "config.env");
13089
14012
  if (!existsSync35(p)) return {};
13090
14013
  const out = {};
13091
- for (const line of readFileSync34(p, "utf-8").split("\n")) {
14014
+ for (const line of readFileSync35(p, "utf-8").split("\n")) {
13092
14015
  const t = line.trim();
13093
14016
  if (!t || t.startsWith("#")) continue;
13094
14017
  const eq = t.indexOf("=");
@@ -13120,11 +14043,11 @@ async function pushToCloud(cfg, repo) {
13120
14043
  while (gwBase.endsWith("/")) gwBase = gwBase.slice(0, -1);
13121
14044
  let jwt2 = "";
13122
14045
  try {
13123
- jwt2 = readFileSync34(join35(SYNKRO_DIR17, ".mcp-jwt"), "utf-8").trim();
14046
+ jwt2 = readFileSync35(join36(SYNKRO_DIR17, ".mcp-jwt"), "utf-8").trim();
13124
14047
  } catch {
13125
14048
  }
13126
14049
  if (!jwt2 || !existsSync35(REACHABILITY_PATH)) return;
13127
- const body = readFileSync34(REACHABILITY_PATH, "utf-8");
14050
+ const body = readFileSync35(REACHABILITY_PATH, "utf-8");
13128
14051
  try {
13129
14052
  const resp = await fetch(gwBase + "/api/v1/reachability?repo=" + encodeURIComponent(repo), {
13130
14053
  method: "POST",
@@ -13156,7 +14079,7 @@ var init_reachabilityScan2 = __esm({
13156
14079
  "cli/commands/reachabilityScan.ts"() {
13157
14080
  "use strict";
13158
14081
  init_reachabilityScan();
13159
- SYNKRO_DIR17 = join35(homedir35(), ".synkro");
14082
+ SYNKRO_DIR17 = join36(homedir36(), ".synkro");
13160
14083
  }
13161
14084
  });
13162
14085
 
@@ -13188,9 +14111,9 @@ async function startCommand(rest = []) {
13188
14111
  console.log(`Synkro: starting server (${cfg.claudeWorkers} claude + ${cfg.cursorWorkers} cursor + ${cfg.codexWorkers} codex)
13189
14112
  `);
13190
14113
  await dockerUpdate({ claudeWorkers: cfg.claudeWorkers, cursorWorkers: cfg.cursorWorkers, codexWorkers: cfg.codexWorkers, conductorProvider: cfg.conductorProvider, connectedRepo: resolveConnectedRepo() });
13191
- const ready = await waitForContainerReady(6e4);
14114
+ const ready = await waitForContainerReady(LOCAL_CONTAINER_READY_TIMEOUT_MS);
13192
14115
  if (!ready) {
13193
- console.error("\n\u26A0 container did not pass /healthz within 60s");
14116
+ console.error("\n\u26A0 container did not pass /healthz within 10m");
13194
14117
  process.exit(1);
13195
14118
  }
13196
14119
  console.log("\nServer is running.");
@@ -13220,9 +14143,9 @@ async function updateCommand() {
13220
14143
  console.log(` preserving pool: ${claudeWorkers} claude + ${cursorWorkers} cursor + ${codexWorkers} codex worker(s)
13221
14144
  `);
13222
14145
  await dockerUpdate({ claudeWorkers, cursorWorkers, codexWorkers, conductorProvider, connectedRepo: resolveConnectedRepo() });
13223
- const ready = await waitForContainerReady(9e4);
14146
+ const ready = await waitForContainerReady(LOCAL_CONTAINER_READY_TIMEOUT_MS);
13224
14147
  if (!ready) {
13225
- console.error("\n\u26A0 container did not pass its health check within 90s \u2014 check: docker logs synkro-server");
14148
+ console.error("\n\u26A0 container did not pass its health check within 10m \u2014 check: docker logs synkro-server");
13226
14149
  process.exit(1);
13227
14150
  }
13228
14151
  try {
@@ -13257,9 +14180,9 @@ async function restartCommand(rest = []) {
13257
14180
  console.log(`Synkro: restarting server (${claudeWorkers} claude + ${cursorWorkers} cursor + ${codexWorkers} codex)
13258
14181
  `);
13259
14182
  await dockerUpdate({ claudeWorkers, cursorWorkers, codexWorkers, conductorProvider, connectedRepo: resolveConnectedRepo() });
13260
- const ready = await waitForContainerReady(6e4);
14183
+ const ready = await waitForContainerReady(LOCAL_CONTAINER_READY_TIMEOUT_MS);
13261
14184
  if (!ready) {
13262
- console.error("\n\u26A0 container did not pass /healthz within 60s");
14185
+ console.error("\n\u26A0 container did not pass /healthz within 10m");
13263
14186
  process.exit(1);
13264
14187
  }
13265
14188
  console.log("\nServer restarted successfully.");
@@ -13271,11 +14194,13 @@ async function restartCommand(rest = []) {
13271
14194
  console.warn("\u26A0 workers did not register within 30s \u2014 skill sync skipped");
13272
14195
  }
13273
14196
  }
14197
+ var LOCAL_CONTAINER_READY_TIMEOUT_MS;
13274
14198
  var init_lifecycle = __esm({
13275
14199
  "cli/commands/lifecycle.ts"() {
13276
14200
  "use strict";
13277
14201
  init_dockerInstall();
13278
14202
  init_install();
14203
+ LOCAL_CONTAINER_READY_TIMEOUT_MS = 6e5;
13279
14204
  }
13280
14205
  });
13281
14206
 
@@ -13284,13 +14209,13 @@ var config_exports = {};
13284
14209
  __export(config_exports, {
13285
14210
  configCommand: () => configCommand
13286
14211
  });
13287
- import { readFileSync as readFileSync35, writeFileSync as writeFileSync25, existsSync as existsSync36 } from "fs";
13288
- import { join as join36 } from "path";
13289
- import { homedir as homedir36 } from "os";
14212
+ import { readFileSync as readFileSync36, writeFileSync as writeFileSync26, existsSync as existsSync36 } from "fs";
14213
+ import { join as join37 } from "path";
14214
+ import { homedir as homedir37 } from "os";
13290
14215
  function readConfigEnv5() {
13291
14216
  if (!existsSync36(CONFIG_PATH9)) return {};
13292
14217
  const out = {};
13293
- for (const line of readFileSync35(CONFIG_PATH9, "utf-8").split("\n")) {
14218
+ for (const line of readFileSync36(CONFIG_PATH9, "utf-8").split("\n")) {
13294
14219
  const t = line.trim();
13295
14220
  if (!t || t.startsWith("#")) continue;
13296
14221
  const eq = t.indexOf("=");
@@ -13303,7 +14228,7 @@ function updateConfigValue(key, value) {
13303
14228
  console.error("No config found. Run `synkro install` first.");
13304
14229
  process.exit(1);
13305
14230
  }
13306
- const lines = readFileSync35(CONFIG_PATH9, "utf-8").split("\n");
14231
+ const lines = readFileSync36(CONFIG_PATH9, "utf-8").split("\n");
13307
14232
  const pattern = new RegExp(`^${key}=`);
13308
14233
  let found = false;
13309
14234
  const updated = lines.map((line) => {
@@ -13314,7 +14239,7 @@ function updateConfigValue(key, value) {
13314
14239
  return line;
13315
14240
  });
13316
14241
  if (!found) updated.splice(updated.length - 1, 0, `${key}='${value}'`);
13317
- writeFileSync25(CONFIG_PATH9, updated.join("\n"), "utf-8");
14242
+ writeFileSync26(CONFIG_PATH9, updated.join("\n"), "utf-8");
13318
14243
  }
13319
14244
  function resolveInferenceMode(cfg) {
13320
14245
  if ((cfg.SYNKRO_GRADING_MODE || "local") === "byok") return "byok";
@@ -13472,8 +14397,8 @@ var init_config = __esm({
13472
14397
  "use strict";
13473
14398
  init_stub();
13474
14399
  init_optout();
13475
- SYNKRO_DIR18 = join36(homedir36(), ".synkro");
13476
- CONFIG_PATH9 = join36(SYNKRO_DIR18, "config.env");
14400
+ SYNKRO_DIR18 = join37(homedir37(), ".synkro");
14401
+ CONFIG_PATH9 = join37(SYNKRO_DIR18, "config.env");
13477
14402
  }
13478
14403
  });
13479
14404
 
@@ -13662,14 +14587,14 @@ Usage:
13662
14587
  });
13663
14588
 
13664
14589
  // cli/bootstrap.js
13665
- import { readFileSync as readFileSync36, existsSync as existsSync37 } from "fs";
14590
+ import { readFileSync as readFileSync37, existsSync as existsSync37 } from "fs";
13666
14591
  import { resolve as resolve5 } from "path";
13667
14592
  var envCandidates = [
13668
14593
  resolve5(process.env.HOME ?? "", ".synkro", "config.env")
13669
14594
  ];
13670
14595
  for (const envPath of envCandidates) {
13671
14596
  if (!existsSync37(envPath)) continue;
13672
- const envContent = readFileSync36(envPath, "utf-8");
14597
+ const envContent = readFileSync37(envPath, "utf-8");
13673
14598
  for (const line of envContent.split("\n")) {
13674
14599
  const trimmed = line.trim();
13675
14600
  if (!trimmed || trimmed.startsWith("#")) continue;
@@ -13686,7 +14611,7 @@ var subArgs = args.slice(1);
13686
14611
  var isDetachedChild = process.env.SYNKRO_TELEMETRY_DETACHED === "1";
13687
14612
  var FLUSH_SKIP = /* @__PURE__ */ new Set(["grade", "version", "--version", "-v", "help", "--help", "-h", ""]);
13688
14613
  function printVersion() {
13689
- console.log("1.7.88");
14614
+ console.log("1.7.90");
13690
14615
  }
13691
14616
  function printHelp2() {
13692
14617
  console.log(`Synkro CLI \u2014 runtime safety for AI coding agents