@synkro-sh/cli 1.6.82 → 1.6.83
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 +116 -30
- package/dist/bootstrap.js.map +1 -1
- package/package.json +1 -1
package/dist/bootstrap.js
CHANGED
|
@@ -1744,6 +1744,26 @@ async function hostedGrade(role: GradeRole, prompt: string, jwt: string, timeout
|
|
|
1744
1744
|
return String(data.result || '');
|
|
1745
1745
|
}
|
|
1746
1746
|
|
|
1747
|
+
// Option B: when a cloud grade fails (esp. a timeout), the REAL reason lives
|
|
1748
|
+
// inside the container, not on the wire \u2014 the client only ever sees "timed out".
|
|
1749
|
+
// Pull the org's per-worker claude/cursor sick reasons + log tails from the
|
|
1750
|
+
// hosted /debug endpoint so the diagnostic records WHY the grade actually died
|
|
1751
|
+
// (e.g. a claude 401 inside the worker). Best-effort + tightly timed; null if
|
|
1752
|
+
// the container is itself unreachable.
|
|
1753
|
+
async function fetchContainerSickReason(jwt: string, timeoutMs = 2500): Promise<string | null> {
|
|
1754
|
+
try {
|
|
1755
|
+
const r = await fetch(CONTAINERS_URL + '/debug', {
|
|
1756
|
+
headers: { Authorization: 'Bearer ' + jwt },
|
|
1757
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
1758
|
+
});
|
|
1759
|
+
if (!r.ok) return 'debug ' + r.status + ': ' + (await r.text().catch(() => '')).slice(0, 400);
|
|
1760
|
+
const data = await r.json().catch(() => null);
|
|
1761
|
+
return data ? JSON.stringify(data).slice(0, 8000) : null;
|
|
1762
|
+
} catch {
|
|
1763
|
+
return null;
|
|
1764
|
+
}
|
|
1765
|
+
}
|
|
1766
|
+
|
|
1747
1767
|
// Cloud transcript capture \u2014 the cloud mirror of the local /api/conversation-sync
|
|
1748
1768
|
// + /api/session-action telemetry. Posts to the API /api/v1/cli/sync-transcripts
|
|
1749
1769
|
// (the org is derived from the login JWT, same one cloud grading uses). Best-effort:
|
|
@@ -3749,35 +3769,99 @@ export function isGraderNotConfigured(errorMessage: string): boolean {
|
|
|
3749
3769
|
return lower.includes('no worker') || lower.includes('not configured') || lower.includes('agent_kind') || lower.includes('no pool');
|
|
3750
3770
|
}
|
|
3751
3771
|
|
|
3752
|
-
|
|
3772
|
+
// Categorize a grader failure into a stable machine category + a human "why"
|
|
3773
|
+
// (with a fix hint). Shared by the terminal message and the diagnostic log so
|
|
3774
|
+
// the two never disagree about the reason.
|
|
3775
|
+
export function classifyGraderError(errorMessage: string): { category: string; why: string } {
|
|
3776
|
+
const e = (errorMessage || '').toLowerCase();
|
|
3777
|
+
const snippet = (errorMessage || '').replace(/\\s+/g, ' ').trim().slice(0, 160);
|
|
3753
3778
|
if (errorMessage === 'SYNKRO_CHANNEL_DOWN') {
|
|
3754
|
-
return
|
|
3779
|
+
return { category: 'channel_down', why: 'grader container not running (run \`synkro status\`)' };
|
|
3755
3780
|
}
|
|
3756
3781
|
if (isGraderNotConfigured(errorMessage)) {
|
|
3757
|
-
|
|
3758
|
-
|
|
3782
|
+
return { category: 'not_configured', why: 'grader not configured for this agent \u2014 add it to synkro.toml and run \`synkro install\`' };
|
|
3783
|
+
}
|
|
3784
|
+
if (/tim(e|ed)\\s*out|aborted|operation was aborted/.test(e)) {
|
|
3785
|
+
return { category: 'timeout', why: 'grade timed out (45s) \u2014 grader cold/slow, or its Claude token is invalid (re-run \`synkro install\`)' };
|
|
3786
|
+
}
|
|
3787
|
+
if (/\\b401\\b|unauthorized|token expired|invalid token|\\b403\\b|forbidden/.test(e)) {
|
|
3788
|
+
return { category: 'auth', why: 'auth rejected \u2014 session token expired (re-run \`synkro login\`, or \`synkro install\` if the grader Claude token is stale)' };
|
|
3789
|
+
}
|
|
3790
|
+
if (/econnrefused|enotfound|eai_again|fetch failed|socket|econnreset|getaddrinfo|network/.test(e)) {
|
|
3791
|
+
return { category: 'unreachable', why: 'grader unreachable \u2014 network or container down' };
|
|
3759
3792
|
}
|
|
3760
|
-
|
|
3761
|
-
|
|
3762
|
-
|
|
3793
|
+
if (/\\b5\\d\\d\\b/.test(e)) {
|
|
3794
|
+
return { category: 'server_error', why: 'grader server error: ' + snippet };
|
|
3795
|
+
}
|
|
3796
|
+
return { category: 'unknown', why: snippet || 'unknown error' };
|
|
3797
|
+
}
|
|
3798
|
+
|
|
3799
|
+
export function graderUnavailableMessage(hook: string, target: string, errorMessage: string, agentKind: AgentKind = 'claude_code'): string {
|
|
3800
|
+
const base = hook + ' ' + target + ' \u2192 ';
|
|
3801
|
+
if (isGraderNotConfigured(errorMessage)) {
|
|
3802
|
+
const agent = agentKind === 'cursor' ? 'Cursor' : 'Claude Code';
|
|
3803
|
+
return base + 'local grader not configured for ' + agent + '. Add this agent to your synkro.toml file and run \`synkro install\`.';
|
|
3804
|
+
}
|
|
3805
|
+
// Always surface WHY (not just "unavailable"), categorized, with a fix hint \u2014
|
|
3806
|
+
// the full raw error + the exact graded prompt are in grader-unavailable.log.
|
|
3807
|
+
const where = process.env.SYNKRO_DEPLOY_LOCATION === 'cloud' ? 'cloud grader' : 'local grader';
|
|
3808
|
+
const { why } = classifyGraderError(errorMessage);
|
|
3809
|
+
return base + where + ' unavailable \u2014 ' + why + '; skipped (full detail: ~/.synkro/grader-unavailable.log)';
|
|
3810
|
+
}
|
|
3811
|
+
|
|
3812
|
+
// Diagnostic record for a skipped grade \u2014 the categorized reason AND the EXACT
|
|
3813
|
+
// prompt that was sent for inference (gradeInput), so you can see WHAT was being
|
|
3814
|
+
// graded when it failed. Routing differs by mode:
|
|
3815
|
+
// \u2022 CLOUD \u2192 ship to Timescale via /api/v1/cli/grader-unavailable (central, so
|
|
3816
|
+
// we can debug customer failures), enriched on timeout/outage with the
|
|
3817
|
+
// container's own claude error (option B). NOT written to the user's machine.
|
|
3818
|
+
// \u2022 LOCAL \u2192 the on-device ~/.synkro/grader-unavailable.log file.
|
|
3819
|
+
// Fire-and-forget in cloud mode: callers don't await \u2014 the pending fetch keeps
|
|
3820
|
+
// the hook process alive until it settles (or the watchdog ends the run).
|
|
3821
|
+
export async function logGraderUnavailable(
|
|
3822
|
+
hook: string, target: string, errorMessage: string,
|
|
3823
|
+
gradeInput?: string, ctx?: { sessionId?: string; repo?: string },
|
|
3824
|
+
): Promise<void> {
|
|
3825
|
+
const { category, why } = classifyGraderError(errorMessage);
|
|
3826
|
+
|
|
3763
3827
|
if (process.env.SYNKRO_DEPLOY_LOCATION === 'cloud') {
|
|
3764
|
-
|
|
3765
|
-
|
|
3766
|
-
|
|
3828
|
+
try {
|
|
3829
|
+
const jwt = loadJwt();
|
|
3830
|
+
if (!jwt) return;
|
|
3831
|
+
// Only chase the container's internal reason for failures where it adds
|
|
3832
|
+
// signal (a timeout/outage hides the real claude error); skip it for fast,
|
|
3833
|
+
// already-explanatory failures like auth.
|
|
3834
|
+
let sickReason: string | null = null;
|
|
3835
|
+
if (category === 'timeout' || category === 'unreachable' || category === 'server_error') {
|
|
3836
|
+
sickReason = await fetchContainerSickReason(jwt);
|
|
3837
|
+
}
|
|
3838
|
+
await fetch(GATEWAY_URL + '/api/v1/cli/grader-unavailable', {
|
|
3839
|
+
method: 'POST',
|
|
3840
|
+
headers: { 'Content-Type': 'application/json', Authorization: 'Bearer ' + jwt },
|
|
3841
|
+
body: JSON.stringify({
|
|
3842
|
+
hook, target, category, why,
|
|
3843
|
+
error: (errorMessage || '').slice(0, 2000),
|
|
3844
|
+
grade_input: gradeInput ? String(gradeInput).slice(0, 20000) : null,
|
|
3845
|
+
sick_reason: sickReason,
|
|
3846
|
+
session_id: ctx?.sessionId || null,
|
|
3847
|
+
repo: ctx?.repo || null,
|
|
3848
|
+
}),
|
|
3849
|
+
signal: AbortSignal.timeout(3000),
|
|
3850
|
+
}).catch(() => {});
|
|
3851
|
+
} catch {
|
|
3852
|
+
// best-effort \u2014 never let diagnostics cascade into a hook failure
|
|
3767
3853
|
}
|
|
3768
|
-
return
|
|
3854
|
+
return;
|
|
3769
3855
|
}
|
|
3770
|
-
return hook + ' ' + target + ' \u2192 local grader unavailable, skipped';
|
|
3771
|
-
}
|
|
3772
3856
|
|
|
3773
|
-
|
|
3857
|
+
// Local mode: on-device diagnostic log.
|
|
3774
3858
|
try {
|
|
3775
|
-
const entry = {
|
|
3776
|
-
ts: new Date().toISOString(),
|
|
3777
|
-
hook,
|
|
3778
|
-
|
|
3779
|
-
error: errorMessage.slice(0, 500),
|
|
3859
|
+
const entry: Record<string, unknown> = {
|
|
3860
|
+
ts: new Date().toISOString(), mode: 'local',
|
|
3861
|
+
hook, target, category, why,
|
|
3862
|
+
error: (errorMessage || '').slice(0, 1000),
|
|
3780
3863
|
};
|
|
3864
|
+
if (gradeInput) entry.gradeInput = String(gradeInput).slice(0, 20000);
|
|
3781
3865
|
appendFileSync(UNAVAIL_LOG, JSON.stringify(entry) + '\\n', 'utf-8');
|
|
3782
3866
|
} catch {
|
|
3783
3867
|
// best-effort \u2014 never let logging failure cascade into a hook failure
|
|
@@ -4037,7 +4121,7 @@ async function main() {
|
|
|
4037
4121
|
}
|
|
4038
4122
|
} catch (err) {
|
|
4039
4123
|
const errMsg = (err as Error).message || String(err);
|
|
4040
|
-
logGraderUnavailable('editGuard', fileShort, errMsg);
|
|
4124
|
+
logGraderUnavailable('editGuard', fileShort, errMsg, buildCombined(proposed), { sessionId, repo: gitRepo });
|
|
4041
4125
|
outputJson({ systemMessage: tagStr + ' ' + graderUnavailableMessage('editGuard', fileShort, errMsg, graderPool) });
|
|
4042
4126
|
return;
|
|
4043
4127
|
}
|
|
@@ -4111,7 +4195,7 @@ async function main() {
|
|
|
4111
4195
|
gradeResp = await localGrade('edit', graderPrompt, undefined, graderPool);
|
|
4112
4196
|
} catch (err) {
|
|
4113
4197
|
const errMsg = (err as Error).message || String(err);
|
|
4114
|
-
logGraderUnavailable('editGuard', fileShort, errMsg);
|
|
4198
|
+
logGraderUnavailable('editGuard', fileShort, errMsg, graderPrompt, { sessionId, repo: gitRepo });
|
|
4115
4199
|
outputJson({ systemMessage: tagStr + ' ' + graderUnavailableMessage('editGuard', fileShort, errMsg, graderPool) });
|
|
4116
4200
|
return;
|
|
4117
4201
|
}
|
|
@@ -4678,7 +4762,7 @@ async function main() {
|
|
|
4678
4762
|
gradeResponses = [resp1, resp2];
|
|
4679
4763
|
} catch (gradeErr: any) {
|
|
4680
4764
|
const reason = gradeErr?.message || String(gradeErr);
|
|
4681
|
-
logGraderUnavailable('cweGuard', fileShort, reason);
|
|
4765
|
+
logGraderUnavailable('cweGuard', fileShort, reason, buildCwePrompt(cweContent), { sessionId, repo: gitRepo });
|
|
4682
4766
|
outputJson({ systemMessage: cweTag + ' ' + graderUnavailableMessage('cweGuard', fileShort, reason, graderPool) });
|
|
4683
4767
|
return;
|
|
4684
4768
|
}
|
|
@@ -4687,7 +4771,7 @@ async function main() {
|
|
|
4687
4771
|
gradeResponses = [await localGradeCwe(buildCwePrompt(cweContent), graderPool)];
|
|
4688
4772
|
} catch (gradeErr: any) {
|
|
4689
4773
|
const reason = gradeErr?.message || String(gradeErr);
|
|
4690
|
-
logGraderUnavailable('cweGuard', fileShort, reason);
|
|
4774
|
+
logGraderUnavailable('cweGuard', fileShort, reason, buildCwePrompt(cweContent), { sessionId, repo: gitRepo });
|
|
4691
4775
|
outputJson({ systemMessage: cweTag + ' ' + graderUnavailableMessage('cweGuard', fileShort, reason, graderPool) });
|
|
4692
4776
|
return;
|
|
4693
4777
|
}
|
|
@@ -5435,7 +5519,7 @@ async function main() {
|
|
|
5435
5519
|
gradeResp = await localGrade('bash', graderPrompt, undefined, graderPool);
|
|
5436
5520
|
} catch (err) {
|
|
5437
5521
|
const errMsg = (err as Error).message || String(err);
|
|
5438
|
-
logGraderUnavailable('bashGuard', toolInput.command?.slice(0, 200) || '', errMsg);
|
|
5522
|
+
logGraderUnavailable('bashGuard', toolInput.command?.slice(0, 200) || '', errMsg, graderPrompt, { sessionId, repo: gitRepo });
|
|
5439
5523
|
if (scanConcern) {
|
|
5440
5524
|
const ctx = scanBlockContext + ' Synkro flagged this install but the grader is unavailable to check consent — ask the user for explicit consent, then retry.';
|
|
5441
5525
|
outputJson({
|
|
@@ -5653,7 +5737,7 @@ async function main() {
|
|
|
5653
5737
|
gradeResp = await localGrade('bash', graderPrompt, undefined, graderPool);
|
|
5654
5738
|
} catch (err) {
|
|
5655
5739
|
const errMsg = (err as Error).message || String(err);
|
|
5656
|
-
logGraderUnavailable('agentGuard', subagentType || (description || '').slice(0, 100), errMsg);
|
|
5740
|
+
logGraderUnavailable('agentGuard', subagentType || (description || '').slice(0, 100), errMsg, graderPrompt, { sessionId, repo: gitRepo });
|
|
5657
5741
|
outputJson({ systemMessage: tagStr + ' ' + graderUnavailableMessage('agentGuard', subagentType || 'agent', errMsg, graderPool) });
|
|
5658
5742
|
return;
|
|
5659
5743
|
}
|
|
@@ -5744,7 +5828,7 @@ import {
|
|
|
5744
5828
|
loadJwt, ensureFreshJwt, detectRepo, loadConfig, route, tag, localGrade,
|
|
5745
5829
|
parseVerdict, dispatchCapture, appendSessionAction, readSessionLog, compressSessionLog, postWithRetry, readStdin, log,
|
|
5746
5830
|
outputJson, outputEmpty, setupCursorHookSignals, installHookWatchdog, isPlanTool, hookSessionId, GATEWAY_URL,
|
|
5747
|
-
filterRules, graderUnavailableMessage, resolveTranscriptPath, isCursorInvokingCcHook,
|
|
5831
|
+
filterRules, graderUnavailableMessage, logGraderUnavailable, resolveTranscriptPath, isCursorInvokingCcHook,
|
|
5748
5832
|
loadSynkroFile, effectiveGraderPool, synkroFilePresent, noSynkroSkipMessage,
|
|
5749
5833
|
} from './_synkro-common.ts';
|
|
5750
5834
|
import { existsSync, readFileSync, writeFileSync, readdirSync, statSync } from 'node:fs';
|
|
@@ -5864,7 +5948,9 @@ async function main() {
|
|
|
5864
5948
|
try {
|
|
5865
5949
|
gradeResp = await localGrade('plan', graderPrompt, undefined, graderPool);
|
|
5866
5950
|
} catch (err) {
|
|
5867
|
-
|
|
5951
|
+
const errMsg = (err as Error).message || String(err);
|
|
5952
|
+
logGraderUnavailable('planReview', 'plan', errMsg, graderPrompt, { sessionId, repo: gitRepo });
|
|
5953
|
+
outputJson({ systemMessage: tagStr + ' ' + graderUnavailableMessage('planReview', 'plan', errMsg, graderPool) });
|
|
5868
5954
|
return;
|
|
5869
5955
|
}
|
|
5870
5956
|
|
|
@@ -6478,7 +6564,7 @@ async function main() {
|
|
|
6478
6564
|
try {
|
|
6479
6565
|
gradeResp = await localGrade('bash', graderPrompt, CURSOR_GRADE_TIMEOUT_MS, graderPool);
|
|
6480
6566
|
} catch (e) {
|
|
6481
|
-
logGraderUnavailable('bashGuard', command.slice(0, 200), (e as Error).message || String(e));
|
|
6567
|
+
logGraderUnavailable('bashGuard', command.slice(0, 200), (e as Error).message || String(e), graderPrompt, { sessionId, repo });
|
|
6482
6568
|
if (scanConcern) {
|
|
6483
6569
|
// Grader unavailable to run the consent check \u2014 fail closed on a
|
|
6484
6570
|
// scanner-flagged install (ask-mode so the user can still consent).
|
|
@@ -10753,7 +10839,7 @@ function writeConfigEnv(opts) {
|
|
|
10753
10839
|
`SYNKRO_CREDENTIALS_PATH=${shellQuoteSingle(credsPath)}`,
|
|
10754
10840
|
`SYNKRO_TIER=${shellQuoteSingle(safeTier)}`,
|
|
10755
10841
|
`SYNKRO_INFERENCE=${shellQuoteSingle(safeInference)}`,
|
|
10756
|
-
`SYNKRO_VERSION=${shellQuoteSingle("1.6.
|
|
10842
|
+
`SYNKRO_VERSION=${shellQuoteSingle("1.6.83")}`
|
|
10757
10843
|
];
|
|
10758
10844
|
if (safeSynkroBin) lines.push(`SYNKRO_CLI_BIN=${shellQuoteSingle(safeSynkroBin)}`);
|
|
10759
10845
|
if (safeUserId) lines.push(`SYNKRO_USER_ID=${shellQuoteSingle(safeUserId)}`);
|
|
@@ -14614,7 +14700,7 @@ var args = process.argv.slice(2);
|
|
|
14614
14700
|
var cmd = args[0] || "";
|
|
14615
14701
|
var subArgs = args.slice(1);
|
|
14616
14702
|
function printVersion() {
|
|
14617
|
-
console.log("1.6.
|
|
14703
|
+
console.log("1.6.83");
|
|
14618
14704
|
}
|
|
14619
14705
|
function printHelp2() {
|
|
14620
14706
|
console.log(`Synkro CLI \u2014 runtime safety for AI coding agents
|