@synkro-sh/cli 1.6.93 → 1.6.95

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
@@ -1775,6 +1775,27 @@ async function hostedGrade(role: GradeRole, prompt: string, jwt: string, timeout
1775
1775
  return String(data.result || '');
1776
1776
  }
1777
1777
 
1778
+ // Cloud task-adherence: fire the task-only 'task-drift' surface on the org's
1779
+ // container and return ONLY the progress systemMessage (informational \u2014 never
1780
+ // blocks an edit). Fail-silent: any hiccup returns '' so task tracking can't
1781
+ // break grading. The payload IS a ScanEnvelope; the container runs getActiveTask
1782
+ // (resolved per (org,user,repo) via active_tasks) + taskDrift and returns the
1783
+ // decision. Secrets are scrubbed before the envelope crosses the network.
1784
+ export async function cloudTaskDrift(envelope: Record<string, unknown>, jwt: string, timeoutMs = 20000): Promise<string> {
1785
+ try {
1786
+ const body = JSON.stringify({ role: 'task-drift', payload: scrubSecrets(JSON.stringify(envelope)), content: '' });
1787
+ const resp = await fetch(CONTAINERS_URL + '/grade/submit', {
1788
+ method: 'POST',
1789
+ headers: { 'Content-Type': 'application/json', Authorization: 'Bearer ' + jwt },
1790
+ body,
1791
+ signal: AbortSignal.timeout(timeoutMs),
1792
+ });
1793
+ if (!resp.ok) return '';
1794
+ const j = await resp.json() as { systemMessage?: unknown };
1795
+ return typeof j?.systemMessage === 'string' ? j.systemMessage : '';
1796
+ } catch { return ''; }
1797
+ }
1798
+
1778
1799
  // Option B: when a cloud grade fails (esp. a timeout), the REAL reason lives
1779
1800
  // inside the container, not on the wire \u2014 the client only ever sees "timed out".
1780
1801
  // Pull the org's per-worker claude/cursor sick reasons + log tails from the
@@ -3666,6 +3687,31 @@ export function countEditLineDelta(source: {
3666
3687
  return { linesAdded, linesRemoved };
3667
3688
  }
3668
3689
 
3690
+ // Build the recorded edit diff (code_change) for the audit trail, same format as
3691
+ // the routed hook's summarizeEdit: trim the common prefix/suffix between the file
3692
+ // before the edit and the proposed full content, then emit minus/plus prefixed,
3693
+ // line-numbered rows (the dashboard CodeChangeCard colors each row by its leading
3694
+ // sign). Without this the event detail falls back to the raw file/content dump
3695
+ // instead of an added/removed diff.
3696
+ export function summarizeEditDiff(baseContent: string, proposed: string): string {
3697
+ const EDIT_AUDIT_CAP = 16000;
3698
+ const a = baseContent ? baseContent.split('\\n') : [];
3699
+ const b = proposed ? proposed.split('\\n') : [];
3700
+ let start = 0;
3701
+ while (start < a.length && start < b.length && a[start] === b[start]) start++;
3702
+ let endA = a.length;
3703
+ let endB = b.length;
3704
+ while (endA > start && endB > start && a[endA - 1] === b[endB - 1]) { endA--; endB--; }
3705
+ const removed = a.slice(start, endA);
3706
+ const added = b.slice(start, endB);
3707
+ const out: string[] = [];
3708
+ for (let i = 0; i < removed.length; i++) out.push('-' + String(start + 1 + i).padStart(5) + ' ' + removed[i]);
3709
+ for (let i = 0; i < added.length; i++) out.push('+' + String(start + 1 + i).padStart(5) + ' ' + added[i]);
3710
+ let diff = out.join('\\n');
3711
+ if (diff.length > EDIT_AUDIT_CAP) diff = diff.slice(0, EDIT_AUDIT_CAP) + '\\n... [truncated]';
3712
+ return diff;
3713
+ }
3714
+
3669
3715
  /** Normalize model names so the same Cursor session doesn't split across agent buckets. */
3670
3716
  export function normalizeCaptureModel(model: string): string {
3671
3717
  const m = (model || '').trim();
@@ -4265,12 +4311,12 @@ export function outputEmpty(): void {
4265
4311
  `;
4266
4312
  EDIT_PRECHECK_TS = `#!/usr/bin/env bun
4267
4313
  import {
4268
- loadJwt, ensureFreshJwt, detectRepo, loadConfig, route, tag, localGrade,
4314
+ loadJwt, ensureFreshJwt, detectRepo, loadConfig, route, tag, localGrade, cloudTaskDrift,
4269
4315
  parseVerdict, parseCombinedVerdict, combinedEditGradeEnabled, dispatchCapture, dispatchFinding, ruleMode, reconstructContent, editSecretDenial, isPathUnder, postWithRetry,
4270
4316
  readStdin, extractTranscript, readLastPrompt, findNearestDeps, filePathFromToolInput,
4271
4317
  appendSessionAction, readSessionLog, compressSessionLog, sessionLogForGrade, log,
4272
4318
  outputJson, outputEmpty, setupCursorHookSignals, installHookWatchdog, isEditTool, hookSessionId, GATEWAY_URL,
4273
- logGraderUnavailable, graderUnavailableMessage, filterRules, ruleFilterText, normalizeMode, countEditLineDelta,
4319
+ logGraderUnavailable, graderUnavailableMessage, filterRules, ruleFilterText, normalizeMode, countEditLineDelta, summarizeEditDiff,
4274
4320
  captureLineMetrics, cursorModelFromPayload, resolveTranscriptPath, isCursorInvokingCcHook,
4275
4321
  loadSynkroFile, effectiveGraderPool, synkroFilePresent, noSynkroSkipMessage,
4276
4322
  type HookConfig, type Rule,
@@ -4365,6 +4411,11 @@ async function main() {
4365
4411
  try { fileBefore = readFileSync(fullPath, 'utf-8').slice(0, 65536); } catch {}
4366
4412
  }
4367
4413
 
4414
+ // Audit diff (code_change): added/removed lines for the event detail's Code Change
4415
+ // card. Base = the file before this edit (Write \u2192 file on disk if any). Captured on
4416
+ // every edit verdict below so cloud-graded events show the diff, not a raw dump.
4417
+ const editDiff = summarizeEditDiff(toolName === 'Write' ? existingContent : fileBefore, proposed);
4418
+
4368
4419
  // Extract transcript context
4369
4420
  const transcript = extractTranscript(transcriptPath);
4370
4421
  const lastPrompt = readLastPrompt(sessionId);
@@ -4422,6 +4473,15 @@ async function main() {
4422
4473
  ].join('\\n');
4423
4474
  const graderPrompt = buildGraderPrompt(proposedShort);
4424
4475
 
4476
+ // Cloud task-adherence (additive, informational): fire the task-only grade in
4477
+ // PARALLEL with the rule grade so it adds no latency, then merge its progress
4478
+ // message into the pass output. Fail-silent ('' on any issue). Sent as a
4479
+ // synthetic Write of the reconstructed content so the container needs no base
4480
+ // bytes. cwd = the repo identifier the container keys active_tasks on.
4481
+ const taskDriftP: Promise<string> = process.env.SYNKRO_DEPLOY_LOCATION === 'cloud'
4482
+ ? cloudTaskDrift({ payload: { tool_name: 'Write', tool_input: { file_path: filePath, content: proposedShort } }, harness: agentKind, cwd: gitRepo, sessionId }, jwt)
4483
+ : Promise.resolve('');
4484
+
4425
4485
  // \u2500\u2500\u2500 Combined org-rules + CWE in ONE inference (SYNKRO_COMBINED_EDIT_GRADE) \u2500\u2500\u2500
4426
4486
  // Self-contained early-return branch \u2014 the default two-grade path below is
4427
4487
  // left untouched. Appends the CWE rule set to the rules prompt, runs one
@@ -4500,7 +4560,7 @@ async function main() {
4500
4560
  toolName, gitRepo, sessionId, config.captureDepth, {
4501
4561
  command: editContent, reasoning: guardReason,
4502
4562
  rulesChecked: relevantRules, violatedRules,
4503
- ccModel: captureModel, ...lineMetrics,
4563
+ ccModel: captureModel, codeChange: editDiff, ...lineMetrics,
4504
4564
  });
4505
4565
  outputJson({
4506
4566
  systemMessage: tagStr + ' editGuard ' + fileShort + ' \u2192 blocked: ' + guardReason + (cweFindings.length ? ' (+' + cweFindings.length + ' CWE)' : ''),
@@ -4524,10 +4584,11 @@ async function main() {
4524
4584
  toolName, gitRepo, sessionId, config.captureDepth, {
4525
4585
  command: editContent, reasoning: ruleVerdict.reason || 'no violations (rules + CWE)',
4526
4586
  rulesChecked: relevantRules, violatedRules: [],
4527
- ccModel: captureModel, ...lineMetrics,
4587
+ ccModel: captureModel, codeChange: editDiff, ...lineMetrics,
4528
4588
  });
4529
4589
  const cPassLine = tagStr + ' editGuard ' + fileShort + ' \u2192 pass: ' + (ruleVerdict.reason || 'clean (rules + CWE)');
4530
- outputJson({ systemMessage: cPassLine, hookSpecificOutput: { hookEventName: 'PreToolUse', additionalContext: 'Synkro combined edit judge (rules + CWE). ' + (ruleVerdict.reason || 'clean') } });
4590
+ const cTaskMsg = await taskDriftP;
4591
+ outputJson({ systemMessage: cPassLine + (cTaskMsg ? '\\n' + cTaskMsg : ''), hookSpecificOutput: { hookEventName: 'PreToolUse', additionalContext: 'Synkro combined edit judge (rules + CWE). ' + (ruleVerdict.reason || 'clean') } });
4531
4592
  return;
4532
4593
  }
4533
4594
 
@@ -4556,7 +4617,7 @@ async function main() {
4556
4617
  toolName, gitRepo, sessionId, config.captureDepth, {
4557
4618
  command: editContent, reasoning: guardReason,
4558
4619
  rulesChecked: relevantRules, violatedRules,
4559
- ccModel: captureModel, ...lineMetrics,
4620
+ ccModel: captureModel, codeChange: editDiff, ...lineMetrics,
4560
4621
  });
4561
4622
  outputJson({
4562
4623
  systemMessage: tagStr + ' editGuard ' + fileShort + ' \u2192 blocked: ' + guardReason,
@@ -4570,10 +4631,11 @@ async function main() {
4570
4631
  toolName, gitRepo, sessionId, config.captureDepth, {
4571
4632
  command: editContent, reasoning: verdict.reason || 'no policy violations detected',
4572
4633
  rulesChecked: relevantRules, violatedRules: [],
4573
- ccModel: captureModel, ...lineMetrics,
4634
+ ccModel: captureModel, codeChange: editDiff, ...lineMetrics,
4574
4635
  });
4575
4636
  const passLine = tagStr + ' editGuard ' + fileShort + ' \u2192 pass: ' + (verdict.reason || 'no policy violations detected');
4576
- outputJson({ systemMessage: passLine, hookSpecificOutput: { hookEventName: 'PreToolUse', additionalContext: 'Synkro local edit judge. ' + (verdict.reason || 'no policy violations detected') } });
4637
+ const rTaskMsg = await taskDriftP;
4638
+ outputJson({ systemMessage: passLine + (rTaskMsg ? '\\n' + rTaskMsg : ''), hookSpecificOutput: { hookEventName: 'PreToolUse', additionalContext: 'Synkro local edit judge. ' + (verdict.reason || 'no policy violations detected') } });
4577
4639
  return;
4578
4640
  }
4579
4641
 
@@ -11237,7 +11299,7 @@ function writeConfigEnv(opts) {
11237
11299
  `SYNKRO_CREDENTIALS_PATH=${shellQuoteSingle(credsPath)}`,
11238
11300
  `SYNKRO_TIER=${shellQuoteSingle(safeTier)}`,
11239
11301
  `SYNKRO_INFERENCE=${shellQuoteSingle(safeInference)}`,
11240
- `SYNKRO_VERSION=${shellQuoteSingle("1.6.93")}`
11302
+ `SYNKRO_VERSION=${shellQuoteSingle("1.6.95")}`
11241
11303
  ];
11242
11304
  if (safeSynkroBin) lines.push(`SYNKRO_CLI_BIN=${shellQuoteSingle(safeSynkroBin)}`);
11243
11305
  if (safeUserId) lines.push(`SYNKRO_USER_ID=${shellQuoteSingle(safeUserId)}`);
@@ -15302,7 +15364,7 @@ var args = process.argv.slice(2);
15302
15364
  var cmd = args[0] || "";
15303
15365
  var subArgs = args.slice(1);
15304
15366
  function printVersion() {
15305
- console.log("1.6.93");
15367
+ console.log("1.6.95");
15306
15368
  }
15307
15369
  function printHelp2() {
15308
15370
  console.log(`Synkro CLI \u2014 runtime safety for AI coding agents