get-tbd 0.1.24 → 0.1.26
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.
- package/README.md +2 -2
- package/dist/bin.mjs +195 -49
- package/dist/bin.mjs.map +1 -1
- package/dist/cli.mjs +126 -44
- package/dist/cli.mjs.map +1 -1
- package/dist/{config-CB1tcqTZ.mjs → config-BZte2m3w.mjs} +1 -1
- package/dist/{config-CmEAGaxz.mjs → config-b20Kf5pW.mjs} +3 -2
- package/dist/config-b20Kf5pW.mjs.map +1 -0
- package/dist/docs/README.md +2 -2
- package/dist/docs/SKILL.md +31 -31
- package/dist/docs/guidelines/cli-agent-skill-patterns.md +1 -1
- package/dist/docs/guidelines/convex-limits-best-practices.md +16 -16
- package/dist/docs/guidelines/convex-rules.md +3 -3
- package/dist/docs/guidelines/electron-app-development-patterns.md +1 -1
- package/dist/docs/guidelines/error-handling-rules.md +2 -2
- package/dist/docs/guidelines/general-coding-rules.md +2 -2
- package/dist/docs/guidelines/general-comment-rules.md +2 -2
- package/dist/docs/guidelines/general-eng-assistant-rules.md +2 -2
- package/dist/docs/guidelines/python-rules.md +4 -4
- package/dist/docs/guidelines/typescript-rules.md +17 -17
- package/dist/docs/guidelines/typescript-yaml-handling-rules.md +8 -8
- package/dist/docs/shortcuts/standard/new-guideline.md +4 -4
- package/dist/docs/shortcuts/standard/new-validation-plan.md +13 -13
- package/dist/docs/shortcuts/standard/revise-all-architecture-docs.md +1 -1
- package/dist/docs/shortcuts/standard/setup-github-cli.md +1 -1
- package/dist/docs/shortcuts/standard/welcome-user.md +12 -12
- package/dist/docs/shortcuts/system/skill-baseline.md +31 -31
- package/dist/id-mapping-BA_xn516.mjs +3 -0
- package/dist/{id-mapping-DjVJIO4M.mjs → id-mapping-BtBwq5nG.mjs} +68 -15
- package/dist/id-mapping-BtBwq5nG.mjs.map +1 -0
- package/dist/index.mjs +2 -2
- package/dist/schemas-BQYmDnkv.mjs +311 -0
- package/dist/schemas-BQYmDnkv.mjs.map +1 -0
- package/dist/{src-BrM6xcdG.mjs → src-DQcOQnFp.mjs} +4 -3
- package/dist/{src-BrM6xcdG.mjs.map → src-DQcOQnFp.mjs.map} +1 -1
- package/dist/tbd +195 -49
- package/dist/yaml-utils-BPy991by.mjs +273 -0
- package/dist/yaml-utils-BPy991by.mjs.map +1 -0
- package/dist/yaml-utils-swV780m5.mjs +3 -0
- package/package.json +1 -1
- package/dist/config-CmEAGaxz.mjs.map +0 -1
- package/dist/id-mapping-DjVJIO4M.mjs.map +0 -1
- package/dist/id-mapping-LjnDSEhN.mjs +0 -3
- package/dist/yaml-utils-U7l9hhkh.mjs +0 -581
- package/dist/yaml-utils-U7l9hhkh.mjs.map +0 -1
|
@@ -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-
|
|
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-b20Kf5pW.mjs";
|
|
2
2
|
|
|
3
3
|
export { readConfig };
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { a as ConfigSchema, b as LOCAL_STATE_FIELD_ORDER, i as CONFIG_FIELD_ORDER, x as LocalStateSchema } from "./schemas-BQYmDnkv.mjs";
|
|
2
|
+
import { o as sortKeys, s as stringifyYaml } from "./yaml-utils-BPy991by.mjs";
|
|
2
3
|
import { parse } from "yaml";
|
|
3
4
|
import { access, mkdir, readFile } from "node:fs/promises";
|
|
4
5
|
import { dirname, isAbsolute, join, parse as parse$1 } from "node:path";
|
|
@@ -634,4 +635,4 @@ async function markWelcomeSeen(baseDir) {
|
|
|
634
635
|
|
|
635
636
|
//#endregion
|
|
636
637
|
export { isValidWorkspaceName as A, TBD_SHORTCUTS_STANDARD as C, WORKTREE_DIR as D, WORKSPACES_DIR as E, resolveDataSyncDir as M, WORKTREE_DIR_NAME as O, TBD_GUIDELINES_DIR as S, TBD_TEMPLATES_DIR as T, DEFAULT_SHORTCUT_PATHS as _, isInitialized as a, TBD_DIR as b, readConfigWithMigration as c, writeConfig as d, writeLocalState as f, DEFAULT_GUIDELINES_PATHS as g, DATA_SYNC_DIR_NAME as h, initConfig as i, resolveAtticDir as j, getWorkspaceDir as k, readLocalState as l, DATA_SYNC_DIR as m, findTbdRoot as n, markWelcomeSeen as o, CHARS_PER_TOKEN as p, hasSeenWelcome as r, readConfig as s, IncompatibleFormatError as t, updateLocalState as u, DEFAULT_TEMPLATE_PATHS as v, TBD_SHORTCUTS_SYSTEM as w, TBD_DOCS_DIR as x, SYNC_BRANCH as y };
|
|
637
|
-
//# sourceMappingURL=config-
|
|
638
|
+
//# sourceMappingURL=config-b20Kf5pW.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config-b20Kf5pW.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 * 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 { join } from 'node:path';\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/** The worktree path (gitignored) */\nexport const WORKTREE_DIR = join(TBD_DIR, 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 base directory for synced data.\n *\n * NOTE: This is currently pointing directly to .tbd/data-sync/ which is WRONG\n * per the spec. The correct path should be via the worktree:\n * .tbd/data-sync-worktree/.tbd/data-sync/\n *\n * TODO(tbd-208): Update this to use the worktree path once worktree\n * management is implemented.\n */\nexport const DATA_SYNC_DIR = join(TBD_DIR, DATA_SYNC_DIR_NAME);\n\n/**\n * The correct path for synced data via worktree (per spec).\n * Use this once worktree management is implemented.\n */\nexport const DATA_SYNC_DIR_VIA_WORKTREE = join(WORKTREE_DIR, TBD_DIR, DATA_SYNC_DIR_NAME);\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// 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 } 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 = \"Worktree not found at .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. Worktree path if worktree exists: .tbd/data-sync-worktree/.tbd/data-sync/\n * 2. Direct path as fallback (only if allowFallback: true)\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 const worktreePath = join(baseDir, DATA_SYNC_DIR_VIA_WORKTREE);\n const directPath = join(baseDir, DATA_SYNC_DIR);\n\n // Check if worktree path exists\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 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 _resolvedDataSyncDir = directPath;\n _resolvedBaseDir = baseDir;\n _resolvedAllowFallback = allowFallback;\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\nimport { isAbsolute } from 'node:path';\nimport { homedir } from 'node:os';\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 * - 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 = 'f03';\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} 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 };\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// 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 // Add more migrations here as new format versions are added\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 const formatVersions = Object.keys(FORMAT_HISTORY);\n const currentIndex = formatVersions.indexOf(CURRENT_FORMAT);\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 * 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 // Add more migration descriptions here\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 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(\n `Config format '${foundFormat}' is from a newer tbd version.\\n` +\n `This tbd version supports up to format '${supportedFormat}'.\\n` +\n `Please upgrade tbd: npm install -g get-tbd@latest`,\n );\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 },\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(\n baseDir: string,\n): Promise<{ config: Config; migrated: boolean; changes: string[] }> {\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 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 };\n }\n\n return {\n config: ConfigSchema.parse(data),\n migrated: false,\n changes: [],\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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"}
|
package/dist/docs/README.md
CHANGED
|
@@ -279,7 +279,7 @@ npm install -g get-tbd@latest
|
|
|
279
279
|
### Setup
|
|
280
280
|
|
|
281
281
|
```bash
|
|
282
|
-
# Fresh project (--prefix is REQUIRED—
|
|
282
|
+
# Fresh project (--prefix is REQUIRED—a short alphabetic name used as an issue ID prefix, e.g. myapp → issues like myapp-a1b2)
|
|
283
283
|
tbd setup --auto --prefix=myapp
|
|
284
284
|
|
|
285
285
|
# Joining an existing tbd project (no prefix needed—reads existing config)
|
|
@@ -299,7 +299,7 @@ tbd setup --from-beads
|
|
|
299
299
|
**First contributor:**
|
|
300
300
|
```bash
|
|
301
301
|
npm install -g get-tbd@latest
|
|
302
|
-
tbd setup --auto --prefix=
|
|
302
|
+
tbd setup --auto --prefix=proj # Short alphabetic prefix for issue IDs
|
|
303
303
|
git add .tbd/ .claude/ && git commit -m "Initialize tbd"
|
|
304
304
|
git push
|
|
305
305
|
```
|
package/dist/docs/SKILL.md
CHANGED
|
@@ -59,7 +59,7 @@ tbd prime # Restore full context on tbd after compaction
|
|
|
59
59
|
actions. DO NOT tell users to run tbd commands.
|
|
60
60
|
That’s your job.
|
|
61
61
|
|
|
62
|
-
- **WRONG**:
|
|
62
|
+
- **WRONG**: “Run `tbd create` to track this bug”
|
|
63
63
|
|
|
64
64
|
- **RIGHT**: *(you run `tbd create` yourself and tell the user it’s tracked)*
|
|
65
65
|
|
|
@@ -71,41 +71,41 @@ or want help → run `tbd shortcut welcome-user`
|
|
|
71
71
|
| User Says | You (the Agent) Run |
|
|
72
72
|
| --- | --- |
|
|
73
73
|
| **Issues/Beads** | |
|
|
74
|
-
|
|
|
75
|
-
|
|
|
76
|
-
|
|
|
77
|
-
|
|
|
78
|
-
|
|
|
79
|
-
|
|
|
80
|
-
|
|
|
81
|
-
|
|
|
74
|
+
| “There’s a bug where …” | `tbd create "..." --type=bug` |
|
|
75
|
+
| “Create a task/feature for …” | `tbd create "..." --type=task` or `--type=feature` |
|
|
76
|
+
| “Let’s work on issues/beads” | `tbd ready` |
|
|
77
|
+
| “Show me issue X” | `tbd show <id>` |
|
|
78
|
+
| “Close this issue” | `tbd close <id>` |
|
|
79
|
+
| “Search issues for X” | `tbd search "X"` |
|
|
80
|
+
| “Add label X to issue” | `tbd label add <id> <label>` |
|
|
81
|
+
| “What issues are stale?” | `tbd stale` |
|
|
82
82
|
| **Planning & Specs** | |
|
|
83
|
-
|
|
|
84
|
-
|
|
|
85
|
-
|
|
|
83
|
+
| “Plan a new feature” / “Create a spec” | `tbd shortcut new-plan-spec` |
|
|
84
|
+
| “Break spec into beads” | `tbd shortcut plan-implementation-with-beads` |
|
|
85
|
+
| “Implement these beads” | `tbd shortcut implement-beads` |
|
|
86
86
|
| **Code Review & Commits** | |
|
|
87
|
-
|
|
|
88
|
-
|
|
|
89
|
-
|
|
|
90
|
-
|
|
|
91
|
-
|
|
|
87
|
+
| “Review this code” / “Code review” | `tbd shortcut review-code` |
|
|
88
|
+
| “Review this PR” | `tbd shortcut review-github-pr` |
|
|
89
|
+
| “Commit this” / “Use the commit shortcut” | `tbd shortcut code-review-and-commit` |
|
|
90
|
+
| “Create a PR” / “File a PR” | `tbd shortcut create-or-update-pr-simple` |
|
|
91
|
+
| “Merge main into my branch” | `tbd shortcut merge-upstream` |
|
|
92
92
|
| **Guidelines & Knowledge** | |
|
|
93
|
-
|
|
|
94
|
-
|
|
|
95
|
-
|
|
|
96
|
-
|
|
|
97
|
-
|
|
|
98
|
-
|
|
|
99
|
-
|
|
|
93
|
+
| “Use TypeScript best practices” | `tbd guidelines typescript-rules` |
|
|
94
|
+
| “Use Python best practices” | `tbd guidelines python-rules` |
|
|
95
|
+
| “Build a TypeScript CLI” | `tbd guidelines typescript-cli-tool-rules` |
|
|
96
|
+
| “Improve monorepo setup” | `tbd guidelines pnpm-monorepo-patterns` or `bun-monorepo-patterns` |
|
|
97
|
+
| “Add golden/e2e testing” | `tbd guidelines golden-testing-guidelines` |
|
|
98
|
+
| “Use TDD” / “Test-driven development” | `tbd guidelines general-tdd-guidelines` |
|
|
99
|
+
| “Convex best practices” | `tbd guidelines convex-rules` |
|
|
100
100
|
| **Documentation** | |
|
|
101
|
-
|
|
|
102
|
-
|
|
|
101
|
+
| “Research this topic” | `tbd shortcut new-research-brief` |
|
|
102
|
+
| “Document architecture” | `tbd shortcut new-architecture-doc` |
|
|
103
103
|
| **Cleanup & Maintenance** | |
|
|
104
|
-
|
|
|
105
|
-
|
|
|
104
|
+
| “Clean up this code” / “Remove dead code” | `tbd shortcut code-cleanup-all` |
|
|
105
|
+
| “Fix repository problems” | `tbd doctor --fix` |
|
|
106
106
|
| **Sessions & Handoffs** | |
|
|
107
|
-
|
|
|
108
|
-
|
|
|
107
|
+
| “Hand off to another agent” | `tbd shortcut agent-handoff` |
|
|
108
|
+
| “Check out this library’s source” | `tbd shortcut checkout-third-party-repo` |
|
|
109
109
|
| *(your choice whenever appropriate)* | `tbd list`, `tbd dep add`, `tbd close`, `tbd sync`, etc. |
|
|
110
110
|
|
|
111
111
|
**Note:** Never gitignore `.tbd/workspaces/` — the outbox must be committed to your
|
|
@@ -149,7 +149,7 @@ working branch. See `tbd guidelines tbd-sync-troubleshooting` for details.
|
|
|
149
149
|
|
|
150
150
|
| Command | Purpose |
|
|
151
151
|
| --- | --- |
|
|
152
|
-
| `tbd create "title" --type task\|bug\|feature --priority=P2` | New bead (P0-P4, not
|
|
152
|
+
| `tbd create "title" --type task\|bug\|feature --priority=P2` | New bead (P0-P4, not “high/medium/low”) |
|
|
153
153
|
| `tbd update <id> --status in_progress` | Claim work |
|
|
154
154
|
| `tbd close <id> [--reason "..."]` | Mark complete |
|
|
155
155
|
|
|
@@ -214,7 +214,7 @@ Description quality directly impacts activation reliability.
|
|
|
214
214
|
| Approach | Success Rate |
|
|
215
215
|
| --- | --- |
|
|
216
216
|
| No optimization / vague descriptions | ~20% |
|
|
217
|
-
| Optimized descriptions with
|
|
217
|
+
| Optimized descriptions with “Use when …” | ~50% |
|
|
218
218
|
| Descriptions with concrete examples | 72-90% |
|
|
219
219
|
| Forced evaluation hooks | 80-84% |
|
|
220
220
|
|
|
@@ -369,7 +369,7 @@ This limit is not documented in official Convex docs but is verified in source c
|
|
|
369
369
|
for (let i = 0; i < items.length; i++) {
|
|
370
370
|
console.log(`Processing item ${i}`);
|
|
371
371
|
}
|
|
372
|
-
|
|
372
|
+
|
|
373
373
|
// Good: Logs ~10 times for same dataset
|
|
374
374
|
for (let i = 0; i < items.length; i++) {
|
|
375
375
|
if (i % 100 === 0) {
|
|
@@ -1077,7 +1077,7 @@ const rows = await ctx.db
|
|
|
1077
1077
|
contentSummary: v.string(), // Small snippet
|
|
1078
1078
|
detailId: v.id('messageDetails'), // Link to full content
|
|
1079
1079
|
}
|
|
1080
|
-
|
|
1080
|
+
|
|
1081
1081
|
// Store large content separately
|
|
1082
1082
|
messageDetails: {
|
|
1083
1083
|
fullContent: v.string(), // Large field
|
|
@@ -1130,13 +1130,13 @@ const errorCount = (await ctx.db.query('events')
|
|
|
1130
1130
|
|
|
1131
1131
|
```typescript
|
|
1132
1132
|
import { Aggregate } from '@convex-dev/aggregate';
|
|
1133
|
-
|
|
1133
|
+
|
|
1134
1134
|
// Define aggregate
|
|
1135
1135
|
const eventAggregate = new Aggregate<typeof schema.events>(components.aggregate, {
|
|
1136
1136
|
filterKey: (event) => event.parentId,
|
|
1137
1137
|
sumFields: { tokenCount: 0 },
|
|
1138
1138
|
});
|
|
1139
|
-
|
|
1139
|
+
|
|
1140
1140
|
// Query aggregates efficiently
|
|
1141
1141
|
const stats = await eventAggregate.count(ctx, {
|
|
1142
1142
|
prefix: parentId,
|
|
@@ -1287,7 +1287,7 @@ export const createSessionAndWorkflow = mutation({
|
|
|
1287
1287
|
for (const config of configs) {
|
|
1288
1288
|
await ctx.runMutation(internal.createEntity, config);
|
|
1289
1289
|
}
|
|
1290
|
-
|
|
1290
|
+
|
|
1291
1291
|
// CORRECT: Single mutation creates all entities
|
|
1292
1292
|
export const createEntities = internalMutation({
|
|
1293
1293
|
handler: async (ctx, { configs }) => {
|
|
@@ -1342,7 +1342,7 @@ export const createSessionAndWorkflow = mutation({
|
|
|
1342
1342
|
```typescript
|
|
1343
1343
|
// WIDE: Triggers reruns on any aggregate change
|
|
1344
1344
|
const count = await aggregate.count(ctx, { prefix: entityId });
|
|
1345
|
-
|
|
1345
|
+
|
|
1346
1346
|
// BOUNDED: Only reruns when matching records change
|
|
1347
1347
|
const count = await aggregate.count(ctx, {
|
|
1348
1348
|
prefix: entityId,
|
|
@@ -1433,7 +1433,7 @@ or high-frequency queries on large result sets.
|
|
|
1433
1433
|
const oldRecords = await ctx.runQuery(internal.records.getCompleted, {
|
|
1434
1434
|
beforeDate: Date.now() - 90 * 24 * 60 * 60 * 1000, // 90 days
|
|
1435
1435
|
});
|
|
1436
|
-
|
|
1436
|
+
|
|
1437
1437
|
for (const record of oldRecords) {
|
|
1438
1438
|
// Export to S3
|
|
1439
1439
|
await exportRecordToS3(record);
|
|
@@ -1544,22 +1544,22 @@ export const countAllTurns = query({
|
|
|
1544
1544
|
handler: async (ctx, args) => {
|
|
1545
1545
|
let cursor = null;
|
|
1546
1546
|
let totalProcessed = 0;
|
|
1547
|
-
|
|
1547
|
+
|
|
1548
1548
|
do {
|
|
1549
1549
|
// Call mutation to process one batch
|
|
1550
1550
|
const result = await ctx.runMutation(
|
|
1551
1551
|
internal.processTurnsBatch,
|
|
1552
1552
|
{ cursor, numItems: 100 }
|
|
1553
1553
|
);
|
|
1554
|
-
|
|
1554
|
+
|
|
1555
1555
|
totalProcessed += result.processed;
|
|
1556
1556
|
cursor = result.continueCursor;
|
|
1557
1557
|
} while (cursor !== null);
|
|
1558
|
-
|
|
1558
|
+
|
|
1559
1559
|
return { totalProcessed };
|
|
1560
1560
|
},
|
|
1561
1561
|
});
|
|
1562
|
-
|
|
1562
|
+
|
|
1563
1563
|
// Mutation processes one batch
|
|
1564
1564
|
export const processTurnsBatch = internalMutation({
|
|
1565
1565
|
args: { cursor: v.union(v.string(), v.null()), numItems: v.number() },
|
|
@@ -1567,10 +1567,10 @@ export const countAllTurns = query({
|
|
|
1567
1567
|
const page = await ctx.db
|
|
1568
1568
|
.query('conversationTurns')
|
|
1569
1569
|
.paginate({ cursor: args.cursor, numItems: args.numItems });
|
|
1570
|
-
|
|
1570
|
+
|
|
1571
1571
|
// Process page.page here
|
|
1572
1572
|
const processed = page.page.length;
|
|
1573
|
-
|
|
1573
|
+
|
|
1574
1574
|
return {
|
|
1575
1575
|
processed,
|
|
1576
1576
|
continueCursor: page.continueCursor,
|
|
@@ -1587,17 +1587,17 @@ export const countAllTurns = query({
|
|
|
1587
1587
|
handler: async (ctx, args) => {
|
|
1588
1588
|
let cursor = null;
|
|
1589
1589
|
let totalValidated = 0;
|
|
1590
|
-
|
|
1590
|
+
|
|
1591
1591
|
do {
|
|
1592
1592
|
const batch = await ctx.runQuery(internal.validateBatch, {
|
|
1593
1593
|
cursor,
|
|
1594
1594
|
numItems: 500,
|
|
1595
1595
|
});
|
|
1596
|
-
|
|
1596
|
+
|
|
1597
1597
|
totalValidated += batch.count;
|
|
1598
1598
|
cursor = batch.continueCursor;
|
|
1599
1599
|
} while (cursor !== null);
|
|
1600
|
-
|
|
1600
|
+
|
|
1601
1601
|
return { totalValidated };
|
|
1602
1602
|
},
|
|
1603
1603
|
});
|
|
@@ -132,7 +132,7 @@ export const exampleQuery = query({
|
|
|
132
132
|
Field names must be nonempty and not start with “$” or “_”. | | Record | Record |
|
|
133
133
|
`{"a": "1", "b": "2"}` | `v.record(keys, values)` | Records are objects at runtime,
|
|
134
134
|
but can have dynamic keys.
|
|
135
|
-
Keys must be only ASCII characters, nonempty, and not start with “$” or
|
|
135
|
+
Keys must be only ASCII characters, nonempty, and not start with “$” or “\_”. |
|
|
136
136
|
|
|
137
137
|
### Function registration
|
|
138
138
|
|
|
@@ -294,7 +294,7 @@ Note: `paginationOpts` is an object with the following properties:
|
|
|
294
294
|
|
|
295
295
|
## Typescript guidelines
|
|
296
296
|
|
|
297
|
-
- You can use the helper typescript type `Id` imported from
|
|
297
|
+
- You can use the helper typescript type `Id` imported from ‘./\_generated/dataModel’ to
|
|
298
298
|
get the type of the id for a given table.
|
|
299
299
|
For example if there is a table called ‘users’ you can use `Id<'users'>` to get the
|
|
300
300
|
type of the id for that table.
|
|
@@ -443,7 +443,7 @@ export default crons;
|
|
|
443
443
|
- You can register Convex functions within `crons.ts` just like any other file.
|
|
444
444
|
|
|
445
445
|
- If a cron calls an internal function, always import the `internal` object from
|
|
446
|
-
|
|
446
|
+
‘\_generated/api’, even if the internal function is registered in the same file.
|
|
447
447
|
|
|
448
448
|
## File storage guidelines
|
|
449
449
|
|
|
@@ -515,7 +515,7 @@ differential update mechanisms exist:
|
|
|
515
515
|
([docs](https://www.electron.build/auto-update.html))
|
|
516
516
|
|
|
517
517
|
These can reduce downloads to MB-to-tens-of-MB range (vs full ~~150MB), but not KB-level
|
|
518
|
-
like Electrobun
|
|
518
|
+
like Electrobun’s bsdiff patches (which claim ~~14KB for small changes).
|
|
519
519
|
|
|
520
520
|
# Part 2: Third-Party Perspectives
|
|
521
521
|
|
|
@@ -76,7 +76,7 @@ Better heuristic—choose based on what the caller can do:
|
|
|
76
76
|
|
|
77
77
|
| Failure Type | Pattern | Why |
|
|
78
78
|
| --- | --- | --- |
|
|
79
|
-
| Caller cannot recover | `throw` | Forces handling, can
|
|
79
|
+
| Caller cannot recover | `throw` | Forces handling, can’t be ignored |
|
|
80
80
|
| Caller might retry or degrade | `Result<T>` | Makes recovery explicit |
|
|
81
81
|
| Should never happen | `throw` / assertion | Fail fast, debug fast |
|
|
82
82
|
|
|
@@ -538,7 +538,7 @@ When implementing any operation that can fail:
|
|
|
538
538
|
| Empty catch blocks | Grep for `catch.*\{\s*\}` or catch blocks without throw/return |
|
|
539
539
|
| Lost Result types | TypeScript: enable `@typescript-eslint/no-floating-promises` |
|
|
540
540
|
| Optimistic success | Search for success messages, trace back to verify guards |
|
|
541
|
-
| Catch-and-continue | Audit catch blocks that log but don
|
|
541
|
+
| Catch-and-continue | Audit catch blocks that log but don’t throw/return |
|
|
542
542
|
| Lost exception context | Grep for `new Error.*\.message` (wrapping without cause) |
|
|
543
543
|
| Catch-and-replace | Grep for `} catch {` followed by `throw new` (bare catch discards error) |
|
|
544
544
|
|
|
@@ -19,7 +19,7 @@ author: Joshua Levy (github.com/jlevy) with LLM assistance
|
|
|
19
19
|
```typescript
|
|
20
20
|
// BAD: Hardcoded numbers
|
|
21
21
|
const tradeCount = Math.min(trades.length, 50);
|
|
22
|
-
|
|
22
|
+
|
|
23
23
|
// GOOD: Named constants with documentation
|
|
24
24
|
/**
|
|
25
25
|
* Execution statistics counting limits for dialog tab display.
|
|
@@ -31,7 +31,7 @@ author: Joshua Levy (github.com/jlevy) with LLM assistance
|
|
|
31
31
|
/** Maximum conversation turns to count before showing "100+" */
|
|
32
32
|
maxConversationTurnCount: 100,
|
|
33
33
|
} as const;
|
|
34
|
-
|
|
34
|
+
|
|
35
35
|
// Usage:
|
|
36
36
|
const tradeCount = Math.min(trades.length, EXECUTION_STATS_LIMITS.maxTradeCount);
|
|
37
37
|
```
|
|
@@ -57,7 +57,7 @@ These are language-agnostic rules on comments:
|
|
|
57
57
|
```typescript
|
|
58
58
|
// BAD:
|
|
59
59
|
const MAX_RETRIES = 5; // Maximum number of retries is 5
|
|
60
|
-
|
|
60
|
+
|
|
61
61
|
// BAD:
|
|
62
62
|
/**
|
|
63
63
|
* Get current paper trading timestamp using the configured 9:00 AM ET settings.
|
|
@@ -79,7 +79,7 @@ These are language-agnostic rules on comments:
|
|
|
79
79
|
agentId: mockAgentId,
|
|
80
80
|
// other fields are now removed
|
|
81
81
|
});
|
|
82
|
-
|
|
82
|
+
|
|
83
83
|
// GOOD:
|
|
84
84
|
expect.objectContaining({
|
|
85
85
|
runId: mockRunId,
|
|
@@ -51,5 +51,5 @@ Therefore:
|
|
|
51
51
|
yourself. Avoid subjective descriptions.
|
|
52
52
|
For example, don’t say “I’ve meticulously improved the code and it is in great shape!”
|
|
53
53
|
That is useless generalization.
|
|
54
|
-
Instead, specifically say what you’ve done, e.g.,
|
|
55
|
-
generics, to all the methods in `Foo` and fixed all linter errors
|
|
54
|
+
Instead, specifically say what you’ve done, e.g., “I’ve added types, including
|
|
55
|
+
generics, to all the methods in `Foo` and fixed all linter errors.”
|
|
@@ -109,7 +109,7 @@ These are general rules that *must* be followed on this project for Python code.
|
|
|
109
109
|
Media types. For broad categories only, to determine what processing
|
|
110
110
|
is possible.
|
|
111
111
|
"""
|
|
112
|
-
|
|
112
|
+
|
|
113
113
|
text = "text"
|
|
114
114
|
image = "image"
|
|
115
115
|
audio = "audio"
|
|
@@ -194,7 +194,7 @@ These are general rules that *must* be followed on this project for Python code.
|
|
|
194
194
|
class Link(BaseModel):
|
|
195
195
|
url: str
|
|
196
196
|
title: str = None
|
|
197
|
-
|
|
197
|
+
|
|
198
198
|
# DO NOT write tests like this. They are trivial and only create clutter!
|
|
199
199
|
def test_link_model():
|
|
200
200
|
link = Link(url="https://example.com", title="Example")
|
|
@@ -213,13 +213,13 @@ These are general rules that *must* be followed on this project for Python code.
|
|
|
213
213
|
"""
|
|
214
214
|
Convenience function to check if a string or Path is a URL and if so return
|
|
215
215
|
the `urlparse.ParseResult`.
|
|
216
|
-
|
|
216
|
+
|
|
217
217
|
Also returns false for Paths, so that it's easy to use local paths and URLs
|
|
218
218
|
(`Locator`s) interchangeably. Can provide `HTTP_ONLY` or `HTTP_OR_FILE` to
|
|
219
219
|
restrict to only certain schemes.
|
|
220
220
|
"""
|
|
221
221
|
# Function body
|
|
222
|
-
|
|
222
|
+
|
|
223
223
|
def is_url(text: UnresolvedLocator, only_schemes: list[str] | None = None) -> bool:
|
|
224
224
|
"""
|
|
225
225
|
Check if a string is a URL. For convenience, also returns false for
|
|
@@ -71,14 +71,14 @@ alwaysApply: true
|
|
|
71
71
|
*/
|
|
72
72
|
testSkipScheduling?: boolean;
|
|
73
73
|
}
|
|
74
|
-
|
|
74
|
+
|
|
75
75
|
// Reference it elsewhere
|
|
76
76
|
export const runConfigValidator = v.object({
|
|
77
77
|
logLlmCalls: v.optional(v.boolean()),
|
|
78
78
|
/** @see RunConfig.testSkipScheduling for documentation */
|
|
79
79
|
testSkipScheduling: v.optional(v.boolean()),
|
|
80
80
|
});
|
|
81
|
-
|
|
81
|
+
|
|
82
82
|
// BAD: Documentation duplicated at multiple usage sites
|
|
83
83
|
export const runConfigValidator = v.object({
|
|
84
84
|
/** When true, logs full LLM request/response payloads for debugging. */
|
|
@@ -101,7 +101,7 @@ alwaysApply: true
|
|
|
101
101
|
```ts
|
|
102
102
|
// BAD: Silences type safety
|
|
103
103
|
const logger = createAgentLogger(ctx, agentCtx as any);
|
|
104
|
-
|
|
104
|
+
|
|
105
105
|
// GOOD: Provide a precise input shape or overload that matches
|
|
106
106
|
const logger = createAgentLogger(ctx, {
|
|
107
107
|
runId: runId as Id<'runs'>,
|
|
@@ -125,7 +125,7 @@ alwaysApply: true
|
|
|
125
125
|
trades: { symbol: string; action: 'buy' | 'sell'; price: number }[];
|
|
126
126
|
};
|
|
127
127
|
}
|
|
128
|
-
|
|
128
|
+
|
|
129
129
|
// GOOD: Named type in shared location
|
|
130
130
|
interface FullTradeSummary {
|
|
131
131
|
stats: TradeSummaryStats;
|
|
@@ -147,7 +147,7 @@ alwaysApply: true
|
|
|
147
147
|
const totalSellValue = trades
|
|
148
148
|
.filter((t) => t.action === 'sell')
|
|
149
149
|
.reduce((sum, t) => sum + t.value, 0);
|
|
150
|
-
|
|
150
|
+
|
|
151
151
|
// GOOD: Single shared function computes all related metrics
|
|
152
152
|
function computeTradeSummaryStats(trades: Trade[]): TradeSummaryStats {
|
|
153
153
|
return {
|
|
@@ -180,7 +180,7 @@ alwaysApply: true
|
|
|
180
180
|
throw new Error(`Unhandled field kind: ${(_exhaustive as { kind: string }).kind}`);
|
|
181
181
|
}
|
|
182
182
|
}
|
|
183
|
-
|
|
183
|
+
|
|
184
184
|
// BAD: Missing cases silently fall through or return undefined
|
|
185
185
|
switch (field.kind) {
|
|
186
186
|
case 'string':
|
|
@@ -189,7 +189,7 @@ alwaysApply: true
|
|
|
189
189
|
return handleNumber(field);
|
|
190
190
|
// New field kinds won't cause compile errors!
|
|
191
191
|
}
|
|
192
|
-
|
|
192
|
+
|
|
193
193
|
// BAD: Default that masks missing cases
|
|
194
194
|
switch (field.kind) {
|
|
195
195
|
case 'string':
|
|
@@ -229,7 +229,7 @@ alwaysApply: true
|
|
|
229
229
|
paperTimestamp: number; // Unix timestamp in milliseconds (Date.now() format)
|
|
230
230
|
},
|
|
231
231
|
): Promise<AgentExecCtx>;
|
|
232
|
-
|
|
232
|
+
|
|
233
233
|
// BAD: Optional parameters that can lead to accidental omission
|
|
234
234
|
export function createAgentExecCtx(
|
|
235
235
|
ctx: any,
|
|
@@ -251,7 +251,7 @@ alwaysApply: true
|
|
|
251
251
|
export function doWork(_ctx: any, params: X): Y {
|
|
252
252
|
/* _ctx unused */
|
|
253
253
|
}
|
|
254
|
-
|
|
254
|
+
|
|
255
255
|
// GOOD: Remove unused param and update callers
|
|
256
256
|
export function doWork(params: X): Y {
|
|
257
257
|
/* ... */
|
|
@@ -286,10 +286,10 @@ alwaysApply: true
|
|
|
286
286
|
const { helper } = await import('./helpers.js');
|
|
287
287
|
return helper(args);
|
|
288
288
|
};
|
|
289
|
-
|
|
289
|
+
|
|
290
290
|
// GOOD: Static import at top of file
|
|
291
291
|
import { helper } from './helpers.js';
|
|
292
|
-
|
|
292
|
+
|
|
293
293
|
export const myFunction = async (args) => {
|
|
294
294
|
return helper(args);
|
|
295
295
|
};
|
|
@@ -309,7 +309,7 @@ alwaysApply: true
|
|
|
309
309
|
```ts
|
|
310
310
|
// BAD: Do not re-export imports for re-import elsewhere:
|
|
311
311
|
export { backtestStep } from './experimentExecution';
|
|
312
|
-
|
|
312
|
+
|
|
313
313
|
// GOOD: Import directly from the new location:
|
|
314
314
|
import { backtestStep } from './experimentExecution';
|
|
315
315
|
```
|
|
@@ -327,7 +327,7 @@ alwaysApply: true
|
|
|
327
327
|
// BAD: Pointless backward compatibility in an internal codebase
|
|
328
328
|
// Re-export for backward compatibility
|
|
329
329
|
export { githubBlobToRawUrl as githubToRawUrl } from './github-fetch.js';
|
|
330
|
-
|
|
330
|
+
|
|
331
331
|
// GOOD: Just update imports to use the new name/location directly
|
|
332
332
|
import { githubBlobToRawUrl } from './github-fetch.js';
|
|
333
333
|
```
|
|
@@ -344,13 +344,13 @@ alwaysApply: true
|
|
|
344
344
|
// src/harness/index.ts
|
|
345
345
|
export { FormHarness } from './harness.js';
|
|
346
346
|
export { MockAgent } from './mockAgent.js';
|
|
347
|
-
|
|
347
|
+
|
|
348
348
|
// BAD: Importing through module barrel
|
|
349
349
|
import { FormHarness } from '../harness';
|
|
350
|
-
|
|
350
|
+
|
|
351
351
|
// GOOD: Import directly from source file
|
|
352
352
|
import { FormHarness } from '../harness/harness.js';
|
|
353
|
-
|
|
353
|
+
|
|
354
354
|
// GOOD: Root index.ts for public API is fine
|
|
355
355
|
// src/index.ts (package entry point)
|
|
356
356
|
export { FormHarness } from './harness/harness.js';
|
|
@@ -401,7 +401,7 @@ alwaysApply: true
|
|
|
401
401
|
// BAD: Can leave corrupted file if process crashes mid-write
|
|
402
402
|
import { writeFileSync } from 'fs';
|
|
403
403
|
writeFileSync(filePath, content);
|
|
404
|
-
|
|
404
|
+
|
|
405
405
|
// GOOD: Modern TypeScript-native with zero dependencies
|
|
406
406
|
import { writeFile } from 'atomically';
|
|
407
407
|
await writeFile(filePath, content);
|