get-tbd 0.2.2 → 0.3.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 (86) hide show
  1. package/README.md +38 -6
  2. package/dist/bin.mjs +4036 -1074
  3. package/dist/bin.mjs.map +1 -1
  4. package/dist/cli.mjs +2505 -1585
  5. package/dist/cli.mjs.map +1 -1
  6. package/dist/{config-BJz1m9eN.mjs → config-B1w3pAkY.mjs} +142 -279
  7. package/dist/config-B1w3pAkY.mjs.map +1 -0
  8. package/dist/config-DXhifxOw.mjs +3 -0
  9. package/dist/doc-cache-CamtXfi4.mjs +1035 -0
  10. package/dist/doc-cache-CamtXfi4.mjs.map +1 -0
  11. package/dist/doc-cache-CiBwpDTf.mjs +3 -0
  12. package/dist/doc-fork-BMjqzfAs.mjs +3 -0
  13. package/dist/doc-fork-CoPi1G1N.mjs +304 -0
  14. package/dist/doc-fork-CoPi1G1N.mjs.map +1 -0
  15. package/dist/docref-C7g0sjvL.mjs +167 -0
  16. package/dist/docref-C7g0sjvL.mjs.map +1 -0
  17. package/dist/docs/README.md +38 -6
  18. package/dist/docs/SKILL.md +21 -5
  19. package/dist/docs/guidelines/backward-compatibility-rules.md +1 -0
  20. package/dist/docs/guidelines/bun-monorepo-patterns.md +1 -0
  21. package/dist/docs/guidelines/cli-agent-skill-patterns.md +228 -52
  22. package/dist/docs/guidelines/commit-conventions.md +1 -0
  23. package/dist/docs/guidelines/common-doc-guidelines.md +1 -0
  24. package/dist/docs/guidelines/convex-limits-best-practices.md +1 -0
  25. package/dist/docs/guidelines/convex-rules.md +1 -0
  26. package/dist/docs/guidelines/electron-app-development-patterns.md +1 -0
  27. package/dist/docs/guidelines/error-handling-rules.md +4 -0
  28. package/dist/docs/guidelines/general-coding-rules.md +3 -1
  29. package/dist/docs/guidelines/general-comment-rules.md +3 -1
  30. package/dist/docs/guidelines/general-eng-agent-principles.md +127 -0
  31. package/dist/docs/guidelines/general-tdd-guidelines.md +7 -0
  32. package/dist/docs/guidelines/general-testing-rules.md +5 -0
  33. package/dist/docs/guidelines/golden-testing-guidelines.md +1 -0
  34. package/dist/docs/guidelines/pnpm-monorepo-patterns.md +1 -0
  35. package/dist/docs/guidelines/python-cli-patterns.md +5 -0
  36. package/dist/docs/guidelines/python-modern-guidelines.md +4 -0
  37. package/dist/docs/guidelines/python-rules.md +7 -0
  38. package/dist/docs/guidelines/release-notes-guidelines.md +1 -0
  39. package/dist/docs/guidelines/supply-chain-hardening.md +3 -2
  40. package/dist/docs/guidelines/tbd-sync-troubleshooting.md +25 -2
  41. package/dist/docs/guidelines/typescript-cli-tool-rules.md +4 -3
  42. package/dist/docs/guidelines/typescript-code-coverage.md +7 -4
  43. package/dist/docs/guidelines/typescript-rules.md +8 -9
  44. package/dist/docs/guidelines/typescript-sorting-patterns.md +2 -1
  45. package/dist/docs/guidelines/typescript-yaml-handling-rules.md +6 -5
  46. package/dist/docs/references/docmap-format.md +64 -0
  47. package/dist/docs/references/docref-format.md +71 -0
  48. package/dist/docs/shortcuts/standard/checkout-third-party-repo.md +17 -4
  49. package/dist/docs/shortcuts/standard/new-shortcut.md +17 -3
  50. package/dist/docs/shortcuts/standard/plan-implementation-with-beads.md +1 -1
  51. package/dist/docs/shortcuts/standard/setup-github-cli.md +4 -1
  52. package/dist/docs/shortcuts/standard/suggest-upstream-improvements.md +69 -0
  53. package/dist/docs/shortcuts/standard/sync-failure-recovery.md +1 -1
  54. package/dist/docs/shortcuts/standard/welcome-user.md +28 -0
  55. package/dist/docs/shortcuts/system/shortcut-explanation.md +16 -1
  56. package/dist/docs/shortcuts/system/skill-baseline.md +21 -5
  57. package/dist/docs/tbd-design.md +144 -3
  58. package/dist/docs/tbd-docs.md +169 -5
  59. package/dist/docs/tbd-prime.md +0 -1
  60. package/dist/docs/templates/qa-playbook.md +3 -1
  61. package/dist/docs/templates/research-brief.md +2 -2
  62. package/dist/fork-manifest-ByU7U2do.mjs +253 -0
  63. package/dist/fork-manifest-ByU7U2do.mjs.map +1 -0
  64. package/dist/fork-manifest-C7lGRq-6.mjs +3 -0
  65. package/dist/{id-mapping-mtoSP9Qt.mjs → id-mapping-D0iitY-F.mjs} +1 -1
  66. package/dist/{id-mapping-687_UEsy.mjs → id-mapping-DAIeLKzm.mjs} +8 -200
  67. package/dist/id-mapping-DAIeLKzm.mjs.map +1 -0
  68. package/dist/index.d.mts +90 -13
  69. package/dist/index.mjs +3 -3
  70. package/dist/lockfile-BR0laSDy.mjs +198 -0
  71. package/dist/lockfile-BR0laSDy.mjs.map +1 -0
  72. package/dist/paths-C1DpnZJW.mjs +405 -0
  73. package/dist/paths-C1DpnZJW.mjs.map +1 -0
  74. package/dist/{schemas-f0EcuAVu.mjs → schemas-lCwRk30L.mjs} +19 -6
  75. package/dist/schemas-lCwRk30L.mjs.map +1 -0
  76. package/dist/{src-BpvcrLnq.mjs → src-CxKOynr1.mjs} +3 -3
  77. package/dist/src-CxKOynr1.mjs.map +1 -0
  78. package/dist/tbd +4036 -1074
  79. package/dist/yaml-utils-BPy991by.mjs.map +1 -1
  80. package/package.json +1 -1
  81. package/dist/config-BJz1m9eN.mjs.map +0 -1
  82. package/dist/config-DlCUMyCG.mjs +0 -3
  83. package/dist/docs/guidelines/general-eng-assistant-rules.md +0 -59
  84. package/dist/id-mapping-687_UEsy.mjs.map +0 -1
  85. package/dist/schemas-f0EcuAVu.mjs.map +0 -1
  86. package/dist/src-BpvcrLnq.mjs.map +0 -1
@@ -1,302 +1,61 @@
1
- import { D as LocalStateSchema, E as LOCAL_STATE_FIELD_ORDER, a as CONFIG_FIELD_ORDER, s as ConfigSchema } from "./schemas-f0EcuAVu.mjs";
1
+ import { D as LocalStateSchema, E as LOCAL_STATE_FIELD_ORDER, a as CONFIG_FIELD_ORDER, s as ConfigSchema } from "./schemas-lCwRk30L.mjs";
2
2
  import { o as sortKeys, s as stringifyYaml } from "./yaml-utils-BPy991by.mjs";
3
+ import { b as SYNC_BRANCH, o as CONFIG_FILE, y as STATE_FILE } from "./paths-C1DpnZJW.mjs";
3
4
  import { parse } from "yaml";
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";
5
+ import { access, mkdir, readFile } from "node:fs/promises";
6
+ import { dirname, join, parse as parse$1 } from "node:path";
7
7
  import { writeFile } from "atomically";
8
- import { homedir } from "node:os";
9
- import { promisify } from "node:util";
10
8
 
11
- //#region src/lib/paths.ts
9
+ //#region src/utils/time-utils.ts
12
10
  /**
13
- * Centralized path constants for tbd.
11
+ * Time utilities for tbd.
14
12
  *
15
- * Directory structure (per spec):
16
- *
17
- * On main/dev branches:
18
- * .tbd/
19
- * Committed to the repo:
20
- * config.yml - Project configuration
21
- * .gitignore - Controls what's gitignored below
22
- * workspaces/ - Persistent state (outbox, named workspaces)
23
- * Gitignored (local only):
24
- * state.yml - Local state
25
- * docs/ - Installed documentation (regenerated on setup)
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
34
- *
35
- * On tbd-sync branch:
36
- * .tbd/
37
- * data-sync/
38
- * issues/
39
- * mappings/
40
- * attic/
41
- * meta.yml
42
- */
43
- /** The tbd configuration directory on main branch */
44
- const TBD_DIR = ".tbd";
45
- /** The config file path */
46
- const CONFIG_FILE = join(TBD_DIR, "config.yml");
47
- /** The local state file (gitignored) */
48
- const STATE_FILE = join(TBD_DIR, "state.yml");
49
- /** The worktree directory name */
50
- const WORKTREE_DIR_NAME = "data-sync-worktree";
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);
62
- /** The data directory name on the sync branch */
63
- const DATA_SYNC_DIR_NAME = "data-sync";
64
- /**
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().
69
- */
70
- const DATA_SYNC_DIR = join(TBD_DIR, DATA_SYNC_DIR_NAME);
71
- /**
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.
77
- */
78
- const PRIMARY_CHECKOUT_DATA_SYNC_DIR = join(PRIMARY_CHECKOUT_WORKTREE_DIR, TBD_DIR, DATA_SYNC_DIR_NAME);
79
- /** Issues directory */
80
- const ISSUES_DIR = join(DATA_SYNC_DIR, "issues");
81
- /** Mappings directory */
82
- const MAPPINGS_DIR = join(DATA_SYNC_DIR, "mappings");
83
- /** Attic directory for conflict resolution */
84
- const ATTIC_DIR = join(DATA_SYNC_DIR, "attic");
85
- /** Meta file for schema version */
86
- const META_FILE = join(DATA_SYNC_DIR, "meta.yml");
87
- /** The sync branch name */
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.
13
+ * All timestamps should be ISO 8601 UTC format with Z suffix.
14
+ * Use these functions instead of raw Date calls for consistency.
149
15
  */
150
- async function resolveSharedTbdPaths(baseDir) {
151
- return buildSharedTbdPaths(await resolveGitCommonDir(baseDir));
152
- }
153
- /** The workspaces directory name within .tbd/ */
154
- const WORKSPACES_DIR_NAME = "workspaces";
155
- /** Full path to workspaces directory: .tbd/workspaces/ */
156
- const WORKSPACES_DIR = join(TBD_DIR, WORKSPACES_DIR_NAME);
157
16
  /**
158
- * Get the path to a named workspace directory.
159
- *
160
- * Workspaces are stored at: .tbd/workspaces/{name}/
161
- *
162
- * @param workspaceName - The name of the workspace (e.g., 'outbox', 'my-feature')
163
- * @returns Path to the workspace directory
17
+ * Get current time as ISO 8601 UTC string.
18
+ * Use this when recording timestamps for new/updated issues.
164
19
  */
165
- function getWorkspaceDir(workspaceName) {
166
- return join(WORKSPACES_DIR, workspaceName);
20
+ function now() {
21
+ return (/* @__PURE__ */ new Date()).toISOString();
167
22
  }
168
23
  /**
169
- * Validate a workspace name.
170
- *
171
- * Valid workspace names:
172
- * - Lowercase alphanumeric characters
173
- * - Hyphens and underscores allowed
174
- * - Must not be empty
175
- * - Must not contain path separators or dots at start
176
- *
177
- * @param name - The workspace name to validate
178
- * @returns true if the name is valid
24
+ * Get current time as Date object.
25
+ * Use this when you need date arithmetic or comparisons.
179
26
  */
180
- function isValidWorkspaceName(name) {
181
- if (!name || name.length === 0) return false;
182
- if (name.startsWith(".")) return false;
183
- return /^[a-z0-9][a-z0-9_-]*$/.test(name);
27
+ function nowDate() {
28
+ return /* @__PURE__ */ new Date();
184
29
  }
185
- /** Docs directory name within .tbd/ */
186
- const DOCS_DIR = "docs";
187
- /** Shortcuts directory name within docs/ */
188
- const SHORTCUTS_DIR = "shortcuts";
189
- /** System shortcuts directory name (core docs like skill-baseline.md) */
190
- const SYSTEM_DIR = "system";
191
- /** Standard shortcuts directory name (workflow shortcuts) */
192
- const STANDARD_DIR = "standard";
193
- /** Guidelines directory name (coding rules and best practices) */
194
- const GUIDELINES_DIR = "guidelines";
195
- /** Templates directory name (document templates) */
196
- const TEMPLATES_DIR = "templates";
197
- /** Full path to docs directory: .tbd/docs/ */
198
- const TBD_DOCS_DIR = join(TBD_DIR, DOCS_DIR);
199
- /** Full path to shortcuts directory: .tbd/docs/shortcuts/ */
200
- const TBD_SHORTCUTS_DIR = join(TBD_DOCS_DIR, SHORTCUTS_DIR);
201
- /** Full path to system shortcuts: .tbd/docs/shortcuts/system/ */
202
- const TBD_SHORTCUTS_SYSTEM = join(TBD_SHORTCUTS_DIR, SYSTEM_DIR);
203
- /** Full path to standard shortcuts: .tbd/docs/shortcuts/standard/ */
204
- const TBD_SHORTCUTS_STANDARD = join(TBD_SHORTCUTS_DIR, STANDARD_DIR);
205
- /** Full path to guidelines: .tbd/docs/guidelines/ (top-level, not under shortcuts) */
206
- const TBD_GUIDELINES_DIR = join(TBD_DOCS_DIR, GUIDELINES_DIR);
207
- /** Full path to templates: .tbd/docs/templates/ (top-level, not under shortcuts) */
208
- const TBD_TEMPLATES_DIR = join(TBD_DOCS_DIR, TEMPLATES_DIR);
209
- /** Built-in docs source paths (relative to package docs/) */
210
- const BUILTIN_SHORTCUTS_SYSTEM = join(SHORTCUTS_DIR, SYSTEM_DIR);
211
- const BUILTIN_SHORTCUTS_STANDARD = join(SHORTCUTS_DIR, STANDARD_DIR);
212
- /**
213
- * Default shortcut lookup paths (searched in order, relative to tbd root).
214
- * Earlier paths take precedence over later paths.
215
- * Note: Guidelines and templates are now separate top-level directories.
216
- */
217
- const DEFAULT_SHORTCUT_PATHS = [TBD_SHORTCUTS_SYSTEM, TBD_SHORTCUTS_STANDARD];
218
- /**
219
- * Default guidelines lookup paths (relative to tbd root).
220
- */
221
- const DEFAULT_GUIDELINES_PATHS = [TBD_GUIDELINES_DIR];
222
- /**
223
- * Default template lookup paths (relative to tbd root).
224
- */
225
- const DEFAULT_TEMPLATE_PATHS = [TBD_TEMPLATES_DIR];
226
- /**
227
- * Error thrown when worktree is missing and fallback is not allowed.
228
- * Defined inline to avoid circular dependency with errors.ts.
229
- */
230
- var WorktreeMissingError = class extends Error {
231
- constructor(message = "Shared worktree not found under $GIT_COMMON_DIR/tbd/data-sync-worktree/. Run 'tbd doctor --fix' to repair.") {
232
- super(message);
233
- this.name = "WorktreeMissingError";
234
- }
235
- };
236
30
  /**
237
- * Cache for resolved data sync directory.
238
- * Reset when baseDir changes.
31
+ * Parse a timestamp string to Date object.
32
+ * Returns null if the timestamp is invalid.
239
33
  */
240
- let _resolvedDataSyncDir = null;
241
- let _resolvedBaseDir = null;
242
- let _resolvedAllowFallback = null;
243
- /**
244
- * Resolve the actual data sync directory path.
245
- *
246
- * This function detects whether we're running with a git worktree
247
- * (production) or in a test environment without worktree.
248
- *
249
- * Order of preference:
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)
253
- *
254
- * @param baseDir - The tbd root directory (from requireInit or findTbdRoot)
255
- * @param options - Options for path resolution
256
- * @returns Resolved data sync directory path
257
- * @throws WorktreeMissingError if worktree missing and allowFallback is false
258
- *
259
- * See: plan-2026-01-28-sync-worktree-recovery-and-hardening.md
260
- */
261
- async function resolveDataSyncDir(baseDir, options) {
262
- const allowFallback = options?.allowFallback ?? true;
263
- if (_resolvedDataSyncDir && _resolvedBaseDir === baseDir && _resolvedAllowFallback === allowFallback) return _resolvedDataSyncDir;
264
- let worktreePath = null;
34
+ function parseDate(timestamp) {
35
+ if (!timestamp) return null;
265
36
  try {
266
- worktreePath = (await resolveSharedTbdPaths(baseDir)).sharedDataSyncDir;
37
+ const date = new Date(timestamp);
38
+ if (isNaN(date.getTime())) return null;
39
+ return date;
267
40
  } catch {
268
- worktreePath = join(baseDir, PRIMARY_CHECKOUT_DATA_SYNC_DIR);
41
+ return null;
269
42
  }
270
- const directPath = join(baseDir, DATA_SYNC_DIR);
271
- if (worktreePath) try {
272
- await access(worktreePath);
273
- _resolvedDataSyncDir = worktreePath;
274
- _resolvedBaseDir = baseDir;
275
- _resolvedAllowFallback = allowFallback;
276
- return worktreePath;
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;
281
43
  }
282
44
  /**
283
- * Resolve attic directory path.
45
+ * Normalize any timestamp to ISO 8601 UTC format with Z suffix.
46
+ * Handles various formats including timezone offsets like -08:00.
47
+ * Returns null if the timestamp is invalid or missing.
284
48
  */
285
- async function resolveAtticDir(baseDir, options) {
286
- return join(await resolveDataSyncDir(baseDir, options), "attic");
49
+ function normalizeTimestamp(timestamp) {
50
+ return parseDate(timestamp)?.toISOString() ?? null;
287
51
  }
288
52
  /**
289
- * Characters per token ratio for estimating token counts.
290
- *
291
- * Based on research of OpenAI (tiktoken) and Claude tokenizers:
292
- * - Pure English prose: ~4-5 chars/token
293
- * - Code and symbols: ~3 chars/token
294
- * - Mixed markdown/code docs: ~3.5 chars/token
295
- *
296
- * We use 3.5 as our docs are markdown with code examples.
297
- * This provides ~15-20% accuracy, sufficient for cost estimation.
53
+ * Get current time as a filename-safe timestamp.
54
+ * Replaces colons with dashes and removes milliseconds for filesystem compatibility.
298
55
  */
299
- const CHARS_PER_TOKEN = 3.5;
56
+ function nowFilenameTimestamp() {
57
+ return (/* @__PURE__ */ new Date()).toISOString().replace(/:/g, "-").replace(/\.\d+Z$/, "Z");
58
+ }
300
59
 
301
60
  //#endregion
302
61
  //#region src/lib/tbd-format.ts
@@ -338,7 +97,7 @@ const CHARS_PER_TOKEN = 3.5;
338
97
  * Current format version.
339
98
  * Bump this ONLY for breaking changes that require migration.
340
99
  */
341
- const CURRENT_FORMAT = "f04";
100
+ const CURRENT_FORMAT = "f06";
342
101
  /**
343
102
  * Initial format version for configs that don't have tbd_format field.
344
103
  */
@@ -388,6 +147,22 @@ const FORMAT_HISTORY = {
388
147
  "Added $GIT_COMMON_DIR/tbd/layout.yml using the same tbd_format ID"
389
148
  ],
390
149
  migration: "Initializes shared common-dir sync layout before writing config.yml with tbd_format f04"
150
+ },
151
+ f05: {
152
+ introduced: "0.3.0",
153
+ description: "Adds the forkable-docs layout (committed fork manifest + visible fork dir)",
154
+ changes: [
155
+ "Added committed fork state under .tbd/doc-forks/ (forks.yml manifest + base/ snapshots)",
156
+ "Added the visible fork dir (docs/tbd/) which shadows the doc cache in lookups",
157
+ "Doc commands (tbd docs fork/unfork/status/update/diff/list) manage that state"
158
+ ],
159
+ migration: "Metadata-only: stamps tbd_format f05 (fork artifacts appear only when fork is first used). The shared common-dir layout.yml is re-stamped in place on the next data command. Revert: restore .tbd/config.yml (git checkout) and delete $GIT_COMMON_DIR/tbd/layout.yml; it regenerates from the config."
160
+ },
161
+ f06: {
162
+ introduced: "0.3.0",
163
+ description: "Adds the config upgrade history (tbd_upgrades) and redefines tbd_version",
164
+ changes: ["Redefined tbd_version: now the version that last ran `tbd setup` (was install-time)", "Added tbd_upgrades: ordered history of tbd versions that have run setup here"],
165
+ migration: "Seeds tbd_upgrades from the existing tbd_version (no timestamp; the original install time is unknown). Setup then stamps the running version and appends it when changed. Revert: restore .tbd/config.yml (git checkout) and delete $GIT_COMMON_DIR/tbd/layout.yml; it regenerates from the config."
391
166
  }
392
167
  };
393
168
  /**
@@ -472,6 +247,52 @@ function migrate_f03_to_f04(config) {
472
247
  };
473
248
  }
474
249
  /**
250
+ * Migrate from f04 to f05.
251
+ * - Metadata-only stamp: the forkable-docs artifacts (.tbd/doc-forks/, docs/tbd/)
252
+ * are created lazily by `tbd docs fork`, not by migration.
253
+ * - The bump gates older clients: they must not serve upstream copies of docs that
254
+ * this repo has forked and customized.
255
+ */
256
+ function migrate_f04_to_f05(config) {
257
+ const changes = [];
258
+ const migrated = { ...config };
259
+ migrated.tbd_format = "f05";
260
+ changes.push("Updated tbd_format: f05");
261
+ return {
262
+ config: migrated,
263
+ fromFormat: "f04",
264
+ toFormat: "f05",
265
+ changed: changes.length > 0,
266
+ changes
267
+ };
268
+ }
269
+ /**
270
+ * Migrate from f05 to f06.
271
+ * - Adds the config upgrade history (`tbd_upgrades`) and redefines `tbd_version` to mean
272
+ * "the version that last ran setup" (the value itself is advanced by the setup-time
273
+ * stamp, which knows the running version; this pure migration does not).
274
+ * - Seeds the first history entry from the existing `tbd_version` (the install-time
275
+ * stamp), without a timestamp because the original install time is unknown. If the
276
+ * config has no `tbd_version`, the history starts empty and setup fills it in.
277
+ */
278
+ function migrate_f05_to_f06(config) {
279
+ const changes = [];
280
+ const migrated = { ...config };
281
+ migrated.tbd_format = "f06";
282
+ changes.push("Updated tbd_format: f06");
283
+ if (migrated.tbd_upgrades === void 0) {
284
+ migrated.tbd_upgrades = migrated.tbd_version ? [{ version: migrated.tbd_version }] : [];
285
+ changes.push("Seeded tbd_upgrades history from tbd_version");
286
+ }
287
+ return {
288
+ config: migrated,
289
+ fromFormat: "f05",
290
+ toFormat: "f06",
291
+ changed: changes.length > 0,
292
+ changes
293
+ };
294
+ }
295
+ /**
475
296
  * Detect the format version of a config.
476
297
  * Returns INITIAL_FORMAT ('f01') if no tbd_format field is present.
477
298
  */
@@ -527,6 +348,18 @@ function migrateToLatest(config) {
527
348
  currentFormat = "f04";
528
349
  allChanges.push(...result.changes);
529
350
  }
351
+ if (currentFormat === "f04") {
352
+ const result = migrate_f04_to_f05(current);
353
+ current = result.config;
354
+ currentFormat = "f05";
355
+ allChanges.push(...result.changes);
356
+ }
357
+ if (currentFormat === "f05") {
358
+ const result = migrate_f05_to_f06(current);
359
+ current = result.config;
360
+ currentFormat = "f06";
361
+ allChanges.push(...result.changes);
362
+ }
530
363
  return {
531
364
  config: current,
532
365
  fromFormat,
@@ -601,6 +434,10 @@ function createDefaultConfig(version, prefix) {
601
434
  return ConfigSchema.parse({
602
435
  tbd_format: CURRENT_FORMAT,
603
436
  tbd_version: version,
437
+ tbd_upgrades: [{
438
+ version,
439
+ at: now()
440
+ }],
604
441
  sync: {
605
442
  branch: SYNC_BRANCH,
606
443
  remote: "origin",
@@ -625,6 +462,31 @@ async function initConfig(baseDir, version, prefix) {
625
462
  return config;
626
463
  }
627
464
  /**
465
+ * Stamp the config with the version that is running `tbd setup`.
466
+ *
467
+ * Sets `tbd_version` to the running version and, when that version differs from the most
468
+ * recent `tbd_upgrades` entry, appends a new `{ version, at }` entry. Deduping by the
469
+ * last entry keeps no-op re-runs and identical dev rebuilds from spamming the history.
470
+ *
471
+ * Pure: returns the SAME object when nothing changed (so callers can skip the write via
472
+ * an identity check), or a new config otherwise. The caller persists the result.
473
+ */
474
+ function stampSetupVersion(config, version, at = now()) {
475
+ const upgrades = config.tbd_upgrades ?? [];
476
+ if (upgrades[upgrades.length - 1]?.version === version) return config.tbd_version === version ? config : {
477
+ ...config,
478
+ tbd_version: version
479
+ };
480
+ return {
481
+ ...config,
482
+ tbd_version: version,
483
+ tbd_upgrades: [...upgrades, {
484
+ version,
485
+ at
486
+ }]
487
+ };
488
+ }
489
+ /**
628
490
  * Read config from file with automatic migration if needed.
629
491
  *
630
492
  * ⚠️ FORMAT VERSIONING: See tbd-format.ts for version history and migration rules.
@@ -676,7 +538,8 @@ async function writeConfig(baseDir, config) {
676
538
  lineWidth: 0,
677
539
  sortMapEntries: false
678
540
  });
679
- 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:");
541
+ if (config.tbd_upgrades && config.tbd_upgrades.length > 0) content = content.replace("tbd_upgrades:", "# tbd_upgrades: tbd versions that have run `tbd setup` in this repo (oldest first);\n# tbd_version above is the most recent. Informational; updated automatically by setup.\ntbd_upgrades:");
542
+ if (config.docs_cache && Object.keys(config.docs_cache).length > 0) content = content.replace("docs_cache:", "# Documentation cache: the docs tbd serves (guidelines, shortcuts, templates), synced\n# into the gitignored .tbd/docs/ cache. Managed with `tbd docs` (run `tbd docs --help`).\n# files: destination path (under .tbd/docs/) -> source docref. Common source forms:\n# - internal:<path> bundled with tbd (e.g. internal:guidelines/typescript-rules.md)\n# - github:owner/repo@ref//path/to/file.md a file in a git repo (gitlab: too; @ref optional)\n# - https://host/path/file.md a plain URL, kept verbatim\n# Full docref grammar: `tbd docs show docref-format`.\n# lookup_path: doc lookup search order, like shell $PATH (earlier paths win).\n# Refresh the cache with `tbd docs sync`; it also auto-syncs when stale\n# (settings.doc_auto_sync_hours, default 24; 0 disables).\ndocs_cache:");
680
543
  await writeFile(configPath, content);
681
544
  }
682
545
  /**
@@ -778,5 +641,5 @@ async function markWelcomeSeen(baseDir) {
778
641
  }
779
642
 
780
643
  //#endregion
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
644
+ export { parseDate as C, nowFilenameTimestamp as S, isCompatibleFormat as _, isInitialized as a, now as b, readConfigWithMigration as c, updateLocalState as d, writeConfig as f, formatUpgradeMessage as g, FORMAT_HISTORY as h, initConfig as i, readLocalState as l, CURRENT_FORMAT as m, findTbdRoot as n, markWelcomeSeen as o, writeLocalState as p, hasSeenWelcome as r, readConfig as s, IncompatibleFormatError as t, stampSetupVersion as u, isFormatCompatibleWithSupported as v, nowDate as x, normalizeTimestamp as y };
645
+ //# sourceMappingURL=config-B1w3pAkY.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config-B1w3pAkY.mjs","names":["parseYaml","parsePath"],"sources":["../src/utils/time-utils.ts","../src/lib/tbd-format.ts","../src/file/config.ts"],"sourcesContent":["/**\n * Time utilities for tbd.\n *\n * All timestamps should be ISO 8601 UTC format with Z suffix.\n * Use these functions instead of raw Date calls for consistency.\n */\n\n/**\n * Get current time as ISO 8601 UTC string.\n * Use this when recording timestamps for new/updated issues.\n */\nexport function now(): string {\n return new Date().toISOString();\n}\n\n/**\n * Get current time as Date object.\n * Use this when you need date arithmetic or comparisons.\n */\nexport function nowDate(): Date {\n return new Date();\n}\n\n/**\n * Parse a timestamp string to Date object.\n * Returns null if the timestamp is invalid.\n */\nexport function parseDate(timestamp: string | undefined | null): Date | null {\n if (!timestamp) return null;\n try {\n const date = new Date(timestamp);\n if (isNaN(date.getTime())) return null;\n return date;\n } catch {\n return null;\n }\n}\n\n/**\n * Normalize any timestamp to ISO 8601 UTC format with Z suffix.\n * Handles various formats including timezone offsets like -08:00.\n * Returns null if the timestamp is invalid or missing.\n */\nexport function normalizeTimestamp(timestamp: string | undefined | null): string | null {\n const date = parseDate(timestamp);\n return date?.toISOString() ?? null;\n}\n\n/**\n * Check if a timestamp string is valid.\n */\nexport function isValidTimestamp(timestamp: string | undefined | null): boolean {\n return parseDate(timestamp) !== null;\n}\n\n/**\n * Get current time as a filename-safe timestamp.\n * Replaces colons with dashes and removes milliseconds for filesystem compatibility.\n */\nexport function nowFilenameTimestamp(): string {\n return new Date()\n .toISOString()\n .replace(/:/g, '-')\n .replace(/\\.\\d+Z$/, 'Z');\n}\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 = 'f06';\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 f05: {\n introduced: '0.3.0',\n description: 'Adds the forkable-docs layout (committed fork manifest + visible fork dir)',\n changes: [\n 'Added committed fork state under .tbd/doc-forks/ (forks.yml manifest + base/ snapshots)',\n 'Added the visible fork dir (docs/tbd/) which shadows the doc cache in lookups',\n 'Doc commands (tbd docs fork/unfork/status/update/diff/list) manage that state',\n ],\n migration:\n 'Metadata-only: stamps tbd_format f05 (fork artifacts appear only when fork is first ' +\n 'used). The shared common-dir layout.yml is re-stamped in place on the next data ' +\n 'command. Revert: restore .tbd/config.yml (git checkout) and delete ' +\n '$GIT_COMMON_DIR/tbd/layout.yml; it regenerates from the config.',\n },\n f06: {\n introduced: '0.3.0',\n description: 'Adds the config upgrade history (tbd_upgrades) and redefines tbd_version',\n changes: [\n 'Redefined tbd_version: now the version that last ran `tbd setup` (was install-time)',\n 'Added tbd_upgrades: ordered history of tbd versions that have run setup here',\n ],\n migration:\n 'Seeds tbd_upgrades from the existing tbd_version (no timestamp; the original ' +\n 'install time is unknown). Setup then stamps the running version and appends it ' +\n 'when changed. Revert: restore .tbd/config.yml (git checkout) and delete ' +\n '$GIT_COMMON_DIR/tbd/layout.yml; it regenerates from the config.',\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 /** Upgrade history (f06+). Seeded on migration; appended by setup. */\n tbd_upgrades?: { version: string; at?: 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 * Migrate from f04 to f05.\n * - Metadata-only stamp: the forkable-docs artifacts (.tbd/doc-forks/, docs/tbd/)\n * are created lazily by `tbd docs fork`, not by migration.\n * - The bump gates older clients: they must not serve upstream copies of docs that\n * this repo has forked and customized.\n */\nfunction migrate_f04_to_f05(config: RawConfig): MigrationResult {\n const changes: string[] = [];\n const migrated = { ...config };\n\n migrated.tbd_format = 'f05';\n changes.push('Updated tbd_format: f05');\n\n return {\n config: migrated,\n fromFormat: 'f04',\n toFormat: 'f05',\n changed: changes.length > 0,\n changes,\n };\n}\n\n/**\n * Migrate from f05 to f06.\n * - Adds the config upgrade history (`tbd_upgrades`) and redefines `tbd_version` to mean\n * \"the version that last ran setup\" (the value itself is advanced by the setup-time\n * stamp, which knows the running version; this pure migration does not).\n * - Seeds the first history entry from the existing `tbd_version` (the install-time\n * stamp), without a timestamp because the original install time is unknown. If the\n * config has no `tbd_version`, the history starts empty and setup fills it in.\n */\nfunction migrate_f05_to_f06(config: RawConfig): MigrationResult {\n const changes: string[] = [];\n const migrated = { ...config };\n\n migrated.tbd_format = 'f06';\n changes.push('Updated tbd_format: f06');\n\n if (migrated.tbd_upgrades === undefined) {\n migrated.tbd_upgrades = migrated.tbd_version ? [{ version: migrated.tbd_version }] : [];\n changes.push('Seeded tbd_upgrades history from tbd_version');\n }\n\n return {\n config: migrated,\n fromFormat: 'f05',\n toFormat: 'f06',\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 if (currentFormat === 'f04') {\n const result = migrate_f04_to_f05(current);\n current = result.config;\n currentFormat = 'f05' as FormatVersion;\n allChanges.push(...result.changes);\n }\n\n if (currentFormat === 'f05') {\n const result = migrate_f05_to_f06(current);\n current = result.config;\n currentFormat = 'f06' 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 if (current === 'f04') {\n descriptions.push('f04 → f05: Add forkable-docs layout (stamp only)');\n current = 'f05';\n }\n\n if (current === 'f05') {\n descriptions.push('f05 → f06: Add config upgrade history (tbd_upgrades)');\n current = 'f06';\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 { now } from '../utils/time-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 // Seed the upgrade history with the version that created this repo.\n tbd_upgrades: [{ version, at: now() }],\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 * Stamp the config with the version that is running `tbd setup`.\n *\n * Sets `tbd_version` to the running version and, when that version differs from the most\n * recent `tbd_upgrades` entry, appends a new `{ version, at }` entry. Deduping by the\n * last entry keeps no-op re-runs and identical dev rebuilds from spamming the history.\n *\n * Pure: returns the SAME object when nothing changed (so callers can skip the write via\n * an identity check), or a new config otherwise. The caller persists the result.\n */\nexport function stampSetupVersion(config: Config, version: string, at: string = now()): Config {\n const upgrades = config.tbd_upgrades ?? [];\n const last = upgrades[upgrades.length - 1];\n\n if (last?.version === version) {\n // This version already heads the history; only ensure tbd_version reflects it.\n return config.tbd_version === version ? config : { ...config, tbd_version: version };\n }\n\n return {\n ...config,\n tbd_version: version,\n tbd_upgrades: [...upgrades, { version, at }],\n };\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 by injecting them before each section's key. This is a\n // simple string replace, so the anchor keys (`tbd_upgrades:`, `docs_cache:`) must stay\n // literal. Comments are brief and point to the command that shows full details, since\n // an agent often reads this file directly.\n let content = yaml;\n\n if (config.tbd_upgrades && config.tbd_upgrades.length > 0) {\n const upgradesComment = `# tbd_upgrades: tbd versions that have run \\`tbd setup\\` in this repo (oldest first);\n# tbd_version above is the most recent. Informational; updated automatically by setup.\n`;\n content = content.replace('tbd_upgrades:', upgradesComment + 'tbd_upgrades:');\n }\n\n if (config.docs_cache && Object.keys(config.docs_cache).length > 0) {\n const docsCacheComment = `# Documentation cache: the docs tbd serves (guidelines, shortcuts, templates), synced\n# into the gitignored .tbd/docs/ cache. Managed with \\`tbd docs\\` (run \\`tbd docs --help\\`).\n# files: destination path (under .tbd/docs/) -> source docref. Common source forms:\n# - internal:<path> bundled with tbd (e.g. internal:guidelines/typescript-rules.md)\n# - github:owner/repo@ref//path/to/file.md a file in a git repo (gitlab: too; @ref optional)\n# - https://host/path/file.md a plain URL, kept verbatim\n# Full docref grammar: \\`tbd docs show docref-format\\`.\n# lookup_path: doc lookup search order, like shell $PATH (earlier paths win).\n# Refresh the cache with \\`tbd docs sync\\`; it also auto-syncs when stale\n# (settings.doc_auto_sync_hours, default 24; 0 disables).\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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"}
@@ -0,0 +1,3 @@
1
+ import { a as isInitialized, c as readConfigWithMigration, d as updateLocalState, f as writeConfig, i as initConfig, l as readLocalState, n as findTbdRoot, o as markWelcomeSeen, p as writeLocalState, r as hasSeenWelcome, s as readConfig, t as IncompatibleFormatError, u as stampSetupVersion } from "./config-B1w3pAkY.mjs";
2
+
3
+ export { readConfig };