get-tbd 0.1.13 → 0.1.15

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 (57) hide show
  1. package/README.md +47 -28
  2. package/dist/bin.mjs +410 -170
  3. package/dist/bin.mjs.map +1 -1
  4. package/dist/cli.mjs +202 -94
  5. package/dist/cli.mjs.map +1 -1
  6. package/dist/docs/README.md +47 -28
  7. package/dist/docs/SKILL.md +61 -18
  8. package/dist/docs/guidelines/bun-monorepo-patterns.md +2096 -0
  9. package/dist/docs/guidelines/cli-agent-skill-patterns.md +79 -5
  10. package/dist/docs/guidelines/error-handling-rules.md +66 -0
  11. package/dist/docs/guidelines/pnpm-monorepo-patterns.md +2868 -0
  12. package/dist/docs/guidelines/release-notes-guidelines.md +140 -0
  13. package/dist/docs/guidelines/{sync-troubleshooting.md → tbd-sync-troubleshooting.md} +1 -1
  14. package/dist/docs/guidelines/typescript-sorting-patterns.md +234 -0
  15. package/dist/docs/guidelines/typescript-yaml-handling-rules.md +195 -0
  16. package/dist/docs/install/claude-header.md +13 -6
  17. package/dist/docs/shortcuts/standard/agent-handoff.md +1 -0
  18. package/dist/docs/shortcuts/standard/checkout-third-party-repo.md +50 -0
  19. package/dist/docs/shortcuts/standard/{cleanup-all.md → code-cleanup-all.md} +3 -2
  20. package/dist/docs/shortcuts/standard/{cleanup-update-docstrings.md → code-cleanup-docstrings.md} +1 -0
  21. package/dist/docs/shortcuts/standard/{cleanup-remove-trivial-tests.md → code-cleanup-tests.md} +1 -0
  22. package/dist/docs/shortcuts/standard/{commit-code.md → code-review-and-commit.md} +1 -0
  23. package/dist/docs/shortcuts/standard/coding-spike.md +54 -0
  24. package/dist/docs/shortcuts/standard/create-or-update-pr-simple.md +1 -0
  25. package/dist/docs/shortcuts/standard/create-or-update-pr-with-validation-plan.md +1 -0
  26. package/dist/docs/shortcuts/standard/implement-beads.md +1 -0
  27. package/dist/docs/shortcuts/standard/merge-upstream.md +1 -0
  28. package/dist/docs/shortcuts/standard/new-architecture-doc.md +1 -0
  29. package/dist/docs/shortcuts/standard/new-guideline.md +8 -0
  30. package/dist/docs/shortcuts/standard/new-plan-spec.md +1 -0
  31. package/dist/docs/shortcuts/standard/new-research-brief.md +1 -0
  32. package/dist/docs/shortcuts/standard/new-shortcut.md +27 -1
  33. package/dist/docs/shortcuts/standard/new-validation-plan.md +1 -0
  34. package/dist/docs/shortcuts/standard/plan-implementation-with-beads.md +1 -0
  35. package/dist/docs/shortcuts/standard/precommit-process.md +1 -0
  36. package/dist/docs/shortcuts/standard/review-code-python.md +1 -0
  37. package/dist/docs/shortcuts/standard/review-code-typescript.md +1 -0
  38. package/dist/docs/shortcuts/standard/review-code.md +1 -0
  39. package/dist/docs/shortcuts/standard/review-github-pr.md +89 -17
  40. package/dist/docs/shortcuts/standard/revise-all-architecture-docs.md +1 -0
  41. package/dist/docs/shortcuts/standard/revise-architecture-doc.md +1 -0
  42. package/dist/docs/shortcuts/standard/setup-github-cli.md +1 -0
  43. package/dist/docs/shortcuts/standard/sync-failure-recovery.md +6 -53
  44. package/dist/docs/shortcuts/standard/update-specs-status.md +1 -0
  45. package/dist/docs/shortcuts/standard/welcome-user.md +2 -1
  46. package/dist/docs/shortcuts/system/skill-brief.md +1 -1
  47. package/dist/docs/shortcuts/system/skill.md +48 -12
  48. package/dist/docs/skill-brief.md +1 -1
  49. package/dist/docs/tbd-design.md +13 -1
  50. package/dist/index.d.mts +20 -6
  51. package/dist/index.mjs +2 -2
  52. package/dist/{src-BfhjLZXE.mjs → src-Ct16P2Ox.mjs} +154 -22
  53. package/dist/src-Ct16P2Ox.mjs.map +1 -0
  54. package/dist/tbd +410 -170
  55. package/package.json +1 -1
  56. package/dist/docs/guidelines/typescript-monorepo-patterns.md +0 -72
  57. package/dist/src-BfhjLZXE.mjs.map +0 -1
package/dist/bin.mjs CHANGED
@@ -6638,8 +6638,16 @@ const IssueId = stringType().regex(/^is-[0-9a-z]{26}$/);
6638
6638
  * Short ID: 1+ base36 characters used for external/display IDs.
6639
6639
  * Typically 4 chars for new IDs (e.g., a7k2, b3m9).
6640
6640
  * Imports may preserve longer numeric IDs (e.g., "100" from "tbd-100").
6641
+ * Legacy imports may include dots (e.g., "208.1" from hierarchical numbering).
6642
+ * Imports may include dashes/underscores (e.g., "stat-in_progress" from JSONL).
6641
6643
  */
6642
- const ShortId = stringType().regex(/^[0-9a-z]+$/);
6644
+ const ShortId = stringType().regex(/^[0-9a-z._-]+$/);
6645
+ /**
6646
+ * ULID: 26 lowercase alphanumeric characters.
6647
+ * Used in internal IDs and ID mappings.
6648
+ * Example: 01hx5zzkbkactav9wevgemmvrz
6649
+ */
6650
+ const Ulid = stringType().regex(/^[0-9a-z]{26}$/);
6643
6651
  /**
6644
6652
  * External Issue ID input: accepts {prefix}-{short} or just {short}.
6645
6653
  * Examples: bd-a7k2, a7k2, bd-100, 100
@@ -6744,15 +6752,15 @@ const GitRemoteName = stringType().min(1).max(255).regex(/^[a-zA-Z0-9._-]+$/, "I
6744
6752
  /**
6745
6753
  * Doc cache configuration - maps destination paths to source locations.
6746
6754
  *
6747
- * Keys are destination paths relative to .tbd/docs/ (e.g., "shortcuts/standard/commit-code.md")
6755
+ * Keys are destination paths relative to .tbd/docs/ (e.g., "shortcuts/standard/code-review-and-commit.md")
6748
6756
  * Values are source locations:
6749
- * - internal: prefix for bundled docs (e.g., "internal:shortcuts/standard/commit-code.md")
6757
+ * - internal: prefix for bundled docs (e.g., "internal:shortcuts/standard/code-review-and-commit.md")
6750
6758
  * - Full URL for external docs (e.g., "https://raw.githubusercontent.com/org/repo/main/file.md")
6751
6759
  *
6752
6760
  * Example:
6753
6761
  * ```yaml
6754
6762
  * doc_cache:
6755
- * shortcuts/standard/commit-code.md: internal:shortcuts/standard/commit-code.md
6763
+ * shortcuts/standard/code-review-and-commit.md: internal:shortcuts/standard/code-review-and-commit.md
6756
6764
  * shortcuts/custom/my-shortcut.md: https://raw.githubusercontent.com/org/repo/main/shortcuts/my-shortcut.md
6757
6765
  * ```
6758
6766
  */
@@ -6835,77 +6843,12 @@ const AtticEntrySchema = objectType({
6835
6843
  remote_updated_at: Timestamp
6836
6844
  })
6837
6845
  });
6838
-
6839
- //#endregion
6840
- //#region src/utils/markdown-utils.ts
6841
6846
  /**
6842
- * Markdown utilities for processing markdown content.
6843
- *
6844
- * Uses gray-matter for consistent frontmatter parsing across the codebase.
6845
- */
6846
- /**
6847
- * Normalize line endings to LF.
6848
- */
6849
- function normalizeLineEndings(content) {
6850
- return content.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
6851
- }
6852
- /**
6853
- * Parse markdown content into frontmatter and body.
6854
- * Handles both LF and CRLF line endings.
6855
- *
6856
- * @returns Object with frontmatter (null if none) and body
6847
+ * ID mapping YAML file schema for ids.yml.
6848
+ * Maps short IDs to ULIDs.
6849
+ * Format: { "a7k2": "01hx5zzkbkactav9wevgemmvrz", ... }
6857
6850
  */
6858
- function parseMarkdown(content) {
6859
- const normalized = normalizeLineEndings(content);
6860
- if (!matter.test(normalized)) return {
6861
- frontmatter: null,
6862
- body: content
6863
- };
6864
- try {
6865
- const parsed = matter(normalized);
6866
- const data = parsed.data;
6867
- let frontmatter = null;
6868
- if (data && Object.keys(data).length > 0) {
6869
- const lines = [];
6870
- for (const [key, value] of Object.entries(data)) if (Array.isArray(value)) {
6871
- lines.push(`${key}:`);
6872
- for (const item of value) lines.push(` - ${String(item)}`);
6873
- } else if (typeof value === "object" && value !== null) {
6874
- lines.push(`${key}:`);
6875
- for (const [subKey, subValue] of Object.entries(value)) lines.push(` ${subKey}: ${String(subValue)}`);
6876
- } else lines.push(`${key}: ${String(value)}`);
6877
- frontmatter = lines.join("\n");
6878
- } else frontmatter = "";
6879
- const body = parsed.content.replace(/^\n+/, "");
6880
- return {
6881
- frontmatter,
6882
- body
6883
- };
6884
- } catch {
6885
- return {
6886
- frontmatter: null,
6887
- body: content
6888
- };
6889
- }
6890
- }
6891
- /**
6892
- * Strip YAML frontmatter from markdown content.
6893
- * Returns the body content without frontmatter, with leading newlines trimmed.
6894
- * Handles both LF and CRLF line endings.
6895
- */
6896
- function stripFrontmatter(content) {
6897
- return parseMarkdown(content).body;
6898
- }
6899
- /**
6900
- * Insert content after YAML frontmatter.
6901
- * If no frontmatter exists, prepends the content.
6902
- * Content is inserted directly after ---. Include leading newlines in toInsert if needed.
6903
- */
6904
- function insertAfterFrontmatter(content, toInsert) {
6905
- const { frontmatter, body } = parseMarkdown(content);
6906
- if (frontmatter === null) return toInsert + content;
6907
- return `${frontmatter ? `---\n${frontmatter}\n---` : "---\n---"}\n${toInsert}\n\n${body}`;
6908
- }
6851
+ const IdMappingYamlSchema = recordType(ShortId, Ulid);
6909
6852
 
6910
6853
  //#endregion
6911
6854
  //#region ../../node_modules/.pnpm/yaml@2.8.2/node_modules/yaml/dist/nodes/identity.js
@@ -13557,6 +13500,197 @@ var require_dist$2 = /* @__PURE__ */ __commonJSMin(((exports) => {
13557
13500
  exports.visitAsync = visit.visitAsync;
13558
13501
  }));
13559
13502
 
13503
+ //#endregion
13504
+ //#region src/lib/settings.ts
13505
+ var import_dist$2 = require_dist$2();
13506
+ /**
13507
+ * Default line width for YAML serialization.
13508
+ * 88 characters is a good balance between readability and avoiding excessive wrapping.
13509
+ * (Matches Python's Black formatter default.)
13510
+ */
13511
+ const YAML_LINE_WIDTH = 88;
13512
+ /**
13513
+ * Default string type for YAML serialization.
13514
+ * 'PLAIN' means no forced quoting - YAML only quotes when necessary.
13515
+ */
13516
+ const YAML_DEFAULT_STRING_TYPE = "PLAIN";
13517
+ /**
13518
+ * Default key type for YAML serialization.
13519
+ * 'PLAIN' means object keys are unquoted unless required.
13520
+ */
13521
+ const YAML_DEFAULT_KEY_TYPE = "PLAIN";
13522
+ /**
13523
+ * Default YAML serialization options for readable output.
13524
+ *
13525
+ * Design principles:
13526
+ * - No forced quoting: YAML only quotes when necessary for special characters
13527
+ * - Reasonable line wrapping: 88 chars prevents overly long lines
13528
+ * - Plain keys: No unnecessary quotes around object keys
13529
+ * - Sorted keys: Deterministic output for diffs and version control
13530
+ */
13531
+ const YAML_STRINGIFY_OPTIONS = {
13532
+ lineWidth: YAML_LINE_WIDTH,
13533
+ defaultStringType: YAML_DEFAULT_STRING_TYPE,
13534
+ defaultKeyType: YAML_DEFAULT_KEY_TYPE,
13535
+ sortMapEntries: true
13536
+ };
13537
+ /**
13538
+ * YAML serialization options for compact output (e.g., frontmatter).
13539
+ * Uses lineWidth: 0 to prevent wrapping within values.
13540
+ */
13541
+ const YAML_STRINGIFY_OPTIONS_COMPACT = {
13542
+ lineWidth: 0,
13543
+ defaultStringType: YAML_DEFAULT_STRING_TYPE,
13544
+ defaultKeyType: YAML_DEFAULT_KEY_TYPE,
13545
+ sortMapEntries: true
13546
+ };
13547
+
13548
+ //#endregion
13549
+ //#region src/utils/yaml-utils.ts
13550
+ /**
13551
+ * YAML utility functions.
13552
+ *
13553
+ * Provides centralized YAML parsing and serialization with:
13554
+ * - Merge conflict detection for user-editable files
13555
+ * - Consistent, readable formatting defaults
13556
+ * - Proper handling of special characters (colons, quotes, etc.)
13557
+ *
13558
+ * IMPORTANT: Always use these utilities instead of raw yaml package functions.
13559
+ * This ensures consistent formatting and proper error handling across the codebase.
13560
+ */
13561
+ /**
13562
+ * Serialize data to YAML with readable formatting.
13563
+ *
13564
+ * Uses consistent defaults:
13565
+ * - No forced quoting (YAML only quotes when necessary)
13566
+ * - lineWidth of 88 provides reasonable wrapping for long strings
13567
+ * - Plain keys without quotes
13568
+ * - Sorted keys for deterministic output
13569
+ *
13570
+ * @param data - Data to serialize
13571
+ * @param options - Optional overrides for default options
13572
+ * @returns YAML string
13573
+ */
13574
+ function stringifyYaml(data, options) {
13575
+ return (0, import_dist$2.stringify)(data, {
13576
+ ...YAML_STRINGIFY_OPTIONS,
13577
+ ...options
13578
+ });
13579
+ }
13580
+ /**
13581
+ * Serialize data to YAML without line wrapping (compact mode).
13582
+ * Useful for frontmatter where values should stay on single lines.
13583
+ *
13584
+ * @param data - Data to serialize
13585
+ * @returns YAML string with no line wrapping
13586
+ */
13587
+ function stringifyYamlCompact(data) {
13588
+ return (0, import_dist$2.stringify)(data, YAML_STRINGIFY_OPTIONS_COMPACT);
13589
+ }
13590
+ /**
13591
+ * Error thrown when YAML content contains unresolved merge conflict markers.
13592
+ */
13593
+ var MergeConflictError = class extends Error {
13594
+ constructor(message, filePath) {
13595
+ super(message);
13596
+ this.filePath = filePath;
13597
+ this.name = "MergeConflictError";
13598
+ }
13599
+ };
13600
+ /**
13601
+ * Regex patterns for git merge conflict markers.
13602
+ */
13603
+ const CONFLICT_PATTERNS = {
13604
+ start: /^<<<<<<< /m,
13605
+ separator: /^=======/m,
13606
+ end: /^>>>>>>> /m
13607
+ };
13608
+ /**
13609
+ * Check if content contains git merge conflict markers.
13610
+ */
13611
+ function hasMergeConflictMarkers(content) {
13612
+ return CONFLICT_PATTERNS.start.test(content) || CONFLICT_PATTERNS.separator.test(content) || CONFLICT_PATTERNS.end.test(content);
13613
+ }
13614
+ /**
13615
+ * Parse YAML content with merge conflict detection.
13616
+ *
13617
+ * If the content contains merge conflict markers, throws a MergeConflictError
13618
+ * with a helpful message instead of a cryptic YAML parse error.
13619
+ *
13620
+ * @param content - The YAML content to parse
13621
+ * @param filePath - Optional file path for error messages
13622
+ * @returns Parsed YAML data
13623
+ * @throws MergeConflictError if content has conflict markers
13624
+ * @throws Error if YAML is invalid for other reasons
13625
+ */
13626
+ function parseYamlWithConflictDetection(content, filePath) {
13627
+ if (hasMergeConflictMarkers(content)) throw new MergeConflictError(`File${filePath ? ` in ${filePath}` : ""} contains unresolved git merge conflict markers.\nThis usually happens when 'tbd sync' encountered conflicts that weren't properly resolved.\nTo fix: manually edit the file to resolve conflicts, or run 'tbd doctor --fix'.`, filePath);
13628
+ return (0, import_dist$2.parse)(content);
13629
+ }
13630
+
13631
+ //#endregion
13632
+ //#region src/utils/markdown-utils.ts
13633
+ /**
13634
+ * Markdown utilities for processing markdown content.
13635
+ *
13636
+ * Uses gray-matter for parsing and centralized yaml-utils for stringify to ensure
13637
+ * proper handling of special YAML characters (colons, quotes, etc.).
13638
+ */
13639
+ /**
13640
+ * Normalize line endings to LF.
13641
+ */
13642
+ function normalizeLineEndings(content) {
13643
+ return content.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
13644
+ }
13645
+ /**
13646
+ * Parse markdown content into frontmatter and body.
13647
+ * Handles both LF and CRLF line endings.
13648
+ *
13649
+ * @returns Object with frontmatter (null if none) and body
13650
+ */
13651
+ function parseMarkdown(content) {
13652
+ const normalized = normalizeLineEndings(content);
13653
+ if (!matter.test(normalized)) return {
13654
+ frontmatter: null,
13655
+ body: content
13656
+ };
13657
+ try {
13658
+ const parsed = matter(normalized);
13659
+ const data = parsed.data;
13660
+ let frontmatter = null;
13661
+ if (data && Object.keys(data).length > 0) frontmatter = stringifyYamlCompact(data).trimEnd();
13662
+ else frontmatter = "";
13663
+ const body = parsed.content.replace(/^\n+/, "");
13664
+ return {
13665
+ frontmatter,
13666
+ body
13667
+ };
13668
+ } catch {
13669
+ return {
13670
+ frontmatter: null,
13671
+ body: content
13672
+ };
13673
+ }
13674
+ }
13675
+ /**
13676
+ * Strip YAML frontmatter from markdown content.
13677
+ * Returns the body content without frontmatter, with leading newlines trimmed.
13678
+ * Handles both LF and CRLF line endings.
13679
+ */
13680
+ function stripFrontmatter(content) {
13681
+ return parseMarkdown(content).body;
13682
+ }
13683
+ /**
13684
+ * Insert content after YAML frontmatter.
13685
+ * If no frontmatter exists, prepends the content.
13686
+ * Content is inserted directly after ---. Include leading newlines in toInsert if needed.
13687
+ */
13688
+ function insertAfterFrontmatter(content, toInsert) {
13689
+ const { frontmatter, body } = parseMarkdown(content);
13690
+ if (frontmatter === null) return toInsert + content;
13691
+ return `${frontmatter ? `---\n${frontmatter}\n---` : "---\n---"}\n${toInsert}\n\n${body}`;
13692
+ }
13693
+
13560
13694
  //#endregion
13561
13695
  //#region src/file/parser.ts
13562
13696
  /**
@@ -13577,14 +13711,13 @@ var require_dist$2 = /* @__PURE__ */ __commonJSMin(((exports) => {
13577
13711
  *
13578
13712
  * See: tbd-design.md §2.1 Markdown + YAML Front Matter Format
13579
13713
  */
13580
- var import_dist$2 = require_dist$2();
13581
13714
  /**
13582
13715
  * gray-matter options using the 'yaml' package as engine.
13583
13716
  * This preserves date strings instead of converting them to Date objects.
13584
13717
  */
13585
13718
  const matterOptions = { engines: { yaml: {
13586
13719
  parse: (str) => (0, import_dist$2.parse)(str),
13587
- stringify: (obj) => (0, import_dist$2.stringify)(obj)
13720
+ stringify: (obj) => stringifyYaml(obj)
13588
13721
  } } };
13589
13722
  /**
13590
13723
  * Parse a Markdown file with YAML front matter.
@@ -13641,8 +13774,7 @@ function serializeIssue(issue) {
13641
13774
  for (const key of Object.keys(metadata).sort()) sortedMetadata[key] = metadata[key];
13642
13775
  const parts = [
13643
13776
  "---",
13644
- (0, import_dist$2.stringify)(sortedMetadata, {
13645
- sortMapEntries: true,
13777
+ stringifyYaml(sortedMetadata, {
13646
13778
  lineWidth: 0,
13647
13779
  nullStr: "null"
13648
13780
  }).trim(),
@@ -13664,7 +13796,7 @@ function serializeIssue(issue) {
13664
13796
  * Package version, derived from git at build time.
13665
13797
  * Format: X.Y.Z for releases, X.Y.Z-dev.N.hash for dev builds.
13666
13798
  */
13667
- const VERSION$1 = "0.1.13";
13799
+ const VERSION$1 = "0.1.15";
13668
13800
 
13669
13801
  //#endregion
13670
13802
  //#region src/cli/lib/version.ts
@@ -97476,11 +97608,8 @@ async function readConfigWithMigration(baseDir) {
97476
97608
  */
97477
97609
  async function writeConfig(baseDir, config) {
97478
97610
  const configPath = join(baseDir, CONFIG_FILE);
97479
- let content = (0, import_dist$2.stringify)(config, {
97480
- sortMapEntries: true,
97481
- lineWidth: 0
97482
- });
97483
- if (config.docs_cache && Object.keys(config.docs_cache).length > 0) content = content.replace("docs_cache:", "# Documentation cache configuration.\n# files: Maps destination paths (relative to .tbd/docs/) to source locations.\n# Sources can be:\n# - internal: prefix for bundled docs (e.g., \"internal:shortcuts/standard/commit-code.md\")\n# - Full URL for external docs (e.g., \"https://raw.githubusercontent.com/org/repo/main/file.md\")\n# lookup_path: Search paths for doc lookup (like shell $PATH). Earlier paths take precedence.\n#\n# To sync docs: tbd sync --docs\n# To check status: tbd sync --status\n#\n# Auto-sync: Docs are automatically synced when stale (default: every 24 hours).\n# Configure with settings.doc_auto_sync_hours (0 = disabled).\ndocs_cache:");
97611
+ let content = stringifyYaml(config, { lineWidth: 0 });
97612
+ if (config.docs_cache && Object.keys(config.docs_cache).length > 0) content = content.replace("docs_cache:", "# Documentation cache configuration.\n# files: Maps destination paths (relative to .tbd/docs/) to source locations.\n# Sources can be:\n# - internal: prefix for bundled docs (e.g., \"internal:shortcuts/standard/code-review-and-commit.md\")\n# - Full URL for external docs (e.g., \"https://raw.githubusercontent.com/org/repo/main/file.md\")\n# lookup_path: Search paths for doc lookup (like shell $PATH). Earlier paths take precedence.\n#\n# To sync docs: tbd sync --docs\n# To check status: tbd sync --status\n#\n# Auto-sync: Docs are automatically synced when stale (default: every 24 hours).\n# Configure with settings.doc_auto_sync_hours (0 = disabled).\ndocs_cache:");
97484
97613
  await writeFile(configPath, content);
97485
97614
  }
97486
97615
  /**
@@ -97539,10 +97668,7 @@ async function readLocalState(baseDir) {
97539
97668
  async function writeLocalState(baseDir, state) {
97540
97669
  const statePath = join(baseDir, STATE_FILE);
97541
97670
  await mkdir(join(baseDir, ".tbd"), { recursive: true });
97542
- await writeFile(statePath, (0, import_dist$2.stringify)(state, {
97543
- sortMapEntries: true,
97544
- lineWidth: 0
97545
- }));
97671
+ await writeFile(statePath, stringifyYaml(state, { lineWidth: 0 }));
97546
97672
  }
97547
97673
  /**
97548
97674
  * Update specific fields in local state (merge with existing).
@@ -97799,6 +97925,67 @@ async function ensureGitignorePatterns(gitignorePath, patterns, header) {
97799
97925
  };
97800
97926
  }
97801
97927
 
97928
+ //#endregion
97929
+ //#region src/cli/lib/prefix-detection.ts
97930
+ /**
97931
+ * Prefix validation and beads prefix extraction module.
97932
+ *
97933
+ * Provides functions to validate prefixes and extract prefix from beads config.
97934
+ * Used by setup commands to validate user-provided prefixes and migrate from beads.
97935
+ */
97936
+ /** Maximum length for a valid prefix */
97937
+ const MAX_PREFIX_LENGTH = 20;
97938
+ /** Minimum length for a valid prefix */
97939
+ const MIN_PREFIX_LENGTH = 1;
97940
+ /** Recommended minimum length */
97941
+ const RECOMMENDED_MIN_LENGTH = 2;
97942
+ /** Recommended maximum length */
97943
+ const RECOMMENDED_MAX_LENGTH = 8;
97944
+ /**
97945
+ * Check if a prefix is valid (hard rules, always enforced).
97946
+ * - Must be 1-20 characters
97947
+ * - Must start with a letter (a-z)
97948
+ * - Must end with alphanumeric (a-z0-9)
97949
+ * - Middle characters can be alphanumeric, dot, or underscore
97950
+ * - No dashes allowed (breaks ID syntax)
97951
+ */
97952
+ function isValidPrefix(s) {
97953
+ if (!s) return false;
97954
+ if (s.length < MIN_PREFIX_LENGTH || s.length > MAX_PREFIX_LENGTH) return false;
97955
+ if (!/^[a-z]/.test(s)) return false;
97956
+ if (s.length > 1 && !/[a-z0-9]$/.test(s)) return false;
97957
+ return /^[a-z][a-z0-9._]*$/.test(s);
97958
+ }
97959
+ /**
97960
+ * Check if a prefix follows recommended format (soft rules).
97961
+ * - Must be 2-8 characters
97962
+ * - Must be alphabetic only (a-z)
97963
+ *
97964
+ * Prefixes that don't match this can still be used with --force.
97965
+ */
97966
+ function isRecommendedPrefix(s) {
97967
+ if (!s) return false;
97968
+ if (s.length < RECOMMENDED_MIN_LENGTH || s.length > RECOMMENDED_MAX_LENGTH) return false;
97969
+ return /^[a-z]+$/.test(s);
97970
+ }
97971
+ /**
97972
+ * Get prefix from existing beads config.
97973
+ *
97974
+ * Looks for .beads/config.yaml and extracts display.id_prefix
97975
+ *
97976
+ * @param cwd Current working directory
97977
+ * @returns The beads prefix, or null if not found
97978
+ */
97979
+ async function getBeadsPrefix(cwd) {
97980
+ try {
97981
+ const prefix = ((0, import_dist$2.parse)(await readFile(join(cwd, ".beads", "config.yaml"), "utf-8"))?.display)?.id_prefix;
97982
+ if (typeof prefix === "string" && isValidPrefix(prefix)) return prefix;
97983
+ return null;
97984
+ } catch {
97985
+ return null;
97986
+ }
97987
+ }
97988
+
97802
97989
  //#endregion
97803
97990
  //#region src/utils/time-utils.ts
97804
97991
  /**
@@ -98644,7 +98831,15 @@ var InitHandler = class extends BaseCommand {
98644
98831
  } catch (error) {
98645
98832
  if (error instanceof CLIError) throw error;
98646
98833
  }
98647
- if (!options.prefix) throw new ValidationError("The --prefix option is required\n\nUsage: tbd init --prefix=<name>\n\nThe prefix is used for display IDs (e.g., proj-a7k2, myapp-b3m9)\nChoose a short 2-4 letter prefix for your project (e.g., tbd, myp).\n\nFor full setup with integrations: tbd setup --auto --prefix=<name>");
98834
+ if (!options.prefix) throw new ValidationError("The --prefix option is required\n\nUsage: tbd init --prefix=<name>\n\nThe prefix is used for display IDs (e.g., proj-a7k2, myapp-b3m9)\nChoose a short 2-8 letter prefix for your project (e.g., tbd, myp, proj).\n\nFor full setup with integrations: tbd setup --auto --prefix=<name>");
98835
+ const prefix = options.prefix;
98836
+ if (!isValidPrefix(prefix)) throw new ValidationError("Invalid prefix format.\nPrefix must be 1-20 lowercase characters:\n - Must start with a letter (a-z)\n - Must end with alphanumeric (a-z, 0-9)\n - Middle characters can include dots (.) and underscores (_)\n - No dashes allowed (breaks ID syntax)\n\nExample:\n tbd init --prefix=tbd");
98837
+ if (!isRecommendedPrefix(prefix) && !options.force) throw new ValidationError(`Prefix "${prefix}" is not recommended.\nRecommended prefixes are 2-8 alphabetic characters (e.g., "tbd", "myp", "proj").
98838
+
98839
+ If you really want to use this prefix, add --force to override.
98840
+
98841
+ Example:
98842
+ tbd init --prefix=${prefix} --force`);
98648
98843
  if (this.checkDryRun("Would initialize tbd repository", options)) return;
98649
98844
  await this.execute(async () => {
98650
98845
  await initConfig(cwd, VERSION, options.prefix);
@@ -98708,7 +98903,7 @@ var InitHandler = class extends BaseCommand {
98708
98903
  });
98709
98904
  }
98710
98905
  };
98711
- const initCommand = new Command("init").description("Initialize tbd in a git repository").option("--prefix <name>", "Project prefix for display IDs (e.g., \"proj\", \"myapp\")").option("--sync-branch <name>", "Sync branch name (default: tbd-sync)").option("--remote <name>", "Remote name (default: origin)").action(async (options, command) => {
98906
+ const initCommand = new Command("init").description("Initialize tbd in a git repository").option("--prefix <name>", "Project prefix for display IDs (2-8 alphabetic recommended)").option("--force", "Allow non-recommended prefix format").option("--sync-branch <name>", "Sync branch name (default: tbd-sync)").option("--remote <name>", "Remote name (default: origin)").action(async (options, command) => {
98712
98907
  await new InitHandler(command).run(options);
98713
98908
  });
98714
98909
 
@@ -99174,7 +99369,10 @@ async function loadIdMapping(baseDir) {
99174
99369
  ulidToShort: /* @__PURE__ */ new Map()
99175
99370
  };
99176
99371
  }
99177
- const data = (0, import_dist$2.parse)(content) || {};
99372
+ const rawData = parseYamlWithConflictDetection(content, filePath) ?? {};
99373
+ const parseResult = IdMappingYamlSchema.safeParse(rawData);
99374
+ if (!parseResult.success) throw new Error(`Invalid ID mapping format in ${filePath}: ${parseResult.error.message}`);
99375
+ const data = parseResult.data;
99178
99376
  const shortToUlid = /* @__PURE__ */ new Map();
99179
99377
  const ulidToShort = /* @__PURE__ */ new Map();
99180
99378
  for (const [shortId, ulid] of Object.entries(data)) {
@@ -99195,7 +99393,7 @@ async function saveIdMapping(baseDir, mapping) {
99195
99393
  const data = {};
99196
99394
  const sortedKeys = naturalSort(Array.from(mapping.shortToUlid.keys()));
99197
99395
  for (const key of sortedKeys) data[key] = mapping.shortToUlid.get(key);
99198
- await writeFile(filePath, (0, import_dist$2.stringify)(data));
99396
+ await writeFile(filePath, stringifyYaml(data));
99199
99397
  }
99200
99398
  /**
99201
99399
  * Calculate the optimal short ID length based on existing ID count.
@@ -99264,6 +99462,55 @@ function resolveToInternalId(input, mapping) {
99264
99462
  if (!ulid) throw new Error(`Unknown issue ID: ${input}. Short ID "${shortId}" not found in mapping.`);
99265
99463
  return makeInternalId(ulid);
99266
99464
  }
99465
+ /**
99466
+ * Parse an ID mapping from raw YAML content.
99467
+ * Used for loading mappings from git show output during conflict resolution.
99468
+ *
99469
+ * @throws MergeConflictError if content contains merge conflict markers
99470
+ */
99471
+ function parseIdMappingFromYaml(content) {
99472
+ const rawData = parseYamlWithConflictDetection(content) ?? {};
99473
+ const parseResult = IdMappingYamlSchema.safeParse(rawData);
99474
+ if (!parseResult.success) throw new Error(`Invalid ID mapping format: ${parseResult.error.message}`);
99475
+ const data = parseResult.data;
99476
+ const shortToUlid = /* @__PURE__ */ new Map();
99477
+ const ulidToShort = /* @__PURE__ */ new Map();
99478
+ for (const [shortId, ulid] of Object.entries(data)) {
99479
+ shortToUlid.set(shortId, ulid);
99480
+ ulidToShort.set(ulid, shortId);
99481
+ }
99482
+ return {
99483
+ shortToUlid,
99484
+ ulidToShort
99485
+ };
99486
+ }
99487
+ /**
99488
+ * Merge two ID mappings by combining all entries from both.
99489
+ * ID mappings are always additive (new IDs are only added, never removed),
99490
+ * so merging simply unions all key-value pairs.
99491
+ *
99492
+ * If the same short ID maps to different ULIDs in each mapping (a conflict),
99493
+ * the local mapping takes precedence (caller should log a warning).
99494
+ *
99495
+ * @param local - The local ID mapping
99496
+ * @param remote - The remote ID mapping
99497
+ * @returns Merged mapping with all entries from both
99498
+ */
99499
+ function mergeIdMappings(local, remote) {
99500
+ const merged = {
99501
+ shortToUlid: new Map(local.shortToUlid),
99502
+ ulidToShort: new Map(local.ulidToShort)
99503
+ };
99504
+ for (const [shortId, ulid] of remote.shortToUlid) if (!merged.shortToUlid.has(shortId)) {
99505
+ merged.shortToUlid.set(shortId, ulid);
99506
+ merged.ulidToShort.set(ulid, shortId);
99507
+ }
99508
+ for (const [ulid, shortId] of remote.ulidToShort) if (!merged.ulidToShort.has(ulid) && !merged.shortToUlid.has(shortId)) {
99509
+ merged.shortToUlid.set(shortId, ulid);
99510
+ merged.ulidToShort.set(ulid, shortId);
99511
+ }
99512
+ return merged;
99513
+ }
99267
99514
 
99268
99515
  //#endregion
99269
99516
  //#region src/lib/priority.ts
@@ -99472,8 +99719,9 @@ var CreateHandler = class extends BaseCommand {
99472
99719
  let description = options.description;
99473
99720
  if (options.file) try {
99474
99721
  description = await readFile(options.file, "utf-8");
99475
- } catch {
99476
- throw new CLIError(`Failed to read description from file: ${options.file}`);
99722
+ } catch (error) {
99723
+ const message = error instanceof Error ? error.message : String(error);
99724
+ throw new CLIError(`Failed to read description from file '${options.file}': ${message}`);
99477
99725
  }
99478
99726
  let specPath;
99479
99727
  if (options.spec) try {
@@ -100128,15 +100376,8 @@ function normalizePath(path) {
100128
100376
  */
100129
100377
  var ListHandler = class extends BaseCommand {
100130
100378
  async run(options) {
100131
- const tbdRoot = await requireInit();
100132
- let issues;
100133
- let dataCtx;
100134
- try {
100135
- dataCtx = await loadDataContext(tbdRoot);
100136
- issues = await listIssues(dataCtx.dataSyncDir);
100137
- } catch {
100138
- throw new CLIError("Failed to read issues");
100139
- }
100379
+ const dataCtx = await loadDataContext(await requireInit());
100380
+ let issues = await listIssues(dataCtx.dataSyncDir);
100140
100381
  issues = this.filterIssues(issues, options, dataCtx.mapping);
100141
100382
  issues = this.sortIssues(issues, options.sort ?? "priority", dataCtx.mapping);
100142
100383
  issues = applyLimit(issues, options.limit);
@@ -101449,8 +101690,8 @@ var DocSync = class {
101449
101690
  * Parse a source string into a DocSource.
101450
101691
  *
101451
101692
  * @example
101452
- * parseSource('internal:shortcuts/standard/commit-code.md')
101453
- * // => { type: 'internal', location: 'shortcuts/standard/commit-code.md' }
101693
+ * parseSource('internal:shortcuts/standard/code-review-and-commit.md')
101694
+ * // => { type: 'internal', location: 'shortcuts/standard/code-review-and-commit.md' }
101454
101695
  *
101455
101696
  * @example
101456
101697
  * parseSource('https://raw.githubusercontent.com/org/repo/main/file.md')
@@ -102136,7 +102377,24 @@ var SyncHandler = class extends BaseCommand {
102136
102377
  } catch {
102137
102378
  this.output.debug(`Issue ${localIssue.id} not on remote, keeping local`);
102138
102379
  }
102380
+ try {
102381
+ const remoteIdsContent = await git("show", `${remote}/${syncBranch}:${DATA_SYNC_DIR}/mappings/ids.yml`);
102382
+ if (remoteIdsContent) {
102383
+ const localMapping = await loadIdMapping(this.dataSyncDir);
102384
+ const remoteMapping = parseIdMappingFromYaml(remoteIdsContent);
102385
+ const mergedMapping = mergeIdMappings(localMapping, remoteMapping);
102386
+ await saveIdMapping(this.dataSyncDir, mergedMapping);
102387
+ this.output.debug(`Merged ID mappings: ${localMapping.shortToUlid.size} local + ${remoteMapping.shortToUlid.size} remote = ${mergedMapping.shortToUlid.size} total`);
102388
+ }
102389
+ } catch (error) {
102390
+ this.output.debug(`Could not merge ids.yml: ${error.message}`);
102391
+ }
102139
102392
  await git("-C", worktreePath, "add", "-A");
102393
+ const conflictCheck = await git("-C", worktreePath, "diff", "--cached", "-S<<<<<<< ", "--name-only");
102394
+ if (conflictCheck.trim()) {
102395
+ const conflictedFiles = conflictCheck.trim().split("\n");
102396
+ throw new SyncError(`Cannot commit: ${conflictedFiles.length} file(s) still have merge conflict markers:\n` + conflictedFiles.map((f) => ` - ${f}`).join("\n") + `\n\nThis is a bug in tbd sync. Please report it and manually resolve conflicts in:\n ${worktreePath}`);
102397
+ }
102140
102398
  try {
102141
102399
  await git("-C", worktreePath, "commit", "--no-verify", "-m", "tbd sync: resolved merge conflicts");
102142
102400
  } catch {
@@ -102282,7 +102540,7 @@ async function readState() {
102282
102540
  * Update local state file.
102283
102541
  */
102284
102542
  async function updateState(updates) {
102285
- await writeFile(STATE_FILE, (0, import_dist$2.stringify)({
102543
+ await writeFile(STATE_FILE, stringifyYaml({
102286
102544
  ...await readState(),
102287
102545
  ...updates
102288
102546
  }));
@@ -102751,7 +103009,7 @@ async function saveConflictToAttic(atticDir, conflict, winnerSource) {
102751
103009
  }
102752
103010
  };
102753
103011
  const safeTimestamp = timestamp.replace(/:/g, "-");
102754
- await writeFile(join(atticDir, `${conflict.issue_id}_${safeTimestamp}_${conflict.field}.yml`), (0, import_dist$2.stringify)(entry, { sortMapEntries: true }));
103012
+ await writeFile(join(atticDir, `${conflict.issue_id}_${safeTimestamp}_${conflict.field}.yml`), stringifyYaml(entry));
102755
103013
  }
102756
103014
  /**
102757
103015
  * Get the target/source directory for workspace operations.
@@ -104285,7 +104543,9 @@ async function listAtticEntries(filterById) {
104285
104543
  if (!parsed) continue;
104286
104544
  if (filterById && parsed.entityId !== filterById) continue;
104287
104545
  try {
104288
- const entry = (0, import_dist$2.parse)(await readFile(join(atticPath, file), "utf-8"));
104546
+ const filePath = join(atticPath, file);
104547
+ const rawData = parseYamlWithConflictDetection(await readFile(filePath, "utf-8"), filePath);
104548
+ const entry = AtticEntrySchema.parse(rawData);
104289
104549
  entries.push(entry);
104290
104550
  } catch {}
104291
104551
  }
@@ -104997,7 +105257,7 @@ var DocsHandler = class extends BaseCommand {
104997
105257
  console.log(colors.bold("Workflows (Shortcuts):"));
104998
105258
  console.log(" tbd shortcut --list List all available shortcuts");
104999
105259
  console.log(" tbd shortcut new-plan-spec Plan a new feature");
105000
- console.log(" tbd shortcut commit-code Commit code properly");
105260
+ console.log(" tbd shortcut code-review-and-commit Commit code properly");
105001
105261
  console.log(" tbd shortcut create-or-update-pr-simple Create a pull request");
105002
105262
  console.log("");
105003
105263
  console.log(colors.bold("Guidelines (Coding Standards):"));
@@ -105338,8 +105598,8 @@ var UninstallHandler = class extends BaseCommand {
105338
105598
  force: true
105339
105599
  });
105340
105600
  console.log(` ${colors.success("✓")} Removed .tbd directory`);
105341
- } catch {
105342
- throw new CLIError("Failed to remove .tbd directory");
105601
+ } catch (error) {
105602
+ throw new CLIError(`Failed to remove .tbd directory: ${error instanceof Error ? error.message : String(error)}`);
105343
105603
  }
105344
105604
  console.log("");
105345
105605
  this.output.success("tbd has been uninstalled from this repository.");
@@ -105521,6 +105781,7 @@ var DocCache = class {
105521
105781
  return {
105522
105782
  title: typeof parsed.title === "string" ? parsed.title : void 0,
105523
105783
  description: typeof parsed.description === "string" ? parsed.description : void 0,
105784
+ category: typeof parsed.category === "string" ? parsed.category : void 0,
105524
105785
  tags: Array.isArray(parsed.tags) ? parsed.tags.filter((t) => typeof t === "string") : void 0
105525
105786
  };
105526
105787
  } catch {
@@ -105649,7 +105910,7 @@ function buildTableRows(docs, skipNames = []) {
105649
105910
  * // ## Available Shortcuts
105650
105911
  * // | Name | Description |
105651
105912
  * // | --- | --- |
105652
- * // | commit-code | Run pre-commit checks, review changes, and commit code |
105913
+ * // | code-review-and-commit | Run pre-commit checks, review changes, and commit code |
105653
105914
  * // ...
105654
105915
  * // ## Available Guidelines
105655
105916
  * // | Name | Description |
@@ -106134,16 +106395,6 @@ async function addDoc(tbdRoot, options) {
106134
106395
  *
106135
106396
  * See: docs/project/specs/active/plan-2026-01-22-doc-cache-abstraction.md
106136
106397
  */
106137
- /**
106138
- * Infer category from shortcut name.
106139
- * Returns undefined if no category matches.
106140
- */
106141
- function inferCategory(name) {
106142
- if (name.includes("plan-spec") || name.includes("architecture") || name.includes("research") || name.includes("validation-spec") || name.includes("implementation-spec") || name.includes("beads-from-spec") || name.includes("update-spec") || name.includes("refine-spec") || name.includes("revise-")) return "planning";
106143
- if (name.includes("implement-") || name.includes("coding-spike")) return "implementation";
106144
- if (name.includes("review-") || name.includes("precommit") || name.includes("cleanup-") || name.includes("validation-plan")) return "quality";
106145
- if (name.includes("commit-") || name.includes("pr-") || name.includes("merge-")) return "shipping";
106146
- }
106147
106398
  var ShortcutHandler = class extends BaseCommand {
106148
106399
  async run(query, options) {
106149
106400
  await this.execute(async () => {
@@ -106201,13 +106452,14 @@ var ShortcutHandler = class extends BaseCommand {
106201
106452
  async handleList(cache, includeAll, category) {
106202
106453
  let docs = cache.list(includeAll);
106203
106454
  if (category) docs = docs.filter((d) => {
106204
- return inferCategory(d.name) === category;
106455
+ return d.frontmatter?.category === category;
106205
106456
  });
106206
106457
  if (this.ctx.json) {
106207
106458
  this.output.data(docs.map((d) => ({
106208
106459
  name: d.name,
106209
106460
  title: d.frontmatter?.title,
106210
106461
  description: d.frontmatter?.description,
106462
+ category: d.frontmatter?.category,
106211
106463
  path: d.path,
106212
106464
  sourceDir: d.sourceDir,
106213
106465
  sizeBytes: d.sizeBytes,
@@ -106361,7 +106613,7 @@ var ShortcutHandler = class extends BaseCommand {
106361
106613
  }
106362
106614
  }
106363
106615
  };
106364
- const shortcutCommand = new Command("shortcut").description("Find and output documentation shortcuts").argument("[query]", "Shortcut name or description to search for").option("--list", "List all available shortcuts").option("--all", "Include shadowed shortcuts (use with --list)").option("--category <category>", "Filter by category: planning, implementation, quality, shipping").option("--refresh", "Refresh the cached shortcut directory").option("--quiet", "Suppress output (use with --refresh)").option("--add <url>", "Add a shortcut from a URL").option("--name <name>", "Name for the added shortcut (required with --add)").action(async (query, options, command) => {
106616
+ const shortcutCommand = new Command("shortcut").description("Find and output documentation shortcuts").argument("[query]", "Shortcut name or description to search for").option("--list", "List all available shortcuts").option("--all", "Include shadowed shortcuts (use with --list)").option("--category <category>", "Filter by category: planning, documentation, review, git, cleanup, session, meta").option("--refresh", "Refresh the cached shortcut directory").option("--quiet", "Suppress output (use with --refresh)").option("--add <url>", "Add a shortcut from a URL").option("--name <name>", "Name for the added shortcut (required with --add)").action(async (query, options, command) => {
106365
106617
  await new ShortcutHandler(command).run(query, options);
106366
106618
  });
106367
106619
 
@@ -106728,47 +106980,6 @@ const templateCommand = new Command("template").description("Find and output doc
106728
106980
  await new TemplateHandler(command).run(query, options);
106729
106981
  });
106730
106982
 
106731
- //#endregion
106732
- //#region src/cli/lib/prefix-detection.ts
106733
- /**
106734
- * Prefix validation and beads prefix extraction module.
106735
- *
106736
- * Provides functions to validate prefixes and extract prefix from beads config.
106737
- * Used by setup commands to validate user-provided prefixes and migrate from beads.
106738
- */
106739
- /** Maximum length for a valid prefix */
106740
- const MAX_PREFIX_LENGTH = 10;
106741
- /** Minimum length for a valid prefix */
106742
- const MIN_PREFIX_LENGTH = 1;
106743
- /**
106744
- * Check if a prefix is valid.
106745
- * - Must be 1-10 characters
106746
- * - Must start with a letter
106747
- * - Must be alphanumeric only (lowercase)
106748
- */
106749
- function isValidPrefix(s) {
106750
- if (!s) return false;
106751
- if (s.length < MIN_PREFIX_LENGTH || s.length > MAX_PREFIX_LENGTH) return false;
106752
- return /^[a-z][a-z0-9]*$/.test(s);
106753
- }
106754
- /**
106755
- * Get prefix from existing beads config.
106756
- *
106757
- * Looks for .beads/config.yaml and extracts display.id_prefix
106758
- *
106759
- * @param cwd Current working directory
106760
- * @returns The beads prefix, or null if not found
106761
- */
106762
- async function getBeadsPrefix(cwd) {
106763
- try {
106764
- const prefix = ((0, import_dist$2.parse)(await readFile(join(cwd, ".beads", "config.yaml"), "utf-8"))?.display)?.id_prefix;
106765
- if (typeof prefix === "string" && isValidPrefix(prefix)) return prefix;
106766
- return null;
106767
- } catch {
106768
- return null;
106769
- }
106770
- }
106771
-
106772
106983
  //#endregion
106773
106984
  //#region src/cli/commands/setup.ts
106774
106985
  /**
@@ -107563,8 +107774,14 @@ var SetupDefaultHandler = class extends BaseCommand {
107563
107774
  }
107564
107775
  const beadsPrefix = await getBeadsPrefix(cwd);
107565
107776
  const prefix = options.prefix ?? beadsPrefix;
107566
- if (!prefix) throw new CLIError("Could not read prefix from beads config.\nPlease specify a prefix (2-4 letters recommended):\n tbd setup --auto --prefix=tbd");
107567
- if (!isValidPrefix(prefix)) throw new CLIError("Invalid prefix format.\nPrefix must be 1-10 lowercase alphanumeric characters, starting with a letter.\nRecommended: 2-4 letters for clear, readable issue IDs.\nPlease specify a valid prefix:\n tbd setup --auto --prefix=tbd");
107777
+ if (!prefix) throw new CLIError("Could not read prefix from beads config.\nPlease specify a prefix (2-8 letters recommended):\n tbd setup --auto --prefix=tbd");
107778
+ if (!isValidPrefix(prefix)) throw new CLIError("Invalid prefix format.\nPrefix must be 1-20 lowercase characters:\n - Must start with a letter (a-z)\n - Must end with alphanumeric (a-z, 0-9)\n - Middle characters can include dots (.) and underscores (_)\n - No dashes allowed (breaks ID syntax)\n\nPlease specify a valid prefix:\n tbd setup --from-beads --prefix=tbd");
107779
+ if (!(beadsPrefix && prefix === beadsPrefix) && !isRecommendedPrefix(prefix) && !options.force) throw new CLIError(`Prefix "${prefix}" is not recommended.\nRecommended prefixes are 2-8 alphabetic characters (e.g., "tbd", "myp", "proj").
107780
+
107781
+ If you really want to use this prefix, add --force to override.
107782
+
107783
+ Example:
107784
+ tbd setup --from-beads --prefix=${prefix} --force`);
107568
107785
  await this.initializeTbd(cwd, prefix);
107569
107786
  if (options.ghCli === false) {
107570
107787
  const config = await readConfig(cwd);
@@ -107603,7 +107820,13 @@ var SetupDefaultHandler = class extends BaseCommand {
107603
107820
  const colors = this.output.getColors();
107604
107821
  const prefix = options.prefix;
107605
107822
  if (!prefix) throw new CLIError("--prefix is required for tbd setup --auto\n\nThe --prefix flag specifies your project name for issue IDs.\nUse a short 2-4 letter prefix so issue IDs stand out clearly.\n\nExample:\n tbd setup --auto --prefix=tbd # Issues: tbd-a1b2\n tbd setup --auto --prefix=myp # Issues: myp-c3d4\n\nNote: If migrating from beads, the prefix is automatically read from your beads config.");
107606
- if (!isValidPrefix(prefix)) throw new CLIError("Invalid prefix format.\nPrefix must be 1-10 lowercase alphanumeric characters, starting with a letter.\nRecommended: 2-4 letters for clear, readable issue IDs.\n\nExample:\n tbd setup --auto --prefix=tbd");
107823
+ if (!isValidPrefix(prefix)) throw new CLIError("Invalid prefix format.\nPrefix must be 1-20 lowercase characters:\n - Must start with a letter (a-z)\n - Must end with alphanumeric (a-z, 0-9)\n - Middle characters can include dots (.) and underscores (_)\n - No dashes allowed (breaks ID syntax)\n\nExample:\n tbd setup --auto --prefix=tbd");
107824
+ if (!isRecommendedPrefix(prefix) && !options.force) throw new CLIError(`Prefix "${prefix}" is not recommended.\nRecommended prefixes are 2-8 alphabetic characters (e.g., "tbd", "myp", "proj").
107825
+
107826
+ If you really want to use this prefix, add --force to override.
107827
+
107828
+ Example:
107829
+ tbd setup --auto --prefix=${prefix} --force`);
107607
107830
  console.log(`Initializing with prefix "${prefix}"...`);
107608
107831
  await this.initializeTbd(cwd, prefix);
107609
107832
  if (options.ghCli === false) {
@@ -107876,7 +108099,7 @@ var SetupAutoHandler = class extends BaseCommand {
107876
108099
  return result;
107877
108100
  }
107878
108101
  };
107879
- const setupCommand = new Command("setup").description("Configure tbd integration with editors and tools").option("--auto", "Non-interactive mode with smart defaults (for agents/scripts)").option("--interactive", "Interactive mode with prompts (for humans)").option("--from-beads", "Migrate from Beads to tbd").option("--prefix <name>", "Project prefix for issue IDs (required for fresh setup)").option("--no-gh-cli", "Disable automatic GitHub CLI installation hook").action(async (options, command) => {
108102
+ const setupCommand = new Command("setup").description("Configure tbd integration with editors and tools").option("--auto", "Non-interactive mode with smart defaults (for agents/scripts)").option("--interactive", "Interactive mode with prompts (for humans)").option("--from-beads", "Migrate from Beads to tbd").option("--prefix <name>", "Project prefix for issue IDs (required for fresh setup)").option("--force", "Allow non-recommended prefix format (not 2-8 alphabetic)").option("--no-gh-cli", "Disable automatic GitHub CLI installation hook").action(async (options, command) => {
107880
108103
  if (options.auto || options.interactive) {
107881
108104
  await new SetupDefaultHandler(command).run(options);
107882
108105
  return;
@@ -107900,7 +108123,8 @@ const setupCommand = new Command("setup").description("Configure tbd integration
107900
108123
  console.log(" --from-beads Migrate from Beads to tbd (implies --auto)");
107901
108124
  console.log("");
107902
108125
  console.log("Options:");
107903
- console.log(" --prefix <name> Project prefix for issue IDs (e.g., \"tbd\", \"myapp\")");
108126
+ console.log(" --prefix <name> Project prefix for issue IDs (2-8 alphabetic recommended)");
108127
+ console.log(" --force Allow non-recommended prefix format");
107904
108128
  console.log(" --no-gh-cli Disable automatic GitHub CLI installation hook");
107905
108129
  console.log("");
107906
108130
  console.log("Examples:");
@@ -108102,15 +108326,31 @@ function isJsonMode() {
108102
108326
  return process.argv.includes("--json");
108103
108327
  }
108104
108328
  /**
108329
+ * Check if --debug flag is present in argv.
108330
+ */
108331
+ function isDebugMode() {
108332
+ return process.argv.includes("--debug");
108333
+ }
108334
+ /**
108105
108335
  * Output error in the appropriate format (JSON or text).
108336
+ * In debug mode, shows full error details and stack trace.
108106
108337
  */
108107
108338
  function outputError(message, error) {
108339
+ const debugMode = isDebugMode();
108108
108340
  if (isJsonMode()) {
108109
108341
  const errorObj = { error: message };
108110
108342
  if (error instanceof CLIError) errorObj.type = error.name;
108111
108343
  if (error && error.message !== message) errorObj.details = error.message;
108344
+ if (debugMode && error?.stack) errorObj.stack = error.stack;
108112
108345
  console.error(JSON.stringify(errorObj));
108113
- } else console.error(`Error: ${message}`);
108346
+ } else {
108347
+ console.error(`Error: ${message}`);
108348
+ if (debugMode && error?.stack) {
108349
+ console.error("");
108350
+ console.error("Stack trace:");
108351
+ console.error(error.stack);
108352
+ }
108353
+ }
108114
108354
  }
108115
108355
  /**
108116
108356
  * Check if running with no command (just options or nothing).