@synkro-sh/cli 1.6.72 → 1.6.74
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 +227 -37
- package/dist/bootstrap.js.map +1 -1
- package/package.json +1 -1
package/dist/bootstrap.js
CHANGED
|
@@ -145,7 +145,7 @@ function installCCHooks(settingsPath, config) {
|
|
|
145
145
|
{
|
|
146
146
|
type: "command",
|
|
147
147
|
command: config.bashJudgeScriptPath,
|
|
148
|
-
timeout:
|
|
148
|
+
timeout: 50
|
|
149
149
|
}
|
|
150
150
|
],
|
|
151
151
|
[SYNKRO_MARKER]: true
|
|
@@ -156,12 +156,12 @@ function installCCHooks(settingsPath, config) {
|
|
|
156
156
|
{
|
|
157
157
|
type: "command",
|
|
158
158
|
command: config.editPrecheckScriptPath,
|
|
159
|
-
timeout:
|
|
159
|
+
timeout: 50
|
|
160
160
|
},
|
|
161
161
|
{
|
|
162
162
|
type: "command",
|
|
163
163
|
command: config.cwePrecheckScriptPath,
|
|
164
|
-
timeout:
|
|
164
|
+
timeout: 50
|
|
165
165
|
},
|
|
166
166
|
{
|
|
167
167
|
type: "command",
|
|
@@ -177,7 +177,7 @@ function installCCHooks(settingsPath, config) {
|
|
|
177
177
|
{
|
|
178
178
|
type: "command",
|
|
179
179
|
command: config.agentJudgeScriptPath,
|
|
180
|
-
timeout:
|
|
180
|
+
timeout: 50
|
|
181
181
|
}
|
|
182
182
|
],
|
|
183
183
|
[SYNKRO_MARKER]: true
|
|
@@ -188,7 +188,7 @@ function installCCHooks(settingsPath, config) {
|
|
|
188
188
|
{
|
|
189
189
|
type: "command",
|
|
190
190
|
command: config.planJudgeScriptPath,
|
|
191
|
-
timeout:
|
|
191
|
+
timeout: 50
|
|
192
192
|
}
|
|
193
193
|
],
|
|
194
194
|
[SYNKRO_MARKER]: true
|
|
@@ -200,7 +200,7 @@ function installCCHooks(settingsPath, config) {
|
|
|
200
200
|
{
|
|
201
201
|
type: "command",
|
|
202
202
|
command: config.mcpGateScriptPath,
|
|
203
|
-
timeout:
|
|
203
|
+
timeout: 50
|
|
204
204
|
}
|
|
205
205
|
],
|
|
206
206
|
[SYNKRO_MARKER]: true
|
|
@@ -1272,7 +1272,7 @@ export function findGitRoot(cwd: string): string {
|
|
|
1272
1272
|
export interface SynkroFileConfig {
|
|
1273
1273
|
version: number;
|
|
1274
1274
|
harness: ('claude-code' | 'cursor')[];
|
|
1275
|
-
grader: { pool: 'auto' | 'claude' | 'cursor'; mode?: string };
|
|
1275
|
+
grader: { pool: 'auto' | 'claude' | 'cursor'; mode?: string; combinedEditGrade?: boolean };
|
|
1276
1276
|
workers: { claude?: number; cursor?: number };
|
|
1277
1277
|
ruleset: string;
|
|
1278
1278
|
skills: string[];
|
|
@@ -1379,6 +1379,7 @@ export function loadSynkroFile(cwd?: string): SynkroFileConfig {
|
|
|
1379
1379
|
grader: {
|
|
1380
1380
|
pool: normalizePoolToken(parsed.grader?.pool),
|
|
1381
1381
|
mode: ['local', 'byok'].includes(parsed.grader?.mode) ? parsed.grader.mode : undefined,
|
|
1382
|
+
combinedEditGrade: parsed.grader?.combined_edit_grade === true,
|
|
1382
1383
|
},
|
|
1383
1384
|
workers: {
|
|
1384
1385
|
// Counts may live under the grader block (unified form) or the legacy
|
|
@@ -1632,7 +1633,7 @@ export function tag(rt: string, config: HookConfig, grader?: string): string {
|
|
|
1632
1633
|
|
|
1633
1634
|
// \u2500\u2500\u2500 Local Grading (direct channel call) \u2500\u2500\u2500
|
|
1634
1635
|
|
|
1635
|
-
type GradeRole = 'grade-edit' | 'grade-bash' | 'grade-plan' | 'grade-cwe';
|
|
1636
|
+
type GradeRole = 'grade-edit' | 'grade-bash' | 'grade-plan' | 'grade-cwe' | 'grade-edit-cwe';
|
|
1636
1637
|
|
|
1637
1638
|
// Which coding agent fired this grade. The dispatcher routes grades to a
|
|
1638
1639
|
// worker pool of the matching kind so a Cursor grade is judged by Cursor and
|
|
@@ -1642,8 +1643,21 @@ export type AgentKind = 'claude_code' | 'cursor';
|
|
|
1642
1643
|
|
|
1643
1644
|
const ROLE_MAP: Record<string, GradeRole> = {
|
|
1644
1645
|
edit: 'grade-edit', bash: 'grade-bash', plan: 'grade-plan', cwe: 'grade-cwe',
|
|
1646
|
+
'edit-cwe': 'grade-edit-cwe',
|
|
1645
1647
|
};
|
|
1646
1648
|
|
|
1649
|
+
// Combined edit-time grade (org rules + CWE in one inference). OFF by default;
|
|
1650
|
+
// flipped on by env var or synkro.toml [grader] combined_edit_grade = true.
|
|
1651
|
+
// When on: the edit-precheck hook runs ONE grade-edit-cwe call and the
|
|
1652
|
+
// cwe-precheck hook no-ops (the combined call owns CWE) \u2014 halving inferences
|
|
1653
|
+
// per edit. See GRADER_PRIMER_EDIT_CWE.
|
|
1654
|
+
export function combinedEditGradeEnabled(synkroFile?: SynkroFileConfig): boolean {
|
|
1655
|
+
const env = process.env.SYNKRO_COMBINED_EDIT_GRADE;
|
|
1656
|
+
if (env === '1' || env === 'true') return true;
|
|
1657
|
+
if (env === '0' || env === 'false') return false;
|
|
1658
|
+
return synkroFile?.grader?.combinedEditGrade === true;
|
|
1659
|
+
}
|
|
1660
|
+
|
|
1647
1661
|
async function channelGrade(role: GradeRole, prompt: string, _jwt: string, port: number, timeoutMs = 30000, agentKind: AgentKind = 'claude_code'): Promise<string> {
|
|
1648
1662
|
const body = JSON.stringify({ role, payload: prompt, content: prompt, agent_kind: agentKind });
|
|
1649
1663
|
|
|
@@ -1689,7 +1703,12 @@ async function cloudGrade(surface: string, prompt: string, jwt: string, timeoutM
|
|
|
1689
1703
|
// JWT (the Worker derives the org from the token and routes to that org's
|
|
1690
1704
|
// container). Any non-2xx throws so the caller's catch fails open.
|
|
1691
1705
|
const CONTAINERS_URL = process.env.SYNKRO_CONTAINERS_URL || 'https://containers.synkro.sh';
|
|
1692
|
-
|
|
1706
|
+
// Cloud grades cross the network and can hit a cold/queued worker, so they get a
|
|
1707
|
+
// longer leash than on-device channel grades. The hook chain is sized around it:
|
|
1708
|
+
// grade abort (45s) < watchdog (48s) < CC hook budget (50s) \u2014 the grade aborts
|
|
1709
|
+
// FIRST so the caller's catch fails open cleanly before CC ever force-kills us.
|
|
1710
|
+
const CLOUD_GRADE_TIMEOUT_MS = 45000;
|
|
1711
|
+
async function hostedGrade(role: GradeRole, prompt: string, jwt: string, timeoutMs = CLOUD_GRADE_TIMEOUT_MS, agentKind: AgentKind = 'claude_code'): Promise<string> {
|
|
1693
1712
|
const resp = await fetch(CONTAINERS_URL + '/grade/submit', {
|
|
1694
1713
|
method: 'POST',
|
|
1695
1714
|
headers: { 'Content-Type': 'application/json', Authorization: 'Bearer ' + jwt },
|
|
@@ -1705,18 +1724,20 @@ async function hostedGrade(role: GradeRole, prompt: string, jwt: string, timeout
|
|
|
1705
1724
|
return String(data.result || '');
|
|
1706
1725
|
}
|
|
1707
1726
|
|
|
1708
|
-
//
|
|
1709
|
-
// ccHookConfig.ts) so the AbortSignal fires and the caller's
|
|
1710
|
-
//
|
|
1711
|
-
//
|
|
1712
|
-
// prompt as a hook error instead of a clean pass.
|
|
1727
|
+
// Local-channel default 24s \u2014 MUST stay below the CC hook timeout (50s for
|
|
1728
|
+
// edit/bash/cwe in ccHookConfig.ts) so the AbortSignal fires and the caller's
|
|
1729
|
+
// catch fails open cleanly. A hung on-device worker should fail open fast; only
|
|
1730
|
+
// cloud grades (network + cold workers) get the longer 45s leash below.
|
|
1713
1731
|
export async function localGrade(surface: string, prompt: string, timeoutMs = 24000, agentKind: AgentKind = 'claude_code'): Promise<string> {
|
|
1714
1732
|
const jwt = loadJwt();
|
|
1715
1733
|
if (!jwt) throw new Error('NO_JWT');
|
|
1716
1734
|
// Cloud-container: the grader runs on Cloudflare Containers \u2014 route to the
|
|
1717
1735
|
// hosted ingress instead of the local channel. Same prompt + parseVerdict.
|
|
1736
|
+
// Cloud grades get the full 45s (not the local 24s): the round-trip + a cold
|
|
1737
|
+
// worker routinely needs >24s, which is exactly what was timing out and
|
|
1738
|
+
// failing open on big bash prompts.
|
|
1718
1739
|
if (process.env.SYNKRO_DEPLOY_LOCATION === 'cloud') {
|
|
1719
|
-
return hostedGrade(ROLE_MAP[surface] || 'grade-edit', prompt, jwt,
|
|
1740
|
+
return hostedGrade(ROLE_MAP[surface] || 'grade-edit', prompt, jwt, CLOUD_GRADE_TIMEOUT_MS, agentKind);
|
|
1720
1741
|
}
|
|
1721
1742
|
// BYOK grading mode routes the grade through an LLM API instead of the
|
|
1722
1743
|
// on-device channel worker pool. The grader prompt + parseVerdict are shared.
|
|
@@ -1727,14 +1748,14 @@ export async function localGrade(surface: string, prompt: string, timeoutMs = 24
|
|
|
1727
1748
|
return channelGrade(ROLE_MAP[surface] || 'grade-edit', prompt, jwt, 18929, timeoutMs, agentKind);
|
|
1728
1749
|
}
|
|
1729
1750
|
|
|
1730
|
-
//
|
|
1731
|
-
//
|
|
1732
|
-
//
|
|
1751
|
+
// Local-channel default 24s. Two cwe chunks grade in parallel within the same CC
|
|
1752
|
+
// budget (now 50s), so each must finish under it. Cloud cwe grades take the 45s
|
|
1753
|
+
// hosted leash (parallel chunks \u2192 wall-clock ~45s, still inside the 50s budget).
|
|
1733
1754
|
export async function localGradeCwe(prompt: string, agentKind: AgentKind = 'claude_code', timeoutMs = 24000): Promise<string> {
|
|
1734
1755
|
const jwt = loadJwt();
|
|
1735
1756
|
if (!jwt) throw new Error('NO_JWT');
|
|
1736
1757
|
if (process.env.SYNKRO_DEPLOY_LOCATION === 'cloud') {
|
|
1737
|
-
return hostedGrade('grade-cwe', prompt, jwt,
|
|
1758
|
+
return hostedGrade('grade-cwe', prompt, jwt, CLOUD_GRADE_TIMEOUT_MS, agentKind);
|
|
1738
1759
|
}
|
|
1739
1760
|
return channelGrade('grade-cwe', prompt, jwt, 18930, timeoutMs, agentKind);
|
|
1740
1761
|
}
|
|
@@ -2181,6 +2202,67 @@ export function parseVerdict(resp: string): Verdict {
|
|
|
2181
2202
|
return verdict;
|
|
2182
2203
|
}
|
|
2183
2204
|
|
|
2205
|
+
export interface CombinedFinding {
|
|
2206
|
+
id: string;
|
|
2207
|
+
reason: string;
|
|
2208
|
+
suggestedFix: string;
|
|
2209
|
+
severity: string;
|
|
2210
|
+
ruleMode: string;
|
|
2211
|
+
codeSnippet: string;
|
|
2212
|
+
}
|
|
2213
|
+
|
|
2214
|
+
// Splits a grade-edit-cwe verdict (one <synkro-verdict> whose <violation> blocks
|
|
2215
|
+
// MIX org-rule ids and cwe-### ids) into (a) a rules Verdict the existing rule
|
|
2216
|
+
// enforcement consumes verbatim and (b) a list of CWE findings for the CWE block
|
|
2217
|
+
// path. A block whose rule_id starts with "cwe-" is a CWE finding; anything else
|
|
2218
|
+
// is an org-rule violation (org rules take precedence). Org-rule verdict is ok
|
|
2219
|
+
// UNLESS a non-cwe violation is present \u2014 so a CWE-only failure still passes the
|
|
2220
|
+
// rule gate while blocking on CWE.
|
|
2221
|
+
export function parseCombinedVerdict(resp: string): { ruleVerdict: Verdict; cweFindings: CombinedFinding[] } {
|
|
2222
|
+
const base = parseVerdict(resp);
|
|
2223
|
+
const ruleVerdict: Verdict = { ok: true, reason: '', suggestedFix: '', ruleId: '', ruleMode: '', severity: 'low', category: 'clean' };
|
|
2224
|
+
const cweFindings: CombinedFinding[] = [];
|
|
2225
|
+
const blocks = resp.match(/<violation>[\\s\\S]*?<\\/violation>/g) || [];
|
|
2226
|
+
let firstRule: CombinedFinding | null = null;
|
|
2227
|
+
for (const b of blocks) {
|
|
2228
|
+
const idRaw = (b.match(/<rule_id>([^<]+)<\\/rule_id>/) || [])[1];
|
|
2229
|
+
if (!idRaw) continue;
|
|
2230
|
+
const id = idRaw.trim();
|
|
2231
|
+
const finding: CombinedFinding = {
|
|
2232
|
+
id,
|
|
2233
|
+
reason: ((b.match(/<reason>([\\s\\S]*?)<\\/reason>/) || [])[1] || '').trim(),
|
|
2234
|
+
suggestedFix: ((b.match(/<suggested_fix>([\\s\\S]*?)<\\/suggested_fix>/) || [])[1] || '').trim(),
|
|
2235
|
+
severity: ((b.match(/<severity>([^<]+)<\\/severity>/) || [])[1] || '').trim(),
|
|
2236
|
+
ruleMode: ((b.match(/<rule_mode>([^<]+)<\\/rule_mode>/) || [])[1] || '').trim(),
|
|
2237
|
+
codeSnippet: ((b.match(/<code_snippet>([\\s\\S]*?)<\\/code_snippet>/) || [])[1] || '').replace(/^\\n+|\\n+$/g, ''),
|
|
2238
|
+
};
|
|
2239
|
+
if (/^cwe-/i.test(id)) {
|
|
2240
|
+
finding.id = id.replace(/^cwe-/i, 'CWE-');
|
|
2241
|
+
if (!cweFindings.some(f => f.id === finding.id)) cweFindings.push(finding);
|
|
2242
|
+
} else if (!firstRule) {
|
|
2243
|
+
firstRule = finding;
|
|
2244
|
+
}
|
|
2245
|
+
}
|
|
2246
|
+
if (firstRule) {
|
|
2247
|
+
ruleVerdict.ok = false;
|
|
2248
|
+
ruleVerdict.ruleId = firstRule.id;
|
|
2249
|
+
ruleVerdict.reason = firstRule.reason;
|
|
2250
|
+
ruleVerdict.suggestedFix = firstRule.suggestedFix;
|
|
2251
|
+
ruleVerdict.ruleMode = firstRule.ruleMode;
|
|
2252
|
+
ruleVerdict.severity = firstRule.severity || 'high';
|
|
2253
|
+
ruleVerdict.category = base.category || 'uncategorized';
|
|
2254
|
+
} else if (base.ok === false && base.ruleId && !/^cwe-/i.test(base.ruleId)) {
|
|
2255
|
+
// No <violation> blocks but a top-level rule_id failure (defensive).
|
|
2256
|
+
ruleVerdict.ok = false;
|
|
2257
|
+
ruleVerdict.ruleId = base.ruleId;
|
|
2258
|
+
ruleVerdict.reason = base.reason;
|
|
2259
|
+
ruleVerdict.suggestedFix = base.suggestedFix;
|
|
2260
|
+
ruleVerdict.ruleMode = base.ruleMode;
|
|
2261
|
+
ruleVerdict.severity = base.severity || 'high';
|
|
2262
|
+
}
|
|
2263
|
+
return { ruleVerdict, cweFindings };
|
|
2264
|
+
}
|
|
2265
|
+
|
|
2184
2266
|
// \u2500\u2500\u2500 Telemetry Dispatch \u2500\u2500\u2500
|
|
2185
2267
|
|
|
2186
2268
|
// Gated cloud telemetry POST \u2014 fires only when storage mode is 'cloud'. The
|
|
@@ -3540,6 +3622,16 @@ export function graderUnavailableMessage(hook: string, target: string, errorMess
|
|
|
3540
3622
|
const agent = agentKind === 'cursor' ? 'Cursor' : 'Claude Code';
|
|
3541
3623
|
return hook + ' ' + target + ' \u2192 local grader not configured for ' + agent + '. Add this agent to your synkro.toml file and run \`synkro install\`.';
|
|
3542
3624
|
}
|
|
3625
|
+
// In cloud-container mode the grade ran against the hosted ingress, NOT a local
|
|
3626
|
+
// worker \u2014 say so, and distinguish a timeout (slow/cold grade) from a true
|
|
3627
|
+
// outage so "local grader unavailable" stops masquerading as a dead grader.
|
|
3628
|
+
if (process.env.SYNKRO_DEPLOY_LOCATION === 'cloud') {
|
|
3629
|
+
const isTimeout = /tim(e|ed)\\s*out|aborted|the operation was aborted/i.test(errorMessage);
|
|
3630
|
+
if (isTimeout) {
|
|
3631
|
+
return hook + ' ' + target + ' \u2192 cloud grade timed out (45s), skipped';
|
|
3632
|
+
}
|
|
3633
|
+
return hook + ' ' + target + ' \u2192 cloud grader unavailable, skipped';
|
|
3634
|
+
}
|
|
3543
3635
|
return hook + ' ' + target + ' \u2192 local grader unavailable, skipped';
|
|
3544
3636
|
}
|
|
3545
3637
|
|
|
@@ -3619,7 +3711,7 @@ export function outputEmpty(): void {
|
|
|
3619
3711
|
EDIT_PRECHECK_TS = `#!/usr/bin/env bun
|
|
3620
3712
|
import {
|
|
3621
3713
|
loadJwt, ensureFreshJwt, detectRepo, loadConfig, route, tag, localGrade,
|
|
3622
|
-
parseVerdict, dispatchCapture, ruleMode, reconstructContent, isPathUnder, postWithRetry,
|
|
3714
|
+
parseVerdict, parseCombinedVerdict, combinedEditGradeEnabled, dispatchCapture, dispatchFinding, ruleMode, reconstructContent, isPathUnder, postWithRetry,
|
|
3623
3715
|
readStdin, extractTranscript, readLastPrompt, findNearestDeps, filePathFromToolInput,
|
|
3624
3716
|
appendSessionAction, readSessionLog, compressSessionLog, log,
|
|
3625
3717
|
outputJson, outputEmpty, setupCursorHookSignals, installHookWatchdog, isEditTool, hookSessionId, GATEWAY_URL,
|
|
@@ -3635,7 +3727,7 @@ const agentKind = (process.env.SYNKRO_HOOK_FORMAT === 'cursor' || process.argv.i
|
|
|
3635
3727
|
|
|
3636
3728
|
async function main() {
|
|
3637
3729
|
setupCursorHookSignals();
|
|
3638
|
-
installHookWatchdog(
|
|
3730
|
+
installHookWatchdog(48000);
|
|
3639
3731
|
try {
|
|
3640
3732
|
const input = await readStdin();
|
|
3641
3733
|
if (!input.trim()) { outputEmpty(); return; }
|
|
@@ -3769,6 +3861,90 @@ async function main() {
|
|
|
3769
3861
|
'The rules shown were pre-selected as the ones relevant to this edit \u2014 every rule here IS relevant, do not label any "not relevant". When passing (ok=true), give a terse, specific reason each rule passes. Format: "R003: no hardcoded secrets in file. R005: in-repo path only." Cover every rule shown.',
|
|
3770
3862
|
].join('\\n');
|
|
3771
3863
|
|
|
3864
|
+
// \u2500\u2500\u2500 Combined org-rules + CWE in ONE inference (SYNKRO_COMBINED_EDIT_GRADE) \u2500\u2500\u2500
|
|
3865
|
+
// Self-contained early-return branch \u2014 the default two-grade path below is
|
|
3866
|
+
// left untouched. Appends the CWE rule set to the rules prompt, runs one
|
|
3867
|
+
// grade-edit-cwe call, then splits the verdict: org-rule violations drive
|
|
3868
|
+
// the same ask/fix enforcement; cwe-### violations drive a fix-mode block.
|
|
3869
|
+
// The cwe-precheck hook no-ops when this flag is on (see CWE_PRECHECK_TS).
|
|
3870
|
+
if (combinedEditGradeEnabled(synkroFile)) {
|
|
3871
|
+
const fileExt = (filePath.match(/\\.([^.\\/]+)$/) || ['', ''])[1].toLowerCase();
|
|
3872
|
+
let cweRules: any[] = [];
|
|
3873
|
+
try {
|
|
3874
|
+
const cr = await fetch(GATEWAY_URL + '/api/v1/cwe-rules?ext=' + encodeURIComponent(fileExt), {
|
|
3875
|
+
headers: { Authorization: 'Bearer ' + jwt },
|
|
3876
|
+
signal: AbortSignal.timeout(4000),
|
|
3877
|
+
});
|
|
3878
|
+
if (cr.ok) cweRules = ((await cr.json() as any) || {}).rules || [];
|
|
3879
|
+
} catch { /* CWE rules optional \u2014 rule grading still runs in the combined pass */ }
|
|
3880
|
+
|
|
3881
|
+
const combinedPrompt = graderPrompt
|
|
3882
|
+
+ '\\n\\nCWE rules to ALSO check the proposed content against (emit cwe-### violations with a <code_snippet>): '
|
|
3883
|
+
+ JSON.stringify(cweRules);
|
|
3884
|
+
|
|
3885
|
+
let cResp: string;
|
|
3886
|
+
try {
|
|
3887
|
+
cResp = await localGrade('edit-cwe', combinedPrompt, undefined, graderPool);
|
|
3888
|
+
} catch (err) {
|
|
3889
|
+
const errMsg = (err as Error).message || String(err);
|
|
3890
|
+
logGraderUnavailable('editGuard', fileShort, errMsg);
|
|
3891
|
+
outputJson({ systemMessage: tagStr + ' ' + graderUnavailableMessage('editGuard', fileShort, errMsg, graderPool) });
|
|
3892
|
+
return;
|
|
3893
|
+
}
|
|
3894
|
+
|
|
3895
|
+
const { ruleVerdict, cweFindings } = parseCombinedVerdict(cResp);
|
|
3896
|
+
const editContent = 'file=' + filePath + ' content=' + proposed.slice(0, 2000);
|
|
3897
|
+
const violatedRules = ruleVerdict.ruleId ? [ruleVerdict.ruleId] : [];
|
|
3898
|
+
const cweBlock = cweFindings.slice(0, 5)
|
|
3899
|
+
.map(f => '[' + f.id + '] ' + (f.reason || 'weakness') + (f.suggestedFix ? ' Fix: ' + f.suggestedFix : ''))
|
|
3900
|
+
.join('\\n');
|
|
3901
|
+
for (const f of cweFindings) {
|
|
3902
|
+
dispatchFinding(jwt, { session_id: sessionId, file_path: filePath, finding_type: 'cwe', finding_id: f.id, status: 'open' }, config.captureDepth);
|
|
3903
|
+
}
|
|
3904
|
+
|
|
3905
|
+
// Org-rule violation \u2192 ask/fix enforcement (CWE findings appended to the deny).
|
|
3906
|
+
if (!ruleVerdict.ok) {
|
|
3907
|
+
const mode = normalizeMode(ruleVerdict.ruleMode || ruleMode(ruleVerdict.ruleId, config.rules));
|
|
3908
|
+
const guardReason = (ruleVerdict.ruleId ? '(' + ruleVerdict.ruleId + ') ' : '') + (ruleVerdict.reason || 'policy violation');
|
|
3909
|
+
let denyReason = mode === 'fix'
|
|
3910
|
+
? 'Guard: ' + guardReason + '\\nFix all issues before retrying. Do NOT ask the user to make the edit manually \u2014 resolve the violation in code yourself.'
|
|
3911
|
+
: 'Guard: ' + guardReason + '\\nAsk the user for explicit consent before retrying.';
|
|
3912
|
+
if (cweBlock) denyReason += '\\nAlso resolve these CWE weaknesses in code:\\n' + cweBlock;
|
|
3913
|
+
dispatchCapture(jwt, 'edit', 'block', ruleVerdict.severity || 'critical', ruleVerdict.category || 'security',
|
|
3914
|
+
toolName, gitRepo, sessionId, config.captureDepth, {
|
|
3915
|
+
command: editContent, reasoning: guardReason,
|
|
3916
|
+
rulesChecked: relevantRules, violatedRules,
|
|
3917
|
+
ccModel: captureModel, ...lineMetrics,
|
|
3918
|
+
});
|
|
3919
|
+
outputJson({
|
|
3920
|
+
systemMessage: tagStr + ' editGuard ' + fileShort + ' \u2192 blocked: ' + guardReason + (cweFindings.length ? ' (+' + cweFindings.length + ' CWE)' : ''),
|
|
3921
|
+
hookSpecificOutput: { hookEventName: 'PreToolUse', permissionDecision: 'deny', permissionDecisionReason: denyReason, additionalContext: denyReason },
|
|
3922
|
+
});
|
|
3923
|
+
return;
|
|
3924
|
+
}
|
|
3925
|
+
|
|
3926
|
+
// Rules clean but CWE weakness(es) \u2192 fix-mode block (resolve in code).
|
|
3927
|
+
if (cweFindings.length > 0) {
|
|
3928
|
+
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.';
|
|
3929
|
+
outputJson({
|
|
3930
|
+
systemMessage: tagStr + ' editGuard ' + fileShort + ' \u2192 blocked (' + cweFindings.map(f => f.id).join(', ') + ')',
|
|
3931
|
+
hookSpecificOutput: { hookEventName: 'PreToolUse', permissionDecision: 'deny', permissionDecisionReason: ctx, additionalContext: ctx },
|
|
3932
|
+
});
|
|
3933
|
+
return;
|
|
3934
|
+
}
|
|
3935
|
+
|
|
3936
|
+
// Both clean.
|
|
3937
|
+
dispatchCapture(jwt, 'edit', 'pass', 'clean', ruleVerdict.category || 'trivial_edit',
|
|
3938
|
+
toolName, gitRepo, sessionId, config.captureDepth, {
|
|
3939
|
+
command: editContent, reasoning: ruleVerdict.reason || 'no violations (rules + CWE)',
|
|
3940
|
+
rulesChecked: relevantRules, violatedRules: [],
|
|
3941
|
+
ccModel: captureModel, ...lineMetrics,
|
|
3942
|
+
});
|
|
3943
|
+
const cPassLine = tagStr + ' editGuard ' + fileShort + ' \u2192 pass: ' + (ruleVerdict.reason || 'clean (rules + CWE)');
|
|
3944
|
+
outputJson({ systemMessage: cPassLine, hookSpecificOutput: { hookEventName: 'PreToolUse', additionalContext: 'Synkro combined edit judge (rules + CWE). ' + (ruleVerdict.reason || 'clean') } });
|
|
3945
|
+
return;
|
|
3946
|
+
}
|
|
3947
|
+
|
|
3772
3948
|
let gradeResp: string;
|
|
3773
3949
|
try {
|
|
3774
3950
|
gradeResp = await localGrade('edit', graderPrompt, undefined, graderPool);
|
|
@@ -3890,7 +4066,7 @@ import {
|
|
|
3890
4066
|
outputJson, outputEmpty, setupCursorHookSignals, installHookWatchdog, isEditTool, isShellTool, isCursorHookFormat,
|
|
3891
4067
|
extractShellCodeWrites, hookSessionId, filePathFromToolInput, emitBlockScanFindings, cweSnippetLines, dispatchFinding, dispatchCapture, GATEWAY_URL,
|
|
3892
4068
|
logGraderUnavailable, graderUnavailableMessage, resolveTranscriptPath, isCursorInvokingCcHook,
|
|
3893
|
-
loadSynkroFile, effectiveGraderPool, synkroFilePresent, noSynkroSkipMessage,
|
|
4069
|
+
loadSynkroFile, effectiveGraderPool, synkroFilePresent, noSynkroSkipMessage, combinedEditGradeEnabled,
|
|
3894
4070
|
} from './_synkro-common.ts';
|
|
3895
4071
|
import { basename, extname, resolve, join, dirname } from 'node:path';
|
|
3896
4072
|
import { readFileSync, readdirSync, existsSync } from 'node:fs';
|
|
@@ -4029,7 +4205,7 @@ function scanPackageCapabilities(pkgName: string, cwd: string): PackageCapabilit
|
|
|
4029
4205
|
|
|
4030
4206
|
async function main() {
|
|
4031
4207
|
setupCursorHookSignals();
|
|
4032
|
-
installHookWatchdog(
|
|
4208
|
+
installHookWatchdog(48000);
|
|
4033
4209
|
try {
|
|
4034
4210
|
const input = await readStdin();
|
|
4035
4211
|
if (!input.trim()) { outputEmpty(); return; }
|
|
@@ -4118,6 +4294,11 @@ async function main() {
|
|
|
4118
4294
|
const graderPool = effectiveGraderPool(synkroFile, agentKind);
|
|
4119
4295
|
const rt = await cweRoute(config, synkroFile);
|
|
4120
4296
|
|
|
4297
|
+
// Combined-grade mode: the edit-precheck hook runs CWE inside its single
|
|
4298
|
+
// grade-edit-cwe call, so this standalone CWE scan must no-op — otherwise the
|
|
4299
|
+
// edit would be graded for CWE twice (defeating the inference-merge).
|
|
4300
|
+
if (combinedEditGradeEnabled(synkroFile)) { outputEmpty(); return; }
|
|
4301
|
+
|
|
4121
4302
|
if (config.silent) {
|
|
4122
4303
|
outputJson({ systemMessage: '[synkro:' + rt + ':cweScan:' + (graderPool === 'claude_code' ? 'claude' : graderPool) + '] skipped (silent mode)' });
|
|
4123
4304
|
return;
|
|
@@ -4543,14 +4724,16 @@ async function main() {
|
|
|
4543
4724
|
jwt = await ensureFreshJwt(jwt);
|
|
4544
4725
|
|
|
4545
4726
|
const config = await loadConfig(jwt);
|
|
4546
|
-
const rt = await route(config);
|
|
4547
4727
|
|
|
4548
4728
|
if (config.silent) {
|
|
4549
|
-
outputJson({ systemMessage: '[synkro:
|
|
4729
|
+
outputJson({ systemMessage: '[synkro:cveScan] ' + fileShort + ' \u2192 skipped (silent mode)' });
|
|
4550
4730
|
return;
|
|
4551
4731
|
}
|
|
4552
4732
|
|
|
4553
|
-
|
|
4733
|
+
// CVE scanning has no local/cloud distinction from the user's view \u2014 it's an
|
|
4734
|
+
// OSV database lookup that always runs server-side \u2014 so the tag omits the
|
|
4735
|
+
// location segment that grading/CWE carry. Just [synkro:cveScan].
|
|
4736
|
+
const cveTag = '[synkro:cveScan]';
|
|
4554
4737
|
|
|
4555
4738
|
// Reconstruct proposed content
|
|
4556
4739
|
const proposed = reconstructContent(toolName, toolInput, filePath, cwd);
|
|
@@ -4650,7 +4833,7 @@ async function main() {
|
|
|
4650
4833
|
finding_ids: violatedIds, severity: 'critical',
|
|
4651
4834
|
repo: gitRepo || undefined,
|
|
4652
4835
|
});
|
|
4653
|
-
const tagStr = '[synkro:
|
|
4836
|
+
const tagStr = '[synkro:pkgScan]';
|
|
4654
4837
|
const denyReason = tagStr + ' BLOCKED: ' + summary + '\\nDo not write this version. Pick a fixed/safe version instead.';
|
|
4655
4838
|
outputJson({
|
|
4656
4839
|
systemMessage: denyReason,
|
|
@@ -4942,7 +5125,7 @@ function isDuplicate(command: string, sessionId: string): boolean {
|
|
|
4942
5125
|
|
|
4943
5126
|
async function main() {
|
|
4944
5127
|
setupCursorHookSignals();
|
|
4945
|
-
installHookWatchdog(
|
|
5128
|
+
installHookWatchdog(48000);
|
|
4946
5129
|
try {
|
|
4947
5130
|
const input = await readStdin();
|
|
4948
5131
|
if (!input.trim()) { outputEmpty(); return; }
|
|
@@ -5214,7 +5397,7 @@ const agentKind = (process.env.SYNKRO_HOOK_FORMAT === 'cursor' || process.argv.i
|
|
|
5214
5397
|
|
|
5215
5398
|
async function main() {
|
|
5216
5399
|
setupCursorHookSignals();
|
|
5217
|
-
installHookWatchdog(
|
|
5400
|
+
installHookWatchdog(48000);
|
|
5218
5401
|
try {
|
|
5219
5402
|
const input = await readStdin();
|
|
5220
5403
|
if (!input.trim()) { outputEmpty(); return; }
|
|
@@ -5451,7 +5634,7 @@ function appendReviewToPlan(planFile: string, verdict: string): void {
|
|
|
5451
5634
|
|
|
5452
5635
|
async function main() {
|
|
5453
5636
|
setupCursorHookSignals();
|
|
5454
|
-
installHookWatchdog(
|
|
5637
|
+
installHookWatchdog(48000);
|
|
5455
5638
|
try {
|
|
5456
5639
|
const input = await readStdin();
|
|
5457
5640
|
if (!input.trim()) { outputEmpty(); return; }
|
|
@@ -10386,7 +10569,7 @@ function writeConfigEnv(opts) {
|
|
|
10386
10569
|
`SYNKRO_CREDENTIALS_PATH=${shellQuoteSingle(credsPath)}`,
|
|
10387
10570
|
`SYNKRO_TIER=${shellQuoteSingle(safeTier)}`,
|
|
10388
10571
|
`SYNKRO_INFERENCE=${shellQuoteSingle(safeInference)}`,
|
|
10389
|
-
`SYNKRO_VERSION=${shellQuoteSingle("1.6.
|
|
10572
|
+
`SYNKRO_VERSION=${shellQuoteSingle("1.6.74")}`
|
|
10390
10573
|
];
|
|
10391
10574
|
if (safeSynkroBin) lines.push(`SYNKRO_CLI_BIN=${shellQuoteSingle(safeSynkroBin)}`);
|
|
10392
10575
|
if (safeUserId) lines.push(`SYNKRO_USER_ID=${shellQuoteSingle(safeUserId)}`);
|
|
@@ -12075,7 +12258,14 @@ function writePluginFiles() {
|
|
|
12075
12258
|
c.pluginSettingsPath,
|
|
12076
12259
|
JSON.stringify({
|
|
12077
12260
|
fastMode: true,
|
|
12078
|
-
enabledMcpjsonServers: ["synkro-local"]
|
|
12261
|
+
enabledMcpjsonServers: ["synkro-local"],
|
|
12262
|
+
// Extended thinking is pure latency tax on a structured verdict grade and
|
|
12263
|
+
// emits reasoning the verdict parser isn't tuned for. effortLevel only
|
|
12264
|
+
// SOFTENS it; CLAUDE_CODE_DISABLE_THINKING=1 is the authoritative off
|
|
12265
|
+
// switch. Mirror the cloud pool (pool-config.sh) so local + cloud graders
|
|
12266
|
+
// behave identically.
|
|
12267
|
+
effortLevel: "low",
|
|
12268
|
+
env: { CLAUDE_CODE_DISABLE_THINKING: "1" }
|
|
12079
12269
|
}, null, 2) + "\n",
|
|
12080
12270
|
"utf-8"
|
|
12081
12271
|
);
|
|
@@ -12316,7 +12506,7 @@ tmux kill-session -t "=$SESSION" 2>/dev/null || true
|
|
|
12316
12506
|
# Start claude inside a detached tmux session so it has a real pty.
|
|
12317
12507
|
# Redirect stderr to the log so we can see why it dies.
|
|
12318
12508
|
tmux new-session -d -s "$SESSION" \\
|
|
12319
|
-
"SYNKRO_CHANNEL_PORT=${CHANNEL_1_PORT} claude --dangerously-load-development-channels server:synkro-local --dangerously-skip-permissions --setting-sources project,local --model claude-sonnet-4-6 2>>$LOG; echo 'claude exited with code '$'?' >> $LOG"
|
|
12509
|
+
"SYNKRO_CHANNEL_PORT=${CHANNEL_1_PORT} CLAUDE_CODE_DISABLE_THINKING=1 claude --dangerously-load-development-channels server:synkro-local --dangerously-skip-permissions --setting-sources project,local --model claude-sonnet-4-6 2>>$LOG; echo 'claude exited with code '$'?' >> $LOG"
|
|
12320
12510
|
|
|
12321
12511
|
# Claude's --dangerously-load-development-channels shows a confirmation
|
|
12322
12512
|
# prompt: option 1 = "I am using this for local development" (accept),
|
|
@@ -12378,7 +12568,7 @@ log "claude version: $(claude --version 2>&1 | head -1)"
|
|
|
12378
12568
|
tmux kill-session -t "=$SESSION" 2>/dev/null || true
|
|
12379
12569
|
|
|
12380
12570
|
tmux new-session -d -s "$SESSION" \\
|
|
12381
|
-
"SYNKRO_CHANNEL_PORT=${CHANNEL_2_PORT} claude --dangerously-load-development-channels server:synkro-local --dangerously-skip-permissions --setting-sources project,local --model claude-sonnet-4-6 2>>$LOG; echo 'claude exited with code '$'?' >> $LOG"
|
|
12571
|
+
"SYNKRO_CHANNEL_PORT=${CHANNEL_2_PORT} CLAUDE_CODE_DISABLE_THINKING=1 claude --dangerously-load-development-channels server:synkro-local --dangerously-skip-permissions --setting-sources project,local --model claude-sonnet-4-6 2>>$LOG; echo 'claude exited with code '$'?' >> $LOG"
|
|
12382
12572
|
|
|
12383
12573
|
sleep 3
|
|
12384
12574
|
if tmux has-session -t "=$SESSION" 2>/dev/null; then
|
|
@@ -12434,7 +12624,7 @@ log "claude version: $(claude --version 2>&1 | head -1)"
|
|
|
12434
12624
|
tmux kill-session -t "=$SESSION" 2>/dev/null || true
|
|
12435
12625
|
|
|
12436
12626
|
tmux new-session -d -s "$SESSION" \\
|
|
12437
|
-
"SYNKRO_CHANNEL_PORT=${CHANNEL_3_PORT} claude --dangerously-load-development-channels server:synkro-local --dangerously-skip-permissions --setting-sources project,local --model claude-sonnet-4-6 2>>$LOG; echo 'claude exited with code '$'?' >> $LOG"
|
|
12627
|
+
"SYNKRO_CHANNEL_PORT=${CHANNEL_3_PORT} CLAUDE_CODE_DISABLE_THINKING=1 claude --dangerously-load-development-channels server:synkro-local --dangerously-skip-permissions --setting-sources project,local --model claude-sonnet-4-6 2>>$LOG; echo 'claude exited with code '$'?' >> $LOG"
|
|
12438
12628
|
|
|
12439
12629
|
sleep 3
|
|
12440
12630
|
if tmux has-session -t "=$SESSION" 2>/dev/null; then
|
|
@@ -12490,7 +12680,7 @@ log "claude version: $(claude --version 2>&1 | head -1)"
|
|
|
12490
12680
|
tmux kill-session -t "=$SESSION" 2>/dev/null || true
|
|
12491
12681
|
|
|
12492
12682
|
tmux new-session -d -s "$SESSION" \\
|
|
12493
|
-
"SYNKRO_CHANNEL_PORT=${CHANNEL_4_PORT} claude --dangerously-load-development-channels server:synkro-local --dangerously-skip-permissions --setting-sources project,local --model claude-sonnet-4-6 2>>$LOG; echo 'claude exited with code '$'?' >> $LOG"
|
|
12683
|
+
"SYNKRO_CHANNEL_PORT=${CHANNEL_4_PORT} CLAUDE_CODE_DISABLE_THINKING=1 claude --dangerously-load-development-channels server:synkro-local --dangerously-skip-permissions --setting-sources project,local --model claude-sonnet-4-6 2>>$LOG; echo 'claude exited with code '$'?' >> $LOG"
|
|
12494
12684
|
|
|
12495
12685
|
sleep 3
|
|
12496
12686
|
if tmux has-session -t "=$SESSION" 2>/dev/null; then
|
|
@@ -14031,7 +14221,7 @@ var args = process.argv.slice(2);
|
|
|
14031
14221
|
var cmd = args[0] || "";
|
|
14032
14222
|
var subArgs = args.slice(1);
|
|
14033
14223
|
function printVersion() {
|
|
14034
|
-
console.log("1.6.
|
|
14224
|
+
console.log("1.6.74");
|
|
14035
14225
|
}
|
|
14036
14226
|
function printHelp2() {
|
|
14037
14227
|
console.log(`Synkro CLI \u2014 runtime safety for AI coding agents
|