get-tbd 0.1.29 → 0.1.30

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.
@@ -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-B38rbI9u.mjs";
1
+ import { a as isInitialized, c as readConfigWithMigration, d as writeConfig, f as writeLocalState, i as initConfig, l as readLocalState, n as findTbdRoot, o as markWelcomeSeen, r as hasSeenWelcome, s as readConfig, t as IncompatibleFormatError, u as updateLocalState } from "./config-DVap9omo.mjs";
2
2
 
3
3
  export { readConfig };
@@ -227,6 +227,10 @@ const CHARS_PER_TOKEN = 3.5;
227
227
  * WHEN TO BUMP THE FORMAT VERSION:
228
228
  * - Bump when changes REQUIRE migration (deleting files, changing formats, moving files)
229
229
  * - **Bump when changing config schema** (adding, removing, or modifying fields)
230
+ * - **Bump when the shape of a generated agent-integration surface changes** (e.g. the
231
+ * managed AGENTS.md block). This same format is stamped there via
232
+ * AGENT_INTEGRATION_FORMAT (integration-paths.ts), so there is ONE format code across
233
+ * all tbd-managed surfaces.
230
234
  * - Do NOT bump for additive changes that don't affect config.yml (new directories, etc.)
231
235
  *
232
236
  * HOW TO ADD A NEW FORMAT VERSION:
@@ -634,5 +638,5 @@ async function markWelcomeSeen(baseDir) {
634
638
  }
635
639
 
636
640
  //#endregion
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 };
638
- //# sourceMappingURL=config-B38rbI9u.mjs.map
641
+ export { getWorkspaceDir as A, TBD_GUIDELINES_DIR as C, WORKSPACES_DIR as D, TBD_TEMPLATES_DIR as E, resolveAtticDir as M, resolveDataSyncDir as N, WORKTREE_DIR as O, TBD_DOCS_DIR as S, TBD_SHORTCUTS_SYSTEM as T, DEFAULT_GUIDELINES_PATHS as _, isInitialized as a, SYNC_BRANCH as b, readConfigWithMigration as c, writeConfig as d, writeLocalState as f, DATA_SYNC_DIR_NAME as g, DATA_SYNC_DIR as h, initConfig as i, isValidWorkspaceName as j, WORKTREE_DIR_NAME as k, readLocalState as l, CHARS_PER_TOKEN as m, findTbdRoot as n, markWelcomeSeen as o, CURRENT_FORMAT as p, hasSeenWelcome as r, readConfig as s, IncompatibleFormatError as t, updateLocalState as u, DEFAULT_SHORTCUT_PATHS as v, TBD_SHORTCUTS_STANDARD as w, TBD_DIR as x, DEFAULT_TEMPLATE_PATHS as y };
642
+ //# sourceMappingURL=config-DVap9omo.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config-DVap9omo.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 * - **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 = '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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"}
@@ -16,7 +16,6 @@ description: >-
16
16
  source code review, or any workflow shortcut.
17
17
  allowed-tools: Bash(tbd:*), Read, Write
18
18
  ---
19
-
20
19
  **`tbd` helps humans and agents ship code with greater speed, quality, and discipline.**
21
20
 
22
21
  1. **Beads**: Git-native issue tracking (tasks, bugs, features).
@@ -111,6 +111,9 @@ So:
111
111
  (§7)
112
112
  - **Maximum reach across many agents** → layer them: AGENTS.md + SKILL.md + CLI + MCP.
113
113
  (§1)
114
+ - **Self-installs into agents & ships evolving skills?** → that is the advanced Tier 2
115
+ pattern (self-upgrade + format versioning); most tools are Tier 1: a pure skill run
116
+ via a **pinned** `npx`/`uvx`. (§6.0)
114
117
 
115
118
  Everything below is reference material.
116
119
  You do not need most of it for most tools.
@@ -172,7 +175,30 @@ Canonical spec: [agents.md](https://agents.md).
172
175
  context).
173
176
  Put deep, on-demand material in skills or files the agent can open when needed.
174
177
  `AGENTS.override.md` lets a developer override the committed file locally without
175
- editing it.
178
+ editing it. If a CLI writes a managed `AGENTS.md` block, keep that block as a compact
179
+ bootstrap: name the tool, state the always-on operating rule, and point to commands such
180
+ as `mycli prime`, `mycli skill`, `mycli shortcut --list`, and `mycli guidelines --list`.
181
+ Do not paste the full skill body or generated resource directories into `AGENTS.md`;
182
+ prefer less than 80-150 lines, and shorter is better.
183
+ If `AGENTS.md` already exists without your markers, preserve the file and append a
184
+ compact marked block instead of overwriting user content.
185
+ Version the managed block by carrying the format on the **begin marker line itself** (an
186
+ `fNN` string, like a config-format version) so future setup runs can upgrade old
187
+ generated content without touching user-authored text:
188
+
189
+ ```markdown
190
+ <!-- BEGIN MYCLI INTEGRATION format=f02 surface=agents-md -->
191
+ ## mycli
192
+
193
+ - Run `mycli prime` for current project context.
194
+ - Run `mycli skill` for complete skill instructions.
195
+
196
+ <!-- END MYCLI INTEGRATION -->
197
+ ```
198
+
199
+ Keep the begin/end marker *names* stable (`<!-- BEGIN MYCLI INTEGRATION`) — match on
200
+ that prefix so detection finds both legacy blocks (no `format=`, treated as `f01`) and
201
+ current ones. Only the `format=fNN` value changes when the block’s shape changes.
176
202
 
177
203
  * * *
178
204
 
@@ -343,7 +369,7 @@ you care about.
343
369
  | Agent | Project file | Skill / rules mechanism | MCP | Hooks | Best integration path |
344
370
  | --- | --- | --- | --- | --- | --- |
345
371
  | **Claude Code** | `CLAUDE.md` | Agent Skills (`SKILL.md`), `.claude/skills/`; plugins/marketplaces | Yes (stdio + Streamable HTTP) | 29 events | SKILL.md (+ plugin for distribution) |
346
- | **Codex CLI** | `AGENTS.md` | `SKILL.md` skills + plugins (skills+MCP); `~/.codex/prompts` (deprecated) | Yes (stdio + Streamable HTTP) | | AGENTS.md + skills/plugins + MCP |
372
+ | **Codex CLI** | `AGENTS.md` | `SKILL.md` skills + plugins (skills+MCP); `~/.codex/prompts` (deprecated) | Yes (stdio + Streamable HTTP) | Claude-compatible engine (`SessionStart`, `Pre/PostCompact`, `Pre/PostToolUse`, `UserPromptSubmit`, `Stop`, …) | AGENTS.md + skills/plugins + MCP |
347
373
  | **Cursor** | `.cursor/rules/*.mdc`, `AGENTS.md` | MDC rules (Always/Auto-glob/Agent-requested/Manual) | Yes | 6 events (incl. `beforeShellExecution`) | AGENTS.md + `.mdc` for glob scoping |
348
374
  | **GitHub Copilot** | `.github/copilot-instructions.md`, `AGENTS.md` | `SKILL.md` (VS Code); `.agent.md` custom agents | Yes | `preToolUse`/`postToolUse`/… | SKILL.md + MCP; enterprise-managed plugins |
349
375
  | **Gemini CLI** | `GEMINI.md` + `AGENTS.md` | Agent Skills; extensions (bundle hooks) | Yes (stdio + SSE) | ~12 events | AGENTS.md + MCP/extension |
@@ -367,11 +393,28 @@ clean endorsement of the CLI-as-skill approach: a self-documenting CLI plus a `S
367
393
  is exactly what a minimal agent wants.
368
394
 
369
395
  **Codex specifics** (it gained a real skill system in 2026): skills are `SKILL.md`
370
- folders with the same progressive disclosure, discovered from
371
- repository/user/admin/system `.agents/skills/` directories.
372
- **Plugins** are one distribution layer on top (installable units bundling skills + MCP
373
- servers 90+ ship with Codex), not the only install path a plain
374
- `.agents/skills/<name>/SKILL.md` works without packaging.
396
+ folders with the same progressive disclosure.
397
+ Verified against the Codex source (`codex-rs/core-skills/src/loader.rs`, tags
398
+ `rust-v0.130.0`…`v0.133.0`): the loader scans several **scopes** `Repo` (every
399
+ `<dir>/.agents/skills/` from the project root down to cwd), `User`
400
+ (`$HOME/.agents/skills/`), `Admin`, plus plugin roots and `$CODEX_HOME/skills`. So a
401
+ **plain repo-root `.agents/skills/<name>/SKILL.md` is read directly**, no manifest
402
+ required. (The repo path is built by joining `.agents` + `skills` at runtime, so it does
403
+ *not* appear as a contiguous `.agents/skills` literal in the binary — a `strings`-based
404
+ inspection will miss it and see only `.agents/plugins/marketplace.json`; confirm against
405
+ the source, not binary strings.)
406
+ **Plugins** are an *additional* distribution layer (installable units bundling skills +
407
+ MCP servers — 90+ ship with Codex), declared in `.agents/plugins/marketplace.json` (also
408
+ reads `.claude-plugin/marketplace.json`) — useful for *publishing a bundle*, but not
409
+ needed to make a repo-local skill load.
410
+ Codex skills may carry a richer **`agents/openai.yaml`** companion (e.g.
411
+ `interface.display_name`, icons, `dependencies.tools[]`,
412
+ `policy.allow_implicit_invocation`); map the portable
413
+ `name`/`description`/`allowed-tools` onto it only when you specifically want that Codex
414
+ polish — it’s optional.
415
+ An experimental, off-by-default **`external_migration`** feature can import `.claude/`
416
+ config (hooks/MCP/skills) into `.codex/`; don’t depend on it yet, but expect the
417
+ portable duplication to shrink if it stabilizes.
375
418
  Operational config lives in `~/.codex/config.toml` (or trusted per-project
376
419
  `.codex/config.toml`): `model`, `approval_policy`
377
420
  (`untrusted`/`on-request`/`granular`/`never`), `sandbox_mode`
@@ -394,6 +437,36 @@ Use this when you have many capabilities, need cross-session state, or want a cu
394
437
  knowledge library the agent pulls from.
395
438
  For a single capability, the §0 baseline is better — don’t reach for this prematurely.
396
439
 
440
+ ### 6.0 Two integration tiers — pick the lighter one
441
+
442
+ Most tools should **not** self-install.
443
+ Decide which tier you are before adding any setup machinery:
444
+
445
+ - **Tier 1 — pure skill (the default for most tools).** Ship a `SKILL.md` (optionally an
446
+ `AGENTS.md` snippet); users install it once (commit to `.agents/skills/`,
447
+ `npx skills add`, or the Claude mirror).
448
+ Invoke the tool through a **version-pinned** zero-install runner —
449
+ `npx --yes pkg@<ver>`, `uvx --from pkg@<ver>`, or `pipx run pkg==<ver>` (§6.7). No
450
+ hooks, no managed `AGENTS.md` block, no `setup` command, no format versioning.
451
+ Pinning the version here does **double duty**:
452
+ - **Supply-chain control** — an unpinned runner (`npx pkg`, `uvx --from pkg`) silently
453
+ re-resolves to the latest published version on every run and bypasses any cool-off
454
+ window. A pinned version is the artifact you actually vetted.
455
+ - **Consistency control** — every teammate and every agent runs the *same* tool
456
+ version, so skill behavior is reproducible across a team and across agents rather
457
+ than drifting as upstream publishes new releases.
458
+ - **Tier 2 — self-installing CLI (advanced; the rest of §6).** A tool that writes its
459
+ own integration files into multiple agents (`.agents/skills/`, `.claude/skills/`, a
460
+ managed `AGENTS.md` block, hooks, `.codex/` config) **and** whose skill content
461
+ evolves across releases.
462
+ Take on this complexity only for a tool with many capabilities, cross-session state,
463
+ or a curated knowledge library.
464
+ The self-upgrade and format-versioning rules in §6.6 apply **only to this tier** — a
465
+ pure skill never needs them.
466
+
467
+ If in doubt, you are Tier 1. `tbd` is a Tier-2 reference implementation; most CLIs are
468
+ not.
469
+
397
470
  ### 6.1 Two kinds of commands
398
471
 
399
472
  | Type | Purpose | Examples |
@@ -483,31 +556,186 @@ Rules: reference commands **explicitly** (`mycli command arg`, never “see the
483
556
  ### 6.6 Distribution & multi-agent install
484
557
 
485
558
  A CLI can install itself into multiple agents from one `setup` run.
486
- tbd writes a CLI-managed `SKILL.md` to `.claude/skills/tbd/` and a **marker-bounded
487
- section** into `AGENTS.md` (which now also feeds Cursor, Codex, and Factory), preserving
488
- user content outside the markers:
559
+ Use the portable Agent Skills location as the primary project skill surface and mirror
560
+ only where a target agent requires it:
561
+
562
+ - `.agents/skills/<tool>/SKILL.md` — the portable project skill.
563
+ **Be precise about who reads this path natively vs.
564
+ who reaches it via an installer**, rather than claiming a flat “all agents read it”:
565
+ - **Scans repo-root `.agents/skills/` natively** (verified): **Codex** (source above)
566
+ and **Gemini CLI** (documents `.agents/skills/` as a workspace/user alias).
567
+ **pi** / **OpenCode** scan project Agent Skills dirs.
568
+ - **Reached via the cross-agent installer**: `npx skills add` copies the same
569
+ `SKILL.md` into `.agents/skills/` and **symlinks it into each agent’s own dir**
570
+ (Cursor, Copilot, Cline, Amp, Windsurf, …). For those agents the *installer*, not
571
+ the agent, is what binds `.agents/skills/` to their native location — so “works with
572
+ Cursor/Copilot/…” means “via skills.sh”, not “Cursor scans `.agents/skills/`
573
+ itself.”
574
+ - **Claude Code does NOT scan `.agents/`** at all — it reads only `.claude/skills/`
575
+ (next bullet), which is why the mirror is required, not optional.
576
+ - When in doubt, verify against the agent’s source/docs before asserting native
577
+ scanning.
578
+ - `.claude/skills/<tool>/SKILL.md` — Claude Code compatibility mirror (required: Claude
579
+ Code reads only this path).
580
+ - `AGENTS.md` — compact always-on project bootstrap, not a full copy of the skill.
581
+ - `.codex/hooks.json` or `.codex/config.toml` — Codex lifecycle automation, not policy
582
+ text or skill content.
583
+
584
+ tbd should write a CLI-managed `SKILL.md` to `.agents/skills/tbd/`, mirror it to
585
+ `.claude/skills/tbd/`, and maintain a **marker-bounded section** in `AGENTS.md` (which
586
+ also feeds Cursor, Codex, and Factory), preserving user content outside the markers:
489
587
 
490
588
  ```markdown
491
- <!-- BEGIN MYCLI INTEGRATION -->
492
- …CLI-generated… ← owned by the CLI; regenerated on setup
589
+ <!-- BEGIN MYCLI INTEGRATION format=f02 surface=agents-md -->
590
+ ## mycli
591
+
592
+ - Run `mycli prime` for current project context.
593
+ - Run `mycli skill` for the full reusable skill instructions.
594
+ - Run `mycli shortcut --list` and `mycli guidelines --list` for on-demand resources.
595
+
493
596
  <!-- END MYCLI INTEGRATION -->
494
597
  ```
495
598
 
599
+ **Quick recipe for a new project** (portable-first, both agents covered):
600
+
601
+ ```text
602
+ .agents/skills/<tool>/SKILL.md # canonical portable skill (Codex, Gemini, Cursor, …)
603
+ .claude/skills/<tool>/SKILL.md # identical copy — Claude Code mirror
604
+ AGENTS.md # compact marked block (see above), every agent reads it
605
+ CLAUDE.md # symlink → AGENTS.md, or a short separate file (Claude only)
606
+ scripts/agent/<tool>-session.sh # shared hook script, referenced by both agents
607
+ .codex/hooks.json # Codex hook entry → shared script (or inline [hooks])
608
+ .claude/settings.json # Claude hook entry → same shared script
609
+ ```
610
+
611
+ Copy (don’t symlink) the `SKILL.md` payload to both skill paths — symlinks behave
612
+ unevenly across Windows, sandboxes, and remote worktrees.
613
+ Claude Code does **not** auto-load `AGENTS.md` (it reads `CLAUDE.md`), so a multi-agent
614
+ project needs both.
615
+
496
616
  **File-ownership rules** — distinguish three categories:
497
617
 
498
618
  - **Project instruction files** (`AGENTS.md`, `CLAUDE.md`): *commit these*. They hold
499
619
  human-authored project norms (§2). A CLI may own a **marker-bounded section** inside
500
620
  `AGENTS.md` (regenerated on setup) while the user owns everything outside the markers.
501
- - **Fully generated install artifacts** (`.claude/skills/<tool>/SKILL.md` and the like):
502
- CLI-owned; mark them “DO NOT EDIT.” Commit them only if the project intentionally
503
- dogfoods them otherwise leave them to `setup` (and consider gitignoring).
621
+ - **Fully generated install artifacts** (`.agents/skills/<tool>/SKILL.md`,
622
+ `.claude/skills/<tool>/SKILL.md`, generated hook scripts, and the like): CLI-owned;
623
+ mark them “DO NOT EDIT.” Pick **one of two modes** and be consistent:
624
+ - **Commit + dogfood** (what `tbd` does): check the generated artifacts in, and add a
625
+ **drift test** that regenerates them and fails if they differ.
626
+ Pros: browsable on GitHub / skills.sh, the repo demonstrates its own output,
627
+ reviewers see changes.
628
+ Con: a regeneration shows up as a diff to commit.
629
+ Keep generated output deterministic and formatter-stable (below) or the drift
630
+ test/commits will churn.
631
+ - **Gitignore + regenerate** (what `metaproc` does): add `.../skills/*/SKILL.md` to
632
+ `.gitignore` and let `setup`/`--install` (re)create them on demand.
633
+ Pros: zero commit churn, no drift to guard.
634
+ Con: not browsable in the repo, and no committed artifact to diff in review.
635
+ With this mode a format-version stamp matters less (there is no committed artifact
636
+ for an older tool to clobber).
504
637
  - **Source files** in the CLI package (header, baseline, brief): the canonical inputs —
505
638
  always version-controlled.
506
639
 
507
640
  Make setup idempotent: dedupe hooks before merging, overwrite generated skills rather
508
641
  than patching them, update only the marked section of `AGENTS.md`, and clean up legacy
509
- files each run. (Generated output must also be stable under whatever formatter the repo
510
- runs — e.g. don’t emit a second YAML frontmatter block mid-document.)
642
+ files each run.
643
+
644
+ **Generated output must be deterministic.** A given input state must always produce
645
+ byte-identical output — no timestamps, no random IDs, no machine-specific paths, no
646
+ unstable ordering. This is what makes the artifact diff-stable, drift-testable, and safe
647
+ to regenerate. It must also be stable under whatever formatter the repo runs (e.g. emit
648
+ the managed block in the formatter’s canonical form — sentence-aware line wrapping,
649
+ correct quote style — so a format pass is a no-op; and don’t emit a second YAML
650
+ frontmatter block mid-document).
651
+ Because Codex and Claude Code now share a hook event schema (§8), prefer **one shared
652
+ script referenced by two thin per-agent configs**: keep the logic in a neutral location
653
+ (e.g. `scripts/agent/<tool>-session.sh`) and reference it from both
654
+ `.claude/settings.json` and the Codex `[hooks]`/`.codex/hooks.json` entry.
655
+ Do not make Codex hooks call scripts stored under `.claude/` — that couples Codex setup
656
+ to Claude setup. If a script must move out of `.claude/scripts/`, update the tbd-owned
657
+ hook commands (or leave a wrapper) so existing Claude hooks keep working.
658
+
659
+ **Upgrade existing installs deliberately (Tier 2 only).** A self-installing tool whose
660
+ skill content evolves *will* leave older generated files in users’ repos.
661
+ Treat generated integration files like config migrations:
662
+
663
+ - Version the generated surfaces with an `fNN` format code.
664
+ Prefer **one format code for all the tool’s managed surfaces** — reuse the tool’s
665
+ existing config/data-format version as the single source of truth (tbd stamps the
666
+ AGENTS.md block with the same `tbd_format`, currently `f03`) rather than maintaining a
667
+ parallel counter. Bump it when any managed surface — config schema or a generated agent
668
+ surface — changes shape.
669
+ - Stamp the format on the generated artifact itself: on the `AGENTS.md` begin-marker
670
+ line (`<!-- BEGIN … format=fNN … -->`), the skill “DO NOT EDIT” marker, script
671
+ headers, or an equivalent hook signature.
672
+ Prefer one marker line over a separate metadata comment.
673
+ - On every `setup`/`setup --auto` run, **self-upgrade in place, safely and
674
+ idempotently**: detect older formats and rewrite only the tool-owned regions (managed
675
+ `AGENTS.md` block, generated skills, tool-owned hooks, `.codex/` config), re-running
676
+ cleanly with no change when already current.
677
+ - Treat old marked `AGENTS.md` blocks with no metadata as legacy generated content and
678
+ replace only the managed region.
679
+ - Detect tool-owned hook entries by command/path/signature, replace only those entries,
680
+ and preserve unrelated user hooks.
681
+ - **Forward-compatibility guard.** When the tool finds a generated artifact whose
682
+ `integration-format` is **newer** than the running version understands, it must **stop
683
+ and tell the user to upgrade the tool** (e.g. `npm install -g get-tbd@latest`) rather
684
+ than overwrite or downgrade it.
685
+ This is what makes pinning safe for teams: a teammate on an older version fails loudly
686
+ instead of silently clobbering a newer managed block.
687
+ - Print an itemized setup summary: current, installed, upgraded, removed legacy, skipped
688
+ by config, user-owned/unmarked, and format-too-new (upgrade required).
689
+ - Test upgrades from at least the previous shipped setup layout plus partial installs,
690
+ and test that a too-new format string produces the upgrade-the-tool error.
691
+
692
+ Recommended setup flags:
693
+
694
+ | Flag | Purpose |
695
+ | --- | --- |
696
+ | `--auto` | Detect and refresh relevant project-local integrations |
697
+ | `--all` | Install every supported project-local integration surface |
698
+ | `--claude` | Install or refresh the Claude Code surface (skill mirror + hooks) |
699
+ | `--codex` | Install or refresh the Codex surface (`AGENTS.md` block + `.codex` hooks) |
700
+ | `--skip-<surface>` | Suppress a surface (e.g. `--skip-claude`) that auto-detection would otherwise update |
701
+
702
+ Use a true tri-state: with no targeting flag a surface is detection-based; a positive
703
+ flag forces it on (and suppresses auto-detection of untargeted surfaces); `--skip-*`
704
+ forces it off. Avoid Commander’s `--no-<x>` for surfaces — it defaults the value to
705
+ `true`, which would force-install on every run.
706
+ (`tbd` itself ships `--all`, `--claude`, `--codex`, `--skip-claude`, `--skip-codex`;
707
+ `AGENTS.md` installs as part of the Codex surface.)
708
+
709
+ Keep project-local setup separate from global/user setup.
710
+ Writing `~/.codex/AGENTS.md`, `~/.agents/skills/`, or `~/.claude/skills/` should be an
711
+ explicit global install command or documented manual step, not something `setup --auto`
712
+ does silently.
713
+
714
+ #### 6.6.1 Extensible skill registries (let other packages contribute skills)
715
+
716
+ A single bundled skill is enough for most tools.
717
+ But when a tool is a **platform** that other packages extend, don’t hard-code its skill
718
+ list — expose a **registry** so any installed package can contribute a skill that the
719
+ CLI discovers at runtime.
720
+
721
+ The clean implementation is the host language’s plugin mechanism:
722
+
723
+ - **Python**: an entry-point group.
724
+ The host defines a group (e.g. `[project.entry-points."mytool.skills"]`); each plugin
725
+ package points an entry at a factory that returns a skill spec; the host enumerates
726
+ them with `importlib.metadata`. (`metaproc` does exactly this: a `metaproc.skills`
727
+ group, with `metaproc skill --list` / `--install` composing each registered skill.
728
+ Its `earnings_predictions` package registers an `eia-batch` skill that the core tool
729
+ never hard-codes.)
730
+ - **Node/TypeScript**: a documented `package.json` key or a registration API a plugin
731
+ calls on load.
732
+
733
+ Keep each registered skill a **spec** (name, two-part description, `allowed-tools`, a
734
+ baseline source, and an optional dynamic catalog function) and run them all through the
735
+ **same** `compose` + `--install` path, so every skill — first-party or third-party —
736
+ gets identical frontmatter, the `DO NOT EDIT`/format marker, and deterministic output.
737
+ This keeps the “one tool, many self-injecting commands” model open for extension without
738
+ the core tool taking a dependency on every plugin.
511
739
 
512
740
  ### 6.7 Making the CLI available: global install vs. zero-install
513
741
 
@@ -554,16 +782,32 @@ npx mytool@1.4.2 ... # not `npx mytool@latest`
554
782
  ```
555
783
 
556
784
  Global installs get the same guarantee from the lockfile/manifest; zero-install gets it
557
- only from an explicit `@version`.
785
+ only from an explicit `@version`. Generated skill instructions should use a local-first,
786
+ pinned fallback chain:
787
+
788
+ 1. Try `mycli <command>` if it is already on `PATH`.
789
+ 2. Fall back to a version-pinned runner:
790
+ - npm: `npx --yes my-package@<version> mycli <command>`
791
+ - uv: `uvx --from my-package@<version> mycli <command>`
792
+ - pipx: `pipx run my-package==<version> mycli <command>`
793
+ - Go: `go run module/path@<version> <args>`
794
+ 3. If the local command and pinned fallback both fail, stop and tell the user how to
795
+ install the CLI.
796
+
797
+ Never put an unpinned network runner such as `uvx --from my-package` or `npx my-package`
798
+ in generated skill instructions unless the user explicitly opts into that risk.
799
+ This is a supply-chain control, not just ergonomics: an unpinned runner re-resolves to
800
+ the latest published version on every run and bypasses any cool-off window.
801
+ See `tbd guidelines supply-chain-hardening` for the cross-ecosystem policy.
558
802
 
559
803
  **Current tooling (May 2026)**
560
804
 
561
805
  - **Node / TypeScript**: zero-install via `npx <pkg>@<ver>` (`-y` to skip the prompt),
562
806
  `bunx`, `pnpm dlx`, or `deno run`; persistent via `npm i -g` / a project
563
807
  devDependency.
564
- - **Python**: `uvx <pkg>@<ver>` (= `uv tool run`, bundled with Astral’s `uv`, Rust-fast,
565
- no Python prereq) or `pipx run`; persistent via `uv tool install` / `pipx install`.
566
- `uvx` reuses a persistent install if one exists.
808
+ - **Python**: `uvx --from <pkg>@<ver> <entrypoint>` (= `uv tool run`, bundled with
809
+ Astral’s `uv`, Rust-fast, no Python prereq) or `pipx run <pkg>==<ver>`; persistent via
810
+ `uv tool install` / `pipx install`. `uvx` reuses a persistent install if one exists.
567
811
  - **Go**: `go run <module>@<ver>` (compiles on the fly) or `go install`.
568
812
  - **Rust**: no first-class zero-install runner — ship **prebuilt binaries** (GitHub
569
813
  releases + a `curl … | sh` installer) or `cargo binstall`; `cargo install` compiles.
@@ -580,6 +824,60 @@ configs; **CLIs** like Beads offer `brew` / `npm -g` / `curl` installers, while
580
824
  **global install + a `SessionStart` bootstrap** as the optimization for persistent
581
825
  environments where the project wants lockfile-managed versions and warm-start speed.
582
826
 
827
+ ### 6.8 Publishing & discovery — make the skill installable
828
+
829
+ Most “skill registries” (May 2026) are **GitHub-repo discoverers, not gated app
830
+ stores**. You don’t submit a form; you put a spec-compliant `SKILL.md` in a public repo
831
+ and the ecosystem finds it.
832
+ The landscape worth targeting:
833
+
834
+ - **`skills.sh` / `npx skills add <owner/repo>`** (Vercel) — the cross-agent “npm for
835
+ skills”: one command installs into `.agents/skills/` + symlinks per agent (Claude
836
+ Code, Codex, Cursor, Copilot, Gemini, …). No review; ranked by install telemetry.
837
+ **This is the highest-leverage target** and needs zero extra infra.
838
+ - **GitHub-scraping indexers** (SkillsMP ~800k skills, ClaudeSkills.info, LobeHub,
839
+ claudemarketplaces.com) — auto-list public repos that contain a `SKILL.md` (often
840
+ gated on ≥2 stars). You get listed for free just by being public + discoverable.
841
+ - **Plugin marketplaces** — `.claude-plugin/marketplace.json` (Claude Code, the official
842
+ Anthropic channel) and `.agents/plugins/marketplace.json` (Codex; Codex reads both).
843
+ These are *plugin* channels: bundles of skills + MCP + hooks + commands.
844
+ They are **only for publishing a bundle** — a repo-local skill already loads from
845
+ `.claude/skills/` (Claude Code) and `.agents/skills/` (Codex) **without any
846
+ manifest**, so don’t add one just to be discovered.
847
+ If you *do* emit a `marketplace.json` / `.codex-plugin/plugin.json`, treat it like any
848
+ generated artifact (§6.6): point it at the same generated `SKILL.md` payload (no body
849
+ duplication), mark it `DO NOT EDIT`, make it deterministic so re-install is a no-op,
850
+ and pick the same commit-vs-gitignore mode as the skill it references.
851
+
852
+ **The simplest publishable structure** (works for all of the above at once):
853
+
854
+ ```
855
+ your-repo/
856
+ └── skills/
857
+ └── <name>/
858
+ └── SKILL.md # spec frontmatter: name + two-part description
859
+ ```
860
+
861
+ `skills/<name>/SKILL.md` at the repo root is the universal discovery location
862
+ (`npx skills add`, the indexers, and agent installers all scan it).
863
+ That’s the whole publishing step — push it public.
864
+
865
+ **For a CLI-backed skill** (the §6 pattern), one extra rule matters: a registry installs
866
+ **only the Markdown**, never your binary.
867
+ So the published `SKILL.md` must **bootstrap its own CLI** — lead with a pinned install
868
+ line (`npm i -g <pkg>@<ver>` / `uvx --from <pkg>@<ver>`) and a one-time `setup`, and
869
+ have commands degrade with a clear “install the CLI first” message.
870
+ Treat the registry copy as a **landing page that installs the engine**, not the engine
871
+ itself. Generate this distribution `SKILL.md` from the same source as your in-repo skill
872
+ (so it can’t drift), and validate it with `npx skills-ref validate skills/<name>` before
873
+ pushing.
874
+
875
+ `tbd` does exactly this: `skills/tbd/SKILL.md` is generated at build time from the same
876
+ baseline, carries `name: tbd` + a trigger-rich description, and opens with the
877
+ `npm install -g get-tbd` + `tbd setup --auto` bootstrap — so `npx skills add jlevy/tbd`
878
+ gives an agent a working landing page, and `tbd setup` then upgrades it to the full
879
+ multi-agent install (§6.6).
880
+
583
881
  * * *
584
882
 
585
883
  ## 7. CLI vs MCP vs Skill — Choosing the Surface
@@ -628,6 +926,17 @@ Support varies:
628
926
  `beforeShellExecution`/`beforeMCPExecution`), **Windsurf** (pre-hooks can **block**
629
927
  via exit code 2), **Gemini CLI** (~12), and **opencode** (25+, with tool interception)
630
928
  all have lifecycle hooks.
929
+ - **Codex** (as of May 2026) ships a **Claude-style hooks engine that uses the same
930
+ event schema as Claude Code** — `SessionStart`, `PreCompact`/`PostCompact`,
931
+ `PreToolUse`/`PostToolUse`, `UserPromptSubmit`, `Stop`,
932
+ `SubagentStart`/`SubagentStop`, and `PermissionRequest`. Hooks load from `hooks.json`
933
+ **or an inline `[hooks]` table in `config.toml`** next to an active config layer
934
+ (`~/.codex/…` for user scope, `<repo>/.codex/…` for project scope).
935
+ Only **command** handlers run today; `prompt`/ `agent` handlers are parsed but
936
+ skipped. Because the schema matches Claude’s, a tool’s
937
+ `SessionStart`/`PreCompact`/`PostToolUse` hooks map almost 1:1 across both agents.
938
+ Repo-local hooks should resolve scripts from the git root or `.codex/`, not from
939
+ `.claude/`, so Codex setup stays independent of Claude Code setup.
631
940
  - **Aider**, **Jules**, **Zed** have no agent hooks (Aider integrates Git pre-commit
632
941
  hooks only).
633
942
 
@@ -757,10 +1066,13 @@ going:
757
1066
  - [ ] `SKILL.md` with `name` + two-part `description`
758
1067
  - [ ] Body < 500 lines; bulky material in supporting files one level deep
759
1068
  - [ ] Third-person description, trigger keywords front-loaded
760
- - [ ] Installable via commit to `.claude/skills/` and/or `npx skills add`
1069
+ - [ ] Installable via commit to `.agents/skills/`, Claude mirror at `.claude/skills/`,
1070
+ and/or `npx skills add`
761
1071
 
762
1072
  **Project**
763
1073
  - [ ] `AGENTS.md` with build/test/style/conventions (concise)
1074
+ - [ ] Managed `AGENTS.md` block uses a stable begin/end marker with a `format=fNN` field
1075
+ on the begin line
764
1076
  - [ ] `CLAUDE.md` strategy decided (symlink to `AGENTS.md`, copy, or separate)
765
1077
 
766
1078
  **CLI tool (if applicable)**
@@ -768,7 +1080,7 @@ going:
768
1080
  - [ ] Idempotent `setup --auto`; `init` for surgical config
769
1081
  - [ ] Help epilog with `IMPORTANT:` + Getting Started one-liner
770
1082
  - [ ] `prime` (status/context) and `skill` (pure docs) commands
771
- - [ ] Invocation strategy chosen (§6.7): pinned zero-install (`npx`/`uvx <pkg>@<ver>`)
1083
+ - [ ] Invocation strategy chosen (§6.7): local-first plus pinned zero-install fallback
772
1084
  by default, or global install + `SessionStart` bootstrap for cloud/ephemeral agents
773
1085
 
774
1086
  **Advanced (many subcommands / knowledge library)**
@@ -782,6 +1094,7 @@ going:
782
1094
  - [ ] Decide target agents; add per-agent files only where needed
783
1095
  - [ ] MCP server only if no CLI fits, or for OAuth/multi-tenant/remote
784
1096
  - [ ] Marker-bounded multi-agent install; “DO NOT EDIT” on generated files
1097
+ - [ ] Existing installs upgrade item-by-item without rewriting user-owned content
785
1098
 
786
1099
  **Security**
787
1100
  - [ ] Third-party skills vetted, scanned, and pinned