@karmaniverous/jeeves-watcher 0.19.0-3 → 0.19.1
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/config.schema.json +207 -183
- package/dist/cli/jeeves-watcher/{index-CD0v-oGg.js → index-C_bf9c9m.js} +821 -330
- package/dist/cli/jeeves-watcher/index.js +62 -44
- package/dist/index.d.ts +90 -28
- package/dist/index.js +871 -362
- package/package.json +3 -3
|
@@ -265,7 +265,7 @@ async function startFromConfig(configPath, descriptor) {
|
|
|
265
265
|
const config = await loadConfig(resolvedPath);
|
|
266
266
|
// Dynamic import breaks the circular dependency between this module
|
|
267
267
|
// and app/index.ts (which defines JeevesWatcher and re-exports startFromConfig).
|
|
268
|
-
const { JeevesWatcher } = await import('./index-
|
|
268
|
+
const { JeevesWatcher } = await import('./index-C_bf9c9m.js');
|
|
269
269
|
const app = new JeevesWatcher(config, resolvedPath, descriptor);
|
|
270
270
|
installShutdownHandlers(() => app.stop());
|
|
271
271
|
await app.start();
|
|
@@ -636,6 +636,7 @@ function computeDelayMs(attempt, baseDelayMs, maxDelayMs, jitter = 0) {
|
|
|
636
636
|
*/
|
|
637
637
|
async function retry(fn, options) {
|
|
638
638
|
const attempts = Math.max(1, options.attempts);
|
|
639
|
+
const shouldRetry = options.shouldRetry ?? (() => true);
|
|
639
640
|
let lastError;
|
|
640
641
|
for (let attempt = 1; attempt <= attempts; attempt++) {
|
|
641
642
|
try {
|
|
@@ -644,7 +645,7 @@ async function retry(fn, options) {
|
|
|
644
645
|
catch (error) {
|
|
645
646
|
lastError = error;
|
|
646
647
|
const isLast = attempt >= attempts;
|
|
647
|
-
if (isLast)
|
|
648
|
+
if (isLast || !shouldRetry(error))
|
|
648
649
|
break;
|
|
649
650
|
const delayMs = computeDelayMs(attempt, options.baseDelayMs, options.maxDelayMs, options.jitter);
|
|
650
651
|
options.onRetry?.({ attempt, attempts, delayMs, error });
|
|
@@ -656,10 +657,28 @@ async function retry(fn, options) {
|
|
|
656
657
|
|
|
657
658
|
/**
|
|
658
659
|
* @module vcs/gitExec
|
|
659
|
-
* Shared git execution utilities
|
|
660
|
+
* Shared git execution utilities: the pinned argv prefix, timeouts, error
|
|
661
|
+
* field extraction and classification, and path comparison helpers.
|
|
660
662
|
*/
|
|
661
663
|
/** Promisified execFile for git commands. */
|
|
662
664
|
const execFileAsync = promisify(execFile);
|
|
665
|
+
/**
|
|
666
|
+
* Leading git arguments applied to every VCS-owned git invocation.
|
|
667
|
+
*
|
|
668
|
+
* `-c core.longpaths=true` lets Windows checkouts handle paths beyond the
|
|
669
|
+
* legacy MAX_PATH limit (260 chars) without requiring a machine-wide
|
|
670
|
+
* `core.longpaths` git config or registry change. See #249.
|
|
671
|
+
*/
|
|
672
|
+
const GIT_BASE_ARGS = ['-c', 'core.longpaths=true'];
|
|
673
|
+
/**
|
|
674
|
+
* Build a git argv array with {@link GIT_BASE_ARGS} prefixed.
|
|
675
|
+
*
|
|
676
|
+
* @param args - The subcommand and its arguments (e.g. `'commit', '-m', msg`).
|
|
677
|
+
* @returns The full argv array, base args first.
|
|
678
|
+
*/
|
|
679
|
+
function gitArgs(...args) {
|
|
680
|
+
return [...GIT_BASE_ARGS, ...args];
|
|
681
|
+
}
|
|
663
682
|
/** Standard timeout (ms) for most git operations. */
|
|
664
683
|
const GIT_TIMEOUT_STANDARD = 30_000;
|
|
665
684
|
/** Extended timeout (ms) for cherry-pick operations. */
|
|
@@ -681,49 +700,37 @@ function getExecErrorFields(error) {
|
|
|
681
700
|
return { message: error.message || '', stderr, stdout };
|
|
682
701
|
}
|
|
683
702
|
/**
|
|
684
|
-
*
|
|
703
|
+
* Read the `code` property of an unknown rejection value, if present.
|
|
704
|
+
* execFile rejects with the child's numeric exit status in `code`; fs calls
|
|
705
|
+
* reject with a string errno code (e.g. `'ENOENT'`).
|
|
706
|
+
*
|
|
707
|
+
* @param error - The caught value.
|
|
708
|
+
* @returns The code, or undefined if the value has none.
|
|
709
|
+
*/
|
|
710
|
+
function getErrorCode(error) {
|
|
711
|
+
if (typeof error !== 'object' || error === null || !('code' in error)) {
|
|
712
|
+
return undefined;
|
|
713
|
+
}
|
|
714
|
+
const { code } = error;
|
|
715
|
+
return typeof code === 'string' || typeof code === 'number'
|
|
716
|
+
? code
|
|
717
|
+
: undefined;
|
|
718
|
+
}
|
|
719
|
+
/**
|
|
720
|
+
* Check whether an error is caused by `index.lock` contention.
|
|
685
721
|
*
|
|
686
|
-
*
|
|
687
|
-
*
|
|
688
|
-
*
|
|
689
|
-
* limit that triggers ENAMETOOLONG when batches contain many files
|
|
690
|
-
* with long absolute paths.
|
|
722
|
+
* Requires the literal `index.lock` in the message or stderr: a bare
|
|
723
|
+
* `EEXIST` (any file that already exists) is not lock contention and must
|
|
724
|
+
* not be retried as if it were. Only Error instances qualify.
|
|
691
725
|
*
|
|
692
|
-
* @param
|
|
693
|
-
* @
|
|
694
|
-
* @param timeoutMs - Kill the child process after this many milliseconds. Default: 30000.
|
|
726
|
+
* @param error - The caught value.
|
|
727
|
+
* @returns true if the error names `index.lock`.
|
|
695
728
|
*/
|
|
696
|
-
function
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
if (timedOut)
|
|
702
|
-
return;
|
|
703
|
-
if (error)
|
|
704
|
-
reject(error);
|
|
705
|
-
else
|
|
706
|
-
resolve();
|
|
707
|
-
});
|
|
708
|
-
const timer = setTimeout(() => {
|
|
709
|
-
timedOut = true;
|
|
710
|
-
child.kill();
|
|
711
|
-
reject(new Error(`git add timed out after ${String(timeoutMs)}ms`));
|
|
712
|
-
}, timeoutMs);
|
|
713
|
-
if (!child.stdin) {
|
|
714
|
-
clearTimeout(timer);
|
|
715
|
-
reject(new Error('Failed to open stdin for git add'));
|
|
716
|
-
return;
|
|
717
|
-
}
|
|
718
|
-
// Suppress EPIPE — expected if git exits before we finish writing
|
|
719
|
-
child.stdin.on('error', (err) => {
|
|
720
|
-
if (err.code !== 'EPIPE') {
|
|
721
|
-
clearTimeout(timer);
|
|
722
|
-
reject(err);
|
|
723
|
-
}
|
|
724
|
-
});
|
|
725
|
-
child.stdin.end(files.join('\0'));
|
|
726
|
-
});
|
|
729
|
+
function isIndexLockError(error) {
|
|
730
|
+
if (!(error instanceof Error))
|
|
731
|
+
return false;
|
|
732
|
+
const { message, stderr } = getExecErrorFields(error);
|
|
733
|
+
return message.includes('index.lock') || stderr.includes('index.lock');
|
|
727
734
|
}
|
|
728
735
|
/**
|
|
729
736
|
* Normalize a path for case-insensitive comparison on Windows.
|
|
@@ -736,6 +743,17 @@ function gitAddViaStdin(files, cwd, timeoutMs = 30_000) {
|
|
|
736
743
|
function normalizePathCase(p, platform = process.platform) {
|
|
737
744
|
return platform === 'win32' ? p.toLowerCase() : p;
|
|
738
745
|
}
|
|
746
|
+
/**
|
|
747
|
+
* Build a comparison key for an absolute path: resolved, forward slashes,
|
|
748
|
+
* and lowercased on Windows.
|
|
749
|
+
*
|
|
750
|
+
* @param p - The path.
|
|
751
|
+
* @param platform - The platform to check against (default: `process.platform`).
|
|
752
|
+
* @returns The comparison key.
|
|
753
|
+
*/
|
|
754
|
+
function pathKey(p, platform = process.platform) {
|
|
755
|
+
return normalizePathCase(normalizeSlashes(resolve(p)), platform);
|
|
756
|
+
}
|
|
739
757
|
/**
|
|
740
758
|
* Find the longest-prefix-match root for a normalized path.
|
|
741
759
|
* Roots must be sorted longest-first for correct nested matching.
|
|
@@ -1639,4 +1657,4 @@ const watcherDescriptor = {
|
|
|
1639
1657
|
const program = createServiceCli(watcherDescriptor);
|
|
1640
1658
|
program.parse();
|
|
1641
1659
|
|
|
1642
|
-
export { CONFIG_WATCH_VALID_SCOPES as C, GIT_TIMEOUT_STANDARD as G, VALID_REINDEX_SCOPES as V, normalizeError as a,
|
|
1660
|
+
export { CONFIG_WATCH_VALID_SCOPES as C, GIT_TIMEOUT_STANDARD as G, VALID_REINDEX_SCOPES as V, normalizeError as a, getErrorCode as b, GIT_TIMEOUT_CHERRY_PICK as c, GIT_TIMEOUT_PUSH as d, execFileAsync as e, normalizePathCase as f, gitArgs as g, findRootForPath as h, isIndexLockError as i, GitignoreFilter as j, executeReindex as k, isPathWatched as l, mergeInferenceRules as m, normalizeSlashes as n, getWatchRootBases as o, pathKey as p, loadConfig as q, retry as r, sleep as s, startFromConfig as t };
|
package/dist/index.d.ts
CHANGED
|
@@ -1227,6 +1227,25 @@ declare class EventQueue {
|
|
|
1227
1227
|
private maybeResolveIdleWorkers;
|
|
1228
1228
|
}
|
|
1229
1229
|
|
|
1230
|
+
/**
|
|
1231
|
+
* @module vcs/CommitCircuitBreaker
|
|
1232
|
+
* Per-root circuit breaker for the VCS commit pipeline. Counts consecutive
|
|
1233
|
+
* failures, trips at a threshold, and allows one half-open attempt per
|
|
1234
|
+
* cooldown. It holds no files: callers retain pending work while it is
|
|
1235
|
+
* tripped. See #249.
|
|
1236
|
+
*/
|
|
1237
|
+
/** Serializable breaker state. */
|
|
1238
|
+
interface CircuitBreakerSnapshot {
|
|
1239
|
+
/** Consecutive commit failures since the last success. */
|
|
1240
|
+
consecutiveFailures: number;
|
|
1241
|
+
/** Whether the breaker is tripped. */
|
|
1242
|
+
tripped: boolean;
|
|
1243
|
+
/** ISO timestamp of the current trip, or null. */
|
|
1244
|
+
trippedAt: string | null;
|
|
1245
|
+
/** Message of the most recent failure, or null. */
|
|
1246
|
+
lastError: string | null;
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1230
1249
|
/**
|
|
1231
1250
|
* @module vcs/CommitMessageGenerator
|
|
1232
1251
|
* Generates AI-powered commit messages via the Anthropic Messages API.
|
|
@@ -1272,6 +1291,11 @@ interface PushError {
|
|
|
1272
1291
|
* Per-root VCS manager for git-backed content versioning.
|
|
1273
1292
|
*/
|
|
1274
1293
|
|
|
1294
|
+
/** Per-root circuit breaker state, exposed via /vcs/status. */
|
|
1295
|
+
interface VcsBreakerState extends CircuitBreakerSnapshot {
|
|
1296
|
+
/** Files queued or in flight, not yet committed. */
|
|
1297
|
+
pendingCount: number;
|
|
1298
|
+
}
|
|
1275
1299
|
/**
|
|
1276
1300
|
* Per-root VCS manager for git-backed content versioning.
|
|
1277
1301
|
*
|
|
@@ -1280,7 +1304,9 @@ interface PushError {
|
|
|
1280
1304
|
* (with optional AI-generated messages), and optionally pushed to a remote.
|
|
1281
1305
|
*
|
|
1282
1306
|
* Concurrency: only one commit is in-flight at a time per root. Index.lock
|
|
1283
|
-
* contention is handled with exponential backoff retries (D10).
|
|
1307
|
+
* contention is handled with exponential backoff retries (D10). Failed
|
|
1308
|
+
* batches are always retained; a circuit breaker spaces out retries of a
|
|
1309
|
+
* persistently failing root (#249).
|
|
1284
1310
|
*/
|
|
1285
1311
|
declare class VcsManager {
|
|
1286
1312
|
readonly config: VcsConfig;
|
|
@@ -1289,13 +1315,15 @@ declare class VcsManager {
|
|
|
1289
1315
|
private readonly accessToken;
|
|
1290
1316
|
private readonly logger;
|
|
1291
1317
|
private readonly commitMessageBuilder;
|
|
1318
|
+
private readonly breaker;
|
|
1292
1319
|
private readonly pending;
|
|
1293
1320
|
private readonly pendingReversions;
|
|
1294
1321
|
private readonly _pushErrors;
|
|
1295
1322
|
private readonly squashManager;
|
|
1296
1323
|
private throttleTimer;
|
|
1324
|
+
private breakerRetryTimer;
|
|
1297
1325
|
private commitInFlight;
|
|
1298
|
-
private
|
|
1326
|
+
private inFlightFileCount;
|
|
1299
1327
|
private started;
|
|
1300
1328
|
private paused;
|
|
1301
1329
|
private isBaseline;
|
|
@@ -1303,12 +1331,15 @@ declare class VcsManager {
|
|
|
1303
1331
|
constructor(rootPath: string, config: VcsConfig, logger: pino.Logger, commitMessageGenerator?: CommitMessageGenerator, remoteUrl?: string, accessToken?: string);
|
|
1304
1332
|
get lastPushTime(): string | null;
|
|
1305
1333
|
get pushErrors(): readonly PushError[];
|
|
1334
|
+
/** Current circuit breaker state and backlog, for /vcs/status. */
|
|
1335
|
+
get breakerState(): VcsBreakerState;
|
|
1306
1336
|
/**
|
|
1307
1337
|
* Begin accepting file changes. Sets the manager to started state and
|
|
1308
1338
|
* starts the squash manager if configured.
|
|
1309
1339
|
*
|
|
1310
1340
|
* Performs startup orphan detection/recovery before starting the squash
|
|
1311
|
-
* manager, ensuring the repo is on the configured branch
|
|
1341
|
+
* manager, ensuring the repo is on the configured branch, and reports
|
|
1342
|
+
* (never clears) an abandoned cherry-pick, rebase, or merge.
|
|
1312
1343
|
*
|
|
1313
1344
|
* Baseline mode (commit prefix `"baseline:"`) remains active until
|
|
1314
1345
|
* {@link endBaseline} is called by the coordinator after the initial scan.
|
|
@@ -1321,7 +1352,10 @@ declare class VcsManager {
|
|
|
1321
1352
|
endBaseline(): void;
|
|
1322
1353
|
/**
|
|
1323
1354
|
* Add a file to the pending set and start the throttle timer.
|
|
1324
|
-
* If the pending set
|
|
1355
|
+
* If the pending set reaches maxBatchSize, commit a batch immediately.
|
|
1356
|
+
*
|
|
1357
|
+
* The circuit breaker is intentionally not reset here: recovery is
|
|
1358
|
+
* time-based, so a busy root cannot hammer a deterministic failure.
|
|
1325
1359
|
*
|
|
1326
1360
|
* @param filePath - Absolute path of the changed file.
|
|
1327
1361
|
*/
|
|
@@ -1333,8 +1367,7 @@ declare class VcsManager {
|
|
|
1333
1367
|
*/
|
|
1334
1368
|
addPendingReversion(reversion: PendingReversion): void;
|
|
1335
1369
|
/**
|
|
1336
|
-
*
|
|
1337
|
-
* git add handles deleted files when the file is gone from disk.
|
|
1370
|
+
* Queue a deleted file. Staging records the deletion if git tracks it.
|
|
1338
1371
|
*
|
|
1339
1372
|
* @param filePath - Absolute path of the deleted file.
|
|
1340
1373
|
*/
|
|
@@ -1352,7 +1385,7 @@ declare class VcsManager {
|
|
|
1352
1385
|
*/
|
|
1353
1386
|
resume(): void;
|
|
1354
1387
|
/**
|
|
1355
|
-
*
|
|
1388
|
+
* Commit all pending files now, in batches of at most maxBatchSize.
|
|
1356
1389
|
*/
|
|
1357
1390
|
flush(): Promise<void>;
|
|
1358
1391
|
/**
|
|
@@ -1360,32 +1393,46 @@ declare class VcsManager {
|
|
|
1360
1393
|
*/
|
|
1361
1394
|
stop(): Promise<void>;
|
|
1362
1395
|
/**
|
|
1363
|
-
*
|
|
1396
|
+
* Add tracked-but-deleted paths (`git ls-files --deleted`) in this
|
|
1397
|
+
* manager's watch scope to pending, so the next commit (typically the
|
|
1398
|
+
* baseline) records deletions that happened while nothing was watching.
|
|
1399
|
+
* Called by the coordinator before flush/endBaseline on startup. See #249.
|
|
1400
|
+
*
|
|
1401
|
+
* @param isInScope - Returns true if the absolute path routes to this
|
|
1402
|
+
* manager (the coordinator's routing and watch-scope logic).
|
|
1364
1403
|
*/
|
|
1404
|
+
reconcileDeletions(isInScope: (absolutePath: string) => boolean): Promise<void>;
|
|
1405
|
+
/** Take up to maxBatchSize files from the pending set. */
|
|
1365
1406
|
private takeBatch;
|
|
1366
|
-
/**
|
|
1367
|
-
|
|
1368
|
-
|
|
1407
|
+
/** Chain a batch behind the in-flight commit, tracking it as backlog. */
|
|
1408
|
+
private enqueueBatch;
|
|
1409
|
+
/** Put files back in the pending set. Pending is a set, so this cannot grow it past the distinct paths seen. */
|
|
1410
|
+
private retain;
|
|
1369
1411
|
private clearThrottle;
|
|
1370
1412
|
/**
|
|
1371
1413
|
* Start the throttle timer to fire flush() after commitThrottleMs.
|
|
1372
1414
|
* If a timer is already running, do not reset it — this is throttle, not debounce.
|
|
1373
1415
|
*/
|
|
1374
1416
|
private resetThrottle;
|
|
1417
|
+
private clearBreakerRetry;
|
|
1375
1418
|
/**
|
|
1376
|
-
*
|
|
1419
|
+
* Ensure a timer will flush pending files when the breaker's cooldown
|
|
1420
|
+
* elapses, without needing a new file-change event.
|
|
1377
1421
|
*/
|
|
1378
|
-
private
|
|
1422
|
+
private scheduleBreakerRetry;
|
|
1379
1423
|
/**
|
|
1380
|
-
*
|
|
1381
|
-
*
|
|
1424
|
+
* Commit a batch with index.lock retry, stale lock detection, pause
|
|
1425
|
+
* support, and the circuit breaker. A failed batch is always retained.
|
|
1382
1426
|
*/
|
|
1383
|
-
private
|
|
1427
|
+
private commitBatch;
|
|
1384
1428
|
/**
|
|
1385
|
-
*
|
|
1386
|
-
*
|
|
1429
|
+
* One commit attempt: stage, skip if nothing is staged, commit, push.
|
|
1430
|
+
*
|
|
1431
|
+
* @returns true if a commit was created; false for a no-op.
|
|
1387
1432
|
*/
|
|
1388
|
-
private
|
|
1433
|
+
private tryCommit;
|
|
1434
|
+
private onCommitSuccess;
|
|
1435
|
+
private onCommitFailure;
|
|
1389
1436
|
}
|
|
1390
1437
|
|
|
1391
1438
|
/**
|
|
@@ -1401,6 +1448,7 @@ declare class VcsCoordinator {
|
|
|
1401
1448
|
private readonly managers;
|
|
1402
1449
|
private readonly roots;
|
|
1403
1450
|
private readonly logger;
|
|
1451
|
+
private readonly isInWatchScope;
|
|
1404
1452
|
constructor(config: JeevesWatcherConfig, logger: pino.Logger);
|
|
1405
1453
|
/**
|
|
1406
1454
|
* Start all VcsManager instances.
|
|
@@ -1952,15 +2000,6 @@ interface CommitInfo {
|
|
|
1952
2000
|
hash: string;
|
|
1953
2001
|
date: Date;
|
|
1954
2002
|
}
|
|
1955
|
-
/**
|
|
1956
|
-
* Manages scheduled squash retention for a single VCS root.
|
|
1957
|
-
*
|
|
1958
|
-
* Runs on a cron schedule (checked every 60s). When triggered, squashes
|
|
1959
|
-
* commits older than the retention boundary (the tighter of maxAgeDays
|
|
1960
|
-
* and maxVersions) into a single "historical baseline" orphan commit,
|
|
1961
|
-
* then cherry-picks retained commits on top. Force-pushes to remote
|
|
1962
|
-
* if configured (D13).
|
|
1963
|
-
*/
|
|
1964
2003
|
/** Options for SquashManager construction beyond the required params. */
|
|
1965
2004
|
interface SquashManagerOptions {
|
|
1966
2005
|
/** Git branch name for squash operations. Default: "master". */
|
|
@@ -1974,6 +2013,15 @@ interface SquashManagerOptions {
|
|
|
1974
2013
|
/** Access token for HTTPS push authentication. */
|
|
1975
2014
|
accessToken?: string;
|
|
1976
2015
|
}
|
|
2016
|
+
/**
|
|
2017
|
+
* Manages scheduled squash retention for a single VCS root.
|
|
2018
|
+
*
|
|
2019
|
+
* Runs on a cron schedule (checked every 60s). When triggered, squashes
|
|
2020
|
+
* commits older than the retention boundary (the tighter of maxAgeDays
|
|
2021
|
+
* and maxVersions) into a single "historical baseline" orphan commit,
|
|
2022
|
+
* then cherry-picks retained commits on top. Force-pushes to remote
|
|
2023
|
+
* if configured (D13).
|
|
2024
|
+
*/
|
|
1977
2025
|
declare class SquashManager {
|
|
1978
2026
|
private readonly rootPath;
|
|
1979
2027
|
private readonly retention;
|
|
@@ -2002,6 +2050,14 @@ declare class SquashManager {
|
|
|
2002
2050
|
* to drain and suspend the commit pipeline during the squash.
|
|
2003
2051
|
*/
|
|
2004
2052
|
runSquash(): Promise<SquashResult>;
|
|
2053
|
+
/**
|
|
2054
|
+
* Refuse to start a multi-step history rewrite while another git
|
|
2055
|
+
* operation holds the index or an abandoned cherry-pick, rebase, or merge
|
|
2056
|
+
* is present (#249: report only, never auto-clear).
|
|
2057
|
+
*
|
|
2058
|
+
* @returns The refusal reason, or undefined if the squash may proceed.
|
|
2059
|
+
*/
|
|
2060
|
+
private checkRepoPreconditions;
|
|
2005
2061
|
/**
|
|
2006
2062
|
* Get all commits in chronological order (oldest first).
|
|
2007
2063
|
*/
|
|
@@ -2022,6 +2078,12 @@ declare class SquashManager {
|
|
|
2022
2078
|
* Uses the configured branch name instead of dynamic detection (Bug 1).
|
|
2023
2079
|
*/
|
|
2024
2080
|
private performSquash;
|
|
2081
|
+
/**
|
|
2082
|
+
* Best-effort cleanup after a failed squash. Aborts any half-finished
|
|
2083
|
+
* cherry-pick first, so a failure never leaves a sequencer behind (#249),
|
|
2084
|
+
* then returns to the configured branch and removes a leftover lock.
|
|
2085
|
+
*/
|
|
2086
|
+
private cleanupFailedSquash;
|
|
2025
2087
|
/**
|
|
2026
2088
|
* Force push to remote after squash (history rewrite requires force).
|
|
2027
2089
|
*/
|