get-tbd 0.1.25 → 0.1.27

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 (43) hide show
  1. package/dist/bin.mjs +253 -43
  2. package/dist/bin.mjs.map +1 -1
  3. package/dist/cli.mjs +196 -46
  4. package/dist/cli.mjs.map +1 -1
  5. package/dist/{config-CmEAGaxz.mjs → config-B38rbI9u.mjs} +3 -2
  6. package/dist/config-B38rbI9u.mjs.map +1 -0
  7. package/dist/{config-CB1tcqTZ.mjs → config-C0ITTrtc.mjs} +1 -1
  8. package/dist/docs/SKILL.md +31 -31
  9. package/dist/docs/guidelines/cli-agent-skill-patterns.md +1 -1
  10. package/dist/docs/guidelines/convex-limits-best-practices.md +16 -16
  11. package/dist/docs/guidelines/convex-rules.md +3 -3
  12. package/dist/docs/guidelines/electron-app-development-patterns.md +1 -1
  13. package/dist/docs/guidelines/error-handling-rules.md +2 -2
  14. package/dist/docs/guidelines/general-coding-rules.md +2 -2
  15. package/dist/docs/guidelines/general-comment-rules.md +2 -2
  16. package/dist/docs/guidelines/general-eng-assistant-rules.md +2 -2
  17. package/dist/docs/guidelines/python-rules.md +4 -4
  18. package/dist/docs/guidelines/typescript-rules.md +17 -17
  19. package/dist/docs/guidelines/typescript-yaml-handling-rules.md +8 -8
  20. package/dist/docs/shortcuts/standard/new-guideline.md +4 -4
  21. package/dist/docs/shortcuts/standard/new-validation-plan.md +13 -13
  22. package/dist/docs/shortcuts/standard/revise-all-architecture-docs.md +1 -1
  23. package/dist/docs/shortcuts/standard/setup-github-cli.md +1 -1
  24. package/dist/docs/shortcuts/standard/welcome-user.md +12 -12
  25. package/dist/docs/shortcuts/system/skill-baseline.md +31 -31
  26. package/dist/{id-mapping-BSNsaOCC.mjs → id-mapping-CqrrLgeX.mjs} +42 -4
  27. package/dist/{id-mapping-BSNsaOCC.mjs.map → id-mapping-CqrrLgeX.mjs.map} +1 -1
  28. package/dist/id-mapping-Ctfl_nc1.mjs +3 -0
  29. package/dist/index.d.mts +13 -1
  30. package/dist/index.mjs +3 -3
  31. package/dist/schemas-C8mOQykE.mjs +323 -0
  32. package/dist/schemas-C8mOQykE.mjs.map +1 -0
  33. package/dist/{src-DuXy2Uyd.mjs → src-BIE27KDA.mjs} +4 -3
  34. package/dist/{src-DuXy2Uyd.mjs.map → src-BIE27KDA.mjs.map} +1 -1
  35. package/dist/tbd +253 -43
  36. package/dist/yaml-utils-BPy991by.mjs +273 -0
  37. package/dist/yaml-utils-BPy991by.mjs.map +1 -0
  38. package/dist/yaml-utils-swV780m5.mjs +3 -0
  39. package/package.json +1 -1
  40. package/dist/config-CmEAGaxz.mjs.map +0 -1
  41. package/dist/id-mapping-DMMKwXZv.mjs +0 -3
  42. package/dist/yaml-utils-U7l9hhkh.mjs +0 -581
  43. package/dist/yaml-utils-U7l9hhkh.mjs.map +0 -1
package/dist/bin.mjs CHANGED
@@ -6710,6 +6710,18 @@ const IssueKind = enumType([
6710
6710
  "chore"
6711
6711
  ]);
6712
6712
  /**
6713
+ * Maximum issue title length before detail belongs in the description body.
6714
+ */
6715
+ const ISSUE_TITLE_MAX_LENGTH = 500;
6716
+ /**
6717
+ * Maximum issue body section length to keep issue files practical to review and sync.
6718
+ */
6719
+ const ISSUE_BODY_MAX_LENGTH = 5e4;
6720
+ /**
6721
+ * Issue title text as persisted in issue files.
6722
+ */
6723
+ const IssueTitle = stringType().min(1).max(ISSUE_TITLE_MAX_LENGTH);
6724
+ /**
6713
6725
  * Priority: 0 (highest/critical) to 4 (lowest).
6714
6726
  */
6715
6727
  const Priority = numberType().int().min(0).max(4);
@@ -6743,12 +6755,12 @@ const Dependency = objectType({
6743
6755
  */
6744
6756
  const IssueSchema = BaseEntity.extend({
6745
6757
  type: literalType("is"),
6746
- title: stringType().min(1).max(500),
6758
+ title: IssueTitle,
6747
6759
  kind: IssueKind.default("task"),
6748
6760
  status: IssueStatus.default("open"),
6749
6761
  priority: Priority.default(2),
6750
- description: stringType().max(5e4).nullable().optional(),
6751
- notes: stringType().max(5e4).nullable().optional(),
6762
+ description: stringType().max(ISSUE_BODY_MAX_LENGTH).nullable().optional(),
6763
+ notes: stringType().max(ISSUE_BODY_MAX_LENGTH).nullable().optional(),
6752
6764
  spec_path: stringType().nullable().optional(),
6753
6765
  assignee: stringType().nullable().optional(),
6754
6766
  labels: arrayType(stringType()).default([]),
@@ -13735,6 +13747,18 @@ const ordering = {
13735
13747
  * IMPORTANT: Always use these utilities instead of raw yaml package functions.
13736
13748
  * This ensures consistent formatting and proper error handling across the codebase.
13737
13749
  */
13750
+ var yaml_utils_exports = /* @__PURE__ */ __exportAll({
13751
+ MergeConflictError: () => MergeConflictError,
13752
+ YAML_STRINGIFY_OPTIONS: () => YAML_STRINGIFY_OPTIONS,
13753
+ YAML_STRINGIFY_OPTIONS_COMPACT: () => YAML_STRINGIFY_OPTIONS_COMPACT,
13754
+ detectDuplicateYamlKeys: () => detectDuplicateYamlKeys,
13755
+ hasMergeConflictMarkers: () => hasMergeConflictMarkers,
13756
+ parseYamlToleratingDuplicateKeys: () => parseYamlToleratingDuplicateKeys,
13757
+ parseYamlWithConflictDetection: () => parseYamlWithConflictDetection,
13758
+ sortKeys: () => sortKeys,
13759
+ stringifyYaml: () => stringifyYaml,
13760
+ stringifyYamlCompact: () => stringifyYamlCompact
13761
+ });
13738
13762
  /**
13739
13763
  * Serialize data to YAML with readable formatting.
13740
13764
  *
@@ -14033,7 +14057,7 @@ function serializeIssue(issue) {
14033
14057
  * Package version, derived from git at build time.
14034
14058
  * Format: X.Y.Z for releases, X.Y.Z-dev.N.hash for dev builds.
14035
14059
  */
14036
- const VERSION$1 = "0.1.25";
14060
+ const VERSION$1 = "0.1.27";
14037
14061
 
14038
14062
  //#endregion
14039
14063
  //#region src/cli/lib/version.ts
@@ -99037,6 +99061,7 @@ async function initWorktree(baseDir, remote = "origin", syncBranch = SYNC_BRANCH
99037
99061
  await writeFile(join(dataSyncPath, "meta.yml"), "schema_version: 1\n");
99038
99062
  await writeFile(join(dataSyncPath, "issues", ".gitkeep"), "");
99039
99063
  await writeFile(join(dataSyncPath, "mappings", ".gitkeep"), "");
99064
+ await writeFile(join(dataSyncPath, "mappings", ".gitattributes"), "ids.yml merge=union\n");
99040
99065
  await git("-C", worktreePath, "add", ".");
99041
99066
  await git("-C", worktreePath, "commit", "--no-verify", "-m", "Initialize tbd-sync branch");
99042
99067
  return {
@@ -99274,9 +99299,16 @@ async function migrateDataToWorktree(baseDir, removeSource = false) {
99274
99299
  await mkdir(correctMappingsPath, { recursive: true });
99275
99300
  for (const file of issueFiles) await cp(join(wrongIssuesPath, file), join(correctIssuesPath, file));
99276
99301
  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));
99302
+ const { readFile } = await import("node:fs/promises");
99303
+ const { loadIdMapping, mergeIdMappings, saveIdMapping, resolveIdMappingConflicts } = await Promise.resolve().then(() => id_mapping_exports);
99304
+ const sourceMapping = resolveIdMappingConflicts(await readFile(join(wrongMappingsPath, file), "utf-8"));
99305
+ let targetMapping;
99306
+ try {
99307
+ targetMapping = resolveIdMappingConflicts(await readFile(join(correctMappingsPath, file), "utf-8"));
99308
+ } catch {
99309
+ targetMapping = await loadIdMapping(correctPath);
99310
+ }
99311
+ await saveIdMapping(correctPath, mergeIdMappings(targetMapping, sourceMapping));
99280
99312
  } else await cp(join(wrongMappingsPath, file), join(correctMappingsPath, file));
99281
99313
  const totalFiles = issueFiles.length + mappingFiles.length;
99282
99314
  await git("-C", worktreePath, "add", "-A");
@@ -99706,6 +99738,29 @@ function formatDebugId(internalId, mapping, prefix = "tbd") {
99706
99738
  return `${formatDisplayId(internalId, mapping, prefix)} (${internalId})`;
99707
99739
  }
99708
99740
 
99741
+ //#endregion
99742
+ //#region src/utils/zod-error-utils.ts
99743
+ /**
99744
+ * Helpers for rendering Zod errors without relying on object inspection.
99745
+ */
99746
+ /**
99747
+ * Format a ZodError as concise path-qualified messages for CLI output.
99748
+ */
99749
+ function formatZodError(error) {
99750
+ const messages = error.issues.map((issue) => {
99751
+ return `${issue.path.length > 0 ? issue.path.join(".") : "<root>"}: ${issue.message}`;
99752
+ });
99753
+ return messages.length > 0 ? messages.join("; ") : error.message;
99754
+ }
99755
+ /**
99756
+ * Format unknown thrown values as safe strings for warnings and diagnostics.
99757
+ */
99758
+ function formatUnknownError(error) {
99759
+ if (error instanceof ZodError) return formatZodError(error);
99760
+ if (error instanceof Error) return error.message;
99761
+ return String(error);
99762
+ }
99763
+
99709
99764
  //#endregion
99710
99765
  //#region src/file/storage.ts
99711
99766
  /**
@@ -99717,6 +99772,10 @@ function formatDebugId(internalId, mapping, prefix = "tbd") {
99717
99772
  * See: tbd-design.md §3.2 Storage Layer
99718
99773
  */
99719
99774
  /**
99775
+ * Maximum issue files read concurrently to avoid exhausting file descriptors in large repos.
99776
+ */
99777
+ const ISSUE_READ_BATCH_SIZE = 200;
99778
+ /**
99720
99779
  * Get the path to an issue file.
99721
99780
  */
99722
99781
  function getIssuePath(baseDir, id) {
@@ -99734,7 +99793,8 @@ async function readIssue(baseDir, id) {
99734
99793
  * Uses atomic write to prevent corruption.
99735
99794
  */
99736
99795
  async function writeIssue(baseDir, issue) {
99737
- await writeFile(getIssuePath(baseDir, issue.id), serializeIssue(issue));
99796
+ const validIssue = IssueSchema.parse(issue);
99797
+ await writeFile(getIssuePath(baseDir, validIssue.id), serializeIssue(validIssue));
99738
99798
  }
99739
99799
  /**
99740
99800
  * List all issues in the worktree.
@@ -99742,7 +99802,8 @@ async function writeIssue(baseDir, issue) {
99742
99802
  *
99743
99803
  * Uses parallel file reading for better performance with many issues.
99744
99804
  */
99745
- async function listIssues(baseDir) {
99805
+ async function listIssues(baseDir, options = {}) {
99806
+ const warnOnInvalid = options.warnOnInvalid ?? true;
99746
99807
  const issuesDir = join(baseDir, "issues");
99747
99808
  let files;
99748
99809
  try {
@@ -99751,10 +99812,9 @@ async function listIssues(baseDir) {
99751
99812
  return [];
99752
99813
  }
99753
99814
  const mdFiles = files.filter((f) => f.endsWith(".md"));
99754
- const BATCH_SIZE = 200;
99755
99815
  const issues = [];
99756
- for (let i = 0; i < mdFiles.length; i += BATCH_SIZE) {
99757
- const batch = mdFiles.slice(i, i + BATCH_SIZE);
99816
+ for (let i = 0; i < mdFiles.length; i += ISSUE_READ_BATCH_SIZE) {
99817
+ const batch = mdFiles.slice(i, i + ISSUE_READ_BATCH_SIZE);
99758
99818
  const fileContents = await Promise.all(batch.map(async (file) => {
99759
99819
  const filePath = join(issuesDir, file);
99760
99820
  try {
@@ -99762,25 +99822,38 @@ async function listIssues(baseDir) {
99762
99822
  file,
99763
99823
  content: await readFile(filePath, "utf-8")
99764
99824
  };
99765
- } catch {
99825
+ } catch (error) {
99766
99826
  return {
99767
99827
  file,
99768
- content: null
99828
+ error: formatUnknownError(error)
99769
99829
  };
99770
99830
  }
99771
99831
  }));
99772
- for (const { file, content } of fileContents) {
99773
- if (content === null) continue;
99832
+ for (const result of fileContents) {
99833
+ if ("error" in result) {
99834
+ reportInvalidIssueFile({
99835
+ file: result.file,
99836
+ reason: `failed to read file: ${result.error}`
99837
+ }, warnOnInvalid, options.onInvalidIssue);
99838
+ continue;
99839
+ }
99774
99840
  try {
99775
- const issue = parseIssue(content);
99841
+ const issue = parseIssue(result.content);
99776
99842
  issues.push(issue);
99777
99843
  } catch (error) {
99778
- console.warn(`Skipping invalid issue file: ${file}`, error);
99844
+ reportInvalidIssueFile({
99845
+ file: result.file,
99846
+ reason: formatUnknownError(error)
99847
+ }, warnOnInvalid, options.onInvalidIssue);
99779
99848
  }
99780
99849
  }
99781
99850
  }
99782
99851
  return issues;
99783
99852
  }
99853
+ function reportInvalidIssueFile(invalidIssue, warnOnInvalid, onInvalidIssue) {
99854
+ onInvalidIssue?.(invalidIssue);
99855
+ if (warnOnInvalid) console.warn(`Skipping invalid issue file: ${invalidIssue.file}: ${invalidIssue.reason}`);
99856
+ }
99784
99857
 
99785
99858
  //#endregion
99786
99859
  //#region src/utils/lockfile.ts
@@ -99877,7 +99950,7 @@ async function withLockfile(lockPath, fn, options) {
99877
99950
  acquired = true;
99878
99951
  break;
99879
99952
  } catch (error) {
99880
- if (error.code !== "EEXIST") break;
99953
+ if (error.code !== "EEXIST") throw error;
99881
99954
  try {
99882
99955
  const lockStat = await stat(lockPath);
99883
99956
  if (Date.now() - lockStat.mtimeMs > staleMs) {
@@ -100003,6 +100076,7 @@ var id_mapping_exports = /* @__PURE__ */ __exportAll({
100003
100076
  mergeIdMappings: () => mergeIdMappings,
100004
100077
  parseIdMappingFromYaml: () => parseIdMappingFromYaml,
100005
100078
  reconcileMappings: () => reconcileMappings,
100079
+ resolveIdMappingConflicts: () => resolveIdMappingConflicts,
100006
100080
  resolveToInternalId: () => resolveToInternalId,
100007
100081
  saveIdMapping: () => saveIdMapping
100008
100082
  });
@@ -100266,6 +100340,43 @@ function mergeIdMappings(local, remote) {
100266
100340
  }
100267
100341
  return merged;
100268
100342
  }
100343
+ /**
100344
+ * Resolve merge conflicts in ids.yml content by extracting both sides and merging.
100345
+ *
100346
+ * ids.yml is a sorted key-value YAML map where entries are append-only.
100347
+ * The most common merge conflict is both sides adding non-overlapping keys,
100348
+ * which is trivially auto-resolvable by keeping all entries from both sides.
100349
+ *
100350
+ * @param content - Raw file content that may contain git merge conflict markers
100351
+ * @returns Merged IdMapping with entries from both sides
100352
+ */
100353
+ function resolveIdMappingConflicts(content) {
100354
+ if (!hasMergeConflictMarkers(content)) return parseIdMappingFromYaml(content);
100355
+ const lines = content.split("\n");
100356
+ const oursLines = [];
100357
+ const theirsLines = [];
100358
+ let inConflict = "none";
100359
+ for (const line of lines) {
100360
+ if (line.startsWith("<<<<<<< ")) {
100361
+ inConflict = "ours";
100362
+ continue;
100363
+ }
100364
+ if (line === "=======" && inConflict === "ours") {
100365
+ inConflict = "theirs";
100366
+ continue;
100367
+ }
100368
+ if (line.startsWith(">>>>>>> ") && inConflict === "theirs") {
100369
+ inConflict = "none";
100370
+ continue;
100371
+ }
100372
+ if (inConflict === "none") {
100373
+ oursLines.push(line);
100374
+ theirsLines.push(line);
100375
+ } else if (inConflict === "ours") oursLines.push(line);
100376
+ else theirsLines.push(line);
100377
+ }
100378
+ return mergeIdMappings(parseIdMappingFromYaml(oursLines.join("\n")), parseIdMappingFromYaml(theirsLines.join("\n")));
100379
+ }
100269
100380
 
100270
100381
  //#endregion
100271
100382
  //#region src/lib/priority.ts
@@ -100458,6 +100569,22 @@ async function resolveAndValidatePath(inputPath, projectRoot, cwd) {
100458
100569
  return resolved;
100459
100570
  }
100460
100571
 
100572
+ //#endregion
100573
+ //#region src/cli/lib/issue-input-validation.ts
100574
+ /**
100575
+ * CLI validation helpers for user-provided issue fields.
100576
+ */
100577
+ /**
100578
+ * Validate a CLI-provided issue title with actionable user-facing errors.
100579
+ */
100580
+ function validateIssueTitle(title, options) {
100581
+ if (options.rejectBlank ? title.trim().length === 0 : title.length === 0) throw new ValidationError(options.emptyMessage);
100582
+ if (title.length > ISSUE_TITLE_MAX_LENGTH) throw new ValidationError(`Title is too long (${title.length} chars, max ${ISSUE_TITLE_MAX_LENGTH}). Move detail into the description body.`);
100583
+ const result = IssueTitle.safeParse(title);
100584
+ if (!result.success) throw new ValidationError(`Invalid title: ${formatZodError(result.error)}`);
100585
+ return title;
100586
+ }
100587
+
100461
100588
  //#endregion
100462
100589
  //#region src/cli/commands/create.ts
100463
100590
  /**
@@ -100469,6 +100596,10 @@ var CreateHandler = class extends BaseCommand {
100469
100596
  async run(title, options) {
100470
100597
  const tbdRoot = await requireInit();
100471
100598
  if (!title && !options.fromFile) throw new ValidationError("Title is required. Use: tbd create \"Issue title\"");
100599
+ const validatedTitle = title === void 0 ? void 0 : validateIssueTitle(title, {
100600
+ emptyMessage: "Title is required. Use: tbd create \"Issue title\"",
100601
+ rejectBlank: true
100602
+ });
100472
100603
  const kind = this.parseKind(options.type ?? "task");
100473
100604
  const priority = this.validatePriority(options.priority ?? "2");
100474
100605
  let description = options.description;
@@ -100485,7 +100616,7 @@ var CreateHandler = class extends BaseCommand {
100485
100616
  throw new ValidationError(getPathErrorMessage(error));
100486
100617
  }
100487
100618
  if (this.checkDryRun("Would create issue", {
100488
- title,
100619
+ title: validatedTitle,
100489
100620
  kind,
100490
100621
  priority,
100491
100622
  spec: specPath,
@@ -100517,7 +100648,7 @@ var CreateHandler = class extends BaseCommand {
100517
100648
  type: "is",
100518
100649
  id,
100519
100650
  version: 1,
100520
- title,
100651
+ title: validatedTitle,
100521
100652
  kind,
100522
100653
  status: "open",
100523
100654
  priority,
@@ -100549,9 +100680,9 @@ var CreateHandler = class extends BaseCommand {
100549
100680
  this.output.data({
100550
100681
  id: displayId,
100551
100682
  internalId: id,
100552
- title
100683
+ title: validatedTitle
100553
100684
  }, () => {
100554
- this.output.success(`Created ${displayId}: ${title}`);
100685
+ this.output.success(`Created ${displayId}: ${validatedTitle}`);
100555
100686
  });
100556
100687
  }
100557
100688
  parseKind(value) {
@@ -101036,10 +101167,7 @@ function matchesSpecPath(storedPath, queryPath) {
101036
101167
  if (!normalizedStored || !normalizedQuery) return false;
101037
101168
  if (normalizedStored === normalizedQuery) return true;
101038
101169
  if (normalizedStored.endsWith("/" + normalizedQuery)) return true;
101039
- const storedFilename = basename(normalizedStored);
101040
- const queryFilename = basename(normalizedQuery);
101041
- if (!normalizedQuery.includes("/") && storedFilename === normalizedQuery) return true;
101042
- if (!normalizedQuery.includes("/") && storedFilename === queryFilename) return true;
101170
+ if (!normalizedQuery.includes("/") && basename(normalizedStored) === normalizedQuery) return true;
101043
101171
  return false;
101044
101172
  }
101045
101173
  /**
@@ -101370,7 +101498,10 @@ var UpdateHandler = class extends BaseCommand {
101370
101498
  }
101371
101499
  try {
101372
101500
  const { frontmatter, description, notes } = parseMarkdownWithFrontmatter(content);
101373
- if (typeof frontmatter.title === "string") updates.title = frontmatter.title;
101501
+ if (typeof frontmatter.title === "string") updates.title = validateIssueTitle(frontmatter.title, {
101502
+ emptyMessage: "Title cannot be empty",
101503
+ rejectBlank: true
101504
+ });
101374
101505
  if (typeof frontmatter.status === "string") {
101375
101506
  const result = IssueStatus.safeParse(frontmatter.status);
101376
101507
  if (result.success) updates.status = result.data;
@@ -101401,10 +101532,10 @@ var UpdateHandler = class extends BaseCommand {
101401
101532
  }
101402
101533
  return updates;
101403
101534
  }
101404
- if (options.title !== void 0) {
101405
- if (!options.title.trim()) throw new ValidationError("Title cannot be empty");
101406
- updates.title = options.title;
101407
- }
101535
+ if (options.title !== void 0) updates.title = validateIssueTitle(options.title, {
101536
+ emptyMessage: "Title cannot be empty",
101537
+ rejectBlank: true
101538
+ });
101408
101539
  if (options.status) {
101409
101540
  const result = IssueStatus.safeParse(options.status);
101410
101541
  if (!result.success) throw new ValidationError(`Invalid status: ${options.status}`);
@@ -102384,7 +102515,7 @@ async function fetchWithGhFallback(url, options) {
102384
102515
  * See: docs/project/specs/active/plan-2026-01-26-configurable-doc-cache-sync.md
102385
102516
  */
102386
102517
  /** Prefix for internal bundled doc sources */
102387
- const INTERNAL_PREFIX = "internal:";
102518
+ const INTERNAL_SOURCE_PREFIX = "internal:";
102388
102519
  /**
102389
102520
  * Syncs documentation files from configured sources.
102390
102521
  *
@@ -102417,7 +102548,7 @@ var DocSync = class {
102417
102548
  * // => { type: 'url', location: 'https://...' }
102418
102549
  */
102419
102550
  parseSource(source) {
102420
- if (source.startsWith(INTERNAL_PREFIX)) return {
102551
+ if (source.startsWith(INTERNAL_SOURCE_PREFIX)) return {
102421
102552
  type: "internal",
102422
102553
  location: source.slice(9)
102423
102554
  };
@@ -102596,7 +102727,7 @@ async function generateDefaultDocCacheConfig() {
102596
102727
  const entries = await readdir(fullDir, { withFileTypes: true });
102597
102728
  for (const entry of entries) if (entry.isFile() && entry.name.endsWith(".md")) {
102598
102729
  const relativePath = `${prefix}/${entry.name}`;
102599
- config[relativePath] = `${INTERNAL_PREFIX}${relativePath}`;
102730
+ config[relativePath] = `${INTERNAL_SOURCE_PREFIX}${relativePath}`;
102600
102731
  }
102601
102732
  } catch {}
102602
102733
  }
@@ -102633,8 +102764,6 @@ function isDocsStale(lastSyncAt, autoSyncHours) {
102633
102764
  const lastSync = new Date(lastSyncAt).getTime();
102634
102765
  return (Date.now() - lastSync) / (1e3 * 60 * 60) >= autoSyncHours;
102635
102766
  }
102636
- /** Prefix for internal bundled doc sources */
102637
- const INTERNAL_SOURCE_PREFIX = "internal:";
102638
102767
  /**
102639
102768
  * Check if an internal bundled doc exists.
102640
102769
  *
@@ -103459,6 +103588,19 @@ var SyncHandler = class extends BaseCommand {
103459
103588
  } catch {
103460
103589
  this.output.debug("Remote sync branch does not exist yet");
103461
103590
  }
103591
+ {
103592
+ const { access, writeFile } = await import("node:fs/promises");
103593
+ const attrPath = join(this.dataSyncDir, "mappings", ".gitattributes");
103594
+ try {
103595
+ await access(attrPath);
103596
+ } catch {
103597
+ await writeFile(attrPath, "ids.yml merge=union\n");
103598
+ await git("-C", worktreePath, "add", attrPath);
103599
+ try {
103600
+ await git("-C", worktreePath, "commit", "--no-verify", "-m", "chore: add merge=union for ids.yml");
103601
+ } catch {}
103602
+ }
103603
+ }
103462
103604
  if (behindCommits > 0) {
103463
103605
  let headBeforeMerge = "";
103464
103606
  try {
@@ -103503,7 +103645,8 @@ var SyncHandler = class extends BaseCommand {
103503
103645
  const remoteIdsContent = await git("show", `${remote}/${syncBranch}:${DATA_SYNC_DIR}/mappings/ids.yml`);
103504
103646
  if (remoteIdsContent) {
103505
103647
  conflictRemoteMapping = parseIdMappingFromYaml(remoteIdsContent);
103506
- const localMapping = await loadIdMapping(this.dataSyncDir);
103648
+ const { readFile } = await import("node:fs/promises");
103649
+ const localMapping = resolveIdMappingConflicts(await readFile(join(this.dataSyncDir, "mappings", "ids.yml"), "utf-8"));
103507
103650
  const mergedMapping = mergeIdMappings(localMapping, conflictRemoteMapping);
103508
103651
  await saveIdMapping(this.dataSyncDir, mergedMapping);
103509
103652
  this.output.debug(`Merged ID mappings: ${localMapping.shortToUlid.size} local + ${conflictRemoteMapping.shortToUlid.size} remote = ${mergedMapping.shortToUlid.size} total`);
@@ -104601,6 +104744,7 @@ var DoctorHandler = class extends BaseCommand {
104601
104744
  cwd = "";
104602
104745
  config = null;
104603
104746
  issues = [];
104747
+ invalidIssueFiles = [];
104604
104748
  async run(options) {
104605
104749
  const tbdRoot = await requireInit();
104606
104750
  this.cwd = tbdRoot;
@@ -104609,7 +104753,11 @@ var DoctorHandler = class extends BaseCommand {
104609
104753
  this.config = await readConfig(this.cwd);
104610
104754
  } catch {}
104611
104755
  try {
104612
- this.issues = await listIssues(this.dataSyncDir);
104756
+ this.invalidIssueFiles = [];
104757
+ this.issues = await listIssues(this.dataSyncDir, {
104758
+ warnOnInvalid: false,
104759
+ onInvalidIssue: (invalidIssue) => this.invalidIssueFiles.push(invalidIssue)
104760
+ });
104613
104761
  } catch {}
104614
104762
  const statusInfo = await this.gatherStatusInfo();
104615
104763
  const statsInfo = await this.gatherStatsInfo();
@@ -104619,16 +104767,21 @@ var DoctorHandler = class extends BaseCommand {
104619
104767
  healthChecks.push(await this.checkIssuesDirectory());
104620
104768
  healthChecks.push(this.checkOrphanedDependencies(this.issues));
104621
104769
  healthChecks.push(this.checkDuplicateIds(this.issues));
104770
+ healthChecks.push(await this.checkIdMappingConflicts(options.fix));
104622
104771
  healthChecks.push(await this.checkIdMappingDuplicates(options.fix));
104623
104772
  healthChecks.push(await this.checkTempFiles(options.fix));
104624
- healthChecks.push(this.checkIssueValidity(this.issues));
104773
+ healthChecks.push(this.checkIssueValidity(this.issues, this.invalidIssueFiles));
104625
104774
  healthChecks.push(await this.checkWorktree(options.fix));
104626
104775
  const dataLocationResult = await this.checkDataLocation(options.fix);
104627
104776
  healthChecks.push(dataLocationResult);
104628
104777
  if (dataLocationResult.status === "ok" && dataLocationResult.message?.includes("migrated")) {
104629
104778
  this.dataSyncDir = await resolveDataSyncDir(this.cwd);
104630
104779
  try {
104631
- this.issues = await listIssues(this.dataSyncDir);
104780
+ this.invalidIssueFiles = [];
104781
+ this.issues = await listIssues(this.dataSyncDir, {
104782
+ warnOnInvalid: false,
104783
+ onInvalidIssue: (invalidIssue) => this.invalidIssueFiles.push(invalidIssue)
104784
+ });
104632
104785
  } catch {}
104633
104786
  }
104634
104787
  const parsedMaxHistory = options.maxHistory ? parseInt(options.maxHistory, 10) : 50;
@@ -104831,6 +104984,58 @@ var DoctorHandler = class extends BaseCommand {
104831
104984
  };
104832
104985
  }
104833
104986
  /**
104987
+ * Check 5b: Merge conflict markers in ids.yml.
104988
+ *
104989
+ * After a failed git merge during sync, ids.yml may retain unresolved
104990
+ * conflict markers (<<<<<<< / ======= / >>>>>>>). This blocks all tbd
104991
+ * commands since YAML parsing throws MergeConflictError.
104992
+ *
104993
+ * For ids.yml, both sides are simple key-value pairs that are append-only,
104994
+ * so the resolution is trivial: keep all entries from both sides.
104995
+ *
104996
+ * With --fix, extracts both sides, merges them, and re-saves.
104997
+ */
104998
+ async checkIdMappingConflicts(fix) {
104999
+ const mappingPath = join(this.dataSyncDir, "mappings", "ids.yml");
105000
+ let content;
105001
+ try {
105002
+ content = await readFile(mappingPath, "utf-8");
105003
+ } catch {
105004
+ return {
105005
+ name: "ID mapping conflicts",
105006
+ status: "ok"
105007
+ };
105008
+ }
105009
+ const { hasMergeConflictMarkers } = await Promise.resolve().then(() => yaml_utils_exports);
105010
+ if (!hasMergeConflictMarkers(content)) return {
105011
+ name: "ID mapping conflicts",
105012
+ status: "ok"
105013
+ };
105014
+ if (fix && !this.checkDryRun("Resolve merge conflicts in ids.yml")) try {
105015
+ const { resolveIdMappingConflicts, saveIdMapping } = await Promise.resolve().then(() => id_mapping_exports);
105016
+ const resolved = resolveIdMappingConflicts(content);
105017
+ await saveIdMapping(this.dataSyncDir, resolved);
105018
+ return {
105019
+ name: "ID mapping conflicts",
105020
+ status: "ok",
105021
+ message: `resolved merge conflicts (${resolved.shortToUlid.size} entries)`
105022
+ };
105023
+ } catch (error) {
105024
+ return {
105025
+ name: "ID mapping conflicts",
105026
+ status: "error",
105027
+ message: `failed to resolve conflicts: ${error instanceof Error ? error.message : String(error)}`
105028
+ };
105029
+ }
105030
+ return {
105031
+ name: "ID mapping conflicts",
105032
+ status: "error",
105033
+ message: "ids.yml contains unresolved merge conflict markers",
105034
+ fixable: true,
105035
+ suggestion: "Run: tbd doctor --fix to auto-resolve"
105036
+ };
105037
+ }
105038
+ /**
104834
105039
  * Check for duplicate keys in the ID mapping file (ids.yml).
104835
105040
  *
104836
105041
  * After a git merge conflict resolution that keeps entries from both sides,
@@ -104919,8 +105124,12 @@ var DoctorHandler = class extends BaseCommand {
104919
105124
  suggestion: "Run: tbd doctor --fix"
104920
105125
  };
104921
105126
  }
104922
- checkIssueValidity(issues) {
105127
+ checkIssueValidity(issues, invalidIssueFiles) {
104923
105128
  const invalid = [];
105129
+ for (const invalidIssueFile of invalidIssueFiles) invalid.push({
105130
+ id: invalidIssueFile.file,
105131
+ reason: invalidIssueFile.reason
105132
+ });
104924
105133
  for (const issue of issues) {
104925
105134
  const issueId = issue.id ?? "unknown";
104926
105135
  if (!issue.id) {
@@ -104970,7 +105179,8 @@ var DoctorHandler = class extends BaseCommand {
104970
105179
  return {
104971
105180
  name: "Issue validity",
104972
105181
  status: "error",
104973
- message: `${invalid.length} invalid issue(s)`,
105182
+ message: `${invalid.length} invalid issue file(s)`,
105183
+ path: join(CONFIG_DIR, "issues"),
104974
105184
  details: invalid.map((i) => `${i.id}: ${i.reason}`),
104975
105185
  suggestion: "Manually fix or delete invalid issue files"
104976
105186
  };