@wrongstack/tools 0.306.0 → 0.306.3

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/index.js CHANGED
@@ -11503,6 +11503,85 @@ function runSqliteWithRetry(fn) {
11503
11503
  throw lastError;
11504
11504
  }
11505
11505
 
11506
+ // src/codebase-index/vector-search.ts
11507
+ var RRF_K = 60;
11508
+ var VECTOR_DIMENSIONS = 384;
11509
+ var NGRAM_SIZE = 3;
11510
+ function embedText(text) {
11511
+ const vec = new Float32Array(VECTOR_DIMENSIONS);
11512
+ const normalized = text.toLowerCase().trim();
11513
+ if (normalized.length < NGRAM_SIZE) {
11514
+ const padded = ` ${normalized} `.slice(0, Math.max(NGRAM_SIZE, normalized.length + 2));
11515
+ for (let i = 0; i <= padded.length - NGRAM_SIZE; i++) {
11516
+ const ngram = padded.slice(i, i + NGRAM_SIZE);
11517
+ const bucket = hashNgram(ngram) % VECTOR_DIMENSIONS;
11518
+ vec[bucket] += 1;
11519
+ }
11520
+ } else {
11521
+ for (let i = 0; i <= normalized.length - NGRAM_SIZE; i++) {
11522
+ const ngram = normalized.slice(i, i + NGRAM_SIZE);
11523
+ const bucket = hashNgram(ngram) % VECTOR_DIMENSIONS;
11524
+ vec[bucket] += 1;
11525
+ }
11526
+ }
11527
+ let norm = 0;
11528
+ for (let i = 0; i < VECTOR_DIMENSIONS; i++) {
11529
+ norm += vec[i] * vec[i];
11530
+ }
11531
+ norm = Math.sqrt(norm);
11532
+ if (norm > 0) {
11533
+ for (let i = 0; i < VECTOR_DIMENSIONS; i++) {
11534
+ vec[i] /= norm;
11535
+ }
11536
+ }
11537
+ return vec;
11538
+ }
11539
+ function hashNgram(str) {
11540
+ let hash = 2166136261;
11541
+ for (let i = 0; i < str.length; i++) {
11542
+ hash ^= str.charCodeAt(i);
11543
+ hash = Math.imul(hash, 16777619);
11544
+ }
11545
+ return hash >>> 0;
11546
+ }
11547
+ function cosineSimilarity(a, b) {
11548
+ let dot = 0;
11549
+ const len = Math.min(a.length, b.length);
11550
+ for (let i = 0; i < len; i++) {
11551
+ dot += a[i] * b[i];
11552
+ }
11553
+ return dot;
11554
+ }
11555
+ function encodeVector(vec) {
11556
+ return Buffer.from(vec.buffer, vec.byteOffset, vec.byteLength);
11557
+ }
11558
+ function decodeVector(buf) {
11559
+ const view = new DataView(
11560
+ buf.buffer,
11561
+ buf.byteOffset,
11562
+ buf.byteLength
11563
+ );
11564
+ const copy = new Float32Array(buf.byteLength / 4);
11565
+ for (let i = 0; i < copy.length; i++) {
11566
+ copy[i] = view.getFloat32(i * 4, true);
11567
+ }
11568
+ return copy;
11569
+ }
11570
+ function reciprocalRankFusion(bm25Ranks, vectorRanks, k = RRF_K) {
11571
+ const allIds = /* @__PURE__ */ new Set([...bm25Ranks.keys(), ...vectorRanks.keys()]);
11572
+ const scored = [];
11573
+ for (const id of allIds) {
11574
+ const bm25Rank = bm25Ranks.get(id);
11575
+ const vecRank = vectorRanks.get(id);
11576
+ let score = 0;
11577
+ if (bm25Rank !== void 0) score += 1 / (k + bm25Rank);
11578
+ if (vecRank !== void 0) score += 1 / (k + vecRank);
11579
+ scored.push([id, score]);
11580
+ }
11581
+ scored.sort((a, b) => b[1] - a[1]);
11582
+ return scored;
11583
+ }
11584
+
11506
11585
  // src/codebase-index/writer-admin.ts
11507
11586
  import * as fs9 from "node:fs";
11508
11587
  import * as path14 from "node:path";
@@ -12864,90 +12943,19 @@ var StorePool = class {
12864
12943
  }
12865
12944
  };
12866
12945
 
12867
- // src/codebase-index/vector-search.ts
12868
- var RRF_K = 60;
12869
- var VECTOR_DIMENSIONS = 384;
12870
- var NGRAM_SIZE = 3;
12871
- function embedText(text) {
12872
- const vec = new Float32Array(VECTOR_DIMENSIONS);
12873
- const normalized = text.toLowerCase().trim();
12874
- if (normalized.length < NGRAM_SIZE) {
12875
- const padded = ` ${normalized} `.slice(0, Math.max(NGRAM_SIZE, normalized.length + 2));
12876
- for (let i = 0; i <= padded.length - NGRAM_SIZE; i++) {
12877
- const ngram = padded.slice(i, i + NGRAM_SIZE);
12878
- const bucket = hashNgram(ngram) % VECTOR_DIMENSIONS;
12879
- vec[bucket] += 1;
12880
- }
12881
- } else {
12882
- for (let i = 0; i <= normalized.length - NGRAM_SIZE; i++) {
12883
- const ngram = normalized.slice(i, i + NGRAM_SIZE);
12884
- const bucket = hashNgram(ngram) % VECTOR_DIMENSIONS;
12885
- vec[bucket] += 1;
12886
- }
12887
- }
12888
- let norm = 0;
12889
- for (let i = 0; i < VECTOR_DIMENSIONS; i++) {
12890
- norm += vec[i] * vec[i];
12891
- }
12892
- norm = Math.sqrt(norm);
12893
- if (norm > 0) {
12894
- for (let i = 0; i < VECTOR_DIMENSIONS; i++) {
12895
- vec[i] /= norm;
12896
- }
12897
- }
12898
- return vec;
12899
- }
12900
- function hashNgram(str) {
12901
- let hash = 2166136261;
12902
- for (let i = 0; i < str.length; i++) {
12903
- hash ^= str.charCodeAt(i);
12904
- hash = Math.imul(hash, 16777619);
12905
- }
12906
- return hash >>> 0;
12907
- }
12908
- function cosineSimilarity(a, b) {
12909
- let dot = 0;
12910
- const len = Math.min(a.length, b.length);
12911
- for (let i = 0; i < len; i++) {
12912
- dot += a[i] * b[i];
12913
- }
12914
- return dot;
12915
- }
12916
- function encodeVector(vec) {
12917
- return Buffer.from(vec.buffer, vec.byteOffset, vec.byteLength);
12918
- }
12919
- function decodeVector(buf) {
12920
- const view = new DataView(
12921
- buf.buffer,
12922
- buf.byteOffset,
12923
- buf.byteLength
12924
- );
12925
- const copy = new Float32Array(buf.byteLength / 4);
12926
- for (let i = 0; i < copy.length; i++) {
12927
- copy[i] = view.getFloat32(i * 4, true);
12928
- }
12929
- return copy;
12930
- }
12931
- function reciprocalRankFusion(bm25Ranks, vectorRanks, k = RRF_K) {
12932
- const allIds = /* @__PURE__ */ new Set([...bm25Ranks.keys(), ...vectorRanks.keys()]);
12933
- const scored = [];
12934
- for (const id of allIds) {
12935
- const bm25Rank = bm25Ranks.get(id);
12936
- const vecRank = vectorRanks.get(id);
12937
- let score = 0;
12938
- if (bm25Rank !== void 0) score += 1 / (k + bm25Rank);
12939
- if (vecRank !== void 0) score += 1 / (k + vecRank);
12940
- scored.push([id, score]);
12941
- }
12942
- scored.sort((a, b) => b[1] - a[1]);
12943
- return scored;
12944
- }
12945
-
12946
12946
  // src/codebase-index/writer.ts
12947
12947
  var DB_FILE2 = "index.db";
12948
12948
  var MAX_STATEMENT_CACHE = 128;
12949
12949
  var IndexStore = class _IndexStore {
12950
12950
  db;
12951
+ /**
12952
+ * True while an index run owns one outer SQLite transaction. Individual
12953
+ * writer methods normally protect themselves with BEGIN/COMMIT, but during
12954
+ * a refresh they join this transaction so readers observe either the last
12955
+ * completed index or the next completed index, never an in-between batch.
12956
+ */
12957
+ atomicIndexUpdateActive = false;
12958
+ writeSavepointSequence = 0;
12951
12959
  /** Absolute path to this project's index directory. */
12952
12960
  indexDir;
12953
12961
  /**
@@ -13028,6 +13036,51 @@ var IndexStore = class _IndexStore {
13028
13036
  runWithRetry(fn) {
13029
13037
  return runSqliteWithRetry(fn);
13030
13038
  }
13039
+ /** Run a complete index mutation as one WAL-visible publication. */
13040
+ async runAtomicIndexUpdate(job) {
13041
+ if (this.atomicIndexUpdateActive) return job();
13042
+ this.runWithRetry(() => this.db.exec("BEGIN IMMEDIATE"));
13043
+ this.atomicIndexUpdateActive = true;
13044
+ try {
13045
+ const result = await job();
13046
+ this.db.exec("COMMIT");
13047
+ return result;
13048
+ } catch (error) {
13049
+ try {
13050
+ this.db.exec("ROLLBACK");
13051
+ } catch {
13052
+ }
13053
+ throw error;
13054
+ } finally {
13055
+ this.atomicIndexUpdateActive = false;
13056
+ }
13057
+ }
13058
+ /**
13059
+ * Begin a method-local transaction. Inside an atomic index publication a
13060
+ * SAVEPOINT preserves the old per-batch rollback boundary, which is needed
13061
+ * when commitBatch falls back to per-file writes after one batch fails.
13062
+ */
13063
+ beginWriteTransaction() {
13064
+ if (this.atomicIndexUpdateActive) {
13065
+ const savepoint = `index_write_${++this.writeSavepointSequence}`;
13066
+ this.db.exec(`SAVEPOINT ${savepoint}`);
13067
+ return savepoint;
13068
+ }
13069
+ this.db.exec("BEGIN IMMEDIATE");
13070
+ return null;
13071
+ }
13072
+ commitWriteTransaction(savepoint) {
13073
+ if (savepoint) this.db.exec(`RELEASE SAVEPOINT ${savepoint}`);
13074
+ else this.db.exec("COMMIT");
13075
+ }
13076
+ rollbackWriteTransaction(savepoint) {
13077
+ if (savepoint) {
13078
+ this.db.exec(`ROLLBACK TO SAVEPOINT ${savepoint}`);
13079
+ this.db.exec(`RELEASE SAVEPOINT ${savepoint}`);
13080
+ } else {
13081
+ this.db.exec("ROLLBACK");
13082
+ }
13083
+ }
13031
13084
  /**
13032
13085
  * Mirror the in-process language→family map into SQLite.
13033
13086
  *
@@ -13262,7 +13315,7 @@ var IndexStore = class _IndexStore {
13262
13315
  insertSymbols(symbols) {
13263
13316
  this.invalidateBm25();
13264
13317
  return this.runWithRetry(() => {
13265
- this.db.exec("BEGIN IMMEDIATE");
13318
+ const ownsTransaction = this.beginWriteTransaction();
13266
13319
  try {
13267
13320
  let nextId = this.allocateSymbolIds(symbols.length);
13268
13321
  const result = [];
@@ -13289,7 +13342,9 @@ var IndexStore = class _IndexStore {
13289
13342
  }
13290
13343
  vectorRows.push({
13291
13344
  id,
13292
- vector: encodeVector(embedText(s.text || buildIndexableText(s.name, s.signature, s.docComment)))
13345
+ vector: encodeVector(
13346
+ embedText(s.text || buildIndexableText(s.name, s.signature, s.docComment))
13347
+ )
13293
13348
  });
13294
13349
  result.push({ ...s, id });
13295
13350
  }
@@ -13307,10 +13362,10 @@ var IndexStore = class _IndexStore {
13307
13362
  vectorRows
13308
13363
  );
13309
13364
  }
13310
- this.db.exec("COMMIT");
13365
+ this.commitWriteTransaction(ownsTransaction);
13311
13366
  return result;
13312
13367
  } catch (err) {
13313
- this.db.exec("ROLLBACK");
13368
+ this.rollbackWriteTransaction(ownsTransaction);
13314
13369
  throw err;
13315
13370
  }
13316
13371
  });
@@ -13318,7 +13373,7 @@ var IndexStore = class _IndexStore {
13318
13373
  deleteSymbolsForFile(file) {
13319
13374
  this.invalidateBm25();
13320
13375
  this.runWithRetry(() => {
13321
- this.db.exec("BEGIN IMMEDIATE");
13376
+ const ownsTransaction = this.beginWriteTransaction();
13322
13377
  try {
13323
13378
  const affectedNames = this.invalidateIncomingRefsForFiles([file]);
13324
13379
  if (this.ftsAvailable) {
@@ -13333,9 +13388,9 @@ var IndexStore = class _IndexStore {
13333
13388
  }
13334
13389
  this.stmt("DELETE FROM symbols WHERE file_fk = ?").run(file);
13335
13390
  this.resolveRefsForNamesUnsafe(affectedNames);
13336
- this.db.exec("COMMIT");
13391
+ this.commitWriteTransaction(ownsTransaction);
13337
13392
  } catch (error) {
13338
- this.db.exec("ROLLBACK");
13393
+ this.rollbackWriteTransaction(ownsTransaction);
13339
13394
  throw error;
13340
13395
  }
13341
13396
  });
@@ -13348,7 +13403,7 @@ var IndexStore = class _IndexStore {
13348
13403
  deleteFile(file) {
13349
13404
  this.invalidateBm25();
13350
13405
  this.runWithRetry(() => {
13351
- this.db.exec("BEGIN IMMEDIATE");
13406
+ const ownsTransaction = this.beginWriteTransaction();
13352
13407
  try {
13353
13408
  const affectedNames = this.invalidateIncomingRefsForFiles([file]);
13354
13409
  if (this.ftsAvailable) {
@@ -13367,9 +13422,9 @@ var IndexStore = class _IndexStore {
13367
13422
  this.stmt("DELETE FROM symbols WHERE file_fk = ?").run(file);
13368
13423
  this.stmt("DELETE FROM files WHERE file = ?").run(file);
13369
13424
  this.resolveRefsForNamesUnsafe(affectedNames);
13370
- this.db.exec("COMMIT");
13425
+ this.commitWriteTransaction(ownsTransaction);
13371
13426
  } catch (err) {
13372
- this.db.exec("ROLLBACK");
13427
+ this.rollbackWriteTransaction(ownsTransaction);
13373
13428
  throw err;
13374
13429
  }
13375
13430
  });
@@ -13756,7 +13811,7 @@ var IndexStore = class _IndexStore {
13756
13811
  clearAll() {
13757
13812
  this.invalidateBm25();
13758
13813
  this.runWithRetry(() => {
13759
- this.db.exec("BEGIN IMMEDIATE");
13814
+ const ownsTransaction = this.beginWriteTransaction();
13760
13815
  try {
13761
13816
  this.db.exec("DROP TABLE IF EXISTS refs");
13762
13817
  this.db.exec("DROP TABLE IF EXISTS symbols");
@@ -13764,15 +13819,15 @@ var IndexStore = class _IndexStore {
13764
13819
  this.db.exec("DROP TABLE IF EXISTS metadata");
13765
13820
  if (this.ftsAvailable) this.db.exec("DROP TABLE IF EXISTS symbols_fts");
13766
13821
  this.db.exec("DROP TABLE IF EXISTS symbol_vectors");
13767
- this.db.exec("COMMIT");
13768
13822
  this.stmtCache.clear();
13769
13823
  this.initSchema();
13770
13824
  this.stmt("INSERT OR REPLACE INTO metadata(key, value) VALUES (?, ?)").run(
13771
13825
  _IndexStore.NEXT_SYMBOL_ID_KEY,
13772
13826
  "1"
13773
13827
  );
13828
+ this.commitWriteTransaction(ownsTransaction);
13774
13829
  } catch (err) {
13775
- this.db.exec("ROLLBACK");
13830
+ this.rollbackWriteTransaction(ownsTransaction);
13776
13831
  throw err;
13777
13832
  }
13778
13833
  });
@@ -13834,7 +13889,7 @@ var IndexStore = class _IndexStore {
13834
13889
  }
13835
13890
  this.invalidateBm25();
13836
13891
  return this.runWithRetry(() => {
13837
- this.db.exec("BEGIN IMMEDIATE");
13892
+ const ownsTransaction = this.beginWriteTransaction();
13838
13893
  try {
13839
13894
  const affectedNames = /* @__PURE__ */ new Set();
13840
13895
  for (const entry of entries) {
@@ -13895,7 +13950,9 @@ var IndexStore = class _IndexStore {
13895
13950
  }
13896
13951
  vectorRows.push({
13897
13952
  id,
13898
- vector: encodeVector(embedText(s.text || buildIndexableText(s.name, s.signature, s.docComment)))
13953
+ vector: encodeVector(
13954
+ embedText(s.text || buildIndexableText(s.name, s.signature, s.docComment))
13955
+ )
13899
13956
  });
13900
13957
  const inserted = { ...s, id };
13901
13958
  allInserted.push(inserted);
@@ -13940,10 +13997,10 @@ var IndexStore = class _IndexStore {
13940
13997
  );
13941
13998
  }
13942
13999
  this.resolveRefsForNamesUnsafe(affectedNames);
13943
- this.db.exec("COMMIT");
14000
+ this.commitWriteTransaction(ownsTransaction);
13944
14001
  return allInserted;
13945
14002
  } catch (err) {
13946
- this.db.exec("ROLLBACK");
14003
+ this.rollbackWriteTransaction(ownsTransaction);
13947
14004
  throw err;
13948
14005
  }
13949
14006
  });
@@ -14020,7 +14077,7 @@ var IndexStore = class _IndexStore {
14020
14077
  replaceEmptyFile(meta) {
14021
14078
  this.invalidateBm25();
14022
14079
  this.runWithRetry(() => {
14023
- this.db.exec("BEGIN IMMEDIATE");
14080
+ const ownsTransaction = this.beginWriteTransaction();
14024
14081
  try {
14025
14082
  const affectedNames = this.invalidateIncomingRefsForFiles([meta.file]);
14026
14083
  if (this.ftsAvailable) {
@@ -14055,9 +14112,9 @@ var IndexStore = class _IndexStore {
14055
14112
  meta.lastIndexed
14056
14113
  );
14057
14114
  this.resolveRefsForNamesUnsafe(affectedNames);
14058
- this.db.exec("COMMIT");
14115
+ this.commitWriteTransaction(ownsTransaction);
14059
14116
  } catch (err) {
14060
- this.db.exec("ROLLBACK");
14117
+ this.rollbackWriteTransaction(ownsTransaction);
14061
14118
  throw err;
14062
14119
  }
14063
14120
  });
@@ -15015,6 +15072,7 @@ import { Worker as Worker2 } from "node:worker_threads";
15015
15072
  // src/codebase-index/indexer.ts
15016
15073
  import { expectDefined as expectDefined6 } from "@wrongstack/core/utils";
15017
15074
  import { execFile } from "node:child_process";
15075
+ import { createHash as createHash5 } from "node:crypto";
15018
15076
  import * as fs18 from "node:fs/promises";
15019
15077
  import { availableParallelism } from "node:os";
15020
15078
  import * as path24 from "node:path";
@@ -15855,6 +15913,10 @@ var DEFAULT_IGNORE_FILES = /* @__PURE__ */ new Set(["package-lock.json", "pnpm-l
15855
15913
  var INDEXABLE_EXTENSION_SET = new Set(INDEXABLE_EXTENSIONS);
15856
15914
  var MAX_INDEX_FILE_BYTES = 5 * 1024 * 1024;
15857
15915
  var MAX_GIT_FILE_LIST_BYTES = 64 * 1024 * 1024;
15916
+ var GIT_SNAPSHOT_METADATA_KEY = "git_discovery_snapshot";
15917
+ var IndexSourceChangedError = class extends Error {
15918
+ name = "IndexSourceChangedError";
15919
+ };
15858
15920
  function isWithinProject(projectRoot, file) {
15859
15921
  const rel = path24.relative(projectRoot, file);
15860
15922
  return rel !== "" && !rel.startsWith(`..${path24.sep}`) && rel !== ".." && !path24.isAbsolute(rel);
@@ -15891,7 +15953,7 @@ async function findGitSourceFiles(projectRoot, ignore, signal) {
15891
15953
  if (normalizeComparablePath(topLevel) !== normalizeComparablePath(projectRoot)) return null;
15892
15954
  throwIfAborted(signal);
15893
15955
  const ignoreSet = /* @__PURE__ */ new Set([...DEFAULT_IGNORE, ...ignore]);
15894
- const [output, statusOutput] = await Promise.all([
15956
+ const [output, statusOutput, stagedOutput] = await Promise.all([
15895
15957
  gitOutput(projectRoot, ["ls-files", "--cached", "--others", "--exclude-standard", "-z"]),
15896
15958
  gitOutput(projectRoot, [
15897
15959
  "status",
@@ -15899,7 +15961,8 @@ async function findGitSourceFiles(projectRoot, ignore, signal) {
15899
15961
  "-z",
15900
15962
  "--untracked-files=all",
15901
15963
  "--ignored=no"
15902
- ])
15964
+ ]),
15965
+ gitOutput(projectRoot, ["ls-files", "--stage", "-z"])
15903
15966
  ]);
15904
15967
  throwIfAborted(signal);
15905
15968
  const dirty = /* @__PURE__ */ new Set();
@@ -15929,9 +15992,17 @@ async function findGitSourceFiles(projectRoot, ignore, signal) {
15929
15992
  const ext = path24.extname(relative13).toLowerCase();
15930
15993
  if (INDEXABLE_EXTENSION_SET.has(ext) || detectLang(full) !== null) files.push(full);
15931
15994
  }
15995
+ const snapshot = createHash5("sha256").update(stagedOutput).update("\0").update(statusOutput);
15996
+ const indexedFiles = new Set(files);
15997
+ for (const dirtyFile of [...dirty].sort()) {
15998
+ if (!indexedFiles.has(dirtyFile) || deleted.has(dirtyFile)) continue;
15999
+ snapshot.update("\0").update(dirtyFile).update("\0");
16000
+ snapshot.update(xxhash64String(await fs18.readFile(dirtyFile, "utf8")));
16001
+ }
15932
16002
  return {
15933
16003
  files,
15934
- trustedUnchanged: new Set(files.filter((file) => !dirty.has(file)))
16004
+ trustedUnchanged: new Set(files.filter((file) => !dirty.has(file))),
16005
+ snapshotKey: snapshot.digest("hex")
15935
16006
  };
15936
16007
  } catch {
15937
16008
  return null;
@@ -15944,7 +16015,8 @@ async function findSourceFiles(projectRoot, ignore, isGitIgnored, signal) {
15944
16015
  files: gitFiles.files,
15945
16016
  complete: true,
15946
16017
  errors: [],
15947
- trustedUnchanged: gitFiles.trustedUnchanged
16018
+ trustedUnchanged: gitFiles.trustedUnchanged,
16019
+ snapshotKey: gitFiles.snapshotKey
15948
16020
  };
15949
16021
  }
15950
16022
  const results = [];
@@ -16031,6 +16103,17 @@ async function resolveProjectRelations(store, projectRoot, opts) {
16031
16103
  }
16032
16104
  }
16033
16105
  async function runIndexerWithStore(store, opts) {
16106
+ let result;
16107
+ try {
16108
+ result = await store.runAtomicIndexUpdate(() => runIndexerAtomic(store, opts));
16109
+ } catch (error) {
16110
+ if (!(error instanceof IndexSourceChangedError)) throw error;
16111
+ result = await store.runAtomicIndexUpdate(() => runIndexerAtomic(store, opts));
16112
+ }
16113
+ if (!opts.files) store.compactIfNeeded();
16114
+ return result;
16115
+ }
16116
+ async function runIndexerAtomic(store, opts) {
16034
16117
  const { projectRoot, langs, ignore = [], signal } = opts;
16035
16118
  const relationGraphVersion = "2";
16036
16119
  const refResolutionVersion = "2";
@@ -16050,6 +16133,7 @@ async function runIndexerWithStore(store, opts) {
16050
16133
  let discoveredFiles = null;
16051
16134
  let discoveryComplete = true;
16052
16135
  let trustedUnchanged;
16136
+ let discoverySnapshotKey;
16053
16137
  if (opts.files && opts.files.length > 0) {
16054
16138
  files = opts.files.map((f) => path24.resolve(projectRoot, f)).filter((f) => {
16055
16139
  if (!isWithinProject(projectRoot, f)) return false;
@@ -16063,6 +16147,7 @@ async function runIndexerWithStore(store, opts) {
16063
16147
  discoveryComplete = discovery.complete;
16064
16148
  discoveredFiles = new Set(files);
16065
16149
  trustedUnchanged = discovery.trustedUnchanged;
16150
+ discoverySnapshotKey = discovery.snapshotKey;
16066
16151
  }
16067
16152
  if (langs && langs.length > 0) {
16068
16153
  const langSet = new Set(langs);
@@ -16076,6 +16161,8 @@ async function runIndexerWithStore(store, opts) {
16076
16161
  if (!force) {
16077
16162
  for (const meta of store.getAllFileMetas()) existingMeta.set(meta.file, meta);
16078
16163
  }
16164
+ const snapshotTrusted = !force && discoverySnapshotKey !== void 0 && store.getMetadata(GIT_SNAPSHOT_METADATA_KEY) === discoverySnapshotKey;
16165
+ if (!snapshotTrusted) trustedUnchanged = void 0;
16079
16166
  const totalFilesForProgress = files.length;
16080
16167
  let filesPreSkipped = 0;
16081
16168
  if (!force && trustedUnchanged) {
@@ -16138,9 +16225,6 @@ async function runIndexerWithStore(store, opts) {
16138
16225
  };
16139
16226
  }
16140
16227
  const meta = existingMeta.get(file);
16141
- if (!force && meta && meta.mtimeMs === Math.floor(stat20.mtimeMs)) {
16142
- return { file, stat: stat20, lang, parsed: null, skippedMeta: meta };
16143
- }
16144
16228
  let content;
16145
16229
  try {
16146
16230
  content = await fs18.readFile(file, { encoding: "utf8", signal });
@@ -16369,9 +16453,18 @@ async function runIndexerWithStore(store, opts) {
16369
16453
  });
16370
16454
  store.setMetadata("ref_resolution_version", refResolutionVersion);
16371
16455
  store.setMetadata("relation_graph_version", relationGraphVersion);
16456
+ const completeProjectScope = !opts.files && (!langs || langs.length === 0) && (!opts.ignore || opts.ignore.length === 0);
16457
+ if (completeProjectScope && discoverySnapshotKey !== void 0) {
16458
+ const finalSnapshot = await findGitSourceFiles(projectRoot, ignore, signal);
16459
+ if (!finalSnapshot || finalSnapshot.snapshotKey !== discoverySnapshotKey) {
16460
+ throw new IndexSourceChangedError(
16461
+ "Project files changed during indexing; retrying before publishing the generation."
16462
+ );
16463
+ }
16464
+ store.setMetadata(GIT_SNAPSHOT_METADATA_KEY, errors.length === 0 ? discoverySnapshotKey : "");
16465
+ }
16372
16466
  if (!opts.files || filesIndexed >= 50) store.optimize();
16373
16467
  store.setLastIndexed(Date.now());
16374
- if (!opts.files) store.compactIfNeeded();
16375
16468
  const durationMs = Date.now() - startMs;
16376
16469
  return {
16377
16470
  filesIndexed,
@@ -16682,6 +16775,13 @@ function callIndexOp(op, args, opts) {
16682
16775
  });
16683
16776
  }
16684
16777
  async function callInline(op, args, opts) {
16778
+ if (op !== "index" && _indexing) {
16779
+ const error = new Error(
16780
+ "Codebase index refresh in progress; retry after the completed generation is published."
16781
+ );
16782
+ error.name = "IndexRefreshInProgressError";
16783
+ throw error;
16784
+ }
16685
16785
  const ac = new AbortController();
16686
16786
  const onOuterAbort = () => ac.abort(opts.signal?.reason ?? new Error("Indexing cancelled"));
16687
16787
  if (opts.signal?.aborted) onOuterAbort();
@@ -17026,12 +17126,12 @@ var codebaseSearchTool = {
17026
17126
  },
17027
17127
  async execute(input, ctx, execOpts) {
17028
17128
  const state = getIndexState();
17029
- if (state.indexing && !state.ready) {
17129
+ if (state.indexing) {
17030
17130
  return {
17031
17131
  results: [],
17032
17132
  total: 0,
17033
17133
  query: input.query,
17034
- indexStatus: `Indexing in progress (${state.currentFile}/${state.totalFiles} files) \u2014 retry in a moment.`
17134
+ indexStatus: `Index refresh in progress (${state.currentFile}/${state.totalFiles} files) \u2014 retry after the completed generation is published.`
17035
17135
  };
17036
17136
  }
17037
17137
  if (state.lastError) {
@@ -17212,12 +17312,12 @@ var codebaseIncomingCallsTool = {
17212
17312
  },
17213
17313
  async execute(input, ctx) {
17214
17314
  const state = getIndexState();
17215
- if (state.indexing && !state.ready) {
17315
+ if (state.indexing) {
17216
17316
  return {
17217
17317
  symbol: input.symbol,
17218
17318
  calls: [],
17219
17319
  total: 0,
17220
- indexStatus: `Indexing in progress (${state.currentFile}/${state.totalFiles} files) \u2014 retry in a moment.`
17320
+ indexStatus: `Index refresh in progress (${state.currentFile}/${state.totalFiles} files) \u2014 retry after the completed generation is published.`
17221
17321
  };
17222
17322
  }
17223
17323
  if (state.lastError) {
@@ -17234,16 +17334,14 @@ var codebaseIncomingCallsTool = {
17234
17334
  const transitive = input.transitive === true;
17235
17335
  let serviced;
17236
17336
  try {
17237
- serviced = await incomingCallsService2(
17238
- {
17239
- projectRoot: ctx.projectRoot,
17240
- indexDir: codebaseIndexDirOverride(ctx),
17241
- symbol: input.symbol,
17242
- file: input.file,
17243
- limit,
17244
- transitive
17245
- }
17246
- );
17337
+ serviced = await incomingCallsService2({
17338
+ projectRoot: ctx.projectRoot,
17339
+ indexDir: codebaseIndexDirOverride(ctx),
17340
+ symbol: input.symbol,
17341
+ file: input.file,
17342
+ limit,
17343
+ transitive
17344
+ });
17247
17345
  } catch (err) {
17248
17346
  return {
17249
17347
  symbol: input.symbol,
@@ -17282,10 +17380,14 @@ var codebaseIncomingCallsTool = {
17282
17380
  }
17283
17381
  const notes = [];
17284
17382
  if (totalMatches > limit) {
17285
- notes.push(`Results capped at ${limit} of ${totalMatches} call sites. Increase \`limit\` or use \`file\` to narrow.`);
17383
+ notes.push(
17384
+ `Results capped at ${limit} of ${totalMatches} call sites. Increase \`limit\` or use \`file\` to narrow.`
17385
+ );
17286
17386
  }
17287
17387
  if (ambiguous) {
17288
- notes.push(`Symbol "${input.symbol}" exists in multiple files. Results include callers of all same-named symbols. Use codebase-search to find the exact file and pass it as \`file\`.`);
17388
+ notes.push(
17389
+ `Symbol "${input.symbol}" exists in multiple files. Results include callers of all same-named symbols. Use codebase-search to find the exact file and pass it as \`file\`.`
17390
+ );
17289
17391
  }
17290
17392
  return {
17291
17393
  symbol: input.symbol,
@@ -17335,12 +17437,12 @@ var codebaseOutgoingCallsTool = {
17335
17437
  },
17336
17438
  async execute(input, ctx) {
17337
17439
  const state = getIndexState();
17338
- if (state.indexing && !state.ready) {
17440
+ if (state.indexing) {
17339
17441
  return {
17340
17442
  symbol: input.symbol,
17341
17443
  calls: [],
17342
17444
  total: 0,
17343
- indexStatus: `Indexing in progress (${state.currentFile}/${state.totalFiles} files) \u2014 retry in a moment.`
17445
+ indexStatus: `Index refresh in progress (${state.currentFile}/${state.totalFiles} files) \u2014 retry after the completed generation is published.`
17344
17446
  };
17345
17447
  }
17346
17448
  if (state.lastError) {
@@ -17357,16 +17459,14 @@ var codebaseOutgoingCallsTool = {
17357
17459
  const transitive = input.transitive === true;
17358
17460
  let serviced;
17359
17461
  try {
17360
- serviced = await outgoingCallsService2(
17361
- {
17362
- projectRoot: ctx.projectRoot,
17363
- indexDir: codebaseIndexDirOverride(ctx),
17364
- symbol: input.symbol,
17365
- file: input.file,
17366
- limit,
17367
- transitive
17368
- }
17369
- );
17462
+ serviced = await outgoingCallsService2({
17463
+ projectRoot: ctx.projectRoot,
17464
+ indexDir: codebaseIndexDirOverride(ctx),
17465
+ symbol: input.symbol,
17466
+ file: input.file,
17467
+ limit,
17468
+ transitive
17469
+ });
17370
17470
  } catch (err) {
17371
17471
  return {
17372
17472
  symbol: input.symbol,
@@ -17405,10 +17505,14 @@ var codebaseOutgoingCallsTool = {
17405
17505
  }
17406
17506
  const notes = [];
17407
17507
  if (totalMatches > limit) {
17408
- notes.push(`Results capped at ${limit} of ${totalMatches} call sites. Increase \`limit\` or use \`file\` to narrow.`);
17508
+ notes.push(
17509
+ `Results capped at ${limit} of ${totalMatches} call sites. Increase \`limit\` or use \`file\` to narrow.`
17510
+ );
17409
17511
  }
17410
17512
  if (unresolvedCount > 0) {
17411
- notes.push(`${unresolvedCount} unresolved reference(s) not shown \u2014 their targets could not be resolved during indexing.`);
17513
+ notes.push(
17514
+ `${unresolvedCount} unresolved reference(s) not shown \u2014 their targets could not be resolved during indexing.`
17515
+ );
17412
17516
  }
17413
17517
  return {
17414
17518
  symbol: input.symbol,
@@ -25035,6 +25139,7 @@ function sourceStatus(task) {
25035
25139
  function todoStatus(task) {
25036
25140
  const status = sourceStatus(task);
25037
25141
  if (status === "completed") return "completed";
25142
+ if (status === "review" && task.assignment?.status === "completed") return "completed";
25038
25143
  if (status === "in_progress" || status === "review") return "in_progress";
25039
25144
  return "pending";
25040
25145
  }
@@ -25156,11 +25261,12 @@ async function applySessionKanbanTaskToSource(context, task, options = {}) {
25156
25261
  const graphId = task.origin?.graphId ?? "";
25157
25262
  if (!originId) return { source: null };
25158
25263
  if (task.origin?.system === "session-todo" || graphId.startsWith("todo:")) {
25264
+ const mappedStatus = todoStatus(task);
25159
25265
  const next = options.remove ? context.todos.filter((todo) => todo.id !== originId) : context.todos.map(
25160
25266
  (todo) => todo.id === originId ? {
25161
25267
  ...todo,
25162
25268
  content: task.title,
25163
- status: sourceStatus(task) === "completed" ? "completed" : sourceStatus(task) === "in_progress" || sourceStatus(task) === "review" ? "in_progress" : "pending"
25269
+ status: mappedStatus
25164
25270
  } : todo
25165
25271
  );
25166
25272
  suppressedTodoMirrors.add(context);
@@ -27077,6 +27183,20 @@ async function synchronizeManagedKanban(items, board, ctx, signal) {
27077
27183
  }
27078
27184
  const task = board.tasks.find((candidate) => candidate.id === item.kanbanTaskId);
27079
27185
  if (!task || task.status === "completed") continue;
27186
+ const stage = task.lifecycle?.currentStage;
27187
+ if (stage === "backlog" || stage === "todo") {
27188
+ const started = await execute({
27189
+ action: "start_task",
27190
+ boardId: board.id,
27191
+ taskId: task.id,
27192
+ author: actor,
27193
+ agentId: actor,
27194
+ transitionComment: `Auto-started for completion: ${item.content}`
27195
+ });
27196
+ if (!started.ok) {
27197
+ continue;
27198
+ }
27199
+ }
27080
27200
  await execute({
27081
27201
  action: "mark_assignment",
27082
27202
  boardId: board.id,