@synkro-sh/cli 1.6.95 → 1.6.96

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
@@ -1781,7 +1781,21 @@ async function hostedGrade(role: GradeRole, prompt: string, jwt: string, timeout
1781
1781
  // break grading. The payload IS a ScanEnvelope; the container runs getActiveTask
1782
1782
  // (resolved per (org,user,repo) via active_tasks) + taskDrift and returns the
1783
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> {
1784
+ // Cloud task-drift returns BOTH channels so the hook can STEER the agent, not just
1785
+ // narrate to the user: systemMessage \u2192 user terminal; additionalContext \u2192 the agent;
1786
+ // decision='deny' \u2192 a contradiction the agent must fix (hard block, matching local).
1787
+ // The container ships formatDecision()'s shape ({ systemMessage, hookSpecificOutput:
1788
+ // { additionalContext, permissionDecision } }); we surface all three.
1789
+ export interface TaskDriftResult { systemMessage: string; additionalContext: string; decision: 'allow' | 'deny' }
1790
+ const TASK_DRIFT_NONE: TaskDriftResult = { systemMessage: '', additionalContext: '', decision: 'allow' };
1791
+
1792
+ export async function cloudTaskDrift(envelope: Record<string, unknown>, jwt: string, timeoutMs = CLOUD_GRADE_TIMEOUT_MS): Promise<TaskDriftResult> {
1793
+ const sid = typeof envelope.sessionId === 'string' ? envelope.sessionId : undefined;
1794
+ const repo = typeof envelope.cwd === 'string' ? envelope.cwd : undefined;
1795
+ // What was being graded (the action), captured for the failure record so a dead call
1796
+ // still shows WHAT timed out. The fully-assembled grader prompt is captured separately
1797
+ // server-side (grader_failures.grade_input); this is the client's view.
1798
+ const gradeInput = scrubSecrets(JSON.stringify(envelope.payload ?? envelope)).slice(0, 6000);
1785
1799
  try {
1786
1800
  const body = JSON.stringify({ role: 'task-drift', payload: scrubSecrets(JSON.stringify(envelope)), content: '' });
1787
1801
  const resp = await fetch(CONTAINERS_URL + '/grade/submit', {
@@ -1790,10 +1804,32 @@ export async function cloudTaskDrift(envelope: Record<string, unknown>, jwt: str
1790
1804
  body,
1791
1805
  signal: AbortSignal.timeout(timeoutMs),
1792
1806
  });
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 ''; }
1807
+ if (!resp.ok) {
1808
+ // Non-2xx \u2014 record it (rule grades log the same way) so the task message
1809
+ // silently not firing is observable, not invisible. Fire-and-forget per
1810
+ // logGraderUnavailable's contract; still fail-OPEN.
1811
+ void logGraderUnavailable('taskGuard', 'task-drift', 'http ' + resp.status, gradeInput, { sessionId: sid, repo }).catch(() => {});
1812
+ return TASK_DRIFT_NONE;
1813
+ }
1814
+ const j = await resp.json() as { systemMessage?: unknown; hookSpecificOutput?: { additionalContext?: unknown; permissionDecision?: unknown } };
1815
+ const hso = j?.hookSpecificOutput || {};
1816
+ return {
1817
+ systemMessage: typeof j?.systemMessage === 'string' ? j.systemMessage : '',
1818
+ additionalContext: typeof hso.additionalContext === 'string' ? hso.additionalContext : '',
1819
+ decision: hso.permissionDecision === 'deny' ? 'deny' : 'allow',
1820
+ };
1821
+ } catch (e) {
1822
+ // Track the failure \u2014 ESP. the 45s timeout, the most likely reason a task message
1823
+ // doesn't fire. AbortSignal.timeout throws TimeoutError on expiry; everything else
1824
+ // (network, parse) carries its own message. Fire-and-forget (don't await \u2014 would
1825
+ // add ~3s on the timeout path and blow the watchdog); fail-open: never block.
1826
+ const err = e as { name?: string; message?: string };
1827
+ const reason = err?.name === 'TimeoutError'
1828
+ ? 'task-drift timed out after ' + timeoutMs + 'ms'
1829
+ : (err?.message || 'task-drift failed');
1830
+ void logGraderUnavailable('taskGuard', 'task-drift', reason, gradeInput, { sessionId: sid, repo }).catch(() => {});
1831
+ return TASK_DRIFT_NONE;
1832
+ }
1797
1833
  }
1798
1834
 
1799
1835
  // Option B: when a cloud grade fails (esp. a timeout), the REAL reason lives
@@ -4454,6 +4490,13 @@ async function main() {
4454
4490
  if (rt === 'local') {
4455
4491
  // \u2500\u2500\u2500 Local grading: org rules ONLY (channel 1, port 18929) \u2500\u2500\u2500
4456
4492
  const proposedShort = proposed.slice(0, 4000);
4493
+ // The TASK grade sees the DIFF for large files, so a contradiction or drift DEEP
4494
+ // in a big file is visible \u2014 the old "first 4KB of the whole file" window missed
4495
+ // any change past ~2KB. Small files keep full content (cleaner for the grader);
4496
+ // editDiff is the already-computed changed-lines summary (- old / + new).
4497
+ const taskGradeContent = (proposed.length <= 3500 || !editDiff)
4498
+ ? proposedShort
4499
+ : ('Changed lines of ' + filePath + ' (- old / + new):\\n' + editDiff).slice(0, 4000);
4457
4500
  const sessionLog = await sessionLogForGrade(sessionId, config.rules);
4458
4501
  const graderContent = 'file=' + filePath + ' content=' + proposedShort;
4459
4502
  const relevantRules = await filterRules(
@@ -4478,9 +4521,9 @@ async function main() {
4478
4521
  // message into the pass output. Fail-silent ('' on any issue). Sent as a
4479
4522
  // synthetic Write of the reconstructed content so the container needs no base
4480
4523
  // 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('');
4524
+ const taskDriftP = process.env.SYNKRO_DEPLOY_LOCATION === 'cloud'
4525
+ ? cloudTaskDrift({ payload: { tool_name: 'Write', tool_input: { file_path: filePath, content: taskGradeContent } }, harness: agentKind, cwd: gitRepo, sessionId }, jwt)
4526
+ : Promise.resolve({ systemMessage: '', additionalContext: '', decision: 'allow' as const });
4484
4527
 
4485
4528
  // \u2500\u2500\u2500 Combined org-rules + CWE in ONE inference (SYNKRO_COMBINED_EDIT_GRADE) \u2500\u2500\u2500
4486
4529
  // Self-contained early-return branch \u2014 the default two-grade path below is
@@ -4548,6 +4591,15 @@ async function main() {
4548
4591
  dispatchFinding(jwt, { session_id: sessionId, file_path: filePath, finding_type: 'cwe', finding_id: f.id, status: 'open' }, config.captureDepth);
4549
4592
  }
4550
4593
 
4594
+ // Resolve the parallel task grade NOW so a CONTRADICTION folds into the rule/CWE
4595
+ // block too \u2014 the agent gets rules + CWE + task drift in ONE deny instead of
4596
+ // discovering the contradiction a retry later. taskDriftP has been running in
4597
+ // parallel, so this rarely adds wall-clock. Only a contradiction (decision==='deny')
4598
+ // is folded into a block; progress/off-spec messages stay on the pass path below.
4599
+ const cTask = await taskDriftP;
4600
+ const cTaskSys = cTask.systemMessage;
4601
+ const taskDeny = cTask.decision === 'deny' ? (cTask.additionalContext || cTaskSys) : '';
4602
+
4551
4603
  // Org-rule violation \u2192 ask/fix enforcement (CWE findings appended to the deny).
4552
4604
  if (!ruleVerdict.ok) {
4553
4605
  const mode = normalizeMode(ruleVerdict.ruleMode || ruleMode(ruleVerdict.ruleId, config.rules));
@@ -4556,6 +4608,7 @@ async function main() {
4556
4608
  ? 'Guard: ' + guardReason + '\\nFix all issues before retrying. Do NOT ask the user to make the edit manually \u2014 resolve the violation in code yourself.'
4557
4609
  : 'Guard: ' + guardReason + '\\nAsk the user for explicit consent before retrying.';
4558
4610
  if (cweBlock) denyReason += '\\nAlso resolve these CWE weaknesses in code:\\n' + cweBlock;
4611
+ if (taskDeny) denyReason += '\\nThis edit ALSO contradicts the active task \u2014 fix that too:\\n' + taskDeny;
4559
4612
  dispatchCapture(jwt, 'edit', 'block', ruleVerdict.severity || 'critical', ruleVerdict.category || 'security',
4560
4613
  toolName, gitRepo, sessionId, config.captureDepth, {
4561
4614
  command: editContent, reasoning: guardReason,
@@ -4563,17 +4616,19 @@ async function main() {
4563
4616
  ccModel: captureModel, codeChange: editDiff, ...lineMetrics,
4564
4617
  });
4565
4618
  outputJson({
4566
- systemMessage: tagStr + ' editGuard ' + fileShort + ' \u2192 blocked: ' + guardReason + (cweFindings.length ? ' (+' + cweFindings.length + ' CWE)' : ''),
4619
+ systemMessage: tagStr + ' editGuard ' + fileShort + ' \u2192 blocked: ' + guardReason + (cweFindings.length ? ' (+' + cweFindings.length + ' CWE)' : '') + (taskDeny ? ' (+task)' : ''),
4567
4620
  hookSpecificOutput: { hookEventName: 'PreToolUse', permissionDecision: 'deny', permissionDecisionReason: denyReason, additionalContext: denyReason },
4568
4621
  });
4569
4622
  return;
4570
4623
  }
4571
4624
 
4572
- // Rules clean but CWE weakness(es) \u2192 fix-mode block (resolve in code).
4625
+ // Rules clean but CWE weakness(es) \u2192 fix-mode block (resolve in code; task drift
4626
+ // folded in too if this edit also contradicts the active task).
4573
4627
  if (cweFindings.length > 0) {
4574
- const ctx = 'CWE: ' + cweBlock + '\\nFix all issues before retrying. Do NOT ask the user to make the edit manually \u2014 resolve the weakness in code yourself.';
4628
+ let ctx = 'CWE: ' + cweBlock + '\\nFix all issues before retrying. Do NOT ask the user to make the edit manually \u2014 resolve the weakness in code yourself.';
4629
+ if (taskDeny) ctx += '\\nThis edit ALSO contradicts the active task \u2014 fix that too:\\n' + taskDeny;
4575
4630
  outputJson({
4576
- systemMessage: tagStr + ' editGuard ' + fileShort + ' \u2192 blocked (' + cweFindings.map(f => f.id).join(', ') + ')',
4631
+ systemMessage: tagStr + ' editGuard ' + fileShort + ' \u2192 blocked (' + cweFindings.map(f => f.id).join(', ') + ')' + (taskDeny ? ' (+task)' : ''),
4577
4632
  hookSpecificOutput: { hookEventName: 'PreToolUse', permissionDecision: 'deny', permissionDecisionReason: ctx, additionalContext: ctx },
4578
4633
  });
4579
4634
  return;
@@ -4587,8 +4642,15 @@ async function main() {
4587
4642
  ccModel: captureModel, codeChange: editDiff, ...lineMetrics,
4588
4643
  });
4589
4644
  const cPassLine = tagStr + ' editGuard ' + fileShort + ' \u2192 pass: ' + (ruleVerdict.reason || 'clean (rules + CWE)');
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') } });
4645
+ if (cTask.decision === 'deny') {
4646
+ // Cloud parity with local: a task CONTRADICTION hard-blocks the edit and
4647
+ // tells the AGENT (additionalContext) to fix it before proceeding.
4648
+ outputJson({ systemMessage: cPassLine + (cTaskSys ? '\\n' + cTaskSys : ''), hookSpecificOutput: { hookEventName: 'PreToolUse', permissionDecision: 'deny', permissionDecisionReason: cTask.additionalContext || cTaskSys, additionalContext: cTask.additionalContext || cTaskSys } });
4649
+ } else {
4650
+ // On-track / off-spec: pass, but pipe any task additionalContext to the AGENT
4651
+ // (not just systemMessage to the user) so drift/regression actually steers.
4652
+ outputJson({ systemMessage: cPassLine + (cTaskSys ? '\\n' + cTaskSys : ''), hookSpecificOutput: { hookEventName: 'PreToolUse', additionalContext: 'Synkro combined edit judge (rules + CWE). ' + (ruleVerdict.reason || 'clean') + (cTask.additionalContext ? '\\n[task] ' + cTask.additionalContext : '') } });
4653
+ }
4592
4654
  return;
4593
4655
  }
4594
4656
 
@@ -4634,8 +4696,13 @@ async function main() {
4634
4696
  ccModel: captureModel, codeChange: editDiff, ...lineMetrics,
4635
4697
  });
4636
4698
  const passLine = tagStr + ' editGuard ' + fileShort + ' \u2192 pass: ' + (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') } });
4699
+ const rTask = await taskDriftP;
4700
+ const rTaskSys = rTask.systemMessage;
4701
+ if (rTask.decision === 'deny') {
4702
+ outputJson({ systemMessage: passLine + (rTaskSys ? '\\n' + rTaskSys : ''), hookSpecificOutput: { hookEventName: 'PreToolUse', permissionDecision: 'deny', permissionDecisionReason: rTask.additionalContext || rTaskSys, additionalContext: rTask.additionalContext || rTaskSys } });
4703
+ } else {
4704
+ outputJson({ systemMessage: passLine + (rTaskSys ? '\\n' + rTaskSys : ''), hookSpecificOutput: { hookEventName: 'PreToolUse', additionalContext: 'Synkro local edit judge. ' + (verdict.reason || 'no policy violations detected') + (rTask.additionalContext ? '\\n[task] ' + rTask.additionalContext : '') } });
4705
+ }
4639
4706
  return;
4640
4707
  }
4641
4708
 
@@ -11299,7 +11366,7 @@ function writeConfigEnv(opts) {
11299
11366
  `SYNKRO_CREDENTIALS_PATH=${shellQuoteSingle(credsPath)}`,
11300
11367
  `SYNKRO_TIER=${shellQuoteSingle(safeTier)}`,
11301
11368
  `SYNKRO_INFERENCE=${shellQuoteSingle(safeInference)}`,
11302
- `SYNKRO_VERSION=${shellQuoteSingle("1.6.95")}`
11369
+ `SYNKRO_VERSION=${shellQuoteSingle("1.6.96")}`
11303
11370
  ];
11304
11371
  if (safeSynkroBin) lines.push(`SYNKRO_CLI_BIN=${shellQuoteSingle(safeSynkroBin)}`);
11305
11372
  if (safeUserId) lines.push(`SYNKRO_USER_ID=${shellQuoteSingle(safeUserId)}`);
@@ -15364,7 +15431,7 @@ var args = process.argv.slice(2);
15364
15431
  var cmd = args[0] || "";
15365
15432
  var subArgs = args.slice(1);
15366
15433
  function printVersion() {
15367
- console.log("1.6.95");
15434
+ console.log("1.6.96");
15368
15435
  }
15369
15436
  function printHelp2() {
15370
15437
  console.log(`Synkro CLI \u2014 runtime safety for AI coding agents