@synkro-sh/cli 1.6.44 → 1.6.46
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 +171 -38
- 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 ?? [];
|
|
@@ -1280,6 +1280,39 @@ export function effectiveGraderPool(synkroFile: SynkroFileConfig, hookAgentKind:
|
|
|
1280
1280
|
return 'cursor';
|
|
1281
1281
|
}
|
|
1282
1282
|
|
|
1283
|
+
// \u2500\u2500\u2500 .synkro presence (per-repo onboarding) \u2500\u2500\u2500
|
|
1284
|
+
// A repo is "onboarded" to Synkro when it has a .synkro file at its git root.
|
|
1285
|
+
// The hooks are installed globally, so they fire in every repo \u2014 including ones
|
|
1286
|
+
// the user never set up. Without a .synkro there, grading is SKIPPED gracefully
|
|
1287
|
+
// rather than attempting a grade that surfaces a confusing "grader unavailable"
|
|
1288
|
+
// error: the tool call passes through and we show a one-time hint pointing at
|
|
1289
|
+
// "synkro install".
|
|
1290
|
+
export function synkroFilePresent(cwd?: string): boolean {
|
|
1291
|
+
try {
|
|
1292
|
+
const root = cwd ? findGitRoot(cwd) : '';
|
|
1293
|
+
if (!root) return false;
|
|
1294
|
+
return existsSync(root + '/.synkro');
|
|
1295
|
+
} catch { return false; }
|
|
1296
|
+
}
|
|
1297
|
+
|
|
1298
|
+
const NO_SYNKRO_HINT_DIR = join(HOME, '.synkro', '.no-synkro-hint');
|
|
1299
|
+
|
|
1300
|
+
// Returns the onboarding hint the FIRST time a session touches a repo with no
|
|
1301
|
+
// .synkro file, then null on subsequent calls so it doesn't repeat on every
|
|
1302
|
+
// tool call. Caller passes null straight through to outputEmpty().
|
|
1303
|
+
export function noSynkroSkipMessage(sessionId: string): string | null {
|
|
1304
|
+
try {
|
|
1305
|
+
mkdirSync(NO_SYNKRO_HINT_DIR, { recursive: true });
|
|
1306
|
+
const key = (sessionId || 'nosession').replace(/[^a-zA-Z0-9_-]/g, '_');
|
|
1307
|
+
const marker = join(NO_SYNKRO_HINT_DIR, key);
|
|
1308
|
+
if (existsSync(marker)) return null;
|
|
1309
|
+
writeFileSync(marker, '', { flag: 'w' });
|
|
1310
|
+
} catch {
|
|
1311
|
+
// best-effort dedup \u2014 if the marker can't be written, still show the hint
|
|
1312
|
+
}
|
|
1313
|
+
return '[synkro] No .synkro config in this repo \u2014 grading skipped. Run \`synkro install\` here to enable Synkro.';
|
|
1314
|
+
}
|
|
1315
|
+
|
|
1283
1316
|
// \u2500\u2500\u2500 Channel Health \u2500\u2500\u2500
|
|
1284
1317
|
|
|
1285
1318
|
export async function channelUp(port = 18929): Promise<boolean> {
|
|
@@ -3218,6 +3251,7 @@ export function isCursorInvokingCcHook(agentKind: string, model: string): boolea
|
|
|
3218
3251
|
}
|
|
3219
3252
|
|
|
3220
3253
|
let cursorHookExited = false;
|
|
3254
|
+
let hookFinalized = false;
|
|
3221
3255
|
|
|
3222
3256
|
export function setupCursorHookSignals(): void {
|
|
3223
3257
|
if (!isCursorHookFormat()) return;
|
|
@@ -3226,9 +3260,28 @@ export function setupCursorHookSignals(): void {
|
|
|
3226
3260
|
|
|
3227
3261
|
function cursorHookExit(): never {
|
|
3228
3262
|
cursorHookExited = true;
|
|
3263
|
+
hookFinalized = true;
|
|
3229
3264
|
process.exit(0);
|
|
3230
3265
|
}
|
|
3231
3266
|
|
|
3267
|
+
// Self-imposed deadline. CC/Cursor kill the hook process at the configured hook
|
|
3268
|
+
// timeout with a signal; if a grade fetch is still in flight when that happens,
|
|
3269
|
+
// the runtime dumps the in-flight request body (the grader prompt) to stderr and
|
|
3270
|
+
// exits non-zero \u2014 surfacing as a "hook error" toast with leaked prompt text.
|
|
3271
|
+
// To prevent that, each grading hook arms this watchdog a few seconds UNDER its
|
|
3272
|
+
// configured budget: when it fires we emit a clean empty result and exit 0
|
|
3273
|
+
// ourselves, so the harness never has to kill us. The timer is unref'd, so a
|
|
3274
|
+
// hook that finishes early exits normally and the watchdog never runs.
|
|
3275
|
+
export function installHookWatchdog(budgetMs: number): void {
|
|
3276
|
+
const t = setTimeout(() => {
|
|
3277
|
+
if (hookFinalized) return;
|
|
3278
|
+
hookFinalized = true;
|
|
3279
|
+
try { process.stdout.write('{}\\n'); } catch {}
|
|
3280
|
+
process.exit(0);
|
|
3281
|
+
}, budgetMs);
|
|
3282
|
+
if (typeof (t as any).unref === 'function') (t as any).unref();
|
|
3283
|
+
}
|
|
3284
|
+
|
|
3232
3285
|
// \u2500\u2500\u2500 Grader-unavailable diagnostic log \u2500\u2500\u2500
|
|
3233
3286
|
// Records every time a hook tried to call the local grader and fell open
|
|
3234
3287
|
// because the call failed. JSONL at ~/.synkro/grader-unavailable.log so the
|
|
@@ -3308,6 +3361,8 @@ export function outputJson(obj: any): void {
|
|
|
3308
3361
|
outputEmpty();
|
|
3309
3362
|
return;
|
|
3310
3363
|
}
|
|
3364
|
+
if (hookFinalized) return;
|
|
3365
|
+
hookFinalized = true;
|
|
3311
3366
|
console.log(JSON.stringify(obj));
|
|
3312
3367
|
}
|
|
3313
3368
|
|
|
@@ -3319,6 +3374,8 @@ export function outputEmpty(): void {
|
|
|
3319
3374
|
}
|
|
3320
3375
|
cursorHookExit();
|
|
3321
3376
|
}
|
|
3377
|
+
if (hookFinalized) return;
|
|
3378
|
+
hookFinalized = true;
|
|
3322
3379
|
console.log('{}');
|
|
3323
3380
|
}
|
|
3324
3381
|
`;
|
|
@@ -3328,10 +3385,10 @@ import {
|
|
|
3328
3385
|
parseVerdict, dispatchCapture, ruleMode, reconstructContent, isPathUnder, postWithRetry,
|
|
3329
3386
|
readStdin, extractTranscript, readLastPrompt, findNearestDeps, filePathFromToolInput,
|
|
3330
3387
|
appendSessionAction, readSessionLog, compressSessionLog, log,
|
|
3331
|
-
outputJson, outputEmpty, setupCursorHookSignals, isEditTool, hookSessionId, GATEWAY_URL,
|
|
3388
|
+
outputJson, outputEmpty, setupCursorHookSignals, installHookWatchdog, isEditTool, hookSessionId, GATEWAY_URL,
|
|
3332
3389
|
logGraderUnavailable, graderUnavailableMessage, filterRules, ruleFilterText, normalizeMode, countEditLineDelta,
|
|
3333
3390
|
captureLineMetrics, cursorModelFromPayload, resolveTranscriptPath, isCursorInvokingCcHook,
|
|
3334
|
-
loadSynkroFile, effectiveGraderPool,
|
|
3391
|
+
loadSynkroFile, effectiveGraderPool, synkroFilePresent, noSynkroSkipMessage,
|
|
3335
3392
|
type HookConfig, type Rule,
|
|
3336
3393
|
} from './_synkro-common.ts';
|
|
3337
3394
|
import { existsSync, readFileSync } from 'node:fs';
|
|
@@ -3341,6 +3398,7 @@ const agentKind = (process.env.SYNKRO_HOOK_FORMAT === 'cursor' || process.argv.i
|
|
|
3341
3398
|
|
|
3342
3399
|
async function main() {
|
|
3343
3400
|
setupCursorHookSignals();
|
|
3401
|
+
installHookWatchdog(27000);
|
|
3344
3402
|
try {
|
|
3345
3403
|
const input = await readStdin();
|
|
3346
3404
|
if (!input.trim()) { outputEmpty(); return; }
|
|
@@ -3365,6 +3423,13 @@ async function main() {
|
|
|
3365
3423
|
|
|
3366
3424
|
if (filePath.includes('/.synkro/hooks/')) { outputEmpty(); return; }
|
|
3367
3425
|
|
|
3426
|
+
if (!synkroFilePresent(cwd)) {
|
|
3427
|
+
const _skipMsg = noSynkroSkipMessage(sessionId);
|
|
3428
|
+
if (_skipMsg) outputJson({ systemMessage: _skipMsg, hookSpecificOutput: { hookEventName: 'PreToolUse', additionalContext: _skipMsg } });
|
|
3429
|
+
else outputEmpty();
|
|
3430
|
+
return;
|
|
3431
|
+
}
|
|
3432
|
+
|
|
3368
3433
|
const fileShort = basename(filePath);
|
|
3369
3434
|
log('editGuard checking: ' + fileShort);
|
|
3370
3435
|
|
|
@@ -3463,7 +3528,7 @@ async function main() {
|
|
|
3463
3528
|
'Last user prompt: ' + (lastPrompt || 'none'),
|
|
3464
3529
|
'Org rules: ' + JSON.stringify(relevantRules),
|
|
3465
3530
|
'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.',
|
|
3466
|
-
'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.
|
|
3531
|
+
'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.',
|
|
3467
3532
|
'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.',
|
|
3468
3533
|
].join('\\n');
|
|
3469
3534
|
|
|
@@ -3585,10 +3650,10 @@ main();
|
|
|
3585
3650
|
import {
|
|
3586
3651
|
loadJwt, ensureFreshJwt, detectRepo, loadConfig, cweRoute, tag,
|
|
3587
3652
|
localGradeCwe, parseVerdict, reconstructContent, readStdin, log,
|
|
3588
|
-
outputJson, outputEmpty, setupCursorHookSignals, isEditTool, isShellTool, isCursorHookFormat,
|
|
3653
|
+
outputJson, outputEmpty, setupCursorHookSignals, installHookWatchdog, isEditTool, isShellTool, isCursorHookFormat,
|
|
3589
3654
|
extractShellCodeWrites, hookSessionId, filePathFromToolInput, emitBlockScanFindings, dispatchFinding, dispatchCapture, GATEWAY_URL,
|
|
3590
3655
|
logGraderUnavailable, graderUnavailableMessage, resolveTranscriptPath, isCursorInvokingCcHook,
|
|
3591
|
-
loadSynkroFile, effectiveGraderPool,
|
|
3656
|
+
loadSynkroFile, effectiveGraderPool, synkroFilePresent, noSynkroSkipMessage,
|
|
3592
3657
|
} from './_synkro-common.ts';
|
|
3593
3658
|
import { basename, extname, resolve, join, dirname } from 'node:path';
|
|
3594
3659
|
import { readFileSync, readdirSync, existsSync } from 'node:fs';
|
|
@@ -3725,6 +3790,7 @@ function scanPackageCapabilities(pkgName: string, cwd: string): PackageCapabilit
|
|
|
3725
3790
|
|
|
3726
3791
|
async function main() {
|
|
3727
3792
|
setupCursorHookSignals();
|
|
3793
|
+
installHookWatchdog(27000);
|
|
3728
3794
|
try {
|
|
3729
3795
|
const input = await readStdin();
|
|
3730
3796
|
if (!input.trim()) { outputEmpty(); return; }
|
|
@@ -3741,6 +3807,13 @@ async function main() {
|
|
|
3741
3807
|
|
|
3742
3808
|
if (isCursorInvokingCcHook(agentKind, ccModel)) { outputEmpty(); return; }
|
|
3743
3809
|
|
|
3810
|
+
if (!synkroFilePresent(cwd)) {
|
|
3811
|
+
const _skipMsg = noSynkroSkipMessage(sessionId);
|
|
3812
|
+
if (_skipMsg) outputJson({ systemMessage: _skipMsg, hookSpecificOutput: { hookEventName: 'PreToolUse', additionalContext: _skipMsg } });
|
|
3813
|
+
else outputEmpty();
|
|
3814
|
+
return;
|
|
3815
|
+
}
|
|
3816
|
+
|
|
3744
3817
|
const targets: CweScanTarget[] = [];
|
|
3745
3818
|
|
|
3746
3819
|
if (isCursorHookFormat() && (shellCommand || isShellTool(toolName))) {
|
|
@@ -4147,7 +4220,7 @@ import {
|
|
|
4147
4220
|
loadJwt, ensureFreshJwt, detectRepo, loadConfig, route, tag,
|
|
4148
4221
|
reconstructContent, readStdin, findNearestDeps, filePathFromToolInput, log,
|
|
4149
4222
|
outputJson, outputEmpty, setupCursorHookSignals, isEditTool, hookSessionId, dispatchFinding, dispatchCapture, dispatchScanResult, extractTranscript, emitBlockScanFindings, resolveTranscriptPath, GATEWAY_URL,
|
|
4150
|
-
isCursorHookFormat,
|
|
4223
|
+
isCursorHookFormat, synkroFilePresent, noSynkroSkipMessage,
|
|
4151
4224
|
} from './_synkro-common.ts';
|
|
4152
4225
|
import { basename } from 'node:path';
|
|
4153
4226
|
import { readFileSync } from 'node:fs';
|
|
@@ -4193,6 +4266,13 @@ async function main() {
|
|
|
4193
4266
|
|
|
4194
4267
|
if (filePath.includes('/.synkro/hooks/')) { outputEmpty(); return; }
|
|
4195
4268
|
|
|
4269
|
+
if (!synkroFilePresent(cwd)) {
|
|
4270
|
+
const _skipMsg = noSynkroSkipMessage(sessionId);
|
|
4271
|
+
if (_skipMsg) outputJson({ systemMessage: _skipMsg, hookSpecificOutput: { hookEventName: 'PreToolUse', additionalContext: _skipMsg } });
|
|
4272
|
+
else outputEmpty();
|
|
4273
|
+
return;
|
|
4274
|
+
}
|
|
4275
|
+
|
|
4196
4276
|
const fileShort = basename(filePath);
|
|
4197
4277
|
|
|
4198
4278
|
let jwt = loadJwt();
|
|
@@ -4429,7 +4509,7 @@ import {
|
|
|
4429
4509
|
loadJwt, ensureFreshJwt, detectRepo, loadConfig, route, tag,
|
|
4430
4510
|
readStdin, runInstallScan, emitBlockScanFindings, dispatchCapture, dispatchScanResult, hashCommand,
|
|
4431
4511
|
outputJson, outputEmpty, setupCursorHookSignals, isShellTool, hookSessionId, log, GATEWAY_URL,
|
|
4432
|
-
resolveTranscriptPath, isCursorHookFormat, loadSynkroFile, effectiveGraderPool,
|
|
4512
|
+
resolveTranscriptPath, isCursorHookFormat, loadSynkroFile, effectiveGraderPool, synkroFilePresent, noSynkroSkipMessage,
|
|
4433
4513
|
} from './_synkro-common.ts';
|
|
4434
4514
|
import { writeFileSync, mkdirSync } from 'node:fs';
|
|
4435
4515
|
import { join } from 'node:path';
|
|
@@ -4454,6 +4534,14 @@ async function main() {
|
|
|
4454
4534
|
// beforeShellExecution supplies command directly; preToolUse uses tool_name + tool_input.
|
|
4455
4535
|
if (!isShellTool(toolName) && typeof payload.command !== 'string') { outputEmpty(); return; }
|
|
4456
4536
|
|
|
4537
|
+
const _cwd = payload.cwd || (Array.isArray(payload.workspace_roots) ? payload.workspace_roots[0] : '') || '';
|
|
4538
|
+
if (!synkroFilePresent(_cwd)) {
|
|
4539
|
+
const _skipMsg = noSynkroSkipMessage(hookSessionId(payload));
|
|
4540
|
+
if (_skipMsg) outputJson({ systemMessage: _skipMsg, hookSpecificOutput: { hookEventName: 'PreToolUse', additionalContext: _skipMsg } });
|
|
4541
|
+
else outputEmpty();
|
|
4542
|
+
return;
|
|
4543
|
+
}
|
|
4544
|
+
|
|
4457
4545
|
let jwt = loadJwt();
|
|
4458
4546
|
if (!jwt) { outputEmpty(); return; }
|
|
4459
4547
|
jwt = await ensureFreshJwt(jwt);
|
|
@@ -4548,9 +4636,9 @@ import {
|
|
|
4548
4636
|
loadJwt, ensureFreshJwt, detectRepo, loadConfig, route, tag, localGrade,
|
|
4549
4637
|
parseVerdict, dispatchCapture, dispatchFinding, ruleMode, postWithRetry, readStdin,
|
|
4550
4638
|
extractTranscript, readLastPrompt, appendSessionAction, readSessionLog, compressSessionLog, log,
|
|
4551
|
-
outputJson, outputEmpty, setupCursorHookSignals, isShellTool, hookSessionId, GATEWAY_URL,
|
|
4639
|
+
outputJson, outputEmpty, setupCursorHookSignals, installHookWatchdog, isShellTool, hookSessionId, GATEWAY_URL,
|
|
4552
4640
|
logGraderUnavailable, graderUnavailableMessage, filterRules, ruleFilterText, normalizeMode, appendLocalTelemetry, isSafeInRepoRead,
|
|
4553
|
-
loadSynkroFile, effectiveGraderPool,
|
|
4641
|
+
loadSynkroFile, effectiveGraderPool, synkroFilePresent, noSynkroSkipMessage,
|
|
4554
4642
|
hashCommand, resolveTranscriptPath, isCursorHookFormat,
|
|
4555
4643
|
type HookConfig, type Rule,
|
|
4556
4644
|
} from './_synkro-common.ts';
|
|
@@ -4591,6 +4679,7 @@ function isDuplicate(command: string, sessionId: string): boolean {
|
|
|
4591
4679
|
|
|
4592
4680
|
async function main() {
|
|
4593
4681
|
setupCursorHookSignals();
|
|
4682
|
+
installHookWatchdog(27000);
|
|
4594
4683
|
try {
|
|
4595
4684
|
const input = await readStdin();
|
|
4596
4685
|
if (!input.trim()) { outputEmpty(); return; }
|
|
@@ -4630,6 +4719,13 @@ async function main() {
|
|
|
4630
4719
|
|
|
4631
4720
|
const gitRepo = detectRepo(cwd, transcriptPath, command, workspaceRoots);
|
|
4632
4721
|
|
|
4722
|
+
if (!synkroFilePresent(cwd)) {
|
|
4723
|
+
const _skipMsg = noSynkroSkipMessage(sessionId);
|
|
4724
|
+
if (_skipMsg) outputJson({ systemMessage: _skipMsg, hookSpecificOutput: { hookEventName: 'PreToolUse', additionalContext: _skipMsg } });
|
|
4725
|
+
else outputEmpty();
|
|
4726
|
+
return;
|
|
4727
|
+
}
|
|
4728
|
+
|
|
4633
4729
|
if (isDuplicate(command, sessionId)) {
|
|
4634
4730
|
log('bashGuard skip (dedup): ' + command.slice(0, 80));
|
|
4635
4731
|
outputEmpty();
|
|
@@ -4722,7 +4818,7 @@ async function main() {
|
|
|
4722
4818
|
'Org rules: ' + JSON.stringify(relevantRules),
|
|
4723
4819
|
scanConcern,
|
|
4724
4820
|
'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.',
|
|
4725
|
-
'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.
|
|
4821
|
+
'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.',
|
|
4726
4822
|
'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.',
|
|
4727
4823
|
'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.',
|
|
4728
4824
|
].filter(Boolean).join('\\n');
|
|
@@ -4845,9 +4941,9 @@ import {
|
|
|
4845
4941
|
loadJwt, ensureFreshJwt, detectRepo, loadConfig, route, tag, localGrade,
|
|
4846
4942
|
parseVerdict, dispatchCapture, ruleMode, postWithRetry, readStdin,
|
|
4847
4943
|
extractTranscript, readLastPrompt, appendSessionAction, readSessionLog, compressSessionLog, log,
|
|
4848
|
-
outputJson, outputEmpty, setupCursorHookSignals, isAgentTool, hookSessionId, GATEWAY_URL,
|
|
4944
|
+
outputJson, outputEmpty, setupCursorHookSignals, installHookWatchdog, isAgentTool, hookSessionId, GATEWAY_URL,
|
|
4849
4945
|
logGraderUnavailable, graderUnavailableMessage, filterRules, normalizeMode, resolveTranscriptPath, isCursorInvokingCcHook,
|
|
4850
|
-
loadSynkroFile, effectiveGraderPool,
|
|
4946
|
+
loadSynkroFile, effectiveGraderPool, synkroFilePresent, noSynkroSkipMessage,
|
|
4851
4947
|
type HookConfig, type Rule,
|
|
4852
4948
|
} from './_synkro-common.ts';
|
|
4853
4949
|
|
|
@@ -4855,6 +4951,7 @@ const agentKind = (process.env.SYNKRO_HOOK_FORMAT === 'cursor' || process.argv.i
|
|
|
4855
4951
|
|
|
4856
4952
|
async function main() {
|
|
4857
4953
|
setupCursorHookSignals();
|
|
4954
|
+
installHookWatchdog(27000);
|
|
4858
4955
|
try {
|
|
4859
4956
|
const input = await readStdin();
|
|
4860
4957
|
if (!input.trim()) { outputEmpty(); return; }
|
|
@@ -4877,6 +4974,13 @@ async function main() {
|
|
|
4877
4974
|
const permissionMode = payload.permission_mode || '';
|
|
4878
4975
|
const transcriptPath = resolveTranscriptPath(payload);
|
|
4879
4976
|
|
|
4977
|
+
if (!synkroFilePresent(cwd)) {
|
|
4978
|
+
const _skipMsg = noSynkroSkipMessage(sessionId);
|
|
4979
|
+
if (_skipMsg) outputJson({ systemMessage: _skipMsg, hookSpecificOutput: { hookEventName: 'PreToolUse', additionalContext: _skipMsg } });
|
|
4980
|
+
else outputEmpty();
|
|
4981
|
+
return;
|
|
4982
|
+
}
|
|
4983
|
+
|
|
4880
4984
|
const prompt = toolInput.prompt || '';
|
|
4881
4985
|
const description = toolInput.description || '';
|
|
4882
4986
|
const subagentType = toolInput.subagent_type || 'general-purpose';
|
|
@@ -4934,7 +5038,7 @@ async function main() {
|
|
|
4934
5038
|
'Last user prompt: ' + (lastPrompt || 'none'),
|
|
4935
5039
|
'Org rules: ' + JSON.stringify(relevantRules),
|
|
4936
5040
|
'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.',
|
|
4937
|
-
'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.
|
|
5041
|
+
'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.',
|
|
4938
5042
|
].filter(Boolean).join('\\n');
|
|
4939
5043
|
|
|
4940
5044
|
let gradeResp: string;
|
|
@@ -5032,9 +5136,9 @@ main();
|
|
|
5032
5136
|
import {
|
|
5033
5137
|
loadJwt, ensureFreshJwt, detectRepo, loadConfig, route, tag, localGrade,
|
|
5034
5138
|
parseVerdict, dispatchCapture, appendSessionAction, readSessionLog, compressSessionLog, postWithRetry, readStdin, log,
|
|
5035
|
-
outputJson, outputEmpty, setupCursorHookSignals, isPlanTool, hookSessionId, GATEWAY_URL,
|
|
5139
|
+
outputJson, outputEmpty, setupCursorHookSignals, installHookWatchdog, isPlanTool, hookSessionId, GATEWAY_URL,
|
|
5036
5140
|
filterRules, graderUnavailableMessage, resolveTranscriptPath, isCursorInvokingCcHook,
|
|
5037
|
-
loadSynkroFile, effectiveGraderPool,
|
|
5141
|
+
loadSynkroFile, effectiveGraderPool, synkroFilePresent, noSynkroSkipMessage,
|
|
5038
5142
|
} from './_synkro-common.ts';
|
|
5039
5143
|
import { existsSync, readFileSync, writeFileSync, readdirSync, statSync } from 'node:fs';
|
|
5040
5144
|
import { join } from 'node:path';
|
|
@@ -5084,6 +5188,7 @@ function appendReviewToPlan(planFile: string, verdict: string): void {
|
|
|
5084
5188
|
|
|
5085
5189
|
async function main() {
|
|
5086
5190
|
setupCursorHookSignals();
|
|
5191
|
+
installHookWatchdog(42000);
|
|
5087
5192
|
try {
|
|
5088
5193
|
const input = await readStdin();
|
|
5089
5194
|
if (!input.trim()) { outputEmpty(); return; }
|
|
@@ -5106,6 +5211,13 @@ async function main() {
|
|
|
5106
5211
|
const transcriptPath = resolveTranscriptPath(payload);
|
|
5107
5212
|
const gitRepo = detectRepo(cwd, transcriptPath, plan, workspaceRoots);
|
|
5108
5213
|
|
|
5214
|
+
if (!synkroFilePresent(cwd)) {
|
|
5215
|
+
const _skipMsg = noSynkroSkipMessage(sessionId);
|
|
5216
|
+
if (_skipMsg) outputJson({ systemMessage: _skipMsg, hookSpecificOutput: { hookEventName: 'PreToolUse', additionalContext: _skipMsg } });
|
|
5217
|
+
else outputEmpty();
|
|
5218
|
+
return;
|
|
5219
|
+
}
|
|
5220
|
+
|
|
5109
5221
|
appendSessionAction(sessionId, { ts: new Date().toISOString(), tool: 'ExitPlanMode', summary: 'plan review: ' + plan.slice(0, 80) });
|
|
5110
5222
|
|
|
5111
5223
|
const planShort = plan.slice(0, 80);
|
|
@@ -5220,7 +5332,7 @@ main();
|
|
|
5220
5332
|
import {
|
|
5221
5333
|
loadJwt, detectRepo, loadConfig, tag, readStdin, aggregateUsage, cleanupSessionLog,
|
|
5222
5334
|
outputJson, outputEmpty, shipCloud, setupCursorHookSignals, hookSessionId, GATEWAY_URL,
|
|
5223
|
-
resolveTranscriptPath, emitUsageTick, cursorModelFromPayload, log,
|
|
5335
|
+
resolveTranscriptPath, emitUsageTick, cursorModelFromPayload, log, synkroFilePresent,
|
|
5224
5336
|
} from './_synkro-common.ts';
|
|
5225
5337
|
|
|
5226
5338
|
async function main() {
|
|
@@ -5235,6 +5347,7 @@ async function main() {
|
|
|
5235
5347
|
|
|
5236
5348
|
const workspaceRoots = Array.isArray(payload.workspace_roots) ? payload.workspace_roots as string[] : [];
|
|
5237
5349
|
const cwd = payload.cwd || workspaceRoots[0] || '';
|
|
5350
|
+
if (!synkroFilePresent(cwd)) { outputEmpty(); return; }
|
|
5238
5351
|
const transcriptPath = resolveTranscriptPath(payload);
|
|
5239
5352
|
const gitRepo = detectRepo(cwd, transcriptPath, '', workspaceRoots);
|
|
5240
5353
|
const modelFallback = cursorModelFromPayload(payload);
|
|
@@ -5288,7 +5401,7 @@ main();
|
|
|
5288
5401
|
import {
|
|
5289
5402
|
loadJwt, detectRepo, channelUp, tag, readStdin, writeCachedRepo,
|
|
5290
5403
|
outputJson, outputEmpty, setupCursorHookSignals, hookSessionId, resolveTranscriptPath, GATEWAY_URL,
|
|
5291
|
-
isLocalStorageMode, loadSynkroFile, log, type HookConfig,
|
|
5404
|
+
isLocalStorageMode, loadSynkroFile, log, synkroFilePresent, type HookConfig,
|
|
5292
5405
|
} from './_synkro-common.ts';
|
|
5293
5406
|
|
|
5294
5407
|
async function main() {
|
|
@@ -5300,6 +5413,7 @@ async function main() {
|
|
|
5300
5413
|
const payload = JSON.parse(input);
|
|
5301
5414
|
const workspaceRoots = Array.isArray(payload.workspace_roots) ? payload.workspace_roots as string[] : [];
|
|
5302
5415
|
const cwd = payload.cwd || workspaceRoots[0] || '';
|
|
5416
|
+
if (!synkroFilePresent(cwd)) { outputEmpty(); return; }
|
|
5303
5417
|
const transcriptPath = resolveTranscriptPath(payload);
|
|
5304
5418
|
const sessionId = hookSessionId(payload);
|
|
5305
5419
|
const gitRepo = detectRepo(cwd, transcriptPath, '', workspaceRoots);
|
|
@@ -5379,7 +5493,7 @@ main();
|
|
|
5379
5493
|
import {
|
|
5380
5494
|
loadJwt, loadConfig, readStdin, hashCommand, consentGrant, consentHasActive, consentConsume,
|
|
5381
5495
|
appendSessionAction,
|
|
5382
|
-
outputEmpty, appendLocalTelemetry, shipCloud, setupCursorHookSignals, isShellTool, hookSessionId, GATEWAY_URL,
|
|
5496
|
+
outputEmpty, appendLocalTelemetry, shipCloud, setupCursorHookSignals, isShellTool, hookSessionId, GATEWAY_URL, synkroFilePresent,
|
|
5383
5497
|
} from './_synkro-common.ts';
|
|
5384
5498
|
|
|
5385
5499
|
async function main() {
|
|
@@ -5389,6 +5503,8 @@ async function main() {
|
|
|
5389
5503
|
if (!input.trim()) { outputEmpty(); return; }
|
|
5390
5504
|
|
|
5391
5505
|
const payload = JSON.parse(input);
|
|
5506
|
+
const _cwd = payload.cwd || (Array.isArray(payload.workspace_roots) ? payload.workspace_roots[0] : '') || '';
|
|
5507
|
+
if (!synkroFilePresent(_cwd)) { outputEmpty(); return; }
|
|
5392
5508
|
const toolName = payload.tool_name || '';
|
|
5393
5509
|
const shellCmd = typeof payload.command === 'string' ? payload.command : (payload.tool_input?.command || '');
|
|
5394
5510
|
if (!isShellTool(toolName) && !shellCmd) { outputEmpty(); return; }
|
|
@@ -5443,7 +5559,7 @@ main();
|
|
|
5443
5559
|
import {
|
|
5444
5560
|
loadJwt, detectRepo, readStdin, aggregateUsage, appendLocalTelemetry,
|
|
5445
5561
|
outputEmpty, setupCursorHookSignals, hookSessionId, GATEWAY_URL, readSessionLog, shipCloud,
|
|
5446
|
-
resolveTranscriptPath, syncConversationTranscript, emitUsageTick, cursorModelFromPayload,
|
|
5562
|
+
resolveTranscriptPath, syncConversationTranscript, emitUsageTick, cursorModelFromPayload, synkroFilePresent,
|
|
5447
5563
|
} from './_synkro-common.ts';
|
|
5448
5564
|
import { readFileSync } from 'node:fs';
|
|
5449
5565
|
|
|
@@ -5457,6 +5573,7 @@ async function main() {
|
|
|
5457
5573
|
const sessionId = hookSessionId(payload);
|
|
5458
5574
|
const workspaceRoots = Array.isArray(payload.workspace_roots) ? payload.workspace_roots as string[] : [];
|
|
5459
5575
|
const cwd = payload.cwd || workspaceRoots[0] || '';
|
|
5576
|
+
if (!synkroFilePresent(cwd)) { outputEmpty(); return; }
|
|
5460
5577
|
const transcriptPath = resolveTranscriptPath(payload);
|
|
5461
5578
|
|
|
5462
5579
|
if (!sessionId || !transcriptPath) {
|
|
@@ -5502,7 +5619,7 @@ async function main() {
|
|
|
5502
5619
|
main();
|
|
5503
5620
|
`;
|
|
5504
5621
|
USER_PROMPT_SUBMIT_TS = `#!/usr/bin/env bun
|
|
5505
|
-
import { readStdin, appendLocalTelemetry, aggregateUsage, outputEmpty, setupCursorHookSignals, hookSessionId, detectRepo, resolveTranscriptPath, syncConversationTranscript, emitUsageTick, cursorModelFromPayload } from './_synkro-common.ts';
|
|
5622
|
+
import { readStdin, appendLocalTelemetry, aggregateUsage, outputEmpty, setupCursorHookSignals, hookSessionId, detectRepo, resolveTranscriptPath, syncConversationTranscript, emitUsageTick, cursorModelFromPayload, synkroFilePresent } from './_synkro-common.ts';
|
|
5506
5623
|
import { writeFileSync, mkdirSync } from 'node:fs';
|
|
5507
5624
|
import { join, dirname } from 'node:path';
|
|
5508
5625
|
import { homedir } from 'node:os';
|
|
@@ -5513,7 +5630,12 @@ async function main() {
|
|
|
5513
5630
|
const input = await readStdin();
|
|
5514
5631
|
if (!input.trim()) { outputEmpty(); return; }
|
|
5515
5632
|
const payload = JSON.parse(input);
|
|
5516
|
-
const
|
|
5633
|
+
const _cwd = typeof payload.cwd === 'string' ? payload.cwd
|
|
5634
|
+
: (Array.isArray(payload.workspace_roots) && typeof payload.workspace_roots[0] === 'string' ? payload.workspace_roots[0] : '');
|
|
5635
|
+
if (!synkroFilePresent(_cwd)) { outputEmpty(); return; }
|
|
5636
|
+
const msg = typeof payload.message === 'string' ? payload.message
|
|
5637
|
+
: typeof payload.prompt === 'string' ? payload.prompt
|
|
5638
|
+
: typeof payload.content === 'string' ? payload.content : '';
|
|
5517
5639
|
if (msg) {
|
|
5518
5640
|
const promptFile = join(homedir(), '.synkro', '.last-prompt');
|
|
5519
5641
|
mkdirSync(dirname(promptFile), { recursive: true, mode: 0o700 });
|
|
@@ -5552,7 +5674,7 @@ import {
|
|
|
5552
5674
|
isSafeInRepoRead, resolveTranscriptPath, postWithRetry, readStdin, hashCommand,
|
|
5553
5675
|
extractTranscript, readLastPrompt, readSessionLog, compressSessionLog,
|
|
5554
5676
|
appendLocalTelemetry, logGraderUnavailable, graderUnavailableMessage, log, GATEWAY_URL,
|
|
5555
|
-
loadSynkroFile, effectiveGraderPool,
|
|
5677
|
+
loadSynkroFile, effectiveGraderPool, synkroFilePresent,
|
|
5556
5678
|
type Rule,
|
|
5557
5679
|
} from './_synkro-common.ts';
|
|
5558
5680
|
import { createHash } from 'node:crypto';
|
|
@@ -5590,8 +5712,10 @@ function isDuplicate(command: string, sessionId: string): boolean {
|
|
|
5590
5712
|
return false;
|
|
5591
5713
|
}
|
|
5592
5714
|
|
|
5593
|
-
// Cursor
|
|
5594
|
-
|
|
5715
|
+
// Cursor hook timeouts now match CC (30s for bash/edit/agent), so use the same
|
|
5716
|
+
// 24s internal grade budget as the CC path \u2014 stays under the 30s hook timeout
|
|
5717
|
+
// (JWT refresh + grade) and fails open cleanly via the caller's catch.
|
|
5718
|
+
const CURSOR_GRADE_TIMEOUT_MS = 24000;
|
|
5595
5719
|
const CURSOR_CLOUD_TIMEOUT_MS = 9000;
|
|
5596
5720
|
|
|
5597
5721
|
let hookDone = false;
|
|
@@ -5668,6 +5792,10 @@ async function main() {
|
|
|
5668
5792
|
const model = rawModel ? (rawModel.startsWith('cursor/') ? rawModel : 'cursor/' + rawModel) : 'cursor';
|
|
5669
5793
|
const repo = detectRepo(cwd, transcriptPath, command, workspaceRoots);
|
|
5670
5794
|
|
|
5795
|
+
// No .synkro at the resolved repo root \u2192 Synkro is dormant here; allow
|
|
5796
|
+
// without grading. Keyed off the validated detectRepo() root, not raw cwd.
|
|
5797
|
+
if (!repo || !synkroFilePresent(repo)) finishAllow();
|
|
5798
|
+
|
|
5671
5799
|
const cmdShort = command.slice(0, 80);
|
|
5672
5800
|
log('bashGuard checking: ' + cmdShort);
|
|
5673
5801
|
|
|
@@ -5733,7 +5861,7 @@ async function main() {
|
|
|
5733
5861
|
'Org rules: ' + JSON.stringify(relevantRules),
|
|
5734
5862
|
scanConcern,
|
|
5735
5863
|
'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.',
|
|
5736
|
-
'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.
|
|
5864
|
+
'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.',
|
|
5737
5865
|
'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.',
|
|
5738
5866
|
'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.',
|
|
5739
5867
|
].filter(Boolean).join('\\n');
|
|
@@ -5851,7 +5979,7 @@ import {
|
|
|
5851
5979
|
loadJwt, ensureFreshJwt, detectRepo, readStdin, resolveTranscriptPath,
|
|
5852
5980
|
appendSessionAction, appendLocalTelemetry, shipCloud, log, GATEWAY_URL,
|
|
5853
5981
|
countEditLineDelta, dispatchCapture, hookSessionId, cursorModelFromPayload,
|
|
5854
|
-
isLocalStorageMode,
|
|
5982
|
+
isLocalStorageMode, synkroFilePresent, isPathUnder,
|
|
5855
5983
|
} from './_synkro-common.ts';
|
|
5856
5984
|
import { existsSync, readFileSync } from 'node:fs';
|
|
5857
5985
|
import { basename, dirname, join } from 'node:path';
|
|
@@ -5877,6 +6005,10 @@ async function main() {
|
|
|
5877
6005
|
const filePath = String(payload.file_path || payload.path || payload.target_file || '');
|
|
5878
6006
|
if (!filePath) finish();
|
|
5879
6007
|
|
|
6008
|
+
// No .synkro at the resolved repo root \u2192 Synkro is dormant here; skip capture.
|
|
6009
|
+
const _root = detectRepo((typeof payload.cwd === 'string' && payload.cwd) || dirname(filePath), '', filePath, []);
|
|
6010
|
+
if (!_root || !isPathUnder(filePath, _root) || !synkroFilePresent(_root)) finish();
|
|
6011
|
+
|
|
5880
6012
|
const workspaceRoots = Array.isArray(payload.workspace_roots)
|
|
5881
6013
|
? (payload.workspace_roots as unknown[]).filter((r): r is string => typeof r === 'string')
|
|
5882
6014
|
: [];
|
|
@@ -5979,7 +6111,7 @@ main().catch((e) => {
|
|
|
5979
6111
|
/** Capture Cursor agent thinking/response text before transcript JSONL redacts it. */
|
|
5980
6112
|
import {
|
|
5981
6113
|
readStdin, outputEmpty, setupCursorHookSignals, hookSessionId, detectRepo,
|
|
5982
|
-
appendThoughtOverlay, pushConversationMessage,
|
|
6114
|
+
appendThoughtOverlay, pushConversationMessage, synkroFilePresent,
|
|
5983
6115
|
} from './_synkro-common.ts';
|
|
5984
6116
|
|
|
5985
6117
|
async function main() {
|
|
@@ -5999,6 +6131,7 @@ async function main() {
|
|
|
5999
6131
|
? payload.workspace_roots.filter((r): r is string => typeof r === 'string')
|
|
6000
6132
|
: [];
|
|
6001
6133
|
const cwd = (typeof payload.cwd === 'string' && payload.cwd) || workspaceRoots[0] || '';
|
|
6134
|
+
if (!synkroFilePresent(cwd)) { outputEmpty(); return; }
|
|
6002
6135
|
const gitRepo = detectRepo(cwd, '', '', workspaceRoots);
|
|
6003
6136
|
|
|
6004
6137
|
if (event === 'afterAgentThought') {
|
|
@@ -8270,7 +8403,7 @@ function writeConfigEnv(opts) {
|
|
|
8270
8403
|
`SYNKRO_CREDENTIALS_PATH=${shellQuoteSingle(credsPath)}`,
|
|
8271
8404
|
`SYNKRO_TIER=${shellQuoteSingle(safeTier)}`,
|
|
8272
8405
|
`SYNKRO_INFERENCE=${shellQuoteSingle(safeInference)}`,
|
|
8273
|
-
`SYNKRO_VERSION=${shellQuoteSingle("1.6.
|
|
8406
|
+
`SYNKRO_VERSION=${shellQuoteSingle("1.6.46")}`
|
|
8274
8407
|
];
|
|
8275
8408
|
if (safeSynkroBin) lines.push(`SYNKRO_CLI_BIN=${shellQuoteSingle(safeSynkroBin)}`);
|
|
8276
8409
|
if (safeUserId) lines.push(`SYNKRO_USER_ID=${shellQuoteSingle(safeUserId)}`);
|
|
@@ -11355,7 +11488,7 @@ var args = process.argv.slice(2);
|
|
|
11355
11488
|
var cmd = args[0] || "";
|
|
11356
11489
|
var subArgs = args.slice(1);
|
|
11357
11490
|
function printVersion() {
|
|
11358
|
-
console.log("1.6.
|
|
11491
|
+
console.log("1.6.46");
|
|
11359
11492
|
}
|
|
11360
11493
|
function printHelp2() {
|
|
11361
11494
|
console.log(`Synkro CLI \u2014 runtime safety for AI coding agents
|