@karmaniverous/jeeves-watcher 0.18.3 → 0.18.5

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.
@@ -10,7 +10,7 @@ import Ajv from 'ajv';
10
10
  import addFormats from 'ajv-formats';
11
11
  import picomatch from 'picomatch';
12
12
  import { deserialize, serialize } from 'node:v8';
13
- import { normalizeWatchPaths, extractWatchPathStrings, inferenceRuleSchema, jeevesWatcherConfigSchema } from '@karmaniverous/jeeves-watcher-core';
13
+ import { normalizeWatchPaths, extractWatchPathStrings, vcsAuthorConfigSchema, inferenceRuleSchema, jeevesWatcherConfigSchema } from '@karmaniverous/jeeves-watcher-core';
14
14
  import { execFile } from 'node:child_process';
15
15
  import { promisify } from 'node:util';
16
16
  import { homedir } from 'node:os';
@@ -934,17 +934,32 @@ class CommitMessageGenerator {
934
934
  */
935
935
  /** Promisified execFile for git commands. */
936
936
  const execFileAsync = promisify(execFile);
937
+ /**
938
+ * Normalize a path for case-insensitive comparison on Windows.
939
+ * On Windows, lowercases the entire path; on other platforms, returns as-is.
940
+ *
941
+ * @param p - The path string to normalize.
942
+ * @param platform - The platform to check against (default: `process.platform`).
943
+ * @returns The normalized path.
944
+ */
945
+ function normalizePathCase(p, platform = process.platform) {
946
+ return platform === 'win32' ? p.toLowerCase() : p;
947
+ }
937
948
  /**
938
949
  * Find the longest-prefix-match root for a normalized path.
939
950
  * Roots must be sorted longest-first for correct nested matching.
940
951
  *
941
952
  * @param roots - Root paths sorted longest-first.
942
953
  * @param normalizedPath - Normalized absolute path (forward slashes).
954
+ * @param platform - The platform to check against (default: `process.platform`).
943
955
  * @returns The matching root, or undefined.
944
956
  */
945
- function findRootForPath(roots, normalizedPath) {
957
+ function findRootForPath(roots, normalizedPath, platform = process.platform) {
958
+ const comparePath = normalizePathCase(normalizedPath, platform);
946
959
  for (const root of roots) {
947
- if (normalizedPath === root || normalizedPath.startsWith(root + '/')) {
960
+ const compareRoot = normalizePathCase(root, platform);
961
+ if (comparePath === compareRoot ||
962
+ comparePath.startsWith(compareRoot + '/')) {
948
963
  return root;
949
964
  }
950
965
  }
@@ -1303,6 +1318,7 @@ class VcsManager {
1303
1318
  debounceTimer;
1304
1319
  commitInFlight = Promise.resolve();
1305
1320
  started = false;
1321
+ isBaseline = false;
1306
1322
  _lastPushTime = null;
1307
1323
  constructor(rootPath, config, logger, commitMessageGenerator, remoteUrl, accessToken) {
1308
1324
  this.rootPath = rootPath;
@@ -1323,11 +1339,47 @@ class VcsManager {
1323
1339
  }
1324
1340
  /**
1325
1341
  * Begin accepting file changes. Sets up the debounce mechanism.
1342
+ * Enqueues a baseline commit for pre-existing files if the repo has no commits.
1326
1343
  */
1327
- start() {
1344
+ async start() {
1328
1345
  this.started = true;
1329
1346
  this.squashManager?.start();
1330
1347
  this.logger.info({ root: this.rootPath }, 'VcsManager started');
1348
+ await this.enqueueBaselineIfNeeded();
1349
+ }
1350
+ /**
1351
+ * If the repo has no commits, enumerate untracked files and feed them
1352
+ * through fileChanged() to create a baseline commit.
1353
+ */
1354
+ async enqueueBaselineIfNeeded() {
1355
+ try {
1356
+ await execFileAsync('git', ['rev-parse', 'HEAD'], {
1357
+ cwd: this.rootPath,
1358
+ });
1359
+ // HEAD exists — repo already has commits
1360
+ return;
1361
+ }
1362
+ catch {
1363
+ // No commits yet — proceed with baseline
1364
+ }
1365
+ try {
1366
+ const { stdout } = await execFileAsync('git', ['ls-files', '--others', '--exclude-standard', '-z'], { cwd: this.rootPath });
1367
+ const files = stdout
1368
+ .split('\0')
1369
+ .map((line) => line.trim())
1370
+ .filter((line) => line.length > 0);
1371
+ if (files.length === 0)
1372
+ return;
1373
+ this.logger.info({ root: this.rootPath, fileCount: files.length }, 'Enqueuing baseline commit for pre-existing files');
1374
+ this.isBaseline = true;
1375
+ const absolutePaths = files.map((f) => this.rootPath + '/' + f);
1376
+ for (const filePath of absolutePaths) {
1377
+ this.fileChanged(filePath);
1378
+ }
1379
+ }
1380
+ catch (error) {
1381
+ this.logger.warn({ root: this.rootPath, err: normalizeError(error) }, 'Failed to enumerate untracked files for baseline commit');
1382
+ }
1331
1383
  }
1332
1384
  /**
1333
1385
  * Add a file to the pending set and reset the debounce timer.
@@ -1442,6 +1494,9 @@ class VcsManager {
1442
1494
  */
1443
1495
  buildTemplateMessage(fileCount) {
1444
1496
  if (this.pendingReversions.length === 0) {
1497
+ if (this.isBaseline) {
1498
+ return `baseline: batch (${String(fileCount)} files)`;
1499
+ }
1445
1500
  const timestamp = new Date().toISOString();
1446
1501
  return `watcher: batch ${timestamp} (${String(fileCount)} files)`;
1447
1502
  }
@@ -1499,10 +1554,15 @@ class VcsManager {
1499
1554
  cwd: this.rootPath,
1500
1555
  });
1501
1556
  // Build message after staging so getStagedDiff can see the changes
1502
- const message = attempt === 1
1503
- ? await this.buildCommitMessage(files)
1504
- : this.buildTemplateMessage(files.length);
1557
+ // Skip AI for baselines and retries — use template directly
1558
+ const message = this.isBaseline || attempt > 1
1559
+ ? this.buildTemplateMessage(files.length)
1560
+ : await this.buildCommitMessage(files);
1505
1561
  this.pendingReversions.length = 0;
1562
+ // Only clear baseline flag when all baseline files have been committed
1563
+ if (this.pending.size === 0) {
1564
+ this.isBaseline = false;
1565
+ }
1506
1566
  await execFileAsync('git', ['commit', '-m', message], {
1507
1567
  cwd: this.rootPath,
1508
1568
  });
@@ -1579,8 +1639,9 @@ class VcsCoordinator {
1579
1639
  if (!rootVcs)
1580
1640
  continue;
1581
1641
  const resolvedRoot = normalizeSlashes(resolve(globRoot(entry.path)));
1642
+ const rootKey = normalizePathCase(resolvedRoot);
1582
1643
  // Deduplicate: skip if another glob already resolved to this root.
1583
- if (this.managers.has(resolvedRoot))
1644
+ if (this.managers.has(rootKey))
1584
1645
  continue;
1585
1646
  const mergedConfig = {
1586
1647
  ...config.vcs,
@@ -1600,7 +1661,7 @@ class VcsCoordinator {
1600
1661
  const remoteUrl = entry.vcs?.remote;
1601
1662
  const accessToken = entry.vcs?.accessToken ?? config.vcs.defaultAccessToken;
1602
1663
  const manager = new VcsManager(resolvedRoot, mergedConfig, logger.child({ vcsRoot: resolvedRoot }), generator, remoteUrl, accessToken);
1603
- this.managers.set(resolvedRoot, manager);
1664
+ this.managers.set(rootKey, manager);
1604
1665
  this.roots.push(resolvedRoot);
1605
1666
  }
1606
1667
  // Sort roots longest-first so nested paths match before parents.
@@ -1609,10 +1670,12 @@ class VcsCoordinator {
1609
1670
  /**
1610
1671
  * Start all VcsManager instances.
1611
1672
  */
1612
- start() {
1673
+ async start() {
1674
+ const startPromises = [];
1613
1675
  this.managers.forEach((manager) => {
1614
- manager.start();
1676
+ startPromises.push(manager.start());
1615
1677
  });
1678
+ await Promise.all(startPromises);
1616
1679
  this.logger.info({ rootCount: this.managers.size }, 'VcsCoordinator started');
1617
1680
  }
1618
1681
  /**
@@ -1654,7 +1717,7 @@ class VcsCoordinator {
1654
1717
  * Get the VcsManager for a specific root path.
1655
1718
  */
1656
1719
  getManager(root) {
1657
- return this.managers.get(root);
1720
+ return this.managers.get(normalizePathCase(root));
1658
1721
  }
1659
1722
  /**
1660
1723
  * Get all managers as [root, manager] pairs.
@@ -1671,7 +1734,7 @@ class VcsCoordinator {
1671
1734
  */
1672
1735
  findManagerForPath(normalizedPath) {
1673
1736
  const root = findRootForPath(this.roots, normalizedPath);
1674
- return root ? this.managers.get(root) : undefined;
1737
+ return root ? this.managers.get(normalizePathCase(root)) : undefined;
1675
1738
  }
1676
1739
  }
1677
1740
 
@@ -2892,15 +2955,18 @@ function resolveWatchRootsForGlob(coordinator, glob) {
2892
2955
  const normalizedGlob = normalizeSlashes(glob);
2893
2956
  const roots = coordinator.getRoots();
2894
2957
  const results = [];
2958
+ const compareGlob = normalizePathCase(normalizedGlob);
2895
2959
  for (const root of roots) {
2896
- if (normalizedGlob.startsWith(root + '/') || normalizedGlob === root) {
2960
+ const compareRoot = normalizePathCase(root);
2961
+ if (compareGlob.startsWith(compareRoot + '/') ||
2962
+ compareGlob === compareRoot) {
2897
2963
  // Glob is under this root
2898
2964
  results.push({
2899
2965
  root,
2900
2966
  relativePath: normalizeSlashes(relative(root, normalizedGlob)),
2901
2967
  });
2902
2968
  }
2903
- else if (root.startsWith(normalizedGlob.replace(/[*?[\]{}]/g, ''))) {
2969
+ else if (compareRoot.startsWith(compareGlob.replace(/[*?[\]{}]/g, ''))) {
2904
2970
  // Root is under the glob's base path — include with root-relative glob
2905
2971
  results.push({
2906
2972
  root,
@@ -2986,6 +3052,22 @@ async function initRepo(rootPath) {
2986
3052
  await execFileAsync('git', ['init'], { cwd: rootPath });
2987
3053
  }
2988
3054
  }
3055
+ /**
3056
+ * Configure local git identity for a repository.
3057
+ * Sets repo-level (not global) user.name and user.email.
3058
+ *
3059
+ * @param rootPath - Directory of the git repository.
3060
+ * @param name - Author name for commits.
3061
+ * @param email - Author email for commits.
3062
+ */
3063
+ async function configureRepoIdentity(rootPath, name, email) {
3064
+ await execFileAsync('git', ['config', '--local', 'user.name', name], {
3065
+ cwd: rootPath,
3066
+ });
3067
+ await execFileAsync('git', ['config', '--local', 'user.email', email], {
3068
+ cwd: rootPath,
3069
+ });
3070
+ }
2989
3071
  /**
2990
3072
  * Ensure a .gitignore file exists at rootPath with all always-on entries.
2991
3073
  * Creates the file if missing. Appends only entries not already present.
@@ -3200,7 +3282,15 @@ async function initVcs(config, logger) {
3200
3282
  initializedRoots.add(root);
3201
3283
  await initRepo(root);
3202
3284
  await ensureGitignore(root);
3285
+ // Resolve author identity: per-path → root-level → schema defaults.
3286
+ const defaults = vcsAuthorConfigSchema.parse({});
3287
+ const pathAuthor = entry.vcs?.author;
3288
+ const rootAuthor = config.vcs.author;
3289
+ const authorName = pathAuthor?.name ?? rootAuthor?.name ?? defaults.name;
3290
+ const authorEmail = pathAuthor?.email ?? rootAuthor?.email ?? defaults.email;
3291
+ await configureRepoIdentity(root, authorName, authorEmail);
3203
3292
  logger.info({ root }, 'VCS initialized for watch root');
3293
+ logger.debug({ root, authorName, authorEmail }, 'VCS author identity configured');
3204
3294
  }
3205
3295
  return true;
3206
3296
  }
@@ -7694,7 +7784,7 @@ class JeevesWatcher {
7694
7784
  this.watcher = watcher;
7695
7785
  this.gitignoreFilter = gitignoreFilter;
7696
7786
  this.server = await this.startApiServer();
7697
- vcsCoordinator.start();
7787
+ await vcsCoordinator.start();
7698
7788
  this.watcher.start();
7699
7789
  this.startConfigWatch();
7700
7790
  logger.info('jeeves-watcher started');
@@ -224,7 +224,7 @@ async function startFromConfig(configPath, descriptor) {
224
224
  const config = await loadConfig(resolvedPath);
225
225
  // Dynamic import breaks the circular dependency between this module
226
226
  // and app/index.ts (which defines JeevesWatcher and re-exports startFromConfig).
227
- const { JeevesWatcher } = await import('./index-Dk_WrV0T.js');
227
+ const { JeevesWatcher } = await import('./index-CaibsSrT.js');
228
228
  const app = new JeevesWatcher(config, resolvedPath, descriptor);
229
229
  installShutdownHandlers(() => app.stop());
230
230
  await app.start();
package/dist/index.d.ts CHANGED
@@ -1291,14 +1291,21 @@ declare class VcsManager {
1291
1291
  private debounceTimer;
1292
1292
  private commitInFlight;
1293
1293
  private started;
1294
+ private isBaseline;
1294
1295
  private _lastPushTime;
1295
1296
  constructor(rootPath: string, config: VcsConfig, logger: pino.Logger, commitMessageGenerator?: CommitMessageGenerator, remoteUrl?: string, accessToken?: string);
1296
1297
  get lastPushTime(): string | null;
1297
1298
  get pushErrors(): readonly PushError[];
1298
1299
  /**
1299
1300
  * Begin accepting file changes. Sets up the debounce mechanism.
1301
+ * Enqueues a baseline commit for pre-existing files if the repo has no commits.
1300
1302
  */
1301
- start(): void;
1303
+ start(): Promise<void>;
1304
+ /**
1305
+ * If the repo has no commits, enumerate untracked files and feed them
1306
+ * through fileChanged() to create a baseline commit.
1307
+ */
1308
+ private enqueueBaselineIfNeeded;
1302
1309
  /**
1303
1310
  * Add a file to the pending set and reset the debounce timer.
1304
1311
  * If the pending set exceeds maxBatchSize, flush immediately.
@@ -1379,7 +1386,7 @@ declare class VcsCoordinator {
1379
1386
  /**
1380
1387
  * Start all VcsManager instances.
1381
1388
  */
1382
- start(): void;
1389
+ start(): Promise<void>;
1383
1390
  /**
1384
1391
  * Route a file event to the correct root's VcsManager.
1385
1392
  *
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { join, dirname, resolve, relative, extname, basename, isAbsolute, sep } from 'node:path';
2
2
  import { createConfigQueryHandler as createConfigQueryHandler$1, createStatusHandler, createConfigApplyHandler, substituteEnvVars, DEFAULT_PORTS, getBindAddress, postJson, fetchJson, getPackageVersion } from '@karmaniverous/jeeves';
3
- import { extractWatchPathStrings, inferenceRuleSchema, jeevesWatcherConfigSchema, normalizeWatchPaths, LOGGING_DEFAULTS, API_DEFAULTS, EMBEDDING_DEFAULTS, CONFIG_WATCH_DEFAULTS, WATCH_DEFAULTS, ROOT_DEFAULTS, INIT_CONFIG_TEMPLATE, DEFAULT_PORT as DEFAULT_PORT$1, PLUGIN_PACKAGE, SERVICE_PACKAGE, COMPONENT_NAME } from '@karmaniverous/jeeves-watcher-core';
3
+ import { extractWatchPathStrings, inferenceRuleSchema, jeevesWatcherConfigSchema, normalizeWatchPaths, vcsAuthorConfigSchema, LOGGING_DEFAULTS, API_DEFAULTS, EMBEDDING_DEFAULTS, CONFIG_WATCH_DEFAULTS, WATCH_DEFAULTS, ROOT_DEFAULTS, INIT_CONFIG_TEMPLATE, DEFAULT_PORT as DEFAULT_PORT$1, PLUGIN_PACKAGE, SERVICE_PACKAGE, COMPONENT_NAME } from '@karmaniverous/jeeves-watcher-core';
4
4
  export { apiConfigSchema, configWatchConfigSchema, embeddingConfigSchema, inferenceRuleSchema, jeevesWatcherConfigSchema, loggingConfigSchema, vectorStoreConfigSchema, watchConfigSchema } from '@karmaniverous/jeeves-watcher-core';
5
5
  import Fastify from 'fastify';
6
6
  import { readdir, stat, readFile, writeFile, mkdir, appendFile, access, rm } from 'node:fs/promises';
@@ -3375,17 +3375,32 @@ function createSearchHandler(deps) {
3375
3375
  */
3376
3376
  /** Promisified execFile for git commands. */
3377
3377
  const execFileAsync = promisify(execFile);
3378
+ /**
3379
+ * Normalize a path for case-insensitive comparison on Windows.
3380
+ * On Windows, lowercases the entire path; on other platforms, returns as-is.
3381
+ *
3382
+ * @param p - The path string to normalize.
3383
+ * @param platform - The platform to check against (default: `process.platform`).
3384
+ * @returns The normalized path.
3385
+ */
3386
+ function normalizePathCase(p, platform = process.platform) {
3387
+ return platform === 'win32' ? p.toLowerCase() : p;
3388
+ }
3378
3389
  /**
3379
3390
  * Find the longest-prefix-match root for a normalized path.
3380
3391
  * Roots must be sorted longest-first for correct nested matching.
3381
3392
  *
3382
3393
  * @param roots - Root paths sorted longest-first.
3383
3394
  * @param normalizedPath - Normalized absolute path (forward slashes).
3395
+ * @param platform - The platform to check against (default: `process.platform`).
3384
3396
  * @returns The matching root, or undefined.
3385
3397
  */
3386
- function findRootForPath(roots, normalizedPath) {
3398
+ function findRootForPath(roots, normalizedPath, platform = process.platform) {
3399
+ const comparePath = normalizePathCase(normalizedPath, platform);
3387
3400
  for (const root of roots) {
3388
- if (normalizedPath === root || normalizedPath.startsWith(root + '/')) {
3401
+ const compareRoot = normalizePathCase(root, platform);
3402
+ if (comparePath === compareRoot ||
3403
+ comparePath.startsWith(compareRoot + '/')) {
3389
3404
  return root;
3390
3405
  }
3391
3406
  }
@@ -3425,15 +3440,18 @@ function resolveWatchRootsForGlob(coordinator, glob) {
3425
3440
  const normalizedGlob = normalizeSlashes(glob);
3426
3441
  const roots = coordinator.getRoots();
3427
3442
  const results = [];
3443
+ const compareGlob = normalizePathCase(normalizedGlob);
3428
3444
  for (const root of roots) {
3429
- if (normalizedGlob.startsWith(root + '/') || normalizedGlob === root) {
3445
+ const compareRoot = normalizePathCase(root);
3446
+ if (compareGlob.startsWith(compareRoot + '/') ||
3447
+ compareGlob === compareRoot) {
3430
3448
  // Glob is under this root
3431
3449
  results.push({
3432
3450
  root,
3433
3451
  relativePath: normalizeSlashes(relative(root, normalizedGlob)),
3434
3452
  });
3435
3453
  }
3436
- else if (root.startsWith(normalizedGlob.replace(/[*?[\]{}]/g, ''))) {
3454
+ else if (compareRoot.startsWith(compareGlob.replace(/[*?[\]{}]/g, ''))) {
3437
3455
  // Root is under the glob's base path — include with root-relative glob
3438
3456
  results.push({
3439
3457
  root,
@@ -4624,6 +4642,22 @@ async function initRepo(rootPath) {
4624
4642
  await execFileAsync('git', ['init'], { cwd: rootPath });
4625
4643
  }
4626
4644
  }
4645
+ /**
4646
+ * Configure local git identity for a repository.
4647
+ * Sets repo-level (not global) user.name and user.email.
4648
+ *
4649
+ * @param rootPath - Directory of the git repository.
4650
+ * @param name - Author name for commits.
4651
+ * @param email - Author email for commits.
4652
+ */
4653
+ async function configureRepoIdentity(rootPath, name, email) {
4654
+ await execFileAsync('git', ['config', '--local', 'user.name', name], {
4655
+ cwd: rootPath,
4656
+ });
4657
+ await execFileAsync('git', ['config', '--local', 'user.email', email], {
4658
+ cwd: rootPath,
4659
+ });
4660
+ }
4627
4661
  /**
4628
4662
  * Ensure a .gitignore file exists at rootPath with all always-on entries.
4629
4663
  * Creates the file if missing. Appends only entries not already present.
@@ -4780,6 +4814,7 @@ class VcsManager {
4780
4814
  debounceTimer;
4781
4815
  commitInFlight = Promise.resolve();
4782
4816
  started = false;
4817
+ isBaseline = false;
4783
4818
  _lastPushTime = null;
4784
4819
  constructor(rootPath, config, logger, commitMessageGenerator, remoteUrl, accessToken) {
4785
4820
  this.rootPath = rootPath;
@@ -4800,11 +4835,47 @@ class VcsManager {
4800
4835
  }
4801
4836
  /**
4802
4837
  * Begin accepting file changes. Sets up the debounce mechanism.
4838
+ * Enqueues a baseline commit for pre-existing files if the repo has no commits.
4803
4839
  */
4804
- start() {
4840
+ async start() {
4805
4841
  this.started = true;
4806
4842
  this.squashManager?.start();
4807
4843
  this.logger.info({ root: this.rootPath }, 'VcsManager started');
4844
+ await this.enqueueBaselineIfNeeded();
4845
+ }
4846
+ /**
4847
+ * If the repo has no commits, enumerate untracked files and feed them
4848
+ * through fileChanged() to create a baseline commit.
4849
+ */
4850
+ async enqueueBaselineIfNeeded() {
4851
+ try {
4852
+ await execFileAsync('git', ['rev-parse', 'HEAD'], {
4853
+ cwd: this.rootPath,
4854
+ });
4855
+ // HEAD exists — repo already has commits
4856
+ return;
4857
+ }
4858
+ catch {
4859
+ // No commits yet — proceed with baseline
4860
+ }
4861
+ try {
4862
+ const { stdout } = await execFileAsync('git', ['ls-files', '--others', '--exclude-standard', '-z'], { cwd: this.rootPath });
4863
+ const files = stdout
4864
+ .split('\0')
4865
+ .map((line) => line.trim())
4866
+ .filter((line) => line.length > 0);
4867
+ if (files.length === 0)
4868
+ return;
4869
+ this.logger.info({ root: this.rootPath, fileCount: files.length }, 'Enqueuing baseline commit for pre-existing files');
4870
+ this.isBaseline = true;
4871
+ const absolutePaths = files.map((f) => this.rootPath + '/' + f);
4872
+ for (const filePath of absolutePaths) {
4873
+ this.fileChanged(filePath);
4874
+ }
4875
+ }
4876
+ catch (error) {
4877
+ this.logger.warn({ root: this.rootPath, err: normalizeError(error) }, 'Failed to enumerate untracked files for baseline commit');
4878
+ }
4808
4879
  }
4809
4880
  /**
4810
4881
  * Add a file to the pending set and reset the debounce timer.
@@ -4919,6 +4990,9 @@ class VcsManager {
4919
4990
  */
4920
4991
  buildTemplateMessage(fileCount) {
4921
4992
  if (this.pendingReversions.length === 0) {
4993
+ if (this.isBaseline) {
4994
+ return `baseline: batch (${String(fileCount)} files)`;
4995
+ }
4922
4996
  const timestamp = new Date().toISOString();
4923
4997
  return `watcher: batch ${timestamp} (${String(fileCount)} files)`;
4924
4998
  }
@@ -4976,10 +5050,15 @@ class VcsManager {
4976
5050
  cwd: this.rootPath,
4977
5051
  });
4978
5052
  // Build message after staging so getStagedDiff can see the changes
4979
- const message = attempt === 1
4980
- ? await this.buildCommitMessage(files)
4981
- : this.buildTemplateMessage(files.length);
5053
+ // Skip AI for baselines and retries — use template directly
5054
+ const message = this.isBaseline || attempt > 1
5055
+ ? this.buildTemplateMessage(files.length)
5056
+ : await this.buildCommitMessage(files);
4982
5057
  this.pendingReversions.length = 0;
5058
+ // Only clear baseline flag when all baseline files have been committed
5059
+ if (this.pending.size === 0) {
5060
+ this.isBaseline = false;
5061
+ }
4983
5062
  await execFileAsync('git', ['commit', '-m', message], {
4984
5063
  cwd: this.rootPath,
4985
5064
  });
@@ -5056,8 +5135,9 @@ class VcsCoordinator {
5056
5135
  if (!rootVcs)
5057
5136
  continue;
5058
5137
  const resolvedRoot = normalizeSlashes(resolve(globRoot(entry.path)));
5138
+ const rootKey = normalizePathCase(resolvedRoot);
5059
5139
  // Deduplicate: skip if another glob already resolved to this root.
5060
- if (this.managers.has(resolvedRoot))
5140
+ if (this.managers.has(rootKey))
5061
5141
  continue;
5062
5142
  const mergedConfig = {
5063
5143
  ...config.vcs,
@@ -5077,7 +5157,7 @@ class VcsCoordinator {
5077
5157
  const remoteUrl = entry.vcs?.remote;
5078
5158
  const accessToken = entry.vcs?.accessToken ?? config.vcs.defaultAccessToken;
5079
5159
  const manager = new VcsManager(resolvedRoot, mergedConfig, logger.child({ vcsRoot: resolvedRoot }), generator, remoteUrl, accessToken);
5080
- this.managers.set(resolvedRoot, manager);
5160
+ this.managers.set(rootKey, manager);
5081
5161
  this.roots.push(resolvedRoot);
5082
5162
  }
5083
5163
  // Sort roots longest-first so nested paths match before parents.
@@ -5086,10 +5166,12 @@ class VcsCoordinator {
5086
5166
  /**
5087
5167
  * Start all VcsManager instances.
5088
5168
  */
5089
- start() {
5169
+ async start() {
5170
+ const startPromises = [];
5090
5171
  this.managers.forEach((manager) => {
5091
- manager.start();
5172
+ startPromises.push(manager.start());
5092
5173
  });
5174
+ await Promise.all(startPromises);
5093
5175
  this.logger.info({ rootCount: this.managers.size }, 'VcsCoordinator started');
5094
5176
  }
5095
5177
  /**
@@ -5131,7 +5213,7 @@ class VcsCoordinator {
5131
5213
  * Get the VcsManager for a specific root path.
5132
5214
  */
5133
5215
  getManager(root) {
5134
- return this.managers.get(root);
5216
+ return this.managers.get(normalizePathCase(root));
5135
5217
  }
5136
5218
  /**
5137
5219
  * Get all managers as [root, manager] pairs.
@@ -5148,7 +5230,7 @@ class VcsCoordinator {
5148
5230
  */
5149
5231
  findManagerForPath(normalizedPath) {
5150
5232
  const root = findRootForPath(this.roots, normalizedPath);
5151
- return root ? this.managers.get(root) : undefined;
5233
+ return root ? this.managers.get(normalizePathCase(root)) : undefined;
5152
5234
  }
5153
5235
  }
5154
5236
 
@@ -5336,7 +5418,15 @@ async function initVcs(config, logger) {
5336
5418
  initializedRoots.add(root);
5337
5419
  await initRepo(root);
5338
5420
  await ensureGitignore(root);
5421
+ // Resolve author identity: per-path → root-level → schema defaults.
5422
+ const defaults = vcsAuthorConfigSchema.parse({});
5423
+ const pathAuthor = entry.vcs?.author;
5424
+ const rootAuthor = config.vcs.author;
5425
+ const authorName = pathAuthor?.name ?? rootAuthor?.name ?? defaults.name;
5426
+ const authorEmail = pathAuthor?.email ?? rootAuthor?.email ?? defaults.email;
5427
+ await configureRepoIdentity(root, authorName, authorEmail);
5339
5428
  logger.info({ root }, 'VCS initialized for watch root');
5429
+ logger.debug({ root, authorName, authorEmail }, 'VCS author identity configured');
5340
5430
  }
5341
5431
  return true;
5342
5432
  }
@@ -8845,7 +8935,7 @@ class JeevesWatcher {
8845
8935
  this.watcher = watcher;
8846
8936
  this.gitignoreFilter = gitignoreFilter;
8847
8937
  this.server = await this.startApiServer();
8848
- vcsCoordinator.start();
8938
+ await vcsCoordinator.start();
8849
8939
  this.watcher.start();
8850
8940
  this.startConfigWatch();
8851
8941
  logger.info('jeeves-watcher started');
package/package.json CHANGED
@@ -9,7 +9,7 @@
9
9
  "dependencies": {
10
10
  "@commander-js/extra-typings": "^14.0.0",
11
11
  "@karmaniverous/jeeves": "^0.5.12",
12
- "@karmaniverous/jeeves-watcher-core": "^0.2.1",
12
+ "@karmaniverous/jeeves-watcher-core": "^0.2.2",
13
13
  "@karmaniverous/jsonmap": "^2.1.1",
14
14
  "@langchain/textsplitters": "*",
15
15
  "@qdrant/js-client-rest": "*",
@@ -149,5 +149,5 @@
149
149
  },
150
150
  "type": "module",
151
151
  "types": "dist/index.d.ts",
152
- "version": "0.18.3"
152
+ "version": "0.18.5"
153
153
  }