@synkro-sh/cli 1.6.43 → 1.6.45
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 +113 -52
- package/dist/bootstrap.js.map +1 -1
- package/package.json +5 -2
package/dist/bootstrap.js
CHANGED
|
@@ -350,13 +350,13 @@ function installCursorHooks(hooksJsonPath, config) {
|
|
|
350
350
|
});
|
|
351
351
|
h.beforeShellExecution.push({
|
|
352
352
|
command: cursorCcCmd(config.cwePrecheckScriptPath),
|
|
353
|
-
timeout:
|
|
353
|
+
timeout: 30,
|
|
354
354
|
failClosed: false,
|
|
355
355
|
[SYNKRO_MARKER2]: true
|
|
356
356
|
});
|
|
357
357
|
h.beforeShellExecution.push({
|
|
358
358
|
command: bunRunCmd(config.bashJudgeScriptPath),
|
|
359
|
-
timeout:
|
|
359
|
+
timeout: 30,
|
|
360
360
|
failClosed: false,
|
|
361
361
|
[SYNKRO_MARKER2]: true
|
|
362
362
|
});
|
|
@@ -371,36 +371,36 @@ function installCursorHooks(hooksJsonPath, config) {
|
|
|
371
371
|
});
|
|
372
372
|
h.preToolUse.push({
|
|
373
373
|
command: cursorCcCmd(config.cwePrecheckScriptPath),
|
|
374
|
-
timeout:
|
|
374
|
+
timeout: 30,
|
|
375
375
|
failClosed: false,
|
|
376
376
|
matcher: "Shell|Bash|terminal|run_terminal_cmd|execute_command",
|
|
377
377
|
[SYNKRO_MARKER2]: true
|
|
378
378
|
});
|
|
379
379
|
h.preToolUse.push({
|
|
380
380
|
command: bunRunCmd(config.bashJudgeScriptPath),
|
|
381
|
-
timeout:
|
|
381
|
+
timeout: 30,
|
|
382
382
|
failClosed: false,
|
|
383
383
|
matcher: "Shell|Bash|Read|ReadFile|Grep|Glob|terminal|run_terminal_cmd|execute_command|read_file|grep_search|file_search|list_dir|codebase_search|delete_file",
|
|
384
384
|
[SYNKRO_MARKER2]: true
|
|
385
385
|
});
|
|
386
386
|
pushCcHook(h, "preToolUse", config.editPrecheckScriptPath, {
|
|
387
|
-
timeout:
|
|
387
|
+
timeout: 30,
|
|
388
388
|
matcher: "Write|Edit|StrReplace|MultiEdit|NotebookEdit|edit_file|reapply|edit_notebook|ApplyPatch|apply_patch"
|
|
389
389
|
});
|
|
390
390
|
pushCcHook(h, "preToolUse", config.cwePrecheckScriptPath, {
|
|
391
|
-
timeout:
|
|
391
|
+
timeout: 30,
|
|
392
392
|
matcher: "Write|Edit|StrReplace|MultiEdit|NotebookEdit|edit_file|reapply|edit_notebook|ApplyPatch|apply_patch"
|
|
393
393
|
});
|
|
394
394
|
pushCcHook(h, "preToolUse", config.cvePrecheckScriptPath, {
|
|
395
|
-
timeout:
|
|
395
|
+
timeout: 10,
|
|
396
396
|
matcher: "Write|Edit|StrReplace|MultiEdit|NotebookEdit|edit_file|reapply|edit_notebook|ApplyPatch|apply_patch"
|
|
397
397
|
});
|
|
398
398
|
pushCcHook(h, "preToolUse", config.agentJudgeScriptPath, {
|
|
399
|
-
timeout:
|
|
399
|
+
timeout: 30,
|
|
400
400
|
matcher: "Agent|Task"
|
|
401
401
|
});
|
|
402
402
|
pushCcHook(h, "preToolUse", config.planJudgeScriptPath, {
|
|
403
|
-
timeout:
|
|
403
|
+
timeout: 45,
|
|
404
404
|
matcher: "ExitPlanMode|SwitchMode|CreatePlan"
|
|
405
405
|
});
|
|
406
406
|
h.afterFileEdit = h.afterFileEdit ?? [];
|
|
@@ -1499,7 +1499,12 @@ async function cloudGrade(surface: string, prompt: string, jwt: string, timeoutM
|
|
|
1499
1499
|
return String(data.verdict || '');
|
|
1500
1500
|
}
|
|
1501
1501
|
|
|
1502
|
-
|
|
1502
|
+
// Default 24s \u2014 MUST stay below the CC hook timeout (30s for edit/bash/cwe in
|
|
1503
|
+
// ccHookConfig.ts) so the AbortSignal fires and the caller's catch fails open
|
|
1504
|
+
// cleanly. If this matches or exceeds the CC budget, CC force-kills the bun
|
|
1505
|
+
// process mid-fetch (exit 142 / SIGALRM) and surfaces the in-flight grader
|
|
1506
|
+
// prompt as a hook error instead of a clean pass.
|
|
1507
|
+
export async function localGrade(surface: string, prompt: string, timeoutMs = 24000, agentKind: AgentKind = 'claude_code'): Promise<string> {
|
|
1503
1508
|
const jwt = loadJwt();
|
|
1504
1509
|
if (!jwt) throw new Error('NO_JWT');
|
|
1505
1510
|
// BYOK grading mode routes the grade through an LLM API instead of the
|
|
@@ -1511,7 +1516,10 @@ export async function localGrade(surface: string, prompt: string, timeoutMs = 30
|
|
|
1511
1516
|
return channelGrade(ROLE_MAP[surface] || 'grade-edit', prompt, jwt, 18929, timeoutMs, agentKind);
|
|
1512
1517
|
}
|
|
1513
1518
|
|
|
1514
|
-
|
|
1519
|
+
// Default 24s \u2014 was 45000, which EXCEEDED the CC cwe hook timeout (30s) and
|
|
1520
|
+
// guaranteed a force-kill on any slow grade. Two cwe chunks grade in parallel
|
|
1521
|
+
// within the same CC budget, so each must finish well under it.
|
|
1522
|
+
export async function localGradeCwe(prompt: string, agentKind: AgentKind = 'claude_code', timeoutMs = 24000): Promise<string> {
|
|
1515
1523
|
const jwt = loadJwt();
|
|
1516
1524
|
if (!jwt) throw new Error('NO_JWT');
|
|
1517
1525
|
return channelGrade('grade-cwe', prompt, jwt, 18930, timeoutMs, agentKind);
|
|
@@ -3210,6 +3218,7 @@ export function isCursorInvokingCcHook(agentKind: string, model: string): boolea
|
|
|
3210
3218
|
}
|
|
3211
3219
|
|
|
3212
3220
|
let cursorHookExited = false;
|
|
3221
|
+
let hookFinalized = false;
|
|
3213
3222
|
|
|
3214
3223
|
export function setupCursorHookSignals(): void {
|
|
3215
3224
|
if (!isCursorHookFormat()) return;
|
|
@@ -3218,9 +3227,28 @@ export function setupCursorHookSignals(): void {
|
|
|
3218
3227
|
|
|
3219
3228
|
function cursorHookExit(): never {
|
|
3220
3229
|
cursorHookExited = true;
|
|
3230
|
+
hookFinalized = true;
|
|
3221
3231
|
process.exit(0);
|
|
3222
3232
|
}
|
|
3223
3233
|
|
|
3234
|
+
// Self-imposed deadline. CC/Cursor kill the hook process at the configured hook
|
|
3235
|
+
// timeout with a signal; if a grade fetch is still in flight when that happens,
|
|
3236
|
+
// the runtime dumps the in-flight request body (the grader prompt) to stderr and
|
|
3237
|
+
// exits non-zero \u2014 surfacing as a "hook error" toast with leaked prompt text.
|
|
3238
|
+
// To prevent that, each grading hook arms this watchdog a few seconds UNDER its
|
|
3239
|
+
// configured budget: when it fires we emit a clean empty result and exit 0
|
|
3240
|
+
// ourselves, so the harness never has to kill us. The timer is unref'd, so a
|
|
3241
|
+
// hook that finishes early exits normally and the watchdog never runs.
|
|
3242
|
+
export function installHookWatchdog(budgetMs: number): void {
|
|
3243
|
+
const t = setTimeout(() => {
|
|
3244
|
+
if (hookFinalized) return;
|
|
3245
|
+
hookFinalized = true;
|
|
3246
|
+
try { process.stdout.write('{}\\n'); } catch {}
|
|
3247
|
+
process.exit(0);
|
|
3248
|
+
}, budgetMs);
|
|
3249
|
+
if (typeof (t as any).unref === 'function') (t as any).unref();
|
|
3250
|
+
}
|
|
3251
|
+
|
|
3224
3252
|
// \u2500\u2500\u2500 Grader-unavailable diagnostic log \u2500\u2500\u2500
|
|
3225
3253
|
// Records every time a hook tried to call the local grader and fell open
|
|
3226
3254
|
// because the call failed. JSONL at ~/.synkro/grader-unavailable.log so the
|
|
@@ -3300,6 +3328,8 @@ export function outputJson(obj: any): void {
|
|
|
3300
3328
|
outputEmpty();
|
|
3301
3329
|
return;
|
|
3302
3330
|
}
|
|
3331
|
+
if (hookFinalized) return;
|
|
3332
|
+
hookFinalized = true;
|
|
3303
3333
|
console.log(JSON.stringify(obj));
|
|
3304
3334
|
}
|
|
3305
3335
|
|
|
@@ -3311,6 +3341,8 @@ export function outputEmpty(): void {
|
|
|
3311
3341
|
}
|
|
3312
3342
|
cursorHookExit();
|
|
3313
3343
|
}
|
|
3344
|
+
if (hookFinalized) return;
|
|
3345
|
+
hookFinalized = true;
|
|
3314
3346
|
console.log('{}');
|
|
3315
3347
|
}
|
|
3316
3348
|
`;
|
|
@@ -3320,7 +3352,7 @@ import {
|
|
|
3320
3352
|
parseVerdict, dispatchCapture, ruleMode, reconstructContent, isPathUnder, postWithRetry,
|
|
3321
3353
|
readStdin, extractTranscript, readLastPrompt, findNearestDeps, filePathFromToolInput,
|
|
3322
3354
|
appendSessionAction, readSessionLog, compressSessionLog, log,
|
|
3323
|
-
outputJson, outputEmpty, setupCursorHookSignals, isEditTool, hookSessionId, GATEWAY_URL,
|
|
3355
|
+
outputJson, outputEmpty, setupCursorHookSignals, installHookWatchdog, isEditTool, hookSessionId, GATEWAY_URL,
|
|
3324
3356
|
logGraderUnavailable, graderUnavailableMessage, filterRules, ruleFilterText, normalizeMode, countEditLineDelta,
|
|
3325
3357
|
captureLineMetrics, cursorModelFromPayload, resolveTranscriptPath, isCursorInvokingCcHook,
|
|
3326
3358
|
loadSynkroFile, effectiveGraderPool,
|
|
@@ -3333,6 +3365,7 @@ const agentKind = (process.env.SYNKRO_HOOK_FORMAT === 'cursor' || process.argv.i
|
|
|
3333
3365
|
|
|
3334
3366
|
async function main() {
|
|
3335
3367
|
setupCursorHookSignals();
|
|
3368
|
+
installHookWatchdog(27000);
|
|
3336
3369
|
try {
|
|
3337
3370
|
const input = await readStdin();
|
|
3338
3371
|
if (!input.trim()) { outputEmpty(); return; }
|
|
@@ -3455,7 +3488,7 @@ async function main() {
|
|
|
3455
3488
|
'Last user prompt: ' + (lastPrompt || 'none'),
|
|
3456
3489
|
'Org rules: ' + JSON.stringify(relevantRules),
|
|
3457
3490
|
'IMPORTANT: If a rule is violated, ALWAYS return ok=false with the rule_id and reason, regardless of the rule mode. Do NOT pass a command just because the rule mode is "fix". The enforcement layer handles ask vs fix \u2014 your job is only to detect violations.',
|
|
3458
|
-
'CRITICAL: The user requesting or instructing an action does NOT exempt it from rules. Even if the user explicitly said "drop the database" or "delete everything", you MUST still flag the rule violation on first encounter. User intent is NOT consent. However, for ask-mode rules ONLY: if the session history shows a prior block for the SAME rule AND the user explicitly consented after seeing that block, subsequent commands covered by that same rule may pass \u2014 but each distinct command is consumed once. Look for the sequence: block event \u2192 user acknowledgment \u2192 retry. Once a specific command has successfully executed under that consent, it is consumed. If the same command appears again later, it requires fresh consent (a new block \u2192 consent cycle). Example: R012 covers deploy, publish, push. Block on deploy \u2192 user consents \u2192 deploy passes (consumed), publish passes (consumed), push passes (consumed). A later deploy triggers a fresh block.
|
|
3491
|
+
'CRITICAL: The user requesting or instructing an action does NOT exempt it from rules. Even if the user explicitly said "drop the database" or "delete everything", you MUST still flag the rule violation on first encounter. User intent is NOT consent. However, for ask-mode rules ONLY: if the session history shows a prior block for the SAME rule AND the user explicitly consented after seeing that block, subsequent commands covered by that same rule may pass \u2014 but each distinct command is consumed once. Look for the sequence: block event \u2192 user acknowledgment \u2192 retry. Once a specific command has successfully executed under that consent, it is consumed. If the same command appears again later, it requires fresh consent (a new block \u2192 consent cycle). Example: R012 covers deploy, publish, push. Block on deploy \u2192 user consents \u2192 deploy passes (consumed), publish passes (consumed), push passes (consumed). A later deploy triggers a fresh block. An initial user instruction is NEVER consent \u2014 only a response to a shown block counts.',
|
|
3459
3492
|
'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.',
|
|
3460
3493
|
].join('\\n');
|
|
3461
3494
|
|
|
@@ -3577,7 +3610,7 @@ main();
|
|
|
3577
3610
|
import {
|
|
3578
3611
|
loadJwt, ensureFreshJwt, detectRepo, loadConfig, cweRoute, tag,
|
|
3579
3612
|
localGradeCwe, parseVerdict, reconstructContent, readStdin, log,
|
|
3580
|
-
outputJson, outputEmpty, setupCursorHookSignals, isEditTool, isShellTool, isCursorHookFormat,
|
|
3613
|
+
outputJson, outputEmpty, setupCursorHookSignals, installHookWatchdog, isEditTool, isShellTool, isCursorHookFormat,
|
|
3581
3614
|
extractShellCodeWrites, hookSessionId, filePathFromToolInput, emitBlockScanFindings, dispatchFinding, dispatchCapture, GATEWAY_URL,
|
|
3582
3615
|
logGraderUnavailable, graderUnavailableMessage, resolveTranscriptPath, isCursorInvokingCcHook,
|
|
3583
3616
|
loadSynkroFile, effectiveGraderPool,
|
|
@@ -3717,6 +3750,7 @@ function scanPackageCapabilities(pkgName: string, cwd: string): PackageCapabilit
|
|
|
3717
3750
|
|
|
3718
3751
|
async function main() {
|
|
3719
3752
|
setupCursorHookSignals();
|
|
3753
|
+
installHookWatchdog(27000);
|
|
3720
3754
|
try {
|
|
3721
3755
|
const input = await readStdin();
|
|
3722
3756
|
if (!input.trim()) { outputEmpty(); return; }
|
|
@@ -4540,7 +4574,7 @@ import {
|
|
|
4540
4574
|
loadJwt, ensureFreshJwt, detectRepo, loadConfig, route, tag, localGrade,
|
|
4541
4575
|
parseVerdict, dispatchCapture, dispatchFinding, ruleMode, postWithRetry, readStdin,
|
|
4542
4576
|
extractTranscript, readLastPrompt, appendSessionAction, readSessionLog, compressSessionLog, log,
|
|
4543
|
-
outputJson, outputEmpty, setupCursorHookSignals, isShellTool, hookSessionId, GATEWAY_URL,
|
|
4577
|
+
outputJson, outputEmpty, setupCursorHookSignals, installHookWatchdog, isShellTool, hookSessionId, GATEWAY_URL,
|
|
4544
4578
|
logGraderUnavailable, graderUnavailableMessage, filterRules, ruleFilterText, normalizeMode, appendLocalTelemetry, isSafeInRepoRead,
|
|
4545
4579
|
loadSynkroFile, effectiveGraderPool,
|
|
4546
4580
|
hashCommand, resolveTranscriptPath, isCursorHookFormat,
|
|
@@ -4583,6 +4617,7 @@ function isDuplicate(command: string, sessionId: string): boolean {
|
|
|
4583
4617
|
|
|
4584
4618
|
async function main() {
|
|
4585
4619
|
setupCursorHookSignals();
|
|
4620
|
+
installHookWatchdog(27000);
|
|
4586
4621
|
try {
|
|
4587
4622
|
const input = await readStdin();
|
|
4588
4623
|
if (!input.trim()) { outputEmpty(); return; }
|
|
@@ -4714,7 +4749,7 @@ async function main() {
|
|
|
4714
4749
|
'Org rules: ' + JSON.stringify(relevantRules),
|
|
4715
4750
|
scanConcern,
|
|
4716
4751
|
'IMPORTANT: If a rule is violated, ALWAYS return ok=false with the rule_id and reason, regardless of the rule mode. Do NOT pass a command just because the rule mode is "fix". The enforcement layer handles ask vs fix — your job is only to detect violations.',
|
|
4717
|
-
'CRITICAL: The user requesting or instructing an action does NOT exempt it from rules. Even if the user explicitly said "drop the database" or "delete everything", you MUST still flag the rule violation on first encounter. User intent is NOT consent. However, for ask-mode rules ONLY: if the session history shows a prior block for the SAME rule AND the user explicitly consented after seeing that block, subsequent commands covered by that same rule may pass — but each distinct command is consumed once. Look for the sequence: block event → user acknowledgment → retry. Once a specific command has successfully executed under that consent, it is consumed. If the same command appears again later, it requires fresh consent (a new block → consent cycle). Example: R012 covers deploy, publish, push. Block on deploy → user consents → deploy passes (consumed), publish passes (consumed), push passes (consumed). A later deploy triggers a fresh block.
|
|
4752
|
+
'CRITICAL: The user requesting or instructing an action does NOT exempt it from rules. Even if the user explicitly said "drop the database" or "delete everything", you MUST still flag the rule violation on first encounter. User intent is NOT consent. However, for ask-mode rules ONLY: if the session history shows a prior block for the SAME rule AND the user explicitly consented after seeing that block, subsequent commands covered by that same rule may pass — but each distinct command is consumed once. Look for the sequence: block event → user acknowledgment → retry. Once a specific command has successfully executed under that consent, it is consumed. If the same command appears again later, it requires fresh consent (a new block → consent cycle). Example: R012 covers deploy, publish, push. Block on deploy → user consents → deploy passes (consumed), publish passes (consumed), push passes (consumed). A later deploy triggers a fresh block. An initial user instruction is NEVER consent — only a response to a shown block counts.',
|
|
4718
4753
|
'The rules shown were pre-selected as the ones relevant to this command — 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 secrets in grep args. R005: in-repo path only." Cover every rule shown.',
|
|
4719
4754
|
'Rules with preconditions (e.g. "run X before Y") are CONSUMED after the protected action completes. Use the session history timestamps to determine ordering: a precondition satisfied before the last occurrence of the protected action does NOT satisfy the next occurrence. Each new protected action needs its precondition re-satisfied.',
|
|
4720
4755
|
].filter(Boolean).join('\\n');
|
|
@@ -4837,7 +4872,7 @@ import {
|
|
|
4837
4872
|
loadJwt, ensureFreshJwt, detectRepo, loadConfig, route, tag, localGrade,
|
|
4838
4873
|
parseVerdict, dispatchCapture, ruleMode, postWithRetry, readStdin,
|
|
4839
4874
|
extractTranscript, readLastPrompt, appendSessionAction, readSessionLog, compressSessionLog, log,
|
|
4840
|
-
outputJson, outputEmpty, setupCursorHookSignals, isAgentTool, hookSessionId, GATEWAY_URL,
|
|
4875
|
+
outputJson, outputEmpty, setupCursorHookSignals, installHookWatchdog, isAgentTool, hookSessionId, GATEWAY_URL,
|
|
4841
4876
|
logGraderUnavailable, graderUnavailableMessage, filterRules, normalizeMode, resolveTranscriptPath, isCursorInvokingCcHook,
|
|
4842
4877
|
loadSynkroFile, effectiveGraderPool,
|
|
4843
4878
|
type HookConfig, type Rule,
|
|
@@ -4847,6 +4882,7 @@ const agentKind = (process.env.SYNKRO_HOOK_FORMAT === 'cursor' || process.argv.i
|
|
|
4847
4882
|
|
|
4848
4883
|
async function main() {
|
|
4849
4884
|
setupCursorHookSignals();
|
|
4885
|
+
installHookWatchdog(27000);
|
|
4850
4886
|
try {
|
|
4851
4887
|
const input = await readStdin();
|
|
4852
4888
|
if (!input.trim()) { outputEmpty(); return; }
|
|
@@ -4926,7 +4962,7 @@ async function main() {
|
|
|
4926
4962
|
'Last user prompt: ' + (lastPrompt || 'none'),
|
|
4927
4963
|
'Org rules: ' + JSON.stringify(relevantRules),
|
|
4928
4964
|
'IMPORTANT: If a rule is violated, ALWAYS return ok=false with the rule_id and reason, regardless of the rule mode. Do NOT pass a command just because the rule mode is "fix". The enforcement layer handles ask vs fix \u2014 your job is only to detect violations.',
|
|
4929
|
-
'CRITICAL: The user requesting or instructing an action does NOT exempt it from rules. Even if the user explicitly said "drop the database" or "delete everything", you MUST still flag the rule violation on first encounter. User intent is NOT consent. However, for ask-mode rules ONLY: if the session history shows a prior block for the SAME rule AND the user explicitly consented after seeing that block, subsequent commands covered by that same rule may pass \u2014 but each distinct command is consumed once. Look for the sequence: block event \u2192 user acknowledgment \u2192 retry. Once a specific command has successfully executed under that consent, it is consumed. If the same command appears again later, it requires fresh consent (a new block \u2192 consent cycle). Example: R012 covers deploy, publish, push. Block on deploy \u2192 user consents \u2192 deploy passes (consumed), publish passes (consumed), push passes (consumed). A later deploy triggers a fresh block.
|
|
4965
|
+
'CRITICAL: The user requesting or instructing an action does NOT exempt it from rules. Even if the user explicitly said "drop the database" or "delete everything", you MUST still flag the rule violation on first encounter. User intent is NOT consent. However, for ask-mode rules ONLY: if the session history shows a prior block for the SAME rule AND the user explicitly consented after seeing that block, subsequent commands covered by that same rule may pass \u2014 but each distinct command is consumed once. Look for the sequence: block event \u2192 user acknowledgment \u2192 retry. Once a specific command has successfully executed under that consent, it is consumed. If the same command appears again later, it requires fresh consent (a new block \u2192 consent cycle). Example: R012 covers deploy, publish, push. Block on deploy \u2192 user consents \u2192 deploy passes (consumed), publish passes (consumed), push passes (consumed). A later deploy triggers a fresh block. An initial user instruction is NEVER consent \u2014 only a response to a shown block counts.',
|
|
4930
4966
|
].filter(Boolean).join('\\n');
|
|
4931
4967
|
|
|
4932
4968
|
let gradeResp: string;
|
|
@@ -5024,7 +5060,7 @@ main();
|
|
|
5024
5060
|
import {
|
|
5025
5061
|
loadJwt, ensureFreshJwt, detectRepo, loadConfig, route, tag, localGrade,
|
|
5026
5062
|
parseVerdict, dispatchCapture, appendSessionAction, readSessionLog, compressSessionLog, postWithRetry, readStdin, log,
|
|
5027
|
-
outputJson, outputEmpty, setupCursorHookSignals, isPlanTool, hookSessionId, GATEWAY_URL,
|
|
5063
|
+
outputJson, outputEmpty, setupCursorHookSignals, installHookWatchdog, isPlanTool, hookSessionId, GATEWAY_URL,
|
|
5028
5064
|
filterRules, graderUnavailableMessage, resolveTranscriptPath, isCursorInvokingCcHook,
|
|
5029
5065
|
loadSynkroFile, effectiveGraderPool,
|
|
5030
5066
|
} from './_synkro-common.ts';
|
|
@@ -5076,6 +5112,7 @@ function appendReviewToPlan(planFile: string, verdict: string): void {
|
|
|
5076
5112
|
|
|
5077
5113
|
async function main() {
|
|
5078
5114
|
setupCursorHookSignals();
|
|
5115
|
+
installHookWatchdog(42000);
|
|
5079
5116
|
try {
|
|
5080
5117
|
const input = await readStdin();
|
|
5081
5118
|
if (!input.trim()) { outputEmpty(); return; }
|
|
@@ -5582,8 +5619,10 @@ function isDuplicate(command: string, sessionId: string): boolean {
|
|
|
5582
5619
|
return false;
|
|
5583
5620
|
}
|
|
5584
5621
|
|
|
5585
|
-
// Cursor
|
|
5586
|
-
|
|
5622
|
+
// Cursor hook timeouts now match CC (30s for bash/edit/agent), so use the same
|
|
5623
|
+
// 24s internal grade budget as the CC path \u2014 stays under the 30s hook timeout
|
|
5624
|
+
// (JWT refresh + grade) and fails open cleanly via the caller's catch.
|
|
5625
|
+
const CURSOR_GRADE_TIMEOUT_MS = 24000;
|
|
5587
5626
|
const CURSOR_CLOUD_TIMEOUT_MS = 9000;
|
|
5588
5627
|
|
|
5589
5628
|
let hookDone = false;
|
|
@@ -5725,7 +5764,7 @@ async function main() {
|
|
|
5725
5764
|
'Org rules: ' + JSON.stringify(relevantRules),
|
|
5726
5765
|
scanConcern,
|
|
5727
5766
|
'IMPORTANT: If a rule is violated, ALWAYS return ok=false with the rule_id and reason, regardless of the rule mode. Do NOT pass a command just because the rule mode is "fix". The enforcement layer handles ask vs fix \u2014 your job is only to detect violations.',
|
|
5728
|
-
'CRITICAL: The user requesting or instructing an action does NOT exempt it from rules. Even if the user explicitly said "drop the database" or "delete everything", you MUST still flag the rule violation on first encounter. User intent is NOT consent. However, for ask-mode rules ONLY: if the session history shows a prior block for the SAME rule AND the user explicitly consented after seeing that block, subsequent commands covered by that same rule may pass \u2014 but each distinct command is consumed once. Look for the sequence: block event \u2192 user acknowledgment \u2192 retry. Once a specific command has successfully executed under that consent, it is consumed. If the same command appears again later, it requires fresh consent (a new block \u2192 consent cycle). Example: R012 covers deploy, publish, push. Block on deploy \u2192 user consents \u2192 deploy passes (consumed), publish passes (consumed), push passes (consumed). A later deploy triggers a fresh block.
|
|
5767
|
+
'CRITICAL: The user requesting or instructing an action does NOT exempt it from rules. Even if the user explicitly said "drop the database" or "delete everything", you MUST still flag the rule violation on first encounter. User intent is NOT consent. However, for ask-mode rules ONLY: if the session history shows a prior block for the SAME rule AND the user explicitly consented after seeing that block, subsequent commands covered by that same rule may pass \u2014 but each distinct command is consumed once. Look for the sequence: block event \u2192 user acknowledgment \u2192 retry. Once a specific command has successfully executed under that consent, it is consumed. If the same command appears again later, it requires fresh consent (a new block \u2192 consent cycle). Example: R012 covers deploy, publish, push. Block on deploy \u2192 user consents \u2192 deploy passes (consumed), publish passes (consumed), push passes (consumed). A later deploy triggers a fresh block. An initial user instruction is NEVER consent \u2014 only a response to a shown block counts.',
|
|
5729
5768
|
'The rules shown were pre-selected as the ones relevant to this command \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 secrets in grep args. R005: in-repo path only." Cover every rule shown.',
|
|
5730
5769
|
'Rules with preconditions (e.g. "run X before Y") are CONSUMED after the protected action completes. Use the session history timestamps to determine ordering: a precondition satisfied before the last occurrence of the protected action does NOT satisfy the next occurrence. Each new protected action needs its precondition re-satisfied.',
|
|
5731
5770
|
].filter(Boolean).join('\\n');
|
|
@@ -8262,7 +8301,7 @@ function writeConfigEnv(opts) {
|
|
|
8262
8301
|
`SYNKRO_CREDENTIALS_PATH=${shellQuoteSingle(credsPath)}`,
|
|
8263
8302
|
`SYNKRO_TIER=${shellQuoteSingle(safeTier)}`,
|
|
8264
8303
|
`SYNKRO_INFERENCE=${shellQuoteSingle(safeInference)}`,
|
|
8265
|
-
`SYNKRO_VERSION=${shellQuoteSingle("1.6.
|
|
8304
|
+
`SYNKRO_VERSION=${shellQuoteSingle("1.6.45")}`
|
|
8266
8305
|
];
|
|
8267
8306
|
if (safeSynkroBin) lines.push(`SYNKRO_CLI_BIN=${shellQuoteSingle(safeSynkroBin)}`);
|
|
8268
8307
|
if (safeUserId) lines.push(`SYNKRO_USER_ID=${shellQuoteSingle(safeUserId)}`);
|
|
@@ -8439,28 +8478,59 @@ async function installCommand(opts = {}) {
|
|
|
8439
8478
|
const ghToken = null;
|
|
8440
8479
|
setApiBaseUrl(`${gatewayUrl}/api`);
|
|
8441
8480
|
await promptRepoConnection({ linkRepo: opts.linkRepo });
|
|
8481
|
+
const existingSynkro = readFullSynkroFile();
|
|
8442
8482
|
const detected = detectAgents();
|
|
8443
|
-
|
|
8444
|
-
|
|
8445
|
-
|
|
8446
|
-
|
|
8447
|
-
|
|
8448
|
-
|
|
8449
|
-
|
|
8450
|
-
|
|
8451
|
-
|
|
8452
|
-
|
|
8453
|
-
|
|
8454
|
-
|
|
8483
|
+
let agents;
|
|
8484
|
+
let gradingMode;
|
|
8485
|
+
let storageMode;
|
|
8486
|
+
let transcriptConsent = true;
|
|
8487
|
+
if (existingSynkro) {
|
|
8488
|
+
const wantCC = existingSynkro.harness.includes("claude-code");
|
|
8489
|
+
const wantCursor = existingSynkro.harness.includes("cursor");
|
|
8490
|
+
agents = detected.filter(
|
|
8491
|
+
(a) => a.kind === "claude_code" && wantCC || a.kind === "cursor" && wantCursor
|
|
8492
|
+
);
|
|
8493
|
+
if (agents.length === 0 && detected.length > 0) agents = detected;
|
|
8494
|
+
gradingMode = existingSynkro.grader.mode === "byok" ? "byok" : "local";
|
|
8495
|
+
storageMode = "local";
|
|
8496
|
+
console.log(`Using .synkro config:`);
|
|
8497
|
+
console.log(` harness: ${existingSynkro.harness.join(", ")}`);
|
|
8498
|
+
console.log(` grading: ${gradingMode} pool: ${existingSynkro.grader.pool}`);
|
|
8499
|
+
for (const a of agents) {
|
|
8500
|
+
console.log(` \u2713 ${a.name}${a.version ? ` (${a.version})` : ""}`);
|
|
8501
|
+
}
|
|
8502
|
+
console.log();
|
|
8503
|
+
} else {
|
|
8504
|
+
if (detected.length === 0) {
|
|
8505
|
+
console.error("No supported coding agents detected. Install Claude Code or Cursor first.");
|
|
8506
|
+
process.exit(1);
|
|
8507
|
+
}
|
|
8508
|
+
console.log("Detected agents:");
|
|
8509
|
+
for (const a of detected) {
|
|
8510
|
+
console.log(` \u2713 ${a.name}${a.version ? ` (${a.version})` : ""}`);
|
|
8511
|
+
}
|
|
8512
|
+
console.log();
|
|
8513
|
+
agents = await promptAgentSelection(detected);
|
|
8514
|
+
if (agents.length < detected.length) {
|
|
8515
|
+
console.log(`Installing hooks for: ${agents.map((a) => a.name).join(", ")}
|
|
8455
8516
|
`);
|
|
8456
|
-
|
|
8457
|
-
|
|
8458
|
-
|
|
8459
|
-
|
|
8517
|
+
}
|
|
8518
|
+
gradingMode = await promptGradingMode();
|
|
8519
|
+
storageMode = await promptStorageMode();
|
|
8520
|
+
console.log(` grading: ${gradingMode} storage: ${storageMode}
|
|
8460
8521
|
`);
|
|
8461
|
-
|
|
8462
|
-
|
|
8463
|
-
|
|
8522
|
+
if (gradingMode === "byok") {
|
|
8523
|
+
console.log(" BYOK grading uses your own provider key \u2014 register one in the");
|
|
8524
|
+
console.log(" dashboard under Settings \u2192 Provider Keys if you have not already.\n");
|
|
8525
|
+
}
|
|
8526
|
+
if (process.stdin.isTTY) {
|
|
8527
|
+
transcriptConsent = await promptTranscriptConsent();
|
|
8528
|
+
if (transcriptConsent) {
|
|
8529
|
+
console.log(" \u2713 Session import enabled\n");
|
|
8530
|
+
} else {
|
|
8531
|
+
console.log(" \u2717 Session import skipped\n");
|
|
8532
|
+
}
|
|
8533
|
+
}
|
|
8464
8534
|
}
|
|
8465
8535
|
ensureSynkroDir();
|
|
8466
8536
|
const scripts = writeHookScripts();
|
|
@@ -8476,15 +8546,6 @@ async function installCommand(opts = {}) {
|
|
|
8476
8546
|
} catch {
|
|
8477
8547
|
}
|
|
8478
8548
|
}
|
|
8479
|
-
let transcriptConsent = true;
|
|
8480
|
-
if (process.stdin.isTTY) {
|
|
8481
|
-
transcriptConsent = await promptTranscriptConsent();
|
|
8482
|
-
if (transcriptConsent) {
|
|
8483
|
-
console.log(" \u2713 Session import enabled\n");
|
|
8484
|
-
} else {
|
|
8485
|
-
console.log(" \u2717 Session import skipped\n");
|
|
8486
|
-
}
|
|
8487
|
-
}
|
|
8488
8549
|
let hasClaudeCode = false;
|
|
8489
8550
|
let hasCursor = false;
|
|
8490
8551
|
for (const agent of agents) {
|
|
@@ -11325,7 +11386,7 @@ var args = process.argv.slice(2);
|
|
|
11325
11386
|
var cmd = args[0] || "";
|
|
11326
11387
|
var subArgs = args.slice(1);
|
|
11327
11388
|
function printVersion() {
|
|
11328
|
-
console.log("1.6.
|
|
11389
|
+
console.log("1.6.45");
|
|
11329
11390
|
}
|
|
11330
11391
|
function printHelp2() {
|
|
11331
11392
|
console.log(`Synkro CLI \u2014 runtime safety for AI coding agents
|