threadroot 0.1.0 → 0.1.1

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/dist/index.js CHANGED
@@ -36,7 +36,7 @@ var harnessManifestSchema = z2.object({
36
36
  name: z2.string().min(1),
37
37
  version: z2.literal(1),
38
38
  profile: profileIdSchema,
39
- adapters: z2.array(adapterIdSchema).min(1),
39
+ adapters: z2.array(adapterIdSchema).default([]),
40
40
  references: z2.array(referenceSchema).default([]),
41
41
  memory: z2.object({
42
42
  budget: z2.record(memoryTypeSchema, z2.number().int().positive()).default({})
@@ -1042,13 +1042,13 @@ async function runDiff(repoRoot) {
1042
1042
  }
1043
1043
  }
1044
1044
  if (changed === 0) {
1045
- console.log("No drift: every vendor file matches the canonical harness.");
1045
+ console.log("No drift: optional compiled outputs match the canonical harness.");
1046
1046
  }
1047
1047
  }
1048
1048
 
1049
1049
  // src/core/doctor.ts
1050
- import { stat as stat4 } from "fs/promises";
1051
- import path14 from "path";
1050
+ import { stat as stat5 } from "fs/promises";
1051
+ import path16 from "path";
1052
1052
 
1053
1053
  // src/core/install/lock.ts
1054
1054
  import { mkdir as mkdir3, readFile as readFile4, writeFile as writeFile3 } from "fs/promises";
@@ -1711,6 +1711,407 @@ async function checkConnections(repoRoot, options = {}) {
1711
1711
  return Promise.all(harness.connections.map((connection) => checkConnection(repoRoot, connection)));
1712
1712
  }
1713
1713
 
1714
+ // src/core/setup.ts
1715
+ import { mkdir as mkdir5, readFile as readFile6, rm, stat as stat3, writeFile as writeFile5 } from "fs/promises";
1716
+ import { homedir } from "os";
1717
+ import path13 from "path";
1718
+
1719
+ // src/core/agent-providers.ts
1720
+ import path12 from "path";
1721
+ var AGENT_PROVIDER_IDS = [
1722
+ "antigravity",
1723
+ "claude",
1724
+ "codex",
1725
+ "cursor",
1726
+ "gemini",
1727
+ "copilot",
1728
+ "opencode",
1729
+ "windsurf"
1730
+ ];
1731
+ var AGENT_PROVIDERS = {
1732
+ antigravity: {
1733
+ id: "antigravity",
1734
+ label: "Antigravity",
1735
+ projectSkillDir: path12.join(".agent", "skills"),
1736
+ globalSkillDir: path12.join(".gemini", "antigravity", "skills")
1737
+ },
1738
+ claude: {
1739
+ id: "claude",
1740
+ label: "Claude Code",
1741
+ projectSkillDir: path12.join(".claude", "skills"),
1742
+ globalSkillDir: path12.join(".claude", "skills")
1743
+ },
1744
+ codex: {
1745
+ id: "codex",
1746
+ label: "Codex",
1747
+ projectSkillDir: path12.join(".agents", "skills"),
1748
+ globalSkillDir: path12.join(".agents", "skills")
1749
+ },
1750
+ cursor: {
1751
+ id: "cursor",
1752
+ label: "Cursor",
1753
+ projectSkillDir: path12.join(".cursor", "skills"),
1754
+ globalSkillDir: path12.join(".cursor", "skills")
1755
+ },
1756
+ gemini: {
1757
+ id: "gemini",
1758
+ label: "Gemini CLI",
1759
+ projectSkillDir: path12.join(".gemini", "skills"),
1760
+ globalSkillDir: path12.join(".gemini", "skills")
1761
+ },
1762
+ copilot: {
1763
+ id: "copilot",
1764
+ label: "GitHub Copilot",
1765
+ projectSkillDir: path12.join(".github", "skills"),
1766
+ globalSkillDir: path12.join(".copilot", "skills")
1767
+ },
1768
+ opencode: {
1769
+ id: "opencode",
1770
+ label: "OpenCode",
1771
+ projectSkillDir: path12.join(".opencode", "skills"),
1772
+ globalSkillDir: path12.join(".config", "opencode", "skills")
1773
+ },
1774
+ windsurf: {
1775
+ id: "windsurf",
1776
+ label: "Windsurf",
1777
+ projectSkillDir: path12.join(".windsurf", "skills"),
1778
+ globalSkillDir: path12.join(".codeium", "windsurf", "skills")
1779
+ }
1780
+ };
1781
+ var ALIASES = {
1782
+ all: "all",
1783
+ agent: "antigravity",
1784
+ antigravity: "antigravity",
1785
+ claude: "claude",
1786
+ "claude-code": "claude",
1787
+ codex: "codex",
1788
+ cursor: "cursor",
1789
+ gemini: "gemini",
1790
+ "gemini-cli": "gemini",
1791
+ copilot: "copilot",
1792
+ github: "copilot",
1793
+ "github-copilot": "copilot",
1794
+ opencode: "opencode",
1795
+ "open-code": "opencode",
1796
+ windsurf: "windsurf"
1797
+ };
1798
+ function parseAgentProviderList(value, fallback = "all") {
1799
+ const raw = value?.trim();
1800
+ if (!raw) {
1801
+ return fallback === "all" ? [...AGENT_PROVIDER_IDS] : fallback;
1802
+ }
1803
+ const ids = /* @__PURE__ */ new Set();
1804
+ for (const entry of raw.split(",")) {
1805
+ const key = entry.trim().toLowerCase();
1806
+ if (!key) {
1807
+ continue;
1808
+ }
1809
+ const resolved = ALIASES[key];
1810
+ if (!resolved) {
1811
+ throw new Error(`Unknown agent provider \`${entry}\`. Supported: ${AGENT_PROVIDER_IDS.join(", ")}, all.`);
1812
+ }
1813
+ if (resolved === "all") {
1814
+ for (const id of AGENT_PROVIDER_IDS) {
1815
+ ids.add(id);
1816
+ }
1817
+ continue;
1818
+ }
1819
+ ids.add(resolved);
1820
+ }
1821
+ return [...ids];
1822
+ }
1823
+
1824
+ // src/core/managed-block.ts
1825
+ function hasManagedBlock(content, begin, end) {
1826
+ return content.includes(begin) && content.includes(end);
1827
+ }
1828
+ function upsertManagedBlock(content, block, begin, end) {
1829
+ const start = content.indexOf(begin);
1830
+ const finish = content.indexOf(end);
1831
+ const normalizedBlock = block.endsWith("\n") ? block : `${block}
1832
+ `;
1833
+ if (start !== -1 && finish !== -1 && finish > start) {
1834
+ const afterEnd = finish + end.length;
1835
+ return `${content.slice(0, start)}${normalizedBlock}${content.slice(afterEnd).replace(/^\n+/, "")}`;
1836
+ }
1837
+ const prefix = content.trim().length > 0 ? `${content.trimEnd()}
1838
+
1839
+ ` : "";
1840
+ return `${prefix}${normalizedBlock}`;
1841
+ }
1842
+ function removeManagedBlock(content, begin, end) {
1843
+ const start = content.indexOf(begin);
1844
+ const finish = content.indexOf(end);
1845
+ if (start === -1 || finish === -1 || finish < start) {
1846
+ return content;
1847
+ }
1848
+ const afterEnd = finish + end.length;
1849
+ return `${content.slice(0, start).trimEnd()}${content.slice(afterEnd).replace(/^\n+/, "\n")}`.trimStart();
1850
+ }
1851
+
1852
+ // src/core/threadroot-skill.ts
1853
+ var THREADROOT_SKILL_NAME = "threadroot";
1854
+ var THREADROOT_MANAGED_MARKER = "<!-- threadroot:managed skill -->";
1855
+ function threadrootSkillContent(provider, scope) {
1856
+ const scopeLine = scope === "global" ? "This is a global machine-level skill. Use it only when the current repository contains `.threadroot/` or the user asks for Threadroot." : "This is a project-level skill for the current repository.";
1857
+ return [
1858
+ "---",
1859
+ `name: ${THREADROOT_SKILL_NAME}`,
1860
+ "description: Use when a repository contains .threadroot/ or the user asks to initialize, inspect, repair, or use Threadroot harness context, skills, tools, memory, connections, or agent setup.",
1861
+ "---",
1862
+ "",
1863
+ THREADROOT_MANAGED_MARKER,
1864
+ "",
1865
+ "# Threadroot Harness",
1866
+ "",
1867
+ scopeLine,
1868
+ "",
1869
+ "Threadroot keeps agent-facing project context in `.threadroot/` and exposes it through deterministic CLI commands. Keep broad context out of the chat until it is task-relevant.",
1870
+ "",
1871
+ "## Workflow",
1872
+ "",
1873
+ "1. If `threadroot --version` works, use `threadroot`. Otherwise use `npx --yes threadroot@latest` for one-off commands.",
1874
+ "2. If `.threadroot/harness.yaml` is missing and the user wants setup, run `threadroot init` or `npx --yes threadroot@latest init`.",
1875
+ "3. Before coding in a Threadroot repo, run `threadroot doctor` and resolve errors. Treat warnings as review items, not automatic blockers.",
1876
+ '4. For the current task, run `threadroot context "<task>"` and use the returned skills, rules, tools, memory, and references before doing broad file reads.',
1877
+ "5. Use `threadroot status` to inspect harness state and `threadroot diff` only when compiled adapter outputs are enabled.",
1878
+ "6. Use `threadroot tools list`, `threadroot tools check`, and `threadroot run <tool>` for explicit local capabilities. Confirm risky tools when required.",
1879
+ "7. Do not create provider-specific files unless the user asks. Use `threadroot expose <agent>` when native project skill shims are desired.",
1880
+ "",
1881
+ "## Useful Commands",
1882
+ "",
1883
+ "```bash",
1884
+ "threadroot doctor",
1885
+ "threadroot status",
1886
+ 'threadroot context "<task>"',
1887
+ "threadroot skills list",
1888
+ "threadroot tools list",
1889
+ "threadroot packs list",
1890
+ "```",
1891
+ "",
1892
+ "## Boundaries",
1893
+ "",
1894
+ "- `.threadroot/` is the source of truth.",
1895
+ "- Keep generated or exposed provider files thin.",
1896
+ "- Never store secrets in Threadroot. Connections should wrap locally authenticated CLIs.",
1897
+ "- Inspect external skills, scripts, tools, and MCP servers before trusting them.",
1898
+ "",
1899
+ `Provider target: ${provider.label}.`,
1900
+ ""
1901
+ ].join("\n");
1902
+ }
1903
+
1904
+ // src/core/setup.ts
1905
+ var CODEX_AGENTS_BEGIN = "<!-- threadroot:begin global-codex -->";
1906
+ var CODEX_AGENTS_END = "<!-- threadroot:end global-codex -->";
1907
+ var CODEX_MCP_BEGIN = "# threadroot:begin codex-mcp";
1908
+ var CODEX_MCP_END = "# threadroot:end codex-mcp";
1909
+ async function readMaybe(filePath) {
1910
+ try {
1911
+ return await readFile6(filePath, "utf8");
1912
+ } catch (error) {
1913
+ if (error.code === "ENOENT") {
1914
+ return void 0;
1915
+ }
1916
+ throw error;
1917
+ }
1918
+ }
1919
+ async function fileExists(filePath) {
1920
+ try {
1921
+ const info = await stat3(filePath);
1922
+ return info.isFile();
1923
+ } catch (error) {
1924
+ if (error.code === "ENOENT") {
1925
+ return false;
1926
+ }
1927
+ throw error;
1928
+ }
1929
+ }
1930
+ function displayPath(home, filePath) {
1931
+ const relative = path13.relative(home, filePath);
1932
+ return relative && !relative.startsWith("..") ? path13.join("~", relative) : filePath;
1933
+ }
1934
+ function globalSkillPath(home, provider) {
1935
+ return path13.join(home, provider.globalSkillDir, THREADROOT_SKILL_NAME, "SKILL.md");
1936
+ }
1937
+ function codexAgentsPath(home) {
1938
+ return path13.join(home, ".codex", "AGENTS.md");
1939
+ }
1940
+ function codexConfigPath(home) {
1941
+ return path13.join(home, ".codex", "config.toml");
1942
+ }
1943
+ function codexAgentsBlock() {
1944
+ return [
1945
+ CODEX_AGENTS_BEGIN,
1946
+ "## Threadroot",
1947
+ "",
1948
+ "When a repository contains `.threadroot/`, treat it as the source of truth for agent harness context.",
1949
+ 'Before coding, prefer `threadroot doctor` and `threadroot context "<task>"` over broad, unfocused file reads.',
1950
+ "Do not create provider-specific project files unless the user asks; use `threadroot expose <agent>` for that.",
1951
+ CODEX_AGENTS_END,
1952
+ ""
1953
+ ].join("\n");
1954
+ }
1955
+ function codexMcpBlock() {
1956
+ return [
1957
+ CODEX_MCP_BEGIN,
1958
+ "[mcp_servers.threadroot]",
1959
+ 'command = "threadroot"',
1960
+ 'args = ["mcp"]',
1961
+ CODEX_MCP_END,
1962
+ ""
1963
+ ].join("\n");
1964
+ }
1965
+ async function setupGlobalSkill(home, provider, mode, force) {
1966
+ const filePath = globalSkillPath(home, provider);
1967
+ const shown = displayPath(home, filePath);
1968
+ const desired = threadrootSkillContent(provider, "global");
1969
+ const existing = await readMaybe(filePath);
1970
+ if (mode === "check") {
1971
+ if (existing === void 0) {
1972
+ return { kind: "skill", agent: provider.id, label: provider.label, path: shown, status: "missing" };
1973
+ }
1974
+ return {
1975
+ kind: "skill",
1976
+ agent: provider.id,
1977
+ label: provider.label,
1978
+ path: shown,
1979
+ status: existing === desired ? "unchanged" : "present",
1980
+ message: existing === desired ? void 0 : "Existing global skill differs from the current Threadroot template."
1981
+ };
1982
+ }
1983
+ if (mode === "undo") {
1984
+ if (existing === void 0) {
1985
+ return { kind: "skill", agent: provider.id, label: provider.label, path: shown, status: "missing" };
1986
+ }
1987
+ if (!existing.includes(THREADROOT_MANAGED_MARKER)) {
1988
+ return {
1989
+ kind: "skill",
1990
+ agent: provider.id,
1991
+ label: provider.label,
1992
+ path: shown,
1993
+ status: "skipped",
1994
+ message: "Existing skill is not Threadroot-managed."
1995
+ };
1996
+ }
1997
+ await rm(path13.dirname(filePath), { recursive: true, force: true });
1998
+ return { kind: "skill", agent: provider.id, label: provider.label, path: shown, status: "removed" };
1999
+ }
2000
+ if (existing === desired) {
2001
+ return { kind: "skill", agent: provider.id, label: provider.label, path: shown, status: "unchanged" };
2002
+ }
2003
+ if (existing !== void 0 && !existing.includes(THREADROOT_MANAGED_MARKER) && !force) {
2004
+ return {
2005
+ kind: "skill",
2006
+ agent: provider.id,
2007
+ label: provider.label,
2008
+ path: shown,
2009
+ status: "skipped",
2010
+ message: "Existing skill is not Threadroot-managed. Re-run with --force to replace it."
2011
+ };
2012
+ }
2013
+ const status = existing === void 0 ? "create" : "update";
2014
+ if (mode === "dry-run") {
2015
+ return { kind: "skill", agent: provider.id, label: provider.label, path: shown, status };
2016
+ }
2017
+ await mkdir5(path13.dirname(filePath), { recursive: true });
2018
+ await writeFile5(filePath, desired, "utf8");
2019
+ return { kind: "skill", agent: provider.id, label: provider.label, path: shown, status };
2020
+ }
2021
+ async function setupCodexAgents(home, mode) {
2022
+ const filePath = codexAgentsPath(home);
2023
+ const shown = displayPath(home, filePath);
2024
+ const existing = await readMaybe(filePath) ?? "";
2025
+ const desired = upsertManagedBlock(existing, codexAgentsBlock(), CODEX_AGENTS_BEGIN, CODEX_AGENTS_END);
2026
+ if (mode === "check") {
2027
+ return {
2028
+ kind: "codex-agents",
2029
+ agent: "codex",
2030
+ label: "Codex global AGENTS.md",
2031
+ path: shown,
2032
+ status: hasManagedBlock(existing, CODEX_AGENTS_BEGIN, CODEX_AGENTS_END) ? "present" : "missing"
2033
+ };
2034
+ }
2035
+ if (mode === "undo") {
2036
+ if (!hasManagedBlock(existing, CODEX_AGENTS_BEGIN, CODEX_AGENTS_END)) {
2037
+ return { kind: "codex-agents", agent: "codex", label: "Codex global AGENTS.md", path: shown, status: "missing" };
2038
+ }
2039
+ await writeFile5(filePath, removeManagedBlock(existing, CODEX_AGENTS_BEGIN, CODEX_AGENTS_END), "utf8");
2040
+ return { kind: "codex-agents", agent: "codex", label: "Codex global AGENTS.md", path: shown, status: "removed" };
2041
+ }
2042
+ if (existing === desired) {
2043
+ return { kind: "codex-agents", agent: "codex", label: "Codex global AGENTS.md", path: shown, status: "unchanged" };
2044
+ }
2045
+ const status = existing.trim() ? "update" : "create";
2046
+ if (mode === "dry-run") {
2047
+ return { kind: "codex-agents", agent: "codex", label: "Codex global AGENTS.md", path: shown, status };
2048
+ }
2049
+ await mkdir5(path13.dirname(filePath), { recursive: true });
2050
+ await writeFile5(filePath, desired, "utf8");
2051
+ return { kind: "codex-agents", agent: "codex", label: "Codex global AGENTS.md", path: shown, status };
2052
+ }
2053
+ async function setupCodexMcp(home, mode) {
2054
+ const filePath = codexConfigPath(home);
2055
+ const shown = displayPath(home, filePath);
2056
+ const existing = await readMaybe(filePath) ?? "";
2057
+ if (existing.includes("[mcp_servers.threadroot]") && !hasManagedBlock(existing, CODEX_MCP_BEGIN, CODEX_MCP_END)) {
2058
+ return {
2059
+ kind: "codex-mcp",
2060
+ agent: "codex",
2061
+ label: "Codex MCP config",
2062
+ path: shown,
2063
+ status: "skipped",
2064
+ message: "Existing unmanaged [mcp_servers.threadroot] table found. Leaving it untouched."
2065
+ };
2066
+ }
2067
+ const desired = upsertManagedBlock(existing, codexMcpBlock(), CODEX_MCP_BEGIN, CODEX_MCP_END);
2068
+ if (mode === "check") {
2069
+ return {
2070
+ kind: "codex-mcp",
2071
+ agent: "codex",
2072
+ label: "Codex MCP config",
2073
+ path: shown,
2074
+ status: hasManagedBlock(existing, CODEX_MCP_BEGIN, CODEX_MCP_END) ? "present" : "missing"
2075
+ };
2076
+ }
2077
+ if (mode === "undo") {
2078
+ if (!hasManagedBlock(existing, CODEX_MCP_BEGIN, CODEX_MCP_END)) {
2079
+ return { kind: "codex-mcp", agent: "codex", label: "Codex MCP config", path: shown, status: "missing" };
2080
+ }
2081
+ await writeFile5(filePath, removeManagedBlock(existing, CODEX_MCP_BEGIN, CODEX_MCP_END), "utf8");
2082
+ return { kind: "codex-mcp", agent: "codex", label: "Codex MCP config", path: shown, status: "removed" };
2083
+ }
2084
+ if (existing === desired) {
2085
+ return { kind: "codex-mcp", agent: "codex", label: "Codex MCP config", path: shown, status: "unchanged" };
2086
+ }
2087
+ const status = existing.trim() ? "update" : "create";
2088
+ if (mode === "dry-run") {
2089
+ return { kind: "codex-mcp", agent: "codex", label: "Codex MCP config", path: shown, status };
2090
+ }
2091
+ await mkdir5(path13.dirname(filePath), { recursive: true });
2092
+ await writeFile5(filePath, desired, "utf8");
2093
+ return { kind: "codex-mcp", agent: "codex", label: "Codex MCP config", path: shown, status };
2094
+ }
2095
+ async function setupGlobal(options = {}) {
2096
+ const home = options.home ?? homedir();
2097
+ const mode = options.mode ?? "write";
2098
+ const providerIds = parseAgentProviderList(options.agents, "all");
2099
+ const entries = [];
2100
+ for (const id of providerIds) {
2101
+ entries.push(await setupGlobalSkill(home, AGENT_PROVIDERS[id], mode, options.force ?? false));
2102
+ }
2103
+ if (providerIds.includes("codex")) {
2104
+ entries.push(await setupCodexAgents(home, mode));
2105
+ if (options.mcp) {
2106
+ entries.push(await setupCodexMcp(home, mode));
2107
+ }
2108
+ }
2109
+ return { entries };
2110
+ }
2111
+ async function hasGlobalThreadrootSkill(home, agent) {
2112
+ return fileExists(globalSkillPath(home ?? homedir(), AGENT_PROVIDERS[agent]));
2113
+ }
2114
+
1714
2115
  // src/core/tools/authorize.ts
1715
2116
  function authorizeTool(tool, options) {
1716
2117
  const trusted = options.trusted ?? true;
@@ -1834,8 +2235,8 @@ function inputEnv(values) {
1834
2235
  }
1835
2236
 
1836
2237
  // src/core/tools/create.ts
1837
- import { mkdir as mkdir5, writeFile as writeFile5 } from "fs/promises";
1838
- import path12 from "path";
2238
+ import { mkdir as mkdir6, writeFile as writeFile6 } from "fs/promises";
2239
+ import path14 from "path";
1839
2240
  import { stringify as stringifyYaml3 } from "yaml";
1840
2241
  var ToolCreateError = class extends Error {
1841
2242
  constructor(message) {
@@ -1852,7 +2253,7 @@ function assertSafeName(name) {
1852
2253
  }
1853
2254
  }
1854
2255
  function assertSafeScript(script) {
1855
- if (path12.isAbsolute(script) || script.split(/[\\/]/).includes("..")) {
2256
+ if (path14.isAbsolute(script) || script.split(/[\\/]/).includes("..")) {
1856
2257
  throw new ToolCreateError(`Script path must be inside the harness directory: ${script}`);
1857
2258
  }
1858
2259
  }
@@ -1881,9 +2282,9 @@ async function createTool(repoRoot, input2, options) {
1881
2282
  throw new ToolCreateError(`Invalid tool definition: ${detail}`);
1882
2283
  }
1883
2284
  const dir = scope === "project" ? projectObjectDir(repoRoot, "tools") : userObjectDir("tools", options.home);
1884
- const filePath = path12.join(dir, `${input2.name}.yaml`);
1885
- await mkdir5(dir, { recursive: true });
1886
- await writeFile5(filePath, stringifyYaml3(parsed.data), { encoding: "utf8", flag: options.force ? "w" : "wx" }).catch(
2285
+ const filePath = path14.join(dir, `${input2.name}.yaml`);
2286
+ await mkdir6(dir, { recursive: true });
2287
+ await writeFile6(filePath, stringifyYaml3(parsed.data), { encoding: "utf8", flag: options.force ? "w" : "wx" }).catch(
1887
2288
  (error) => {
1888
2289
  if (error.code === "EEXIST") {
1889
2290
  throw new ToolCreateError(`Tool \`${input2.name}\` already exists at ${filePath}. Pass force to overwrite.`);
@@ -1895,8 +2296,8 @@ async function createTool(repoRoot, input2, options) {
1895
2296
  }
1896
2297
 
1897
2298
  // src/core/tools/catalog.ts
1898
- import { readFile as readFile6, stat as stat3 } from "fs/promises";
1899
- import path13 from "path";
2299
+ import { readFile as readFile7, stat as stat4 } from "fs/promises";
2300
+ import path15 from "path";
1900
2301
  var NAME_RE3 = /^[a-z0-9][a-z0-9-]*$/;
1901
2302
  var DESTRUCTIVE_RE = /\b(migrate|deploy|publish|release|prune|reset|destroy|drop|delete|rm|push|seed|wipe)\b/i;
1902
2303
  function looksDestructive(name, command) {
@@ -1908,20 +2309,20 @@ function sanitize(name) {
1908
2309
  }
1909
2310
  async function exists2(file) {
1910
2311
  try {
1911
- await stat3(file);
2312
+ await stat4(file);
1912
2313
  return true;
1913
2314
  } catch {
1914
2315
  return false;
1915
2316
  }
1916
2317
  }
1917
2318
  async function detectPackageManager(repoRoot) {
1918
- if (await exists2(path13.join(repoRoot, "pnpm-lock.yaml"))) {
2319
+ if (await exists2(path15.join(repoRoot, "pnpm-lock.yaml"))) {
1919
2320
  return "pnpm";
1920
2321
  }
1921
- if (await exists2(path13.join(repoRoot, "yarn.lock"))) {
2322
+ if (await exists2(path15.join(repoRoot, "yarn.lock"))) {
1922
2323
  return "yarn";
1923
2324
  }
1924
- if (await exists2(path13.join(repoRoot, "bun.lockb"))) {
2325
+ if (await exists2(path15.join(repoRoot, "bun.lockb"))) {
1925
2326
  return "bun";
1926
2327
  }
1927
2328
  return "npm";
@@ -1942,10 +2343,10 @@ function orderScripts(names) {
1942
2343
  });
1943
2344
  }
1944
2345
  async function fromPackageJson(repoRoot) {
1945
- const file = path13.join(repoRoot, "package.json");
2346
+ const file = path15.join(repoRoot, "package.json");
1946
2347
  let raw;
1947
2348
  try {
1948
- raw = await readFile6(file, "utf8");
2349
+ raw = await readFile7(file, "utf8");
1949
2350
  } catch {
1950
2351
  return [];
1951
2352
  }
@@ -1979,7 +2380,7 @@ async function fromPackageJson(repoRoot) {
1979
2380
  async function fromTargets(repoRoot, fileName, runner, source) {
1980
2381
  let raw;
1981
2382
  try {
1982
- raw = await readFile6(path13.join(repoRoot, fileName), "utf8");
2383
+ raw = await readFile7(path15.join(repoRoot, fileName), "utf8");
1983
2384
  } catch {
1984
2385
  return [];
1985
2386
  }
@@ -2145,7 +2546,7 @@ async function checkToolHealth(repoRoot, tool) {
2145
2546
  // src/core/doctor.ts
2146
2547
  async function exists3(filePath) {
2147
2548
  try {
2148
- await stat4(filePath);
2549
+ await stat5(filePath);
2149
2550
  return true;
2150
2551
  } catch (error) {
2151
2552
  if (error.code === "ENOENT") {
@@ -2157,17 +2558,29 @@ async function exists3(filePath) {
2157
2558
  function finding2(severity, code, message, pathValue) {
2158
2559
  return pathValue ? { severity, code, message, path: pathValue } : { severity, code, message };
2159
2560
  }
2160
- async function mcpConfigWarnings(repoRoot) {
2561
+ async function mcpConfigHints(repoRoot) {
2161
2562
  const configs = [".vscode/mcp.json", ".cursor/mcp.json", ".mcp.json"];
2162
- const present = await Promise.all(configs.map((config) => exists3(path14.join(repoRoot, config))));
2563
+ const present = await Promise.all(configs.map((config) => exists3(path16.join(repoRoot, config))));
2163
2564
  if (present.some(Boolean)) {
2164
2565
  return [];
2165
2566
  }
2166
2567
  return [
2167
2568
  finding2(
2168
- "warning",
2569
+ "info",
2169
2570
  "mcp_config_missing",
2170
- "No project-local MCP config found. Run `threadroot mcp setup --write` if this repo should expose Threadroot tools to local agents."
2571
+ "No project-local MCP config found. This is fine for local-only harnesses; run `threadroot mcp setup --write` only when this repo should expose MCP tools to local agents."
2572
+ )
2573
+ ];
2574
+ }
2575
+ async function globalSetupHints(home) {
2576
+ if (await hasGlobalThreadrootSkill(home, "codex")) {
2577
+ return [];
2578
+ }
2579
+ return [
2580
+ finding2(
2581
+ "info",
2582
+ "global_setup_missing",
2583
+ "Codex global Threadroot setup was not detected. Run `threadroot setup --global --agent codex` for one-time machine setup."
2171
2584
  )
2172
2585
  ];
2173
2586
  }
@@ -2321,7 +2734,7 @@ async function doctor(repoRoot, options = {}) {
2321
2734
  )
2322
2735
  );
2323
2736
  }
2324
- const scriptsDir = path14.join(path14.dirname(skill.sourcePath), "scripts");
2737
+ const scriptsDir = path16.join(path16.dirname(skill.sourcePath), "scripts");
2325
2738
  if (await exists3(scriptsDir)) {
2326
2739
  findings.push(
2327
2740
  finding2(
@@ -2333,25 +2746,35 @@ async function doctor(repoRoot, options = {}) {
2333
2746
  );
2334
2747
  }
2335
2748
  }
2336
- findings.push(...await mcpConfigWarnings(repoRoot));
2749
+ findings.push(...await globalSetupHints(options.home));
2750
+ findings.push(...await mcpConfigHints(repoRoot));
2337
2751
  return summarize(findings);
2338
2752
  }
2339
2753
  function summarize(findings) {
2340
2754
  const errors = findings.filter((entry) => entry.severity === "error").length;
2341
2755
  const warnings = findings.filter((entry) => entry.severity === "warning").length;
2342
- return { ok: errors === 0, findings, summary: { errors, warnings } };
2756
+ const info = findings.filter((entry) => entry.severity === "info").length;
2757
+ return { ok: errors === 0, findings, summary: { errors, warnings, info } };
2343
2758
  }
2344
2759
 
2345
2760
  // src/commands/doctor.ts
2346
2761
  async function runDoctor(repoRoot) {
2347
2762
  const report = await doctor(repoRoot);
2348
- if (report.findings.length === 0) {
2763
+ const actionable = report.findings.filter((finding3) => finding3.severity !== "info");
2764
+ const hints = report.findings.filter((finding3) => finding3.severity === "info");
2765
+ if (actionable.length === 0) {
2349
2766
  console.log("Threadroot doctor: clean");
2767
+ for (const finding3 of hints) {
2768
+ const suffix = finding3.path ? ` (${finding3.path})` : "";
2769
+ console.log(`- hint ${finding3.code}: ${finding3.message}${suffix}`);
2770
+ }
2350
2771
  return;
2351
2772
  }
2352
- console.log(`Threadroot doctor: ${report.summary.errors} error(s), ${report.summary.warnings} warning(s)`);
2773
+ console.log(
2774
+ `Threadroot doctor: ${report.summary.errors} error(s), ${report.summary.warnings} warning(s)`
2775
+ );
2353
2776
  for (const finding3 of report.findings) {
2354
- const label = finding3.severity === "error" ? "error" : "warning";
2777
+ const label = finding3.severity === "error" ? "error" : finding3.severity === "warning" ? "warning" : "hint";
2355
2778
  const suffix = finding3.path ? ` (${finding3.path})` : "";
2356
2779
  console.log(`- ${label} ${finding3.code}: ${finding3.message}${suffix}`);
2357
2780
  }
@@ -2360,13 +2783,117 @@ async function runDoctor(repoRoot) {
2360
2783
  }
2361
2784
  }
2362
2785
 
2786
+ // src/core/expose.ts
2787
+ import { mkdir as mkdir7, readFile as readFile8, rm as rm2, stat as stat6, writeFile as writeFile7 } from "fs/promises";
2788
+ import path17 from "path";
2789
+ async function readMaybe2(filePath) {
2790
+ try {
2791
+ return await readFile8(filePath, "utf8");
2792
+ } catch (error) {
2793
+ if (error.code === "ENOENT") {
2794
+ return void 0;
2795
+ }
2796
+ throw error;
2797
+ }
2798
+ }
2799
+ function projectSkillPath(repoRoot, provider) {
2800
+ return path17.join(repoRoot, provider.projectSkillDir, THREADROOT_SKILL_NAME, "SKILL.md");
2801
+ }
2802
+ function relSkillPath(provider) {
2803
+ return path17.join(provider.projectSkillDir, THREADROOT_SKILL_NAME, "SKILL.md");
2804
+ }
2805
+ async function exposeOne(repoRoot, provider, mode, force) {
2806
+ const relativePath = relSkillPath(provider);
2807
+ const absolutePath = projectSkillPath(repoRoot, provider);
2808
+ const desired = threadrootSkillContent(provider, "project");
2809
+ const existing = await readMaybe2(absolutePath);
2810
+ if (mode === "check") {
2811
+ if (existing === void 0) {
2812
+ return { agent: provider.id, label: provider.label, path: relativePath, status: "missing" };
2813
+ }
2814
+ return {
2815
+ agent: provider.id,
2816
+ label: provider.label,
2817
+ path: relativePath,
2818
+ status: existing === desired ? "unchanged" : "present",
2819
+ message: existing === desired ? void 0 : "Existing project skill differs from the current Threadroot template."
2820
+ };
2821
+ }
2822
+ if (mode === "undo") {
2823
+ if (existing === void 0) {
2824
+ return { agent: provider.id, label: provider.label, path: relativePath, status: "missing" };
2825
+ }
2826
+ if (!existing.includes(THREADROOT_MANAGED_MARKER)) {
2827
+ return {
2828
+ agent: provider.id,
2829
+ label: provider.label,
2830
+ path: relativePath,
2831
+ status: "skipped",
2832
+ message: "Existing skill is not Threadroot-managed."
2833
+ };
2834
+ }
2835
+ await rm2(path17.dirname(absolutePath), { recursive: true, force: true });
2836
+ return { agent: provider.id, label: provider.label, path: relativePath, status: "removed" };
2837
+ }
2838
+ if (existing === desired) {
2839
+ return { agent: provider.id, label: provider.label, path: relativePath, status: "unchanged" };
2840
+ }
2841
+ if (existing !== void 0 && !existing.includes(THREADROOT_MANAGED_MARKER) && !force) {
2842
+ return {
2843
+ agent: provider.id,
2844
+ label: provider.label,
2845
+ path: relativePath,
2846
+ status: "skipped",
2847
+ message: "Existing skill is not Threadroot-managed. Re-run with --force to replace it."
2848
+ };
2849
+ }
2850
+ const status = existing === void 0 ? "create" : "update";
2851
+ if (mode === "dry-run") {
2852
+ return { agent: provider.id, label: provider.label, path: relativePath, status };
2853
+ }
2854
+ await mkdir7(path17.dirname(absolutePath), { recursive: true });
2855
+ await writeFile7(absolutePath, desired, "utf8");
2856
+ return { agent: provider.id, label: provider.label, path: relativePath, status };
2857
+ }
2858
+ async function exposeProject(repoRoot, options = {}) {
2859
+ const providerIds = parseAgentProviderList(options.agents, ["codex"]);
2860
+ const mode = options.mode ?? "write";
2861
+ const entries = [];
2862
+ for (const id of providerIds) {
2863
+ entries.push(await exposeOne(repoRoot, AGENT_PROVIDERS[id], mode, options.force ?? false));
2864
+ }
2865
+ return { entries };
2866
+ }
2867
+
2868
+ // src/commands/expose.ts
2869
+ function modeFromOptions(options) {
2870
+ if (options.undo) return "undo";
2871
+ if (options.check) return "check";
2872
+ if (options.dryRun) return "dry-run";
2873
+ return "write";
2874
+ }
2875
+ async function runExpose(repoRoot, agent, options) {
2876
+ const mode = modeFromOptions(options);
2877
+ const result = await exposeProject(repoRoot, {
2878
+ agents: agent,
2879
+ mode,
2880
+ force: options.force
2881
+ });
2882
+ const verb = mode === "dry-run" ? "Project exposure plan" : mode === "check" ? "Project exposure check" : mode === "undo" ? "Removed project exposure" : "Exposed Threadroot project skills";
2883
+ console.log(`${verb}:`);
2884
+ for (const entry of result.entries) {
2885
+ const suffix = entry.message ? ` - ${entry.message}` : "";
2886
+ console.log(`- ${entry.label}: ${entry.status} ${entry.path}${suffix}`);
2887
+ }
2888
+ }
2889
+
2363
2890
  // src/core/init/index.ts
2364
- import { mkdir as mkdir7, stat as stat6, writeFile as writeFile6 } from "fs/promises";
2365
- import path19 from "path";
2891
+ import { mkdir as mkdir9, stat as stat8, writeFile as writeFile8 } from "fs/promises";
2892
+ import path22 from "path";
2366
2893
 
2367
2894
  // src/core/scan/package.ts
2368
2895
  import fs2 from "fs/promises";
2369
- import path15 from "path";
2896
+ import path18 from "path";
2370
2897
 
2371
2898
  // src/core/scan/rules.ts
2372
2899
  var ignoredDirectories = /* @__PURE__ */ new Set([
@@ -2384,13 +2911,13 @@ var ignoredDirectories = /* @__PURE__ */ new Set([
2384
2911
  // src/core/scan/package.ts
2385
2912
  async function readJson(repoRoot, relativePath) {
2386
2913
  try {
2387
- return JSON.parse(await fs2.readFile(path15.join(repoRoot, relativePath), "utf8"));
2914
+ return JSON.parse(await fs2.readFile(path18.join(repoRoot, relativePath), "utf8"));
2388
2915
  } catch {
2389
2916
  return void 0;
2390
2917
  }
2391
2918
  }
2392
2919
  function inferProfile(files, packageJson) {
2393
- if (files.some((file) => path15.basename(file) === "dbt_project.yml" || path15.basename(file) === "dbt_project.yaml")) {
2920
+ if (files.some((file) => path18.basename(file) === "dbt_project.yml" || path18.basename(file) === "dbt_project.yaml")) {
2394
2921
  return "dbt";
2395
2922
  }
2396
2923
  const packageMeta = packageJson && typeof packageJson === "object" ? packageJson : void 0;
@@ -2415,9 +2942,9 @@ function inferProfile(files, packageJson) {
2415
2942
 
2416
2943
  // src/core/scan/walk.ts
2417
2944
  import fs3 from "fs/promises";
2418
- import path16 from "path";
2945
+ import path19 from "path";
2419
2946
  function toPosix(relativePath) {
2420
- return relativePath.split(path16.sep).join("/");
2947
+ return relativePath.split(path19.sep).join("/");
2421
2948
  }
2422
2949
  async function walkRepo(repoRoot, directory = repoRoot) {
2423
2950
  let entries;
@@ -2428,8 +2955,8 @@ async function walkRepo(repoRoot, directory = repoRoot) {
2428
2955
  }
2429
2956
  const files = [];
2430
2957
  for (const entry of entries) {
2431
- const absolutePath = path16.join(directory, entry.name);
2432
- const relativePath = toPosix(path16.relative(repoRoot, absolutePath));
2958
+ const absolutePath = path19.join(directory, entry.name);
2959
+ const relativePath = toPosix(path19.relative(repoRoot, absolutePath));
2433
2960
  if (entry.isDirectory()) {
2434
2961
  if (!ignoredDirectories.has(entry.name)) {
2435
2962
  files.push(...await walkRepo(repoRoot, absolutePath));
@@ -2447,17 +2974,17 @@ async function walkRepo(repoRoot, directory = repoRoot) {
2447
2974
  import { stringify as stringifyYaml4 } from "yaml";
2448
2975
 
2449
2976
  // src/core/init/builtins.ts
2450
- import { cp, mkdir as mkdir6, readdir as readdir3, stat as stat5 } from "fs/promises";
2451
- import path17 from "path";
2977
+ import { cp, mkdir as mkdir8, readdir as readdir3, stat as stat7 } from "fs/promises";
2978
+ import path20 from "path";
2452
2979
  import { fileURLToPath } from "url";
2453
- var DIST_DIR = path17.dirname(fileURLToPath(import.meta.url));
2454
- var PACKAGE_ROOT_FROM_BUNDLE = path17.resolve(DIST_DIR, "..");
2455
- var PACKAGE_ROOT_FROM_DIST = path17.resolve(DIST_DIR, "../../..");
2456
- var PACKAGE_ROOT_FROM_SRC = path17.resolve(DIST_DIR, "../../../..");
2980
+ var DIST_DIR = path20.dirname(fileURLToPath(import.meta.url));
2981
+ var PACKAGE_ROOT_FROM_BUNDLE = path20.resolve(DIST_DIR, "..");
2982
+ var PACKAGE_ROOT_FROM_DIST = path20.resolve(DIST_DIR, "../../..");
2983
+ var PACKAGE_ROOT_FROM_SRC = path20.resolve(DIST_DIR, "../../../..");
2457
2984
  var SKILL_PACK_CANDIDATES = [
2458
- path17.join(PACKAGE_ROOT_FROM_BUNDLE, "skills"),
2459
- path17.join(PACKAGE_ROOT_FROM_DIST, "skills"),
2460
- path17.join(PACKAGE_ROOT_FROM_SRC, "skills")
2985
+ path20.join(PACKAGE_ROOT_FROM_BUNDLE, "skills"),
2986
+ path20.join(PACKAGE_ROOT_FROM_DIST, "skills"),
2987
+ path20.join(PACKAGE_ROOT_FROM_SRC, "skills")
2461
2988
  ];
2462
2989
  var PROJECT_MEMORY_TEMPLATE = [
2463
2990
  "# Project",
@@ -2470,7 +2997,7 @@ var PROJECT_MEMORY_TEMPLATE = [
2470
2997
  ].join("\n");
2471
2998
  async function exists4(target) {
2472
2999
  try {
2473
- const info = await stat5(target);
3000
+ const info = await stat7(target);
2474
3001
  return info.isDirectory();
2475
3002
  } catch {
2476
3003
  return false;
@@ -2487,20 +3014,20 @@ async function bundledSkillsDir() {
2487
3014
  async function writeBuiltinSkills(repoRoot) {
2488
3015
  const sourceDir = await bundledSkillsDir();
2489
3016
  const targetDir = projectObjectDir(repoRoot, "skills");
2490
- await mkdir6(targetDir, { recursive: true });
3017
+ await mkdir8(targetDir, { recursive: true });
2491
3018
  const written = [];
2492
3019
  const entries = await readdir3(sourceDir, { withFileTypes: true });
2493
3020
  for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
2494
3021
  if (!entry.isDirectory()) {
2495
3022
  continue;
2496
3023
  }
2497
- const sourceSkill = path17.join(sourceDir, entry.name);
2498
- const sourceSkillFile = path17.join(sourceSkill, "SKILL.md");
2499
- if (!await exists4(sourceSkill) || !await stat5(sourceSkillFile).then((info) => info.isFile()).catch(() => false)) {
3024
+ const sourceSkill = path20.join(sourceDir, entry.name);
3025
+ const sourceSkillFile = path20.join(sourceSkill, "SKILL.md");
3026
+ if (!await exists4(sourceSkill) || !await stat7(sourceSkillFile).then((info) => info.isFile()).catch(() => false)) {
2500
3027
  continue;
2501
3028
  }
2502
- const targetSkill = path17.join(targetDir, entry.name);
2503
- const targetSkillFile = path17.join(targetSkill, "SKILL.md");
3029
+ const targetSkill = path20.join(targetDir, entry.name);
3030
+ const targetSkillFile = path20.join(targetSkill, "SKILL.md");
2504
3031
  try {
2505
3032
  await cp(sourceSkill, targetSkill, { recursive: true, force: false, errorOnExist: true });
2506
3033
  written.push(targetSkillFile);
@@ -2514,14 +3041,14 @@ async function writeBuiltinSkills(repoRoot) {
2514
3041
  }
2515
3042
 
2516
3043
  // src/core/init/import.ts
2517
- import { readFile as readFile7, readdir as readdir4 } from "fs/promises";
2518
- import path18 from "path";
3044
+ import { readFile as readFile9, readdir as readdir4 } from "fs/promises";
3045
+ import path21 from "path";
2519
3046
  var PROSE_PRECEDENCE = ["AGENTS.md", "CLAUDE.md", ".github/copilot-instructions.md"];
2520
3047
  var CURSOR_RULES_DIR = ".cursor/rules";
2521
3048
  var NAME_RE4 = /^[a-z0-9][a-z0-9-]*$/;
2522
3049
  async function readIfExists3(filePath) {
2523
3050
  try {
2524
- return await readFile7(filePath, "utf8");
3051
+ return await readFile9(filePath, "utf8");
2525
3052
  } catch (error) {
2526
3053
  if (error.code === "ENOENT") {
2527
3054
  return void 0;
@@ -2565,7 +3092,7 @@ function novelSections(canonical, other) {
2565
3092
  });
2566
3093
  }
2567
3094
  async function listCursorRules(repoRoot) {
2568
- const dir = path18.join(repoRoot, CURSOR_RULES_DIR);
3095
+ const dir = path21.join(repoRoot, CURSOR_RULES_DIR);
2569
3096
  let entries;
2570
3097
  try {
2571
3098
  entries = await readdir4(dir);
@@ -2579,7 +3106,7 @@ async function listCursorRules(repoRoot) {
2579
3106
  return Promise.all(
2580
3107
  files.map(async (name) => ({
2581
3108
  file: `${CURSOR_RULES_DIR}/${name}`,
2582
- content: await readFile7(path18.join(dir, name), "utf8")
3109
+ content: await readFile9(path21.join(dir, name), "utf8")
2583
3110
  }))
2584
3111
  );
2585
3112
  }
@@ -2594,7 +3121,7 @@ function globsToApplyTo(value) {
2594
3121
  return void 0;
2595
3122
  }
2596
3123
  function ruleName(fileName) {
2597
- const base = path18.basename(fileName, ".mdc").toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/^-+|-+$/g, "");
3124
+ const base = path21.basename(fileName, ".mdc").toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/^-+|-+$/g, "");
2598
3125
  return NAME_RE4.test(base) ? base : "imported-rule";
2599
3126
  }
2600
3127
  async function importVendorFiles(repoRoot, options = {}) {
@@ -2605,7 +3132,7 @@ async function importVendorFiles(repoRoot, options = {}) {
2605
3132
  if (!wanted(file)) {
2606
3133
  continue;
2607
3134
  }
2608
- const content = await readIfExists3(path18.join(repoRoot, file));
3135
+ const content = await readIfExists3(path21.join(repoRoot, file));
2609
3136
  if (content && content.trim()) {
2610
3137
  prose.push({ file, content });
2611
3138
  }
@@ -2650,7 +3177,7 @@ ${novel.map((section) => section.text).join("\n\n")}`.trim();
2650
3177
  }
2651
3178
 
2652
3179
  // src/core/init/index.ts
2653
- var DEFAULT_ADAPTERS = ["agents", "claude", "copilot", "cursor"];
3180
+ var DEFAULT_ADAPTERS = [];
2654
3181
  var AGENTS_FILE2 = "AGENTS.md";
2655
3182
  var InitError = class extends Error {
2656
3183
  constructor(message) {
@@ -2660,7 +3187,7 @@ var InitError = class extends Error {
2660
3187
  };
2661
3188
  async function pathExists(target) {
2662
3189
  try {
2663
- await stat6(target);
3190
+ await stat8(target);
2664
3191
  return true;
2665
3192
  } catch {
2666
3193
  return false;
@@ -2680,7 +3207,7 @@ async function detectName(repoRoot) {
2680
3207
  if (packageJson && typeof packageJson.name === "string" && packageJson.name.trim()) {
2681
3208
  return packageJson.name.trim();
2682
3209
  }
2683
- return path19.basename(repoRoot);
3210
+ return path22.basename(repoRoot);
2684
3211
  }
2685
3212
  async function writeManifest(repoRoot, manifest) {
2686
3213
  const body = {
@@ -2692,15 +3219,15 @@ async function writeManifest(repoRoot, manifest) {
2692
3219
  if (manifest.tools.allow.length > 0) {
2693
3220
  body.tools = { allow: manifest.tools.allow };
2694
3221
  }
2695
- await mkdir7(projectHarnessDir(repoRoot), { recursive: true });
2696
- await writeFile6(projectManifestPath(repoRoot), stringifyYaml4(body), "utf8");
3222
+ await mkdir9(projectHarnessDir(repoRoot), { recursive: true });
3223
+ await writeFile8(projectManifestPath(repoRoot), stringifyYaml4(body), "utf8");
2697
3224
  }
2698
3225
  async function writeProjectMemory(repoRoot) {
2699
3226
  const dir = projectObjectDir(repoRoot, "memory");
2700
- await mkdir7(dir, { recursive: true });
2701
- const filePath = path19.join(dir, "project.md");
3227
+ await mkdir9(dir, { recursive: true });
3228
+ const filePath = path22.join(dir, "project.md");
2702
3229
  try {
2703
- await writeFile6(filePath, `${PROJECT_MEMORY_TEMPLATE}
3230
+ await writeFile8(filePath, `${PROJECT_MEMORY_TEMPLATE}
2704
3231
  `, { encoding: "utf8", flag: "wx" });
2705
3232
  return [filePath];
2706
3233
  } catch (error) {
@@ -2715,16 +3242,16 @@ async function writeImportedRules(repoRoot, report) {
2715
3242
  return [];
2716
3243
  }
2717
3244
  const dir = projectObjectDir(repoRoot, "rules");
2718
- await mkdir7(dir, { recursive: true });
3245
+ await mkdir9(dir, { recursive: true });
2719
3246
  const written = [];
2720
3247
  for (const rule of report.importedRules) {
2721
- const filePath = path19.join(dir, `${rule.name}.md`);
3248
+ const filePath = path22.join(dir, `${rule.name}.md`);
2722
3249
  const data = { name: rule.name, scope: "project" };
2723
3250
  if (rule.applyTo) {
2724
3251
  data.applyTo = rule.applyTo;
2725
3252
  }
2726
3253
  try {
2727
- await writeFile6(filePath, serializeFrontmatter(data, rule.body), { encoding: "utf8", flag: "wx" });
3254
+ await writeFile8(filePath, serializeFrontmatter(data, rule.body), { encoding: "utf8", flag: "wx" });
2728
3255
  written.push(filePath);
2729
3256
  } catch (error) {
2730
3257
  if (error.code !== "EEXIST") {
@@ -2763,7 +3290,7 @@ async function writeStarterTools(repoRoot, profile, force) {
2763
3290
  async function initHarness(repoRoot, options = {}) {
2764
3291
  if (!options.force && await pathExists(projectManifestPath(repoRoot))) {
2765
3292
  throw new InitError(
2766
- `A harness already exists at ${path19.join(".threadroot", "harness.yaml")}. Re-run with --force to overwrite.`
3293
+ `A harness already exists at ${path22.join(".threadroot", "harness.yaml")}. Re-run with --force to overwrite.`
2767
3294
  );
2768
3295
  }
2769
3296
  const profile = await detectProfile(repoRoot, options.profile);
@@ -2785,13 +3312,14 @@ async function initHarness(repoRoot, options = {}) {
2785
3312
  if (options.import !== false) {
2786
3313
  report = await importVendorFiles(repoRoot, { include: options.importFiles });
2787
3314
  if (report.canonicalBody.trim()) {
2788
- await writeFile6(path19.join(repoRoot, AGENTS_FILE2), `${report.canonicalBody.trim()}
3315
+ await writeFile8(path22.join(repoRoot, AGENTS_FILE2), `${report.canonicalBody.trim()}
2789
3316
  `, "utf8");
2790
3317
  }
2791
3318
  rules = await writeImportedRules(repoRoot, report);
2792
3319
  }
2793
3320
  const { written } = await runCompile(repoRoot, { home: options.home });
2794
- return { name, profile, adapters, skills, tools: tools2, memory, rules, import: report, compiled: written };
3321
+ const exposed = options.expose ? (await exposeProject(repoRoot, { agents: options.expose })).entries.filter((entry) => entry.status !== "missing" && entry.status !== "skipped").map((entry) => entry.path) : [];
3322
+ return { name, profile, adapters, skills, tools: tools2, memory, rules, import: report, compiled: written, exposed };
2795
3323
  }
2796
3324
 
2797
3325
  // src/commands/init.ts
@@ -2806,12 +3334,13 @@ async function runInit(repoRoot, options) {
2806
3334
  force: options.force,
2807
3335
  import: options.import,
2808
3336
  profile: options.profile ? profileIdSchema.parse(options.profile) : void 0,
2809
- adapters: parseAdapters(options.adapters)
3337
+ adapters: parseAdapters(options.adapters),
3338
+ expose: options.expose
2810
3339
  };
2811
3340
  try {
2812
3341
  const report = await initHarness(repoRoot, initOptions);
2813
3342
  console.log(`Initialized harness \`${report.name}\` (profile: ${report.profile}).`);
2814
- console.log(`adapters: ${report.adapters.join(", ")}`);
3343
+ console.log(`adapters: ${report.adapters.length > 0 ? report.adapters.join(", ") : "none (local-only)"}`);
2815
3344
  console.log(`skills: ${report.skills.length}, tools: ${report.tools.length}, memory: ${report.memory.length}`);
2816
3345
  if (report.import?.canonicalSource) {
2817
3346
  console.log(`imported canonical prose from ${report.import.canonicalSource}`);
@@ -2826,6 +3355,9 @@ async function runInit(repoRoot, options) {
2826
3355
  console.log(`imported ${report.rules.length} cursor rule(s)`);
2827
3356
  }
2828
3357
  console.log(`compiled ${report.compiled.length} vendor file(s).`);
3358
+ if (report.exposed.length > 0) {
3359
+ console.log(`exposed ${report.exposed.length} provider skill shim(s).`);
3360
+ }
2829
3361
  } catch (error) {
2830
3362
  if (error instanceof InitError) {
2831
3363
  console.error(error.message);
@@ -2837,13 +3369,13 @@ async function runInit(repoRoot, options) {
2837
3369
  }
2838
3370
 
2839
3371
  // src/commands/install.ts
2840
- import path22 from "path";
3372
+ import path25 from "path";
2841
3373
 
2842
3374
  // src/core/install/fetch.ts
2843
3375
  import { execFile } from "child_process";
2844
- import { mkdtemp, rm } from "fs/promises";
3376
+ import { mkdtemp, rm as rm3 } from "fs/promises";
2845
3377
  import os2 from "os";
2846
- import path20 from "path";
3378
+ import path23 from "path";
2847
3379
  import { promisify } from "util";
2848
3380
  var run = promisify(execFile);
2849
3381
  function cloneUrl(ref) {
@@ -2864,8 +3396,8 @@ async function git(cwd, args) {
2864
3396
  }
2865
3397
  async function fetchGitSource(ref) {
2866
3398
  const url = cloneUrl(ref);
2867
- const dir = await mkdtemp(path20.join(os2.tmpdir(), "threadroot-fetch-"));
2868
- const cleanup = () => rm(dir, { recursive: true, force: true });
3399
+ const dir = await mkdtemp(path23.join(os2.tmpdir(), "threadroot-fetch-"));
3400
+ const cleanup = () => rm3(dir, { recursive: true, force: true });
2869
3401
  try {
2870
3402
  if (ref.ref) {
2871
3403
  try {
@@ -2886,9 +3418,9 @@ async function fetchGitSource(ref) {
2886
3418
  }
2887
3419
 
2888
3420
  // src/core/install/install.ts
2889
- import { cp as cp2, mkdir as mkdir8, readFile as readFile8, readdir as readdir5, stat as stat7, writeFile as writeFile7 } from "fs/promises";
3421
+ import { cp as cp2, mkdir as mkdir10, readFile as readFile10, readdir as readdir5, stat as stat9, writeFile as writeFile9 } from "fs/promises";
2890
3422
  import { createHash as createHash2 } from "crypto";
2891
- import path21 from "path";
3423
+ import path24 from "path";
2892
3424
  var NAME_RE5 = /^[a-z0-9][a-z0-9-]*$/;
2893
3425
  var KIND_DIR = {
2894
3426
  skill: "skills",
@@ -2900,8 +3432,8 @@ function objectExt(kind) {
2900
3432
  return kind === "tool" || kind === "connection" ? ".yaml" : ".md";
2901
3433
  }
2902
3434
  function safeRepoPath(objectPath) {
2903
- const normalized = path21.normalize(objectPath);
2904
- if (path21.isAbsolute(normalized) || normalized === ".." || normalized.startsWith(`..${path21.sep}`)) {
3435
+ const normalized = path24.normalize(objectPath);
3436
+ if (path24.isAbsolute(normalized) || normalized === ".." || normalized.startsWith(`..${path24.sep}`)) {
2905
3437
  throw new Error(`Unsafe object path: ${objectPath}`);
2906
3438
  }
2907
3439
  return normalized;
@@ -2915,7 +3447,7 @@ function inferKind(objectPath, override) {
2915
3447
  if (segments.includes("tools")) return "tool";
2916
3448
  if (segments.includes("connections")) return "connection";
2917
3449
  if (segments.includes("rules")) return "rule";
2918
- const ext = path21.extname(objectPath).toLowerCase();
3450
+ const ext = path24.extname(objectPath).toLowerCase();
2919
3451
  if (ext === ".yaml" || ext === ".yml") return "tool";
2920
3452
  if (ext === ".md") return "skill";
2921
3453
  throw new Error(
@@ -2923,7 +3455,7 @@ function inferKind(objectPath, override) {
2923
3455
  );
2924
3456
  }
2925
3457
  function deriveName(objectPath) {
2926
- const base = path21.basename(objectPath, path21.extname(objectPath));
3458
+ const base = path24.basename(objectPath, path24.extname(objectPath));
2927
3459
  if (!NAME_RE5.test(base)) {
2928
3460
  throw new Error(`Invalid object name \`${base}\` (use lowercase letters, digits, and dashes).`);
2929
3461
  }
@@ -2935,8 +3467,8 @@ async function hashDirectory(root) {
2935
3467
  async function walk(dir) {
2936
3468
  const entries = (await readdir5(dir, { withFileTypes: true })).sort((a, b) => a.name.localeCompare(b.name));
2937
3469
  for (const entry of entries) {
2938
- const full = path21.join(dir, entry.name);
2939
- const rel = path21.relative(root, full).split(path21.sep).join("/");
3470
+ const full = path24.join(dir, entry.name);
3471
+ const rel = path24.relative(root, full).split(path24.sep).join("/");
2940
3472
  if (entry.isSymbolicLink()) {
2941
3473
  throw new Error(`Refusing to install skill directory with symlink: ${rel}`);
2942
3474
  }
@@ -2947,7 +3479,7 @@ async function hashDirectory(root) {
2947
3479
  if (entry.isFile()) {
2948
3480
  hash.update(`file:${rel}
2949
3481
  `);
2950
- hash.update(await readFile8(full));
3482
+ hash.update(await readFile10(full));
2951
3483
  hash.update("\n");
2952
3484
  }
2953
3485
  }
@@ -2956,8 +3488,8 @@ async function hashDirectory(root) {
2956
3488
  return hash.digest("hex");
2957
3489
  }
2958
3490
  async function validateSkillDirectory2(sourcePath, expectedName) {
2959
- const skillPath = path21.join(sourcePath, "SKILL.md");
2960
- const parsed = parseFrontmatter(await readFile8(skillPath, "utf8"));
3491
+ const skillPath = path24.join(sourcePath, "SKILL.md");
3492
+ const parsed = parseFrontmatter(await readFile10(skillPath, "utf8"));
2961
3493
  const result = skillFrontmatterSchema.safeParse(parsed.data);
2962
3494
  if (!result.success) {
2963
3495
  const detail = result.error.issues.map((issue) => issue.message).join("; ");
@@ -2986,7 +3518,7 @@ async function installObject(repoRoot, rawSource, options = {}) {
2986
3518
  objectPath = safeRepoPath(within);
2987
3519
  refLabel = ref.ref;
2988
3520
  const fetched = await fetchGitSource(ref);
2989
- sourcePath = path21.join(fetched.dir, objectPath);
3521
+ sourcePath = path24.join(fetched.dir, objectPath);
2990
3522
  resolved = fetched.sha;
2991
3523
  cleanup = fetched.cleanup;
2992
3524
  } else {
@@ -3001,20 +3533,20 @@ async function installObject(repoRoot, rawSource, options = {}) {
3001
3533
  const destDir = scope === "user" ? userObjectDir(dirKey, options.home) : projectObjectDir(repoRoot, dirKey);
3002
3534
  let destPath;
3003
3535
  let integrity;
3004
- const info = await stat7(sourcePath);
3536
+ const info = await stat9(sourcePath);
3005
3537
  if (info.isDirectory()) {
3006
3538
  if (kind !== "skill") {
3007
3539
  throw new Error("Only skill objects may be installed from a directory.");
3008
3540
  }
3009
3541
  integrity = `sha256:${await validateSkillDirectory2(sourcePath, name)}`;
3010
- destPath = path21.join(destDir, name);
3011
- await mkdir8(destDir, { recursive: true });
3542
+ destPath = path24.join(destDir, name);
3543
+ await mkdir10(destDir, { recursive: true });
3012
3544
  await cp2(sourcePath, destPath, { recursive: true, force: true });
3013
3545
  } else {
3014
- const content = await readFile8(sourcePath, "utf8");
3015
- destPath = path21.join(destDir, `${name}${objectExt(kind)}`);
3016
- await mkdir8(destDir, { recursive: true });
3017
- await writeFile7(destPath, content, "utf8");
3546
+ const content = await readFile10(sourcePath, "utf8");
3547
+ destPath = path24.join(destDir, `${name}${objectExt(kind)}`);
3548
+ await mkdir10(destDir, { recursive: true });
3549
+ await writeFile9(destPath, content, "utf8");
3018
3550
  integrity = `sha256:${hashContent(content)}`;
3019
3551
  }
3020
3552
  const entry = {
@@ -3066,7 +3598,7 @@ async function runInstall(repoRoot, source, options) {
3066
3598
  if (installed.kind === "skill" && installed.entry.sourceKind !== "local") {
3067
3599
  console.log(" note: inspect external skills before trusting bundled scripts, assets, or allowed tools.");
3068
3600
  if (scope === "project") {
3069
- console.log(` inspect: threadroot skills inspect ${path22.relative(repoRoot, installed.path)}`);
3601
+ console.log(` inspect: threadroot skills inspect ${path25.relative(repoRoot, installed.path)}`);
3070
3602
  }
3071
3603
  }
3072
3604
  } catch (error) {
@@ -3390,7 +3922,7 @@ async function handleMessage(repoRoot, request) {
3390
3922
  if (request.method === "initialize") {
3391
3923
  return resultResponse(request, {
3392
3924
  protocolVersion: "2024-11-05",
3393
- serverInfo: { name: "threadroot", version: "0.1.0" },
3925
+ serverInfo: { name: "threadroot", version: "0.1.1" },
3394
3926
  capabilities: { tools: {} }
3395
3927
  });
3396
3928
  }
@@ -3532,7 +4064,7 @@ Repository:
3532
4064
  ${repoRoot}
3533
4065
 
3534
4066
  Goal:
3535
- Initialize Threadroot so this repo has portable AI-agent instructions, durable memory, curated skills, executable tools, and vendor-specific adapter files generated from one canonical harness.
4067
+ Initialize Threadroot so this repo has a portable AI-agent harness in one canonical \`.threadroot/\` directory.
3536
4068
 
3537
4069
  Rules:
3538
4070
  - Prefer deterministic CLI commands.
@@ -3545,9 +4077,9 @@ Steps:
3545
4077
  3. Run \`threadroot status\` to check whether a harness already exists.
3546
4078
  4. If no harness exists, run \`threadroot init\`. Use \`--no-import\` only when the user explicitly wants a blank-slate harness.
3547
4079
  5. Run \`threadroot status\` again.
3548
- 6. If status reports drift, run \`threadroot diff\` and summarize the drift before changing generated files.
4080
+ 6. If the user asks for provider-native project skill files, run \`threadroot expose <agent>\` or \`threadroot expose all\`.
3549
4081
  7. Run \`threadroot context "current task"\` with the user's actual task to find relevant skills, rules, tools, and memory.
3550
- 8. If project-local MCP config is useful, run \`threadroot mcp setup --write\` and tell the user to reload their agent surface.
4082
+ 8. If project-local MCP config is useful, ask before running \`threadroot mcp setup --write\`, then tell the user to reload their agent surface.
3551
4083
 
3552
4084
  Final response:
3553
4085
  Say exactly:
@@ -3614,11 +4146,11 @@ function agentNotes(agent) {
3614
4146
  }
3615
4147
 
3616
4148
  // src/core/mcp-config.ts
3617
- import { mkdir as mkdir9, readFile as readFile9, writeFile as writeFile8 } from "fs/promises";
3618
- import path23 from "path";
4149
+ import { mkdir as mkdir11, readFile as readFile11, writeFile as writeFile10 } from "fs/promises";
4150
+ import path26 from "path";
3619
4151
  var TARGETS = [
3620
- { agent: "copilot", file: path23.join(".vscode", "mcp.json"), key: "servers" },
3621
- { agent: "cursor", file: path23.join(".cursor", "mcp.json"), key: "mcpServers" },
4152
+ { agent: "copilot", file: path26.join(".vscode", "mcp.json"), key: "servers" },
4153
+ { agent: "cursor", file: path26.join(".cursor", "mcp.json"), key: "mcpServers" },
3622
4154
  { agent: "claude", file: ".mcp.json", key: "mcpServers" }
3623
4155
  ];
3624
4156
  function mcpServerEntry(command, scriptPath) {
@@ -3627,7 +4159,7 @@ function mcpServerEntry(command, scriptPath) {
3627
4159
  async function mergeConfig(filePath, key, entry) {
3628
4160
  let config = {};
3629
4161
  try {
3630
- const raw = await readFile9(filePath, "utf8");
4162
+ const raw = await readFile11(filePath, "utf8");
3631
4163
  const parsed = JSON.parse(raw);
3632
4164
  if (parsed && typeof parsed === "object") {
3633
4165
  config = parsed;
@@ -3640,8 +4172,8 @@ async function mergeConfig(filePath, key, entry) {
3640
4172
  const servers = config[key] && typeof config[key] === "object" ? config[key] : {};
3641
4173
  servers.threadroot = { ...entry };
3642
4174
  config[key] = servers;
3643
- await mkdir9(path23.dirname(filePath), { recursive: true });
3644
- await writeFile8(filePath, `${JSON.stringify(config, null, 2)}
4175
+ await mkdir11(path26.dirname(filePath), { recursive: true });
4176
+ await writeFile10(filePath, `${JSON.stringify(config, null, 2)}
3645
4177
  `, "utf8");
3646
4178
  }
3647
4179
  async function writeProjectMcpConfigs(input2) {
@@ -3649,7 +4181,7 @@ async function writeProjectMcpConfigs(input2) {
3649
4181
  const targets = agents ? TARGETS.filter((target) => agents.includes(target.agent)) : TARGETS;
3650
4182
  const written = [];
3651
4183
  for (const target of targets) {
3652
- const filePath = path23.join(input2.repoRoot, target.file);
4184
+ const filePath = path26.join(input2.repoRoot, target.file);
3653
4185
  await mergeConfig(filePath, target.key, input2.entry);
3654
4186
  written.push(target.file);
3655
4187
  }
@@ -3751,6 +4283,37 @@ async function runSkillsInspect(repoRoot, targetPath) {
3751
4283
  }
3752
4284
  }
3753
4285
 
4286
+ // src/commands/setup.ts
4287
+ function modeFromOptions2(options) {
4288
+ if (options.undo) return "undo";
4289
+ if (options.check) return "check";
4290
+ if (options.dryRun) return "dry-run";
4291
+ return "write";
4292
+ }
4293
+ async function runSetup(_repoRoot, options) {
4294
+ if (!options.global) {
4295
+ console.error("Only global setup is supported right now. Run `threadroot setup --global`.");
4296
+ process.exitCode = 1;
4297
+ return;
4298
+ }
4299
+ const mode = modeFromOptions2(options);
4300
+ const result = await setupGlobal({
4301
+ agents: options.agent,
4302
+ mode,
4303
+ force: options.force,
4304
+ mcp: options.mcp
4305
+ });
4306
+ const title = mode === "dry-run" ? "Global setup plan" : mode === "check" ? "Global setup check" : mode === "undo" ? "Global setup undo" : "Global setup complete";
4307
+ console.log(`${title}:`);
4308
+ for (const entry of result.entries) {
4309
+ const suffix = entry.message ? ` - ${entry.message}` : "";
4310
+ console.log(`- ${entry.label}: ${entry.status} ${entry.path}${suffix}`);
4311
+ }
4312
+ if (mode === "write") {
4313
+ console.log("Reload or restart open agent sessions so new global skills/config are discovered.");
4314
+ }
4315
+ }
4316
+
3754
4317
  // src/commands/status.ts
3755
4318
  async function runStatus(repoRoot) {
3756
4319
  const status = await harnessStatus(repoRoot);
@@ -3759,7 +4322,7 @@ async function runStatus(repoRoot) {
3759
4322
  return;
3760
4323
  }
3761
4324
  console.log(`harness: ${status.manifest.name} (${status.manifest.profile})`);
3762
- console.log(`adapters: ${status.manifest.adapters.join(", ")}`);
4325
+ console.log(`adapters: ${status.manifest.adapters.length > 0 ? status.manifest.adapters.join(", ") : "none (local-only)"}`);
3763
4326
  console.log(
3764
4327
  `objects: ${status.counts.skills} skills, ${status.counts.rules} rules, ${status.counts.tools} tools, ${status.counts.memory} memory`
3765
4328
  );
@@ -3775,8 +4338,8 @@ async function runStatus(repoRoot) {
3775
4338
  }
3776
4339
 
3777
4340
  // src/core/packs/index.ts
3778
- import { cp as cp3, mkdir as mkdir10, readFile as readFile10, readdir as readdir6, stat as stat8 } from "fs/promises";
3779
- import path24 from "path";
4341
+ import { cp as cp3, mkdir as mkdir12, readFile as readFile12, readdir as readdir6, stat as stat10 } from "fs/promises";
4342
+ import path27 from "path";
3780
4343
  import { fileURLToPath as fileURLToPath2 } from "url";
3781
4344
  import { parse as parseYaml3 } from "yaml";
3782
4345
  import { z as z5 } from "zod";
@@ -3789,18 +4352,18 @@ var packManifestSchema = z5.object({
3789
4352
  rules: z5.array(z5.string()).default([]),
3790
4353
  connections: z5.array(z5.string()).default([])
3791
4354
  });
3792
- var DIST_DIR2 = path24.dirname(fileURLToPath2(import.meta.url));
3793
- var PACKAGE_ROOT_FROM_BUNDLE2 = path24.resolve(DIST_DIR2, "..");
3794
- var PACKAGE_ROOT_FROM_DIST2 = path24.resolve(DIST_DIR2, "../../..");
3795
- var PACKAGE_ROOT_FROM_SRC2 = path24.resolve(DIST_DIR2, "../../../..");
4355
+ var DIST_DIR2 = path27.dirname(fileURLToPath2(import.meta.url));
4356
+ var PACKAGE_ROOT_FROM_BUNDLE2 = path27.resolve(DIST_DIR2, "..");
4357
+ var PACKAGE_ROOT_FROM_DIST2 = path27.resolve(DIST_DIR2, "../../..");
4358
+ var PACKAGE_ROOT_FROM_SRC2 = path27.resolve(DIST_DIR2, "../../../..");
3796
4359
  var PACK_CANDIDATES = [
3797
- path24.join(PACKAGE_ROOT_FROM_BUNDLE2, "packs"),
3798
- path24.join(PACKAGE_ROOT_FROM_DIST2, "packs"),
3799
- path24.join(PACKAGE_ROOT_FROM_SRC2, "packs")
4360
+ path27.join(PACKAGE_ROOT_FROM_BUNDLE2, "packs"),
4361
+ path27.join(PACKAGE_ROOT_FROM_DIST2, "packs"),
4362
+ path27.join(PACKAGE_ROOT_FROM_SRC2, "packs")
3800
4363
  ];
3801
4364
  async function exists5(target) {
3802
4365
  try {
3803
- await stat8(target);
4366
+ await stat10(target);
3804
4367
  return true;
3805
4368
  } catch (error) {
3806
4369
  if (error.code === "ENOENT") {
@@ -3828,22 +4391,22 @@ async function isPackRoot(candidate) {
3828
4391
  return false;
3829
4392
  }
3830
4393
  for (const entry of entries) {
3831
- if (entry.isDirectory() && await exists5(path24.join(candidate, entry.name, "pack.yaml"))) {
4394
+ if (entry.isDirectory() && await exists5(path27.join(candidate, entry.name, "pack.yaml"))) {
3832
4395
  return true;
3833
4396
  }
3834
4397
  }
3835
4398
  return false;
3836
4399
  }
3837
4400
  function safeRelative(ref) {
3838
- const normalized = path24.normalize(ref);
3839
- if (path24.isAbsolute(normalized) || normalized === ".." || normalized.startsWith(`..${path24.sep}`)) {
4401
+ const normalized = path27.normalize(ref);
4402
+ if (path27.isAbsolute(normalized) || normalized === ".." || normalized.startsWith(`..${path27.sep}`)) {
3840
4403
  throw new Error(`Unsafe pack reference: ${ref}`);
3841
4404
  }
3842
4405
  return normalized;
3843
4406
  }
3844
4407
  async function readPackManifest(packDir) {
3845
- const file = path24.join(packDir, "pack.yaml");
3846
- const parsed = packManifestSchema.safeParse(parseYaml3(await readFile10(file, "utf8")));
4408
+ const file = path27.join(packDir, "pack.yaml");
4409
+ const parsed = packManifestSchema.safeParse(parseYaml3(await readFile12(file, "utf8")));
3847
4410
  if (!parsed.success) {
3848
4411
  const detail = parsed.error.issues.map((issue) => issue.message).join("; ");
3849
4412
  throw new Error(`Invalid pack manifest ${file}: ${detail}`);
@@ -3851,7 +4414,7 @@ async function readPackManifest(packDir) {
3851
4414
  return parsed.data;
3852
4415
  }
3853
4416
  async function packDirFor(repoRoot, nameOrPath) {
3854
- if (path24.isAbsolute(nameOrPath)) {
4417
+ if (path27.isAbsolute(nameOrPath)) {
3855
4418
  return nameOrPath;
3856
4419
  }
3857
4420
  if (nameOrPath.startsWith(".") || nameOrPath.includes("/") || nameOrPath.includes("\\")) {
@@ -3859,14 +4422,14 @@ async function packDirFor(repoRoot, nameOrPath) {
3859
4422
  }
3860
4423
  const bundled = await bundledPacksDir();
3861
4424
  if (bundled) {
3862
- return path24.join(bundled, nameOrPath);
4425
+ return path27.join(bundled, nameOrPath);
3863
4426
  }
3864
- return toRepoPath(repoRoot, path24.join("packs", nameOrPath));
4427
+ return toRepoPath(repoRoot, path27.join("packs", nameOrPath));
3865
4428
  }
3866
4429
  async function directFiles(dir, ext) {
3867
4430
  try {
3868
4431
  const entries = await readdir6(dir, { withFileTypes: true });
3869
- return entries.filter((entry) => entry.isFile() && entry.name.endsWith(ext)).map((entry) => path24.join(dir, entry.name)).sort();
4432
+ return entries.filter((entry) => entry.isFile() && entry.name.endsWith(ext)).map((entry) => path27.join(dir, entry.name)).sort();
3870
4433
  } catch (error) {
3871
4434
  if (error.code === "ENOENT") {
3872
4435
  return [];
@@ -3879,11 +4442,11 @@ async function skillEntries(dir) {
3879
4442
  const entries = await readdir6(dir, { withFileTypes: true });
3880
4443
  const result = [];
3881
4444
  for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
3882
- const full = path24.join(dir, entry.name);
4445
+ const full = path27.join(dir, entry.name);
3883
4446
  if (entry.isFile() && entry.name.endsWith(".md")) {
3884
4447
  result.push(full);
3885
4448
  }
3886
- if (entry.isDirectory() && await exists5(path24.join(full, "SKILL.md"))) {
4449
+ if (entry.isDirectory() && await exists5(path27.join(full, "SKILL.md"))) {
3887
4450
  result.push(full);
3888
4451
  }
3889
4452
  }
@@ -3898,34 +4461,34 @@ async function skillEntries(dir) {
3898
4461
  async function collectObjects(packDir, manifest) {
3899
4462
  async function resolveRef(ref) {
3900
4463
  const safe = safeRelative(ref);
3901
- const local = path24.resolve(packDir, safe);
4464
+ const local = path27.resolve(packDir, safe);
3902
4465
  if (await exists5(local)) {
3903
4466
  return local;
3904
4467
  }
3905
- return path24.resolve(packDir, "..", "..", safe);
4468
+ return path27.resolve(packDir, "..", "..", safe);
3906
4469
  }
3907
4470
  return {
3908
4471
  skills: [
3909
4472
  ...await Promise.all(manifest.skills.map(resolveRef)),
3910
- ...await skillEntries(path24.join(packDir, "skills"))
4473
+ ...await skillEntries(path27.join(packDir, "skills"))
3911
4474
  ],
3912
4475
  tools: [
3913
4476
  ...await Promise.all(manifest.tools.map(resolveRef)),
3914
- ...await directFiles(path24.join(packDir, "tools"), ".yaml")
4477
+ ...await directFiles(path27.join(packDir, "tools"), ".yaml")
3915
4478
  ],
3916
4479
  rules: [
3917
4480
  ...await Promise.all(manifest.rules.map(resolveRef)),
3918
- ...await directFiles(path24.join(packDir, "rules"), ".md")
4481
+ ...await directFiles(path27.join(packDir, "rules"), ".md")
3919
4482
  ],
3920
4483
  connections: [
3921
4484
  ...await Promise.all(manifest.connections.map(resolveRef)),
3922
- ...await directFiles(path24.join(packDir, "connections"), ".yaml")
4485
+ ...await directFiles(path27.join(packDir, "connections"), ".yaml")
3923
4486
  ]
3924
4487
  };
3925
4488
  }
3926
4489
  function baseName(source) {
3927
- const parsed = path24.basename(source) === "SKILL.md" ? path24.dirname(source) : source;
3928
- return path24.basename(parsed, path24.extname(parsed));
4490
+ const parsed = path27.basename(source) === "SKILL.md" ? path27.dirname(source) : source;
4491
+ return path27.basename(parsed, path27.extname(parsed));
3929
4492
  }
3930
4493
  async function listPacks(repoRoot) {
3931
4494
  const dirs = [toRepoPath(repoRoot, "packs"), await bundledPacksDir()].filter((dir) => Boolean(dir));
@@ -3942,8 +4505,8 @@ async function listPacks(repoRoot) {
3942
4505
  if (!entry.isDirectory() || seen.has(entry.name)) {
3943
4506
  continue;
3944
4507
  }
3945
- const packDir = path24.join(root, entry.name);
3946
- if (!await exists5(path24.join(packDir, "pack.yaml"))) {
4508
+ const packDir = path27.join(root, entry.name);
4509
+ if (!await exists5(path27.join(packDir, "pack.yaml"))) {
3947
4510
  continue;
3948
4511
  }
3949
4512
  seen.add(entry.name);
@@ -3967,14 +4530,14 @@ async function inspectPack(repoRoot, nameOrPath) {
3967
4530
  };
3968
4531
  }
3969
4532
  async function validateProse(file, kind) {
3970
- const target = path24.basename(file) === "SKILL.md" ? file : file;
3971
- const content = await readFile10(target, "utf8");
4533
+ const target = path27.basename(file) === "SKILL.md" ? file : file;
4534
+ const content = await readFile12(target, "utf8");
3972
4535
  const parsed = parseFrontmatter(content);
3973
4536
  const schema = kind === "skill" ? skillFrontmatterSchema : ruleFrontmatterSchema;
3974
4537
  schema.parse(parsed.data);
3975
4538
  }
3976
4539
  async function validateYaml(file, kind) {
3977
- const content = await readFile10(file, "utf8");
4540
+ const content = await readFile12(file, "utf8");
3978
4541
  const schema = kind === "tool" ? toolManifestSchema : connectionManifestSchema;
3979
4542
  schema.parse(parseYaml3(content));
3980
4543
  }
@@ -3985,7 +4548,7 @@ async function validatePack(repoRoot, nameOrPath) {
3985
4548
  const manifest = await readPackManifest(packDir);
3986
4549
  const objects = await collectObjects(packDir, manifest);
3987
4550
  for (const skill of objects.skills) {
3988
- await validateProse(path24.basename(skill) === "SKILL.md" ? skill : path24.join(skill, "SKILL.md"), "skill");
4551
+ await validateProse(path27.basename(skill) === "SKILL.md" ? skill : path27.join(skill, "SKILL.md"), "skill");
3989
4552
  }
3990
4553
  for (const rule of objects.rules) await validateProse(rule, "rule");
3991
4554
  for (const tool of objects.tools) await validateYaml(tool, "tool");
@@ -3999,10 +4562,10 @@ async function validatePack(repoRoot, nameOrPath) {
3999
4562
  return { ok: !findings.some((finding3) => finding3.severity === "error"), findings };
4000
4563
  }
4001
4564
  async function copyObject(source, destDir) {
4002
- const info = await stat8(source);
4565
+ const info = await stat10(source);
4003
4566
  const name = baseName(source);
4004
- const dest = info.isDirectory() ? path24.join(destDir, name) : path24.join(destDir, path24.basename(source));
4005
- await mkdir10(destDir, { recursive: true });
4567
+ const dest = info.isDirectory() ? path27.join(destDir, name) : path27.join(destDir, path27.basename(source));
4568
+ await mkdir12(destDir, { recursive: true });
4006
4569
  await cp3(source, dest, { recursive: true, force: true });
4007
4570
  return dest;
4008
4571
  }
@@ -4290,8 +4853,10 @@ async function runConnectionsCheck(repoRoot) {
4290
4853
  // src/cli.ts
4291
4854
  function createProgram(repoRoot = process.cwd()) {
4292
4855
  const program = new Command();
4293
- program.name("threadroot").description("Git for your AI agent harness: one canonical source, compiled to every vendor format.").version("0.1.0");
4294
- program.command("init").description("Scaffold a Threadroot harness, import existing vendor files once, and compile.").option("--force", "Re-initialize over an existing harness.").option("--no-import", "Skip importing existing vendor files (blank-slate init).").option("--profile <profile>", "Override the detected project profile.").option("--adapters <list>", "Comma-separated adapters: agents,claude,copilot,cursor.").action((options) => runInit(repoRoot, options));
4856
+ program.name("threadroot").description("Git for your AI agent harness: one canonical .threadroot source, optional provider exposure.").version("0.1.1");
4857
+ program.command("init").description("Scaffold a local-only Threadroot harness and import existing vendor files once.").option("--force", "Re-initialize over an existing harness.").option("--no-import", "Skip importing existing vendor files (blank-slate init).").option("--profile <profile>", "Override the detected project profile.").option("--adapters <list>", "Comma-separated adapters: agents,claude,copilot,cursor.").option("--expose <list>", "Comma-separated provider skill shims to write: codex,claude,cursor,copilot,gemini,windsurf,antigravity,opencode,all.").action((options) => runInit(repoRoot, options));
4858
+ program.command("expose").argument("[agent]", "Provider(s) to expose: codex,claude,cursor,copilot,gemini,windsurf,antigravity,opencode,all.").option("--dry-run", "Show project files that would be written.").option("--check", "Check current project exposure state.").option("--undo", "Remove Threadroot-managed project exposure files.").option("--force", "Replace an existing unmanaged threadroot skill.").description("Write thin provider project skills that point agents at `.threadroot/`.").action((agent, options) => runExpose(repoRoot, agent, options));
4859
+ program.command("setup").option("--global", "Install machine-level Threadroot agent bootstrap skills/config.").option("--agent <list>", "Provider(s): codex,claude,cursor,copilot,gemini,windsurf,antigravity,opencode,all.").option("--dry-run", "Show global files that would be written.").option("--check", "Check global Threadroot setup state.").option("--undo", "Remove Threadroot-managed global setup files/blocks.").option("--force", "Replace an existing unmanaged threadroot skill.").option("--mcp", "Also add Threadroot MCP to Codex global config when Codex is selected.").description("Set up Threadroot once per machine for supported coding agents.").action((options) => runSetup(repoRoot, options));
4295
4860
  program.command("status").description("Show harness state, object counts, and compiled-output drift.").action(() => runStatus(repoRoot));
4296
4861
  program.command("diff").description("Show the diff between the canonical harness and each compiled vendor file.").action(() => runDiff(repoRoot));
4297
4862
  program.command("doctor").description("Check harness validity, compiled output health, MCP hints, and tool trust.").action(() => runDoctor(repoRoot));