@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/plan.js CHANGED
@@ -426,6 +426,7 @@ function sourceStatus(task) {
426
426
  function todoStatus(task) {
427
427
  const status = sourceStatus(task);
428
428
  if (status === "completed") return "completed";
429
+ if (status === "review" && task.assignment?.status === "completed") return "completed";
429
430
  if (status === "in_progress" || status === "review") return "in_progress";
430
431
  return "pending";
431
432
  }
@@ -2599,6 +2600,20 @@ async function synchronizeManagedKanban(items, board, ctx, signal) {
2599
2600
  }
2600
2601
  const task = board.tasks.find((candidate) => candidate.id === item.kanbanTaskId);
2601
2602
  if (!task || task.status === "completed") continue;
2603
+ const stage = task.lifecycle?.currentStage;
2604
+ if (stage === "backlog" || stage === "todo") {
2605
+ const started = await execute({
2606
+ action: "start_task",
2607
+ boardId: board.id,
2608
+ taskId: task.id,
2609
+ author: actor,
2610
+ agentId: actor,
2611
+ transitionComment: `Auto-started for completion: ${item.content}`
2612
+ });
2613
+ if (!started.ok) {
2614
+ continue;
2615
+ }
2616
+ }
2602
2617
  await execute({
2603
2618
  action: "mark_assignment",
2604
2619
  boardId: board.id,
package/dist/read.js CHANGED
@@ -2858,6 +2858,7 @@ var indexCircuitBreaker = new IndexCircuitBreaker();
2858
2858
  // src/codebase-index/indexer.ts
2859
2859
  import { expectDefined as expectDefined5 } from "@wrongstack/core/utils";
2860
2860
  import { execFile } from "node:child_process";
2861
+ import { createHash as createHash2 } from "node:crypto";
2861
2862
  import * as fs9 from "node:fs/promises";
2862
2863
  import { availableParallelism } from "node:os";
2863
2864
  import * as path13 from "node:path";
@@ -4178,6 +4179,85 @@ function runSqliteWithRetry(fn) {
4178
4179
  throw lastError;
4179
4180
  }
4180
4181
 
4182
+ // src/codebase-index/vector-search.ts
4183
+ var RRF_K = 60;
4184
+ var VECTOR_DIMENSIONS = 384;
4185
+ var NGRAM_SIZE = 3;
4186
+ function embedText(text) {
4187
+ const vec = new Float32Array(VECTOR_DIMENSIONS);
4188
+ const normalized = text.toLowerCase().trim();
4189
+ if (normalized.length < NGRAM_SIZE) {
4190
+ const padded = ` ${normalized} `.slice(0, Math.max(NGRAM_SIZE, normalized.length + 2));
4191
+ for (let i = 0; i <= padded.length - NGRAM_SIZE; i++) {
4192
+ const ngram = padded.slice(i, i + NGRAM_SIZE);
4193
+ const bucket = hashNgram(ngram) % VECTOR_DIMENSIONS;
4194
+ vec[bucket] += 1;
4195
+ }
4196
+ } else {
4197
+ for (let i = 0; i <= normalized.length - NGRAM_SIZE; i++) {
4198
+ const ngram = normalized.slice(i, i + NGRAM_SIZE);
4199
+ const bucket = hashNgram(ngram) % VECTOR_DIMENSIONS;
4200
+ vec[bucket] += 1;
4201
+ }
4202
+ }
4203
+ let norm = 0;
4204
+ for (let i = 0; i < VECTOR_DIMENSIONS; i++) {
4205
+ norm += vec[i] * vec[i];
4206
+ }
4207
+ norm = Math.sqrt(norm);
4208
+ if (norm > 0) {
4209
+ for (let i = 0; i < VECTOR_DIMENSIONS; i++) {
4210
+ vec[i] /= norm;
4211
+ }
4212
+ }
4213
+ return vec;
4214
+ }
4215
+ function hashNgram(str) {
4216
+ let hash = 2166136261;
4217
+ for (let i = 0; i < str.length; i++) {
4218
+ hash ^= str.charCodeAt(i);
4219
+ hash = Math.imul(hash, 16777619);
4220
+ }
4221
+ return hash >>> 0;
4222
+ }
4223
+ function cosineSimilarity(a, b) {
4224
+ let dot = 0;
4225
+ const len = Math.min(a.length, b.length);
4226
+ for (let i = 0; i < len; i++) {
4227
+ dot += a[i] * b[i];
4228
+ }
4229
+ return dot;
4230
+ }
4231
+ function encodeVector(vec) {
4232
+ return Buffer.from(vec.buffer, vec.byteOffset, vec.byteLength);
4233
+ }
4234
+ function decodeVector(buf) {
4235
+ const view = new DataView(
4236
+ buf.buffer,
4237
+ buf.byteOffset,
4238
+ buf.byteLength
4239
+ );
4240
+ const copy = new Float32Array(buf.byteLength / 4);
4241
+ for (let i = 0; i < copy.length; i++) {
4242
+ copy[i] = view.getFloat32(i * 4, true);
4243
+ }
4244
+ return copy;
4245
+ }
4246
+ function reciprocalRankFusion(bm25Ranks, vectorRanks, k = RRF_K) {
4247
+ const allIds = /* @__PURE__ */ new Set([...bm25Ranks.keys(), ...vectorRanks.keys()]);
4248
+ const scored = [];
4249
+ for (const id of allIds) {
4250
+ const bm25Rank = bm25Ranks.get(id);
4251
+ const vecRank = vectorRanks.get(id);
4252
+ let score = 0;
4253
+ if (bm25Rank !== void 0) score += 1 / (k + bm25Rank);
4254
+ if (vecRank !== void 0) score += 1 / (k + vecRank);
4255
+ scored.push([id, score]);
4256
+ }
4257
+ scored.sort((a, b) => b[1] - a[1]);
4258
+ return scored;
4259
+ }
4260
+
4181
4261
  // src/codebase-index/writer-admin.ts
4182
4262
  import * as fs7 from "node:fs";
4183
4263
  import * as path11 from "node:path";
@@ -5237,90 +5317,19 @@ var StorePool = class {
5237
5317
  }
5238
5318
  };
5239
5319
 
5240
- // src/codebase-index/vector-search.ts
5241
- var RRF_K = 60;
5242
- var VECTOR_DIMENSIONS = 384;
5243
- var NGRAM_SIZE = 3;
5244
- function embedText(text) {
5245
- const vec = new Float32Array(VECTOR_DIMENSIONS);
5246
- const normalized = text.toLowerCase().trim();
5247
- if (normalized.length < NGRAM_SIZE) {
5248
- const padded = ` ${normalized} `.slice(0, Math.max(NGRAM_SIZE, normalized.length + 2));
5249
- for (let i = 0; i <= padded.length - NGRAM_SIZE; i++) {
5250
- const ngram = padded.slice(i, i + NGRAM_SIZE);
5251
- const bucket = hashNgram(ngram) % VECTOR_DIMENSIONS;
5252
- vec[bucket] += 1;
5253
- }
5254
- } else {
5255
- for (let i = 0; i <= normalized.length - NGRAM_SIZE; i++) {
5256
- const ngram = normalized.slice(i, i + NGRAM_SIZE);
5257
- const bucket = hashNgram(ngram) % VECTOR_DIMENSIONS;
5258
- vec[bucket] += 1;
5259
- }
5260
- }
5261
- let norm = 0;
5262
- for (let i = 0; i < VECTOR_DIMENSIONS; i++) {
5263
- norm += vec[i] * vec[i];
5264
- }
5265
- norm = Math.sqrt(norm);
5266
- if (norm > 0) {
5267
- for (let i = 0; i < VECTOR_DIMENSIONS; i++) {
5268
- vec[i] /= norm;
5269
- }
5270
- }
5271
- return vec;
5272
- }
5273
- function hashNgram(str) {
5274
- let hash = 2166136261;
5275
- for (let i = 0; i < str.length; i++) {
5276
- hash ^= str.charCodeAt(i);
5277
- hash = Math.imul(hash, 16777619);
5278
- }
5279
- return hash >>> 0;
5280
- }
5281
- function cosineSimilarity(a, b) {
5282
- let dot = 0;
5283
- const len = Math.min(a.length, b.length);
5284
- for (let i = 0; i < len; i++) {
5285
- dot += a[i] * b[i];
5286
- }
5287
- return dot;
5288
- }
5289
- function encodeVector(vec) {
5290
- return Buffer.from(vec.buffer, vec.byteOffset, vec.byteLength);
5291
- }
5292
- function decodeVector(buf) {
5293
- const view = new DataView(
5294
- buf.buffer,
5295
- buf.byteOffset,
5296
- buf.byteLength
5297
- );
5298
- const copy = new Float32Array(buf.byteLength / 4);
5299
- for (let i = 0; i < copy.length; i++) {
5300
- copy[i] = view.getFloat32(i * 4, true);
5301
- }
5302
- return copy;
5303
- }
5304
- function reciprocalRankFusion(bm25Ranks, vectorRanks, k = RRF_K) {
5305
- const allIds = /* @__PURE__ */ new Set([...bm25Ranks.keys(), ...vectorRanks.keys()]);
5306
- const scored = [];
5307
- for (const id of allIds) {
5308
- const bm25Rank = bm25Ranks.get(id);
5309
- const vecRank = vectorRanks.get(id);
5310
- let score = 0;
5311
- if (bm25Rank !== void 0) score += 1 / (k + bm25Rank);
5312
- if (vecRank !== void 0) score += 1 / (k + vecRank);
5313
- scored.push([id, score]);
5314
- }
5315
- scored.sort((a, b) => b[1] - a[1]);
5316
- return scored;
5317
- }
5318
-
5319
5320
  // src/codebase-index/writer.ts
5320
5321
  var DB_FILE2 = "index.db";
5321
5322
  var MAX_STATEMENT_CACHE = 128;
5322
5323
  var IndexStore = class _IndexStore {
5323
5324
  db;
5325
+ /**
5326
+ * True while an index run owns one outer SQLite transaction. Individual
5327
+ * writer methods normally protect themselves with BEGIN/COMMIT, but during
5328
+ * a refresh they join this transaction so readers observe either the last
5329
+ * completed index or the next completed index, never an in-between batch.
5330
+ */
5331
+ atomicIndexUpdateActive = false;
5332
+ writeSavepointSequence = 0;
5324
5333
  /** Absolute path to this project's index directory. */
5325
5334
  indexDir;
5326
5335
  /**
@@ -5401,6 +5410,51 @@ var IndexStore = class _IndexStore {
5401
5410
  runWithRetry(fn) {
5402
5411
  return runSqliteWithRetry(fn);
5403
5412
  }
5413
+ /** Run a complete index mutation as one WAL-visible publication. */
5414
+ async runAtomicIndexUpdate(job) {
5415
+ if (this.atomicIndexUpdateActive) return job();
5416
+ this.runWithRetry(() => this.db.exec("BEGIN IMMEDIATE"));
5417
+ this.atomicIndexUpdateActive = true;
5418
+ try {
5419
+ const result = await job();
5420
+ this.db.exec("COMMIT");
5421
+ return result;
5422
+ } catch (error) {
5423
+ try {
5424
+ this.db.exec("ROLLBACK");
5425
+ } catch {
5426
+ }
5427
+ throw error;
5428
+ } finally {
5429
+ this.atomicIndexUpdateActive = false;
5430
+ }
5431
+ }
5432
+ /**
5433
+ * Begin a method-local transaction. Inside an atomic index publication a
5434
+ * SAVEPOINT preserves the old per-batch rollback boundary, which is needed
5435
+ * when commitBatch falls back to per-file writes after one batch fails.
5436
+ */
5437
+ beginWriteTransaction() {
5438
+ if (this.atomicIndexUpdateActive) {
5439
+ const savepoint = `index_write_${++this.writeSavepointSequence}`;
5440
+ this.db.exec(`SAVEPOINT ${savepoint}`);
5441
+ return savepoint;
5442
+ }
5443
+ this.db.exec("BEGIN IMMEDIATE");
5444
+ return null;
5445
+ }
5446
+ commitWriteTransaction(savepoint) {
5447
+ if (savepoint) this.db.exec(`RELEASE SAVEPOINT ${savepoint}`);
5448
+ else this.db.exec("COMMIT");
5449
+ }
5450
+ rollbackWriteTransaction(savepoint) {
5451
+ if (savepoint) {
5452
+ this.db.exec(`ROLLBACK TO SAVEPOINT ${savepoint}`);
5453
+ this.db.exec(`RELEASE SAVEPOINT ${savepoint}`);
5454
+ } else {
5455
+ this.db.exec("ROLLBACK");
5456
+ }
5457
+ }
5404
5458
  /**
5405
5459
  * Mirror the in-process language→family map into SQLite.
5406
5460
  *
@@ -5635,7 +5689,7 @@ var IndexStore = class _IndexStore {
5635
5689
  insertSymbols(symbols) {
5636
5690
  this.invalidateBm25();
5637
5691
  return this.runWithRetry(() => {
5638
- this.db.exec("BEGIN IMMEDIATE");
5692
+ const ownsTransaction = this.beginWriteTransaction();
5639
5693
  try {
5640
5694
  let nextId = this.allocateSymbolIds(symbols.length);
5641
5695
  const result = [];
@@ -5662,7 +5716,9 @@ var IndexStore = class _IndexStore {
5662
5716
  }
5663
5717
  vectorRows.push({
5664
5718
  id,
5665
- vector: encodeVector(embedText(s.text || buildIndexableText(s.name, s.signature, s.docComment)))
5719
+ vector: encodeVector(
5720
+ embedText(s.text || buildIndexableText(s.name, s.signature, s.docComment))
5721
+ )
5666
5722
  });
5667
5723
  result.push({ ...s, id });
5668
5724
  }
@@ -5680,10 +5736,10 @@ var IndexStore = class _IndexStore {
5680
5736
  vectorRows
5681
5737
  );
5682
5738
  }
5683
- this.db.exec("COMMIT");
5739
+ this.commitWriteTransaction(ownsTransaction);
5684
5740
  return result;
5685
5741
  } catch (err) {
5686
- this.db.exec("ROLLBACK");
5742
+ this.rollbackWriteTransaction(ownsTransaction);
5687
5743
  throw err;
5688
5744
  }
5689
5745
  });
@@ -5691,7 +5747,7 @@ var IndexStore = class _IndexStore {
5691
5747
  deleteSymbolsForFile(file) {
5692
5748
  this.invalidateBm25();
5693
5749
  this.runWithRetry(() => {
5694
- this.db.exec("BEGIN IMMEDIATE");
5750
+ const ownsTransaction = this.beginWriteTransaction();
5695
5751
  try {
5696
5752
  const affectedNames = this.invalidateIncomingRefsForFiles([file]);
5697
5753
  if (this.ftsAvailable) {
@@ -5706,9 +5762,9 @@ var IndexStore = class _IndexStore {
5706
5762
  }
5707
5763
  this.stmt("DELETE FROM symbols WHERE file_fk = ?").run(file);
5708
5764
  this.resolveRefsForNamesUnsafe(affectedNames);
5709
- this.db.exec("COMMIT");
5765
+ this.commitWriteTransaction(ownsTransaction);
5710
5766
  } catch (error) {
5711
- this.db.exec("ROLLBACK");
5767
+ this.rollbackWriteTransaction(ownsTransaction);
5712
5768
  throw error;
5713
5769
  }
5714
5770
  });
@@ -5721,7 +5777,7 @@ var IndexStore = class _IndexStore {
5721
5777
  deleteFile(file) {
5722
5778
  this.invalidateBm25();
5723
5779
  this.runWithRetry(() => {
5724
- this.db.exec("BEGIN IMMEDIATE");
5780
+ const ownsTransaction = this.beginWriteTransaction();
5725
5781
  try {
5726
5782
  const affectedNames = this.invalidateIncomingRefsForFiles([file]);
5727
5783
  if (this.ftsAvailable) {
@@ -5740,9 +5796,9 @@ var IndexStore = class _IndexStore {
5740
5796
  this.stmt("DELETE FROM symbols WHERE file_fk = ?").run(file);
5741
5797
  this.stmt("DELETE FROM files WHERE file = ?").run(file);
5742
5798
  this.resolveRefsForNamesUnsafe(affectedNames);
5743
- this.db.exec("COMMIT");
5799
+ this.commitWriteTransaction(ownsTransaction);
5744
5800
  } catch (err) {
5745
- this.db.exec("ROLLBACK");
5801
+ this.rollbackWriteTransaction(ownsTransaction);
5746
5802
  throw err;
5747
5803
  }
5748
5804
  });
@@ -6129,7 +6185,7 @@ var IndexStore = class _IndexStore {
6129
6185
  clearAll() {
6130
6186
  this.invalidateBm25();
6131
6187
  this.runWithRetry(() => {
6132
- this.db.exec("BEGIN IMMEDIATE");
6188
+ const ownsTransaction = this.beginWriteTransaction();
6133
6189
  try {
6134
6190
  this.db.exec("DROP TABLE IF EXISTS refs");
6135
6191
  this.db.exec("DROP TABLE IF EXISTS symbols");
@@ -6137,15 +6193,15 @@ var IndexStore = class _IndexStore {
6137
6193
  this.db.exec("DROP TABLE IF EXISTS metadata");
6138
6194
  if (this.ftsAvailable) this.db.exec("DROP TABLE IF EXISTS symbols_fts");
6139
6195
  this.db.exec("DROP TABLE IF EXISTS symbol_vectors");
6140
- this.db.exec("COMMIT");
6141
6196
  this.stmtCache.clear();
6142
6197
  this.initSchema();
6143
6198
  this.stmt("INSERT OR REPLACE INTO metadata(key, value) VALUES (?, ?)").run(
6144
6199
  _IndexStore.NEXT_SYMBOL_ID_KEY,
6145
6200
  "1"
6146
6201
  );
6202
+ this.commitWriteTransaction(ownsTransaction);
6147
6203
  } catch (err) {
6148
- this.db.exec("ROLLBACK");
6204
+ this.rollbackWriteTransaction(ownsTransaction);
6149
6205
  throw err;
6150
6206
  }
6151
6207
  });
@@ -6207,7 +6263,7 @@ var IndexStore = class _IndexStore {
6207
6263
  }
6208
6264
  this.invalidateBm25();
6209
6265
  return this.runWithRetry(() => {
6210
- this.db.exec("BEGIN IMMEDIATE");
6266
+ const ownsTransaction = this.beginWriteTransaction();
6211
6267
  try {
6212
6268
  const affectedNames = /* @__PURE__ */ new Set();
6213
6269
  for (const entry of entries) {
@@ -6268,7 +6324,9 @@ var IndexStore = class _IndexStore {
6268
6324
  }
6269
6325
  vectorRows.push({
6270
6326
  id,
6271
- vector: encodeVector(embedText(s.text || buildIndexableText(s.name, s.signature, s.docComment)))
6327
+ vector: encodeVector(
6328
+ embedText(s.text || buildIndexableText(s.name, s.signature, s.docComment))
6329
+ )
6272
6330
  });
6273
6331
  const inserted = { ...s, id };
6274
6332
  allInserted.push(inserted);
@@ -6313,10 +6371,10 @@ var IndexStore = class _IndexStore {
6313
6371
  );
6314
6372
  }
6315
6373
  this.resolveRefsForNamesUnsafe(affectedNames);
6316
- this.db.exec("COMMIT");
6374
+ this.commitWriteTransaction(ownsTransaction);
6317
6375
  return allInserted;
6318
6376
  } catch (err) {
6319
- this.db.exec("ROLLBACK");
6377
+ this.rollbackWriteTransaction(ownsTransaction);
6320
6378
  throw err;
6321
6379
  }
6322
6380
  });
@@ -6393,7 +6451,7 @@ var IndexStore = class _IndexStore {
6393
6451
  replaceEmptyFile(meta) {
6394
6452
  this.invalidateBm25();
6395
6453
  this.runWithRetry(() => {
6396
- this.db.exec("BEGIN IMMEDIATE");
6454
+ const ownsTransaction = this.beginWriteTransaction();
6397
6455
  try {
6398
6456
  const affectedNames = this.invalidateIncomingRefsForFiles([meta.file]);
6399
6457
  if (this.ftsAvailable) {
@@ -6428,9 +6486,9 @@ var IndexStore = class _IndexStore {
6428
6486
  meta.lastIndexed
6429
6487
  );
6430
6488
  this.resolveRefsForNamesUnsafe(affectedNames);
6431
- this.db.exec("COMMIT");
6489
+ this.commitWriteTransaction(ownsTransaction);
6432
6490
  } catch (err) {
6433
- this.db.exec("ROLLBACK");
6491
+ this.rollbackWriteTransaction(ownsTransaction);
6434
6492
  throw err;
6435
6493
  }
6436
6494
  });
@@ -6623,6 +6681,10 @@ var DEFAULT_IGNORE_FILES = /* @__PURE__ */ new Set(["package-lock.json", "pnpm-l
6623
6681
  var INDEXABLE_EXTENSION_SET = new Set(INDEXABLE_EXTENSIONS);
6624
6682
  var MAX_INDEX_FILE_BYTES = 5 * 1024 * 1024;
6625
6683
  var MAX_GIT_FILE_LIST_BYTES = 64 * 1024 * 1024;
6684
+ var GIT_SNAPSHOT_METADATA_KEY = "git_discovery_snapshot";
6685
+ var IndexSourceChangedError = class extends Error {
6686
+ name = "IndexSourceChangedError";
6687
+ };
6626
6688
  function isWithinProject(projectRoot, file) {
6627
6689
  const rel = path13.relative(projectRoot, file);
6628
6690
  return rel !== "" && !rel.startsWith(`..${path13.sep}`) && rel !== ".." && !path13.isAbsolute(rel);
@@ -6659,7 +6721,7 @@ async function findGitSourceFiles(projectRoot, ignore, signal) {
6659
6721
  if (normalizeComparablePath(topLevel) !== normalizeComparablePath(projectRoot)) return null;
6660
6722
  throwIfAborted(signal);
6661
6723
  const ignoreSet = /* @__PURE__ */ new Set([...DEFAULT_IGNORE, ...ignore]);
6662
- const [output, statusOutput] = await Promise.all([
6724
+ const [output, statusOutput, stagedOutput] = await Promise.all([
6663
6725
  gitOutput(projectRoot, ["ls-files", "--cached", "--others", "--exclude-standard", "-z"]),
6664
6726
  gitOutput(projectRoot, [
6665
6727
  "status",
@@ -6667,7 +6729,8 @@ async function findGitSourceFiles(projectRoot, ignore, signal) {
6667
6729
  "-z",
6668
6730
  "--untracked-files=all",
6669
6731
  "--ignored=no"
6670
- ])
6732
+ ]),
6733
+ gitOutput(projectRoot, ["ls-files", "--stage", "-z"])
6671
6734
  ]);
6672
6735
  throwIfAborted(signal);
6673
6736
  const dirty = /* @__PURE__ */ new Set();
@@ -6697,9 +6760,17 @@ async function findGitSourceFiles(projectRoot, ignore, signal) {
6697
6760
  const ext = path13.extname(relative3).toLowerCase();
6698
6761
  if (INDEXABLE_EXTENSION_SET.has(ext) || detectLang(full) !== null) files.push(full);
6699
6762
  }
6763
+ const snapshot = createHash2("sha256").update(stagedOutput).update("\0").update(statusOutput);
6764
+ const indexedFiles = new Set(files);
6765
+ for (const dirtyFile of [...dirty].sort()) {
6766
+ if (!indexedFiles.has(dirtyFile) || deleted.has(dirtyFile)) continue;
6767
+ snapshot.update("\0").update(dirtyFile).update("\0");
6768
+ snapshot.update(xxhash64String(await fs9.readFile(dirtyFile, "utf8")));
6769
+ }
6700
6770
  return {
6701
6771
  files,
6702
- trustedUnchanged: new Set(files.filter((file) => !dirty.has(file)))
6772
+ trustedUnchanged: new Set(files.filter((file) => !dirty.has(file))),
6773
+ snapshotKey: snapshot.digest("hex")
6703
6774
  };
6704
6775
  } catch {
6705
6776
  return null;
@@ -6712,7 +6783,8 @@ async function findSourceFiles(projectRoot, ignore, isGitIgnored, signal) {
6712
6783
  files: gitFiles.files,
6713
6784
  complete: true,
6714
6785
  errors: [],
6715
- trustedUnchanged: gitFiles.trustedUnchanged
6786
+ trustedUnchanged: gitFiles.trustedUnchanged,
6787
+ snapshotKey: gitFiles.snapshotKey
6716
6788
  };
6717
6789
  }
6718
6790
  const results = [];
@@ -6799,6 +6871,17 @@ async function resolveProjectRelations(store, projectRoot, opts) {
6799
6871
  }
6800
6872
  }
6801
6873
  async function runIndexerWithStore(store, opts) {
6874
+ let result;
6875
+ try {
6876
+ result = await store.runAtomicIndexUpdate(() => runIndexerAtomic(store, opts));
6877
+ } catch (error) {
6878
+ if (!(error instanceof IndexSourceChangedError)) throw error;
6879
+ result = await store.runAtomicIndexUpdate(() => runIndexerAtomic(store, opts));
6880
+ }
6881
+ if (!opts.files) store.compactIfNeeded();
6882
+ return result;
6883
+ }
6884
+ async function runIndexerAtomic(store, opts) {
6802
6885
  const { projectRoot, langs, ignore = [], signal } = opts;
6803
6886
  const relationGraphVersion = "2";
6804
6887
  const refResolutionVersion = "2";
@@ -6818,6 +6901,7 @@ async function runIndexerWithStore(store, opts) {
6818
6901
  let discoveredFiles = null;
6819
6902
  let discoveryComplete = true;
6820
6903
  let trustedUnchanged;
6904
+ let discoverySnapshotKey;
6821
6905
  if (opts.files && opts.files.length > 0) {
6822
6906
  files = opts.files.map((f) => path13.resolve(projectRoot, f)).filter((f) => {
6823
6907
  if (!isWithinProject(projectRoot, f)) return false;
@@ -6831,6 +6915,7 @@ async function runIndexerWithStore(store, opts) {
6831
6915
  discoveryComplete = discovery.complete;
6832
6916
  discoveredFiles = new Set(files);
6833
6917
  trustedUnchanged = discovery.trustedUnchanged;
6918
+ discoverySnapshotKey = discovery.snapshotKey;
6834
6919
  }
6835
6920
  if (langs && langs.length > 0) {
6836
6921
  const langSet = new Set(langs);
@@ -6844,6 +6929,8 @@ async function runIndexerWithStore(store, opts) {
6844
6929
  if (!force) {
6845
6930
  for (const meta of store.getAllFileMetas()) existingMeta.set(meta.file, meta);
6846
6931
  }
6932
+ const snapshotTrusted = !force && discoverySnapshotKey !== void 0 && store.getMetadata(GIT_SNAPSHOT_METADATA_KEY) === discoverySnapshotKey;
6933
+ if (!snapshotTrusted) trustedUnchanged = void 0;
6847
6934
  const totalFilesForProgress = files.length;
6848
6935
  let filesPreSkipped = 0;
6849
6936
  if (!force && trustedUnchanged) {
@@ -6906,9 +6993,6 @@ async function runIndexerWithStore(store, opts) {
6906
6993
  };
6907
6994
  }
6908
6995
  const meta = existingMeta.get(file);
6909
- if (!force && meta && meta.mtimeMs === Math.floor(stat3.mtimeMs)) {
6910
- return { file, stat: stat3, lang, parsed: null, skippedMeta: meta };
6911
- }
6912
6996
  let content;
6913
6997
  try {
6914
6998
  content = await fs9.readFile(file, { encoding: "utf8", signal });
@@ -7137,9 +7221,18 @@ async function runIndexerWithStore(store, opts) {
7137
7221
  });
7138
7222
  store.setMetadata("ref_resolution_version", refResolutionVersion);
7139
7223
  store.setMetadata("relation_graph_version", relationGraphVersion);
7224
+ const completeProjectScope = !opts.files && (!langs || langs.length === 0) && (!opts.ignore || opts.ignore.length === 0);
7225
+ if (completeProjectScope && discoverySnapshotKey !== void 0) {
7226
+ const finalSnapshot = await findGitSourceFiles(projectRoot, ignore, signal);
7227
+ if (!finalSnapshot || finalSnapshot.snapshotKey !== discoverySnapshotKey) {
7228
+ throw new IndexSourceChangedError(
7229
+ "Project files changed during indexing; retrying before publishing the generation."
7230
+ );
7231
+ }
7232
+ store.setMetadata(GIT_SNAPSHOT_METADATA_KEY, errors.length === 0 ? discoverySnapshotKey : "");
7233
+ }
7140
7234
  if (!opts.files || filesIndexed >= 50) store.optimize();
7141
7235
  store.setLastIndexed(Date.now());
7142
- if (!opts.files) store.compactIfNeeded();
7143
7236
  const durationMs = Date.now() - startMs;
7144
7237
  return {
7145
7238
  filesIndexed,
@@ -7272,7 +7365,7 @@ function decodeBinaryFrame(payload) {
7272
7365
  }
7273
7366
 
7274
7367
  // src/codebase-index/project-server-endpoint.ts
7275
- import { createHash as createHash2 } from "node:crypto";
7368
+ import { createHash as createHash3 } from "node:crypto";
7276
7369
  import * as fs10 from "node:fs";
7277
7370
  import * as os3 from "node:os";
7278
7371
  import * as path14 from "node:path";
@@ -7290,7 +7383,7 @@ function projectIndexServerBuildId(entrypoint) {
7290
7383
  if (buildIdCache?.file === file && buildIdCache.mtimeMs === stat3.mtimeMs && buildIdCache.size === stat3.size) {
7291
7384
  return buildIdCache.buildId;
7292
7385
  }
7293
- const buildId = createHash2("sha256").update(fs10.readFileSync(file)).digest("hex").slice(0, 24);
7386
+ const buildId = createHash3("sha256").update(fs10.readFileSync(file)).digest("hex").slice(0, 24);
7294
7387
  buildIdCache = { file, mtimeMs: stat3.mtimeMs, size: stat3.size, buildId };
7295
7388
  return buildId;
7296
7389
  } catch {
@@ -7303,7 +7396,7 @@ function normalizeLocalPath(value) {
7303
7396
  }
7304
7397
  function projectIndexServerKey(projectRoot, indexDir) {
7305
7398
  const resolvedIndexDir = normalizeLocalPath(resolveIndexDir(projectRoot, indexDir));
7306
- return createHash2("sha256").update(resolvedIndexDir).digest("hex").slice(0, 24);
7399
+ return createHash3("sha256").update(resolvedIndexDir).digest("hex").slice(0, 24);
7307
7400
  }
7308
7401
  function projectIndexServerEndpoint(projectRoot, indexDir) {
7309
7402
  const key = projectIndexServerKey(projectRoot, indexDir);
@@ -8192,6 +8285,13 @@ function callIndexOp(op, args, opts) {
8192
8285
  });
8193
8286
  }
8194
8287
  async function callInline(op, args, opts) {
8288
+ if (op !== "index" && _indexing) {
8289
+ const error = new Error(
8290
+ "Codebase index refresh in progress; retry after the completed generation is published."
8291
+ );
8292
+ error.name = "IndexRefreshInProgressError";
8293
+ throw error;
8294
+ }
8195
8295
  const ac = new AbortController();
8196
8296
  const onOuterAbort = () => ac.abort(opts.signal?.reason ?? new Error("Indexing cancelled"));
8197
8297
  if (opts.signal?.aborted) onOuterAbort();
@@ -770,6 +770,7 @@ function sourceStatus(task) {
770
770
  function todoStatus(task) {
771
771
  const status = sourceStatus(task);
772
772
  if (status === "completed") return "completed";
773
+ if (status === "review" && task.assignment?.status === "completed") return "completed";
773
774
  if (status === "in_progress" || status === "review") return "in_progress";
774
775
  return "pending";
775
776
  }
@@ -891,11 +892,12 @@ async function applySessionKanbanTaskToSource(context, task, options = {}) {
891
892
  const graphId = task.origin?.graphId ?? "";
892
893
  if (!originId) return { source: null };
893
894
  if (task.origin?.system === "session-todo" || graphId.startsWith("todo:")) {
895
+ const mappedStatus = todoStatus(task);
894
896
  const next = options.remove ? context.todos.filter((todo) => todo.id !== originId) : context.todos.map(
895
897
  (todo) => todo.id === originId ? {
896
898
  ...todo,
897
899
  content: task.title,
898
- status: sourceStatus(task) === "completed" ? "completed" : sourceStatus(task) === "in_progress" || sourceStatus(task) === "review" ? "in_progress" : "pending"
900
+ status: mappedStatus
899
901
  } : todo
900
902
  );
901
903
  suppressedTodoMirrors.add(context);
package/dist/task.js CHANGED
@@ -386,6 +386,7 @@ function sourceStatus(task) {
386
386
  function todoStatus(task) {
387
387
  const status = sourceStatus(task);
388
388
  if (status === "completed") return "completed";
389
+ if (status === "review" && task.assignment?.status === "completed") return "completed";
389
390
  if (status === "in_progress" || status === "review") return "in_progress";
390
391
  return "pending";
391
392
  }
@@ -2559,6 +2560,20 @@ async function synchronizeManagedKanban(items, board, ctx, signal) {
2559
2560
  }
2560
2561
  const task = board.tasks.find((candidate) => candidate.id === item.kanbanTaskId);
2561
2562
  if (!task || task.status === "completed") continue;
2563
+ const stage = task.lifecycle?.currentStage;
2564
+ if (stage === "backlog" || stage === "todo") {
2565
+ const started = await execute({
2566
+ action: "start_task",
2567
+ boardId: board.id,
2568
+ taskId: task.id,
2569
+ author: actor,
2570
+ agentId: actor,
2571
+ transitionComment: `Auto-started for completion: ${item.content}`
2572
+ });
2573
+ if (!started.ok) {
2574
+ continue;
2575
+ }
2576
+ }
2562
2577
  await execute({
2563
2578
  action: "mark_assignment",
2564
2579
  boardId: board.id,
package/dist/todo.js CHANGED
@@ -1437,6 +1437,7 @@ function sourceStatus(task) {
1437
1437
  function todoStatus(task) {
1438
1438
  const status = sourceStatus(task);
1439
1439
  if (status === "completed") return "completed";
1440
+ if (status === "review" && task.assignment?.status === "completed") return "completed";
1440
1441
  if (status === "in_progress" || status === "review") return "in_progress";
1441
1442
  return "pending";
1442
1443
  }
@@ -2546,6 +2547,20 @@ async function synchronizeManagedKanban(items, board, ctx, signal) {
2546
2547
  }
2547
2548
  const task = board.tasks.find((candidate) => candidate.id === item.kanbanTaskId);
2548
2549
  if (!task || task.status === "completed") continue;
2550
+ const stage = task.lifecycle?.currentStage;
2551
+ if (stage === "backlog" || stage === "todo") {
2552
+ const started = await execute({
2553
+ action: "start_task",
2554
+ boardId: board.id,
2555
+ taskId: task.id,
2556
+ author: actor,
2557
+ agentId: actor,
2558
+ transitionComment: `Auto-started for completion: ${item.content}`
2559
+ });
2560
+ if (!started.ok) {
2561
+ continue;
2562
+ }
2563
+ }
2549
2564
  await execute({
2550
2565
  action: "mark_assignment",
2551
2566
  boardId: board.id,