threadroot 0.1.3 → 0.1.5

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
@@ -4,8 +4,8 @@
4
4
  import { Command } from "commander";
5
5
 
6
6
  // src/core/bootstrap.ts
7
- import { stat as stat9 } from "fs/promises";
8
- import path23 from "path";
7
+ import { stat as stat10 } from "fs/promises";
8
+ import path24 from "path";
9
9
 
10
10
  // src/core/doctor.ts
11
11
  import { stat as stat5 } from "fs/promises";
@@ -1487,6 +1487,8 @@ async function createConnection(repoRoot, input2, options = {}) {
1487
1487
  risk: input2.risk ?? "medium",
1488
1488
  confirm: input2.confirm ?? input2.risk === "high",
1489
1489
  healthcheck: input2.healthcheck ? { run: input2.healthcheck, expectExitCode: 0 } : void 0,
1490
+ allow: input2.allow ?? [],
1491
+ deny: input2.deny ?? [],
1490
1492
  scope
1491
1493
  };
1492
1494
  const parsed = connectionManifestSchema.safeParse(candidate);
@@ -1716,16 +1718,17 @@ function threadrootSkillContent(provider, scope) {
1716
1718
  "## Workflow",
1717
1719
  "",
1718
1720
  "1. If `threadroot --version` works, use `threadroot`. Otherwise use `npx --yes threadroot@latest` for one-off commands.",
1719
- "2. If `.threadroot/harness.yaml` is missing and the user wants setup, run `threadroot bootstrap --yes` or `npx --yes threadroot@latest bootstrap --yes`.",
1721
+ "2. If `.threadroot/harness.yaml` is missing and the user wants setup, run `threadroot bootstrap --yes --mcp` or `npx --yes threadroot@latest bootstrap --yes --mcp`.",
1720
1722
  '3. At the start of a coding session, run `threadroot start "<task>"` to get doctor status, project state, relevant skills, tools, memory, and the command map.',
1721
- '4. For a narrower slice, run `threadroot context "<task>"` and use the returned skills, rules, tools, memory, and references before doing broad file reads.',
1722
- "5. Use `threadroot tools list`, `threadroot tools check`, and `threadroot run <tool>` for explicit local capabilities. Confirm risky tools when required.",
1723
- "6. Do not create provider-specific files unless the user asks. Use `threadroot expose <agent>` when native project skill shims are desired.",
1723
+ "4. If the project needs curated capabilities, inspect `threadroot packs list` and install relevant packs with `threadroot packs install <pack>` or `threadroot bootstrap --packs <list>` during setup.",
1724
+ '5. For a narrower slice, run `threadroot context "<task>"` and use the returned skills, rules, tools, memory, and references before doing broad file reads.',
1725
+ "6. Use `threadroot tools list`, `threadroot tools check`, and `threadroot run <tool>` for explicit local capabilities. Confirm risky tools only after human review.",
1726
+ "7. Do not create provider-specific files unless the user asks. Use `threadroot expose <agent>` when native project skill shims are desired.",
1724
1727
  "",
1725
1728
  "## Useful Commands",
1726
1729
  "",
1727
1730
  "```bash",
1728
- "threadroot bootstrap --yes",
1731
+ "threadroot bootstrap --yes --mcp",
1729
1732
  'threadroot start "<task>"',
1730
1733
  "threadroot doctor",
1731
1734
  "threadroot status",
@@ -1997,6 +2000,56 @@ function authorizeTool(tool, options) {
1997
2000
  return { allowed: true };
1998
2001
  }
1999
2002
 
2003
+ // src/core/tools/connection-policy.ts
2004
+ function normalize(value) {
2005
+ return value.trim().replace(/\s+/g, " ").toLowerCase();
2006
+ }
2007
+ function commandBody(command, connectionCommand) {
2008
+ const normalized = normalize(command);
2009
+ const prefix = normalize(connectionCommand);
2010
+ if (normalized === prefix) {
2011
+ return "";
2012
+ }
2013
+ if (normalized.startsWith(`${prefix} `)) {
2014
+ return normalized.slice(prefix.length).trim();
2015
+ }
2016
+ return normalized;
2017
+ }
2018
+ function includesPattern(command, pattern) {
2019
+ return command.includes(normalize(pattern));
2020
+ }
2021
+ function authorizeConnectionCommand(connection, command) {
2022
+ const { allow, deny } = connection.manifest;
2023
+ if (allow.length === 0 && deny.length === 0) {
2024
+ return { allowed: true };
2025
+ }
2026
+ if (!command) {
2027
+ return {
2028
+ allowed: false,
2029
+ message: `Connection \`${connection.name}\` defines allow/deny rules, but this tool uses a script that Threadroot cannot policy-check. Use a shell \`run\` tool for connection-backed actions.`
2030
+ };
2031
+ }
2032
+ const body = commandBody(command, connection.manifest.command);
2033
+ const full = normalize(command);
2034
+ const denied = deny.find((pattern) => includesPattern(body, pattern) || includesPattern(full, pattern));
2035
+ if (denied) {
2036
+ return {
2037
+ allowed: false,
2038
+ message: `Connection \`${connection.name}\` denies command fragment \`${denied}\`.`
2039
+ };
2040
+ }
2041
+ if (allow.length > 0) {
2042
+ const allowed = allow.some((pattern) => includesPattern(body, pattern) || includesPattern(full, pattern));
2043
+ if (!allowed) {
2044
+ return {
2045
+ allowed: false,
2046
+ message: `Connection \`${connection.name}\` only allows: ${allow.map((pattern) => `\`${pattern}\``).join(", ")}.`
2047
+ };
2048
+ }
2049
+ }
2050
+ return { allowed: true };
2051
+ }
2052
+
2000
2053
  // src/core/tools/interpolate.ts
2001
2054
  var ToolInputError = class extends Error {
2002
2055
  constructor(message) {
@@ -2373,7 +2426,19 @@ async function runTool(repoRoot, options) {
2373
2426
  const values = resolveInputs(tool.manifest, options.input);
2374
2427
  const env = inputEnv(values);
2375
2428
  const execOptions = { cwd: repoRoot, env, timeoutMs: options.timeoutMs, signal: options.signal };
2376
- const result = tool.manifest.run ? await executeShell(interpolateRun(tool.manifest.run, values), execOptions) : await executeScript(repoRoot, tool.manifest.script, execOptions);
2429
+ const command = tool.manifest.run ? interpolateRun(tool.manifest.run, values) : void 0;
2430
+ if (connection) {
2431
+ const connectionDecision = authorizeConnectionCommand(connection, command);
2432
+ if (!connectionDecision.allowed) {
2433
+ return {
2434
+ status: "blocked",
2435
+ tool: tool.name,
2436
+ reason: "not-allowed",
2437
+ message: connectionDecision.message
2438
+ };
2439
+ }
2440
+ }
2441
+ const result = command ? await executeShell(command, execOptions) : await executeScript(repoRoot, tool.manifest.script, execOptions);
2377
2442
  return { status: "ran", tool: tool.name, result };
2378
2443
  }
2379
2444
  async function checkToolHealth(repoRoot, tool) {
@@ -2399,6 +2464,11 @@ import { access, mkdtemp, readFile as readFile8, rm as rm2, writeFile as writeFi
2399
2464
  import { constants, realpathSync } from "fs";
2400
2465
  import { homedir as homedir2, tmpdir } from "os";
2401
2466
  import path14 from "path";
2467
+
2468
+ // src/core/version.ts
2469
+ var THREADROOT_VERSION = "0.1.5";
2470
+
2471
+ // src/core/mcp-check.ts
2402
2472
  var REQUIRED_MCP_TOOLS = [
2403
2473
  "context",
2404
2474
  "skills_list",
@@ -2419,14 +2489,19 @@ function codexConfigPath2(home = homedir2()) {
2419
2489
  return path14.join(home, ".codex", "config.toml");
2420
2490
  }
2421
2491
  function mcpEntryForCurrentProcess() {
2422
- const scriptPath = currentScriptPath();
2492
+ return mcpEntryForScriptPath(process.argv[1]);
2493
+ }
2494
+ function mcpEntryForScriptPath(rawScriptPath) {
2495
+ const scriptPath = currentScriptPath(rawScriptPath);
2496
+ if (scriptPath && isNpxPackagePath(scriptPath)) {
2497
+ return { command: "npx", args: ["--yes", `threadroot@${THREADROOT_VERSION}`, "mcp"] };
2498
+ }
2423
2499
  if (scriptPath && path14.basename(scriptPath) === "index.js" && scriptPath.includes(`${path14.sep}dist${path14.sep}`)) {
2424
2500
  return { command: process.execPath, args: [scriptPath, "mcp"] };
2425
2501
  }
2426
2502
  return { command: "threadroot", args: ["mcp"] };
2427
2503
  }
2428
- function currentScriptPath() {
2429
- const scriptPath = process.argv[1];
2504
+ function currentScriptPath(scriptPath) {
2430
2505
  if (!scriptPath) {
2431
2506
  return void 0;
2432
2507
  }
@@ -2436,6 +2511,10 @@ function currentScriptPath() {
2436
2511
  return scriptPath;
2437
2512
  }
2438
2513
  }
2514
+ function isNpxPackagePath(scriptPath) {
2515
+ const normalized = scriptPath.split(path14.sep).join("/");
2516
+ return normalized.includes("/.npm/_npx/") && normalized.includes("/node_modules/threadroot/");
2517
+ }
2439
2518
  async function readCodexThreadrootMcpEntry(home = homedir2()) {
2440
2519
  let raw;
2441
2520
  try {
@@ -2635,7 +2714,7 @@ function runMcpHandshake(entry, repoRoot, timeoutMs) {
2635
2714
  params: {
2636
2715
  protocolVersion: "2024-11-05",
2637
2716
  capabilities: {},
2638
- clientInfo: { name: "threadroot-check", version: "0.1.3" }
2717
+ clientInfo: { name: "threadroot-check", version: THREADROOT_VERSION }
2639
2718
  }
2640
2719
  })}
2641
2720
  `
@@ -2658,7 +2737,7 @@ async function runOneShotMcpHandshake(entry, repoRoot, timeoutMs) {
2658
2737
  params: {
2659
2738
  protocolVersion: "2024-11-05",
2660
2739
  capabilities: {},
2661
- clientInfo: { name: "threadroot-check", version: "0.1.3" }
2740
+ clientInfo: { name: "threadroot-check", version: THREADROOT_VERSION }
2662
2741
  }
2663
2742
  }),
2664
2743
  JSON.stringify({ jsonrpc: "2.0", method: "notifications/initialized" }),
@@ -3264,7 +3343,7 @@ async function readIfExists2(filePath) {
3264
3343
  throw error;
3265
3344
  }
3266
3345
  }
3267
- function normalize(text) {
3346
+ function normalize2(text) {
3268
3347
  return text.toLowerCase().replace(/\s+/g, " ").trim();
3269
3348
  }
3270
3349
  function splitSections(markdown) {
@@ -3280,7 +3359,7 @@ function splitSections(markdown) {
3280
3359
  for (const line of markdown.split(/\r?\n/)) {
3281
3360
  if (/^#{1,6}\s/.test(line)) {
3282
3361
  flush();
3283
- heading = normalize(line.replace(/^#+\s*/, ""));
3362
+ heading = normalize2(line.replace(/^#+\s*/, ""));
3284
3363
  buffer = [line];
3285
3364
  } else {
3286
3365
  buffer.push(line);
@@ -3290,13 +3369,13 @@ function splitSections(markdown) {
3290
3369
  return sections;
3291
3370
  }
3292
3371
  function novelSections(canonical, other) {
3293
- const haystack = normalize(canonical);
3372
+ const haystack = normalize2(canonical);
3294
3373
  const seenHeadings = new Set(splitSections(canonical).map((section) => section.heading).filter(Boolean));
3295
3374
  return splitSections(other).filter((section) => {
3296
3375
  if (section.heading && seenHeadings.has(section.heading)) {
3297
3376
  return false;
3298
3377
  }
3299
- return !haystack.includes(normalize(section.text));
3378
+ return !haystack.includes(normalize2(section.text));
3300
3379
  });
3301
3380
  }
3302
3381
  async function listCursorRules(repoRoot) {
@@ -3530,43 +3609,32 @@ async function initHarness(repoRoot, options = {}) {
3530
3609
  return { name, profile, adapters, skills, tools: tools2, memory, rules, import: report, compiled: written, exposed };
3531
3610
  }
3532
3611
 
3533
- // src/core/status.ts
3534
- async function harnessStatus(repoRoot, options = {}) {
3535
- let harness;
3536
- try {
3537
- harness = await resolveHarness(repoRoot, { home: options.home });
3538
- } catch (error) {
3539
- if (error instanceof HarnessError) {
3540
- return { exists: false };
3541
- }
3542
- throw error;
3543
- }
3544
- const files = await compile(repoRoot, harness);
3545
- const drift = await detectDrift(repoRoot, files);
3546
- return {
3547
- exists: true,
3548
- manifest: {
3549
- name: harness.manifest.name,
3550
- profile: harness.manifest.profile,
3551
- adapters: harness.manifest.adapters,
3552
- toolsAllow: harness.manifest.tools.allow
3553
- },
3554
- counts: {
3555
- skills: harness.skills.length,
3556
- rules: harness.rules.length,
3557
- tools: harness.tools.length,
3558
- memory: harness.memory.length
3559
- },
3560
- drift
3561
- };
3562
- }
3563
-
3564
- // src/core/bootstrap.ts
3565
- var DEFAULT_TASK = "start this project";
3566
- async function harnessExists(repoRoot) {
3567
- return pathExists2(path23.join(repoRoot, ".threadroot", "harness.yaml"));
3568
- }
3569
- async function pathExists2(target) {
3612
+ // src/core/packs/index.ts
3613
+ import { createHash as createHash2 } from "crypto";
3614
+ import { cp as cp2, lstat, mkdir as mkdir10, readFile as readFile11, readdir as readdir5, stat as stat9 } from "fs/promises";
3615
+ import path23 from "path";
3616
+ import { fileURLToPath as fileURLToPath2 } from "url";
3617
+ import { parse as parseYaml3 } from "yaml";
3618
+ import { z as z4 } from "zod";
3619
+ var packManifestSchema = z4.object({
3620
+ name: z4.string().min(1),
3621
+ version: z4.literal(1),
3622
+ description: z4.string().min(1),
3623
+ skills: z4.array(z4.string()).default([]),
3624
+ tools: z4.array(z4.string()).default([]),
3625
+ rules: z4.array(z4.string()).default([]),
3626
+ connections: z4.array(z4.string()).default([])
3627
+ });
3628
+ var DIST_DIR2 = path23.dirname(fileURLToPath2(import.meta.url));
3629
+ var PACKAGE_ROOT_FROM_BUNDLE2 = path23.resolve(DIST_DIR2, "..");
3630
+ var PACKAGE_ROOT_FROM_DIST2 = path23.resolve(DIST_DIR2, "../../..");
3631
+ var PACKAGE_ROOT_FROM_SRC2 = path23.resolve(DIST_DIR2, "../../../..");
3632
+ var PACK_CANDIDATES = [
3633
+ path23.join(PACKAGE_ROOT_FROM_BUNDLE2, "packs"),
3634
+ path23.join(PACKAGE_ROOT_FROM_DIST2, "packs"),
3635
+ path23.join(PACKAGE_ROOT_FROM_SRC2, "packs")
3636
+ ];
3637
+ async function exists5(target) {
3570
3638
  try {
3571
3639
  await stat9(target);
3572
3640
  return true;
@@ -3577,146 +3645,521 @@ async function pathExists2(target) {
3577
3645
  throw error;
3578
3646
  }
3579
3647
  }
3580
- function modeFor(options) {
3581
- return options.yes && !options.dryRun ? "write" : "dry-run";
3582
- }
3583
- async function bootstrapProject(repoRoot, options = {}) {
3584
- const task = options.task?.trim() || DEFAULT_TASK;
3585
- const mode = modeFor(options);
3586
- const write2 = mode === "write";
3587
- const notes = [];
3588
- const existed = await harnessExists(repoRoot);
3589
- let setup;
3590
- let init;
3591
- let exposed;
3592
- if (!options.noGlobal) {
3593
- setup = await setupGlobal({
3594
- agents: options.agents ?? "all",
3595
- mode,
3596
- home: options.home,
3597
- mcp: options.mcp,
3598
- mcpEntry: options.mcpEntry
3599
- });
3600
- } else {
3601
- notes.push("Skipped global setup because --no-global was set.");
3602
- }
3603
- if (!existed && !options.noInit) {
3604
- if (write2) {
3605
- init = await initHarness(repoRoot, {
3606
- import: options.import,
3607
- profile: options.profile,
3608
- home: options.home
3609
- });
3610
- } else {
3611
- notes.push(`Would initialize local-only harness at ${path23.join(".threadroot", "harness.yaml")}.`);
3648
+ async function firstExisting(candidates) {
3649
+ for (const candidate of candidates) {
3650
+ if (await isPackRoot(candidate)) {
3651
+ return candidate;
3612
3652
  }
3613
- } else if (existed) {
3614
- notes.push("Existing harness detected; bootstrap will not reinitialize it.");
3615
- } else {
3616
- notes.push("Skipped project initialization because --no-init was set.");
3617
- }
3618
- const hasHarnessAfterInit = existed || Boolean(init) || await pathExists2(path23.join(repoRoot, ".threadroot", "harness.yaml"));
3619
- if (options.expose) {
3620
- exposed = await exposeProject(repoRoot, {
3621
- agents: options.expose,
3622
- mode
3623
- });
3624
3653
  }
3625
- let status;
3626
- let doctorReport;
3627
- let context;
3628
- let mcpCheck;
3629
- if (options.mcp && write2) {
3630
- mcpCheck = await checkCodexMcp({ repoRoot, home: options.home });
3654
+ return void 0;
3655
+ }
3656
+ async function bundledPacksDir() {
3657
+ return firstExisting(PACK_CANDIDATES);
3658
+ }
3659
+ async function isPackRoot(candidate) {
3660
+ let entries;
3661
+ try {
3662
+ entries = await readdir5(candidate, { withFileTypes: true });
3663
+ } catch {
3664
+ return false;
3631
3665
  }
3632
- if (hasHarnessAfterInit) {
3633
- status = await harnessStatus(repoRoot, { home: options.home });
3634
- doctorReport = await doctor(repoRoot, { home: options.home });
3635
- if (status.exists) {
3636
- context = await assembleContext(repoRoot, task, { home: options.home, fallbackSkills: true });
3666
+ for (const entry of entries) {
3667
+ if (entry.isDirectory() && await exists5(path23.join(candidate, entry.name, "pack.yaml"))) {
3668
+ return true;
3637
3669
  }
3638
- } else {
3639
- notes.push("Skipped doctor/status/context because no harness exists yet.");
3640
3670
  }
3641
- if (!write2) {
3642
- notes.push("Run `threadroot bootstrap --yes` to apply this plan.");
3671
+ return false;
3672
+ }
3673
+ function safeRelative(ref) {
3674
+ const normalized = path23.normalize(ref);
3675
+ if (path23.isAbsolute(normalized) || normalized === ".." || normalized.startsWith(`..${path23.sep}`)) {
3676
+ throw new Error(`Unsafe pack reference: ${ref}`);
3643
3677
  }
3644
- return {
3645
- mode: write2 ? "write" : "plan",
3646
- task,
3647
- harnessExisted: existed,
3648
- setup,
3649
- init,
3650
- expose: exposed,
3651
- status,
3652
- doctor: doctorReport,
3653
- context,
3654
- mcpCheck,
3655
- notes
3656
- };
3678
+ return normalized;
3657
3679
  }
3658
- async function startSession(repoRoot, options = {}) {
3659
- const task = options.task?.trim() || DEFAULT_TASK;
3660
- const status = await harnessStatus(repoRoot, { home: options.home });
3661
- const notes = [];
3662
- if (!status.exists) {
3663
- return {
3664
- task,
3665
- status,
3666
- notes: ["No harness found. Run `threadroot bootstrap --yes` first."]
3667
- };
3680
+ async function readPackManifest(packDir) {
3681
+ const file = path23.join(packDir, "pack.yaml");
3682
+ const parsed = packManifestSchema.safeParse(parseYaml3(await readFile11(file, "utf8")));
3683
+ if (!parsed.success) {
3684
+ const detail = parsed.error.issues.map((issue) => issue.message).join("; ");
3685
+ throw new Error(`Invalid pack manifest ${file}: ${detail}`);
3668
3686
  }
3669
- const doctorReport = await doctor(repoRoot, { home: options.home });
3670
- const context = await assembleContext(repoRoot, task, { home: options.home, fallbackSkills: true });
3671
- return { task, status, doctor: doctorReport, context, notes };
3687
+ return parsed.data;
3672
3688
  }
3673
-
3674
- // src/commands/session-output.ts
3675
- function printDoctor(report) {
3676
- if (!report) {
3677
- return;
3689
+ async function packDirFor(repoRoot, nameOrPath) {
3690
+ if (path23.isAbsolute(nameOrPath)) {
3691
+ const root = path23.resolve(repoRoot);
3692
+ const target = path23.resolve(nameOrPath);
3693
+ const repoRelative = path23.relative(root, target);
3694
+ const bundled2 = await bundledPacksDir();
3695
+ const bundledRelative = bundled2 ? path23.relative(path23.resolve(bundled2), target) : void 0;
3696
+ const insideRepo = repoRelative !== "" && !repoRelative.startsWith("..") && !path23.isAbsolute(repoRelative);
3697
+ const insideBundled = bundledRelative !== void 0 && bundledRelative !== "" && !bundledRelative.startsWith("..") && !path23.isAbsolute(bundledRelative);
3698
+ if (!insideRepo && !insideBundled) {
3699
+ throw new Error(`Pack path must be repo-relative or a built-in pack name: ${nameOrPath}`);
3700
+ }
3701
+ return nameOrPath;
3678
3702
  }
3679
- const actionable = report.findings.filter((finding3) => finding3.severity !== "info");
3680
- console.log(actionable.length === 0 ? "doctor: clean" : `doctor: ${report.summary.errors} error(s), ${report.summary.warnings} warning(s)`);
3681
- for (const finding3 of report.findings.slice(0, 8)) {
3682
- const label = finding3.severity === "info" ? "hint" : finding3.severity;
3683
- const suffix = finding3.path ? ` (${finding3.path})` : "";
3684
- console.log(`- ${label} ${finding3.code}: ${finding3.message}${suffix}`);
3703
+ if (nameOrPath.startsWith(".") || nameOrPath.includes("/") || nameOrPath.includes("\\")) {
3704
+ return toRepoPath(repoRoot, nameOrPath);
3685
3705
  }
3686
- if (report.findings.length > 8) {
3687
- console.log(`- ... ${report.findings.length - 8} more finding(s)`);
3706
+ const bundled = await bundledPacksDir();
3707
+ if (bundled) {
3708
+ return path23.join(bundled, nameOrPath);
3688
3709
  }
3710
+ return toRepoPath(repoRoot, path23.join("packs", nameOrPath));
3689
3711
  }
3690
- function printStatus(status) {
3691
- if (!status) {
3692
- return;
3693
- }
3694
- if (!status.exists) {
3695
- console.log("harness: missing");
3696
- return;
3712
+ async function directFiles(dir, ext) {
3713
+ try {
3714
+ const entries = await readdir5(dir, { withFileTypes: true });
3715
+ return entries.filter((entry) => entry.isFile() && entry.name.endsWith(ext)).map((entry) => path23.join(dir, entry.name)).sort();
3716
+ } catch (error) {
3717
+ if (error.code === "ENOENT") {
3718
+ return [];
3719
+ }
3720
+ throw error;
3697
3721
  }
3698
- console.log(`harness: ${status.manifest.name} (${status.manifest.profile})`);
3699
- console.log(`adapters: ${status.manifest.adapters.length > 0 ? status.manifest.adapters.join(", ") : "none (local-only)"}`);
3700
- console.log(
3701
- `objects: ${status.counts.skills} skills, ${status.counts.rules} rules, ${status.counts.tools} tools, ${status.counts.memory} memory`
3702
- );
3703
3722
  }
3704
- function printContext(context) {
3705
- if (!context) {
3706
- return;
3707
- }
3708
- console.log(`task: ${context.task}`);
3709
- if (context.skills.length > 0) {
3710
- const skillLabel = context.skills.some((skill) => skill.score > 0) ? "relevant skills:" : "starter skills:";
3711
- console.log(skillLabel);
3712
- for (const skill of context.skills.slice(0, 8)) {
3713
- console.log(`- ${skill.name} - ${skill.when}`);
3723
+ async function skillEntries(dir) {
3724
+ try {
3725
+ const entries = await readdir5(dir, { withFileTypes: true });
3726
+ const result = [];
3727
+ for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
3728
+ const full = path23.join(dir, entry.name);
3729
+ if (entry.isFile() && entry.name.endsWith(".md")) {
3730
+ result.push(full);
3731
+ }
3732
+ if (entry.isDirectory() && await exists5(path23.join(full, "SKILL.md"))) {
3733
+ result.push(full);
3734
+ }
3714
3735
  }
3715
- } else {
3716
- console.log("relevant skills: none matched; run `threadroot skills list` to inspect all skills.");
3736
+ return result;
3737
+ } catch (error) {
3738
+ if (error.code === "ENOENT") {
3739
+ return [];
3740
+ }
3741
+ throw error;
3717
3742
  }
3718
- if (context.tools.length > 0) {
3719
- console.log("available tools:");
3743
+ }
3744
+ async function collectObjects(packDir, manifest) {
3745
+ async function resolveRef(ref) {
3746
+ const safe = safeRelative(ref);
3747
+ const local = path23.resolve(packDir, safe);
3748
+ if (await exists5(local)) {
3749
+ return local;
3750
+ }
3751
+ return path23.resolve(packDir, "..", "..", safe);
3752
+ }
3753
+ return {
3754
+ skills: [
3755
+ ...await Promise.all(manifest.skills.map(resolveRef)),
3756
+ ...await skillEntries(path23.join(packDir, "skills"))
3757
+ ],
3758
+ tools: [
3759
+ ...await Promise.all(manifest.tools.map(resolveRef)),
3760
+ ...await directFiles(path23.join(packDir, "tools"), ".yaml")
3761
+ ],
3762
+ rules: [
3763
+ ...await Promise.all(manifest.rules.map(resolveRef)),
3764
+ ...await directFiles(path23.join(packDir, "rules"), ".md")
3765
+ ],
3766
+ connections: [
3767
+ ...await Promise.all(manifest.connections.map(resolveRef)),
3768
+ ...await directFiles(path23.join(packDir, "connections"), ".yaml")
3769
+ ]
3770
+ };
3771
+ }
3772
+ function baseName(source) {
3773
+ const parsed = path23.basename(source) === "SKILL.md" ? path23.dirname(source) : source;
3774
+ return path23.basename(parsed, path23.extname(parsed));
3775
+ }
3776
+ async function listPacks(repoRoot) {
3777
+ const dirs = [toRepoPath(repoRoot, "packs"), await bundledPacksDir()].filter((dir) => Boolean(dir));
3778
+ const seen = /* @__PURE__ */ new Set();
3779
+ const packs = [];
3780
+ for (const root of dirs) {
3781
+ let entries;
3782
+ try {
3783
+ entries = await readdir5(root, { withFileTypes: true });
3784
+ } catch {
3785
+ continue;
3786
+ }
3787
+ for (const entry of entries) {
3788
+ if (!entry.isDirectory() || seen.has(entry.name)) {
3789
+ continue;
3790
+ }
3791
+ const packDir = path23.join(root, entry.name);
3792
+ if (!await exists5(path23.join(packDir, "pack.yaml"))) {
3793
+ continue;
3794
+ }
3795
+ seen.add(entry.name);
3796
+ packs.push(await inspectPack(repoRoot, packDir));
3797
+ }
3798
+ }
3799
+ return packs.sort((a, b) => a.name.localeCompare(b.name));
3800
+ }
3801
+ async function inspectPack(repoRoot, nameOrPath) {
3802
+ const packDir = await packDirFor(repoRoot, nameOrPath);
3803
+ const manifest = await readPackManifest(packDir);
3804
+ const objects = await collectObjects(packDir, manifest);
3805
+ return {
3806
+ name: manifest.name,
3807
+ description: manifest.description,
3808
+ path: packDir,
3809
+ skills: objects.skills.map(baseName),
3810
+ tools: objects.tools.map(baseName),
3811
+ rules: objects.rules.map(baseName),
3812
+ connections: objects.connections.map(baseName)
3813
+ };
3814
+ }
3815
+ async function validateProse(file, kind) {
3816
+ const target = path23.basename(file) === "SKILL.md" ? file : file;
3817
+ const content = await readFile11(target, "utf8");
3818
+ const parsed = parseFrontmatter(content);
3819
+ const schema = kind === "skill" ? skillFrontmatterSchema : ruleFrontmatterSchema;
3820
+ schema.parse(parsed.data);
3821
+ }
3822
+ async function validateYaml(file, kind) {
3823
+ const content = await readFile11(file, "utf8");
3824
+ const schema = kind === "tool" ? toolManifestSchema : connectionManifestSchema;
3825
+ schema.parse(parseYaml3(content));
3826
+ }
3827
+ async function validatePack(repoRoot, nameOrPath) {
3828
+ const findings = [];
3829
+ try {
3830
+ const packDir = await packDirFor(repoRoot, nameOrPath);
3831
+ const manifest = await readPackManifest(packDir);
3832
+ const objects = await collectObjects(packDir, manifest);
3833
+ for (const skill of objects.skills) {
3834
+ await validateProse(path23.basename(skill) === "SKILL.md" ? skill : path23.join(skill, "SKILL.md"), "skill");
3835
+ }
3836
+ for (const rule of objects.rules) await validateProse(rule, "rule");
3837
+ for (const tool of objects.tools) await validateYaml(tool, "tool");
3838
+ for (const connection of objects.connections) await validateYaml(connection, "connection");
3839
+ if (Object.values(objects).every((items) => items.length === 0)) {
3840
+ findings.push({ severity: "warning", message: "Pack does not include any objects." });
3841
+ }
3842
+ } catch (error) {
3843
+ findings.push({ severity: "error", message: error instanceof Error ? error.message : String(error) });
3844
+ }
3845
+ return { ok: !findings.some((finding3) => finding3.severity === "error"), findings };
3846
+ }
3847
+ async function copyObject(source, destDir) {
3848
+ if ((await lstat(source)).isSymbolicLink()) {
3849
+ throw new Error(`Refusing to install pack object symlink: ${source}`);
3850
+ }
3851
+ const info = await stat9(source);
3852
+ const name = baseName(source);
3853
+ const dest = info.isDirectory() ? path23.join(destDir, name) : path23.join(destDir, path23.basename(source));
3854
+ await mkdir10(destDir, { recursive: true });
3855
+ await cp2(source, dest, { recursive: true, force: true });
3856
+ return dest;
3857
+ }
3858
+ async function hashFile(filePath) {
3859
+ return createHash2("sha256").update(await readFile11(filePath)).digest("hex");
3860
+ }
3861
+ async function hashDirectory(root) {
3862
+ const hash = createHash2("sha256");
3863
+ hash.update("threadroot-pack-directory-v1\n");
3864
+ async function walk(dir) {
3865
+ const entries = (await readdir5(dir, { withFileTypes: true })).sort((a, b) => a.name.localeCompare(b.name));
3866
+ for (const entry of entries) {
3867
+ const full = path23.join(dir, entry.name);
3868
+ const rel = path23.relative(root, full).split(path23.sep).join("/");
3869
+ if (entry.isSymbolicLink()) {
3870
+ throw new Error(`Refusing to install pack object with symlink: ${rel}`);
3871
+ }
3872
+ if (entry.isDirectory()) {
3873
+ await walk(full);
3874
+ continue;
3875
+ }
3876
+ if (entry.isFile()) {
3877
+ hash.update(`file:${rel}
3878
+ `);
3879
+ hash.update(await readFile11(full));
3880
+ hash.update("\n");
3881
+ }
3882
+ }
3883
+ }
3884
+ await walk(root);
3885
+ return hash.digest("hex");
3886
+ }
3887
+ async function integrityFor(source) {
3888
+ if ((await lstat(source)).isSymbolicLink()) {
3889
+ throw new Error(`Refusing to hash pack object symlink: ${source}`);
3890
+ }
3891
+ const info = await stat9(source);
3892
+ const digest = info.isDirectory() ? await hashDirectory(source) : await hashFile(source);
3893
+ return `sha256:${digest}`;
3894
+ }
3895
+ function normalizeLockPath(value) {
3896
+ return value.split(path23.sep).join("/");
3897
+ }
3898
+ function inside(root, target) {
3899
+ const relative = path23.relative(path23.resolve(root), path23.resolve(target));
3900
+ if (relative === "" || relative.startsWith("..") || path23.isAbsolute(relative)) {
3901
+ return void 0;
3902
+ }
3903
+ return normalizeLockPath(relative);
3904
+ }
3905
+ function lockObjectPath(packDir, source) {
3906
+ return inside(packDir, source) ?? inside(path23.resolve(packDir, "..", ".."), source) ?? path23.basename(source);
3907
+ }
3908
+ async function lockEntryForPackObject(packDir, manifest, kind, source, installedAt) {
3909
+ return {
3910
+ name: baseName(source),
3911
+ kind,
3912
+ sourceKind: "local",
3913
+ source: `pack:${manifest.name}`,
3914
+ objectPath: lockObjectPath(packDir, source),
3915
+ integrity: await integrityFor(source),
3916
+ installedAt
3917
+ };
3918
+ }
3919
+ async function writePackLockEntries(repoRoot, packDir, manifest, objects) {
3920
+ const installedAt = (/* @__PURE__ */ new Date()).toISOString();
3921
+ const entries = await Promise.all([
3922
+ ...objects.skills.map((source) => lockEntryForPackObject(packDir, manifest, "skill", source, installedAt)),
3923
+ ...objects.tools.map((source) => lockEntryForPackObject(packDir, manifest, "tool", source, installedAt)),
3924
+ ...objects.rules.map((source) => lockEntryForPackObject(packDir, manifest, "rule", source, installedAt)),
3925
+ ...objects.connections.map(
3926
+ (source) => lockEntryForPackObject(packDir, manifest, "connection", source, installedAt)
3927
+ )
3928
+ ]);
3929
+ let lock = await readLockFile(projectLockPath(repoRoot));
3930
+ for (const entry of entries) {
3931
+ lock = upsertLockEntry(lock, entry);
3932
+ }
3933
+ await writeLockFile(projectLockPath(repoRoot), lock);
3934
+ }
3935
+ async function installPack(repoRoot, nameOrPath) {
3936
+ const validation = await validatePack(repoRoot, nameOrPath);
3937
+ if (!validation.ok) {
3938
+ throw new Error(validation.findings.map((finding3) => finding3.message).join("; "));
3939
+ }
3940
+ const packDir = await packDirFor(repoRoot, nameOrPath);
3941
+ const manifest = await readPackManifest(packDir);
3942
+ const objects = await collectObjects(packDir, manifest);
3943
+ await Promise.all([
3944
+ ...objects.skills.map((source) => copyObject(source, projectObjectDir(repoRoot, "skills"))),
3945
+ ...objects.tools.map((source) => copyObject(source, projectObjectDir(repoRoot, "tools"))),
3946
+ ...objects.rules.map((source) => copyObject(source, projectObjectDir(repoRoot, "rules"))),
3947
+ ...objects.connections.map((source) => copyObject(source, projectObjectDir(repoRoot, "connections")))
3948
+ ]);
3949
+ await writePackLockEntries(repoRoot, packDir, manifest, objects);
3950
+ return inspectPack(repoRoot, packDir);
3951
+ }
3952
+
3953
+ // src/core/status.ts
3954
+ async function harnessStatus(repoRoot, options = {}) {
3955
+ let harness;
3956
+ try {
3957
+ harness = await resolveHarness(repoRoot, { home: options.home });
3958
+ } catch (error) {
3959
+ if (error instanceof HarnessError) {
3960
+ return { exists: false };
3961
+ }
3962
+ throw error;
3963
+ }
3964
+ const files = await compile(repoRoot, harness);
3965
+ const drift = await detectDrift(repoRoot, files);
3966
+ return {
3967
+ exists: true,
3968
+ manifest: {
3969
+ name: harness.manifest.name,
3970
+ profile: harness.manifest.profile,
3971
+ adapters: harness.manifest.adapters,
3972
+ toolsAllow: harness.manifest.tools.allow
3973
+ },
3974
+ counts: {
3975
+ skills: harness.skills.length,
3976
+ rules: harness.rules.length,
3977
+ tools: harness.tools.length,
3978
+ memory: harness.memory.length
3979
+ },
3980
+ drift
3981
+ };
3982
+ }
3983
+
3984
+ // src/core/bootstrap.ts
3985
+ var DEFAULT_TASK = "start this project";
3986
+ async function harnessExists(repoRoot) {
3987
+ return pathExists2(path24.join(repoRoot, ".threadroot", "harness.yaml"));
3988
+ }
3989
+ async function pathExists2(target) {
3990
+ try {
3991
+ await stat10(target);
3992
+ return true;
3993
+ } catch (error) {
3994
+ if (error.code === "ENOENT") {
3995
+ return false;
3996
+ }
3997
+ throw error;
3998
+ }
3999
+ }
4000
+ function modeFor(options) {
4001
+ return options.yes && !options.dryRun ? "write" : "dry-run";
4002
+ }
4003
+ function parseListOption(value) {
4004
+ return (value ?? "").split(",").map((item) => item.trim()).filter(Boolean);
4005
+ }
4006
+ async function bootstrapProject(repoRoot, options = {}) {
4007
+ const task = options.task?.trim() || DEFAULT_TASK;
4008
+ const mode = modeFor(options);
4009
+ const write2 = mode === "write";
4010
+ const notes = [];
4011
+ const existed = await harnessExists(repoRoot);
4012
+ let setup;
4013
+ let init;
4014
+ let exposed;
4015
+ let packs;
4016
+ if (!options.noGlobal) {
4017
+ setup = await setupGlobal({
4018
+ agents: options.agents ?? "all",
4019
+ mode,
4020
+ home: options.home,
4021
+ mcp: options.mcp,
4022
+ mcpEntry: options.mcpEntry
4023
+ });
4024
+ } else {
4025
+ notes.push("Skipped global setup because --no-global was set.");
4026
+ }
4027
+ if (!existed && !options.noInit) {
4028
+ if (write2) {
4029
+ init = await initHarness(repoRoot, {
4030
+ import: options.import,
4031
+ profile: options.profile,
4032
+ home: options.home
4033
+ });
4034
+ } else {
4035
+ notes.push(`Would initialize local-only harness at ${path24.join(".threadroot", "harness.yaml")}.`);
4036
+ }
4037
+ } else if (existed) {
4038
+ notes.push("Existing harness detected; bootstrap will not reinitialize it.");
4039
+ } else {
4040
+ notes.push("Skipped project initialization because --no-init was set.");
4041
+ }
4042
+ const hasHarnessAfterInit = existed || Boolean(init) || await pathExists2(path24.join(repoRoot, ".threadroot", "harness.yaml"));
4043
+ if (options.expose) {
4044
+ exposed = await exposeProject(repoRoot, {
4045
+ agents: options.expose,
4046
+ mode
4047
+ });
4048
+ }
4049
+ const packNames = parseListOption(options.packs);
4050
+ if (packNames.length > 0) {
4051
+ if (!hasHarnessAfterInit) {
4052
+ notes.push("Skipped pack installation because no harness exists yet.");
4053
+ } else if (write2) {
4054
+ packs = [];
4055
+ for (const packName of packNames) {
4056
+ packs.push(await installPack(repoRoot, packName));
4057
+ }
4058
+ } else {
4059
+ notes.push(`Would install pack(s): ${packNames.join(", ")}.`);
4060
+ }
4061
+ }
4062
+ let status;
4063
+ let doctorReport;
4064
+ let context;
4065
+ let mcpCheck;
4066
+ if (options.mcp && write2) {
4067
+ mcpCheck = await checkCodexMcp({ repoRoot, home: options.home });
4068
+ }
4069
+ if (hasHarnessAfterInit) {
4070
+ status = await harnessStatus(repoRoot, { home: options.home });
4071
+ doctorReport = await doctor(repoRoot, { home: options.home });
4072
+ if (status.exists) {
4073
+ context = await assembleContext(repoRoot, task, { home: options.home, fallbackSkills: true });
4074
+ }
4075
+ } else {
4076
+ notes.push("Skipped doctor/status/context because no harness exists yet.");
4077
+ }
4078
+ if (!write2) {
4079
+ notes.push("Run `threadroot bootstrap --yes` to apply this plan.");
4080
+ }
4081
+ return {
4082
+ mode: write2 ? "write" : "plan",
4083
+ task,
4084
+ harnessExisted: existed,
4085
+ setup,
4086
+ init,
4087
+ expose: exposed,
4088
+ packs,
4089
+ status,
4090
+ doctor: doctorReport,
4091
+ context,
4092
+ mcpCheck,
4093
+ notes
4094
+ };
4095
+ }
4096
+ async function startSession(repoRoot, options = {}) {
4097
+ const task = options.task?.trim() || DEFAULT_TASK;
4098
+ const status = await harnessStatus(repoRoot, { home: options.home });
4099
+ const notes = [];
4100
+ if (!status.exists) {
4101
+ return {
4102
+ task,
4103
+ status,
4104
+ notes: ["No harness found. Run `threadroot bootstrap --yes` first."]
4105
+ };
4106
+ }
4107
+ const doctorReport = await doctor(repoRoot, { home: options.home });
4108
+ const context = await assembleContext(repoRoot, task, { home: options.home, fallbackSkills: true });
4109
+ return { task, status, doctor: doctorReport, context, notes };
4110
+ }
4111
+
4112
+ // src/commands/json.ts
4113
+ function printJson(value) {
4114
+ console.log(JSON.stringify(value, null, 2));
4115
+ }
4116
+
4117
+ // src/commands/session-output.ts
4118
+ function printDoctor(report) {
4119
+ if (!report) {
4120
+ return;
4121
+ }
4122
+ const actionable = report.findings.filter((finding3) => finding3.severity !== "info");
4123
+ console.log(actionable.length === 0 ? "doctor: clean" : `doctor: ${report.summary.errors} error(s), ${report.summary.warnings} warning(s)`);
4124
+ for (const finding3 of report.findings.slice(0, 8)) {
4125
+ const label = finding3.severity === "info" ? "hint" : finding3.severity;
4126
+ const suffix = finding3.path ? ` (${finding3.path})` : "";
4127
+ console.log(`- ${label} ${finding3.code}: ${finding3.message}${suffix}`);
4128
+ }
4129
+ if (report.findings.length > 8) {
4130
+ console.log(`- ... ${report.findings.length - 8} more finding(s)`);
4131
+ }
4132
+ }
4133
+ function printStatus(status) {
4134
+ if (!status) {
4135
+ return;
4136
+ }
4137
+ if (!status.exists) {
4138
+ console.log("harness: missing");
4139
+ return;
4140
+ }
4141
+ console.log(`harness: ${status.manifest.name} (${status.manifest.profile})`);
4142
+ console.log(`adapters: ${status.manifest.adapters.length > 0 ? status.manifest.adapters.join(", ") : "none (local-only)"}`);
4143
+ console.log(
4144
+ `objects: ${status.counts.skills} skills, ${status.counts.rules} rules, ${status.counts.tools} tools, ${status.counts.memory} memory`
4145
+ );
4146
+ }
4147
+ function printContext(context) {
4148
+ if (!context) {
4149
+ return;
4150
+ }
4151
+ console.log(`task: ${context.task}`);
4152
+ if (context.skills.length > 0) {
4153
+ const skillLabel = context.skills.some((skill) => skill.score > 0) ? "relevant skills:" : "starter skills:";
4154
+ console.log(skillLabel);
4155
+ for (const skill of context.skills.slice(0, 8)) {
4156
+ console.log(`- ${skill.name} - ${skill.when}`);
4157
+ }
4158
+ } else {
4159
+ console.log("relevant skills: none matched; run `threadroot skills list` to inspect all skills.");
4160
+ }
4161
+ if (context.tools.length > 0) {
4162
+ console.log("available tools:");
3720
4163
  for (const tool of context.tools.slice(0, 8)) {
3721
4164
  console.log(`- ${tool.name} (${tool.risk}) - ${tool.description}`);
3722
4165
  }
@@ -3767,6 +4210,9 @@ function printBootstrapReport(report) {
3767
4210
  console.log(`- ${entry.label}: ${entry.status} ${entry.path}${suffix}`);
3768
4211
  }
3769
4212
  }
4213
+ if (report.packs && report.packs.length > 0) {
4214
+ console.log(`packs: ${report.packs.map((pack) => pack.name).join(", ")}`);
4215
+ }
3770
4216
  printStatus(report.status);
3771
4217
  printMcpCheck(report.mcpCheck);
3772
4218
  printDoctor(report.doctor);
@@ -3799,13 +4245,18 @@ async function runBootstrap(repoRoot, options) {
3799
4245
  task: options.task,
3800
4246
  mcp: options.mcp,
3801
4247
  expose: options.expose,
4248
+ packs: options.packs,
3802
4249
  noGlobal: options.global === false,
3803
4250
  noInit: options.init === false,
3804
4251
  import: options.import,
3805
4252
  profile: options.profile ? profileIdSchema.parse(options.profile) : void 0,
3806
4253
  mcpEntry: options.mcp ? mcpEntryForCurrentProcess() : void 0
3807
4254
  });
3808
- printBootstrapReport(report);
4255
+ if (options.json) {
4256
+ printJson(report);
4257
+ } else {
4258
+ printBootstrapReport(report);
4259
+ }
3809
4260
  if (report.mode === "write" && report.doctor && !report.doctor.ok) {
3810
4261
  process.exitCode = 1;
3811
4262
  }
@@ -3832,17 +4283,25 @@ async function runCompileCommand(repoRoot, options) {
3832
4283
  }
3833
4284
 
3834
4285
  // src/commands/context.ts
3835
- async function runContext(repoRoot, task) {
4286
+ async function runContext(repoRoot, task, options = {}) {
3836
4287
  let context;
3837
4288
  try {
3838
4289
  context = await assembleContext(repoRoot, task);
3839
4290
  } catch (error) {
3840
4291
  if (error instanceof HarnessError) {
3841
- console.log("No harness found. Run `tr init` first.");
4292
+ if (options.json) {
4293
+ printJson({ ok: false, error: "harness_missing", message: "No harness found. Run `tr init` first." });
4294
+ } else {
4295
+ console.log("No harness found. Run `tr init` first.");
4296
+ }
3842
4297
  return;
3843
4298
  }
3844
4299
  throw error;
3845
4300
  }
4301
+ if (options.json) {
4302
+ printJson(context);
4303
+ return;
4304
+ }
3846
4305
  console.log(`task: ${context.task}`);
3847
4306
  if (context.skills.length > 0) {
3848
4307
  console.log("\nskills:");
@@ -3875,7 +4334,7 @@ async function runContext(repoRoot, task) {
3875
4334
 
3876
4335
  // src/commands/diff.ts
3877
4336
  import fs3 from "fs/promises";
3878
- import path24 from "path";
4337
+ import path25 from "path";
3879
4338
  async function readIfExists3(filePath) {
3880
4339
  try {
3881
4340
  return await fs3.readFile(filePath, "utf8");
@@ -3934,7 +4393,7 @@ async function runDiff(repoRoot) {
3934
4393
  const files = await compile(repoRoot, harness);
3935
4394
  let changed = 0;
3936
4395
  for (const file of files) {
3937
- const existing = await readIfExists3(path24.join(repoRoot, file.path));
4396
+ const existing = await readIfExists3(path25.join(repoRoot, file.path));
3938
4397
  if (existing === void 0) {
3939
4398
  changed += 1;
3940
4399
  console.log(`+ ${file.path} (new)`);
@@ -3955,8 +4414,15 @@ async function runDiff(repoRoot) {
3955
4414
  }
3956
4415
 
3957
4416
  // src/commands/doctor.ts
3958
- async function runDoctor(repoRoot) {
4417
+ async function runDoctor(repoRoot, options = {}) {
3959
4418
  const report = await doctor(repoRoot);
4419
+ if (options.json) {
4420
+ printJson(report);
4421
+ if (!report.ok) {
4422
+ process.exitCode = 1;
4423
+ }
4424
+ return;
4425
+ }
3960
4426
  const actionable = report.findings.filter((finding3) => finding3.severity !== "info");
3961
4427
  const hints = report.findings.filter((finding3) => finding3.severity === "info");
3962
4428
  if (actionable.length === 0) {
@@ -4049,13 +4515,13 @@ async function runInit(repoRoot, options) {
4049
4515
  }
4050
4516
 
4051
4517
  // src/commands/install.ts
4052
- import path27 from "path";
4518
+ import path28 from "path";
4053
4519
 
4054
4520
  // src/core/install/fetch.ts
4055
4521
  import { execFile } from "child_process";
4056
4522
  import { mkdtemp as mkdtemp2, rm as rm4 } from "fs/promises";
4057
4523
  import os2 from "os";
4058
- import path25 from "path";
4524
+ import path26 from "path";
4059
4525
  import { promisify } from "util";
4060
4526
  var run = promisify(execFile);
4061
4527
  function cloneUrl(ref) {
@@ -4076,7 +4542,7 @@ async function git(cwd, args) {
4076
4542
  }
4077
4543
  async function fetchGitSource(ref) {
4078
4544
  const url = cloneUrl(ref);
4079
- const dir = await mkdtemp2(path25.join(os2.tmpdir(), "threadroot-fetch-"));
4545
+ const dir = await mkdtemp2(path26.join(os2.tmpdir(), "threadroot-fetch-"));
4080
4546
  const cleanup = () => rm4(dir, { recursive: true, force: true });
4081
4547
  try {
4082
4548
  if (ref.ref) {
@@ -4098,9 +4564,9 @@ async function fetchGitSource(ref) {
4098
4564
  }
4099
4565
 
4100
4566
  // src/core/install/install.ts
4101
- import { cp as cp2, mkdir as mkdir10, readFile as readFile11, readdir as readdir5, stat as stat10, writeFile as writeFile10 } from "fs/promises";
4102
- import { createHash as createHash2 } from "crypto";
4103
- import path26 from "path";
4567
+ import { cp as cp3, mkdir as mkdir11, readFile as readFile12, readdir as readdir6, stat as stat11, writeFile as writeFile10 } from "fs/promises";
4568
+ import { createHash as createHash3 } from "crypto";
4569
+ import path27 from "path";
4104
4570
  var NAME_RE5 = /^[a-z0-9][a-z0-9-]*$/;
4105
4571
  var KIND_DIR = {
4106
4572
  skill: "skills",
@@ -4112,8 +4578,8 @@ function objectExt(kind) {
4112
4578
  return kind === "tool" || kind === "connection" ? ".yaml" : ".md";
4113
4579
  }
4114
4580
  function safeRepoPath(objectPath) {
4115
- const normalized = path26.normalize(objectPath);
4116
- if (path26.isAbsolute(normalized) || normalized === ".." || normalized.startsWith(`..${path26.sep}`)) {
4581
+ const normalized = path27.normalize(objectPath);
4582
+ if (path27.isAbsolute(normalized) || normalized === ".." || normalized.startsWith(`..${path27.sep}`)) {
4117
4583
  throw new Error(`Unsafe object path: ${objectPath}`);
4118
4584
  }
4119
4585
  return normalized;
@@ -4127,7 +4593,7 @@ function inferKind(objectPath, override) {
4127
4593
  if (segments.includes("tools")) return "tool";
4128
4594
  if (segments.includes("connections")) return "connection";
4129
4595
  if (segments.includes("rules")) return "rule";
4130
- const ext = path26.extname(objectPath).toLowerCase();
4596
+ const ext = path27.extname(objectPath).toLowerCase();
4131
4597
  if (ext === ".yaml" || ext === ".yml") return "tool";
4132
4598
  if (ext === ".md") return "skill";
4133
4599
  throw new Error(
@@ -4135,20 +4601,20 @@ function inferKind(objectPath, override) {
4135
4601
  );
4136
4602
  }
4137
4603
  function deriveName(objectPath) {
4138
- const base = path26.basename(objectPath, path26.extname(objectPath));
4604
+ const base = path27.basename(objectPath, path27.extname(objectPath));
4139
4605
  if (!NAME_RE5.test(base)) {
4140
4606
  throw new Error(`Invalid object name \`${base}\` (use lowercase letters, digits, and dashes).`);
4141
4607
  }
4142
4608
  return base;
4143
4609
  }
4144
- async function hashDirectory(root) {
4145
- const hash = createHash2("sha256");
4610
+ async function hashDirectory2(root) {
4611
+ const hash = createHash3("sha256");
4146
4612
  hash.update("threadroot-directory-v1\n");
4147
4613
  async function walk(dir) {
4148
- const entries = (await readdir5(dir, { withFileTypes: true })).sort((a, b) => a.name.localeCompare(b.name));
4614
+ const entries = (await readdir6(dir, { withFileTypes: true })).sort((a, b) => a.name.localeCompare(b.name));
4149
4615
  for (const entry of entries) {
4150
- const full = path26.join(dir, entry.name);
4151
- const rel = path26.relative(root, full).split(path26.sep).join("/");
4616
+ const full = path27.join(dir, entry.name);
4617
+ const rel = path27.relative(root, full).split(path27.sep).join("/");
4152
4618
  if (entry.isSymbolicLink()) {
4153
4619
  throw new Error(`Refusing to install skill directory with symlink: ${rel}`);
4154
4620
  }
@@ -4159,7 +4625,7 @@ async function hashDirectory(root) {
4159
4625
  if (entry.isFile()) {
4160
4626
  hash.update(`file:${rel}
4161
4627
  `);
4162
- hash.update(await readFile11(full));
4628
+ hash.update(await readFile12(full));
4163
4629
  hash.update("\n");
4164
4630
  }
4165
4631
  }
@@ -4168,8 +4634,8 @@ async function hashDirectory(root) {
4168
4634
  return hash.digest("hex");
4169
4635
  }
4170
4636
  async function validateSkillDirectory2(sourcePath, expectedName) {
4171
- const skillPath = path26.join(sourcePath, "SKILL.md");
4172
- const parsed = parseFrontmatter(await readFile11(skillPath, "utf8"));
4637
+ const skillPath = path27.join(sourcePath, "SKILL.md");
4638
+ const parsed = parseFrontmatter(await readFile12(skillPath, "utf8"));
4173
4639
  const result = skillFrontmatterSchema.safeParse(parsed.data);
4174
4640
  if (!result.success) {
4175
4641
  const detail = result.error.issues.map((issue) => issue.message).join("; ");
@@ -4178,7 +4644,7 @@ async function validateSkillDirectory2(sourcePath, expectedName) {
4178
4644
  if (result.data.name !== expectedName) {
4179
4645
  throw new Error(`Skill directory name \`${expectedName}\` must match SKILL.md name \`${result.data.name}\`.`);
4180
4646
  }
4181
- return await hashDirectory(sourcePath);
4647
+ return await hashDirectory2(sourcePath);
4182
4648
  }
4183
4649
  async function installObject(repoRoot, rawSource, options = {}) {
4184
4650
  const ref = parseSourceRef(rawSource);
@@ -4198,7 +4664,7 @@ async function installObject(repoRoot, rawSource, options = {}) {
4198
4664
  objectPath = safeRepoPath(within);
4199
4665
  refLabel = ref.ref;
4200
4666
  const fetched = await fetchGitSource(ref);
4201
- sourcePath = path26.join(fetched.dir, objectPath);
4667
+ sourcePath = path27.join(fetched.dir, objectPath);
4202
4668
  resolved = fetched.sha;
4203
4669
  cleanup = fetched.cleanup;
4204
4670
  } else {
@@ -4213,19 +4679,19 @@ async function installObject(repoRoot, rawSource, options = {}) {
4213
4679
  const destDir = scope === "user" ? userObjectDir(dirKey, options.home) : projectObjectDir(repoRoot, dirKey);
4214
4680
  let destPath;
4215
4681
  let integrity;
4216
- const info = await stat10(sourcePath);
4682
+ const info = await stat11(sourcePath);
4217
4683
  if (info.isDirectory()) {
4218
4684
  if (kind !== "skill") {
4219
4685
  throw new Error("Only skill objects may be installed from a directory.");
4220
4686
  }
4221
4687
  integrity = `sha256:${await validateSkillDirectory2(sourcePath, name)}`;
4222
- destPath = path26.join(destDir, name);
4223
- await mkdir10(destDir, { recursive: true });
4224
- await cp2(sourcePath, destPath, { recursive: true, force: true });
4688
+ destPath = path27.join(destDir, name);
4689
+ await mkdir11(destDir, { recursive: true });
4690
+ await cp3(sourcePath, destPath, { recursive: true, force: true });
4225
4691
  } else {
4226
- const content = await readFile11(sourcePath, "utf8");
4227
- destPath = path26.join(destDir, `${name}${objectExt(kind)}`);
4228
- await mkdir10(destDir, { recursive: true });
4692
+ const content = await readFile12(sourcePath, "utf8");
4693
+ destPath = path27.join(destDir, `${name}${objectExt(kind)}`);
4694
+ await mkdir11(destDir, { recursive: true });
4229
4695
  await writeFile10(destPath, content, "utf8");
4230
4696
  integrity = `sha256:${hashContent(content)}`;
4231
4697
  }
@@ -4278,7 +4744,7 @@ async function runInstall(repoRoot, source, options) {
4278
4744
  if (installed.kind === "skill" && installed.entry.sourceKind !== "local") {
4279
4745
  console.log(" note: inspect external skills before trusting bundled scripts, assets, or allowed tools.");
4280
4746
  if (scope === "project") {
4281
- console.log(` inspect: threadroot skills inspect ${path27.relative(repoRoot, installed.path)}`);
4747
+ console.log(` inspect: threadroot skills inspect ${path28.relative(repoRoot, installed.path)}`);
4282
4748
  }
4283
4749
  }
4284
4750
  } catch (error) {
@@ -4290,7 +4756,7 @@ async function runInstall(repoRoot, source, options) {
4290
4756
  // src/mcp/server.ts
4291
4757
  import readline from "readline";
4292
4758
  import { stdin as input, stdout as output } from "process";
4293
- import { z as z4 } from "zod";
4759
+ import { z as z5 } from "zod";
4294
4760
  function defineTool(spec) {
4295
4761
  return spec;
4296
4762
  }
@@ -4302,7 +4768,7 @@ var toolRegistry = [
4302
4768
  { task: { type: "string", description: "The coding task to assemble context for." } },
4303
4769
  ["task"]
4304
4770
  ),
4305
- args: z4.object({ task: z4.string().min(1) }),
4771
+ args: z5.object({ task: z5.string().min(1) }),
4306
4772
  run: async (repoRoot, args) => {
4307
4773
  const harness = await loadHarnessOrNull(repoRoot);
4308
4774
  if (!harness) {
@@ -4315,7 +4781,7 @@ var toolRegistry = [
4315
4781
  name: "skills_list",
4316
4782
  description: "List the skills defined in this repo's harness (name, when, tags).",
4317
4783
  inputSchema: objectSchema({}),
4318
- args: z4.object({}),
4784
+ args: z5.object({}),
4319
4785
  run: async (repoRoot) => {
4320
4786
  const harness = await loadHarnessOrNull(repoRoot);
4321
4787
  if (!harness) {
@@ -4335,7 +4801,7 @@ var toolRegistry = [
4335
4801
  name: "skills_get",
4336
4802
  description: "Return a harness skill's full body and metadata by name.",
4337
4803
  inputSchema: objectSchema({ name: { type: "string", description: "Skill name." } }, ["name"]),
4338
- args: z4.object({ name: z4.string().min(1) }),
4804
+ args: z5.object({ name: z5.string().min(1) }),
4339
4805
  run: async (repoRoot, args) => {
4340
4806
  const harness = await loadHarnessOrNull(repoRoot);
4341
4807
  const skill = harness?.skills.find((entry) => entry.name === args.name);
@@ -4347,9 +4813,9 @@ var toolRegistry = [
4347
4813
  }),
4348
4814
  defineTool({
4349
4815
  name: "tools_list",
4350
- description: "List the executable tools defined in this repo's harness (name, inputs, confirm).",
4816
+ description: "List the executable tools defined in this repo's harness (name, inputs, risk, connection, confirm).",
4351
4817
  inputSchema: objectSchema({}),
4352
- args: z4.object({}),
4818
+ args: z5.object({}),
4353
4819
  run: async (repoRoot) => {
4354
4820
  const harness = await loadHarnessOrNull(repoRoot);
4355
4821
  if (!harness) {
@@ -4360,7 +4826,10 @@ var toolRegistry = [
4360
4826
  name: tool.name,
4361
4827
  description: tool.manifest.description,
4362
4828
  scope: tool.manifest.scope,
4829
+ risk: tool.manifest.risk,
4363
4830
  confirm: tool.manifest.confirm,
4831
+ connection: tool.manifest.connection,
4832
+ healthcheck: Boolean(tool.manifest.healthcheck),
4364
4833
  kind: tool.manifest.run ? "shell" : "script",
4365
4834
  input: tool.manifest.input
4366
4835
  }))
@@ -4371,7 +4840,7 @@ var toolRegistry = [
4371
4840
  name: "tools_check",
4372
4841
  description: "Run configured harness tool healthchecks without running primary tool actions.",
4373
4842
  inputSchema: objectSchema({}),
4374
- args: z4.object({}),
4843
+ args: z5.object({}),
4375
4844
  run: async (repoRoot) => {
4376
4845
  const harness = await loadHarnessOrNull(repoRoot);
4377
4846
  if (!harness) {
@@ -4382,28 +4851,30 @@ var toolRegistry = [
4382
4851
  }),
4383
4852
  defineTool({
4384
4853
  name: "tools_run",
4385
- description: "Execute a harness tool locally. Tools marked confirm:true require `confirm: true` after user approval.",
4854
+ description: "Execute a safe harness tool locally. MCP cannot self-confirm risky tools; use `threadroot run <tool> --yes` after human review.",
4386
4855
  inputSchema: objectSchema(
4387
4856
  {
4388
4857
  name: { type: "string", description: "Tool name." },
4389
- input: { type: "object", description: "Tool inputs as key/value pairs.", additionalProperties: true },
4390
- confirm: { type: "boolean", description: "Confirm running a tool that requires confirmation." }
4858
+ input: { type: "object", description: "Tool inputs as key/value pairs.", additionalProperties: true }
4391
4859
  },
4392
4860
  ["name"]
4393
4861
  ),
4394
- args: z4.object({
4395
- name: z4.string().min(1),
4396
- input: z4.record(z4.unknown()).optional(),
4397
- confirm: z4.boolean().optional()
4862
+ args: z5.object({
4863
+ name: z5.string().min(1),
4864
+ input: z5.record(z5.unknown()).optional()
4398
4865
  }),
4399
4866
  run: async (repoRoot, args) => {
4400
4867
  const outcome = await runTool(repoRoot, {
4401
4868
  name: args.name,
4402
4869
  input: args.input,
4403
- confirmed: args.confirm
4870
+ confirmed: false
4404
4871
  });
4405
4872
  if (outcome.status === "blocked") {
4406
- return { ok: false, blocked: outcome.reason, message: outcome.message };
4873
+ return {
4874
+ ok: false,
4875
+ blocked: outcome.reason,
4876
+ message: outcome.reason === "needs-confirmation" ? `${outcome.message} Ask the user to run \`threadroot run ${args.name} --yes\` after review.` : outcome.message
4877
+ };
4407
4878
  }
4408
4879
  const { result } = outcome;
4409
4880
  return {
@@ -4426,6 +4897,9 @@ var toolRegistry = [
4426
4897
  description: { type: "string", description: "What the tool does." },
4427
4898
  run: { type: "string", description: "Shell command (use {{param}} for inputs)." },
4428
4899
  script: { type: "string", description: "Harness-relative script path (alternative to run)." },
4900
+ risk: { type: "string", enum: ["low", "medium", "high"], description: "Risk level." },
4901
+ connection: { type: "string", description: "Optional connection dependency." },
4902
+ healthcheck: { type: "string", description: "Command that verifies this tool is available." },
4429
4903
  confirm: { type: "boolean", description: "Ask before running. Defaults to true for agents." },
4430
4904
  scope: { type: "string", enum: ["user", "project"], description: "Tool scope." },
4431
4905
  input: {
@@ -4436,14 +4910,17 @@ var toolRegistry = [
4436
4910
  },
4437
4911
  ["name", "description"]
4438
4912
  ),
4439
- args: z4.object({
4440
- name: z4.string().min(1),
4441
- description: z4.string().min(1),
4442
- run: z4.string().optional(),
4443
- script: z4.string().optional(),
4444
- confirm: z4.boolean().optional(),
4445
- scope: z4.enum(["user", "project"]).optional(),
4446
- input: z4.record(z4.unknown()).optional()
4913
+ args: z5.object({
4914
+ name: z5.string().min(1),
4915
+ description: z5.string().min(1),
4916
+ run: z5.string().optional(),
4917
+ script: z5.string().optional(),
4918
+ risk: z5.enum(["low", "medium", "high"]).optional(),
4919
+ connection: z5.string().optional(),
4920
+ healthcheck: z5.string().optional(),
4921
+ confirm: z5.boolean().optional(),
4922
+ scope: z5.enum(["user", "project"]).optional(),
4923
+ input: z5.record(z5.unknown()).optional()
4447
4924
  }),
4448
4925
  run: async (repoRoot, args) => {
4449
4926
  const created = await createTool(
@@ -4453,6 +4930,9 @@ var toolRegistry = [
4453
4930
  description: args.description,
4454
4931
  run: args.run,
4455
4932
  script: args.script,
4933
+ risk: args.risk,
4934
+ connection: args.connection,
4935
+ healthcheck: args.healthcheck ? { run: args.healthcheck, expectExitCode: 0 } : void 0,
4456
4936
  confirm: args.confirm,
4457
4937
  scope: args.scope,
4458
4938
  input: args.input
@@ -4466,7 +4946,7 @@ var toolRegistry = [
4466
4946
  name: "tools_detect",
4467
4947
  description: "Propose starter tools from the repo's existing command surface (scripts, Make/just targets).",
4468
4948
  inputSchema: objectSchema({}),
4469
- args: z4.object({}),
4949
+ args: z5.object({}),
4470
4950
  run: async (repoRoot) => {
4471
4951
  const harness = await loadHarnessOrNull(repoRoot);
4472
4952
  const profile = harness?.manifest.profile ?? "empty";
@@ -4477,7 +4957,7 @@ var toolRegistry = [
4477
4957
  name: "connections_list",
4478
4958
  description: "List local CLI connections defined in this repo's harness.",
4479
4959
  inputSchema: objectSchema({}),
4480
- args: z4.object({}),
4960
+ args: z5.object({}),
4481
4961
  run: async (repoRoot) => {
4482
4962
  const harness = await loadHarnessOrNull(repoRoot);
4483
4963
  if (!harness) {
@@ -4500,7 +4980,7 @@ var toolRegistry = [
4500
4980
  name: "connections_check",
4501
4981
  description: "Check local CLI connections and their configured healthchecks.",
4502
4982
  inputSchema: objectSchema({}),
4503
- args: z4.object({}),
4983
+ args: z5.object({}),
4504
4984
  run: (repoRoot) => checkConnections(repoRoot)
4505
4985
  }),
4506
4986
  defineTool({
@@ -4509,7 +4989,7 @@ var toolRegistry = [
4509
4989
  inputSchema: objectSchema({
4510
4990
  type: { type: "string", description: "Memory type (project, repo-map, current-focus, handoff, pitfalls)." }
4511
4991
  }),
4512
- args: z4.object({ type: z4.string().optional() }),
4992
+ args: z5.object({ type: z5.string().optional() }),
4513
4993
  run: async (repoRoot, args) => {
4514
4994
  if (args.type) {
4515
4995
  return { type: args.type, body: await readMemory(repoRoot, args.type) };
@@ -4528,21 +5008,21 @@ var toolRegistry = [
4528
5008
  },
4529
5009
  ["type", "note"]
4530
5010
  ),
4531
- args: z4.object({ type: z4.string().min(1), note: z4.string().min(1) }),
5011
+ args: z5.object({ type: z5.string().min(1), note: z5.string().min(1) }),
4532
5012
  run: (repoRoot, args) => appendMemory(repoRoot, args.type, args.note)
4533
5013
  }),
4534
5014
  defineTool({
4535
5015
  name: "status",
4536
5016
  description: "Return harness state: manifest, object counts, and drift between canonical and compiled outputs.",
4537
5017
  inputSchema: objectSchema({}),
4538
- args: z4.object({}),
5018
+ args: z5.object({}),
4539
5019
  run: (repoRoot) => harnessStatus(repoRoot)
4540
5020
  }),
4541
5021
  defineTool({
4542
5022
  name: "doctor",
4543
5023
  description: "Check harness validity, compiled output health, MCP hints, and tool trust.",
4544
5024
  inputSchema: objectSchema({}),
4545
- args: z4.object({}),
5025
+ args: z5.object({}),
4546
5026
  run: (repoRoot) => doctor(repoRoot)
4547
5027
  })
4548
5028
  ];
@@ -4569,7 +5049,7 @@ async function handleMessage(repoRoot, request) {
4569
5049
  if (request.method === "initialize") {
4570
5050
  return resultResponse(request, {
4571
5051
  protocolVersion: "2024-11-05",
4572
- serverInfo: { name: "threadroot", version: "0.1.3" },
5052
+ serverInfo: { name: "threadroot", version: THREADROOT_VERSION },
4573
5053
  capabilities: { tools: {} },
4574
5054
  instructions: "Threadroot exposes the repository's AI agent harness. Call `context` before broad coding work, `doctor` for health and trust checks, inspect skills/tools before risky actions, and use `memory_append` for durable handoffs."
4575
5055
  });
@@ -4584,7 +5064,8 @@ async function handleMessage(repoRoot, request) {
4584
5064
  const params = request.params;
4585
5065
  const result = await callTool(repoRoot, params?.name, params?.arguments ?? {});
4586
5066
  return resultResponse(request, {
4587
- content: [{ type: "text", text: typeof result === "string" ? result : JSON.stringify(result, null, 2) }]
5067
+ content: [{ type: "text", text: typeof result === "string" ? result : JSON.stringify(result, null, 2) }],
5068
+ structuredContent: result
4588
5069
  });
4589
5070
  }
4590
5071
  return errorResponse(request, -32601, `Unknown method: ${request.method ?? "<missing>"}`);
@@ -4723,10 +5204,11 @@ Rules:
4723
5204
  Steps:
4724
5205
  1. Check whether Threadroot is available with \`threadroot --version\`.
4725
5206
  2. If it is not available, try \`npm exec threadroot -- --help\` or \`pnpm dlx threadroot --help\`. If this is a local checkout, use \`${localCommand} --help\`.
4726
- 3. Run \`threadroot bootstrap --yes --agent all --task "current task"\`. If this is a local checkout, run \`${localCommand} bootstrap --yes --agent all --task "current task"\`.
4727
- 4. Run \`threadroot start "current task"\` with the user's actual task.
4728
- 5. If the user asks for provider-native project skill files, run \`threadroot expose <agent>\` or \`threadroot expose all\`.
4729
- 6. If project-local MCP config is useful, ask before running \`threadroot mcp setup --write\`, then tell the user to reload their agent surface.
5207
+ 3. Run \`threadroot bootstrap --yes --agent all --mcp --task "current task"\`. If this is a local checkout, run \`${localCommand} bootstrap --yes --agent all --mcp --task "current task"\`.
5208
+ 4. If the user named a stack or workflow, add curated packs with \`--packs <comma-separated-pack-list>\` when a matching built-in pack exists.
5209
+ 5. Run \`threadroot start "current task"\` with the user's actual task.
5210
+ 6. If the user asks for provider-native project skill files, run \`threadroot expose <agent>\` or \`threadroot expose all\`.
5211
+ 7. If project-local MCP config is useful, ask before running \`threadroot mcp setup --write\`, then tell the user to reload their agent surface.
4730
5212
 
4731
5213
  Final response:
4732
5214
  Say exactly:
@@ -4793,11 +5275,11 @@ function agentNotes(agent) {
4793
5275
  }
4794
5276
 
4795
5277
  // src/core/mcp-config.ts
4796
- import { mkdir as mkdir11, readFile as readFile12, writeFile as writeFile11 } from "fs/promises";
4797
- import path28 from "path";
5278
+ import { mkdir as mkdir12, readFile as readFile13, writeFile as writeFile11 } from "fs/promises";
5279
+ import path29 from "path";
4798
5280
  var TARGETS = [
4799
- { agent: "copilot", file: path28.join(".vscode", "mcp.json"), key: "servers" },
4800
- { agent: "cursor", file: path28.join(".cursor", "mcp.json"), key: "mcpServers" },
5281
+ { agent: "copilot", file: path29.join(".vscode", "mcp.json"), key: "servers" },
5282
+ { agent: "cursor", file: path29.join(".cursor", "mcp.json"), key: "mcpServers" },
4801
5283
  { agent: "claude", file: ".mcp.json", key: "mcpServers" }
4802
5284
  ];
4803
5285
  function mcpServerEntry(command, scriptPath) {
@@ -4806,7 +5288,7 @@ function mcpServerEntry(command, scriptPath) {
4806
5288
  async function mergeConfig(filePath, key, entry) {
4807
5289
  let config = {};
4808
5290
  try {
4809
- const raw = await readFile12(filePath, "utf8");
5291
+ const raw = await readFile13(filePath, "utf8");
4810
5292
  const parsed = JSON.parse(raw);
4811
5293
  if (parsed && typeof parsed === "object") {
4812
5294
  config = parsed;
@@ -4819,7 +5301,7 @@ async function mergeConfig(filePath, key, entry) {
4819
5301
  const servers = config[key] && typeof config[key] === "object" ? config[key] : {};
4820
5302
  servers.threadroot = { ...entry };
4821
5303
  config[key] = servers;
4822
- await mkdir11(path28.dirname(filePath), { recursive: true });
5304
+ await mkdir12(path29.dirname(filePath), { recursive: true });
4823
5305
  await writeFile11(filePath, `${JSON.stringify(config, null, 2)}
4824
5306
  `, "utf8");
4825
5307
  }
@@ -4828,7 +5310,7 @@ async function writeProjectMcpConfigs(input2) {
4828
5310
  const targets = agents ? TARGETS.filter((target) => agents.includes(target.agent)) : TARGETS;
4829
5311
  const written = [];
4830
5312
  for (const target of targets) {
4831
- const filePath = path28.join(input2.repoRoot, target.file);
5313
+ const filePath = path29.join(input2.repoRoot, target.file);
4832
5314
  await mergeConfig(filePath, target.key, input2.entry);
4833
5315
  written.push(target.file);
4834
5316
  }
@@ -4851,6 +5333,10 @@ async function runMcpSetup(repoRoot, options) {
4851
5333
  const scriptPath = process.argv[1];
4852
5334
  const entry = mcpServerEntry(command, scriptPath);
4853
5335
  const result = await writeProjectMcpConfigs({ repoRoot, entry });
5336
+ if (options.json) {
5337
+ printJson(result);
5338
+ return;
5339
+ }
4854
5340
  console.log("Wrote project MCP config:");
4855
5341
  for (const file of result.written) {
4856
5342
  console.log(`- ${file}`);
@@ -4860,11 +5346,23 @@ async function runMcpSetup(repoRoot, options) {
4860
5346
  }
4861
5347
  return;
4862
5348
  }
4863
- console.log(mcpSetupGuide({ repoRoot, agent: options.agent }));
5349
+ const guide = mcpSetupGuide({ repoRoot, agent: options.agent });
5350
+ if (options.json) {
5351
+ printJson({ guide });
5352
+ } else {
5353
+ console.log(guide);
5354
+ }
4864
5355
  }
4865
5356
  async function runMcpCheck(repoRoot, options) {
4866
5357
  const timeoutMs = options.timeout ? Number.parseInt(options.timeout, 10) : void 0;
4867
5358
  const report = await checkCodexMcp({ repoRoot, timeoutMs });
5359
+ if (options.json) {
5360
+ printJson(report);
5361
+ if (report.status === "error") {
5362
+ process.exitCode = 1;
5363
+ }
5364
+ return;
5365
+ }
4868
5366
  console.log(`Threadroot MCP check: ${report.status}`);
4869
5367
  console.log(`config: ${report.configPath}`);
4870
5368
  if (report.entry) {
@@ -4899,9 +5397,21 @@ async function runRemember(repoRoot, note, options = {}) {
4899
5397
  }
4900
5398
 
4901
5399
  // src/commands/skills.ts
4902
- async function runSkillsList(repoRoot) {
5400
+ async function runSkillsList(repoRoot, options = {}) {
4903
5401
  try {
4904
5402
  const harness = await resolveHarness(repoRoot);
5403
+ const skills = harness.skills.map((skill) => ({
5404
+ name: skill.name,
5405
+ origin: skill.origin,
5406
+ description: skill.frontmatter.description,
5407
+ when: skill.frontmatter.when,
5408
+ tags: skill.frontmatter.tags,
5409
+ sourcePath: skill.sourcePath
5410
+ }));
5411
+ if (options.json) {
5412
+ printJson({ skills });
5413
+ return;
5414
+ }
4905
5415
  if (harness.skills.length === 0) {
4906
5416
  console.log("No skills defined. Add folder skills under `.threadroot/skills/<name>/SKILL.md`.");
4907
5417
  return;
@@ -4911,7 +5421,11 @@ async function runSkillsList(repoRoot) {
4911
5421
  }
4912
5422
  } catch (error) {
4913
5423
  if (error instanceof HarnessError) {
4914
- console.log("No harness found. Run `tr init` first.");
5424
+ if (options.json) {
5425
+ printJson({ skills: [], ok: false, error: "harness_missing", message: "No harness found. Run `tr init` first." });
5426
+ } else {
5427
+ console.log("No harness found. Run `tr init` first.");
5428
+ }
4915
5429
  return;
4916
5430
  }
4917
5431
  throw error;
@@ -4919,6 +5433,13 @@ async function runSkillsList(repoRoot) {
4919
5433
  }
4920
5434
  async function runSkillsValidate(repoRoot, options = {}) {
4921
5435
  const report = options.path ? await validateSkillPath(toRepoPath(repoRoot, options.path)) : await validateSkills(repoRoot);
5436
+ if (options.json) {
5437
+ printJson(report);
5438
+ if (!report.ok) {
5439
+ process.exitCode = 1;
5440
+ }
5441
+ return;
5442
+ }
4922
5443
  if (report.findings.length === 0) {
4923
5444
  console.log("Skills valid.");
4924
5445
  return;
@@ -4933,8 +5454,12 @@ async function runSkillsValidate(repoRoot, options = {}) {
4933
5454
  process.exitCode = 1;
4934
5455
  }
4935
5456
  }
4936
- async function runSkillsInspect(repoRoot, targetPath) {
5457
+ async function runSkillsInspect(repoRoot, targetPath, options = {}) {
4937
5458
  const inspection = await inspectSkillPath(toRepoPath(repoRoot, targetPath));
5459
+ if (options.json) {
5460
+ printJson(inspection);
5461
+ return;
5462
+ }
4938
5463
  console.log(`${inspection.name}`);
4939
5464
  console.log(`description: ${inspection.description}`);
4940
5465
  console.log(`path: ${inspection.path}`);
@@ -4996,15 +5521,23 @@ async function runSetup(_repoRoot, options) {
4996
5521
  // src/commands/start.ts
4997
5522
  async function runStart(repoRoot, task, options) {
4998
5523
  const report = await startSession(repoRoot, { task: task ?? options.task });
4999
- printStartReport(report);
5524
+ if (options.json) {
5525
+ printJson(report);
5526
+ } else {
5527
+ printStartReport(report);
5528
+ }
5000
5529
  if (!report.status.exists || report.doctor && !report.doctor.ok) {
5001
5530
  process.exitCode = 1;
5002
5531
  }
5003
5532
  }
5004
5533
 
5005
5534
  // src/commands/status.ts
5006
- async function runStatus(repoRoot) {
5535
+ async function runStatus(repoRoot, options = {}) {
5007
5536
  const status = await harnessStatus(repoRoot);
5537
+ if (options.json) {
5538
+ printJson(status);
5539
+ return;
5540
+ }
5008
5541
  if (!status.exists) {
5009
5542
  console.log("No harness found. Run `tr init` first.");
5010
5543
  return;
@@ -5025,261 +5558,16 @@ async function runStatus(repoRoot) {
5025
5558
  }
5026
5559
  }
5027
5560
 
5028
- // src/core/packs/index.ts
5029
- import { cp as cp3, mkdir as mkdir12, readFile as readFile13, readdir as readdir6, stat as stat11 } from "fs/promises";
5030
- import path29 from "path";
5031
- import { fileURLToPath as fileURLToPath2 } from "url";
5032
- import { parse as parseYaml3 } from "yaml";
5033
- import { z as z5 } from "zod";
5034
- var packManifestSchema = z5.object({
5035
- name: z5.string().min(1),
5036
- version: z5.literal(1),
5037
- description: z5.string().min(1),
5038
- skills: z5.array(z5.string()).default([]),
5039
- tools: z5.array(z5.string()).default([]),
5040
- rules: z5.array(z5.string()).default([]),
5041
- connections: z5.array(z5.string()).default([])
5042
- });
5043
- var DIST_DIR2 = path29.dirname(fileURLToPath2(import.meta.url));
5044
- var PACKAGE_ROOT_FROM_BUNDLE2 = path29.resolve(DIST_DIR2, "..");
5045
- var PACKAGE_ROOT_FROM_DIST2 = path29.resolve(DIST_DIR2, "../../..");
5046
- var PACKAGE_ROOT_FROM_SRC2 = path29.resolve(DIST_DIR2, "../../../..");
5047
- var PACK_CANDIDATES = [
5048
- path29.join(PACKAGE_ROOT_FROM_BUNDLE2, "packs"),
5049
- path29.join(PACKAGE_ROOT_FROM_DIST2, "packs"),
5050
- path29.join(PACKAGE_ROOT_FROM_SRC2, "packs")
5051
- ];
5052
- async function exists5(target) {
5053
- try {
5054
- await stat11(target);
5055
- return true;
5056
- } catch (error) {
5057
- if (error.code === "ENOENT") {
5058
- return false;
5059
- }
5060
- throw error;
5061
- }
5062
- }
5063
- async function firstExisting(candidates) {
5064
- for (const candidate of candidates) {
5065
- if (await isPackRoot(candidate)) {
5066
- return candidate;
5067
- }
5068
- }
5069
- return void 0;
5070
- }
5071
- async function bundledPacksDir() {
5072
- return firstExisting(PACK_CANDIDATES);
5073
- }
5074
- async function isPackRoot(candidate) {
5075
- let entries;
5076
- try {
5077
- entries = await readdir6(candidate, { withFileTypes: true });
5078
- } catch {
5079
- return false;
5080
- }
5081
- for (const entry of entries) {
5082
- if (entry.isDirectory() && await exists5(path29.join(candidate, entry.name, "pack.yaml"))) {
5083
- return true;
5084
- }
5085
- }
5086
- return false;
5087
- }
5088
- function safeRelative(ref) {
5089
- const normalized = path29.normalize(ref);
5090
- if (path29.isAbsolute(normalized) || normalized === ".." || normalized.startsWith(`..${path29.sep}`)) {
5091
- throw new Error(`Unsafe pack reference: ${ref}`);
5092
- }
5093
- return normalized;
5094
- }
5095
- async function readPackManifest(packDir) {
5096
- const file = path29.join(packDir, "pack.yaml");
5097
- const parsed = packManifestSchema.safeParse(parseYaml3(await readFile13(file, "utf8")));
5098
- if (!parsed.success) {
5099
- const detail = parsed.error.issues.map((issue) => issue.message).join("; ");
5100
- throw new Error(`Invalid pack manifest ${file}: ${detail}`);
5101
- }
5102
- return parsed.data;
5103
- }
5104
- async function packDirFor(repoRoot, nameOrPath) {
5105
- if (path29.isAbsolute(nameOrPath)) {
5106
- return nameOrPath;
5107
- }
5108
- if (nameOrPath.startsWith(".") || nameOrPath.includes("/") || nameOrPath.includes("\\")) {
5109
- return toRepoPath(repoRoot, nameOrPath);
5110
- }
5111
- const bundled = await bundledPacksDir();
5112
- if (bundled) {
5113
- return path29.join(bundled, nameOrPath);
5114
- }
5115
- return toRepoPath(repoRoot, path29.join("packs", nameOrPath));
5116
- }
5117
- async function directFiles(dir, ext) {
5118
- try {
5119
- const entries = await readdir6(dir, { withFileTypes: true });
5120
- return entries.filter((entry) => entry.isFile() && entry.name.endsWith(ext)).map((entry) => path29.join(dir, entry.name)).sort();
5121
- } catch (error) {
5122
- if (error.code === "ENOENT") {
5123
- return [];
5124
- }
5125
- throw error;
5126
- }
5127
- }
5128
- async function skillEntries(dir) {
5129
- try {
5130
- const entries = await readdir6(dir, { withFileTypes: true });
5131
- const result = [];
5132
- for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
5133
- const full = path29.join(dir, entry.name);
5134
- if (entry.isFile() && entry.name.endsWith(".md")) {
5135
- result.push(full);
5136
- }
5137
- if (entry.isDirectory() && await exists5(path29.join(full, "SKILL.md"))) {
5138
- result.push(full);
5139
- }
5140
- }
5141
- return result;
5142
- } catch (error) {
5143
- if (error.code === "ENOENT") {
5144
- return [];
5145
- }
5146
- throw error;
5147
- }
5148
- }
5149
- async function collectObjects(packDir, manifest) {
5150
- async function resolveRef(ref) {
5151
- const safe = safeRelative(ref);
5152
- const local = path29.resolve(packDir, safe);
5153
- if (await exists5(local)) {
5154
- return local;
5155
- }
5156
- return path29.resolve(packDir, "..", "..", safe);
5157
- }
5158
- return {
5159
- skills: [
5160
- ...await Promise.all(manifest.skills.map(resolveRef)),
5161
- ...await skillEntries(path29.join(packDir, "skills"))
5162
- ],
5163
- tools: [
5164
- ...await Promise.all(manifest.tools.map(resolveRef)),
5165
- ...await directFiles(path29.join(packDir, "tools"), ".yaml")
5166
- ],
5167
- rules: [
5168
- ...await Promise.all(manifest.rules.map(resolveRef)),
5169
- ...await directFiles(path29.join(packDir, "rules"), ".md")
5170
- ],
5171
- connections: [
5172
- ...await Promise.all(manifest.connections.map(resolveRef)),
5173
- ...await directFiles(path29.join(packDir, "connections"), ".yaml")
5174
- ]
5175
- };
5176
- }
5177
- function baseName(source) {
5178
- const parsed = path29.basename(source) === "SKILL.md" ? path29.dirname(source) : source;
5179
- return path29.basename(parsed, path29.extname(parsed));
5180
- }
5181
- async function listPacks(repoRoot) {
5182
- const dirs = [toRepoPath(repoRoot, "packs"), await bundledPacksDir()].filter((dir) => Boolean(dir));
5183
- const seen = /* @__PURE__ */ new Set();
5184
- const packs = [];
5185
- for (const root of dirs) {
5186
- let entries;
5187
- try {
5188
- entries = await readdir6(root, { withFileTypes: true });
5189
- } catch {
5190
- continue;
5191
- }
5192
- for (const entry of entries) {
5193
- if (!entry.isDirectory() || seen.has(entry.name)) {
5194
- continue;
5195
- }
5196
- const packDir = path29.join(root, entry.name);
5197
- if (!await exists5(path29.join(packDir, "pack.yaml"))) {
5198
- continue;
5199
- }
5200
- seen.add(entry.name);
5201
- packs.push(await inspectPack(repoRoot, packDir));
5202
- }
5203
- }
5204
- return packs.sort((a, b) => a.name.localeCompare(b.name));
5205
- }
5206
- async function inspectPack(repoRoot, nameOrPath) {
5207
- const packDir = await packDirFor(repoRoot, nameOrPath);
5208
- const manifest = await readPackManifest(packDir);
5209
- const objects = await collectObjects(packDir, manifest);
5210
- return {
5211
- name: manifest.name,
5212
- description: manifest.description,
5213
- path: packDir,
5214
- skills: objects.skills.map(baseName),
5215
- tools: objects.tools.map(baseName),
5216
- rules: objects.rules.map(baseName),
5217
- connections: objects.connections.map(baseName)
5218
- };
5219
- }
5220
- async function validateProse(file, kind) {
5221
- const target = path29.basename(file) === "SKILL.md" ? file : file;
5222
- const content = await readFile13(target, "utf8");
5223
- const parsed = parseFrontmatter(content);
5224
- const schema = kind === "skill" ? skillFrontmatterSchema : ruleFrontmatterSchema;
5225
- schema.parse(parsed.data);
5226
- }
5227
- async function validateYaml(file, kind) {
5228
- const content = await readFile13(file, "utf8");
5229
- const schema = kind === "tool" ? toolManifestSchema : connectionManifestSchema;
5230
- schema.parse(parseYaml3(content));
5231
- }
5232
- async function validatePack(repoRoot, nameOrPath) {
5233
- const findings = [];
5234
- try {
5235
- const packDir = await packDirFor(repoRoot, nameOrPath);
5236
- const manifest = await readPackManifest(packDir);
5237
- const objects = await collectObjects(packDir, manifest);
5238
- for (const skill of objects.skills) {
5239
- await validateProse(path29.basename(skill) === "SKILL.md" ? skill : path29.join(skill, "SKILL.md"), "skill");
5240
- }
5241
- for (const rule of objects.rules) await validateProse(rule, "rule");
5242
- for (const tool of objects.tools) await validateYaml(tool, "tool");
5243
- for (const connection of objects.connections) await validateYaml(connection, "connection");
5244
- if (Object.values(objects).every((items) => items.length === 0)) {
5245
- findings.push({ severity: "warning", message: "Pack does not include any objects." });
5246
- }
5247
- } catch (error) {
5248
- findings.push({ severity: "error", message: error instanceof Error ? error.message : String(error) });
5249
- }
5250
- return { ok: !findings.some((finding3) => finding3.severity === "error"), findings };
5251
- }
5252
- async function copyObject(source, destDir) {
5253
- const info = await stat11(source);
5254
- const name = baseName(source);
5255
- const dest = info.isDirectory() ? path29.join(destDir, name) : path29.join(destDir, path29.basename(source));
5256
- await mkdir12(destDir, { recursive: true });
5257
- await cp3(source, dest, { recursive: true, force: true });
5258
- return dest;
5259
- }
5260
- async function installPack(repoRoot, nameOrPath) {
5261
- const validation = await validatePack(repoRoot, nameOrPath);
5262
- if (!validation.ok) {
5263
- throw new Error(validation.findings.map((finding3) => finding3.message).join("; "));
5264
- }
5265
- const packDir = await packDirFor(repoRoot, nameOrPath);
5266
- const manifest = await readPackManifest(packDir);
5267
- const objects = await collectObjects(packDir, manifest);
5268
- await Promise.all([
5269
- ...objects.skills.map((source) => copyObject(source, projectObjectDir(repoRoot, "skills"))),
5270
- ...objects.tools.map((source) => copyObject(source, projectObjectDir(repoRoot, "tools"))),
5271
- ...objects.rules.map((source) => copyObject(source, projectObjectDir(repoRoot, "rules"))),
5272
- ...objects.connections.map((source) => copyObject(source, projectObjectDir(repoRoot, "connections")))
5273
- ]);
5274
- return inspectPack(repoRoot, packDir);
5275
- }
5276
-
5277
5561
  // src/commands/packs.ts
5278
5562
  function printList(label, values) {
5279
5563
  console.log(`${label}: ${values.length > 0 ? values.join(", ") : "none"}`);
5280
5564
  }
5281
- async function runPacksList(repoRoot) {
5565
+ async function runPacksList(repoRoot, options = {}) {
5282
5566
  const packs = await listPacks(repoRoot);
5567
+ if (options.json) {
5568
+ printJson({ packs });
5569
+ return;
5570
+ }
5283
5571
  if (packs.length === 0) {
5284
5572
  console.log("No packs found.");
5285
5573
  return;
@@ -5288,8 +5576,12 @@ async function runPacksList(repoRoot) {
5288
5576
  console.log(`${pack.name} - ${pack.description}`);
5289
5577
  }
5290
5578
  }
5291
- async function runPacksInspect(repoRoot, nameOrPath) {
5579
+ async function runPacksInspect(repoRoot, nameOrPath, options = {}) {
5292
5580
  const pack = await inspectPack(repoRoot, nameOrPath);
5581
+ if (options.json) {
5582
+ printJson(pack);
5583
+ return;
5584
+ }
5293
5585
  console.log(pack.name);
5294
5586
  console.log(`description: ${pack.description}`);
5295
5587
  console.log(`path: ${pack.path}`);
@@ -5298,8 +5590,15 @@ async function runPacksInspect(repoRoot, nameOrPath) {
5298
5590
  printList("rules", pack.rules);
5299
5591
  printList("connections", pack.connections);
5300
5592
  }
5301
- async function runPacksValidate(repoRoot, nameOrPath) {
5593
+ async function runPacksValidate(repoRoot, nameOrPath, options = {}) {
5302
5594
  const report = await validatePack(repoRoot, nameOrPath);
5595
+ if (options.json) {
5596
+ printJson(report);
5597
+ if (!report.ok) {
5598
+ process.exitCode = 1;
5599
+ }
5600
+ return;
5601
+ }
5303
5602
  if (report.findings.length === 0) {
5304
5603
  console.log("Pack valid.");
5305
5604
  return;
@@ -5311,8 +5610,12 @@ async function runPacksValidate(repoRoot, nameOrPath) {
5311
5610
  process.exitCode = 1;
5312
5611
  }
5313
5612
  }
5314
- async function runPacksInstall(repoRoot, nameOrPath) {
5613
+ async function runPacksInstall(repoRoot, nameOrPath, options = {}) {
5315
5614
  const pack = await installPack(repoRoot, nameOrPath);
5615
+ if (options.json) {
5616
+ printJson(pack);
5617
+ return;
5618
+ }
5316
5619
  console.log(`Installed pack \`${pack.name}\`.`);
5317
5620
  printList("skills", pack.skills);
5318
5621
  printList("tools", pack.tools);
@@ -5332,17 +5635,37 @@ function parseInputs(pairs = []) {
5332
5635
  }
5333
5636
  return input2;
5334
5637
  }
5335
- async function runToolsList(repoRoot) {
5638
+ async function runToolsList(repoRoot, options = {}) {
5336
5639
  let harness;
5337
5640
  try {
5338
5641
  harness = await resolveHarness(repoRoot);
5339
5642
  } catch (error) {
5340
5643
  if (error instanceof HarnessError) {
5341
- console.log("No harness found. Run `tr init` first.");
5644
+ if (options.json) {
5645
+ printJson({ tools: [], ok: false, error: "harness_missing", message: "No harness found. Run `tr init` first." });
5646
+ } else {
5647
+ console.log("No harness found. Run `tr init` first.");
5648
+ }
5342
5649
  return;
5343
5650
  }
5344
5651
  throw error;
5345
5652
  }
5653
+ const tools2 = harness.tools.map((tool) => ({
5654
+ name: tool.name,
5655
+ description: tool.manifest.description,
5656
+ origin: tool.origin,
5657
+ scope: tool.manifest.scope,
5658
+ risk: tool.manifest.risk,
5659
+ confirm: tool.manifest.confirm,
5660
+ connection: tool.manifest.connection,
5661
+ healthcheck: Boolean(tool.manifest.healthcheck),
5662
+ kind: tool.manifest.run ? "shell" : "script",
5663
+ input: tool.manifest.input
5664
+ }));
5665
+ if (options.json) {
5666
+ printJson({ tools: tools2 });
5667
+ return;
5668
+ }
5346
5669
  if (harness.tools.length === 0) {
5347
5670
  console.log("No tools defined. Add one with `tr tools add` or `tr tools detect`.");
5348
5671
  return;
@@ -5366,6 +5689,11 @@ async function runToolRun(repoRoot, name, options) {
5366
5689
  timeoutMs: options.timeout ? Number(options.timeout) : void 0
5367
5690
  });
5368
5691
  if (outcome.status === "blocked") {
5692
+ if (options.json) {
5693
+ printJson(outcome);
5694
+ process.exitCode = 1;
5695
+ return;
5696
+ }
5369
5697
  if (outcome.reason === "needs-confirmation") {
5370
5698
  console.log(`${outcome.message} Re-run with --yes to confirm.`);
5371
5699
  } else {
@@ -5375,6 +5703,13 @@ async function runToolRun(repoRoot, name, options) {
5375
5703
  return;
5376
5704
  }
5377
5705
  const { result } = outcome;
5706
+ if (options.json) {
5707
+ printJson(outcome);
5708
+ if (!result.ok) {
5709
+ process.exitCode = result.exitCode ?? 1;
5710
+ }
5711
+ return;
5712
+ }
5378
5713
  if (result.stdout) {
5379
5714
  process.stdout.write(result.stdout.endsWith("\n") ? result.stdout : `${result.stdout}
5380
5715
  `);
@@ -5412,7 +5747,11 @@ async function runToolsAdd(repoRoot, name, options) {
5412
5747
  },
5413
5748
  { actor: "human", force: options.force }
5414
5749
  );
5415
- console.log(`Created ${created.scope} tool \`${name}\` at ${created.path}.`);
5750
+ if (options.json) {
5751
+ printJson(created);
5752
+ } else {
5753
+ console.log(`Created ${created.scope} tool \`${name}\` at ${created.path}.`);
5754
+ }
5416
5755
  }
5417
5756
  function deriveNameFromCommand(command) {
5418
5757
  const first = command.trim().split(/\s+/)[0] ?? "tool";
@@ -5432,29 +5771,42 @@ async function runToolsCreate(repoRoot, options) {
5432
5771
  confirm: options.confirm ?? options.risk === "high"
5433
5772
  });
5434
5773
  }
5435
- async function runToolsCheck(repoRoot) {
5774
+ async function runToolsCheck(repoRoot, options = {}) {
5436
5775
  const harness = await resolveHarness(repoRoot);
5437
5776
  if (harness.tools.length === 0) {
5438
- console.log("No tools defined.");
5777
+ if (options.json) {
5778
+ printJson({ checks: [] });
5779
+ } else {
5780
+ console.log("No tools defined.");
5781
+ }
5439
5782
  return;
5440
5783
  }
5441
5784
  let failures = 0;
5785
+ const checks = [];
5442
5786
  for (const tool of harness.tools) {
5443
5787
  const check = await checkToolHealth(repoRoot, tool);
5444
- if (check.status === "ok") {
5445
- console.log(`${tool.name}: ok`);
5446
- } else if (check.status === "skipped") {
5447
- console.log(`${tool.name}: skipped - ${check.message}`);
5448
- } else {
5788
+ checks.push(check);
5789
+ if (!options.json) {
5790
+ if (check.status === "ok") {
5791
+ console.log(`${tool.name}: ok`);
5792
+ } else if (check.status === "skipped") {
5793
+ console.log(`${tool.name}: skipped - ${check.message}`);
5794
+ } else {
5795
+ console.log(`${tool.name}: error - ${check.message}`);
5796
+ }
5797
+ }
5798
+ if (check.status === "error") {
5449
5799
  failures += 1;
5450
- console.log(`${tool.name}: error - ${check.message}`);
5451
5800
  }
5452
5801
  }
5802
+ if (options.json) {
5803
+ printJson({ checks });
5804
+ }
5453
5805
  if (failures > 0) {
5454
5806
  process.exitCode = 1;
5455
5807
  }
5456
5808
  }
5457
- async function runToolsDetect(repoRoot) {
5809
+ async function runToolsDetect(repoRoot, options = {}) {
5458
5810
  let profile;
5459
5811
  try {
5460
5812
  profile = (await resolveHarness(repoRoot)).manifest.profile;
@@ -5464,6 +5816,10 @@ async function runToolsDetect(repoRoot) {
5464
5816
  }
5465
5817
  }
5466
5818
  const candidates = await detectToolCandidates(repoRoot, profile ?? "empty");
5819
+ if (options.json) {
5820
+ printJson({ candidates });
5821
+ return;
5822
+ }
5467
5823
  if (candidates.length === 0) {
5468
5824
  console.log("No starter tools detected.");
5469
5825
  return;
@@ -5477,17 +5833,40 @@ async function runToolsDetect(repoRoot) {
5477
5833
  }
5478
5834
 
5479
5835
  // src/commands/connections.ts
5480
- async function runConnectionsList(repoRoot) {
5836
+ function parseList(value) {
5837
+ return (value ?? "").split(",").map((entry) => entry.trim()).filter(Boolean);
5838
+ }
5839
+ async function runConnectionsList(repoRoot, options = {}) {
5481
5840
  let harness;
5482
5841
  try {
5483
5842
  harness = await resolveHarness(repoRoot);
5484
5843
  } catch (error) {
5485
5844
  if (error instanceof HarnessError) {
5486
- console.log("No harness found. Run `tr init` first.");
5845
+ if (options.json) {
5846
+ printJson({ connections: [], ok: false, error: "harness_missing", message: "No harness found. Run `tr init` first." });
5847
+ } else {
5848
+ console.log("No harness found. Run `tr init` first.");
5849
+ }
5487
5850
  return;
5488
5851
  }
5489
5852
  throw error;
5490
5853
  }
5854
+ const connections = harness.connections.map((connection) => ({
5855
+ name: connection.name,
5856
+ origin: connection.origin,
5857
+ provider: connection.manifest.provider,
5858
+ command: connection.manifest.command,
5859
+ profile: connection.manifest.profile,
5860
+ risk: connection.manifest.risk,
5861
+ confirm: connection.manifest.confirm,
5862
+ healthcheck: Boolean(connection.manifest.healthcheck),
5863
+ allow: connection.manifest.allow,
5864
+ deny: connection.manifest.deny
5865
+ }));
5866
+ if (options.json) {
5867
+ printJson({ connections });
5868
+ return;
5869
+ }
5491
5870
  if (harness.connections.length === 0) {
5492
5871
  console.log("No connections defined. Add one with `tr connections add`.");
5493
5872
  return;
@@ -5514,14 +5893,27 @@ async function runConnectionsAdd(repoRoot, name, options) {
5514
5893
  risk: options.risk,
5515
5894
  confirm: options.confirm,
5516
5895
  healthcheck: options.healthcheck,
5896
+ allow: parseList(options.allow),
5897
+ deny: parseList(options.deny),
5517
5898
  scope: options.scope
5518
5899
  },
5519
5900
  { force: options.force }
5520
5901
  );
5521
- console.log(`Created ${created.manifest.scope} connection \`${name}\` at ${created.path}.`);
5902
+ if (options.json) {
5903
+ printJson(created);
5904
+ } else {
5905
+ console.log(`Created ${created.manifest.scope} connection \`${name}\` at ${created.path}.`);
5906
+ }
5522
5907
  }
5523
- async function runConnectionsCheck(repoRoot) {
5908
+ async function runConnectionsCheck(repoRoot, options = {}) {
5524
5909
  const checks = await checkConnections(repoRoot);
5910
+ if (options.json) {
5911
+ printJson({ checks });
5912
+ if (checks.some((check) => check.status === "error")) {
5913
+ process.exitCode = 1;
5914
+ }
5915
+ return;
5916
+ }
5525
5917
  if (checks.length === 0) {
5526
5918
  console.log("No connections defined.");
5527
5919
  return;
@@ -5541,46 +5933,46 @@ async function runConnectionsCheck(repoRoot) {
5541
5933
  // src/cli.ts
5542
5934
  function createProgram(repoRoot = process.cwd()) {
5543
5935
  const program = new Command();
5544
- program.name("threadroot").description("Git for your AI agent harness: one command to bootstrap, one .threadroot source.").version("0.1.3");
5545
- program.command("bootstrap").description("Plan or apply first-run Threadroot setup for this machine and repository.").option("-y, --yes", "Apply the setup plan. Without --yes, bootstrap prints a dry-run plan.").option("--dry-run", "Print the setup plan without writing files.").option("--agent <list>", "Provider(s): codex,claude,cursor,copilot,gemini,windsurf,antigravity,opencode,all.").option("--task <task>", "Task used for the initial context slice.").option("--mcp", "Also add Threadroot MCP to Codex global config when Codex is selected.").option("--expose <list>", "Also write project provider skill shims: codex,claude,cursor,copilot,gemini,windsurf,antigravity,opencode,all.").option("--no-global", "Skip one-time machine-level agent setup.").option("--no-init", "Skip project harness initialization.").option("--no-import", "Skip importing existing vendor files during init.").option("--profile <profile>", "Override the detected project profile during init.").action((options) => runBootstrap(repoRoot, options));
5546
- program.command("start").argument("[task]", "Task to prepare context for.").option("--task <task>", "Task to prepare context for.").description("Start a focused Threadroot agent session: doctor, status, context, and command map.").action((task, options) => runStart(repoRoot, task, options));
5936
+ program.name("threadroot").description("Git for your AI agent harness: one command to bootstrap, one .threadroot source.").version(THREADROOT_VERSION);
5937
+ program.command("bootstrap").description("Plan or apply first-run Threadroot setup for this machine and repository.").option("-y, --yes", "Apply the setup plan. Without --yes, bootstrap prints a dry-run plan.").option("--dry-run", "Print the setup plan without writing files.").option("--agent <list>", "Provider(s): codex,claude,cursor,copilot,gemini,windsurf,antigravity,opencode,all.").option("--task <task>", "Task used for the initial context slice.").option("--mcp", "Also add Threadroot MCP to Codex global config when Codex is selected.").option("--expose <list>", "Also write project provider skill shims: codex,claude,cursor,copilot,gemini,windsurf,antigravity,opencode,all.").option("--packs <list>", "Comma-separated capability packs to install, such as testing,typescript-node.").option("--json", "Print machine-readable JSON.").option("--no-global", "Skip one-time machine-level agent setup.").option("--no-init", "Skip project harness initialization.").option("--no-import", "Skip importing existing vendor files during init.").option("--profile <profile>", "Override the detected project profile during init.").action((options) => runBootstrap(repoRoot, options));
5938
+ program.command("start").argument("[task]", "Task to prepare context for.").option("--task <task>", "Task to prepare context for.").option("--json", "Print machine-readable JSON.").description("Start a focused Threadroot agent session: doctor, status, context, and command map.").action((task, options) => runStart(repoRoot, task, options));
5547
5939
  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));
5548
5940
  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));
5549
5941
  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));
5550
- program.command("status").description("Show harness state, object counts, and compiled-output drift.").action(() => runStatus(repoRoot));
5942
+ program.command("status").description("Show harness state, object counts, and compiled-output drift.").option("--json", "Print machine-readable JSON.").action((options) => runStatus(repoRoot, options));
5551
5943
  program.command("diff").description("Show the diff between the canonical harness and each compiled vendor file.").action(() => runDiff(repoRoot));
5552
- program.command("doctor").description("Check harness validity, compiled output health, MCP hints, and tool trust.").action(() => runDoctor(repoRoot));
5944
+ program.command("doctor").description("Check harness validity, compiled output health, MCP hints, and tool trust.").option("--json", "Print machine-readable JSON.").action((options) => runDoctor(repoRoot, options));
5553
5945
  program.command("compile").option("--adapter <adapter>", "Restrict output to one adapter: agents, claude, copilot, or cursor.").description("Compile the canonical harness into vendor files.").action((options) => runCompileCommand(repoRoot, options));
5554
- program.command("context").argument("<task>", "Task to assemble a relevant harness slice for.").description("Assemble the task-relevant harness slice: skills, rules, tools, and memory.").action((task) => runContext(repoRoot, task));
5555
- program.command("run").argument("<tool>", "Harness tool name.").option("--input <pair...>", "Tool input as key=value (repeatable).").option("-y, --yes", "Confirm running a tool marked confirm:true.").option("--timeout <ms>", "Override the execution timeout in milliseconds.").description("Execute a harness tool locally.").action((tool, options) => runToolRun(repoRoot, tool, options));
5946
+ program.command("context").argument("<task>", "Task to assemble a relevant harness slice for.").description("Assemble the task-relevant harness slice: skills, rules, tools, and memory.").option("--json", "Print machine-readable JSON.").action((task, options) => runContext(repoRoot, task, options));
5947
+ program.command("run").argument("<tool>", "Harness tool name.").option("--input <pair...>", "Tool input as key=value (repeatable).").option("-y, --yes", "Confirm running a tool marked confirm:true.").option("--timeout <ms>", "Override the execution timeout in milliseconds.").option("--json", "Print machine-readable JSON.").description("Execute a harness tool locally.").action((tool, options) => runToolRun(repoRoot, tool, options));
5556
5948
  program.command("install").argument("<source>", "Object source: local path or git (github:owner/repo/path[@ref]).").option("--kind <kind>", "Object kind: skill, tool, rule, or connection (inferred when omitted).").option("--path <path>", "Path to the object within a git source repo.").option("--user", "Install into the user harness (~/.threadroot) instead of the project.").description("Install a harness object from a local path or git source.").action((source, options) => runInstall(repoRoot, source, options));
5557
5949
  program.command("remember").argument("<note>", "Durable note to record.").option("--type <type>", "Memory type: project, current-focus, handoff, or pitfalls.").description("Append a durable note to harness memory (defaults to handoff).").action((note, options) => runRemember(repoRoot, note, options));
5558
5950
  const memory = program.command("memory").description("Read and append durable harness memory.");
5559
5951
  memory.command("read").argument("<type>", "Memory type: project, repo-map, current-focus, handoff, or pitfalls.").description("Print a memory file.").action((type) => runMemoryRead(repoRoot, type));
5560
5952
  memory.command("append").argument("<type>", "Memory type: project, repo-map, current-focus, handoff, or pitfalls.").argument("<note>", "Note to append.").description("Append a durable note to memory.").action((type, note) => runMemoryAppend(repoRoot, type, note));
5561
5953
  const tools2 = program.command("tools").description("Manage executable harness tools.");
5562
- tools2.command("list").description("List harness tools.").action(() => runToolsList(repoRoot));
5563
- tools2.command("check").description("Run configured tool healthchecks.").action(() => runToolsCheck(repoRoot));
5564
- tools2.command("detect").description("Propose starter tools from the repo's existing command surface.").action(() => runToolsDetect(repoRoot));
5565
- tools2.command("add").argument("<name>", "Tool name (lowercase, hyphenated).").requiredOption("--description <text>", "What the tool does.").option("--run <command>", "Shell command (use {{param}} for inputs).").option("--script <path>", "Harness-relative script path (alternative to --run).").option("--risk <risk>", "Risk level: low, medium, or high.").option("--connection <name>", "Connection this tool depends on.").option("--healthcheck <command>", "Command that verifies the tool is available.").option("--confirm", "Require confirmation before running.").option("--scope <scope>", "user or project.").option("--force", "Overwrite an existing tool.").description("Author a new harness tool.").action((name, options) => runToolsAdd(repoRoot, name, options));
5566
- tools2.command("create").option("--from-command <command>", "Create a tool around an existing command.").option("--description <text>", "What the tool does.").option("--risk <risk>", "Risk level: low, medium, or high.").option("--connection <name>", "Connection this tool depends on.").option("--healthcheck <command>", "Command that verifies the tool is available.").option("--confirm", "Require confirmation before running.").option("--scope <scope>", "user or project.").option("--force", "Overwrite an existing tool.").description("Guided safe tool builder.").action((options) => runToolsCreate(repoRoot, options));
5954
+ tools2.command("list").option("--json", "Print machine-readable JSON.").description("List harness tools.").action((options) => runToolsList(repoRoot, options));
5955
+ tools2.command("check").option("--json", "Print machine-readable JSON.").description("Run configured tool healthchecks.").action((options) => runToolsCheck(repoRoot, options));
5956
+ tools2.command("detect").option("--json", "Print machine-readable JSON.").description("Propose starter tools from the repo's existing command surface.").action((options) => runToolsDetect(repoRoot, options));
5957
+ tools2.command("add").argument("<name>", "Tool name (lowercase, hyphenated).").requiredOption("--description <text>", "What the tool does.").option("--run <command>", "Shell command (use {{param}} for inputs).").option("--script <path>", "Harness-relative script path (alternative to --run).").option("--risk <risk>", "Risk level: low, medium, or high.").option("--connection <name>", "Connection this tool depends on.").option("--healthcheck <command>", "Command that verifies the tool is available.").option("--confirm", "Require confirmation before running.").option("--scope <scope>", "user or project.").option("--force", "Overwrite an existing tool.").option("--json", "Print machine-readable JSON.").description("Author a new harness tool.").action((name, options) => runToolsAdd(repoRoot, name, options));
5958
+ tools2.command("create").option("--from-command <command>", "Create a tool around an existing command.").option("--description <text>", "What the tool does.").option("--risk <risk>", "Risk level: low, medium, or high.").option("--connection <name>", "Connection this tool depends on.").option("--healthcheck <command>", "Command that verifies the tool is available.").option("--confirm", "Require confirmation before running.").option("--scope <scope>", "user or project.").option("--force", "Overwrite an existing tool.").option("--json", "Print machine-readable JSON.").description("Guided safe tool builder.").action((options) => runToolsCreate(repoRoot, options));
5567
5959
  const connections = program.command("connections").description("Manage local CLI connections.");
5568
- connections.command("list").description("List harness connections.").action(() => runConnectionsList(repoRoot));
5569
- connections.command("check").description("Run configured connection healthchecks.").action(() => runConnectionsCheck(repoRoot));
5570
- connections.command("add").argument("<name>", "Connection name (lowercase, hyphenated).").requiredOption("--provider <provider>", "Provider name, such as aws, github, azure, or snowflake.").requiredOption("--command <command>", "Local CLI command, such as aws, gh, az, or snow.").option("--description <text>", "What this connection is for.").option("--profile <profile>", "Local CLI profile/account label.").option("--risk <risk>", "Risk level: low, medium, or high.").option("--confirm", "Require confirmation before connection-backed tools run.").option("--healthcheck <command>", "Command that verifies the connection works.").option("--scope <scope>", "user or project.").option("--force", "Overwrite an existing connection.").description("Author a local CLI connection manifest.").action((name, options) => runConnectionsAdd(repoRoot, name, options));
5960
+ connections.command("list").option("--json", "Print machine-readable JSON.").description("List harness connections.").action((options) => runConnectionsList(repoRoot, options));
5961
+ connections.command("check").option("--json", "Print machine-readable JSON.").description("Run configured connection healthchecks.").action((options) => runConnectionsCheck(repoRoot, options));
5962
+ connections.command("add").argument("<name>", "Connection name (lowercase, hyphenated).").requiredOption("--provider <provider>", "Provider name, such as aws, github, azure, or snowflake.").requiredOption("--command <command>", "Local CLI command, such as aws, gh, az, or snow.").option("--description <text>", "What this connection is for.").option("--profile <profile>", "Local CLI profile/account label.").option("--risk <risk>", "Risk level: low, medium, or high.").option("--confirm", "Require confirmation before connection-backed tools run.").option("--healthcheck <command>", "Command that verifies the connection works.").option("--allow <patterns>", "Comma-separated allowed command fragments for this connection.").option("--deny <patterns>", "Comma-separated denied command fragments for this connection.").option("--scope <scope>", "user or project.").option("--force", "Overwrite an existing connection.").option("--json", "Print machine-readable JSON.").description("Author a local CLI connection manifest.").action((name, options) => runConnectionsAdd(repoRoot, name, options));
5571
5963
  const packs = program.command("packs").description("Inspect, validate, and install capability packs.");
5572
- packs.command("list").description("List built-in and repo-local packs.").action(() => runPacksList(repoRoot));
5573
- packs.command("inspect").argument("<name-or-path>", "Built-in pack name or repo-relative pack path.").description("Inspect a capability pack.").action((nameOrPath) => runPacksInspect(repoRoot, nameOrPath));
5574
- packs.command("validate").argument("<name-or-path>", "Built-in pack name or repo-relative pack path.").description("Validate a capability pack.").action((nameOrPath) => runPacksValidate(repoRoot, nameOrPath));
5575
- packs.command("install").argument("<name-or-path>", "Built-in pack name or repo-relative pack path.").description("Install a capability pack into the project harness.").action((nameOrPath) => runPacksInstall(repoRoot, nameOrPath));
5964
+ packs.command("list").option("--json", "Print machine-readable JSON.").description("List built-in and repo-local packs.").action((options) => runPacksList(repoRoot, options));
5965
+ packs.command("inspect").argument("<name-or-path>", "Built-in pack name or repo-relative pack path.").option("--json", "Print machine-readable JSON.").description("Inspect a capability pack.").action((nameOrPath, options) => runPacksInspect(repoRoot, nameOrPath, options));
5966
+ packs.command("validate").argument("<name-or-path>", "Built-in pack name or repo-relative pack path.").option("--json", "Print machine-readable JSON.").description("Validate a capability pack.").action((nameOrPath, options) => runPacksValidate(repoRoot, nameOrPath, options));
5967
+ packs.command("install").argument("<name-or-path>", "Built-in pack name or repo-relative pack path.").option("--json", "Print machine-readable JSON.").description("Install a capability pack into the project harness.").action((nameOrPath, options) => runPacksInstall(repoRoot, nameOrPath, options));
5576
5968
  const skills = program.command("skills").description("Inspect and validate harness skills.");
5577
- skills.command("list").description("List harness skills.").action(() => runSkillsList(repoRoot));
5578
- skills.command("inspect").argument("<path>", "Repo-relative skill file or skill directory.").description("Inspect a skill's metadata, references, scripts, assets, and eval files.").action((targetPath) => runSkillsInspect(repoRoot, targetPath));
5579
- skills.command("validate").option("--path <path>", "Validate a repo-relative skill file, skill directory, or skill collection.").description("Validate skill frontmatter, naming, trigger descriptions, and progressive-disclosure hygiene.").action((options) => runSkillsValidate(repoRoot, options));
5969
+ skills.command("list").option("--json", "Print machine-readable JSON.").description("List harness skills.").action((options) => runSkillsList(repoRoot, options));
5970
+ skills.command("inspect").argument("<path>", "Repo-relative skill file or skill directory.").option("--json", "Print machine-readable JSON.").description("Inspect a skill's metadata, references, scripts, assets, and eval files.").action((targetPath, options) => runSkillsInspect(repoRoot, targetPath, options));
5971
+ skills.command("validate").option("--path <path>", "Validate a repo-relative skill file, skill directory, or skill collection.").option("--json", "Print machine-readable JSON.").description("Validate skill frontmatter, naming, trigger descriptions, and progressive-disclosure hygiene.").action((options) => runSkillsValidate(repoRoot, options));
5580
5972
  const mcp = program.command("mcp").description("Run or configure the local Threadroot MCP server.");
5581
5973
  mcp.action(() => runMcp(repoRoot));
5582
- mcp.command("check").option("--timeout <ms>", "Handshake timeout in milliseconds.").description("Verify Codex MCP config and the Threadroot stdio server handshake.").action((options) => runMcpCheck(repoRoot, options));
5583
- mcp.command("setup").option("--agent <agent>", "all, generic, codex, copilot, cursor, or claude.").option("--write", "Write project-local MCP config files for the agents.").description("Print MCP config snippets and a pasteable agent bootstrap prompt.").action((options) => runMcpSetup(repoRoot, options));
5974
+ mcp.command("check").option("--timeout <ms>", "Handshake timeout in milliseconds.").option("--json", "Print machine-readable JSON.").description("Verify Codex MCP config and the Threadroot stdio server handshake.").action((options) => runMcpCheck(repoRoot, options));
5975
+ mcp.command("setup").option("--agent <agent>", "all, generic, codex, copilot, cursor, or claude.").option("--write", "Write project-local MCP config files for the agents.").option("--json", "Print machine-readable JSON.").description("Print MCP config snippets and a pasteable agent bootstrap prompt.").action((options) => runMcpSetup(repoRoot, options));
5584
5976
  return program;
5585
5977
  }
5586
5978