@synkro-sh/cli 1.6.31 → 1.6.33

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
@@ -1131,6 +1131,73 @@ export function detectRepo(
1131
1131
  return '';
1132
1132
  }
1133
1133
 
1134
+ // \u2500\u2500\u2500 Git Root \u2500\u2500\u2500
1135
+
1136
+ export function findGitRoot(cwd: string): string {
1137
+ if (!cwd) return '';
1138
+ try {
1139
+ return execSync('git rev-parse --show-toplevel 2>/dev/null', { cwd, timeout: 2000, encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }).trim();
1140
+ } catch { return ''; }
1141
+ }
1142
+
1143
+ // \u2500\u2500\u2500 .synkro file \u2500\u2500\u2500
1144
+
1145
+ export interface SynkroFileConfig {
1146
+ version: number;
1147
+ harness: ('claude-code' | 'cursor')[];
1148
+ grader: { pool: 'auto' | 'claude' | 'cursor'; mode?: string };
1149
+ ruleset: string;
1150
+ scanning: { cwe: boolean; cve: boolean };
1151
+ }
1152
+
1153
+ const SYNKRO_FILE_DEFAULTS: SynkroFileConfig = {
1154
+ version: 1,
1155
+ harness: ['claude-code', 'cursor'],
1156
+ grader: { pool: 'auto' },
1157
+ ruleset: 'default',
1158
+ scanning: { cwe: true, cve: true },
1159
+ };
1160
+
1161
+ let _synkroFileCache: SynkroFileConfig | undefined;
1162
+
1163
+ export function loadSynkroFile(cwd?: string): SynkroFileConfig {
1164
+ if (_synkroFileCache) return _synkroFileCache;
1165
+ const root = cwd ? findGitRoot(cwd) : '';
1166
+ if (!root) { _synkroFileCache = SYNKRO_FILE_DEFAULTS; return _synkroFileCache; }
1167
+ const fp = root + '/.synkro';
1168
+ try {
1169
+ if (!existsSync(fp)) { _synkroFileCache = SYNKRO_FILE_DEFAULTS; return _synkroFileCache; }
1170
+ const parsed = JSON.parse(readFileSync(fp, 'utf-8'));
1171
+ const validHarness = ['claude-code', 'cursor'] as const;
1172
+ const harness = Array.isArray(parsed.harness)
1173
+ ? parsed.harness.filter((h: string) => validHarness.includes(h as any))
1174
+ : ['claude-code', 'cursor'];
1175
+ _synkroFileCache = {
1176
+ version: parsed.version || 1,
1177
+ harness: harness.length > 0 ? harness : ['claude-code', 'cursor'],
1178
+ grader: {
1179
+ pool: ['auto', 'claude', 'cursor'].includes(parsed.grader?.pool) ? parsed.grader.pool : 'auto',
1180
+ mode: ['local', 'byok'].includes(parsed.grader?.mode) ? parsed.grader.mode : undefined,
1181
+ },
1182
+ ruleset: typeof parsed.ruleset === 'string' ? parsed.ruleset : 'default',
1183
+ scanning: {
1184
+ cwe: parsed.scanning?.cwe !== false,
1185
+ cve: parsed.scanning?.cve !== false,
1186
+ },
1187
+ };
1188
+ return _synkroFileCache;
1189
+ } catch {
1190
+ _synkroFileCache = SYNKRO_FILE_DEFAULTS;
1191
+ return _synkroFileCache;
1192
+ }
1193
+ }
1194
+
1195
+ export function effectiveGraderPool(synkroFile: SynkroFileConfig, hookAgentKind: AgentKind): AgentKind {
1196
+ if (synkroFile.grader.pool === 'auto') return hookAgentKind;
1197
+ if (synkroFile.grader.pool === 'claude') return 'claude_code';
1198
+ return 'cursor';
1199
+ }
1200
+
1134
1201
  // \u2500\u2500\u2500 Channel Health \u2500\u2500\u2500
1135
1202
 
1136
1203
  export async function channelUp(port = 18929): Promise<boolean> {
@@ -1273,14 +1340,16 @@ export async function loadConfig(jwt: string, query?: string): Promise<HookConfi
1273
1340
 
1274
1341
  // \u2500\u2500\u2500 Routing \u2500\u2500\u2500
1275
1342
 
1276
- export async function route(config: HookConfig): Promise<'local' | 'cloud'> {
1343
+ export async function route(config: HookConfig, synkroFile?: SynkroFileConfig): Promise<'local' | 'cloud'> {
1344
+ const gradingMode = synkroFile?.grader?.mode || process.env.SYNKRO_GRADING_MODE || config.gradingMode || 'local';
1345
+ if (gradingMode === 'byok') return 'cloud';
1277
1346
  if (config.captureDepth === 'local_only') return 'local';
1278
1347
  if (await channelUp()) return 'local';
1279
1348
  return 'cloud';
1280
1349
  }
1281
1350
 
1282
- export async function cweRoute(config: HookConfig): Promise<'local' | 'byok' | 'skip'> {
1283
- const gradingMode = process.env.SYNKRO_GRADING_MODE || config.gradingMode || 'local';
1351
+ export async function cweRoute(config: HookConfig, synkroFile?: SynkroFileConfig): Promise<'local' | 'byok' | 'skip'> {
1352
+ const gradingMode = synkroFile?.grader?.mode || process.env.SYNKRO_GRADING_MODE || config.gradingMode || 'local';
1284
1353
  if (gradingMode === 'byok') return 'byok';
1285
1354
  if (await cweChannelUp()) return 'local';
1286
1355
  return 'skip';
@@ -1288,10 +1357,11 @@ export async function cweRoute(config: HookConfig): Promise<'local' | 'byok' | '
1288
1357
 
1289
1358
  // \u2500\u2500\u2500 Tag Building \u2500\u2500\u2500
1290
1359
 
1291
- export function tag(rt: string, config: HookConfig): string {
1360
+ export function tag(rt: string, config: HookConfig, grader?: string): string {
1292
1361
  if (config.silent) return '[synkro:silent]';
1293
1362
  const rs = config.policyName || 'all';
1294
- return '[synkro:' + rt + ':' + rs + ']';
1363
+ const g = grader ? ':' + (grader === 'claude_code' ? 'claude' : grader) : '';
1364
+ return '[synkro:' + rt + ':' + rs + g + ']';
1295
1365
  }
1296
1366
 
1297
1367
  // \u2500\u2500\u2500 Local Grading (direct channel call) \u2500\u2500\u2500
@@ -3077,6 +3147,22 @@ function cursorHookExit(): never {
3077
3147
 
3078
3148
  const UNAVAIL_LOG = join(HOME, '.synkro', 'grader-unavailable.log');
3079
3149
 
3150
+ export function isGraderNotConfigured(errorMessage: string): boolean {
3151
+ const lower = errorMessage.toLowerCase();
3152
+ return lower.includes('no worker') || lower.includes('not configured') || lower.includes('agent_kind') || lower.includes('no pool');
3153
+ }
3154
+
3155
+ export function graderUnavailableMessage(hook: string, target: string, errorMessage: string, agentKind: AgentKind = 'claude_code'): string {
3156
+ if (errorMessage === 'SYNKRO_CHANNEL_DOWN') {
3157
+ return hook + ' ' + target + ' \u2192 local grader unavailable (container not running), skipped';
3158
+ }
3159
+ if (isGraderNotConfigured(errorMessage)) {
3160
+ const agent = agentKind === 'cursor' ? 'Cursor' : 'Claude Code';
3161
+ return hook + ' ' + target + ' \u2192 local grader not configured for ' + agent + '. Add this agent to your .synkro file and run \`synkro install\`.';
3162
+ }
3163
+ return hook + ' ' + target + ' \u2192 local grader unavailable, skipped';
3164
+ }
3165
+
3080
3166
  export function logGraderUnavailable(hook: string, target: string, errorMessage: string): void {
3081
3167
  try {
3082
3168
  const entry = {
@@ -3153,8 +3239,9 @@ import {
3153
3239
  readStdin, extractTranscript, readLastPrompt, findNearestDeps, filePathFromToolInput,
3154
3240
  appendSessionAction, readSessionLog, compressSessionLog, log,
3155
3241
  outputJson, outputEmpty, setupCursorHookSignals, isEditTool, hookSessionId, GATEWAY_URL,
3156
- logGraderUnavailable, filterRules, ruleFilterText, normalizeMode, countEditLineDelta,
3242
+ logGraderUnavailable, graderUnavailableMessage, filterRules, ruleFilterText, normalizeMode, countEditLineDelta,
3157
3243
  captureLineMetrics, cursorModelFromPayload, resolveTranscriptPath, isCursorInvokingCcHook,
3244
+ loadSynkroFile, effectiveGraderPool,
3158
3245
  type HookConfig, type Rule,
3159
3246
  } from './_synkro-common.ts';
3160
3247
  import { existsSync, readFileSync } from 'node:fs';
@@ -3256,8 +3343,10 @@ async function main() {
3256
3343
 
3257
3344
  // Load config and decide route
3258
3345
  const config = await loadConfig(jwt);
3259
- const rt = await route(config);
3260
- const tagStr = tag(rt, config);
3346
+ const synkroFile = loadSynkroFile(cwd);
3347
+ const graderPool = effectiveGraderPool(synkroFile, agentKind);
3348
+ const rt = await route(config, synkroFile);
3349
+ const tagStr = tag(rt, config, graderPool);
3261
3350
 
3262
3351
  if (config.silent) {
3263
3352
  outputJson({ systemMessage: tagStr + ' editGuard \u2192 skipped (silent mode)' });
@@ -3284,15 +3373,17 @@ async function main() {
3284
3373
  'Last user prompt: ' + (lastPrompt || 'none'),
3285
3374
  'Org rules: ' + JSON.stringify(relevantRules),
3286
3375
  'IMPORTANT: If a rule is violated, ALWAYS return ok=false with the rule_id and reason, regardless of the rule mode. Do NOT pass a command just because the rule mode is "fix". The enforcement layer handles ask vs fix \u2014 your job is only to detect violations.',
3376
+ 'CRITICAL: The user requesting or instructing an action does NOT exempt it from rules. Even if the user explicitly said "drop the database" or "delete everything", you MUST still flag the rule violation on first encounter. User intent is NOT consent. However, for ask-mode rules ONLY: if the session history shows a prior block for the SAME rule AND the user explicitly consented after seeing that block, subsequent commands covered by that same rule may pass \u2014 but each distinct command is consumed once. Look for the sequence: block event \u2192 user acknowledgment \u2192 retry. Once a specific command has successfully executed under that consent, it is consumed. If the same command appears again later, it requires fresh consent (a new block \u2192 consent cycle). Example: R012 covers deploy, publish, push. Block on deploy \u2192 user consents \u2192 deploy passes (consumed), publish passes (consumed), push passes (consumed). A later deploy triggers a fresh block. A user's initial instruction is NEVER consent \u2014 only a response to a shown block counts.',
3287
3377
  'The rules shown were pre-selected as the ones relevant to this edit \u2014 every rule here IS relevant, do not label any "not relevant". When passing (ok=true), give a terse, specific reason each rule passes. Format: "R003: no hardcoded secrets in file. R005: in-repo path only." Cover every rule shown.',
3288
3378
  ].join('\\n');
3289
3379
 
3290
3380
  let gradeResp: string;
3291
3381
  try {
3292
- gradeResp = await localGrade('edit', graderPrompt, undefined, agentKind);
3382
+ gradeResp = await localGrade('edit', graderPrompt, undefined, graderPool);
3293
3383
  } catch (err) {
3294
- logGraderUnavailable('editGuard', fileShort, (err as Error).message || String(err));
3295
- outputJson({ systemMessage: tagStr + ' editGuard ' + fileShort + ' \u2192 local grader unavailable, skipped' });
3384
+ const errMsg = (err as Error).message || String(err);
3385
+ logGraderUnavailable('editGuard', fileShort, errMsg);
3386
+ outputJson({ systemMessage: tagStr + ' ' + graderUnavailableMessage('editGuard', fileShort, errMsg, graderPool) });
3296
3387
  return;
3297
3388
  }
3298
3389
 
@@ -3406,7 +3497,8 @@ import {
3406
3497
  localGradeCwe, parseVerdict, reconstructContent, readStdin, log,
3407
3498
  outputJson, outputEmpty, setupCursorHookSignals, isEditTool, isShellTool, isCursorHookFormat,
3408
3499
  extractShellCodeWrites, hookSessionId, filePathFromToolInput, emitBlockScanFindings, dispatchFinding, dispatchCapture, GATEWAY_URL,
3409
- logGraderUnavailable, resolveTranscriptPath, isCursorInvokingCcHook,
3500
+ logGraderUnavailable, graderUnavailableMessage, resolveTranscriptPath, isCursorInvokingCcHook,
3501
+ loadSynkroFile, effectiveGraderPool,
3410
3502
  } from './_synkro-common.ts';
3411
3503
  import { basename, extname, resolve, join, dirname } from 'node:path';
3412
3504
  import { readFileSync, readdirSync, existsSync } from 'node:fs';
@@ -3619,10 +3711,12 @@ async function main() {
3619
3711
  jwt = await ensureFreshJwt(jwt);
3620
3712
 
3621
3713
  const config = await loadConfig(jwt);
3622
- const rt = await cweRoute(config);
3714
+ const synkroFile = loadSynkroFile(cwd);
3715
+ const graderPool = effectiveGraderPool(synkroFile, agentKind);
3716
+ const rt = await cweRoute(config, synkroFile);
3623
3717
 
3624
3718
  if (config.silent) {
3625
- outputJson({ systemMessage: '[synkro:' + rt + ':cweScan] skipped (silent mode)' });
3719
+ outputJson({ systemMessage: '[synkro:' + rt + ':cweScan:' + (graderPool === 'claude_code' ? 'claude' : graderPool) + '] skipped (silent mode)' });
3626
3720
  return;
3627
3721
  }
3628
3722
 
@@ -3643,10 +3737,11 @@ async function main() {
3643
3737
  }
3644
3738
  }
3645
3739
 
3646
- const cweTag = '[synkro:' + rt + ':cweScan]';
3740
+ const graderLabel = graderPool === 'claude_code' ? 'claude' : graderPool;
3741
+ const cweTag = '[synkro:' + rt + ':cweScan:' + graderLabel + ']';
3647
3742
 
3648
3743
  if (rt === 'skip') {
3649
- outputJson({ systemMessage: cweTag + ' ' + fileShort + ' \u2192 local CWE grader unavailable, skipped' });
3744
+ outputJson({ systemMessage: cweTag + ' ' + graderUnavailableMessage('cweGuard', fileShort, 'channel down', graderPool) });
3650
3745
  return;
3651
3746
  }
3652
3747
 
@@ -3828,23 +3923,23 @@ async function main() {
3828
3923
  const chunk2 = cweContent.slice(mid - OVERLAP);
3829
3924
  try {
3830
3925
  const [resp1, resp2] = await Promise.all([
3831
- localGradeCwe(buildCwePrompt(chunk1), agentKind),
3832
- localGradeCwe(buildCwePrompt(chunk2), agentKind),
3926
+ localGradeCwe(buildCwePrompt(chunk1), graderPool),
3927
+ localGradeCwe(buildCwePrompt(chunk2), graderPool),
3833
3928
  ]);
3834
3929
  gradeResponses = [resp1, resp2];
3835
3930
  } catch (gradeErr: any) {
3836
3931
  const reason = gradeErr?.message || String(gradeErr);
3837
3932
  logGraderUnavailable('cweGuard', fileShort, reason);
3838
- outputJson({ systemMessage: cweTag + ' ' + fileShort + ' \u2192 grader unavailable (' + reason + '), skipped' });
3933
+ outputJson({ systemMessage: cweTag + ' ' + graderUnavailableMessage('cweGuard', fileShort, reason, graderPool) });
3839
3934
  return;
3840
3935
  }
3841
3936
  } else {
3842
3937
  try {
3843
- gradeResponses = [await localGradeCwe(buildCwePrompt(cweContent), agentKind)];
3938
+ gradeResponses = [await localGradeCwe(buildCwePrompt(cweContent), graderPool)];
3844
3939
  } catch (gradeErr: any) {
3845
3940
  const reason = gradeErr?.message || String(gradeErr);
3846
3941
  logGraderUnavailable('cweGuard', fileShort, reason);
3847
- outputJson({ systemMessage: cweTag + ' ' + fileShort + ' \u2192 grader unavailable (' + reason + '), skipped' });
3942
+ outputJson({ systemMessage: cweTag + ' ' + graderUnavailableMessage('cweGuard', fileShort, reason, graderPool) });
3848
3943
  return;
3849
3944
  }
3850
3945
  }
@@ -4244,7 +4339,7 @@ import {
4244
4339
  loadJwt, ensureFreshJwt, detectRepo, loadConfig, route, tag,
4245
4340
  readStdin, runInstallScan, emitBlockScanFindings, dispatchCapture, dispatchScanResult, hashCommand,
4246
4341
  outputJson, outputEmpty, setupCursorHookSignals, isShellTool, hookSessionId, log, GATEWAY_URL,
4247
- resolveTranscriptPath, isCursorHookFormat,
4342
+ resolveTranscriptPath, isCursorHookFormat, loadSynkroFile, effectiveGraderPool,
4248
4343
  } from './_synkro-common.ts';
4249
4344
  import { writeFileSync, mkdirSync } from 'node:fs';
4250
4345
  import { join } from 'node:path';
@@ -4290,11 +4385,13 @@ async function main() {
4290
4385
  const transcriptPath = resolveTranscriptPath(payload);
4291
4386
  const repo = detectRepo(cwd, transcriptPath, command, workspaceRoots);
4292
4387
  const config = await loadConfig(jwt);
4293
- const rt = await route(config);
4294
- const tagStr = tag(rt, config);
4388
+ const isCursor = isCursorHookFormat();
4389
+ const synkroFile = loadSynkroFile(cwd);
4390
+ const graderPool = effectiveGraderPool(synkroFile, isCursor ? 'cursor' : 'claude_code');
4391
+ const rt = await route(config, synkroFile);
4392
+ const tagStr = tag(rt, config, graderPool);
4295
4393
 
4296
4394
  const rawModel = String(payload.model ?? payload.model_id ?? '');
4297
- const isCursor = isCursorHookFormat();
4298
4395
  const model = isCursor ? (rawModel ? (rawModel.startsWith('cursor/') ? rawModel : 'cursor/' + rawModel) : 'cursor') : (rawModel || '');
4299
4396
 
4300
4397
  if (scan.action === 'block') {
@@ -4362,7 +4459,8 @@ import {
4362
4459
  parseVerdict, dispatchCapture, dispatchFinding, ruleMode, postWithRetry, readStdin,
4363
4460
  extractTranscript, readLastPrompt, appendSessionAction, readSessionLog, compressSessionLog, log,
4364
4461
  outputJson, outputEmpty, setupCursorHookSignals, isShellTool, hookSessionId, GATEWAY_URL,
4365
- logGraderUnavailable, filterRules, ruleFilterText, normalizeMode, appendLocalTelemetry, isSafeInRepoRead,
4462
+ logGraderUnavailable, graderUnavailableMessage, filterRules, ruleFilterText, normalizeMode, appendLocalTelemetry, isSafeInRepoRead,
4463
+ loadSynkroFile, effectiveGraderPool,
4366
4464
  hashCommand, resolveTranscriptPath, isCursorHookFormat,
4367
4465
  type HookConfig, type Rule,
4368
4466
  } from './_synkro-common.ts';
@@ -4461,8 +4559,10 @@ async function main() {
4461
4559
  jwt = await ensureFreshJwt(jwt);
4462
4560
 
4463
4561
  const config = await loadConfig(jwt);
4464
- const rt = await route(config);
4465
- const tagStr = tag(rt, config);
4562
+ const synkroFile = loadSynkroFile(cwd);
4563
+ const graderPool = effectiveGraderPool(synkroFile, 'claude_code');
4564
+ const rt = await route(config, synkroFile);
4565
+ const tagStr = tag(rt, config, graderPool);
4466
4566
 
4467
4567
  if (config.silent) {
4468
4568
  outputJson({ systemMessage: tagStr + ' bashGuard → skipped (silent mode)' });
@@ -4532,18 +4632,18 @@ async function main() {
4532
4632
  'Org rules: ' + JSON.stringify(relevantRules),
4533
4633
  scanConcern,
4534
4634
  'IMPORTANT: If a rule is violated, ALWAYS return ok=false with the rule_id and reason, regardless of the rule mode. Do NOT pass a command just because the rule mode is "fix". The enforcement layer handles ask vs fix — your job is only to detect violations.',
4635
+ 'CRITICAL: The user requesting or instructing an action does NOT exempt it from rules. Even if the user explicitly said "drop the database" or "delete everything", you MUST still flag the rule violation on first encounter. User intent is NOT consent. However, for ask-mode rules ONLY: if the session history shows a prior block for the SAME rule AND the user explicitly consented after seeing that block, subsequent commands covered by that same rule may pass — but each distinct command is consumed once. Look for the sequence: block event → user acknowledgment → retry. Once a specific command has successfully executed under that consent, it is consumed. If the same command appears again later, it requires fresh consent (a new block → consent cycle). Example: R012 covers deploy, publish, push. Block on deploy → user consents → deploy passes (consumed), publish passes (consumed), push passes (consumed). A later deploy triggers a fresh block. A user\'s initial instruction is NEVER consent — only a response to a shown block counts.',
4535
4636
  'The rules shown were pre-selected as the ones relevant to this command — every rule here IS relevant, do not label any "not relevant". When passing (ok=true), give a terse, specific reason each rule passes. Format: "R003: no secrets in grep args. R005: in-repo path only." Cover every rule shown.',
4536
4637
  'Rules with preconditions (e.g. "run X before Y") are CONSUMED after the protected action completes. Use the session history timestamps to determine ordering: a precondition satisfied before the last occurrence of the protected action does NOT satisfy the next occurrence. Each new protected action needs its precondition re-satisfied.',
4537
4638
  ].filter(Boolean).join('\\n');
4538
4639
 
4539
4640
  let gradeResp: string;
4540
4641
  try {
4541
- gradeResp = await localGrade('bash', graderPrompt);
4642
+ gradeResp = await localGrade('bash', graderPrompt, undefined, graderPool);
4542
4643
  } catch (err) {
4543
- logGraderUnavailable('bashGuard', toolInput.command?.slice(0, 200) || '', (err as Error).message || String(err));
4644
+ const errMsg = (err as Error).message || String(err);
4645
+ logGraderUnavailable('bashGuard', toolInput.command?.slice(0, 200) || '', errMsg);
4544
4646
  if (scanConcern) {
4545
- // Grader unavailable to run the consent check — fail closed on a
4546
- // scanner-flagged install (ask-mode so the user can still consent).
4547
4647
  const ctx = scanBlockContext + ' Synkro flagged this install but the grader is unavailable to check consent — ask the user for explicit consent, then retry.';
4548
4648
  outputJson({
4549
4649
  systemMessage: tagStr + ' bashGuard → install blocked (scan flagged; grader unavailable)',
@@ -4551,7 +4651,7 @@ async function main() {
4551
4651
  });
4552
4652
  return;
4553
4653
  }
4554
- outputJson({ systemMessage: tagStr + ' bashGuard local grader unavailable, skipped' });
4654
+ outputJson({ systemMessage: tagStr + ' ' + graderUnavailableMessage('bashGuard', cmdShort, errMsg) });
4555
4655
  return;
4556
4656
  }
4557
4657
 
@@ -4656,7 +4756,8 @@ import {
4656
4756
  parseVerdict, dispatchCapture, ruleMode, postWithRetry, readStdin,
4657
4757
  extractTranscript, readLastPrompt, appendSessionAction, readSessionLog, compressSessionLog, log,
4658
4758
  outputJson, outputEmpty, setupCursorHookSignals, isAgentTool, hookSessionId, GATEWAY_URL,
4659
- logGraderUnavailable, filterRules, normalizeMode, resolveTranscriptPath, isCursorInvokingCcHook,
4759
+ logGraderUnavailable, graderUnavailableMessage, filterRules, normalizeMode, resolveTranscriptPath, isCursorInvokingCcHook,
4760
+ loadSynkroFile, effectiveGraderPool,
4660
4761
  type HookConfig, type Rule,
4661
4762
  } from './_synkro-common.ts';
4662
4763
 
@@ -4715,8 +4816,10 @@ async function main() {
4715
4816
  }
4716
4817
 
4717
4818
  const config = await loadConfig(jwt);
4718
- const rt = await route(config);
4719
- const tagStr = tag(rt, config);
4819
+ const synkroFile = loadSynkroFile(cwd);
4820
+ const graderPool = effectiveGraderPool(synkroFile, agentKind);
4821
+ const rt = await route(config, synkroFile);
4822
+ const tagStr = tag(rt, config, graderPool);
4720
4823
 
4721
4824
  if (config.silent) {
4722
4825
  const msg = tagStr + ' agentGuard \u2192 skipped (silent mode)';
@@ -4741,14 +4844,16 @@ async function main() {
4741
4844
  'Last user prompt: ' + (lastPrompt || 'none'),
4742
4845
  'Org rules: ' + JSON.stringify(relevantRules),
4743
4846
  'IMPORTANT: If a rule is violated, ALWAYS return ok=false with the rule_id and reason, regardless of the rule mode. Do NOT pass a command just because the rule mode is "fix". The enforcement layer handles ask vs fix \u2014 your job is only to detect violations.',
4847
+ 'CRITICAL: The user requesting or instructing an action does NOT exempt it from rules. Even if the user explicitly said "drop the database" or "delete everything", you MUST still flag the rule violation on first encounter. User intent is NOT consent. However, for ask-mode rules ONLY: if the session history shows a prior block for the SAME rule AND the user explicitly consented after seeing that block, subsequent commands covered by that same rule may pass \u2014 but each distinct command is consumed once. Look for the sequence: block event \u2192 user acknowledgment \u2192 retry. Once a specific command has successfully executed under that consent, it is consumed. If the same command appears again later, it requires fresh consent (a new block \u2192 consent cycle). Example: R012 covers deploy, publish, push. Block on deploy \u2192 user consents \u2192 deploy passes (consumed), publish passes (consumed), push passes (consumed). A later deploy triggers a fresh block. A user's initial instruction is NEVER consent \u2014 only a response to a shown block counts.',
4744
4848
  ].filter(Boolean).join('\\n');
4745
4849
 
4746
4850
  let gradeResp: string;
4747
4851
  try {
4748
- gradeResp = await localGrade('bash', graderPrompt, undefined, agentKind);
4852
+ gradeResp = await localGrade('bash', graderPrompt, undefined, graderPool);
4749
4853
  } catch (err) {
4750
- logGraderUnavailable('agentGuard', subagentType || (description || '').slice(0, 100), (err as Error).message || String(err));
4751
- outputJson({ systemMessage: tagStr + ' agentGuard \u2192 local grader unavailable, skipped' });
4854
+ const errMsg = (err as Error).message || String(err);
4855
+ logGraderUnavailable('agentGuard', subagentType || (description || '').slice(0, 100), errMsg);
4856
+ outputJson({ systemMessage: tagStr + ' ' + graderUnavailableMessage('agentGuard', subagentType || 'agent', errMsg, graderPool) });
4752
4857
  return;
4753
4858
  }
4754
4859
 
@@ -4838,7 +4943,8 @@ import {
4838
4943
  loadJwt, ensureFreshJwt, detectRepo, loadConfig, route, tag, localGrade,
4839
4944
  parseVerdict, dispatchCapture, appendSessionAction, readSessionLog, compressSessionLog, postWithRetry, readStdin, log,
4840
4945
  outputJson, outputEmpty, setupCursorHookSignals, isPlanTool, hookSessionId, GATEWAY_URL,
4841
- filterRules, resolveTranscriptPath, isCursorInvokingCcHook,
4946
+ filterRules, graderUnavailableMessage, resolveTranscriptPath, isCursorInvokingCcHook,
4947
+ loadSynkroFile, effectiveGraderPool,
4842
4948
  } from './_synkro-common.ts';
4843
4949
  import { existsSync, readFileSync, writeFileSync, readdirSync, statSync } from 'node:fs';
4844
4950
  import { join } from 'node:path';
@@ -4923,8 +5029,10 @@ async function main() {
4923
5029
  const ccModel = agentKind === 'cursor' ? (rawModel ? (rawModel.startsWith('cursor/') ? rawModel : 'cursor/' + rawModel) : 'cursor') : (rawModel || '');
4924
5030
 
4925
5031
  const config = await loadConfig(jwt);
4926
- const rt = await route(config);
4927
- const tagStr = tag(rt, config);
5032
+ const synkroFile = loadSynkroFile(cwd);
5033
+ const graderPool = effectiveGraderPool(synkroFile, agentKind);
5034
+ const rt = await route(config, synkroFile);
5035
+ const tagStr = tag(rt, config, graderPool);
4928
5036
 
4929
5037
  if (config.silent) {
4930
5038
  outputJson({ systemMessage: tagStr + ' planReview \u2192 skipped (silent mode)' });
@@ -4945,9 +5053,9 @@ async function main() {
4945
5053
 
4946
5054
  let gradeResp: string;
4947
5055
  try {
4948
- gradeResp = await localGrade('plan', graderPrompt, undefined, agentKind);
4949
- } catch {
4950
- outputJson({ systemMessage: tagStr + ' planReview \u2192 local grader unavailable, skipped' });
5056
+ gradeResp = await localGrade('plan', graderPrompt, undefined, graderPool);
5057
+ } catch (err) {
5058
+ outputJson({ systemMessage: tagStr + ' ' + graderUnavailableMessage('planReview', 'plan', (err as Error).message || String(err), graderPool) });
4951
5059
  return;
4952
5060
  }
4953
5061
 
@@ -5090,7 +5198,7 @@ main();
5090
5198
  import {
5091
5199
  loadJwt, detectRepo, channelUp, tag, readStdin, writeCachedRepo,
5092
5200
  outputJson, outputEmpty, setupCursorHookSignals, hookSessionId, resolveTranscriptPath, GATEWAY_URL,
5093
- isLocalStorageMode, log, type HookConfig,
5201
+ isLocalStorageMode, loadSynkroFile, log, type HookConfig,
5094
5202
  } from './_synkro-common.ts';
5095
5203
 
5096
5204
  async function main() {
@@ -5110,7 +5218,9 @@ async function main() {
5110
5218
  let jwt = loadJwt();
5111
5219
 
5112
5220
  const isChannelUp = await channelUp();
5113
- const rt = isChannelUp ? 'local' : 'cloud';
5221
+ const synkroFile = loadSynkroFile(cwd);
5222
+ const gradingMode = synkroFile.grader.mode || process.env.SYNKRO_GRADING_MODE || 'local';
5223
+ const rt = gradingMode === 'byok' ? 'cloud' : (isChannelUp ? 'local' : 'cloud');
5114
5224
 
5115
5225
  let policyName = '';
5116
5226
  let silent = false;
@@ -5142,15 +5252,24 @@ async function main() {
5142
5252
  } catch {}
5143
5253
  }
5144
5254
 
5145
- const fakeConfig: HookConfig = { captureDepth: 'local_only', tier: 'standard', silent, policyName, rules: [] };
5255
+ const fakeConfig: HookConfig = { captureDepth: 'local_only', tier: 'standard', silent, policyName, rules: [], scanExemptions: [], gradingMode, storageMode: process.env.SYNKRO_STORAGE_MODE || 'local' };
5146
5256
  const tagStr = tag(rt, fakeConfig);
5147
- const routeLine = tagStr + ' inference: ' + (isChannelUp ? 'local-cc (channel reachable on 127.0.0.1:18929)' : 'cloud (local-cc channel not reachable)');
5257
+ const routeLine = tagStr + ' inference: ' + (gradingMode === 'byok' ? 'cloud (BYOK)' : isChannelUp ? 'local-cc (channel reachable on 127.0.0.1:18929)' : 'cloud (local-cc channel not reachable)');
5148
5258
 
5149
5259
  if (!jwt) {
5150
5260
  outputJson({ systemMessage: routeLine });
5151
5261
  return;
5152
5262
  }
5153
5263
 
5264
+ // Sync grading mode to API profile so the dashboard reflects the actual config
5265
+ const fastInference = gradingMode === 'byok';
5266
+ fetch(GATEWAY_URL + '/api/v1/cli/me', {
5267
+ method: 'PATCH',
5268
+ headers: { 'Content-Type': 'application/json', Authorization: 'Bearer ' + jwt },
5269
+ body: JSON.stringify({ fast_inference: fastInference }),
5270
+ signal: AbortSignal.timeout(3000),
5271
+ }).catch(() => {});
5272
+
5154
5273
  if (!openFindings) {
5155
5274
  outputJson({ systemMessage: routeLine });
5156
5275
  } else if (openFindings === 1) {
@@ -5342,7 +5461,8 @@ import {
5342
5461
  parseVerdict, dispatchCapture, dispatchFinding, ruleMode, normalizeMode, filterRules, ruleFilterText,
5343
5462
  isSafeInRepoRead, resolveTranscriptPath, postWithRetry, readStdin, hashCommand,
5344
5463
  extractTranscript, readLastPrompt, readSessionLog, compressSessionLog,
5345
- appendLocalTelemetry, logGraderUnavailable, log, GATEWAY_URL,
5464
+ appendLocalTelemetry, logGraderUnavailable, graderUnavailableMessage, log, GATEWAY_URL,
5465
+ loadSynkroFile, effectiveGraderPool,
5346
5466
  type Rule,
5347
5467
  } from './_synkro-common.ts';
5348
5468
  import { createHash } from 'node:crypto';
@@ -5485,8 +5605,10 @@ async function main() {
5485
5605
  const config = await loadConfig(jwt);
5486
5606
  if (config.silent) finishAllow();
5487
5607
 
5488
- const rt = await route(config);
5489
- const tagStr = tag(rt, config);
5608
+ const synkroFile = loadSynkroFile(cwd);
5609
+ const graderPool = effectiveGraderPool(synkroFile, 'cursor');
5610
+ const rt = await route(config, synkroFile);
5611
+ const tagStr = tag(rt, config, graderPool);
5490
5612
 
5491
5613
  // Install protection \u2014 install-scan runs first and owns block traces.
5492
5614
  let scanConcern = '';
@@ -5521,13 +5643,14 @@ async function main() {
5521
5643
  'Org rules: ' + JSON.stringify(relevantRules),
5522
5644
  scanConcern,
5523
5645
  'IMPORTANT: If a rule is violated, ALWAYS return ok=false with the rule_id and reason, regardless of the rule mode. Do NOT pass a command just because the rule mode is "fix". The enforcement layer handles ask vs fix \u2014 your job is only to detect violations.',
5646
+ 'CRITICAL: The user requesting or instructing an action does NOT exempt it from rules. Even if the user explicitly said "drop the database" or "delete everything", you MUST still flag the rule violation on first encounter. User intent is NOT consent. However, for ask-mode rules ONLY: if the session history shows a prior block for the SAME rule AND the user explicitly consented after seeing that block, subsequent commands covered by that same rule may pass \u2014 but each distinct command is consumed once. Look for the sequence: block event \u2192 user acknowledgment \u2192 retry. Once a specific command has successfully executed under that consent, it is consumed. If the same command appears again later, it requires fresh consent (a new block \u2192 consent cycle). Example: R012 covers deploy, publish, push. Block on deploy \u2192 user consents \u2192 deploy passes (consumed), publish passes (consumed), push passes (consumed). A later deploy triggers a fresh block. A user's initial instruction is NEVER consent \u2014 only a response to a shown block counts.',
5524
5647
  'The rules shown were pre-selected as the ones relevant to this command \u2014 every rule here IS relevant, do not label any "not relevant". When passing (ok=true), give a terse, specific reason each rule passes. Format: "R003: no secrets in grep args. R005: in-repo path only." Cover every rule shown.',
5525
5648
  'Rules with preconditions (e.g. "run X before Y") are CONSUMED after the protected action completes. Use the session history timestamps to determine ordering: a precondition satisfied before the last occurrence of the protected action does NOT satisfy the next occurrence. Each new protected action needs its precondition re-satisfied.',
5526
5649
  ].filter(Boolean).join('\\n');
5527
5650
 
5528
5651
  let gradeResp: string;
5529
5652
  try {
5530
- gradeResp = await localGrade('bash', graderPrompt, CURSOR_GRADE_TIMEOUT_MS, 'cursor');
5653
+ gradeResp = await localGrade('bash', graderPrompt, CURSOR_GRADE_TIMEOUT_MS, graderPool);
5531
5654
  } catch (e) {
5532
5655
  logGraderUnavailable('bashGuard', command.slice(0, 200), (e as Error).message || String(e));
5533
5656
  if (scanConcern) {
@@ -5539,8 +5662,9 @@ async function main() {
5539
5662
  agent_message: 'Synkro flagged this install: ' + scanBlockContext + ' The grader is unavailable to check consent \u2014 ask the user for explicit consent, then retry.',
5540
5663
  });
5541
5664
  }
5542
- log('bashGuard ' + cmdShort + ' \u2192 pass (grade unavailable): ' + String(e));
5543
- finishWith({ permission: 'allow' });
5665
+ const errMsg = (e as Error).message || String(e);
5666
+ log('bashGuard ' + cmdShort + ' \u2192 pass (grade unavailable): ' + errMsg);
5667
+ finishWith({ permission: 'allow', user_message: tagStr + ' ' + graderUnavailableMessage('bashGuard', cmdShort, errMsg, graderPool) });
5544
5668
  }
5545
5669
 
5546
5670
  const verdict = parseVerdict(gradeResp);
@@ -6908,10 +7032,10 @@ __export(dockerInstall_exports, {
6908
7032
  splitWorkers: () => splitWorkers,
6909
7033
  waitForContainerReady: () => waitForContainerReady
6910
7034
  });
6911
- import { copyFileSync, existsSync as existsSync7, mkdirSync as mkdirSync7, readdirSync as readdirSync2 } from "fs";
7035
+ import { copyFileSync, existsSync as existsSync7, mkdirSync as mkdirSync7, readFileSync as readFileSync6, readdirSync as readdirSync2 } from "fs";
6912
7036
  import { homedir as homedir6 } from "os";
6913
7037
  import { join as join6 } from "path";
6914
- import { spawnSync as spawnSync2 } from "child_process";
7038
+ import { execSync as execSync4, spawnSync as spawnSync2 } from "child_process";
6915
7039
  function splitWorkers(total, providers) {
6916
7040
  const t = Math.max(0, Math.floor(total));
6917
7041
  const hasClaude = providers.includes("claude_code");
@@ -6929,6 +7053,19 @@ function normalizeProvider(p) {
6929
7053
  if (v === "cursor") return "cursor";
6930
7054
  return null;
6931
7055
  }
7056
+ function readSynkroFilePool() {
7057
+ try {
7058
+ const root = execSync4("git rev-parse --show-toplevel 2>/dev/null", { encoding: "utf-8", timeout: 3e3, stdio: ["pipe", "pipe", "pipe"] }).trim();
7059
+ if (!root) return "auto";
7060
+ const fp = join6(root, ".synkro");
7061
+ if (!existsSync7(fp)) return "auto";
7062
+ const parsed = JSON.parse(readFileSync6(fp, "utf-8"));
7063
+ const pool = parsed?.grader?.pool;
7064
+ if (pool === "cursor" || pool === "claude") return pool;
7065
+ } catch {
7066
+ }
7067
+ return "auto";
7068
+ }
6932
7069
  function resolveWorkerConfig(rest) {
6933
7070
  let workers = 8;
6934
7071
  let explicit = false;
@@ -6962,8 +7099,15 @@ function resolveWorkerConfig(rest) {
6962
7099
  workers = Math.min(workers, 64);
6963
7100
  let provs = providers;
6964
7101
  if (provs.length === 0) {
6965
- provs = detectAgents().map((a) => a.kind);
6966
- if (provs.length === 0) provs = ["claude_code"];
7102
+ const synkroPool = readSynkroFilePool();
7103
+ if (synkroPool === "cursor") {
7104
+ provs = ["cursor"];
7105
+ } else if (synkroPool === "claude") {
7106
+ provs = ["claude_code"];
7107
+ } else {
7108
+ provs = detectAgents().map((a) => a.kind);
7109
+ if (provs.length === 0) provs = ["claude_code"];
7110
+ }
6967
7111
  }
6968
7112
  return { ...splitWorkers(workers, provs), explicit };
6969
7113
  }
@@ -7368,15 +7512,15 @@ __export(setupGithub_exports, {
7368
7512
  });
7369
7513
  import { createInterface as createInterface2 } from "readline/promises";
7370
7514
  import { stdin as input, stdout as output } from "process";
7371
- import { execSync as execSync4, spawn as nodeSpawn } from "child_process";
7372
- import { existsSync as existsSync8, readFileSync as readFileSync6, unlinkSync as unlinkSync3 } from "fs";
7515
+ import { execSync as execSync5, spawn as nodeSpawn } from "child_process";
7516
+ import { existsSync as existsSync8, readFileSync as readFileSync7, unlinkSync as unlinkSync3 } from "fs";
7373
7517
  import { homedir as homedir7, platform as platform3 } from "os";
7374
7518
  import { join as join7 } from "path";
7375
7519
  import { execFile as execFile2 } from "child_process";
7376
7520
  function readConfig() {
7377
7521
  if (!existsSync8(CONFIG_PATH)) return {};
7378
7522
  const out = {};
7379
- for (const line of readFileSync6(CONFIG_PATH, "utf-8").split("\n")) {
7523
+ for (const line of readFileSync7(CONFIG_PATH, "utf-8").split("\n")) {
7380
7524
  const t = line.trim();
7381
7525
  if (!t || t.startsWith("#")) continue;
7382
7526
  const eq = t.indexOf("=");
@@ -7446,7 +7590,7 @@ function captureClaudeSetupToken() {
7446
7590
  proc.on("close", (code) => {
7447
7591
  let raw = "";
7448
7592
  try {
7449
- raw = readFileSync6(tmpFile, "utf-8");
7593
+ raw = readFileSync7(tmpFile, "utf-8");
7450
7594
  } catch (e) {
7451
7595
  reject(new Error(`Could not read script output file: ${e.message}`));
7452
7596
  return;
@@ -7578,7 +7722,7 @@ async function setupGithubCommand(opts = {}) {
7578
7722
  }
7579
7723
  } catch {
7580
7724
  try {
7581
- ghToken = execSync4("gh auth token", { encoding: "utf-8", timeout: 5e3 }).trim();
7725
+ ghToken = execSync5("gh auth token", { encoding: "utf-8", timeout: 5e3 }).trim();
7582
7726
  } catch {
7583
7727
  console.error("GitHub not connected. Run `synkro-cli setup-github` interactively to connect.");
7584
7728
  return;
@@ -7612,7 +7756,7 @@ async function setupGithubCommand(opts = {}) {
7612
7756
  }
7613
7757
  console.log(" Validating token...");
7614
7758
  try {
7615
- const validateResult = execSync4(
7759
+ const validateResult = execSync5(
7616
7760
  'claude --print --output-format json "say ok"',
7617
7761
  { env: { ...process.env, CLAUDE_CODE_OAUTH_TOKEN: claudeToken }, encoding: "utf-8", timeout: 3e4, stdio: ["ignore", "pipe", "pipe"] }
7618
7762
  );
@@ -7629,7 +7773,7 @@ async function setupGithubCommand(opts = {}) {
7629
7773
  if (opts.nonInteractive) {
7630
7774
  let currentFullName = null;
7631
7775
  try {
7632
- const remoteUrl = execSync4("git remote get-url origin", { encoding: "utf-8", timeout: 5e3 }).trim();
7776
+ const remoteUrl = execSync5("git remote get-url origin", { encoding: "utf-8", timeout: 5e3 }).trim();
7633
7777
  const m = remoteUrl.match(/(?:github\.com)[:/](.+?)(?:\.git)?$/);
7634
7778
  if (m) currentFullName = m[1];
7635
7779
  } catch {
@@ -7728,12 +7872,14 @@ var install_exports = {};
7728
7872
  __export(install_exports, {
7729
7873
  detectGitRepo: () => detectGitRepo2,
7730
7874
  installCommand: () => installCommand,
7731
- parseArgs: () => parseArgs
7875
+ parseArgs: () => parseArgs,
7876
+ reconcileHarness: () => reconcileHarness,
7877
+ writeHookScripts: () => writeHookScripts
7732
7878
  });
7733
- import { existsSync as existsSync9, mkdirSync as mkdirSync8, writeFileSync as writeFileSync7, chmodSync as chmodSync2, readFileSync as readFileSync7, readdirSync as readdirSync3 } from "fs";
7879
+ import { existsSync as existsSync9, mkdirSync as mkdirSync8, writeFileSync as writeFileSync7, chmodSync as chmodSync2, readFileSync as readFileSync8, readdirSync as readdirSync3 } from "fs";
7734
7880
  import { homedir as homedir8 } from "os";
7735
7881
  import { join as join8 } from "path";
7736
- import { execSync as execSync5, spawnSync as spawnSync3 } from "child_process";
7882
+ import { execSync as execSync6, spawnSync as spawnSync3 } from "child_process";
7737
7883
  import { createInterface as createInterface3 } from "readline";
7738
7884
  function sanitizeGatewayCandidate(raw) {
7739
7885
  if (!raw) return void 0;
@@ -7976,7 +8122,7 @@ function writeConfigEnv(opts) {
7976
8122
  `SYNKRO_CREDENTIALS_PATH=${shellQuoteSingle(credsPath)}`,
7977
8123
  `SYNKRO_TIER=${shellQuoteSingle(safeTier)}`,
7978
8124
  `SYNKRO_INFERENCE=${shellQuoteSingle(safeInference)}`,
7979
- `SYNKRO_VERSION=${shellQuoteSingle("1.6.31")}`
8125
+ `SYNKRO_VERSION=${shellQuoteSingle("1.6.33")}`
7980
8126
  ];
7981
8127
  if (safeSynkroBin) lines.push(`SYNKRO_CLI_BIN=${shellQuoteSingle(safeSynkroBin)}`);
7982
8128
  if (safeUserId) lines.push(`SYNKRO_USER_ID=${shellQuoteSingle(safeUserId)}`);
@@ -7999,7 +8145,7 @@ function resolveDeploymentMode() {
7999
8145
  if (envOverride === "bare-host" || envOverride === "docker") return envOverride;
8000
8146
  try {
8001
8147
  if (existsSync9(CONFIG_PATH2)) {
8002
- const m = readFileSync7(CONFIG_PATH2, "utf-8").match(/^SYNKRO_DEPLOYMENT_MODE='([^']*)'/m);
8148
+ const m = readFileSync8(CONFIG_PATH2, "utf-8").match(/^SYNKRO_DEPLOYMENT_MODE='([^']*)'/m);
8003
8149
  const val = m?.[1]?.toLowerCase();
8004
8150
  if (val === "bare-host" || val === "docker") return val;
8005
8151
  }
@@ -8007,40 +8153,41 @@ function resolveDeploymentMode() {
8007
8153
  }
8008
8154
  return "docker";
8009
8155
  }
8010
- function collectLocalMetadata() {
8156
+ function collectLocalMetadata(includeClaudeCode = true) {
8011
8157
  const meta = { platform: process.platform };
8012
8158
  try {
8013
- meta.display_name = execSync5("git config user.name", { encoding: "utf-8", timeout: 3e3 }).trim();
8159
+ meta.display_name = execSync6("git config user.name", { encoding: "utf-8", timeout: 3e3 }).trim();
8014
8160
  } catch {
8015
8161
  }
8016
8162
  try {
8017
- const remote = execSync5("git remote get-url origin", { encoding: "utf-8", timeout: 3e3, stdio: ["pipe", "pipe", "pipe"] }).trim();
8163
+ const remote = execSync6("git remote get-url origin", { encoding: "utf-8", timeout: 3e3, stdio: ["pipe", "pipe", "pipe"] }).trim();
8018
8164
  const sshMatch = remote.match(/^git@[^:]+:(.+?)(?:\.git)?$/);
8019
8165
  const httpMatch = remote.match(/^https?:\/\/[^/]+\/(.+?)(?:\.git)?$/);
8020
8166
  const m = sshMatch || httpMatch;
8021
8167
  if (m) meta.active_repo = m[1];
8022
8168
  } catch {
8023
8169
  }
8170
+ if (!includeClaudeCode) return meta;
8024
8171
  try {
8025
- meta.cc_version = execSync5("claude --version", { encoding: "utf-8", timeout: 5e3 }).trim().split("\n")[0];
8172
+ meta.cc_version = execSync6("claude --version", { encoding: "utf-8", timeout: 5e3 }).trim().split("\n")[0];
8026
8173
  } catch {
8027
8174
  }
8028
8175
  const claudeDir = join8(homedir8(), ".claude");
8029
8176
  try {
8030
- const settings = JSON.parse(readFileSync7(join8(claudeDir, "settings.json"), "utf-8"));
8177
+ const settings = JSON.parse(readFileSync8(join8(claudeDir, "settings.json"), "utf-8"));
8031
8178
  const plugins = Object.keys(settings.enabledPlugins ?? {}).filter((k) => settings.enabledPlugins[k]);
8032
8179
  if (plugins.length) meta.enabled_plugins = plugins;
8033
8180
  if (settings.permissions?.defaultMode) meta.permissions_mode = settings.permissions.defaultMode;
8034
8181
  } catch {
8035
8182
  }
8036
8183
  try {
8037
- const mcpCache = JSON.parse(readFileSync7(join8(claudeDir, "mcp-needs-auth-cache.json"), "utf-8"));
8184
+ const mcpCache = JSON.parse(readFileSync8(join8(claudeDir, "mcp-needs-auth-cache.json"), "utf-8"));
8038
8185
  const mcpNames = Object.keys(mcpCache);
8039
8186
  if (mcpNames.length) meta.mcp_servers = mcpNames;
8040
8187
  } catch {
8041
8188
  }
8042
8189
  try {
8043
- const mcpList = execSync5("claude mcp list 2>/dev/null", { encoding: "utf-8", timeout: 1e4 });
8190
+ const mcpList = execSync6("claude mcp list 2>/dev/null", { encoding: "utf-8", timeout: 1e4 });
8044
8191
  const connected = mcpList.split("\n").filter((l) => l.includes("Connected")).map((l) => l.split(":")[0].trim()).filter(Boolean);
8045
8192
  if (connected.length) meta.mcp_servers_connected = connected;
8046
8193
  } catch {
@@ -8049,7 +8196,7 @@ function collectLocalMetadata() {
8049
8196
  const sessionsDir = join8(claudeDir, "sessions");
8050
8197
  const files = readdirSync3(sessionsDir).filter((f) => f.endsWith(".json")).slice(-5);
8051
8198
  for (const f of files) {
8052
- const s = JSON.parse(readFileSync7(join8(sessionsDir, f), "utf-8"));
8199
+ const s = JSON.parse(readFileSync8(join8(sessionsDir, f), "utf-8"));
8053
8200
  if (s.version) {
8054
8201
  meta.cc_version = meta.cc_version || s.version;
8055
8202
  break;
@@ -8059,14 +8206,14 @@ function collectLocalMetadata() {
8059
8206
  }
8060
8207
  return meta;
8061
8208
  }
8062
- async function fetchUserProfile(gatewayUrl, token) {
8209
+ async function fetchUserProfile(gatewayUrl, token, hasClaudeCode = true) {
8063
8210
  try {
8064
8211
  const resp = await fetch(`${gatewayUrl}/api/v1/cli/me`, {
8065
8212
  headers: { "Authorization": `Bearer ${token}` }
8066
8213
  });
8067
8214
  if (!resp.ok) return { tier: "pro", inference: "fast", localInference: false, captureDepth: "full" };
8068
8215
  const data = await resp.json();
8069
- const meta = collectLocalMetadata();
8216
+ const meta = collectLocalMetadata(hasClaudeCode);
8070
8217
  fetch(`${gatewayUrl}/api/v1/cli/me`, {
8071
8218
  method: "PATCH",
8072
8219
  headers: { "Authorization": `Bearer ${token}`, "Content-Type": "application/json" },
@@ -8177,22 +8324,11 @@ async function installCommand(opts = {}) {
8177
8324
  }
8178
8325
  ensureSynkroDir();
8179
8326
  const scripts = writeHookScripts();
8180
- console.log("Wrote hook scripts:");
8181
- console.log(` ${scripts.bashScript}`);
8182
- console.log(` ${scripts.bashFollowupScript}`);
8183
- console.log(` ${scripts.editPrecheckScript}`);
8184
- console.log(` ${scripts.cwePrecheckScript}`);
8185
- console.log(` ${scripts.cvePrecheckScript}`);
8186
- console.log(` ${scripts.planJudgeScript}`);
8187
- console.log(` ${scripts.agentJudgeScript}`);
8188
- console.log(` ${scripts.stopSummaryScript}`);
8189
- console.log(` ${scripts.sessionStartScript}`);
8190
- console.log(` ${scripts.transcriptSyncScript}
8191
- `);
8327
+ console.log("Wrote hook scripts to ~/.synkro/hooks/\n");
8192
8328
  for (const mode of ["edit", "bash"]) {
8193
8329
  const pidFile = join8(SYNKRO_DIR4, "daemon", mode, "daemon.pid");
8194
8330
  try {
8195
- const pid = parseInt(readFileSync7(pidFile, "utf-8").trim(), 10);
8331
+ const pid = parseInt(readFileSync8(pidFile, "utf-8").trim(), 10);
8196
8332
  if (pid > 0) {
8197
8333
  process.kill(pid, "SIGTERM");
8198
8334
  console.log(`Stopped stale ${mode} grader daemon (pid ${pid})`);
@@ -8262,7 +8398,7 @@ async function installCommand(opts = {}) {
8262
8398
  email = info.email;
8263
8399
  } catch {
8264
8400
  }
8265
- const profile = await fetchUserProfile(gatewayUrl, token);
8401
+ const profile = await fetchUserProfile(gatewayUrl, token, hasClaudeCode);
8266
8402
  const cloudOnly = gradingMode === "byok" && storageMode === "cloud";
8267
8403
  const useLocalMcp = !cloudOnly;
8268
8404
  if (cloudOnly) {
@@ -8379,6 +8515,7 @@ async function installCommand(opts = {}) {
8379
8515
  } catch (err) {
8380
8516
  console.warn(` \u26A0 Could not cache judge prompts: ${err.message}`);
8381
8517
  }
8518
+ writeSynkroFileIfMissing({ hasClaudeCode, hasCursor, gradingMode });
8382
8519
  console.log();
8383
8520
  if (useLocalMcp) {
8384
8521
  const { assertDockerAvailable: assertDockerAvailable2 } = await Promise.resolve().then(() => (init_dockerInstall(), dockerInstall_exports));
@@ -8394,10 +8531,18 @@ async function installCommand(opts = {}) {
8394
8531
  }
8395
8532
  console.log("Installing Synkro server container...");
8396
8533
  const totalWorkers = parseInt(process.env.SYNKRO_WORKERS_PER_POOL || "8", 10);
8397
- const providers = [];
8398
- if (hasClaudeCode) providers.push("claude_code");
8399
- if (hasCursor) providers.push("cursor");
8534
+ const synkroFilePool = readSynkroFilePool2();
8535
+ let providers = [];
8536
+ if (synkroFilePool === "cursor") {
8537
+ providers = ["cursor"];
8538
+ } else if (synkroFilePool === "claude") {
8539
+ providers = ["claude_code"];
8540
+ } else {
8541
+ if (hasClaudeCode) providers.push("claude_code");
8542
+ if (hasCursor) providers.push("cursor");
8543
+ }
8400
8544
  const { claudeWorkers, cursorWorkers } = splitWorkers(totalWorkers, providers);
8545
+ if (synkroFilePool !== "auto") console.log(` .synkro: grader pool set to ${synkroFilePool}`);
8401
8546
  console.log(` worker pool: ${claudeWorkers} claude + ${cursorWorkers} cursor`);
8402
8547
  const connectedRepo = detectGitRepo2() || void 0;
8403
8548
  const { image, hostMcpPort, hostGraderPort, hostCwePort, hostPglitePort } = await dockerInstall({ claudeWorkers, cursorWorkers, connectedRepo });
@@ -8407,7 +8552,7 @@ async function installCommand(opts = {}) {
8407
8552
  const ready = await waitForContainerReady(6e4);
8408
8553
  if (ready) {
8409
8554
  console.log(" \u2713 container ready");
8410
- const mcpJwt = readFileSync7(join8(SYNKRO_DIR4, ".mcp-jwt"), "utf-8").trim();
8555
+ const mcpJwt = readFileSync8(join8(SYNKRO_DIR4, ".mcp-jwt"), "utf-8").trim();
8411
8556
  try {
8412
8557
  const ingestResp = await fetch(`http://127.0.0.1:${hostMcpPort}/api/ingest`, {
8413
8558
  method: "POST",
@@ -8431,14 +8576,14 @@ async function installCommand(opts = {}) {
8431
8576
  }
8432
8577
  console.log();
8433
8578
  }
8434
- if (transcriptConsent) {
8579
+ if (transcriptConsent && hasClaudeCode) {
8435
8580
  const repo = detectGitRepo2();
8436
8581
  if (repo) {
8437
8582
  if (storageMode === "local") {
8438
8583
  try {
8439
8584
  let mcpToken = "";
8440
8585
  try {
8441
- mcpToken = readFileSync7(join8(SYNKRO_DIR4, ".mcp-jwt"), "utf-8").trim();
8586
+ mcpToken = readFileSync8(join8(SYNKRO_DIR4, ".mcp-jwt"), "utf-8").trim();
8442
8587
  } catch {
8443
8588
  }
8444
8589
  if (mcpToken) {
@@ -8502,10 +8647,160 @@ async function installCommand(opts = {}) {
8502
8647
  }
8503
8648
  console.log("\u2713 Synkro installed.");
8504
8649
  }
8650
+ function writeSynkroFileIfMissing(opts) {
8651
+ try {
8652
+ const root = execSync6("git rev-parse --show-toplevel", { encoding: "utf-8", timeout: 3e3, stdio: ["pipe", "pipe", "pipe"] }).trim();
8653
+ if (!root) return;
8654
+ const fp = join8(root, ".synkro");
8655
+ if (existsSync9(fp)) {
8656
+ console.log(` .synkro: ${fp} (existing, respected)`);
8657
+ return;
8658
+ }
8659
+ let pool = "auto";
8660
+ const harness = [];
8661
+ if (opts.hasClaudeCode) {
8662
+ harness.push("claude-code");
8663
+ if (!opts.hasCursor) pool = "claude";
8664
+ }
8665
+ if (opts.hasCursor) {
8666
+ harness.push("cursor");
8667
+ if (!opts.hasClaudeCode) pool = "cursor";
8668
+ }
8669
+ const config = {
8670
+ version: 1,
8671
+ harness,
8672
+ grader: {
8673
+ pool,
8674
+ mode: opts.gradingMode === "byok" ? "byok" : "local"
8675
+ },
8676
+ ruleset: "default",
8677
+ scanning: { cwe: true, cve: true }
8678
+ };
8679
+ writeFileSync7(fp, JSON.stringify(config, null, 2) + "\n", "utf-8");
8680
+ console.log(` .synkro: wrote ${fp} (pool=${pool}, mode=${config.grader.mode})`);
8681
+ } catch {
8682
+ }
8683
+ }
8684
+ function readSynkroFilePool2() {
8685
+ try {
8686
+ const root = execSync6("git rev-parse --show-toplevel", { encoding: "utf-8", timeout: 3e3, stdio: ["pipe", "pipe", "pipe"] }).trim();
8687
+ if (!root) return "auto";
8688
+ const fp = join8(root, ".synkro");
8689
+ if (!existsSync9(fp)) return "auto";
8690
+ const parsed = JSON.parse(readFileSync8(fp, "utf-8"));
8691
+ const pool = parsed?.grader?.pool;
8692
+ if (pool === "cursor" || pool === "claude") return pool;
8693
+ } catch {
8694
+ }
8695
+ return "auto";
8696
+ }
8697
+ function readFullSynkroFile() {
8698
+ try {
8699
+ const root = execSync6("git rev-parse --show-toplevel", { encoding: "utf-8", timeout: 3e3, stdio: ["pipe", "pipe", "pipe"] }).trim();
8700
+ if (!root) return null;
8701
+ const fp = join8(root, ".synkro");
8702
+ if (!existsSync9(fp)) return null;
8703
+ const parsed = JSON.parse(readFileSync8(fp, "utf-8"));
8704
+ const valid = ["claude-code", "cursor"];
8705
+ const harness = Array.isArray(parsed.harness) ? parsed.harness.filter((h) => valid.includes(h)) : ["claude-code", "cursor"];
8706
+ return {
8707
+ harness: harness.length > 0 ? harness : ["claude-code", "cursor"],
8708
+ grader: {
8709
+ pool: ["auto", "claude", "cursor"].includes(parsed.grader?.pool) ? parsed.grader.pool : "auto",
8710
+ mode: ["local", "byok"].includes(parsed.grader?.mode) ? parsed.grader.mode : "local"
8711
+ },
8712
+ ruleset: parsed.ruleset || "default",
8713
+ scanning: { cwe: parsed.scanning?.cwe !== false, cve: parsed.scanning?.cve !== false }
8714
+ };
8715
+ } catch {
8716
+ return null;
8717
+ }
8718
+ }
8719
+ function reconcileHarness() {
8720
+ const sf = readFullSynkroFile();
8721
+ if (!sf) {
8722
+ console.log("No .synkro file found in repo root \u2014 skipping harness reconciliation.");
8723
+ return null;
8724
+ }
8725
+ const wantCC = sf.harness.includes("claude-code");
8726
+ const wantCursor = sf.harness.includes("cursor");
8727
+ console.log(`.synkro: harness=[${sf.harness.join(", ")}] pool=${sf.grader.pool} mode=${sf.grader.mode}`);
8728
+ const scripts = writeHookScripts();
8729
+ console.log("Wrote hook scripts to ~/.synkro/hooks/");
8730
+ const ccSettings = join8(homedir8(), ".claude", "settings.json");
8731
+ if (wantCC) {
8732
+ installCCHooks(ccSettings, {
8733
+ bashJudgeScriptPath: scripts.bashScript,
8734
+ bashFollowupScriptPath: scripts.bashFollowupScript,
8735
+ editPrecheckScriptPath: scripts.editPrecheckScript,
8736
+ cwePrecheckScriptPath: scripts.cwePrecheckScript,
8737
+ cvePrecheckScriptPath: scripts.cvePrecheckScript,
8738
+ planJudgeScriptPath: scripts.planJudgeScript,
8739
+ agentJudgeScriptPath: scripts.agentJudgeScript,
8740
+ stopSummaryScriptPath: scripts.stopSummaryScript,
8741
+ sessionStartScriptPath: scripts.sessionStartScript,
8742
+ transcriptSyncScriptPath: scripts.transcriptSyncScript,
8743
+ userPromptSubmitScriptPath: scripts.userPromptSubmitScript,
8744
+ installScanScriptPath: scripts.installScanScript
8745
+ });
8746
+ console.log(" \u2713 Claude Code hooks registered");
8747
+ try {
8748
+ const mcpJwt = readFileSync8(join8(SYNKRO_DIR4, ".mcp-jwt"), "utf-8").trim();
8749
+ if (mcpJwt) {
8750
+ installMcpConfig({ gatewayUrl: "", bearerToken: mcpJwt, local: true });
8751
+ console.log(" \u2713 Claude Code MCP registered");
8752
+ }
8753
+ } catch {
8754
+ }
8755
+ } else {
8756
+ if (uninstallCCHooks(ccSettings)) console.log(" \u2717 Claude Code hooks removed");
8757
+ if (uninstallMcpConfig()) console.log(" \u2717 Claude Code MCP removed");
8758
+ }
8759
+ const cursorHooks = join8(homedir8(), ".cursor", "hooks.json");
8760
+ if (wantCursor) {
8761
+ installCursorHooks(cursorHooks, {
8762
+ bashJudgeScriptPath: scripts.cursorBashJudgeScript,
8763
+ editCaptureScriptPath: scripts.cursorEditCaptureScript,
8764
+ agentCaptureScriptPath: scripts.cursorAgentCaptureScript,
8765
+ bashFollowupScriptPath: scripts.bashFollowupScript,
8766
+ editPrecheckScriptPath: scripts.editPrecheckScript,
8767
+ cwePrecheckScriptPath: scripts.cwePrecheckScript,
8768
+ cvePrecheckScriptPath: scripts.cvePrecheckScript,
8769
+ planJudgeScriptPath: scripts.planJudgeScript,
8770
+ agentJudgeScriptPath: scripts.agentJudgeScript,
8771
+ stopSummaryScriptPath: scripts.stopSummaryScript,
8772
+ sessionStartScriptPath: scripts.sessionStartScript,
8773
+ userPromptSubmitScriptPath: scripts.userPromptSubmitScript,
8774
+ transcriptSyncScriptPath: scripts.transcriptSyncScript,
8775
+ installScanScriptPath: scripts.installScanScript
8776
+ });
8777
+ console.log(" \u2713 Cursor hooks registered");
8778
+ try {
8779
+ installCursorMcpConfig({ gatewayUrl: "", bearerToken: "", local: true });
8780
+ console.log(" \u2713 Cursor MCP registered");
8781
+ } catch {
8782
+ }
8783
+ } else {
8784
+ if (uninstallCursorHooks(cursorHooks)) console.log(" \u2717 Cursor hooks removed");
8785
+ if (uninstallCursorMcpConfig()) console.log(" \u2717 Cursor MCP removed");
8786
+ }
8787
+ const total = parseInt(process.env.SYNKRO_WORKERS_PER_POOL || "8", 10);
8788
+ const providers = [];
8789
+ if (sf.grader.pool === "cursor") {
8790
+ providers.push("cursor");
8791
+ } else if (sf.grader.pool === "claude") {
8792
+ providers.push("claude_code");
8793
+ } else {
8794
+ if (wantCC) providers.push("claude_code");
8795
+ if (wantCursor) providers.push("cursor");
8796
+ }
8797
+ if (providers.length === 0) providers.push("claude_code");
8798
+ return splitWorkers(total, providers);
8799
+ }
8505
8800
  function detectGitRepo2() {
8506
8801
  const run = (cmd2) => {
8507
8802
  try {
8508
- return execSync5(cmd2, { encoding: "utf-8", timeout: 5e3, stdio: ["pipe", "pipe", "pipe"] }).trim();
8803
+ return execSync6(cmd2, { encoding: "utf-8", timeout: 5e3, stdio: ["pipe", "pipe", "pipe"] }).trim();
8509
8804
  } catch {
8510
8805
  return "";
8511
8806
  }
@@ -8530,7 +8825,7 @@ function extractSessionInsights(projectsDir) {
8530
8825
  const sessionId = file.replace(".jsonl", "");
8531
8826
  const filePath = join8(projectsDir, file);
8532
8827
  try {
8533
- const content = readFileSync7(filePath, "utf-8");
8828
+ const content = readFileSync8(filePath, "utf-8");
8534
8829
  const lines = content.split("\n").filter(Boolean);
8535
8830
  for (let i = 0; i < lines.length; i++) {
8536
8831
  try {
@@ -8606,7 +8901,7 @@ function extractTextContent(content) {
8606
8901
  return "";
8607
8902
  }
8608
8903
  function parseTranscriptFile(filePath) {
8609
- const content = readFileSync7(filePath, "utf-8");
8904
+ const content = readFileSync8(filePath, "utf-8");
8610
8905
  const lines = content.split("\n").filter(Boolean);
8611
8906
  const messages = [];
8612
8907
  for (let i = 0; i < lines.length; i++) {
@@ -8676,7 +8971,7 @@ async function syncTranscriptsLocal(mcpPort, mcpToken, repo) {
8676
8971
  process.stdout.write(`\r Progress: ${i + 1}/${files.length} sessions (${totalMessages} messages embedded) `);
8677
8972
  }
8678
8973
  try {
8679
- const content = readFileSync7(join8(projectsDir, file), "utf-8");
8974
+ const content = readFileSync8(join8(projectsDir, file), "utf-8");
8680
8975
  const lineCount = content.split("\n").filter(Boolean).length;
8681
8976
  writeFileSync7(join8(OFFSETS_DIR, sessionId), String(lineCount), "utf-8");
8682
8977
  } catch {
@@ -8730,7 +9025,7 @@ async function syncTranscriptsBulk(gatewayUrl, token, repo) {
8730
9025
  const sessionId = file.replace(".jsonl", "");
8731
9026
  const filePath = join8(projectsDir, file);
8732
9027
  try {
8733
- const content = readFileSync7(filePath, "utf-8");
9028
+ const content = readFileSync8(filePath, "utf-8");
8734
9029
  const lineCount = content.split("\n").filter(Boolean).length;
8735
9030
  writeFileSync7(join8(OFFSETS_DIR, sessionId), String(lineCount), "utf-8");
8736
9031
  } catch {
@@ -8809,7 +9104,7 @@ rl.on('line', async (line) => {
8809
9104
  });
8810
9105
 
8811
9106
  // cli/local-cc/install.ts
8812
- import { existsSync as existsSync10, mkdirSync as mkdirSync9, writeFileSync as writeFileSync8, readFileSync as readFileSync8, chmodSync as chmodSync3, copyFileSync as copyFileSync2, renameSync as renameSync4, unlinkSync as unlinkSync4, openSync, fsyncSync, closeSync } from "fs";
9107
+ import { existsSync as existsSync10, mkdirSync as mkdirSync9, writeFileSync as writeFileSync8, readFileSync as readFileSync9, chmodSync as chmodSync3, copyFileSync as copyFileSync2, renameSync as renameSync4, unlinkSync as unlinkSync4, openSync, fsyncSync, closeSync } from "fs";
8813
9108
  import { join as join9 } from "path";
8814
9109
  import { homedir as homedir9 } from "os";
8815
9110
  import { spawnSync as spawnSync4 } from "child_process";
@@ -8848,7 +9143,7 @@ function safelyMutateClaudeJson(mutator) {
8848
9143
  if (!existsSync10(CLAUDE_JSON_PATH)) {
8849
9144
  return;
8850
9145
  }
8851
- const originalText = readFileSync8(CLAUDE_JSON_PATH, "utf-8");
9146
+ const originalText = readFileSync9(CLAUDE_JSON_PATH, "utf-8");
8852
9147
  let parsed;
8853
9148
  try {
8854
9149
  parsed = JSON.parse(originalText);
@@ -9418,7 +9713,7 @@ var init_disconnect = __esm({
9418
9713
  });
9419
9714
 
9420
9715
  // cli/local-cc/turnLog.ts
9421
- import { appendFileSync, existsSync as existsSync12, mkdirSync as mkdirSync10, openSync as openSync2, readFileSync as readFileSync9, readSync, closeSync as closeSync2, statSync as statSync2, watchFile, unwatchFile } from "fs";
9716
+ import { appendFileSync, existsSync as existsSync12, mkdirSync as mkdirSync10, openSync as openSync2, readFileSync as readFileSync10, readSync, closeSync as closeSync2, statSync as statSync2, watchFile, unwatchFile } from "fs";
9422
9717
  import { dirname as dirname6, join as join11 } from "path";
9423
9718
  import { homedir as homedir11 } from "os";
9424
9719
  function truncate(s, max = PREVIEW_MAX) {
@@ -9460,7 +9755,7 @@ function readRecentTurns(n = 20) {
9460
9755
  try {
9461
9756
  const size = statSync2(TURN_LOG_PATH).size;
9462
9757
  if (size === 0) return [];
9463
- const text = readFileSync9(TURN_LOG_PATH, "utf-8");
9758
+ const text = readFileSync10(TURN_LOG_PATH, "utf-8");
9464
9759
  const lines = text.split("\n").filter(Boolean);
9465
9760
  const lastN = lines.slice(-n).reverse();
9466
9761
  return lastN.map((line) => {
@@ -9906,13 +10201,13 @@ var init_pueue = __esm({
9906
10201
  });
9907
10202
 
9908
10203
  // cli/local-cc/settings.ts
9909
- import { existsSync as existsSync13, readFileSync as readFileSync10 } from "fs";
10204
+ import { existsSync as existsSync13, readFileSync as readFileSync11 } from "fs";
9910
10205
  import { homedir as homedir13 } from "os";
9911
10206
  import { join as join13 } from "path";
9912
10207
  function isLocalCCEnabled() {
9913
10208
  if (!existsSync13(CONFIG_PATH3)) return false;
9914
10209
  try {
9915
- const content = readFileSync10(CONFIG_PATH3, "utf-8");
10210
+ const content = readFileSync11(CONFIG_PATH3, "utf-8");
9916
10211
  const match = content.match(/^SYNKRO_LOCAL_INFERENCE='([^']*)'/m);
9917
10212
  return match?.[1] === "yes";
9918
10213
  } catch {
@@ -9936,7 +10231,7 @@ import { spawnSync as spawnSync7 } from "child_process";
9936
10231
  import { homedir as homedir14 } from "os";
9937
10232
  import { join as join14 } from "path";
9938
10233
  import { readFileSync as fsReadFileSync, existsSync as fsExistsSync } from "fs";
9939
- import { existsSync as existsSync14, readFileSync as readFileSync11, writeFileSync as writeFileSync9 } from "fs";
10234
+ import { existsSync as existsSync14, readFileSync as readFileSync12, writeFileSync as writeFileSync9 } from "fs";
9940
10235
  function deploymentMode() {
9941
10236
  const env = (process.env.SYNKRO_DEPLOYMENT_MODE || "").toLowerCase();
9942
10237
  if (env === "docker") return "docker";
@@ -10043,14 +10338,14 @@ TROUBLESHOOTING
10043
10338
  }
10044
10339
  function readGatewayUrl() {
10045
10340
  if (existsSync14(CONFIG_PATH4)) {
10046
- const m = readFileSync11(CONFIG_PATH4, "utf-8").match(/^SYNKRO_GATEWAY_URL='([^']*)'/m);
10341
+ const m = readFileSync12(CONFIG_PATH4, "utf-8").match(/^SYNKRO_GATEWAY_URL='([^']*)'/m);
10047
10342
  if (m) return m[1];
10048
10343
  }
10049
10344
  return "https://api.synkro.sh";
10050
10345
  }
10051
10346
  function updateLocalInferenceFlag(enabled) {
10052
10347
  if (!existsSync14(CONFIG_PATH4)) return;
10053
- let content = readFileSync11(CONFIG_PATH4, "utf-8");
10348
+ let content = readFileSync12(CONFIG_PATH4, "utf-8");
10054
10349
  const flag = enabled ? "yes" : "no";
10055
10350
  if (content.includes("SYNKRO_LOCAL_INFERENCE=")) {
10056
10351
  content = content.replace(/^SYNKRO_LOCAL_INFERENCE='[^']*'/m, `SYNKRO_LOCAL_INFERENCE='${flag}'`);
@@ -10225,7 +10520,19 @@ function cmdStop() {
10225
10520
  }
10226
10521
  async function cmdRestart(rest = []) {
10227
10522
  if (inDockerMode()) {
10228
- const { claudeWorkers, cursorWorkers } = resolveWorkerConfig(rest);
10523
+ const { explicit } = resolveWorkerConfig(rest);
10524
+ let claudeWorkers;
10525
+ let cursorWorkers;
10526
+ if (explicit) {
10527
+ ({ claudeWorkers, cursorWorkers } = resolveWorkerConfig(rest));
10528
+ } else {
10529
+ const reconciled = reconcileHarness();
10530
+ if (reconciled) {
10531
+ ({ claudeWorkers, cursorWorkers } = reconciled);
10532
+ } else {
10533
+ ({ claudeWorkers, cursorWorkers } = resolveWorkerConfig(rest));
10534
+ }
10535
+ }
10229
10536
  console.log(`Restarting synkro-server container (${claudeWorkers} claude + ${cursorWorkers} cursor, pulling latest image)...`);
10230
10537
  await dockerUpdate({ claudeWorkers, cursorWorkers });
10231
10538
  const ready = await waitForContainerReady(6e4);
@@ -10469,6 +10776,7 @@ var init_localCc = __esm({
10469
10776
  init_settings();
10470
10777
  init_macKeychain();
10471
10778
  init_dockerInstall();
10779
+ init_install();
10472
10780
  init_client();
10473
10781
  init_stub();
10474
10782
  SYNKRO_CONFIG_PATH = join14(homedir14(), ".synkro", "config.env");
@@ -10579,13 +10887,13 @@ var config_exports = {};
10579
10887
  __export(config_exports, {
10580
10888
  configCommand: () => configCommand
10581
10889
  });
10582
- import { readFileSync as readFileSync12, writeFileSync as writeFileSync10, existsSync as existsSync15 } from "fs";
10890
+ import { readFileSync as readFileSync13, writeFileSync as writeFileSync10, existsSync as existsSync15 } from "fs";
10583
10891
  import { join as join15 } from "path";
10584
10892
  import { homedir as homedir15 } from "os";
10585
10893
  function readConfigEnv() {
10586
10894
  if (!existsSync15(CONFIG_PATH5)) return {};
10587
10895
  const out = {};
10588
- for (const line of readFileSync12(CONFIG_PATH5, "utf-8").split("\n")) {
10896
+ for (const line of readFileSync13(CONFIG_PATH5, "utf-8").split("\n")) {
10589
10897
  const t = line.trim();
10590
10898
  if (!t || t.startsWith("#")) continue;
10591
10899
  const eq = t.indexOf("=");
@@ -10598,7 +10906,7 @@ function updateConfigValue(key, value) {
10598
10906
  console.error("No config found. Run `synkro install` first.");
10599
10907
  process.exit(1);
10600
10908
  }
10601
- const lines = readFileSync12(CONFIG_PATH5, "utf-8").split("\n");
10909
+ const lines = readFileSync13(CONFIG_PATH5, "utf-8").split("\n");
10602
10910
  const pattern = new RegExp(`^${key}=`);
10603
10911
  let found = false;
10604
10912
  const updated = lines.map((line) => {
@@ -10725,14 +11033,14 @@ var init_config = __esm({
10725
11033
  });
10726
11034
 
10727
11035
  // cli/bootstrap.js
10728
- import { readFileSync as readFileSync13, existsSync as existsSync16 } from "fs";
11036
+ import { readFileSync as readFileSync14, existsSync as existsSync16 } from "fs";
10729
11037
  import { resolve as resolve2 } from "path";
10730
11038
  var envCandidates = [
10731
11039
  resolve2(process.env.HOME ?? "", ".synkro", "config.env")
10732
11040
  ];
10733
11041
  for (const envPath of envCandidates) {
10734
11042
  if (!existsSync16(envPath)) continue;
10735
- const envContent = readFileSync13(envPath, "utf-8");
11043
+ const envContent = readFileSync14(envPath, "utf-8");
10736
11044
  for (const line of envContent.split("\n")) {
10737
11045
  const trimmed = line.trim();
10738
11046
  if (!trimmed || trimmed.startsWith("#")) continue;
@@ -10747,7 +11055,7 @@ var args = process.argv.slice(2);
10747
11055
  var cmd = args[0] || "";
10748
11056
  var subArgs = args.slice(1);
10749
11057
  function printVersion() {
10750
- console.log("1.6.31");
11058
+ console.log("1.6.33");
10751
11059
  }
10752
11060
  function printHelp2() {
10753
11061
  console.log(`Synkro CLI \u2014 runtime safety for AI coding agents