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/cli.mjs CHANGED
@@ -1,7 +1,7 @@
1
- import { S as LocalStateSchema, a as insertAfterFrontmatter, i as serializeIssue, l as ConfigSchema, n as parseIssue, o as stripFrontmatter, r as parseMarkdownWithFrontmatter, t as VERSION$1, x as IssueStatus, y as IssueKind } from "./src-BfhjLZXE.mjs";
1
+ import { S as IssueKind, T as LocalStateSchema, a as insertAfterFrontmatter, b as IdMappingYamlSchema, c as stringifyYaml, d as ConfigSchema, i as serializeIssue, l as AtticEntrySchema, n as parseIssue, o as stripFrontmatter, r as parseMarkdownWithFrontmatter, s as parseYamlWithConflictDetection, t as VERSION$1, w as IssueStatus } from "./src-Ct16P2Ox.mjs";
2
2
  import { createRequire } from "node:module";
3
3
  import matter from "gray-matter";
4
- import { parse, stringify } from "yaml";
4
+ import { parse } from "yaml";
5
5
  import { Command } from "commander";
6
6
  import pc from "picocolors";
7
7
  import { marked } from "marked";
@@ -963,11 +963,8 @@ async function readConfigWithMigration(baseDir) {
963
963
  */
964
964
  async function writeConfig(baseDir, config) {
965
965
  const configPath = join(baseDir, CONFIG_FILE);
966
- let content = stringify(config, {
967
- sortMapEntries: true,
968
- lineWidth: 0
969
- });
970
- 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:");
966
+ let content = stringifyYaml(config, { lineWidth: 0 });
967
+ 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:");
971
968
  await writeFile(configPath, content);
972
969
  }
973
970
  /**
@@ -1026,10 +1023,7 @@ async function readLocalState(baseDir) {
1026
1023
  async function writeLocalState(baseDir, state) {
1027
1024
  const statePath = join(baseDir, STATE_FILE);
1028
1025
  await mkdir(join(baseDir, ".tbd"), { recursive: true });
1029
- await writeFile(statePath, stringify(state, {
1030
- sortMapEntries: true,
1031
- lineWidth: 0
1032
- }));
1026
+ await writeFile(statePath, stringifyYaml(state, { lineWidth: 0 }));
1033
1027
  }
1034
1028
  /**
1035
1029
  * Update specific fields in local state (merge with existing).
@@ -1286,6 +1280,67 @@ async function ensureGitignorePatterns(gitignorePath, patterns, header) {
1286
1280
  };
1287
1281
  }
1288
1282
 
1283
+ //#endregion
1284
+ //#region src/cli/lib/prefix-detection.ts
1285
+ /**
1286
+ * Prefix validation and beads prefix extraction module.
1287
+ *
1288
+ * Provides functions to validate prefixes and extract prefix from beads config.
1289
+ * Used by setup commands to validate user-provided prefixes and migrate from beads.
1290
+ */
1291
+ /** Maximum length for a valid prefix */
1292
+ const MAX_PREFIX_LENGTH = 20;
1293
+ /** Minimum length for a valid prefix */
1294
+ const MIN_PREFIX_LENGTH = 1;
1295
+ /** Recommended minimum length */
1296
+ const RECOMMENDED_MIN_LENGTH = 2;
1297
+ /** Recommended maximum length */
1298
+ const RECOMMENDED_MAX_LENGTH = 8;
1299
+ /**
1300
+ * Check if a prefix is valid (hard rules, always enforced).
1301
+ * - Must be 1-20 characters
1302
+ * - Must start with a letter (a-z)
1303
+ * - Must end with alphanumeric (a-z0-9)
1304
+ * - Middle characters can be alphanumeric, dot, or underscore
1305
+ * - No dashes allowed (breaks ID syntax)
1306
+ */
1307
+ function isValidPrefix(s) {
1308
+ if (!s) return false;
1309
+ if (s.length < MIN_PREFIX_LENGTH || s.length > MAX_PREFIX_LENGTH) return false;
1310
+ if (!/^[a-z]/.test(s)) return false;
1311
+ if (s.length > 1 && !/[a-z0-9]$/.test(s)) return false;
1312
+ return /^[a-z][a-z0-9._]*$/.test(s);
1313
+ }
1314
+ /**
1315
+ * Check if a prefix follows recommended format (soft rules).
1316
+ * - Must be 2-8 characters
1317
+ * - Must be alphabetic only (a-z)
1318
+ *
1319
+ * Prefixes that don't match this can still be used with --force.
1320
+ */
1321
+ function isRecommendedPrefix(s) {
1322
+ if (!s) return false;
1323
+ if (s.length < RECOMMENDED_MIN_LENGTH || s.length > RECOMMENDED_MAX_LENGTH) return false;
1324
+ return /^[a-z]+$/.test(s);
1325
+ }
1326
+ /**
1327
+ * Get prefix from existing beads config.
1328
+ *
1329
+ * Looks for .beads/config.yaml and extracts display.id_prefix
1330
+ *
1331
+ * @param cwd Current working directory
1332
+ * @returns The beads prefix, or null if not found
1333
+ */
1334
+ async function getBeadsPrefix(cwd) {
1335
+ try {
1336
+ const prefix = (parse(await readFile(join(cwd, ".beads", "config.yaml"), "utf-8"))?.display)?.id_prefix;
1337
+ if (typeof prefix === "string" && isValidPrefix(prefix)) return prefix;
1338
+ return null;
1339
+ } catch {
1340
+ return null;
1341
+ }
1342
+ }
1343
+
1289
1344
  //#endregion
1290
1345
  //#region src/utils/time-utils.ts
1291
1346
  /**
@@ -2131,7 +2186,15 @@ var InitHandler = class extends BaseCommand {
2131
2186
  } catch (error) {
2132
2187
  if (error instanceof CLIError) throw error;
2133
2188
  }
2134
- 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>");
2189
+ 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>");
2190
+ const prefix = options.prefix;
2191
+ 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");
2192
+ 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").
2193
+
2194
+ If you really want to use this prefix, add --force to override.
2195
+
2196
+ Example:
2197
+ tbd init --prefix=${prefix} --force`);
2135
2198
  if (this.checkDryRun("Would initialize tbd repository", options)) return;
2136
2199
  await this.execute(async () => {
2137
2200
  await initConfig(cwd, VERSION, options.prefix);
@@ -2195,7 +2258,7 @@ var InitHandler = class extends BaseCommand {
2195
2258
  });
2196
2259
  }
2197
2260
  };
2198
- 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) => {
2261
+ 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) => {
2199
2262
  await new InitHandler(command).run(options);
2200
2263
  });
2201
2264
 
@@ -2566,7 +2629,10 @@ async function loadIdMapping(baseDir) {
2566
2629
  ulidToShort: /* @__PURE__ */ new Map()
2567
2630
  };
2568
2631
  }
2569
- const data = parse(content) || {};
2632
+ const rawData = parseYamlWithConflictDetection(content, filePath) ?? {};
2633
+ const parseResult = IdMappingYamlSchema.safeParse(rawData);
2634
+ if (!parseResult.success) throw new Error(`Invalid ID mapping format in ${filePath}: ${parseResult.error.message}`);
2635
+ const data = parseResult.data;
2570
2636
  const shortToUlid = /* @__PURE__ */ new Map();
2571
2637
  const ulidToShort = /* @__PURE__ */ new Map();
2572
2638
  for (const [shortId, ulid] of Object.entries(data)) {
@@ -2587,7 +2653,7 @@ async function saveIdMapping(baseDir, mapping) {
2587
2653
  const data = {};
2588
2654
  const sortedKeys = naturalSort(Array.from(mapping.shortToUlid.keys()));
2589
2655
  for (const key of sortedKeys) data[key] = mapping.shortToUlid.get(key);
2590
- await writeFile(filePath, stringify(data));
2656
+ await writeFile(filePath, stringifyYaml(data));
2591
2657
  }
2592
2658
  /**
2593
2659
  * Calculate the optimal short ID length based on existing ID count.
@@ -2656,6 +2722,55 @@ function resolveToInternalId(input, mapping) {
2656
2722
  if (!ulid) throw new Error(`Unknown issue ID: ${input}. Short ID "${shortId}" not found in mapping.`);
2657
2723
  return makeInternalId(ulid);
2658
2724
  }
2725
+ /**
2726
+ * Parse an ID mapping from raw YAML content.
2727
+ * Used for loading mappings from git show output during conflict resolution.
2728
+ *
2729
+ * @throws MergeConflictError if content contains merge conflict markers
2730
+ */
2731
+ function parseIdMappingFromYaml(content) {
2732
+ const rawData = parseYamlWithConflictDetection(content) ?? {};
2733
+ const parseResult = IdMappingYamlSchema.safeParse(rawData);
2734
+ if (!parseResult.success) throw new Error(`Invalid ID mapping format: ${parseResult.error.message}`);
2735
+ const data = parseResult.data;
2736
+ const shortToUlid = /* @__PURE__ */ new Map();
2737
+ const ulidToShort = /* @__PURE__ */ new Map();
2738
+ for (const [shortId, ulid] of Object.entries(data)) {
2739
+ shortToUlid.set(shortId, ulid);
2740
+ ulidToShort.set(ulid, shortId);
2741
+ }
2742
+ return {
2743
+ shortToUlid,
2744
+ ulidToShort
2745
+ };
2746
+ }
2747
+ /**
2748
+ * Merge two ID mappings by combining all entries from both.
2749
+ * ID mappings are always additive (new IDs are only added, never removed),
2750
+ * so merging simply unions all key-value pairs.
2751
+ *
2752
+ * If the same short ID maps to different ULIDs in each mapping (a conflict),
2753
+ * the local mapping takes precedence (caller should log a warning).
2754
+ *
2755
+ * @param local - The local ID mapping
2756
+ * @param remote - The remote ID mapping
2757
+ * @returns Merged mapping with all entries from both
2758
+ */
2759
+ function mergeIdMappings(local, remote) {
2760
+ const merged = {
2761
+ shortToUlid: new Map(local.shortToUlid),
2762
+ ulidToShort: new Map(local.ulidToShort)
2763
+ };
2764
+ for (const [shortId, ulid] of remote.shortToUlid) if (!merged.shortToUlid.has(shortId)) {
2765
+ merged.shortToUlid.set(shortId, ulid);
2766
+ merged.ulidToShort.set(ulid, shortId);
2767
+ }
2768
+ for (const [ulid, shortId] of remote.ulidToShort) if (!merged.ulidToShort.has(ulid) && !merged.shortToUlid.has(shortId)) {
2769
+ merged.shortToUlid.set(shortId, ulid);
2770
+ merged.ulidToShort.set(ulid, shortId);
2771
+ }
2772
+ return merged;
2773
+ }
2659
2774
 
2660
2775
  //#endregion
2661
2776
  //#region src/lib/priority.ts
@@ -2864,8 +2979,9 @@ var CreateHandler = class extends BaseCommand {
2864
2979
  let description = options.description;
2865
2980
  if (options.file) try {
2866
2981
  description = await readFile(options.file, "utf-8");
2867
- } catch {
2868
- throw new CLIError(`Failed to read description from file: ${options.file}`);
2982
+ } catch (error) {
2983
+ const message = error instanceof Error ? error.message : String(error);
2984
+ throw new CLIError(`Failed to read description from file '${options.file}': ${message}`);
2869
2985
  }
2870
2986
  let specPath;
2871
2987
  if (options.spec) try {
@@ -3520,15 +3636,8 @@ function normalizePath(path) {
3520
3636
  */
3521
3637
  var ListHandler = class extends BaseCommand {
3522
3638
  async run(options) {
3523
- const tbdRoot = await requireInit();
3524
- let issues;
3525
- let dataCtx;
3526
- try {
3527
- dataCtx = await loadDataContext(tbdRoot);
3528
- issues = await listIssues(dataCtx.dataSyncDir);
3529
- } catch {
3530
- throw new CLIError("Failed to read issues");
3531
- }
3639
+ const dataCtx = await loadDataContext(await requireInit());
3640
+ let issues = await listIssues(dataCtx.dataSyncDir);
3532
3641
  issues = this.filterIssues(issues, options, dataCtx.mapping);
3533
3642
  issues = this.sortIssues(issues, options.sort ?? "priority", dataCtx.mapping);
3534
3643
  issues = applyLimit(issues, options.limit);
@@ -4841,8 +4950,8 @@ var DocSync = class {
4841
4950
  * Parse a source string into a DocSource.
4842
4951
  *
4843
4952
  * @example
4844
- * parseSource('internal:shortcuts/standard/commit-code.md')
4845
- * // => { type: 'internal', location: 'shortcuts/standard/commit-code.md' }
4953
+ * parseSource('internal:shortcuts/standard/code-review-and-commit.md')
4954
+ * // => { type: 'internal', location: 'shortcuts/standard/code-review-and-commit.md' }
4846
4955
  *
4847
4956
  * @example
4848
4957
  * parseSource('https://raw.githubusercontent.com/org/repo/main/file.md')
@@ -5528,7 +5637,24 @@ var SyncHandler = class extends BaseCommand {
5528
5637
  } catch {
5529
5638
  this.output.debug(`Issue ${localIssue.id} not on remote, keeping local`);
5530
5639
  }
5640
+ try {
5641
+ const remoteIdsContent = await git("show", `${remote}/${syncBranch}:${DATA_SYNC_DIR}/mappings/ids.yml`);
5642
+ if (remoteIdsContent) {
5643
+ const localMapping = await loadIdMapping(this.dataSyncDir);
5644
+ const remoteMapping = parseIdMappingFromYaml(remoteIdsContent);
5645
+ const mergedMapping = mergeIdMappings(localMapping, remoteMapping);
5646
+ await saveIdMapping(this.dataSyncDir, mergedMapping);
5647
+ this.output.debug(`Merged ID mappings: ${localMapping.shortToUlid.size} local + ${remoteMapping.shortToUlid.size} remote = ${mergedMapping.shortToUlid.size} total`);
5648
+ }
5649
+ } catch (error) {
5650
+ this.output.debug(`Could not merge ids.yml: ${error.message}`);
5651
+ }
5531
5652
  await git("-C", worktreePath, "add", "-A");
5653
+ const conflictCheck = await git("-C", worktreePath, "diff", "--cached", "-S<<<<<<< ", "--name-only");
5654
+ if (conflictCheck.trim()) {
5655
+ const conflictedFiles = conflictCheck.trim().split("\n");
5656
+ 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}`);
5657
+ }
5532
5658
  try {
5533
5659
  await git("-C", worktreePath, "commit", "--no-verify", "-m", "tbd sync: resolved merge conflicts");
5534
5660
  } catch {
@@ -5674,7 +5800,7 @@ async function readState() {
5674
5800
  * Update local state file.
5675
5801
  */
5676
5802
  async function updateState(updates) {
5677
- await writeFile(STATE_FILE, stringify({
5803
+ await writeFile(STATE_FILE, stringifyYaml({
5678
5804
  ...await readState(),
5679
5805
  ...updates
5680
5806
  }));
@@ -6143,7 +6269,7 @@ async function saveConflictToAttic(atticDir, conflict, winnerSource) {
6143
6269
  }
6144
6270
  };
6145
6271
  const safeTimestamp = timestamp.replace(/:/g, "-");
6146
- await writeFile(join(atticDir, `${conflict.issue_id}_${safeTimestamp}_${conflict.field}.yml`), stringify(entry, { sortMapEntries: true }));
6272
+ await writeFile(join(atticDir, `${conflict.issue_id}_${safeTimestamp}_${conflict.field}.yml`), stringifyYaml(entry));
6147
6273
  }
6148
6274
  /**
6149
6275
  * Get the target/source directory for workspace operations.
@@ -7677,7 +7803,9 @@ async function listAtticEntries(filterById) {
7677
7803
  if (!parsed) continue;
7678
7804
  if (filterById && parsed.entityId !== filterById) continue;
7679
7805
  try {
7680
- const entry = parse(await readFile(join(atticPath, file), "utf-8"));
7806
+ const filePath = join(atticPath, file);
7807
+ const rawData = parseYamlWithConflictDetection(await readFile(filePath, "utf-8"), filePath);
7808
+ const entry = AtticEntrySchema.parse(rawData);
7681
7809
  entries.push(entry);
7682
7810
  } catch {}
7683
7811
  }
@@ -8316,7 +8444,7 @@ var DocsHandler = class extends BaseCommand {
8316
8444
  console.log(colors.bold("Workflows (Shortcuts):"));
8317
8445
  console.log(" tbd shortcut --list List all available shortcuts");
8318
8446
  console.log(" tbd shortcut new-plan-spec Plan a new feature");
8319
- console.log(" tbd shortcut commit-code Commit code properly");
8447
+ console.log(" tbd shortcut code-review-and-commit Commit code properly");
8320
8448
  console.log(" tbd shortcut create-or-update-pr-simple Create a pull request");
8321
8449
  console.log("");
8322
8450
  console.log(colors.bold("Guidelines (Coding Standards):"));
@@ -8657,8 +8785,8 @@ var UninstallHandler = class extends BaseCommand {
8657
8785
  force: true
8658
8786
  });
8659
8787
  console.log(` ${colors.success("✓")} Removed .tbd directory`);
8660
- } catch {
8661
- throw new CLIError("Failed to remove .tbd directory");
8788
+ } catch (error) {
8789
+ throw new CLIError(`Failed to remove .tbd directory: ${error instanceof Error ? error.message : String(error)}`);
8662
8790
  }
8663
8791
  console.log("");
8664
8792
  this.output.success("tbd has been uninstalled from this repository.");
@@ -8840,6 +8968,7 @@ var DocCache = class {
8840
8968
  return {
8841
8969
  title: typeof parsed.title === "string" ? parsed.title : void 0,
8842
8970
  description: typeof parsed.description === "string" ? parsed.description : void 0,
8971
+ category: typeof parsed.category === "string" ? parsed.category : void 0,
8843
8972
  tags: Array.isArray(parsed.tags) ? parsed.tags.filter((t) => typeof t === "string") : void 0
8844
8973
  };
8845
8974
  } catch {
@@ -8968,7 +9097,7 @@ function buildTableRows(docs, skipNames = []) {
8968
9097
  * // ## Available Shortcuts
8969
9098
  * // | Name | Description |
8970
9099
  * // | --- | --- |
8971
- * // | commit-code | Run pre-commit checks, review changes, and commit code |
9100
+ * // | code-review-and-commit | Run pre-commit checks, review changes, and commit code |
8972
9101
  * // ...
8973
9102
  * // ## Available Guidelines
8974
9103
  * // | Name | Description |
@@ -9453,16 +9582,6 @@ async function addDoc(tbdRoot, options) {
9453
9582
  *
9454
9583
  * See: docs/project/specs/active/plan-2026-01-22-doc-cache-abstraction.md
9455
9584
  */
9456
- /**
9457
- * Infer category from shortcut name.
9458
- * Returns undefined if no category matches.
9459
- */
9460
- function inferCategory(name) {
9461
- 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";
9462
- if (name.includes("implement-") || name.includes("coding-spike")) return "implementation";
9463
- if (name.includes("review-") || name.includes("precommit") || name.includes("cleanup-") || name.includes("validation-plan")) return "quality";
9464
- if (name.includes("commit-") || name.includes("pr-") || name.includes("merge-")) return "shipping";
9465
- }
9466
9585
  var ShortcutHandler = class extends BaseCommand {
9467
9586
  async run(query, options) {
9468
9587
  await this.execute(async () => {
@@ -9520,13 +9639,14 @@ var ShortcutHandler = class extends BaseCommand {
9520
9639
  async handleList(cache, includeAll, category) {
9521
9640
  let docs = cache.list(includeAll);
9522
9641
  if (category) docs = docs.filter((d) => {
9523
- return inferCategory(d.name) === category;
9642
+ return d.frontmatter?.category === category;
9524
9643
  });
9525
9644
  if (this.ctx.json) {
9526
9645
  this.output.data(docs.map((d) => ({
9527
9646
  name: d.name,
9528
9647
  title: d.frontmatter?.title,
9529
9648
  description: d.frontmatter?.description,
9649
+ category: d.frontmatter?.category,
9530
9650
  path: d.path,
9531
9651
  sourceDir: d.sourceDir,
9532
9652
  sizeBytes: d.sizeBytes,
@@ -9680,7 +9800,7 @@ var ShortcutHandler = class extends BaseCommand {
9680
9800
  }
9681
9801
  }
9682
9802
  };
9683
- 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) => {
9803
+ 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) => {
9684
9804
  await new ShortcutHandler(command).run(query, options);
9685
9805
  });
9686
9806
 
@@ -10047,47 +10167,6 @@ const templateCommand = new Command("template").description("Find and output doc
10047
10167
  await new TemplateHandler(command).run(query, options);
10048
10168
  });
10049
10169
 
10050
- //#endregion
10051
- //#region src/cli/lib/prefix-detection.ts
10052
- /**
10053
- * Prefix validation and beads prefix extraction module.
10054
- *
10055
- * Provides functions to validate prefixes and extract prefix from beads config.
10056
- * Used by setup commands to validate user-provided prefixes and migrate from beads.
10057
- */
10058
- /** Maximum length for a valid prefix */
10059
- const MAX_PREFIX_LENGTH = 10;
10060
- /** Minimum length for a valid prefix */
10061
- const MIN_PREFIX_LENGTH = 1;
10062
- /**
10063
- * Check if a prefix is valid.
10064
- * - Must be 1-10 characters
10065
- * - Must start with a letter
10066
- * - Must be alphanumeric only (lowercase)
10067
- */
10068
- function isValidPrefix(s) {
10069
- if (!s) return false;
10070
- if (s.length < MIN_PREFIX_LENGTH || s.length > MAX_PREFIX_LENGTH) return false;
10071
- return /^[a-z][a-z0-9]*$/.test(s);
10072
- }
10073
- /**
10074
- * Get prefix from existing beads config.
10075
- *
10076
- * Looks for .beads/config.yaml and extracts display.id_prefix
10077
- *
10078
- * @param cwd Current working directory
10079
- * @returns The beads prefix, or null if not found
10080
- */
10081
- async function getBeadsPrefix(cwd) {
10082
- try {
10083
- const prefix = (parse(await readFile(join(cwd, ".beads", "config.yaml"), "utf-8"))?.display)?.id_prefix;
10084
- if (typeof prefix === "string" && isValidPrefix(prefix)) return prefix;
10085
- return null;
10086
- } catch {
10087
- return null;
10088
- }
10089
- }
10090
-
10091
10170
  //#endregion
10092
10171
  //#region src/cli/commands/setup.ts
10093
10172
  /**
@@ -10882,8 +10961,14 @@ var SetupDefaultHandler = class extends BaseCommand {
10882
10961
  }
10883
10962
  const beadsPrefix = await getBeadsPrefix(cwd);
10884
10963
  const prefix = options.prefix ?? beadsPrefix;
10885
- 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");
10886
- 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");
10964
+ 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");
10965
+ 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");
10966
+ 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").
10967
+
10968
+ If you really want to use this prefix, add --force to override.
10969
+
10970
+ Example:
10971
+ tbd setup --from-beads --prefix=${prefix} --force`);
10887
10972
  await this.initializeTbd(cwd, prefix);
10888
10973
  if (options.ghCli === false) {
10889
10974
  const config = await readConfig(cwd);
@@ -10922,7 +11007,13 @@ var SetupDefaultHandler = class extends BaseCommand {
10922
11007
  const colors = this.output.getColors();
10923
11008
  const prefix = options.prefix;
10924
11009
  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.");
10925
- 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");
11010
+ 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");
11011
+ 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").
11012
+
11013
+ If you really want to use this prefix, add --force to override.
11014
+
11015
+ Example:
11016
+ tbd setup --auto --prefix=${prefix} --force`);
10926
11017
  console.log(`Initializing with prefix "${prefix}"...`);
10927
11018
  await this.initializeTbd(cwd, prefix);
10928
11019
  if (options.ghCli === false) {
@@ -11195,7 +11286,7 @@ var SetupAutoHandler = class extends BaseCommand {
11195
11286
  return result;
11196
11287
  }
11197
11288
  };
11198
- 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) => {
11289
+ 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) => {
11199
11290
  if (options.auto || options.interactive) {
11200
11291
  await new SetupDefaultHandler(command).run(options);
11201
11292
  return;
@@ -11219,7 +11310,8 @@ const setupCommand = new Command("setup").description("Configure tbd integration
11219
11310
  console.log(" --from-beads Migrate from Beads to tbd (implies --auto)");
11220
11311
  console.log("");
11221
11312
  console.log("Options:");
11222
- console.log(" --prefix <name> Project prefix for issue IDs (e.g., \"tbd\", \"myapp\")");
11313
+ console.log(" --prefix <name> Project prefix for issue IDs (2-8 alphabetic recommended)");
11314
+ console.log(" --force Allow non-recommended prefix format");
11223
11315
  console.log(" --no-gh-cli Disable automatic GitHub CLI installation hook");
11224
11316
  console.log("");
11225
11317
  console.log("Examples:");
@@ -11421,15 +11513,31 @@ function isJsonMode() {
11421
11513
  return process.argv.includes("--json");
11422
11514
  }
11423
11515
  /**
11516
+ * Check if --debug flag is present in argv.
11517
+ */
11518
+ function isDebugMode() {
11519
+ return process.argv.includes("--debug");
11520
+ }
11521
+ /**
11424
11522
  * Output error in the appropriate format (JSON or text).
11523
+ * In debug mode, shows full error details and stack trace.
11425
11524
  */
11426
11525
  function outputError(message, error) {
11526
+ const debugMode = isDebugMode();
11427
11527
  if (isJsonMode()) {
11428
11528
  const errorObj = { error: message };
11429
11529
  if (error instanceof CLIError) errorObj.type = error.name;
11430
11530
  if (error && error.message !== message) errorObj.details = error.message;
11531
+ if (debugMode && error?.stack) errorObj.stack = error.stack;
11431
11532
  console.error(JSON.stringify(errorObj));
11432
- } else console.error(`Error: ${message}`);
11533
+ } else {
11534
+ console.error(`Error: ${message}`);
11535
+ if (debugMode && error?.stack) {
11536
+ console.error("");
11537
+ console.error("Stack trace:");
11538
+ console.error(error.stack);
11539
+ }
11540
+ }
11433
11541
  }
11434
11542
  /**
11435
11543
  * Check if running with no command (just options or nothing).