threadroot 0.1.0

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.
Files changed (39) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/LICENSE +21 -0
  3. package/README.md +261 -0
  4. package/SECURITY.md +22 -0
  5. package/dist/index.d.ts +1 -0
  6. package/dist/index.js +4341 -0
  7. package/package.json +81 -0
  8. package/packs/code-review/pack.yaml +7 -0
  9. package/packs/python/pack.yaml +10 -0
  10. package/packs/react-app/pack.yaml +10 -0
  11. package/packs/security-review/pack.yaml +7 -0
  12. package/packs/system-design/pack.yaml +5 -0
  13. package/packs/testing/pack.yaml +7 -0
  14. package/packs/typescript-node/pack.yaml +9 -0
  15. package/skills/README.md +39 -0
  16. package/skills/add-test/SKILL.md +20 -0
  17. package/skills/add-test/evals/triggers.json +12 -0
  18. package/skills/build-skill/SKILL.md +36 -0
  19. package/skills/build-skill/evals/triggers.json +16 -0
  20. package/skills/build-skill/references/eval-prompts.md +20 -0
  21. package/skills/build-skill/references/skill-quality.md +18 -0
  22. package/skills/build-tool/SKILL.md +35 -0
  23. package/skills/build-tool/evals/triggers.json +13 -0
  24. package/skills/catalog.json +50 -0
  25. package/skills/code-review/SKILL.md +24 -0
  26. package/skills/code-review/evals/triggers.json +12 -0
  27. package/skills/conventional-commits/SKILL.md +29 -0
  28. package/skills/conventional-commits/evals/triggers.json +12 -0
  29. package/skills/debug-failure/SKILL.md +21 -0
  30. package/skills/debug-failure/evals/triggers.json +12 -0
  31. package/skills/security-review/SKILL.md +24 -0
  32. package/skills/security-review/evals/triggers.json +12 -0
  33. package/skills/system-design/SKILL.md +33 -0
  34. package/skills/system-design/evals/triggers.json +17 -0
  35. package/skills/system-design/references/api-data-design.md +15 -0
  36. package/skills/system-design/references/architecture-checklist.md +26 -0
  37. package/skills/system-design/references/reliability-observability.md +17 -0
  38. package/skills/write-docs/SKILL.md +20 -0
  39. package/skills/write-docs/evals/triggers.json +12 -0
package/dist/index.js ADDED
@@ -0,0 +1,4341 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/cli.ts
4
+ import { Command } from "commander";
5
+
6
+ // src/core/compile/write.ts
7
+ import { mkdir as mkdir2, writeFile as writeFile2 } from "fs/promises";
8
+ import path6 from "path";
9
+
10
+ // src/core/harness/schema.ts
11
+ import { z as z2 } from "zod";
12
+
13
+ // src/types.ts
14
+ import { z } from "zod";
15
+ var profileIdSchema = z.enum([
16
+ "nextjs",
17
+ "vite-react",
18
+ "fastapi",
19
+ "python-cli",
20
+ "node-cli",
21
+ "dbt",
22
+ "empty"
23
+ ]);
24
+
25
+ // src/core/harness/schema.ts
26
+ var objectScopeSchema = z2.enum(["user", "project"]);
27
+ var riskLevelSchema = z2.enum(["low", "medium", "high"]);
28
+ var adapterIdSchema = z2.enum(["agents", "claude", "copilot", "cursor"]);
29
+ var memoryTypeSchema = z2.enum(["project", "repo-map", "current-focus", "handoff", "pitfalls"]);
30
+ var referenceSchema = z2.object({
31
+ path: z2.string().min(1),
32
+ description: z2.string().optional(),
33
+ load: z2.enum(["link", "eager"]).default("link")
34
+ });
35
+ var harnessManifestSchema = z2.object({
36
+ name: z2.string().min(1),
37
+ version: z2.literal(1),
38
+ profile: profileIdSchema,
39
+ adapters: z2.array(adapterIdSchema).min(1),
40
+ references: z2.array(referenceSchema).default([]),
41
+ memory: z2.object({
42
+ budget: z2.record(memoryTypeSchema, z2.number().int().positive()).default({})
43
+ }).default({ budget: {} }),
44
+ tools: z2.object({
45
+ allow: z2.array(z2.string()).default([])
46
+ }).default({ allow: [] })
47
+ });
48
+ var skillFrontmatterSchema = z2.object({
49
+ name: z2.string().min(1),
50
+ description: z2.string().min(1).optional(),
51
+ when: z2.string().min(1).optional(),
52
+ license: z2.string().min(1).optional(),
53
+ compatibility: z2.string().max(500).optional(),
54
+ metadata: z2.record(z2.unknown()).optional(),
55
+ allowedTools: z2.union([z2.string(), z2.array(z2.string())]).optional(),
56
+ "allowed-tools": z2.union([z2.string(), z2.array(z2.string())]).optional(),
57
+ scope: objectScopeSchema.default("project"),
58
+ tags: z2.array(z2.string()).default([])
59
+ }).refine((skill) => Boolean(skill.description ?? skill.when), {
60
+ message: "A skill must define `description` or legacy `when`.",
61
+ path: ["description"]
62
+ }).transform((skill) => {
63
+ const trigger = skill.description ?? skill.when;
64
+ const allowedTools = skill.allowedTools ?? skill["allowed-tools"];
65
+ return {
66
+ ...skill,
67
+ description: skill.description ?? trigger,
68
+ when: skill.when ?? trigger,
69
+ allowedTools,
70
+ "allowed-tools": void 0
71
+ };
72
+ });
73
+ var ruleFrontmatterSchema = z2.object({
74
+ name: z2.string().min(1),
75
+ applyTo: z2.string().min(1).optional(),
76
+ scope: objectScopeSchema.default("project")
77
+ });
78
+ var toolInputParamSchema = z2.object({
79
+ type: z2.enum(["string", "number", "boolean"]).default("string"),
80
+ description: z2.string().optional(),
81
+ default: z2.union([z2.string(), z2.number(), z2.boolean()]).optional()
82
+ });
83
+ var healthcheckSchema = z2.object({
84
+ run: z2.string().min(1),
85
+ expectExitCode: z2.number().int().default(0)
86
+ });
87
+ var toolManifestSchema = z2.object({
88
+ name: z2.string().min(1),
89
+ description: z2.string().min(1),
90
+ scope: objectScopeSchema.default("project"),
91
+ risk: riskLevelSchema.default("low"),
92
+ confirm: z2.boolean().default(false),
93
+ connection: z2.string().min(1).optional(),
94
+ healthcheck: healthcheckSchema.optional(),
95
+ input: z2.record(z2.string(), toolInputParamSchema).default({}),
96
+ run: z2.string().min(1).optional(),
97
+ script: z2.string().min(1).optional()
98
+ }).refine((tool) => Boolean(tool.run) !== Boolean(tool.script), {
99
+ message: "A tool must define exactly one of `run` or `script`.",
100
+ path: ["run"]
101
+ });
102
+ var connectionManifestSchema = z2.object({
103
+ name: z2.string().min(1),
104
+ provider: z2.string().min(1),
105
+ kind: z2.literal("cli").default("cli"),
106
+ command: z2.string().min(1),
107
+ profile: z2.string().min(1).optional(),
108
+ description: z2.string().min(1),
109
+ scope: objectScopeSchema.default("project"),
110
+ risk: riskLevelSchema.default("medium"),
111
+ confirm: z2.boolean().default(false),
112
+ healthcheck: healthcheckSchema.optional(),
113
+ allow: z2.array(z2.string()).default([]),
114
+ deny: z2.array(z2.string()).default([])
115
+ });
116
+
117
+ // src/core/harness/paths.ts
118
+ import os from "os";
119
+ import path2 from "path";
120
+
121
+ // src/core/paths.ts
122
+ import path from "path";
123
+ function toRepoPath(repoRoot, relativePath) {
124
+ if (path.isAbsolute(relativePath)) {
125
+ throw new Error(`Refusing to access an absolute repository path: ${relativePath}`);
126
+ }
127
+ const root = path.resolve(repoRoot);
128
+ const resolved = path.resolve(root, relativePath);
129
+ const relative = path.relative(root, resolved);
130
+ if (relative === "" || relative.startsWith("..") || path.isAbsolute(relative)) {
131
+ throw new Error(`Refusing to access a path outside the repository: ${relativePath}`);
132
+ }
133
+ return resolved;
134
+ }
135
+
136
+ // src/core/harness/paths.ts
137
+ var HARNESS_DIR = ".threadroot";
138
+ var HARNESS_MANIFEST = "harness.yaml";
139
+ var LOCK_FILE = "lock.json";
140
+ var HARNESS_SUBDIRS = {
141
+ skills: "skills",
142
+ tools: "tools",
143
+ connections: "connections",
144
+ rules: "rules",
145
+ memory: "memory"
146
+ };
147
+ var HARNESS_OBJECT_EXT = {
148
+ prose: ".md",
149
+ tool: ".yaml"
150
+ };
151
+ function projectHarnessDir(repoRoot) {
152
+ return toRepoPath(repoRoot, HARNESS_DIR);
153
+ }
154
+ function projectManifestPath(repoRoot) {
155
+ return toRepoPath(repoRoot, path2.join(HARNESS_DIR, HARNESS_MANIFEST));
156
+ }
157
+ function projectLockPath(repoRoot) {
158
+ return toRepoPath(repoRoot, path2.join(HARNESS_DIR, LOCK_FILE));
159
+ }
160
+ function projectObjectDir(repoRoot, dir) {
161
+ return toRepoPath(repoRoot, path2.join(HARNESS_DIR, HARNESS_SUBDIRS[dir]));
162
+ }
163
+ function userHarnessDir(home = os.homedir()) {
164
+ return path2.join(home, HARNESS_DIR);
165
+ }
166
+ function userObjectDir(dir, home = os.homedir()) {
167
+ return path2.join(userHarnessDir(home), HARNESS_SUBDIRS[dir]);
168
+ }
169
+ function userLockPath(home = os.homedir()) {
170
+ return path2.join(userHarnessDir(home), LOCK_FILE);
171
+ }
172
+
173
+ // src/core/harness/frontmatter.ts
174
+ import { parse as parseYaml, stringify as stringifyYaml } from "yaml";
175
+ var FRONTMATTER_RE = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/;
176
+ function parseFrontmatter(raw) {
177
+ const normalized = raw.replace(/^\uFEFF/, "");
178
+ const match = FRONTMATTER_RE.exec(normalized);
179
+ if (!match) {
180
+ return { data: {}, body: normalized.trim() };
181
+ }
182
+ const parsed = parseYaml(match[1] ?? "");
183
+ const data = parsed && typeof parsed === "object" ? parsed : {};
184
+ return { data, body: (match[2] ?? "").trim() };
185
+ }
186
+ function serializeFrontmatter(data, body) {
187
+ const front = stringifyYaml(data).trimEnd();
188
+ return `---
189
+ ${front}
190
+ ---
191
+
192
+ ${body.trim()}
193
+ `;
194
+ }
195
+
196
+ // src/core/harness/load.ts
197
+ import { readFile, readdir } from "fs/promises";
198
+ import path3 from "path";
199
+ import { parse as parseYaml2 } from "yaml";
200
+ var HarnessError = class extends Error {
201
+ constructor(message) {
202
+ super(message);
203
+ this.name = "HarnessError";
204
+ }
205
+ };
206
+ async function readObjectFiles(dir, ext) {
207
+ let entries;
208
+ try {
209
+ entries = await readdir(dir);
210
+ } catch (error) {
211
+ if (error.code === "ENOENT") {
212
+ return [];
213
+ }
214
+ throw error;
215
+ }
216
+ const files = entries.filter((name) => name.endsWith(ext)).sort();
217
+ return Promise.all(
218
+ files.map(async (name) => {
219
+ const full = path3.join(dir, name);
220
+ return { path: full, content: await readFile(full, "utf8") };
221
+ })
222
+ );
223
+ }
224
+ async function readSkillFiles(dir) {
225
+ let entries;
226
+ try {
227
+ entries = await readdir(dir, { withFileTypes: true });
228
+ } catch (error) {
229
+ if (error.code === "ENOENT") {
230
+ return [];
231
+ }
232
+ throw error;
233
+ }
234
+ const files = [];
235
+ for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
236
+ if (entry.isFile() && entry.name.endsWith(HARNESS_OBJECT_EXT.prose)) {
237
+ const full = path3.join(dir, entry.name);
238
+ files.push({ path: full, content: await readFile(full, "utf8") });
239
+ continue;
240
+ }
241
+ if (entry.isDirectory()) {
242
+ const full = path3.join(dir, entry.name, "SKILL.md");
243
+ try {
244
+ files.push({ path: full, content: await readFile(full, "utf8") });
245
+ } catch (error) {
246
+ if (error.code !== "ENOENT") {
247
+ throw error;
248
+ }
249
+ }
250
+ }
251
+ }
252
+ return files;
253
+ }
254
+ function objectDirFor(repoRoot, dir, origin, home) {
255
+ return origin === "project" ? projectObjectDir(repoRoot, dir) : userObjectDir(dir, home);
256
+ }
257
+ function describe(error) {
258
+ if (error && typeof error === "object" && "issues" in error) {
259
+ const issues = error.issues;
260
+ return issues.map((issue) => `${issue.path.join(".") || "<root>"}: ${issue.message}`).join("; ");
261
+ }
262
+ return error instanceof Error ? error.message : String(error);
263
+ }
264
+ async function loadSkillsFrom(dir, origin) {
265
+ const files = await readSkillFiles(dir);
266
+ return files.map((file) => {
267
+ const { data, body } = parseFrontmatter(file.content);
268
+ const result = skillFrontmatterSchema.safeParse(data);
269
+ if (!result.success) {
270
+ throw new HarnessError(`Invalid skill ${file.path}: ${describe(result.error)}`);
271
+ }
272
+ return { name: result.data.name, origin, sourcePath: file.path, frontmatter: result.data, body };
273
+ });
274
+ }
275
+ async function loadRulesFrom(dir, origin) {
276
+ const files = await readObjectFiles(dir, HARNESS_OBJECT_EXT.prose);
277
+ return files.map((file) => {
278
+ const { data, body } = parseFrontmatter(file.content);
279
+ const result = ruleFrontmatterSchema.safeParse(data);
280
+ if (!result.success) {
281
+ throw new HarnessError(`Invalid rule ${file.path}: ${describe(result.error)}`);
282
+ }
283
+ return { name: result.data.name, origin, sourcePath: file.path, frontmatter: result.data, body };
284
+ });
285
+ }
286
+ async function loadToolsFrom(dir, origin) {
287
+ const files = await readObjectFiles(dir, HARNESS_OBJECT_EXT.tool);
288
+ return files.map((file) => {
289
+ const parsed = parseYaml2(file.content);
290
+ const result = toolManifestSchema.safeParse(parsed);
291
+ if (!result.success) {
292
+ throw new HarnessError(`Invalid tool ${file.path}: ${describe(result.error)}`);
293
+ }
294
+ return { name: result.data.name, origin, sourcePath: file.path, manifest: result.data };
295
+ });
296
+ }
297
+ async function loadConnectionsFrom(dir, origin) {
298
+ const files = await readObjectFiles(dir, HARNESS_OBJECT_EXT.tool);
299
+ return files.map((file) => {
300
+ const parsed = parseYaml2(file.content);
301
+ const result = connectionManifestSchema.safeParse(parsed);
302
+ if (!result.success) {
303
+ throw new HarnessError(`Invalid connection ${file.path}: ${describe(result.error)}`);
304
+ }
305
+ return { name: result.data.name, origin, sourcePath: file.path, manifest: result.data };
306
+ });
307
+ }
308
+ async function loadMemoryFrom(dir, origin) {
309
+ const files = await readObjectFiles(dir, HARNESS_OBJECT_EXT.prose);
310
+ const memory = [];
311
+ for (const file of files) {
312
+ const base = path3.basename(file.path, HARNESS_OBJECT_EXT.prose);
313
+ const type = memoryTypeSchema.safeParse(base);
314
+ if (!type.success) {
315
+ continue;
316
+ }
317
+ const { body } = parseFrontmatter(file.content);
318
+ memory.push({ type: type.data, origin, sourcePath: file.path, body });
319
+ }
320
+ return memory;
321
+ }
322
+ function mergeByName(user, project) {
323
+ const merged = /* @__PURE__ */ new Map();
324
+ for (const item of user) {
325
+ merged.set(item.name, item);
326
+ }
327
+ for (const item of project) {
328
+ merged.set(item.name, item);
329
+ }
330
+ return [...merged.values()].sort((a, b) => a.name.localeCompare(b.name));
331
+ }
332
+ async function loadManifest(repoRoot) {
333
+ const manifestPath = projectManifestPath(repoRoot);
334
+ let raw;
335
+ try {
336
+ raw = await readFile(manifestPath, "utf8");
337
+ } catch (error) {
338
+ if (error.code === "ENOENT") {
339
+ throw new HarnessError(`No harness found at ${manifestPath}. Run \`tr init\` first.`);
340
+ }
341
+ throw error;
342
+ }
343
+ const result = harnessManifestSchema.safeParse(parseYaml2(raw));
344
+ if (!result.success) {
345
+ throw new HarnessError(`Invalid ${manifestPath}: ${describe(result.error)}`);
346
+ }
347
+ return result.data;
348
+ }
349
+ async function resolveHarness(repoRoot, opts = {}) {
350
+ const { home } = opts;
351
+ const manifest = await loadManifest(repoRoot);
352
+ const [
353
+ userSkills,
354
+ projectSkills,
355
+ userRules,
356
+ projectRules,
357
+ userTools,
358
+ projectTools,
359
+ userConnections,
360
+ projectConnections,
361
+ userMemory,
362
+ projectMemory
363
+ ] = await Promise.all([
364
+ loadSkillsFrom(objectDirFor(repoRoot, "skills", "user", home), "user"),
365
+ loadSkillsFrom(objectDirFor(repoRoot, "skills", "project", home), "project"),
366
+ loadRulesFrom(objectDirFor(repoRoot, "rules", "user", home), "user"),
367
+ loadRulesFrom(objectDirFor(repoRoot, "rules", "project", home), "project"),
368
+ loadToolsFrom(objectDirFor(repoRoot, "tools", "user", home), "user"),
369
+ loadToolsFrom(objectDirFor(repoRoot, "tools", "project", home), "project"),
370
+ loadConnectionsFrom(objectDirFor(repoRoot, "connections", "user", home), "user"),
371
+ loadConnectionsFrom(objectDirFor(repoRoot, "connections", "project", home), "project"),
372
+ loadMemoryFrom(objectDirFor(repoRoot, "memory", "user", home), "user"),
373
+ loadMemoryFrom(objectDirFor(repoRoot, "memory", "project", home), "project")
374
+ ]);
375
+ return {
376
+ manifest,
377
+ skills: mergeByName(userSkills, projectSkills),
378
+ rules: mergeByName(userRules, projectRules),
379
+ tools: mergeByName(userTools, projectTools),
380
+ connections: mergeByName(userConnections, projectConnections),
381
+ memory: [...userMemory, ...projectMemory]
382
+ };
383
+ }
384
+
385
+ // src/core/harness/context.ts
386
+ function taskTerms(task) {
387
+ return [
388
+ ...new Set(
389
+ task.toLowerCase().split(/[^a-z0-9+#.-]+/).filter((term) => term.length > 2)
390
+ )
391
+ ];
392
+ }
393
+ function scoreSkill(haystack, terms) {
394
+ const lower = haystack.toLowerCase();
395
+ return terms.reduce((score, term) => score + (lower.includes(term) ? 1 : 0), 0);
396
+ }
397
+ async function assembleContext(repoRoot, task, options = {}) {
398
+ const harness = options.harness ?? await resolveHarness(repoRoot, { home: options.home });
399
+ const terms = taskTerms(task);
400
+ const ranked = harness.skills.map((skill) => ({
401
+ skill,
402
+ score: scoreSkill(`${skill.name} ${skill.frontmatter.when} ${skill.frontmatter.tags.join(" ")}`, terms)
403
+ })).filter((entry) => entry.score > 0).sort((a, b) => b.score - a.score || a.skill.name.localeCompare(b.skill.name)).slice(0, options.limit ?? 8).map(({ skill, score }) => ({
404
+ name: skill.name,
405
+ when: skill.frontmatter.when,
406
+ tags: skill.frontmatter.tags,
407
+ scope: skill.frontmatter.scope,
408
+ sourcePath: skill.sourcePath,
409
+ score
410
+ }));
411
+ return {
412
+ task,
413
+ skills: ranked,
414
+ rules: harness.rules.map((rule) => ({ name: rule.name, applyTo: rule.frontmatter.applyTo })),
415
+ tools: harness.tools.map((tool) => ({
416
+ name: tool.name,
417
+ description: tool.manifest.description,
418
+ confirm: tool.manifest.confirm,
419
+ risk: tool.manifest.risk,
420
+ connection: tool.manifest.connection,
421
+ healthcheck: Boolean(tool.manifest.healthcheck),
422
+ kind: tool.manifest.run ? "shell" : "script"
423
+ })),
424
+ memory: harness.memory.map((entry) => ({ type: entry.type, body: entry.body }))
425
+ };
426
+ }
427
+
428
+ // src/core/harness/memory.ts
429
+ import { mkdir, readFile as readFile2, writeFile } from "fs/promises";
430
+ import path4 from "path";
431
+ function assertMemoryType(type) {
432
+ const parsed = memoryTypeSchema.safeParse(type);
433
+ if (!parsed.success) {
434
+ throw new HarnessError(
435
+ `Unknown memory type \`${type}\`. Expected one of: ${memoryTypeSchema.options.join(", ")}.`
436
+ );
437
+ }
438
+ return parsed.data;
439
+ }
440
+ function memoryDir(repoRoot, scope, home) {
441
+ return scope === "project" ? projectObjectDir(repoRoot, "memory") : userObjectDir("memory", home);
442
+ }
443
+ function memoryFilePath(repoRoot, type, scope = "project", home) {
444
+ return path4.join(memoryDir(repoRoot, scope, home), `${type}${HARNESS_OBJECT_EXT.prose}`);
445
+ }
446
+ async function readMemory(repoRoot, type, options = {}) {
447
+ const memoryType = assertMemoryType(type);
448
+ const file = memoryFilePath(repoRoot, memoryType, options.scope ?? "project", options.home);
449
+ try {
450
+ const raw = await readFile2(file, "utf8");
451
+ return parseFrontmatter(raw).body;
452
+ } catch (error) {
453
+ if (error.code === "ENOENT") {
454
+ return null;
455
+ }
456
+ throw error;
457
+ }
458
+ }
459
+ function headingFor(type) {
460
+ const title = type.replace(/-/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
461
+ return `# ${title}`;
462
+ }
463
+ async function appendMemory(repoRoot, type, note, options = {}) {
464
+ const memoryType = assertMemoryType(type);
465
+ const trimmed = note.trim();
466
+ if (!trimmed) {
467
+ throw new HarnessError("Cannot append an empty memory note.");
468
+ }
469
+ const scope = options.scope ?? "project";
470
+ const dir = memoryDir(repoRoot, scope, options.home);
471
+ const file = memoryFilePath(repoRoot, memoryType, scope, options.home);
472
+ let existing = "";
473
+ try {
474
+ existing = await readFile2(file, "utf8");
475
+ } catch (error) {
476
+ if (error.code !== "ENOENT") {
477
+ throw error;
478
+ }
479
+ }
480
+ await mkdir(dir, { recursive: true });
481
+ const base = existing.trim() ? existing.replace(/\s*$/, "") : headingFor(memoryType);
482
+ await writeFile(file, `${base}
483
+ - ${trimmed}
484
+ `, "utf8");
485
+ return { type: memoryType, scope, path: file };
486
+ }
487
+
488
+ // src/core/compile/index.ts
489
+ import { readFile as readFile3, stat } from "fs/promises";
490
+ import path5 from "path";
491
+
492
+ // src/core/hash.ts
493
+ import { createHash } from "crypto";
494
+ function hashContent(content) {
495
+ return createHash("sha256").update(content).digest("hex");
496
+ }
497
+
498
+ // src/core/compile/adapters/agents.ts
499
+ var agentsAdapter = {
500
+ id: "agents",
501
+ compile(ctx) {
502
+ return [{ path: "AGENTS.md", content: ctx.canonicalAgents }];
503
+ }
504
+ };
505
+
506
+ // src/core/compile/adapters/shared.ts
507
+ function slug(name) {
508
+ return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "rule";
509
+ }
510
+ function ruleBody(rule) {
511
+ const body = rule.body.trim();
512
+ if (body.startsWith("#")) {
513
+ return body;
514
+ }
515
+ return `# ${rule.name}
516
+
517
+ ${body}`;
518
+ }
519
+
520
+ // src/core/compile/adapters/claude.ts
521
+ function claudeRoot() {
522
+ const lines = [
523
+ "# CLAUDE.md",
524
+ "",
525
+ "@AGENTS.md",
526
+ "",
527
+ "<!-- The shared harness lives in AGENTS.md (imported above). Claude-specific",
528
+ " guidance can be added below this comment; it is preserved across compiles. -->"
529
+ ];
530
+ return `${lines.join("\n")}
531
+ `;
532
+ }
533
+ function ruleFile(name, applyTo, body) {
534
+ const frontmatter = ["---", `name: ${name}`, "paths:", ` - "${applyTo}"`, "---", ""].join("\n");
535
+ return { path: `.claude/rules/${slug(name)}.md`, content: `${frontmatter}
536
+ ${body}
537
+ ` };
538
+ }
539
+ function commandFile(name, description, run2, script) {
540
+ const frontmatter = ["---", `description: ${description}`, "---", ""].join("\n");
541
+ const invocation = run2 ? `Run this shell command and report the result:
542
+
543
+ \`\`\`sh
544
+ ${run2}
545
+ \`\`\`` : `Run the script at \`.threadroot/tools/${script}\` and report the result.`;
546
+ const body = [`# /${slug(name)}`, "", description, "", invocation].join("\n");
547
+ return { path: `.claude/commands/${slug(name)}.md`, content: `${frontmatter}
548
+ ${body}
549
+ ` };
550
+ }
551
+ var claudeAdapter = {
552
+ id: "claude",
553
+ compile(ctx) {
554
+ const files = [{ path: "CLAUDE.md", content: claudeRoot() }];
555
+ for (const rule of ctx.scopedRules) {
556
+ const applyTo = rule.frontmatter.applyTo;
557
+ if (applyTo) {
558
+ files.push(ruleFile(rule.name, applyTo, ruleBody(rule)));
559
+ }
560
+ }
561
+ for (const tool of ctx.tools) {
562
+ files.push(
563
+ commandFile(tool.name, tool.manifest.description, tool.manifest.run, tool.manifest.script)
564
+ );
565
+ }
566
+ return files;
567
+ }
568
+ };
569
+
570
+ // src/core/compile/adapters/copilot.ts
571
+ function instructionsFile(name, applyTo, body) {
572
+ const frontmatter = ["---", `name: ${name}`, `applyTo: "${applyTo}"`, "---", ""].join("\n");
573
+ return {
574
+ path: `.github/instructions/${slug(name)}.instructions.md`,
575
+ content: `${frontmatter}
576
+ ${body}
577
+ `
578
+ };
579
+ }
580
+ var copilotAdapter = {
581
+ id: "copilot",
582
+ compile(ctx) {
583
+ const files = [
584
+ { path: ".github/copilot-instructions.md", content: ctx.canonicalAgents }
585
+ ];
586
+ for (const rule of ctx.scopedRules) {
587
+ const applyTo = rule.frontmatter.applyTo;
588
+ if (applyTo) {
589
+ files.push(instructionsFile(rule.name, applyTo, ruleBody(rule)));
590
+ }
591
+ }
592
+ return files;
593
+ }
594
+ };
595
+
596
+ // src/core/compile/adapters/cursor.ts
597
+ function mdcFile(name, applyTo, body) {
598
+ const frontmatter = [
599
+ "---",
600
+ `description: ${name}`,
601
+ `globs: ${applyTo}`,
602
+ "alwaysApply: false",
603
+ "---",
604
+ ""
605
+ ].join("\n");
606
+ return { path: `.cursor/rules/${slug(name)}.mdc`, content: `${frontmatter}
607
+ ${body}
608
+ ` };
609
+ }
610
+ var cursorAdapter = {
611
+ id: "cursor",
612
+ compile(ctx) {
613
+ const files = [];
614
+ for (const rule of ctx.scopedRules) {
615
+ const applyTo = rule.frontmatter.applyTo;
616
+ if (applyTo) {
617
+ files.push(mdcFile(rule.name, applyTo, ruleBody(rule)));
618
+ }
619
+ }
620
+ return files;
621
+ }
622
+ };
623
+
624
+ // src/core/compile/managed.ts
625
+ var MANAGED_BEGIN = "<!-- threadroot:begin (generated \u2014 edit sources in .threadroot/) -->";
626
+ var MANAGED_END = "<!-- threadroot:end -->";
627
+ var BLOCK_RE = new RegExp(
628
+ `\\n*${escapeRegExp(MANAGED_BEGIN)}[\\s\\S]*?${escapeRegExp(MANAGED_END)}\\n*`,
629
+ "g"
630
+ );
631
+ function escapeRegExp(value) {
632
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
633
+ }
634
+ function extractHandAuthored(content) {
635
+ return content.replace(BLOCK_RE, "\n").trim();
636
+ }
637
+ function composeWithManaged(handAuthored, managed) {
638
+ const head = handAuthored.trim();
639
+ const block = `${MANAGED_BEGIN}
640
+ ${managed.trim()}
641
+ ${MANAGED_END}`;
642
+ return head ? `${head}
643
+
644
+ ${block}
645
+ ` : `${block}
646
+ `;
647
+ }
648
+
649
+ // src/core/compile/sections.ts
650
+ var DEFAULT_PROJECT_BUDGET = 4e3;
651
+ function truncate(text, max) {
652
+ if (text.length <= max) {
653
+ return text;
654
+ }
655
+ return `${text.slice(0, max).trimEnd()}
656
+
657
+ _(truncated to fit the memory budget)_`;
658
+ }
659
+ function skillsSection(ctx) {
660
+ if (ctx.skills.length === 0) {
661
+ return void 0;
662
+ }
663
+ const lines = ctx.skills.map((skill) => `- **${skill.name}** \u2014 ${skill.frontmatter.when}`);
664
+ return ["## Skills", "Available procedures (load the matching one when its trigger applies):", "", ...lines].join(
665
+ "\n"
666
+ );
667
+ }
668
+ function toolsSection(ctx) {
669
+ if (ctx.tools.length === 0) {
670
+ return void 0;
671
+ }
672
+ const lines = ctx.tools.map((tool) => {
673
+ const flags = [
674
+ tool.manifest.risk !== "low" ? tool.manifest.risk : null,
675
+ tool.manifest.confirm ? "asks before running" : null,
676
+ tool.manifest.connection ? `connection: ${tool.manifest.connection}` : null
677
+ ].filter(Boolean).join(", ");
678
+ const suffix = flags ? ` _(${flags})_` : "";
679
+ return `- \`${tool.name}\` \u2014 ${tool.manifest.description}${suffix}`;
680
+ });
681
+ return [
682
+ "## Tools",
683
+ "Callable via the threadroot MCP server or `tr run <tool>`:",
684
+ "",
685
+ ...lines
686
+ ].join("\n");
687
+ }
688
+ function connectionsSection(ctx) {
689
+ if (ctx.connections.length === 0) {
690
+ return void 0;
691
+ }
692
+ const lines = ctx.connections.map((connection) => {
693
+ const flags = [
694
+ connection.manifest.provider,
695
+ connection.manifest.risk,
696
+ connection.manifest.confirm ? "asks before running" : null
697
+ ].filter(Boolean).join(", ");
698
+ return `- \`${connection.name}\` \u2014 ${connection.manifest.description} _(${flags})_`;
699
+ });
700
+ return [
701
+ "## Connections",
702
+ "Local CLI bridges available to connection-aware tools. Credentials stay in the user's local CLI configuration.",
703
+ "",
704
+ ...lines
705
+ ].join("\n");
706
+ }
707
+ function conventionsSection(ctx) {
708
+ if (ctx.globalRules.length === 0) {
709
+ return void 0;
710
+ }
711
+ const blocks = ctx.globalRules.map((rule) => `### ${rule.name}
712
+
713
+ ${rule.body.trim()}`);
714
+ return ["## Conventions", "", ...blocks].join("\n");
715
+ }
716
+ function referencesSection(ctx) {
717
+ if (ctx.references.length === 0) {
718
+ return void 0;
719
+ }
720
+ const linked = ctx.references.filter((resolved) => !resolved.inlined);
721
+ const inlined = ctx.references.filter((resolved) => resolved.inlined);
722
+ const parts = ["## Additional context", "Existing project docs worth reading when relevant:"];
723
+ if (linked.length > 0) {
724
+ const lines = linked.map((resolved) => {
725
+ const { reference } = resolved;
726
+ const note = reference.description ? ` \u2014 ${reference.description}` : "";
727
+ const missing = resolved.exists ? "" : " _(missing)_";
728
+ return `- [${reference.path}](${reference.path})${note}${missing}`;
729
+ });
730
+ parts.push("", ...lines);
731
+ }
732
+ for (const resolved of inlined) {
733
+ const note = resolved.reference.description ? `
734
+
735
+ ${resolved.reference.description}` : "";
736
+ parts.push("", `### ${resolved.reference.path}${note}`, "", resolved.inlined.trim());
737
+ }
738
+ return parts.join("\n");
739
+ }
740
+ function memorySection(ctx) {
741
+ const project = ctx.memory.find((entry) => entry.type === "project");
742
+ const others = ctx.memory.filter((entry) => entry.type !== "project");
743
+ if (!project && others.length === 0) {
744
+ return void 0;
745
+ }
746
+ const parts = ["## Memory"];
747
+ if (project) {
748
+ const budget = ctx.manifest.memory.budget.project ?? DEFAULT_PROJECT_BUDGET;
749
+ parts.push("", truncate(project.body.trim(), budget));
750
+ }
751
+ if (others.length > 0) {
752
+ const links = [...new Set(others.map((entry) => entry.type))].map((type) => `\`.threadroot/memory/${type}.md\``).join(", ");
753
+ parts.push("", `Task-scoped memory (loaded on demand): ${links}.`);
754
+ }
755
+ return parts.join("\n");
756
+ }
757
+ function buildManagedBlock(ctx) {
758
+ const sections = [
759
+ skillsSection(ctx),
760
+ toolsSection(ctx),
761
+ connectionsSection(ctx),
762
+ conventionsSection(ctx),
763
+ referencesSection(ctx),
764
+ memorySection(ctx)
765
+ ].filter((section) => Boolean(section));
766
+ if (sections.length === 0) {
767
+ return "_No threadroot-managed context yet. Add skills, tools, rules, or references._";
768
+ }
769
+ return sections.join("\n\n");
770
+ }
771
+
772
+ // src/core/compile/index.ts
773
+ var ADAPTERS = {
774
+ agents: agentsAdapter,
775
+ claude: claudeAdapter,
776
+ copilot: copilotAdapter,
777
+ cursor: cursorAdapter
778
+ };
779
+ var EAGER_INLINE_LIMIT = 8e3;
780
+ var AGENTS_FILE = "AGENTS.md";
781
+ function splitRules(rules) {
782
+ const global = [];
783
+ const scoped = [];
784
+ for (const rule of rules) {
785
+ if (rule.frontmatter.applyTo) {
786
+ scoped.push(rule);
787
+ } else {
788
+ global.push(rule);
789
+ }
790
+ }
791
+ return { global, scoped };
792
+ }
793
+ function isGlob(value) {
794
+ return /[*?[\]{}]/.test(value);
795
+ }
796
+ async function readIfExists(filePath) {
797
+ try {
798
+ return await readFile3(filePath, "utf8");
799
+ } catch (error) {
800
+ if (error.code === "ENOENT") {
801
+ return void 0;
802
+ }
803
+ throw error;
804
+ }
805
+ }
806
+ async function resolveReference(repoRoot, reference) {
807
+ if (isGlob(reference.path)) {
808
+ return { reference, exists: true };
809
+ }
810
+ const absolute = toRepoPath(repoRoot, reference.path);
811
+ try {
812
+ const info = await stat(absolute);
813
+ if (!info.isFile()) {
814
+ return { reference, exists: true };
815
+ }
816
+ if (reference.load === "eager" && info.size <= EAGER_INLINE_LIMIT) {
817
+ const inlined = await readFile3(absolute, "utf8");
818
+ return { reference, exists: true, inlined };
819
+ }
820
+ return { reference, exists: true };
821
+ } catch (error) {
822
+ if (error.code === "ENOENT") {
823
+ return { reference, exists: false };
824
+ }
825
+ throw error;
826
+ }
827
+ }
828
+ async function buildContext(repoRoot, harness) {
829
+ const existingAgents = await readIfExists(path5.join(repoRoot, AGENTS_FILE)) ?? "";
830
+ const handAuthored = extractHandAuthored(existingAgents);
831
+ const { global, scoped } = splitRules(harness.rules);
832
+ const references = await Promise.all(
833
+ harness.manifest.references.map((reference) => resolveReference(repoRoot, reference))
834
+ );
835
+ const partial = {
836
+ repoRoot,
837
+ manifest: harness.manifest,
838
+ handAuthored,
839
+ canonicalAgents: "",
840
+ skills: harness.skills,
841
+ globalRules: global,
842
+ scopedRules: scoped,
843
+ tools: harness.tools,
844
+ connections: harness.connections,
845
+ memory: harness.memory,
846
+ references
847
+ };
848
+ const managed = buildManagedBlock(partial);
849
+ partial.canonicalAgents = composeWithManaged(handAuthored, managed);
850
+ return partial;
851
+ }
852
+ async function compile(repoRoot, harness) {
853
+ const ctx = await buildContext(repoRoot, harness);
854
+ const files = [];
855
+ const seen = /* @__PURE__ */ new Set();
856
+ for (const adapterId of harness.manifest.adapters) {
857
+ const adapter = ADAPTERS[adapterId];
858
+ if (!adapter) {
859
+ continue;
860
+ }
861
+ for (const file of adapter.compile(ctx)) {
862
+ if (seen.has(file.path)) {
863
+ continue;
864
+ }
865
+ seen.add(file.path);
866
+ files.push(file);
867
+ }
868
+ }
869
+ files.sort((a, b) => a.path.localeCompare(b.path));
870
+ return files;
871
+ }
872
+ async function detectDrift(repoRoot, files) {
873
+ const entries = await Promise.all(
874
+ files.map(async (file) => {
875
+ const existing = await readIfExists(path5.join(repoRoot, file.path));
876
+ if (existing === void 0) {
877
+ return { path: file.path, status: "create" };
878
+ }
879
+ const status = hashContent(existing) === hashContent(file.content) ? "unchanged" : "drift";
880
+ return { path: file.path, status };
881
+ })
882
+ );
883
+ return entries;
884
+ }
885
+
886
+ // src/core/compile/write.ts
887
+ async function writeCompiled(repoRoot, files) {
888
+ await Promise.all(
889
+ files.map(async (file) => {
890
+ const absolute = path6.join(repoRoot, file.path);
891
+ await mkdir2(path6.dirname(absolute), { recursive: true });
892
+ await writeFile2(absolute, file.content, "utf8");
893
+ })
894
+ );
895
+ return files.map((file) => file.path);
896
+ }
897
+ async function runCompile(repoRoot, options = {}) {
898
+ const resolved = options.harness ?? await resolveHarness(repoRoot, { home: options.home });
899
+ const harness = options.adapter ? { ...resolved, manifest: { ...resolved.manifest, adapters: [options.adapter] } } : resolved;
900
+ const files = await compile(repoRoot, harness);
901
+ const drift = await detectDrift(repoRoot, files);
902
+ const written = await writeCompiled(repoRoot, files);
903
+ return { written, drift };
904
+ }
905
+
906
+ // src/commands/compile.ts
907
+ async function runCompileCommand(repoRoot, options) {
908
+ const adapter = options.adapter ? adapterIdSchema.parse(options.adapter) : void 0;
909
+ try {
910
+ const { written, drift } = await runCompile(repoRoot, { adapter });
911
+ const changed = drift.filter((entry) => entry.status !== "unchanged").length;
912
+ console.log(`Compiled ${written.length} vendor file(s)${changed > 0 ? ` (${changed} changed)` : ""}.`);
913
+ for (const file of written) {
914
+ console.log(` ${file}`);
915
+ }
916
+ } catch (error) {
917
+ if (error instanceof HarnessError) {
918
+ console.error(error.message);
919
+ process.exitCode = 1;
920
+ return;
921
+ }
922
+ throw error;
923
+ }
924
+ }
925
+
926
+ // src/commands/context.ts
927
+ async function runContext(repoRoot, task) {
928
+ let context;
929
+ try {
930
+ context = await assembleContext(repoRoot, task);
931
+ } catch (error) {
932
+ if (error instanceof HarnessError) {
933
+ console.log("No harness found. Run `tr init` first.");
934
+ return;
935
+ }
936
+ throw error;
937
+ }
938
+ console.log(`task: ${context.task}`);
939
+ if (context.skills.length > 0) {
940
+ console.log("\nskills:");
941
+ for (const skill of context.skills) {
942
+ console.log(`- ${skill.name} - ${skill.when}`);
943
+ }
944
+ }
945
+ if (context.rules.length > 0) {
946
+ console.log("\nrules:");
947
+ for (const rule of context.rules) {
948
+ console.log(`- ${rule.name}${rule.applyTo ? ` (${rule.applyTo})` : ""}`);
949
+ }
950
+ }
951
+ if (context.tools.length > 0) {
952
+ console.log("\ntools:");
953
+ for (const tool of context.tools) {
954
+ console.log(`- ${tool.name} - ${tool.description}`);
955
+ }
956
+ }
957
+ if (context.memory.length > 0) {
958
+ console.log("\nmemory:");
959
+ for (const entry of context.memory) {
960
+ console.log(`- ${entry.type}`);
961
+ }
962
+ }
963
+ if (context.skills.length === 0 && context.rules.length === 0 && context.tools.length === 0 && context.memory.length === 0) {
964
+ console.log("\nNo matching harness context for this task yet.");
965
+ }
966
+ }
967
+
968
+ // src/commands/diff.ts
969
+ import fs from "fs/promises";
970
+ import path7 from "path";
971
+ async function readIfExists2(filePath) {
972
+ try {
973
+ return await fs.readFile(filePath, "utf8");
974
+ } catch (error) {
975
+ if (error.code === "ENOENT") {
976
+ return void 0;
977
+ }
978
+ throw error;
979
+ }
980
+ }
981
+ function lineDiff(before, after) {
982
+ const a = before.length === 0 ? [] : before.split("\n");
983
+ const b = after.length === 0 ? [] : after.split("\n");
984
+ const lcs = Array.from({ length: a.length + 1 }, () => new Array(b.length + 1).fill(0));
985
+ for (let i2 = a.length - 1; i2 >= 0; i2 -= 1) {
986
+ for (let j2 = b.length - 1; j2 >= 0; j2 -= 1) {
987
+ lcs[i2][j2] = a[i2] === b[j2] ? lcs[i2 + 1][j2 + 1] + 1 : Math.max(lcs[i2 + 1][j2], lcs[i2][j2 + 1]);
988
+ }
989
+ }
990
+ const lines = [];
991
+ let i = 0;
992
+ let j = 0;
993
+ while (i < a.length && j < b.length) {
994
+ if (a[i] === b[j]) {
995
+ i += 1;
996
+ j += 1;
997
+ } else if (lcs[i + 1][j] >= lcs[i][j + 1]) {
998
+ lines.push(`- ${a[i]}`);
999
+ i += 1;
1000
+ } else {
1001
+ lines.push(`+ ${b[j]}`);
1002
+ j += 1;
1003
+ }
1004
+ }
1005
+ while (i < a.length) {
1006
+ lines.push(`- ${a[i]}`);
1007
+ i += 1;
1008
+ }
1009
+ while (j < b.length) {
1010
+ lines.push(`+ ${b[j]}`);
1011
+ j += 1;
1012
+ }
1013
+ return lines;
1014
+ }
1015
+ async function runDiff(repoRoot) {
1016
+ let harness;
1017
+ try {
1018
+ harness = await resolveHarness(repoRoot);
1019
+ } catch (error) {
1020
+ if (error instanceof HarnessError) {
1021
+ console.log("No harness found. Run `tr init` first.");
1022
+ return;
1023
+ }
1024
+ throw error;
1025
+ }
1026
+ const files = await compile(repoRoot, harness);
1027
+ let changed = 0;
1028
+ for (const file of files) {
1029
+ const existing = await readIfExists2(path7.join(repoRoot, file.path));
1030
+ if (existing === void 0) {
1031
+ changed += 1;
1032
+ console.log(`+ ${file.path} (new)`);
1033
+ continue;
1034
+ }
1035
+ if (existing === file.content) {
1036
+ continue;
1037
+ }
1038
+ changed += 1;
1039
+ console.log(`~ ${file.path}`);
1040
+ for (const line of lineDiff(existing, file.content)) {
1041
+ console.log(` ${line}`);
1042
+ }
1043
+ }
1044
+ if (changed === 0) {
1045
+ console.log("No drift: every vendor file matches the canonical harness.");
1046
+ }
1047
+ }
1048
+
1049
+ // src/core/doctor.ts
1050
+ import { stat as stat4 } from "fs/promises";
1051
+ import path14 from "path";
1052
+
1053
+ // src/core/install/lock.ts
1054
+ import { mkdir as mkdir3, readFile as readFile4, writeFile as writeFile3 } from "fs/promises";
1055
+ import path8 from "path";
1056
+
1057
+ // src/core/install/source.ts
1058
+ import { z as z3 } from "zod";
1059
+ var objectKindSchema = z3.enum(["skill", "tool", "rule", "connection"]);
1060
+ var objectSourceKindSchema = z3.enum(["local", "git", "registry"]);
1061
+ function splitRef(body) {
1062
+ const at = body.lastIndexOf("@");
1063
+ if (at > 0) {
1064
+ return { body: body.slice(0, at), ref: body.slice(at + 1) || void 0 };
1065
+ }
1066
+ return { body };
1067
+ }
1068
+ function parseGithub(raw) {
1069
+ const { body, ref } = splitRef(raw.slice("github:".length));
1070
+ const parts = body.split("/").filter(Boolean);
1071
+ const [owner, repo, ...rest] = parts;
1072
+ if (!owner || !repo) {
1073
+ throw new Error(`Invalid github source: ${raw} (expected github:owner/repo[/path][@ref]).`);
1074
+ }
1075
+ return {
1076
+ kind: "git",
1077
+ raw,
1078
+ provider: "github",
1079
+ owner,
1080
+ repo,
1081
+ objectPath: rest.length > 0 ? rest.join("/") : void 0,
1082
+ ref
1083
+ };
1084
+ }
1085
+ function isLocalPath(raw) {
1086
+ return raw.startsWith("./") || raw.startsWith("../") || raw.startsWith("/") || raw === "." || raw === "..";
1087
+ }
1088
+ function parseSourceRef(raw) {
1089
+ const value = raw.trim();
1090
+ if (!value) {
1091
+ throw new Error("Empty source reference.");
1092
+ }
1093
+ if (value.startsWith("github:")) {
1094
+ return parseGithub(value);
1095
+ }
1096
+ if (value.startsWith("registry:")) {
1097
+ const { body: body2, ref: ref2 } = splitRef(value.slice("registry:".length));
1098
+ if (!body2) {
1099
+ throw new Error(`Invalid registry source: ${raw} (expected registry:name[@version]).`);
1100
+ }
1101
+ return { kind: "registry", raw: value, name: body2, version: ref2 };
1102
+ }
1103
+ if (value.startsWith("git+") || /^https?:\/\/.+\.git(@.+)?$/.test(value) || value.startsWith("git@")) {
1104
+ const stripped = value.startsWith("git+") ? value.slice("git+".length) : value;
1105
+ const { body: body2, ref: ref2 } = splitRef(stripped);
1106
+ return { kind: "git", raw: value, provider: "url", url: body2, ref: ref2 };
1107
+ }
1108
+ if (isLocalPath(value)) {
1109
+ return { kind: "local", raw: value, path: value };
1110
+ }
1111
+ const { body, ref } = splitRef(value);
1112
+ return { kind: "registry", raw: value, name: body, version: ref };
1113
+ }
1114
+ var lockEntrySchema = z3.object({
1115
+ name: z3.string().min(1),
1116
+ kind: objectKindSchema,
1117
+ sourceKind: objectSourceKindSchema,
1118
+ source: z3.string().min(1),
1119
+ /** Path of the object within the source repository, when applicable. */
1120
+ objectPath: z3.string().optional(),
1121
+ /** Human-supplied ref (tag/branch/commit) before resolution. */
1122
+ ref: z3.string().optional(),
1123
+ /** Immutable resolved identifier — the commit SHA for git sources. */
1124
+ resolved: z3.string().optional(),
1125
+ /** `sha256:<hex>` content digest for tamper detection + reproducibility. */
1126
+ integrity: z3.string().optional(),
1127
+ installedAt: z3.string()
1128
+ });
1129
+ var lockFileSchema = z3.object({
1130
+ version: z3.literal(1),
1131
+ objects: z3.array(lockEntrySchema).default([])
1132
+ });
1133
+ function emptyLockFile() {
1134
+ return { version: 1, objects: [] };
1135
+ }
1136
+
1137
+ // src/core/install/lock.ts
1138
+ async function readLockFile(lockPath) {
1139
+ let raw;
1140
+ try {
1141
+ raw = await readFile4(lockPath, "utf8");
1142
+ } catch (error) {
1143
+ if (error.code === "ENOENT") {
1144
+ return emptyLockFile();
1145
+ }
1146
+ throw error;
1147
+ }
1148
+ return lockFileSchema.parse(JSON.parse(raw));
1149
+ }
1150
+ async function writeLockFile(lockPath, lock) {
1151
+ const sorted = {
1152
+ version: lock.version,
1153
+ objects: [...lock.objects].sort((a, b) => a.name.localeCompare(b.name))
1154
+ };
1155
+ await mkdir3(path8.dirname(lockPath), { recursive: true });
1156
+ await writeFile3(lockPath, `${JSON.stringify(sorted, null, 2)}
1157
+ `, "utf8");
1158
+ }
1159
+ function upsertLockEntry(lock, entry) {
1160
+ const objects = lock.objects.filter((existing) => !(existing.name === entry.name && existing.kind === entry.kind));
1161
+ objects.push(entry);
1162
+ return { version: lock.version, objects };
1163
+ }
1164
+ function externalToolNames(lock) {
1165
+ const names = /* @__PURE__ */ new Set();
1166
+ for (const entry of lock.objects) {
1167
+ if (entry.kind === "tool" && entry.sourceKind !== "local") {
1168
+ names.add(entry.name);
1169
+ }
1170
+ }
1171
+ return names;
1172
+ }
1173
+ function externalSkillNames(lock) {
1174
+ const names = /* @__PURE__ */ new Set();
1175
+ for (const entry of lock.objects) {
1176
+ if (entry.kind === "skill" && entry.sourceKind !== "local") {
1177
+ names.add(entry.name);
1178
+ }
1179
+ }
1180
+ return names;
1181
+ }
1182
+
1183
+ // src/core/skills.ts
1184
+ import { readFile as readFile5, readdir as readdir2, stat as stat2 } from "fs/promises";
1185
+ import path9 from "path";
1186
+ var SKILL_NAME_RE = /^[a-z0-9][a-z0-9-]{0,62}[a-z0-9]$/;
1187
+ var MIN_DESCRIPTION_LENGTH = 40;
1188
+ var MAX_DESCRIPTION_LENGTH = 1024;
1189
+ var MAX_BODY_LINES = 500;
1190
+ var LINK_RE = /\]\(([^)]+)\)/g;
1191
+ function finding(severity, skill, message) {
1192
+ return { severity, skill: skill.name, message, path: skill.sourcePath };
1193
+ }
1194
+ function validateResolvedSkills(harness) {
1195
+ const findings = [];
1196
+ for (const skill of harness.skills) {
1197
+ if (!SKILL_NAME_RE.test(skill.name)) {
1198
+ findings.push(finding("error", skill, "Skill names must use lowercase letters, digits, and hyphens."));
1199
+ }
1200
+ if (path9.basename(skill.sourcePath) === "SKILL.md") {
1201
+ const folderName = path9.basename(path9.dirname(skill.sourcePath));
1202
+ if (folderName !== skill.name) {
1203
+ findings.push(finding("error", skill, "Folder-based skill directory must match frontmatter `name`."));
1204
+ }
1205
+ }
1206
+ if (skill.frontmatter.description.length < MIN_DESCRIPTION_LENGTH) {
1207
+ findings.push(
1208
+ finding(
1209
+ "warning",
1210
+ skill,
1211
+ "Skill description is short; include what the skill does and concrete trigger contexts."
1212
+ )
1213
+ );
1214
+ }
1215
+ if (skill.frontmatter.description.length > MAX_DESCRIPTION_LENGTH) {
1216
+ findings.push(finding("error", skill, "Skill description must be 1024 characters or less."));
1217
+ }
1218
+ if (!/\b(use when|when|reviewing|writing|designing|debugging|creating)\b/i.test(skill.frontmatter.description)) {
1219
+ findings.push(
1220
+ finding(
1221
+ "warning",
1222
+ skill,
1223
+ "Skill description should include trigger language so agents know when to load it."
1224
+ )
1225
+ );
1226
+ }
1227
+ if (skill.body.trim().length === 0) {
1228
+ findings.push(finding("error", skill, "Skill body must not be empty."));
1229
+ }
1230
+ if (skill.body.split(/\r?\n/).length > MAX_BODY_LINES) {
1231
+ findings.push(
1232
+ finding(
1233
+ "warning",
1234
+ skill,
1235
+ "Skill body is long; move variant details into references for progressive disclosure."
1236
+ )
1237
+ );
1238
+ }
1239
+ if (skill.frontmatter.allowedTools) {
1240
+ findings.push(
1241
+ finding(
1242
+ "warning",
1243
+ skill,
1244
+ "Skill declares allowed tools; inspect tool permissions before trusting external installs."
1245
+ )
1246
+ );
1247
+ }
1248
+ }
1249
+ return { ok: findings.every((entry) => entry.severity !== "error"), findings };
1250
+ }
1251
+ function pushPathFinding(findings, severity, skill, message, pathValue = skill.sourcePath) {
1252
+ findings.push({ severity, skill: skill.name, message, path: pathValue });
1253
+ }
1254
+ async function exists(filePath) {
1255
+ try {
1256
+ await stat2(filePath);
1257
+ return true;
1258
+ } catch (error) {
1259
+ if (error.code === "ENOENT") {
1260
+ return false;
1261
+ }
1262
+ throw error;
1263
+ }
1264
+ }
1265
+ async function validateSkillDirectory(skill) {
1266
+ if (path9.basename(skill.sourcePath) !== "SKILL.md") {
1267
+ return [];
1268
+ }
1269
+ const findings = [];
1270
+ const skillDir = path9.dirname(skill.sourcePath);
1271
+ const referencesDir = path9.join(skillDir, "references");
1272
+ const scriptsDir = path9.join(skillDir, "scripts");
1273
+ const evalsDir = path9.join(skillDir, "evals");
1274
+ if (await exists(scriptsDir)) {
1275
+ pushPathFinding(
1276
+ findings,
1277
+ "warning",
1278
+ skill,
1279
+ "Skill includes scripts; inspect scripts before trusting external installs.",
1280
+ scriptsDir
1281
+ );
1282
+ }
1283
+ for (const match of skill.body.matchAll(LINK_RE)) {
1284
+ const target = match[1] ?? "";
1285
+ if (/^[a-z]+:\/\//i.test(target) || target.startsWith("#")) {
1286
+ continue;
1287
+ }
1288
+ if (path9.isAbsolute(target) || target.split(/[\\/]/).includes("..")) {
1289
+ pushPathFinding(findings, "error", skill, `Skill link must stay inside the skill directory: ${target}`);
1290
+ continue;
1291
+ }
1292
+ const resolved = path9.join(skillDir, target);
1293
+ if (!await exists(resolved)) {
1294
+ pushPathFinding(findings, "error", skill, `Skill links to missing file: ${target}`);
1295
+ }
1296
+ const segments = target.split(/[\\/]/).filter(Boolean);
1297
+ if (segments[0] === "references" && segments.length > 2) {
1298
+ pushPathFinding(findings, "warning", skill, `Reference links should stay one level deep: ${target}`);
1299
+ }
1300
+ }
1301
+ if (await exists(referencesDir)) {
1302
+ const references = await readdir2(referencesDir, { withFileTypes: true });
1303
+ for (const entry of references) {
1304
+ if (!entry.isFile()) {
1305
+ continue;
1306
+ }
1307
+ const filePath = path9.join(referencesDir, entry.name);
1308
+ const body = await readFile5(filePath, "utf8");
1309
+ if (!body.trim()) {
1310
+ pushPathFinding(findings, "error", skill, "Reference file must not be empty.", filePath);
1311
+ }
1312
+ }
1313
+ }
1314
+ if (await exists(evalsDir)) {
1315
+ const triggersPath = path9.join(evalsDir, "triggers.json");
1316
+ if (!await exists(triggersPath)) {
1317
+ pushPathFinding(findings, "warning", skill, "Skill evals directory should include triggers.json.", evalsDir);
1318
+ } else {
1319
+ const parsed = JSON.parse(await readFile5(triggersPath, "utf8"));
1320
+ const value = parsed;
1321
+ if (!Array.isArray(value.shouldTrigger) || value.shouldTrigger.length === 0) {
1322
+ pushPathFinding(findings, "error", skill, "evals/triggers.json must include non-empty shouldTrigger.");
1323
+ }
1324
+ if (!Array.isArray(value.shouldNotTrigger) || value.shouldNotTrigger.length === 0) {
1325
+ pushPathFinding(findings, "error", skill, "evals/triggers.json must include non-empty shouldNotTrigger.");
1326
+ }
1327
+ }
1328
+ }
1329
+ return findings;
1330
+ }
1331
+ async function validateResolvedSkillsDeep(harness) {
1332
+ const shallow = validateResolvedSkills(harness);
1333
+ const findings = [...shallow.findings];
1334
+ for (const skill of harness.skills) {
1335
+ findings.push(...await validateSkillDirectory(skill));
1336
+ }
1337
+ return { ok: findings.every((entry) => entry.severity !== "error"), findings };
1338
+ }
1339
+ async function loadSkillFile(filePath, origin = "project") {
1340
+ const raw = await readFile5(filePath, "utf8");
1341
+ const { data, body } = parseFrontmatter(raw);
1342
+ const result = skillFrontmatterSchema.safeParse(data);
1343
+ if (!result.success) {
1344
+ const detail = result.error.issues.map((issue) => `${issue.path.join(".") || "<root>"}: ${issue.message}`).join("; ");
1345
+ throw new HarnessError(`Invalid skill ${filePath}: ${detail}`);
1346
+ }
1347
+ return {
1348
+ name: result.data.name,
1349
+ origin,
1350
+ sourcePath: filePath,
1351
+ frontmatter: result.data,
1352
+ body
1353
+ };
1354
+ }
1355
+ async function loadSkillsAtPath(targetPath) {
1356
+ const info = await stat2(targetPath);
1357
+ if (info.isFile()) {
1358
+ return [await loadSkillFile(targetPath)];
1359
+ }
1360
+ const directSkill = path9.join(targetPath, "SKILL.md");
1361
+ try {
1362
+ return [await loadSkillFile(directSkill)];
1363
+ } catch (error) {
1364
+ if (!(error instanceof HarnessError) && error.code !== "ENOENT") {
1365
+ throw error;
1366
+ }
1367
+ if (error instanceof HarnessError) {
1368
+ throw error;
1369
+ }
1370
+ }
1371
+ const entries = await readdir2(targetPath, { withFileTypes: true });
1372
+ const skills = [];
1373
+ for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
1374
+ const full = path9.join(targetPath, entry.name);
1375
+ if (entry.isFile() && entry.name.endsWith(".md") && entry.name !== "README.md") {
1376
+ skills.push(await loadSkillFile(full));
1377
+ }
1378
+ if (entry.isDirectory()) {
1379
+ const skillPath = path9.join(full, "SKILL.md");
1380
+ try {
1381
+ skills.push(await loadSkillFile(skillPath));
1382
+ } catch (error) {
1383
+ if (error.code !== "ENOENT") {
1384
+ throw error;
1385
+ }
1386
+ }
1387
+ }
1388
+ }
1389
+ return skills;
1390
+ }
1391
+ async function listFilesIfDir(dir) {
1392
+ if (!await exists(dir)) {
1393
+ return [];
1394
+ }
1395
+ const entries = await readdir2(dir, { withFileTypes: true });
1396
+ return entries.filter((entry) => entry.isFile()).map((entry) => entry.name).sort();
1397
+ }
1398
+ async function inspectSkillPath(targetPath) {
1399
+ const skills = await loadSkillsAtPath(targetPath);
1400
+ if (skills.length !== 1) {
1401
+ throw new HarnessError(`Expected exactly one skill at ${targetPath}; found ${skills.length}.`);
1402
+ }
1403
+ const skill = skills[0];
1404
+ const skillDir = path9.dirname(skill.sourcePath);
1405
+ return {
1406
+ name: skill.name,
1407
+ description: skill.frontmatter.description,
1408
+ path: skill.sourcePath,
1409
+ references: await listFilesIfDir(path9.join(skillDir, "references")),
1410
+ scripts: await listFilesIfDir(path9.join(skillDir, "scripts")),
1411
+ assets: await listFilesIfDir(path9.join(skillDir, "assets")),
1412
+ evals: await listFilesIfDir(path9.join(skillDir, "evals")),
1413
+ allowedTools: skill.frontmatter.allowedTools
1414
+ };
1415
+ }
1416
+ async function validateSkillPath(targetPath) {
1417
+ try {
1418
+ const harness = {
1419
+ manifest: {
1420
+ name: "skill-path",
1421
+ version: 1,
1422
+ profile: "empty",
1423
+ adapters: ["agents"],
1424
+ references: [],
1425
+ memory: { budget: {} },
1426
+ tools: { allow: [] }
1427
+ },
1428
+ skills: await loadSkillsAtPath(targetPath),
1429
+ rules: [],
1430
+ tools: [],
1431
+ connections: [],
1432
+ memory: []
1433
+ };
1434
+ return await validateResolvedSkillsDeep(harness);
1435
+ } catch (error) {
1436
+ if (error instanceof HarnessError || error instanceof Error) {
1437
+ return {
1438
+ ok: false,
1439
+ findings: [
1440
+ {
1441
+ severity: "error",
1442
+ skill: "<path>",
1443
+ path: targetPath,
1444
+ message: error.message
1445
+ }
1446
+ ]
1447
+ };
1448
+ }
1449
+ throw error;
1450
+ }
1451
+ }
1452
+ async function validateSkills(repoRoot, options = {}) {
1453
+ try {
1454
+ return await validateResolvedSkillsDeep(await resolveHarness(repoRoot, { home: options.home }));
1455
+ } catch (error) {
1456
+ if (error instanceof HarnessError) {
1457
+ return {
1458
+ ok: false,
1459
+ findings: [
1460
+ {
1461
+ severity: "error",
1462
+ skill: "<harness>",
1463
+ path: repoRoot,
1464
+ message: error.message
1465
+ }
1466
+ ]
1467
+ };
1468
+ }
1469
+ throw error;
1470
+ }
1471
+ }
1472
+
1473
+ // src/core/connections/index.ts
1474
+ import { mkdir as mkdir4, writeFile as writeFile4 } from "fs/promises";
1475
+ import path11 from "path";
1476
+ import { stringify as stringifyYaml2 } from "yaml";
1477
+
1478
+ // src/core/tools/execute.ts
1479
+ import { spawn } from "child_process";
1480
+ import path10 from "path";
1481
+ var ToolExecutionError = class extends Error {
1482
+ constructor(message) {
1483
+ super(message);
1484
+ this.name = "ToolExecutionError";
1485
+ }
1486
+ };
1487
+ var DEFAULT_TIMEOUT_MS = 12e4;
1488
+ var MAX_OUTPUT_CHARS = 1e6;
1489
+ var KILL_GRACE_MS = 2e3;
1490
+ var INTERPRETERS = {
1491
+ ".sh": "bash",
1492
+ ".bash": "bash",
1493
+ ".js": "node",
1494
+ ".cjs": "node",
1495
+ ".mjs": "node",
1496
+ ".py": "python3",
1497
+ ".rb": "ruby"
1498
+ };
1499
+ function cap(text) {
1500
+ if (text.length <= MAX_OUTPUT_CHARS) {
1501
+ return text;
1502
+ }
1503
+ return `${text.slice(0, MAX_OUTPUT_CHARS)}
1504
+ \u2026[output truncated]`;
1505
+ }
1506
+ function runProcess(plan, opts) {
1507
+ return new Promise((resolve, reject) => {
1508
+ const started = Date.now();
1509
+ const child = spawn(plan.file, plan.args, {
1510
+ cwd: opts.cwd,
1511
+ env: opts.env,
1512
+ shell: plan.shell,
1513
+ stdio: ["ignore", "pipe", "pipe"]
1514
+ });
1515
+ let stdout = "";
1516
+ let stderr = "";
1517
+ let timedOut = false;
1518
+ let settled = false;
1519
+ const timer = setTimeout(() => {
1520
+ timedOut = true;
1521
+ child.kill("SIGTERM");
1522
+ setTimeout(() => child.kill("SIGKILL"), KILL_GRACE_MS).unref();
1523
+ }, opts.timeoutMs);
1524
+ const onAbort = () => {
1525
+ child.kill("SIGTERM");
1526
+ };
1527
+ opts.signal?.addEventListener("abort", onAbort, { once: true });
1528
+ child.stdout?.on("data", (chunk) => {
1529
+ if (stdout.length < MAX_OUTPUT_CHARS) {
1530
+ stdout += chunk.toString("utf8");
1531
+ }
1532
+ });
1533
+ child.stderr?.on("data", (chunk) => {
1534
+ if (stderr.length < MAX_OUTPUT_CHARS) {
1535
+ stderr += chunk.toString("utf8");
1536
+ }
1537
+ });
1538
+ child.on("error", (error) => {
1539
+ if (settled) {
1540
+ return;
1541
+ }
1542
+ settled = true;
1543
+ clearTimeout(timer);
1544
+ opts.signal?.removeEventListener("abort", onAbort);
1545
+ reject(new ToolExecutionError(`Failed to start \`${plan.label}\`: ${error.message}`));
1546
+ });
1547
+ child.on("close", (code, signal) => {
1548
+ if (settled) {
1549
+ return;
1550
+ }
1551
+ settled = true;
1552
+ clearTimeout(timer);
1553
+ opts.signal?.removeEventListener("abort", onAbort);
1554
+ resolve({
1555
+ ok: !timedOut && code === 0,
1556
+ exitCode: code,
1557
+ signal,
1558
+ stdout: cap(stdout),
1559
+ stderr: cap(stderr),
1560
+ durationMs: Date.now() - started,
1561
+ timedOut,
1562
+ command: plan.label
1563
+ });
1564
+ });
1565
+ });
1566
+ }
1567
+ function planScript(repoRoot, scriptRef) {
1568
+ const resolved = path10.resolve(repoRoot, scriptRef);
1569
+ const projectRoot = projectHarnessDir(repoRoot);
1570
+ const userRoot = userHarnessDir();
1571
+ const withinProject = resolved === projectRoot || resolved.startsWith(`${projectRoot}${path10.sep}`);
1572
+ const withinUser = resolved === userRoot || resolved.startsWith(`${userRoot}${path10.sep}`);
1573
+ if (!withinProject && !withinUser) {
1574
+ throw new ToolExecutionError(`Script must live under the harness directory: ${scriptRef}`);
1575
+ }
1576
+ const interpreter = INTERPRETERS[path10.extname(resolved).toLowerCase()];
1577
+ if (interpreter) {
1578
+ return { file: interpreter, args: [resolved], shell: false, label: `${interpreter} ${scriptRef}` };
1579
+ }
1580
+ return { file: resolved, args: [], shell: false, label: scriptRef };
1581
+ }
1582
+ function executeShell(command, opts) {
1583
+ return runProcess(
1584
+ { file: command, args: [], shell: true, label: command },
1585
+ {
1586
+ cwd: opts.cwd,
1587
+ env: { ...process.env, ...opts.env },
1588
+ timeoutMs: opts.timeoutMs ?? DEFAULT_TIMEOUT_MS,
1589
+ signal: opts.signal
1590
+ }
1591
+ );
1592
+ }
1593
+ function executeScript(repoRoot, scriptRef, opts) {
1594
+ let plan;
1595
+ try {
1596
+ plan = planScript(repoRoot, scriptRef);
1597
+ } catch (error) {
1598
+ return Promise.reject(error);
1599
+ }
1600
+ return runProcess(plan, {
1601
+ cwd: opts.cwd,
1602
+ env: { ...process.env, ...opts.env },
1603
+ timeoutMs: opts.timeoutMs ?? DEFAULT_TIMEOUT_MS,
1604
+ signal: opts.signal
1605
+ });
1606
+ }
1607
+
1608
+ // src/core/connections/index.ts
1609
+ var NAME_RE = /^[a-z0-9][a-z0-9-]*$/;
1610
+ var ConnectionCreateError = class extends Error {
1611
+ constructor(message) {
1612
+ super(message);
1613
+ this.name = "ConnectionCreateError";
1614
+ }
1615
+ };
1616
+ function shellQuote(value) {
1617
+ if (/^[A-Za-z0-9_./:=@+-]+$/.test(value)) {
1618
+ return value;
1619
+ }
1620
+ return `'${value.replace(/'/g, "'\\''")}'`;
1621
+ }
1622
+ function commandExists(command) {
1623
+ return executeShell(`command -v ${shellQuote(command)}`, { cwd: process.cwd(), timeoutMs: 1e4 });
1624
+ }
1625
+ function defaultDescription(input2) {
1626
+ return `Local ${input2.provider} CLI connection using \`${input2.command}\`.`;
1627
+ }
1628
+ async function createConnection(repoRoot, input2, options = {}) {
1629
+ if (!NAME_RE.test(input2.name)) {
1630
+ throw new ConnectionCreateError(
1631
+ `Invalid connection name \`${input2.name}\`. Use lowercase letters, numbers, and hyphens.`
1632
+ );
1633
+ }
1634
+ const scope = input2.scope ?? "project";
1635
+ const candidate = {
1636
+ name: input2.name,
1637
+ provider: input2.provider,
1638
+ kind: "cli",
1639
+ command: input2.command,
1640
+ description: input2.description ?? defaultDescription(input2),
1641
+ profile: input2.profile,
1642
+ risk: input2.risk ?? "medium",
1643
+ confirm: input2.confirm ?? input2.risk === "high",
1644
+ healthcheck: input2.healthcheck ? { run: input2.healthcheck, expectExitCode: 0 } : void 0,
1645
+ scope
1646
+ };
1647
+ const parsed = connectionManifestSchema.safeParse(candidate);
1648
+ if (!parsed.success) {
1649
+ const detail = parsed.error.issues.map((issue) => issue.message).join("; ");
1650
+ throw new ConnectionCreateError(`Invalid connection definition: ${detail}`);
1651
+ }
1652
+ const dir = scope === "project" ? projectObjectDir(repoRoot, "connections") : userObjectDir("connections", options.home);
1653
+ const filePath = path11.join(dir, `${input2.name}.yaml`);
1654
+ await mkdir4(dir, { recursive: true });
1655
+ await writeFile4(filePath, stringifyYaml2(parsed.data), { encoding: "utf8", flag: options.force ? "w" : "wx" }).catch(
1656
+ (error) => {
1657
+ if (error.code === "EEXIST") {
1658
+ throw new ConnectionCreateError(
1659
+ `Connection \`${input2.name}\` already exists at ${filePath}. Pass force to overwrite.`
1660
+ );
1661
+ }
1662
+ throw error;
1663
+ }
1664
+ );
1665
+ return { path: filePath, manifest: parsed.data };
1666
+ }
1667
+ async function checkConnection(repoRoot, connection) {
1668
+ const exists6 = await commandExists(connection.manifest.command);
1669
+ if (!exists6.ok) {
1670
+ return {
1671
+ name: connection.name,
1672
+ status: "error",
1673
+ message: `Command \`${connection.manifest.command}\` was not found on PATH.`,
1674
+ sourcePath: connection.sourcePath,
1675
+ command: connection.manifest.command,
1676
+ healthcheck: exists6
1677
+ };
1678
+ }
1679
+ if (!connection.manifest.healthcheck) {
1680
+ return {
1681
+ name: connection.name,
1682
+ status: "warning",
1683
+ message: "No healthcheck configured.",
1684
+ sourcePath: connection.sourcePath,
1685
+ command: connection.manifest.command
1686
+ };
1687
+ }
1688
+ const result = await executeShell(connection.manifest.healthcheck.run, { cwd: repoRoot, timeoutMs: 3e4 });
1689
+ const expected = connection.manifest.healthcheck.expectExitCode;
1690
+ if (result.exitCode !== expected) {
1691
+ return {
1692
+ name: connection.name,
1693
+ status: "error",
1694
+ message: `Healthcheck exited ${result.exitCode}; expected ${expected}.`,
1695
+ sourcePath: connection.sourcePath,
1696
+ command: connection.manifest.command,
1697
+ healthcheck: result
1698
+ };
1699
+ }
1700
+ return {
1701
+ name: connection.name,
1702
+ status: "ok",
1703
+ message: "Connection healthcheck passed.",
1704
+ sourcePath: connection.sourcePath,
1705
+ command: connection.manifest.command,
1706
+ healthcheck: result
1707
+ };
1708
+ }
1709
+ async function checkConnections(repoRoot, options = {}) {
1710
+ const harness = await resolveHarness(repoRoot, { home: options.home });
1711
+ return Promise.all(harness.connections.map((connection) => checkConnection(repoRoot, connection)));
1712
+ }
1713
+
1714
+ // src/core/tools/authorize.ts
1715
+ function authorizeTool(tool, options) {
1716
+ const trusted = options.trusted ?? true;
1717
+ const allowListed = options.allow.includes(tool.name);
1718
+ if (!trusted && !allowListed) {
1719
+ return {
1720
+ allowed: false,
1721
+ reason: "not-allowed",
1722
+ message: `\`${tool.name}\` is from an untrusted source. Add it to \`tools.allow\` in harness.yaml to permit it.`
1723
+ };
1724
+ }
1725
+ if (tool.manifest.confirm && options.confirmed !== true) {
1726
+ return {
1727
+ allowed: false,
1728
+ reason: "needs-confirmation",
1729
+ message: `\`${tool.name}\` requires confirmation before running.`
1730
+ };
1731
+ }
1732
+ if (tool.manifest.risk === "high" && options.confirmed !== true) {
1733
+ return {
1734
+ allowed: false,
1735
+ reason: "needs-confirmation",
1736
+ message: `\`${tool.name}\` is high risk and requires confirmation before running.`
1737
+ };
1738
+ }
1739
+ if (options.connectionRisk === "high" && tool.manifest.risk !== "low" && options.confirmed !== true) {
1740
+ return {
1741
+ allowed: false,
1742
+ reason: "needs-confirmation",
1743
+ message: `\`${tool.name}\` uses a high-risk connection and requires confirmation before running.`
1744
+ };
1745
+ }
1746
+ return { allowed: true };
1747
+ }
1748
+
1749
+ // src/core/tools/interpolate.ts
1750
+ var ToolInputError = class extends Error {
1751
+ constructor(message) {
1752
+ super(message);
1753
+ this.name = "ToolInputError";
1754
+ }
1755
+ };
1756
+ var INPUT_TOKEN = /\{\{\s*([\w-]+)\s*\}\}/g;
1757
+ function shellQuote2(value) {
1758
+ if (value === "") {
1759
+ return "''";
1760
+ }
1761
+ if (/^[A-Za-z0-9_@%+=:,./-]+$/.test(value)) {
1762
+ return value;
1763
+ }
1764
+ return `'${value.replace(/'/g, "'\\''")}'`;
1765
+ }
1766
+ function coerce(name, type, raw) {
1767
+ if (type === "string") {
1768
+ if (typeof raw === "string") {
1769
+ return raw;
1770
+ }
1771
+ if (typeof raw === "number" || typeof raw === "boolean") {
1772
+ return String(raw);
1773
+ }
1774
+ throw new ToolInputError(`Input \`${name}\` must be a string.`);
1775
+ }
1776
+ if (type === "number") {
1777
+ const value = typeof raw === "number" ? raw : Number(raw);
1778
+ if (typeof raw === "boolean" || Number.isNaN(value)) {
1779
+ throw new ToolInputError(`Input \`${name}\` must be a number.`);
1780
+ }
1781
+ return value;
1782
+ }
1783
+ if (typeof raw === "boolean") {
1784
+ return raw;
1785
+ }
1786
+ if (raw === "true") {
1787
+ return true;
1788
+ }
1789
+ if (raw === "false") {
1790
+ return false;
1791
+ }
1792
+ throw new ToolInputError(`Input \`${name}\` must be a boolean.`);
1793
+ }
1794
+ function resolveInputs(manifest, provided = {}) {
1795
+ const declared = manifest.input ?? {};
1796
+ const unknown = Object.keys(provided).filter((key) => !(key in declared));
1797
+ if (unknown.length > 0) {
1798
+ throw new ToolInputError(`Unknown input(s) for \`${manifest.name}\`: ${unknown.join(", ")}.`);
1799
+ }
1800
+ const values = {};
1801
+ const missing = [];
1802
+ for (const [name, param] of Object.entries(declared)) {
1803
+ if (name in provided && provided[name] !== void 0) {
1804
+ values[name] = coerce(name, param.type, provided[name]);
1805
+ } else if (param.default !== void 0) {
1806
+ values[name] = param.default;
1807
+ } else {
1808
+ missing.push(name);
1809
+ }
1810
+ }
1811
+ if (missing.length > 0) {
1812
+ throw new ToolInputError(`Missing required input(s) for \`${manifest.name}\`: ${missing.join(", ")}.`);
1813
+ }
1814
+ return values;
1815
+ }
1816
+ function interpolateRun(run2, values) {
1817
+ const result = run2.replace(INPUT_TOKEN, (_match, name) => {
1818
+ if (!(name in values)) {
1819
+ throw new ToolInputError(`Command references undeclared input \`${name}\`.`);
1820
+ }
1821
+ return shellQuote2(String(values[name]));
1822
+ });
1823
+ if (/\{\{|\}\}/.test(result)) {
1824
+ throw new ToolInputError("Command has malformed interpolation tokens.");
1825
+ }
1826
+ return result;
1827
+ }
1828
+ function inputEnv(values) {
1829
+ const env = { TR_INPUT_JSON: JSON.stringify(values) };
1830
+ for (const [name, value] of Object.entries(values)) {
1831
+ env[`TR_INPUT_${name.toUpperCase().replace(/-/g, "_")}`] = String(value);
1832
+ }
1833
+ return env;
1834
+ }
1835
+
1836
+ // src/core/tools/create.ts
1837
+ import { mkdir as mkdir5, writeFile as writeFile5 } from "fs/promises";
1838
+ import path12 from "path";
1839
+ import { stringify as stringifyYaml3 } from "yaml";
1840
+ var ToolCreateError = class extends Error {
1841
+ constructor(message) {
1842
+ super(message);
1843
+ this.name = "ToolCreateError";
1844
+ }
1845
+ };
1846
+ var NAME_RE2 = /^[a-z0-9][a-z0-9-]*$/;
1847
+ function assertSafeName(name) {
1848
+ if (!NAME_RE2.test(name)) {
1849
+ throw new ToolCreateError(
1850
+ `Invalid tool name \`${name}\`. Use lowercase letters, numbers, and hyphens.`
1851
+ );
1852
+ }
1853
+ }
1854
+ function assertSafeScript(script) {
1855
+ if (path12.isAbsolute(script) || script.split(/[\\/]/).includes("..")) {
1856
+ throw new ToolCreateError(`Script path must be inside the harness directory: ${script}`);
1857
+ }
1858
+ }
1859
+ async function createTool(repoRoot, input2, options) {
1860
+ assertSafeName(input2.name);
1861
+ if (input2.script) {
1862
+ assertSafeScript(input2.script);
1863
+ }
1864
+ const confirm = input2.confirm ?? options.actor === "agent";
1865
+ const scope = input2.scope ?? "project";
1866
+ const candidate = {
1867
+ name: input2.name,
1868
+ description: input2.description,
1869
+ scope,
1870
+ risk: input2.risk ?? "low",
1871
+ confirm,
1872
+ ...input2.connection ? { connection: input2.connection } : {},
1873
+ ...input2.healthcheck ? { healthcheck: input2.healthcheck } : {},
1874
+ input: input2.input ?? {},
1875
+ ...input2.run ? { run: input2.run } : {},
1876
+ ...input2.script ? { script: input2.script } : {}
1877
+ };
1878
+ const parsed = toolManifestSchema.safeParse(candidate);
1879
+ if (!parsed.success) {
1880
+ const detail = parsed.error.issues.map((issue) => issue.message).join("; ");
1881
+ throw new ToolCreateError(`Invalid tool definition: ${detail}`);
1882
+ }
1883
+ const dir = scope === "project" ? projectObjectDir(repoRoot, "tools") : userObjectDir("tools", options.home);
1884
+ const filePath = path12.join(dir, `${input2.name}.yaml`);
1885
+ await mkdir5(dir, { recursive: true });
1886
+ await writeFile5(filePath, stringifyYaml3(parsed.data), { encoding: "utf8", flag: options.force ? "w" : "wx" }).catch(
1887
+ (error) => {
1888
+ if (error.code === "EEXIST") {
1889
+ throw new ToolCreateError(`Tool \`${input2.name}\` already exists at ${filePath}. Pass force to overwrite.`);
1890
+ }
1891
+ throw error;
1892
+ }
1893
+ );
1894
+ return { path: filePath, scope, manifest: parsed.data };
1895
+ }
1896
+
1897
+ // src/core/tools/catalog.ts
1898
+ import { readFile as readFile6, stat as stat3 } from "fs/promises";
1899
+ import path13 from "path";
1900
+ var NAME_RE3 = /^[a-z0-9][a-z0-9-]*$/;
1901
+ var DESTRUCTIVE_RE = /\b(migrate|deploy|publish|release|prune|reset|destroy|drop|delete|rm|push|seed|wipe)\b/i;
1902
+ function looksDestructive(name, command) {
1903
+ return DESTRUCTIVE_RE.test(name) || DESTRUCTIVE_RE.test(command);
1904
+ }
1905
+ function sanitize(name) {
1906
+ const slug2 = name.toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/^-+|-+$/g, "");
1907
+ return NAME_RE3.test(slug2) ? slug2 : void 0;
1908
+ }
1909
+ async function exists2(file) {
1910
+ try {
1911
+ await stat3(file);
1912
+ return true;
1913
+ } catch {
1914
+ return false;
1915
+ }
1916
+ }
1917
+ async function detectPackageManager(repoRoot) {
1918
+ if (await exists2(path13.join(repoRoot, "pnpm-lock.yaml"))) {
1919
+ return "pnpm";
1920
+ }
1921
+ if (await exists2(path13.join(repoRoot, "yarn.lock"))) {
1922
+ return "yarn";
1923
+ }
1924
+ if (await exists2(path13.join(repoRoot, "bun.lockb"))) {
1925
+ return "bun";
1926
+ }
1927
+ return "npm";
1928
+ }
1929
+ var SCRIPT_PRIORITY = ["dev", "start", "build", "test", "lint", "typecheck", "format"];
1930
+ var HEALTHCHECK_NAMES = /* @__PURE__ */ new Set(["build", "test", "lint", "typecheck", "format"]);
1931
+ function healthcheckFor(name, command) {
1932
+ return HEALTHCHECK_NAMES.has(name) && !looksDestructive(name, command) ? command : void 0;
1933
+ }
1934
+ function orderScripts(names) {
1935
+ return [...names].sort((a, b) => {
1936
+ const ai = SCRIPT_PRIORITY.indexOf(a);
1937
+ const bi = SCRIPT_PRIORITY.indexOf(b);
1938
+ if (ai !== -1 || bi !== -1) {
1939
+ return (ai === -1 ? Infinity : ai) - (bi === -1 ? Infinity : bi);
1940
+ }
1941
+ return a.localeCompare(b);
1942
+ });
1943
+ }
1944
+ async function fromPackageJson(repoRoot) {
1945
+ const file = path13.join(repoRoot, "package.json");
1946
+ let raw;
1947
+ try {
1948
+ raw = await readFile6(file, "utf8");
1949
+ } catch {
1950
+ return [];
1951
+ }
1952
+ let scripts = {};
1953
+ try {
1954
+ const parsed = JSON.parse(raw);
1955
+ scripts = parsed.scripts ?? {};
1956
+ } catch {
1957
+ return [];
1958
+ }
1959
+ const pm = await detectPackageManager(repoRoot);
1960
+ const candidates = [];
1961
+ for (const scriptName of orderScripts(Object.keys(scripts))) {
1962
+ const name = sanitize(scriptName);
1963
+ if (!name) {
1964
+ continue;
1965
+ }
1966
+ const command = scripts[scriptName] ?? "";
1967
+ candidates.push({
1968
+ name,
1969
+ description: `Run the \`${scriptName}\` package script`,
1970
+ run: `${pm} run ${scriptName}`,
1971
+ risk: looksDestructive(scriptName, command) ? "high" : "low",
1972
+ confirm: looksDestructive(scriptName, command),
1973
+ healthcheck: healthcheckFor(scriptName, `${pm} run ${scriptName}`),
1974
+ source: "package.json"
1975
+ });
1976
+ }
1977
+ return candidates;
1978
+ }
1979
+ async function fromTargets(repoRoot, fileName, runner, source) {
1980
+ let raw;
1981
+ try {
1982
+ raw = await readFile6(path13.join(repoRoot, fileName), "utf8");
1983
+ } catch {
1984
+ return [];
1985
+ }
1986
+ const candidates = [];
1987
+ const seen = /* @__PURE__ */ new Set();
1988
+ for (const line of raw.split(/\r?\n/)) {
1989
+ const match = /^([a-zA-Z][\w-]*)\s*:/.exec(line);
1990
+ if (!match) {
1991
+ continue;
1992
+ }
1993
+ const target = match[1];
1994
+ if (target === "PHONY" || seen.has(target)) {
1995
+ continue;
1996
+ }
1997
+ seen.add(target);
1998
+ const name = sanitize(target);
1999
+ if (!name) {
2000
+ continue;
2001
+ }
2002
+ candidates.push({
2003
+ name,
2004
+ description: `Run the \`${target}\` ${source === "makefile" ? "Make target" : "recipe"}`,
2005
+ run: `${runner} ${target}`,
2006
+ risk: looksDestructive(target, "") ? "high" : "low",
2007
+ confirm: looksDestructive(target, ""),
2008
+ healthcheck: healthcheckFor(target, `${runner} ${target}`),
2009
+ source
2010
+ });
2011
+ }
2012
+ return candidates;
2013
+ }
2014
+ var PROFILE_STARTERS = {
2015
+ nextjs: [
2016
+ starter("dev", "Start the Next.js dev server", "next dev"),
2017
+ starter("build", "Build the Next.js app", "next build"),
2018
+ starter("lint", "Lint with Next.js ESLint", "next lint")
2019
+ ],
2020
+ "vite-react": [
2021
+ starter("dev", "Start the Vite dev server", "vite"),
2022
+ starter("build", "Build the app", "vite build"),
2023
+ starter("test", "Run the test suite", "vitest run")
2024
+ ],
2025
+ fastapi: [
2026
+ starter("dev", "Run the FastAPI dev server", "uvicorn app.main:app --reload"),
2027
+ starter("test", "Run the test suite", "pytest"),
2028
+ starter("lint", "Lint with Ruff", "ruff check .")
2029
+ ],
2030
+ "python-cli": [
2031
+ starter("test", "Run the test suite", "pytest"),
2032
+ starter("lint", "Lint with Ruff", "ruff check ."),
2033
+ starter("format", "Format with Ruff", "ruff format .")
2034
+ ],
2035
+ "node-cli": [
2036
+ starter("build", "Build the project", "npm run build"),
2037
+ starter("test", "Run the test suite", "npm test"),
2038
+ starter("lint", "Lint the project", "npm run lint")
2039
+ ],
2040
+ dbt: [
2041
+ starter("build", "Build dbt models", "dbt build"),
2042
+ starter("run", "Run dbt models", "dbt run"),
2043
+ starter("test", "Test dbt models", "dbt test")
2044
+ ],
2045
+ empty: []
2046
+ };
2047
+ function starter(name, description, run2) {
2048
+ const destructive = looksDestructive(name, run2);
2049
+ return {
2050
+ name,
2051
+ description,
2052
+ run: run2,
2053
+ risk: destructive ? "high" : "low",
2054
+ confirm: destructive,
2055
+ healthcheck: healthcheckFor(name, run2),
2056
+ source: "profile"
2057
+ };
2058
+ }
2059
+ function dedupeByName(candidates) {
2060
+ const seen = /* @__PURE__ */ new Set();
2061
+ const result = [];
2062
+ for (const candidate of candidates) {
2063
+ if (seen.has(candidate.name)) {
2064
+ continue;
2065
+ }
2066
+ seen.add(candidate.name);
2067
+ result.push(candidate);
2068
+ }
2069
+ return result;
2070
+ }
2071
+ function profileStarterTools(profile) {
2072
+ return PROFILE_STARTERS[profile] ?? [];
2073
+ }
2074
+ async function detectToolCandidates(repoRoot, profile = "empty") {
2075
+ const detected = dedupeByName([
2076
+ ...await fromPackageJson(repoRoot),
2077
+ ...await fromTargets(repoRoot, "Makefile", "make", "makefile"),
2078
+ ...await fromTargets(repoRoot, "justfile", "just", "justfile")
2079
+ ]);
2080
+ if (detected.length > 0) {
2081
+ return detected;
2082
+ }
2083
+ return profileStarterTools(profile);
2084
+ }
2085
+
2086
+ // src/core/tools/index.ts
2087
+ var ToolNotFoundError = class extends Error {
2088
+ constructor(name) {
2089
+ super(`Unknown tool: \`${name}\`.`);
2090
+ this.name = "ToolNotFoundError";
2091
+ }
2092
+ };
2093
+ async function runTool(repoRoot, options) {
2094
+ const harness = options.harness ?? await resolveHarness(repoRoot, { home: options.home });
2095
+ const tool = harness.tools.find((entry) => entry.name === options.name);
2096
+ if (!tool) {
2097
+ throw new ToolNotFoundError(options.name);
2098
+ }
2099
+ const [projectLock, userLock] = await Promise.all([
2100
+ readLockFile(projectLockPath(repoRoot)),
2101
+ readLockFile(userLockPath(options.home))
2102
+ ]);
2103
+ const external = /* @__PURE__ */ new Set([...externalToolNames(projectLock), ...externalToolNames(userLock)]);
2104
+ const connection = tool.manifest.connection ? harness.connections.find((entry) => entry.name === tool.manifest.connection) : void 0;
2105
+ if (tool.manifest.connection && !connection) {
2106
+ return {
2107
+ status: "blocked",
2108
+ tool: tool.name,
2109
+ reason: "not-allowed",
2110
+ message: `\`${tool.name}\` references unknown connection \`${tool.manifest.connection}\`.`
2111
+ };
2112
+ }
2113
+ const decision = authorizeTool(tool, {
2114
+ allow: harness.manifest.tools.allow,
2115
+ confirmed: options.confirmed,
2116
+ trusted: !external.has(tool.name),
2117
+ connectionRisk: connection?.manifest.risk
2118
+ });
2119
+ if (!decision.allowed) {
2120
+ return { status: "blocked", tool: tool.name, reason: decision.reason, message: decision.message };
2121
+ }
2122
+ const values = resolveInputs(tool.manifest, options.input);
2123
+ const env = inputEnv(values);
2124
+ const execOptions = { cwd: repoRoot, env, timeoutMs: options.timeoutMs, signal: options.signal };
2125
+ const result = tool.manifest.run ? await executeShell(interpolateRun(tool.manifest.run, values), execOptions) : await executeScript(repoRoot, tool.manifest.script, execOptions);
2126
+ return { status: "ran", tool: tool.name, result };
2127
+ }
2128
+ async function checkToolHealth(repoRoot, tool) {
2129
+ if (!tool.manifest.healthcheck) {
2130
+ return { status: "skipped", tool: tool.name, message: "No healthcheck configured." };
2131
+ }
2132
+ const result = await executeShell(tool.manifest.healthcheck.run, { cwd: repoRoot, timeoutMs: 3e4 });
2133
+ const expected = tool.manifest.healthcheck.expectExitCode;
2134
+ if (result.exitCode !== expected) {
2135
+ return {
2136
+ status: "error",
2137
+ tool: tool.name,
2138
+ message: `Healthcheck exited ${result.exitCode}; expected ${expected}.`,
2139
+ result
2140
+ };
2141
+ }
2142
+ return { status: "ok", tool: tool.name, result };
2143
+ }
2144
+
2145
+ // src/core/doctor.ts
2146
+ async function exists3(filePath) {
2147
+ try {
2148
+ await stat4(filePath);
2149
+ return true;
2150
+ } catch (error) {
2151
+ if (error.code === "ENOENT") {
2152
+ return false;
2153
+ }
2154
+ throw error;
2155
+ }
2156
+ }
2157
+ function finding2(severity, code, message, pathValue) {
2158
+ return pathValue ? { severity, code, message, path: pathValue } : { severity, code, message };
2159
+ }
2160
+ async function mcpConfigWarnings(repoRoot) {
2161
+ const configs = [".vscode/mcp.json", ".cursor/mcp.json", ".mcp.json"];
2162
+ const present = await Promise.all(configs.map((config) => exists3(path14.join(repoRoot, config))));
2163
+ if (present.some(Boolean)) {
2164
+ return [];
2165
+ }
2166
+ return [
2167
+ finding2(
2168
+ "warning",
2169
+ "mcp_config_missing",
2170
+ "No project-local MCP config found. Run `threadroot mcp setup --write` if this repo should expose Threadroot tools to local agents."
2171
+ )
2172
+ ];
2173
+ }
2174
+ async function doctor(repoRoot, options = {}) {
2175
+ const findings = [];
2176
+ let harness;
2177
+ try {
2178
+ harness = await resolveHarness(repoRoot, { home: options.home });
2179
+ } catch (error) {
2180
+ if (error instanceof HarnessError) {
2181
+ findings.push(finding2("error", "harness_invalid", error.message));
2182
+ return summarize(findings);
2183
+ }
2184
+ throw error;
2185
+ }
2186
+ try {
2187
+ const files = await compile(repoRoot, harness);
2188
+ const drift = await detectDrift(repoRoot, files);
2189
+ for (const entry of drift) {
2190
+ if (entry.status === "create") {
2191
+ findings.push(finding2("error", "compiled_output_missing", `Missing compiled output: ${entry.path}`, entry.path));
2192
+ } else if (entry.status === "drift") {
2193
+ findings.push(
2194
+ finding2(
2195
+ "warning",
2196
+ "compiled_output_drift",
2197
+ `Compiled output differs from the canonical harness: ${entry.path}`,
2198
+ entry.path
2199
+ )
2200
+ );
2201
+ }
2202
+ }
2203
+ } catch (error) {
2204
+ findings.push(
2205
+ finding2(
2206
+ "error",
2207
+ "compile_failed",
2208
+ `Cannot compile harness: ${error instanceof Error ? error.message : String(error)}`
2209
+ )
2210
+ );
2211
+ }
2212
+ for (const tool of harness.tools) {
2213
+ if (tool.manifest.confirm) {
2214
+ findings.push(
2215
+ finding2(
2216
+ "warning",
2217
+ "tool_requires_confirmation",
2218
+ `Tool \`${tool.name}\` requires explicit confirmation before running.`,
2219
+ tool.sourcePath
2220
+ )
2221
+ );
2222
+ }
2223
+ if (tool.manifest.risk === "high" && !tool.manifest.confirm) {
2224
+ findings.push(
2225
+ finding2(
2226
+ "warning",
2227
+ "high_risk_tool_without_confirm",
2228
+ `High-risk tool \`${tool.name}\` does not set confirm:true. Runtime execution will still require confirmation.`,
2229
+ tool.sourcePath
2230
+ )
2231
+ );
2232
+ }
2233
+ if (tool.manifest.connection && !harness.connections.some((connection) => connection.name === tool.manifest.connection)) {
2234
+ findings.push(
2235
+ finding2(
2236
+ "error",
2237
+ "unknown_tool_connection",
2238
+ `Tool \`${tool.name}\` references unknown connection \`${tool.manifest.connection}\`.`,
2239
+ tool.sourcePath
2240
+ )
2241
+ );
2242
+ }
2243
+ const toolHealth = await checkToolHealth(repoRoot, tool);
2244
+ if (toolHealth.status === "error") {
2245
+ findings.push(
2246
+ finding2("error", "tool_healthcheck_failed", `Tool \`${tool.name}\`: ${toolHealth.message}`, tool.sourcePath)
2247
+ );
2248
+ }
2249
+ }
2250
+ for (const connection of harness.connections) {
2251
+ if (connection.manifest.risk === "high" && !connection.manifest.confirm) {
2252
+ findings.push(
2253
+ finding2(
2254
+ "warning",
2255
+ "high_risk_connection_without_confirm",
2256
+ `High-risk connection \`${connection.name}\` should set confirm:true.`,
2257
+ connection.sourcePath
2258
+ )
2259
+ );
2260
+ }
2261
+ const check = await checkConnection(repoRoot, connection);
2262
+ if (check.status === "error") {
2263
+ findings.push(
2264
+ finding2(
2265
+ "error",
2266
+ "connection_check_failed",
2267
+ `Connection \`${connection.name}\`: ${check.message}`,
2268
+ connection.sourcePath
2269
+ )
2270
+ );
2271
+ } else if (check.status === "warning") {
2272
+ findings.push(
2273
+ finding2(
2274
+ "warning",
2275
+ "connection_check_warning",
2276
+ `Connection \`${connection.name}\`: ${check.message}`,
2277
+ connection.sourcePath
2278
+ )
2279
+ );
2280
+ }
2281
+ }
2282
+ const skillReport = await validateResolvedSkillsDeep(harness);
2283
+ for (const skillFinding of skillReport.findings) {
2284
+ findings.push(
2285
+ finding2(
2286
+ skillFinding.severity,
2287
+ `skill_${skillFinding.severity}`,
2288
+ `Skill \`${skillFinding.skill}\`: ${skillFinding.message}`,
2289
+ skillFinding.path
2290
+ )
2291
+ );
2292
+ }
2293
+ const [projectLock, userLock] = await Promise.all([
2294
+ readLockFile(projectLockPath(repoRoot)),
2295
+ readLockFile(userLockPath(options.home))
2296
+ ]);
2297
+ const externalTools = /* @__PURE__ */ new Set([...externalToolNames(projectLock), ...externalToolNames(userLock)]);
2298
+ const externalSkills = /* @__PURE__ */ new Set([...externalSkillNames(projectLock), ...externalSkillNames(userLock)]);
2299
+ for (const name of externalTools) {
2300
+ if (!harness.manifest.tools.allow.includes(name)) {
2301
+ findings.push(
2302
+ finding2(
2303
+ "error",
2304
+ "external_tool_not_allowed",
2305
+ `External installed tool \`${name}\` is not listed in tools.allow.`
2306
+ )
2307
+ );
2308
+ }
2309
+ }
2310
+ for (const skill of harness.skills) {
2311
+ if (!externalSkills.has(skill.name)) {
2312
+ continue;
2313
+ }
2314
+ if (skill.frontmatter.allowedTools) {
2315
+ findings.push(
2316
+ finding2(
2317
+ "warning",
2318
+ "external_skill_allowed_tools",
2319
+ `External installed skill \`${skill.name}\` declares allowed tools; inspect it before trusting.`,
2320
+ skill.sourcePath
2321
+ )
2322
+ );
2323
+ }
2324
+ const scriptsDir = path14.join(path14.dirname(skill.sourcePath), "scripts");
2325
+ if (await exists3(scriptsDir)) {
2326
+ findings.push(
2327
+ finding2(
2328
+ "warning",
2329
+ "external_skill_scripts",
2330
+ `External installed skill \`${skill.name}\` includes scripts; inspect them before trusting.`,
2331
+ scriptsDir
2332
+ )
2333
+ );
2334
+ }
2335
+ }
2336
+ findings.push(...await mcpConfigWarnings(repoRoot));
2337
+ return summarize(findings);
2338
+ }
2339
+ function summarize(findings) {
2340
+ const errors = findings.filter((entry) => entry.severity === "error").length;
2341
+ const warnings = findings.filter((entry) => entry.severity === "warning").length;
2342
+ return { ok: errors === 0, findings, summary: { errors, warnings } };
2343
+ }
2344
+
2345
+ // src/commands/doctor.ts
2346
+ async function runDoctor(repoRoot) {
2347
+ const report = await doctor(repoRoot);
2348
+ if (report.findings.length === 0) {
2349
+ console.log("Threadroot doctor: clean");
2350
+ return;
2351
+ }
2352
+ console.log(`Threadroot doctor: ${report.summary.errors} error(s), ${report.summary.warnings} warning(s)`);
2353
+ for (const finding3 of report.findings) {
2354
+ const label = finding3.severity === "error" ? "error" : "warning";
2355
+ const suffix = finding3.path ? ` (${finding3.path})` : "";
2356
+ console.log(`- ${label} ${finding3.code}: ${finding3.message}${suffix}`);
2357
+ }
2358
+ if (!report.ok) {
2359
+ process.exitCode = 1;
2360
+ }
2361
+ }
2362
+
2363
+ // src/core/init/index.ts
2364
+ import { mkdir as mkdir7, stat as stat6, writeFile as writeFile6 } from "fs/promises";
2365
+ import path19 from "path";
2366
+
2367
+ // src/core/scan/package.ts
2368
+ import fs2 from "fs/promises";
2369
+ import path15 from "path";
2370
+
2371
+ // src/core/scan/rules.ts
2372
+ var ignoredDirectories = /* @__PURE__ */ new Set([
2373
+ ".git",
2374
+ ".threadroot",
2375
+ "node_modules",
2376
+ "dist",
2377
+ "coverage",
2378
+ ".next",
2379
+ "target",
2380
+ "dbt_packages",
2381
+ "__pycache__"
2382
+ ]);
2383
+
2384
+ // src/core/scan/package.ts
2385
+ async function readJson(repoRoot, relativePath) {
2386
+ try {
2387
+ return JSON.parse(await fs2.readFile(path15.join(repoRoot, relativePath), "utf8"));
2388
+ } catch {
2389
+ return void 0;
2390
+ }
2391
+ }
2392
+ function inferProfile(files, packageJson) {
2393
+ if (files.some((file) => path15.basename(file) === "dbt_project.yml" || path15.basename(file) === "dbt_project.yaml")) {
2394
+ return "dbt";
2395
+ }
2396
+ const packageMeta = packageJson && typeof packageJson === "object" ? packageJson : void 0;
2397
+ const dependencies = packageJson && typeof packageJson === "object" ? {
2398
+ ...packageJson.dependencies,
2399
+ ...packageJson.devDependencies
2400
+ } : {};
2401
+ if ("next" in dependencies) {
2402
+ return "nextjs";
2403
+ }
2404
+ if ("vite" in dependencies || files.some((file) => file.startsWith("vite.config."))) {
2405
+ return "vite-react";
2406
+ }
2407
+ if (packageMeta?.bin || "commander" in dependencies || "ink" in dependencies || "tsup" in dependencies || files.some((file) => file.startsWith("src/commands/"))) {
2408
+ return "node-cli";
2409
+ }
2410
+ if (files.includes("pyproject.toml")) {
2411
+ return "python-cli";
2412
+ }
2413
+ return "unknown";
2414
+ }
2415
+
2416
+ // src/core/scan/walk.ts
2417
+ import fs3 from "fs/promises";
2418
+ import path16 from "path";
2419
+ function toPosix(relativePath) {
2420
+ return relativePath.split(path16.sep).join("/");
2421
+ }
2422
+ async function walkRepo(repoRoot, directory = repoRoot) {
2423
+ let entries;
2424
+ try {
2425
+ entries = await fs3.readdir(directory, { withFileTypes: true });
2426
+ } catch {
2427
+ return [];
2428
+ }
2429
+ const files = [];
2430
+ for (const entry of entries) {
2431
+ const absolutePath = path16.join(directory, entry.name);
2432
+ const relativePath = toPosix(path16.relative(repoRoot, absolutePath));
2433
+ if (entry.isDirectory()) {
2434
+ if (!ignoredDirectories.has(entry.name)) {
2435
+ files.push(...await walkRepo(repoRoot, absolutePath));
2436
+ }
2437
+ continue;
2438
+ }
2439
+ if (entry.isFile()) {
2440
+ files.push(relativePath);
2441
+ }
2442
+ }
2443
+ return files;
2444
+ }
2445
+
2446
+ // src/core/init/index.ts
2447
+ import { stringify as stringifyYaml4 } from "yaml";
2448
+
2449
+ // src/core/init/builtins.ts
2450
+ import { cp, mkdir as mkdir6, readdir as readdir3, stat as stat5 } from "fs/promises";
2451
+ import path17 from "path";
2452
+ import { fileURLToPath } from "url";
2453
+ var DIST_DIR = path17.dirname(fileURLToPath(import.meta.url));
2454
+ var PACKAGE_ROOT_FROM_BUNDLE = path17.resolve(DIST_DIR, "..");
2455
+ var PACKAGE_ROOT_FROM_DIST = path17.resolve(DIST_DIR, "../../..");
2456
+ var PACKAGE_ROOT_FROM_SRC = path17.resolve(DIST_DIR, "../../../..");
2457
+ var SKILL_PACK_CANDIDATES = [
2458
+ path17.join(PACKAGE_ROOT_FROM_BUNDLE, "skills"),
2459
+ path17.join(PACKAGE_ROOT_FROM_DIST, "skills"),
2460
+ path17.join(PACKAGE_ROOT_FROM_SRC, "skills")
2461
+ ];
2462
+ var PROJECT_MEMORY_TEMPLATE = [
2463
+ "# Project",
2464
+ "",
2465
+ "<!-- Stable, rarely-changing facts about this project. Keep it short. -->",
2466
+ "",
2467
+ "- What it is:",
2468
+ "- Key technologies:",
2469
+ "- How to run it:"
2470
+ ].join("\n");
2471
+ async function exists4(target) {
2472
+ try {
2473
+ const info = await stat5(target);
2474
+ return info.isDirectory();
2475
+ } catch {
2476
+ return false;
2477
+ }
2478
+ }
2479
+ async function bundledSkillsDir() {
2480
+ for (const candidate of SKILL_PACK_CANDIDATES) {
2481
+ if (await exists4(candidate)) {
2482
+ return candidate;
2483
+ }
2484
+ }
2485
+ throw new Error("Bundled Threadroot skills were not found in this package.");
2486
+ }
2487
+ async function writeBuiltinSkills(repoRoot) {
2488
+ const sourceDir = await bundledSkillsDir();
2489
+ const targetDir = projectObjectDir(repoRoot, "skills");
2490
+ await mkdir6(targetDir, { recursive: true });
2491
+ const written = [];
2492
+ const entries = await readdir3(sourceDir, { withFileTypes: true });
2493
+ for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
2494
+ if (!entry.isDirectory()) {
2495
+ continue;
2496
+ }
2497
+ const sourceSkill = path17.join(sourceDir, entry.name);
2498
+ const sourceSkillFile = path17.join(sourceSkill, "SKILL.md");
2499
+ if (!await exists4(sourceSkill) || !await stat5(sourceSkillFile).then((info) => info.isFile()).catch(() => false)) {
2500
+ continue;
2501
+ }
2502
+ const targetSkill = path17.join(targetDir, entry.name);
2503
+ const targetSkillFile = path17.join(targetSkill, "SKILL.md");
2504
+ try {
2505
+ await cp(sourceSkill, targetSkill, { recursive: true, force: false, errorOnExist: true });
2506
+ written.push(targetSkillFile);
2507
+ } catch (error) {
2508
+ if (error.code !== "ERR_FS_CP_EEXIST" && error.code !== "EEXIST") {
2509
+ throw error;
2510
+ }
2511
+ }
2512
+ }
2513
+ return written;
2514
+ }
2515
+
2516
+ // src/core/init/import.ts
2517
+ import { readFile as readFile7, readdir as readdir4 } from "fs/promises";
2518
+ import path18 from "path";
2519
+ var PROSE_PRECEDENCE = ["AGENTS.md", "CLAUDE.md", ".github/copilot-instructions.md"];
2520
+ var CURSOR_RULES_DIR = ".cursor/rules";
2521
+ var NAME_RE4 = /^[a-z0-9][a-z0-9-]*$/;
2522
+ async function readIfExists3(filePath) {
2523
+ try {
2524
+ return await readFile7(filePath, "utf8");
2525
+ } catch (error) {
2526
+ if (error.code === "ENOENT") {
2527
+ return void 0;
2528
+ }
2529
+ throw error;
2530
+ }
2531
+ }
2532
+ function normalize(text) {
2533
+ return text.toLowerCase().replace(/\s+/g, " ").trim();
2534
+ }
2535
+ function splitSections(markdown) {
2536
+ const sections = [];
2537
+ let heading = "";
2538
+ let buffer = [];
2539
+ const flush = () => {
2540
+ const text = buffer.join("\n").trim();
2541
+ if (text) {
2542
+ sections.push({ heading, text });
2543
+ }
2544
+ };
2545
+ for (const line of markdown.split(/\r?\n/)) {
2546
+ if (/^#{1,6}\s/.test(line)) {
2547
+ flush();
2548
+ heading = normalize(line.replace(/^#+\s*/, ""));
2549
+ buffer = [line];
2550
+ } else {
2551
+ buffer.push(line);
2552
+ }
2553
+ }
2554
+ flush();
2555
+ return sections;
2556
+ }
2557
+ function novelSections(canonical, other) {
2558
+ const haystack = normalize(canonical);
2559
+ const seenHeadings = new Set(splitSections(canonical).map((section) => section.heading).filter(Boolean));
2560
+ return splitSections(other).filter((section) => {
2561
+ if (section.heading && seenHeadings.has(section.heading)) {
2562
+ return false;
2563
+ }
2564
+ return !haystack.includes(normalize(section.text));
2565
+ });
2566
+ }
2567
+ async function listCursorRules(repoRoot) {
2568
+ const dir = path18.join(repoRoot, CURSOR_RULES_DIR);
2569
+ let entries;
2570
+ try {
2571
+ entries = await readdir4(dir);
2572
+ } catch (error) {
2573
+ if (error.code === "ENOENT") {
2574
+ return [];
2575
+ }
2576
+ throw error;
2577
+ }
2578
+ const files = entries.filter((name) => name.endsWith(".mdc")).sort();
2579
+ return Promise.all(
2580
+ files.map(async (name) => ({
2581
+ file: `${CURSOR_RULES_DIR}/${name}`,
2582
+ content: await readFile7(path18.join(dir, name), "utf8")
2583
+ }))
2584
+ );
2585
+ }
2586
+ function globsToApplyTo(value) {
2587
+ if (typeof value === "string") {
2588
+ const first = value.split(",")[0]?.trim();
2589
+ return first || void 0;
2590
+ }
2591
+ if (Array.isArray(value) && value.length > 0 && typeof value[0] === "string") {
2592
+ return value[0].trim() || void 0;
2593
+ }
2594
+ return void 0;
2595
+ }
2596
+ function ruleName(fileName) {
2597
+ const base = path18.basename(fileName, ".mdc").toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/^-+|-+$/g, "");
2598
+ return NAME_RE4.test(base) ? base : "imported-rule";
2599
+ }
2600
+ async function importVendorFiles(repoRoot, options = {}) {
2601
+ const include = options.include ? new Set(options.include) : void 0;
2602
+ const wanted = (file) => !include || include.has(file);
2603
+ const prose = [];
2604
+ for (const file of PROSE_PRECEDENCE) {
2605
+ if (!wanted(file)) {
2606
+ continue;
2607
+ }
2608
+ const content = await readIfExists3(path18.join(repoRoot, file));
2609
+ if (content && content.trim()) {
2610
+ prose.push({ file, content });
2611
+ }
2612
+ }
2613
+ const cursorRules = (await listCursorRules(repoRoot)).filter((rule) => wanted(rule.file));
2614
+ let canonicalSource;
2615
+ let canonicalBody = "";
2616
+ let rest = [];
2617
+ if (prose.length > 0) {
2618
+ canonicalSource = prose[0].file;
2619
+ canonicalBody = extractHandAuthored(prose[0].content);
2620
+ rest = prose.slice(1);
2621
+ } else if (cursorRules.length > 0) {
2622
+ canonicalSource = CURSOR_RULES_DIR;
2623
+ canonicalBody = cursorRules.map((rule) => parseFrontmatter(rule.content).body).join("\n\n").trim();
2624
+ }
2625
+ const foldedFrom = [];
2626
+ const skippedDuplicates = [];
2627
+ let body = canonicalBody;
2628
+ for (const file of rest) {
2629
+ const hand = extractHandAuthored(file.content);
2630
+ const novel = novelSections(body, hand);
2631
+ if (novel.length === 0) {
2632
+ skippedDuplicates.push(file.file);
2633
+ continue;
2634
+ }
2635
+ body = `${body}
2636
+
2637
+ <!-- imported from ${file.file} -->
2638
+ ${novel.map((section) => section.text).join("\n\n")}`.trim();
2639
+ foldedFrom.push(file.file);
2640
+ }
2641
+ const importedRules = cursorRules.map((rule) => {
2642
+ const { data, body: ruleBody2 } = parseFrontmatter(rule.content);
2643
+ return {
2644
+ name: ruleName(rule.file),
2645
+ applyTo: globsToApplyTo(data.globs ?? data.applyTo),
2646
+ body: ruleBody2.trim()
2647
+ };
2648
+ });
2649
+ return { canonicalSource, canonicalBody: body, foldedFrom, importedRules, skippedDuplicates };
2650
+ }
2651
+
2652
+ // src/core/init/index.ts
2653
+ var DEFAULT_ADAPTERS = ["agents", "claude", "copilot", "cursor"];
2654
+ var AGENTS_FILE2 = "AGENTS.md";
2655
+ var InitError = class extends Error {
2656
+ constructor(message) {
2657
+ super(message);
2658
+ this.name = "InitError";
2659
+ }
2660
+ };
2661
+ async function pathExists(target) {
2662
+ try {
2663
+ await stat6(target);
2664
+ return true;
2665
+ } catch {
2666
+ return false;
2667
+ }
2668
+ }
2669
+ async function detectProfile(repoRoot, override) {
2670
+ if (override) {
2671
+ return override;
2672
+ }
2673
+ const files = await walkRepo(repoRoot);
2674
+ const packageJson = await readJson(repoRoot, "package.json");
2675
+ const inferred = inferProfile(files, packageJson);
2676
+ return inferred === "unknown" ? "empty" : inferred;
2677
+ }
2678
+ async function detectName(repoRoot) {
2679
+ const packageJson = await readJson(repoRoot, "package.json");
2680
+ if (packageJson && typeof packageJson.name === "string" && packageJson.name.trim()) {
2681
+ return packageJson.name.trim();
2682
+ }
2683
+ return path19.basename(repoRoot);
2684
+ }
2685
+ async function writeManifest(repoRoot, manifest) {
2686
+ const body = {
2687
+ name: manifest.name,
2688
+ version: manifest.version,
2689
+ profile: manifest.profile,
2690
+ adapters: manifest.adapters
2691
+ };
2692
+ if (manifest.tools.allow.length > 0) {
2693
+ body.tools = { allow: manifest.tools.allow };
2694
+ }
2695
+ await mkdir7(projectHarnessDir(repoRoot), { recursive: true });
2696
+ await writeFile6(projectManifestPath(repoRoot), stringifyYaml4(body), "utf8");
2697
+ }
2698
+ async function writeProjectMemory(repoRoot) {
2699
+ const dir = projectObjectDir(repoRoot, "memory");
2700
+ await mkdir7(dir, { recursive: true });
2701
+ const filePath = path19.join(dir, "project.md");
2702
+ try {
2703
+ await writeFile6(filePath, `${PROJECT_MEMORY_TEMPLATE}
2704
+ `, { encoding: "utf8", flag: "wx" });
2705
+ return [filePath];
2706
+ } catch (error) {
2707
+ if (error.code === "EEXIST") {
2708
+ return [];
2709
+ }
2710
+ throw error;
2711
+ }
2712
+ }
2713
+ async function writeImportedRules(repoRoot, report) {
2714
+ if (report.importedRules.length === 0) {
2715
+ return [];
2716
+ }
2717
+ const dir = projectObjectDir(repoRoot, "rules");
2718
+ await mkdir7(dir, { recursive: true });
2719
+ const written = [];
2720
+ for (const rule of report.importedRules) {
2721
+ const filePath = path19.join(dir, `${rule.name}.md`);
2722
+ const data = { name: rule.name, scope: "project" };
2723
+ if (rule.applyTo) {
2724
+ data.applyTo = rule.applyTo;
2725
+ }
2726
+ try {
2727
+ await writeFile6(filePath, serializeFrontmatter(data, rule.body), { encoding: "utf8", flag: "wx" });
2728
+ written.push(filePath);
2729
+ } catch (error) {
2730
+ if (error.code !== "EEXIST") {
2731
+ throw error;
2732
+ }
2733
+ }
2734
+ }
2735
+ return written;
2736
+ }
2737
+ async function writeStarterTools(repoRoot, profile, force) {
2738
+ const candidates = await detectToolCandidates(repoRoot, profile);
2739
+ const names = [];
2740
+ for (const candidate of candidates) {
2741
+ try {
2742
+ await createTool(
2743
+ repoRoot,
2744
+ {
2745
+ name: candidate.name,
2746
+ description: candidate.description,
2747
+ run: candidate.run,
2748
+ risk: candidate.risk,
2749
+ confirm: candidate.confirm
2750
+ },
2751
+ { actor: "human", force }
2752
+ );
2753
+ names.push(candidate.name);
2754
+ } catch (error) {
2755
+ if (error instanceof ToolCreateError) {
2756
+ continue;
2757
+ }
2758
+ throw error;
2759
+ }
2760
+ }
2761
+ return names;
2762
+ }
2763
+ async function initHarness(repoRoot, options = {}) {
2764
+ if (!options.force && await pathExists(projectManifestPath(repoRoot))) {
2765
+ throw new InitError(
2766
+ `A harness already exists at ${path19.join(".threadroot", "harness.yaml")}. Re-run with --force to overwrite.`
2767
+ );
2768
+ }
2769
+ const profile = await detectProfile(repoRoot, options.profile);
2770
+ const name = await detectName(repoRoot);
2771
+ const adapters = options.adapters ?? DEFAULT_ADAPTERS;
2772
+ const tools2 = await writeStarterTools(repoRoot, profile, options.force ?? false);
2773
+ const skills = await writeBuiltinSkills(repoRoot);
2774
+ const memory = await writeProjectMemory(repoRoot);
2775
+ const manifest = harnessManifestSchema.parse({
2776
+ name,
2777
+ version: 1,
2778
+ profile,
2779
+ adapters,
2780
+ tools: { allow: tools2 }
2781
+ });
2782
+ await writeManifest(repoRoot, manifest);
2783
+ let report;
2784
+ let rules = [];
2785
+ if (options.import !== false) {
2786
+ report = await importVendorFiles(repoRoot, { include: options.importFiles });
2787
+ if (report.canonicalBody.trim()) {
2788
+ await writeFile6(path19.join(repoRoot, AGENTS_FILE2), `${report.canonicalBody.trim()}
2789
+ `, "utf8");
2790
+ }
2791
+ rules = await writeImportedRules(repoRoot, report);
2792
+ }
2793
+ const { written } = await runCompile(repoRoot, { home: options.home });
2794
+ return { name, profile, adapters, skills, tools: tools2, memory, rules, import: report, compiled: written };
2795
+ }
2796
+
2797
+ // src/commands/init.ts
2798
+ function parseAdapters(value) {
2799
+ if (!value) {
2800
+ return void 0;
2801
+ }
2802
+ return value.split(",").map((entry) => entry.trim()).filter(Boolean).map((entry) => adapterIdSchema.parse(entry));
2803
+ }
2804
+ async function runInit(repoRoot, options) {
2805
+ const initOptions = {
2806
+ force: options.force,
2807
+ import: options.import,
2808
+ profile: options.profile ? profileIdSchema.parse(options.profile) : void 0,
2809
+ adapters: parseAdapters(options.adapters)
2810
+ };
2811
+ try {
2812
+ const report = await initHarness(repoRoot, initOptions);
2813
+ console.log(`Initialized harness \`${report.name}\` (profile: ${report.profile}).`);
2814
+ console.log(`adapters: ${report.adapters.join(", ")}`);
2815
+ console.log(`skills: ${report.skills.length}, tools: ${report.tools.length}, memory: ${report.memory.length}`);
2816
+ if (report.import?.canonicalSource) {
2817
+ console.log(`imported canonical prose from ${report.import.canonicalSource}`);
2818
+ if (report.import.foldedFrom.length > 0) {
2819
+ console.log(` folded novel sections from: ${report.import.foldedFrom.join(", ")}`);
2820
+ }
2821
+ if (report.import.skippedDuplicates.length > 0) {
2822
+ console.log(` skipped duplicates: ${report.import.skippedDuplicates.join(", ")}`);
2823
+ }
2824
+ }
2825
+ if (report.rules.length > 0) {
2826
+ console.log(`imported ${report.rules.length} cursor rule(s)`);
2827
+ }
2828
+ console.log(`compiled ${report.compiled.length} vendor file(s).`);
2829
+ } catch (error) {
2830
+ if (error instanceof InitError) {
2831
+ console.error(error.message);
2832
+ process.exitCode = 1;
2833
+ return;
2834
+ }
2835
+ throw error;
2836
+ }
2837
+ }
2838
+
2839
+ // src/commands/install.ts
2840
+ import path22 from "path";
2841
+
2842
+ // src/core/install/fetch.ts
2843
+ import { execFile } from "child_process";
2844
+ import { mkdtemp, rm } from "fs/promises";
2845
+ import os2 from "os";
2846
+ import path20 from "path";
2847
+ import { promisify } from "util";
2848
+ var run = promisify(execFile);
2849
+ function cloneUrl(ref) {
2850
+ if (ref.provider === "github") {
2851
+ return `https://github.com/${ref.owner}/${ref.repo}.git`;
2852
+ }
2853
+ if (!ref.url) {
2854
+ throw new Error(`Git source ${ref.raw} has no URL.`);
2855
+ }
2856
+ return ref.url;
2857
+ }
2858
+ async function git(cwd, args) {
2859
+ const { stdout } = await run("git", args, {
2860
+ cwd,
2861
+ env: { ...process.env, GIT_TERMINAL_PROMPT: "0" }
2862
+ });
2863
+ return stdout.trim();
2864
+ }
2865
+ async function fetchGitSource(ref) {
2866
+ const url = cloneUrl(ref);
2867
+ const dir = await mkdtemp(path20.join(os2.tmpdir(), "threadroot-fetch-"));
2868
+ const cleanup = () => rm(dir, { recursive: true, force: true });
2869
+ try {
2870
+ if (ref.ref) {
2871
+ try {
2872
+ await git(void 0, ["clone", "--depth", "1", "--branch", ref.ref, url, dir]);
2873
+ } catch {
2874
+ await git(void 0, ["clone", url, dir]);
2875
+ await git(dir, ["checkout", ref.ref]);
2876
+ }
2877
+ } else {
2878
+ await git(void 0, ["clone", "--depth", "1", url, dir]);
2879
+ }
2880
+ const sha = await git(dir, ["rev-parse", "HEAD"]);
2881
+ return { dir, sha, cleanup };
2882
+ } catch (error) {
2883
+ await cleanup();
2884
+ throw error;
2885
+ }
2886
+ }
2887
+
2888
+ // src/core/install/install.ts
2889
+ import { cp as cp2, mkdir as mkdir8, readFile as readFile8, readdir as readdir5, stat as stat7, writeFile as writeFile7 } from "fs/promises";
2890
+ import { createHash as createHash2 } from "crypto";
2891
+ import path21 from "path";
2892
+ var NAME_RE5 = /^[a-z0-9][a-z0-9-]*$/;
2893
+ var KIND_DIR = {
2894
+ skill: "skills",
2895
+ tool: "tools",
2896
+ connection: "connections",
2897
+ rule: "rules"
2898
+ };
2899
+ function objectExt(kind) {
2900
+ return kind === "tool" || kind === "connection" ? ".yaml" : ".md";
2901
+ }
2902
+ function safeRepoPath(objectPath) {
2903
+ const normalized = path21.normalize(objectPath);
2904
+ if (path21.isAbsolute(normalized) || normalized === ".." || normalized.startsWith(`..${path21.sep}`)) {
2905
+ throw new Error(`Unsafe object path: ${objectPath}`);
2906
+ }
2907
+ return normalized;
2908
+ }
2909
+ function inferKind(objectPath, override) {
2910
+ if (override) {
2911
+ return override;
2912
+ }
2913
+ const segments = objectPath.split(/[\\/]/);
2914
+ if (segments.includes("skills")) return "skill";
2915
+ if (segments.includes("tools")) return "tool";
2916
+ if (segments.includes("connections")) return "connection";
2917
+ if (segments.includes("rules")) return "rule";
2918
+ const ext = path21.extname(objectPath).toLowerCase();
2919
+ if (ext === ".yaml" || ext === ".yml") return "tool";
2920
+ if (ext === ".md") return "skill";
2921
+ throw new Error(
2922
+ `Cannot infer object kind from ${objectPath}; pass an explicit kind (skill, tool, rule, or connection).`
2923
+ );
2924
+ }
2925
+ function deriveName(objectPath) {
2926
+ const base = path21.basename(objectPath, path21.extname(objectPath));
2927
+ if (!NAME_RE5.test(base)) {
2928
+ throw new Error(`Invalid object name \`${base}\` (use lowercase letters, digits, and dashes).`);
2929
+ }
2930
+ return base;
2931
+ }
2932
+ async function hashDirectory(root) {
2933
+ const hash = createHash2("sha256");
2934
+ hash.update("threadroot-directory-v1\n");
2935
+ async function walk(dir) {
2936
+ const entries = (await readdir5(dir, { withFileTypes: true })).sort((a, b) => a.name.localeCompare(b.name));
2937
+ for (const entry of entries) {
2938
+ const full = path21.join(dir, entry.name);
2939
+ const rel = path21.relative(root, full).split(path21.sep).join("/");
2940
+ if (entry.isSymbolicLink()) {
2941
+ throw new Error(`Refusing to install skill directory with symlink: ${rel}`);
2942
+ }
2943
+ if (entry.isDirectory()) {
2944
+ await walk(full);
2945
+ continue;
2946
+ }
2947
+ if (entry.isFile()) {
2948
+ hash.update(`file:${rel}
2949
+ `);
2950
+ hash.update(await readFile8(full));
2951
+ hash.update("\n");
2952
+ }
2953
+ }
2954
+ }
2955
+ await walk(root);
2956
+ return hash.digest("hex");
2957
+ }
2958
+ async function validateSkillDirectory2(sourcePath, expectedName) {
2959
+ const skillPath = path21.join(sourcePath, "SKILL.md");
2960
+ const parsed = parseFrontmatter(await readFile8(skillPath, "utf8"));
2961
+ const result = skillFrontmatterSchema.safeParse(parsed.data);
2962
+ if (!result.success) {
2963
+ const detail = result.error.issues.map((issue) => issue.message).join("; ");
2964
+ throw new Error(`Invalid skill directory ${sourcePath}: ${detail}`);
2965
+ }
2966
+ if (result.data.name !== expectedName) {
2967
+ throw new Error(`Skill directory name \`${expectedName}\` must match SKILL.md name \`${result.data.name}\`.`);
2968
+ }
2969
+ return await hashDirectory(sourcePath);
2970
+ }
2971
+ async function installObject(repoRoot, rawSource, options = {}) {
2972
+ const ref = parseSourceRef(rawSource);
2973
+ if (ref.kind === "registry") {
2974
+ throw new Error(`Registry sources are not available yet: ${rawSource}`);
2975
+ }
2976
+ let objectPath;
2977
+ let resolved;
2978
+ let refLabel;
2979
+ let sourcePath;
2980
+ let cleanup;
2981
+ if (ref.kind === "git") {
2982
+ const within = options.objectPath ?? ref.objectPath;
2983
+ if (!within) {
2984
+ throw new Error(`Git source ${rawSource} needs an object path (e.g. github:owner/repo/skills/x.md).`);
2985
+ }
2986
+ objectPath = safeRepoPath(within);
2987
+ refLabel = ref.ref;
2988
+ const fetched = await fetchGitSource(ref);
2989
+ sourcePath = path21.join(fetched.dir, objectPath);
2990
+ resolved = fetched.sha;
2991
+ cleanup = fetched.cleanup;
2992
+ } else {
2993
+ objectPath = options.objectPath ?? ref.path;
2994
+ sourcePath = toRepoPath(repoRoot, objectPath);
2995
+ }
2996
+ try {
2997
+ const kind = inferKind(objectPath, options.kind);
2998
+ const name = deriveName(objectPath);
2999
+ const scope = options.scope ?? "project";
3000
+ const dirKey = KIND_DIR[kind];
3001
+ const destDir = scope === "user" ? userObjectDir(dirKey, options.home) : projectObjectDir(repoRoot, dirKey);
3002
+ let destPath;
3003
+ let integrity;
3004
+ const info = await stat7(sourcePath);
3005
+ if (info.isDirectory()) {
3006
+ if (kind !== "skill") {
3007
+ throw new Error("Only skill objects may be installed from a directory.");
3008
+ }
3009
+ integrity = `sha256:${await validateSkillDirectory2(sourcePath, name)}`;
3010
+ destPath = path21.join(destDir, name);
3011
+ await mkdir8(destDir, { recursive: true });
3012
+ await cp2(sourcePath, destPath, { recursive: true, force: true });
3013
+ } else {
3014
+ const content = await readFile8(sourcePath, "utf8");
3015
+ destPath = path21.join(destDir, `${name}${objectExt(kind)}`);
3016
+ await mkdir8(destDir, { recursive: true });
3017
+ await writeFile7(destPath, content, "utf8");
3018
+ integrity = `sha256:${hashContent(content)}`;
3019
+ }
3020
+ const entry = {
3021
+ name,
3022
+ kind,
3023
+ sourceKind: ref.kind,
3024
+ source: ref.raw,
3025
+ objectPath,
3026
+ ref: refLabel,
3027
+ resolved,
3028
+ integrity,
3029
+ installedAt: (/* @__PURE__ */ new Date()).toISOString()
3030
+ };
3031
+ const lockPath = scope === "user" ? userLockPath(options.home) : projectLockPath(repoRoot);
3032
+ const lock = await readLockFile(lockPath);
3033
+ await writeLockFile(lockPath, upsertLockEntry(lock, entry));
3034
+ return { name, kind, scope, path: destPath, entry };
3035
+ } finally {
3036
+ await cleanup?.();
3037
+ }
3038
+ }
3039
+
3040
+ // src/commands/install.ts
3041
+ var KINDS = /* @__PURE__ */ new Set(["skill", "tool", "rule"]);
3042
+ async function runInstall(repoRoot, source, options) {
3043
+ if (options.kind && !KINDS.has(options.kind)) {
3044
+ console.error(`Invalid --kind \`${options.kind}\` (expected skill, tool, or rule).`);
3045
+ process.exitCode = 1;
3046
+ return;
3047
+ }
3048
+ const scope = options.user ? "user" : "project";
3049
+ try {
3050
+ const installed = await installObject(repoRoot, source, {
3051
+ kind: options.kind,
3052
+ objectPath: options.path,
3053
+ scope
3054
+ });
3055
+ console.log(`installed ${installed.kind} \`${installed.name}\` (${scope})`);
3056
+ console.log(` path: ${installed.path}`);
3057
+ if (installed.entry.resolved) {
3058
+ console.log(` commit: ${installed.entry.resolved}`);
3059
+ }
3060
+ if (installed.entry.integrity) {
3061
+ console.log(` integrity: ${installed.entry.integrity}`);
3062
+ }
3063
+ if (installed.kind === "tool" && installed.entry.sourceKind !== "local") {
3064
+ console.log(" note: installed tools are untrusted; add to `tools.allow` in harness.yaml to run.");
3065
+ }
3066
+ if (installed.kind === "skill" && installed.entry.sourceKind !== "local") {
3067
+ console.log(" note: inspect external skills before trusting bundled scripts, assets, or allowed tools.");
3068
+ if (scope === "project") {
3069
+ console.log(` inspect: threadroot skills inspect ${path22.relative(repoRoot, installed.path)}`);
3070
+ }
3071
+ }
3072
+ } catch (error) {
3073
+ console.error(`Install failed: ${error.message}`);
3074
+ process.exitCode = 1;
3075
+ }
3076
+ }
3077
+
3078
+ // src/mcp/server.ts
3079
+ import readline from "readline";
3080
+ import { stdin as input, stdout as output } from "process";
3081
+ import { z as z4 } from "zod";
3082
+
3083
+ // src/core/status.ts
3084
+ async function harnessStatus(repoRoot, options = {}) {
3085
+ let harness;
3086
+ try {
3087
+ harness = await resolveHarness(repoRoot, { home: options.home });
3088
+ } catch (error) {
3089
+ if (error instanceof HarnessError) {
3090
+ return { exists: false };
3091
+ }
3092
+ throw error;
3093
+ }
3094
+ const files = await compile(repoRoot, harness);
3095
+ const drift = await detectDrift(repoRoot, files);
3096
+ return {
3097
+ exists: true,
3098
+ manifest: {
3099
+ name: harness.manifest.name,
3100
+ profile: harness.manifest.profile,
3101
+ adapters: harness.manifest.adapters,
3102
+ toolsAllow: harness.manifest.tools.allow
3103
+ },
3104
+ counts: {
3105
+ skills: harness.skills.length,
3106
+ rules: harness.rules.length,
3107
+ tools: harness.tools.length,
3108
+ memory: harness.memory.length
3109
+ },
3110
+ drift
3111
+ };
3112
+ }
3113
+
3114
+ // src/mcp/server.ts
3115
+ function defineTool(spec) {
3116
+ return spec;
3117
+ }
3118
+ var toolRegistry = [
3119
+ defineTool({
3120
+ name: "context",
3121
+ description: "Return the task-relevant harness slice: ranked skills, rules, tools, and memory.",
3122
+ inputSchema: objectSchema(
3123
+ { task: { type: "string", description: "The coding task to assemble context for." } },
3124
+ ["task"]
3125
+ ),
3126
+ args: z4.object({ task: z4.string().min(1) }),
3127
+ run: async (repoRoot, args) => {
3128
+ const harness = await loadHarnessOrNull(repoRoot);
3129
+ if (!harness) {
3130
+ return { note: "No harness found. Run `tr init` first." };
3131
+ }
3132
+ return assembleContext(repoRoot, args.task, { harness });
3133
+ }
3134
+ }),
3135
+ defineTool({
3136
+ name: "skills_list",
3137
+ description: "List the skills defined in this repo's harness (name, when, tags).",
3138
+ inputSchema: objectSchema({}),
3139
+ args: z4.object({}),
3140
+ run: async (repoRoot) => {
3141
+ const harness = await loadHarnessOrNull(repoRoot);
3142
+ if (!harness) {
3143
+ return { skills: [], note: "No harness found. Run `tr init` first." };
3144
+ }
3145
+ return {
3146
+ skills: harness.skills.map((skill) => ({
3147
+ name: skill.name,
3148
+ when: skill.frontmatter.when,
3149
+ tags: skill.frontmatter.tags,
3150
+ scope: skill.frontmatter.scope
3151
+ }))
3152
+ };
3153
+ }
3154
+ }),
3155
+ defineTool({
3156
+ name: "skills_get",
3157
+ description: "Return a harness skill's full body and metadata by name.",
3158
+ inputSchema: objectSchema({ name: { type: "string", description: "Skill name." } }, ["name"]),
3159
+ args: z4.object({ name: z4.string().min(1) }),
3160
+ run: async (repoRoot, args) => {
3161
+ const harness = await loadHarnessOrNull(repoRoot);
3162
+ const skill = harness?.skills.find((entry) => entry.name === args.name);
3163
+ if (!skill) {
3164
+ throw new Error(`Unknown skill: ${args.name}`);
3165
+ }
3166
+ return { name: skill.name, frontmatter: skill.frontmatter, body: skill.body, sourcePath: skill.sourcePath };
3167
+ }
3168
+ }),
3169
+ defineTool({
3170
+ name: "tools_list",
3171
+ description: "List the executable tools defined in this repo's harness (name, inputs, confirm).",
3172
+ inputSchema: objectSchema({}),
3173
+ args: z4.object({}),
3174
+ run: async (repoRoot) => {
3175
+ const harness = await loadHarnessOrNull(repoRoot);
3176
+ if (!harness) {
3177
+ return { tools: [], note: "No harness found. Run `tr init` to create one." };
3178
+ }
3179
+ return {
3180
+ tools: harness.tools.map((tool) => ({
3181
+ name: tool.name,
3182
+ description: tool.manifest.description,
3183
+ scope: tool.manifest.scope,
3184
+ confirm: tool.manifest.confirm,
3185
+ kind: tool.manifest.run ? "shell" : "script",
3186
+ input: tool.manifest.input
3187
+ }))
3188
+ };
3189
+ }
3190
+ }),
3191
+ defineTool({
3192
+ name: "tools_check",
3193
+ description: "Run configured harness tool healthchecks without running primary tool actions.",
3194
+ inputSchema: objectSchema({}),
3195
+ args: z4.object({}),
3196
+ run: async (repoRoot) => {
3197
+ const harness = await loadHarnessOrNull(repoRoot);
3198
+ if (!harness) {
3199
+ return { checks: [], note: "No harness found. Run `tr init` first." };
3200
+ }
3201
+ return { checks: await Promise.all(harness.tools.map((tool) => checkToolHealth(repoRoot, tool))) };
3202
+ }
3203
+ }),
3204
+ defineTool({
3205
+ name: "tools_run",
3206
+ description: "Execute a harness tool locally. Tools marked confirm:true require `confirm: true` after user approval.",
3207
+ inputSchema: objectSchema(
3208
+ {
3209
+ name: { type: "string", description: "Tool name." },
3210
+ input: { type: "object", description: "Tool inputs as key/value pairs.", additionalProperties: true },
3211
+ confirm: { type: "boolean", description: "Confirm running a tool that requires confirmation." }
3212
+ },
3213
+ ["name"]
3214
+ ),
3215
+ args: z4.object({
3216
+ name: z4.string().min(1),
3217
+ input: z4.record(z4.unknown()).optional(),
3218
+ confirm: z4.boolean().optional()
3219
+ }),
3220
+ run: async (repoRoot, args) => {
3221
+ const outcome = await runTool(repoRoot, {
3222
+ name: args.name,
3223
+ input: args.input,
3224
+ confirmed: args.confirm
3225
+ });
3226
+ if (outcome.status === "blocked") {
3227
+ return { ok: false, blocked: outcome.reason, message: outcome.message };
3228
+ }
3229
+ const { result } = outcome;
3230
+ return {
3231
+ ok: result.ok,
3232
+ exitCode: result.exitCode,
3233
+ timedOut: result.timedOut,
3234
+ durationMs: result.durationMs,
3235
+ command: result.command,
3236
+ stdout: result.stdout,
3237
+ stderr: result.stderr
3238
+ };
3239
+ }
3240
+ }),
3241
+ defineTool({
3242
+ name: "tools_create",
3243
+ description: "Author a new harness tool (writes a validated manifest; never executes). Agent-created tools default to confirm:true.",
3244
+ inputSchema: objectSchema(
3245
+ {
3246
+ name: { type: "string", description: "Tool name (lowercase, hyphenated)." },
3247
+ description: { type: "string", description: "What the tool does." },
3248
+ run: { type: "string", description: "Shell command (use {{param}} for inputs)." },
3249
+ script: { type: "string", description: "Harness-relative script path (alternative to run)." },
3250
+ confirm: { type: "boolean", description: "Ask before running. Defaults to true for agents." },
3251
+ scope: { type: "string", enum: ["user", "project"], description: "Tool scope." },
3252
+ input: {
3253
+ type: "object",
3254
+ description: "Declared inputs (name -> {type, description, default}).",
3255
+ additionalProperties: true
3256
+ }
3257
+ },
3258
+ ["name", "description"]
3259
+ ),
3260
+ args: z4.object({
3261
+ name: z4.string().min(1),
3262
+ description: z4.string().min(1),
3263
+ run: z4.string().optional(),
3264
+ script: z4.string().optional(),
3265
+ confirm: z4.boolean().optional(),
3266
+ scope: z4.enum(["user", "project"]).optional(),
3267
+ input: z4.record(z4.unknown()).optional()
3268
+ }),
3269
+ run: async (repoRoot, args) => {
3270
+ const created = await createTool(
3271
+ repoRoot,
3272
+ {
3273
+ name: args.name,
3274
+ description: args.description,
3275
+ run: args.run,
3276
+ script: args.script,
3277
+ confirm: args.confirm,
3278
+ scope: args.scope,
3279
+ input: args.input
3280
+ },
3281
+ { actor: "agent" }
3282
+ );
3283
+ return { path: created.path, scope: created.scope, tool: created.manifest };
3284
+ }
3285
+ }),
3286
+ defineTool({
3287
+ name: "tools_detect",
3288
+ description: "Propose starter tools from the repo's existing command surface (scripts, Make/just targets).",
3289
+ inputSchema: objectSchema({}),
3290
+ args: z4.object({}),
3291
+ run: async (repoRoot) => {
3292
+ const harness = await loadHarnessOrNull(repoRoot);
3293
+ const profile = harness?.manifest.profile ?? "empty";
3294
+ return { candidates: await detectToolCandidates(repoRoot, profile) };
3295
+ }
3296
+ }),
3297
+ defineTool({
3298
+ name: "connections_list",
3299
+ description: "List local CLI connections defined in this repo's harness.",
3300
+ inputSchema: objectSchema({}),
3301
+ args: z4.object({}),
3302
+ run: async (repoRoot) => {
3303
+ const harness = await loadHarnessOrNull(repoRoot);
3304
+ if (!harness) {
3305
+ return { connections: [], note: "No harness found. Run `tr init` first." };
3306
+ }
3307
+ return {
3308
+ connections: harness.connections.map((connection) => ({
3309
+ name: connection.name,
3310
+ provider: connection.manifest.provider,
3311
+ command: connection.manifest.command,
3312
+ profile: connection.manifest.profile,
3313
+ risk: connection.manifest.risk,
3314
+ confirm: connection.manifest.confirm,
3315
+ healthcheck: Boolean(connection.manifest.healthcheck)
3316
+ }))
3317
+ };
3318
+ }
3319
+ }),
3320
+ defineTool({
3321
+ name: "connections_check",
3322
+ description: "Check local CLI connections and their configured healthchecks.",
3323
+ inputSchema: objectSchema({}),
3324
+ args: z4.object({}),
3325
+ run: (repoRoot) => checkConnections(repoRoot)
3326
+ }),
3327
+ defineTool({
3328
+ name: "memory_read",
3329
+ description: "Read durable harness memory. Returns one type, or all when no type is given.",
3330
+ inputSchema: objectSchema({
3331
+ type: { type: "string", description: "Memory type (project, repo-map, current-focus, handoff, pitfalls)." }
3332
+ }),
3333
+ args: z4.object({ type: z4.string().optional() }),
3334
+ run: async (repoRoot, args) => {
3335
+ if (args.type) {
3336
+ return { type: args.type, body: await readMemory(repoRoot, args.type) };
3337
+ }
3338
+ const harness = await loadHarnessOrNull(repoRoot);
3339
+ return { memory: (harness?.memory ?? []).map((entry) => ({ type: entry.type, body: entry.body })) };
3340
+ }
3341
+ }),
3342
+ defineTool({
3343
+ name: "memory_append",
3344
+ description: "Append a durable note to a harness memory file (creates it if missing).",
3345
+ inputSchema: objectSchema(
3346
+ {
3347
+ type: { type: "string", description: "Memory type (project, repo-map, current-focus, handoff, pitfalls)." },
3348
+ note: { type: "string", description: "The note to append." }
3349
+ },
3350
+ ["type", "note"]
3351
+ ),
3352
+ args: z4.object({ type: z4.string().min(1), note: z4.string().min(1) }),
3353
+ run: (repoRoot, args) => appendMemory(repoRoot, args.type, args.note)
3354
+ }),
3355
+ defineTool({
3356
+ name: "status",
3357
+ description: "Return harness state: manifest, object counts, and drift between canonical and compiled outputs.",
3358
+ inputSchema: objectSchema({}),
3359
+ args: z4.object({}),
3360
+ run: (repoRoot) => harnessStatus(repoRoot)
3361
+ }),
3362
+ defineTool({
3363
+ name: "doctor",
3364
+ description: "Check harness validity, compiled output health, MCP hints, and tool trust.",
3365
+ inputSchema: objectSchema({}),
3366
+ args: z4.object({}),
3367
+ run: (repoRoot) => doctor(repoRoot)
3368
+ })
3369
+ ];
3370
+ var tools = toolRegistry.map(({ name, description, inputSchema }) => ({ name, description, inputSchema }));
3371
+ async function runMcpServer(repoRoot) {
3372
+ const lines = readline.createInterface({ input, crlfDelay: Infinity });
3373
+ for await (const line of lines) {
3374
+ if (!line.trim()) {
3375
+ continue;
3376
+ }
3377
+ const request = parseRequest(line);
3378
+ if (!request) {
3379
+ write({ jsonrpc: "2.0", id: null, error: { code: -32700, message: "Parse error" } });
3380
+ continue;
3381
+ }
3382
+ const response = await handleMessage(repoRoot, request);
3383
+ if (response) {
3384
+ write(response);
3385
+ }
3386
+ }
3387
+ }
3388
+ async function handleMessage(repoRoot, request) {
3389
+ try {
3390
+ if (request.method === "initialize") {
3391
+ return resultResponse(request, {
3392
+ protocolVersion: "2024-11-05",
3393
+ serverInfo: { name: "threadroot", version: "0.1.0" },
3394
+ capabilities: { tools: {} }
3395
+ });
3396
+ }
3397
+ if (request.method === "notifications/initialized") {
3398
+ return void 0;
3399
+ }
3400
+ if (request.method === "tools/list") {
3401
+ return resultResponse(request, { tools });
3402
+ }
3403
+ if (request.method === "tools/call") {
3404
+ const params = request.params;
3405
+ const result = await callTool(repoRoot, params?.name, params?.arguments ?? {});
3406
+ return resultResponse(request, {
3407
+ content: [{ type: "text", text: typeof result === "string" ? result : JSON.stringify(result, null, 2) }]
3408
+ });
3409
+ }
3410
+ return errorResponse(request, -32601, `Unknown method: ${request.method ?? "<missing>"}`);
3411
+ } catch (error) {
3412
+ return errorResponse(request, -32e3, error instanceof Error ? error.message : String(error));
3413
+ }
3414
+ }
3415
+ async function callTool(repoRoot, name, rawArgs) {
3416
+ const tool = toolRegistry.find((entry) => entry.name === name);
3417
+ if (!tool) {
3418
+ throw new Error(`Unknown tool: ${name ?? "<missing>"}`);
3419
+ }
3420
+ const parsed = tool.args.safeParse(rawArgs);
3421
+ if (!parsed.success) {
3422
+ throw new Error(`Invalid arguments for ${tool.name}: ${formatZodIssues(parsed.error)}`);
3423
+ }
3424
+ return tool.run(repoRoot, parsed.data);
3425
+ }
3426
+ function formatZodIssues(error) {
3427
+ return error.issues.map((issue) => `${issue.path.join(".") || "(root)"} ${issue.message}`).join("; ");
3428
+ }
3429
+ async function loadHarnessOrNull(repoRoot) {
3430
+ try {
3431
+ return await resolveHarness(repoRoot);
3432
+ } catch (error) {
3433
+ if (error instanceof HarnessError) {
3434
+ return null;
3435
+ }
3436
+ throw error;
3437
+ }
3438
+ }
3439
+ function objectSchema(properties, required = []) {
3440
+ return {
3441
+ type: "object",
3442
+ properties,
3443
+ required,
3444
+ additionalProperties: false
3445
+ };
3446
+ }
3447
+ function parseRequest(line) {
3448
+ try {
3449
+ return JSON.parse(line);
3450
+ } catch {
3451
+ return void 0;
3452
+ }
3453
+ }
3454
+ function resultResponse(request, result) {
3455
+ if (request.id === void 0) {
3456
+ return void 0;
3457
+ }
3458
+ return { jsonrpc: "2.0", id: request.id, result };
3459
+ }
3460
+ function errorResponse(request, code, message) {
3461
+ return { jsonrpc: "2.0", id: request.id ?? null, error: { code, message } };
3462
+ }
3463
+ function write(payload) {
3464
+ output.write(`${JSON.stringify(payload)}
3465
+ `);
3466
+ }
3467
+
3468
+ // src/core/mcp-setup.ts
3469
+ function mcpSetupGuide(input2) {
3470
+ const agent = parseAgent(input2.agent);
3471
+ const command = input2.executable ?? process.execPath;
3472
+ const scriptPath = input2.scriptPath ?? process.argv[1] ?? "threadroot";
3473
+ const nodeConfig = {
3474
+ mcpServers: {
3475
+ threadroot: {
3476
+ command,
3477
+ args: [scriptPath, "mcp"],
3478
+ cwd: input2.repoRoot
3479
+ }
3480
+ }
3481
+ };
3482
+ const installedConfig = {
3483
+ mcpServers: {
3484
+ threadroot: {
3485
+ command: "threadroot",
3486
+ args: ["mcp"],
3487
+ cwd: input2.repoRoot
3488
+ }
3489
+ }
3490
+ };
3491
+ return `Threadroot MCP setup
3492
+
3493
+ Project root:
3494
+ ${input2.repoRoot}
3495
+
3496
+ What "waiting" means:
3497
+ MCP tools appear only after your coding-agent client is configured to launch this stdio server and the agent surface reloads or starts a new session.
3498
+
3499
+ Server command:
3500
+ \`\`\`bash
3501
+ threadroot mcp
3502
+ \`\`\`
3503
+
3504
+ Source checkout fallback:
3505
+ \`\`\`bash
3506
+ ${command} ${scriptPath} mcp
3507
+ \`\`\`
3508
+
3509
+ Generic MCP JSON for installed Threadroot:
3510
+ \`\`\`json
3511
+ ${JSON.stringify(installedConfig, null, 2)}
3512
+ \`\`\`
3513
+
3514
+ Generic MCP JSON for this checkout:
3515
+ \`\`\`json
3516
+ ${JSON.stringify(nodeConfig, null, 2)}
3517
+ \`\`\`
3518
+
3519
+ Agent notes:
3520
+ ${agentNotes(agent)}
3521
+
3522
+ Pasteable agent bootstrap prompt:
3523
+ \`\`\`text
3524
+ ${agentLaunchPrompt(input2.repoRoot, `${command} ${scriptPath}`)}
3525
+ \`\`\`
3526
+ `;
3527
+ }
3528
+ function agentLaunchPrompt(repoRoot, localCommand = "node /path/to/threadroot/dist/index.js") {
3529
+ return `You are setting up Threadroot in this repository.
3530
+
3531
+ Repository:
3532
+ ${repoRoot}
3533
+
3534
+ Goal:
3535
+ Initialize Threadroot so this repo has portable AI-agent instructions, durable memory, curated skills, executable tools, and vendor-specific adapter files generated from one canonical harness.
3536
+
3537
+ Rules:
3538
+ - Prefer deterministic CLI commands.
3539
+ - Do not overwrite user-owned files without checking Threadroot status/diff.
3540
+ - Keep context small. Use Threadroot context output before reading broad project files.
3541
+
3542
+ Steps:
3543
+ 1. Check whether Threadroot is available with \`threadroot --version\`.
3544
+ 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\`.
3545
+ 3. Run \`threadroot status\` to check whether a harness already exists.
3546
+ 4. If no harness exists, run \`threadroot init\`. Use \`--no-import\` only when the user explicitly wants a blank-slate harness.
3547
+ 5. Run \`threadroot status\` again.
3548
+ 6. If status reports drift, run \`threadroot diff\` and summarize the drift before changing generated files.
3549
+ 7. Run \`threadroot context "current task"\` with the user's actual task to find relevant skills, rules, tools, and memory.
3550
+ 8. If project-local MCP config is useful, run \`threadroot mcp setup --write\` and tell the user to reload their agent surface.
3551
+
3552
+ Final response:
3553
+ Say exactly:
3554
+ "Success: Threadroot is initialized. Run \`threadroot status\` or \`threadroot context "<task>"\` to use it."
3555
+
3556
+ If using a local checkout instead of an installed package, say:
3557
+ "Success: Threadroot is initialized. Run \`${localCommand} status\` or \`${localCommand} context "<task>"\` to use it."`;
3558
+ }
3559
+ function parseAgent(value) {
3560
+ if (value === "codex" || value === "copilot" || value === "cursor" || value === "claude" || value === "generic") {
3561
+ return value;
3562
+ }
3563
+ return "all";
3564
+ }
3565
+ function agentNotes(agent) {
3566
+ const generic = [
3567
+ "- Add an MCP server named `threadroot`.",
3568
+ '- Use command `threadroot` with args `["mcp"]` when Threadroot is installed.',
3569
+ "- Use the source checkout fallback when testing this repo before publishing.",
3570
+ "- Set cwd to the project root, not to the Threadroot package source unless those are the same project."
3571
+ ];
3572
+ const notes = {
3573
+ generic,
3574
+ codex: [
3575
+ ...generic,
3576
+ "- In Codex, configure the MCP server in the MCP/client configuration available to your Codex environment.",
3577
+ "- MCP tools will not appear inside an already-running session until that environment has loaded the server."
3578
+ ],
3579
+ copilot: [
3580
+ ...generic,
3581
+ "- In VS Code/Copilot-capable MCP clients, add the JSON server config to the workspace or user MCP configuration.",
3582
+ "- Restart or reload the agent surface after adding the server."
3583
+ ],
3584
+ cursor: [
3585
+ ...generic,
3586
+ "- In Cursor, add the JSON server config to Cursor's MCP settings for this project or user profile.",
3587
+ "- Reload the agent surface after saving the MCP config."
3588
+ ],
3589
+ claude: [
3590
+ ...generic,
3591
+ '- In Claude MCP clients, add a local stdio server with command `threadroot` and args `["mcp"]`.',
3592
+ "- Restart the client session after adding the server."
3593
+ ]
3594
+ };
3595
+ if (agent === "all") {
3596
+ return [
3597
+ "Generic:",
3598
+ ...generic,
3599
+ "",
3600
+ "Codex:",
3601
+ ...notes.codex,
3602
+ "",
3603
+ "Copilot / VS Code:",
3604
+ ...notes.copilot,
3605
+ "",
3606
+ "Cursor:",
3607
+ ...notes.cursor,
3608
+ "",
3609
+ "Claude:",
3610
+ ...notes.claude
3611
+ ].join("\n");
3612
+ }
3613
+ return notes[agent].join("\n");
3614
+ }
3615
+
3616
+ // src/core/mcp-config.ts
3617
+ import { mkdir as mkdir9, readFile as readFile9, writeFile as writeFile8 } from "fs/promises";
3618
+ import path23 from "path";
3619
+ var TARGETS = [
3620
+ { agent: "copilot", file: path23.join(".vscode", "mcp.json"), key: "servers" },
3621
+ { agent: "cursor", file: path23.join(".cursor", "mcp.json"), key: "mcpServers" },
3622
+ { agent: "claude", file: ".mcp.json", key: "mcpServers" }
3623
+ ];
3624
+ function mcpServerEntry(command, scriptPath) {
3625
+ return scriptPath ? { command, args: [scriptPath, "mcp"] } : { command, args: ["mcp"] };
3626
+ }
3627
+ async function mergeConfig(filePath, key, entry) {
3628
+ let config = {};
3629
+ try {
3630
+ const raw = await readFile9(filePath, "utf8");
3631
+ const parsed = JSON.parse(raw);
3632
+ if (parsed && typeof parsed === "object") {
3633
+ config = parsed;
3634
+ }
3635
+ } catch (error) {
3636
+ if (error.code !== "ENOENT") {
3637
+ throw new Error(`Refusing to overwrite unparseable ${filePath}: ${error.message}`);
3638
+ }
3639
+ }
3640
+ const servers = config[key] && typeof config[key] === "object" ? config[key] : {};
3641
+ servers.threadroot = { ...entry };
3642
+ config[key] = servers;
3643
+ await mkdir9(path23.dirname(filePath), { recursive: true });
3644
+ await writeFile8(filePath, `${JSON.stringify(config, null, 2)}
3645
+ `, "utf8");
3646
+ }
3647
+ async function writeProjectMcpConfigs(input2) {
3648
+ const agents = input2.agents;
3649
+ const targets = agents ? TARGETS.filter((target) => agents.includes(target.agent)) : TARGETS;
3650
+ const written = [];
3651
+ for (const target of targets) {
3652
+ const filePath = path23.join(input2.repoRoot, target.file);
3653
+ await mergeConfig(filePath, target.key, input2.entry);
3654
+ written.push(target.file);
3655
+ }
3656
+ return {
3657
+ written,
3658
+ notes: [
3659
+ "Codex reads ~/.codex/config.toml (global) \u2014 add a [mcp_servers.threadroot] entry manually.",
3660
+ "Reload each agent after writing config for the server to appear."
3661
+ ]
3662
+ };
3663
+ }
3664
+
3665
+ // src/commands/mcp.ts
3666
+ async function runMcp(repoRoot) {
3667
+ await runMcpServer(repoRoot);
3668
+ }
3669
+ async function runMcpSetup(repoRoot, options) {
3670
+ if (options.write) {
3671
+ const command = process.execPath;
3672
+ const scriptPath = process.argv[1];
3673
+ const entry = mcpServerEntry(command, scriptPath);
3674
+ const result = await writeProjectMcpConfigs({ repoRoot, entry });
3675
+ console.log("Wrote project MCP config:");
3676
+ for (const file of result.written) {
3677
+ console.log(`- ${file}`);
3678
+ }
3679
+ for (const note of result.notes) {
3680
+ console.log(`note: ${note}`);
3681
+ }
3682
+ return;
3683
+ }
3684
+ console.log(mcpSetupGuide({ repoRoot, agent: options.agent }));
3685
+ }
3686
+
3687
+ // src/commands/memory.ts
3688
+ async function runMemoryRead(repoRoot, type) {
3689
+ const body = await readMemory(repoRoot, type);
3690
+ if (body === null) {
3691
+ console.log(`No ${type} memory yet.`);
3692
+ return;
3693
+ }
3694
+ console.log(body);
3695
+ }
3696
+ async function runMemoryAppend(repoRoot, type, note) {
3697
+ const result = await appendMemory(repoRoot, type, note);
3698
+ console.log(`Appended to ${result.scope} ${result.type} memory (${result.path}).`);
3699
+ }
3700
+ async function runRemember(repoRoot, note, options = {}) {
3701
+ await runMemoryAppend(repoRoot, options.type ?? "handoff", note);
3702
+ }
3703
+
3704
+ // src/commands/skills.ts
3705
+ async function runSkillsList(repoRoot) {
3706
+ try {
3707
+ const harness = await resolveHarness(repoRoot);
3708
+ if (harness.skills.length === 0) {
3709
+ console.log("No skills defined. Add folder skills under `.threadroot/skills/<name>/SKILL.md`.");
3710
+ return;
3711
+ }
3712
+ for (const skill of harness.skills) {
3713
+ console.log(`${skill.name} [${skill.origin}] - ${skill.frontmatter.description}`);
3714
+ }
3715
+ } catch (error) {
3716
+ if (error instanceof HarnessError) {
3717
+ console.log("No harness found. Run `tr init` first.");
3718
+ return;
3719
+ }
3720
+ throw error;
3721
+ }
3722
+ }
3723
+ async function runSkillsValidate(repoRoot, options = {}) {
3724
+ const report = options.path ? await validateSkillPath(toRepoPath(repoRoot, options.path)) : await validateSkills(repoRoot);
3725
+ if (report.findings.length === 0) {
3726
+ console.log("Skills valid.");
3727
+ return;
3728
+ }
3729
+ const errors = report.findings.filter((entry) => entry.severity === "error").length;
3730
+ const warnings = report.findings.filter((entry) => entry.severity === "warning").length;
3731
+ console.log(`Skills validation: ${errors} error(s), ${warnings} warning(s)`);
3732
+ for (const finding3 of report.findings) {
3733
+ console.log(`- ${finding3.severity} ${finding3.skill}: ${finding3.message} (${finding3.path})`);
3734
+ }
3735
+ if (!report.ok) {
3736
+ process.exitCode = 1;
3737
+ }
3738
+ }
3739
+ async function runSkillsInspect(repoRoot, targetPath) {
3740
+ const inspection = await inspectSkillPath(toRepoPath(repoRoot, targetPath));
3741
+ console.log(`${inspection.name}`);
3742
+ console.log(`description: ${inspection.description}`);
3743
+ console.log(`path: ${inspection.path}`);
3744
+ console.log(`references: ${inspection.references.length > 0 ? inspection.references.join(", ") : "none"}`);
3745
+ console.log(`scripts: ${inspection.scripts.length > 0 ? inspection.scripts.join(", ") : "none"}`);
3746
+ console.log(`assets: ${inspection.assets.length > 0 ? inspection.assets.join(", ") : "none"}`);
3747
+ console.log(`evals: ${inspection.evals.length > 0 ? inspection.evals.join(", ") : "none"}`);
3748
+ if (inspection.allowedTools) {
3749
+ const tools2 = Array.isArray(inspection.allowedTools) ? inspection.allowedTools.join(", ") : inspection.allowedTools;
3750
+ console.log(`allowed-tools: ${tools2}`);
3751
+ }
3752
+ }
3753
+
3754
+ // src/commands/status.ts
3755
+ async function runStatus(repoRoot) {
3756
+ const status = await harnessStatus(repoRoot);
3757
+ if (!status.exists) {
3758
+ console.log("No harness found. Run `tr init` first.");
3759
+ return;
3760
+ }
3761
+ console.log(`harness: ${status.manifest.name} (${status.manifest.profile})`);
3762
+ console.log(`adapters: ${status.manifest.adapters.join(", ")}`);
3763
+ console.log(
3764
+ `objects: ${status.counts.skills} skills, ${status.counts.rules} rules, ${status.counts.tools} tools, ${status.counts.memory} memory`
3765
+ );
3766
+ const changed = status.drift.filter((entry) => entry.status !== "unchanged");
3767
+ if (changed.length === 0) {
3768
+ console.log("compiled outputs: up to date");
3769
+ return;
3770
+ }
3771
+ console.log("compiled outputs:");
3772
+ for (const entry of changed) {
3773
+ console.log(`- ${entry.status}: ${entry.path}`);
3774
+ }
3775
+ }
3776
+
3777
+ // src/core/packs/index.ts
3778
+ import { cp as cp3, mkdir as mkdir10, readFile as readFile10, readdir as readdir6, stat as stat8 } from "fs/promises";
3779
+ import path24 from "path";
3780
+ import { fileURLToPath as fileURLToPath2 } from "url";
3781
+ import { parse as parseYaml3 } from "yaml";
3782
+ import { z as z5 } from "zod";
3783
+ var packManifestSchema = z5.object({
3784
+ name: z5.string().min(1),
3785
+ version: z5.literal(1),
3786
+ description: z5.string().min(1),
3787
+ skills: z5.array(z5.string()).default([]),
3788
+ tools: z5.array(z5.string()).default([]),
3789
+ rules: z5.array(z5.string()).default([]),
3790
+ connections: z5.array(z5.string()).default([])
3791
+ });
3792
+ var DIST_DIR2 = path24.dirname(fileURLToPath2(import.meta.url));
3793
+ var PACKAGE_ROOT_FROM_BUNDLE2 = path24.resolve(DIST_DIR2, "..");
3794
+ var PACKAGE_ROOT_FROM_DIST2 = path24.resolve(DIST_DIR2, "../../..");
3795
+ var PACKAGE_ROOT_FROM_SRC2 = path24.resolve(DIST_DIR2, "../../../..");
3796
+ var PACK_CANDIDATES = [
3797
+ path24.join(PACKAGE_ROOT_FROM_BUNDLE2, "packs"),
3798
+ path24.join(PACKAGE_ROOT_FROM_DIST2, "packs"),
3799
+ path24.join(PACKAGE_ROOT_FROM_SRC2, "packs")
3800
+ ];
3801
+ async function exists5(target) {
3802
+ try {
3803
+ await stat8(target);
3804
+ return true;
3805
+ } catch (error) {
3806
+ if (error.code === "ENOENT") {
3807
+ return false;
3808
+ }
3809
+ throw error;
3810
+ }
3811
+ }
3812
+ async function firstExisting(candidates) {
3813
+ for (const candidate of candidates) {
3814
+ if (await isPackRoot(candidate)) {
3815
+ return candidate;
3816
+ }
3817
+ }
3818
+ return void 0;
3819
+ }
3820
+ async function bundledPacksDir() {
3821
+ return firstExisting(PACK_CANDIDATES);
3822
+ }
3823
+ async function isPackRoot(candidate) {
3824
+ let entries;
3825
+ try {
3826
+ entries = await readdir6(candidate, { withFileTypes: true });
3827
+ } catch {
3828
+ return false;
3829
+ }
3830
+ for (const entry of entries) {
3831
+ if (entry.isDirectory() && await exists5(path24.join(candidate, entry.name, "pack.yaml"))) {
3832
+ return true;
3833
+ }
3834
+ }
3835
+ return false;
3836
+ }
3837
+ function safeRelative(ref) {
3838
+ const normalized = path24.normalize(ref);
3839
+ if (path24.isAbsolute(normalized) || normalized === ".." || normalized.startsWith(`..${path24.sep}`)) {
3840
+ throw new Error(`Unsafe pack reference: ${ref}`);
3841
+ }
3842
+ return normalized;
3843
+ }
3844
+ async function readPackManifest(packDir) {
3845
+ const file = path24.join(packDir, "pack.yaml");
3846
+ const parsed = packManifestSchema.safeParse(parseYaml3(await readFile10(file, "utf8")));
3847
+ if (!parsed.success) {
3848
+ const detail = parsed.error.issues.map((issue) => issue.message).join("; ");
3849
+ throw new Error(`Invalid pack manifest ${file}: ${detail}`);
3850
+ }
3851
+ return parsed.data;
3852
+ }
3853
+ async function packDirFor(repoRoot, nameOrPath) {
3854
+ if (path24.isAbsolute(nameOrPath)) {
3855
+ return nameOrPath;
3856
+ }
3857
+ if (nameOrPath.startsWith(".") || nameOrPath.includes("/") || nameOrPath.includes("\\")) {
3858
+ return toRepoPath(repoRoot, nameOrPath);
3859
+ }
3860
+ const bundled = await bundledPacksDir();
3861
+ if (bundled) {
3862
+ return path24.join(bundled, nameOrPath);
3863
+ }
3864
+ return toRepoPath(repoRoot, path24.join("packs", nameOrPath));
3865
+ }
3866
+ async function directFiles(dir, ext) {
3867
+ try {
3868
+ const entries = await readdir6(dir, { withFileTypes: true });
3869
+ return entries.filter((entry) => entry.isFile() && entry.name.endsWith(ext)).map((entry) => path24.join(dir, entry.name)).sort();
3870
+ } catch (error) {
3871
+ if (error.code === "ENOENT") {
3872
+ return [];
3873
+ }
3874
+ throw error;
3875
+ }
3876
+ }
3877
+ async function skillEntries(dir) {
3878
+ try {
3879
+ const entries = await readdir6(dir, { withFileTypes: true });
3880
+ const result = [];
3881
+ for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
3882
+ const full = path24.join(dir, entry.name);
3883
+ if (entry.isFile() && entry.name.endsWith(".md")) {
3884
+ result.push(full);
3885
+ }
3886
+ if (entry.isDirectory() && await exists5(path24.join(full, "SKILL.md"))) {
3887
+ result.push(full);
3888
+ }
3889
+ }
3890
+ return result;
3891
+ } catch (error) {
3892
+ if (error.code === "ENOENT") {
3893
+ return [];
3894
+ }
3895
+ throw error;
3896
+ }
3897
+ }
3898
+ async function collectObjects(packDir, manifest) {
3899
+ async function resolveRef(ref) {
3900
+ const safe = safeRelative(ref);
3901
+ const local = path24.resolve(packDir, safe);
3902
+ if (await exists5(local)) {
3903
+ return local;
3904
+ }
3905
+ return path24.resolve(packDir, "..", "..", safe);
3906
+ }
3907
+ return {
3908
+ skills: [
3909
+ ...await Promise.all(manifest.skills.map(resolveRef)),
3910
+ ...await skillEntries(path24.join(packDir, "skills"))
3911
+ ],
3912
+ tools: [
3913
+ ...await Promise.all(manifest.tools.map(resolveRef)),
3914
+ ...await directFiles(path24.join(packDir, "tools"), ".yaml")
3915
+ ],
3916
+ rules: [
3917
+ ...await Promise.all(manifest.rules.map(resolveRef)),
3918
+ ...await directFiles(path24.join(packDir, "rules"), ".md")
3919
+ ],
3920
+ connections: [
3921
+ ...await Promise.all(manifest.connections.map(resolveRef)),
3922
+ ...await directFiles(path24.join(packDir, "connections"), ".yaml")
3923
+ ]
3924
+ };
3925
+ }
3926
+ function baseName(source) {
3927
+ const parsed = path24.basename(source) === "SKILL.md" ? path24.dirname(source) : source;
3928
+ return path24.basename(parsed, path24.extname(parsed));
3929
+ }
3930
+ async function listPacks(repoRoot) {
3931
+ const dirs = [toRepoPath(repoRoot, "packs"), await bundledPacksDir()].filter((dir) => Boolean(dir));
3932
+ const seen = /* @__PURE__ */ new Set();
3933
+ const packs = [];
3934
+ for (const root of dirs) {
3935
+ let entries;
3936
+ try {
3937
+ entries = await readdir6(root, { withFileTypes: true });
3938
+ } catch {
3939
+ continue;
3940
+ }
3941
+ for (const entry of entries) {
3942
+ if (!entry.isDirectory() || seen.has(entry.name)) {
3943
+ continue;
3944
+ }
3945
+ const packDir = path24.join(root, entry.name);
3946
+ if (!await exists5(path24.join(packDir, "pack.yaml"))) {
3947
+ continue;
3948
+ }
3949
+ seen.add(entry.name);
3950
+ packs.push(await inspectPack(repoRoot, packDir));
3951
+ }
3952
+ }
3953
+ return packs.sort((a, b) => a.name.localeCompare(b.name));
3954
+ }
3955
+ async function inspectPack(repoRoot, nameOrPath) {
3956
+ const packDir = await packDirFor(repoRoot, nameOrPath);
3957
+ const manifest = await readPackManifest(packDir);
3958
+ const objects = await collectObjects(packDir, manifest);
3959
+ return {
3960
+ name: manifest.name,
3961
+ description: manifest.description,
3962
+ path: packDir,
3963
+ skills: objects.skills.map(baseName),
3964
+ tools: objects.tools.map(baseName),
3965
+ rules: objects.rules.map(baseName),
3966
+ connections: objects.connections.map(baseName)
3967
+ };
3968
+ }
3969
+ async function validateProse(file, kind) {
3970
+ const target = path24.basename(file) === "SKILL.md" ? file : file;
3971
+ const content = await readFile10(target, "utf8");
3972
+ const parsed = parseFrontmatter(content);
3973
+ const schema = kind === "skill" ? skillFrontmatterSchema : ruleFrontmatterSchema;
3974
+ schema.parse(parsed.data);
3975
+ }
3976
+ async function validateYaml(file, kind) {
3977
+ const content = await readFile10(file, "utf8");
3978
+ const schema = kind === "tool" ? toolManifestSchema : connectionManifestSchema;
3979
+ schema.parse(parseYaml3(content));
3980
+ }
3981
+ async function validatePack(repoRoot, nameOrPath) {
3982
+ const findings = [];
3983
+ try {
3984
+ const packDir = await packDirFor(repoRoot, nameOrPath);
3985
+ const manifest = await readPackManifest(packDir);
3986
+ const objects = await collectObjects(packDir, manifest);
3987
+ for (const skill of objects.skills) {
3988
+ await validateProse(path24.basename(skill) === "SKILL.md" ? skill : path24.join(skill, "SKILL.md"), "skill");
3989
+ }
3990
+ for (const rule of objects.rules) await validateProse(rule, "rule");
3991
+ for (const tool of objects.tools) await validateYaml(tool, "tool");
3992
+ for (const connection of objects.connections) await validateYaml(connection, "connection");
3993
+ if (Object.values(objects).every((items) => items.length === 0)) {
3994
+ findings.push({ severity: "warning", message: "Pack does not include any objects." });
3995
+ }
3996
+ } catch (error) {
3997
+ findings.push({ severity: "error", message: error instanceof Error ? error.message : String(error) });
3998
+ }
3999
+ return { ok: !findings.some((finding3) => finding3.severity === "error"), findings };
4000
+ }
4001
+ async function copyObject(source, destDir) {
4002
+ const info = await stat8(source);
4003
+ const name = baseName(source);
4004
+ const dest = info.isDirectory() ? path24.join(destDir, name) : path24.join(destDir, path24.basename(source));
4005
+ await mkdir10(destDir, { recursive: true });
4006
+ await cp3(source, dest, { recursive: true, force: true });
4007
+ return dest;
4008
+ }
4009
+ async function installPack(repoRoot, nameOrPath) {
4010
+ const validation = await validatePack(repoRoot, nameOrPath);
4011
+ if (!validation.ok) {
4012
+ throw new Error(validation.findings.map((finding3) => finding3.message).join("; "));
4013
+ }
4014
+ const packDir = await packDirFor(repoRoot, nameOrPath);
4015
+ const manifest = await readPackManifest(packDir);
4016
+ const objects = await collectObjects(packDir, manifest);
4017
+ await Promise.all([
4018
+ ...objects.skills.map((source) => copyObject(source, projectObjectDir(repoRoot, "skills"))),
4019
+ ...objects.tools.map((source) => copyObject(source, projectObjectDir(repoRoot, "tools"))),
4020
+ ...objects.rules.map((source) => copyObject(source, projectObjectDir(repoRoot, "rules"))),
4021
+ ...objects.connections.map((source) => copyObject(source, projectObjectDir(repoRoot, "connections")))
4022
+ ]);
4023
+ return inspectPack(repoRoot, packDir);
4024
+ }
4025
+
4026
+ // src/commands/packs.ts
4027
+ function printList(label, values) {
4028
+ console.log(`${label}: ${values.length > 0 ? values.join(", ") : "none"}`);
4029
+ }
4030
+ async function runPacksList(repoRoot) {
4031
+ const packs = await listPacks(repoRoot);
4032
+ if (packs.length === 0) {
4033
+ console.log("No packs found.");
4034
+ return;
4035
+ }
4036
+ for (const pack of packs) {
4037
+ console.log(`${pack.name} - ${pack.description}`);
4038
+ }
4039
+ }
4040
+ async function runPacksInspect(repoRoot, nameOrPath) {
4041
+ const pack = await inspectPack(repoRoot, nameOrPath);
4042
+ console.log(pack.name);
4043
+ console.log(`description: ${pack.description}`);
4044
+ console.log(`path: ${pack.path}`);
4045
+ printList("skills", pack.skills);
4046
+ printList("tools", pack.tools);
4047
+ printList("rules", pack.rules);
4048
+ printList("connections", pack.connections);
4049
+ }
4050
+ async function runPacksValidate(repoRoot, nameOrPath) {
4051
+ const report = await validatePack(repoRoot, nameOrPath);
4052
+ if (report.findings.length === 0) {
4053
+ console.log("Pack valid.");
4054
+ return;
4055
+ }
4056
+ for (const finding3 of report.findings) {
4057
+ console.log(`${finding3.severity}: ${finding3.message}`);
4058
+ }
4059
+ if (!report.ok) {
4060
+ process.exitCode = 1;
4061
+ }
4062
+ }
4063
+ async function runPacksInstall(repoRoot, nameOrPath) {
4064
+ const pack = await installPack(repoRoot, nameOrPath);
4065
+ console.log(`Installed pack \`${pack.name}\`.`);
4066
+ printList("skills", pack.skills);
4067
+ printList("tools", pack.tools);
4068
+ printList("rules", pack.rules);
4069
+ printList("connections", pack.connections);
4070
+ }
4071
+
4072
+ // src/commands/tools.ts
4073
+ function parseInputs(pairs = []) {
4074
+ const input2 = {};
4075
+ for (const pair of pairs) {
4076
+ const eq = pair.indexOf("=");
4077
+ if (eq === -1) {
4078
+ throw new Error(`Invalid --input \`${pair}\`. Expected key=value.`);
4079
+ }
4080
+ input2[pair.slice(0, eq)] = pair.slice(eq + 1);
4081
+ }
4082
+ return input2;
4083
+ }
4084
+ async function runToolsList(repoRoot) {
4085
+ let harness;
4086
+ try {
4087
+ harness = await resolveHarness(repoRoot);
4088
+ } catch (error) {
4089
+ if (error instanceof HarnessError) {
4090
+ console.log("No harness found. Run `tr init` first.");
4091
+ return;
4092
+ }
4093
+ throw error;
4094
+ }
4095
+ if (harness.tools.length === 0) {
4096
+ console.log("No tools defined. Add one with `tr tools add` or `tr tools detect`.");
4097
+ return;
4098
+ }
4099
+ for (const tool of harness.tools) {
4100
+ const flags = [
4101
+ tool.manifest.risk,
4102
+ tool.manifest.confirm ? "confirm" : null,
4103
+ tool.manifest.connection ? `connection:${tool.manifest.connection}` : null,
4104
+ tool.manifest.healthcheck ? "healthcheck" : null,
4105
+ tool.manifest.run ? "shell" : "script"
4106
+ ].filter(Boolean).join(", ");
4107
+ console.log(`${tool.name} [${flags}] - ${tool.manifest.description}`);
4108
+ }
4109
+ }
4110
+ async function runToolRun(repoRoot, name, options) {
4111
+ const outcome = await runTool(repoRoot, {
4112
+ name,
4113
+ input: parseInputs(options.input),
4114
+ confirmed: options.yes === true,
4115
+ timeoutMs: options.timeout ? Number(options.timeout) : void 0
4116
+ });
4117
+ if (outcome.status === "blocked") {
4118
+ if (outcome.reason === "needs-confirmation") {
4119
+ console.log(`${outcome.message} Re-run with --yes to confirm.`);
4120
+ } else {
4121
+ console.log(outcome.message);
4122
+ }
4123
+ process.exitCode = 1;
4124
+ return;
4125
+ }
4126
+ const { result } = outcome;
4127
+ if (result.stdout) {
4128
+ process.stdout.write(result.stdout.endsWith("\n") ? result.stdout : `${result.stdout}
4129
+ `);
4130
+ }
4131
+ if (result.stderr) {
4132
+ process.stderr.write(result.stderr.endsWith("\n") ? result.stderr : `${result.stderr}
4133
+ `);
4134
+ }
4135
+ if (result.timedOut) {
4136
+ console.error(`Tool \`${name}\` timed out after ${result.durationMs}ms.`);
4137
+ }
4138
+ if (!result.ok) {
4139
+ process.exitCode = result.exitCode ?? 1;
4140
+ }
4141
+ }
4142
+ async function runToolsAdd(repoRoot, name, options) {
4143
+ if (!options.description) {
4144
+ throw new Error("`tr tools add` requires --description.");
4145
+ }
4146
+ if (!options.run && !options.script) {
4147
+ throw new Error("Provide either --run <command> or --script <path>.");
4148
+ }
4149
+ const created = await createTool(
4150
+ repoRoot,
4151
+ {
4152
+ name,
4153
+ description: options.description,
4154
+ run: options.run,
4155
+ script: options.script,
4156
+ risk: options.risk,
4157
+ connection: options.connection,
4158
+ healthcheck: options.healthcheck ? { run: options.healthcheck, expectExitCode: 0 } : void 0,
4159
+ confirm: options.confirm,
4160
+ scope: options.scope
4161
+ },
4162
+ { actor: "human", force: options.force }
4163
+ );
4164
+ console.log(`Created ${created.scope} tool \`${name}\` at ${created.path}.`);
4165
+ }
4166
+ function deriveNameFromCommand(command) {
4167
+ const first = command.trim().split(/\s+/)[0] ?? "tool";
4168
+ return first.toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/^-+|-+$/g, "") || "tool";
4169
+ }
4170
+ async function runToolsCreate(repoRoot, options) {
4171
+ const run2 = options.run ?? options.fromCommand;
4172
+ const name = options.fromCommand ? deriveNameFromCommand(options.fromCommand) : void 0;
4173
+ if (!name) {
4174
+ throw new Error("`tr tools create` currently requires --from-command <command>.");
4175
+ }
4176
+ await runToolsAdd(repoRoot, name, {
4177
+ ...options,
4178
+ run: run2,
4179
+ description: options.description ?? `Run \`${run2}\`.`,
4180
+ healthcheck: options.healthcheck ?? run2,
4181
+ confirm: options.confirm ?? options.risk === "high"
4182
+ });
4183
+ }
4184
+ async function runToolsCheck(repoRoot) {
4185
+ const harness = await resolveHarness(repoRoot);
4186
+ if (harness.tools.length === 0) {
4187
+ console.log("No tools defined.");
4188
+ return;
4189
+ }
4190
+ let failures = 0;
4191
+ for (const tool of harness.tools) {
4192
+ const check = await checkToolHealth(repoRoot, tool);
4193
+ if (check.status === "ok") {
4194
+ console.log(`${tool.name}: ok`);
4195
+ } else if (check.status === "skipped") {
4196
+ console.log(`${tool.name}: skipped - ${check.message}`);
4197
+ } else {
4198
+ failures += 1;
4199
+ console.log(`${tool.name}: error - ${check.message}`);
4200
+ }
4201
+ }
4202
+ if (failures > 0) {
4203
+ process.exitCode = 1;
4204
+ }
4205
+ }
4206
+ async function runToolsDetect(repoRoot) {
4207
+ let profile;
4208
+ try {
4209
+ profile = (await resolveHarness(repoRoot)).manifest.profile;
4210
+ } catch (error) {
4211
+ if (!(error instanceof HarnessError)) {
4212
+ throw error;
4213
+ }
4214
+ }
4215
+ const candidates = await detectToolCandidates(repoRoot, profile ?? "empty");
4216
+ if (candidates.length === 0) {
4217
+ console.log("No starter tools detected.");
4218
+ return;
4219
+ }
4220
+ console.log("Proposed starter tools (materialize with `tr tools add`):");
4221
+ for (const candidate of candidates) {
4222
+ const confirm = candidate.confirm ? " (confirm)" : "";
4223
+ const healthcheck = candidate.healthcheck ? ", healthcheck suggested" : "";
4224
+ console.log(`- ${candidate.name}${confirm}: ${candidate.run} [${candidate.source}, ${candidate.risk}${healthcheck}]`);
4225
+ }
4226
+ }
4227
+
4228
+ // src/commands/connections.ts
4229
+ async function runConnectionsList(repoRoot) {
4230
+ let harness;
4231
+ try {
4232
+ harness = await resolveHarness(repoRoot);
4233
+ } catch (error) {
4234
+ if (error instanceof HarnessError) {
4235
+ console.log("No harness found. Run `tr init` first.");
4236
+ return;
4237
+ }
4238
+ throw error;
4239
+ }
4240
+ if (harness.connections.length === 0) {
4241
+ console.log("No connections defined. Add one with `tr connections add`.");
4242
+ return;
4243
+ }
4244
+ for (const connection of harness.connections) {
4245
+ const flags = [
4246
+ connection.manifest.provider,
4247
+ connection.manifest.risk,
4248
+ connection.manifest.confirm ? "confirm" : null,
4249
+ connection.manifest.healthcheck ? "healthcheck" : null
4250
+ ].filter(Boolean).join(", ");
4251
+ console.log(`${connection.name} [${flags}] - ${connection.manifest.description}`);
4252
+ }
4253
+ }
4254
+ async function runConnectionsAdd(repoRoot, name, options) {
4255
+ const created = await createConnection(
4256
+ repoRoot,
4257
+ {
4258
+ name,
4259
+ provider: options.provider,
4260
+ command: options.command,
4261
+ description: options.description,
4262
+ profile: options.profile,
4263
+ risk: options.risk,
4264
+ confirm: options.confirm,
4265
+ healthcheck: options.healthcheck,
4266
+ scope: options.scope
4267
+ },
4268
+ { force: options.force }
4269
+ );
4270
+ console.log(`Created ${created.manifest.scope} connection \`${name}\` at ${created.path}.`);
4271
+ }
4272
+ async function runConnectionsCheck(repoRoot) {
4273
+ const checks = await checkConnections(repoRoot);
4274
+ if (checks.length === 0) {
4275
+ console.log("No connections defined.");
4276
+ return;
4277
+ }
4278
+ let failures = 0;
4279
+ for (const check of checks) {
4280
+ console.log(`${check.name}: ${check.status} - ${check.message}`);
4281
+ if (check.status === "error") {
4282
+ failures += 1;
4283
+ }
4284
+ }
4285
+ if (failures > 0) {
4286
+ process.exitCode = 1;
4287
+ }
4288
+ }
4289
+
4290
+ // src/cli.ts
4291
+ function createProgram(repoRoot = process.cwd()) {
4292
+ const program = new Command();
4293
+ program.name("threadroot").description("Git for your AI agent harness: one canonical source, compiled to every vendor format.").version("0.1.0");
4294
+ program.command("init").description("Scaffold a Threadroot harness, import existing vendor files once, and compile.").option("--force", "Re-initialize over an existing harness.").option("--no-import", "Skip importing existing vendor files (blank-slate init).").option("--profile <profile>", "Override the detected project profile.").option("--adapters <list>", "Comma-separated adapters: agents,claude,copilot,cursor.").action((options) => runInit(repoRoot, options));
4295
+ program.command("status").description("Show harness state, object counts, and compiled-output drift.").action(() => runStatus(repoRoot));
4296
+ program.command("diff").description("Show the diff between the canonical harness and each compiled vendor file.").action(() => runDiff(repoRoot));
4297
+ program.command("doctor").description("Check harness validity, compiled output health, MCP hints, and tool trust.").action(() => runDoctor(repoRoot));
4298
+ 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));
4299
+ 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));
4300
+ 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));
4301
+ 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));
4302
+ 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));
4303
+ const memory = program.command("memory").description("Read and append durable harness memory.");
4304
+ 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));
4305
+ 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));
4306
+ const tools2 = program.command("tools").description("Manage executable harness tools.");
4307
+ tools2.command("list").description("List harness tools.").action(() => runToolsList(repoRoot));
4308
+ tools2.command("check").description("Run configured tool healthchecks.").action(() => runToolsCheck(repoRoot));
4309
+ tools2.command("detect").description("Propose starter tools from the repo's existing command surface.").action(() => runToolsDetect(repoRoot));
4310
+ 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));
4311
+ 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));
4312
+ const connections = program.command("connections").description("Manage local CLI connections.");
4313
+ connections.command("list").description("List harness connections.").action(() => runConnectionsList(repoRoot));
4314
+ connections.command("check").description("Run configured connection healthchecks.").action(() => runConnectionsCheck(repoRoot));
4315
+ 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));
4316
+ const packs = program.command("packs").description("Inspect, validate, and install capability packs.");
4317
+ packs.command("list").description("List built-in and repo-local packs.").action(() => runPacksList(repoRoot));
4318
+ 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));
4319
+ 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));
4320
+ 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));
4321
+ const skills = program.command("skills").description("Inspect and validate harness skills.");
4322
+ skills.command("list").description("List harness skills.").action(() => runSkillsList(repoRoot));
4323
+ 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));
4324
+ 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));
4325
+ const mcp = program.command("mcp").description("Run or configure the local Threadroot MCP server.");
4326
+ mcp.action(() => runMcp(repoRoot));
4327
+ 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));
4328
+ return program;
4329
+ }
4330
+
4331
+ // src/index.ts
4332
+ function normalizeArgv(argv) {
4333
+ if (argv[2] === "--") {
4334
+ return [argv[0] ?? "node", argv[1] ?? "threadroot", ...argv.slice(3)];
4335
+ }
4336
+ return argv;
4337
+ }
4338
+ createProgram().parseAsync(normalizeArgv(process.argv)).catch((error) => {
4339
+ console.error(error instanceof Error ? error.message : String(error));
4340
+ process.exitCode = 1;
4341
+ });