get-tbd 0.1.30 → 0.2.0

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 (91) hide show
  1. package/README.md +5 -1
  2. package/dist/bin.mjs +2823 -2226
  3. package/dist/bin.mjs.map +1 -1
  4. package/dist/cli.mjs +1063 -665
  5. package/dist/cli.mjs.map +1 -1
  6. package/dist/{config-DVap9omo.mjs → config-BJz1m9eN.mjs} +179 -39
  7. package/dist/config-BJz1m9eN.mjs.map +1 -0
  8. package/dist/{config-BPHcePSm.mjs → config-DlCUMyCG.mjs} +1 -1
  9. package/dist/docs/README.md +5 -1
  10. package/dist/docs/guidelines/backward-compatibility-rules.md +4 -0
  11. package/dist/docs/guidelines/bun-monorepo-patterns.md +20 -4
  12. package/dist/docs/guidelines/cli-agent-skill-patterns.md +38 -34
  13. package/dist/docs/guidelines/commit-conventions.md +4 -0
  14. package/dist/docs/guidelines/common-doc-guidelines.md +234 -0
  15. package/dist/docs/guidelines/convex-limits-best-practices.md +4 -0
  16. package/dist/docs/guidelines/convex-rules.md +4 -0
  17. package/dist/docs/guidelines/electron-app-development-patterns.md +4 -0
  18. package/dist/docs/guidelines/error-handling-rules.md +4 -0
  19. package/dist/docs/guidelines/general-coding-rules.md +4 -0
  20. package/dist/docs/guidelines/general-comment-rules.md +4 -0
  21. package/dist/docs/guidelines/general-eng-assistant-rules.md +4 -0
  22. package/dist/docs/guidelines/general-tdd-guidelines.md +4 -0
  23. package/dist/docs/guidelines/general-testing-rules.md +4 -0
  24. package/dist/docs/guidelines/golden-testing-guidelines.md +4 -0
  25. package/dist/docs/guidelines/pnpm-monorepo-patterns.md +27 -6
  26. package/dist/docs/guidelines/python-cli-patterns.md +4 -0
  27. package/dist/docs/guidelines/python-modern-guidelines.md +30 -0
  28. package/dist/docs/guidelines/python-rules.md +4 -0
  29. package/dist/docs/guidelines/release-notes-guidelines.md +4 -0
  30. package/dist/docs/guidelines/supply-chain-hardening.md +11 -7
  31. package/dist/docs/guidelines/tbd-sync-troubleshooting.md +10 -4
  32. package/dist/docs/guidelines/typescript-cli-tool-rules.md +27 -24
  33. package/dist/docs/guidelines/typescript-code-coverage.md +11 -7
  34. package/dist/docs/guidelines/typescript-rules.md +10 -6
  35. package/dist/docs/guidelines/typescript-sorting-patterns.md +4 -0
  36. package/dist/docs/guidelines/typescript-yaml-handling-rules.md +7 -3
  37. package/dist/docs/shortcuts/standard/agent-handoff.md +4 -0
  38. package/dist/docs/shortcuts/standard/checkout-third-party-repo.md +4 -0
  39. package/dist/docs/shortcuts/standard/code-cleanup-all.md +4 -0
  40. package/dist/docs/shortcuts/standard/code-cleanup-docstrings.md +4 -0
  41. package/dist/docs/shortcuts/standard/code-cleanup-tests.md +4 -0
  42. package/dist/docs/shortcuts/standard/code-review-and-commit.md +4 -0
  43. package/dist/docs/shortcuts/standard/coding-spike.md +4 -0
  44. package/dist/docs/shortcuts/standard/create-or-update-pr-simple.md +4 -0
  45. package/dist/docs/shortcuts/standard/create-or-update-pr-with-validation-plan.md +4 -0
  46. package/dist/docs/shortcuts/standard/implement-beads.md +4 -0
  47. package/dist/docs/shortcuts/standard/merge-upstream.md +4 -0
  48. package/dist/docs/shortcuts/standard/new-architecture-doc.md +4 -0
  49. package/dist/docs/shortcuts/standard/new-guideline.md +4 -0
  50. package/dist/docs/shortcuts/standard/new-plan-spec.md +4 -0
  51. package/dist/docs/shortcuts/standard/new-qa-playbook.md +4 -0
  52. package/dist/docs/shortcuts/standard/new-research-brief.md +4 -0
  53. package/dist/docs/shortcuts/standard/new-shortcut.md +4 -0
  54. package/dist/docs/shortcuts/standard/new-validation-plan.md +4 -0
  55. package/dist/docs/shortcuts/standard/plan-implementation-with-beads.md +4 -0
  56. package/dist/docs/shortcuts/standard/precommit-process.md +4 -0
  57. package/dist/docs/shortcuts/standard/review-code-python.md +4 -0
  58. package/dist/docs/shortcuts/standard/review-code-typescript.md +4 -0
  59. package/dist/docs/shortcuts/standard/review-code.md +4 -0
  60. package/dist/docs/shortcuts/standard/review-github-pr.md +4 -0
  61. package/dist/docs/shortcuts/standard/revise-all-architecture-docs.md +4 -0
  62. package/dist/docs/shortcuts/standard/revise-architecture-doc.md +4 -0
  63. package/dist/docs/shortcuts/standard/setup-github-cli.md +4 -0
  64. package/dist/docs/shortcuts/standard/sync-failure-recovery.md +4 -0
  65. package/dist/docs/shortcuts/standard/update-specs-status.md +4 -0
  66. package/dist/docs/shortcuts/standard/welcome-user.md +4 -0
  67. package/dist/docs/tbd-closing.md +4 -0
  68. package/dist/docs/tbd-design.md +109 -68
  69. package/dist/docs/tbd-docs.md +20 -13
  70. package/dist/docs/tbd-prime.md +4 -0
  71. package/dist/docs/templates/architecture-doc.md +4 -0
  72. package/dist/docs/templates/plan-spec.md +4 -0
  73. package/dist/docs/templates/qa-playbook.md +4 -0
  74. package/dist/docs/templates/research-brief.md +4 -0
  75. package/dist/{id-mapping-Ctfl_nc1.mjs → id-mapping-CFoPVinz.mjs} +1 -1
  76. package/dist/{id-mapping-CqrrLgeX.mjs → id-mapping-CtfTfGIh.mjs} +146 -122
  77. package/dist/id-mapping-CtfTfGIh.mjs.map +1 -0
  78. package/dist/index.d.mts +53 -1
  79. package/dist/index.mjs +3 -3
  80. package/dist/{schemas-C8mOQykE.mjs → schemas-f0EcuAVu.mjs} +40 -3
  81. package/dist/schemas-f0EcuAVu.mjs.map +1 -0
  82. package/dist/{src-BK_EF6mk.mjs → src-rIE4xSVs.mjs} +3 -3
  83. package/dist/src-rIE4xSVs.mjs.map +1 -0
  84. package/dist/tbd +2823 -2226
  85. package/package.json +1 -1
  86. package/dist/config-DVap9omo.mjs.map +0 -1
  87. package/dist/docs/guidelines/general-style-rules.md +0 -38
  88. package/dist/docs/guidelines/writing-style-guidelines.md +0 -42
  89. package/dist/id-mapping-CqrrLgeX.mjs.map +0 -1
  90. package/dist/schemas-C8mOQykE.mjs.map +0 -1
  91. package/dist/src-BK_EF6mk.mjs.map +0 -1
@@ -1,10 +1,12 @@
1
- import { C as LOCAL_STATE_FIELD_ORDER, a as ConfigSchema, i as CONFIG_FIELD_ORDER, w as LocalStateSchema } from "./schemas-C8mOQykE.mjs";
1
+ import { D as LocalStateSchema, E as LOCAL_STATE_FIELD_ORDER, a as CONFIG_FIELD_ORDER, s as ConfigSchema } from "./schemas-f0EcuAVu.mjs";
2
2
  import { o as sortKeys, s as stringifyYaml } from "./yaml-utils-BPy991by.mjs";
3
3
  import { parse } from "yaml";
4
- import { access, mkdir, readFile } from "node:fs/promises";
5
- import { dirname, isAbsolute, join, parse as parse$1 } from "node:path";
4
+ import { execFile } from "node:child_process";
5
+ import { access, mkdir, readFile, realpath } from "node:fs/promises";
6
+ import { dirname, isAbsolute, join, parse as parse$1, resolve } from "node:path";
6
7
  import { writeFile } from "atomically";
7
8
  import { homedir } from "node:os";
9
+ import { promisify } from "node:util";
8
10
 
9
11
  //#region src/lib/paths.ts
10
12
  /**
@@ -21,8 +23,14 @@ import { homedir } from "node:os";
21
23
  * Gitignored (local only):
22
24
  * state.yml - Local state
23
25
  * docs/ - Installed documentation (regenerated on setup)
24
- * data-sync-worktree/ - Hidden worktree checkout of tbd-sync branch
25
- * .tbd/data-sync/ - issues/, mappings/, attic/, meta.yml
26
+ *
27
+ * In the Git common dir shared by all linked worktrees:
28
+ * $GIT_COMMON_DIR/tbd/
29
+ * layout.yml - Shared layout metadata
30
+ * locks/data-sync.lock/ - Repo-scoped lock directory
31
+ * backups/ - Local repair/migration backups
32
+ * data-sync-worktree/ - Hidden worktree checkout of tbd-sync branch
33
+ * .tbd/data-sync/ - issues/, mappings/, attic/, meta.yml
26
34
  *
27
35
  * On tbd-sync branch:
28
36
  * .tbd/
@@ -40,26 +48,34 @@ const CONFIG_FILE = join(TBD_DIR, "config.yml");
40
48
  const STATE_FILE = join(TBD_DIR, "state.yml");
41
49
  /** The worktree directory name */
42
50
  const WORKTREE_DIR_NAME = "data-sync-worktree";
43
- /** The worktree path (gitignored) */
44
- const WORKTREE_DIR = join(TBD_DIR, WORKTREE_DIR_NAME);
51
+ /** Legacy per-checkout worktree path used by f03 and earlier clients. */
52
+ const LEGACY_WORKTREE_DIR = join(TBD_DIR, WORKTREE_DIR_NAME);
53
+ /**
54
+ * @internal Primary-checkout relative path to the shared sync worktree.
55
+ *
56
+ * Only valid when `.git` is a directory (i.e., the primary checkout). Production
57
+ * code must call resolveSharedTbdPaths() instead: linked worktrees have a `.git`
58
+ * file, so this constant resolves to the wrong location for them. Intended for
59
+ * tests and the non-git fallback in resolveDataSyncDir().
60
+ */
61
+ const PRIMARY_CHECKOUT_WORKTREE_DIR = join(".git", "tbd", WORKTREE_DIR_NAME);
45
62
  /** The data directory name on the sync branch */
46
63
  const DATA_SYNC_DIR_NAME = "data-sync";
47
64
  /**
48
- * The base directory for synced data.
49
- *
50
- * NOTE: This is currently pointing directly to .tbd/data-sync/ which is WRONG
51
- * per the spec. The correct path should be via the worktree:
52
- * .tbd/data-sync-worktree/.tbd/data-sync/
53
- *
54
- * TODO(tbd-208): Update this to use the worktree path once worktree
55
- * management is implemented.
65
+ * The data directory path as it appears on the tbd-sync branch.
66
+ * In a normal checkout this same relative path is a legacy/wrong-location fallback;
67
+ * production callers should resolve the absolute shared worktree path with
68
+ * resolveDataSyncDir().
56
69
  */
57
70
  const DATA_SYNC_DIR = join(TBD_DIR, DATA_SYNC_DIR_NAME);
58
71
  /**
59
- * The correct path for synced data via worktree (per spec).
60
- * Use this once worktree management is implemented.
72
+ * @internal Primary-checkout relative path to the synced data via the shared worktree.
73
+ *
74
+ * Same caveat as `PRIMARY_CHECKOUT_WORKTREE_DIR`: only valid for a primary checkout.
75
+ * Production code should resolve the absolute path with resolveDataSyncDir(); this
76
+ * constant is intended for tests and the non-git fallback path.
61
77
  */
62
- const DATA_SYNC_DIR_VIA_WORKTREE = join(WORKTREE_DIR, TBD_DIR, DATA_SYNC_DIR_NAME);
78
+ const PRIMARY_CHECKOUT_DATA_SYNC_DIR = join(PRIMARY_CHECKOUT_WORKTREE_DIR, TBD_DIR, DATA_SYNC_DIR_NAME);
63
79
  /** Issues directory */
64
80
  const ISSUES_DIR = join(DATA_SYNC_DIR, "issues");
65
81
  /** Mappings directory */
@@ -70,6 +86,70 @@ const ATTIC_DIR = join(DATA_SYNC_DIR, "attic");
70
86
  const META_FILE = join(DATA_SYNC_DIR, "meta.yml");
71
87
  /** The sync branch name */
72
88
  const SYNC_BRANCH = "tbd-sync";
89
+ const execFileAsync = promisify(execFile);
90
+ /** Directory name under $GIT_COMMON_DIR for tbd local machinery. */
91
+ const GIT_COMMON_TBD_DIR_NAME = "tbd";
92
+ /** Common-dir layout metadata file name. */
93
+ const COMMON_DIR_LAYOUT_FILE_NAME = "layout.yml";
94
+ /** Shared lock directory name under $GIT_COMMON_DIR/tbd/. */
95
+ const SHARED_LOCKS_DIR_NAME = "locks";
96
+ /** Shared backups directory name under $GIT_COMMON_DIR/tbd/. */
97
+ const SHARED_BACKUPS_DIR_NAME = "backups";
98
+ /** Directory-lock name for shared data-sync operations. */
99
+ const DATA_SYNC_LOCK_DIR_NAME = "data-sync.lock";
100
+ /**
101
+ * Resolve Git's common directory from any checkout or linked worktree.
102
+ */
103
+ async function resolveGitCommonDir(cwd) {
104
+ let output;
105
+ try {
106
+ const { stdout } = await execFileAsync("git", [
107
+ "-C",
108
+ cwd,
109
+ "rev-parse",
110
+ "--git-common-dir"
111
+ ], { maxBuffer: 1024 * 1024 });
112
+ output = stdout.trim();
113
+ } catch {
114
+ const { stdout } = await execFileAsync("git", [
115
+ "-C",
116
+ cwd,
117
+ "rev-parse",
118
+ "--path-format=absolute",
119
+ "--git-common-dir"
120
+ ], { maxBuffer: 1024 * 1024 });
121
+ output = stdout.trim();
122
+ }
123
+ if (!output) throw new Error(`Unable to resolve Git common directory from ${cwd}`);
124
+ const gitCommonDir = isAbsolute(output) ? output : resolve(cwd, output);
125
+ return realpath(gitCommonDir).catch(() => gitCommonDir);
126
+ }
127
+ /**
128
+ * Build all shared tbd paths from an absolute Git common directory.
129
+ */
130
+ function buildSharedTbdPaths(gitCommonDir) {
131
+ const sharedTbdDir = join(gitCommonDir, GIT_COMMON_TBD_DIR_NAME);
132
+ const sharedWorktreePath = join(sharedTbdDir, WORKTREE_DIR_NAME);
133
+ const sharedDataSyncDir = join(sharedWorktreePath, TBD_DIR, DATA_SYNC_DIR_NAME);
134
+ const sharedLayoutPath = join(sharedTbdDir, COMMON_DIR_LAYOUT_FILE_NAME);
135
+ const sharedLocksDir = join(sharedTbdDir, SHARED_LOCKS_DIR_NAME);
136
+ return {
137
+ gitCommonDir,
138
+ sharedTbdDir,
139
+ sharedWorktreePath,
140
+ sharedDataSyncDir,
141
+ sharedLayoutPath,
142
+ sharedLocksDir,
143
+ sharedLockPath: join(sharedLocksDir, DATA_SYNC_LOCK_DIR_NAME),
144
+ sharedBackupsDir: join(sharedTbdDir, SHARED_BACKUPS_DIR_NAME)
145
+ };
146
+ }
147
+ /**
148
+ * Resolve the shared tbd paths for the repository containing baseDir.
149
+ */
150
+ async function resolveSharedTbdPaths(baseDir) {
151
+ return buildSharedTbdPaths(await resolveGitCommonDir(baseDir));
152
+ }
73
153
  /** The workspaces directory name within .tbd/ */
74
154
  const WORKSPACES_DIR_NAME = "workspaces";
75
155
  /** Full path to workspaces directory: .tbd/workspaces/ */
@@ -148,7 +228,7 @@ const DEFAULT_TEMPLATE_PATHS = [TBD_TEMPLATES_DIR];
148
228
  * Defined inline to avoid circular dependency with errors.ts.
149
229
  */
150
230
  var WorktreeMissingError = class extends Error {
151
- constructor(message = "Worktree not found at .tbd/data-sync-worktree/. Run 'tbd doctor --fix' to repair.") {
231
+ constructor(message = "Shared worktree not found under $GIT_COMMON_DIR/tbd/data-sync-worktree/. Run 'tbd doctor --fix' to repair.") {
152
232
  super(message);
153
233
  this.name = "WorktreeMissingError";
154
234
  }
@@ -167,8 +247,9 @@ let _resolvedAllowFallback = null;
167
247
  * (production) or in a test environment without worktree.
168
248
  *
169
249
  * Order of preference:
170
- * 1. Worktree path if worktree exists: .tbd/data-sync-worktree/.tbd/data-sync/
171
- * 2. Direct path as fallback (only if allowFallback: true)
250
+ * 1. Shared worktree path if it exists:
251
+ * $GIT_COMMON_DIR/tbd/data-sync-worktree/.tbd/data-sync/
252
+ * 2. Direct path as fallback (only if allowFallback: true, for tests/diagnostics)
172
253
  *
173
254
  * @param baseDir - The tbd root directory (from requireInit or findTbdRoot)
174
255
  * @param options - Options for path resolution
@@ -180,22 +261,23 @@ let _resolvedAllowFallback = null;
180
261
  async function resolveDataSyncDir(baseDir, options) {
181
262
  const allowFallback = options?.allowFallback ?? true;
182
263
  if (_resolvedDataSyncDir && _resolvedBaseDir === baseDir && _resolvedAllowFallback === allowFallback) return _resolvedDataSyncDir;
183
- const worktreePath = join(baseDir, DATA_SYNC_DIR_VIA_WORKTREE);
184
- const directPath = join(baseDir, DATA_SYNC_DIR);
264
+ let worktreePath = null;
185
265
  try {
266
+ worktreePath = (await resolveSharedTbdPaths(baseDir)).sharedDataSyncDir;
267
+ } catch {
268
+ worktreePath = join(baseDir, PRIMARY_CHECKOUT_DATA_SYNC_DIR);
269
+ }
270
+ const directPath = join(baseDir, DATA_SYNC_DIR);
271
+ if (worktreePath) try {
186
272
  await access(worktreePath);
187
273
  _resolvedDataSyncDir = worktreePath;
188
274
  _resolvedBaseDir = baseDir;
189
275
  _resolvedAllowFallback = allowFallback;
190
276
  return worktreePath;
191
- } catch {
192
- if (!allowFallback) throw new WorktreeMissingError();
193
- if (process.env.DEBUG || process.env.TBD_DEBUG) console.warn("[tbd:paths] resolveDataSyncDir: worktree not found, falling back to direct path");
194
- _resolvedDataSyncDir = directPath;
195
- _resolvedBaseDir = baseDir;
196
- _resolvedAllowFallback = allowFallback;
197
- return directPath;
198
- }
277
+ } catch {}
278
+ if (!allowFallback) throw new WorktreeMissingError();
279
+ if (process.env.DEBUG || process.env.TBD_DEBUG) console.warn("[tbd:paths] resolveDataSyncDir: worktree not found, falling back to direct path");
280
+ return directPath;
199
281
  }
200
282
  /**
201
283
  * Resolve attic directory path.
@@ -256,7 +338,7 @@ const CHARS_PER_TOKEN = 3.5;
256
338
  * Current format version.
257
339
  * Bump this ONLY for breaking changes that require migration.
258
340
  */
259
- const CURRENT_FORMAT = "f03";
341
+ const CURRENT_FORMAT = "f04";
260
342
  /**
261
343
  * Initial format version for configs that don't have tbd_format field.
262
344
  */
@@ -296,6 +378,16 @@ const FORMAT_HISTORY = {
296
378
  "Removed separate docs: key"
297
379
  ],
298
380
  migration: "Migrates old config keys to new docs_cache structure"
381
+ },
382
+ f04: {
383
+ introduced: "0.2.0",
384
+ description: "Moves local issue sync worktree into the Git common directory",
385
+ changes: [
386
+ "Added sync.storage: git-common-dir-v1 to config.yml",
387
+ "Moved local data-sync worktree machinery to $GIT_COMMON_DIR/tbd/",
388
+ "Added $GIT_COMMON_DIR/tbd/layout.yml using the same tbd_format ID"
389
+ ],
390
+ migration: "Initializes shared common-dir sync layout before writing config.yml with tbd_format f04"
299
391
  }
300
392
  };
301
393
  /**
@@ -357,6 +449,29 @@ function migrate_f02_to_f03(config) {
357
449
  };
358
450
  }
359
451
  /**
452
+ * Migrate from f03 to f04.
453
+ * - Adds sync.storage marker for the Git common-dir shared worktree layout
454
+ * - Bumps tbd_format so old clients fail before writing legacy worktrees
455
+ */
456
+ function migrate_f03_to_f04(config) {
457
+ const changes = [];
458
+ const migrated = { ...config };
459
+ migrated.tbd_format = "f04";
460
+ changes.push("Updated tbd_format: f04");
461
+ migrated.sync = {
462
+ ...migrated.sync,
463
+ storage: "git-common-dir-v1"
464
+ };
465
+ changes.push("Added sync.storage: git-common-dir-v1");
466
+ return {
467
+ config: migrated,
468
+ fromFormat: "f03",
469
+ toFormat: "f04",
470
+ changed: changes.length > 0,
471
+ changes
472
+ };
473
+ }
474
+ /**
360
475
  * Detect the format version of a config.
361
476
  * Returns INITIAL_FORMAT ('f01') if no tbd_format field is present.
362
477
  */
@@ -406,6 +521,12 @@ function migrateToLatest(config) {
406
521
  currentFormat = "f03";
407
522
  allChanges.push(...result.changes);
408
523
  }
524
+ if (currentFormat === "f03") {
525
+ const result = migrate_f03_to_f04(current);
526
+ current = result.config;
527
+ currentFormat = "f04";
528
+ allChanges.push(...result.changes);
529
+ }
409
530
  return {
410
531
  config: current,
411
532
  fromFormat,
@@ -419,12 +540,27 @@ function migrateToLatest(config) {
419
540
  * Future format versions are considered incompatible (would need tbd upgrade).
420
541
  */
421
542
  function isCompatibleFormat(format) {
543
+ return isFormatCompatibleWithSupported(format, CURRENT_FORMAT);
544
+ }
545
+ /**
546
+ * Check whether a format version is compatible with a tbd client that supports
547
+ * versions up to supportedFormat. This makes the old-client contract testable:
548
+ * an f03 client must reject an f04 repository instead of writing legacy data.
549
+ */
550
+ function isFormatCompatibleWithSupported(format, supportedFormat) {
422
551
  const formatVersions = Object.keys(FORMAT_HISTORY);
423
- const currentIndex = formatVersions.indexOf(CURRENT_FORMAT);
552
+ const currentIndex = formatVersions.indexOf(supportedFormat);
424
553
  const checkIndex = formatVersions.indexOf(format);
425
554
  if (checkIndex === -1) return false;
426
555
  return checkIndex <= currentIndex;
427
556
  }
557
+ /**
558
+ * Build the standard message shown when a repository has a format newer than
559
+ * this tbd client supports.
560
+ */
561
+ function formatUpgradeMessage(subject, foundFormat, supportedFormat) {
562
+ return `This repository requires a newer version of tbd.\n${subject} format '${foundFormat}' is from a newer tbd version.\nThis tbd version supports up to format '${supportedFormat}'.\nUpgrade tbd: npm install -g get-tbd@latest`;
563
+ }
428
564
 
429
565
  //#endregion
430
566
  //#region src/file/config.ts
@@ -443,7 +579,7 @@ function isCompatibleFormat(format) {
443
579
  */
444
580
  var IncompatibleFormatError = class extends Error {
445
581
  constructor(foundFormat, supportedFormat) {
446
- super(`Config format '${foundFormat}' is from a newer tbd version.\nThis tbd version supports up to format '${supportedFormat}'.\nPlease upgrade tbd: npm install -g get-tbd@latest`);
582
+ super(formatUpgradeMessage("Config", foundFormat, supportedFormat));
447
583
  this.foundFormat = foundFormat;
448
584
  this.supportedFormat = supportedFormat;
449
585
  this.name = "IncompatibleFormatError";
@@ -467,7 +603,8 @@ function createDefaultConfig(version, prefix) {
467
603
  tbd_version: version,
468
604
  sync: {
469
605
  branch: SYNC_BRANCH,
470
- remote: "origin"
606
+ remote: "origin",
607
+ storage: "git-common-dir-v1"
471
608
  },
472
609
  display: { id_prefix: prefix },
473
610
  settings: {
@@ -513,18 +650,21 @@ async function readConfig(baseDir) {
513
650
  async function readConfigWithMigration(baseDir) {
514
651
  const data = parse(await readFile(join(baseDir, CONFIG_FILE), "utf-8"));
515
652
  checkFormatCompatibility(data);
653
+ const fromFormat = data.tbd_format;
516
654
  if (needsMigration(data)) {
517
655
  const result = migrateToLatest(data);
518
656
  return {
519
657
  config: ConfigSchema.parse(result.config),
520
658
  migrated: result.changed,
521
- changes: result.changes
659
+ changes: result.changes,
660
+ fromFormat
522
661
  };
523
662
  }
524
663
  return {
525
664
  config: ConfigSchema.parse(data),
526
665
  migrated: false,
527
- changes: []
666
+ changes: [],
667
+ fromFormat
528
668
  };
529
669
  }
530
670
  /**
@@ -638,5 +778,5 @@ async function markWelcomeSeen(baseDir) {
638
778
  }
639
779
 
640
780
  //#endregion
641
- export { getWorkspaceDir as A, TBD_GUIDELINES_DIR as C, WORKSPACES_DIR as D, TBD_TEMPLATES_DIR as E, resolveAtticDir as M, resolveDataSyncDir as N, WORKTREE_DIR as O, TBD_DOCS_DIR as S, TBD_SHORTCUTS_SYSTEM as T, DEFAULT_GUIDELINES_PATHS as _, isInitialized as a, SYNC_BRANCH as b, readConfigWithMigration as c, writeConfig as d, writeLocalState as f, DATA_SYNC_DIR_NAME as g, DATA_SYNC_DIR as h, initConfig as i, isValidWorkspaceName as j, WORKTREE_DIR_NAME as k, readLocalState as l, CHARS_PER_TOKEN as m, findTbdRoot as n, markWelcomeSeen as o, CURRENT_FORMAT as p, hasSeenWelcome as r, readConfig as s, IncompatibleFormatError as t, updateLocalState as u, DEFAULT_SHORTCUT_PATHS as v, TBD_SHORTCUTS_STANDARD as w, TBD_DIR as x, DEFAULT_TEMPLATE_PATHS as y };
642
- //# sourceMappingURL=config-DVap9omo.mjs.map
781
+ export { WORKSPACES_DIR as A, SYNC_BRANCH as C, TBD_SHORTCUTS_STANDARD as D, TBD_GUIDELINES_DIR as E, resolveDataSyncDir as F, resolveSharedTbdPaths as I, getWorkspaceDir as M, isValidWorkspaceName as N, TBD_SHORTCUTS_SYSTEM as O, resolveAtticDir as P, LEGACY_WORKTREE_DIR as S, TBD_DOCS_DIR as T, DATA_SYNC_DIR as _, isInitialized as a, DEFAULT_SHORTCUT_PATHS as b, readConfigWithMigration as c, writeConfig as d, writeLocalState as f, CHARS_PER_TOKEN as g, isCompatibleFormat as h, initConfig as i, WORKTREE_DIR_NAME as j, TBD_TEMPLATES_DIR as k, readLocalState as l, formatUpgradeMessage as m, findTbdRoot as n, markWelcomeSeen as o, CURRENT_FORMAT as p, hasSeenWelcome as r, readConfig as s, IncompatibleFormatError as t, updateLocalState as u, DATA_SYNC_DIR_NAME as v, TBD_DIR as w, DEFAULT_TEMPLATE_PATHS as x, DEFAULT_GUIDELINES_PATHS as y };
782
+ //# sourceMappingURL=config-BJz1m9eN.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config-BJz1m9eN.mjs","names":["parseYaml","parsePath"],"sources":["../src/lib/paths.ts","../src/lib/tbd-format.ts","../src/file/config.ts"],"sourcesContent":["/**\n * Centralized path constants for tbd.\n *\n * Directory structure (per spec):\n *\n * On main/dev branches:\n * .tbd/\n * Committed to the repo:\n * config.yml - Project configuration\n * .gitignore - Controls what's gitignored below\n * workspaces/ - Persistent state (outbox, named workspaces)\n * Gitignored (local only):\n * state.yml - Local state\n * docs/ - Installed documentation (regenerated on setup)\n *\n * In the Git common dir shared by all linked worktrees:\n * $GIT_COMMON_DIR/tbd/\n * layout.yml - Shared layout metadata\n * locks/data-sync.lock/ - Repo-scoped lock directory\n * backups/ - Local repair/migration backups\n * data-sync-worktree/ - Hidden worktree checkout of tbd-sync branch\n * .tbd/data-sync/ - issues/, mappings/, attic/, meta.yml\n *\n * On tbd-sync branch:\n * .tbd/\n * data-sync/\n * issues/\n * mappings/\n * attic/\n * meta.yml\n */\n\nimport { execFile } from 'node:child_process';\nimport { homedir } from 'node:os';\nimport { isAbsolute, join, resolve } from 'node:path';\nimport { promisify } from 'node:util';\n\n/** The tbd configuration directory on main branch */\nexport const TBD_DIR = '.tbd';\n\n/** The config file path */\nexport const CONFIG_FILE = join(TBD_DIR, 'config.yml');\n\n/** The local state file (gitignored) */\nexport const STATE_FILE = join(TBD_DIR, 'state.yml');\n\n/** The worktree directory name */\nexport const WORKTREE_DIR_NAME = 'data-sync-worktree';\n\n/** Legacy per-checkout worktree path used by f03 and earlier clients. */\nexport const LEGACY_WORKTREE_DIR = join(TBD_DIR, WORKTREE_DIR_NAME);\n\n/**\n * @internal Primary-checkout relative path to the shared sync worktree.\n *\n * Only valid when `.git` is a directory (i.e., the primary checkout). Production\n * code must call resolveSharedTbdPaths() instead: linked worktrees have a `.git`\n * file, so this constant resolves to the wrong location for them. Intended for\n * tests and the non-git fallback in resolveDataSyncDir().\n */\nexport const PRIMARY_CHECKOUT_WORKTREE_DIR = join('.git', 'tbd', WORKTREE_DIR_NAME);\n\n/** The data directory name on the sync branch */\nexport const DATA_SYNC_DIR_NAME = 'data-sync';\n\n/**\n * The data directory path as it appears on the tbd-sync branch.\n * In a normal checkout this same relative path is a legacy/wrong-location fallback;\n * production callers should resolve the absolute shared worktree path with\n * resolveDataSyncDir().\n */\nexport const DATA_SYNC_DIR = join(TBD_DIR, DATA_SYNC_DIR_NAME);\n\n/**\n * @internal Primary-checkout relative path to the synced data via the shared worktree.\n *\n * Same caveat as `PRIMARY_CHECKOUT_WORKTREE_DIR`: only valid for a primary checkout.\n * Production code should resolve the absolute path with resolveDataSyncDir(); this\n * constant is intended for tests and the non-git fallback path.\n */\nexport const PRIMARY_CHECKOUT_DATA_SYNC_DIR = join(\n PRIMARY_CHECKOUT_WORKTREE_DIR,\n TBD_DIR,\n DATA_SYNC_DIR_NAME,\n);\n\n/** Issues directory */\nexport const ISSUES_DIR = join(DATA_SYNC_DIR, 'issues');\n\n/** Mappings directory */\nexport const MAPPINGS_DIR = join(DATA_SYNC_DIR, 'mappings');\n\n/** Attic directory for conflict resolution */\nexport const ATTIC_DIR = join(DATA_SYNC_DIR, 'attic');\n\n/** Meta file for schema version */\nexport const META_FILE = join(DATA_SYNC_DIR, 'meta.yml');\n\n/** The sync branch name */\nexport const SYNC_BRANCH = 'tbd-sync';\n\n// =============================================================================\n// Git Common-Dir Shared Sync Paths\n// =============================================================================\n\nconst execFileAsync = promisify(execFile);\n\n/** Directory name under $GIT_COMMON_DIR for tbd local machinery. */\nexport const GIT_COMMON_TBD_DIR_NAME = 'tbd';\n\n/** Common-dir layout metadata file name. */\nexport const COMMON_DIR_LAYOUT_FILE_NAME = 'layout.yml';\n\n/** Shared lock directory name under $GIT_COMMON_DIR/tbd/. */\nexport const SHARED_LOCKS_DIR_NAME = 'locks';\n\n/** Shared backups directory name under $GIT_COMMON_DIR/tbd/. */\nexport const SHARED_BACKUPS_DIR_NAME = 'backups';\n\n/** Directory-lock name for shared data-sync operations. */\nexport const DATA_SYNC_LOCK_DIR_NAME = 'data-sync.lock';\n\n/**\n * Resolved Git common-dir paths for the repo-scoped sync layout.\n */\nexport interface SharedTbdPaths {\n /** Absolute Git common directory shared by all linked worktrees. */\n gitCommonDir: string;\n /** Absolute $GIT_COMMON_DIR/tbd path. */\n sharedTbdDir: string;\n /** Absolute shared hidden worktree path. */\n sharedWorktreePath: string;\n /** Absolute data-sync directory inside the shared worktree. */\n sharedDataSyncDir: string;\n /** Absolute common-dir layout metadata path. */\n sharedLayoutPath: string;\n /** Absolute shared lock directory parent. */\n sharedLocksDir: string;\n /** Absolute data-sync lock path. */\n sharedLockPath: string;\n /** Absolute shared backups directory. */\n sharedBackupsDir: string;\n}\n\n/**\n * Resolve Git's common directory from any checkout or linked worktree.\n */\nexport async function resolveGitCommonDir(cwd: string): Promise<string> {\n let output: string;\n try {\n const { stdout } = await execFileAsync('git', ['-C', cwd, 'rev-parse', '--git-common-dir'], {\n maxBuffer: 1024 * 1024,\n });\n output = stdout.trim();\n } catch {\n const { stdout } = await execFileAsync(\n 'git',\n ['-C', cwd, 'rev-parse', '--path-format=absolute', '--git-common-dir'],\n { maxBuffer: 1024 * 1024 },\n );\n output = stdout.trim();\n }\n\n if (!output) {\n throw new Error(`Unable to resolve Git common directory from ${cwd}`);\n }\n\n const gitCommonDir = isAbsolute(output) ? output : resolve(cwd, output);\n return realpath(gitCommonDir).catch(() => gitCommonDir);\n}\n\n/**\n * Build all shared tbd paths from an absolute Git common directory.\n */\nexport function buildSharedTbdPaths(gitCommonDir: string): SharedTbdPaths {\n const sharedTbdDir = join(gitCommonDir, GIT_COMMON_TBD_DIR_NAME);\n const sharedWorktreePath = join(sharedTbdDir, WORKTREE_DIR_NAME);\n const sharedDataSyncDir = join(sharedWorktreePath, TBD_DIR, DATA_SYNC_DIR_NAME);\n const sharedLayoutPath = join(sharedTbdDir, COMMON_DIR_LAYOUT_FILE_NAME);\n const sharedLocksDir = join(sharedTbdDir, SHARED_LOCKS_DIR_NAME);\n const sharedLockPath = join(sharedLocksDir, DATA_SYNC_LOCK_DIR_NAME);\n const sharedBackupsDir = join(sharedTbdDir, SHARED_BACKUPS_DIR_NAME);\n\n return {\n gitCommonDir,\n sharedTbdDir,\n sharedWorktreePath,\n sharedDataSyncDir,\n sharedLayoutPath,\n sharedLocksDir,\n sharedLockPath,\n sharedBackupsDir,\n };\n}\n\n/**\n * Resolve the shared tbd paths for the repository containing baseDir.\n */\nexport async function resolveSharedTbdPaths(baseDir: string): Promise<SharedTbdPaths> {\n return buildSharedTbdPaths(await resolveGitCommonDir(baseDir));\n}\n\n// =============================================================================\n// Workspace Paths (for sync failure recovery, backups, bulk editing)\n// =============================================================================\n\n/** The workspaces directory name within .tbd/ */\nexport const WORKSPACES_DIR_NAME = 'workspaces';\n\n/** Full path to workspaces directory: .tbd/workspaces/ */\nexport const WORKSPACES_DIR = join(TBD_DIR, WORKSPACES_DIR_NAME);\n\n/**\n * Get the path to a named workspace directory.\n *\n * Workspaces are stored at: .tbd/workspaces/{name}/\n *\n * @param workspaceName - The name of the workspace (e.g., 'outbox', 'my-feature')\n * @returns Path to the workspace directory\n */\nexport function getWorkspaceDir(workspaceName: string): string {\n return join(WORKSPACES_DIR, workspaceName);\n}\n\n/**\n * Get the path to a workspace's issues directory.\n *\n * @param workspaceName - The name of the workspace\n * @returns Path to the workspace's issues directory\n */\nexport function getWorkspaceIssuesDir(workspaceName: string): string {\n return join(getWorkspaceDir(workspaceName), 'issues');\n}\n\n/**\n * Get the path to a workspace's mappings directory.\n *\n * @param workspaceName - The name of the workspace\n * @returns Path to the workspace's mappings directory\n */\nexport function getWorkspaceMappingsDir(workspaceName: string): string {\n return join(getWorkspaceDir(workspaceName), 'mappings');\n}\n\n/**\n * Get the path to a workspace's attic directory.\n *\n * The attic stores conflict backups during workspace save operations.\n *\n * @param workspaceName - The name of the workspace\n * @returns Path to the workspace's attic directory\n */\nexport function getWorkspaceAtticDir(workspaceName: string): string {\n return join(getWorkspaceDir(workspaceName), 'attic');\n}\n\n/**\n * Validate a workspace name.\n *\n * Valid workspace names:\n * - Lowercase alphanumeric characters\n * - Hyphens and underscores allowed\n * - Must not be empty\n * - Must not contain path separators or dots at start\n *\n * @param name - The workspace name to validate\n * @returns true if the name is valid\n */\nexport function isValidWorkspaceName(name: string): boolean {\n if (!name || name.length === 0) {\n return false;\n }\n\n // Must not start with dot (hidden files)\n if (name.startsWith('.')) {\n return false;\n }\n\n // Only allow lowercase alphanumeric, hyphens, and underscores\n // No spaces, path separators, or special characters\n const validPattern = /^[a-z0-9][a-z0-9_-]*$/;\n return validPattern.test(name);\n}\n\n// =============================================================================\n// Documentation/Shortcuts Paths\n// =============================================================================\n\n/** Docs directory name within .tbd/ */\nexport const DOCS_DIR = 'docs';\n\n/** Shortcuts directory name within docs/ */\nexport const SHORTCUTS_DIR = 'shortcuts';\n\n/** System shortcuts directory name (core docs like skill-baseline.md) */\nexport const SYSTEM_DIR = 'system';\n\n/** Standard shortcuts directory name (workflow shortcuts) */\nexport const STANDARD_DIR = 'standard';\n\n/** Guidelines directory name (coding rules and best practices) */\nexport const GUIDELINES_DIR = 'guidelines';\n\n/** Templates directory name (document templates) */\nexport const TEMPLATES_DIR = 'templates';\n\n/** Full path to docs directory: .tbd/docs/ */\nexport const TBD_DOCS_DIR = join(TBD_DIR, DOCS_DIR);\n\n/** Full path to shortcuts directory: .tbd/docs/shortcuts/ */\nexport const TBD_SHORTCUTS_DIR = join(TBD_DOCS_DIR, SHORTCUTS_DIR);\n\n/** Full path to system shortcuts: .tbd/docs/shortcuts/system/ */\nexport const TBD_SHORTCUTS_SYSTEM = join(TBD_SHORTCUTS_DIR, SYSTEM_DIR);\n\n/** Full path to standard shortcuts: .tbd/docs/shortcuts/standard/ */\nexport const TBD_SHORTCUTS_STANDARD = join(TBD_SHORTCUTS_DIR, STANDARD_DIR);\n\n/** Full path to guidelines: .tbd/docs/guidelines/ (top-level, not under shortcuts) */\nexport const TBD_GUIDELINES_DIR = join(TBD_DOCS_DIR, GUIDELINES_DIR);\n\n/** Full path to templates: .tbd/docs/templates/ (top-level, not under shortcuts) */\nexport const TBD_TEMPLATES_DIR = join(TBD_DOCS_DIR, TEMPLATES_DIR);\n\n/** Built-in docs source paths (relative to package docs/) */\nexport const BUILTIN_SHORTCUTS_SYSTEM = join(SHORTCUTS_DIR, SYSTEM_DIR);\nexport const BUILTIN_SHORTCUTS_STANDARD = join(SHORTCUTS_DIR, STANDARD_DIR);\n\n/** Built-in guidelines source path (relative to package docs/) */\nexport const BUILTIN_GUIDELINES_DIR = GUIDELINES_DIR;\n\n/** Built-in templates source path (relative to package docs/) */\nexport const BUILTIN_TEMPLATES_DIR = TEMPLATES_DIR;\n\n/** Install directory name (header files for tool-specific installation) */\nexport const INSTALL_DIR = 'install';\n\n/** Built-in install source path (relative to package docs/) */\nexport const BUILTIN_INSTALL_DIR = INSTALL_DIR;\n\n/**\n * Default shortcut lookup paths (searched in order, relative to tbd root).\n * Earlier paths take precedence over later paths.\n * Note: Guidelines and templates are now separate top-level directories.\n */\nexport const DEFAULT_SHORTCUT_PATHS = [\n TBD_SHORTCUTS_SYSTEM, // .tbd/docs/shortcuts/system/\n TBD_SHORTCUTS_STANDARD, // .tbd/docs/shortcuts/standard/\n];\n\n/**\n * Default guidelines lookup paths (relative to tbd root).\n */\nexport const DEFAULT_GUIDELINES_PATHS = [\n TBD_GUIDELINES_DIR, // .tbd/docs/guidelines/\n];\n\n/**\n * Default template lookup paths (relative to tbd root).\n */\nexport const DEFAULT_TEMPLATE_PATHS = [\n TBD_TEMPLATES_DIR, // .tbd/docs/templates/\n];\n\n/**\n * Get the full path to an issue file.\n */\nexport function getIssuePath(issueId: string): string {\n return join(ISSUES_DIR, `${issueId}.md`);\n}\n\n/**\n * Get the full path to a mapping file.\n */\nexport function getMappingPath(name: string): string {\n return join(MAPPINGS_DIR, `${name}.yml`);\n}\n\n/**\n * Get the full path to an attic entry.\n */\nexport function getAtticPath(issueId: string, filename: string): string {\n return join(ATTIC_DIR, 'conflicts', issueId, filename);\n}\n\n// =============================================================================\n// Dynamic Path Resolution\n// =============================================================================\n\nimport { access, realpath } from 'node:fs/promises';\n\n/**\n * Options for resolveDataSyncDir.\n */\nexport interface ResolveDataSyncDirOptions {\n /**\n * Allow fallback to direct path when worktree is missing.\n * Set to true for test environments or diagnostic tools.\n * Default: true. When false and worktree is missing, throws WorktreeMissingError.\n */\n allowFallback?: boolean;\n}\n\n/**\n * Error thrown when worktree is missing and fallback is not allowed.\n * Defined inline to avoid circular dependency with errors.ts.\n */\nexport class WorktreeMissingError extends Error {\n constructor(\n message = \"Shared worktree not found under $GIT_COMMON_DIR/tbd/data-sync-worktree/. Run 'tbd doctor --fix' to repair.\",\n ) {\n super(message);\n this.name = 'WorktreeMissingError';\n }\n}\n\n/**\n * Cache for resolved data sync directory.\n * Reset when baseDir changes.\n */\nlet _resolvedDataSyncDir: string | null = null;\nlet _resolvedBaseDir: string | null = null;\nlet _resolvedAllowFallback: boolean | null = null;\n\n/**\n * Resolve the actual data sync directory path.\n *\n * This function detects whether we're running with a git worktree\n * (production) or in a test environment without worktree.\n *\n * Order of preference:\n * 1. Shared worktree path if it exists:\n * $GIT_COMMON_DIR/tbd/data-sync-worktree/.tbd/data-sync/\n * 2. Direct path as fallback (only if allowFallback: true, for tests/diagnostics)\n *\n * @param baseDir - The tbd root directory (from requireInit or findTbdRoot)\n * @param options - Options for path resolution\n * @returns Resolved data sync directory path\n * @throws WorktreeMissingError if worktree missing and allowFallback is false\n *\n * See: plan-2026-01-28-sync-worktree-recovery-and-hardening.md\n */\nexport async function resolveDataSyncDir(\n baseDir: string,\n options?: ResolveDataSyncDirOptions,\n): Promise<string> {\n const allowFallback = options?.allowFallback ?? true;\n\n // Return cached result if baseDir and options haven't changed\n if (\n _resolvedDataSyncDir &&\n _resolvedBaseDir === baseDir &&\n _resolvedAllowFallback === allowFallback\n ) {\n return _resolvedDataSyncDir;\n }\n\n let worktreePath: string | null = null;\n try {\n worktreePath = (await resolveSharedTbdPaths(baseDir)).sharedDataSyncDir;\n } catch {\n // Not in a git repository or git is unavailable. Check the static primary-checkout\n // path for unit tests before falling back to the direct diagnostic path.\n worktreePath = join(baseDir, PRIMARY_CHECKOUT_DATA_SYNC_DIR);\n }\n const directPath = join(baseDir, DATA_SYNC_DIR);\n\n // Check if worktree path exists\n if (worktreePath) {\n try {\n await access(worktreePath);\n _resolvedDataSyncDir = worktreePath;\n _resolvedBaseDir = baseDir;\n _resolvedAllowFallback = allowFallback;\n return worktreePath;\n } catch {\n // Worktree doesn't exist\n }\n }\n\n {\n // Worktree doesn't exist\n if (!allowFallback) {\n throw new WorktreeMissingError();\n }\n\n // Fallback to direct path (test mode or diagnostic tools)\n // Note: In production, sync.ts checks worktree health before calling this\n // Debug warning to help detect unintended fallback usage\n if (process.env.DEBUG || process.env.TBD_DEBUG) {\n console.warn(\n '[tbd:paths] resolveDataSyncDir: worktree not found, falling back to direct path',\n );\n }\n // Intentionally do NOT cache the fallback result: a later call after the\n // worktree is created must rediscover the real path, not keep returning\n // the stale fallback.\n return directPath;\n }\n}\n\n/**\n * Resolve issues directory path.\n */\nexport async function resolveIssuesDir(\n baseDir: string,\n options?: ResolveDataSyncDirOptions,\n): Promise<string> {\n const dataSyncDir = await resolveDataSyncDir(baseDir, options);\n return join(dataSyncDir, 'issues');\n}\n\n/**\n * Resolve mappings directory path.\n */\nexport async function resolveMappingsDir(\n baseDir: string,\n options?: ResolveDataSyncDirOptions,\n): Promise<string> {\n const dataSyncDir = await resolveDataSyncDir(baseDir, options);\n return join(dataSyncDir, 'mappings');\n}\n\n/**\n * Resolve attic directory path.\n */\nexport async function resolveAtticDir(\n baseDir: string,\n options?: ResolveDataSyncDirOptions,\n): Promise<string> {\n const dataSyncDir = await resolveDataSyncDir(baseDir, options);\n return join(dataSyncDir, 'attic');\n}\n\n/**\n * Clear the resolved path cache.\n * Call this when the repository state changes (e.g., after init).\n */\nexport function clearPathCache(): void {\n _resolvedDataSyncDir = null;\n _resolvedBaseDir = null;\n _resolvedAllowFallback = null;\n}\n\n// =============================================================================\n// Doc Path Resolution\n// =============================================================================\n\n/**\n * Resolve a doc path for consistent handling across the codebase.\n *\n * Path resolution rules:\n * - Absolute paths (starting with /): used as-is\n * - Home directory paths (starting with ~/): expanded to user home directory\n * - Relative paths: resolved from tbd root (baseDir)\n *\n * @param docPath - The path to resolve\n * @param baseDir - The tbd root directory (parent of .tbd/)\n * @returns Resolved absolute path\n *\n * @example\n * // Absolute path - returned as-is\n * resolveDocPath('/usr/local/docs/file.md') // => '/usr/local/docs/file.md'\n *\n * // Home path - expanded\n * resolveDocPath('~/docs/file.md') // => '/Users/username/docs/file.md'\n *\n * // Relative path - resolved from baseDir\n * resolveDocPath('docs/file.md', '/project') // => '/project/docs/file.md'\n */\nexport function resolveDocPath(docPath: string, baseDir: string): string {\n // Handle home directory expansion\n if (docPath.startsWith('~/')) {\n return join(homedir(), docPath.slice(2));\n }\n\n // Absolute paths used as-is\n if (isAbsolute(docPath)) {\n return docPath;\n }\n\n // Relative paths resolved from baseDir (tbd root)\n return join(baseDir, docPath);\n}\n\n// =============================================================================\n// Token Estimation Settings\n// =============================================================================\n\n/**\n * Characters per token ratio for estimating token counts.\n *\n * Based on research of OpenAI (tiktoken) and Claude tokenizers:\n * - Pure English prose: ~4-5 chars/token\n * - Code and symbols: ~3 chars/token\n * - Mixed markdown/code docs: ~3.5 chars/token\n *\n * We use 3.5 as our docs are markdown with code examples.\n * This provides ~15-20% accuracy, sufficient for cost estimation.\n */\nexport const CHARS_PER_TOKEN = 3.5;\n","/**\n * tbd Directory Format Versioning\n * ================================\n *\n * This file is the SINGLE SOURCE OF TRUTH for .tbd/ directory format versions.\n *\n * WHEN TO BUMP THE FORMAT VERSION:\n * - Bump when changes REQUIRE migration (deleting files, changing formats, moving files)\n * - **Bump when changing config schema** (adding, removing, or modifying fields)\n * - **Bump when the shape of a generated agent-integration surface changes** (e.g. the\n * managed AGENTS.md block). This same format is stamped there via\n * AGENT_INTEGRATION_FORMAT (integration-paths.ts), so there is ONE format code across\n * all tbd-managed surfaces.\n * - Do NOT bump for additive changes that don't affect config.yml (new directories, etc.)\n *\n * HOW TO ADD A NEW FORMAT VERSION:\n * 1. Add entry to FORMAT_HISTORY with detailed description\n * 2. Implement migrate_fXX_to_fYY() function\n * 3. Add case to migrateToLatest()\n * 4. Update CURRENT_FORMAT\n * 5. Add tests for the migration path\n *\n * FORWARD COMPATIBILITY POLICY:\n * ConfigSchema uses Zod's strip() mode, which discards unknown fields. To prevent\n * data loss when users mix tbd versions:\n *\n * 1. When changing config schema, bump the format version (e.g., f03 → f04)\n * 2. config.ts checks format compatibility via isCompatibleFormat()\n * 3. Older tbd versions will error with \"format 'fXX' is from a newer tbd version\"\n * 4. The error tells users to upgrade: npm install -g get-tbd@latest\n *\n * This ensures older versions fail fast rather than silently corrupting config.\n * See ConfigSchema in schemas.ts and checkFormatCompatibility() in config.ts.\n */\n\n// =============================================================================\n// Format Constants\n// =============================================================================\n\n/**\n * Current format version.\n * Bump this ONLY for breaking changes that require migration.\n */\nexport const CURRENT_FORMAT = 'f04';\n\n/**\n * Initial format version for configs that don't have tbd_format field.\n */\nexport const INITIAL_FORMAT = 'f01';\n\n// =============================================================================\n// Format History\n// =============================================================================\n\n/**\n * Complete history of format versions with their changes.\n * This serves as documentation and enables version detection.\n */\nexport const FORMAT_HISTORY = {\n f01: {\n introduced: '0.1.0',\n description: 'Initial format',\n structure: {\n 'config.yml': 'Project configuration',\n 'state.yml': 'Local state (gitignored)',\n 'docs/': 'Documentation cache (gitignored)',\n 'issues/': 'Issue YAML files',\n },\n },\n f02: {\n introduced: '0.1.5',\n description: 'Adds configurable doc_cache',\n changes: [\n 'Added doc_cache: key to config.yml for configurable doc sources',\n 'Added settings.doc_auto_sync_hours for automatic doc refresh',\n 'Added last_doc_sync_at to state.yml for tracking sync time',\n ],\n migration: 'Populates default doc_cache config from bundled docs',\n },\n f03: {\n introduced: '0.1.6',\n description: 'Consolidates docs_cache config structure',\n changes: [\n 'Consolidated doc_cache: and docs: into single docs_cache: key',\n 'Moved doc_cache: -> docs_cache.files:',\n 'Moved docs.paths: -> docs_cache.lookup_path:',\n 'Removed separate docs: key',\n ],\n migration: 'Migrates old config keys to new docs_cache structure',\n },\n f04: {\n introduced: '0.2.0',\n description: 'Moves local issue sync worktree into the Git common directory',\n changes: [\n 'Added sync.storage: git-common-dir-v1 to config.yml',\n 'Moved local data-sync worktree machinery to $GIT_COMMON_DIR/tbd/',\n 'Added $GIT_COMMON_DIR/tbd/layout.yml using the same tbd_format ID',\n ],\n migration:\n 'Initializes shared common-dir sync layout before writing config.yml with tbd_format f04',\n },\n} as const;\n\nexport type FormatVersion = keyof typeof FORMAT_HISTORY;\n\n// =============================================================================\n// Migration Types\n// =============================================================================\n\n/**\n * Raw config data before parsing/validation.\n * Used during migration when we need to work with potentially old formats.\n */\nexport interface RawConfig {\n tbd_format?: string;\n tbd_version?: string;\n sync?: {\n branch?: string;\n remote?: string;\n storage?: 'git-common-dir-v1';\n };\n display?: {\n id_prefix?: string;\n };\n settings?: {\n auto_sync?: boolean;\n doc_auto_sync_hours?: number;\n };\n // Old format (f02 and earlier)\n docs?: {\n paths?: string[];\n };\n doc_cache?: Record<string, string>;\n // New format (f03+)\n docs_cache?: {\n files?: Record<string, string>;\n lookup_path?: string[];\n };\n}\n\n/**\n * Result of a migration operation.\n */\nexport interface MigrationResult {\n /** The migrated config */\n config: RawConfig;\n /** Format version before migration */\n fromFormat: FormatVersion;\n /** Format version after migration */\n toFormat: FormatVersion;\n /** Whether any changes were made */\n changed: boolean;\n /** Description of changes made */\n changes: string[];\n}\n\n// =============================================================================\n// Migration Functions\n// =============================================================================\n\n/**\n * Migrate from f01 to f02.\n * - Adds tbd_format field\n * - Adds doc_auto_sync_hours setting (default: 24)\n * - doc_cache will be populated separately during setup (requires file system access)\n */\nfunction migrate_f01_to_f02(config: RawConfig): MigrationResult {\n const changes: string[] = [];\n const migrated = { ...config };\n\n // Add format version\n migrated.tbd_format = 'f02';\n changes.push('Added tbd_format: f02');\n\n // Ensure settings exists and add doc_auto_sync_hours\n migrated.settings ??= {};\n if (migrated.settings.doc_auto_sync_hours === undefined) {\n migrated.settings.doc_auto_sync_hours = 24;\n changes.push('Added settings.doc_auto_sync_hours: 24');\n }\n\n // Note: doc_cache is intentionally NOT added here.\n // It will be populated during setup when we have access to the file system\n // and can enumerate the bundled docs.\n\n return {\n config: migrated,\n fromFormat: 'f01',\n toFormat: 'f02',\n changed: changes.length > 0,\n changes,\n };\n}\n\n/**\n * Migrate from f02 to f03.\n * - Consolidates doc_cache: and docs: into docs_cache:\n * - Moves doc_cache: -> docs_cache.files:\n * - Moves docs.paths: -> docs_cache.lookup_path:\n * - Removes separate docs: and doc_cache: keys\n */\nfunction migrate_f02_to_f03(config: RawConfig): MigrationResult {\n const changes: string[] = [];\n const migrated = { ...config };\n\n // Update format version\n migrated.tbd_format = 'f03';\n changes.push('Updated tbd_format: f03');\n\n // Initialize docs_cache if it doesn't exist\n migrated.docs_cache ??= {};\n\n // Migrate doc_cache -> docs_cache.files\n if (migrated.doc_cache && Object.keys(migrated.doc_cache).length > 0) {\n migrated.docs_cache.files = { ...migrated.doc_cache };\n changes.push('Moved doc_cache: -> docs_cache.files:');\n delete migrated.doc_cache;\n }\n\n // Migrate docs.paths -> docs_cache.lookup_path\n if (migrated.docs?.paths && migrated.docs.paths.length > 0) {\n migrated.docs_cache.lookup_path = [...migrated.docs.paths];\n changes.push('Moved docs.paths: -> docs_cache.lookup_path:');\n }\n\n // Remove old docs: key\n if (migrated.docs) {\n delete migrated.docs;\n changes.push('Removed docs: key');\n }\n\n return {\n config: migrated,\n fromFormat: 'f02',\n toFormat: 'f03',\n changed: changes.length > 0,\n changes,\n };\n}\n\n/**\n * Migrate from f03 to f04.\n * - Adds sync.storage marker for the Git common-dir shared worktree layout\n * - Bumps tbd_format so old clients fail before writing legacy worktrees\n */\nfunction migrate_f03_to_f04(config: RawConfig): MigrationResult {\n const changes: string[] = [];\n const migrated = { ...config };\n\n migrated.tbd_format = 'f04';\n changes.push('Updated tbd_format: f04');\n\n migrated.sync = { ...migrated.sync, storage: 'git-common-dir-v1' };\n changes.push('Added sync.storage: git-common-dir-v1');\n\n return {\n config: migrated,\n fromFormat: 'f03',\n toFormat: 'f04',\n changed: changes.length > 0,\n changes,\n };\n}\n\n// =============================================================================\n// Public API\n// =============================================================================\n\n/**\n * Detect the format version of a config.\n * Returns INITIAL_FORMAT ('f01') if no tbd_format field is present.\n */\nexport function detectFormat(config: RawConfig): FormatVersion {\n const format = config.tbd_format;\n if (!format) {\n return INITIAL_FORMAT;\n }\n if (format in FORMAT_HISTORY) {\n return format as FormatVersion;\n }\n // Unknown format - treat as latest (will fail validation if incompatible)\n return CURRENT_FORMAT;\n}\n\n/**\n * Check if a config needs migration.\n */\nexport function needsMigration(config: RawConfig): boolean {\n const currentFormat = detectFormat(config);\n return currentFormat !== CURRENT_FORMAT;\n}\n\n/**\n * Migrate a config to the latest format version.\n *\n * This function applies all necessary migrations in sequence.\n * It does NOT populate doc_cache - that requires file system access\n * and should be done separately during setup.\n *\n * @param config - The raw config to migrate\n * @returns Migration result with the migrated config and change log\n */\nexport function migrateToLatest(config: RawConfig): MigrationResult {\n const fromFormat = detectFormat(config);\n\n if (fromFormat === CURRENT_FORMAT) {\n return {\n config,\n fromFormat,\n toFormat: CURRENT_FORMAT,\n changed: false,\n changes: [],\n };\n }\n\n let current = config;\n let currentFormat: FormatVersion = fromFormat;\n const allChanges: string[] = [];\n\n // Apply migrations in sequence\n if (currentFormat === 'f01') {\n const result = migrate_f01_to_f02(current);\n current = result.config;\n currentFormat = 'f02' as FormatVersion;\n allChanges.push(...result.changes);\n }\n\n if (currentFormat === 'f02') {\n const result = migrate_f02_to_f03(current);\n current = result.config;\n currentFormat = 'f03' as FormatVersion;\n allChanges.push(...result.changes);\n }\n\n if (currentFormat === 'f03') {\n const result = migrate_f03_to_f04(current);\n current = result.config;\n currentFormat = 'f04' as FormatVersion;\n allChanges.push(...result.changes);\n }\n\n return {\n config: current,\n fromFormat,\n toFormat: currentFormat,\n changed: allChanges.length > 0,\n changes: allChanges,\n };\n}\n\n/**\n * Check if a format version is compatible with the current tbd version.\n * Future format versions are considered incompatible (would need tbd upgrade).\n */\nexport function isCompatibleFormat(format: string): boolean {\n return isFormatCompatibleWithSupported(format, CURRENT_FORMAT);\n}\n\n/**\n * Check whether a format version is compatible with a tbd client that supports\n * versions up to supportedFormat. This makes the old-client contract testable:\n * an f03 client must reject an f04 repository instead of writing legacy data.\n */\nexport function isFormatCompatibleWithSupported(\n format: string,\n supportedFormat: FormatVersion,\n): boolean {\n const formatVersions = Object.keys(FORMAT_HISTORY);\n const currentIndex = formatVersions.indexOf(supportedFormat);\n const checkIndex = formatVersions.indexOf(format);\n\n if (checkIndex === -1) {\n // Unknown format - might be from a newer tbd version\n return false;\n }\n\n // Compatible if same or older format (we can migrate up)\n return checkIndex <= currentIndex;\n}\n\n/**\n * Build the standard message shown when a repository has a format newer than\n * this tbd client supports.\n */\nexport function formatUpgradeMessage(\n subject: string,\n foundFormat: string,\n supportedFormat: string,\n): string {\n return (\n `This repository requires a newer version of tbd.\\n` +\n `${subject} format '${foundFormat}' is from a newer tbd version.\\n` +\n `This tbd version supports up to format '${supportedFormat}'.\\n` +\n `Upgrade tbd: npm install -g get-tbd@latest`\n );\n}\n\n/**\n * Get a human-readable description of what migrations will be applied.\n */\nexport function describeMigration(fromFormat: FormatVersion): string[] {\n const descriptions: string[] = [];\n let current = fromFormat;\n\n if (current === 'f01') {\n descriptions.push('f01 → f02: Add doc_cache configuration support');\n current = 'f02';\n }\n\n if (current === 'f02') {\n descriptions.push('f02 → f03: Consolidate doc_cache and docs into docs_cache');\n current = 'f03';\n }\n\n if (current === 'f03') {\n descriptions.push('f03 → f04: Move local sync worktree to Git common directory');\n current = 'f04';\n }\n\n return descriptions;\n}\n","/**\n * Config file operations.\n *\n * Config is stored at .tbd/config.yml and contains project-level settings.\n *\n * ⚠️ FORMAT VERSIONING: See tbd-format.ts for version history and migration rules.\n *\n * See: tbd-design.md §2.2.2 Config File\n */\n\nimport { readFile, mkdir, access } from 'node:fs/promises';\nimport { join, dirname, parse as parsePath } from 'node:path';\nimport { writeFile } from 'atomically';\nimport { parse as parseYaml } from 'yaml';\n\nimport { sortKeys, stringifyYaml } from '../utils/yaml-utils.js';\nimport type { Config, LocalState } from '../lib/types.js';\nimport {\n ConfigSchema,\n LocalStateSchema,\n CONFIG_FIELD_ORDER,\n LOCAL_STATE_FIELD_ORDER,\n} from '../lib/schemas.js';\nimport { CONFIG_FILE, STATE_FILE, SYNC_BRANCH } from '../lib/paths.js';\nimport {\n CURRENT_FORMAT,\n formatUpgradeMessage,\n needsMigration,\n migrateToLatest,\n isCompatibleFormat,\n type RawConfig,\n} from '../lib/tbd-format.js';\n\n/**\n * Error thrown when the config format version is from a newer tbd version.\n * This prevents older tbd versions from silently stripping new config fields.\n */\nexport class IncompatibleFormatError extends Error {\n constructor(\n public readonly foundFormat: string,\n public readonly supportedFormat: string,\n ) {\n super(formatUpgradeMessage('Config', foundFormat, supportedFormat));\n this.name = 'IncompatibleFormatError';\n }\n}\n\n/**\n * Check if config format is compatible, throw if not.\n * This prevents older tbd versions from silently stripping fields added by newer versions.\n */\nfunction checkFormatCompatibility(data: RawConfig): void {\n const format = data.tbd_format;\n if (format && !isCompatibleFormat(format)) {\n throw new IncompatibleFormatError(format, CURRENT_FORMAT);\n }\n}\n\n/**\n * Create default config for a new project.\n * @param prefix - Required: the project prefix for display IDs (e.g., \"proj\", \"myapp\")\n */\nfunction createDefaultConfig(version: string, prefix: string): Config {\n return ConfigSchema.parse({\n tbd_format: CURRENT_FORMAT,\n tbd_version: version,\n sync: {\n branch: SYNC_BRANCH,\n remote: 'origin',\n storage: 'git-common-dir-v1',\n },\n display: {\n id_prefix: prefix,\n },\n settings: {\n auto_sync: false,\n doc_auto_sync_hours: 24,\n },\n });\n}\n\n/**\n * Initialize a new config file with default settings.\n * Creates .tbd directory if it doesn't exist.\n * @param prefix - Required: the project prefix for display IDs (e.g., \"proj\", \"myapp\")\n */\nexport async function initConfig(\n baseDir: string,\n version: string,\n prefix: string,\n): Promise<Config> {\n const tbdDir = join(baseDir, '.tbd');\n await mkdir(tbdDir, { recursive: true });\n\n const config = createDefaultConfig(version, prefix);\n await writeConfig(baseDir, config);\n\n return config;\n}\n\n/**\n * Read config from file with automatic migration if needed.\n *\n * ⚠️ FORMAT VERSIONING: See tbd-format.ts for version history and migration rules.\n *\n * @throws {IncompatibleFormatError} If config is from a newer tbd version.\n * @throws If config file doesn't exist or is invalid.\n */\nexport async function readConfig(baseDir: string): Promise<Config> {\n const configPath = join(baseDir, CONFIG_FILE);\n const content = await readFile(configPath, 'utf-8');\n const data = parseYaml(content) as RawConfig;\n\n // Check for incompatible (future) format versions first\n checkFormatCompatibility(data);\n\n // Check if migration is needed (for older formats)\n if (needsMigration(data)) {\n const result = migrateToLatest(data);\n // Note: We don't automatically write the migrated config here.\n // Migration writes should be explicit via writeConfig() after setup.\n return ConfigSchema.parse(result.config);\n }\n\n return ConfigSchema.parse(data);\n}\n\n/**\n * Read config from file, returning migration info if a migration was applied.\n * Use this when you need to know if the config was migrated.\n *\n * @throws {IncompatibleFormatError} If config is from a newer tbd version.\n */\nexport async function readConfigWithMigration(baseDir: string): Promise<{\n config: Config;\n migrated: boolean;\n changes: string[];\n /**\n * The `tbd_format` value found in the file before migration. Useful for showing\n * the user what was upgraded (e.g., \"f03 → f04\"). `undefined` for very old\n * configs that have no `tbd_format` field.\n */\n fromFormat: string | undefined;\n}> {\n const configPath = join(baseDir, CONFIG_FILE);\n const content = await readFile(configPath, 'utf-8');\n const data = parseYaml(content) as RawConfig;\n\n // Check for incompatible (future) format versions first\n checkFormatCompatibility(data);\n\n const fromFormat = data.tbd_format;\n\n if (needsMigration(data)) {\n const result = migrateToLatest(data);\n return {\n config: ConfigSchema.parse(result.config),\n migrated: result.changed,\n changes: result.changes,\n fromFormat,\n };\n }\n\n return {\n config: ConfigSchema.parse(data),\n migrated: false,\n changes: [],\n fromFormat,\n };\n}\n\n/**\n * Write config to file with explanatory comments.\n */\nexport async function writeConfig(baseDir: string, config: Config): Promise<void> {\n const configPath = join(baseDir, CONFIG_FILE);\n\n // Sort keys using canonical field order, then serialize with compact output.\n // sortMapEntries: false preserves our manual ordering.\n const sorted = sortKeys(config as unknown as Record<string, unknown>, CONFIG_FIELD_ORDER);\n const yaml = stringifyYaml(sorted, { lineWidth: 0, sortMapEntries: false });\n\n // Add explanatory comments for docs_cache section\n let content = yaml;\n if (config.docs_cache && Object.keys(config.docs_cache).length > 0) {\n const docsCacheComment = `# 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).\n`;\n content = content.replace('docs_cache:', docsCacheComment + 'docs_cache:');\n }\n\n await writeFile(configPath, content);\n}\n\n/**\n * Check if tbd is properly initialized in the given directory.\n * Returns true only if .tbd/config.yml exists (not just a .tbd/ directory).\n *\n * This prevents spurious .tbd/ directories (e.g., containing only state.yml\n * created by a bug) from being mistaken for tbd roots. A valid tbd root\n * always has config.yml created during `tbd init`.\n */\nasync function hasTbdDir(dir: string): Promise<boolean> {\n const configPath = join(dir, CONFIG_FILE);\n try {\n await access(configPath);\n return true;\n } catch {\n return false;\n }\n}\n\n/**\n * Find the tbd repository root by walking up the directory tree.\n * Similar to how git finds .git/ directories.\n *\n * @param startDir - Directory to start searching from\n * @returns The tbd root directory path, or null if not found\n */\nexport async function findTbdRoot(startDir: string): Promise<string | null> {\n let currentDir = startDir;\n const { root } = parsePath(startDir);\n\n while (currentDir !== root) {\n if (await hasTbdDir(currentDir)) {\n return currentDir;\n }\n currentDir = dirname(currentDir);\n }\n\n // Check root directory as well\n if (await hasTbdDir(root)) {\n return root;\n }\n\n return null;\n}\n\n/**\n * Check if tbd is initialized in the given directory or any parent directory.\n * Walks up the directory tree looking for .tbd/.\n */\nexport async function isInitialized(baseDir: string): Promise<boolean> {\n const root = await findTbdRoot(baseDir);\n return root !== null;\n}\n\n// =============================================================================\n// Local State Operations\n// =============================================================================\n\n/**\n * Read local state from .tbd/state.yml\n * Returns empty state if file doesn't exist.\n */\nexport async function readLocalState(baseDir: string): Promise<LocalState> {\n const statePath = join(baseDir, STATE_FILE);\n try {\n const content = await readFile(statePath, 'utf-8');\n const data: unknown = parseYaml(content);\n return LocalStateSchema.parse(data ?? {});\n } catch {\n // File doesn't exist or is invalid - return empty state\n return {};\n }\n}\n\n/**\n * Write local state to .tbd/state.yml\n *\n * Uses `atomically` for safe writes (atomic rename, auto parent-dir creation).\n * However, we intentionally guard against .tbd/ not existing: `atomically`\n * would auto-create it, which is wrong if baseDir is a subdirectory rather\n * than the true tbd root. Only `tbd init` (via initConfig) should create .tbd/.\n */\nexport async function writeLocalState(baseDir: string, state: LocalState): Promise<void> {\n // Guard: refuse to write if .tbd/ directory doesn't exist.\n // Without this, `atomically` would auto-create .tbd/ in subdirectories,\n // producing spurious directories that confuse findTbdRoot().\n const tbdDir = join(baseDir, '.tbd');\n try {\n await access(tbdDir);\n } catch {\n throw new Error(\n `Cannot write state: .tbd/ directory does not exist at ${baseDir}. ` +\n `Run 'tbd init' first or ensure the correct tbd root is being used.`,\n );\n }\n\n const statePath = join(baseDir, STATE_FILE);\n\n // Sort keys using canonical field order, then serialize with compact output.\n // sortMapEntries: false preserves our manual ordering.\n const sorted = sortKeys(state as unknown as Record<string, unknown>, LOCAL_STATE_FIELD_ORDER);\n const yaml = stringifyYaml(sorted, { lineWidth: 0, sortMapEntries: false });\n\n await writeFile(statePath, yaml);\n}\n\n/**\n * Update specific fields in local state (merge with existing).\n */\nexport async function updateLocalState(\n baseDir: string,\n updates: Partial<LocalState>,\n): Promise<LocalState> {\n const current = await readLocalState(baseDir);\n const updated = { ...current, ...updates };\n await writeLocalState(baseDir, updated);\n return updated;\n}\n\n// =============================================================================\n// Welcome State Operations\n// =============================================================================\n\n/**\n * Check if the user has seen the welcome message.\n */\nexport async function hasSeenWelcome(baseDir: string): Promise<boolean> {\n const state = await readLocalState(baseDir);\n return state.welcome_seen === true;\n}\n\n/**\n * Mark the welcome message as seen.\n */\nexport async function markWelcomeSeen(baseDir: string): Promise<void> {\n await updateLocalState(baseDir, { welcome_seen: true });\n}\n"],"mappings":"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"}
@@ -1,3 +1,3 @@
1
- import { a as isInitialized, c as readConfigWithMigration, d as writeConfig, f as writeLocalState, i as initConfig, l as readLocalState, n as findTbdRoot, o as markWelcomeSeen, r as hasSeenWelcome, s as readConfig, t as IncompatibleFormatError, u as updateLocalState } from "./config-DVap9omo.mjs";
1
+ import { a as isInitialized, c as readConfigWithMigration, d as writeConfig, f as writeLocalState, i as initConfig, l as readLocalState, n as findTbdRoot, o as markWelcomeSeen, r as hasSeenWelcome, s as readConfig, t as IncompatibleFormatError, u as updateLocalState } from "./config-BJz1m9eN.mjs";
2
2
 
3
3
  export { readConfig };
@@ -258,7 +258,7 @@ opinionated rules with concrete examples, built from months of heavy agentic cod
258
258
  Plus guidelines on [coding rules](packages/tbd/docs/guidelines/general-coding-rules.md),
259
259
  [comment quality](packages/tbd/docs/guidelines/general-comment-rules.md),
260
260
  [commit conventions](packages/tbd/docs/guidelines/commit-conventions.md), and
261
- [style](packages/tbd/docs/guidelines/general-style-rules.md).
261
+ [documentation style](packages/tbd/docs/guidelines/common-doc-guidelines.md).
262
262
 
263
263
  You can also add your own team’s guidelines from any URL:
264
264
 
@@ -610,3 +610,7 @@ See [docs/development.md](docs/development.md) for build and test instructions.
610
610
  ## License
611
611
 
612
612
  MIT
613
+
614
+ <!-- This document follows common-doc-guidelines.md.
615
+ See github.com/jlevy/practical-prose and review guidelines before editing.
616
+ -->
@@ -77,3 +77,7 @@ For the following areas:
77
77
 
78
78
  - When doing normal refactoring or reorganizing code, REMOVE deprecated functions,
79
79
  methods, classes, or files completely if backward compatibility is not needed.
80
+
81
+ <!-- This document follows common-doc-guidelines.md.
82
+ See github.com/jlevy/practical-prose and review guidelines before editing.
83
+ -->
@@ -79,7 +79,8 @@ covering the same architectural scope but using Bun-native tooling wherever poss
79
79
 
80
80
  The recommended stack uses **Bun workspaces** for dependency management, **Bunup** for
81
81
  building ESM (or dual ESM/CJS) outputs with TypeScript declarations, **Changesets**
82
- (with Bun workarounds) for versioning and release automation, **Biome** for formatting
82
+ (multi-package monorepos, with Bun workarounds) or **tag-triggered OIDC publishing**
83
+ (single-package repos) for versioning and release automation, **Biome** for formatting
83
84
  and linting, **publint** for package validation, and **lefthook** for git hooks.
84
85
  The architecture also covers Bun’s unique capability for **compiling standalone
85
86
  executables** — a native binary distribution path unavailable in the pnpm ecosystem.
@@ -787,12 +788,13 @@ publint is runtime-agnostic.
787
788
 
788
789
  #### Changesets (with Bun Workarounds)
789
790
 
790
- **Status**: Recommended (with workarounds)
791
+ **Status**: Recommended for multi-package monorepos, with workarounds (for a single
792
+ published package, prefer the tag-triggered approach in this section)
791
793
 
792
794
  **Details**:
793
795
 
794
- Changesets is the de facto standard for monorepo versioning, but it has known issues
795
- with Bun workspaces.
796
+ Changesets is the de facto standard for multi-package monorepo versioning, but it has
797
+ known issues with Bun workspaces.
796
798
  The key problem is that `changeset version` does not resolve `workspace:*` references to
797
799
  actual version numbers, which breaks published packages.
798
800
 
@@ -1387,6 +1389,16 @@ Instead of using the `changesets/action` GitHub Action, an alternative approach
1387
1389
  tags to trigger releases with npm provenance attestation via OIDC. This is simpler for
1388
1390
  projects that prefer manual version control and want provenance guarantees.
1389
1391
 
1392
+ > **When to prefer this over Changesets (LLM-era note):** For a **single published
1393
+ > package**, Changesets’ main wins (multi-package coordination, per-PR changelog
1394
+ > accumulation) mostly evaporate while its ceremony stays — and Bun adds the extra
1395
+ > `workspace:*` workarounds above.
1396
+ > When releases are cut by an agent/maintainer who assembles notes from clean
1397
+ > conventional commits at release time (see a release-notes template), tag-triggered
1398
+ > publishing is simpler: clean commits → bump + `## X.Y.Z` CHANGELOG section → tag →
1399
+ > auto-publish. Keep Changesets when you publish several interdependent packages or want
1400
+ > contributors to declare intent in each PR.
1401
+
1390
1402
  **`.github/workflows/release.yml`**:
1391
1403
 
1392
1404
  ```yaml
@@ -2903,3 +2915,7 @@ This is a valid pattern — enable in the base, override in packages that need i
2903
2915
  | CI | 3-OS matrix + separate lint job |
2904
2916
  | Release | Tag-triggered OIDC with npm provenance |
2905
2917
  | Package validation | publint `^0.3.17` |
2918
+
2919
+ <!-- This document follows common-doc-guidelines.md.
2920
+ See github.com/jlevy/practical-prose and review guidelines before editing.
2921
+ -->