@synkro-sh/cli 1.7.95 → 1.8.0

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
@@ -145,9 +145,9 @@ function hostnameHash() {
145
145
  }
146
146
  function getIdentity() {
147
147
  if (cached2) return cached2;
148
- let cliVersion = "0.0.0";
148
+ let cliVersion2 = "0.0.0";
149
149
  try {
150
- cliVersion = "1.7.95";
150
+ cliVersion2 = "1.8.0";
151
151
  } catch {
152
152
  }
153
153
  const creds = loadCredentialsIdentity();
@@ -157,7 +157,7 @@ function getIdentity() {
157
157
  user_id: creds.user_id,
158
158
  org_id: creds.org_id,
159
159
  email: creds.email,
160
- cli_version: cliVersion,
160
+ cli_version: cliVersion2,
161
161
  platform: platform(),
162
162
  hostname_hash: hostnameHash(),
163
163
  node_version: process.version,
@@ -183,8 +183,8 @@ import { appendFileSync, existsSync as existsSync3, mkdirSync as mkdirSync2 } fr
183
183
  import { homedir as homedir3 } from "os";
184
184
  import { join as join3 } from "path";
185
185
  function deriveGit(cwd) {
186
- const cached4 = gitCache.get(cwd);
187
- if (cached4) return cached4;
186
+ const cached5 = gitCache.get(cwd);
187
+ if (cached5) return cached5;
188
188
  const info = { branch: null, sha: null };
189
189
  try {
190
190
  const branch = execFileSync("git", ["rev-parse", "--abbrev-ref", "HEAD"], {
@@ -1777,6 +1777,27 @@ function uninstallCCHooks(settingsPath) {
1777
1777
  writeSettingsAtomic(settingsPath, settings);
1778
1778
  return true;
1779
1779
  }
1780
+ function inspectCCHooks(settingsPath) {
1781
+ if (!existsSync11(settingsPath)) {
1782
+ return { installed: false, preToolUseBash: false, postToolUseEdit: false, sessionEnd: false, sessionStart: false };
1783
+ }
1784
+ const settings = readSettings(settingsPath);
1785
+ const pre = settings.hooks?.PreToolUse ?? [];
1786
+ const post = settings.hooks?.PostToolUse ?? [];
1787
+ const sessionEndHooks = settings.hooks?.SessionEnd ?? [];
1788
+ const sessionStartHooks = settings.hooks?.SessionStart ?? [];
1789
+ const preToolUseBash = pre.some((e) => e?.[SYNKRO_MARKER] === true);
1790
+ const postToolUseEdit = post.some((e) => e?.[SYNKRO_MARKER] === true);
1791
+ const sessionEnd = sessionEndHooks.some((e) => e?.[SYNKRO_MARKER] === true);
1792
+ const sessionStart = sessionStartHooks.some((e) => e?.[SYNKRO_MARKER] === true);
1793
+ return {
1794
+ installed: preToolUseBash || postToolUseEdit || sessionEnd || sessionStart,
1795
+ preToolUseBash,
1796
+ postToolUseEdit,
1797
+ sessionEnd,
1798
+ sessionStart
1799
+ };
1800
+ }
1780
1801
  var SYNKRO_MARKER, USAGE_PROXY_URL;
1781
1802
  var init_ccHookConfig = __esm({
1782
1803
  "cli/installer/ccHookConfig.ts"() {
@@ -1985,6 +2006,74 @@ function uninstallCursorHooks(hooksJsonPath) {
1985
2006
  writeHooksFileAtomic(hooksJsonPath, file);
1986
2007
  return true;
1987
2008
  }
2009
+ function preToolUseUsesScript(hooks, scriptBasename) {
2010
+ return (hooks ?? []).some(
2011
+ (e) => isSynkroEntry2(e) && typeof e.command === "string" && e.command.includes(scriptBasename)
2012
+ );
2013
+ }
2014
+ function inspectCursorHooks(hooksJsonPath) {
2015
+ let file;
2016
+ try {
2017
+ file = readHooksFile(hooksJsonPath);
2018
+ } catch {
2019
+ return {
2020
+ installed: false,
2021
+ sessionStart: false,
2022
+ sessionEnd: false,
2023
+ beforeSubmitPrompt: false,
2024
+ stop: false,
2025
+ beforeShellExecution: false,
2026
+ afterShellExecution: false,
2027
+ preToolUse: false,
2028
+ preToolUseBash: false,
2029
+ preToolUseEdit: false,
2030
+ preToolUseCwe: false,
2031
+ preToolUseCve: false,
2032
+ preToolUseAgent: false,
2033
+ preToolUsePlan: false,
2034
+ afterFileEdit: false,
2035
+ postToolUse: false,
2036
+ beforeMCPExecution: false
2037
+ };
2038
+ }
2039
+ const h = file.hooks ?? {};
2040
+ const sessionStart = (h.sessionStart ?? []).some((e) => isSynkroEntry2(e));
2041
+ const sessionEnd = (h.sessionEnd ?? []).some((e) => isSynkroEntry2(e));
2042
+ const beforeSubmitPrompt = (h.beforeSubmitPrompt ?? []).some((e) => isSynkroEntry2(e));
2043
+ const stop = (h.stop ?? []).some((e) => isSynkroEntry2(e));
2044
+ const beforeShellExecution = (h.beforeShellExecution ?? []).some((e) => isSynkroEntry2(e));
2045
+ const afterShellExecution = (h.afterShellExecution ?? []).some((e) => isSynkroEntry2(e));
2046
+ const pre = h.preToolUse ?? [];
2047
+ const preToolUseBash = preToolUseUsesScript(pre, "cc-bash-judge") || preToolUseUsesScript(pre, "cursor-bash-judge");
2048
+ const preToolUseEdit = preToolUseUsesScript(pre, "cc-edit-precheck") || preToolUseUsesScript(pre, "cursor-edit-precheck");
2049
+ const preToolUseCwe = preToolUseUsesScript(pre, "cc-cwe-precheck");
2050
+ const preToolUseCve = preToolUseUsesScript(pre, "cc-cve-precheck");
2051
+ const preToolUseAgent = preToolUseUsesScript(pre, "cc-agent-judge");
2052
+ const preToolUsePlan = preToolUseUsesScript(pre, "cc-plan-judge");
2053
+ const preToolUse = preToolUseBash || preToolUseEdit || preToolUseCwe || preToolUseCve || preToolUseAgent || preToolUsePlan;
2054
+ const afterFileEdit = (h.afterFileEdit ?? []).some((e) => isSynkroEntry2(e));
2055
+ const postToolUse = (h.postToolUse ?? []).some((e) => isSynkroEntry2(e));
2056
+ const beforeMCPExecution = (h.beforeMCPExecution ?? []).some((e) => isSynkroEntry2(e));
2057
+ return {
2058
+ installed: sessionStart || sessionEnd || beforeSubmitPrompt || stop || beforeShellExecution || afterShellExecution || preToolUse || afterFileEdit || postToolUse || beforeMCPExecution,
2059
+ sessionStart,
2060
+ sessionEnd,
2061
+ beforeSubmitPrompt,
2062
+ stop,
2063
+ beforeShellExecution,
2064
+ afterShellExecution,
2065
+ preToolUse,
2066
+ preToolUseBash,
2067
+ preToolUseEdit,
2068
+ preToolUseCwe,
2069
+ preToolUseCve,
2070
+ preToolUseAgent,
2071
+ preToolUsePlan,
2072
+ afterFileEdit,
2073
+ postToolUse,
2074
+ beforeMCPExecution
2075
+ };
2076
+ }
1988
2077
  var SYNKRO_MARKER2, ALLOWED_PARENT_DIRS, ALL_EVENTS;
1989
2078
  var init_cursorHookConfig = __esm({
1990
2079
  "cli/installer/cursorHookConfig.ts"() {
@@ -2128,6 +2217,52 @@ function uninstallCodexHooks(hooksJsonPath) {
2128
2217
  writeHooksFileAtomic2(hooksJsonPath, file);
2129
2218
  return true;
2130
2219
  }
2220
+ function inspectCodexHooks(hooksJsonPath) {
2221
+ let file;
2222
+ try {
2223
+ file = readHooksFile2(hooksJsonPath);
2224
+ } catch {
2225
+ return {
2226
+ installed: false,
2227
+ preToolUse: false,
2228
+ preToolUseEdit: false,
2229
+ preToolUseCwe: false,
2230
+ preToolUseCve: false,
2231
+ postToolUse: false,
2232
+ postToolUseEdit: false,
2233
+ sessionStart: false,
2234
+ sessionEnd: false
2235
+ };
2236
+ }
2237
+ const h = file.hooks ?? {};
2238
+ const preToolUse = (h.PreToolUse ?? []).some((e) => isSynkroEntry3(e));
2239
+ const editEntries = (h.PreToolUse ?? []).filter(
2240
+ (e) => typeof e?.matcher === "string" && (e.matcher.includes("apply_patch") || e.matcher.includes("ApplyPatch") || e.matcher.includes("Edit"))
2241
+ );
2242
+ const entryUsesScript = (needle) => editEntries.some(
2243
+ (entry) => (entry.hooks ?? []).some((handler) => String(handler?.command || "").includes(needle))
2244
+ );
2245
+ const preToolUseEdit = entryUsesScript("cc-edit-precheck");
2246
+ const preToolUseCwe = entryUsesScript("cc-cwe-precheck");
2247
+ const preToolUseCve = entryUsesScript("cc-cve-precheck");
2248
+ const postToolUse = (h.PostToolUse ?? []).some((e) => isSynkroEntry3(e));
2249
+ const postToolUseEdit = (h.PostToolUse ?? []).some(
2250
+ (entry) => entry.matcher === CODEX_EDIT_MATCHER && (entry.hooks ?? []).some((handler) => String(handler?.command || "").includes("cc-edit-followup"))
2251
+ );
2252
+ const sessionStart = (h.SessionStart ?? []).some((e) => isSynkroEntry3(e));
2253
+ const sessionEnd = (h.SessionEnd ?? []).some((e) => isSynkroEntry3(e));
2254
+ return {
2255
+ installed: preToolUseEdit && preToolUseCwe && preToolUseCve && postToolUseEdit,
2256
+ preToolUse,
2257
+ preToolUseEdit,
2258
+ preToolUseCwe,
2259
+ preToolUseCve,
2260
+ postToolUse,
2261
+ postToolUseEdit,
2262
+ sessionStart,
2263
+ sessionEnd
2264
+ };
2265
+ }
2131
2266
  var SYNKRO_MARKER3, CODEX_BASH_MATCHER, M_BASH, CODEX_EDIT_MATCHER, M_EDIT, M_AGENT, M_MCP, M_ACTIVATE, ALL_EVENTS2;
2132
2267
  var init_codexHookConfig = __esm({
2133
2268
  "cli/installer/codexHookConfig.ts"() {
@@ -2189,7 +2324,7 @@ function buildCodexHookTrustEdits(hooks) {
2189
2324
  return [...edits.values()];
2190
2325
  }
2191
2326
  function queryCodexHookTrust(codexBinary = "codex", cwd = process.cwd(), autoTrust = false) {
2192
- return new Promise((resolve6) => {
2327
+ return new Promise((resolve7) => {
2193
2328
  let settled = false;
2194
2329
  let stdout = "";
2195
2330
  let pending = "";
@@ -2207,7 +2342,7 @@ function queryCodexHookTrust(codexBinary = "codex", cwd = process.cwd(), autoTru
2207
2342
  child.kill();
2208
2343
  } catch {
2209
2344
  }
2210
- resolve6(summary);
2345
+ resolve7(summary);
2211
2346
  };
2212
2347
  const timer = setTimeout(() => finish(null), 1e4);
2213
2348
  try {
@@ -2763,9 +2898,9 @@ var init_hookScriptsTs = __esm({
2763
2898
  "cli/installer/hookScriptsTs.ts"() {
2764
2899
  "use strict";
2765
2900
  STUB_COMMON_TS = String.raw`import { existsSync, readFileSync, readdirSync, mkdirSync, writeFileSync, appendFileSync, statSync, realpathSync, openSync, readSync, closeSync } from 'node:fs';
2766
- import { execSync } from 'node:child_process';
2901
+ import { execFileSync, execSync } from 'node:child_process';
2767
2902
  import { homedir } from 'node:os';
2768
- import { basename, join, resolve, relative, isAbsolute } from 'node:path';
2903
+ import { basename, dirname, join, resolve, relative, isAbsolute } from 'node:path';
2769
2904
  import { randomUUID, createHash } from 'node:crypto';
2770
2905
 
2771
2906
  const HOME = homedir();
@@ -3001,6 +3136,453 @@ function gitRoot(cwd: string): string {
3001
3136
  } catch { return ''; }
3002
3137
  }
3003
3138
 
3139
+ function taskRepoContext(root: string): any {
3140
+ if (!root) return undefined;
3141
+ try {
3142
+ const run = (args: string[]) => execFileSync('git', args, {
3143
+ cwd: root,
3144
+ timeout: 2000,
3145
+ encoding: 'utf-8',
3146
+ stdio: ['pipe', 'pipe', 'pipe'],
3147
+ }).trim();
3148
+ const branch = run(['rev-parse', '--abbrev-ref', 'HEAD']);
3149
+ const sha = run(['rev-parse', 'HEAD']);
3150
+ const remote = run(['remote', 'get-url', 'origin']);
3151
+ if (!branch || !sha || !remote) return undefined;
3152
+ let baseBranch = branch === 'HEAD' ? '' : branch;
3153
+ let baseSha = sha;
3154
+ try {
3155
+ const remoteHead = run(['symbolic-ref', '--quiet', '--short', 'refs/remotes/origin/HEAD']);
3156
+ if (remoteHead.startsWith('origin/')) {
3157
+ baseBranch = remoteHead.slice('origin/'.length);
3158
+ baseSha = run(['rev-parse', remoteHead]);
3159
+ }
3160
+ } catch {}
3161
+ if (!baseBranch || baseBranch === branch) {
3162
+ for (const candidate of ['main', 'master']) {
3163
+ try {
3164
+ let candidateSha = '';
3165
+ try { candidateSha = run(['rev-parse', '--verify', 'refs/remotes/origin/' + candidate]); }
3166
+ catch { candidateSha = run(['rev-parse', '--verify', 'refs/heads/' + candidate]); }
3167
+ if (candidateSha) {
3168
+ baseBranch = candidate;
3169
+ baseSha = candidateSha;
3170
+ break;
3171
+ }
3172
+ } catch {
3173
+ continue;
3174
+ }
3175
+ }
3176
+ }
3177
+ if (!baseBranch || !baseSha) return undefined;
3178
+ let worktree = false;
3179
+ try { worktree = statSync(join(root, '.git')).isFile(); } catch {}
3180
+ const commonRoot = sharedRepoRoot(root);
3181
+ return { root, commonRoot, remote, branch, sha, baseBranch, baseSha, worktree };
3182
+ } catch { return undefined; }
3183
+ }
3184
+
3185
+ function taskScmBlockResponse(harness: string, reason: string): string {
3186
+ const message = '[synkro:scm] ' + reason;
3187
+ if (harness === 'cursor') return JSON.stringify({ permission: 'deny', user_message: message, agent_message: message });
3188
+ return JSON.stringify({
3189
+ systemMessage: message,
3190
+ hookSpecificOutput: {
3191
+ hookEventName: 'PreToolUse',
3192
+ permissionDecision: 'deny',
3193
+ permissionDecisionReason: message,
3194
+ additionalContext: message + ' Resolve the repository state, then retry the tool call.',
3195
+ },
3196
+ });
3197
+ }
3198
+
3199
+ function safeTaskBranch(value: string): boolean {
3200
+ return /^[a-z][a-z0-9]*-[0-9]+\/[a-z0-9][a-z0-9-]{0,47}$/.test(value);
3201
+ }
3202
+
3203
+ function gitOutput(root: string, args: string[], timeout = 5000): string {
3204
+ return execFileSync('git', args, {
3205
+ cwd: root,
3206
+ timeout,
3207
+ encoding: 'utf-8',
3208
+ stdio: ['pipe', 'pipe', 'pipe'],
3209
+ }).trim();
3210
+ }
3211
+
3212
+ function gitSucceeds(root: string, args: string[]): boolean {
3213
+ try { execFileSync('git', args, { cwd: root, timeout: 5000, stdio: 'ignore' }); return true; }
3214
+ catch { return false; }
3215
+ }
3216
+
3217
+ function samePath(left: string, right: string): boolean {
3218
+ try { return realpathSync(left) === realpathSync(right); }
3219
+ catch { return resolve(left) === resolve(right); }
3220
+ }
3221
+
3222
+ function sharedRepoRoot(root: string): string {
3223
+ const raw = gitOutput(root, ['rev-parse', '--git-common-dir']);
3224
+ const common = resolve(root, raw);
3225
+ if (basename(common) !== '.git') {
3226
+ throw new Error('task worktrees require a non-bare repository with shared Git metadata');
3227
+ }
3228
+ return dirname(common);
3229
+ }
3230
+
3231
+ function taskWorktreePath(root: string, taskId: string): string {
3232
+ return join(sharedRepoRoot(root), '.synkro-worktrees', taskId);
3233
+ }
3234
+
3235
+ function codexDesktopOrigin(): boolean {
3236
+ const origin = String(process.env.CODEX_INTERNAL_ORIGINATOR_OVERRIDE || '').toLowerCase();
3237
+ const bundle = String(process.env.__CFBundleIdentifier || '').toLowerCase();
3238
+ return origin === 'codex desktop' || bundle === 'com.openai.codex';
3239
+ }
3240
+
3241
+ function ignoreManagedWorktreeDirectory(root: string): void {
3242
+ const common = join(sharedRepoRoot(root), '.git');
3243
+ const excludePath = join(common, 'info', 'exclude');
3244
+ const entry = '.synkro-worktrees/';
3245
+ let existing = '';
3246
+ try { existing = readFileSync(excludePath, 'utf-8'); } catch {}
3247
+ if (existing.split(/\r?\n/).includes(entry)) return;
3248
+ mkdirSync(dirname(excludePath), { recursive: true });
3249
+ appendFileSync(excludePath, (existing && !existing.endsWith('\n') ? '\n' : '') + entry + '\n');
3250
+ }
3251
+
3252
+ interface TaskWorktreeRecord { path: string; branch: string }
3253
+
3254
+ function taskWorktreeRecords(root: string): TaskWorktreeRecord[] {
3255
+ const records: TaskWorktreeRecord[] = [];
3256
+ let current: TaskWorktreeRecord = { path: '', branch: '' };
3257
+ const flush = () => {
3258
+ if (current.path) records.push(current);
3259
+ current = { path: '', branch: '' };
3260
+ };
3261
+ for (const line of (gitOutput(root, ['worktree', 'list', '--porcelain']) + '\n').split('\n')) {
3262
+ if (!line) { flush(); continue; }
3263
+ if (line.startsWith('worktree ')) current.path = line.slice('worktree '.length);
3264
+ else if (line.startsWith('branch refs/heads/')) current.branch = line.slice('branch refs/heads/'.length);
3265
+ }
3266
+ return records;
3267
+ }
3268
+
3269
+ function gitOperationInProgress(root: string): boolean {
3270
+ if (gitSucceeds(root, ['rev-parse', '--verify', '--quiet', 'MERGE_HEAD'])) return true;
3271
+ for (const state of ['rebase-merge', 'rebase-apply']) {
3272
+ try {
3273
+ const raw = gitOutput(root, ['rev-parse', '--git-path', state]);
3274
+ const path = isAbsolute(raw) ? raw : resolve(root, raw);
3275
+ if (existsSync(path)) return true;
3276
+ } catch {}
3277
+ }
3278
+ return false;
3279
+ }
3280
+
3281
+ function validateTaskWorktree(root: string, currentRoot: string, op: any): string {
3282
+ const expected = taskWorktreePath(root, String(op.taskId || ''));
3283
+ const supplied = String(op.worktreePath || '');
3284
+ const managed = Boolean(supplied) && samePath(supplied, expected);
3285
+ const nativeCurrent = Boolean(supplied) && samePath(supplied, currentRoot);
3286
+ if (!managed && !nativeCurrent) {
3287
+ throw new Error('task worktree path does not match the managed or current native workspace');
3288
+ }
3289
+ const record = taskWorktreeRecords(root).find((item) => samePath(item.path, supplied));
3290
+ if (!record || record.branch !== String(op.branchName || '')) {
3291
+ throw new Error('task worktree is stale or no longer owns the canonical branch');
3292
+ }
3293
+ if (gitOutput(supplied, ['rev-parse', '--abbrev-ref', 'HEAD']) !== String(op.branchName || '')) {
3294
+ throw new Error('task worktree is not on the canonical branch');
3295
+ }
3296
+ return supplied;
3297
+ }
3298
+
3299
+ async function completeTaskScm(sessionId: string, op: any, ok: boolean, error: string, worktreePath: string): Promise<void> {
3300
+ try {
3301
+ await fetch('http://127.0.0.1:' + PORT + '/api/local/task-scm/complete', {
3302
+ method: 'POST',
3303
+ headers: { 'Content-Type': 'application/json', Authorization: 'Bearer ' + loadMcpJwt() },
3304
+ body: JSON.stringify({ taskId: op.taskId, sessionId, kind: op.kind, ok, error, worktreePath }),
3305
+ signal: AbortSignal.timeout(5000),
3306
+ });
3307
+ } catch {}
3308
+ }
3309
+
3310
+ function executeTaskScm(root: string, op: any): string {
3311
+ if (!op || typeof op !== 'object') throw new Error('invalid SCM operation');
3312
+ if (!/^task_[a-z0-9]{8}$/i.test(String(op.taskId || ''))) throw new Error('invalid task id');
3313
+ const branch = String(op.branchName || '');
3314
+ if (!safeTaskBranch(branch)) throw new Error('invalid canonical task branch');
3315
+ const boundRoot = String(op.repoRoot || '');
3316
+ if (!boundRoot || (!samePath(boundRoot, root) && !samePath(sharedRepoRoot(boundRoot), sharedRepoRoot(root)))) {
3317
+ throw new Error('task workspace does not match this repository');
3318
+ }
3319
+
3320
+ if (op.kind === 'branch') {
3321
+ if (op.prepareHandoffBranch === true) {
3322
+ if (gitOperationInProgress(root)) {
3323
+ throw new Error('finish the active merge or rebase before task automation');
3324
+ }
3325
+ const originalBranch = gitOutput(root, ['rev-parse', '--abbrev-ref', 'HEAD']);
3326
+ const head = gitOutput(root, ['rev-parse', 'HEAD']);
3327
+ if (!op.startSha || head !== String(op.startSha)) {
3328
+ throw new Error('Codex local checkout HEAD does not match the task creation revision');
3329
+ }
3330
+ if (gitSucceeds(boundRoot, ['show-ref', '--verify', '--quiet', 'refs/heads/' + branch])) {
3331
+ throw new Error('the canonical task branch already exists in another workspace');
3332
+ }
3333
+ if (!op.baseSha || !gitSucceeds(boundRoot, ['cat-file', '-e', String(op.baseSha) + '^{commit}'])) {
3334
+ throw new Error('task base revision is unavailable in this repository');
3335
+ }
3336
+ gitOutput(root, ['branch', branch, String(op.baseSha)], 30000);
3337
+ if (gitOutput(root, ['rev-parse', 'HEAD']) !== head
3338
+ || gitOutput(root, ['rev-parse', '--abbrev-ref', 'HEAD']) !== originalBranch) {
3339
+ throw new Error('Git changed the shared checkout while reserving the task branch');
3340
+ }
3341
+ return '';
3342
+ }
3343
+ if (op.adoptExistingWorktree === true) {
3344
+ const supplied = String(op.worktreePath || '');
3345
+ if (!supplied || !samePath(root, supplied)) {
3346
+ throw new Error('Codex native worktree does not match the current session workspace');
3347
+ }
3348
+ const record = taskWorktreeRecords(boundRoot).find((item) => samePath(item.path, root));
3349
+ if (!record) throw new Error('Codex native worktree is not registered with Git');
3350
+ if (gitOperationInProgress(root)) {
3351
+ throw new Error('finish the active merge or rebase before task automation');
3352
+ }
3353
+ if (gitOutput(root, ['status', '--porcelain'])) {
3354
+ throw new Error('Codex native worktree must be clean before it can be assigned to a new task');
3355
+ }
3356
+ const head = gitOutput(root, ['rev-parse', 'HEAD']);
3357
+ if (!op.startSha || head !== String(op.startSha)) {
3358
+ throw new Error('Codex native worktree HEAD does not match the task creation revision');
3359
+ }
3360
+ const currentBranch = gitOutput(root, ['rev-parse', '--abbrev-ref', 'HEAD']);
3361
+ if (currentBranch !== branch) {
3362
+ if (gitSucceeds(boundRoot, ['show-ref', '--verify', '--quiet', 'refs/heads/' + branch])) {
3363
+ throw new Error('the canonical task branch already exists in another workspace');
3364
+ }
3365
+ if (!op.baseSha || !gitSucceeds(boundRoot, ['cat-file', '-e', String(op.baseSha) + '^{commit}'])) {
3366
+ throw new Error('task base revision is unavailable in this repository');
3367
+ }
3368
+ gitOutput(root, ['switch', '-c', branch, String(op.baseSha)], 30000);
3369
+ }
3370
+ if (gitOutput(root, ['rev-parse', '--abbrev-ref', 'HEAD']) !== branch) {
3371
+ throw new Error('Codex native worktree did not adopt the canonical task branch');
3372
+ }
3373
+ return root;
3374
+ }
3375
+ const worktreePath = taskWorktreePath(boundRoot, String(op.taskId));
3376
+ const existing = taskWorktreeRecords(boundRoot).find((item) => samePath(item.path, worktreePath));
3377
+ if (existing) {
3378
+ if (existing.branch !== branch || gitOutput(worktreePath, ['rev-parse', '--abbrev-ref', 'HEAD']) !== branch) {
3379
+ throw new Error('managed task worktree exists on a conflicting branch');
3380
+ }
3381
+ return worktreePath;
3382
+ }
3383
+ if (existsSync(worktreePath)) {
3384
+ throw new Error('managed task worktree path exists but is not registered with Git');
3385
+ }
3386
+ if (gitOperationInProgress(root)) {
3387
+ throw new Error('finish the active merge or rebase before task automation');
3388
+ }
3389
+ const head = gitOutput(root, ['rev-parse', 'HEAD']);
3390
+ if (!op.startSha || head !== String(op.startSha)) {
3391
+ throw new Error('repository HEAD changed after task creation');
3392
+ }
3393
+ if (gitSucceeds(root, ['show-ref', '--verify', '--quiet', 'refs/heads/' + branch])) {
3394
+ throw new Error('the canonical task branch already exists in another workspace');
3395
+ }
3396
+ if (!op.baseSha) throw new Error('task base revision is missing');
3397
+ if (!gitSucceeds(root, ['cat-file', '-e', String(op.baseSha) + '^{commit}'])) {
3398
+ throw new Error('task base revision is unavailable in this repository');
3399
+ }
3400
+ const originalBranch = gitOutput(root, ['rev-parse', '--abbrev-ref', 'HEAD']);
3401
+ ignoreManagedWorktreeDirectory(boundRoot);
3402
+ mkdirSync(dirname(worktreePath), { recursive: true });
3403
+ gitOutput(root, ['worktree', 'add', '-b', branch, worktreePath, String(op.baseSha)], 30000);
3404
+ if (gitOutput(root, ['rev-parse', 'HEAD']) !== head
3405
+ || gitOutput(root, ['rev-parse', '--abbrev-ref', 'HEAD']) !== originalBranch) {
3406
+ throw new Error('Git changed the originating checkout while creating the task worktree');
3407
+ }
3408
+ const record = taskWorktreeRecords(boundRoot).find((item) => samePath(item.path, worktreePath));
3409
+ if (!record || record.branch !== branch
3410
+ || gitOutput(worktreePath, ['rev-parse', '--abbrev-ref', 'HEAD']) !== branch) {
3411
+ throw new Error('Git did not register the canonical task worktree');
3412
+ }
3413
+ return worktreePath;
3414
+ }
3415
+
3416
+ if (op.kind !== 'push') throw new Error('unknown SCM operation');
3417
+ const worktreePath = validateTaskWorktree(boundRoot, root, op);
3418
+ if (gitOperationInProgress(worktreePath)) {
3419
+ throw new Error('finish the active merge or rebase in the task worktree before pushing');
3420
+ }
3421
+ if (gitOutput(worktreePath, ['status', '--porcelain'])) {
3422
+ gitOutput(worktreePath, ['add', '-A']);
3423
+ if (!gitSucceeds(worktreePath, ['diff', '--cached', '--quiet'])) {
3424
+ gitOutput(worktreePath, ['commit', '-m', String(op.commitMessage || 'Synkro task')], 30000);
3425
+ }
3426
+ }
3427
+ gitOutput(worktreePath, ['push', '-u', 'origin', branch], 60000);
3428
+ return worktreePath;
3429
+ }
3430
+
3431
+ interface TaskScmReconcileResult { reason: string; context: string }
3432
+
3433
+ function taskScmWorkspaceContext(workspace: any): string {
3434
+ if (!workspace || typeof workspace !== 'object') return '';
3435
+ const taskId = String(workspace.taskId || '');
3436
+ const linearRef = String(workspace.linearRef || '');
3437
+ const branchName = String(workspace.branchName || '');
3438
+ const worktreePath = String(workspace.worktreePath || '');
3439
+ if (!taskId || !branchName) return '';
3440
+ return '[synkro:task-workspace] task=' + taskId
3441
+ + (linearRef ? ' linear=' + linearRef : '')
3442
+ + ' branch=' + branchName
3443
+ + (worktreePath ? ' worktree=' + worktreePath : ' worktree=pending-native-handoff');
3444
+ }
3445
+
3446
+ function shellTaskWorkspaceArg(value: string): string {
3447
+ return "'" + value.replace(/'/g, "'\"'\"'") + "'";
3448
+ }
3449
+
3450
+ function taskScmWorkspaceInstruction(harness: string, sessionId: string, workspace: any): string {
3451
+ const context = taskScmWorkspaceContext(workspace);
3452
+ const worktreePath = String(workspace?.worktreePath || '');
3453
+ const branchName = String(workspace?.branchName || '');
3454
+ if (!context || !branchName) return '';
3455
+ const requirement = 'Do not continue in the shared checkout, use a per-command workdir, prefix commands with cd, '
3456
+ + 'or ask the user to perform the transition. Synkro keeps substantive tools blocked until this exact session reports '
3457
+ + 'the task worktree and canonical branch.';
3458
+ if (harness === 'cc') {
3459
+ if (!worktreePath) return context + '\n[synkro:workspace-handoff] Task worktree creation failed closed; retry workspace preparation.';
3460
+ return context + '\n[synkro:workspace-handoff] Invoke Claude Code EnterWorktree now for the existing worktree '
3461
+ + JSON.stringify(worktreePath) + ' on branch ' + JSON.stringify(branchName) + '. ' + requirement;
3462
+ }
3463
+ if (harness === 'cursor') {
3464
+ if (!worktreePath) return context + '\n[synkro:workspace-handoff] Task worktree creation failed closed; retry workspace preparation.';
3465
+ return context + '\n[synkro:workspace-handoff] Resume this exact Cursor chat in the existing task worktree now: cursor-agent --resume '
3466
+ + shellTaskWorkspaceArg(sessionId) + ' --workspace ' + shellTaskWorkspaceArg(worktreePath)
3467
+ + '. Confirm the resumed workspace is on branch ' + JSON.stringify(branchName) + '. ' + requirement;
3468
+ }
3469
+ if (!worktreePath) {
3470
+ return context + '\n[synkro:workspace-handoff] Invoke Codex\'s native task/worktree Handoff now for this exact task. '
3471
+ + 'Check out branch ' + JSON.stringify(branchName) + ' in a new worktree and continue this same chat there. '
3472
+ + 'Do not switch the shared Local checkout or ask the user to switch branches. '
3473
+ + 'Synkro keeps substantive tools blocked until this exact session reports the new worktree path and canonical branch.';
3474
+ }
3475
+ return context + '\n[synkro:workspace-handoff] Invoke Codex\'s native task/worktree Handoff now for this exact task, '
3476
+ + 'using the existing worktree ' + JSON.stringify(worktreePath) + ' on branch ' + JSON.stringify(branchName) + '. '
3477
+ + 'The Codex Environment panel must show that worktree and branch before retrying the blocked action. ' + requirement;
3478
+ }
3479
+
3480
+ function withTaskScmContext(responseText: string, harness: string, context: string): string {
3481
+ if (!context) return responseText;
3482
+ try {
3483
+ const response = JSON.parse(responseText || '{}') as any;
3484
+ if (harness === 'cursor') {
3485
+ response.agent_message = [response.agent_message, context].filter(Boolean).join('\n');
3486
+ return JSON.stringify(response);
3487
+ }
3488
+ response.systemMessage = [response.systemMessage, context].filter(Boolean).join('\n');
3489
+ response.hookSpecificOutput = response.hookSpecificOutput || { hookEventName: 'PreToolUse' };
3490
+ response.hookSpecificOutput.additionalContext = [response.hookSpecificOutput.additionalContext, context]
3491
+ .filter(Boolean).join('\n');
3492
+ return JSON.stringify(response);
3493
+ } catch { return responseText; }
3494
+ }
3495
+
3496
+ async function reconcileTaskScm(
3497
+ root: string,
3498
+ sessionId: string,
3499
+ harness: string,
3500
+ payload?: any,
3501
+ ): Promise<TaskScmReconcileResult> {
3502
+ if (!root || !sessionId) return { reason: '', context: '' };
3503
+ try {
3504
+ let branch = '';
3505
+ let repoRoot = '';
3506
+ let isWorktree = false;
3507
+ try { branch = gitOutput(root, ['rev-parse', '--abbrev-ref', 'HEAD']); } catch {}
3508
+ try { repoRoot = sharedRepoRoot(root); } catch {}
3509
+ try { isWorktree = statSync(join(root, '.git')).isFile(); } catch {}
3510
+ const nativeCodexWorktree = harness === 'codex' && codexDesktopOrigin();
3511
+ const parentSessionId = codexForkedFromSession(payload, harness, sessionId);
3512
+ const claimWorkspace = (reportedBranch: string) => fetch(
3513
+ 'http://127.0.0.1:' + PORT + '/api/local/task-scm/claim', {
3514
+ method: 'POST',
3515
+ headers: { 'Content-Type': 'application/json', Authorization: 'Bearer ' + loadMcpJwt() },
3516
+ body: JSON.stringify({
3517
+ sessionId, cwd: root, repoRoot, branch: reportedBranch, harness,
3518
+ nativeCodexWorktree, isWorktree, parentSessionId,
3519
+ }),
3520
+ signal: AbortSignal.timeout(5000),
3521
+ },
3522
+ );
3523
+ const claim = await claimWorkspace(branch);
3524
+ if (!claim.ok) return { reason: '', context: '' };
3525
+ let result = await claim.json() as any;
3526
+ for (let attempt = 0; result?.retry === true && attempt < 50; attempt++) {
3527
+ await new Promise((resolveRetry) => setTimeout(resolveRetry, 100));
3528
+ try { branch = gitOutput(root, ['rev-parse', '--abbrev-ref', 'HEAD']); } catch {}
3529
+ const retried = await claimWorkspace(branch);
3530
+ if (!retried.ok) break;
3531
+ result = await retried.json() as any;
3532
+ }
3533
+ if (!result?.op) {
3534
+ const workspace = result?.workspace && typeof result.workspace === 'object'
3535
+ ? { ...result.workspace }
3536
+ : result?.workspace;
3537
+ return {
3538
+ reason: result?.waiting
3539
+ ? (taskScmWorkspaceInstruction(harness, sessionId, workspace)
3540
+ || String(result.reason || 'task source-control preparation is pending'))
3541
+ : '',
3542
+ context: taskScmWorkspaceContext(workspace),
3543
+ };
3544
+ }
3545
+ try {
3546
+ const worktreePath = executeTaskScm(root, result.op);
3547
+ await completeTaskScm(sessionId, result.op, true, '', worktreePath);
3548
+ if (result.op.kind === 'branch') {
3549
+ if (result.op.adoptExistingWorktree === true) {
3550
+ const rebound = await claimWorkspace(String(result.op.branchName || ''));
3551
+ if (rebound.ok) {
3552
+ const reboundResult = await rebound.json() as any;
3553
+ if (!reboundResult?.waiting && reboundResult?.workspace) {
3554
+ return { reason: '', context: taskScmWorkspaceContext(reboundResult.workspace) };
3555
+ }
3556
+ if (reboundResult?.waiting) {
3557
+ return {
3558
+ reason: String(reboundResult.reason || 'Codex native worktree binding is pending'),
3559
+ context: taskScmWorkspaceContext(reboundResult.workspace),
3560
+ };
3561
+ }
3562
+ }
3563
+ return { reason: 'Codex native worktree branch was created but task binding is still pending', context: '' };
3564
+ }
3565
+ const workspace = {
3566
+ taskId: result.op.taskId,
3567
+ linearRef: result.op.linearRef,
3568
+ branchName: result.op.branchName,
3569
+ worktreePath,
3570
+ bindingState: 'handoff_pending',
3571
+ };
3572
+ return {
3573
+ reason: taskScmWorkspaceInstruction(harness, sessionId, workspace),
3574
+ context: '',
3575
+ };
3576
+ }
3577
+ return { reason: '', context: '' };
3578
+ } catch (error) {
3579
+ const message = error instanceof Error ? error.message : String(error);
3580
+ await completeTaskScm(sessionId, result.op, false, message, root);
3581
+ return { reason: message, context: '' };
3582
+ }
3583
+ } catch { return { reason: '', context: '' }; }
3584
+ }
3585
+
3004
3586
  // Once-per-session onboarding hint for repos with no synkro.toml file.
3005
3587
  function noSynkroHint(sessionId: string): string | null {
3006
3588
  const dir = join(HOME, '.synkro', '.no-synkro-hint');
@@ -3022,13 +3604,11 @@ function filePathFromToolInput(ti: any): string {
3022
3604
  // The container reconstructs the proposed post-edit content by finding the edit's old_string
3023
3605
  // inside baseContent, then computes the changed line-range + per-requirement file snapshot off
3024
3606
  // it. The old flat 64KB HEAD silently truncated any edit past ~the first 1000 lines of a big
3025
- // file (e.g. an 890KB server file): old_string wasn't present, reconstruct produced garbage, and
3026
- // that requirement could never be verified it graded 'unknown' forever. Ship the WHOLE file
3027
- // (covers virtually all source, incl. large single-file servers) so reconstruct + region math
3028
- // are exact. Only a pathological >2MB file falls back to a line-aligned WINDOW around the edit
3029
- // (front-padded with newlines so ABSOLUTE line numbers which the container's region slicing
3030
- // keys off — are preserved) instead of the head. This also fixes CWE/CVE scans that previously
3031
- // never saw anything below 64KB in a large file.
3607
+ // file (e.g. an 890KB server file): old_string wasn't present, so reconstruction and the durable
3608
+ // edit fact were incomplete. Ship the WHOLE file (covers virtually all source, incl. large
3609
+ // single-file servers) so reconstruction, audit diffs, and policy/security scans are exact. Only
3610
+ // a pathological >2MB file falls back to a line-aligned WINDOW around the edit (front-padded with
3611
+ // newlines so ABSOLUTE line numbers are preserved) instead of the head.
3032
3612
  const BASE_CONTENT_WHOLE_CAP = 2000000;
3033
3613
  function editAnchorString(ti: any): string {
3034
3614
  if (!ti || typeof ti !== 'object') return '';
@@ -3436,11 +4016,19 @@ function readJsonlHeader(path: string): string {
3436
4016
  let fd = -1;
3437
4017
  try {
3438
4018
  fd = openSync(path, 'r');
3439
- const buf = Buffer.alloc(16384);
3440
- const count = readSync(fd, buf, 0, buf.length, 0);
3441
- const head = buf.subarray(0, count).toString('utf-8');
3442
- const newline = head.indexOf('\n');
3443
- return newline >= 0 ? head.slice(0, newline) : head;
4019
+ const chunks: Buffer[] = [];
4020
+ let position = 0;
4021
+ while (position < 2 * 1024 * 1024) {
4022
+ const buf = Buffer.alloc(Math.min(65536, 2 * 1024 * 1024 - position));
4023
+ const count = readSync(fd, buf, 0, buf.length, position);
4024
+ if (count <= 0) break;
4025
+ const chunk = buf.subarray(0, count);
4026
+ const newline = chunk.indexOf(10);
4027
+ chunks.push(newline >= 0 ? chunk.subarray(0, newline) : chunk);
4028
+ position += count;
4029
+ if (newline >= 0) break;
4030
+ }
4031
+ return Buffer.concat(chunks).toString('utf-8');
3444
4032
  } catch { return ''; }
3445
4033
  finally { if (fd >= 0) { try { closeSync(fd); } catch {} } }
3446
4034
  }
@@ -3518,6 +4106,37 @@ export function resolveHookTranscriptPath(payload: any, harness: string, session
3518
4106
  return found;
3519
4107
  }
3520
4108
 
4109
+ function codexForkedFromSession(payload: any, harness: string, sessionId: string): string {
4110
+ if (harness !== 'codex' || !safeCodexSessionId(sessionId)) return '';
4111
+ const cachePath = join(
4112
+ HOME,
4113
+ '.synkro',
4114
+ 'codex-parent-' + createHash('sha256').update(sessionId).digest('hex').slice(0, 16),
4115
+ );
4116
+ try {
4117
+ const cached = readFileSync(cachePath, 'utf-8').trim();
4118
+ if (cached === '-') return '';
4119
+ if (safeCodexSessionId(cached)) return cached;
4120
+ } catch {}
4121
+
4122
+ let parent = '';
4123
+ const transcriptPath = resolveHookTranscriptPath(payload, harness, sessionId);
4124
+ if (transcriptPath) {
4125
+ try {
4126
+ const first = JSON.parse(readJsonlHeader(transcriptPath));
4127
+ const candidate = String(first?.payload?.forked_from_id || first?.payload?.forkedFromId || '');
4128
+ if (first?.type === 'session_meta' && safeCodexSessionId(candidate) && candidate !== sessionId) {
4129
+ parent = candidate;
4130
+ }
4131
+ } catch {}
4132
+ }
4133
+ try {
4134
+ mkdirSync(join(HOME, '.synkro'), { recursive: true });
4135
+ writeFileSync(cachePath, parent || '-', { encoding: 'utf-8', mode: 0o600 });
4136
+ } catch {}
4137
+ return parent;
4138
+ }
4139
+
3521
4140
  export async function runStub(surface: string, opts: StubOpts = {}): Promise<void> {
3522
4141
  const harness = isCursor(opts.harness) ? 'cursor' : isCodex(opts.harness) ? 'codex' : 'cc';
3523
4142
  const startedAt = Date.now();
@@ -3564,6 +4183,7 @@ export async function runStub(surface: string, opts: StubOpts = {}): Promise<voi
3564
4183
  const cwd = (typeof payload.cwd === 'string' && payload.cwd) || workspaceRoots[0] || '';
3565
4184
  const sessionId = String(payload.session_id || payload.conversation_id || '');
3566
4185
  const root = gitRoot(cwd);
4186
+ telemCwd = root || cwd;
3567
4187
 
3568
4188
  // Dormancy: a repo is onboarded only if it has a synkro.toml FILE at its git
3569
4189
  // root. Guard root !== HOME — ~/.synkro is the config DIRECTORY, so without
@@ -3598,6 +4218,13 @@ export async function runStub(surface: string, opts: StubOpts = {}): Promise<voi
3598
4218
  return;
3599
4219
  }
3600
4220
 
4221
+ const scm = await reconcileTaskScm(root || cwd, sessionId, harness, payload);
4222
+ const substantiveTool = /^(?:Bash|Edit|Write|MultiEdit|NotebookEdit|apply_patch|file_change)$/i.test(String(payload.tool_name || ''));
4223
+ if (scm.reason && substantiveTool) {
4224
+ out(taskScmBlockResponse(harness, scm.reason));
4225
+ return;
4226
+ }
4227
+
3601
4228
  if (harness === 'codex' && opts.needsFile && payload.__synkro_codex_patch_error) {
3602
4229
  const reason = String(payload.__synkro_codex_patch_error);
3603
4230
  out(JSON.stringify({
@@ -3613,7 +4240,17 @@ export async function runStub(surface: string, opts: StubOpts = {}): Promise<voi
3613
4240
  }
3614
4241
 
3615
4242
  // Gather host-only inputs the container can't read.
3616
- const envelope: any = { payload, harness, cwd: root || cwd, sessionId, synkroFileText };
4243
+ const envelope: any = {
4244
+ payload,
4245
+ harness,
4246
+ cwd: root || cwd,
4247
+ sessionId,
4248
+ synkroFileText,
4249
+ taskWorkspaceContext: scm.context || undefined,
4250
+ };
4251
+ // Task creation is the one moment the container needs a stable Git snapshot.
4252
+ // Capture it on the MCP gate only; ordinary tool hooks avoid extra Git calls.
4253
+ if (surface === 'mcp-gate') envelope.repoContext = taskRepoContext(root || cwd);
3617
4254
 
3618
4255
  if (opts.needsFile) {
3619
4256
  const normalizedEdits = harness === 'codex' && Array.isArray(payload.__synkro_codex_edits)
@@ -3694,15 +4331,10 @@ export async function runStub(surface: string, opts: StubOpts = {}): Promise<voi
3694
4331
  envelope.planText = String(ti.plan || ti.content || payload.plan || '');
3695
4332
  }
3696
4333
 
3697
- // bash-followup is telemetry, but it can carry the INLINE "task complete ✓ N/N"
3698
- // result (the server waits up to 30s for the completion grade), so it needs a
3699
- // wait above that budget — not the 6s telemetry default — or the stub abandons
3700
- // the request before the result lands and the completion shows up a hook late.
3701
- // prompt-submit is the opposite: it's INTERACTIVE (blocks the user's prompt from
4334
+ // prompt-submit is INTERACTIVE (blocks the user's prompt from
3702
4335
  // being sent) under a short 5s hook timeout, so it gets ONE short attempt (below)
3703
4336
  // and a tight per-attempt budget that fits inside 5s — never the 6s×3 telemetry path.
3704
4337
  const timeoutMs = surface === 'cwe-precheck' ? 48000
3705
- : surface === 'bash-followup' ? 32000
3706
4338
  : surface === 'prompt-submit' ? 3500
3707
4339
  : (opts.telemetry ? 6000 : 28000);
3708
4340
  // Cloud has no local container to reach. Post the SAME envelope to the org's grader
@@ -3727,8 +4359,8 @@ export async function runStub(surface: string, opts: StubOpts = {}): Promise<voi
3727
4359
  // latency: retry a few times with backoff. Right after a server restart the
3728
4360
  // event loop can be briefly busy / not-yet-listening and a single POST silently
3729
4361
  // drops the event (this was the followup flakiness). Double-delivery is safe here
3730
- // the server side is idempotent-ish (completion in-flight guard, dup evidence
3731
- // is harmless). Blocking hooks make ONE attempt: their response IS the verdict
4362
+ // because the server deduplicates durable hook events and native evidence.
4363
+ // Blocking hooks make ONE attempt: their response IS the verdict
3732
4364
  // and a retry would double-grade. Normal case: first attempt wins, no delay.
3733
4365
  // prompt-submit is EXCLUDED from the retry: it runs on the interactive prompt
3734
4366
  // path under a 5s hook timeout, and 3×6s+backoff (19.5s) blew that budget and
@@ -3744,9 +4376,13 @@ export async function runStub(surface: string, opts: StubOpts = {}): Promise<voi
3744
4376
  if (i < attempts - 1) await new Promise((r) => setTimeout(r, 500 * (i + 1)));
3745
4377
  }
3746
4378
  const rawResponseText = text || failOpen(harness);
3747
- const responseText = opts.stopContract && harness === 'codex'
4379
+ // A completion-report sync can arm the push after the pre-scan reconciliation ran.
4380
+ // Reconcile once more so the same Stop/PostToolUse hook can push immediately.
4381
+ await reconcileTaskScm(root || cwd, sessionId, harness, payload);
4382
+ const contractResponseText = opts.stopContract && harness === 'codex'
3748
4383
  ? codexStopResponse(rawResponseText)
3749
4384
  : rawResponseText;
4385
+ const responseText = withTaskScmContext(contractResponseText, harness, scm.context);
3750
4386
  out(responseText);
3751
4387
  emitStubTelemetry(surface, harness, telemPayload, responseText, Date.now() - startedAt, telemCwd, telemSessionId);
3752
4388
  } catch (err) {
@@ -4129,7 +4765,7 @@ function createCallbackServer() {
4129
4765
  "Access-Control-Allow-Headers": "Content-Type",
4130
4766
  "Vary": "Origin"
4131
4767
  };
4132
- return new Promise((resolve6, reject) => {
4768
+ return new Promise((resolve7, reject) => {
4133
4769
  const server = createServer((req, res) => {
4134
4770
  if (req.method === "OPTIONS") {
4135
4771
  const origin = req.headers.origin;
@@ -4218,7 +4854,7 @@ function createCallbackServer() {
4218
4854
  res.end(JSON.stringify({ ok: true }));
4219
4855
  setTimeout(() => {
4220
4856
  server.close();
4221
- resolve6(authData);
4857
+ resolve7(authData);
4222
4858
  }, 200);
4223
4859
  });
4224
4860
  req.on("error", (e) => {
@@ -4549,7 +5185,7 @@ function detectSubdirRepos() {
4549
5185
  }
4550
5186
  }
4551
5187
  function ask(rl, question) {
4552
- return new Promise((resolve6) => rl.question(question, resolve6));
5188
+ return new Promise((resolve7) => rl.question(question, resolve7));
4553
5189
  }
4554
5190
  async function linkRepo(repo, linkedNames) {
4555
5191
  try {
@@ -4788,7 +5424,7 @@ async function runClaudeDesktopTap(opts = {}) {
4788
5424
  writeFileSync12(join13(sessionDir, "mcp_patch.py"), MCP_PATCH_PY, "utf-8");
4789
5425
  const runnerPath = join13(sessionDir, "run.sh");
4790
5426
  writeFileSync12(runnerPath, buildRunner(sessionDir), { mode: 493 });
4791
- await new Promise((resolve6) => {
5427
+ await new Promise((resolve7) => {
4792
5428
  const child = spawn3("bash", [runnerPath], {
4793
5429
  stdio: "inherit",
4794
5430
  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" : "" }
@@ -4804,7 +5440,7 @@ async function runClaudeDesktopTap(opts = {}) {
4804
5440
  child.on("exit", () => {
4805
5441
  process.off("SIGINT", forward);
4806
5442
  process.off("SIGTERM", forward);
4807
- resolve6();
5443
+ resolve7();
4808
5444
  });
4809
5445
  });
4810
5446
  }
@@ -6610,10 +7246,8 @@ async function dockerInstall(opts = {}) {
6610
7246
  // Pass through the batch-size lever if the operator set it. Defaults
6611
7247
  // inside the container to 5; clamped to [1, 20] by synkro-server.ts.
6612
7248
  ...process.env.SYNKRO_MAX_BATCH_SIZE ? ["-e", `SYNKRO_MAX_BATCH_SIZE=${process.env.SYNKRO_MAX_BATCH_SIZE}`] : [],
6613
- // Full verifier prompt/response tracing is explicit opt-in because it contains source code.
6614
- ...process.env.SYNKRO_VERIFY_TRACE === "1" ? ["-e", "SYNKRO_VERIFY_TRACE=1"] : [],
6615
7249
  // Explicit model overrides are preserved across local install/update for
6616
- // every provider and isolated lane. The server reports the resolved values
7250
+ // every provider and the isolated route lane. The server reports the resolved values
6617
7251
  // in /healthz so operators can audit exactly what is spending tokens.
6618
7252
  ...[
6619
7253
  "SYNKRO_CLAUDE_MODEL",
@@ -6879,7 +7513,7 @@ var init_dockerInstall = __esm({
6879
7513
  HOST_PGLITE_PORT = parseInt(process.env.SYNKRO_HOST_PGLITE_PORT || "15433", 10);
6880
7514
  CONTAINER_NAME = resolveContainerName();
6881
7515
  defaultImageVersion = () => {
6882
- if (true) return "1.7.95";
7516
+ if (true) return "1.8.0";
6883
7517
  try {
6884
7518
  const pkg = JSON.parse(readFileSync17(new URL("../../package.json", import.meta.url), "utf8"));
6885
7519
  if (pkg.version) return pkg.version;
@@ -6911,7 +7545,7 @@ function captureClaudeSetupToken() {
6911
7545
  const bin = "script";
6912
7546
  const args2 = isMac ? ["-q", tmpFile, "claude", "setup-token"] : ["-qec", "claude setup-token", tmpFile];
6913
7547
  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.';
6914
- return new Promise((resolve6, reject) => {
7548
+ return new Promise((resolve7, reject) => {
6915
7549
  const proc = nodeSpawn(bin, args2, {
6916
7550
  stdio: "inherit",
6917
7551
  env: { ...process.env, FORCE_COLOR: "3", COLORTERM: "truecolor", TERM: "xterm-256color" }
@@ -6978,7 +7612,7 @@ function captureClaudeSetupToken() {
6978
7612
  reject(new Error(`Captured no setup token from claude setup-token output. ${reason}`));
6979
7613
  return;
6980
7614
  }
6981
- resolve6(token);
7615
+ resolve7(token);
6982
7616
  });
6983
7617
  });
6984
7618
  }
@@ -7004,13 +7638,13 @@ function findCodexBinary() {
7004
7638
  function runCodexLogin(codexBin, codexHome) {
7005
7639
  mkdirSync13(codexHome, { recursive: true, mode: 448 });
7006
7640
  writeFileSync15(join17(codexHome, "config.toml"), 'cli_auth_credentials_store = "file"\n', { mode: 384 });
7007
- return new Promise((resolve6, reject) => {
7641
+ return new Promise((resolve7, reject) => {
7008
7642
  const proc = nodeSpawn2(codexBin, ["login"], {
7009
7643
  stdio: "inherit",
7010
7644
  env: { ...process.env, CODEX_HOME: codexHome }
7011
7645
  });
7012
7646
  proc.on("error", (err) => reject(new Error(`failed to spawn codex login: ${err.message}`)));
7013
- proc.on("close", (code) => code === 0 ? resolve6() : reject(new Error(`codex login exited with code ${code}`)));
7647
+ proc.on("close", (code) => code === 0 ? resolve7() : reject(new Error(`codex login exited with code ${code}`)));
7014
7648
  });
7015
7649
  }
7016
7650
  async function setupCodexCloud(gatewayUrl, bearerToken, onStatus) {
@@ -7712,20 +8346,20 @@ async function promptAgentSelection(detected) {
7712
8346
  detected.forEach((a, i) => console.log(` ${i + 1}. ${a.name}`));
7713
8347
  console.log(` ${detected.length + 1}. Both / all (default)`);
7714
8348
  const rl = createInterface2({ input: process.stdin, output: process.stdout });
7715
- const ask3 = () => new Promise((resolve6) => {
8349
+ const ask3 = () => new Promise((resolve7) => {
7716
8350
  rl.question(`Pick [1-${detected.length + 1}] (default: all): `, (answer) => {
7717
8351
  const t = answer.trim().toLowerCase();
7718
8352
  if (t === "" || t === String(detected.length + 1) || t === "both" || t === "all") {
7719
8353
  rl.close();
7720
- return resolve6(detected);
8354
+ return resolve7(detected);
7721
8355
  }
7722
8356
  const n = parseInt(t, 10);
7723
8357
  if (Number.isInteger(n) && n >= 1 && n <= detected.length) {
7724
8358
  rl.close();
7725
- return resolve6([detected[n - 1]]);
8359
+ return resolve7([detected[n - 1]]);
7726
8360
  }
7727
8361
  console.log("Invalid choice. Try again.");
7728
- resolve6(ask3());
8362
+ resolve7(ask3());
7729
8363
  });
7730
8364
  });
7731
8365
  return ask3();
@@ -7748,12 +8382,12 @@ async function promptCursorApiKey(opts) {
7748
8382
  return;
7749
8383
  }
7750
8384
  const rl = createInterface2({ input: process.stdin, output: process.stdout });
7751
- const key = await new Promise((resolve6) => {
8385
+ const key = await new Promise((resolve7) => {
7752
8386
  rl.question(
7753
8387
  "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): ",
7754
8388
  (answer) => {
7755
8389
  rl.close();
7756
- resolve6(answer.trim());
8390
+ resolve7(answer.trim());
7757
8391
  }
7758
8392
  );
7759
8393
  });
@@ -7768,7 +8402,7 @@ async function promptDeployLocation(current = "local") {
7768
8402
  if (!process.stdin.isTTY) return current;
7769
8403
  const other = current === "cloud" ? "local" : "cloud";
7770
8404
  const rl = createInterface2({ input: process.stdin, output: process.stdout });
7771
- return new Promise((resolve6) => {
8405
+ return new Promise((resolve7) => {
7772
8406
  rl.question(
7773
8407
  `Where should Synkro run?
7774
8408
  local \u2014 a grading container on this machine (Docker)
@@ -7777,7 +8411,7 @@ Each worker uses the account credentials you authorize. Choose [${current}] / ${
7777
8411
  (answer) => {
7778
8412
  rl.close();
7779
8413
  const a = answer.trim().toLowerCase();
7780
- resolve6(a === "cloud" ? "cloud" : a === "local" ? "local" : current);
8414
+ resolve7(a === "cloud" ? "cloud" : a === "local" ? "local" : current);
7781
8415
  }
7782
8416
  );
7783
8417
  });
@@ -7936,7 +8570,7 @@ function writeConfigEnv(opts) {
7936
8570
  `SYNKRO_CREDENTIALS_PATH=${shellQuoteSingle2(credsPath)}`,
7937
8571
  `SYNKRO_TIER=${shellQuoteSingle2(safeTier)}`,
7938
8572
  `SYNKRO_INFERENCE=${shellQuoteSingle2(safeInference)}`,
7939
- `SYNKRO_VERSION=${shellQuoteSingle2("1.7.95")}`
8573
+ `SYNKRO_VERSION=${shellQuoteSingle2("1.8.0")}`
7940
8574
  ];
7941
8575
  if (safeSynkroBin) lines.push(`SYNKRO_CLI_BIN=${shellQuoteSingle2(safeSynkroBin)}`);
7942
8576
  if (safeUserId) lines.push(`SYNKRO_USER_ID=${shellQuoteSingle2(safeUserId)}`);
@@ -8683,7 +9317,7 @@ async function installCommand(opts = {}) {
8683
9317
  await setTelemetryState({ enabled: true, remoteFlushEnabled: telemetryConsent });
8684
9318
  emit("install", {
8685
9319
  phase: "started",
8686
- cli_version_to: "1.7.95",
9320
+ cli_version_to: "1.8.0",
8687
9321
  agents_detected: agents.map((a) => a.kind),
8688
9322
  with_github: false,
8689
9323
  with_local_cc: false,
@@ -9890,13 +10524,13 @@ function extractTextContent(content) {
9890
10524
  function getCodexTranscriptFiles(repo) {
9891
10525
  const sessionsDir = join19(process.env.CODEX_HOME || join19(homedir21(), ".codex"), "sessions");
9892
10526
  if (!existsSync22(sessionsDir)) return [];
9893
- let relative = [];
10527
+ let relative2 = [];
9894
10528
  try {
9895
- relative = readdirSync4(sessionsDir, { recursive: true, encoding: "utf-8" });
10529
+ relative2 = readdirSync4(sessionsDir, { recursive: true, encoding: "utf-8" });
9896
10530
  } catch {
9897
10531
  return [];
9898
10532
  }
9899
- return relative.filter((p) => p.endsWith(".jsonl")).map((p) => join19(sessionsDir, p)).filter((filePath) => {
10533
+ return relative2.filter((p) => p.endsWith(".jsonl")).map((p) => join19(sessionsDir, p)).filter((filePath) => {
9900
10534
  try {
9901
10535
  const first = readFileSync21(filePath, "utf-8").split("\n", 1)[0];
9902
10536
  const meta = JSON.parse(first);
@@ -10971,10 +11605,10 @@ function confirmPurge() {
10971
11605
  return Promise.resolve(false);
10972
11606
  }
10973
11607
  const rl = createInterface3({ input: process.stdin, output: process.stdout });
10974
- return new Promise((resolve6) => {
11608
+ return new Promise((resolve7) => {
10975
11609
  rl.question(" Type 'yes' to wipe everything (anything else cancels): ", (answer) => {
10976
11610
  rl.close();
10977
- resolve6(answer.trim().toLowerCase() === "yes");
11611
+ resolve7(answer.trim().toLowerCase() === "yes");
10978
11612
  });
10979
11613
  });
10980
11614
  }
@@ -11227,7 +11861,7 @@ async function submitToChannel(role, payload, opts = {}) {
11227
11861
  const port = opts.port ?? CHANNEL_PORT;
11228
11862
  const startedAt = Date.now();
11229
11863
  try {
11230
- const result = await new Promise((resolve6, reject) => {
11864
+ const result = await new Promise((resolve7, reject) => {
11231
11865
  const req = httpRequest({
11232
11866
  host: CHANNEL_HOST,
11233
11867
  port,
@@ -11253,7 +11887,7 @@ async function submitToChannel(role, payload, opts = {}) {
11253
11887
  reject(new LocalCCError(parsed.error));
11254
11888
  return;
11255
11889
  }
11256
- resolve6(String(parsed.result ?? ""));
11890
+ resolve7(String(parsed.result ?? ""));
11257
11891
  } catch (err) {
11258
11892
  reject(new LocalCCError(`malformed channel response: ${text.slice(0, 200)}`, err));
11259
11893
  }
@@ -11279,14 +11913,14 @@ async function submitToChannel(role, payload, opts = {}) {
11279
11913
  }
11280
11914
  }
11281
11915
  function isChannelAvailable(port = CHANNEL_PORT, timeoutMs = 500) {
11282
- return new Promise((resolve6) => {
11916
+ return new Promise((resolve7) => {
11283
11917
  const sock = connect(port, CHANNEL_HOST);
11284
11918
  const done = (ok) => {
11285
11919
  try {
11286
11920
  sock.destroy();
11287
11921
  } catch {
11288
11922
  }
11289
- resolve6(ok);
11923
+ resolve7(ok);
11290
11924
  };
11291
11925
  sock.once("connect", () => done(true));
11292
11926
  sock.once("error", () => done(false));
@@ -11318,10 +11952,10 @@ __export(grade_exports, {
11318
11952
  gradeCommand: () => gradeCommand
11319
11953
  });
11320
11954
  async function readStdin() {
11321
- return new Promise((resolve6, reject) => {
11955
+ return new Promise((resolve7, reject) => {
11322
11956
  const chunks = [];
11323
11957
  process.stdin.on("data", (c) => chunks.push(c));
11324
- process.stdin.on("end", () => resolve6(Buffer.concat(chunks).toString("utf-8")));
11958
+ process.stdin.on("end", () => resolve7(Buffer.concat(chunks).toString("utf-8")));
11325
11959
  process.stdin.on("error", reject);
11326
11960
  });
11327
11961
  }
@@ -11575,7 +12209,7 @@ function spawnClaudeJudge(file, claudeToken, promptHeader) {
11575
12209
  Diff:
11576
12210
  ${hunks}`;
11577
12211
  const fullPrompt = promptHeader + userMessage;
11578
- return new Promise((resolve6) => {
12212
+ return new Promise((resolve7) => {
11579
12213
  const t0 = Date.now();
11580
12214
  const proc = spawn6(
11581
12215
  "claude",
@@ -11603,7 +12237,7 @@ ${hunks}`;
11603
12237
  const latencyMs = Date.now() - t0;
11604
12238
  if (code !== 0) {
11605
12239
  console.warn(` claude exited ${code}: ${(stderr || stdout).slice(0, 500)}`);
11606
- resolve6({ findings: [], latencyMs });
12240
+ resolve7({ findings: [], latencyMs });
11607
12241
  return;
11608
12242
  }
11609
12243
  try {
@@ -11622,10 +12256,10 @@ ${hunks}`;
11622
12256
  description: f.description,
11623
12257
  fix: f.fix
11624
12258
  }));
11625
- resolve6({ findings, latencyMs });
12259
+ resolve7({ findings, latencyMs });
11626
12260
  } catch (parseErr) {
11627
12261
  console.warn(` failed to parse claude response: ${stdout.slice(0, 300)}`);
11628
- resolve6({ findings: [], latencyMs });
12262
+ resolve7({ findings: [], latencyMs });
11629
12263
  }
11630
12264
  });
11631
12265
  });
@@ -11674,7 +12308,7 @@ ${JSON.stringify(findings, null, 2)}
11674
12308
  `;
11675
12309
  }
11676
12310
  function spawnOpusConsolidator(findings, claudeToken) {
11677
- return new Promise((resolve6) => {
12311
+ return new Promise((resolve7) => {
11678
12312
  const prompt = buildConsolidationPrompt(findings);
11679
12313
  const proc = spawn6(
11680
12314
  "claude",
@@ -11701,7 +12335,7 @@ function spawnOpusConsolidator(findings, claudeToken) {
11701
12335
  proc.on("close", (code) => {
11702
12336
  if (code !== 0) {
11703
12337
  console.warn(` opus consolidation exited ${code}: ${(stderr || stdout).slice(0, 300)}`);
11704
- resolve6(fallbackReview(findings));
12338
+ resolve7(fallbackReview(findings));
11705
12339
  return;
11706
12340
  }
11707
12341
  try {
@@ -11722,10 +12356,10 @@ function spawnOpusConsolidator(findings, claudeToken) {
11722
12356
  const order = ["low", "medium", "high", "critical"];
11723
12357
  return order.indexOf(f.severity) > order.indexOf(max) ? f.severity : max;
11724
12358
  }, "low");
11725
- resolve6({ summary: review.summary || "", comments, severity: maxSeverity });
12359
+ resolve7({ summary: review.summary || "", comments, severity: maxSeverity });
11726
12360
  } catch {
11727
12361
  console.warn(` failed to parse opus response, using fallback`);
11728
- resolve6(fallbackReview(findings));
12362
+ resolve7(fallbackReview(findings));
11729
12363
  }
11730
12364
  });
11731
12365
  });
@@ -12208,14 +12842,14 @@ function ensureRunning(opts = {}) {
12208
12842
  return startTask(opts);
12209
12843
  }
12210
12844
  function probePort(host, port, timeoutMs = 500) {
12211
- return new Promise((resolve6) => {
12845
+ return new Promise((resolve7) => {
12212
12846
  const sock = connect2(port, host);
12213
12847
  const done = (ok) => {
12214
12848
  try {
12215
12849
  sock.destroy();
12216
12850
  } catch {
12217
12851
  }
12218
- resolve6(ok);
12852
+ resolve7(ok);
12219
12853
  };
12220
12854
  sock.once("connect", () => done(true));
12221
12855
  sock.once("error", () => done(false));
@@ -12765,7 +13399,7 @@ function cmdLogs(rest) {
12765
13399
  if (!raw) console.log(" " + colorize("(use --raw / -r to see full payloads, --live / -f to follow)", 90));
12766
13400
  return;
12767
13401
  }
12768
- return new Promise((resolve6) => {
13402
+ return new Promise((resolve7) => {
12769
13403
  console.log(" " + colorize("\u2014 following new turns (Ctrl-C to exit) \u2014", 90));
12770
13404
  const stop = followTurns((t) => {
12771
13405
  console.log(" " + formatTurn(t, raw));
@@ -12773,7 +13407,7 @@ function cmdLogs(rest) {
12773
13407
  const onSigint = () => {
12774
13408
  stop();
12775
13409
  process.removeListener("SIGINT", onSigint);
12776
- resolve6();
13410
+ resolve7();
12777
13411
  };
12778
13412
  process.on("SIGINT", onSigint);
12779
13413
  });
@@ -13062,9 +13696,9 @@ function parseSession(file, seenStableIds) {
13062
13696
  }
13063
13697
  function ask2(q) {
13064
13698
  const rl = createInterface4({ input: process.stdin, output: process.stdout });
13065
- return new Promise((resolve6) => rl.question(q, (a) => {
13699
+ return new Promise((resolve7) => rl.question(q, (a) => {
13066
13700
  rl.close();
13067
- resolve6(/^y(es)?$/i.test(a.trim()));
13701
+ resolve7(/^y(es)?$/i.test(a.trim()));
13068
13702
  }));
13069
13703
  }
13070
13704
  async function importCommand() {
@@ -13197,8 +13831,8 @@ function canonicalize(pack) {
13197
13831
  docs: pack.docs ?? []
13198
13832
  });
13199
13833
  }
13200
- function computeDigest(canonical) {
13201
- return "sha256:" + crypto.createHash("sha256").update(canonical, "utf8").digest("hex");
13834
+ function computeDigest(canonical2) {
13835
+ return "sha256:" + crypto.createHash("sha256").update(canonical2, "utf8").digest("hex");
13202
13836
  }
13203
13837
  function verifySignature(digest, signatureB64, publicKeyPem) {
13204
13838
  try {
@@ -14427,12 +15061,12 @@ async function runExport(args2) {
14427
15061
  }
14428
15062
  function confirmYesNo(question) {
14429
15063
  if (!process.stdin.isTTY) return Promise.resolve(false);
14430
- return new Promise((resolve6) => {
15064
+ return new Promise((resolve7) => {
14431
15065
  const rl = createInterface5({ input: process.stdin, output: process.stdout });
14432
15066
  rl.question(`${question} (y/N): `, (answer) => {
14433
15067
  rl.close();
14434
15068
  const t = answer.trim().toLowerCase();
14435
- resolve6(t === "y" || t === "yes");
15069
+ resolve7(t === "y" || t === "yes");
14436
15070
  });
14437
15071
  });
14438
15072
  }
@@ -14525,15 +15159,824 @@ Usage:
14525
15159
  }
14526
15160
  });
14527
15161
 
15162
+ // cli/inventory/identity.ts
15163
+ import { randomUUID as randomUUID5 } from "crypto";
15164
+ import { existsSync as existsSync36, mkdirSync as mkdirSync19, readFileSync as readFileSync35, renameSync as renameSync9, writeFileSync as writeFileSync24 } from "fs";
15165
+ import { homedir as homedir35 } from "os";
15166
+ import { dirname as dirname9, join as join35 } from "path";
15167
+ function operationalIdentityPath() {
15168
+ return process.env.SYNKRO_OPERATIONAL_IDENTITY_PATH || join35(homedir35(), ".synkro", "installation.json");
15169
+ }
15170
+ function validIdentity(value) {
15171
+ if (!value || typeof value !== "object") return false;
15172
+ const row = value;
15173
+ return typeof row.installation_id === "string" && UUID_RE.test(row.installation_id) && typeof row.created_at === "string" && Number.isFinite(Date.parse(row.created_at));
15174
+ }
15175
+ function writeIdentity(path, identity) {
15176
+ mkdirSync19(dirname9(path), { recursive: true, mode: 448 });
15177
+ const temp = `${path}.${process.pid}.${randomUUID5()}.tmp`;
15178
+ writeFileSync24(temp, JSON.stringify(identity, null, 2) + "\n", { encoding: "utf8", mode: 384 });
15179
+ renameSync9(temp, path);
15180
+ }
15181
+ function getOperationalInstallationIdentity(path = operationalIdentityPath()) {
15182
+ const prior = cached4.get(path);
15183
+ if (prior) return prior;
15184
+ if (existsSync36(path)) {
15185
+ try {
15186
+ const parsed = JSON.parse(readFileSync35(path, "utf8"));
15187
+ if (validIdentity(parsed)) {
15188
+ cached4.set(path, parsed);
15189
+ return parsed;
15190
+ }
15191
+ } catch {
15192
+ }
15193
+ }
15194
+ const identity = { installation_id: randomUUID5(), created_at: (/* @__PURE__ */ new Date()).toISOString() };
15195
+ writeIdentity(path, identity);
15196
+ cached4.set(path, identity);
15197
+ return identity;
15198
+ }
15199
+ var UUID_RE, cached4;
15200
+ var init_identity2 = __esm({
15201
+ "cli/inventory/identity.ts"() {
15202
+ "use strict";
15203
+ 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;
15204
+ cached4 = /* @__PURE__ */ new Map();
15205
+ }
15206
+ });
15207
+
15208
+ // cli/inventory/collector.ts
15209
+ import { createHash as createHash5 } from "crypto";
15210
+ import {
15211
+ existsSync as existsSync37,
15212
+ readFileSync as readFileSync36,
15213
+ readdirSync as readdirSync9,
15214
+ statSync as statSync5
15215
+ } from "fs";
15216
+ import { arch, homedir as homedir36, hostname as hostname2, platform as platform5, release } from "os";
15217
+ import { basename as basename3, join as join36, relative, resolve as resolve5 } from "path";
15218
+ import { fileURLToPath } from "url";
15219
+ function sha256(value) {
15220
+ return createHash5("sha256").update(value).digest("hex");
15221
+ }
15222
+ function pseudonymousHostnameHash(installationId, host) {
15223
+ return sha256(`${installationId}:${host}`).slice(0, 32);
15224
+ }
15225
+ function cliVersion() {
15226
+ try {
15227
+ return "1.8.0";
15228
+ } catch {
15229
+ return "0.0.0";
15230
+ }
15231
+ }
15232
+ function readJson(path) {
15233
+ try {
15234
+ if (!existsSync37(path)) return null;
15235
+ const parsed = JSON.parse(readFileSync36(path, "utf8"));
15236
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
15237
+ } catch {
15238
+ return null;
15239
+ }
15240
+ }
15241
+ function readText(path) {
15242
+ try {
15243
+ if (!existsSync37(path)) return "";
15244
+ return readFileSync36(path, "utf8");
15245
+ } catch {
15246
+ return "";
15247
+ }
15248
+ }
15249
+ function urlOrigin(raw) {
15250
+ if (typeof raw !== "string") return void 0;
15251
+ try {
15252
+ const parsed = new URL(raw);
15253
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return void 0;
15254
+ return parsed.origin;
15255
+ } catch {
15256
+ return void 0;
15257
+ }
15258
+ }
15259
+ function canonical(raw) {
15260
+ return raw.trim().toLowerCase().replace(/[\s.]+/g, "-").replace(/[^a-z0-9_:@/+-]/g, "") || "unknown";
15261
+ }
15262
+ function safePackageName(command, args2) {
15263
+ if (typeof command !== "string" || !command.trim()) return void 0;
15264
+ const runner = basename3(command.trim()).replace(/\.exe$/i, "");
15265
+ if (Array.isArray(args2) && ["npx", "bunx", "uvx"].includes(runner)) {
15266
+ const pkg = args2.find((arg) => typeof arg === "string" && !arg.startsWith("-"));
15267
+ if (typeof pkg === "string") {
15268
+ const candidate = pkg.trim();
15269
+ if (/^(?:@[a-z0-9_.-]+\/)?[a-z0-9_.-]+(?:@[a-z0-9_.+~-]+)?$/i.test(candidate)) {
15270
+ return candidate;
15271
+ }
15272
+ return basename3(candidate);
15273
+ }
15274
+ }
15275
+ return runner;
15276
+ }
15277
+ function toolNames(entry) {
15278
+ const raw = entry.enabledTools ?? entry.enabled_tools ?? entry.allowedTools ?? entry.allowed_tools ?? entry.tools;
15279
+ if (!Array.isArray(raw)) return [];
15280
+ return [...new Set(raw.map((tool) => typeof tool === "string" ? tool : tool?.name).filter((tool) => typeof tool === "string" && !!tool.trim()))];
15281
+ }
15282
+ function mcpArtifactsFromJson(harness, config, configScope = "user") {
15283
+ if (!config || !config.mcpServers || typeof config.mcpServers !== "object") return [];
15284
+ const artifacts = [];
15285
+ for (const [name, raw] of Object.entries(config.mcpServers)) {
15286
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) continue;
15287
+ const entry = raw;
15288
+ const transport = typeof entry.type === "string" ? entry.type : entry.command ? "stdio" : entry.url ? "streamable-http" : "unknown";
15289
+ const tools = toolNames(entry);
15290
+ const scopeId = canonical(configScope);
15291
+ const id = `${canonical(name)}@${scopeId}`;
15292
+ const safeShape = {
15293
+ name,
15294
+ transport,
15295
+ origin: urlOrigin(entry.url),
15296
+ package_name: safePackageName(entry.command, entry.args),
15297
+ enabled: entry.enabled !== false && entry.disabled !== true,
15298
+ tools,
15299
+ scope: configScope
15300
+ };
15301
+ artifacts.push({
15302
+ harness,
15303
+ type: "mcp_server",
15304
+ canonical_id: id,
15305
+ display_name: String(name),
15306
+ enabled: safeShape.enabled,
15307
+ config_scope: configScope,
15308
+ transport: String(transport),
15309
+ endpoint_origin: safeShape.origin,
15310
+ package_name: safeShape.package_name,
15311
+ config_hash: sha256(JSON.stringify(safeShape)),
15312
+ metadata: tools.length ? { tool_names: tools } : void 0
15313
+ });
15314
+ for (const tool of tools) {
15315
+ artifacts.push({
15316
+ harness,
15317
+ type: "mcp_tool",
15318
+ canonical_id: `${id}:${canonical(tool)}`,
15319
+ display_name: tool,
15320
+ enabled: safeShape.enabled,
15321
+ config_scope: configScope,
15322
+ metadata: { source: String(name) }
15323
+ });
15324
+ }
15325
+ }
15326
+ return artifacts;
15327
+ }
15328
+ function claudeDesktopConfigCandidates(home, targetPlatform) {
15329
+ if (targetPlatform === "darwin") {
15330
+ return [join36(home, "Library", "Application Support", "Claude", "claude_desktop_config.json")];
15331
+ }
15332
+ if (targetPlatform === "linux") {
15333
+ return [
15334
+ join36(home, ".config", "Claude", "claude_desktop_config.json"),
15335
+ join36(home, ".config", "claude", "claude_desktop_config.json")
15336
+ ];
15337
+ }
15338
+ if (targetPlatform === "win32" && process.env.APPDATA) {
15339
+ return [join36(process.env.APPDATA, "Claude", "claude_desktop_config.json")];
15340
+ }
15341
+ return [];
15342
+ }
15343
+ function claudeManagedMcpConfigCandidates(targetPlatform) {
15344
+ if (targetPlatform === "darwin") return ["/Library/Application Support/ClaudeCode/managed-mcp.json"];
15345
+ if (targetPlatform === "linux") return ["/etc/claude-code/managed-mcp.json"];
15346
+ if (targetPlatform === "win32" && process.env.ProgramFiles) {
15347
+ return [join36(process.env.ProgramFiles, "ClaudeCode", "managed-mcp.json")];
15348
+ }
15349
+ return [];
15350
+ }
15351
+ function discoveredProjectRoots(claudeState, currentDirectory, explicit = [], cursorRoots = []) {
15352
+ const roots = /* @__PURE__ */ new Set();
15353
+ const add = (value) => {
15354
+ if (typeof value !== "string" || !value.trim()) return;
15355
+ const path = resolve5(value);
15356
+ if (existsSync37(path)) roots.add(path);
15357
+ };
15358
+ add(currentDirectory);
15359
+ for (const path of explicit) add(path);
15360
+ for (const path of cursorRoots) add(path);
15361
+ if (claudeState?.projects && typeof claudeState.projects === "object") {
15362
+ for (const path of Object.keys(claudeState.projects)) add(path);
15363
+ }
15364
+ return [...roots];
15365
+ }
15366
+ function cursorWorkspaceStorageCandidates(home, targetPlatform) {
15367
+ if (targetPlatform === "darwin") return [join36(home, "Library", "Application Support", "Cursor", "User", "workspaceStorage")];
15368
+ if (targetPlatform === "linux") return [join36(home, ".config", "Cursor", "User", "workspaceStorage")];
15369
+ if (targetPlatform === "win32" && process.env.APPDATA) {
15370
+ return [join36(process.env.APPDATA, "Cursor", "User", "workspaceStorage")];
15371
+ }
15372
+ return [];
15373
+ }
15374
+ function cursorWorkspaceRoots(home, targetPlatform) {
15375
+ const roots = /* @__PURE__ */ new Set();
15376
+ for (const storage of cursorWorkspaceStorageCandidates(home, targetPlatform)) {
15377
+ if (!existsSync37(storage)) continue;
15378
+ let entries = [];
15379
+ try {
15380
+ entries = readdirSync9(storage, { withFileTypes: true });
15381
+ } catch {
15382
+ continue;
15383
+ }
15384
+ for (const entry of entries) {
15385
+ if (!entry.isDirectory() || entry.isSymbolicLink?.()) continue;
15386
+ const state = readJson(join36(storage, entry.name, "workspace.json"));
15387
+ const raw = state?.folder;
15388
+ if (typeof raw !== "string" || !raw.trim()) continue;
15389
+ try {
15390
+ const path = raw.startsWith("file:") ? fileURLToPath(raw) : raw;
15391
+ if (existsSync37(path)) roots.add(resolve5(path));
15392
+ } catch {
15393
+ }
15394
+ }
15395
+ }
15396
+ return [...roots];
15397
+ }
15398
+ function codexMcpArtifacts(content) {
15399
+ const artifacts = [];
15400
+ const sections = [...content.matchAll(/^\s*\[\s*([^\]]+)\s*\]\s*$/gm)];
15401
+ for (let index = 0; index < sections.length && artifacts.length < 1e3; index++) {
15402
+ const section = sections[index];
15403
+ const key = section[1].trim();
15404
+ const root = key.match(/^mcp_servers\s*\.\s*(?:"((?:[^"\\]|\\.)+)"|'([^']+)'|([A-Za-z0-9_-]+))$/);
15405
+ if (!root) continue;
15406
+ let name = root[1] || root[2] || root[3];
15407
+ if (root[1]) {
15408
+ try {
15409
+ name = JSON.parse(`"${root[1]}"`);
15410
+ } catch {
15411
+ continue;
15412
+ }
15413
+ }
15414
+ const start = (section.index ?? 0) + section[0].length;
15415
+ const end = sections[index + 1]?.index ?? content.length;
15416
+ const block = content.slice(start, end);
15417
+ const stringValue = (key2) => {
15418
+ const found = block.match(new RegExp(`^\\s*${key2}\\s*=\\s*("(?:[^"\\\\]|\\\\.)*")`, "m"));
15419
+ if (!found) return void 0;
15420
+ try {
15421
+ return JSON.parse(found[1]);
15422
+ } catch {
15423
+ return void 0;
15424
+ }
15425
+ };
15426
+ const enabled = !/^\s*enabled\s*=\s*false\s*$/m.test(block);
15427
+ const url = stringValue("url");
15428
+ const command = stringValue("command");
15429
+ const safeShape = { name, enabled, origin: urlOrigin(url), package_name: safePackageName(command, []) };
15430
+ artifacts.push({
15431
+ harness: "codex",
15432
+ type: "mcp_server",
15433
+ canonical_id: `${canonical(name)}@user`,
15434
+ display_name: name,
15435
+ enabled,
15436
+ config_scope: "user",
15437
+ transport: command ? "stdio" : url ? "streamable-http" : "unknown",
15438
+ endpoint_origin: safeShape.origin,
15439
+ package_name: safeShape.package_name,
15440
+ config_hash: sha256(JSON.stringify(safeShape))
15441
+ });
15442
+ }
15443
+ return artifacts;
15444
+ }
15445
+ function flattenHookEntries(value) {
15446
+ if (!Array.isArray(value)) return [];
15447
+ const out = [];
15448
+ for (const entry of value) {
15449
+ if (!entry || typeof entry !== "object") continue;
15450
+ const record = entry;
15451
+ if (typeof record.command === "string") out.push(record);
15452
+ if (Array.isArray(record.hooks)) out.push(...flattenHookEntries(record.hooks));
15453
+ }
15454
+ return out;
15455
+ }
15456
+ function hookArtifacts(harness, config) {
15457
+ if (!config?.hooks || typeof config.hooks !== "object") return [];
15458
+ const artifacts = [];
15459
+ for (const [event, rawEntries] of Object.entries(config.hooks)) {
15460
+ for (const entry of flattenHookEntries(rawEntries)) {
15461
+ const commandHash = sha256(entry.command);
15462
+ const managed = entry.__synkro_managed__ === true || /[\\/]\.synkro[\\/]hooks[\\/]/.test(entry.command);
15463
+ artifacts.push({
15464
+ harness,
15465
+ type: "hook",
15466
+ canonical_id: `${canonical(event)}:${commandHash.slice(0, 20)}`,
15467
+ display_name: `${event} \xB7 ${basename3(String(entry.command).split(/\s+/)[0] || "hook")}`,
15468
+ enabled: entry.enabled !== false,
15469
+ config_scope: "user",
15470
+ package_name: basename3(String(entry.command).split(/\s+/)[0] || "") || void 0,
15471
+ config_hash: commandHash,
15472
+ metadata: { managed, events: [event] }
15473
+ });
15474
+ }
15475
+ }
15476
+ return artifacts;
15477
+ }
15478
+ function parseFrontmatter(content) {
15479
+ const match = content.match(/^---\s*\n([\s\S]*?)\n---/);
15480
+ if (!match) return {};
15481
+ const value = (key) => match[1].match(new RegExp(`^${key}:\\s*["']?([^"'\\n]+)`, "m"))?.[1]?.trim();
15482
+ return { name: value("name"), version: value("version") };
15483
+ }
15484
+ function skillArtifacts(harness, root) {
15485
+ if (!existsSync37(root)) return [];
15486
+ const manifests = [];
15487
+ const visit = (dir) => {
15488
+ let entries;
15489
+ try {
15490
+ entries = readdirSync9(dir, { withFileTypes: true });
15491
+ } catch {
15492
+ return;
15493
+ }
15494
+ for (const entry of entries) {
15495
+ if (entry.isSymbolicLink?.()) continue;
15496
+ const path = join36(dir, entry.name);
15497
+ if (entry.isFile() && entry.name === "SKILL.md") manifests.push(path);
15498
+ else if (entry.isDirectory()) visit(path);
15499
+ }
15500
+ };
15501
+ visit(root);
15502
+ return manifests.map((path) => {
15503
+ const content = readText(path);
15504
+ const frontmatter = parseFrontmatter(content);
15505
+ const rel = relative(root, path).replaceAll("\\", "/");
15506
+ const name = frontmatter.name || basename3(join36(path, "..")) || "skill";
15507
+ return {
15508
+ harness,
15509
+ type: "skill",
15510
+ canonical_id: `${canonical(name)}:${sha256(rel).slice(0, 16)}`,
15511
+ display_name: name,
15512
+ version: frontmatter.version,
15513
+ enabled: true,
15514
+ config_scope: "user",
15515
+ config_hash: content ? sha256(content) : void 0,
15516
+ metadata: { source: "SKILL.md" }
15517
+ };
15518
+ });
15519
+ }
15520
+ function cursorExtensionArtifacts(root) {
15521
+ if (!existsSync37(root)) return [];
15522
+ let dirs = [];
15523
+ try {
15524
+ dirs = readdirSync9(root, { withFileTypes: true }).filter((entry) => entry.isDirectory() && !entry.isSymbolicLink());
15525
+ } catch {
15526
+ return [];
15527
+ }
15528
+ const artifacts = [];
15529
+ for (const dir of dirs) {
15530
+ const pkg = readJson(join36(root, dir.name, "package.json"));
15531
+ if (!pkg) continue;
15532
+ const publisher = typeof pkg.publisher === "string" ? pkg.publisher : void 0;
15533
+ const name = typeof pkg.name === "string" ? pkg.name : dir.name;
15534
+ const id = publisher ? `${publisher}.${name}` : name;
15535
+ artifacts.push({
15536
+ harness: "cursor",
15537
+ type: "extension",
15538
+ canonical_id: canonical(id),
15539
+ display_name: String(pkg.displayName || name),
15540
+ version: typeof pkg.version === "string" ? pkg.version : void 0,
15541
+ enabled: true,
15542
+ config_scope: "user",
15543
+ config_hash: sha256(JSON.stringify({ id, version: pkg.version })),
15544
+ metadata: publisher ? { publisher } : void 0
15545
+ });
15546
+ }
15547
+ return artifacts;
15548
+ }
15549
+ function deploymentMode2(home) {
15550
+ const raw = readText(join36(home, ".synkro", "config.env"));
15551
+ const value = (key) => raw.match(new RegExp(`^${key}=['"]?([^'"\\n]*)`, "m"))?.[1]?.toLowerCase();
15552
+ if (value("SYNKRO_GRADING_MODE") === "byok") return "byok";
15553
+ if (value("SYNKRO_STORAGE_MODE") === "cloud") return "cloud";
15554
+ return "local";
15555
+ }
15556
+ function telemetryHealth(home) {
15557
+ const meta = readJson(join36(home, ".synkro", "telemetry-meta.json"));
15558
+ const health = {};
15559
+ 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;
15560
+ if (meta?.last_flush_error) health.telemetry_last_error = "flush_failed";
15561
+ const queue = join36(home, ".synkro", "telemetry-pending.jsonl");
15562
+ try {
15563
+ const size = statSync5(queue).size;
15564
+ health.telemetry_backlog = size <= 5 * 1024 * 1024 ? readFileSync36(queue, "utf8").split("\n").filter(Boolean).length : Math.ceil(size / 1024);
15565
+ } catch {
15566
+ }
15567
+ return health;
15568
+ }
15569
+ function harnessSnapshot(agent) {
15570
+ if (agent.kind === "claude_code") {
15571
+ const config2 = readJson(agent.settingsPath);
15572
+ const coverage2 = inspectCCHooks(agent.settingsPath);
15573
+ return {
15574
+ row: {
15575
+ harness: "claude_code",
15576
+ installed: true,
15577
+ enabled: coverage2.installed,
15578
+ version: agent.version,
15579
+ permission_mode: config2?.permissions?.defaultMode,
15580
+ hook_coverage: coverage2,
15581
+ hook_config_hash: sha256(JSON.stringify(coverage2)),
15582
+ config_scope: "user"
15583
+ },
15584
+ config: config2
15585
+ };
15586
+ }
15587
+ if (agent.kind === "cursor") {
15588
+ const config2 = readJson(agent.settingsPath);
15589
+ const coverage2 = inspectCursorHooks(agent.settingsPath);
15590
+ return {
15591
+ row: {
15592
+ harness: "cursor",
15593
+ installed: true,
15594
+ enabled: coverage2.installed,
15595
+ version: agent.version,
15596
+ hook_coverage: coverage2,
15597
+ hook_config_hash: sha256(JSON.stringify(coverage2)),
15598
+ config_scope: "user"
15599
+ },
15600
+ config: config2
15601
+ };
15602
+ }
15603
+ const config = readJson(agent.settingsPath);
15604
+ const coverage = inspectCodexHooks(agent.settingsPath);
15605
+ const toml = readText(join36(agent.configDir, "config.toml"));
15606
+ const permission = toml.match(/^\s*approval_policy\s*=\s*["']([^"']+)/m)?.[1];
15607
+ return {
15608
+ row: {
15609
+ harness: "codex",
15610
+ installed: true,
15611
+ enabled: coverage.installed,
15612
+ version: agent.version,
15613
+ permission_mode: permission,
15614
+ hook_coverage: coverage,
15615
+ hook_config_hash: sha256(JSON.stringify(coverage)),
15616
+ config_scope: "user"
15617
+ },
15618
+ config
15619
+ };
15620
+ }
15621
+ function collectOperationalInventory(options = {}) {
15622
+ const home = options.homeDir ?? homedir36();
15623
+ const detected = options.detectedAgents ?? detectAgents();
15624
+ const identity = getOperationalInstallationIdentity(options.identityPath);
15625
+ const targetPlatform = options.platformName ?? platform5();
15626
+ const codexHome = options.homeDir ? join36(home, ".codex") : process.env.CODEX_HOME || join36(home, ".codex");
15627
+ const harnesses = [];
15628
+ const artifacts = [];
15629
+ for (const agent of detected) {
15630
+ const { row, config } = harnessSnapshot(agent);
15631
+ harnesses.push(row);
15632
+ artifacts.push(...hookArtifacts(row.harness, config));
15633
+ }
15634
+ const claudeJson = readJson(join36(home, ".claude.json"));
15635
+ artifacts.push(...mcpArtifactsFromJson("claude_code", claudeJson));
15636
+ if (claudeJson?.projects && typeof claudeJson.projects === "object") {
15637
+ for (const [projectPath, project] of Object.entries(claudeJson.projects)) {
15638
+ if (!project || typeof project !== "object") continue;
15639
+ artifacts.push(...mcpArtifactsFromJson("claude_code", project, `local:${sha256(projectPath).slice(0, 16)}`));
15640
+ }
15641
+ }
15642
+ artifacts.push(...mcpArtifactsFromJson("cursor", readJson(join36(home, ".cursor", "mcp.json"))));
15643
+ artifacts.push(...codexMcpArtifacts(readText(join36(codexHome, "config.toml"))));
15644
+ const projectRoots = discoveredProjectRoots(
15645
+ claudeJson,
15646
+ options.currentDirectory ?? process.cwd(),
15647
+ options.projectRoots,
15648
+ cursorWorkspaceRoots(home, targetPlatform)
15649
+ );
15650
+ for (const projectRoot of projectRoots) {
15651
+ const scopeHash = sha256(projectRoot).slice(0, 16);
15652
+ artifacts.push(...mcpArtifactsFromJson(
15653
+ "claude_code",
15654
+ readJson(join36(projectRoot, ".mcp.json")),
15655
+ `project:${scopeHash}`
15656
+ ));
15657
+ const cursorProjectConfig = join36(projectRoot, ".cursor", "mcp.json");
15658
+ if (resolve5(cursorProjectConfig) !== resolve5(join36(home, ".cursor", "mcp.json"))) {
15659
+ artifacts.push(...mcpArtifactsFromJson(
15660
+ "cursor",
15661
+ readJson(cursorProjectConfig),
15662
+ `project:${scopeHash}`
15663
+ ));
15664
+ }
15665
+ }
15666
+ for (const managedPath of claudeManagedMcpConfigCandidates(targetPlatform)) {
15667
+ artifacts.push(...mcpArtifactsFromJson("claude_code", readJson(managedPath), "managed"));
15668
+ }
15669
+ const desktopConfigPath = claudeDesktopConfigCandidates(home, targetPlatform).find((path) => existsSync37(path));
15670
+ if (desktopConfigPath) {
15671
+ const desktopConfig = readJson(desktopConfigPath);
15672
+ harnesses.push({
15673
+ harness: "claude_desktop",
15674
+ installed: true,
15675
+ enabled: true,
15676
+ config_scope: "user"
15677
+ });
15678
+ artifacts.push(...mcpArtifactsFromJson("claude_desktop", desktopConfig));
15679
+ }
15680
+ const claudeSettings = readJson(join36(home, ".claude", "settings.json"));
15681
+ if (claudeSettings?.enabledPlugins && typeof claudeSettings.enabledPlugins === "object") {
15682
+ for (const [name, enabled] of Object.entries(claudeSettings.enabledPlugins)) {
15683
+ artifacts.push({
15684
+ harness: "claude_code",
15685
+ type: "plugin",
15686
+ canonical_id: canonical(name),
15687
+ display_name: name,
15688
+ enabled: enabled === true,
15689
+ config_scope: "user",
15690
+ metadata: { source: "enabledPlugins" }
15691
+ });
15692
+ }
15693
+ }
15694
+ artifacts.push(...skillArtifacts("claude_code", join36(home, ".claude", "skills")));
15695
+ artifacts.push(...skillArtifacts("cursor", join36(home, ".cursor", "skills")));
15696
+ artifacts.push(...skillArtifacts("codex", join36(codexHome, "skills")));
15697
+ artifacts.push(...cursorExtensionArtifacts(join36(home, ".cursor", "extensions")));
15698
+ const uniqueArtifacts = /* @__PURE__ */ new Map();
15699
+ for (const artifact of artifacts) {
15700
+ const key = `${artifact.harness || "global"}:${artifact.type}:${artifact.canonical_id}`;
15701
+ uniqueArtifacts.set(key, artifact);
15702
+ }
15703
+ const codingHarnesses = harnesses.filter((row) => row.harness === "claude_code" || row.harness === "cursor" || row.harness === "codex");
15704
+ const health = telemetryHealth(home) ?? {};
15705
+ health.scanners = {
15706
+ hook_runtime: codingHarnesses.length === 0 ? "unknown" : codingHarnesses.every((row) => row.enabled) ? "ok" : "degraded"
15707
+ };
15708
+ return {
15709
+ schema_version: 1,
15710
+ collected_at: (/* @__PURE__ */ new Date()).toISOString(),
15711
+ installation: {
15712
+ install_id: identity.installation_id,
15713
+ hostname_hash: pseudonymousHostnameHash(identity.installation_id, hostname2()),
15714
+ platform: targetPlatform,
15715
+ os_version: release(),
15716
+ arch: arch(),
15717
+ cli_version: cliVersion(),
15718
+ node_version: process.version,
15719
+ bun_version: process.versions.bun,
15720
+ deployment_mode: deploymentMode2(home)
15721
+ },
15722
+ harnesses,
15723
+ artifacts: [...uniqueArtifacts.values()],
15724
+ health
15725
+ };
15726
+ }
15727
+ var init_collector = __esm({
15728
+ "cli/inventory/collector.ts"() {
15729
+ "use strict";
15730
+ init_agentDetect();
15731
+ init_ccHookConfig();
15732
+ init_cursorHookConfig();
15733
+ init_codexHookConfig();
15734
+ init_identity2();
15735
+ }
15736
+ });
15737
+
15738
+ // cli/inventory/sync.ts
15739
+ var sync_exports2 = {};
15740
+ __export(sync_exports2, {
15741
+ inventorySnapshotChunks: () => inventorySnapshotChunks,
15742
+ inventorySyncTarget: () => inventorySyncTarget,
15743
+ resolveInventoryGateway: () => resolveInventoryGateway,
15744
+ resolveLocalInventoryUrl: () => resolveLocalInventoryUrl,
15745
+ shouldSyncInventory: () => shouldSyncInventory,
15746
+ syncOperationalInventory: () => syncOperationalInventory,
15747
+ syncOperationalInventoryDetached: () => syncOperationalInventoryDetached
15748
+ });
15749
+ import { createHash as createHash6, randomUUID as randomUUID6 } from "crypto";
15750
+ import { spawn as spawn8 } from "child_process";
15751
+ import {
15752
+ existsSync as existsSync38,
15753
+ mkdirSync as mkdirSync20,
15754
+ readFileSync as readFileSync37,
15755
+ renameSync as renameSync10,
15756
+ writeFileSync as writeFileSync25
15757
+ } from "fs";
15758
+ import { homedir as homedir37 } from "os";
15759
+ import { dirname as dirname10, join as join37 } from "path";
15760
+ function syncStatePath() {
15761
+ return process.env.SYNKRO_INVENTORY_SYNC_STATE_PATH || join37(homedir37(), ".synkro", "inventory-sync.json");
15762
+ }
15763
+ function readState(path = syncStatePath()) {
15764
+ try {
15765
+ const parsed = JSON.parse(readFileSync37(path, "utf8"));
15766
+ return parsed && typeof parsed === "object" ? parsed : {};
15767
+ } catch {
15768
+ return {};
15769
+ }
15770
+ }
15771
+ function writeState(state, path = syncStatePath()) {
15772
+ try {
15773
+ mkdirSync20(dirname10(path), { recursive: true, mode: 448 });
15774
+ const temp = `${path}.${process.pid}.tmp`;
15775
+ writeFileSync25(temp, JSON.stringify(state, null, 2) + "\n", { encoding: "utf8", mode: 384 });
15776
+ renameSync10(temp, path);
15777
+ } catch {
15778
+ }
15779
+ }
15780
+ function shouldSyncInventory(state, now = Date.now(), target) {
15781
+ if (target && state.last_target !== target) return true;
15782
+ const lastOk = state.last_ok_at ? Date.parse(state.last_ok_at) : 0;
15783
+ if (Number.isFinite(lastOk) && lastOk > 0 && now - lastOk < SUCCESS_INTERVAL_MS) return false;
15784
+ const lastAttempt = state.last_attempt_at ? Date.parse(state.last_attempt_at) : 0;
15785
+ return !Number.isFinite(lastAttempt) || lastAttempt <= 0 || now - lastAttempt >= FAILURE_RETRY_MS;
15786
+ }
15787
+ function readConfig() {
15788
+ const path = join37(homedir37(), ".synkro", "config.env");
15789
+ const out = {};
15790
+ try {
15791
+ for (const rawLine of readFileSync37(path, "utf8").split("\n")) {
15792
+ const line = rawLine.trim();
15793
+ if (!line || line.startsWith("#")) continue;
15794
+ const index = line.indexOf("=");
15795
+ if (index <= 0) continue;
15796
+ const key = line.slice(0, index).trim();
15797
+ let value = line.slice(index + 1).trim();
15798
+ if (value.startsWith("'") && value.endsWith("'") || value.startsWith('"') && value.endsWith('"')) value = value.slice(1, -1);
15799
+ out[key] = value;
15800
+ }
15801
+ } catch {
15802
+ }
15803
+ return out;
15804
+ }
15805
+ function inventorySyncTarget(config, env = process.env) {
15806
+ return (config.SYNKRO_STORAGE_MODE || env.SYNKRO_STORAGE_MODE) === "cloud" ? "cloud" : "local";
15807
+ }
15808
+ function resolveLocalInventoryUrl(rawPort) {
15809
+ const parsed = Number.parseInt(rawPort || "", 10);
15810
+ const port = Number.isInteger(parsed) && parsed >= 1 && parsed <= 65535 ? parsed : 18931;
15811
+ return `http://127.0.0.1:${port}/api/local/inventory/snapshot`;
15812
+ }
15813
+ function localhost(host) {
15814
+ return host === "localhost" || host === "127.0.0.1" || host === "::1";
15815
+ }
15816
+ function resolveInventoryGateway(raw) {
15817
+ if (!raw) return DEFAULT_GATEWAY2;
15818
+ try {
15819
+ const url = new URL(raw);
15820
+ const host = url.hostname.toLowerCase();
15821
+ const allowedHost = localhost(host) || host === "synkro.sh" || host.endsWith(".synkro.sh");
15822
+ const allowedProtocol = url.protocol === "https:" || url.protocol === "http:" && localhost(host);
15823
+ return allowedHost && allowedProtocol ? raw.replace(/\/$/, "") : DEFAULT_GATEWAY2;
15824
+ } catch {
15825
+ return DEFAULT_GATEWAY2;
15826
+ }
15827
+ }
15828
+ async function loadToken() {
15829
+ try {
15830
+ const durable = readFileSync37(join37(homedir37(), ".synkro", ".mcp-jwt"), "utf8").trim();
15831
+ if (durable) return durable;
15832
+ } catch {
15833
+ }
15834
+ try {
15835
+ const auth = await Promise.resolve().then(() => (init_stub(), stub_exports));
15836
+ if (!auth.isAuthenticated()) return null;
15837
+ await auth.ensureValidToken();
15838
+ return auth.getAccessToken();
15839
+ } catch {
15840
+ return null;
15841
+ }
15842
+ }
15843
+ function stable(value) {
15844
+ if (Array.isArray(value)) return value.map(stable);
15845
+ if (!value || typeof value !== "object") return value;
15846
+ return Object.fromEntries(Object.entries(value).sort(([a], [b]) => a.localeCompare(b)).map(([key, child]) => [key, stable(child)]));
15847
+ }
15848
+ function inventorySnapshotChunks(snapshot, maxBytes = INVENTORY_CHUNK_BYTES) {
15849
+ const { collected_at: _heartbeat, ...material } = snapshot;
15850
+ const payloadHash = createHash6("sha256").update(JSON.stringify(stable(material))).digest("hex");
15851
+ const snapshotId = randomUUID6();
15852
+ const base = { ...snapshot, artifacts: [] };
15853
+ const baseBytes = Buffer.byteLength(JSON.stringify(base));
15854
+ const chunks = [];
15855
+ let artifacts = [];
15856
+ let bytes = baseBytes;
15857
+ for (const artifact of snapshot.artifacts) {
15858
+ const artifactBytes = Buffer.byteLength(JSON.stringify(artifact)) + 1;
15859
+ if (artifacts.length > 0 && bytes + artifactBytes > maxBytes) {
15860
+ chunks.push({ ...snapshot, artifacts });
15861
+ artifacts = [];
15862
+ bytes = baseBytes;
15863
+ }
15864
+ artifacts.push(artifact);
15865
+ bytes += artifactBytes;
15866
+ }
15867
+ if (artifacts.length > 0 || chunks.length === 0) chunks.push({ ...snapshot, artifacts });
15868
+ return { snapshotId, payloadHash, chunks };
15869
+ }
15870
+ async function postInventoryChunks(url, headers, snapshot) {
15871
+ const delivery = inventorySnapshotChunks(snapshot);
15872
+ let response = null;
15873
+ for (let index = 0; index < delivery.chunks.length; index++) {
15874
+ response = await fetch(url, {
15875
+ method: "POST",
15876
+ headers: {
15877
+ ...headers,
15878
+ "Content-Type": "application/json",
15879
+ "X-Synkro-Inventory-Snapshot-Id": delivery.snapshotId,
15880
+ "X-Synkro-Inventory-Payload-Hash": delivery.payloadHash,
15881
+ "X-Synkro-Inventory-Chunk-Index": String(index),
15882
+ "X-Synkro-Inventory-Chunk-Count": String(delivery.chunks.length),
15883
+ "X-Synkro-Inventory-Artifact-Count": String(snapshot.artifacts.length)
15884
+ },
15885
+ body: JSON.stringify(delivery.chunks[index]),
15886
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS2)
15887
+ });
15888
+ if (!response.ok) return response;
15889
+ }
15890
+ return response;
15891
+ }
15892
+ async function syncOperationalInventory(options = {}) {
15893
+ const path = syncStatePath();
15894
+ const state = readState(path);
15895
+ const config = readConfig();
15896
+ const target = inventorySyncTarget(config);
15897
+ if (!options.force && !shouldSyncInventory(state, Date.now(), target)) return { ok: true, skipped: "throttled", target };
15898
+ const attemptedAt = (/* @__PURE__ */ new Date()).toISOString();
15899
+ writeState({ ...state, last_attempt_at: attemptedAt, last_target: target }, path);
15900
+ try {
15901
+ const snapshot = collectOperationalInventory();
15902
+ if (target === "local") {
15903
+ const response2 = await postInventoryChunks(resolveLocalInventoryUrl(
15904
+ process.env.SYNKRO_HOST_MCP_PORT || process.env.SYNKRO_MCP_PORT || config.SYNKRO_MCP_PORT
15905
+ ), {}, snapshot);
15906
+ if (!response2.ok) {
15907
+ const error = `http_${response2.status}`;
15908
+ writeState({ ...state, last_attempt_at: attemptedAt, last_error: error, last_target: target }, path);
15909
+ return { ok: false, status: response2.status, error, target };
15910
+ }
15911
+ writeState({ last_attempt_at: attemptedAt, last_ok_at: (/* @__PURE__ */ new Date()).toISOString(), last_target: target }, path);
15912
+ return { ok: true, status: response2.status, target };
15913
+ }
15914
+ const token = await loadToken();
15915
+ if (!token) {
15916
+ writeState({ ...state, last_attempt_at: attemptedAt, last_error: "no_auth", last_target: target }, path);
15917
+ return { ok: false, skipped: "no_auth", target };
15918
+ }
15919
+ const gateway = resolveInventoryGateway(
15920
+ process.env.SYNKRO_GATEWAY_URL || config.SYNKRO_GATEWAY_URL || process.env.SYNKRO_API_URL
15921
+ );
15922
+ const response = await postInventoryChunks(
15923
+ `${gateway}/api/v1/cli/inventory/snapshot`,
15924
+ { Authorization: `Bearer ${token}` },
15925
+ snapshot
15926
+ );
15927
+ if (!response.ok) {
15928
+ const error = `http_${response.status}`;
15929
+ writeState({ ...state, last_attempt_at: attemptedAt, last_error: error, last_target: target }, path);
15930
+ return { ok: false, status: response.status, error, target };
15931
+ }
15932
+ writeState({ last_attempt_at: attemptedAt, last_ok_at: (/* @__PURE__ */ new Date()).toISOString(), last_target: target }, path);
15933
+ return { ok: true, status: response.status, target };
15934
+ } catch {
15935
+ writeState({ ...state, last_attempt_at: attemptedAt, last_error: "sync_failed", last_target: target }, path);
15936
+ return { ok: false, error: "sync_failed", target };
15937
+ }
15938
+ }
15939
+ function syncOperationalInventoryDetached() {
15940
+ if (process.env.SYNKRO_INVENTORY_DETACHED === "1") return;
15941
+ const path = syncStatePath();
15942
+ const state = readState(path);
15943
+ const target = inventorySyncTarget(readConfig());
15944
+ if (!shouldSyncInventory(state, Date.now(), target)) return;
15945
+ writeState({ ...state, last_attempt_at: (/* @__PURE__ */ new Date()).toISOString(), last_target: target }, path);
15946
+ try {
15947
+ const script = process.argv[1];
15948
+ if (!script || !existsSync38(script)) return;
15949
+ const child = spawn8(process.execPath, [script, "inventory-sync", "--detached"], {
15950
+ detached: true,
15951
+ stdio: "ignore",
15952
+ env: { ...process.env, SYNKRO_INVENTORY_DETACHED: "1" }
15953
+ });
15954
+ child.unref();
15955
+ } catch {
15956
+ }
15957
+ }
15958
+ var DEFAULT_GATEWAY2, SUCCESS_INTERVAL_MS, FAILURE_RETRY_MS, REQUEST_TIMEOUT_MS2, INVENTORY_CHUNK_BYTES;
15959
+ var init_sync2 = __esm({
15960
+ "cli/inventory/sync.ts"() {
15961
+ "use strict";
15962
+ init_collector();
15963
+ DEFAULT_GATEWAY2 = "https://api.synkro.sh";
15964
+ SUCCESS_INTERVAL_MS = 30 * 6e4;
15965
+ FAILURE_RETRY_MS = 5 * 6e4;
15966
+ REQUEST_TIMEOUT_MS2 = 15e3;
15967
+ INVENTORY_CHUNK_BYTES = 15e5;
15968
+ }
15969
+ });
15970
+
14528
15971
  // cli/bootstrap.js
14529
- import { readFileSync as readFileSync35, existsSync as existsSync36 } from "fs";
14530
- import { resolve as resolve5 } from "path";
15972
+ import { readFileSync as readFileSync38, existsSync as existsSync39 } from "fs";
15973
+ import { resolve as resolve6 } from "path";
14531
15974
  var envCandidates = [
14532
- resolve5(process.env.HOME ?? "", ".synkro", "config.env")
15975
+ resolve6(process.env.HOME ?? "", ".synkro", "config.env")
14533
15976
  ];
14534
15977
  for (const envPath of envCandidates) {
14535
- if (!existsSync36(envPath)) continue;
14536
- const envContent = readFileSync35(envPath, "utf-8");
15978
+ if (!existsSync39(envPath)) continue;
15979
+ const envContent = readFileSync38(envPath, "utf-8");
14537
15980
  for (const line of envContent.split("\n")) {
14538
15981
  const trimmed = line.trim();
14539
15982
  if (!trimmed || trimmed.startsWith("#")) continue;
@@ -14548,9 +15991,9 @@ var args = process.argv.slice(2);
14548
15991
  var cmd2 = args[0] || "";
14549
15992
  var subArgs = args.slice(1);
14550
15993
  var isDetachedChild = process.env.SYNKRO_TELEMETRY_DETACHED === "1";
14551
- var FLUSH_SKIP = /* @__PURE__ */ new Set(["grade", "version", "--version", "-v", "help", "--help", "-h", ""]);
15994
+ var FLUSH_SKIP = /* @__PURE__ */ new Set(["grade", "inventory-sync", "version", "--version", "-v", "help", "--help", "-h", ""]);
14552
15995
  function printVersion() {
14553
- console.log("1.7.95");
15996
+ console.log("1.8.0");
14554
15997
  }
14555
15998
  function printHelp2() {
14556
15999
  console.log(`Synkro CLI \u2014 runtime safety for AI coding agents
@@ -14738,6 +16181,13 @@ async function main() {
14738
16181
  await telemetryCommand2(subArgs);
14739
16182
  break;
14740
16183
  }
16184
+ // Internal detached worker. Operational inventory is independent of product
16185
+ // telemetry and always fail-open; it intentionally stays out of help output.
16186
+ case "inventory-sync": {
16187
+ const { syncOperationalInventory: syncOperationalInventory2 } = await Promise.resolve().then(() => (init_sync2(), sync_exports2));
16188
+ await syncOperationalInventory2({ force: subArgs.includes("--detached") || subArgs.includes("--force") });
16189
+ break;
16190
+ }
14741
16191
  default: {
14742
16192
  console.error(`Unknown command: ${cmd2}`);
14743
16193
  printHelp2();
@@ -14752,6 +16202,11 @@ async function postDispatchFlush() {
14752
16202
  flushDetached2();
14753
16203
  } catch {
14754
16204
  }
16205
+ try {
16206
+ const { syncOperationalInventoryDetached: syncOperationalInventoryDetached2 } = await Promise.resolve().then(() => (init_sync2(), sync_exports2));
16207
+ syncOperationalInventoryDetached2();
16208
+ } catch {
16209
+ }
14755
16210
  }
14756
16211
  async function shutdown(code) {
14757
16212
  try {