get-tbd 0.1.24 → 0.1.26

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.
Files changed (45) hide show
  1. package/README.md +2 -2
  2. package/dist/bin.mjs +195 -49
  3. package/dist/bin.mjs.map +1 -1
  4. package/dist/cli.mjs +126 -44
  5. package/dist/cli.mjs.map +1 -1
  6. package/dist/{config-CB1tcqTZ.mjs → config-BZte2m3w.mjs} +1 -1
  7. package/dist/{config-CmEAGaxz.mjs → config-b20Kf5pW.mjs} +3 -2
  8. package/dist/config-b20Kf5pW.mjs.map +1 -0
  9. package/dist/docs/README.md +2 -2
  10. package/dist/docs/SKILL.md +31 -31
  11. package/dist/docs/guidelines/cli-agent-skill-patterns.md +1 -1
  12. package/dist/docs/guidelines/convex-limits-best-practices.md +16 -16
  13. package/dist/docs/guidelines/convex-rules.md +3 -3
  14. package/dist/docs/guidelines/electron-app-development-patterns.md +1 -1
  15. package/dist/docs/guidelines/error-handling-rules.md +2 -2
  16. package/dist/docs/guidelines/general-coding-rules.md +2 -2
  17. package/dist/docs/guidelines/general-comment-rules.md +2 -2
  18. package/dist/docs/guidelines/general-eng-assistant-rules.md +2 -2
  19. package/dist/docs/guidelines/python-rules.md +4 -4
  20. package/dist/docs/guidelines/typescript-rules.md +17 -17
  21. package/dist/docs/guidelines/typescript-yaml-handling-rules.md +8 -8
  22. package/dist/docs/shortcuts/standard/new-guideline.md +4 -4
  23. package/dist/docs/shortcuts/standard/new-validation-plan.md +13 -13
  24. package/dist/docs/shortcuts/standard/revise-all-architecture-docs.md +1 -1
  25. package/dist/docs/shortcuts/standard/setup-github-cli.md +1 -1
  26. package/dist/docs/shortcuts/standard/welcome-user.md +12 -12
  27. package/dist/docs/shortcuts/system/skill-baseline.md +31 -31
  28. package/dist/id-mapping-BA_xn516.mjs +3 -0
  29. package/dist/{id-mapping-DjVJIO4M.mjs → id-mapping-BtBwq5nG.mjs} +68 -15
  30. package/dist/id-mapping-BtBwq5nG.mjs.map +1 -0
  31. package/dist/index.mjs +2 -2
  32. package/dist/schemas-BQYmDnkv.mjs +311 -0
  33. package/dist/schemas-BQYmDnkv.mjs.map +1 -0
  34. package/dist/{src-BrM6xcdG.mjs → src-DQcOQnFp.mjs} +4 -3
  35. package/dist/{src-BrM6xcdG.mjs.map → src-DQcOQnFp.mjs.map} +1 -1
  36. package/dist/tbd +195 -49
  37. package/dist/yaml-utils-BPy991by.mjs +273 -0
  38. package/dist/yaml-utils-BPy991by.mjs.map +1 -0
  39. package/dist/yaml-utils-swV780m5.mjs +3 -0
  40. package/package.json +1 -1
  41. package/dist/config-CmEAGaxz.mjs.map +0 -1
  42. package/dist/id-mapping-DjVJIO4M.mjs.map +0 -1
  43. package/dist/id-mapping-LjnDSEhN.mjs +0 -3
  44. package/dist/yaml-utils-U7l9hhkh.mjs +0 -581
  45. package/dist/yaml-utils-U7l9hhkh.mjs.map +0 -1
package/README.md CHANGED
@@ -279,7 +279,7 @@ npm install -g get-tbd@latest
279
279
  ### Setup
280
280
 
281
281
  ```bash
282
- # Fresh project (--prefix is REQUIRED—2-8 alphabetic chars, e.g. myapp-a1b2)
282
+ # Fresh project (--prefix is REQUIRED—a short alphabetic name used as an issue ID prefix, e.g. myapp → issues like myapp-a1b2)
283
283
  tbd setup --auto --prefix=myapp
284
284
 
285
285
  # Joining an existing tbd project (no prefix needed—reads existing config)
@@ -299,7 +299,7 @@ tbd setup --from-beads
299
299
  **First contributor:**
300
300
  ```bash
301
301
  npm install -g get-tbd@latest
302
- tbd setup --auto --prefix=myproject
302
+ tbd setup --auto --prefix=proj # Short alphabetic prefix for issue IDs
303
303
  git add .tbd/ .claude/ && git commit -m "Initialize tbd"
304
304
  git push
305
305
  ```
package/dist/bin.mjs CHANGED
@@ -13735,6 +13735,18 @@ const ordering = {
13735
13735
  * IMPORTANT: Always use these utilities instead of raw yaml package functions.
13736
13736
  * This ensures consistent formatting and proper error handling across the codebase.
13737
13737
  */
13738
+ var yaml_utils_exports = /* @__PURE__ */ __exportAll({
13739
+ MergeConflictError: () => MergeConflictError,
13740
+ YAML_STRINGIFY_OPTIONS: () => YAML_STRINGIFY_OPTIONS,
13741
+ YAML_STRINGIFY_OPTIONS_COMPACT: () => YAML_STRINGIFY_OPTIONS_COMPACT,
13742
+ detectDuplicateYamlKeys: () => detectDuplicateYamlKeys,
13743
+ hasMergeConflictMarkers: () => hasMergeConflictMarkers,
13744
+ parseYamlToleratingDuplicateKeys: () => parseYamlToleratingDuplicateKeys,
13745
+ parseYamlWithConflictDetection: () => parseYamlWithConflictDetection,
13746
+ sortKeys: () => sortKeys,
13747
+ stringifyYaml: () => stringifyYaml,
13748
+ stringifyYamlCompact: () => stringifyYamlCompact
13749
+ });
13738
13750
  /**
13739
13751
  * Serialize data to YAML with readable formatting.
13740
13752
  *
@@ -14033,7 +14045,7 @@ function serializeIssue(issue) {
14033
14045
  * Package version, derived from git at build time.
14034
14046
  * Format: X.Y.Z for releases, X.Y.Z-dev.N.hash for dev builds.
14035
14047
  */
14036
- const VERSION$1 = "0.1.24";
14048
+ const VERSION$1 = "0.1.26";
14037
14049
 
14038
14050
  //#endregion
14039
14051
  //#region src/cli/lib/version.ts
@@ -99037,6 +99049,7 @@ async function initWorktree(baseDir, remote = "origin", syncBranch = SYNC_BRANCH
99037
99049
  await writeFile(join(dataSyncPath, "meta.yml"), "schema_version: 1\n");
99038
99050
  await writeFile(join(dataSyncPath, "issues", ".gitkeep"), "");
99039
99051
  await writeFile(join(dataSyncPath, "mappings", ".gitkeep"), "");
99052
+ await writeFile(join(dataSyncPath, "mappings", ".gitattributes"), "ids.yml merge=union\n");
99040
99053
  await git("-C", worktreePath, "add", ".");
99041
99054
  await git("-C", worktreePath, "commit", "--no-verify", "-m", "Initialize tbd-sync branch");
99042
99055
  return {
@@ -99274,9 +99287,16 @@ async function migrateDataToWorktree(baseDir, removeSource = false) {
99274
99287
  await mkdir(correctMappingsPath, { recursive: true });
99275
99288
  for (const file of issueFiles) await cp(join(wrongIssuesPath, file), join(correctIssuesPath, file));
99276
99289
  for (const file of mappingFiles) if (file === "ids.yml") {
99277
- const { loadIdMapping, mergeIdMappings, saveIdMapping } = await Promise.resolve().then(() => id_mapping_exports);
99278
- const sourceMapping = await loadIdMapping(wrongPath);
99279
- await saveIdMapping(correctPath, mergeIdMappings(await loadIdMapping(correctPath), sourceMapping));
99290
+ const { readFile } = await import("node:fs/promises");
99291
+ const { loadIdMapping, mergeIdMappings, saveIdMapping, resolveIdMappingConflicts } = await Promise.resolve().then(() => id_mapping_exports);
99292
+ const sourceMapping = resolveIdMappingConflicts(await readFile(join(wrongMappingsPath, file), "utf-8"));
99293
+ let targetMapping;
99294
+ try {
99295
+ targetMapping = resolveIdMappingConflicts(await readFile(join(correctMappingsPath, file), "utf-8"));
99296
+ } catch {
99297
+ targetMapping = await loadIdMapping(correctPath);
99298
+ }
99299
+ await saveIdMapping(correctPath, mergeIdMappings(targetMapping, sourceMapping));
99280
99300
  } else await cp(join(wrongMappingsPath, file), join(correctMappingsPath, file));
99281
99301
  const totalFiles = issueFiles.length + mappingFiles.length;
99282
99302
  await git("-C", worktreePath, "add", "-A");
@@ -99751,28 +99771,32 @@ async function listIssues(baseDir) {
99751
99771
  return [];
99752
99772
  }
99753
99773
  const mdFiles = files.filter((f) => f.endsWith(".md"));
99754
- const fileContents = await Promise.all(mdFiles.map(async (file) => {
99755
- const filePath = join(issuesDir, file);
99756
- try {
99757
- return {
99758
- file,
99759
- content: await readFile(filePath, "utf-8")
99760
- };
99761
- } catch {
99762
- return {
99763
- file,
99764
- content: null
99765
- };
99766
- }
99767
- }));
99774
+ const BATCH_SIZE = 200;
99768
99775
  const issues = [];
99769
- for (const { file, content } of fileContents) {
99770
- if (content === null) continue;
99771
- try {
99772
- const issue = parseIssue(content);
99773
- issues.push(issue);
99774
- } catch (error) {
99775
- console.warn(`Skipping invalid issue file: ${file}`, error);
99776
+ for (let i = 0; i < mdFiles.length; i += BATCH_SIZE) {
99777
+ const batch = mdFiles.slice(i, i + BATCH_SIZE);
99778
+ const fileContents = await Promise.all(batch.map(async (file) => {
99779
+ const filePath = join(issuesDir, file);
99780
+ try {
99781
+ return {
99782
+ file,
99783
+ content: await readFile(filePath, "utf-8")
99784
+ };
99785
+ } catch {
99786
+ return {
99787
+ file,
99788
+ content: null
99789
+ };
99790
+ }
99791
+ }));
99792
+ for (const { file, content } of fileContents) {
99793
+ if (content === null) continue;
99794
+ try {
99795
+ const issue = parseIssue(content);
99796
+ issues.push(issue);
99797
+ } catch (error) {
99798
+ console.warn(`Skipping invalid issue file: ${file}`, error);
99799
+ }
99776
99800
  }
99777
99801
  }
99778
99802
  return issues;
@@ -99814,30 +99838,44 @@ async function listIssues(baseDir) {
99814
99838
  * crashed and break the lock. This is a heuristic — safe when the critical
99815
99839
  * section is short-lived (sub-second for file I/O).
99816
99840
  *
99817
- * ## Degraded mode
99841
+ * ## Failure on timeout
99842
+ *
99843
+ * If the lock cannot be acquired within the timeout, a LockAcquisitionError is
99844
+ * thrown. This prevents the dangerous "degraded mode" where the critical section
99845
+ * runs without mutual exclusion, which can cause data loss (e.g., lost ID
99846
+ * mappings during concurrent `tbd create`).
99818
99847
  *
99819
- * If the lock cannot be acquired within the timeout (e.g., due to a stuck
99820
- * lockfile that isn't old enough to break), the critical section runs anyway.
99821
- * Callers should design their critical sections to be safe without the lock
99822
- * (e.g., using read-merge-write for append-only data).
99848
+ * IMPORTANT: `timeoutMs` must be greater than `staleMs` so stale locks from
99849
+ * crashed processes are always detected and broken before the timeout expires.
99823
99850
  */
99824
- const DEFAULT_TIMEOUT_MS = 2e3;
99851
+ const DEFAULT_TIMEOUT_MS = 1e4;
99825
99852
  const DEFAULT_POLL_MS = 50;
99826
99853
  const DEFAULT_STALE_MS = 5e3;
99827
99854
  /**
99855
+ * Error thrown when the lock cannot be acquired within the timeout.
99856
+ */
99857
+ var LockAcquisitionError = class extends Error {
99858
+ constructor(lockPath, timeoutMs) {
99859
+ super(`Failed to acquire lock at ${lockPath} within ${timeoutMs}ms. Another process may be holding the lock. If this persists, delete the lock directory manually and retry.`);
99860
+ this.name = "LockAcquisitionError";
99861
+ }
99862
+ };
99863
+ /**
99828
99864
  * Execute `fn` while holding a lockfile.
99829
99865
  *
99830
99866
  * The lock is a directory at `lockPath` (typically `<target-file>.lock`).
99831
99867
  * Concurrent callers will wait up to `timeoutMs` for the lock, polling
99832
99868
  * every `pollMs`. Stale locks older than `staleMs` are broken automatically.
99833
99869
  *
99834
- * If the lock cannot be acquired, `fn` is still executed (degraded mode).
99835
- * This ensures a stuck lockfile never permanently blocks the CLI.
99870
+ * If the lock cannot be acquired within the timeout, a LockAcquisitionError
99871
+ * is thrown. This ensures mutual exclusion is never silently bypassed, which
99872
+ * prevents data loss from concurrent writes.
99836
99873
  *
99837
99874
  * @param lockPath - Path to use as the lock directory (e.g., "/path/to/ids.yml.lock")
99838
99875
  * @param fn - Critical section to execute under the lock
99839
99876
  * @param options - Timing parameters for lock acquisition
99840
99877
  * @returns The return value of `fn`
99878
+ * @throws LockAcquisitionError if the lock cannot be acquired within the timeout
99841
99879
  *
99842
99880
  * @example
99843
99881
  * ```ts
@@ -99859,7 +99897,7 @@ async function withLockfile(lockPath, fn, options) {
99859
99897
  acquired = true;
99860
99898
  break;
99861
99899
  } catch (error) {
99862
- if (error.code !== "EEXIST") break;
99900
+ if (error.code !== "EEXIST") throw error;
99863
99901
  try {
99864
99902
  const lockStat = await stat(lockPath);
99865
99903
  if (Date.now() - lockStat.mtimeMs > staleMs) {
@@ -99873,10 +99911,11 @@ async function withLockfile(lockPath, fn, options) {
99873
99911
  }
99874
99912
  await new Promise((resolve) => setTimeout(resolve, pollMs));
99875
99913
  }
99914
+ if (!acquired) throw new LockAcquisitionError(lockPath, timeoutMs);
99876
99915
  try {
99877
99916
  return await fn();
99878
99917
  } finally {
99879
- if (acquired) try {
99918
+ try {
99880
99919
  await rmdir(lockPath);
99881
99920
  } catch {}
99882
99921
  }
@@ -99984,6 +100023,7 @@ var id_mapping_exports = /* @__PURE__ */ __exportAll({
99984
100023
  mergeIdMappings: () => mergeIdMappings,
99985
100024
  parseIdMappingFromYaml: () => parseIdMappingFromYaml,
99986
100025
  reconcileMappings: () => reconcileMappings,
100026
+ resolveIdMappingConflicts: () => resolveIdMappingConflicts,
99987
100027
  resolveToInternalId: () => resolveToInternalId,
99988
100028
  saveIdMapping: () => saveIdMapping
99989
100029
  });
@@ -100033,8 +100073,8 @@ async function loadIdMapping(baseDir) {
100033
100073
  * commands run in parallel.
100034
100074
  *
100035
100075
  * The merge is safe because ID mappings are append-only — entries are never
100036
- * intentionally removed. Even if the lock acquisition fails (degraded mode),
100037
- * the read-merge-write provides a fallback that preserves entries from other writers.
100076
+ * intentionally removed. If the lock cannot be acquired within the timeout,
100077
+ * a LockAcquisitionError is thrown rather than proceeding without protection.
100038
100078
  */
100039
100079
  async function saveIdMapping(baseDir, mapping) {
100040
100080
  const filePath = getMappingPath(baseDir);
@@ -100247,6 +100287,43 @@ function mergeIdMappings(local, remote) {
100247
100287
  }
100248
100288
  return merged;
100249
100289
  }
100290
+ /**
100291
+ * Resolve merge conflicts in ids.yml content by extracting both sides and merging.
100292
+ *
100293
+ * ids.yml is a sorted key-value YAML map where entries are append-only.
100294
+ * The most common merge conflict is both sides adding non-overlapping keys,
100295
+ * which is trivially auto-resolvable by keeping all entries from both sides.
100296
+ *
100297
+ * @param content - Raw file content that may contain git merge conflict markers
100298
+ * @returns Merged IdMapping with entries from both sides
100299
+ */
100300
+ function resolveIdMappingConflicts(content) {
100301
+ if (!hasMergeConflictMarkers(content)) return parseIdMappingFromYaml(content);
100302
+ const lines = content.split("\n");
100303
+ const oursLines = [];
100304
+ const theirsLines = [];
100305
+ let inConflict = "none";
100306
+ for (const line of lines) {
100307
+ if (line.startsWith("<<<<<<< ")) {
100308
+ inConflict = "ours";
100309
+ continue;
100310
+ }
100311
+ if (line === "=======" && inConflict === "ours") {
100312
+ inConflict = "theirs";
100313
+ continue;
100314
+ }
100315
+ if (line.startsWith(">>>>>>> ") && inConflict === "theirs") {
100316
+ inConflict = "none";
100317
+ continue;
100318
+ }
100319
+ if (inConflict === "none") {
100320
+ oursLines.push(line);
100321
+ theirsLines.push(line);
100322
+ } else if (inConflict === "ours") oursLines.push(line);
100323
+ else theirsLines.push(line);
100324
+ }
100325
+ return mergeIdMappings(parseIdMappingFromYaml(oursLines.join("\n")), parseIdMappingFromYaml(theirsLines.join("\n")));
100326
+ }
100250
100327
 
100251
100328
  //#endregion
100252
100329
  //#region src/lib/priority.ts
@@ -101017,10 +101094,7 @@ function matchesSpecPath(storedPath, queryPath) {
101017
101094
  if (!normalizedStored || !normalizedQuery) return false;
101018
101095
  if (normalizedStored === normalizedQuery) return true;
101019
101096
  if (normalizedStored.endsWith("/" + normalizedQuery)) return true;
101020
- const storedFilename = basename(normalizedStored);
101021
- const queryFilename = basename(normalizedQuery);
101022
- if (!normalizedQuery.includes("/") && storedFilename === normalizedQuery) return true;
101023
- if (!normalizedQuery.includes("/") && storedFilename === queryFilename) return true;
101097
+ if (!normalizedQuery.includes("/") && basename(normalizedStored) === normalizedQuery) return true;
101024
101098
  return false;
101025
101099
  }
101026
101100
  /**
@@ -102365,7 +102439,7 @@ async function fetchWithGhFallback(url, options) {
102365
102439
  * See: docs/project/specs/active/plan-2026-01-26-configurable-doc-cache-sync.md
102366
102440
  */
102367
102441
  /** Prefix for internal bundled doc sources */
102368
- const INTERNAL_PREFIX = "internal:";
102442
+ const INTERNAL_SOURCE_PREFIX = "internal:";
102369
102443
  /**
102370
102444
  * Syncs documentation files from configured sources.
102371
102445
  *
@@ -102398,7 +102472,7 @@ var DocSync = class {
102398
102472
  * // => { type: 'url', location: 'https://...' }
102399
102473
  */
102400
102474
  parseSource(source) {
102401
- if (source.startsWith(INTERNAL_PREFIX)) return {
102475
+ if (source.startsWith(INTERNAL_SOURCE_PREFIX)) return {
102402
102476
  type: "internal",
102403
102477
  location: source.slice(9)
102404
102478
  };
@@ -102577,7 +102651,7 @@ async function generateDefaultDocCacheConfig() {
102577
102651
  const entries = await readdir(fullDir, { withFileTypes: true });
102578
102652
  for (const entry of entries) if (entry.isFile() && entry.name.endsWith(".md")) {
102579
102653
  const relativePath = `${prefix}/${entry.name}`;
102580
- config[relativePath] = `${INTERNAL_PREFIX}${relativePath}`;
102654
+ config[relativePath] = `${INTERNAL_SOURCE_PREFIX}${relativePath}`;
102581
102655
  }
102582
102656
  } catch {}
102583
102657
  }
@@ -102614,8 +102688,6 @@ function isDocsStale(lastSyncAt, autoSyncHours) {
102614
102688
  const lastSync = new Date(lastSyncAt).getTime();
102615
102689
  return (Date.now() - lastSync) / (1e3 * 60 * 60) >= autoSyncHours;
102616
102690
  }
102617
- /** Prefix for internal bundled doc sources */
102618
- const INTERNAL_SOURCE_PREFIX = "internal:";
102619
102691
  /**
102620
102692
  * Check if an internal bundled doc exists.
102621
102693
  *
@@ -103440,6 +103512,19 @@ var SyncHandler = class extends BaseCommand {
103440
103512
  } catch {
103441
103513
  this.output.debug("Remote sync branch does not exist yet");
103442
103514
  }
103515
+ {
103516
+ const { access, writeFile } = await import("node:fs/promises");
103517
+ const attrPath = join(this.dataSyncDir, "mappings", ".gitattributes");
103518
+ try {
103519
+ await access(attrPath);
103520
+ } catch {
103521
+ await writeFile(attrPath, "ids.yml merge=union\n");
103522
+ await git("-C", worktreePath, "add", attrPath);
103523
+ try {
103524
+ await git("-C", worktreePath, "commit", "--no-verify", "-m", "chore: add merge=union for ids.yml");
103525
+ } catch {}
103526
+ }
103527
+ }
103443
103528
  if (behindCommits > 0) {
103444
103529
  let headBeforeMerge = "";
103445
103530
  try {
@@ -103484,7 +103569,8 @@ var SyncHandler = class extends BaseCommand {
103484
103569
  const remoteIdsContent = await git("show", `${remote}/${syncBranch}:${DATA_SYNC_DIR}/mappings/ids.yml`);
103485
103570
  if (remoteIdsContent) {
103486
103571
  conflictRemoteMapping = parseIdMappingFromYaml(remoteIdsContent);
103487
- const localMapping = await loadIdMapping(this.dataSyncDir);
103572
+ const { readFile } = await import("node:fs/promises");
103573
+ const localMapping = resolveIdMappingConflicts(await readFile(join(this.dataSyncDir, "mappings", "ids.yml"), "utf-8"));
103488
103574
  const mergedMapping = mergeIdMappings(localMapping, conflictRemoteMapping);
103489
103575
  await saveIdMapping(this.dataSyncDir, mergedMapping);
103490
103576
  this.output.debug(`Merged ID mappings: ${localMapping.shortToUlid.size} local + ${conflictRemoteMapping.shortToUlid.size} remote = ${mergedMapping.shortToUlid.size} total`);
@@ -104600,14 +104686,22 @@ var DoctorHandler = class extends BaseCommand {
104600
104686
  healthChecks.push(await this.checkIssuesDirectory());
104601
104687
  healthChecks.push(this.checkOrphanedDependencies(this.issues));
104602
104688
  healthChecks.push(this.checkDuplicateIds(this.issues));
104689
+ healthChecks.push(await this.checkIdMappingConflicts(options.fix));
104603
104690
  healthChecks.push(await this.checkIdMappingDuplicates(options.fix));
104604
104691
  healthChecks.push(await this.checkTempFiles(options.fix));
104605
104692
  healthChecks.push(this.checkIssueValidity(this.issues));
104693
+ healthChecks.push(await this.checkWorktree(options.fix));
104694
+ const dataLocationResult = await this.checkDataLocation(options.fix);
104695
+ healthChecks.push(dataLocationResult);
104696
+ if (dataLocationResult.status === "ok" && dataLocationResult.message?.includes("migrated")) {
104697
+ this.dataSyncDir = await resolveDataSyncDir(this.cwd);
104698
+ try {
104699
+ this.issues = await listIssues(this.dataSyncDir);
104700
+ } catch {}
104701
+ }
104606
104702
  const parsedMaxHistory = options.maxHistory ? parseInt(options.maxHistory, 10) : 50;
104607
104703
  const maxHistory = Number.isNaN(parsedMaxHistory) || parsedMaxHistory < 0 ? 50 : parsedMaxHistory;
104608
104704
  healthChecks.push(await this.checkMissingMappings(options.fix, maxHistory));
104609
- healthChecks.push(await this.checkWorktree(options.fix));
104610
- healthChecks.push(await this.checkDataLocation(options.fix));
104611
104705
  healthChecks.push(await this.checkLocalSyncBranch());
104612
104706
  healthChecks.push(await this.checkRemoteSyncBranch());
104613
104707
  healthChecks.push(await this.checkLocalVsRemoteData());
@@ -104805,6 +104899,58 @@ var DoctorHandler = class extends BaseCommand {
104805
104899
  };
104806
104900
  }
104807
104901
  /**
104902
+ * Check 5b: Merge conflict markers in ids.yml.
104903
+ *
104904
+ * After a failed git merge during sync, ids.yml may retain unresolved
104905
+ * conflict markers (<<<<<<< / ======= / >>>>>>>). This blocks all tbd
104906
+ * commands since YAML parsing throws MergeConflictError.
104907
+ *
104908
+ * For ids.yml, both sides are simple key-value pairs that are append-only,
104909
+ * so the resolution is trivial: keep all entries from both sides.
104910
+ *
104911
+ * With --fix, extracts both sides, merges them, and re-saves.
104912
+ */
104913
+ async checkIdMappingConflicts(fix) {
104914
+ const mappingPath = join(this.dataSyncDir, "mappings", "ids.yml");
104915
+ let content;
104916
+ try {
104917
+ content = await readFile(mappingPath, "utf-8");
104918
+ } catch {
104919
+ return {
104920
+ name: "ID mapping conflicts",
104921
+ status: "ok"
104922
+ };
104923
+ }
104924
+ const { hasMergeConflictMarkers } = await Promise.resolve().then(() => yaml_utils_exports);
104925
+ if (!hasMergeConflictMarkers(content)) return {
104926
+ name: "ID mapping conflicts",
104927
+ status: "ok"
104928
+ };
104929
+ if (fix && !this.checkDryRun("Resolve merge conflicts in ids.yml")) try {
104930
+ const { resolveIdMappingConflicts, saveIdMapping } = await Promise.resolve().then(() => id_mapping_exports);
104931
+ const resolved = resolveIdMappingConflicts(content);
104932
+ await saveIdMapping(this.dataSyncDir, resolved);
104933
+ return {
104934
+ name: "ID mapping conflicts",
104935
+ status: "ok",
104936
+ message: `resolved merge conflicts (${resolved.shortToUlid.size} entries)`
104937
+ };
104938
+ } catch (error) {
104939
+ return {
104940
+ name: "ID mapping conflicts",
104941
+ status: "error",
104942
+ message: `failed to resolve conflicts: ${error instanceof Error ? error.message : String(error)}`
104943
+ };
104944
+ }
104945
+ return {
104946
+ name: "ID mapping conflicts",
104947
+ status: "error",
104948
+ message: "ids.yml contains unresolved merge conflict markers",
104949
+ fixable: true,
104950
+ suggestion: "Run: tbd doctor --fix to auto-resolve"
104951
+ };
104952
+ }
104953
+ /**
104808
104954
  * Check for duplicate keys in the ID mapping file (ids.yml).
104809
104955
  *
104810
104956
  * After a git merge conflict resolution that keeps entries from both sides,