@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/builtin.js CHANGED
@@ -11133,6 +11133,85 @@ function runSqliteWithRetry(fn) {
11133
11133
  throw lastError;
11134
11134
  }
11135
11135
 
11136
+ // src/codebase-index/vector-search.ts
11137
+ var RRF_K = 60;
11138
+ var VECTOR_DIMENSIONS = 384;
11139
+ var NGRAM_SIZE = 3;
11140
+ function embedText(text) {
11141
+ const vec = new Float32Array(VECTOR_DIMENSIONS);
11142
+ const normalized = text.toLowerCase().trim();
11143
+ if (normalized.length < NGRAM_SIZE) {
11144
+ const padded = ` ${normalized} `.slice(0, Math.max(NGRAM_SIZE, normalized.length + 2));
11145
+ for (let i = 0; i <= padded.length - NGRAM_SIZE; i++) {
11146
+ const ngram = padded.slice(i, i + NGRAM_SIZE);
11147
+ const bucket = hashNgram(ngram) % VECTOR_DIMENSIONS;
11148
+ vec[bucket] += 1;
11149
+ }
11150
+ } else {
11151
+ for (let i = 0; i <= normalized.length - NGRAM_SIZE; i++) {
11152
+ const ngram = normalized.slice(i, i + NGRAM_SIZE);
11153
+ const bucket = hashNgram(ngram) % VECTOR_DIMENSIONS;
11154
+ vec[bucket] += 1;
11155
+ }
11156
+ }
11157
+ let norm = 0;
11158
+ for (let i = 0; i < VECTOR_DIMENSIONS; i++) {
11159
+ norm += vec[i] * vec[i];
11160
+ }
11161
+ norm = Math.sqrt(norm);
11162
+ if (norm > 0) {
11163
+ for (let i = 0; i < VECTOR_DIMENSIONS; i++) {
11164
+ vec[i] /= norm;
11165
+ }
11166
+ }
11167
+ return vec;
11168
+ }
11169
+ function hashNgram(str) {
11170
+ let hash = 2166136261;
11171
+ for (let i = 0; i < str.length; i++) {
11172
+ hash ^= str.charCodeAt(i);
11173
+ hash = Math.imul(hash, 16777619);
11174
+ }
11175
+ return hash >>> 0;
11176
+ }
11177
+ function cosineSimilarity(a, b) {
11178
+ let dot = 0;
11179
+ const len = Math.min(a.length, b.length);
11180
+ for (let i = 0; i < len; i++) {
11181
+ dot += a[i] * b[i];
11182
+ }
11183
+ return dot;
11184
+ }
11185
+ function encodeVector(vec) {
11186
+ return Buffer.from(vec.buffer, vec.byteOffset, vec.byteLength);
11187
+ }
11188
+ function decodeVector(buf) {
11189
+ const view = new DataView(
11190
+ buf.buffer,
11191
+ buf.byteOffset,
11192
+ buf.byteLength
11193
+ );
11194
+ const copy = new Float32Array(buf.byteLength / 4);
11195
+ for (let i = 0; i < copy.length; i++) {
11196
+ copy[i] = view.getFloat32(i * 4, true);
11197
+ }
11198
+ return copy;
11199
+ }
11200
+ function reciprocalRankFusion(bm25Ranks, vectorRanks, k = RRF_K) {
11201
+ const allIds = /* @__PURE__ */ new Set([...bm25Ranks.keys(), ...vectorRanks.keys()]);
11202
+ const scored = [];
11203
+ for (const id of allIds) {
11204
+ const bm25Rank = bm25Ranks.get(id);
11205
+ const vecRank = vectorRanks.get(id);
11206
+ let score = 0;
11207
+ if (bm25Rank !== void 0) score += 1 / (k + bm25Rank);
11208
+ if (vecRank !== void 0) score += 1 / (k + vecRank);
11209
+ scored.push([id, score]);
11210
+ }
11211
+ scored.sort((a, b) => b[1] - a[1]);
11212
+ return scored;
11213
+ }
11214
+
11136
11215
  // src/codebase-index/writer-admin.ts
11137
11216
  import * as fs9 from "node:fs";
11138
11217
  import * as path14 from "node:path";
@@ -12494,90 +12573,19 @@ var StorePool = class {
12494
12573
  }
12495
12574
  };
12496
12575
 
12497
- // src/codebase-index/vector-search.ts
12498
- var RRF_K = 60;
12499
- var VECTOR_DIMENSIONS = 384;
12500
- var NGRAM_SIZE = 3;
12501
- function embedText(text) {
12502
- const vec = new Float32Array(VECTOR_DIMENSIONS);
12503
- const normalized = text.toLowerCase().trim();
12504
- if (normalized.length < NGRAM_SIZE) {
12505
- const padded = ` ${normalized} `.slice(0, Math.max(NGRAM_SIZE, normalized.length + 2));
12506
- for (let i = 0; i <= padded.length - NGRAM_SIZE; i++) {
12507
- const ngram = padded.slice(i, i + NGRAM_SIZE);
12508
- const bucket = hashNgram(ngram) % VECTOR_DIMENSIONS;
12509
- vec[bucket] += 1;
12510
- }
12511
- } else {
12512
- for (let i = 0; i <= normalized.length - NGRAM_SIZE; i++) {
12513
- const ngram = normalized.slice(i, i + NGRAM_SIZE);
12514
- const bucket = hashNgram(ngram) % VECTOR_DIMENSIONS;
12515
- vec[bucket] += 1;
12516
- }
12517
- }
12518
- let norm = 0;
12519
- for (let i = 0; i < VECTOR_DIMENSIONS; i++) {
12520
- norm += vec[i] * vec[i];
12521
- }
12522
- norm = Math.sqrt(norm);
12523
- if (norm > 0) {
12524
- for (let i = 0; i < VECTOR_DIMENSIONS; i++) {
12525
- vec[i] /= norm;
12526
- }
12527
- }
12528
- return vec;
12529
- }
12530
- function hashNgram(str) {
12531
- let hash = 2166136261;
12532
- for (let i = 0; i < str.length; i++) {
12533
- hash ^= str.charCodeAt(i);
12534
- hash = Math.imul(hash, 16777619);
12535
- }
12536
- return hash >>> 0;
12537
- }
12538
- function cosineSimilarity(a, b) {
12539
- let dot = 0;
12540
- const len = Math.min(a.length, b.length);
12541
- for (let i = 0; i < len; i++) {
12542
- dot += a[i] * b[i];
12543
- }
12544
- return dot;
12545
- }
12546
- function encodeVector(vec) {
12547
- return Buffer.from(vec.buffer, vec.byteOffset, vec.byteLength);
12548
- }
12549
- function decodeVector(buf) {
12550
- const view = new DataView(
12551
- buf.buffer,
12552
- buf.byteOffset,
12553
- buf.byteLength
12554
- );
12555
- const copy = new Float32Array(buf.byteLength / 4);
12556
- for (let i = 0; i < copy.length; i++) {
12557
- copy[i] = view.getFloat32(i * 4, true);
12558
- }
12559
- return copy;
12560
- }
12561
- function reciprocalRankFusion(bm25Ranks, vectorRanks, k = RRF_K) {
12562
- const allIds = /* @__PURE__ */ new Set([...bm25Ranks.keys(), ...vectorRanks.keys()]);
12563
- const scored = [];
12564
- for (const id of allIds) {
12565
- const bm25Rank = bm25Ranks.get(id);
12566
- const vecRank = vectorRanks.get(id);
12567
- let score = 0;
12568
- if (bm25Rank !== void 0) score += 1 / (k + bm25Rank);
12569
- if (vecRank !== void 0) score += 1 / (k + vecRank);
12570
- scored.push([id, score]);
12571
- }
12572
- scored.sort((a, b) => b[1] - a[1]);
12573
- return scored;
12574
- }
12575
-
12576
12576
  // src/codebase-index/writer.ts
12577
12577
  var DB_FILE2 = "index.db";
12578
12578
  var MAX_STATEMENT_CACHE = 128;
12579
12579
  var IndexStore = class _IndexStore {
12580
12580
  db;
12581
+ /**
12582
+ * True while an index run owns one outer SQLite transaction. Individual
12583
+ * writer methods normally protect themselves with BEGIN/COMMIT, but during
12584
+ * a refresh they join this transaction so readers observe either the last
12585
+ * completed index or the next completed index, never an in-between batch.
12586
+ */
12587
+ atomicIndexUpdateActive = false;
12588
+ writeSavepointSequence = 0;
12581
12589
  /** Absolute path to this project's index directory. */
12582
12590
  indexDir;
12583
12591
  /**
@@ -12658,6 +12666,51 @@ var IndexStore = class _IndexStore {
12658
12666
  runWithRetry(fn) {
12659
12667
  return runSqliteWithRetry(fn);
12660
12668
  }
12669
+ /** Run a complete index mutation as one WAL-visible publication. */
12670
+ async runAtomicIndexUpdate(job) {
12671
+ if (this.atomicIndexUpdateActive) return job();
12672
+ this.runWithRetry(() => this.db.exec("BEGIN IMMEDIATE"));
12673
+ this.atomicIndexUpdateActive = true;
12674
+ try {
12675
+ const result = await job();
12676
+ this.db.exec("COMMIT");
12677
+ return result;
12678
+ } catch (error) {
12679
+ try {
12680
+ this.db.exec("ROLLBACK");
12681
+ } catch {
12682
+ }
12683
+ throw error;
12684
+ } finally {
12685
+ this.atomicIndexUpdateActive = false;
12686
+ }
12687
+ }
12688
+ /**
12689
+ * Begin a method-local transaction. Inside an atomic index publication a
12690
+ * SAVEPOINT preserves the old per-batch rollback boundary, which is needed
12691
+ * when commitBatch falls back to per-file writes after one batch fails.
12692
+ */
12693
+ beginWriteTransaction() {
12694
+ if (this.atomicIndexUpdateActive) {
12695
+ const savepoint = `index_write_${++this.writeSavepointSequence}`;
12696
+ this.db.exec(`SAVEPOINT ${savepoint}`);
12697
+ return savepoint;
12698
+ }
12699
+ this.db.exec("BEGIN IMMEDIATE");
12700
+ return null;
12701
+ }
12702
+ commitWriteTransaction(savepoint) {
12703
+ if (savepoint) this.db.exec(`RELEASE SAVEPOINT ${savepoint}`);
12704
+ else this.db.exec("COMMIT");
12705
+ }
12706
+ rollbackWriteTransaction(savepoint) {
12707
+ if (savepoint) {
12708
+ this.db.exec(`ROLLBACK TO SAVEPOINT ${savepoint}`);
12709
+ this.db.exec(`RELEASE SAVEPOINT ${savepoint}`);
12710
+ } else {
12711
+ this.db.exec("ROLLBACK");
12712
+ }
12713
+ }
12661
12714
  /**
12662
12715
  * Mirror the in-process language→family map into SQLite.
12663
12716
  *
@@ -12892,7 +12945,7 @@ var IndexStore = class _IndexStore {
12892
12945
  insertSymbols(symbols) {
12893
12946
  this.invalidateBm25();
12894
12947
  return this.runWithRetry(() => {
12895
- this.db.exec("BEGIN IMMEDIATE");
12948
+ const ownsTransaction = this.beginWriteTransaction();
12896
12949
  try {
12897
12950
  let nextId = this.allocateSymbolIds(symbols.length);
12898
12951
  const result = [];
@@ -12919,7 +12972,9 @@ var IndexStore = class _IndexStore {
12919
12972
  }
12920
12973
  vectorRows.push({
12921
12974
  id,
12922
- vector: encodeVector(embedText(s.text || buildIndexableText(s.name, s.signature, s.docComment)))
12975
+ vector: encodeVector(
12976
+ embedText(s.text || buildIndexableText(s.name, s.signature, s.docComment))
12977
+ )
12923
12978
  });
12924
12979
  result.push({ ...s, id });
12925
12980
  }
@@ -12937,10 +12992,10 @@ var IndexStore = class _IndexStore {
12937
12992
  vectorRows
12938
12993
  );
12939
12994
  }
12940
- this.db.exec("COMMIT");
12995
+ this.commitWriteTransaction(ownsTransaction);
12941
12996
  return result;
12942
12997
  } catch (err) {
12943
- this.db.exec("ROLLBACK");
12998
+ this.rollbackWriteTransaction(ownsTransaction);
12944
12999
  throw err;
12945
13000
  }
12946
13001
  });
@@ -12948,7 +13003,7 @@ var IndexStore = class _IndexStore {
12948
13003
  deleteSymbolsForFile(file) {
12949
13004
  this.invalidateBm25();
12950
13005
  this.runWithRetry(() => {
12951
- this.db.exec("BEGIN IMMEDIATE");
13006
+ const ownsTransaction = this.beginWriteTransaction();
12952
13007
  try {
12953
13008
  const affectedNames = this.invalidateIncomingRefsForFiles([file]);
12954
13009
  if (this.ftsAvailable) {
@@ -12963,9 +13018,9 @@ var IndexStore = class _IndexStore {
12963
13018
  }
12964
13019
  this.stmt("DELETE FROM symbols WHERE file_fk = ?").run(file);
12965
13020
  this.resolveRefsForNamesUnsafe(affectedNames);
12966
- this.db.exec("COMMIT");
13021
+ this.commitWriteTransaction(ownsTransaction);
12967
13022
  } catch (error) {
12968
- this.db.exec("ROLLBACK");
13023
+ this.rollbackWriteTransaction(ownsTransaction);
12969
13024
  throw error;
12970
13025
  }
12971
13026
  });
@@ -12978,7 +13033,7 @@ var IndexStore = class _IndexStore {
12978
13033
  deleteFile(file) {
12979
13034
  this.invalidateBm25();
12980
13035
  this.runWithRetry(() => {
12981
- this.db.exec("BEGIN IMMEDIATE");
13036
+ const ownsTransaction = this.beginWriteTransaction();
12982
13037
  try {
12983
13038
  const affectedNames = this.invalidateIncomingRefsForFiles([file]);
12984
13039
  if (this.ftsAvailable) {
@@ -12997,9 +13052,9 @@ var IndexStore = class _IndexStore {
12997
13052
  this.stmt("DELETE FROM symbols WHERE file_fk = ?").run(file);
12998
13053
  this.stmt("DELETE FROM files WHERE file = ?").run(file);
12999
13054
  this.resolveRefsForNamesUnsafe(affectedNames);
13000
- this.db.exec("COMMIT");
13055
+ this.commitWriteTransaction(ownsTransaction);
13001
13056
  } catch (err) {
13002
- this.db.exec("ROLLBACK");
13057
+ this.rollbackWriteTransaction(ownsTransaction);
13003
13058
  throw err;
13004
13059
  }
13005
13060
  });
@@ -13386,7 +13441,7 @@ var IndexStore = class _IndexStore {
13386
13441
  clearAll() {
13387
13442
  this.invalidateBm25();
13388
13443
  this.runWithRetry(() => {
13389
- this.db.exec("BEGIN IMMEDIATE");
13444
+ const ownsTransaction = this.beginWriteTransaction();
13390
13445
  try {
13391
13446
  this.db.exec("DROP TABLE IF EXISTS refs");
13392
13447
  this.db.exec("DROP TABLE IF EXISTS symbols");
@@ -13394,15 +13449,15 @@ var IndexStore = class _IndexStore {
13394
13449
  this.db.exec("DROP TABLE IF EXISTS metadata");
13395
13450
  if (this.ftsAvailable) this.db.exec("DROP TABLE IF EXISTS symbols_fts");
13396
13451
  this.db.exec("DROP TABLE IF EXISTS symbol_vectors");
13397
- this.db.exec("COMMIT");
13398
13452
  this.stmtCache.clear();
13399
13453
  this.initSchema();
13400
13454
  this.stmt("INSERT OR REPLACE INTO metadata(key, value) VALUES (?, ?)").run(
13401
13455
  _IndexStore.NEXT_SYMBOL_ID_KEY,
13402
13456
  "1"
13403
13457
  );
13458
+ this.commitWriteTransaction(ownsTransaction);
13404
13459
  } catch (err) {
13405
- this.db.exec("ROLLBACK");
13460
+ this.rollbackWriteTransaction(ownsTransaction);
13406
13461
  throw err;
13407
13462
  }
13408
13463
  });
@@ -13464,7 +13519,7 @@ var IndexStore = class _IndexStore {
13464
13519
  }
13465
13520
  this.invalidateBm25();
13466
13521
  return this.runWithRetry(() => {
13467
- this.db.exec("BEGIN IMMEDIATE");
13522
+ const ownsTransaction = this.beginWriteTransaction();
13468
13523
  try {
13469
13524
  const affectedNames = /* @__PURE__ */ new Set();
13470
13525
  for (const entry of entries) {
@@ -13525,7 +13580,9 @@ var IndexStore = class _IndexStore {
13525
13580
  }
13526
13581
  vectorRows.push({
13527
13582
  id,
13528
- vector: encodeVector(embedText(s.text || buildIndexableText(s.name, s.signature, s.docComment)))
13583
+ vector: encodeVector(
13584
+ embedText(s.text || buildIndexableText(s.name, s.signature, s.docComment))
13585
+ )
13529
13586
  });
13530
13587
  const inserted = { ...s, id };
13531
13588
  allInserted.push(inserted);
@@ -13570,10 +13627,10 @@ var IndexStore = class _IndexStore {
13570
13627
  );
13571
13628
  }
13572
13629
  this.resolveRefsForNamesUnsafe(affectedNames);
13573
- this.db.exec("COMMIT");
13630
+ this.commitWriteTransaction(ownsTransaction);
13574
13631
  return allInserted;
13575
13632
  } catch (err) {
13576
- this.db.exec("ROLLBACK");
13633
+ this.rollbackWriteTransaction(ownsTransaction);
13577
13634
  throw err;
13578
13635
  }
13579
13636
  });
@@ -13650,7 +13707,7 @@ var IndexStore = class _IndexStore {
13650
13707
  replaceEmptyFile(meta) {
13651
13708
  this.invalidateBm25();
13652
13709
  this.runWithRetry(() => {
13653
- this.db.exec("BEGIN IMMEDIATE");
13710
+ const ownsTransaction = this.beginWriteTransaction();
13654
13711
  try {
13655
13712
  const affectedNames = this.invalidateIncomingRefsForFiles([meta.file]);
13656
13713
  if (this.ftsAvailable) {
@@ -13685,9 +13742,9 @@ var IndexStore = class _IndexStore {
13685
13742
  meta.lastIndexed
13686
13743
  );
13687
13744
  this.resolveRefsForNamesUnsafe(affectedNames);
13688
- this.db.exec("COMMIT");
13745
+ this.commitWriteTransaction(ownsTransaction);
13689
13746
  } catch (err) {
13690
- this.db.exec("ROLLBACK");
13747
+ this.rollbackWriteTransaction(ownsTransaction);
13691
13748
  throw err;
13692
13749
  }
13693
13750
  });
@@ -14611,6 +14668,7 @@ import { Worker as Worker2 } from "node:worker_threads";
14611
14668
  // src/codebase-index/indexer.ts
14612
14669
  import { expectDefined as expectDefined6 } from "@wrongstack/core/utils";
14613
14670
  import { execFile } from "node:child_process";
14671
+ import { createHash as createHash5 } from "node:crypto";
14614
14672
  import * as fs18 from "node:fs/promises";
14615
14673
  import { availableParallelism } from "node:os";
14616
14674
  import * as path24 from "node:path";
@@ -15451,6 +15509,10 @@ var DEFAULT_IGNORE_FILES = /* @__PURE__ */ new Set(["package-lock.json", "pnpm-l
15451
15509
  var INDEXABLE_EXTENSION_SET = new Set(INDEXABLE_EXTENSIONS);
15452
15510
  var MAX_INDEX_FILE_BYTES = 5 * 1024 * 1024;
15453
15511
  var MAX_GIT_FILE_LIST_BYTES = 64 * 1024 * 1024;
15512
+ var GIT_SNAPSHOT_METADATA_KEY = "git_discovery_snapshot";
15513
+ var IndexSourceChangedError = class extends Error {
15514
+ name = "IndexSourceChangedError";
15515
+ };
15454
15516
  function isWithinProject(projectRoot, file) {
15455
15517
  const rel = path24.relative(projectRoot, file);
15456
15518
  return rel !== "" && !rel.startsWith(`..${path24.sep}`) && rel !== ".." && !path24.isAbsolute(rel);
@@ -15487,7 +15549,7 @@ async function findGitSourceFiles(projectRoot, ignore, signal) {
15487
15549
  if (normalizeComparablePath(topLevel) !== normalizeComparablePath(projectRoot)) return null;
15488
15550
  throwIfAborted(signal);
15489
15551
  const ignoreSet = /* @__PURE__ */ new Set([...DEFAULT_IGNORE, ...ignore]);
15490
- const [output, statusOutput] = await Promise.all([
15552
+ const [output, statusOutput, stagedOutput] = await Promise.all([
15491
15553
  gitOutput(projectRoot, ["ls-files", "--cached", "--others", "--exclude-standard", "-z"]),
15492
15554
  gitOutput(projectRoot, [
15493
15555
  "status",
@@ -15495,7 +15557,8 @@ async function findGitSourceFiles(projectRoot, ignore, signal) {
15495
15557
  "-z",
15496
15558
  "--untracked-files=all",
15497
15559
  "--ignored=no"
15498
- ])
15560
+ ]),
15561
+ gitOutput(projectRoot, ["ls-files", "--stage", "-z"])
15499
15562
  ]);
15500
15563
  throwIfAborted(signal);
15501
15564
  const dirty = /* @__PURE__ */ new Set();
@@ -15525,9 +15588,17 @@ async function findGitSourceFiles(projectRoot, ignore, signal) {
15525
15588
  const ext = path24.extname(relative12).toLowerCase();
15526
15589
  if (INDEXABLE_EXTENSION_SET.has(ext) || detectLang(full) !== null) files.push(full);
15527
15590
  }
15591
+ const snapshot = createHash5("sha256").update(stagedOutput).update("\0").update(statusOutput);
15592
+ const indexedFiles = new Set(files);
15593
+ for (const dirtyFile of [...dirty].sort()) {
15594
+ if (!indexedFiles.has(dirtyFile) || deleted.has(dirtyFile)) continue;
15595
+ snapshot.update("\0").update(dirtyFile).update("\0");
15596
+ snapshot.update(xxhash64String(await fs18.readFile(dirtyFile, "utf8")));
15597
+ }
15528
15598
  return {
15529
15599
  files,
15530
- trustedUnchanged: new Set(files.filter((file) => !dirty.has(file)))
15600
+ trustedUnchanged: new Set(files.filter((file) => !dirty.has(file))),
15601
+ snapshotKey: snapshot.digest("hex")
15531
15602
  };
15532
15603
  } catch {
15533
15604
  return null;
@@ -15540,7 +15611,8 @@ async function findSourceFiles(projectRoot, ignore, isGitIgnored, signal) {
15540
15611
  files: gitFiles.files,
15541
15612
  complete: true,
15542
15613
  errors: [],
15543
- trustedUnchanged: gitFiles.trustedUnchanged
15614
+ trustedUnchanged: gitFiles.trustedUnchanged,
15615
+ snapshotKey: gitFiles.snapshotKey
15544
15616
  };
15545
15617
  }
15546
15618
  const results = [];
@@ -15627,6 +15699,17 @@ async function resolveProjectRelations(store, projectRoot, opts) {
15627
15699
  }
15628
15700
  }
15629
15701
  async function runIndexerWithStore(store, opts) {
15702
+ let result;
15703
+ try {
15704
+ result = await store.runAtomicIndexUpdate(() => runIndexerAtomic(store, opts));
15705
+ } catch (error) {
15706
+ if (!(error instanceof IndexSourceChangedError)) throw error;
15707
+ result = await store.runAtomicIndexUpdate(() => runIndexerAtomic(store, opts));
15708
+ }
15709
+ if (!opts.files) store.compactIfNeeded();
15710
+ return result;
15711
+ }
15712
+ async function runIndexerAtomic(store, opts) {
15630
15713
  const { projectRoot, langs, ignore = [], signal } = opts;
15631
15714
  const relationGraphVersion = "2";
15632
15715
  const refResolutionVersion = "2";
@@ -15646,6 +15729,7 @@ async function runIndexerWithStore(store, opts) {
15646
15729
  let discoveredFiles = null;
15647
15730
  let discoveryComplete = true;
15648
15731
  let trustedUnchanged;
15732
+ let discoverySnapshotKey;
15649
15733
  if (opts.files && opts.files.length > 0) {
15650
15734
  files = opts.files.map((f) => path24.resolve(projectRoot, f)).filter((f) => {
15651
15735
  if (!isWithinProject(projectRoot, f)) return false;
@@ -15659,6 +15743,7 @@ async function runIndexerWithStore(store, opts) {
15659
15743
  discoveryComplete = discovery.complete;
15660
15744
  discoveredFiles = new Set(files);
15661
15745
  trustedUnchanged = discovery.trustedUnchanged;
15746
+ discoverySnapshotKey = discovery.snapshotKey;
15662
15747
  }
15663
15748
  if (langs && langs.length > 0) {
15664
15749
  const langSet = new Set(langs);
@@ -15672,6 +15757,8 @@ async function runIndexerWithStore(store, opts) {
15672
15757
  if (!force) {
15673
15758
  for (const meta of store.getAllFileMetas()) existingMeta.set(meta.file, meta);
15674
15759
  }
15760
+ const snapshotTrusted = !force && discoverySnapshotKey !== void 0 && store.getMetadata(GIT_SNAPSHOT_METADATA_KEY) === discoverySnapshotKey;
15761
+ if (!snapshotTrusted) trustedUnchanged = void 0;
15675
15762
  const totalFilesForProgress = files.length;
15676
15763
  let filesPreSkipped = 0;
15677
15764
  if (!force && trustedUnchanged) {
@@ -15734,9 +15821,6 @@ async function runIndexerWithStore(store, opts) {
15734
15821
  };
15735
15822
  }
15736
15823
  const meta = existingMeta.get(file);
15737
- if (!force && meta && meta.mtimeMs === Math.floor(stat19.mtimeMs)) {
15738
- return { file, stat: stat19, lang, parsed: null, skippedMeta: meta };
15739
- }
15740
15824
  let content;
15741
15825
  try {
15742
15826
  content = await fs18.readFile(file, { encoding: "utf8", signal });
@@ -15965,9 +16049,18 @@ async function runIndexerWithStore(store, opts) {
15965
16049
  });
15966
16050
  store.setMetadata("ref_resolution_version", refResolutionVersion);
15967
16051
  store.setMetadata("relation_graph_version", relationGraphVersion);
16052
+ const completeProjectScope = !opts.files && (!langs || langs.length === 0) && (!opts.ignore || opts.ignore.length === 0);
16053
+ if (completeProjectScope && discoverySnapshotKey !== void 0) {
16054
+ const finalSnapshot = await findGitSourceFiles(projectRoot, ignore, signal);
16055
+ if (!finalSnapshot || finalSnapshot.snapshotKey !== discoverySnapshotKey) {
16056
+ throw new IndexSourceChangedError(
16057
+ "Project files changed during indexing; retrying before publishing the generation."
16058
+ );
16059
+ }
16060
+ store.setMetadata(GIT_SNAPSHOT_METADATA_KEY, errors.length === 0 ? discoverySnapshotKey : "");
16061
+ }
15968
16062
  if (!opts.files || filesIndexed >= 50) store.optimize();
15969
16063
  store.setLastIndexed(Date.now());
15970
- if (!opts.files) store.compactIfNeeded();
15971
16064
  const durationMs = Date.now() - startMs;
15972
16065
  return {
15973
16066
  filesIndexed,
@@ -16252,6 +16345,13 @@ function callIndexOp(op, args, opts) {
16252
16345
  });
16253
16346
  }
16254
16347
  async function callInline(op, args, opts) {
16348
+ if (op !== "index" && _indexing) {
16349
+ const error = new Error(
16350
+ "Codebase index refresh in progress; retry after the completed generation is published."
16351
+ );
16352
+ error.name = "IndexRefreshInProgressError";
16353
+ throw error;
16354
+ }
16255
16355
  const ac = new AbortController();
16256
16356
  const onOuterAbort = () => ac.abort(opts.signal?.reason ?? new Error("Indexing cancelled"));
16257
16357
  if (opts.signal?.aborted) onOuterAbort();
@@ -16479,12 +16579,12 @@ var codebaseSearchTool = {
16479
16579
  },
16480
16580
  async execute(input, ctx, execOpts) {
16481
16581
  const state = getIndexState();
16482
- if (state.indexing && !state.ready) {
16582
+ if (state.indexing) {
16483
16583
  return {
16484
16584
  results: [],
16485
16585
  total: 0,
16486
16586
  query: input.query,
16487
- indexStatus: `Indexing in progress (${state.currentFile}/${state.totalFiles} files) \u2014 retry in a moment.`
16587
+ indexStatus: `Index refresh in progress (${state.currentFile}/${state.totalFiles} files) \u2014 retry after the completed generation is published.`
16488
16588
  };
16489
16589
  }
16490
16590
  if (state.lastError) {
@@ -16665,12 +16765,12 @@ var codebaseIncomingCallsTool = {
16665
16765
  },
16666
16766
  async execute(input, ctx) {
16667
16767
  const state = getIndexState();
16668
- if (state.indexing && !state.ready) {
16768
+ if (state.indexing) {
16669
16769
  return {
16670
16770
  symbol: input.symbol,
16671
16771
  calls: [],
16672
16772
  total: 0,
16673
- indexStatus: `Indexing in progress (${state.currentFile}/${state.totalFiles} files) \u2014 retry in a moment.`
16773
+ indexStatus: `Index refresh in progress (${state.currentFile}/${state.totalFiles} files) \u2014 retry after the completed generation is published.`
16674
16774
  };
16675
16775
  }
16676
16776
  if (state.lastError) {
@@ -16687,16 +16787,14 @@ var codebaseIncomingCallsTool = {
16687
16787
  const transitive = input.transitive === true;
16688
16788
  let serviced;
16689
16789
  try {
16690
- serviced = await incomingCallsService2(
16691
- {
16692
- projectRoot: ctx.projectRoot,
16693
- indexDir: codebaseIndexDirOverride(ctx),
16694
- symbol: input.symbol,
16695
- file: input.file,
16696
- limit,
16697
- transitive
16698
- }
16699
- );
16790
+ serviced = await incomingCallsService2({
16791
+ projectRoot: ctx.projectRoot,
16792
+ indexDir: codebaseIndexDirOverride(ctx),
16793
+ symbol: input.symbol,
16794
+ file: input.file,
16795
+ limit,
16796
+ transitive
16797
+ });
16700
16798
  } catch (err) {
16701
16799
  return {
16702
16800
  symbol: input.symbol,
@@ -16735,10 +16833,14 @@ var codebaseIncomingCallsTool = {
16735
16833
  }
16736
16834
  const notes = [];
16737
16835
  if (totalMatches > limit) {
16738
- notes.push(`Results capped at ${limit} of ${totalMatches} call sites. Increase \`limit\` or use \`file\` to narrow.`);
16836
+ notes.push(
16837
+ `Results capped at ${limit} of ${totalMatches} call sites. Increase \`limit\` or use \`file\` to narrow.`
16838
+ );
16739
16839
  }
16740
16840
  if (ambiguous) {
16741
- 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\`.`);
16841
+ notes.push(
16842
+ `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\`.`
16843
+ );
16742
16844
  }
16743
16845
  return {
16744
16846
  symbol: input.symbol,
@@ -16788,12 +16890,12 @@ var codebaseOutgoingCallsTool = {
16788
16890
  },
16789
16891
  async execute(input, ctx) {
16790
16892
  const state = getIndexState();
16791
- if (state.indexing && !state.ready) {
16893
+ if (state.indexing) {
16792
16894
  return {
16793
16895
  symbol: input.symbol,
16794
16896
  calls: [],
16795
16897
  total: 0,
16796
- indexStatus: `Indexing in progress (${state.currentFile}/${state.totalFiles} files) \u2014 retry in a moment.`
16898
+ indexStatus: `Index refresh in progress (${state.currentFile}/${state.totalFiles} files) \u2014 retry after the completed generation is published.`
16797
16899
  };
16798
16900
  }
16799
16901
  if (state.lastError) {
@@ -16810,16 +16912,14 @@ var codebaseOutgoingCallsTool = {
16810
16912
  const transitive = input.transitive === true;
16811
16913
  let serviced;
16812
16914
  try {
16813
- serviced = await outgoingCallsService2(
16814
- {
16815
- projectRoot: ctx.projectRoot,
16816
- indexDir: codebaseIndexDirOverride(ctx),
16817
- symbol: input.symbol,
16818
- file: input.file,
16819
- limit,
16820
- transitive
16821
- }
16822
- );
16915
+ serviced = await outgoingCallsService2({
16916
+ projectRoot: ctx.projectRoot,
16917
+ indexDir: codebaseIndexDirOverride(ctx),
16918
+ symbol: input.symbol,
16919
+ file: input.file,
16920
+ limit,
16921
+ transitive
16922
+ });
16823
16923
  } catch (err) {
16824
16924
  return {
16825
16925
  symbol: input.symbol,
@@ -16858,10 +16958,14 @@ var codebaseOutgoingCallsTool = {
16858
16958
  }
16859
16959
  const notes = [];
16860
16960
  if (totalMatches > limit) {
16861
- notes.push(`Results capped at ${limit} of ${totalMatches} call sites. Increase \`limit\` or use \`file\` to narrow.`);
16961
+ notes.push(
16962
+ `Results capped at ${limit} of ${totalMatches} call sites. Increase \`limit\` or use \`file\` to narrow.`
16963
+ );
16862
16964
  }
16863
16965
  if (unresolvedCount > 0) {
16864
- notes.push(`${unresolvedCount} unresolved reference(s) not shown \u2014 their targets could not be resolved during indexing.`);
16966
+ notes.push(
16967
+ `${unresolvedCount} unresolved reference(s) not shown \u2014 their targets could not be resolved during indexing.`
16968
+ );
16865
16969
  }
16866
16970
  return {
16867
16971
  symbol: input.symbol,
@@ -24447,6 +24551,7 @@ function sourceStatus(task) {
24447
24551
  function todoStatus(task) {
24448
24552
  const status = sourceStatus(task);
24449
24553
  if (status === "completed") return "completed";
24554
+ if (status === "review" && task.assignment?.status === "completed") return "completed";
24450
24555
  if (status === "in_progress" || status === "review") return "in_progress";
24451
24556
  return "pending";
24452
24557
  }
@@ -26398,6 +26503,20 @@ async function synchronizeManagedKanban(items, board, ctx, signal) {
26398
26503
  }
26399
26504
  const task = board.tasks.find((candidate) => candidate.id === item.kanbanTaskId);
26400
26505
  if (!task || task.status === "completed") continue;
26506
+ const stage = task.lifecycle?.currentStage;
26507
+ if (stage === "backlog" || stage === "todo") {
26508
+ const started = await execute({
26509
+ action: "start_task",
26510
+ boardId: board.id,
26511
+ taskId: task.id,
26512
+ author: actor,
26513
+ agentId: actor,
26514
+ transitionComment: `Auto-started for completion: ${item.content}`
26515
+ });
26516
+ if (!started.ok) {
26517
+ continue;
26518
+ }
26519
+ }
26401
26520
  await execute({
26402
26521
  action: "mark_assignment",
26403
26522
  boardId: board.id,