threadroot 0.1.4 → 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) {
@@ -2401,7 +2466,7 @@ import { homedir as homedir2, tmpdir } from "os";
2401
2466
  import path14 from "path";
2402
2467
 
2403
2468
  // src/core/version.ts
2404
- var THREADROOT_VERSION = "0.1.4";
2469
+ var THREADROOT_VERSION = "0.1.5";
2405
2470
 
2406
2471
  // src/core/mcp-check.ts
2407
2472
  var REQUIRED_MCP_TOOLS = [
@@ -3278,7 +3343,7 @@ async function readIfExists2(filePath) {
3278
3343
  throw error;
3279
3344
  }
3280
3345
  }
3281
- function normalize(text) {
3346
+ function normalize2(text) {
3282
3347
  return text.toLowerCase().replace(/\s+/g, " ").trim();
3283
3348
  }
3284
3349
  function splitSections(markdown) {
@@ -3294,7 +3359,7 @@ function splitSections(markdown) {
3294
3359
  for (const line of markdown.split(/\r?\n/)) {
3295
3360
  if (/^#{1,6}\s/.test(line)) {
3296
3361
  flush();
3297
- heading = normalize(line.replace(/^#+\s*/, ""));
3362
+ heading = normalize2(line.replace(/^#+\s*/, ""));
3298
3363
  buffer = [line];
3299
3364
  } else {
3300
3365
  buffer.push(line);
@@ -3304,13 +3369,13 @@ function splitSections(markdown) {
3304
3369
  return sections;
3305
3370
  }
3306
3371
  function novelSections(canonical, other) {
3307
- const haystack = normalize(canonical);
3372
+ const haystack = normalize2(canonical);
3308
3373
  const seenHeadings = new Set(splitSections(canonical).map((section) => section.heading).filter(Boolean));
3309
3374
  return splitSections(other).filter((section) => {
3310
3375
  if (section.heading && seenHeadings.has(section.heading)) {
3311
3376
  return false;
3312
3377
  }
3313
- return !haystack.includes(normalize(section.text));
3378
+ return !haystack.includes(normalize2(section.text));
3314
3379
  });
3315
3380
  }
3316
3381
  async function listCursorRules(repoRoot) {
@@ -3544,43 +3609,32 @@ async function initHarness(repoRoot, options = {}) {
3544
3609
  return { name, profile, adapters, skills, tools: tools2, memory, rules, import: report, compiled: written, exposed };
3545
3610
  }
3546
3611
 
3547
- // src/core/status.ts
3548
- async function harnessStatus(repoRoot, options = {}) {
3549
- let harness;
3550
- try {
3551
- harness = await resolveHarness(repoRoot, { home: options.home });
3552
- } catch (error) {
3553
- if (error instanceof HarnessError) {
3554
- return { exists: false };
3555
- }
3556
- throw error;
3557
- }
3558
- const files = await compile(repoRoot, harness);
3559
- const drift = await detectDrift(repoRoot, files);
3560
- return {
3561
- exists: true,
3562
- manifest: {
3563
- name: harness.manifest.name,
3564
- profile: harness.manifest.profile,
3565
- adapters: harness.manifest.adapters,
3566
- toolsAllow: harness.manifest.tools.allow
3567
- },
3568
- counts: {
3569
- skills: harness.skills.length,
3570
- rules: harness.rules.length,
3571
- tools: harness.tools.length,
3572
- memory: harness.memory.length
3573
- },
3574
- drift
3575
- };
3576
- }
3577
-
3578
- // src/core/bootstrap.ts
3579
- var DEFAULT_TASK = "start this project";
3580
- async function harnessExists(repoRoot) {
3581
- return pathExists2(path23.join(repoRoot, ".threadroot", "harness.yaml"));
3582
- }
3583
- 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) {
3584
3638
  try {
3585
3639
  await stat9(target);
3586
3640
  return true;
@@ -3591,155 +3645,530 @@ async function pathExists2(target) {
3591
3645
  throw error;
3592
3646
  }
3593
3647
  }
3594
- function modeFor(options) {
3595
- return options.yes && !options.dryRun ? "write" : "dry-run";
3596
- }
3597
- async function bootstrapProject(repoRoot, options = {}) {
3598
- const task = options.task?.trim() || DEFAULT_TASK;
3599
- const mode = modeFor(options);
3600
- const write2 = mode === "write";
3601
- const notes = [];
3602
- const existed = await harnessExists(repoRoot);
3603
- let setup;
3604
- let init;
3605
- let exposed;
3606
- if (!options.noGlobal) {
3607
- setup = await setupGlobal({
3608
- agents: options.agents ?? "all",
3609
- mode,
3610
- home: options.home,
3611
- mcp: options.mcp,
3612
- mcpEntry: options.mcpEntry
3613
- });
3614
- } else {
3615
- notes.push("Skipped global setup because --no-global was set.");
3616
- }
3617
- if (!existed && !options.noInit) {
3618
- if (write2) {
3619
- init = await initHarness(repoRoot, {
3620
- import: options.import,
3621
- profile: options.profile,
3622
- home: options.home
3623
- });
3624
- } else {
3625
- 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;
3626
3652
  }
3627
- } else if (existed) {
3628
- notes.push("Existing harness detected; bootstrap will not reinitialize it.");
3629
- } else {
3630
- notes.push("Skipped project initialization because --no-init was set.");
3631
- }
3632
- const hasHarnessAfterInit = existed || Boolean(init) || await pathExists2(path23.join(repoRoot, ".threadroot", "harness.yaml"));
3633
- if (options.expose) {
3634
- exposed = await exposeProject(repoRoot, {
3635
- agents: options.expose,
3636
- mode
3637
- });
3638
3653
  }
3639
- let status;
3640
- let doctorReport;
3641
- let context;
3642
- let mcpCheck;
3643
- if (options.mcp && write2) {
3644
- 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;
3645
3665
  }
3646
- if (hasHarnessAfterInit) {
3647
- status = await harnessStatus(repoRoot, { home: options.home });
3648
- doctorReport = await doctor(repoRoot, { home: options.home });
3649
- if (status.exists) {
3650
- 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;
3651
3669
  }
3652
- } else {
3653
- notes.push("Skipped doctor/status/context because no harness exists yet.");
3654
3670
  }
3655
- if (!write2) {
3656
- 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}`);
3657
3677
  }
3658
- return {
3659
- mode: write2 ? "write" : "plan",
3660
- task,
3661
- harnessExisted: existed,
3662
- setup,
3663
- init,
3664
- expose: exposed,
3665
- status,
3666
- doctor: doctorReport,
3667
- context,
3668
- mcpCheck,
3669
- notes
3670
- };
3678
+ return normalized;
3671
3679
  }
3672
- async function startSession(repoRoot, options = {}) {
3673
- const task = options.task?.trim() || DEFAULT_TASK;
3674
- const status = await harnessStatus(repoRoot, { home: options.home });
3675
- const notes = [];
3676
- if (!status.exists) {
3677
- return {
3678
- task,
3679
- status,
3680
- notes: ["No harness found. Run `threadroot bootstrap --yes` first."]
3681
- };
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}`);
3682
3686
  }
3683
- const doctorReport = await doctor(repoRoot, { home: options.home });
3684
- const context = await assembleContext(repoRoot, task, { home: options.home, fallbackSkills: true });
3685
- return { task, status, doctor: doctorReport, context, notes };
3687
+ return parsed.data;
3686
3688
  }
3687
-
3688
- // src/commands/session-output.ts
3689
- function printDoctor(report) {
3690
- if (!report) {
3691
- 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;
3692
3702
  }
3693
- const actionable = report.findings.filter((finding3) => finding3.severity !== "info");
3694
- console.log(actionable.length === 0 ? "doctor: clean" : `doctor: ${report.summary.errors} error(s), ${report.summary.warnings} warning(s)`);
3695
- for (const finding3 of report.findings.slice(0, 8)) {
3696
- const label = finding3.severity === "info" ? "hint" : finding3.severity;
3697
- const suffix = finding3.path ? ` (${finding3.path})` : "";
3698
- console.log(`- ${label} ${finding3.code}: ${finding3.message}${suffix}`);
3703
+ if (nameOrPath.startsWith(".") || nameOrPath.includes("/") || nameOrPath.includes("\\")) {
3704
+ return toRepoPath(repoRoot, nameOrPath);
3699
3705
  }
3700
- if (report.findings.length > 8) {
3701
- console.log(`- ... ${report.findings.length - 8} more finding(s)`);
3706
+ const bundled = await bundledPacksDir();
3707
+ if (bundled) {
3708
+ return path23.join(bundled, nameOrPath);
3702
3709
  }
3710
+ return toRepoPath(repoRoot, path23.join("packs", nameOrPath));
3703
3711
  }
3704
- function printStatus(status) {
3705
- if (!status) {
3706
- return;
3707
- }
3708
- if (!status.exists) {
3709
- console.log("harness: missing");
3710
- 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;
3711
3721
  }
3712
- console.log(`harness: ${status.manifest.name} (${status.manifest.profile})`);
3713
- console.log(`adapters: ${status.manifest.adapters.length > 0 ? status.manifest.adapters.join(", ") : "none (local-only)"}`);
3714
- console.log(
3715
- `objects: ${status.counts.skills} skills, ${status.counts.rules} rules, ${status.counts.tools} tools, ${status.counts.memory} memory`
3716
- );
3717
3722
  }
3718
- function printContext(context) {
3719
- if (!context) {
3720
- return;
3721
- }
3722
- console.log(`task: ${context.task}`);
3723
- if (context.skills.length > 0) {
3724
- const skillLabel = context.skills.some((skill) => skill.score > 0) ? "relevant skills:" : "starter skills:";
3725
- console.log(skillLabel);
3726
- for (const skill of context.skills.slice(0, 8)) {
3727
- 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
+ }
3728
3735
  }
3729
- } else {
3730
- console.log("relevant skills: none matched; run `threadroot skills list` to inspect all skills.");
3731
- }
3732
- if (context.tools.length > 0) {
3733
- console.log("available tools:");
3734
- for (const tool of context.tools.slice(0, 8)) {
3735
- console.log(`- ${tool.name} (${tool.risk}) - ${tool.description}`);
3736
+ return result;
3737
+ } catch (error) {
3738
+ if (error.code === "ENOENT") {
3739
+ return [];
3736
3740
  }
3737
- }
3738
- if (context.memory.length > 0) {
3739
- console.log(`memory: ${context.memory.map((entry) => entry.type).join(", ")}`);
3741
+ throw error;
3740
3742
  }
3741
3743
  }
3742
- function printCommandMap() {
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:");
4163
+ for (const tool of context.tools.slice(0, 8)) {
4164
+ console.log(`- ${tool.name} (${tool.risk}) - ${tool.description}`);
4165
+ }
4166
+ }
4167
+ if (context.memory.length > 0) {
4168
+ console.log(`memory: ${context.memory.map((entry) => entry.type).join(", ")}`);
4169
+ }
4170
+ }
4171
+ function printCommandMap() {
3743
4172
  console.log("agent command map:");
3744
4173
  console.log('- `threadroot start "<task>"` - begin a focused agent session');
3745
4174
  console.log('- `threadroot context "<task>"` - get relevant skills, tools, rules, and memory');
@@ -3781,6 +4210,9 @@ function printBootstrapReport(report) {
3781
4210
  console.log(`- ${entry.label}: ${entry.status} ${entry.path}${suffix}`);
3782
4211
  }
3783
4212
  }
4213
+ if (report.packs && report.packs.length > 0) {
4214
+ console.log(`packs: ${report.packs.map((pack) => pack.name).join(", ")}`);
4215
+ }
3784
4216
  printStatus(report.status);
3785
4217
  printMcpCheck(report.mcpCheck);
3786
4218
  printDoctor(report.doctor);
@@ -3813,13 +4245,18 @@ async function runBootstrap(repoRoot, options) {
3813
4245
  task: options.task,
3814
4246
  mcp: options.mcp,
3815
4247
  expose: options.expose,
4248
+ packs: options.packs,
3816
4249
  noGlobal: options.global === false,
3817
4250
  noInit: options.init === false,
3818
4251
  import: options.import,
3819
4252
  profile: options.profile ? profileIdSchema.parse(options.profile) : void 0,
3820
4253
  mcpEntry: options.mcp ? mcpEntryForCurrentProcess() : void 0
3821
4254
  });
3822
- printBootstrapReport(report);
4255
+ if (options.json) {
4256
+ printJson(report);
4257
+ } else {
4258
+ printBootstrapReport(report);
4259
+ }
3823
4260
  if (report.mode === "write" && report.doctor && !report.doctor.ok) {
3824
4261
  process.exitCode = 1;
3825
4262
  }
@@ -3846,17 +4283,25 @@ async function runCompileCommand(repoRoot, options) {
3846
4283
  }
3847
4284
 
3848
4285
  // src/commands/context.ts
3849
- async function runContext(repoRoot, task) {
4286
+ async function runContext(repoRoot, task, options = {}) {
3850
4287
  let context;
3851
4288
  try {
3852
4289
  context = await assembleContext(repoRoot, task);
3853
4290
  } catch (error) {
3854
4291
  if (error instanceof HarnessError) {
3855
- 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
+ }
3856
4297
  return;
3857
4298
  }
3858
4299
  throw error;
3859
4300
  }
4301
+ if (options.json) {
4302
+ printJson(context);
4303
+ return;
4304
+ }
3860
4305
  console.log(`task: ${context.task}`);
3861
4306
  if (context.skills.length > 0) {
3862
4307
  console.log("\nskills:");
@@ -3889,7 +4334,7 @@ async function runContext(repoRoot, task) {
3889
4334
 
3890
4335
  // src/commands/diff.ts
3891
4336
  import fs3 from "fs/promises";
3892
- import path24 from "path";
4337
+ import path25 from "path";
3893
4338
  async function readIfExists3(filePath) {
3894
4339
  try {
3895
4340
  return await fs3.readFile(filePath, "utf8");
@@ -3948,7 +4393,7 @@ async function runDiff(repoRoot) {
3948
4393
  const files = await compile(repoRoot, harness);
3949
4394
  let changed = 0;
3950
4395
  for (const file of files) {
3951
- const existing = await readIfExists3(path24.join(repoRoot, file.path));
4396
+ const existing = await readIfExists3(path25.join(repoRoot, file.path));
3952
4397
  if (existing === void 0) {
3953
4398
  changed += 1;
3954
4399
  console.log(`+ ${file.path} (new)`);
@@ -3969,8 +4414,15 @@ async function runDiff(repoRoot) {
3969
4414
  }
3970
4415
 
3971
4416
  // src/commands/doctor.ts
3972
- async function runDoctor(repoRoot) {
4417
+ async function runDoctor(repoRoot, options = {}) {
3973
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
+ }
3974
4426
  const actionable = report.findings.filter((finding3) => finding3.severity !== "info");
3975
4427
  const hints = report.findings.filter((finding3) => finding3.severity === "info");
3976
4428
  if (actionable.length === 0) {
@@ -4063,13 +4515,13 @@ async function runInit(repoRoot, options) {
4063
4515
  }
4064
4516
 
4065
4517
  // src/commands/install.ts
4066
- import path27 from "path";
4518
+ import path28 from "path";
4067
4519
 
4068
4520
  // src/core/install/fetch.ts
4069
4521
  import { execFile } from "child_process";
4070
4522
  import { mkdtemp as mkdtemp2, rm as rm4 } from "fs/promises";
4071
4523
  import os2 from "os";
4072
- import path25 from "path";
4524
+ import path26 from "path";
4073
4525
  import { promisify } from "util";
4074
4526
  var run = promisify(execFile);
4075
4527
  function cloneUrl(ref) {
@@ -4090,7 +4542,7 @@ async function git(cwd, args) {
4090
4542
  }
4091
4543
  async function fetchGitSource(ref) {
4092
4544
  const url = cloneUrl(ref);
4093
- const dir = await mkdtemp2(path25.join(os2.tmpdir(), "threadroot-fetch-"));
4545
+ const dir = await mkdtemp2(path26.join(os2.tmpdir(), "threadroot-fetch-"));
4094
4546
  const cleanup = () => rm4(dir, { recursive: true, force: true });
4095
4547
  try {
4096
4548
  if (ref.ref) {
@@ -4112,9 +4564,9 @@ async function fetchGitSource(ref) {
4112
4564
  }
4113
4565
 
4114
4566
  // src/core/install/install.ts
4115
- import { cp as cp2, mkdir as mkdir10, readFile as readFile11, readdir as readdir5, stat as stat10, writeFile as writeFile10 } from "fs/promises";
4116
- import { createHash as createHash2 } from "crypto";
4117
- 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";
4118
4570
  var NAME_RE5 = /^[a-z0-9][a-z0-9-]*$/;
4119
4571
  var KIND_DIR = {
4120
4572
  skill: "skills",
@@ -4126,8 +4578,8 @@ function objectExt(kind) {
4126
4578
  return kind === "tool" || kind === "connection" ? ".yaml" : ".md";
4127
4579
  }
4128
4580
  function safeRepoPath(objectPath) {
4129
- const normalized = path26.normalize(objectPath);
4130
- 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}`)) {
4131
4583
  throw new Error(`Unsafe object path: ${objectPath}`);
4132
4584
  }
4133
4585
  return normalized;
@@ -4141,7 +4593,7 @@ function inferKind(objectPath, override) {
4141
4593
  if (segments.includes("tools")) return "tool";
4142
4594
  if (segments.includes("connections")) return "connection";
4143
4595
  if (segments.includes("rules")) return "rule";
4144
- const ext = path26.extname(objectPath).toLowerCase();
4596
+ const ext = path27.extname(objectPath).toLowerCase();
4145
4597
  if (ext === ".yaml" || ext === ".yml") return "tool";
4146
4598
  if (ext === ".md") return "skill";
4147
4599
  throw new Error(
@@ -4149,20 +4601,20 @@ function inferKind(objectPath, override) {
4149
4601
  );
4150
4602
  }
4151
4603
  function deriveName(objectPath) {
4152
- const base = path26.basename(objectPath, path26.extname(objectPath));
4604
+ const base = path27.basename(objectPath, path27.extname(objectPath));
4153
4605
  if (!NAME_RE5.test(base)) {
4154
4606
  throw new Error(`Invalid object name \`${base}\` (use lowercase letters, digits, and dashes).`);
4155
4607
  }
4156
4608
  return base;
4157
4609
  }
4158
- async function hashDirectory(root) {
4159
- const hash = createHash2("sha256");
4610
+ async function hashDirectory2(root) {
4611
+ const hash = createHash3("sha256");
4160
4612
  hash.update("threadroot-directory-v1\n");
4161
4613
  async function walk(dir) {
4162
- 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));
4163
4615
  for (const entry of entries) {
4164
- const full = path26.join(dir, entry.name);
4165
- 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("/");
4166
4618
  if (entry.isSymbolicLink()) {
4167
4619
  throw new Error(`Refusing to install skill directory with symlink: ${rel}`);
4168
4620
  }
@@ -4173,7 +4625,7 @@ async function hashDirectory(root) {
4173
4625
  if (entry.isFile()) {
4174
4626
  hash.update(`file:${rel}
4175
4627
  `);
4176
- hash.update(await readFile11(full));
4628
+ hash.update(await readFile12(full));
4177
4629
  hash.update("\n");
4178
4630
  }
4179
4631
  }
@@ -4182,8 +4634,8 @@ async function hashDirectory(root) {
4182
4634
  return hash.digest("hex");
4183
4635
  }
4184
4636
  async function validateSkillDirectory2(sourcePath, expectedName) {
4185
- const skillPath = path26.join(sourcePath, "SKILL.md");
4186
- const parsed = parseFrontmatter(await readFile11(skillPath, "utf8"));
4637
+ const skillPath = path27.join(sourcePath, "SKILL.md");
4638
+ const parsed = parseFrontmatter(await readFile12(skillPath, "utf8"));
4187
4639
  const result = skillFrontmatterSchema.safeParse(parsed.data);
4188
4640
  if (!result.success) {
4189
4641
  const detail = result.error.issues.map((issue) => issue.message).join("; ");
@@ -4192,7 +4644,7 @@ async function validateSkillDirectory2(sourcePath, expectedName) {
4192
4644
  if (result.data.name !== expectedName) {
4193
4645
  throw new Error(`Skill directory name \`${expectedName}\` must match SKILL.md name \`${result.data.name}\`.`);
4194
4646
  }
4195
- return await hashDirectory(sourcePath);
4647
+ return await hashDirectory2(sourcePath);
4196
4648
  }
4197
4649
  async function installObject(repoRoot, rawSource, options = {}) {
4198
4650
  const ref = parseSourceRef(rawSource);
@@ -4212,7 +4664,7 @@ async function installObject(repoRoot, rawSource, options = {}) {
4212
4664
  objectPath = safeRepoPath(within);
4213
4665
  refLabel = ref.ref;
4214
4666
  const fetched = await fetchGitSource(ref);
4215
- sourcePath = path26.join(fetched.dir, objectPath);
4667
+ sourcePath = path27.join(fetched.dir, objectPath);
4216
4668
  resolved = fetched.sha;
4217
4669
  cleanup = fetched.cleanup;
4218
4670
  } else {
@@ -4227,19 +4679,19 @@ async function installObject(repoRoot, rawSource, options = {}) {
4227
4679
  const destDir = scope === "user" ? userObjectDir(dirKey, options.home) : projectObjectDir(repoRoot, dirKey);
4228
4680
  let destPath;
4229
4681
  let integrity;
4230
- const info = await stat10(sourcePath);
4682
+ const info = await stat11(sourcePath);
4231
4683
  if (info.isDirectory()) {
4232
4684
  if (kind !== "skill") {
4233
4685
  throw new Error("Only skill objects may be installed from a directory.");
4234
4686
  }
4235
4687
  integrity = `sha256:${await validateSkillDirectory2(sourcePath, name)}`;
4236
- destPath = path26.join(destDir, name);
4237
- await mkdir10(destDir, { recursive: true });
4238
- 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 });
4239
4691
  } else {
4240
- const content = await readFile11(sourcePath, "utf8");
4241
- destPath = path26.join(destDir, `${name}${objectExt(kind)}`);
4242
- 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 });
4243
4695
  await writeFile10(destPath, content, "utf8");
4244
4696
  integrity = `sha256:${hashContent(content)}`;
4245
4697
  }
@@ -4292,7 +4744,7 @@ async function runInstall(repoRoot, source, options) {
4292
4744
  if (installed.kind === "skill" && installed.entry.sourceKind !== "local") {
4293
4745
  console.log(" note: inspect external skills before trusting bundled scripts, assets, or allowed tools.");
4294
4746
  if (scope === "project") {
4295
- console.log(` inspect: threadroot skills inspect ${path27.relative(repoRoot, installed.path)}`);
4747
+ console.log(` inspect: threadroot skills inspect ${path28.relative(repoRoot, installed.path)}`);
4296
4748
  }
4297
4749
  }
4298
4750
  } catch (error) {
@@ -4304,7 +4756,7 @@ async function runInstall(repoRoot, source, options) {
4304
4756
  // src/mcp/server.ts
4305
4757
  import readline from "readline";
4306
4758
  import { stdin as input, stdout as output } from "process";
4307
- import { z as z4 } from "zod";
4759
+ import { z as z5 } from "zod";
4308
4760
  function defineTool(spec) {
4309
4761
  return spec;
4310
4762
  }
@@ -4316,7 +4768,7 @@ var toolRegistry = [
4316
4768
  { task: { type: "string", description: "The coding task to assemble context for." } },
4317
4769
  ["task"]
4318
4770
  ),
4319
- args: z4.object({ task: z4.string().min(1) }),
4771
+ args: z5.object({ task: z5.string().min(1) }),
4320
4772
  run: async (repoRoot, args) => {
4321
4773
  const harness = await loadHarnessOrNull(repoRoot);
4322
4774
  if (!harness) {
@@ -4329,7 +4781,7 @@ var toolRegistry = [
4329
4781
  name: "skills_list",
4330
4782
  description: "List the skills defined in this repo's harness (name, when, tags).",
4331
4783
  inputSchema: objectSchema({}),
4332
- args: z4.object({}),
4784
+ args: z5.object({}),
4333
4785
  run: async (repoRoot) => {
4334
4786
  const harness = await loadHarnessOrNull(repoRoot);
4335
4787
  if (!harness) {
@@ -4349,7 +4801,7 @@ var toolRegistry = [
4349
4801
  name: "skills_get",
4350
4802
  description: "Return a harness skill's full body and metadata by name.",
4351
4803
  inputSchema: objectSchema({ name: { type: "string", description: "Skill name." } }, ["name"]),
4352
- args: z4.object({ name: z4.string().min(1) }),
4804
+ args: z5.object({ name: z5.string().min(1) }),
4353
4805
  run: async (repoRoot, args) => {
4354
4806
  const harness = await loadHarnessOrNull(repoRoot);
4355
4807
  const skill = harness?.skills.find((entry) => entry.name === args.name);
@@ -4361,9 +4813,9 @@ var toolRegistry = [
4361
4813
  }),
4362
4814
  defineTool({
4363
4815
  name: "tools_list",
4364
- 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).",
4365
4817
  inputSchema: objectSchema({}),
4366
- args: z4.object({}),
4818
+ args: z5.object({}),
4367
4819
  run: async (repoRoot) => {
4368
4820
  const harness = await loadHarnessOrNull(repoRoot);
4369
4821
  if (!harness) {
@@ -4374,7 +4826,10 @@ var toolRegistry = [
4374
4826
  name: tool.name,
4375
4827
  description: tool.manifest.description,
4376
4828
  scope: tool.manifest.scope,
4829
+ risk: tool.manifest.risk,
4377
4830
  confirm: tool.manifest.confirm,
4831
+ connection: tool.manifest.connection,
4832
+ healthcheck: Boolean(tool.manifest.healthcheck),
4378
4833
  kind: tool.manifest.run ? "shell" : "script",
4379
4834
  input: tool.manifest.input
4380
4835
  }))
@@ -4385,7 +4840,7 @@ var toolRegistry = [
4385
4840
  name: "tools_check",
4386
4841
  description: "Run configured harness tool healthchecks without running primary tool actions.",
4387
4842
  inputSchema: objectSchema({}),
4388
- args: z4.object({}),
4843
+ args: z5.object({}),
4389
4844
  run: async (repoRoot) => {
4390
4845
  const harness = await loadHarnessOrNull(repoRoot);
4391
4846
  if (!harness) {
@@ -4396,28 +4851,30 @@ var toolRegistry = [
4396
4851
  }),
4397
4852
  defineTool({
4398
4853
  name: "tools_run",
4399
- 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.",
4400
4855
  inputSchema: objectSchema(
4401
4856
  {
4402
4857
  name: { type: "string", description: "Tool name." },
4403
- input: { type: "object", description: "Tool inputs as key/value pairs.", additionalProperties: true },
4404
- confirm: { type: "boolean", description: "Confirm running a tool that requires confirmation." }
4858
+ input: { type: "object", description: "Tool inputs as key/value pairs.", additionalProperties: true }
4405
4859
  },
4406
4860
  ["name"]
4407
4861
  ),
4408
- args: z4.object({
4409
- name: z4.string().min(1),
4410
- input: z4.record(z4.unknown()).optional(),
4411
- confirm: z4.boolean().optional()
4862
+ args: z5.object({
4863
+ name: z5.string().min(1),
4864
+ input: z5.record(z5.unknown()).optional()
4412
4865
  }),
4413
4866
  run: async (repoRoot, args) => {
4414
4867
  const outcome = await runTool(repoRoot, {
4415
4868
  name: args.name,
4416
4869
  input: args.input,
4417
- confirmed: args.confirm
4870
+ confirmed: false
4418
4871
  });
4419
4872
  if (outcome.status === "blocked") {
4420
- 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
+ };
4421
4878
  }
4422
4879
  const { result } = outcome;
4423
4880
  return {
@@ -4440,6 +4897,9 @@ var toolRegistry = [
4440
4897
  description: { type: "string", description: "What the tool does." },
4441
4898
  run: { type: "string", description: "Shell command (use {{param}} for inputs)." },
4442
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." },
4443
4903
  confirm: { type: "boolean", description: "Ask before running. Defaults to true for agents." },
4444
4904
  scope: { type: "string", enum: ["user", "project"], description: "Tool scope." },
4445
4905
  input: {
@@ -4450,14 +4910,17 @@ var toolRegistry = [
4450
4910
  },
4451
4911
  ["name", "description"]
4452
4912
  ),
4453
- args: z4.object({
4454
- name: z4.string().min(1),
4455
- description: z4.string().min(1),
4456
- run: z4.string().optional(),
4457
- script: z4.string().optional(),
4458
- confirm: z4.boolean().optional(),
4459
- scope: z4.enum(["user", "project"]).optional(),
4460
- 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()
4461
4924
  }),
4462
4925
  run: async (repoRoot, args) => {
4463
4926
  const created = await createTool(
@@ -4467,6 +4930,9 @@ var toolRegistry = [
4467
4930
  description: args.description,
4468
4931
  run: args.run,
4469
4932
  script: args.script,
4933
+ risk: args.risk,
4934
+ connection: args.connection,
4935
+ healthcheck: args.healthcheck ? { run: args.healthcheck, expectExitCode: 0 } : void 0,
4470
4936
  confirm: args.confirm,
4471
4937
  scope: args.scope,
4472
4938
  input: args.input
@@ -4480,7 +4946,7 @@ var toolRegistry = [
4480
4946
  name: "tools_detect",
4481
4947
  description: "Propose starter tools from the repo's existing command surface (scripts, Make/just targets).",
4482
4948
  inputSchema: objectSchema({}),
4483
- args: z4.object({}),
4949
+ args: z5.object({}),
4484
4950
  run: async (repoRoot) => {
4485
4951
  const harness = await loadHarnessOrNull(repoRoot);
4486
4952
  const profile = harness?.manifest.profile ?? "empty";
@@ -4491,7 +4957,7 @@ var toolRegistry = [
4491
4957
  name: "connections_list",
4492
4958
  description: "List local CLI connections defined in this repo's harness.",
4493
4959
  inputSchema: objectSchema({}),
4494
- args: z4.object({}),
4960
+ args: z5.object({}),
4495
4961
  run: async (repoRoot) => {
4496
4962
  const harness = await loadHarnessOrNull(repoRoot);
4497
4963
  if (!harness) {
@@ -4514,7 +4980,7 @@ var toolRegistry = [
4514
4980
  name: "connections_check",
4515
4981
  description: "Check local CLI connections and their configured healthchecks.",
4516
4982
  inputSchema: objectSchema({}),
4517
- args: z4.object({}),
4983
+ args: z5.object({}),
4518
4984
  run: (repoRoot) => checkConnections(repoRoot)
4519
4985
  }),
4520
4986
  defineTool({
@@ -4523,7 +4989,7 @@ var toolRegistry = [
4523
4989
  inputSchema: objectSchema({
4524
4990
  type: { type: "string", description: "Memory type (project, repo-map, current-focus, handoff, pitfalls)." }
4525
4991
  }),
4526
- args: z4.object({ type: z4.string().optional() }),
4992
+ args: z5.object({ type: z5.string().optional() }),
4527
4993
  run: async (repoRoot, args) => {
4528
4994
  if (args.type) {
4529
4995
  return { type: args.type, body: await readMemory(repoRoot, args.type) };
@@ -4542,21 +5008,21 @@ var toolRegistry = [
4542
5008
  },
4543
5009
  ["type", "note"]
4544
5010
  ),
4545
- 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) }),
4546
5012
  run: (repoRoot, args) => appendMemory(repoRoot, args.type, args.note)
4547
5013
  }),
4548
5014
  defineTool({
4549
5015
  name: "status",
4550
5016
  description: "Return harness state: manifest, object counts, and drift between canonical and compiled outputs.",
4551
5017
  inputSchema: objectSchema({}),
4552
- args: z4.object({}),
5018
+ args: z5.object({}),
4553
5019
  run: (repoRoot) => harnessStatus(repoRoot)
4554
5020
  }),
4555
5021
  defineTool({
4556
5022
  name: "doctor",
4557
5023
  description: "Check harness validity, compiled output health, MCP hints, and tool trust.",
4558
5024
  inputSchema: objectSchema({}),
4559
- args: z4.object({}),
5025
+ args: z5.object({}),
4560
5026
  run: (repoRoot) => doctor(repoRoot)
4561
5027
  })
4562
5028
  ];
@@ -4598,7 +5064,8 @@ async function handleMessage(repoRoot, request) {
4598
5064
  const params = request.params;
4599
5065
  const result = await callTool(repoRoot, params?.name, params?.arguments ?? {});
4600
5066
  return resultResponse(request, {
4601
- 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
4602
5069
  });
4603
5070
  }
4604
5071
  return errorResponse(request, -32601, `Unknown method: ${request.method ?? "<missing>"}`);
@@ -4737,10 +5204,11 @@ Rules:
4737
5204
  Steps:
4738
5205
  1. Check whether Threadroot is available with \`threadroot --version\`.
4739
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\`.
4740
- 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"\`.
4741
- 4. Run \`threadroot start "current task"\` with the user's actual task.
4742
- 5. If the user asks for provider-native project skill files, run \`threadroot expose <agent>\` or \`threadroot expose all\`.
4743
- 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.
4744
5212
 
4745
5213
  Final response:
4746
5214
  Say exactly:
@@ -4807,11 +5275,11 @@ function agentNotes(agent) {
4807
5275
  }
4808
5276
 
4809
5277
  // src/core/mcp-config.ts
4810
- import { mkdir as mkdir11, readFile as readFile12, writeFile as writeFile11 } from "fs/promises";
4811
- import path28 from "path";
5278
+ import { mkdir as mkdir12, readFile as readFile13, writeFile as writeFile11 } from "fs/promises";
5279
+ import path29 from "path";
4812
5280
  var TARGETS = [
4813
- { agent: "copilot", file: path28.join(".vscode", "mcp.json"), key: "servers" },
4814
- { 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" },
4815
5283
  { agent: "claude", file: ".mcp.json", key: "mcpServers" }
4816
5284
  ];
4817
5285
  function mcpServerEntry(command, scriptPath) {
@@ -4820,7 +5288,7 @@ function mcpServerEntry(command, scriptPath) {
4820
5288
  async function mergeConfig(filePath, key, entry) {
4821
5289
  let config = {};
4822
5290
  try {
4823
- const raw = await readFile12(filePath, "utf8");
5291
+ const raw = await readFile13(filePath, "utf8");
4824
5292
  const parsed = JSON.parse(raw);
4825
5293
  if (parsed && typeof parsed === "object") {
4826
5294
  config = parsed;
@@ -4833,7 +5301,7 @@ async function mergeConfig(filePath, key, entry) {
4833
5301
  const servers = config[key] && typeof config[key] === "object" ? config[key] : {};
4834
5302
  servers.threadroot = { ...entry };
4835
5303
  config[key] = servers;
4836
- await mkdir11(path28.dirname(filePath), { recursive: true });
5304
+ await mkdir12(path29.dirname(filePath), { recursive: true });
4837
5305
  await writeFile11(filePath, `${JSON.stringify(config, null, 2)}
4838
5306
  `, "utf8");
4839
5307
  }
@@ -4842,7 +5310,7 @@ async function writeProjectMcpConfigs(input2) {
4842
5310
  const targets = agents ? TARGETS.filter((target) => agents.includes(target.agent)) : TARGETS;
4843
5311
  const written = [];
4844
5312
  for (const target of targets) {
4845
- const filePath = path28.join(input2.repoRoot, target.file);
5313
+ const filePath = path29.join(input2.repoRoot, target.file);
4846
5314
  await mergeConfig(filePath, target.key, input2.entry);
4847
5315
  written.push(target.file);
4848
5316
  }
@@ -4865,6 +5333,10 @@ async function runMcpSetup(repoRoot, options) {
4865
5333
  const scriptPath = process.argv[1];
4866
5334
  const entry = mcpServerEntry(command, scriptPath);
4867
5335
  const result = await writeProjectMcpConfigs({ repoRoot, entry });
5336
+ if (options.json) {
5337
+ printJson(result);
5338
+ return;
5339
+ }
4868
5340
  console.log("Wrote project MCP config:");
4869
5341
  for (const file of result.written) {
4870
5342
  console.log(`- ${file}`);
@@ -4874,11 +5346,23 @@ async function runMcpSetup(repoRoot, options) {
4874
5346
  }
4875
5347
  return;
4876
5348
  }
4877
- 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
+ }
4878
5355
  }
4879
5356
  async function runMcpCheck(repoRoot, options) {
4880
5357
  const timeoutMs = options.timeout ? Number.parseInt(options.timeout, 10) : void 0;
4881
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
+ }
4882
5366
  console.log(`Threadroot MCP check: ${report.status}`);
4883
5367
  console.log(`config: ${report.configPath}`);
4884
5368
  if (report.entry) {
@@ -4913,9 +5397,21 @@ async function runRemember(repoRoot, note, options = {}) {
4913
5397
  }
4914
5398
 
4915
5399
  // src/commands/skills.ts
4916
- async function runSkillsList(repoRoot) {
5400
+ async function runSkillsList(repoRoot, options = {}) {
4917
5401
  try {
4918
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
+ }
4919
5415
  if (harness.skills.length === 0) {
4920
5416
  console.log("No skills defined. Add folder skills under `.threadroot/skills/<name>/SKILL.md`.");
4921
5417
  return;
@@ -4925,7 +5421,11 @@ async function runSkillsList(repoRoot) {
4925
5421
  }
4926
5422
  } catch (error) {
4927
5423
  if (error instanceof HarnessError) {
4928
- 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
+ }
4929
5429
  return;
4930
5430
  }
4931
5431
  throw error;
@@ -4933,6 +5433,13 @@ async function runSkillsList(repoRoot) {
4933
5433
  }
4934
5434
  async function runSkillsValidate(repoRoot, options = {}) {
4935
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
+ }
4936
5443
  if (report.findings.length === 0) {
4937
5444
  console.log("Skills valid.");
4938
5445
  return;
@@ -4947,8 +5454,12 @@ async function runSkillsValidate(repoRoot, options = {}) {
4947
5454
  process.exitCode = 1;
4948
5455
  }
4949
5456
  }
4950
- async function runSkillsInspect(repoRoot, targetPath) {
5457
+ async function runSkillsInspect(repoRoot, targetPath, options = {}) {
4951
5458
  const inspection = await inspectSkillPath(toRepoPath(repoRoot, targetPath));
5459
+ if (options.json) {
5460
+ printJson(inspection);
5461
+ return;
5462
+ }
4952
5463
  console.log(`${inspection.name}`);
4953
5464
  console.log(`description: ${inspection.description}`);
4954
5465
  console.log(`path: ${inspection.path}`);
@@ -5010,15 +5521,23 @@ async function runSetup(_repoRoot, options) {
5010
5521
  // src/commands/start.ts
5011
5522
  async function runStart(repoRoot, task, options) {
5012
5523
  const report = await startSession(repoRoot, { task: task ?? options.task });
5013
- printStartReport(report);
5524
+ if (options.json) {
5525
+ printJson(report);
5526
+ } else {
5527
+ printStartReport(report);
5528
+ }
5014
5529
  if (!report.status.exists || report.doctor && !report.doctor.ok) {
5015
5530
  process.exitCode = 1;
5016
5531
  }
5017
5532
  }
5018
5533
 
5019
5534
  // src/commands/status.ts
5020
- async function runStatus(repoRoot) {
5535
+ async function runStatus(repoRoot, options = {}) {
5021
5536
  const status = await harnessStatus(repoRoot);
5537
+ if (options.json) {
5538
+ printJson(status);
5539
+ return;
5540
+ }
5022
5541
  if (!status.exists) {
5023
5542
  console.log("No harness found. Run `tr init` first.");
5024
5543
  return;
@@ -5039,261 +5558,16 @@ async function runStatus(repoRoot) {
5039
5558
  }
5040
5559
  }
5041
5560
 
5042
- // src/core/packs/index.ts
5043
- import { cp as cp3, mkdir as mkdir12, readFile as readFile13, readdir as readdir6, stat as stat11 } from "fs/promises";
5044
- import path29 from "path";
5045
- import { fileURLToPath as fileURLToPath2 } from "url";
5046
- import { parse as parseYaml3 } from "yaml";
5047
- import { z as z5 } from "zod";
5048
- var packManifestSchema = z5.object({
5049
- name: z5.string().min(1),
5050
- version: z5.literal(1),
5051
- description: z5.string().min(1),
5052
- skills: z5.array(z5.string()).default([]),
5053
- tools: z5.array(z5.string()).default([]),
5054
- rules: z5.array(z5.string()).default([]),
5055
- connections: z5.array(z5.string()).default([])
5056
- });
5057
- var DIST_DIR2 = path29.dirname(fileURLToPath2(import.meta.url));
5058
- var PACKAGE_ROOT_FROM_BUNDLE2 = path29.resolve(DIST_DIR2, "..");
5059
- var PACKAGE_ROOT_FROM_DIST2 = path29.resolve(DIST_DIR2, "../../..");
5060
- var PACKAGE_ROOT_FROM_SRC2 = path29.resolve(DIST_DIR2, "../../../..");
5061
- var PACK_CANDIDATES = [
5062
- path29.join(PACKAGE_ROOT_FROM_BUNDLE2, "packs"),
5063
- path29.join(PACKAGE_ROOT_FROM_DIST2, "packs"),
5064
- path29.join(PACKAGE_ROOT_FROM_SRC2, "packs")
5065
- ];
5066
- async function exists5(target) {
5067
- try {
5068
- await stat11(target);
5069
- return true;
5070
- } catch (error) {
5071
- if (error.code === "ENOENT") {
5072
- return false;
5073
- }
5074
- throw error;
5075
- }
5076
- }
5077
- async function firstExisting(candidates) {
5078
- for (const candidate of candidates) {
5079
- if (await isPackRoot(candidate)) {
5080
- return candidate;
5081
- }
5082
- }
5083
- return void 0;
5084
- }
5085
- async function bundledPacksDir() {
5086
- return firstExisting(PACK_CANDIDATES);
5087
- }
5088
- async function isPackRoot(candidate) {
5089
- let entries;
5090
- try {
5091
- entries = await readdir6(candidate, { withFileTypes: true });
5092
- } catch {
5093
- return false;
5094
- }
5095
- for (const entry of entries) {
5096
- if (entry.isDirectory() && await exists5(path29.join(candidate, entry.name, "pack.yaml"))) {
5097
- return true;
5098
- }
5099
- }
5100
- return false;
5101
- }
5102
- function safeRelative(ref) {
5103
- const normalized = path29.normalize(ref);
5104
- if (path29.isAbsolute(normalized) || normalized === ".." || normalized.startsWith(`..${path29.sep}`)) {
5105
- throw new Error(`Unsafe pack reference: ${ref}`);
5106
- }
5107
- return normalized;
5108
- }
5109
- async function readPackManifest(packDir) {
5110
- const file = path29.join(packDir, "pack.yaml");
5111
- const parsed = packManifestSchema.safeParse(parseYaml3(await readFile13(file, "utf8")));
5112
- if (!parsed.success) {
5113
- const detail = parsed.error.issues.map((issue) => issue.message).join("; ");
5114
- throw new Error(`Invalid pack manifest ${file}: ${detail}`);
5115
- }
5116
- return parsed.data;
5117
- }
5118
- async function packDirFor(repoRoot, nameOrPath) {
5119
- if (path29.isAbsolute(nameOrPath)) {
5120
- return nameOrPath;
5121
- }
5122
- if (nameOrPath.startsWith(".") || nameOrPath.includes("/") || nameOrPath.includes("\\")) {
5123
- return toRepoPath(repoRoot, nameOrPath);
5124
- }
5125
- const bundled = await bundledPacksDir();
5126
- if (bundled) {
5127
- return path29.join(bundled, nameOrPath);
5128
- }
5129
- return toRepoPath(repoRoot, path29.join("packs", nameOrPath));
5130
- }
5131
- async function directFiles(dir, ext) {
5132
- try {
5133
- const entries = await readdir6(dir, { withFileTypes: true });
5134
- return entries.filter((entry) => entry.isFile() && entry.name.endsWith(ext)).map((entry) => path29.join(dir, entry.name)).sort();
5135
- } catch (error) {
5136
- if (error.code === "ENOENT") {
5137
- return [];
5138
- }
5139
- throw error;
5140
- }
5141
- }
5142
- async function skillEntries(dir) {
5143
- try {
5144
- const entries = await readdir6(dir, { withFileTypes: true });
5145
- const result = [];
5146
- for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
5147
- const full = path29.join(dir, entry.name);
5148
- if (entry.isFile() && entry.name.endsWith(".md")) {
5149
- result.push(full);
5150
- }
5151
- if (entry.isDirectory() && await exists5(path29.join(full, "SKILL.md"))) {
5152
- result.push(full);
5153
- }
5154
- }
5155
- return result;
5156
- } catch (error) {
5157
- if (error.code === "ENOENT") {
5158
- return [];
5159
- }
5160
- throw error;
5161
- }
5162
- }
5163
- async function collectObjects(packDir, manifest) {
5164
- async function resolveRef(ref) {
5165
- const safe = safeRelative(ref);
5166
- const local = path29.resolve(packDir, safe);
5167
- if (await exists5(local)) {
5168
- return local;
5169
- }
5170
- return path29.resolve(packDir, "..", "..", safe);
5171
- }
5172
- return {
5173
- skills: [
5174
- ...await Promise.all(manifest.skills.map(resolveRef)),
5175
- ...await skillEntries(path29.join(packDir, "skills"))
5176
- ],
5177
- tools: [
5178
- ...await Promise.all(manifest.tools.map(resolveRef)),
5179
- ...await directFiles(path29.join(packDir, "tools"), ".yaml")
5180
- ],
5181
- rules: [
5182
- ...await Promise.all(manifest.rules.map(resolveRef)),
5183
- ...await directFiles(path29.join(packDir, "rules"), ".md")
5184
- ],
5185
- connections: [
5186
- ...await Promise.all(manifest.connections.map(resolveRef)),
5187
- ...await directFiles(path29.join(packDir, "connections"), ".yaml")
5188
- ]
5189
- };
5190
- }
5191
- function baseName(source) {
5192
- const parsed = path29.basename(source) === "SKILL.md" ? path29.dirname(source) : source;
5193
- return path29.basename(parsed, path29.extname(parsed));
5194
- }
5195
- async function listPacks(repoRoot) {
5196
- const dirs = [toRepoPath(repoRoot, "packs"), await bundledPacksDir()].filter((dir) => Boolean(dir));
5197
- const seen = /* @__PURE__ */ new Set();
5198
- const packs = [];
5199
- for (const root of dirs) {
5200
- let entries;
5201
- try {
5202
- entries = await readdir6(root, { withFileTypes: true });
5203
- } catch {
5204
- continue;
5205
- }
5206
- for (const entry of entries) {
5207
- if (!entry.isDirectory() || seen.has(entry.name)) {
5208
- continue;
5209
- }
5210
- const packDir = path29.join(root, entry.name);
5211
- if (!await exists5(path29.join(packDir, "pack.yaml"))) {
5212
- continue;
5213
- }
5214
- seen.add(entry.name);
5215
- packs.push(await inspectPack(repoRoot, packDir));
5216
- }
5217
- }
5218
- return packs.sort((a, b) => a.name.localeCompare(b.name));
5219
- }
5220
- async function inspectPack(repoRoot, nameOrPath) {
5221
- const packDir = await packDirFor(repoRoot, nameOrPath);
5222
- const manifest = await readPackManifest(packDir);
5223
- const objects = await collectObjects(packDir, manifest);
5224
- return {
5225
- name: manifest.name,
5226
- description: manifest.description,
5227
- path: packDir,
5228
- skills: objects.skills.map(baseName),
5229
- tools: objects.tools.map(baseName),
5230
- rules: objects.rules.map(baseName),
5231
- connections: objects.connections.map(baseName)
5232
- };
5233
- }
5234
- async function validateProse(file, kind) {
5235
- const target = path29.basename(file) === "SKILL.md" ? file : file;
5236
- const content = await readFile13(target, "utf8");
5237
- const parsed = parseFrontmatter(content);
5238
- const schema = kind === "skill" ? skillFrontmatterSchema : ruleFrontmatterSchema;
5239
- schema.parse(parsed.data);
5240
- }
5241
- async function validateYaml(file, kind) {
5242
- const content = await readFile13(file, "utf8");
5243
- const schema = kind === "tool" ? toolManifestSchema : connectionManifestSchema;
5244
- schema.parse(parseYaml3(content));
5245
- }
5246
- async function validatePack(repoRoot, nameOrPath) {
5247
- const findings = [];
5248
- try {
5249
- const packDir = await packDirFor(repoRoot, nameOrPath);
5250
- const manifest = await readPackManifest(packDir);
5251
- const objects = await collectObjects(packDir, manifest);
5252
- for (const skill of objects.skills) {
5253
- await validateProse(path29.basename(skill) === "SKILL.md" ? skill : path29.join(skill, "SKILL.md"), "skill");
5254
- }
5255
- for (const rule of objects.rules) await validateProse(rule, "rule");
5256
- for (const tool of objects.tools) await validateYaml(tool, "tool");
5257
- for (const connection of objects.connections) await validateYaml(connection, "connection");
5258
- if (Object.values(objects).every((items) => items.length === 0)) {
5259
- findings.push({ severity: "warning", message: "Pack does not include any objects." });
5260
- }
5261
- } catch (error) {
5262
- findings.push({ severity: "error", message: error instanceof Error ? error.message : String(error) });
5263
- }
5264
- return { ok: !findings.some((finding3) => finding3.severity === "error"), findings };
5265
- }
5266
- async function copyObject(source, destDir) {
5267
- const info = await stat11(source);
5268
- const name = baseName(source);
5269
- const dest = info.isDirectory() ? path29.join(destDir, name) : path29.join(destDir, path29.basename(source));
5270
- await mkdir12(destDir, { recursive: true });
5271
- await cp3(source, dest, { recursive: true, force: true });
5272
- return dest;
5273
- }
5274
- async function installPack(repoRoot, nameOrPath) {
5275
- const validation = await validatePack(repoRoot, nameOrPath);
5276
- if (!validation.ok) {
5277
- throw new Error(validation.findings.map((finding3) => finding3.message).join("; "));
5278
- }
5279
- const packDir = await packDirFor(repoRoot, nameOrPath);
5280
- const manifest = await readPackManifest(packDir);
5281
- const objects = await collectObjects(packDir, manifest);
5282
- await Promise.all([
5283
- ...objects.skills.map((source) => copyObject(source, projectObjectDir(repoRoot, "skills"))),
5284
- ...objects.tools.map((source) => copyObject(source, projectObjectDir(repoRoot, "tools"))),
5285
- ...objects.rules.map((source) => copyObject(source, projectObjectDir(repoRoot, "rules"))),
5286
- ...objects.connections.map((source) => copyObject(source, projectObjectDir(repoRoot, "connections")))
5287
- ]);
5288
- return inspectPack(repoRoot, packDir);
5289
- }
5290
-
5291
5561
  // src/commands/packs.ts
5292
5562
  function printList(label, values) {
5293
5563
  console.log(`${label}: ${values.length > 0 ? values.join(", ") : "none"}`);
5294
5564
  }
5295
- async function runPacksList(repoRoot) {
5565
+ async function runPacksList(repoRoot, options = {}) {
5296
5566
  const packs = await listPacks(repoRoot);
5567
+ if (options.json) {
5568
+ printJson({ packs });
5569
+ return;
5570
+ }
5297
5571
  if (packs.length === 0) {
5298
5572
  console.log("No packs found.");
5299
5573
  return;
@@ -5302,8 +5576,12 @@ async function runPacksList(repoRoot) {
5302
5576
  console.log(`${pack.name} - ${pack.description}`);
5303
5577
  }
5304
5578
  }
5305
- async function runPacksInspect(repoRoot, nameOrPath) {
5579
+ async function runPacksInspect(repoRoot, nameOrPath, options = {}) {
5306
5580
  const pack = await inspectPack(repoRoot, nameOrPath);
5581
+ if (options.json) {
5582
+ printJson(pack);
5583
+ return;
5584
+ }
5307
5585
  console.log(pack.name);
5308
5586
  console.log(`description: ${pack.description}`);
5309
5587
  console.log(`path: ${pack.path}`);
@@ -5312,8 +5590,15 @@ async function runPacksInspect(repoRoot, nameOrPath) {
5312
5590
  printList("rules", pack.rules);
5313
5591
  printList("connections", pack.connections);
5314
5592
  }
5315
- async function runPacksValidate(repoRoot, nameOrPath) {
5593
+ async function runPacksValidate(repoRoot, nameOrPath, options = {}) {
5316
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
+ }
5317
5602
  if (report.findings.length === 0) {
5318
5603
  console.log("Pack valid.");
5319
5604
  return;
@@ -5325,8 +5610,12 @@ async function runPacksValidate(repoRoot, nameOrPath) {
5325
5610
  process.exitCode = 1;
5326
5611
  }
5327
5612
  }
5328
- async function runPacksInstall(repoRoot, nameOrPath) {
5613
+ async function runPacksInstall(repoRoot, nameOrPath, options = {}) {
5329
5614
  const pack = await installPack(repoRoot, nameOrPath);
5615
+ if (options.json) {
5616
+ printJson(pack);
5617
+ return;
5618
+ }
5330
5619
  console.log(`Installed pack \`${pack.name}\`.`);
5331
5620
  printList("skills", pack.skills);
5332
5621
  printList("tools", pack.tools);
@@ -5346,17 +5635,37 @@ function parseInputs(pairs = []) {
5346
5635
  }
5347
5636
  return input2;
5348
5637
  }
5349
- async function runToolsList(repoRoot) {
5638
+ async function runToolsList(repoRoot, options = {}) {
5350
5639
  let harness;
5351
5640
  try {
5352
5641
  harness = await resolveHarness(repoRoot);
5353
5642
  } catch (error) {
5354
5643
  if (error instanceof HarnessError) {
5355
- 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
+ }
5356
5649
  return;
5357
5650
  }
5358
5651
  throw error;
5359
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
+ }
5360
5669
  if (harness.tools.length === 0) {
5361
5670
  console.log("No tools defined. Add one with `tr tools add` or `tr tools detect`.");
5362
5671
  return;
@@ -5380,6 +5689,11 @@ async function runToolRun(repoRoot, name, options) {
5380
5689
  timeoutMs: options.timeout ? Number(options.timeout) : void 0
5381
5690
  });
5382
5691
  if (outcome.status === "blocked") {
5692
+ if (options.json) {
5693
+ printJson(outcome);
5694
+ process.exitCode = 1;
5695
+ return;
5696
+ }
5383
5697
  if (outcome.reason === "needs-confirmation") {
5384
5698
  console.log(`${outcome.message} Re-run with --yes to confirm.`);
5385
5699
  } else {
@@ -5389,6 +5703,13 @@ async function runToolRun(repoRoot, name, options) {
5389
5703
  return;
5390
5704
  }
5391
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
+ }
5392
5713
  if (result.stdout) {
5393
5714
  process.stdout.write(result.stdout.endsWith("\n") ? result.stdout : `${result.stdout}
5394
5715
  `);
@@ -5426,7 +5747,11 @@ async function runToolsAdd(repoRoot, name, options) {
5426
5747
  },
5427
5748
  { actor: "human", force: options.force }
5428
5749
  );
5429
- 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
+ }
5430
5755
  }
5431
5756
  function deriveNameFromCommand(command) {
5432
5757
  const first = command.trim().split(/\s+/)[0] ?? "tool";
@@ -5446,29 +5771,42 @@ async function runToolsCreate(repoRoot, options) {
5446
5771
  confirm: options.confirm ?? options.risk === "high"
5447
5772
  });
5448
5773
  }
5449
- async function runToolsCheck(repoRoot) {
5774
+ async function runToolsCheck(repoRoot, options = {}) {
5450
5775
  const harness = await resolveHarness(repoRoot);
5451
5776
  if (harness.tools.length === 0) {
5452
- console.log("No tools defined.");
5777
+ if (options.json) {
5778
+ printJson({ checks: [] });
5779
+ } else {
5780
+ console.log("No tools defined.");
5781
+ }
5453
5782
  return;
5454
5783
  }
5455
5784
  let failures = 0;
5785
+ const checks = [];
5456
5786
  for (const tool of harness.tools) {
5457
5787
  const check = await checkToolHealth(repoRoot, tool);
5458
- if (check.status === "ok") {
5459
- console.log(`${tool.name}: ok`);
5460
- } else if (check.status === "skipped") {
5461
- console.log(`${tool.name}: skipped - ${check.message}`);
5462
- } 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") {
5463
5799
  failures += 1;
5464
- console.log(`${tool.name}: error - ${check.message}`);
5465
5800
  }
5466
5801
  }
5802
+ if (options.json) {
5803
+ printJson({ checks });
5804
+ }
5467
5805
  if (failures > 0) {
5468
5806
  process.exitCode = 1;
5469
5807
  }
5470
5808
  }
5471
- async function runToolsDetect(repoRoot) {
5809
+ async function runToolsDetect(repoRoot, options = {}) {
5472
5810
  let profile;
5473
5811
  try {
5474
5812
  profile = (await resolveHarness(repoRoot)).manifest.profile;
@@ -5478,6 +5816,10 @@ async function runToolsDetect(repoRoot) {
5478
5816
  }
5479
5817
  }
5480
5818
  const candidates = await detectToolCandidates(repoRoot, profile ?? "empty");
5819
+ if (options.json) {
5820
+ printJson({ candidates });
5821
+ return;
5822
+ }
5481
5823
  if (candidates.length === 0) {
5482
5824
  console.log("No starter tools detected.");
5483
5825
  return;
@@ -5491,17 +5833,40 @@ async function runToolsDetect(repoRoot) {
5491
5833
  }
5492
5834
 
5493
5835
  // src/commands/connections.ts
5494
- 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 = {}) {
5495
5840
  let harness;
5496
5841
  try {
5497
5842
  harness = await resolveHarness(repoRoot);
5498
5843
  } catch (error) {
5499
5844
  if (error instanceof HarnessError) {
5500
- 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
+ }
5501
5850
  return;
5502
5851
  }
5503
5852
  throw error;
5504
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
+ }
5505
5870
  if (harness.connections.length === 0) {
5506
5871
  console.log("No connections defined. Add one with `tr connections add`.");
5507
5872
  return;
@@ -5528,14 +5893,27 @@ async function runConnectionsAdd(repoRoot, name, options) {
5528
5893
  risk: options.risk,
5529
5894
  confirm: options.confirm,
5530
5895
  healthcheck: options.healthcheck,
5896
+ allow: parseList(options.allow),
5897
+ deny: parseList(options.deny),
5531
5898
  scope: options.scope
5532
5899
  },
5533
5900
  { force: options.force }
5534
5901
  );
5535
- 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
+ }
5536
5907
  }
5537
- async function runConnectionsCheck(repoRoot) {
5908
+ async function runConnectionsCheck(repoRoot, options = {}) {
5538
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
+ }
5539
5917
  if (checks.length === 0) {
5540
5918
  console.log("No connections defined.");
5541
5919
  return;
@@ -5556,45 +5934,45 @@ async function runConnectionsCheck(repoRoot) {
5556
5934
  function createProgram(repoRoot = process.cwd()) {
5557
5935
  const program = new Command();
5558
5936
  program.name("threadroot").description("Git for your AI agent harness: one command to bootstrap, one .threadroot source.").version(THREADROOT_VERSION);
5559
- 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));
5560
- 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));
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));
5561
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));
5562
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));
5563
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));
5564
- 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));
5565
5943
  program.command("diff").description("Show the diff between the canonical harness and each compiled vendor file.").action(() => runDiff(repoRoot));
5566
- 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));
5567
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));
5568
- 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));
5569
- 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));
5570
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));
5571
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));
5572
5950
  const memory = program.command("memory").description("Read and append durable harness memory.");
5573
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));
5574
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));
5575
5953
  const tools2 = program.command("tools").description("Manage executable harness tools.");
5576
- tools2.command("list").description("List harness tools.").action(() => runToolsList(repoRoot));
5577
- tools2.command("check").description("Run configured tool healthchecks.").action(() => runToolsCheck(repoRoot));
5578
- tools2.command("detect").description("Propose starter tools from the repo's existing command surface.").action(() => runToolsDetect(repoRoot));
5579
- 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));
5580
- 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));
5581
5959
  const connections = program.command("connections").description("Manage local CLI connections.");
5582
- connections.command("list").description("List harness connections.").action(() => runConnectionsList(repoRoot));
5583
- connections.command("check").description("Run configured connection healthchecks.").action(() => runConnectionsCheck(repoRoot));
5584
- 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));
5585
5963
  const packs = program.command("packs").description("Inspect, validate, and install capability packs.");
5586
- packs.command("list").description("List built-in and repo-local packs.").action(() => runPacksList(repoRoot));
5587
- 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));
5588
- 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));
5589
- 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));
5590
5968
  const skills = program.command("skills").description("Inspect and validate harness skills.");
5591
- skills.command("list").description("List harness skills.").action(() => runSkillsList(repoRoot));
5592
- 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));
5593
- 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));
5594
5972
  const mcp = program.command("mcp").description("Run or configure the local Threadroot MCP server.");
5595
5973
  mcp.action(() => runMcp(repoRoot));
5596
- 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));
5597
- 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));
5598
5976
  return program;
5599
5977
  }
5600
5978