@synkro-sh/cli 1.10.5 → 1.10.8

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 cliVersion2 = "0.0.0";
149
149
  try {
150
- cliVersion2 = "1.10.5";
150
+ cliVersion2 = "1.10.8";
151
151
  } catch {
152
152
  }
153
153
  const creds = loadCredentialsIdentity();
@@ -2351,7 +2351,7 @@ function buildCodexHookTrustEdits(hooks) {
2351
2351
  return [...edits.values()];
2352
2352
  }
2353
2353
  function queryCodexHookTrust(codexBinary = "codex", cwd = process.cwd(), autoTrust = false) {
2354
- return new Promise((resolve7) => {
2354
+ return new Promise((resolve8) => {
2355
2355
  let settled = false;
2356
2356
  let stdout = "";
2357
2357
  let pending = "";
@@ -2369,7 +2369,7 @@ function queryCodexHookTrust(codexBinary = "codex", cwd = process.cwd(), autoTru
2369
2369
  child.kill();
2370
2370
  } catch {
2371
2371
  }
2372
- resolve7(summary);
2372
+ resolve8(summary);
2373
2373
  };
2374
2374
  const timer = setTimeout(() => finish(null), 1e4);
2375
2375
  try {
@@ -3255,10 +3255,15 @@ function pruneToolCallMarks(now: number): void {
3255
3255
 
3256
3256
  // The user's answer to "move into the task worktree, or stay here?". Written by the
3257
3257
  // "synkro workspace stay" command (the CLI owns ~/.synkro, so nothing hand-writes it)
3258
- // and read here. Keyed by task alone: the decision resets when the active task
3259
- // changes, which is the scope the workspace gate is asking about.
3258
+ // and read here. The filename includes a one-way session key so consent in one
3259
+ // Claude/Codex chat can never silently authorize another chat working the task.
3260
3260
  const WORKSPACE_CHOICE_DIR = join(HOME, '.synkro', 'workspace-choice');
3261
3261
 
3262
+ function taskWorkspaceChoicePath(taskId: string, sessionId: string): string {
3263
+ const sessionKey = createHash('sha256').update(String(sessionId || '')).digest('hex').slice(0, 16);
3264
+ return join(WORKSPACE_CHOICE_DIR, taskId + '.' + sessionKey + '.stay');
3265
+ }
3266
+
3262
3267
  // The user answers the workspace question in plain language on their next
3263
3268
  // prompt. Writing the marker is exactly what the CLI's "workspace stay"
3264
3269
  // command does — but the consent matcher only accepts one literal spelling,
@@ -3279,14 +3284,23 @@ function isWorkspaceStayIntent(prompt: string): boolean {
3279
3284
  ].some((pattern) => pattern.test(normalized));
3280
3285
  }
3281
3286
 
3282
- function taskWorkspaceStayRecorded(taskId: string): boolean {
3283
- if (!/^task_[a-z0-9]{8}$/i.test(String(taskId || ''))) return false;
3284
- try { return existsSync(join(WORKSPACE_CHOICE_DIR, taskId + '.stay')); } catch { return false; }
3287
+ function taskWorkspaceStayRecorded(taskId: string, sessionId: string): boolean {
3288
+ if (!/^task_[a-z0-9]{8}$/i.test(String(taskId || '')) || !sessionId) return false;
3289
+ try { return existsSync(taskWorkspaceChoicePath(taskId, sessionId)); } catch { return false; }
3285
3290
  }
3286
3291
 
3287
- // Our own context string, so the shape is stable: '... task=<id> ...'.
3288
- function taskIdFromScmContext(context: string): string {
3289
- const marker = ' task=';
3292
+ function recordTaskWorkspaceStay(taskId: string, sessionId: string): boolean {
3293
+ if (!/^task_[a-z0-9]{8}$/i.test(String(taskId || '')) || !sessionId) return false;
3294
+ try {
3295
+ mkdirSync(WORKSPACE_CHOICE_DIR, { recursive: true });
3296
+ writeFileSync(taskWorkspaceChoicePath(taskId, sessionId), new Date().toISOString() + '\n', { mode: 0o600 });
3297
+ return true;
3298
+ } catch { return false; }
3299
+ }
3300
+
3301
+ // Our own context string, so the shape is stable: '... task=<id> branch=<name> ...'.
3302
+ function fieldFromScmContext(context: string, field: string): string {
3303
+ const marker = ' ' + field + '=';
3290
3304
  const at = String(context || '').indexOf(marker);
3291
3305
  if (at === -1) return '';
3292
3306
  const rest = context.slice(at + marker.length);
@@ -3294,14 +3308,73 @@ function taskIdFromScmContext(context: string): string {
3294
3308
  return (end === -1 ? rest : rest.slice(0, end)).trim();
3295
3309
  }
3296
3310
 
3311
+ function taskIdFromScmContext(context: string): string {
3312
+ return fieldFromScmContext(context, 'task');
3313
+ }
3314
+
3315
+ // An exact-command allowance must not be defeated by the output plumbing agents
3316
+ // reflexively append ('2>&1 | tail -2') — requiring the bare spelling made the
3317
+ // gate's own escape hatch block itself (observed live). Only harmless trailing
3318
+ // redirection plus a single display filter pass; anything that could chain a
3319
+ // second command (&&, ;, extra pipes, substitution) still rejects.
3320
+ function isBareCommandWithPlumbing(command: string, head: string): boolean {
3321
+ if (!command.startsWith(head)) return false;
3322
+ const rest = command.slice(head.length);
3323
+ return /^(?:\s+2>&1)?(?:\s*\|\s*(?:tail|head|cat)(?:\s+[-\w.+]+)*)?\s*$/.test(rest);
3324
+ }
3325
+
3326
+ function bashCommandOf(payload: any): string {
3327
+ if (String(payload?.tool_name || '') !== 'Bash') return '';
3328
+ const input = payload?.tool_input && typeof payload.tool_input === 'object' ? payload.tool_input : {};
3329
+ return String(input.command || input.cmd || '').trim();
3330
+ }
3331
+
3297
3332
  // Consent must never deadlock behind the block it resolves: the command that records
3298
3333
  // the answer is allowed through for the task currently being asked about, and nothing
3299
3334
  // else is.
3300
- function isWorkspaceConsentCommand(payload: any, taskId: string): boolean {
3301
- if (!taskId || String(payload?.tool_name || '') !== 'Bash') return false;
3335
+ function workspaceReadOnlyShellSegment(segment: string): boolean {
3336
+ const value = String(segment || '').trim();
3337
+ if (!value || /[;&><$]/.test(value) || value.includes(String.fromCharCode(96))) return false;
3338
+ const tokens = value.split(/\s+/);
3339
+ const verb = String(tokens[0] || '').toLowerCase();
3340
+ const writeOptions = new Set(['-i', '--in-place', '-o', '--output', '--output-document']);
3341
+ if (tokens.some((token) => writeOptions.has(token) || token.startsWith('--output='))) return false;
3342
+ const reads = new Set(['cat', 'head', 'tail', 'grep', 'egrep', 'fgrep', 'rg', 'ag', 'find', 'fd', 'ls', 'wc', 'cmp', 'diff', 'file', 'stat', 'pwd', 'sort', 'uniq', 'cut', 'tr', 'jq', 'yq']);
3343
+ if (reads.has(verb)) {
3344
+ if ((verb === 'find' || verb === 'fd') && tokens.some((token) => new Set(['-exec', '-execdir', '-ok', '-okdir', '-delete', '-fprint', '-fprint0', '-fls', '--exec', '--exec-batch']).has(token))) return false;
3345
+ return true;
3346
+ }
3347
+ if (verb !== 'git') return false;
3348
+ return new Set(['log', 'show', 'diff', 'blame', 'status', 'rev-parse', 'ls-files', 'ls-tree', 'cat-file', 'shortlog', 'describe']).has(String(tokens[1] || '').toLowerCase());
3349
+ }
3350
+
3351
+ function isWorkspaceReadOnlyDiagnostic(payload: any): boolean {
3352
+ const toolName = String(payload?.tool_name || '');
3353
+ if (/^(?:Read|ReadFile|read_file|Grep|grep_search|codebase_search|file_search|Glob|list_dir)$/i.test(toolName)) return true;
3354
+ if (!/^(?:Bash|Shell|terminal|run_terminal_cmd|execute_command)$/i.test(toolName)) return false;
3302
3355
  const input = payload?.tool_input && typeof payload.tool_input === 'object' ? payload.tool_input : {};
3303
3356
  const command = String(input.command || input.cmd || '').trim();
3304
- return command === 'synkro workspace stay ' + taskId;
3357
+ if (!command || /\|\||(^|[^&])&([^&]|$)|;/.test(command)) return false;
3358
+ return command.split('|').every(workspaceReadOnlyShellSegment);
3359
+ }
3360
+
3361
+ function isWorkspaceConsentCommand(payload: any, taskId: string): boolean {
3362
+ if (!taskId) return false;
3363
+ const command = bashCommandOf(payload);
3364
+ return Boolean(command) && isBareCommandWithPlumbing(command, 'synkro workspace stay ' + taskId);
3365
+ }
3366
+
3367
+ // A session that is already inside the task worktree but on the wrong branch needs
3368
+ // exactly one repair: checking out the canonical branch. The gate demands that
3369
+ // branch, so blocking the checkout that reaches it would deadlock (observed live
3370
+ // after an agent created a side branch in the task worktree). Elsewhere the command
3371
+ // is harmless: git itself refuses to check out a branch held by another worktree.
3372
+ function isWorkspaceRepairCommand(payload: any, branchName: string): boolean {
3373
+ if (!branchName) return false;
3374
+ const command = bashCommandOf(payload);
3375
+ if (!command) return false;
3376
+ return isBareCommandWithPlumbing(command, 'git checkout ' + branchName)
3377
+ || isBareCommandWithPlumbing(command, 'git switch ' + branchName);
3305
3378
  }
3306
3379
 
3307
3380
  function firstHookForToolCall(sessionId: string, payload: any): boolean {
@@ -3666,7 +3739,7 @@ function shellTaskWorkspaceArg(value: string): string {
3666
3739
  return "'" + value.replace(/'/g, "'\"'\"'") + "'";
3667
3740
  }
3668
3741
 
3669
- function taskScmWorkspaceInstruction(harness: string, sessionId: string, workspace: any): string {
3742
+ function taskScmWorkspaceInstruction(harness: string, sessionId: string, workspace: any, root = ''): string {
3670
3743
  const context = taskScmWorkspaceContext(workspace, harness);
3671
3744
  const worktreePath = String(workspace?.worktreePath || '');
3672
3745
  const branchName = String(workspace?.branchName || '');
@@ -3676,6 +3749,15 @@ function taskScmWorkspaceInstruction(harness: string, sessionId: string, workspa
3676
3749
  + 'the task worktree and canonical branch.';
3677
3750
  if (harness === 'cc') {
3678
3751
  if (!worktreePath) return context + '\nTask worktree creation failed closed; retry workspace preparation.';
3752
+ // Already inside the task worktree, just not on its branch: the move/stay
3753
+ // question was answered the moment the session got here, and re-asking it
3754
+ // offers two exits that both fail. Name the one command that repairs it —
3755
+ // the gate lets exactly that command through.
3756
+ if (root && samePath(root, worktreePath)) {
3757
+ return context + '\nThis session is already in the task worktree but not on its canonical branch. '
3758
+ + 'Run "git checkout ' + branchName + '" to continue; that command is allowed through this gate, '
3759
+ + 'and substantive tools stay blocked until the branch matches.';
3760
+ }
3679
3761
  // Ask, do not command. The user may legitimately want to keep working where they
3680
3762
  // are, and Synkro should not move them without their say-so.
3681
3763
  return context + '\nAsk the user whether to move this task into its isolated worktree '
@@ -3691,14 +3773,18 @@ function taskScmWorkspaceInstruction(harness: string, sessionId: string, workspa
3691
3773
  + '. Confirm the resumed workspace is on branch ' + JSON.stringify(branchName) + '. ' + requirement;
3692
3774
  }
3693
3775
  if (!worktreePath) {
3694
- return context + '\n[synkro:workspace-handoff] Invoke Codex\'s native task/worktree Handoff now for this exact task. '
3695
- + 'Check out branch ' + JSON.stringify(branchName) + ' in a new worktree and continue this same chat there. '
3696
- + 'Do not switch the shared Local checkout or ask the user to switch branches. '
3697
- + 'Synkro keeps substantive tools blocked until this exact session reports the new worktree path and canonical branch.';
3776
+ return context + '\nAsk the user whether to move this task into a generated isolated Codex worktree on branch '
3777
+ + JSON.stringify(branchName) + ', or keep working in the current workspace. Do not decide for them. '
3778
+ + 'To move: use Codex\'s Environment control or native Handoff for this task; never hand off a different task as a workaround. '
3779
+ + 'To stay: run "synkro workspace stay ' + String(workspace?.taskId || '') + '". '
3780
+ + 'Never switch the shared Local checkout.';
3698
3781
  }
3699
- return context + '\n[synkro:workspace-handoff] Invoke Codex\'s native task/worktree Handoff now for this exact task, '
3700
- + 'using the existing worktree ' + JSON.stringify(worktreePath) + ' on branch ' + JSON.stringify(branchName) + '. '
3701
- + 'The Codex Environment panel must show that worktree and branch before retrying the blocked action. ' + requirement;
3782
+ return context + '\nAsk the user whether to move this task to its prepared worktree '
3783
+ + JSON.stringify(worktreePath) + ' on branch ' + JSON.stringify(branchName)
3784
+ + ', or keep working in the current isolated Codex worktree. Do not decide for them. '
3785
+ + 'To move: use Codex\'s Environment control or native Handoff for this task; never hand off a different task as a workaround. '
3786
+ + 'To stay: run "synkro workspace stay ' + String(workspace?.taskId || '') + '". '
3787
+ + 'Never switch the shared Local checkout.';
3702
3788
  }
3703
3789
 
3704
3790
  // CC validates hookSpecificOutput.hookEventName against the event that actually fired.
@@ -3780,7 +3866,7 @@ async function reconcileTaskScm(
3780
3866
  : result?.workspace;
3781
3867
  return {
3782
3868
  reason: result?.waiting
3783
- ? (taskScmWorkspaceInstruction(harness, sessionId, workspace)
3869
+ ? (taskScmWorkspaceInstruction(harness, sessionId, workspace, root)
3784
3870
  || String(result.reason || 'task source-control preparation is pending'))
3785
3871
  : '',
3786
3872
  context: taskScmWorkspaceContext(workspace, harness),
@@ -4479,19 +4565,21 @@ export async function runStub(surface: string, opts: StubOpts = {}): Promise<voi
4479
4565
  // prompt records the same durable marker the CLI command writes. Reconcile
4480
4566
  // just told us WHICH task is being asked, so no extra state is needed and
4481
4567
  // ordinary prompts outside a pending ask are never scanned.
4482
- if (surface === 'prompt-submit' && scm.reason && scmTaskId && !taskWorkspaceStayRecorded(scmTaskId)) {
4568
+ if (surface === 'prompt-submit' && scm.reason && scmTaskId && !taskWorkspaceStayRecorded(scmTaskId, sessionId)) {
4483
4569
  const promptText = String(payload.prompt || payload.user_message || '');
4484
4570
  if (isWorkspaceStayIntent(promptText)) {
4485
- try {
4486
- mkdirSync(WORKSPACE_CHOICE_DIR, { recursive: true });
4487
- writeFileSync(join(WORKSPACE_CHOICE_DIR, scmTaskId + '.stay'), new Date().toISOString() + '\n');
4488
- } catch { /* fail-open: the CLI command and the exact-string path remain */ }
4571
+ recordTaskWorkspaceStay(scmTaskId, sessionId);
4489
4572
  }
4490
4573
  }
4491
- // The user was asked and chose to keep working here, or is answering right now.
4574
+ const workspaceConsentCommand = Boolean(scmTaskId) && isWorkspaceConsentCommand(payload, scmTaskId);
4575
+ if (workspaceConsentCommand) recordTaskWorkspaceStay(scmTaskId, sessionId);
4576
+ // The user was asked and chose to keep working here, is answering right now,
4577
+ // or the tool call is the one git command that repairs the workspace itself.
4492
4578
  const workspaceConsentSettled = Boolean(scmTaskId)
4493
- && (taskWorkspaceStayRecorded(scmTaskId) || isWorkspaceConsentCommand(payload, scmTaskId));
4494
- if (scm.reason && substantiveTool && !workspaceConsentSettled) {
4579
+ && (taskWorkspaceStayRecorded(scmTaskId, sessionId)
4580
+ || workspaceConsentCommand
4581
+ || isWorkspaceRepairCommand(payload, fieldFromScmContext(scm.context, 'branch')));
4582
+ if (scm.reason && substantiveTool && !workspaceConsentSettled && !isWorkspaceReadOnlyDiagnostic(payload)) {
4495
4583
  out(taskScmBlockResponse(harness, scm.reason, firstHookForToolCall(sessionId, payload)));
4496
4584
  return;
4497
4585
  }
@@ -4680,7 +4768,7 @@ export async function runStub(surface: string, opts: StubOpts = {}): Promise<voi
4680
4768
  let scmContext = scm.context && surface === 'prompt-submit' && firstHookForToolCall(sessionId, payload) ? scm.context : '';
4681
4769
  // Say plainly that work is continuing outside the task worktree by the user's choice,
4682
4770
  // so the state is never mistaken for a binding that silently failed.
4683
- if (scmContext && scmTaskId && taskWorkspaceStayRecorded(scmTaskId)) {
4771
+ if (scmContext && scmTaskId && taskWorkspaceStayRecorded(scmTaskId, sessionId)) {
4684
4772
  scmContext += ' workspace=staying-here-by-user-choice';
4685
4773
  }
4686
4774
  const responseText = withActualHookEvent(withTaskScmContext(contractResponseText, harness, scmContext), payload);
@@ -5023,6 +5111,7 @@ if (process.env.SYNKRO_HOOK_FORMAT === 'codex' && hookEventName === 'PermissionR
5023
5111
  // cli/auth/stub.ts
5024
5112
  var stub_exports = {};
5025
5113
  __export(stub_exports, {
5114
+ accessTokenExpired: () => accessTokenExpired,
5026
5115
  authedFetch: () => authedFetch,
5027
5116
  authenticate: () => authenticate,
5028
5117
  clearCredentials: () => clearCredentials,
@@ -5092,7 +5181,7 @@ function createCallbackServer() {
5092
5181
  "Access-Control-Allow-Headers": "Content-Type",
5093
5182
  "Vary": "Origin"
5094
5183
  };
5095
- return new Promise((resolve7, reject) => {
5184
+ return new Promise((resolve8, reject) => {
5096
5185
  const server = createServer((req, res) => {
5097
5186
  if (req.method === "OPTIONS") {
5098
5187
  const origin = req.headers.origin;
@@ -5181,7 +5270,7 @@ function createCallbackServer() {
5181
5270
  res.end(JSON.stringify({ ok: true }));
5182
5271
  setTimeout(() => {
5183
5272
  server.close();
5184
- resolve7(authData);
5273
+ resolve8(authData);
5185
5274
  }, 200);
5186
5275
  });
5187
5276
  req.on("error", (e) => {
@@ -5280,19 +5369,23 @@ function getAccessToken() {
5280
5369
  const creds = loadCredentials();
5281
5370
  return creds?.access_token || null;
5282
5371
  }
5283
- function isTokenExpired() {
5284
- const creds = loadCredentials();
5285
- if (!creds) return true;
5372
+ function accessTokenExpired(accessToken, now = Date.now()) {
5286
5373
  try {
5287
- const decoded = jwt.decode(creds.access_token);
5374
+ const decoded = jwt.decode(accessToken);
5288
5375
  if (!decoded?.exp) return true;
5289
5376
  const expiresAt = decoded.exp * 1e3;
5290
- const buffer = 5 * 60 * 1e3;
5291
- return Date.now() > expiresAt - buffer;
5377
+ const lifetimeMs = decoded.iat ? (decoded.exp - decoded.iat) * 1e3 : 0;
5378
+ const buffer = lifetimeMs > 0 ? Math.min(MAX_EXPIRY_BUFFER_MS, Math.floor(lifetimeMs / 5)) : MAX_EXPIRY_BUFFER_MS;
5379
+ return now > expiresAt - buffer;
5292
5380
  } catch {
5293
5381
  return true;
5294
5382
  }
5295
5383
  }
5384
+ function isTokenExpired() {
5385
+ const creds = loadCredentials();
5386
+ if (!creds?.access_token) return true;
5387
+ return accessTokenExpired(creds.access_token);
5388
+ }
5296
5389
  async function refreshToken() {
5297
5390
  const creds = loadCredentials();
5298
5391
  if (!creds?.refresh_token) return false;
@@ -5360,7 +5453,7 @@ async function getSecrets(userId, integrationId) {
5360
5453
  LANGSMITH_API_KEY: process.env.USER_LANGSMITH_KEY || ""
5361
5454
  };
5362
5455
  }
5363
- var PORT, RAW_WEB_AUTH_URL, SYNKRO_WEB_AUTH_URL, AUTH_FILE, RAW_API_URL, SYNKRO_API_URL, ERROR_HTML, refreshPromise;
5456
+ var PORT, RAW_WEB_AUTH_URL, SYNKRO_WEB_AUTH_URL, AUTH_FILE, RAW_API_URL, SYNKRO_API_URL, ERROR_HTML, MAX_EXPIRY_BUFFER_MS, refreshPromise;
5364
5457
  var init_stub = __esm({
5365
5458
  "cli/auth/stub.ts"() {
5366
5459
  "use strict";
@@ -5410,6 +5503,7 @@ var init_stub = __esm({
5410
5503
  </body>
5411
5504
  </html>
5412
5505
  `;
5506
+ MAX_EXPIRY_BUFFER_MS = 60 * 1e3;
5413
5507
  refreshPromise = null;
5414
5508
  }
5415
5509
  });
@@ -5512,7 +5606,7 @@ function detectSubdirRepos() {
5512
5606
  }
5513
5607
  }
5514
5608
  function ask(rl, question) {
5515
- return new Promise((resolve7) => rl.question(question, resolve7));
5609
+ return new Promise((resolve8) => rl.question(question, resolve8));
5516
5610
  }
5517
5611
  async function linkRepo(repo, linkedNames) {
5518
5612
  try {
@@ -5751,7 +5845,7 @@ async function runClaudeDesktopTap(opts = {}) {
5751
5845
  writeFileSync12(join13(sessionDir, "mcp_patch.py"), MCP_PATCH_PY, "utf-8");
5752
5846
  const runnerPath = join13(sessionDir, "run.sh");
5753
5847
  writeFileSync12(runnerPath, buildRunner(sessionDir), { mode: 493 });
5754
- await new Promise((resolve7) => {
5848
+ await new Promise((resolve8) => {
5755
5849
  const child = spawn3("bash", [runnerPath], {
5756
5850
  stdio: "inherit",
5757
5851
  env: { ...process.env, SYNKRO_CAPTURE_URL: CAPTURE_URL, SYNKRO_SCAN_URL: SCAN_URL, SYNKRO_SCAN_TURN_URL: SCAN_TURN_URL, SYNKRO_DLP_POLICY_URL: DLP_POLICY_URL, SYNKRO_TURN_VERDICTS_URL: TURN_VERDICTS_URL, SYNKRO_TURN_VERDICT_URL: TURN_VERDICT_URL, SYNKRO_MCP_EVENT_URL: MCP_EVENT_URL, SYNKRO_TAP_TOKEN: token, SYNKRO_TAP_TOKEN_FILE: JWT_PATH, SYNKRO_CD_BACKFILL: opts.backfill ? "1" : "" }
@@ -5767,7 +5861,7 @@ async function runClaudeDesktopTap(opts = {}) {
5767
5861
  child.on("exit", () => {
5768
5862
  process.off("SIGINT", forward);
5769
5863
  process.off("SIGTERM", forward);
5770
- resolve7();
5864
+ resolve8();
5771
5865
  });
5772
5866
  });
5773
5867
  }
@@ -7840,7 +7934,7 @@ var init_dockerInstall = __esm({
7840
7934
  HOST_PGLITE_PORT = parseInt(process.env.SYNKRO_HOST_PGLITE_PORT || "15433", 10);
7841
7935
  CONTAINER_NAME = resolveContainerName();
7842
7936
  defaultImageVersion = () => {
7843
- if (true) return "1.10.5";
7937
+ if (true) return "1.10.8";
7844
7938
  try {
7845
7939
  const pkg = JSON.parse(readFileSync17(new URL("../../package.json", import.meta.url), "utf8"));
7846
7940
  if (pkg.version) return pkg.version;
@@ -7872,7 +7966,7 @@ function captureClaudeSetupToken() {
7872
7966
  const bin = "script";
7873
7967
  const args2 = isMac ? ["-q", tmpFile, "claude", "setup-token"] : ["-qec", "claude setup-token", tmpFile];
7874
7968
  const OAUTH_HINT = 'The browser approval did not return a token. This usually means claude.ai rejected the OAuth request (its "Authorization failed \u2014 Unsupported media type" page), most often because a browser extension (Grammarly, ad/script blockers, AI-assistant toolbars) stripped the request headers, or you approved on the wrong Claude account. Fix: retry in a clean incognito window with extensions disabled, and approve on the Claude account you want the cloud workers to use.';
7875
- return new Promise((resolve7, reject) => {
7969
+ return new Promise((resolve8, reject) => {
7876
7970
  const proc = nodeSpawn(bin, args2, {
7877
7971
  stdio: "inherit",
7878
7972
  env: { ...process.env, FORCE_COLOR: "3", COLORTERM: "truecolor", TERM: "xterm-256color" }
@@ -7939,7 +8033,7 @@ function captureClaudeSetupToken() {
7939
8033
  reject(new Error(`Captured no setup token from claude setup-token output. ${reason}`));
7940
8034
  return;
7941
8035
  }
7942
- resolve7(token);
8036
+ resolve8(token);
7943
8037
  });
7944
8038
  });
7945
8039
  }
@@ -7965,13 +8059,13 @@ function findCodexBinary() {
7965
8059
  function runCodexLogin(codexBin, codexHome) {
7966
8060
  mkdirSync13(codexHome, { recursive: true, mode: 448 });
7967
8061
  writeFileSync15(join17(codexHome, "config.toml"), 'cli_auth_credentials_store = "file"\n', { mode: 384 });
7968
- return new Promise((resolve7, reject) => {
8062
+ return new Promise((resolve8, reject) => {
7969
8063
  const proc = nodeSpawn2(codexBin, ["login"], {
7970
8064
  stdio: "inherit",
7971
8065
  env: { ...process.env, CODEX_HOME: codexHome }
7972
8066
  });
7973
8067
  proc.on("error", (err) => reject(new Error(`failed to spawn codex login: ${err.message}`)));
7974
- proc.on("close", (code) => code === 0 ? resolve7() : reject(new Error(`codex login exited with code ${code}`)));
8068
+ proc.on("close", (code) => code === 0 ? resolve8() : reject(new Error(`codex login exited with code ${code}`)));
7975
8069
  });
7976
8070
  }
7977
8071
  async function setupCodexCloud(gatewayUrl, bearerToken, onStatus) {
@@ -8673,20 +8767,20 @@ async function promptAgentSelection(detected) {
8673
8767
  detected.forEach((a, i) => console.log(` ${i + 1}. ${a.name}`));
8674
8768
  console.log(` ${detected.length + 1}. Both / all (default)`);
8675
8769
  const rl = createInterface2({ input: process.stdin, output: process.stdout });
8676
- const ask3 = () => new Promise((resolve7) => {
8770
+ const ask3 = () => new Promise((resolve8) => {
8677
8771
  rl.question(`Pick [1-${detected.length + 1}] (default: all): `, (answer) => {
8678
8772
  const t = answer.trim().toLowerCase();
8679
8773
  if (t === "" || t === String(detected.length + 1) || t === "both" || t === "all") {
8680
8774
  rl.close();
8681
- return resolve7(detected);
8775
+ return resolve8(detected);
8682
8776
  }
8683
8777
  const n = parseInt(t, 10);
8684
8778
  if (Number.isInteger(n) && n >= 1 && n <= detected.length) {
8685
8779
  rl.close();
8686
- return resolve7([detected[n - 1]]);
8780
+ return resolve8([detected[n - 1]]);
8687
8781
  }
8688
8782
  console.log("Invalid choice. Try again.");
8689
- resolve7(ask3());
8783
+ resolve8(ask3());
8690
8784
  });
8691
8785
  });
8692
8786
  return ask3();
@@ -8709,12 +8803,12 @@ async function promptCursorApiKey(opts) {
8709
8803
  return;
8710
8804
  }
8711
8805
  const rl = createInterface2({ input: process.stdin, output: process.stdout });
8712
- const key = await new Promise((resolve7) => {
8806
+ const key = await new Promise((resolve8) => {
8713
8807
  rl.question(
8714
8808
  "Cursor grading needs a Cursor API key (cursor.com \u2192 Settings \u2192 API Keys).\nPaste it now, or press Enter to skip (Cursor workers stay idle until set): ",
8715
8809
  (answer) => {
8716
8810
  rl.close();
8717
- resolve7(answer.trim());
8811
+ resolve8(answer.trim());
8718
8812
  }
8719
8813
  );
8720
8814
  });
@@ -8729,7 +8823,7 @@ async function promptDeployLocation(current = "local") {
8729
8823
  if (!process.stdin.isTTY) return current;
8730
8824
  const other = current === "cloud" ? "local" : "cloud";
8731
8825
  const rl = createInterface2({ input: process.stdin, output: process.stdout });
8732
- return new Promise((resolve7) => {
8826
+ return new Promise((resolve8) => {
8733
8827
  rl.question(
8734
8828
  `Where should Synkro run?
8735
8829
  local \u2014 a grading container on this machine (Docker)
@@ -8738,7 +8832,7 @@ Each worker uses the account credentials you authorize. Choose [${current}] / ${
8738
8832
  (answer) => {
8739
8833
  rl.close();
8740
8834
  const a = answer.trim().toLowerCase();
8741
- resolve7(a === "cloud" ? "cloud" : a === "local" ? "local" : current);
8835
+ resolve8(a === "cloud" ? "cloud" : a === "local" ? "local" : current);
8742
8836
  }
8743
8837
  );
8744
8838
  });
@@ -8900,7 +8994,7 @@ function writeConfigEnv(opts) {
8900
8994
  `SYNKRO_CREDENTIALS_PATH=${shellQuoteSingle2(credsPath)}`,
8901
8995
  `SYNKRO_TIER=${shellQuoteSingle2(safeTier)}`,
8902
8996
  `SYNKRO_INFERENCE=${shellQuoteSingle2(safeInference)}`,
8903
- `SYNKRO_VERSION=${shellQuoteSingle2("1.10.5")}`
8997
+ `SYNKRO_VERSION=${shellQuoteSingle2("1.10.8")}`
8904
8998
  ];
8905
8999
  if (safeSynkroBin) lines.push(`SYNKRO_CLI_BIN=${shellQuoteSingle2(safeSynkroBin)}`);
8906
9000
  if (safeUserId) lines.push(`SYNKRO_USER_ID=${shellQuoteSingle2(safeUserId)}`);
@@ -9647,7 +9741,7 @@ async function installCommand(opts = {}) {
9647
9741
  await setTelemetryState({ enabled: true, remoteFlushEnabled: telemetryConsent });
9648
9742
  emit("install", {
9649
9743
  phase: "started",
9650
- cli_version_to: "1.10.5",
9744
+ cli_version_to: "1.10.8",
9651
9745
  agents_detected: agents.map((a) => a.kind),
9652
9746
  with_github: false,
9653
9747
  with_local_cc: false,
@@ -10715,8 +10809,8 @@ function ensureReachabilityGitHook() {
10715
10809
  }
10716
10810
  return "updated";
10717
10811
  }
10718
- const sep3 = cur.endsWith("\n") ? "" : "\n";
10719
- writeFileSync17(hookPath, cur + sep3 + "\n" + block + "\n");
10812
+ const sep4 = cur.endsWith("\n") ? "" : "\n";
10813
+ writeFileSync17(hookPath, cur + sep4 + "\n" + block + "\n");
10720
10814
  try {
10721
10815
  chmodSync5(hookPath, 493);
10722
10816
  } catch {
@@ -11936,10 +12030,10 @@ function confirmPurge() {
11936
12030
  return Promise.resolve(false);
11937
12031
  }
11938
12032
  const rl = createInterface3({ input: process.stdin, output: process.stdout });
11939
- return new Promise((resolve7) => {
12033
+ return new Promise((resolve8) => {
11940
12034
  rl.question(" Type 'yes' to wipe everything (anything else cancels): ", (answer) => {
11941
12035
  rl.close();
11942
- resolve7(answer.trim().toLowerCase() === "yes");
12036
+ resolve8(answer.trim().toLowerCase() === "yes");
11943
12037
  });
11944
12038
  });
11945
12039
  }
@@ -12192,7 +12286,7 @@ async function submitToChannel(role, payload, opts = {}) {
12192
12286
  const port = opts.port ?? CHANNEL_PORT;
12193
12287
  const startedAt = Date.now();
12194
12288
  try {
12195
- const result = await new Promise((resolve7, reject) => {
12289
+ const result = await new Promise((resolve8, reject) => {
12196
12290
  const req = httpRequest({
12197
12291
  host: CHANNEL_HOST,
12198
12292
  port,
@@ -12218,7 +12312,7 @@ async function submitToChannel(role, payload, opts = {}) {
12218
12312
  reject(new LocalCCError(parsed.error));
12219
12313
  return;
12220
12314
  }
12221
- resolve7(String(parsed.result ?? ""));
12315
+ resolve8(String(parsed.result ?? ""));
12222
12316
  } catch (err) {
12223
12317
  reject(new LocalCCError(`malformed channel response: ${text.slice(0, 200)}`, err));
12224
12318
  }
@@ -12244,14 +12338,14 @@ async function submitToChannel(role, payload, opts = {}) {
12244
12338
  }
12245
12339
  }
12246
12340
  function isChannelAvailable(port = CHANNEL_PORT, timeoutMs = 500) {
12247
- return new Promise((resolve7) => {
12341
+ return new Promise((resolve8) => {
12248
12342
  const sock = connect(port, CHANNEL_HOST);
12249
12343
  const done = (ok) => {
12250
12344
  try {
12251
12345
  sock.destroy();
12252
12346
  } catch {
12253
12347
  }
12254
- resolve7(ok);
12348
+ resolve8(ok);
12255
12349
  };
12256
12350
  sock.once("connect", () => done(true));
12257
12351
  sock.once("error", () => done(false));
@@ -12283,10 +12377,10 @@ __export(grade_exports, {
12283
12377
  gradeCommand: () => gradeCommand
12284
12378
  });
12285
12379
  async function readStdin() {
12286
- return new Promise((resolve7, reject) => {
12380
+ return new Promise((resolve8, reject) => {
12287
12381
  const chunks = [];
12288
12382
  process.stdin.on("data", (c) => chunks.push(c));
12289
- process.stdin.on("end", () => resolve7(Buffer.concat(chunks).toString("utf-8")));
12383
+ process.stdin.on("end", () => resolve8(Buffer.concat(chunks).toString("utf-8")));
12290
12384
  process.stdin.on("error", reject);
12291
12385
  });
12292
12386
  }
@@ -12540,7 +12634,7 @@ function spawnClaudeJudge(file, claudeToken, promptHeader) {
12540
12634
  Diff:
12541
12635
  ${hunks}`;
12542
12636
  const fullPrompt = promptHeader + userMessage;
12543
- return new Promise((resolve7) => {
12637
+ return new Promise((resolve8) => {
12544
12638
  const t0 = Date.now();
12545
12639
  const proc = spawn6(
12546
12640
  "claude",
@@ -12568,7 +12662,7 @@ ${hunks}`;
12568
12662
  const latencyMs = Date.now() - t0;
12569
12663
  if (code !== 0) {
12570
12664
  console.warn(` claude exited ${code}: ${(stderr || stdout).slice(0, 500)}`);
12571
- resolve7({ findings: [], latencyMs });
12665
+ resolve8({ findings: [], latencyMs });
12572
12666
  return;
12573
12667
  }
12574
12668
  try {
@@ -12587,10 +12681,10 @@ ${hunks}`;
12587
12681
  description: f.description,
12588
12682
  fix: f.fix
12589
12683
  }));
12590
- resolve7({ findings, latencyMs });
12684
+ resolve8({ findings, latencyMs });
12591
12685
  } catch (parseErr) {
12592
12686
  console.warn(` failed to parse claude response: ${stdout.slice(0, 300)}`);
12593
- resolve7({ findings: [], latencyMs });
12687
+ resolve8({ findings: [], latencyMs });
12594
12688
  }
12595
12689
  });
12596
12690
  });
@@ -12639,7 +12733,7 @@ ${JSON.stringify(findings, null, 2)}
12639
12733
  `;
12640
12734
  }
12641
12735
  function spawnOpusConsolidator(findings, claudeToken) {
12642
- return new Promise((resolve7) => {
12736
+ return new Promise((resolve8) => {
12643
12737
  const prompt = buildConsolidationPrompt(findings);
12644
12738
  const proc = spawn6(
12645
12739
  "claude",
@@ -12666,7 +12760,7 @@ function spawnOpusConsolidator(findings, claudeToken) {
12666
12760
  proc.on("close", (code) => {
12667
12761
  if (code !== 0) {
12668
12762
  console.warn(` opus consolidation exited ${code}: ${(stderr || stdout).slice(0, 300)}`);
12669
- resolve7(fallbackReview(findings));
12763
+ resolve8(fallbackReview(findings));
12670
12764
  return;
12671
12765
  }
12672
12766
  try {
@@ -12687,10 +12781,10 @@ function spawnOpusConsolidator(findings, claudeToken) {
12687
12781
  const order = ["low", "medium", "high", "critical"];
12688
12782
  return order.indexOf(f.severity) > order.indexOf(max) ? f.severity : max;
12689
12783
  }, "low");
12690
- resolve7({ summary: review.summary || "", comments, severity: maxSeverity });
12784
+ resolve8({ summary: review.summary || "", comments, severity: maxSeverity });
12691
12785
  } catch {
12692
12786
  console.warn(` failed to parse opus response, using fallback`);
12693
- resolve7(fallbackReview(findings));
12787
+ resolve8(fallbackReview(findings));
12694
12788
  }
12695
12789
  });
12696
12790
  });
@@ -13173,14 +13267,14 @@ function ensureRunning(opts = {}) {
13173
13267
  return startTask(opts);
13174
13268
  }
13175
13269
  function probePort(host, port, timeoutMs = 500) {
13176
- return new Promise((resolve7) => {
13270
+ return new Promise((resolve8) => {
13177
13271
  const sock = connect2(port, host);
13178
13272
  const done = (ok) => {
13179
13273
  try {
13180
13274
  sock.destroy();
13181
13275
  } catch {
13182
13276
  }
13183
- resolve7(ok);
13277
+ resolve8(ok);
13184
13278
  };
13185
13279
  sock.once("connect", () => done(true));
13186
13280
  sock.once("error", () => done(false));
@@ -13730,7 +13824,7 @@ function cmdLogs(rest) {
13730
13824
  if (!raw) console.log(" " + colorize("(use --raw / -r to see full payloads, --live / -f to follow)", 90));
13731
13825
  return;
13732
13826
  }
13733
- return new Promise((resolve7) => {
13827
+ return new Promise((resolve8) => {
13734
13828
  console.log(" " + colorize("\u2014 following new turns (Ctrl-C to exit) \u2014", 90));
13735
13829
  const stop = followTurns((t) => {
13736
13830
  console.log(" " + formatTurn(t, raw));
@@ -13738,7 +13832,7 @@ function cmdLogs(rest) {
13738
13832
  const onSigint = () => {
13739
13833
  stop();
13740
13834
  process.removeListener("SIGINT", onSigint);
13741
- resolve7();
13835
+ resolve8();
13742
13836
  };
13743
13837
  process.on("SIGINT", onSigint);
13744
13838
  });
@@ -14027,9 +14121,9 @@ function parseSession(file, seenStableIds) {
14027
14121
  }
14028
14122
  function ask2(q) {
14029
14123
  const rl = createInterface4({ input: process.stdin, output: process.stdout });
14030
- return new Promise((resolve7) => rl.question(q, (a) => {
14124
+ return new Promise((resolve8) => rl.question(q, (a) => {
14031
14125
  rl.close();
14032
- resolve7(/^y(es)?$/i.test(a.trim()));
14126
+ resolve8(/^y(es)?$/i.test(a.trim()));
14033
14127
  }));
14034
14128
  }
14035
14129
  async function importCommand() {
@@ -14429,6 +14523,16 @@ import { homedir as homedir31 } from "os";
14429
14523
  function markerPath(taskId) {
14430
14524
  return join31(WORKSPACE_CHOICE_DIR, `${taskId}.stay`);
14431
14525
  }
14526
+ function markerNamesForTask(taskId) {
14527
+ try {
14528
+ return existsSync33(WORKSPACE_CHOICE_DIR) ? readdirSync8(WORKSPACE_CHOICE_DIR).filter((name) => name === `${taskId}.stay` || name.startsWith(`${taskId}.`) && name.endsWith(".stay")) : [];
14529
+ } catch {
14530
+ return [];
14531
+ }
14532
+ }
14533
+ function taskIdFromMarker(name) {
14534
+ return (name.match(/^(task_[a-z0-9]{8})(?:\.[a-f0-9]{16})?\.stay$/i) || [])[1] || "";
14535
+ }
14432
14536
  function usage() {
14433
14537
  console.log(`synkro workspace \u2014 record where a task's work happens
14434
14538
 
@@ -14437,8 +14541,8 @@ Usage:
14437
14541
  synkro workspace clear <taskId> forget the choice (the gate asks again)
14438
14542
  synkro workspace status [taskId] show recorded choices
14439
14543
 
14440
- The gate asks once per task. "stay" is remembered until the choice is cleared or
14441
- the active task changes.`);
14544
+ The gate asks once per task session. "stay" is remembered in that chat until the
14545
+ choice is cleared or the active task changes.`);
14442
14546
  }
14443
14547
  async function workspaceCommand(args2) {
14444
14548
  const sub = String(args2[0] || "").trim();
@@ -14455,7 +14559,7 @@ async function workspaceCommand(args2) {
14455
14559
  recorded = [];
14456
14560
  }
14457
14561
  if (taskId) {
14458
- const on = recorded.includes(`${taskId}.stay`);
14562
+ const on = markerNamesForTask(taskId).length > 0;
14459
14563
  console.log(`${taskId}: ${on ? "stay recorded" : "no choice recorded"}`);
14460
14564
  return;
14461
14565
  }
@@ -14464,7 +14568,7 @@ async function workspaceCommand(args2) {
14464
14568
  return;
14465
14569
  }
14466
14570
  console.log("Staying in the current checkout for:");
14467
- for (const name of recorded.sort()) console.log(` ${name.replace(/\.stay$/, "")}`);
14571
+ for (const id of [...new Set(recorded.map(taskIdFromMarker).filter(Boolean))].sort()) console.log(` ${id}`);
14468
14572
  return;
14469
14573
  }
14470
14574
  if (sub !== "stay" && sub !== "clear") {
@@ -14479,9 +14583,11 @@ async function workspaceCommand(args2) {
14479
14583
  return;
14480
14584
  }
14481
14585
  if (sub === "clear") {
14482
- try {
14483
- rmSync5(markerPath(taskId), { force: true });
14484
- } catch {
14586
+ for (const name of markerNamesForTask(taskId)) {
14587
+ try {
14588
+ rmSync5(join31(WORKSPACE_CHOICE_DIR, name), { force: true });
14589
+ } catch {
14590
+ }
14485
14591
  }
14486
14592
  console.log(`Cleared the workspace choice for ${taskId}.`);
14487
14593
  return;
@@ -14544,6 +14650,7 @@ function buildSpawnAgent(opts) {
14544
14650
  // and its own history scrolls — without it only the last screen is
14545
14651
  // reachable once the session is nested inside the layout.
14546
14652
  ["tmux", "set-option", "-t", session, "mouse", "on"],
14653
+ ...buildClipboardBindings(session),
14547
14654
  ["tmux", "set-option", "-t", session, "-q", "@synkro_harness", opts.harness],
14548
14655
  ["tmux", "set-option", "-t", session, "-q", "@synkro_space", opts.space],
14549
14656
  ["tmux", "set-option", "-t", session, "-q", "@synkro_backend", opts.backend],
@@ -14552,6 +14659,13 @@ function buildSpawnAgent(opts) {
14552
14659
  ["tmux", "set-option", "-t", session, "remain-on-exit", "on"]
14553
14660
  ];
14554
14661
  }
14662
+ function buildClipboardBindings(session) {
14663
+ return [
14664
+ ["tmux", "set-option", "-t", session, "set-clipboard", "on"],
14665
+ ["tmux", "bind-key", "-T", "copy-mode", "MouseDragEnd1Pane", "send-keys", "-X", "copy-pipe-and-cancel", SYSTEM_CLIPBOARD],
14666
+ ["tmux", "bind-key", "-T", "copy-mode-vi", "MouseDragEnd1Pane", "send-keys", "-X", "copy-pipe-and-cancel", SYSTEM_CLIPBOARD]
14667
+ ];
14668
+ }
14555
14669
  function buildListAgents() {
14556
14670
  return ["tmux", "list-sessions", "-F", ["#{session_name}", "#{?pane_dead,dead,alive}", "#{@synkro_harness}", "#{@synkro_space}", "#{@synkro_backend}", "#{@synkro_pueue}", "#{pane_current_path}"].join(FIELD_SEP)];
14557
14671
  }
@@ -14595,6 +14709,9 @@ function buildSendText(session, text) {
14595
14709
  function buildEnableMouse(session) {
14596
14710
  return ["tmux", "set-option", "-t", session, "mouse", "on"];
14597
14711
  }
14712
+ function buildKeepPaneAfterExit(pane) {
14713
+ return ["tmux", "set-option", "-p", "-t", pane, "remain-on-exit", "on"];
14714
+ }
14598
14715
  function buildInterrupt(session) {
14599
14716
  return ["tmux", "send-keys", "-t", session, "Escape"];
14600
14717
  }
@@ -14661,7 +14778,7 @@ function parseWorktrees(porcelain) {
14661
14778
  }
14662
14779
  return rows;
14663
14780
  }
14664
- var execFileAsync, AGENT_PREFIX, UI_SESSION, CONTAINER_USER, FIELD_SEP;
14781
+ var execFileAsync, AGENT_PREFIX, UI_SESSION, CONTAINER_USER, SYSTEM_CLIPBOARD, FIELD_SEP;
14665
14782
  var init_tmux = __esm({
14666
14783
  "cli/ui/tmux.ts"() {
14667
14784
  "use strict";
@@ -14669,6 +14786,7 @@ var init_tmux = __esm({
14669
14786
  AGENT_PREFIX = "synkro-agent-";
14670
14787
  UI_SESSION = "synkro-ui";
14671
14788
  CONTAINER_USER = "synkro";
14789
+ SYSTEM_CLIPBOARD = "if command -v pbcopy >/dev/null 2>&1; then pbcopy; elif command -v wl-copy >/dev/null 2>&1; then wl-copy; elif command -v xclip >/dev/null 2>&1; then xclip -selection clipboard; else cat >/dev/null; fi";
14672
14790
  FIELD_SEP = "|";
14673
14791
  }
14674
14792
  });
@@ -14801,6 +14919,10 @@ function visibleAgents(all, space, filter, grouped) {
14801
14919
  if (grouped || filter === "all" || !space) return all;
14802
14920
  return all.filter((agent) => agent.repo === space.path);
14803
14921
  }
14922
+ function nextLiveAgent(all, preferredRepo = "") {
14923
+ const live = all.filter((agent) => agent.status !== "offline" && agent.status !== "done");
14924
+ return live.find((agent) => Boolean(preferredRepo) && agent.repo === preferredRepo) || live[0];
14925
+ }
14804
14926
  async function discoverAgents(runner, backend, memory) {
14805
14927
  const snapshot = await run(runner, buildAgentSnapshot());
14806
14928
  const { list, captures } = parseAgentSnapshot(snapshot.ok ? snapshot.stdout : "");
@@ -15039,6 +15161,10 @@ async function styleOuterSession(bootPath, repoCwd, sidebarWidth) {
15039
15161
  // focus it, click rows/chips. Without this the fixed split reads as
15040
15162
  // "blocked in".
15041
15163
  ["set-option", "-t", UI_SESSION, "mouse", "on"],
15164
+ // Scrollback deep enough to hold a real working session. tmux defaults to
15165
+ // 2000 lines, which a long harness session blows through — the start of the
15166
+ // conversation would silently fall off the top with no way back to it.
15167
+ ["set-option", "-t", UI_SESSION, "history-limit", "50000"],
15042
15168
  ["set-option", "-t", UI_SESSION, "status-position", "top"],
15043
15169
  ["set-option", "-t", UI_SESSION, "status-style", "bg=colour233,fg=colour245"],
15044
15170
  ["set-option", "-t", UI_SESSION, "status-format[0]", chipStrip(sidebarWidth)],
@@ -15062,8 +15188,10 @@ async function styleOuterSession(bootPath, repoCwd, sidebarWidth) {
15062
15188
  ["bind-key", "-n", "M-s", "select-pane", "-L"]
15063
15189
  ];
15064
15190
  for (const argv of style) await run(HOST, ["tmux", ...argv]);
15191
+ for (const argv of buildClipboardBindings(UI_SESSION)) await run(HOST, argv);
15065
15192
  }
15066
15193
  async function buildTab(bootPath, repoCwd, spec) {
15194
+ await run(HOST, ["tmux", "set-option", "-g", "history-limit", "50000"]);
15067
15195
  let windowTarget;
15068
15196
  if (!await uiSessionExists()) {
15069
15197
  const cols = String(Number(process.stdout.columns || 0) || 220);
@@ -15103,6 +15231,7 @@ async function buildTab(bootPath, repoCwd, spec) {
15103
15231
  const sidebarPane = split.stdout.trim();
15104
15232
  const panes = await run(HOST, ["tmux", "list-panes", "-t", windowTarget, "-F", "#{pane_id}"]);
15105
15233
  const centerPane = panes.stdout.split("\n").map((line) => line.trim()).filter(Boolean).find((id) => id !== sidebarPane) || "";
15234
+ await run(HOST, buildKeepPaneAfterExit(centerPane));
15106
15235
  if (spec.agentSession) {
15107
15236
  await run(HOST, ["tmux", "set-option", "-t", windowTarget, "-w", "-q", "@synkro_agent", spec.agentSession]);
15108
15237
  }
@@ -15132,11 +15261,29 @@ async function uiSessionExists() {
15132
15261
  const result = await run(HOST, ["tmux", "has-session", "-t", UI_SESSION]);
15133
15262
  return result.ok;
15134
15263
  }
15264
+ function parseCollapsedClients(listOutput) {
15265
+ return String(listOutput || "").split("\n").map((line) => line.trim().split("|")).filter(([name, width, height]) => Boolean(name) && Number.isFinite(Number(width)) && Number.isFinite(Number(height)) && (Number(width) < 20 || Number(height) < 5)).map(([name]) => name);
15266
+ }
15267
+ async function pruneCollapsedClients() {
15268
+ const listed = await run(HOST, [
15269
+ "tmux",
15270
+ "list-clients",
15271
+ "-t",
15272
+ UI_SESSION,
15273
+ "-F",
15274
+ "#{client_name}|#{client_width}|#{client_height}"
15275
+ ]);
15276
+ if (!listed.ok) return 0;
15277
+ const collapsed = parseCollapsedClients(listed.stdout);
15278
+ for (const name of collapsed) await run(HOST, ["tmux", "detach-client", "-t", name]);
15279
+ return collapsed.length;
15280
+ }
15135
15281
  async function launchUi(bootPath, repoCwd) {
15136
15282
  rememberSpace(repoCwd);
15137
15283
  if (!await uiSessionExists()) {
15138
15284
  await buildTab(bootPath, repoCwd, { cwd: repoCwd, center: makeTerminalCommand(bootPath), focus: "sidebar" });
15139
15285
  }
15286
+ await pruneCollapsedClients();
15140
15287
  return runInherit(process.env.TMUX ? ["tmux", "switch-client", "-t", UI_SESSION] : ["tmux", "attach-session", "-t", UI_SESSION]);
15141
15288
  }
15142
15289
  var HOST, TAB_GLYPHS;
@@ -15464,6 +15611,9 @@ async function detectContainerBackend() {
15464
15611
  }
15465
15612
  return { runner: { kind: "host" }, backend: "host", note: "runtime: host (container unavailable)" };
15466
15613
  }
15614
+ function harnessCommand(request) {
15615
+ return request.resume ? resumeCommand(request.harness) : HARNESS_COMMANDS[request.harness] || "claude";
15616
+ }
15467
15617
  async function provisionContainerWorkspace(runner, slug) {
15468
15618
  const dir = CONTAINER_WORK + "/ui-" + slug;
15469
15619
  await run(runner, [
@@ -15481,7 +15631,7 @@ async function spawnAgent(info, request) {
15481
15631
  if (request.backend === "container") {
15482
15632
  cwd = request.cwd.startsWith(CONTAINER_WORK) ? request.cwd : await provisionContainerWorkspace(runner, slug);
15483
15633
  }
15484
- const command = request.resume ? resumeCommand(request.harness) : HARNESS_COMMANDS[request.harness] || "claude";
15634
+ const command = harnessCommand(request);
15485
15635
  for (const argv of buildSpawnAgent({ name: slug, cwd, command, harness: request.harness, space: cwd, backend: request.backend })) {
15486
15636
  const result = await run(runner, argv);
15487
15637
  if (!result.ok && argv[1] === "new-session") {
@@ -15772,6 +15922,24 @@ async function runSidebar() {
15772
15922
  const agent = state.agents[state.agentIndex];
15773
15923
  if (agent) await showAgent(agent);
15774
15924
  }
15925
+ async function showWorkspaceTerminal(cwd) {
15926
+ if (!centerPane || !cwd) return;
15927
+ const command = makeTerminalCommand(bootPath);
15928
+ const argv = ["tmux", "respawn-pane", "-k", "-t", centerPane, "-c", cwd];
15929
+ if (command) argv.push(command);
15930
+ const respawned = await run(host, argv);
15931
+ if (!respawned.ok) {
15932
+ state.message = "could not restore workspace terminal";
15933
+ return;
15934
+ }
15935
+ const spaceName = cwd.split("/").filter(Boolean).pop() || "space";
15936
+ await run(host, ["tmux", "set-option", "-t", centerPane, "-w", "-u", "@synkro_agent"]);
15937
+ await run(host, ["tmux", "set-option", "-t", centerPane, "-w", "-q", "@synkro_cwd", cwd]);
15938
+ await run(host, ["tmux", "set-option", "-t", centerPane, "-w", "-q", "@synkro_kind", "terminal"]);
15939
+ await run(host, ["tmux", "rename-window", "-t", centerPane, tabTitle("terminal", spaceName)]);
15940
+ state.viewing = "";
15941
+ await snapshotTabs();
15942
+ }
15775
15943
  async function showAgent(agent) {
15776
15944
  if (!centerPane) return;
15777
15945
  if (agent.status === "offline") {
@@ -15790,6 +15958,9 @@ async function runSidebar() {
15790
15958
  return;
15791
15959
  }
15792
15960
  await run(runnerFor(agent.backend), buildEnableMouse(agent.session));
15961
+ for (const argv of buildClipboardBindings(agent.session)) {
15962
+ await run(runnerFor(agent.backend), argv);
15963
+ }
15793
15964
  const command = buildCenterAttachCommand(runnerFor(agent.backend), agent.session);
15794
15965
  await run(host, ["tmux", "respawn-pane", "-k", "-t", centerPane, command]);
15795
15966
  const where = agent.space ? agent.space.split("/").filter(Boolean).pop() || "" : "";
@@ -15802,7 +15973,15 @@ async function runSidebar() {
15802
15973
  async function confirmKillAgent() {
15803
15974
  const agent = state.agents[state.agentIndex];
15804
15975
  if (!agent) return;
15976
+ const wasViewing = state.viewing === agent.session;
15805
15977
  await openDialog("kill", [agent.backend, agent.session]);
15978
+ await refresh();
15979
+ if (allAgents.some((candidate) => candidate.session === agent.session && candidate.status !== "offline")) return;
15980
+ state.message = "closed " + agent.name;
15981
+ if (!wasViewing) return;
15982
+ await showWorkspaceTerminal(agent.space || selectedSpace()?.path || repoCwd);
15983
+ const successor = nextLiveAgent(allAgents, agent.repo || "");
15984
+ if (successor) await showAgent(successor);
15806
15985
  }
15807
15986
  async function consent(action) {
15808
15987
  const agent = state.agents[state.agentIndex];
@@ -15825,6 +16004,7 @@ async function runSidebar() {
15825
16004
  const remembered = space ? lastAgentFor(space.path) : "";
15826
16005
  const agent = allAgents.find((row2) => row2.session === remembered && row2.status !== "offline");
15827
16006
  if (agent) await showAgent(agent);
16007
+ else if (space) await showWorkspaceTerminal(space.path);
15828
16008
  } else if (target.kind === "agent") {
15829
16009
  state.section = "agents";
15830
16010
  state.agentIndex = target.index;
@@ -16026,6 +16206,9 @@ var init_repos = __esm({
16026
16206
 
16027
16207
  // cli/ui/tabs.ts
16028
16208
  import { execSync as execSync7 } from "child_process";
16209
+ function isLegacyCodexRenderer(command) {
16210
+ return /(?:^|\s)ui\s+--run\s+codex(?:\s|$)/.test(String(command || "").trim());
16211
+ }
16029
16212
  function repoRoot() {
16030
16213
  try {
16031
16214
  return execSync7("git rev-parse --show-toplevel", { encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] }).trim() || process.cwd();
@@ -16046,16 +16229,6 @@ async function createTab(bootPath, kind, spacePath) {
16046
16229
  });
16047
16230
  return;
16048
16231
  }
16049
- if (kind === "cursor-synkro") {
16050
- await buildTab(bootPath, repo, {
16051
- cwd: spacePath,
16052
- center: ["node", bootPath, "ui", "--run", "cursor", spacePath].map(shellQuote3).join(" "),
16053
- title: tabTitle("cursor", space),
16054
- kind: "cursor-synkro",
16055
- focus: "center"
16056
- });
16057
- return;
16058
- }
16059
16232
  if (kind === "settings") {
16060
16233
  await buildTab(bootPath, repo, {
16061
16234
  cwd: spacePath,
@@ -16066,12 +16239,13 @@ async function createTab(bootPath, kind, spacePath) {
16066
16239
  });
16067
16240
  return;
16068
16241
  }
16242
+ if (!["claude", "codex", "cursor"].includes(kind)) return;
16069
16243
  const info = await detectContainerBackend();
16070
16244
  const spaceName = space;
16071
16245
  const stamp = String(process.pid % 1e4);
16072
- const harness = ["claude", "codex", "cursor"].includes(kind) ? kind : "claude";
16246
+ const harness = kind;
16073
16247
  const reachable = info.backend === "container" && (await run(info.runner, ["test", "-d", spacePath])).ok;
16074
- const backend = reachable ? "container" : "host";
16248
+ const backend = harness === "codex" ? "host" : reachable ? "container" : "host";
16075
16249
  const spawned = await spawnAgent(info, {
16076
16250
  name: spaceName + "-" + harness + "-" + stamp,
16077
16251
  harness,
@@ -16102,16 +16276,34 @@ async function openAgentTab(bootPath, session) {
16102
16276
  ]);
16103
16277
  const [harness, space, backend] = (meta.stdout.trim() || "||").split("|");
16104
16278
  const containerHosted = backend === "container";
16105
- const runner = containerHosted ? info.runner : HOST2;
16106
- const alive = (await run(runner, ["tmux", "has-session", "-t", session])).ok;
16107
- if (!alive) return;
16279
+ let runner = containerHosted ? info.runner : HOST2;
16280
+ let activeSession = session;
16108
16281
  const spaceName = (space || "").split("/").filter(Boolean).pop() || "space";
16282
+ if (harness === "codex") {
16283
+ const command = await run(HOST2, ["tmux", "display-message", "-p", "-t", session, "#{pane_start_command}"]);
16284
+ if (command.ok && isLegacyCodexRenderer(command.stdout)) {
16285
+ await run(HOST2, buildKillSession(session));
16286
+ const restored = await spawnAgent(info, {
16287
+ name: session.replace(/^synkro-agent-/, ""),
16288
+ harness: "codex",
16289
+ spaceName,
16290
+ cwd: space || repoRoot(),
16291
+ backend: "host",
16292
+ resume: true
16293
+ });
16294
+ if (!restored.ok) return;
16295
+ activeSession = restored.session;
16296
+ runner = HOST2;
16297
+ }
16298
+ }
16299
+ const alive = (await run(runner, ["tmux", "has-session", "-t", activeSession])).ok;
16300
+ if (!alive) return;
16109
16301
  await buildTab(bootPath, repoRoot(), {
16110
16302
  cwd: space || repoRoot(),
16111
- center: buildCenterAttachCommand(runner, session),
16303
+ center: buildCenterAttachCommand(runner, activeSession),
16112
16304
  title: tabTitle(harness || "claude", spaceName),
16113
16305
  kind: harness || "claude",
16114
- agentSession: session,
16306
+ agentSession: activeSession,
16115
16307
  focus: "center"
16116
16308
  });
16117
16309
  }
@@ -16127,11 +16319,23 @@ async function restoreTabs(bootPath, repoCwd) {
16127
16319
  const space = tab.cwd.split("/").filter(Boolean).pop() || "space";
16128
16320
  const containerHosted = info.backend === "container" && (await run(info.runner, ["test", "-d", tab.cwd])).ok;
16129
16321
  const harness = ["claude", "codex", "cursor"].includes(tab.kind) ? tab.kind : "";
16130
- if (tab.agentSession && alive.has(tab.agentSession)) {
16131
- await run(containerHosted ? info.runner : HOST2, buildEnableMouse(tab.agentSession));
16322
+ const sessionRunner = harness === "codex" ? HOST2 : containerHosted ? info.runner : HOST2;
16323
+ let sessionAlive = Boolean(tab.agentSession && alive.has(tab.agentSession));
16324
+ if (sessionAlive && harness === "codex") {
16325
+ const command = await run(HOST2, ["tmux", "display-message", "-p", "-t", tab.agentSession, "#{pane_start_command}"]);
16326
+ if (command.ok && isLegacyCodexRenderer(command.stdout)) {
16327
+ await run(HOST2, buildKillSession(tab.agentSession));
16328
+ sessionAlive = false;
16329
+ }
16330
+ }
16331
+ if (tab.agentSession && sessionAlive) {
16332
+ await run(sessionRunner, buildEnableMouse(tab.agentSession));
16333
+ for (const argv of buildClipboardBindings(tab.agentSession)) {
16334
+ await run(sessionRunner, argv);
16335
+ }
16132
16336
  await buildTab(bootPath, repoCwd, {
16133
16337
  cwd: tab.cwd,
16134
- center: buildCenterAttachCommand(containerHosted ? info.runner : HOST2, tab.agentSession),
16338
+ center: buildCenterAttachCommand(sessionRunner, tab.agentSession),
16135
16339
  title: tab.title,
16136
16340
  kind: tab.kind,
16137
16341
  agentSession: tab.agentSession,
@@ -16143,12 +16347,13 @@ async function restoreTabs(bootPath, repoCwd) {
16143
16347
  harness,
16144
16348
  spaceName: space,
16145
16349
  cwd: tab.cwd,
16146
- backend: containerHosted ? "container" : "host",
16350
+ backend: harness === "codex" ? "host" : containerHosted ? "container" : "host",
16147
16351
  resume: true
16148
16352
  });
16353
+ const restoredInContainer = harness !== "codex" && containerHosted;
16149
16354
  await buildTab(bootPath, repoCwd, spawned.ok ? {
16150
16355
  cwd: tab.cwd,
16151
- center: buildCenterAttachCommand(containerHosted ? info.runner : HOST2, spawned.session),
16356
+ center: buildCenterAttachCommand(restoredInContainer ? info.runner : HOST2, spawned.session),
16152
16357
  title: tabTitle(harness, space),
16153
16358
  kind: harness,
16154
16359
  agentSession: spawned.session,
@@ -16241,7 +16446,7 @@ async function pick(opts) {
16241
16446
  const perItem = opts.choices.some((choice) => choice.detail) ? 2 : 1;
16242
16447
  const visibleItems = Math.max(1, Math.floor(Math.max(2, height - 5) / perItem));
16243
16448
  const firstRow = 4;
16244
- return new Promise((resolve7) => {
16449
+ return new Promise((resolve8) => {
16245
16450
  const view = () => opts.choices.filter((choice) => matches(choice, filter));
16246
16451
  const draw = () => {
16247
16452
  const shown = view();
@@ -16289,7 +16494,7 @@ async function pick(opts) {
16289
16494
  const hit = shown.findIndex((_, index) => y >= rowOfItem(index) && y < rowOfItem(index) + perItem);
16290
16495
  if (hit >= 0) {
16291
16496
  process.stdin.off("data", onData);
16292
- resolve7(shown[hit].value);
16497
+ resolve8(shown[hit].value);
16293
16498
  return;
16294
16499
  }
16295
16500
  }
@@ -16297,13 +16502,13 @@ async function pick(opts) {
16297
16502
  if (!sawMouse) {
16298
16503
  if (input === KEY_ESC && !input.includes("[<") || input === KEY_CTRL_C2) {
16299
16504
  process.stdin.off("data", onData);
16300
- resolve7(null);
16505
+ resolve8(null);
16301
16506
  return;
16302
16507
  }
16303
16508
  if (input === "\r") {
16304
16509
  if (shown.length === 0) return;
16305
16510
  process.stdin.off("data", onData);
16306
- resolve7(shown[selected].value);
16511
+ resolve8(shown[selected].value);
16307
16512
  return;
16308
16513
  }
16309
16514
  const down = input === CSI2 + "B" || opts.menu && (input === "j" || input === CSI2 + "C");
@@ -16327,7 +16532,7 @@ async function readLine(opts) {
16327
16532
  let value = "";
16328
16533
  let error = "";
16329
16534
  const width = Math.max(20, Number(process.stdout.columns || 80));
16330
- return new Promise((resolve7) => {
16535
+ return new Promise((resolve8) => {
16331
16536
  const draw = () => {
16332
16537
  const lines = [
16333
16538
  " " + S.title + opts.title + S.reset,
@@ -16346,7 +16551,7 @@ async function readLine(opts) {
16346
16551
  const input = chunk.toString("utf8");
16347
16552
  if (input === KEY_ESC || input === KEY_CTRL_C2) {
16348
16553
  process.stdin.off("data", onData);
16349
- resolve7(null);
16554
+ resolve8(null);
16350
16555
  return;
16351
16556
  }
16352
16557
  if (input === "\r") {
@@ -16358,7 +16563,7 @@ async function readLine(opts) {
16358
16563
  return;
16359
16564
  }
16360
16565
  process.stdin.off("data", onData);
16361
- resolve7(value.trim());
16566
+ resolve8(value.trim());
16362
16567
  })();
16363
16568
  return;
16364
16569
  }
@@ -16625,9 +16830,6 @@ async function runDialog(kind, repoCwd, argA = "", argB = "") {
16625
16830
  label: (TAB_GLYPHS[harness] || "") + " " + (HARNESS_LABELS[harness] || harness)
16626
16831
  }))
16627
16832
  ];
16628
- if (harnesses.includes("cursor")) {
16629
- sessions.push({ value: "cursor-synkro", label: TAB_GLYPHS.cursor + " Cursor in Synkro UX" });
16630
- }
16631
16833
  for (; ; ) {
16632
16834
  const session = await pick({
16633
16835
  menu: true,
@@ -16686,21 +16888,418 @@ var init_dialog = __esm({
16686
16888
  }
16687
16889
  });
16688
16890
 
16891
+ // cli/harness/render.ts
16892
+ function spinnerFrame(tick) {
16893
+ return SPINNER[Math.abs(tick) % SPINNER.length];
16894
+ }
16895
+ function terminalText(text) {
16896
+ return String(text).replace(/[\u0000-\u001F\u007F-\u009F]/g, "");
16897
+ }
16898
+ function clip3(text, max) {
16899
+ const value = String(text || "").replace(/\s+/g, " ").trim();
16900
+ if (max <= 1) return value;
16901
+ return value.length <= max ? value : value.slice(0, max - 1) + "\u2026";
16902
+ }
16903
+ function seconds(ms) {
16904
+ const total = Math.max(0, Math.round(ms / 1e3));
16905
+ if (total < 60) return total + "s";
16906
+ return Math.floor(total / 60) + "m" + String(total % 60).padStart(2, "0") + "s";
16907
+ }
16908
+ function statusLine(opts) {
16909
+ const hint = opts.hint ? " \xB7 " + opts.hint : "";
16910
+ const body = opts.text + " (" + seconds(opts.elapsedMs) + hint + ")";
16911
+ const width = Math.max(20, (opts.width || 100) - 4);
16912
+ return S2.think + " " + spinnerFrame(opts.tick) + " " + clip3(body, width) + S2.reset;
16913
+ }
16914
+ function renderEvent(event, width = 100) {
16915
+ const body = Math.max(30, width - 4);
16916
+ switch (event.type) {
16917
+ // The workspace and both accounts are already in the session header, so
16918
+ // this line carries only what the header could not know before the harness
16919
+ // started: which model answered, and whether a subscription or a key paid.
16920
+ case "session-start":
16921
+ return [
16922
+ "",
16923
+ S2.dim + " " + clip3(event.model, body - 20) + (event.authSource === "login" ? " \xB7 subscription" : " \xB7 api key") + S2.reset,
16924
+ ""
16925
+ ];
16926
+ case "user-message":
16927
+ return ["", S2.user + " \u276F " + S2.reset + S2.bold + clip3(event.text, body) + S2.reset, ""];
16928
+ // Live state, not transcript. The runner promotes these to the status line.
16929
+ case "thinking":
16930
+ return [];
16931
+ case "assistant-message": {
16932
+ const lines = wrap(event.text, body - 2);
16933
+ return [
16934
+ "",
16935
+ ...lines.map((line, index) => index === 0 ? S2.agent + " " + BULLET + " " + line + S2.reset : S2.agent + " " + line + S2.reset),
16936
+ ""
16937
+ ];
16938
+ }
16939
+ case "tool-start": {
16940
+ const label = TOOL_LABEL[event.kind] || TOOL_LABEL.other;
16941
+ return [
16942
+ S2.tool + " " + BULLET + " " + label + S2.reset + S2.dim + "(" + clip3(event.target, body - label.length - 8) + ")" + S2.reset
16943
+ ];
16944
+ }
16945
+ case "tool-end": {
16946
+ if (event.blocked) {
16947
+ return [
16948
+ S2.blocked + " " + BULLET + " Blocked" + S2.reset + S2.dim + " " + clip3(terminalText(event.target), body - 14) + S2.reset,
16949
+ ...wrap(terminalText(event.reason), body - 6).map((line, index) => index === 0 ? S2.blocked + " " + ELBOW + " " + S2.reset + S2.rule + line + S2.reset : S2.rule + " " + line + S2.reset)
16950
+ ];
16951
+ }
16952
+ const detail = event.ok ? event.output.trim() ? clip3(terminalText(event.output), body - 10) : "done" : "failed" + (event.exitCode === null ? "" : " (exit " + event.exitCode + ")");
16953
+ const tint = event.ok ? S2.ok : S2.blocked;
16954
+ return [tint + " " + ELBOW + " " + S2.reset + S2.dim + detail + S2.reset];
16955
+ }
16956
+ // A clean finish needs no announcement: the prompt returning IS the signal.
16957
+ case "turn-end":
16958
+ return event.ok ? [] : ["", S2.blocked + " " + BULLET + " turn ended with an error" + S2.reset, ""];
16959
+ case "notice":
16960
+ return [S2.dim + " " + clip3(event.text, body) + S2.reset];
16961
+ default:
16962
+ return [];
16963
+ }
16964
+ }
16965
+ function wrap(text, width) {
16966
+ const words = String(text || "").replace(/\s+/g, " ").trim().split(" ").filter(Boolean);
16967
+ if (words.length === 0) return [];
16968
+ const lines = [];
16969
+ let line = "";
16970
+ for (const word of words) {
16971
+ if (!line) line = word;
16972
+ else if ((line + " " + word).length <= width) line += " " + word;
16973
+ else {
16974
+ lines.push(line);
16975
+ line = word;
16976
+ }
16977
+ }
16978
+ if (line) lines.push(line);
16979
+ return lines;
16980
+ }
16981
+ var ESC2, S2, CLEAR_LINE, BULLET, ELBOW, SPINNER, TOOL_LABEL;
16982
+ var init_render2 = __esm({
16983
+ "cli/harness/render.ts"() {
16984
+ "use strict";
16985
+ ESC2 = "\x1B[";
16986
+ S2 = {
16987
+ reset: ESC2 + "0m",
16988
+ dim: ESC2 + "2m",
16989
+ bold: ESC2 + "1m",
16990
+ user: ESC2 + "38;5;111m",
16991
+ agent: ESC2 + "38;5;252m",
16992
+ think: ESC2 + "38;5;244m",
16993
+ tool: ESC2 + "38;5;180m",
16994
+ ok: ESC2 + "38;5;114m",
16995
+ blocked: ESC2 + "38;5;203m",
16996
+ rule: ESC2 + "38;5;211m"
16997
+ };
16998
+ CLEAR_LINE = "\r" + ESC2 + "2K";
16999
+ BULLET = "\u23FA";
17000
+ ELBOW = "\u23BF";
17001
+ SPINNER = ["\xB7", "\u2722", "\u2733", "\u2217", "\u273B", "\u273D"];
17002
+ TOOL_LABEL = {
17003
+ shell: "Shell",
17004
+ read: "Read",
17005
+ edit: "Edit",
17006
+ write: "Write",
17007
+ delete: "Delete",
17008
+ search: "Search",
17009
+ list: "List",
17010
+ todo: "Todo",
17011
+ other: "Tool"
17012
+ };
17013
+ }
17014
+ });
17015
+
17016
+ // cli/harness/composer.ts
17017
+ import { execFileSync as execFileSync5, spawnSync as spawnSync12 } from "child_process";
17018
+ import { existsSync as existsSync38, mkdtempSync as mkdtempSync2, rmSync as rmSync7, statSync as statSync5, writeFileSync as writeFileSync27 } from "fs";
17019
+ import { homedir as homedir38, tmpdir } from "os";
17020
+ import { basename as basename3, dirname as dirname12, extname, isAbsolute as isAbsolute2, join as join37, resolve as resolve5, sep as sep3 } from "path";
17021
+ import { createInterface as createInterface5 } from "readline";
17022
+ function markerCarryLength(input, marker) {
17023
+ for (let length = Math.min(input.length, marker.length - 1); length > 0; length -= 1) {
17024
+ if (marker.startsWith(input.slice(-length))) return length;
17025
+ }
17026
+ return 0;
17027
+ }
17028
+ function normalizePathToken(raw) {
17029
+ let value = raw.trim();
17030
+ if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
17031
+ value = value.slice(1, -1);
17032
+ }
17033
+ value = value.replace(/\\([\\ "'()])/g, "$1");
17034
+ if (value === "~") return homedir38();
17035
+ if (value.startsWith("~/")) return join37(homedir38(), value.slice(2));
17036
+ return value;
17037
+ }
17038
+ function imagePathsInText(text, cwd) {
17039
+ const candidates = String(text || "").match(/"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'|(?:\\.|[^\s])+/g) || [];
17040
+ const images = [];
17041
+ for (const candidate of candidates) {
17042
+ const token = normalizePathToken(candidate);
17043
+ if (!IMAGE_EXTENSIONS.has(extname(token).toLowerCase())) continue;
17044
+ const path = isAbsolute2(token) ? token : resolve5(cwd, token);
17045
+ try {
17046
+ if (statSync5(path).isFile() && !images.includes(path)) images.push(path);
17047
+ } catch {
17048
+ }
17049
+ }
17050
+ return images;
17051
+ }
17052
+ function codexUserInput(draft) {
17053
+ const input = [];
17054
+ if (draft.text.trim()) input.push({ type: "text", text: draft.text, text_elements: [] });
17055
+ for (const path of draft.images) input.push({ type: "localImage", path });
17056
+ return input;
17057
+ }
17058
+ function clipboardText() {
17059
+ try {
17060
+ if (process.platform === "darwin") return execFileSync5("pbpaste", [], { encoding: "utf8", timeout: 1500 });
17061
+ const wayland = spawnSync12("wl-paste", ["--no-newline"], { encoding: "utf8", timeout: 1500 });
17062
+ if (wayland.status === 0) return String(wayland.stdout || "");
17063
+ const x11 = spawnSync12("xclip", ["-selection", "clipboard", "-o"], { encoding: "utf8", timeout: 1500 });
17064
+ return x11.status === 0 ? String(x11.stdout || "") : "";
17065
+ } catch {
17066
+ return "";
17067
+ }
17068
+ }
17069
+ function macClipboardPng(target) {
17070
+ const script = [
17071
+ "on run argv",
17072
+ "set outputPath to item 1 of argv",
17073
+ "set imageData to the clipboard as \xABclass PNGf\xBB",
17074
+ "set outputFile to open for access POSIX file outputPath with write permission",
17075
+ "set eof outputFile to 0",
17076
+ "write imageData to outputFile",
17077
+ "close access outputFile",
17078
+ "end run"
17079
+ ].join("\n");
17080
+ const result = spawnSync12("osascript", ["-e", script, target], { encoding: "utf8", timeout: 3e3 });
17081
+ return result.status === 0 && existsSync38(target);
17082
+ }
17083
+ function linuxClipboardPng(target) {
17084
+ const wayland = spawnSync12("wl-paste", ["--type", "image/png"], { encoding: null, timeout: 3e3 });
17085
+ if (wayland.status === 0 && Buffer.isBuffer(wayland.stdout) && wayland.stdout.length > 0) {
17086
+ writeFileSync27(target, wayland.stdout);
17087
+ return true;
17088
+ }
17089
+ const x11 = spawnSync12("xclip", ["-selection", "clipboard", "-t", "image/png", "-o"], { encoding: null, timeout: 3e3 });
17090
+ if (x11.status === 0 && Buffer.isBuffer(x11.stdout) && x11.stdout.length > 0) {
17091
+ writeFileSync27(target, x11.stdout);
17092
+ return true;
17093
+ }
17094
+ return false;
17095
+ }
17096
+ function clipboardImage() {
17097
+ const directory = mkdtempSync2(join37(tmpdir(), "synkro-paste-"));
17098
+ const target = join37(directory, "clipboard.png");
17099
+ try {
17100
+ const copied = process.platform === "darwin" ? macClipboardPng(target) : process.platform === "linux" && linuxClipboardPng(target);
17101
+ if (copied && statSync5(target).size > 0) return target;
17102
+ } catch {
17103
+ }
17104
+ rmSync7(directory, { recursive: true, force: true });
17105
+ return "";
17106
+ }
17107
+ function cleanupPromptDraft(draft) {
17108
+ for (const path of draft.temporaryImages) {
17109
+ const directory = dirname12(resolve5(path));
17110
+ const temporaryRoot = resolve5(tmpdir()) + sep3;
17111
+ if (!directory.startsWith(temporaryRoot) || !basename3(directory).startsWith("synkro-paste-")) continue;
17112
+ try {
17113
+ rmSync7(directory, { recursive: true, force: true });
17114
+ } catch {
17115
+ }
17116
+ }
17117
+ }
17118
+ function fallbackPrompt(cwd) {
17119
+ return new Promise((resolveDraft) => {
17120
+ const rl = createInterface5({ input: process.stdin, output: process.stdout });
17121
+ let settled = false;
17122
+ const finish = (value) => {
17123
+ if (settled) return;
17124
+ settled = true;
17125
+ rl.close();
17126
+ resolveDraft(value);
17127
+ };
17128
+ rl.once("close", () => finish(null));
17129
+ rl.once("SIGINT", () => finish(null));
17130
+ rl.question(S2.user + " \u276F " + S2.reset, (text) => {
17131
+ finish({ text, images: imagePathsInText(text, cwd), temporaryImages: [] });
17132
+ });
17133
+ });
17134
+ }
17135
+ function readPrompt(cwd) {
17136
+ const input = process.stdin;
17137
+ const output = process.stdout;
17138
+ if (!input.isTTY || !output.isTTY || !input.setRawMode) return fallbackPrompt(cwd);
17139
+ return new Promise((resolveDraft) => {
17140
+ let text = "";
17141
+ let cursor = 0;
17142
+ let stream = "";
17143
+ let pasting = false;
17144
+ let settled = false;
17145
+ const images = [];
17146
+ const temporaryImages = [];
17147
+ const wasRaw = Boolean(input.isRaw);
17148
+ const prompt = S2.user + " \u276F " + S2.reset;
17149
+ const redraw = () => {
17150
+ const safe = text.replace(/[\u0000-\u0008\u000B-\u001F\u007F-\u009F]/g, "").replace(/[\r\n]+/g, " / ");
17151
+ const attachmentLabel = images.length ? " [" + images.length + " image" + (images.length === 1 ? "" : "s") + "]" : "";
17152
+ const attachments = attachmentLabel ? S2.dim + attachmentLabel + S2.reset : "";
17153
+ output.write("\r\x1B[2K" + prompt + safe + attachments);
17154
+ const after = text.slice(cursor).replace(/[\r\n]+/g, " / ").length + attachmentLabel.length;
17155
+ if (after > 0) output.write("\x1B[" + after + "D");
17156
+ };
17157
+ const finish = (draft) => {
17158
+ if (settled) return;
17159
+ settled = true;
17160
+ input.off("data", onData);
17161
+ input.setRawMode?.(wasRaw);
17162
+ output.write("\x1B[?2004l\n");
17163
+ if (!draft) cleanupPromptDraft({ temporaryImages });
17164
+ resolveDraft(draft);
17165
+ };
17166
+ const insert = (value) => {
17167
+ text = text.slice(0, cursor) + value + text.slice(cursor);
17168
+ cursor += value.length;
17169
+ };
17170
+ const attachClipboard = () => {
17171
+ const image = clipboardImage();
17172
+ if (image) {
17173
+ images.push(image);
17174
+ temporaryImages.push(image);
17175
+ return;
17176
+ }
17177
+ insert(clipboardText());
17178
+ };
17179
+ const processStream = () => {
17180
+ while (stream && !settled) {
17181
+ if (pasting) {
17182
+ const end = stream.indexOf(PASTE_END);
17183
+ if (end < 0) {
17184
+ const keep = markerCarryLength(stream, PASTE_END);
17185
+ insert(stream.slice(0, stream.length - keep));
17186
+ stream = stream.slice(stream.length - keep);
17187
+ break;
17188
+ }
17189
+ insert(stream.slice(0, end));
17190
+ stream = stream.slice(end + PASTE_END.length);
17191
+ pasting = false;
17192
+ continue;
17193
+ }
17194
+ if (stream.startsWith(PASTE_START)) {
17195
+ stream = stream.slice(PASTE_START.length);
17196
+ pasting = true;
17197
+ continue;
17198
+ }
17199
+ if (PASTE_START.startsWith(stream)) break;
17200
+ if (stream.startsWith("\x1B[D")) {
17201
+ cursor = Math.max(0, cursor - 1);
17202
+ stream = stream.slice(3);
17203
+ continue;
17204
+ }
17205
+ if (stream.startsWith("\x1B[C")) {
17206
+ cursor = Math.min(text.length, cursor + 1);
17207
+ stream = stream.slice(3);
17208
+ continue;
17209
+ }
17210
+ if (stream.startsWith("\x1B[H")) {
17211
+ cursor = 0;
17212
+ stream = stream.slice(3);
17213
+ continue;
17214
+ }
17215
+ if (stream.startsWith("\x1B[F")) {
17216
+ cursor = text.length;
17217
+ stream = stream.slice(3);
17218
+ continue;
17219
+ }
17220
+ if (stream.startsWith("\x1B[A") || stream.startsWith("\x1B[B")) {
17221
+ stream = stream.slice(3);
17222
+ continue;
17223
+ }
17224
+ if (stream.startsWith("\x1B") && stream.length < 3) break;
17225
+ const char = stream[0];
17226
+ stream = stream.slice(1);
17227
+ if (char === "\r" || char === "\n") {
17228
+ const found = imagePathsInText(text, cwd);
17229
+ for (const path of found) if (!images.includes(path)) images.push(path);
17230
+ finish({ text, images, temporaryImages });
17231
+ } else if (char === "" || char === "" && !text && images.length === 0) {
17232
+ finish(null);
17233
+ } else if (char === "\x7F" || char === "\b") {
17234
+ if (cursor > 0) {
17235
+ text = text.slice(0, cursor - 1) + text.slice(cursor);
17236
+ cursor -= 1;
17237
+ }
17238
+ } else if (char === "") {
17239
+ text = text.slice(cursor);
17240
+ cursor = 0;
17241
+ } else if (char === "") {
17242
+ const before = text.slice(0, cursor).replace(/\s*\S+\s*$/, "");
17243
+ text = before + text.slice(cursor);
17244
+ cursor = before.length;
17245
+ } else if (char === "") {
17246
+ attachClipboard();
17247
+ } else if (char >= " " || char === " ") {
17248
+ insert(char);
17249
+ }
17250
+ }
17251
+ if (!settled) redraw();
17252
+ };
17253
+ let queue = Promise.resolve();
17254
+ const onData = (chunk) => {
17255
+ queue = queue.then(() => {
17256
+ stream += chunk.toString("utf8");
17257
+ processStream();
17258
+ });
17259
+ };
17260
+ input.setRawMode(true);
17261
+ input.resume();
17262
+ input.on("data", onData);
17263
+ output.write("\x1B[?2004h" + prompt);
17264
+ });
17265
+ }
17266
+ var PASTE_START, PASTE_END, IMAGE_EXTENSIONS;
17267
+ var init_composer = __esm({
17268
+ "cli/harness/composer.ts"() {
17269
+ "use strict";
17270
+ init_render2();
17271
+ PASTE_START = "\x1B[200~";
17272
+ PASTE_END = "\x1B[201~";
17273
+ IMAGE_EXTENSIONS = /* @__PURE__ */ new Set([".png", ".jpg", ".jpeg", ".gif", ".webp"]);
17274
+ }
17275
+ });
17276
+
16689
17277
  // cli/harness/events.ts
17278
+ function fixPollId(raw) {
17279
+ const text = String(raw || "");
17280
+ return FIX_POLL_MARKER.exec(text)?.[1] || LEGACY_FIX_POLL_MARKER.exec(text)?.[1] || "";
17281
+ }
17282
+ function cleanGuardText(raw) {
17283
+ return String(raw || "").replace(FIX_POLL_MARKER, "").replace(/\n*\s*SYNKRO FIX POLL[\s\S]*$/i, "").replace(/\n?\d{4}-\d\d-\d\dT[^\n]*\sERROR\s+codex_core::tools::router:[^\n]*/gi, "").replace(/\s*Checking command\s*$/i, "").trim();
17284
+ }
16690
17285
  function blockReason(raw) {
16691
- const text = String(raw || "").trim();
17286
+ const text = cleanGuardText(raw);
16692
17287
  if (!text) return "blocked by policy";
17288
+ const guardAt = text.lastIndexOf("Guard:");
17289
+ if (guardAt >= 0) return text.slice(guardAt + "Guard:".length).trim();
16693
17290
  const afterTag = text.match(/\[synkro:[^\]]*\]\s*(.+)/is);
16694
17291
  if (afterTag) return afterTag[1].trim();
16695
17292
  const afterHook = text.match(/blocked by a hook:\s*(.+)/is);
16696
17293
  if (afterHook) return afterHook[1].trim();
16697
17294
  return text;
16698
17295
  }
16699
- var BLOCK_MARKER;
17296
+ var BLOCK_MARKER, FIX_POLL_MARKER, LEGACY_FIX_POLL_MARKER;
16700
17297
  var init_events = __esm({
16701
17298
  "cli/harness/events.ts"() {
16702
17299
  "use strict";
16703
17300
  BLOCK_MARKER = /blocked by a hook|\[synkro:/i;
17301
+ FIX_POLL_MARKER = /\[synkro:fix-poll\s+item_id=\\?["']?([A-Za-z0-9_-]+)\\?["']?\]/i;
17302
+ LEGACY_FIX_POLL_MARKER = /SYNKRO FIX POLL\s*\(item_id=([A-Za-z0-9_-]+)\)/i;
16704
17303
  }
16705
17304
  });
16706
17305
 
@@ -16781,6 +17380,7 @@ function parseCursorLine(line) {
16781
17380
  ok: Boolean(success) && Number(success?.exitCode ?? 0) === 0,
16782
17381
  blocked,
16783
17382
  reason: rejected ? blocked ? blockReason(rawReason) : rawReason || "rejected" : "",
17383
+ pollId: blocked ? fixPollId(rawReason) : "",
16784
17384
  exitCode: success ? Number(success.exitCode ?? 0) : null,
16785
17385
  output: String(success?.stdout || success?.stderr || "")
16786
17386
  }];
@@ -16805,6 +17405,15 @@ function parseCursorLine(line) {
16805
17405
  }
16806
17406
  return [];
16807
17407
  }
17408
+ function stderrNotice(raw) {
17409
+ const text = String(raw || "").trim();
17410
+ if (!text) return "";
17411
+ if (/ActionRequiredError/i.test(text) && /usage limit/i.test(text)) {
17412
+ return "Cursor usage limit reached \u2014 replies still land, but turns will keep stalling until the plan resets or is topped up";
17413
+ }
17414
+ if (/RetriableError/i.test(text)) return "";
17415
+ return /error|fatal/i.test(text) ? text : "";
17416
+ }
16808
17417
  function feed(buffer, chunk) {
16809
17418
  const combined = buffer + chunk;
16810
17419
  const parts = combined.split("\n");
@@ -16843,97 +17452,6 @@ var init_cursor = __esm({
16843
17452
  }
16844
17453
  });
16845
17454
 
16846
- // cli/harness/render.ts
16847
- function clip3(text, max) {
16848
- const value = String(text || "").replace(/\s+/g, " ").trim();
16849
- return value.length <= max ? value : value.slice(0, max - 1) + "\u2026";
16850
- }
16851
- function renderEvent(event, width = 100) {
16852
- const body = Math.max(30, width - 4);
16853
- switch (event.type) {
16854
- case "session-start":
16855
- return [
16856
- "",
16857
- S2.dim + " " + event.model + " \xB7 " + clip3(event.cwd, body - 30) + (event.authSource === "login" ? " \xB7 subscription" : "") + S2.reset,
16858
- ""
16859
- ];
16860
- case "user-message":
16861
- return ["", S2.user + " \u276F " + S2.reset + S2.bold + clip3(event.text, body) + S2.reset, ""];
16862
- case "thinking":
16863
- return [S2.think + " \xB7 thinking\u2026" + S2.reset];
16864
- case "assistant-message":
16865
- return ["", ...wrap(event.text, body).map((line) => S2.agent + " " + line + S2.reset), ""];
16866
- case "tool-start": {
16867
- const glyph = GLYPH[event.kind] || GLYPH.other;
16868
- return [S2.tool + " " + glyph + " " + S2.reset + S2.dim + clip3(event.target, body - 6) + S2.reset];
16869
- }
16870
- case "tool-end": {
16871
- if (event.blocked) {
16872
- return [
16873
- S2.blocked + " \u26D4 blocked" + S2.reset + S2.dim + " " + clip3(event.target, body - 14) + S2.reset,
16874
- ...wrap(event.reason, body - 6).map((line) => " " + S2.rule + line + S2.reset)
16875
- ];
16876
- }
16877
- const mark = event.ok ? S2.ok + " \u2713" : S2.blocked + " \u2717";
16878
- const code = event.exitCode === null || event.exitCode === 0 ? "" : " (exit " + event.exitCode + ")";
16879
- const out = event.output.trim() ? S2.dim + " " + clip3(event.output, body - 20) + S2.reset : "";
16880
- return [mark + S2.reset + code + out];
16881
- }
16882
- case "turn-end":
16883
- return ["", S2.dim + " " + (event.ok ? "done" : "ended with an error") + S2.reset, ""];
16884
- case "notice":
16885
- return [S2.dim + " " + clip3(event.text, body) + S2.reset];
16886
- default:
16887
- return [];
16888
- }
16889
- }
16890
- function wrap(text, width) {
16891
- const words = String(text || "").replace(/\s+/g, " ").trim().split(" ").filter(Boolean);
16892
- if (words.length === 0) return [];
16893
- const lines = [];
16894
- let line = "";
16895
- for (const word of words) {
16896
- if (!line) line = word;
16897
- else if ((line + " " + word).length <= width) line += " " + word;
16898
- else {
16899
- lines.push(line);
16900
- line = word;
16901
- }
16902
- }
16903
- if (line) lines.push(line);
16904
- return lines;
16905
- }
16906
- var ESC2, S2, GLYPH;
16907
- var init_render2 = __esm({
16908
- "cli/harness/render.ts"() {
16909
- "use strict";
16910
- ESC2 = "\x1B[";
16911
- S2 = {
16912
- reset: ESC2 + "0m",
16913
- dim: ESC2 + "2m",
16914
- bold: ESC2 + "1m",
16915
- user: ESC2 + "38;5;111m",
16916
- agent: ESC2 + "38;5;252m",
16917
- think: ESC2 + "38;5;244m",
16918
- tool: ESC2 + "38;5;180m",
16919
- ok: ESC2 + "38;5;114m",
16920
- blocked: ESC2 + "38;5;203m",
16921
- rule: ESC2 + "38;5;211m"
16922
- };
16923
- GLYPH = {
16924
- shell: "\u276F",
16925
- read: "\u25C4",
16926
- edit: "\u270E",
16927
- write: "\u270E",
16928
- delete: "\u2716",
16929
- search: "\u2315",
16930
- list: "\u2630",
16931
- todo: "\u2611",
16932
- other: "\u2022"
16933
- };
16934
- }
16935
- });
16936
-
16937
17455
  // cli/harness/run.ts
16938
17456
  import { spawn as spawn9 } from "child_process";
16939
17457
  function replayKey(event) {
@@ -16952,33 +17470,149 @@ function replayKey(event) {
16952
17470
  }
16953
17471
  function createTurnSink(opts) {
16954
17472
  const events = [];
17473
+ const now = opts.now || (() => Date.now());
17474
+ const showPrompt = opts.showPrompt !== false;
16955
17475
  let blocked = 0;
16956
- let lastWasThinking = false;
16957
17476
  let replaying = false;
16958
17477
  const seen = /* @__PURE__ */ new Set();
16959
17478
  let sawTurnEnd = false;
16960
17479
  let sawAnswer = false;
16961
17480
  let buffer = "";
17481
+ let reconnecting = false;
17482
+ let lastWasAnswer = false;
17483
+ const toolStartedAt = /* @__PURE__ */ new Map();
17484
+ let statusText = "";
17485
+ let statusKind = "progress";
17486
+ let statusShown = false;
17487
+ let tick = 0;
17488
+ const startedAt = now();
17489
+ let timer = null;
17490
+ const drawStatus = () => {
17491
+ if (!opts.animate || !statusText) return;
17492
+ opts.write(statusLine({
17493
+ tick,
17494
+ text: statusText,
17495
+ elapsedMs: now() - startedAt,
17496
+ width: opts.width,
17497
+ hint: "ctrl-c to interrupt"
17498
+ }));
17499
+ statusShown = true;
17500
+ };
17501
+ const clearStatus = () => {
17502
+ if (!statusShown) return;
17503
+ opts.write(CLEAR_LINE);
17504
+ statusShown = false;
17505
+ };
17506
+ const setStatus = (text, kind = "progress") => {
17507
+ statusText = text;
17508
+ statusKind = kind;
17509
+ if (!opts.animate) return;
17510
+ if (!timer) {
17511
+ timer = setInterval(() => {
17512
+ tick += 1;
17513
+ clearStatus();
17514
+ drawStatus();
17515
+ }, TICK_MS);
17516
+ timer.unref?.();
17517
+ }
17518
+ clearStatus();
17519
+ drawStatus();
17520
+ };
17521
+ const stopStatus = () => {
17522
+ statusText = "";
17523
+ statusKind = "progress";
17524
+ if (timer) {
17525
+ clearInterval(timer);
17526
+ timer = null;
17527
+ }
17528
+ clearStatus();
17529
+ };
17530
+ let gaveUp = false;
17531
+ let stall = null;
17532
+ const graceMs = opts.postAnswerGraceMs ?? 15e3;
17533
+ const disarmStall = () => {
17534
+ if (stall) {
17535
+ clearTimeout(stall);
17536
+ stall = null;
17537
+ }
17538
+ };
17539
+ const giveUp = () => {
17540
+ if (gaveUp) return;
17541
+ gaveUp = true;
17542
+ disarmStall();
17543
+ opts.onGiveUp?.();
17544
+ };
17545
+ const armStall = () => {
17546
+ disarmStall();
17547
+ stall = setTimeout(giveUp, graceMs);
17548
+ stall.unref?.();
17549
+ };
17550
+ const settle = () => {
17551
+ reconnecting = false;
17552
+ if (statusKind === "retry") setStatus("Thinking");
17553
+ };
16962
17554
  const emit2 = (event) => {
16963
17555
  events.push(event);
16964
17556
  if (event.type === "tool-end" && event.blocked) blocked += 1;
16965
17557
  opts.onEvent?.(event);
17558
+ if (event.type === "notice" && event.kind === "retry") {
17559
+ reconnecting = true;
17560
+ if (sawAnswer && lastWasAnswer) {
17561
+ setStatus("finishing up", "retry");
17562
+ armStall();
17563
+ } else {
17564
+ setStatus(event.text, "retry");
17565
+ }
17566
+ return;
17567
+ }
16966
17568
  if (event.type === "thinking") {
16967
- if (lastWasThinking) return;
16968
- lastWasThinking = true;
16969
- } else {
16970
- lastWasThinking = false;
17569
+ if (!reconnecting) setStatus("Thinking");
17570
+ return;
17571
+ }
17572
+ if (event.type === "user-message" && !showPrompt) return;
17573
+ if (event.type === "session-start" && opts.showHeader === false) return;
17574
+ if (event.type === "assistant-message" || event.type === "tool-end") settle();
17575
+ if (event.type === "tool-start") {
17576
+ reconnecting = false;
17577
+ setStatus("Running " + event.kind);
16971
17578
  }
17579
+ if (event.type === "turn-end") stopStatus();
16972
17580
  const rendered = renderEvent(event, opts.width);
16973
- if (rendered.length) opts.write(rendered.join("\n") + "\n");
17581
+ if (!rendered.length) return;
17582
+ clearStatus();
17583
+ opts.write(rendered.join("\n") + "\n");
17584
+ drawStatus();
16974
17585
  };
16975
- const take = (event) => {
16976
- if (event.type === "notice" && event.kind === "retry") replaying = true;
17586
+ const take = (incoming) => {
17587
+ let event = incoming;
17588
+ if (event.type === "tool-start" && event.id && !toolStartedAt.has(event.id)) {
17589
+ toolStartedAt.set(event.id, now());
17590
+ } else if (event.type === "tool-end") {
17591
+ const started = toolStartedAt.get(event.id);
17592
+ if ((event.durationMs === null || event.durationMs === void 0) && started !== void 0) {
17593
+ event = { ...event, durationMs: Math.max(0, now() - started) };
17594
+ }
17595
+ toolStartedAt.delete(event.id);
17596
+ }
17597
+ const isRetry = event.type === "notice" && event.kind === "retry";
17598
+ if (!isRetry) disarmStall();
17599
+ if (isRetry) {
17600
+ if (reconnecting && sawAnswer && lastWasAnswer) {
17601
+ giveUp();
17602
+ return;
17603
+ }
17604
+ replaying = true;
17605
+ }
16977
17606
  const key = replayKey(event);
16978
17607
  if (key) {
16979
- if (replaying && seen.has(key)) return;
17608
+ if (replaying && seen.has(key)) {
17609
+ lastWasAnswer = event.type === "assistant-message";
17610
+ settle();
17611
+ return;
17612
+ }
16980
17613
  seen.add(key);
16981
17614
  }
17615
+ if (key || event.type === "thinking") lastWasAnswer = event.type === "assistant-message";
16982
17616
  if (event.type === "turn-end") sawTurnEnd = true;
16983
17617
  if (event.type === "assistant-message") sawAnswer = true;
16984
17618
  emit2(event);
@@ -16992,57 +17626,626 @@ function createTurnSink(opts) {
16992
17626
  notice(text) {
16993
17627
  emit2({ type: "notice", text });
16994
17628
  },
16995
- finish(exit) {
17629
+ event(event) {
17630
+ take(event);
17631
+ },
17632
+ finish(exit, interrupted = false) {
17633
+ disarmStall();
17634
+ stopStatus();
16996
17635
  if (!sawTurnEnd) {
16997
- if (exit !== 0) {
16998
- emit2({
16999
- type: "notice",
17000
- text: sawAnswer ? "cursor-agent exited " + exit + " after retrying; the reply above is complete" : "cursor-agent exited " + exit + " without completing the turn"
17001
- });
17636
+ if (interrupted) {
17637
+ emit2({ type: "notice", text: "interrupted" });
17638
+ emit2({ type: "turn-end", ok: true, text: "" });
17639
+ } else {
17640
+ if (exit !== 0 && !sawAnswer) {
17641
+ emit2({ type: "notice", text: "cursor-agent exited " + exit + " without completing the turn" });
17642
+ }
17643
+ emit2({ type: "turn-end", ok: sawAnswer, text: "" });
17002
17644
  }
17003
- emit2({ type: "turn-end", ok: sawAnswer, text: "" });
17004
17645
  }
17005
- return { events, blocked, exitCode: sawAnswer ? 0 : exit };
17646
+ stopStatus();
17647
+ return { events, blocked, exitCode: interrupted ? 130 : sawAnswer ? 0 : exit };
17006
17648
  }
17007
17649
  };
17008
17650
  }
17009
17651
  async function runCursorTurn(opts) {
17010
17652
  const write2 = opts.write || ((text) => process.stdout.write(text));
17011
17653
  const width = opts.width || Number(process.stdout.columns || 100);
17012
- const sink = createTurnSink({ write: write2, onEvent: opts.onEvent, width });
17013
- return new Promise((resolve7) => {
17654
+ let stopHarness = () => {
17655
+ };
17656
+ const sink = createTurnSink({
17657
+ write: write2,
17658
+ onEvent: opts.onEvent,
17659
+ width,
17660
+ animate: Boolean(process.stdout.isTTY),
17661
+ showPrompt: opts.showPrompt,
17662
+ showHeader: opts.showHeader,
17663
+ onGiveUp: () => stopHarness()
17664
+ });
17665
+ return new Promise((resolve8) => {
17014
17666
  const child = spawn9("cursor-agent", cursorArgs(opts.prompt), {
17015
17667
  cwd: opts.cwd,
17016
17668
  stdio: ["ignore", "pipe", "pipe"]
17017
17669
  });
17670
+ stopHarness = () => {
17671
+ child.kill();
17672
+ const hard = setTimeout(() => child.kill("SIGKILL"), 3e3);
17673
+ hard.unref?.();
17674
+ child.once("close", () => clearTimeout(hard));
17675
+ };
17676
+ let interrupted = false;
17677
+ const onAbort = () => {
17678
+ interrupted = true;
17679
+ stopHarness();
17680
+ };
17681
+ if (opts.signal?.aborted) onAbort();
17682
+ else opts.signal?.addEventListener("abort", onAbort, { once: true });
17018
17683
  child.stdout.on("data", (chunk) => sink.chunk(chunk.toString("utf8")));
17684
+ let lastNote = "";
17019
17685
  child.stderr.on("data", (chunk) => {
17020
- const text = chunk.toString("utf8").trim();
17021
- if (/error|fatal/i.test(text) && !/RetriableError/i.test(text)) sink.notice(text);
17686
+ const note = stderrNotice(chunk.toString("utf8"));
17687
+ if (note && note !== lastNote) {
17688
+ lastNote = note;
17689
+ sink.notice(note);
17690
+ }
17691
+ });
17692
+ child.on("close", (code) => {
17693
+ opts.signal?.removeEventListener("abort", onAbort);
17694
+ resolve8(sink.finish(code ?? 0, interrupted));
17022
17695
  });
17023
- child.on("close", (code) => resolve7(sink.finish(code ?? 0)));
17024
17696
  child.on("error", (error) => {
17025
17697
  sink.notice("failed to start cursor-agent: " + String(error));
17026
- resolve7(sink.finish(1));
17698
+ resolve8(sink.finish(1, interrupted));
17027
17699
  });
17028
17700
  });
17029
17701
  }
17702
+ var TICK_MS;
17030
17703
  var init_run = __esm({
17031
17704
  "cli/harness/run.ts"() {
17032
17705
  "use strict";
17033
17706
  init_cursor();
17034
17707
  init_render2();
17708
+ TICK_MS = 120;
17709
+ }
17710
+ });
17711
+
17712
+ // cli/harness/codex.ts
17713
+ import { spawn as spawn10 } from "child_process";
17714
+ function itemTarget(item) {
17715
+ if (item?.type === "commandExecution") {
17716
+ return { kind: "shell", target: String(item.command || ""), description: "" };
17717
+ }
17718
+ if (item?.type === "fileChange") {
17719
+ const paths = Array.isArray(item.changes) ? item.changes.map((change) => String(change?.path || "")).filter(Boolean) : [];
17720
+ return { kind: "edit", target: paths.join(", ") || "files", description: "" };
17721
+ }
17722
+ if (item?.type === "mcpToolCall") {
17723
+ return { kind: "other", target: String(item.server || "") + "::" + String(item.tool || ""), description: "MCP tool" };
17724
+ }
17725
+ if (item?.type === "dynamicToolCall") {
17726
+ return { kind: "other", target: [item.namespace, item.tool].filter(Boolean).join("::"), description: "tool" };
17727
+ }
17728
+ return { kind: "other", target: String(item?.type || "tool"), description: "" };
17729
+ }
17730
+ function itemOutput(item) {
17731
+ if (item?.type === "commandExecution") return String(item.aggregatedOutput || "");
17732
+ if (item?.type === "mcpToolCall") return item.error ? String(item.error?.message || JSON.stringify(item.error)) : item.result ? JSON.stringify(item.result) : "";
17733
+ if (item?.type === "dynamicToolCall") return item.contentItems ? JSON.stringify(item.contentItems) : "";
17734
+ return "";
17735
+ }
17736
+ function parseCodexNotification(method, params, startedAt = /* @__PURE__ */ new Map(), approvalReasons = /* @__PURE__ */ new Map()) {
17737
+ if (method === "hook/started" || method === "hook/completed") {
17738
+ const run2 = params?.run || {};
17739
+ if (run2.eventName !== "preToolUse") return [];
17740
+ const asMs = (value) => {
17741
+ const number = Number(value || 0);
17742
+ return number > 0 && number < 1e11 ? number * 1e3 : number;
17743
+ };
17744
+ const started = asMs(run2.startedAt);
17745
+ const priorStart = Number(approvalReasons.get("__pretool_started__") || 0);
17746
+ if (started && (!priorStart || started < priorStart)) approvalReasons.set("__pretool_started__", String(started));
17747
+ if (method === "hook/completed") {
17748
+ const completed = asMs(run2.completedAt) || started + Number(run2.durationMs || 0);
17749
+ const priorCompleted = Number(approvalReasons.get("__pretool_completed__") || 0);
17750
+ if (completed > priorCompleted) approvalReasons.set("__pretool_completed__", String(completed));
17751
+ const entries = Array.isArray(run2.entries) ? run2.entries : [];
17752
+ const pollEntry = entries.find((entry) => fixPollId(String(entry?.text || "")));
17753
+ const blockingEntry = entries.find((entry) => entry?.kind === "stop" || entry?.kind === "error");
17754
+ const text = String(pollEntry?.text || blockingEntry?.text || run2.statusMessage || "");
17755
+ if (text && (run2.status === "blocked" || run2.status === "failed" || BLOCK_MARKER.test(text))) {
17756
+ approvalReasons.set("__latest_hook__", text);
17757
+ approvalReasons.set("__latest_hook_id__", String(run2.id || "synkro-policy-block"));
17758
+ approvalReasons.set("__latest_hook_duration__", String(Number(run2.durationMs || Math.max(0, completed - started))));
17759
+ }
17760
+ }
17761
+ return [];
17762
+ }
17763
+ if (method === "item/started") {
17764
+ const item = params?.item;
17765
+ if (!item?.id) return [];
17766
+ startedAt.set(String(item.id), Number(params.startedAtMs || Date.now()));
17767
+ if (!["commandExecution", "fileChange", "mcpToolCall", "dynamicToolCall"].includes(item.type)) return [];
17768
+ return [{ type: "tool-start", id: String(item.id), ...itemTarget(item) }];
17769
+ }
17770
+ if (method === "item/completed") {
17771
+ const item = params?.item;
17772
+ if (!item?.id) return [];
17773
+ if (item.type === "hookPrompt") {
17774
+ const text = Array.isArray(item.fragments) ? item.fragments.map((fragment) => String(fragment?.text || "")).filter(Boolean).join("\n") : "";
17775
+ if (text) {
17776
+ const start2 = startedAt.get(String(item.id));
17777
+ const completed2 = Number(params.completedAtMs || Date.now());
17778
+ approvalReasons.set("__latest_hook__", text);
17779
+ approvalReasons.set("__latest_hook_id__", String(item.id));
17780
+ approvalReasons.set("__latest_hook_duration__", String(start2 === void 0 ? 0 : Math.max(0, completed2 - start2)));
17781
+ }
17782
+ startedAt.delete(String(item.id));
17783
+ return [];
17784
+ }
17785
+ if (item.type === "agentMessage") {
17786
+ return item.text ? [{ type: "assistant-message", text: String(item.text) }] : [];
17787
+ }
17788
+ if (item.type === "reasoning") {
17789
+ const text = [...item.summary || [], ...item.content || []].join(" ").trim();
17790
+ return text ? [{ type: "thinking", text }] : [];
17791
+ }
17792
+ if (!["commandExecution", "fileChange", "mcpToolCall", "dynamicToolCall"].includes(item.type)) return [];
17793
+ const output = itemOutput(item);
17794
+ const approvalReason = approvalReasons.get(String(item.id)) || "";
17795
+ const hookReason = approvalReasons.get("__latest_hook__") || "";
17796
+ const rawReason = [approvalReason, hookReason, output].filter(Boolean).join("\n");
17797
+ const status = String(item.status || "");
17798
+ const blocked = BLOCK_MARKER.test(rawReason) || /blocked by PreToolUse hook/i.test(rawReason) || Boolean(hookReason) && (status === "failed" || status === "declined");
17799
+ const itemStart = startedAt.get(String(item.id));
17800
+ const hookStart = Number(approvalReasons.get("__pretool_started__") || 0);
17801
+ const start = itemStart === void 0 ? hookStart || void 0 : hookStart ? Math.min(itemStart, hookStart) : itemStart;
17802
+ const completed = Number(params.completedAtMs || Date.now());
17803
+ const durationMs = start === void 0 ? item.durationMs ?? null : Math.max(0, completed - start);
17804
+ startedAt.delete(String(item.id));
17805
+ approvalReasons.delete(String(item.id));
17806
+ approvalReasons.delete("__latest_hook__");
17807
+ approvalReasons.delete("__latest_hook_id__");
17808
+ approvalReasons.delete("__latest_hook_duration__");
17809
+ approvalReasons.delete("__pretool_started__");
17810
+ approvalReasons.delete("__pretool_completed__");
17811
+ return [{
17812
+ type: "tool-end",
17813
+ id: String(item.id),
17814
+ ...itemTarget(item),
17815
+ ok: status === "completed" && Number(item.exitCode ?? 0) === 0,
17816
+ blocked,
17817
+ reason: blocked ? blockReason(rawReason) : "",
17818
+ pollId: blocked ? fixPollId(rawReason) : "",
17819
+ exitCode: item.exitCode === null || item.exitCode === void 0 ? null : Number(item.exitCode),
17820
+ output: blocked ? "" : output,
17821
+ durationMs
17822
+ }];
17823
+ }
17824
+ if (method === "turn/completed") {
17825
+ const status = String(params?.turn?.status || "failed");
17826
+ const hookReason = approvalReasons.get("__latest_hook__") || "";
17827
+ const events = [];
17828
+ if (hookReason) {
17829
+ events.push({
17830
+ type: "tool-end",
17831
+ id: approvalReasons.get("__latest_hook_id__") || "synkro-policy-block",
17832
+ kind: "other",
17833
+ target: "policy check",
17834
+ ok: false,
17835
+ blocked: true,
17836
+ reason: blockReason(hookReason),
17837
+ pollId: fixPollId(hookReason),
17838
+ exitCode: null,
17839
+ output: "",
17840
+ durationMs: Number(approvalReasons.get("__latest_hook_duration__") || 0)
17841
+ });
17842
+ approvalReasons.delete("__latest_hook__");
17843
+ approvalReasons.delete("__latest_hook_id__");
17844
+ approvalReasons.delete("__latest_hook_duration__");
17845
+ }
17846
+ approvalReasons.delete("__pretool_started__");
17847
+ approvalReasons.delete("__pretool_completed__");
17848
+ events.push({ type: "turn-end", ok: status === "completed", text: String(params?.turn?.error?.message || "") });
17849
+ return events;
17850
+ }
17851
+ return [];
17852
+ }
17853
+ function codexStderrNotice(raw, initialized) {
17854
+ const text = String(raw || "").trim();
17855
+ if (!text || initialized) return "";
17856
+ return /fatal|panic|authentication failed|not logged in/i.test(text) ? text.split("\n")[0].slice(0, 300) : "";
17857
+ }
17858
+ async function askApproval(text) {
17859
+ const input = process.stdin;
17860
+ const output = process.stdout;
17861
+ if (!input.isTTY || !output.isTTY) return false;
17862
+ output.write("\n" + S2.rule + " Synkro approval required" + S2.reset + "\n " + text.replace(/[\u0000-\u001F\u007F-\u009F]/g, " ").slice(0, 600) + "\n");
17863
+ output.write(S2.dim + " Press y to allow once; any other key denies." + S2.reset + "\n");
17864
+ return new Promise((resolve8) => {
17865
+ const wasRaw = Boolean(input.isRaw);
17866
+ const onData = (chunk) => {
17867
+ input.off("data", onData);
17868
+ if (input.setRawMode) input.setRawMode(wasRaw);
17869
+ resolve8(chunk.toString("utf8").toLowerCase() === "y");
17870
+ };
17871
+ if (input.setRawMode) input.setRawMode(true);
17872
+ input.resume();
17873
+ input.on("data", onData);
17874
+ });
17875
+ }
17876
+ async function runCodexTurn(opts) {
17877
+ const session = new CodexAppSession();
17878
+ try {
17879
+ return await session.turn(opts);
17880
+ } finally {
17881
+ session.close();
17882
+ }
17883
+ }
17884
+ var CodexAppSession;
17885
+ var init_codex = __esm({
17886
+ "cli/harness/codex.ts"() {
17887
+ "use strict";
17888
+ init_composer();
17889
+ init_events();
17890
+ init_run();
17891
+ init_render2();
17892
+ CodexAppSession = class {
17893
+ child = null;
17894
+ buffer = "";
17895
+ nextId = 1;
17896
+ pending = /* @__PURE__ */ new Map();
17897
+ initialized = false;
17898
+ threadId = "";
17899
+ model = "Codex";
17900
+ cwd = "";
17901
+ active = null;
17902
+ send(message) {
17903
+ this.child?.stdin.write(JSON.stringify({ jsonrpc: "2.0", ...message }) + "\n");
17904
+ }
17905
+ request(method, params) {
17906
+ const id = this.nextId++;
17907
+ return new Promise((resolve8, reject) => {
17908
+ this.pending.set(id, { resolve: resolve8, reject });
17909
+ this.send({ id, method, params });
17910
+ });
17911
+ }
17912
+ respond(id, result) {
17913
+ this.send({ id, result });
17914
+ }
17915
+ async handleServerRequest(message) {
17916
+ const method = String(message.method || "");
17917
+ const params = message.params || {};
17918
+ if (params.itemId && params.reason) this.active?.approvalReasons.set(String(params.itemId), String(params.reason));
17919
+ if (method === "item/commandExecution/requestApproval" || method === "item/fileChange/requestApproval") {
17920
+ const summary = String(params.reason || params.command || params.grantRoot || "Allow this action?");
17921
+ const accepted = await askApproval(summary);
17922
+ this.respond(message.id, { decision: accepted ? "accept" : "decline" });
17923
+ return;
17924
+ }
17925
+ if (method === "item/tool/requestUserInput") {
17926
+ const answers = {};
17927
+ for (const question of params.questions || []) answers[String(question.id)] = { answers: [] };
17928
+ this.respond(message.id, { answers });
17929
+ return;
17930
+ }
17931
+ if (method === "mcpServer/elicitation/request") {
17932
+ this.respond(message.id, { action: "decline" });
17933
+ return;
17934
+ }
17935
+ this.respond(message.id, {});
17936
+ }
17937
+ handleLine(line) {
17938
+ let message;
17939
+ try {
17940
+ message = JSON.parse(line);
17941
+ } catch {
17942
+ return;
17943
+ }
17944
+ if (message.method && message.id !== void 0) {
17945
+ void this.handleServerRequest(message);
17946
+ return;
17947
+ }
17948
+ if (message.method) {
17949
+ const active = this.active;
17950
+ if (!active) return;
17951
+ const events = parseCodexNotification(message.method, message.params, active.startedAt, active.approvalReasons);
17952
+ for (const event of events) active.sink.event(event);
17953
+ if (message.method === "turn/started") active.turnId = String(message.params?.turn?.id || "");
17954
+ if (message.method === "turn/completed") {
17955
+ const result = active.sink.finish(message.params?.turn?.status === "completed" ? 0 : 1, active.interrupted);
17956
+ this.active = null;
17957
+ active.resolve(result);
17958
+ }
17959
+ return;
17960
+ }
17961
+ if (message.id !== void 0) {
17962
+ const pending = this.pending.get(Number(message.id));
17963
+ if (!pending) return;
17964
+ this.pending.delete(Number(message.id));
17965
+ if (message.error) pending.reject(new Error(String(message.error?.message || JSON.stringify(message.error))));
17966
+ else pending.resolve(message.result);
17967
+ }
17968
+ }
17969
+ async start(cwd) {
17970
+ if (this.child) return;
17971
+ this.cwd = cwd;
17972
+ this.child = spawn10("codex", ["app-server"], { cwd, stdio: ["pipe", "pipe", "pipe"] });
17973
+ this.child.stdout.on("data", (chunk) => {
17974
+ this.buffer += chunk.toString("utf8");
17975
+ let newline = this.buffer.indexOf("\n");
17976
+ while (newline !== -1) {
17977
+ const line = this.buffer.slice(0, newline).trim();
17978
+ this.buffer = this.buffer.slice(newline + 1);
17979
+ if (line) this.handleLine(line);
17980
+ newline = this.buffer.indexOf("\n");
17981
+ }
17982
+ });
17983
+ this.child.stderr.on("data", (chunk) => {
17984
+ const note = codexStderrNotice(chunk.toString("utf8"), this.initialized);
17985
+ if (note) this.active?.sink.notice(note);
17986
+ });
17987
+ this.child.on("close", (code) => {
17988
+ const active = this.active;
17989
+ if (active) {
17990
+ this.active = null;
17991
+ active.resolve(active.sink.finish(code ?? 1, active.interrupted));
17992
+ }
17993
+ for (const pending of this.pending.values()) pending.reject(new Error("codex app-server exited"));
17994
+ this.pending.clear();
17995
+ this.child = null;
17996
+ });
17997
+ this.child.on("error", (error) => {
17998
+ const failure = new Error("failed to start codex app-server: " + String(error));
17999
+ const active = this.active;
18000
+ if (active) {
18001
+ active.sink.notice(failure.message);
18002
+ this.active = null;
18003
+ active.resolve(active.sink.finish(1, active.interrupted));
18004
+ }
18005
+ for (const pending of this.pending.values()) pending.reject(failure);
18006
+ this.pending.clear();
18007
+ });
18008
+ const initialized = await this.request("initialize", { clientInfo: { name: "synkro-governed", version: "1.0.0" } });
18009
+ this.initialized = true;
18010
+ this.send({ method: "initialized", params: {} });
18011
+ const started = await this.request("thread/start", {
18012
+ cwd,
18013
+ approvalPolicy: "on-request",
18014
+ approvalsReviewer: "user",
18015
+ sandbox: "workspace-write",
18016
+ ephemeral: false
18017
+ });
18018
+ this.threadId = String(started?.thread?.id || "");
18019
+ this.model = String(started?.model || initialized?.userAgent || "Codex");
18020
+ if (!this.threadId) throw new Error("codex app-server did not return a thread id");
18021
+ }
18022
+ async turn(opts) {
18023
+ await this.start(opts.cwd);
18024
+ if (this.active) throw new Error("a Codex turn is already running");
18025
+ const sink = createTurnSink({
18026
+ write: opts.write || ((text) => process.stdout.write(text)),
18027
+ onEvent: opts.onEvent,
18028
+ width: opts.width || Number(process.stdout.columns || 100),
18029
+ animate: false,
18030
+ showPrompt: opts.showPrompt,
18031
+ showHeader: opts.showHeader
18032
+ });
18033
+ sink.event({ type: "session-start", sessionId: this.threadId, model: this.model, cwd: this.cwd, authSource: "login" });
18034
+ sink.event({
18035
+ type: "user-message",
18036
+ text: opts.prompt || (opts.images?.length ? "[" + opts.images.length + " image attached]" : "")
18037
+ });
18038
+ const result = new Promise((resolve8) => {
18039
+ this.active = { sink, resolve: resolve8, startedAt: /* @__PURE__ */ new Map(), approvalReasons: /* @__PURE__ */ new Map(), turnId: "", interrupted: false };
18040
+ });
18041
+ const onAbort = () => {
18042
+ if (!this.active) return;
18043
+ this.active.interrupted = true;
18044
+ if (this.active.turnId) void this.request("turn/interrupt", { threadId: this.threadId, turnId: this.active.turnId }).catch(() => {
18045
+ });
18046
+ };
18047
+ if (opts.signal?.aborted) onAbort();
18048
+ else opts.signal?.addEventListener("abort", onAbort, { once: true });
18049
+ try {
18050
+ await this.request("turn/start", {
18051
+ threadId: this.threadId,
18052
+ cwd: opts.cwd,
18053
+ input: codexUserInput({ text: opts.prompt, images: opts.images || [] })
18054
+ });
18055
+ return await result;
18056
+ } finally {
18057
+ opts.signal?.removeEventListener("abort", onAbort);
18058
+ }
18059
+ }
18060
+ close() {
18061
+ this.child?.kill();
18062
+ this.child = null;
18063
+ }
18064
+ };
18065
+ }
18066
+ });
18067
+
18068
+ // cli/harness/fixPoll.ts
18069
+ function pollBaseUrl() {
18070
+ const port = String(process.env.SYNKRO_MCP_PORT || "18931");
18071
+ return "http://127.0.0.1:" + port + "/api/local/fix-poll";
18072
+ }
18073
+ function neutralizeTerminalControls(value) {
18074
+ return value.replace(/[\u0000-\u0008\u000B-\u001F\u007F-\u009F\u001B]/g, "");
18075
+ }
18076
+ async function loadFixPoll(itemId, fetchImpl = fetch) {
18077
+ const response = await fetchImpl(pollBaseUrl() + "?item_id=" + encodeURIComponent(itemId));
18078
+ if (!response.ok) return null;
18079
+ const body = await response.json();
18080
+ const candidates = Array.isArray(body.candidates) ? body.candidates.filter((candidate) => typeof candidate === "string").slice(0, 8) : [];
18081
+ if (!candidates.length || body.status !== "pending") return null;
18082
+ return {
18083
+ itemId,
18084
+ filePath: String(body.file_path || ""),
18085
+ ruleId: String(body.rule_id || ""),
18086
+ candidates
18087
+ };
18088
+ }
18089
+ async function recordFixPoll(itemId, chosenIdx, fetchImpl = fetch) {
18090
+ const response = await fetchImpl(pollBaseUrl() + "/record", {
18091
+ method: "POST",
18092
+ headers: { "content-type": "application/json" },
18093
+ body: JSON.stringify({ item_id: itemId, chosen_idx: chosenIdx })
18094
+ });
18095
+ return response.ok;
18096
+ }
18097
+ function fixPollLines(poll, selected, width = 100) {
18098
+ const max = Math.max(32, width - 10);
18099
+ const choices = [...poll.candidates, "None of the above"];
18100
+ const scope = [poll.ruleId, poll.filePath].filter(Boolean).join(" \xB7 ");
18101
+ const lines = [
18102
+ "",
18103
+ S2.bold + S2.rule + " Synkro needs your decision" + S2.reset,
18104
+ ...scope ? [S2.dim + " " + neutralizeTerminalControls(scope) + S2.reset] : [],
18105
+ ""
18106
+ ];
18107
+ choices.forEach((choice, index) => {
18108
+ const prefix = index === selected ? S2.user + " \u276F " : S2.dim + " ";
18109
+ const text = wrap(index + 1 + ". " + neutralizeTerminalControls(choice), max);
18110
+ lines.push(prefix + (text[0] || "") + S2.reset);
18111
+ for (const continuation of text.slice(1)) lines.push(" " + continuation);
18112
+ });
18113
+ lines.push("", S2.dim + " \u2191/\u2193 select \xB7 Enter confirm \xB7 1-" + choices.length + " choose" + S2.reset);
18114
+ return lines;
18115
+ }
18116
+ async function pickFixPoll(poll, io = {}) {
18117
+ const input = io.input || process.stdin;
18118
+ const output = io.output || process.stdout;
18119
+ if (!input.isTTY || !output.isTTY) return -1;
18120
+ let selected = 0;
18121
+ let rendered = 0;
18122
+ const draw = () => {
18123
+ if (rendered) output.write("\x1B[" + rendered + "A\x1B[J");
18124
+ const lines = fixPollLines(poll, selected, Number(output.columns || 100));
18125
+ output.write(lines.join("\n") + "\n");
18126
+ rendered = lines.length;
18127
+ };
18128
+ const choice = await new Promise((resolve8) => {
18129
+ const choices = poll.candidates.length + 1;
18130
+ const wasRaw = Boolean(input.isRaw);
18131
+ const done = (index) => {
18132
+ input.off("data", onData);
18133
+ if (input.setRawMode) input.setRawMode(wasRaw);
18134
+ resolve8(index === poll.candidates.length ? -1 : index);
18135
+ };
18136
+ const onData = (chunk) => {
18137
+ const key = chunk.toString("utf8");
18138
+ if (key === "") return done(poll.candidates.length);
18139
+ if (key === "\x1B" || key.toLowerCase() === "n") return done(poll.candidates.length);
18140
+ if (key === "\r" || key === "\n") return done(selected);
18141
+ if (key === "\x1B[A" || key === "k") selected = (selected - 1 + choices) % choices;
18142
+ else if (key === "\x1B[B" || key === "j") selected = (selected + 1) % choices;
18143
+ else if (/^[1-9]$/.test(key)) {
18144
+ const index = Number(key) - 1;
18145
+ if (index < choices) return done(index);
18146
+ } else return;
18147
+ draw();
18148
+ };
18149
+ if (input.setRawMode) input.setRawMode(true);
18150
+ input.resume();
18151
+ input.on("data", onData);
18152
+ draw();
18153
+ });
18154
+ return choice;
18155
+ }
18156
+ async function resolveFixPolls(events, io = {}) {
18157
+ const ids = Array.from(new Set(events.filter((event) => event.type === "tool-end").map((event) => event.pollId || "").filter(Boolean)));
18158
+ const recorded = [];
18159
+ for (const itemId of ids) {
18160
+ const poll = await loadFixPoll(itemId, io.fetchImpl).catch(() => null);
18161
+ if (!poll) continue;
18162
+ const chosen = await pickFixPoll(poll, io);
18163
+ const ok = await recordFixPoll(itemId, chosen, io.fetchImpl).catch(() => false);
18164
+ if (ok) recorded.push(chosen);
18165
+ else (io.output || process.stdout).write(S2.blocked + " Could not record that Synkro decision.\n" + S2.reset);
18166
+ }
18167
+ return recorded;
18168
+ }
18169
+ var init_fixPoll = __esm({
18170
+ "cli/harness/fixPoll.ts"() {
18171
+ "use strict";
18172
+ init_render2();
18173
+ }
18174
+ });
18175
+
18176
+ // cli/harness/identity.ts
18177
+ import { execFileSync as execFileSync6 } from "child_process";
18178
+ import { homedir as homedir39 } from "os";
18179
+ function shortenPath(path, home = homedir39()) {
18180
+ const value = String(path || "");
18181
+ if (home && value === home) return "~";
18182
+ if (home && value.startsWith(home + "/")) return "~" + value.slice(home.length);
18183
+ return value;
18184
+ }
18185
+ function parseCursorAccount(output) {
18186
+ const match = String(output || "").match(/logged in as\s+(\S+)/i);
18187
+ return match ? match[1].trim() : "";
18188
+ }
18189
+ function cursorAccount() {
18190
+ try {
18191
+ const out = execFileSync6("cursor-agent", ["status"], {
18192
+ encoding: "utf8",
18193
+ timeout: 5e3,
18194
+ stdio: ["ignore", "pipe", "pipe"]
18195
+ });
18196
+ return parseCursorAccount(out);
18197
+ } catch {
18198
+ return "";
18199
+ }
18200
+ }
18201
+ function synkroAccount() {
18202
+ let email = "";
18203
+ try {
18204
+ email = String(getUserInfo().email || "");
18205
+ } catch {
18206
+ return { email: "", needsLogin: false };
18207
+ }
18208
+ let needsLogin = false;
18209
+ try {
18210
+ needsLogin = isTokenExpired() && !loadCredentials()?.refresh_token;
18211
+ } catch {
18212
+ }
18213
+ return { email, needsLogin };
18214
+ }
18215
+ function readIdentity(harness) {
18216
+ const synkro = synkroAccount();
18217
+ return {
18218
+ harness: harness === "cursor" ? cursorAccount() : "",
18219
+ synkro: synkro.email,
18220
+ needsLogin: synkro.needsLogin
18221
+ };
18222
+ }
18223
+ function identityHeader(opts) {
18224
+ const lines = ["", S2.bold + " " + shortenPath(opts.cwd, opts.home) + S2.reset];
18225
+ const label = (name, value, note = "") => S2.dim + " " + name.padEnd(7) + S2.reset + S2.think + value + S2.reset + note;
18226
+ if (opts.identity.harness) lines.push(label(opts.harness, opts.identity.harness));
18227
+ if (opts.identity.synkro) {
18228
+ lines.push(label("synkro", opts.identity.synkro, opts.identity.needsLogin ? S2.blocked + " \xB7 session ended, run synkro login" + S2.reset : ""));
18229
+ }
18230
+ lines.push("");
18231
+ return lines;
18232
+ }
18233
+ var init_identity2 = __esm({
18234
+ "cli/harness/identity.ts"() {
18235
+ "use strict";
18236
+ init_auth();
18237
+ init_render2();
17035
18238
  }
17036
18239
  });
17037
18240
 
17038
18241
  // cli/harness/session.ts
17039
- import { createInterface as createInterface5 } from "readline";
17040
- async function runOnce(harness, cwd, prompt) {
17041
- if (harness !== "cursor") {
18242
+ async function runOnce(harness, cwd, prompt, echoPrompt = true, showHeader = true, signal) {
18243
+ if (harness !== "cursor" && harness !== "codex") {
17042
18244
  process.stdout.write(S2.dim + " " + harness + " sessions are not embedded yet\n" + S2.reset);
17043
18245
  return 1;
17044
18246
  }
17045
- const result = await runCursorTurn({ prompt, cwd });
18247
+ const result = harness === "codex" ? await runCodexTurn({ prompt, cwd, showPrompt: echoPrompt, showHeader, signal }) : await runCursorTurn({ prompt, cwd, showPrompt: echoPrompt, showHeader, signal });
18248
+ await resolveFixPolls(result.events);
17046
18249
  if (result.blocked > 0) {
17047
18250
  process.stdout.write(
17048
18251
  S2.blocked + " " + result.blocked + " action" + (result.blocked === 1 ? "" : "s") + " blocked by policy" + S2.reset + "\n"
@@ -17052,31 +18255,52 @@ async function runOnce(harness, cwd, prompt) {
17052
18255
  }
17053
18256
  async function runGovernedSession(harness, cwd, prompt) {
17054
18257
  if (prompt) return runOnce(harness, cwd, prompt);
17055
- process.stdout.write(BANNER.join("\n") + "\n");
17056
- const rl = createInterface5({ input: process.stdin, output: process.stdout });
17057
- const ask3 = () => new Promise((resolve7) => rl.question(S2.user + " \u276F " + S2.reset, resolve7));
17058
- for (; ; ) {
17059
- const line = (await ask3()).trim();
17060
- if (!line) continue;
17061
- if (line === "exit" || line === "quit") break;
17062
- await runOnce(harness, cwd, line);
18258
+ process.stdout.write(identityHeader({ cwd, harness, identity: readIdentity(harness) }).join("\n") + "\n");
18259
+ const codex = harness === "codex" ? new CodexAppSession() : null;
18260
+ let turn = null;
18261
+ const interruptTurn = () => {
18262
+ turn?.abort();
18263
+ };
18264
+ process.on("SIGINT", interruptTurn);
18265
+ let first = true;
18266
+ try {
18267
+ for (; ; ) {
18268
+ const draft = await readPrompt(cwd);
18269
+ if (draft === null) break;
18270
+ const line = draft.text.trim();
18271
+ if (!line && draft.images.length === 0) continue;
18272
+ if (draft.images.length === 0 && (line === "exit" || line === "quit")) break;
18273
+ try {
18274
+ turn = new AbortController();
18275
+ const cursorPrompt = draft.images.length ? [draft.text, "", "Attached image files:", ...draft.images].join("\n") : draft.text;
18276
+ const result = codex ? await codex.turn({ prompt: draft.text, images: draft.images, cwd, showPrompt: false, showHeader: first, signal: turn.signal }) : await runCursorTurn({ prompt: cursorPrompt, cwd, showPrompt: false, showHeader: first, signal: turn.signal });
18277
+ await resolveFixPolls(result.events);
18278
+ if (result.blocked > 0) {
18279
+ process.stdout.write(
18280
+ S2.blocked + " " + result.blocked + " action" + (result.blocked === 1 ? "" : "s") + " blocked by policy" + S2.reset + "\n"
18281
+ );
18282
+ }
18283
+ first = false;
18284
+ } finally {
18285
+ cleanupPromptDraft(draft);
18286
+ turn = null;
18287
+ }
18288
+ }
18289
+ } finally {
18290
+ process.off("SIGINT", interruptTurn);
18291
+ codex?.close();
17063
18292
  }
17064
- rl.close();
17065
18293
  return 0;
17066
18294
  }
17067
- var BANNER;
17068
18295
  var init_session = __esm({
17069
18296
  "cli/harness/session.ts"() {
17070
18297
  "use strict";
18298
+ init_composer();
17071
18299
  init_run();
18300
+ init_codex();
18301
+ init_fixPoll();
18302
+ init_identity2();
17072
18303
  init_render2();
17073
- BANNER = [
17074
- "",
17075
- S2.bold + " synkro" + S2.reset + S2.dim + " governed session" + S2.reset,
17076
- S2.dim + " every tool call passes Synkro policy before it runs" + S2.reset,
17077
- S2.dim + " ctrl-c to leave" + S2.reset,
17078
- ""
17079
- ];
17080
18304
  }
17081
18305
  });
17082
18306
 
@@ -17098,7 +18322,7 @@ async function takeover(kind, cwd) {
17098
18322
  const harness = ["claude", "codex", "cursor"].includes(kind) ? kind : "claude";
17099
18323
  const info = await detectContainerBackend();
17100
18324
  const reachable = info.backend === "container" ? (await run(info.runner, ["test", "-d", cwd])).ok : false;
17101
- const backend = reachable ? "container" : "host";
18325
+ const backend = harness === "codex" ? "host" : reachable ? "container" : "host";
17102
18326
  const spaceName = cwd.split("/").filter(Boolean).pop() || "space";
17103
18327
  const spawned = await spawnAgent(info, {
17104
18328
  name: spaceName + "-" + harness + "-" + String(process.pid % 1e4),
@@ -17126,7 +18350,7 @@ async function restoreSession(session) {
17126
18350
  harness: record.harness,
17127
18351
  spaceName: record.spaceName,
17128
18352
  cwd: record.space,
17129
- backend: record.backend,
18353
+ backend: record.harness === "codex" ? "host" : record.backend,
17130
18354
  resume: true
17131
18355
  });
17132
18356
  }
@@ -17269,11 +18493,11 @@ __export(linear_exports, {
17269
18493
  linearCommand: () => linearCommand
17270
18494
  });
17271
18495
  import { readFileSync as readFileSync33 } from "fs";
17272
- import { homedir as homedir38 } from "os";
17273
- import { join as join37 } from "path";
18496
+ import { homedir as homedir40 } from "os";
18497
+ import { join as join38 } from "path";
17274
18498
  function mcpJwt() {
17275
18499
  try {
17276
- return readFileSync33(join37(SYNKRO_DIR14, ".mcp-jwt"), "utf-8").trim();
18500
+ return readFileSync33(join38(SYNKRO_DIR14, ".mcp-jwt"), "utf-8").trim();
17277
18501
  } catch {
17278
18502
  return "";
17279
18503
  }
@@ -17312,7 +18536,7 @@ var SYNKRO_DIR14, PORT2, BASE;
17312
18536
  var init_linear = __esm({
17313
18537
  "cli/commands/linear.ts"() {
17314
18538
  "use strict";
17315
- SYNKRO_DIR14 = join37(homedir38(), ".synkro");
18539
+ SYNKRO_DIR14 = join38(homedir40(), ".synkro");
17316
18540
  PORT2 = process.env.SYNKRO_MCP_PORT || "18931";
17317
18541
  BASE = `http://127.0.0.1:${PORT2}`;
17318
18542
  }
@@ -17461,10 +18685,10 @@ var init_cveReachability = __esm({
17461
18685
  });
17462
18686
 
17463
18687
  // cli/reachability/reachabilityScan.ts
17464
- import { spawnSync as spawnSync12, execFileSync as execFileSync5 } from "child_process";
17465
- import { readFileSync as readFileSync35, writeFileSync as writeFileSync27, existsSync as existsSync38, readdirSync as readdirSync10 } from "fs";
17466
- import { join as join38 } from "path";
17467
- import { homedir as homedir39 } from "os";
18688
+ import { spawnSync as spawnSync13, execFileSync as execFileSync7 } from "child_process";
18689
+ import { readFileSync as readFileSync35, writeFileSync as writeFileSync28, existsSync as existsSync39, readdirSync as readdirSync10 } from "fs";
18690
+ import { join as join39 } from "path";
18691
+ import { homedir as homedir41 } from "os";
17468
18692
  import { createRequire } from "module";
17469
18693
  function walkSourceFiles(repoRoot3, maxFiles = 4e3, maxBytes = 5e5) {
17470
18694
  const SKIP2 = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "build", "coverage", ".next", ".turbo", "out", ".cache", ".synkro", ".claude", "vendor", "__tests__", "test-results"]);
@@ -17481,7 +18705,7 @@ function walkSourceFiles(repoRoot3, maxFiles = 4e3, maxBytes = 5e5) {
17481
18705
  }
17482
18706
  for (const e of ents) {
17483
18707
  if (files.length >= maxFiles) break;
17484
- const full = join38(dir, e.name);
18708
+ const full = join39(dir, e.name);
17485
18709
  if (e.isDirectory()) {
17486
18710
  if (!SKIP2.has(e.name) && !e.name.startsWith(".")) stack.push(full);
17487
18711
  continue;
@@ -17508,12 +18732,12 @@ function cleanVersion(spec) {
17508
18732
  function gatherManifestVersions(repoRoot3) {
17509
18733
  const out = {};
17510
18734
  const dirs = [repoRoot3];
17511
- const pkgsDir = join38(repoRoot3, "packages");
17512
- if (existsSync38(pkgsDir)) {
18735
+ const pkgsDir = join39(repoRoot3, "packages");
18736
+ if (existsSync39(pkgsDir)) {
17513
18737
  try {
17514
18738
  for (const d of readdirSync10(pkgsDir)) {
17515
- const pd = join38(pkgsDir, d);
17516
- if (existsSync38(join38(pd, "package.json"))) dirs.push(pd);
18739
+ const pd = join39(pkgsDir, d);
18740
+ if (existsSync39(join39(pd, "package.json"))) dirs.push(pd);
17517
18741
  }
17518
18742
  } catch {
17519
18743
  }
@@ -17522,7 +18746,7 @@ function gatherManifestVersions(repoRoot3) {
17522
18746
  for (const dir of dirs) {
17523
18747
  let pkg;
17524
18748
  try {
17525
- pkg = JSON.parse(readFileSync35(join38(dir, "package.json"), "utf8"));
18749
+ pkg = JSON.parse(readFileSync35(join39(dir, "package.json"), "utf8"));
17526
18750
  } catch {
17527
18751
  continue;
17528
18752
  }
@@ -17545,25 +18769,25 @@ function findJelly(repoRoot3) {
17545
18769
  const pkg = JSON.parse(readFileSync35(pkgJson, "utf8"));
17546
18770
  const bin = typeof pkg.bin === "string" ? pkg.bin : pkg.bin && (pkg.bin.jelly || pkg.bin[Object.keys(pkg.bin)[0]]);
17547
18771
  if (bin) {
17548
- const p = join38(dir, bin);
17549
- if (existsSync38(p)) return p;
18772
+ const p = join39(dir, bin);
18773
+ if (existsSync39(p)) return p;
17550
18774
  }
17551
18775
  } catch {
17552
18776
  }
17553
18777
  for (const base of [repoRoot3, process.cwd()]) {
17554
- const b = join38(base, "node_modules", ".bin", "jelly");
17555
- if (existsSync38(b)) return b;
18778
+ const b = join39(base, "node_modules", ".bin", "jelly");
18779
+ if (existsSync39(b)) return b;
17556
18780
  }
17557
18781
  return null;
17558
18782
  }
17559
18783
  function findEntries(repoRoot3) {
17560
18784
  const dirs = [repoRoot3];
17561
- const pkgsDir = join38(repoRoot3, "packages");
17562
- if (existsSync38(pkgsDir)) {
18785
+ const pkgsDir = join39(repoRoot3, "packages");
18786
+ if (existsSync39(pkgsDir)) {
17563
18787
  try {
17564
18788
  for (const d of readdirSync10(pkgsDir)) {
17565
- const pd = join38(pkgsDir, d);
17566
- if (existsSync38(join38(pd, "package.json"))) dirs.push(pd);
18789
+ const pd = join39(pkgsDir, d);
18790
+ if (existsSync39(join39(pd, "package.json"))) dirs.push(pd);
17567
18791
  }
17568
18792
  } catch {
17569
18793
  }
@@ -17571,12 +18795,12 @@ function findEntries(repoRoot3) {
17571
18795
  const entries = [];
17572
18796
  for (const dir of dirs) {
17573
18797
  try {
17574
- const pkg = JSON.parse(readFileSync35(join38(dir, "package.json"), "utf8"));
18798
+ const pkg = JSON.parse(readFileSync35(join39(dir, "package.json"), "utf8"));
17575
18799
  const cands = [pkg.source, pkg.module, pkg.main, "src/index.ts", "src/index.js", "src/main.ts", "src/server.ts", "index.ts", "index.js"];
17576
18800
  for (const c of cands) {
17577
18801
  if (typeof c !== "string") continue;
17578
- const f = join38(dir, c);
17579
- if (existsSync38(f)) {
18802
+ const f = join39(dir, c);
18803
+ if (existsSync39(f)) {
17580
18804
  entries.push(f);
17581
18805
  break;
17582
18806
  }
@@ -17588,7 +18812,7 @@ function findEntries(repoRoot3) {
17588
18812
  }
17589
18813
  function currentCommit(repoRoot3) {
17590
18814
  try {
17591
- return execFileSync5("git", ["rev-parse", "HEAD"], { cwd: repoRoot3, encoding: "utf8" }).trim();
18815
+ return execFileSync7("git", ["rev-parse", "HEAD"], { cwd: repoRoot3, encoding: "utf8" }).trim();
17592
18816
  } catch {
17593
18817
  return "";
17594
18818
  }
@@ -17609,7 +18833,7 @@ function parseApiUsage(log) {
17609
18833
  }
17610
18834
  function runReachabilityScan(repoRoot3, opts = {}) {
17611
18835
  const commit = currentCommit(repoRoot3);
17612
- if (!opts.force && commit && existsSync38(REACHABILITY_PATH)) {
18836
+ if (!opts.force && commit && existsSync39(REACHABILITY_PATH)) {
17613
18837
  try {
17614
18838
  const prev = JSON.parse(readFileSync35(REACHABILITY_PATH, "utf8"));
17615
18839
  if (prev.commit === commit) return { ok: true, cached: true, packages: Object.keys(prev.packages || {}).length };
@@ -17662,7 +18886,7 @@ function runReachabilityScan(repoRoot3, opts = {}) {
17662
18886
  if (jelly) {
17663
18887
  const entries = findEntries(repoRoot3);
17664
18888
  if (entries.length > 0) {
17665
- const r = spawnSync12(
18889
+ const r = spawnSync13(
17666
18890
  process.execPath,
17667
18891
  [jelly, "-b", repoRoot3, "--api-usage", ...entries],
17668
18892
  { encoding: "utf8", timeout: opts.timeoutMs ?? 18e4, maxBuffer: 2e8 }
@@ -17700,7 +18924,7 @@ function runReachabilityScan(repoRoot3, opts = {}) {
17700
18924
  if (Object.keys(packages).length === 0) return { ok: false, reason: "no package usage found (no jelly output, no AST imports)" };
17701
18925
  const file = { generatedAt: (/* @__PURE__ */ new Date()).toISOString(), commit, tool, packages, versions: gatherManifestVersions(repoRoot3) };
17702
18926
  try {
17703
- writeFileSync27(REACHABILITY_PATH, JSON.stringify(file, null, 2));
18927
+ writeFileSync28(REACHABILITY_PATH, JSON.stringify(file, null, 2));
17704
18928
  } catch (e) {
17705
18929
  return { ok: false, reason: "write failed: " + String(e.message || e) };
17706
18930
  }
@@ -17712,7 +18936,7 @@ var init_reachabilityScan = __esm({
17712
18936
  "use strict";
17713
18937
  init_cveReachability();
17714
18938
  require2 = createRequire(import.meta.url);
17715
- REACHABILITY_PATH = join38(homedir39(), ".synkro", "reachability.json");
18939
+ REACHABILITY_PATH = join39(homedir41(), ".synkro", "reachability.json");
17716
18940
  }
17717
18941
  });
17718
18942
 
@@ -17721,13 +18945,13 @@ var reachabilityScan_exports = {};
17721
18945
  __export(reachabilityScan_exports, {
17722
18946
  reachabilityScanCommand: () => reachabilityScanCommand
17723
18947
  });
17724
- import { readFileSync as readFileSync36, existsSync as existsSync39 } from "fs";
17725
- import { join as join39 } from "path";
17726
- import { homedir as homedir40 } from "os";
17727
- import { execFileSync as execFileSync6 } from "child_process";
18948
+ import { readFileSync as readFileSync36, existsSync as existsSync40 } from "fs";
18949
+ import { join as join40 } from "path";
18950
+ import { homedir as homedir42 } from "os";
18951
+ import { execFileSync as execFileSync8 } from "child_process";
17728
18952
  function readConfigEnv4() {
17729
- const p = join39(SYNKRO_DIR15, "config.env");
17730
- if (!existsSync39(p)) return {};
18953
+ const p = join40(SYNKRO_DIR15, "config.env");
18954
+ if (!existsSync40(p)) return {};
17731
18955
  const out = {};
17732
18956
  for (const line of readFileSync36(p, "utf-8").split("\n")) {
17733
18957
  const t = line.trim();
@@ -17739,7 +18963,7 @@ function readConfigEnv4() {
17739
18963
  }
17740
18964
  function repoRoot2() {
17741
18965
  try {
17742
- return execFileSync6("git", ["rev-parse", "--show-toplevel"], { encoding: "utf-8" }).trim();
18966
+ return execFileSync8("git", ["rev-parse", "--show-toplevel"], { encoding: "utf-8" }).trim();
17743
18967
  } catch {
17744
18968
  return process.cwd();
17745
18969
  }
@@ -17747,7 +18971,7 @@ function repoRoot2() {
17747
18971
  function repoSlug(root) {
17748
18972
  const run2 = (a) => {
17749
18973
  try {
17750
- return execFileSync6("git", a, { encoding: "utf-8" }).trim();
18974
+ return execFileSync8("git", a, { encoding: "utf-8" }).trim();
17751
18975
  } catch {
17752
18976
  return "";
17753
18977
  }
@@ -17761,10 +18985,10 @@ async function pushToCloud(cfg, repo) {
17761
18985
  while (gwBase.endsWith("/")) gwBase = gwBase.slice(0, -1);
17762
18986
  let jwt2 = "";
17763
18987
  try {
17764
- jwt2 = readFileSync36(join39(SYNKRO_DIR15, ".mcp-jwt"), "utf-8").trim();
18988
+ jwt2 = readFileSync36(join40(SYNKRO_DIR15, ".mcp-jwt"), "utf-8").trim();
17765
18989
  } catch {
17766
18990
  }
17767
- if (!jwt2 || !existsSync39(REACHABILITY_PATH)) return;
18991
+ if (!jwt2 || !existsSync40(REACHABILITY_PATH)) return;
17768
18992
  const body = readFileSync36(REACHABILITY_PATH, "utf-8");
17769
18993
  try {
17770
18994
  const resp = await fetch(gwBase + "/api/v1/reachability?repo=" + encodeURIComponent(repo), {
@@ -17797,7 +19021,7 @@ var init_reachabilityScan2 = __esm({
17797
19021
  "cli/commands/reachabilityScan.ts"() {
17798
19022
  "use strict";
17799
19023
  init_reachabilityScan();
17800
- SYNKRO_DIR15 = join39(homedir40(), ".synkro");
19024
+ SYNKRO_DIR15 = join40(homedir42(), ".synkro");
17801
19025
  }
17802
19026
  });
17803
19027
 
@@ -17927,11 +19151,11 @@ var config_exports = {};
17927
19151
  __export(config_exports, {
17928
19152
  configCommand: () => configCommand
17929
19153
  });
17930
- import { readFileSync as readFileSync37, writeFileSync as writeFileSync28, existsSync as existsSync40 } from "fs";
17931
- import { join as join40 } from "path";
17932
- import { homedir as homedir41 } from "os";
19154
+ import { readFileSync as readFileSync37, writeFileSync as writeFileSync29, existsSync as existsSync41 } from "fs";
19155
+ import { join as join41 } from "path";
19156
+ import { homedir as homedir43 } from "os";
17933
19157
  function readConfigEnv5() {
17934
- if (!existsSync40(CONFIG_PATH9)) return {};
19158
+ if (!existsSync41(CONFIG_PATH9)) return {};
17935
19159
  const out = {};
17936
19160
  for (const line of readFileSync37(CONFIG_PATH9, "utf-8").split("\n")) {
17937
19161
  const t = line.trim();
@@ -17942,7 +19166,7 @@ function readConfigEnv5() {
17942
19166
  return out;
17943
19167
  }
17944
19168
  function updateConfigValue(key, value) {
17945
- if (!existsSync40(CONFIG_PATH9)) {
19169
+ if (!existsSync41(CONFIG_PATH9)) {
17946
19170
  console.error("No config found. Run `synkro install` first.");
17947
19171
  process.exit(1);
17948
19172
  }
@@ -17957,7 +19181,7 @@ function updateConfigValue(key, value) {
17957
19181
  return line;
17958
19182
  });
17959
19183
  if (!found) updated.splice(updated.length - 1, 0, `${key}='${value}'`);
17960
- writeFileSync28(CONFIG_PATH9, updated.join("\n"), "utf-8");
19184
+ writeFileSync29(CONFIG_PATH9, updated.join("\n"), "utf-8");
17961
19185
  }
17962
19186
  function resolveInferenceMode(cfg) {
17963
19187
  if ((cfg.SYNKRO_GRADING_MODE || "local") === "byok") return "byok";
@@ -18115,8 +19339,8 @@ var init_config = __esm({
18115
19339
  "use strict";
18116
19340
  init_stub();
18117
19341
  init_optout();
18118
- SYNKRO_DIR16 = join40(homedir41(), ".synkro");
18119
- CONFIG_PATH9 = join40(SYNKRO_DIR16, "config.env");
19342
+ SYNKRO_DIR16 = join41(homedir43(), ".synkro");
19343
+ CONFIG_PATH9 = join41(SYNKRO_DIR16, "config.env");
18120
19344
  }
18121
19345
  });
18122
19346
 
@@ -18206,12 +19430,12 @@ async function runExport(args2) {
18206
19430
  }
18207
19431
  function confirmYesNo(question) {
18208
19432
  if (!process.stdin.isTTY) return Promise.resolve(false);
18209
- return new Promise((resolve7) => {
19433
+ return new Promise((resolve8) => {
18210
19434
  const rl = createInterface6({ input: process.stdin, output: process.stdout });
18211
19435
  rl.question(`${question} (y/N): `, (answer) => {
18212
19436
  rl.close();
18213
19437
  const t = answer.trim().toLowerCase();
18214
- resolve7(t === "y" || t === "yes");
19438
+ resolve8(t === "y" || t === "yes");
18215
19439
  });
18216
19440
  });
18217
19441
  }
@@ -18306,11 +19530,11 @@ Usage:
18306
19530
 
18307
19531
  // cli/inventory/identity.ts
18308
19532
  import { randomUUID as randomUUID5 } from "crypto";
18309
- import { existsSync as existsSync41, mkdirSync as mkdirSync24, readFileSync as readFileSync38, renameSync as renameSync9, writeFileSync as writeFileSync29 } from "fs";
18310
- import { homedir as homedir42 } from "os";
18311
- import { dirname as dirname12, join as join41 } from "path";
19533
+ import { existsSync as existsSync42, mkdirSync as mkdirSync24, readFileSync as readFileSync38, renameSync as renameSync9, writeFileSync as writeFileSync30 } from "fs";
19534
+ import { homedir as homedir44 } from "os";
19535
+ import { dirname as dirname13, join as join42 } from "path";
18312
19536
  function operationalIdentityPath() {
18313
- return process.env.SYNKRO_OPERATIONAL_IDENTITY_PATH || join41(homedir42(), ".synkro", "installation.json");
19537
+ return process.env.SYNKRO_OPERATIONAL_IDENTITY_PATH || join42(homedir44(), ".synkro", "installation.json");
18314
19538
  }
18315
19539
  function validIdentity(value) {
18316
19540
  if (!value || typeof value !== "object") return false;
@@ -18318,15 +19542,15 @@ function validIdentity(value) {
18318
19542
  return typeof row2.installation_id === "string" && UUID_RE.test(row2.installation_id) && typeof row2.created_at === "string" && Number.isFinite(Date.parse(row2.created_at));
18319
19543
  }
18320
19544
  function writeIdentity(path, identity) {
18321
- mkdirSync24(dirname12(path), { recursive: true, mode: 448 });
19545
+ mkdirSync24(dirname13(path), { recursive: true, mode: 448 });
18322
19546
  const temp = `${path}.${process.pid}.${randomUUID5()}.tmp`;
18323
- writeFileSync29(temp, JSON.stringify(identity, null, 2) + "\n", { encoding: "utf8", mode: 384 });
19547
+ writeFileSync30(temp, JSON.stringify(identity, null, 2) + "\n", { encoding: "utf8", mode: 384 });
18324
19548
  renameSync9(temp, path);
18325
19549
  }
18326
19550
  function getOperationalInstallationIdentity(path = operationalIdentityPath()) {
18327
19551
  const prior = cached4.get(path);
18328
19552
  if (prior) return prior;
18329
- if (existsSync41(path)) {
19553
+ if (existsSync42(path)) {
18330
19554
  try {
18331
19555
  const parsed = JSON.parse(readFileSync38(path, "utf8"));
18332
19556
  if (validIdentity(parsed)) {
@@ -18342,7 +19566,7 @@ function getOperationalInstallationIdentity(path = operationalIdentityPath()) {
18342
19566
  return identity;
18343
19567
  }
18344
19568
  var UUID_RE, cached4;
18345
- var init_identity2 = __esm({
19569
+ var init_identity3 = __esm({
18346
19570
  "cli/inventory/identity.ts"() {
18347
19571
  "use strict";
18348
19572
  UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
@@ -18353,13 +19577,13 @@ var init_identity2 = __esm({
18353
19577
  // cli/inventory/collector.ts
18354
19578
  import { createHash as createHash5 } from "crypto";
18355
19579
  import {
18356
- existsSync as existsSync42,
19580
+ existsSync as existsSync43,
18357
19581
  readFileSync as readFileSync39,
18358
19582
  readdirSync as readdirSync11,
18359
- statSync as statSync5
19583
+ statSync as statSync6
18360
19584
  } from "fs";
18361
- import { arch, homedir as homedir43, hostname as hostname2, platform as platform6, release as release2 } from "os";
18362
- import { basename as basename3, join as join42, relative, resolve as resolve5 } from "path";
19585
+ import { arch, homedir as homedir45, hostname as hostname2, platform as platform6, release as release2 } from "os";
19586
+ import { basename as basename4, join as join43, relative, resolve as resolve6 } from "path";
18363
19587
  import { fileURLToPath } from "url";
18364
19588
  function sha256(value) {
18365
19589
  return createHash5("sha256").update(value).digest("hex");
@@ -18369,14 +19593,14 @@ function pseudonymousHostnameHash(installationId, host) {
18369
19593
  }
18370
19594
  function cliVersion() {
18371
19595
  try {
18372
- return "1.10.5";
19596
+ return "1.10.8";
18373
19597
  } catch {
18374
19598
  return "0.0.0";
18375
19599
  }
18376
19600
  }
18377
19601
  function readJson(path) {
18378
19602
  try {
18379
- if (!existsSync42(path)) return null;
19603
+ if (!existsSync43(path)) return null;
18380
19604
  const parsed = JSON.parse(readFileSync39(path, "utf8"));
18381
19605
  return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
18382
19606
  } catch {
@@ -18385,7 +19609,7 @@ function readJson(path) {
18385
19609
  }
18386
19610
  function readText(path) {
18387
19611
  try {
18388
- if (!existsSync42(path)) return "";
19612
+ if (!existsSync43(path)) return "";
18389
19613
  return readFileSync39(path, "utf8");
18390
19614
  } catch {
18391
19615
  return "";
@@ -18406,7 +19630,7 @@ function canonical(raw) {
18406
19630
  }
18407
19631
  function safePackageName(command, args2) {
18408
19632
  if (typeof command !== "string" || !command.trim()) return void 0;
18409
- const runner = basename3(command.trim()).replace(/\.exe$/i, "");
19633
+ const runner = basename4(command.trim()).replace(/\.exe$/i, "");
18410
19634
  if (Array.isArray(args2) && ["npx", "bunx", "uvx"].includes(runner)) {
18411
19635
  const pkg = args2.find((arg) => typeof arg === "string" && !arg.startsWith("-"));
18412
19636
  if (typeof pkg === "string") {
@@ -18414,7 +19638,7 @@ function safePackageName(command, args2) {
18414
19638
  if (/^(?:@[a-z0-9_.-]+\/)?[a-z0-9_.-]+(?:@[a-z0-9_.+~-]+)?$/i.test(candidate)) {
18415
19639
  return candidate;
18416
19640
  }
18417
- return basename3(candidate);
19641
+ return basename4(candidate);
18418
19642
  }
18419
19643
  }
18420
19644
  return runner;
@@ -18472,16 +19696,16 @@ function mcpArtifactsFromJson(harness, config, configScope = "user") {
18472
19696
  }
18473
19697
  function claudeDesktopConfigCandidates(home, targetPlatform) {
18474
19698
  if (targetPlatform === "darwin") {
18475
- return [join42(home, "Library", "Application Support", "Claude", "claude_desktop_config.json")];
19699
+ return [join43(home, "Library", "Application Support", "Claude", "claude_desktop_config.json")];
18476
19700
  }
18477
19701
  if (targetPlatform === "linux") {
18478
19702
  return [
18479
- join42(home, ".config", "Claude", "claude_desktop_config.json"),
18480
- join42(home, ".config", "claude", "claude_desktop_config.json")
19703
+ join43(home, ".config", "Claude", "claude_desktop_config.json"),
19704
+ join43(home, ".config", "claude", "claude_desktop_config.json")
18481
19705
  ];
18482
19706
  }
18483
19707
  if (targetPlatform === "win32" && process.env.APPDATA) {
18484
- return [join42(process.env.APPDATA, "Claude", "claude_desktop_config.json")];
19708
+ return [join43(process.env.APPDATA, "Claude", "claude_desktop_config.json")];
18485
19709
  }
18486
19710
  return [];
18487
19711
  }
@@ -18489,7 +19713,7 @@ function claudeManagedMcpConfigCandidates(targetPlatform) {
18489
19713
  if (targetPlatform === "darwin") return ["/Library/Application Support/ClaudeCode/managed-mcp.json"];
18490
19714
  if (targetPlatform === "linux") return ["/etc/claude-code/managed-mcp.json"];
18491
19715
  if (targetPlatform === "win32" && process.env.ProgramFiles) {
18492
- return [join42(process.env.ProgramFiles, "ClaudeCode", "managed-mcp.json")];
19716
+ return [join43(process.env.ProgramFiles, "ClaudeCode", "managed-mcp.json")];
18493
19717
  }
18494
19718
  return [];
18495
19719
  }
@@ -18497,8 +19721,8 @@ function discoveredProjectRoots(claudeState, currentDirectory, explicit = [], cu
18497
19721
  const roots = /* @__PURE__ */ new Set();
18498
19722
  const add = (value) => {
18499
19723
  if (typeof value !== "string" || !value.trim()) return;
18500
- const path = resolve5(value);
18501
- if (existsSync42(path)) roots.add(path);
19724
+ const path = resolve6(value);
19725
+ if (existsSync43(path)) roots.add(path);
18502
19726
  };
18503
19727
  add(currentDirectory);
18504
19728
  for (const path of explicit) add(path);
@@ -18509,17 +19733,17 @@ function discoveredProjectRoots(claudeState, currentDirectory, explicit = [], cu
18509
19733
  return [...roots];
18510
19734
  }
18511
19735
  function cursorWorkspaceStorageCandidates(home, targetPlatform) {
18512
- if (targetPlatform === "darwin") return [join42(home, "Library", "Application Support", "Cursor", "User", "workspaceStorage")];
18513
- if (targetPlatform === "linux") return [join42(home, ".config", "Cursor", "User", "workspaceStorage")];
19736
+ if (targetPlatform === "darwin") return [join43(home, "Library", "Application Support", "Cursor", "User", "workspaceStorage")];
19737
+ if (targetPlatform === "linux") return [join43(home, ".config", "Cursor", "User", "workspaceStorage")];
18514
19738
  if (targetPlatform === "win32" && process.env.APPDATA) {
18515
- return [join42(process.env.APPDATA, "Cursor", "User", "workspaceStorage")];
19739
+ return [join43(process.env.APPDATA, "Cursor", "User", "workspaceStorage")];
18516
19740
  }
18517
19741
  return [];
18518
19742
  }
18519
19743
  function cursorWorkspaceRoots(home, targetPlatform) {
18520
19744
  const roots = /* @__PURE__ */ new Set();
18521
19745
  for (const storage of cursorWorkspaceStorageCandidates(home, targetPlatform)) {
18522
- if (!existsSync42(storage)) continue;
19746
+ if (!existsSync43(storage)) continue;
18523
19747
  let entries = [];
18524
19748
  try {
18525
19749
  entries = readdirSync11(storage, { withFileTypes: true });
@@ -18528,12 +19752,12 @@ function cursorWorkspaceRoots(home, targetPlatform) {
18528
19752
  }
18529
19753
  for (const entry of entries) {
18530
19754
  if (!entry.isDirectory() || entry.isSymbolicLink?.()) continue;
18531
- const state = readJson(join42(storage, entry.name, "workspace.json"));
19755
+ const state = readJson(join43(storage, entry.name, "workspace.json"));
18532
19756
  const raw = state?.folder;
18533
19757
  if (typeof raw !== "string" || !raw.trim()) continue;
18534
19758
  try {
18535
19759
  const path = raw.startsWith("file:") ? fileURLToPath(raw) : raw;
18536
- if (existsSync42(path)) roots.add(resolve5(path));
19760
+ if (existsSync43(path)) roots.add(resolve6(path));
18537
19761
  } catch {
18538
19762
  }
18539
19763
  }
@@ -18609,10 +19833,10 @@ function hookArtifacts(harness, config) {
18609
19833
  harness,
18610
19834
  type: "hook",
18611
19835
  canonical_id: `${canonical(event)}:${commandHash.slice(0, 20)}`,
18612
- display_name: `${event} \xB7 ${basename3(String(entry.command).split(/\s+/)[0] || "hook")}`,
19836
+ display_name: `${event} \xB7 ${basename4(String(entry.command).split(/\s+/)[0] || "hook")}`,
18613
19837
  enabled: entry.enabled !== false,
18614
19838
  config_scope: "user",
18615
- package_name: basename3(String(entry.command).split(/\s+/)[0] || "") || void 0,
19839
+ package_name: basename4(String(entry.command).split(/\s+/)[0] || "") || void 0,
18616
19840
  config_hash: commandHash,
18617
19841
  metadata: { managed, events: [event] }
18618
19842
  });
@@ -18627,7 +19851,7 @@ function parseFrontmatter(content) {
18627
19851
  return { name: value("name"), version: value("version") };
18628
19852
  }
18629
19853
  function skillArtifacts(harness, root) {
18630
- if (!existsSync42(root)) return [];
19854
+ if (!existsSync43(root)) return [];
18631
19855
  const manifests = [];
18632
19856
  const visit = (dir) => {
18633
19857
  let entries;
@@ -18638,7 +19862,7 @@ function skillArtifacts(harness, root) {
18638
19862
  }
18639
19863
  for (const entry of entries) {
18640
19864
  if (entry.isSymbolicLink?.()) continue;
18641
- const path = join42(dir, entry.name);
19865
+ const path = join43(dir, entry.name);
18642
19866
  if (entry.isFile() && entry.name === "SKILL.md") manifests.push(path);
18643
19867
  else if (entry.isDirectory()) visit(path);
18644
19868
  }
@@ -18648,7 +19872,7 @@ function skillArtifacts(harness, root) {
18648
19872
  const content = readText(path);
18649
19873
  const frontmatter = parseFrontmatter(content);
18650
19874
  const rel = relative(root, path).replaceAll("\\", "/");
18651
- const name = frontmatter.name || basename3(join42(path, "..")) || "skill";
19875
+ const name = frontmatter.name || basename4(join43(path, "..")) || "skill";
18652
19876
  return {
18653
19877
  harness,
18654
19878
  type: "skill",
@@ -18663,7 +19887,7 @@ function skillArtifacts(harness, root) {
18663
19887
  });
18664
19888
  }
18665
19889
  function cursorExtensionArtifacts(root) {
18666
- if (!existsSync42(root)) return [];
19890
+ if (!existsSync43(root)) return [];
18667
19891
  let dirs = [];
18668
19892
  try {
18669
19893
  dirs = readdirSync11(root, { withFileTypes: true }).filter((entry) => entry.isDirectory() && !entry.isSymbolicLink());
@@ -18672,7 +19896,7 @@ function cursorExtensionArtifacts(root) {
18672
19896
  }
18673
19897
  const artifacts = [];
18674
19898
  for (const dir of dirs) {
18675
- const pkg = readJson(join42(root, dir.name, "package.json"));
19899
+ const pkg = readJson(join43(root, dir.name, "package.json"));
18676
19900
  if (!pkg) continue;
18677
19901
  const publisher = typeof pkg.publisher === "string" ? pkg.publisher : void 0;
18678
19902
  const name = typeof pkg.name === "string" ? pkg.name : dir.name;
@@ -18692,20 +19916,20 @@ function cursorExtensionArtifacts(root) {
18692
19916
  return artifacts;
18693
19917
  }
18694
19918
  function deploymentMode2(home) {
18695
- const raw = readText(join42(home, ".synkro", "config.env"));
19919
+ const raw = readText(join43(home, ".synkro", "config.env"));
18696
19920
  const value = (key) => raw.match(new RegExp(`^${key}=['"]?([^'"\\n]*)`, "m"))?.[1]?.toLowerCase();
18697
19921
  if (value("SYNKRO_GRADING_MODE") === "byok") return "byok";
18698
19922
  if (value("SYNKRO_STORAGE_MODE") === "cloud") return "cloud";
18699
19923
  return "local";
18700
19924
  }
18701
19925
  function telemetryHealth(home) {
18702
- const meta = readJson(join42(home, ".synkro", "telemetry-meta.json"));
19926
+ const meta = readJson(join43(home, ".synkro", "telemetry-meta.json"));
18703
19927
  const health = {};
18704
19928
  if (meta?.last_flush_ok_at && Number.isFinite(Date.parse(meta.last_flush_ok_at))) health.telemetry_last_flush_at = meta.last_flush_ok_at;
18705
19929
  if (meta?.last_flush_error) health.telemetry_last_error = "flush_failed";
18706
- const queue = join42(home, ".synkro", "telemetry-pending.jsonl");
19930
+ const queue = join43(home, ".synkro", "telemetry-pending.jsonl");
18707
19931
  try {
18708
- const size = statSync5(queue).size;
19932
+ const size = statSync6(queue).size;
18709
19933
  health.telemetry_backlog = size <= 5 * 1024 * 1024 ? readFileSync39(queue, "utf8").split("\n").filter(Boolean).length : Math.ceil(size / 1024);
18710
19934
  } catch {
18711
19935
  }
@@ -18747,7 +19971,7 @@ function harnessSnapshot(agent) {
18747
19971
  }
18748
19972
  const config = readJson(agent.settingsPath);
18749
19973
  const coverage = inspectCodexHooks(agent.settingsPath);
18750
- const toml = readText(join42(agent.configDir, "config.toml"));
19974
+ const toml = readText(join43(agent.configDir, "config.toml"));
18751
19975
  const permission = toml.match(/^\s*approval_policy\s*=\s*["']([^"']+)/m)?.[1];
18752
19976
  return {
18753
19977
  row: {
@@ -18764,11 +19988,11 @@ function harnessSnapshot(agent) {
18764
19988
  };
18765
19989
  }
18766
19990
  function collectOperationalInventory(options = {}) {
18767
- const home = options.homeDir ?? homedir43();
19991
+ const home = options.homeDir ?? homedir45();
18768
19992
  const detected = options.detectedAgents ?? detectAgents();
18769
19993
  const identity = getOperationalInstallationIdentity(options.identityPath);
18770
19994
  const targetPlatform = options.platformName ?? platform6();
18771
- const codexHome = options.homeDir ? join42(home, ".codex") : process.env.CODEX_HOME || join42(home, ".codex");
19995
+ const codexHome = options.homeDir ? join43(home, ".codex") : process.env.CODEX_HOME || join43(home, ".codex");
18772
19996
  const harnesses = [];
18773
19997
  const artifacts = [];
18774
19998
  for (const agent of detected) {
@@ -18776,7 +20000,7 @@ function collectOperationalInventory(options = {}) {
18776
20000
  harnesses.push(row2);
18777
20001
  artifacts.push(...hookArtifacts(row2.harness, config));
18778
20002
  }
18779
- const claudeJson = readJson(join42(home, ".claude.json"));
20003
+ const claudeJson = readJson(join43(home, ".claude.json"));
18780
20004
  artifacts.push(...mcpArtifactsFromJson("claude_code", claudeJson));
18781
20005
  if (claudeJson?.projects && typeof claudeJson.projects === "object") {
18782
20006
  for (const [projectPath, project] of Object.entries(claudeJson.projects)) {
@@ -18784,8 +20008,8 @@ function collectOperationalInventory(options = {}) {
18784
20008
  artifacts.push(...mcpArtifactsFromJson("claude_code", project, `local:${sha256(projectPath).slice(0, 16)}`));
18785
20009
  }
18786
20010
  }
18787
- artifacts.push(...mcpArtifactsFromJson("cursor", readJson(join42(home, ".cursor", "mcp.json"))));
18788
- artifacts.push(...codexMcpArtifacts(readText(join42(codexHome, "config.toml"))));
20011
+ artifacts.push(...mcpArtifactsFromJson("cursor", readJson(join43(home, ".cursor", "mcp.json"))));
20012
+ artifacts.push(...codexMcpArtifacts(readText(join43(codexHome, "config.toml"))));
18789
20013
  const projectRoots = discoveredProjectRoots(
18790
20014
  claudeJson,
18791
20015
  options.currentDirectory ?? process.cwd(),
@@ -18796,11 +20020,11 @@ function collectOperationalInventory(options = {}) {
18796
20020
  const scopeHash = sha256(projectRoot).slice(0, 16);
18797
20021
  artifacts.push(...mcpArtifactsFromJson(
18798
20022
  "claude_code",
18799
- readJson(join42(projectRoot, ".mcp.json")),
20023
+ readJson(join43(projectRoot, ".mcp.json")),
18800
20024
  `project:${scopeHash}`
18801
20025
  ));
18802
- const cursorProjectConfig = join42(projectRoot, ".cursor", "mcp.json");
18803
- if (resolve5(cursorProjectConfig) !== resolve5(join42(home, ".cursor", "mcp.json"))) {
20026
+ const cursorProjectConfig = join43(projectRoot, ".cursor", "mcp.json");
20027
+ if (resolve6(cursorProjectConfig) !== resolve6(join43(home, ".cursor", "mcp.json"))) {
18804
20028
  artifacts.push(...mcpArtifactsFromJson(
18805
20029
  "cursor",
18806
20030
  readJson(cursorProjectConfig),
@@ -18811,7 +20035,7 @@ function collectOperationalInventory(options = {}) {
18811
20035
  for (const managedPath of claudeManagedMcpConfigCandidates(targetPlatform)) {
18812
20036
  artifacts.push(...mcpArtifactsFromJson("claude_code", readJson(managedPath), "managed"));
18813
20037
  }
18814
- const desktopConfigPath = claudeDesktopConfigCandidates(home, targetPlatform).find((path) => existsSync42(path));
20038
+ const desktopConfigPath = claudeDesktopConfigCandidates(home, targetPlatform).find((path) => existsSync43(path));
18815
20039
  if (desktopConfigPath) {
18816
20040
  const desktopConfig = readJson(desktopConfigPath);
18817
20041
  harnesses.push({
@@ -18822,7 +20046,7 @@ function collectOperationalInventory(options = {}) {
18822
20046
  });
18823
20047
  artifacts.push(...mcpArtifactsFromJson("claude_desktop", desktopConfig));
18824
20048
  }
18825
- const claudeSettings = readJson(join42(home, ".claude", "settings.json"));
20049
+ const claudeSettings = readJson(join43(home, ".claude", "settings.json"));
18826
20050
  if (claudeSettings?.enabledPlugins && typeof claudeSettings.enabledPlugins === "object") {
18827
20051
  for (const [name, enabled] of Object.entries(claudeSettings.enabledPlugins)) {
18828
20052
  artifacts.push({
@@ -18836,10 +20060,10 @@ function collectOperationalInventory(options = {}) {
18836
20060
  });
18837
20061
  }
18838
20062
  }
18839
- artifacts.push(...skillArtifacts("claude_code", join42(home, ".claude", "skills")));
18840
- artifacts.push(...skillArtifacts("cursor", join42(home, ".cursor", "skills")));
18841
- artifacts.push(...skillArtifacts("codex", join42(codexHome, "skills")));
18842
- artifacts.push(...cursorExtensionArtifacts(join42(home, ".cursor", "extensions")));
20063
+ artifacts.push(...skillArtifacts("claude_code", join43(home, ".claude", "skills")));
20064
+ artifacts.push(...skillArtifacts("cursor", join43(home, ".cursor", "skills")));
20065
+ artifacts.push(...skillArtifacts("codex", join43(codexHome, "skills")));
20066
+ artifacts.push(...cursorExtensionArtifacts(join43(home, ".cursor", "extensions")));
18843
20067
  const uniqueArtifacts = /* @__PURE__ */ new Map();
18844
20068
  for (const artifact of artifacts) {
18845
20069
  const key = `${artifact.harness || "global"}:${artifact.type}:${artifact.canonical_id}`;
@@ -18876,7 +20100,7 @@ var init_collector = __esm({
18876
20100
  init_ccHookConfig();
18877
20101
  init_cursorHookConfig();
18878
20102
  init_codexHookConfig();
18879
- init_identity2();
20103
+ init_identity3();
18880
20104
  }
18881
20105
  });
18882
20106
 
@@ -18892,18 +20116,18 @@ __export(sync_exports2, {
18892
20116
  syncOperationalInventoryDetached: () => syncOperationalInventoryDetached
18893
20117
  });
18894
20118
  import { createHash as createHash6, randomUUID as randomUUID6 } from "crypto";
18895
- import { spawn as spawn10 } from "child_process";
20119
+ import { spawn as spawn11 } from "child_process";
18896
20120
  import {
18897
- existsSync as existsSync43,
20121
+ existsSync as existsSync44,
18898
20122
  mkdirSync as mkdirSync25,
18899
20123
  readFileSync as readFileSync40,
18900
20124
  renameSync as renameSync10,
18901
- writeFileSync as writeFileSync30
20125
+ writeFileSync as writeFileSync31
18902
20126
  } from "fs";
18903
- import { homedir as homedir44 } from "os";
18904
- import { dirname as dirname13, join as join43 } from "path";
20127
+ import { homedir as homedir46 } from "os";
20128
+ import { dirname as dirname14, join as join44 } from "path";
18905
20129
  function syncStatePath() {
18906
- return process.env.SYNKRO_INVENTORY_SYNC_STATE_PATH || join43(homedir44(), ".synkro", "inventory-sync.json");
20130
+ return process.env.SYNKRO_INVENTORY_SYNC_STATE_PATH || join44(homedir46(), ".synkro", "inventory-sync.json");
18907
20131
  }
18908
20132
  function readState(path = syncStatePath()) {
18909
20133
  try {
@@ -18915,9 +20139,9 @@ function readState(path = syncStatePath()) {
18915
20139
  }
18916
20140
  function writeState(state, path = syncStatePath()) {
18917
20141
  try {
18918
- mkdirSync25(dirname13(path), { recursive: true, mode: 448 });
20142
+ mkdirSync25(dirname14(path), { recursive: true, mode: 448 });
18919
20143
  const temp = `${path}.${process.pid}.tmp`;
18920
- writeFileSync30(temp, JSON.stringify(state, null, 2) + "\n", { encoding: "utf8", mode: 384 });
20144
+ writeFileSync31(temp, JSON.stringify(state, null, 2) + "\n", { encoding: "utf8", mode: 384 });
18921
20145
  renameSync10(temp, path);
18922
20146
  } catch {
18923
20147
  }
@@ -18930,7 +20154,7 @@ function shouldSyncInventory(state, now = Date.now(), target) {
18930
20154
  return !Number.isFinite(lastAttempt) || lastAttempt <= 0 || now - lastAttempt >= FAILURE_RETRY_MS;
18931
20155
  }
18932
20156
  function readConfig() {
18933
- const path = join43(homedir44(), ".synkro", "config.env");
20157
+ const path = join44(homedir46(), ".synkro", "config.env");
18934
20158
  const out = {};
18935
20159
  try {
18936
20160
  for (const rawLine of readFileSync40(path, "utf8").split("\n")) {
@@ -18972,7 +20196,7 @@ function resolveInventoryGateway(raw) {
18972
20196
  }
18973
20197
  async function loadToken() {
18974
20198
  try {
18975
- const durable = readFileSync40(join43(homedir44(), ".synkro", ".mcp-jwt"), "utf8").trim();
20199
+ const durable = readFileSync40(join44(homedir46(), ".synkro", ".mcp-jwt"), "utf8").trim();
18976
20200
  if (durable) return durable;
18977
20201
  } catch {
18978
20202
  }
@@ -19090,8 +20314,8 @@ function syncOperationalInventoryDetached() {
19090
20314
  writeState({ ...state, last_attempt_at: (/* @__PURE__ */ new Date()).toISOString(), last_target: target }, path);
19091
20315
  try {
19092
20316
  const script = process.argv[1];
19093
- if (!script || !existsSync43(script)) return;
19094
- const child = spawn10(process.execPath, [script, "inventory-sync", "--detached"], {
20317
+ if (!script || !existsSync44(script)) return;
20318
+ const child = spawn11(process.execPath, [script, "inventory-sync", "--detached"], {
19095
20319
  detached: true,
19096
20320
  stdio: "ignore",
19097
20321
  env: { ...process.env, SYNKRO_INVENTORY_DETACHED: "1" }
@@ -19114,14 +20338,14 @@ var init_sync2 = __esm({
19114
20338
  });
19115
20339
 
19116
20340
  // cli/bootstrap.js
19117
- import { readFileSync as readFileSync41, existsSync as existsSync44 } from "fs";
19118
- import { resolve as resolve6 } from "path";
20341
+ import { readFileSync as readFileSync41, existsSync as existsSync45 } from "fs";
20342
+ import { resolve as resolve7 } from "path";
19119
20343
  process.title = "synkro";
19120
20344
  var envCandidates = [
19121
- resolve6(process.env.HOME ?? "", ".synkro", "config.env")
20345
+ resolve7(process.env.HOME ?? "", ".synkro", "config.env")
19122
20346
  ];
19123
20347
  for (const envPath of envCandidates) {
19124
- if (!existsSync44(envPath)) continue;
20348
+ if (!existsSync45(envPath)) continue;
19125
20349
  const envContent = readFileSync41(envPath, "utf-8");
19126
20350
  for (const line of envContent.split("\n")) {
19127
20351
  const trimmed = line.trim();
@@ -19139,7 +20363,7 @@ var subArgs = args.slice(1);
19139
20363
  var isDetachedChild = process.env.SYNKRO_TELEMETRY_DETACHED === "1";
19140
20364
  var FLUSH_SKIP = /* @__PURE__ */ new Set(["grade", "inventory-sync", "version", "--version", "-v", "help", "--help", "-h", ""]);
19141
20365
  function printVersion() {
19142
- console.log("1.10.5");
20366
+ console.log("1.10.8");
19143
20367
  }
19144
20368
  function printHelp2() {
19145
20369
  console.log(`Synkro CLI \u2014 runtime safety for AI coding agents